Some missinglogging
[gc-dialer] / src / dialogs.py
1 #!/usr/bin/env python
2
3 from __future__ import with_statement
4 from __future__ import division
5
6 import functools
7 import copy
8 import logging
9
10 from PyQt4 import QtGui
11 from PyQt4 import QtCore
12
13 import constants
14 from util import qui_utils
15 from util import misc as misc_utils
16
17
18 _moduleLogger = logging.getLogger(__name__)
19
20
21 class CredentialsDialog(object):
22
23         def __init__(self, app):
24                 self._usernameField = QtGui.QLineEdit()
25                 self._passwordField = QtGui.QLineEdit()
26                 self._passwordField.setEchoMode(QtGui.QLineEdit.PasswordEchoOnEdit)
27
28                 self._credLayout = QtGui.QGridLayout()
29                 self._credLayout.addWidget(QtGui.QLabel("Username"), 0, 0)
30                 self._credLayout.addWidget(self._usernameField, 0, 1)
31                 self._credLayout.addWidget(QtGui.QLabel("Password"), 1, 0)
32                 self._credLayout.addWidget(self._passwordField, 1, 1)
33
34                 self._loginButton = QtGui.QPushButton("&Login")
35                 self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
36                 self._buttonLayout.addButton(self._loginButton, QtGui.QDialogButtonBox.AcceptRole)
37
38                 self._layout = QtGui.QVBoxLayout()
39                 self._layout.addLayout(self._credLayout)
40                 self._layout.addWidget(self._buttonLayout)
41
42                 self._dialog = QtGui.QDialog()
43                 self._dialog.setWindowTitle("Login")
44                 self._dialog.setLayout(self._layout)
45                 self._dialog.setAttribute(QtCore.Qt.WA_DeleteOnClose, False)
46                 qui_utils.set_autorient(self._dialog, True)
47                 self._buttonLayout.accepted.connect(self._dialog.accept)
48                 self._buttonLayout.rejected.connect(self._dialog.reject)
49
50                 self._closeWindowAction = QtGui.QAction(None)
51                 self._closeWindowAction.setText("Close")
52                 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
53                 self._closeWindowAction.triggered.connect(self._on_close_window)
54
55                 self._dialog.addAction(self._closeWindowAction)
56                 self._dialog.addAction(app.quitAction)
57                 self._dialog.addAction(app.fullscreenAction)
58
59         def run(self, defaultUsername, defaultPassword, parent=None):
60                 self._dialog.setParent(parent, QtCore.Qt.Dialog)
61                 try:
62                         self._usernameField.setText(defaultUsername)
63                         self._passwordField.setText(defaultPassword)
64
65                         response = self._dialog.exec_()
66                         if response == QtGui.QDialog.Accepted:
67                                 return str(self._usernameField.text()), str(self._passwordField.text())
68                         elif response == QtGui.QDialog.Rejected:
69                                 raise RuntimeError("Login Cancelled")
70                         else:
71                                 raise RuntimeError("Unknown Response")
72                 finally:
73                         self._dialog.setParent(None, QtCore.Qt.Dialog)
74
75         def close(self):
76                 self._dialog.reject()
77
78         @QtCore.pyqtSlot()
79         @QtCore.pyqtSlot(bool)
80         @misc_utils.log_exception(_moduleLogger)
81         def _on_close_window(self, checked = True):
82                 self._dialog.reject()
83
84
85 class AboutDialog(object):
86
87         def __init__(self, app):
88                 self._title = QtGui.QLabel(
89                         "<h1>%s</h1><h3>Version: %s</h3>" % (
90                                 constants.__pretty_app_name__, constants.__version__
91                         )
92                 )
93                 self._title.setTextFormat(QtCore.Qt.RichText)
94                 self._title.setAlignment(QtCore.Qt.AlignCenter)
95                 self._copyright = QtGui.QLabel("<h6>Developed by Ed Page<h6><h6>Icons: See website</h6>")
96                 self._copyright.setTextFormat(QtCore.Qt.RichText)
97                 self._copyright.setAlignment(QtCore.Qt.AlignCenter)
98                 self._link = QtGui.QLabel('<a href="http://gc-dialer.garage.maemo.org">DialCentral Website</a>')
99                 self._link.setTextFormat(QtCore.Qt.RichText)
100                 self._link.setAlignment(QtCore.Qt.AlignCenter)
101                 self._link.setOpenExternalLinks(True)
102
103                 self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
104
105                 self._layout = QtGui.QVBoxLayout()
106                 self._layout.addWidget(self._title)
107                 self._layout.addWidget(self._copyright)
108                 self._layout.addWidget(self._link)
109                 self._layout.addWidget(self._buttonLayout)
110
111                 self._dialog = QtGui.QDialog()
112                 self._dialog.setWindowTitle("About")
113                 self._dialog.setLayout(self._layout)
114                 qui_utils.set_autorient(self._dialog, True)
115                 self._buttonLayout.rejected.connect(self._dialog.reject)
116
117                 self._closeWindowAction = QtGui.QAction(None)
118                 self._closeWindowAction.setText("Close")
119                 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
120                 self._closeWindowAction.triggered.connect(self._on_close_window)
121
122                 self._dialog.addAction(self._closeWindowAction)
123                 self._dialog.addAction(app.quitAction)
124                 self._dialog.addAction(app.fullscreenAction)
125
126         def run(self, parent=None):
127                 self._dialog.setParent(parent, QtCore.Qt.Dialog)
128
129                 response = self._dialog.exec_()
130                 return response
131
132         def close(self):
133                 self._dialog.reject()
134
135         @QtCore.pyqtSlot()
136         @QtCore.pyqtSlot(bool)
137         @misc_utils.log_exception(_moduleLogger)
138         def _on_close_window(self, checked = True):
139                 self._dialog.reject()
140
141
142 class AccountDialog(object):
143
144         # @bug Can't enter custom callback numbers
145
146         _RECURRENCE_CHOICES = [
147                 (1, "1 minute"),
148                 (2, "2 minutes"),
149                 (3, "3 minutes"),
150                 (5, "5 minutes"),
151                 (8, "8 minutes"),
152                 (10, "10 minutes"),
153                 (15, "15 minutes"),
154                 (30, "30 minutes"),
155                 (45, "45 minutes"),
156                 (60, "1 hour"),
157                 (3*60, "3 hours"),
158                 (6*60, "6 hours"),
159                 (12*60, "12 hours"),
160         ]
161
162         def __init__(self, app):
163                 self._doClear = False
164
165                 self._accountNumberLabel = QtGui.QLabel("NUMBER NOT SET")
166                 self._notificationButton = QtGui.QCheckBox("Notifications")
167                 self._notificationButton.stateChanged.connect(self._on_notification_change)
168                 self._notificationTimeSelector = QtGui.QComboBox()
169                 #self._notificationTimeSelector.setEditable(True)
170                 self._notificationTimeSelector.setInsertPolicy(QtGui.QComboBox.InsertAtTop)
171                 for _, label in self._RECURRENCE_CHOICES:
172                         self._notificationTimeSelector.addItem(label)
173                 self._missedCallsNotificationButton = QtGui.QCheckBox("Missed Calls")
174                 self._voicemailNotificationButton = QtGui.QCheckBox("Voicemail")
175                 self._smsNotificationButton = QtGui.QCheckBox("SMS")
176                 self._clearButton = QtGui.QPushButton("Clear Account")
177                 self._clearButton.clicked.connect(self._on_clear)
178                 self._callbackSelector = QtGui.QComboBox()
179                 #self._callbackSelector.setEditable(True)
180                 self._callbackSelector.setInsertPolicy(QtGui.QComboBox.InsertAtTop)
181
182                 self._update_notification_state()
183
184                 self._credLayout = QtGui.QGridLayout()
185                 self._credLayout.addWidget(QtGui.QLabel("Account"), 0, 0)
186                 self._credLayout.addWidget(self._accountNumberLabel, 0, 1)
187                 self._credLayout.addWidget(QtGui.QLabel("Callback"), 1, 0)
188                 self._credLayout.addWidget(self._callbackSelector, 1, 1)
189                 self._credLayout.addWidget(self._notificationButton, 2, 0)
190                 self._credLayout.addWidget(self._notificationTimeSelector, 2, 1)
191                 self._credLayout.addWidget(QtGui.QLabel(""), 3, 0)
192                 self._credLayout.addWidget(self._missedCallsNotificationButton, 3, 1)
193                 self._credLayout.addWidget(QtGui.QLabel(""), 4, 0)
194                 self._credLayout.addWidget(self._voicemailNotificationButton, 4, 1)
195                 self._credLayout.addWidget(QtGui.QLabel(""), 5, 0)
196                 self._credLayout.addWidget(self._smsNotificationButton, 5, 1)
197
198                 self._credLayout.addWidget(QtGui.QLabel(""), 6, 0)
199                 self._credLayout.addWidget(self._clearButton, 6, 1)
200                 self._credLayout.addWidget(QtGui.QLabel(""), 3, 0)
201
202                 self._loginButton = QtGui.QPushButton("&Apply")
203                 self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
204                 self._buttonLayout.addButton(self._loginButton, QtGui.QDialogButtonBox.AcceptRole)
205
206                 self._layout = QtGui.QVBoxLayout()
207                 self._layout.addLayout(self._credLayout)
208                 self._layout.addWidget(self._buttonLayout)
209
210                 self._dialog = QtGui.QDialog()
211                 self._dialog.setWindowTitle("Account")
212                 self._dialog.setLayout(self._layout)
213                 qui_utils.set_autorient(self._dialog, True)
214                 self._buttonLayout.accepted.connect(self._dialog.accept)
215                 self._buttonLayout.rejected.connect(self._dialog.reject)
216
217                 self._closeWindowAction = QtGui.QAction(None)
218                 self._closeWindowAction.setText("Close")
219                 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
220                 self._closeWindowAction.triggered.connect(self._on_close_window)
221
222                 self._dialog.addAction(self._closeWindowAction)
223                 self._dialog.addAction(app.quitAction)
224                 self._dialog.addAction(app.fullscreenAction)
225
226         @property
227         def doClear(self):
228                 return self._doClear
229
230         def setIfNotificationsSupported(self, isSupported):
231                 if isSupported:
232                         self._notificationButton.setVisible(True)
233                         self._notificationTimeSelector.setVisible(True)
234                         self._missedCallsNotificationButton.setVisible(True)
235                         self._voicemailNotificationButton.setVisible(True)
236                         self._smsNotificationButton.setVisible(True)
237                 else:
238                         self._notificationButton.setVisible(False)
239                         self._notificationTimeSelector.setVisible(False)
240                         self._missedCallsNotificationButton.setVisible(False)
241                         self._voicemailNotificationButton.setVisible(False)
242                         self._smsNotificationButton.setVisible(False)
243
244         accountNumber = property(
245                 lambda self: str(self._accountNumberLabel.text()),
246                 lambda self, num: self._accountNumberLabel.setText(num),
247         )
248
249         notifications = property(
250                 lambda self: self._notificationButton.isChecked(),
251                 lambda self, enabled: self._notificationButton.setChecked(enabled),
252         )
253
254         notifyOnMissed = property(
255                 lambda self: self._missedCallsNotificationButton.isChecked(),
256                 lambda self, enabled: self._missedCallsNotificationButton.setChecked(enabled),
257         )
258
259         notifyOnVoicemail = property(
260                 lambda self: self._voicemailNotificationButton.isChecked(),
261                 lambda self, enabled: self._voicemailNotificationButton.setChecked(enabled),
262         )
263
264         notifyOnSms = property(
265                 lambda self: self._smsNotificationButton.isChecked(),
266                 lambda self, enabled: self._smsNotificationButton.setChecked(enabled),
267         )
268
269         def _get_notification_time(self):
270                 index = self._notificationTimeSelector.currentIndex()
271                 minutes = self._RECURRENCE_CHOICES[index][0]
272                 return minutes
273
274         def _set_notification_time(self, minutes):
275                 for i, (time, _) in enumerate(self._RECURRENCE_CHOICES):
276                         if time == minutes:
277                                 self._callbackSelector.setCurrentIndex(i)
278                                 break
279                 else:
280                                 self._callbackSelector.setCurrentIndex(0)
281
282         notificationTime = property(_get_notification_time, _set_notification_time)
283
284         @property
285         def selectedCallback(self):
286                 index = self._callbackSelector.currentIndex()
287                 data = str(self._callbackSelector.itemData(index).toPyObject())
288                 return data
289
290         def set_callbacks(self, choices, default):
291                 self._callbackSelector.clear()
292
293                 self._callbackSelector.addItem("Not Set", "")
294
295                 uglyDefault = misc_utils.make_ugly(default)
296                 for number, description in choices.iteritems():
297                         prettyNumber = misc_utils.make_pretty(number)
298                         uglyNumber = misc_utils.make_ugly(number)
299                         if not uglyNumber:
300                                 continue
301
302                         self._callbackSelector.addItem("%s - %s" % (prettyNumber, description), uglyNumber)
303                         if uglyNumber == uglyDefault:
304                                 self._callbackSelector.setCurrentIndex(self._callbackSelector.count() - 1)
305
306         def run(self, parent=None):
307                 self._doClear = False
308                 self._dialog.setParent(parent, QtCore.Qt.Dialog)
309
310                 response = self._dialog.exec_()
311                 return response
312
313         def close(self):
314                 self._dialog.reject()
315
316         def _update_notification_state(self):
317                 if self._notificationButton.isChecked():
318                         self._notificationTimeSelector.setEnabled(True)
319                         self._missedCallsNotificationButton.setEnabled(True)
320                         self._voicemailNotificationButton.setEnabled(True)
321                         self._smsNotificationButton.setEnabled(True)
322                 else:
323                         self._notificationTimeSelector.setEnabled(False)
324                         self._missedCallsNotificationButton.setEnabled(False)
325                         self._voicemailNotificationButton.setEnabled(False)
326                         self._smsNotificationButton.setEnabled(False)
327
328         @QtCore.pyqtSlot(int)
329         @misc_utils.log_exception(_moduleLogger)
330         def _on_notification_change(self, state):
331                 self._update_notification_state()
332
333         @QtCore.pyqtSlot()
334         @QtCore.pyqtSlot(bool)
335         @misc_utils.log_exception(_moduleLogger)
336         def _on_clear(self, checked = False):
337                 self._doClear = True
338                 self._dialog.accept()
339
340         @QtCore.pyqtSlot()
341         @QtCore.pyqtSlot(bool)
342         @misc_utils.log_exception(_moduleLogger)
343         def _on_close_window(self, checked = True):
344                 self._dialog.reject()
345
346
347 class SMSEntryWindow(object):
348
349         MAX_CHAR = 160
350
351         def __init__(self, parent, app, session, errorLog):
352                 self._session = session
353                 self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
354
355                 self._session.draft.sendingMessage.connect(self._on_op_started)
356                 self._session.draft.calling.connect(self._on_op_started)
357                 self._session.draft.calling.connect(self._on_calling_started)
358                 self._session.draft.cancelling.connect(self._on_op_started)
359
360                 self._session.draft.sentMessage.connect(self._on_op_finished)
361                 self._session.draft.called.connect(self._on_op_finished)
362                 self._session.draft.cancelled.connect(self._on_op_finished)
363                 self._session.draft.error.connect(self._on_op_error)
364                 self._errorLog = errorLog
365
366                 self._errorDisplay = qui_utils.ErrorDisplay(self._errorLog)
367
368                 self._targetLayout = QtGui.QVBoxLayout()
369                 self._targetList = QtGui.QWidget()
370                 self._targetList.setLayout(self._targetLayout)
371                 self._history = QtGui.QLabel()
372                 self._history.setTextFormat(QtCore.Qt.RichText)
373                 self._history.setWordWrap(True)
374                 self._smsEntry = QtGui.QTextEdit()
375                 self._smsEntry.textChanged.connect(self._on_letter_count_changed)
376
377                 self._entryLayout = QtGui.QVBoxLayout()
378                 self._entryLayout.addWidget(self._targetList)
379                 self._entryLayout.addWidget(self._history)
380                 self._entryLayout.addWidget(self._smsEntry)
381                 self._entryLayout.setContentsMargins(0, 0, 0, 0)
382                 self._entryWidget = QtGui.QWidget()
383                 self._entryWidget.setLayout(self._entryLayout)
384                 self._entryWidget.setContentsMargins(0, 0, 0, 0)
385                 self._scrollEntry = QtGui.QScrollArea()
386                 self._scrollEntry.setWidget(self._entryWidget)
387                 self._scrollEntry.setWidgetResizable(True)
388                 self._scrollEntry.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignBottom)
389                 self._scrollEntry.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
390                 self._scrollEntry.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
391
392                 self._characterCountLabel = QtGui.QLabel("0 (0)")
393                 self._singleNumberSelector = QtGui.QComboBox()
394                 self._singleNumbersCID = None
395                 self._singleNumberSelector.activated.connect(self._on_single_change_number)
396                 self._smsButton = QtGui.QPushButton("SMS")
397                 self._smsButton.clicked.connect(self._on_sms_clicked)
398                 self._smsButton.setEnabled(False)
399                 self._dialButton = QtGui.QPushButton("Dial")
400                 self._dialButton.clicked.connect(self._on_call_clicked)
401                 self._cancelButton = QtGui.QPushButton("Cancel Call")
402                 self._cancelButton.clicked.connect(self._on_cancel_clicked)
403                 self._cancelButton.setVisible(False)
404
405                 self._buttonLayout = QtGui.QHBoxLayout()
406                 self._buttonLayout.addWidget(self._characterCountLabel)
407                 self._buttonLayout.addWidget(self._singleNumberSelector)
408                 self._buttonLayout.addWidget(self._smsButton)
409                 self._buttonLayout.addWidget(self._dialButton)
410                 self._buttonLayout.addWidget(self._cancelButton)
411
412                 self._layout = QtGui.QVBoxLayout()
413                 self._layout.addWidget(self._errorDisplay.toplevel)
414                 self._layout.addWidget(self._scrollEntry)
415                 self._layout.addLayout(self._buttonLayout)
416
417                 centralWidget = QtGui.QWidget()
418                 centralWidget.setLayout(self._layout)
419
420                 self._window = QtGui.QMainWindow(parent)
421                 qui_utils.set_autorient(self._window, True)
422                 qui_utils.set_stackable(self._window, True)
423                 self._window.setWindowTitle("Contact")
424                 self._window.setCentralWidget(centralWidget)
425
426                 self._closeWindowAction = QtGui.QAction(None)
427                 self._closeWindowAction.setText("Close")
428                 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
429                 self._closeWindowAction.triggered.connect(self._on_close_window)
430
431                 fileMenu = self._window.menuBar().addMenu("&File")
432                 fileMenu.addAction(self._closeWindowAction)
433                 fileMenu.addAction(app.quitAction)
434                 viewMenu = self._window.menuBar().addMenu("&View")
435                 viewMenu.addAction(app.fullscreenAction)
436
437                 self._scrollTimer = QtCore.QTimer()
438                 self._scrollTimer.setInterval(0)
439                 self._scrollTimer.setSingleShot(True)
440                 self._scrollTimer.timeout.connect(self._on_delayed_scroll_to_bottom)
441
442                 self._window.show()
443                 self._update_target_fields()
444
445         def close(self):
446                 self._window.destroy()
447                 self._window = None
448
449         def _update_letter_count(self):
450                 count = self._smsEntry.toPlainText().size()
451                 numTexts, numCharInText = divmod(count, self.MAX_CHAR)
452                 numTexts += 1
453                 numCharsLeftInText = self.MAX_CHAR - numCharInText
454                 self._characterCountLabel.setText("%d (%d)" % (numCharsLeftInText, numTexts))
455
456         def _update_button_state(self):
457                 self._cancelButton.setEnabled(True)
458                 if self._session.draft.get_num_contacts() == 0:
459                         self._dialButton.setEnabled(False)
460                         self._smsButton.setEnabled(False)
461                 elif self._session.draft.get_num_contacts() == 1:
462                         count = self._smsEntry.toPlainText().size()
463                         if count == 0:
464                                 self._dialButton.setEnabled(True)
465                                 self._smsButton.setEnabled(False)
466                         else:
467                                 self._dialButton.setEnabled(False)
468                                 self._smsButton.setEnabled(True)
469                 else:
470                         self._dialButton.setEnabled(False)
471                         if count == 0:
472                                 self._smsButton.setEnabled(False)
473                         else:
474                                 self._smsButton.setEnabled(True)
475
476         def _update_target_fields(self):
477                 draftContactsCount = self._session.draft.get_num_contacts()
478                 if draftContactsCount == 0:
479                         self._clear_target_list()
480                         self._window.hide()
481                         self._singleNumbersCID = None
482                 elif draftContactsCount == 1:
483                         (cid, ) = self._session.draft.get_contacts()
484                         title = self._session.draft.get_title(cid)
485                         description = self._session.draft.get_description(cid)
486                         numbers = self._session.draft.get_numbers(cid)
487
488                         self._targetList.setVisible(False)
489                         self._clear_target_list()
490                         if description:
491                                 self._history.setText(description)
492                                 self._history.setVisible(True)
493                         else:
494                                 self._history.setText("")
495                                 self._history.setVisible(False)
496                         self._populate_number_selector(self._singleNumberSelector, cid, numbers)
497                         self._singleNumbersCID = None
498
499                         self._scroll_to_bottom()
500                         self._window.setWindowTitle(title)
501                         self._window.show()
502                         self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason)
503                 else:
504                         self._targetList.setVisible(True)
505                         self._clear_target_list()
506                         for cid in self._session.draft.get_contacts():
507                                 title = self._session.draft.get_title(cid)
508                                 description = self._session.draft.get_description(cid)
509                                 numbers = self._session.draft.get_numbers(cid)
510
511                                 titleLabel = QtGui.QLabel(title)
512                                 numberSelector = QtGui.QComboBox()
513                                 self._populate_number_selector(numberSelector, cid, numbers)
514                                 deleteButton = QtGui.QPushButton("Delete")
515                                 callback = functools.partial(
516                                         self._on_remove_contact,
517                                         cid
518                                 )
519                                 callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
520                                 deleteButton.clicked.connect(callback)
521
522                                 rowLayout = QtGui.QHBoxLayout()
523                                 rowLayout.addWidget(titleLabel)
524                                 rowLayout.addWidget(numberSelector)
525                                 rowLayout.addWidget(deleteButton)
526                                 rowWidget = QtGui.QWidget()
527                                 rowWidget.setLayout(rowLayout)
528                                 self._targetLayout.addWidget(rowWidget)
529                         self._history.setText("")
530                         self._history.setVisible(False)
531                         self._singleNumberSelector.setVisible(False)
532                         self._singleNumbersCID = None
533
534                         self._scroll_to_bottom()
535                         self._window.setWindowTitle("Contacts")
536                         self._window.show()
537                         self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason)
538
539         def _clear_target_list(self):
540                 while self._targetLayout.count():
541                         removedLayoutItem = self._targetLayout.takeAt(self._targetLayout.count()-1)
542                         removedWidget = removedLayoutItem.widget()
543                         removedWidget.hide()
544                         removedWidget.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
545                         removedWidget.close()
546
547         def _populate_number_selector(self, selector, cid, numbers):
548                 selector.clear()
549
550                 if len(numbers) == 1:
551                         numbers, defaultIndex = _get_contact_numbers(self._session, cid, numbers[0])
552                 else:
553                         defaultIndex = 0
554
555                 for number, description in numbers:
556                         if description:
557                                 label = "%s - %s" % (number, description)
558                         else:
559                                 label = number
560                         selector.addItem(label)
561                 selector.setVisible(True)
562                 if 1 < len(numbers):
563                         selector.setEnabled(True)
564                         selector.setCurrentIndex(defaultIndex)
565                 else:
566                         selector.setEnabled(False)
567
568                 if selector is not self._singleNumberSelector:
569                         callback = functools.partial(
570                                 self._on_change_number,
571                                 cid
572                         )
573                         callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
574                         selector.activated.connect(
575                                 QtCore.pyqtSlot(int)(callback)
576                         )
577
578         def _scroll_to_bottom(self):
579                 self._scrollTimer.start()
580
581         @misc_utils.log_exception(_moduleLogger)
582         def _on_delayed_scroll_to_bottom(self):
583                 self._scrollEntry.ensureWidgetVisible(self._smsEntry)
584
585         @misc_utils.log_exception(_moduleLogger)
586         def _on_sms_clicked(self, arg):
587                 message = unicode(self._smsEntry.toPlainText())
588                 self._session.draft.send(message)
589
590         @misc_utils.log_exception(_moduleLogger)
591         def _on_call_clicked(self, arg):
592                 self._session.draft.call()
593
594         @QtCore.pyqtSlot()
595         @misc_utils.log_exception(_moduleLogger)
596         def _on_cancel_clicked(self, message):
597                 self._session.draft.cancel()
598
599         @misc_utils.log_exception(_moduleLogger)
600         def _on_remove_contact(self, cid, toggled):
601                 self._session.draft.remove_contact(cid)
602
603         @misc_utils.log_exception(_moduleLogger)
604         def _on_single_change_number(self, index):
605                 # Exception thrown when the first item is removed
606                 cid = self._singleNumbersCID
607                 if cid is None:
608                         _moduleLogger.error("Number change occurred on the single selector when in multi-selector mode (%r)" % index)
609                         return
610                 try:
611                         numbers = self._session.draft.get_numbers(cid)
612                 except KeyError:
613                         _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
614                         return
615                 number = numbers[index][0]
616                 self._session.draft.set_selected_number(cid, number)
617
618         @misc_utils.log_exception(_moduleLogger)
619         def _on_change_number(self, cid, index):
620                 # Exception thrown when the first item is removed
621                 try:
622                         numbers = self._session.draft.get_numbers(cid)
623                 except KeyError:
624                         _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
625                         return
626                 number = numbers[index][0]
627                 self._session.draft.set_selected_number(cid, number)
628
629         @QtCore.pyqtSlot()
630         @misc_utils.log_exception(_moduleLogger)
631         def _on_recipients_changed(self):
632                 self._update_target_fields()
633                 self._update_button_state()
634
635         @QtCore.pyqtSlot()
636         @misc_utils.log_exception(_moduleLogger)
637         def _on_op_started(self):
638                 self._smsEntry.setReadOnly(True)
639                 self._smsButton.setVisible(False)
640                 self._dialButton.setVisible(False)
641                 self._window.show()
642
643         @QtCore.pyqtSlot()
644         @misc_utils.log_exception(_moduleLogger)
645         def _on_calling_started(self):
646                 self._cancelButton.setVisible(True)
647
648         @QtCore.pyqtSlot()
649         @misc_utils.log_exception(_moduleLogger)
650         def _on_op_finished(self):
651                 self._smsEntry.setPlainText("")
652                 self._smsEntry.setReadOnly(False)
653                 self._cancelButton.setVisible(False)
654                 self._smsButton.setVisible(True)
655                 self._dialButton.setVisible(True)
656                 self._window.hide()
657
658         @QtCore.pyqtSlot()
659         @misc_utils.log_exception(_moduleLogger)
660         def _on_op_error(self, message):
661                 self._smsEntry.setReadOnly(False)
662                 self._cancelButton.setVisible(False)
663                 self._smsButton.setVisible(True)
664                 self._dialButton.setVisible(True)
665
666                 self._errorLog.push_error(message)
667
668         @QtCore.pyqtSlot()
669         @misc_utils.log_exception(_moduleLogger)
670         def _on_letter_count_changed(self):
671                 self._update_letter_count()
672                 self._update_button_state()
673
674         @QtCore.pyqtSlot()
675         @QtCore.pyqtSlot(bool)
676         @misc_utils.log_exception(_moduleLogger)
677         def _on_close_window(self, checked = True):
678                 self._window.hide()
679
680
681 def _get_contact_numbers(session, contactId, numberDescription):
682         contactPhoneNumbers = []
683         if contactId and contactId != "0":
684                 try:
685                         contactDetails = copy.deepcopy(session.get_contacts()[contactId])
686                         contactPhoneNumbers = contactDetails["numbers"]
687                 except KeyError:
688                         contactPhoneNumbers = []
689                 contactPhoneNumbers = [
690                         (contactPhoneNumber["phoneNumber"], contactPhoneNumber.get("phoneType", "Unknown"))
691                         for contactPhoneNumber in contactPhoneNumbers
692                 ]
693                 if contactPhoneNumbers:
694                         uglyContactNumbers = (
695                                 misc_utils.make_ugly(contactNumber)
696                                 for (contactNumber, _) in contactPhoneNumbers
697                         )
698                         defaultMatches = [
699                                 misc_utils.similar_ugly_numbers(numberDescription[0], contactNumber)
700                                 for contactNumber in uglyContactNumbers
701                         ]
702                         try:
703                                 defaultIndex = defaultMatches.index(True)
704                         except ValueError:
705                                 contactPhoneNumbers.append(numberDescription)
706                                 defaultIndex = len(contactPhoneNumbers)-1
707                                 _moduleLogger.warn(
708                                         "Could not find contact %r's number %s among %r" % (
709                                                 contactId, numberDescription, contactPhoneNumbers
710                                         )
711                                 )
712
713         if not contactPhoneNumbers:
714                 contactPhoneNumbers = [numberDescription]
715                 defaultIndex = -1
716
717         return contactPhoneNumbers, defaultIndex