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