Fixing a button update bug in SMS window
[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         def _on_notification_change(self, state):
330                 self._update_notification_state()
331
332         @QtCore.pyqtSlot()
333         @QtCore.pyqtSlot(bool)
334         def _on_clear(self, checked = False):
335                 self._doClear = True
336                 self._dialog.accept()
337
338         @QtCore.pyqtSlot()
339         @QtCore.pyqtSlot(bool)
340         @misc_utils.log_exception(_moduleLogger)
341         def _on_close_window(self, checked = True):
342                 self._dialog.reject()
343
344
345 class SMSEntryWindow(object):
346
347         MAX_CHAR = 160
348
349         def __init__(self, parent, app, session, errorLog):
350                 self._session = session
351                 self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
352
353                 self._session.draft.sendingMessage.connect(self._on_op_started)
354                 self._session.draft.calling.connect(self._on_op_started)
355                 self._session.draft.calling.connect(self._on_calling_started)
356                 self._session.draft.cancelling.connect(self._on_op_started)
357
358                 self._session.draft.sentMessage.connect(self._on_op_finished)
359                 self._session.draft.called.connect(self._on_op_finished)
360                 self._session.draft.cancelled.connect(self._on_op_finished)
361                 self._session.draft.error.connect(self._on_op_error)
362                 self._errorLog = errorLog
363
364                 self._errorDisplay = qui_utils.ErrorDisplay(self._errorLog)
365
366                 self._targetLayout = QtGui.QVBoxLayout()
367                 self._targetList = QtGui.QWidget()
368                 self._targetList.setLayout(self._targetLayout)
369                 self._history = QtGui.QLabel()
370                 self._history.setTextFormat(QtCore.Qt.RichText)
371                 self._history.setWordWrap(True)
372                 self._smsEntry = QtGui.QTextEdit()
373                 self._smsEntry.textChanged.connect(self._on_letter_count_changed)
374
375                 self._entryLayout = QtGui.QVBoxLayout()
376                 self._entryLayout.addWidget(self._targetList)
377                 self._entryLayout.addWidget(self._history)
378                 self._entryLayout.addWidget(self._smsEntry)
379                 self._entryLayout.setContentsMargins(0, 0, 0, 0)
380                 self._entryWidget = QtGui.QWidget()
381                 self._entryWidget.setLayout(self._entryLayout)
382                 self._entryWidget.setContentsMargins(0, 0, 0, 0)
383                 self._scrollEntry = QtGui.QScrollArea()
384                 self._scrollEntry.setWidget(self._entryWidget)
385                 self._scrollEntry.setWidgetResizable(True)
386                 self._scrollEntry.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignBottom)
387                 self._scrollEntry.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
388                 self._scrollEntry.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
389
390                 self._characterCountLabel = QtGui.QLabel("0 (0)")
391                 self._singleNumberSelector = QtGui.QComboBox()
392                 self._singleNumbersCID = None
393                 self._singleNumberSelector.activated.connect(self._on_single_change_number)
394                 self._smsButton = QtGui.QPushButton("SMS")
395                 self._smsButton.clicked.connect(self._on_sms_clicked)
396                 self._smsButton.setEnabled(False)
397                 self._dialButton = QtGui.QPushButton("Dial")
398                 self._dialButton.clicked.connect(self._on_call_clicked)
399                 self._cancelButton = QtGui.QPushButton("Cancel Call")
400                 self._cancelButton.clicked.connect(self._on_cancel_clicked)
401                 self._cancelButton.setVisible(False)
402
403                 self._buttonLayout = QtGui.QHBoxLayout()
404                 self._buttonLayout.addWidget(self._characterCountLabel)
405                 self._buttonLayout.addWidget(self._singleNumberSelector)
406                 self._buttonLayout.addWidget(self._smsButton)
407                 self._buttonLayout.addWidget(self._dialButton)
408                 self._buttonLayout.addWidget(self._cancelButton)
409
410                 self._layout = QtGui.QVBoxLayout()
411                 self._layout.addWidget(self._errorDisplay.toplevel)
412                 self._layout.addWidget(self._scrollEntry)
413                 self._layout.addLayout(self._buttonLayout)
414
415                 centralWidget = QtGui.QWidget()
416                 centralWidget.setLayout(self._layout)
417
418                 self._window = QtGui.QMainWindow(parent)
419                 qui_utils.set_autorient(self._window, True)
420                 qui_utils.set_stackable(self._window, True)
421                 self._window.setWindowTitle("Contact")
422                 self._window.setCentralWidget(centralWidget)
423
424                 self._closeWindowAction = QtGui.QAction(None)
425                 self._closeWindowAction.setText("Close")
426                 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
427                 self._closeWindowAction.triggered.connect(self._on_close_window)
428
429                 fileMenu = self._window.menuBar().addMenu("&File")
430                 fileMenu.addAction(self._closeWindowAction)
431                 fileMenu.addAction(app.quitAction)
432                 viewMenu = self._window.menuBar().addMenu("&View")
433                 viewMenu.addAction(app.fullscreenAction)
434
435                 self._scrollTimer = QtCore.QTimer()
436                 self._scrollTimer.setInterval(0)
437                 self._scrollTimer.setSingleShot(True)
438                 self._scrollTimer.timeout.connect(self._on_delayed_scroll_to_bottom)
439
440                 self._window.show()
441                 self._update_target_fields()
442
443         def close(self):
444                 self._window.destroy()
445                 self._window = None
446
447         def _update_letter_count(self):
448                 count = self._smsEntry.toPlainText().size()
449                 numTexts, numCharInText = divmod(count, self.MAX_CHAR)
450                 numTexts += 1
451                 numCharsLeftInText = self.MAX_CHAR - numCharInText
452                 self._characterCountLabel.setText("%d (%d)" % (numCharsLeftInText, numTexts))
453
454         def _update_button_state(self):
455                 self._cancelButton.setEnabled(True)
456                 if self._session.draft.get_num_contacts() == 0:
457                         self._dialButton.setEnabled(False)
458                         self._smsButton.setEnabled(False)
459                 elif self._session.draft.get_num_contacts() == 1:
460                         count = self._smsEntry.toPlainText().size()
461                         if count == 0:
462                                 self._dialButton.setEnabled(True)
463                                 self._smsButton.setEnabled(False)
464                         else:
465                                 self._dialButton.setEnabled(False)
466                                 self._smsButton.setEnabled(True)
467                 else:
468                         self._dialButton.setEnabled(False)
469                         if count == 0:
470                                 self._smsButton.setEnabled(False)
471                         else:
472                                 self._smsButton.setEnabled(True)
473
474         def _update_target_fields(self):
475                 draftContactsCount = self._session.draft.get_num_contacts()
476                 if draftContactsCount == 0:
477                         self._clear_target_list()
478                         self._window.hide()
479                         self._singleNumbersCID = None
480                 elif draftContactsCount == 1:
481                         (cid, ) = self._session.draft.get_contacts()
482                         title = self._session.draft.get_title(cid)
483                         description = self._session.draft.get_description(cid)
484                         numbers = self._session.draft.get_numbers(cid)
485
486                         self._targetList.setVisible(False)
487                         self._clear_target_list()
488                         if description:
489                                 self._history.setText(description)
490                                 self._history.setVisible(True)
491                         else:
492                                 self._history.setText("")
493                                 self._history.setVisible(False)
494                         self._populate_number_selector(self._singleNumberSelector, cid, numbers)
495                         self._singleNumbersCID = None
496
497                         self._scroll_to_bottom()
498                         self._window.setWindowTitle(title)
499                         self._window.show()
500                         self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason)
501                 else:
502                         self._targetList.setVisible(True)
503                         self._clear_target_list()
504                         for cid in self._session.draft.get_contacts():
505                                 title = self._session.draft.get_title(cid)
506                                 description = self._session.draft.get_description(cid)
507                                 numbers = self._session.draft.get_numbers(cid)
508
509                                 titleLabel = QtGui.QLabel(title)
510                                 numberSelector = QtGui.QComboBox()
511                                 self._populate_number_selector(numberSelector, cid, numbers)
512                                 deleteButton = QtGui.QPushButton("Delete")
513                                 callback = functools.partial(
514                                         self._on_remove_contact,
515                                         cid
516                                 )
517                                 callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
518                                 deleteButton.clicked.connect(callback)
519
520                                 rowLayout = QtGui.QHBoxLayout()
521                                 rowLayout.addWidget(titleLabel)
522                                 rowLayout.addWidget(numberSelector)
523                                 rowLayout.addWidget(deleteButton)
524                                 rowWidget = QtGui.QWidget()
525                                 rowWidget.setLayout(rowLayout)
526                                 self._targetLayout.addWidget(rowWidget)
527                         self._history.setText("")
528                         self._history.setVisible(False)
529                         self._singleNumberSelector.setVisible(False)
530                         self._singleNumbersCID = None
531
532                         self._scroll_to_bottom()
533                         self._window.setWindowTitle("Contacts")
534                         self._window.show()
535                         self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason)
536
537         def _clear_target_list(self):
538                 while self._targetLayout.count():
539                         removedLayoutItem = self._targetLayout.takeAt(self._targetLayout.count()-1)
540                         removedWidget = removedLayoutItem.widget()
541                         removedWidget.hide()
542                         removedWidget.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
543                         removedWidget.close()
544
545         def _populate_number_selector(self, selector, cid, numbers):
546                 selector.clear()
547
548                 if len(numbers) == 1:
549                         numbers, defaultIndex = _get_contact_numbers(self._session, cid, numbers[0])
550                 else:
551                         defaultIndex = 0
552
553                 for number, description in numbers:
554                         if description:
555                                 label = "%s - %s" % (number, description)
556                         else:
557                                 label = number
558                         selector.addItem(label)
559                 selector.setVisible(True)
560                 if 1 < len(numbers):
561                         selector.setEnabled(True)
562                         selector.setCurrentIndex(defaultIndex)
563                 else:
564                         selector.setEnabled(False)
565
566                 if selector is not self._singleNumberSelector:
567                         callback = functools.partial(
568                                 self._on_change_number,
569                                 cid
570                         )
571                         callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
572                         selector.activated.connect(
573                                 QtCore.pyqtSlot(int)(callback)
574                         )
575
576         def _scroll_to_bottom(self):
577                 self._scrollTimer.start()
578
579         @misc_utils.log_exception(_moduleLogger)
580         def _on_delayed_scroll_to_bottom(self):
581                 self._scrollEntry.ensureWidgetVisible(self._smsEntry)
582
583         @misc_utils.log_exception(_moduleLogger)
584         def _on_sms_clicked(self, arg):
585                 message = unicode(self._smsEntry.toPlainText())
586                 self._session.draft.send(message)
587
588         @misc_utils.log_exception(_moduleLogger)
589         def _on_call_clicked(self, arg):
590                 self._session.draft.call()
591
592         @QtCore.pyqtSlot()
593         @misc_utils.log_exception(_moduleLogger)
594         def _on_cancel_clicked(self, message):
595                 self._session.draft.cancel()
596
597         @misc_utils.log_exception(_moduleLogger)
598         def _on_remove_contact(self, cid, toggled):
599                 self._session.draft.remove_contact(cid)
600
601         @misc_utils.log_exception(_moduleLogger)
602         def _on_single_change_number(self, index):
603                 # Exception thrown when the first item is removed
604                 cid = self._singleNumbersCID
605                 if cid is None:
606                         _moduleLogger.error("Number change occurred on the single selector when in multi-selector mode (%r)" % index)
607                         return
608                 try:
609                         numbers = self._session.draft.get_numbers(cid)
610                 except KeyError:
611                         _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
612                         return
613                 number = numbers[index][0]
614                 self._session.draft.set_selected_number(cid, number)
615
616         @misc_utils.log_exception(_moduleLogger)
617         def _on_change_number(self, cid, index):
618                 # Exception thrown when the first item is removed
619                 try:
620                         numbers = self._session.draft.get_numbers(cid)
621                 except KeyError:
622                         _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
623                         return
624                 number = numbers[index][0]
625                 self._session.draft.set_selected_number(cid, number)
626
627         @QtCore.pyqtSlot()
628         @misc_utils.log_exception(_moduleLogger)
629         def _on_recipients_changed(self):
630                 self._update_target_fields()
631                 self._update_button_state()
632
633         @QtCore.pyqtSlot()
634         @misc_utils.log_exception(_moduleLogger)
635         def _on_op_started(self):
636                 self._smsEntry.setReadOnly(True)
637                 self._smsButton.setVisible(False)
638                 self._dialButton.setVisible(False)
639                 self._window.show()
640
641         @QtCore.pyqtSlot()
642         @misc_utils.log_exception(_moduleLogger)
643         def _on_calling_started(self):
644                 self._cancelButton.setVisible(True)
645
646         @QtCore.pyqtSlot()
647         @misc_utils.log_exception(_moduleLogger)
648         def _on_op_finished(self):
649                 self._smsEntry.setPlainText("")
650                 self._smsEntry.setReadOnly(False)
651                 self._cancelButton.setVisible(False)
652                 self._smsButton.setVisible(True)
653                 self._dialButton.setVisible(True)
654                 self._window.hide()
655
656         @QtCore.pyqtSlot()
657         @misc_utils.log_exception(_moduleLogger)
658         def _on_op_error(self, message):
659                 self._smsEntry.setReadOnly(False)
660                 self._cancelButton.setVisible(False)
661                 self._smsButton.setVisible(True)
662                 self._dialButton.setVisible(True)
663
664                 self._errorLog.push_error(message)
665
666         @QtCore.pyqtSlot()
667         @misc_utils.log_exception(_moduleLogger)
668         def _on_letter_count_changed(self):
669                 self._update_letter_count()
670                 self._update_button_state()
671
672         @QtCore.pyqtSlot()
673         @QtCore.pyqtSlot(bool)
674         @misc_utils.log_exception(_moduleLogger)
675         def _on_close_window(self, checked = True):
676                 self._window.hide()
677
678
679 def _get_contact_numbers(session, contactId, numberDescription):
680         contactPhoneNumbers = []
681         if contactId and contactId != "0":
682                 try:
683                         contactDetails = copy.deepcopy(session.get_contacts()[contactId])
684                         contactPhoneNumbers = contactDetails["numbers"]
685                 except KeyError:
686                         contactPhoneNumbers = []
687                 contactPhoneNumbers = [
688                         (contactPhoneNumber["phoneNumber"], contactPhoneNumber.get("phoneType", "Unknown"))
689                         for contactPhoneNumber in contactPhoneNumbers
690                 ]
691                 if contactPhoneNumbers:
692                         uglyContactNumbers = (
693                                 misc_utils.make_ugly(contactNumber)
694                                 for (contactNumber, _) in contactPhoneNumbers
695                         )
696                         defaultMatches = [
697                                 misc_utils.similar_ugly_numbers(numberDescription[0], contactNumber)
698                                 for contactNumber in uglyContactNumbers
699                         ]
700                         try:
701                                 defaultIndex = defaultMatches.index(True)
702                         except ValueError:
703                                 contactPhoneNumbers.append(numberDescription)
704                                 defaultIndex = len(contactPhoneNumbers)-1
705                                 _moduleLogger.warn(
706                                         "Could not find contact %r's number %s among %r" % (
707                                                 contactId, numberDescription, contactPhoneNumbers
708                                         )
709                                 )
710
711         if not contactPhoneNumbers:
712                 contactPhoneNumbers = [numberDescription]
713                 defaultIndex = -1
714
715         return contactPhoneNumbers, defaultIndex