Fixing letter count on first sms dialog launch
[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                                 raise RuntimeError("Login Cancelled")
69                         else:
70                                 raise RuntimeError("Unknown Response")
71                 finally:
72                         self._dialog.setParent(None, QtCore.Qt.Dialog)
73
74         def close(self):
75                 self._dialog.reject()
76
77         @QtCore.pyqtSlot()
78         @QtCore.pyqtSlot(bool)
79         @misc_utils.log_exception(_moduleLogger)
80         def _on_close_window(self, checked = True):
81                 self._dialog.reject()
82
83
84 class AboutDialog(object):
85
86         def __init__(self, app):
87                 self._title = QtGui.QLabel(
88                         "<h1>%s</h1><h3>Version: %s</h3>" % (
89                                 constants.__pretty_app_name__, constants.__version__
90                         )
91                 )
92                 self._title.setTextFormat(QtCore.Qt.RichText)
93                 self._title.setAlignment(QtCore.Qt.AlignCenter)
94                 self._copyright = QtGui.QLabel("<h6>Developed by Ed Page<h6><h6>Icons: See website</h6>")
95                 self._copyright.setTextFormat(QtCore.Qt.RichText)
96                 self._copyright.setAlignment(QtCore.Qt.AlignCenter)
97                 self._link = QtGui.QLabel('<a href="http://gc-dialer.garage.maemo.org">DialCentral Website</a>')
98                 self._link.setTextFormat(QtCore.Qt.RichText)
99                 self._link.setAlignment(QtCore.Qt.AlignCenter)
100                 self._link.setOpenExternalLinks(True)
101
102                 self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
103
104                 self._layout = QtGui.QVBoxLayout()
105                 self._layout.addWidget(self._title)
106                 self._layout.addWidget(self._copyright)
107                 self._layout.addWidget(self._link)
108                 self._layout.addWidget(self._buttonLayout)
109
110                 self._dialog = QtGui.QDialog()
111                 self._dialog.setWindowTitle("About")
112                 self._dialog.setLayout(self._layout)
113                 self._buttonLayout.rejected.connect(self._dialog.reject)
114
115                 self._closeWindowAction = QtGui.QAction(None)
116                 self._closeWindowAction.setText("Close")
117                 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
118                 self._closeWindowAction.triggered.connect(self._on_close_window)
119
120                 self._dialog.addAction(self._closeWindowAction)
121                 self._dialog.addAction(app.quitAction)
122                 self._dialog.addAction(app.fullscreenAction)
123
124         def run(self, parent=None):
125                 self._dialog.setParent(parent, QtCore.Qt.Dialog)
126
127                 response = self._dialog.exec_()
128                 return response
129
130         def close(self):
131                 self._dialog.reject()
132
133         @QtCore.pyqtSlot()
134         @QtCore.pyqtSlot(bool)
135         @misc_utils.log_exception(_moduleLogger)
136         def _on_close_window(self, checked = True):
137                 self._dialog.reject()
138
139
140 class AccountDialog(object):
141
142         # @bug Can't enter custom callback numbers
143
144         _RECURRENCE_CHOICES = [
145                 (1, "1 minute"),
146                 (2, "2 minutes"),
147                 (3, "3 minutes"),
148                 (5, "5 minutes"),
149                 (8, "8 minutes"),
150                 (10, "10 minutes"),
151                 (15, "15 minutes"),
152                 (30, "30 minutes"),
153                 (45, "45 minutes"),
154                 (60, "1 hour"),
155                 (3*60, "3 hours"),
156                 (6*60, "6 hours"),
157                 (12*60, "12 hours"),
158         ]
159
160         def __init__(self, app):
161                 self._doClear = False
162
163                 self._accountNumberLabel = QtGui.QLabel("NUMBER NOT SET")
164                 self._notificationButton = QtGui.QCheckBox("Notifications")
165                 self._notificationButton.stateChanged.connect(self._on_notification_change)
166                 self._notificationTimeSelector = QtGui.QComboBox()
167                 #self._notificationTimeSelector.setEditable(True)
168                 self._notificationTimeSelector.setInsertPolicy(QtGui.QComboBox.InsertAtTop)
169                 for _, label in self._RECURRENCE_CHOICES:
170                         self._notificationTimeSelector.addItem(label)
171                 self._missedCallsNotificationButton = QtGui.QCheckBox("Missed Calls")
172                 self._voicemailNotificationButton = QtGui.QCheckBox("Voicemail")
173                 self._smsNotificationButton = QtGui.QCheckBox("SMS")
174                 self._clearButton = QtGui.QPushButton("Clear Account")
175                 self._clearButton.clicked.connect(self._on_clear)
176                 self._callbackSelector = QtGui.QComboBox()
177                 #self._callbackSelector.setEditable(True)
178                 self._callbackSelector.setInsertPolicy(QtGui.QComboBox.InsertAtTop)
179
180                 self._update_notification_state()
181
182                 self._credLayout = QtGui.QGridLayout()
183                 self._credLayout.addWidget(QtGui.QLabel("Account"), 0, 0)
184                 self._credLayout.addWidget(self._accountNumberLabel, 0, 1)
185                 self._credLayout.addWidget(QtGui.QLabel("Callback"), 1, 0)
186                 self._credLayout.addWidget(self._callbackSelector, 1, 1)
187                 self._credLayout.addWidget(self._notificationButton, 2, 0)
188                 self._credLayout.addWidget(self._notificationTimeSelector, 2, 1)
189                 self._credLayout.addWidget(QtGui.QLabel(""), 3, 0)
190                 self._credLayout.addWidget(self._missedCallsNotificationButton, 3, 1)
191                 self._credLayout.addWidget(QtGui.QLabel(""), 4, 0)
192                 self._credLayout.addWidget(self._voicemailNotificationButton, 4, 1)
193                 self._credLayout.addWidget(QtGui.QLabel(""), 5, 0)
194                 self._credLayout.addWidget(self._smsNotificationButton, 5, 1)
195
196                 self._credLayout.addWidget(QtGui.QLabel(""), 6, 0)
197                 self._credLayout.addWidget(self._clearButton, 6, 1)
198                 self._credLayout.addWidget(QtGui.QLabel(""), 3, 0)
199
200                 self._loginButton = QtGui.QPushButton("&Apply")
201                 self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
202                 self._buttonLayout.addButton(self._loginButton, QtGui.QDialogButtonBox.AcceptRole)
203
204                 self._layout = QtGui.QVBoxLayout()
205                 self._layout.addLayout(self._credLayout)
206                 self._layout.addWidget(self._buttonLayout)
207
208                 self._dialog = QtGui.QDialog()
209                 self._dialog.setWindowTitle("Account")
210                 self._dialog.setLayout(self._layout)
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 number, description in 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(self._callbackSelector.count() - 1)
302
303         def run(self, parent=None):
304                 self._doClear = False
305                 self._dialog.setParent(parent, QtCore.Qt.Dialog)
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         @misc_utils.log_exception(_moduleLogger)
327         def _on_notification_change(self, state):
328                 self._update_notification_state()
329
330         @QtCore.pyqtSlot()
331         @QtCore.pyqtSlot(bool)
332         @misc_utils.log_exception(_moduleLogger)
333         def _on_clear(self, checked = False):
334                 self._doClear = True
335                 self._dialog.accept()
336
337         @QtCore.pyqtSlot()
338         @QtCore.pyqtSlot(bool)
339         @misc_utils.log_exception(_moduleLogger)
340         def _on_close_window(self, checked = True):
341                 self._dialog.reject()
342
343
344 class SMSEntryWindow(object):
345
346         MAX_CHAR = 160
347
348         def __init__(self, parent, app, session, errorLog):
349                 self._session = session
350                 self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
351
352                 self._session.draft.sendingMessage.connect(self._on_op_started)
353                 self._session.draft.calling.connect(self._on_op_started)
354                 self._session.draft.calling.connect(self._on_calling_started)
355                 self._session.draft.cancelling.connect(self._on_op_started)
356
357                 self._session.draft.sentMessage.connect(self._on_op_finished)
358                 self._session.draft.called.connect(self._on_op_finished)
359                 self._session.draft.cancelled.connect(self._on_op_finished)
360                 self._session.draft.error.connect(self._on_op_error)
361                 self._errorLog = errorLog
362
363                 self._errorDisplay = qui_utils.ErrorDisplay(self._errorLog)
364
365                 self._targetLayout = QtGui.QVBoxLayout()
366                 self._targetList = QtGui.QWidget()
367                 self._targetList.setLayout(self._targetLayout)
368                 self._history = QtGui.QLabel()
369                 self._history.setTextFormat(QtCore.Qt.RichText)
370                 self._history.setWordWrap(True)
371                 self._smsEntry = QtGui.QTextEdit()
372                 self._smsEntry.textChanged.connect(self._on_letter_count_changed)
373
374                 self._entryLayout = QtGui.QVBoxLayout()
375                 self._entryLayout.addWidget(self._targetList)
376                 self._entryLayout.addWidget(self._history)
377                 self._entryLayout.addWidget(self._smsEntry)
378                 self._entryLayout.setContentsMargins(0, 0, 0, 0)
379                 self._entryWidget = QtGui.QWidget()
380                 self._entryWidget.setLayout(self._entryLayout)
381                 self._entryWidget.setContentsMargins(0, 0, 0, 0)
382                 self._scrollEntry = QtGui.QScrollArea()
383                 self._scrollEntry.setWidget(self._entryWidget)
384                 self._scrollEntry.setWidgetResizable(True)
385                 self._scrollEntry.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignBottom)
386                 self._scrollEntry.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
387                 self._scrollEntry.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
388
389                 self._characterCountLabel = QtGui.QLabel("")
390                 self._singleNumberSelector = QtGui.QComboBox()
391                 self._singleNumbersCID = None
392                 self._singleNumberSelector.activated.connect(self._on_single_change_number)
393                 self._smsButton = QtGui.QPushButton("SMS")
394                 self._smsButton.clicked.connect(self._on_sms_clicked)
395                 self._smsButton.setEnabled(False)
396                 self._dialButton = QtGui.QPushButton("Dial")
397                 self._dialButton.clicked.connect(self._on_call_clicked)
398                 self._cancelButton = QtGui.QPushButton("Cancel Call")
399                 self._cancelButton.clicked.connect(self._on_cancel_clicked)
400                 self._cancelButton.setVisible(False)
401
402                 self._buttonLayout = QtGui.QHBoxLayout()
403                 self._buttonLayout.addWidget(self._characterCountLabel)
404                 self._buttonLayout.addWidget(self._singleNumberSelector)
405                 self._buttonLayout.addWidget(self._smsButton)
406                 self._buttonLayout.addWidget(self._dialButton)
407                 self._buttonLayout.addWidget(self._cancelButton)
408
409                 self._layout = QtGui.QVBoxLayout()
410                 self._layout.addWidget(self._errorDisplay.toplevel)
411                 self._layout.addWidget(self._scrollEntry)
412                 self._layout.addLayout(self._buttonLayout)
413
414                 centralWidget = QtGui.QWidget()
415                 centralWidget.setLayout(self._layout)
416
417                 self._window = QtGui.QMainWindow(parent)
418                 qui_utils.set_window_orientation(self._window, QtCore.Qt.Horizontal)
419                 qui_utils.set_stackable(self._window, True)
420                 self._window.setWindowTitle("Contact")
421                 self._window.setCentralWidget(centralWidget)
422
423                 self._closeWindowAction = QtGui.QAction(None)
424                 self._closeWindowAction.setText("Close")
425                 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
426                 self._closeWindowAction.triggered.connect(self._on_close_window)
427
428                 fileMenu = self._window.menuBar().addMenu("&File")
429                 fileMenu.addAction(self._closeWindowAction)
430                 fileMenu.addAction(app.quitAction)
431                 viewMenu = self._window.menuBar().addMenu("&View")
432                 viewMenu.addAction(app.fullscreenAction)
433
434                 self._scrollTimer = QtCore.QTimer()
435                 self._scrollTimer.setInterval(0)
436                 self._scrollTimer.setSingleShot(True)
437                 self._scrollTimer.timeout.connect(self._on_delayed_scroll_to_bottom)
438
439                 self._window.show()
440                 self._update_letter_count()
441                 self._update_target_fields()
442
443         def close(self):
444                 self._window.destroy()
445                 self._window = None
446
447         def _update_letter_count(self):
448                 count = self._smsEntry.toPlainText().size()
449                 numTexts, numCharInText = divmod(count, self.MAX_CHAR)
450                 numTexts += 1
451                 numCharsLeftInText = self.MAX_CHAR - numCharInText
452                 self._characterCountLabel.setText("%d (%d)" % (numCharsLeftInText, numTexts))
453
454         def _update_button_state(self):
455                 self._cancelButton.setEnabled(True)
456                 if self._session.draft.get_num_contacts() == 0:
457                         self._dialButton.setEnabled(False)
458                         self._smsButton.setEnabled(False)
459                 elif self._session.draft.get_num_contacts() == 1:
460                         count = self._smsEntry.toPlainText().size()
461                         if count == 0:
462                                 self._dialButton.setEnabled(True)
463                                 self._smsButton.setEnabled(False)
464                         else:
465                                 self._dialButton.setEnabled(False)
466                                 self._smsButton.setEnabled(True)
467                 else:
468                         self._dialButton.setEnabled(False)
469                         count = self._smsEntry.toPlainText().size()
470                         if count == 0:
471                                 self._smsButton.setEnabled(False)
472                         else:
473                                 self._smsButton.setEnabled(True)
474
475         def _update_target_fields(self):
476                 draftContactsCount = self._session.draft.get_num_contacts()
477                 if draftContactsCount == 0:
478                         self._clear_target_list()
479                         self._window.hide()
480                         self._singleNumbersCID = None
481                 elif draftContactsCount == 1:
482                         (cid, ) = self._session.draft.get_contacts()
483                         title = self._session.draft.get_title(cid)
484                         description = self._session.draft.get_description(cid)
485                         numbers = self._session.draft.get_numbers(cid)
486
487                         self._targetList.setVisible(False)
488                         self._clear_target_list()
489                         if description:
490                                 self._history.setText(description)
491                                 self._history.setVisible(True)
492                         else:
493                                 self._history.setText("")
494                                 self._history.setVisible(False)
495                         self._populate_number_selector(self._singleNumberSelector, cid, numbers)
496                         self._singleNumbersCID = None
497
498                         self._scroll_to_bottom()
499                         self._window.setWindowTitle(title)
500                         self._window.show()
501                         self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason)
502                 else:
503                         self._targetList.setVisible(True)
504                         self._clear_target_list()
505                         for cid in self._session.draft.get_contacts():
506                                 title = self._session.draft.get_title(cid)
507                                 description = self._session.draft.get_description(cid)
508                                 numbers = self._session.draft.get_numbers(cid)
509
510                                 titleLabel = QtGui.QLabel(title)
511                                 numberSelector = QtGui.QComboBox()
512                                 self._populate_number_selector(numberSelector, cid, numbers)
513                                 deleteButton = QtGui.QPushButton("Delete")
514                                 callback = functools.partial(
515                                         self._on_remove_contact,
516                                         cid
517                                 )
518                                 callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
519                                 deleteButton.clicked.connect(callback)
520
521                                 rowLayout = QtGui.QHBoxLayout()
522                                 rowLayout.addWidget(titleLabel)
523                                 rowLayout.addWidget(numberSelector)
524                                 rowLayout.addWidget(deleteButton)
525                                 rowWidget = QtGui.QWidget()
526                                 rowWidget.setLayout(rowLayout)
527                                 self._targetLayout.addWidget(rowWidget)
528                         self._history.setText("")
529                         self._history.setVisible(False)
530                         self._singleNumberSelector.setVisible(False)
531                         self._singleNumbersCID = None
532
533                         self._scroll_to_bottom()
534                         self._window.setWindowTitle("Contacts")
535                         self._window.show()
536                         self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason)
537
538         def _clear_target_list(self):
539                 while self._targetLayout.count():
540                         removedLayoutItem = self._targetLayout.takeAt(self._targetLayout.count()-1)
541                         removedWidget = removedLayoutItem.widget()
542                         removedWidget.hide()
543                         removedWidget.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
544                         removedWidget.close()
545
546         def _populate_number_selector(self, selector, cid, numbers):
547                 selector.clear()
548
549                 if len(numbers) == 1:
550                         numbers, defaultIndex = _get_contact_numbers(self._session, cid, numbers[0])
551                 else:
552                         defaultIndex = 0
553
554                 for number, description in numbers:
555                         if description:
556                                 label = "%s - %s" % (number, description)
557                         else:
558                                 label = number
559                         selector.addItem(label)
560                 selector.setVisible(True)
561                 if 1 < len(numbers):
562                         selector.setEnabled(True)
563                         selector.setCurrentIndex(defaultIndex)
564                 else:
565                         selector.setEnabled(False)
566
567                 if selector is not self._singleNumberSelector:
568                         callback = functools.partial(
569                                 self._on_change_number,
570                                 cid
571                         )
572                         callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
573                         selector.activated.connect(
574                                 QtCore.pyqtSlot(int)(callback)
575                         )
576
577         def _scroll_to_bottom(self):
578                 self._scrollTimer.start()
579
580         @misc_utils.log_exception(_moduleLogger)
581         def _on_delayed_scroll_to_bottom(self):
582                 self._scrollEntry.ensureWidgetVisible(self._smsEntry)
583
584         @misc_utils.log_exception(_moduleLogger)
585         def _on_sms_clicked(self, arg):
586                 message = unicode(self._smsEntry.toPlainText())
587                 self._session.draft.send(message)
588
589         @misc_utils.log_exception(_moduleLogger)
590         def _on_call_clicked(self, arg):
591                 self._session.draft.call()
592
593         @QtCore.pyqtSlot()
594         @misc_utils.log_exception(_moduleLogger)
595         def _on_cancel_clicked(self, message):
596                 self._session.draft.cancel()
597
598         @misc_utils.log_exception(_moduleLogger)
599         def _on_remove_contact(self, cid, toggled):
600                 self._session.draft.remove_contact(cid)
601
602         @misc_utils.log_exception(_moduleLogger)
603         def _on_single_change_number(self, index):
604                 # Exception thrown when the first item is removed
605                 cid = self._singleNumbersCID
606                 if cid is None:
607                         _moduleLogger.error("Number change occurred on the single selector when in multi-selector mode (%r)" % index)
608                         return
609                 try:
610                         numbers = self._session.draft.get_numbers(cid)
611                 except KeyError:
612                         _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
613                         return
614                 number = numbers[index][0]
615                 self._session.draft.set_selected_number(cid, number)
616
617         @misc_utils.log_exception(_moduleLogger)
618         def _on_change_number(self, cid, index):
619                 # Exception thrown when the first item is removed
620                 try:
621                         numbers = self._session.draft.get_numbers(cid)
622                 except KeyError:
623                         _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
624                         return
625                 number = numbers[index][0]
626                 self._session.draft.set_selected_number(cid, number)
627
628         @QtCore.pyqtSlot()
629         @misc_utils.log_exception(_moduleLogger)
630         def _on_recipients_changed(self):
631                 self._update_target_fields()
632                 self._update_button_state()
633
634         @QtCore.pyqtSlot()
635         @misc_utils.log_exception(_moduleLogger)
636         def _on_op_started(self):
637                 self._smsEntry.setReadOnly(True)
638                 self._smsButton.setVisible(False)
639                 self._dialButton.setVisible(False)
640                 self._window.show()
641
642         @QtCore.pyqtSlot()
643         @misc_utils.log_exception(_moduleLogger)
644         def _on_calling_started(self):
645                 self._cancelButton.setVisible(True)
646
647         @QtCore.pyqtSlot()
648         @misc_utils.log_exception(_moduleLogger)
649         def _on_op_finished(self):
650                 self._smsEntry.setPlainText("")
651                 self._smsEntry.setReadOnly(False)
652                 self._cancelButton.setVisible(False)
653                 self._smsButton.setVisible(True)
654                 self._dialButton.setVisible(True)
655                 self._window.hide()
656
657         @QtCore.pyqtSlot()
658         @misc_utils.log_exception(_moduleLogger)
659         def _on_op_error(self, message):
660                 self._smsEntry.setReadOnly(False)
661                 self._cancelButton.setVisible(False)
662                 self._smsButton.setVisible(True)
663                 self._dialButton.setVisible(True)
664
665                 self._errorLog.push_error(message)
666
667         @QtCore.pyqtSlot()
668         @misc_utils.log_exception(_moduleLogger)
669         def _on_letter_count_changed(self):
670                 self._update_letter_count()
671                 self._update_button_state()
672
673         @QtCore.pyqtSlot()
674         @QtCore.pyqtSlot(bool)
675         @misc_utils.log_exception(_moduleLogger)
676         def _on_close_window(self, checked = True):
677                 self._window.hide()
678
679
680 def _get_contact_numbers(session, contactId, numberDescription):
681         contactPhoneNumbers = []
682         if contactId and contactId != "0":
683                 try:
684                         contactDetails = copy.deepcopy(session.get_contacts()[contactId])
685                         contactPhoneNumbers = contactDetails["numbers"]
686                 except KeyError:
687                         contactPhoneNumbers = []
688                 contactPhoneNumbers = [
689                         (contactPhoneNumber["phoneNumber"], contactPhoneNumber.get("phoneType", "Unknown"))
690                         for contactPhoneNumber in contactPhoneNumbers
691                 ]
692                 if contactPhoneNumbers:
693                         uglyContactNumbers = (
694                                 misc_utils.make_ugly(contactNumber)
695                                 for (contactNumber, _) in contactPhoneNumbers
696                         )
697                         defaultMatches = [
698                                 misc_utils.similar_ugly_numbers(numberDescription[0], contactNumber)
699                                 for contactNumber in uglyContactNumbers
700                         ]
701                         try:
702                                 defaultIndex = defaultMatches.index(True)
703                         except ValueError:
704                                 contactPhoneNumbers.append(numberDescription)
705                                 defaultIndex = len(contactPhoneNumbers)-1
706                                 _moduleLogger.warn(
707                                         "Could not find contact %r's number %s among %r" % (
708                                                 contactId, numberDescription, contactPhoneNumbers
709                                         )
710                                 )
711
712         if not contactPhoneNumbers:
713                 contactPhoneNumbers = [numberDescription]
714                 defaultIndex = -1
715
716         return contactPhoneNumbers, defaultIndex