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