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