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