Completely hiding passwords because Qt doesn't have the partial hide-on-edit that...
[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.Password)
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                         count = self._smsEntry.toPlainText().size()
472                         if count == 0:
473                                 self._smsButton.setEnabled(False)
474                         else:
475                                 self._smsButton.setEnabled(True)
476
477         def _update_target_fields(self):
478                 draftContactsCount = self._session.draft.get_num_contacts()
479                 if draftContactsCount == 0:
480                         self._clear_target_list()
481                         self._window.hide()
482                         self._singleNumbersCID = None
483                 elif draftContactsCount == 1:
484                         (cid, ) = self._session.draft.get_contacts()
485                         title = self._session.draft.get_title(cid)
486                         description = self._session.draft.get_description(cid)
487                         numbers = self._session.draft.get_numbers(cid)
488
489                         self._targetList.setVisible(False)
490                         self._clear_target_list()
491                         if description:
492                                 self._history.setText(description)
493                                 self._history.setVisible(True)
494                         else:
495                                 self._history.setText("")
496                                 self._history.setVisible(False)
497                         self._populate_number_selector(self._singleNumberSelector, cid, numbers)
498                         self._singleNumbersCID = None
499
500                         self._scroll_to_bottom()
501                         self._window.setWindowTitle(title)
502                         self._window.show()
503                         self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason)
504                 else:
505                         self._targetList.setVisible(True)
506                         self._clear_target_list()
507                         for cid in self._session.draft.get_contacts():
508                                 title = self._session.draft.get_title(cid)
509                                 description = self._session.draft.get_description(cid)
510                                 numbers = self._session.draft.get_numbers(cid)
511
512                                 titleLabel = QtGui.QLabel(title)
513                                 numberSelector = QtGui.QComboBox()
514                                 self._populate_number_selector(numberSelector, cid, numbers)
515                                 deleteButton = QtGui.QPushButton("Delete")
516                                 callback = functools.partial(
517                                         self._on_remove_contact,
518                                         cid
519                                 )
520                                 callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
521                                 deleteButton.clicked.connect(callback)
522
523                                 rowLayout = QtGui.QHBoxLayout()
524                                 rowLayout.addWidget(titleLabel)
525                                 rowLayout.addWidget(numberSelector)
526                                 rowLayout.addWidget(deleteButton)
527                                 rowWidget = QtGui.QWidget()
528                                 rowWidget.setLayout(rowLayout)
529                                 self._targetLayout.addWidget(rowWidget)
530                         self._history.setText("")
531                         self._history.setVisible(False)
532                         self._singleNumberSelector.setVisible(False)
533                         self._singleNumbersCID = None
534
535                         self._scroll_to_bottom()
536                         self._window.setWindowTitle("Contacts")
537                         self._window.show()
538                         self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason)
539
540         def _clear_target_list(self):
541                 while self._targetLayout.count():
542                         removedLayoutItem = self._targetLayout.takeAt(self._targetLayout.count()-1)
543                         removedWidget = removedLayoutItem.widget()
544                         removedWidget.hide()
545                         removedWidget.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
546                         removedWidget.close()
547
548         def _populate_number_selector(self, selector, cid, numbers):
549                 selector.clear()
550
551                 if len(numbers) == 1:
552                         numbers, defaultIndex = _get_contact_numbers(self._session, cid, numbers[0])
553                 else:
554                         defaultIndex = 0
555
556                 for number, description in numbers:
557                         if description:
558                                 label = "%s - %s" % (number, description)
559                         else:
560                                 label = number
561                         selector.addItem(label)
562                 selector.setVisible(True)
563                 if 1 < len(numbers):
564                         selector.setEnabled(True)
565                         selector.setCurrentIndex(defaultIndex)
566                 else:
567                         selector.setEnabled(False)
568
569                 if selector is not self._singleNumberSelector:
570                         callback = functools.partial(
571                                 self._on_change_number,
572                                 cid
573                         )
574                         callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
575                         selector.activated.connect(
576                                 QtCore.pyqtSlot(int)(callback)
577                         )
578
579         def _scroll_to_bottom(self):
580                 self._scrollTimer.start()
581
582         @misc_utils.log_exception(_moduleLogger)
583         def _on_delayed_scroll_to_bottom(self):
584                 self._scrollEntry.ensureWidgetVisible(self._smsEntry)
585
586         @misc_utils.log_exception(_moduleLogger)
587         def _on_sms_clicked(self, arg):
588                 message = unicode(self._smsEntry.toPlainText())
589                 self._session.draft.send(message)
590
591         @misc_utils.log_exception(_moduleLogger)
592         def _on_call_clicked(self, arg):
593                 self._session.draft.call()
594
595         @QtCore.pyqtSlot()
596         @misc_utils.log_exception(_moduleLogger)
597         def _on_cancel_clicked(self, message):
598                 self._session.draft.cancel()
599
600         @misc_utils.log_exception(_moduleLogger)
601         def _on_remove_contact(self, cid, toggled):
602                 self._session.draft.remove_contact(cid)
603
604         @misc_utils.log_exception(_moduleLogger)
605         def _on_single_change_number(self, index):
606                 # Exception thrown when the first item is removed
607                 cid = self._singleNumbersCID
608                 if cid is None:
609                         _moduleLogger.error("Number change occurred on the single selector when in multi-selector mode (%r)" % index)
610                         return
611                 try:
612                         numbers = self._session.draft.get_numbers(cid)
613                 except KeyError:
614                         _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
615                         return
616                 number = numbers[index][0]
617                 self._session.draft.set_selected_number(cid, number)
618
619         @misc_utils.log_exception(_moduleLogger)
620         def _on_change_number(self, cid, index):
621                 # Exception thrown when the first item is removed
622                 try:
623                         numbers = self._session.draft.get_numbers(cid)
624                 except KeyError:
625                         _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
626                         return
627                 number = numbers[index][0]
628                 self._session.draft.set_selected_number(cid, number)
629
630         @QtCore.pyqtSlot()
631         @misc_utils.log_exception(_moduleLogger)
632         def _on_recipients_changed(self):
633                 self._update_target_fields()
634                 self._update_button_state()
635
636         @QtCore.pyqtSlot()
637         @misc_utils.log_exception(_moduleLogger)
638         def _on_op_started(self):
639                 self._smsEntry.setReadOnly(True)
640                 self._smsButton.setVisible(False)
641                 self._dialButton.setVisible(False)
642                 self._window.show()
643
644         @QtCore.pyqtSlot()
645         @misc_utils.log_exception(_moduleLogger)
646         def _on_calling_started(self):
647                 self._cancelButton.setVisible(True)
648
649         @QtCore.pyqtSlot()
650         @misc_utils.log_exception(_moduleLogger)
651         def _on_op_finished(self):
652                 self._smsEntry.setPlainText("")
653                 self._smsEntry.setReadOnly(False)
654                 self._cancelButton.setVisible(False)
655                 self._smsButton.setVisible(True)
656                 self._dialButton.setVisible(True)
657                 self._window.hide()
658
659         @QtCore.pyqtSlot()
660         @misc_utils.log_exception(_moduleLogger)
661         def _on_op_error(self, message):
662                 self._smsEntry.setReadOnly(False)
663                 self._cancelButton.setVisible(False)
664                 self._smsButton.setVisible(True)
665                 self._dialButton.setVisible(True)
666
667                 self._errorLog.push_error(message)
668
669         @QtCore.pyqtSlot()
670         @misc_utils.log_exception(_moduleLogger)
671         def _on_letter_count_changed(self):
672                 self._update_letter_count()
673                 self._update_button_state()
674
675         @QtCore.pyqtSlot()
676         @QtCore.pyqtSlot(bool)
677         @misc_utils.log_exception(_moduleLogger)
678         def _on_close_window(self, checked = True):
679                 self._window.hide()
680
681
682 def _get_contact_numbers(session, contactId, numberDescription):
683         contactPhoneNumbers = []
684         if contactId and contactId != "0":
685                 try:
686                         contactDetails = copy.deepcopy(session.get_contacts()[contactId])
687                         contactPhoneNumbers = contactDetails["numbers"]
688                 except KeyError:
689                         contactPhoneNumbers = []
690                 contactPhoneNumbers = [
691                         (contactPhoneNumber["phoneNumber"], contactPhoneNumber.get("phoneType", "Unknown"))
692                         for contactPhoneNumber in contactPhoneNumbers
693                 ]
694                 if contactPhoneNumbers:
695                         uglyContactNumbers = (
696                                 misc_utils.make_ugly(contactNumber)
697                                 for (contactNumber, _) in contactPhoneNumbers
698                         )
699                         defaultMatches = [
700                                 misc_utils.similar_ugly_numbers(numberDescription[0], contactNumber)
701                                 for contactNumber in uglyContactNumbers
702                         ]
703                         try:
704                                 defaultIndex = defaultMatches.index(True)
705                         except ValueError:
706                                 contactPhoneNumbers.append(numberDescription)
707                                 defaultIndex = len(contactPhoneNumbers)-1
708                                 _moduleLogger.warn(
709                                         "Could not find contact %r's number %s among %r" % (
710                                                 contactId, numberDescription, contactPhoneNumbers
711                                         )
712                                 )
713
714         if not contactPhoneNumbers:
715                 contactPhoneNumbers = [numberDescription]
716                 defaultIndex = -1
717
718         return contactPhoneNumbers, defaultIndex