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