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