Fixing some issues in propogating fullscreen/orientation settings to child windows
[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         accountNumber = property(
255                 lambda self: str(self._accountNumberLabel.text()),
256                 lambda self, num: self._accountNumberLabel.setText(num),
257         )
258
259         notifications = property(
260                 lambda self: self._notificationButton.isChecked(),
261                 lambda self, enabled: self._notificationButton.setChecked(enabled),
262         )
263
264         notifyOnMissed = property(
265                 lambda self: self._missedCallsNotificationButton.isChecked(),
266                 lambda self, enabled: self._missedCallsNotificationButton.setChecked(enabled),
267         )
268
269         notifyOnVoicemail = property(
270                 lambda self: self._voicemailNotificationButton.isChecked(),
271                 lambda self, enabled: self._voicemailNotificationButton.setChecked(enabled),
272         )
273
274         notifyOnSms = property(
275                 lambda self: self._smsNotificationButton.isChecked(),
276                 lambda self, enabled: self._smsNotificationButton.setChecked(enabled),
277         )
278
279         def _get_notification_time(self):
280                 index = self._notificationTimeSelector.currentIndex()
281                 minutes = self._RECURRENCE_CHOICES[index][0]
282                 return minutes
283
284         def _set_notification_time(self, minutes):
285                 for i, (time, _) in enumerate(self._RECURRENCE_CHOICES):
286                         if time == minutes:
287                                 self._notificationTimeSelector.setCurrentIndex(i)
288                                 break
289                 else:
290                                 self._notificationTimeSelector.setCurrentIndex(0)
291
292         notificationTime = property(_get_notification_time, _set_notification_time)
293
294         @property
295         def selectedCallback(self):
296                 index = self._callbackSelector.currentIndex()
297                 data = str(self._callbackSelector.itemData(index).toPyObject())
298                 return data
299
300         def set_callbacks(self, choices, default):
301                 self._callbackSelector.clear()
302
303                 self._callbackSelector.addItem("Not Set", "")
304
305                 uglyDefault = misc_utils.make_ugly(default)
306                 for number, description in choices.iteritems():
307                         prettyNumber = misc_utils.make_pretty(number)
308                         uglyNumber = misc_utils.make_ugly(number)
309                         if not uglyNumber:
310                                 continue
311
312                         self._callbackSelector.addItem("%s - %s" % (prettyNumber, description), uglyNumber)
313                         if uglyNumber == uglyDefault:
314                                 self._callbackSelector.setCurrentIndex(self._callbackSelector.count() - 1)
315
316         def run(self, parent=None):
317                 self._doClear = False
318                 self._dialog.setParent(parent, QtCore.Qt.Dialog)
319
320                 response = self._dialog.exec_()
321                 return response
322
323         def close(self):
324                 try:
325                         self._dialog.reject()
326                 except RuntimeError:
327                         _moduleLogger.exception("Oh well")
328
329         def _update_notification_state(self):
330                 if self._notificationButton.isChecked():
331                         self._notificationTimeSelector.setEnabled(True)
332                         self._missedCallsNotificationButton.setEnabled(True)
333                         self._voicemailNotificationButton.setEnabled(True)
334                         self._smsNotificationButton.setEnabled(True)
335                 else:
336                         self._notificationTimeSelector.setEnabled(False)
337                         self._missedCallsNotificationButton.setEnabled(False)
338                         self._voicemailNotificationButton.setEnabled(False)
339                         self._smsNotificationButton.setEnabled(False)
340
341         @QtCore.pyqtSlot(int)
342         @misc_utils.log_exception(_moduleLogger)
343         def _on_notification_change(self, state):
344                 with qui_utils.notify_error(self._app.errorLog):
345                         self._update_notification_state()
346
347         @QtCore.pyqtSlot()
348         @QtCore.pyqtSlot(bool)
349         @misc_utils.log_exception(_moduleLogger)
350         def _on_clear(self, checked = False):
351                 with qui_utils.notify_error(self._app.errorLog):
352                         self._doClear = True
353                         self._dialog.accept()
354
355         @QtCore.pyqtSlot()
356         @QtCore.pyqtSlot(bool)
357         @misc_utils.log_exception(_moduleLogger)
358         def _on_close_window(self, checked = True):
359                 with qui_utils.notify_error(self._app.errorLog):
360                         self._dialog.reject()
361
362
363 class ContactList(object):
364
365         _SENTINEL_ICON = QtGui.QIcon()
366
367         def __init__(self, app, session):
368                 self._app = app
369                 self._session = session
370                 self._targetLayout = QtGui.QVBoxLayout()
371                 self._targetList = QtGui.QWidget()
372                 self._targetList.setLayout(self._targetLayout)
373                 self._uiItems = []
374                 self._closeIcon = qui_utils.get_theme_icon(("window-close", "general_close", "gtk-close"), self._SENTINEL_ICON)
375
376         @property
377         def toplevel(self):
378                 return self._targetList
379
380         def setVisible(self, isVisible):
381                 self._targetList.setVisible(isVisible)
382
383         def update(self):
384                 cids = list(self._session.draft.get_contacts())
385                 amountCommon = min(len(cids), len(self._uiItems))
386
387                 # Run through everything in common
388                 for i in xrange(0, amountCommon):
389                         cid = cids[i]
390                         uiItem = self._uiItems[i]
391                         title = self._session.draft.get_title(cid)
392                         description = self._session.draft.get_description(cid)
393                         numbers = self._session.draft.get_numbers(cid)
394                         uiItem["cid"] = cid
395                         uiItem["title"] = title
396                         uiItem["description"] = description
397                         uiItem["numbers"] = numbers
398                         uiItem["label"].setText(title)
399                         self._populate_number_selector(uiItem["selector"], cid, i, numbers)
400                         uiItem["rowWidget"].setVisible(True)
401
402                 # More contacts than ui items
403                 for i in xrange(amountCommon, len(cids)):
404                         cid = cids[i]
405                         title = self._session.draft.get_title(cid)
406                         description = self._session.draft.get_description(cid)
407                         numbers = self._session.draft.get_numbers(cid)
408
409                         titleLabel = QtGui.QLabel(title)
410                         titleLabel.setWordWrap(True)
411                         numberSelector = QtGui.QComboBox()
412                         self._populate_number_selector(numberSelector, cid, i, numbers)
413
414                         callback = functools.partial(
415                                 self._on_change_number,
416                                 i
417                         )
418                         callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
419                         numberSelector.activated.connect(
420                                 QtCore.pyqtSlot(int)(callback)
421                         )
422
423                         if self._closeIcon is self._SENTINEL_ICON:
424                                 deleteButton = QtGui.QPushButton("Delete")
425                         else:
426                                 deleteButton = QtGui.QPushButton(self._closeIcon, "")
427                         deleteButton.setSizePolicy(QtGui.QSizePolicy(
428                                 QtGui.QSizePolicy.Minimum,
429                                 QtGui.QSizePolicy.Minimum,
430                                 QtGui.QSizePolicy.PushButton,
431                         ))
432                         callback = functools.partial(
433                                 self._on_remove_contact,
434                                 i
435                         )
436                         callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
437                         deleteButton.clicked.connect(callback)
438
439                         rowLayout = QtGui.QHBoxLayout()
440                         rowLayout.addWidget(titleLabel, 1000)
441                         rowLayout.addWidget(numberSelector, 0)
442                         rowLayout.addWidget(deleteButton, 0)
443                         rowWidget = QtGui.QWidget()
444                         rowWidget.setLayout(rowLayout)
445                         self._targetLayout.addWidget(rowWidget)
446
447                         uiItem = {}
448                         uiItem["cid"] = cid
449                         uiItem["title"] = title
450                         uiItem["description"] = description
451                         uiItem["numbers"] = numbers
452                         uiItem["label"] = titleLabel
453                         uiItem["selector"] = numberSelector
454                         uiItem["rowWidget"] = rowWidget
455                         self._uiItems.append(uiItem)
456                         amountCommon = i+1
457
458                 # More UI items than contacts
459                 for i in xrange(amountCommon, len(self._uiItems)):
460                         uiItem = self._uiItems[i]
461                         uiItem["rowWidget"].setVisible(False)
462                         amountCommon = i+1
463
464         def _populate_number_selector(self, selector, cid, cidIndex, numbers):
465                 selector.clear()
466
467                 selectedNumber = self._session.draft.get_selected_number(cid)
468                 if len(numbers) == 1:
469                         # If no alt numbers available, check the address book
470                         numbers, defaultIndex = _get_contact_numbers(self._session, cid, selectedNumber, numbers[0][1])
471                 else:
472                         defaultIndex = _index_number(numbers, selectedNumber)
473
474                 for number, description in numbers:
475                         if description:
476                                 label = "%s - %s" % (number, description)
477                         else:
478                                 label = number
479                         selector.addItem(label)
480                 selector.setVisible(True)
481                 if 1 < len(numbers):
482                         selector.setEnabled(True)
483                         selector.setCurrentIndex(defaultIndex)
484                 else:
485                         selector.setEnabled(False)
486
487         @misc_utils.log_exception(_moduleLogger)
488         def _on_change_number(self, cidIndex, index):
489                 with qui_utils.notify_error(self._app.errorLog):
490                         # Exception thrown when the first item is removed
491                         try:
492                                 cid = self._uiItems[cidIndex]["cid"]
493                                 numbers = self._session.draft.get_numbers(cid)
494                         except IndexError:
495                                 _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
496                                 return
497                         except KeyError:
498                                 _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
499                                 return
500                         number = numbers[index][0]
501                         self._session.draft.set_selected_number(cid, number)
502
503         @misc_utils.log_exception(_moduleLogger)
504         def _on_remove_contact(self, index, toggled):
505                 with qui_utils.notify_error(self._app.errorLog):
506                         self._session.draft.remove_contact(self._uiItems[index]["cid"])
507
508
509 class SMSEntryWindow(qwrappers.WindowWrapper):
510
511         MAX_CHAR = 160
512         # @bug Somehow a window is being destroyed on object creation which causes glitches on Maemo 5
513
514         def __init__(self, parent, app, session, errorLog):
515                 qwrappers.WindowWrapper.__init__(self, parent, app)
516                 self._session = session
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.draft.recipientsChanged.disconnect(self._on_recipients_changed)
610                 self._session.draft.sendingMessage.disconnect(self._on_op_started)
611                 self._session.draft.calling.disconnect(self._on_op_started)
612                 self._session.draft.calling.disconnect(self._on_calling_started)
613                 self._session.draft.cancelling.disconnect(self._on_op_started)
614                 self._session.draft.sentMessage.disconnect(self._on_op_finished)
615                 self._session.draft.called.disconnect(self._on_op_finished)
616                 self._session.draft.cancelled.disconnect(self._on_op_finished)
617                 self._session.draft.error.disconnect(self._on_op_error)
618                 window = self._window
619                 self._window = None
620                 try:
621                         window.close()
622                         window.destroy()
623                 except AttributeError:
624                         _moduleLogger.exception("Oh well")
625                 except RuntimeError:
626                         _moduleLogger.exception("Oh well")
627
628         def set_orientation(self, isPortrait):
629                 qwrappers.WindowWrapper.set_orientation(self, isPortrait)
630                 self._scroll_to_bottom()
631
632         def _update_letter_count(self):
633                 count = self._smsEntry.toPlainText().size()
634                 numTexts, numCharInText = divmod(count, self.MAX_CHAR)
635                 numTexts += 1
636                 numCharsLeftInText = self.MAX_CHAR - numCharInText
637                 self._characterCountLabel.setText("%d (%d)" % (numCharsLeftInText, numTexts))
638
639         def _update_button_state(self):
640                 self._cancelButton.setEnabled(True)
641                 if self._session.draft.get_num_contacts() == 0:
642                         self._dialButton.setEnabled(False)
643                         self._smsButton.setEnabled(False)
644                 elif self._session.draft.get_num_contacts() == 1:
645                         count = self._smsEntry.toPlainText().size()
646                         if count == 0:
647                                 self._dialButton.setEnabled(True)
648                                 self._smsButton.setEnabled(False)
649                         else:
650                                 self._dialButton.setEnabled(False)
651                                 self._smsButton.setEnabled(True)
652                 else:
653                         self._dialButton.setEnabled(False)
654                         count = self._smsEntry.toPlainText().size()
655                         if count == 0:
656                                 self._smsButton.setEnabled(False)
657                         else:
658                                 self._smsButton.setEnabled(True)
659
660         def _update_target_fields(self):
661                 draftContactsCount = self._session.draft.get_num_contacts()
662                 if draftContactsCount == 0:
663                         self.hide()
664                         del self._cids[:]
665                 elif draftContactsCount == 1:
666                         (cid, ) = self._session.draft.get_contacts()
667                         title = self._session.draft.get_title(cid)
668                         description = self._session.draft.get_description(cid)
669                         numbers = self._session.draft.get_numbers(cid)
670
671                         self._targetList.setVisible(False)
672                         if description:
673                                 self._history.setText(description)
674                                 self._history.setVisible(True)
675                         else:
676                                 self._history.setText("")
677                                 self._history.setVisible(False)
678                         self._populate_number_selector(self._singleNumberSelector, cid, 0, numbers)
679                         self._cids = [cid]
680
681                         self._scroll_to_bottom()
682                         self._window.setWindowTitle(title)
683                         self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason)
684                         self.show()
685                         self._window.raise_()
686                 else:
687                         self._targetList.setVisible(True)
688                         self._targetList.update()
689                         self._history.setText("")
690                         self._history.setVisible(False)
691                         self._singleNumberSelector.setVisible(False)
692
693                         self._scroll_to_bottom()
694                         self._window.setWindowTitle("Contacts")
695                         self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason)
696                         self.show()
697                         self._window.raise_()
698
699         def _populate_number_selector(self, selector, cid, cidIndex, numbers):
700                 selector.clear()
701
702                 selectedNumber = self._session.draft.get_selected_number(cid)
703                 if len(numbers) == 1:
704                         # If no alt numbers available, check the address book
705                         numbers, defaultIndex = _get_contact_numbers(self._session, cid, selectedNumber, numbers[0][1])
706                 else:
707                         defaultIndex = _index_number(numbers, selectedNumber)
708
709                 for number, description in numbers:
710                         if description:
711                                 label = "%s - %s" % (number, description)
712                         else:
713                                 label = number
714                         selector.addItem(label)
715                 selector.setVisible(True)
716                 if 1 < len(numbers):
717                         selector.setEnabled(True)
718                         selector.setCurrentIndex(defaultIndex)
719                 else:
720                         selector.setEnabled(False)
721
722         def _scroll_to_bottom(self):
723                 self._scrollTimer.start()
724
725         @misc_utils.log_exception(_moduleLogger)
726         def _on_delayed_scroll_to_bottom(self):
727                 with qui_utils.notify_error(self._app.errorLog):
728                         self._scrollEntry.ensureWidgetVisible(self._smsEntry)
729
730         @misc_utils.log_exception(_moduleLogger)
731         def _on_sms_clicked(self, arg):
732                 with qui_utils.notify_error(self._app.errorLog):
733                         message = unicode(self._smsEntry.toPlainText())
734                         self._session.draft.message = message
735                         self._session.draft.send()
736
737         @misc_utils.log_exception(_moduleLogger)
738         def _on_call_clicked(self, arg):
739                 with qui_utils.notify_error(self._app.errorLog):
740                         message = unicode(self._smsEntry.toPlainText())
741                         self._session.draft.message = message
742                         self._session.draft.call()
743
744         @QtCore.pyqtSlot()
745         @misc_utils.log_exception(_moduleLogger)
746         def _on_cancel_clicked(self, message):
747                 with qui_utils.notify_error(self._app.errorLog):
748                         self._session.draft.cancel()
749
750         @misc_utils.log_exception(_moduleLogger)
751         def _on_single_change_number(self, index):
752                 with qui_utils.notify_error(self._app.errorLog):
753                         # Exception thrown when the first item is removed
754                         cid = self._cids[0]
755                         try:
756                                 numbers = self._session.draft.get_numbers(cid)
757                         except KeyError:
758                                 _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
759                                 return
760                         number = numbers[index][0]
761                         self._session.draft.set_selected_number(cid, number)
762
763         @QtCore.pyqtSlot()
764         @misc_utils.log_exception(_moduleLogger)
765         def _on_recipients_changed(self):
766                 with qui_utils.notify_error(self._app.errorLog):
767                         self._update_target_fields()
768                         self._update_button_state()
769
770         @QtCore.pyqtSlot()
771         @misc_utils.log_exception(_moduleLogger)
772         def _on_op_started(self):
773                 with qui_utils.notify_error(self._app.errorLog):
774                         self._smsEntry.setReadOnly(True)
775                         self._smsButton.setVisible(False)
776                         self._dialButton.setVisible(False)
777                         self.show()
778
779         @QtCore.pyqtSlot()
780         @misc_utils.log_exception(_moduleLogger)
781         def _on_calling_started(self):
782                 with qui_utils.notify_error(self._app.errorLog):
783                         self._cancelButton.setVisible(True)
784
785         @QtCore.pyqtSlot()
786         @misc_utils.log_exception(_moduleLogger)
787         def _on_op_finished(self):
788                 with qui_utils.notify_error(self._app.errorLog):
789                         self._smsEntry.setPlainText("")
790                         self._smsEntry.setReadOnly(False)
791                         self._cancelButton.setVisible(False)
792                         self._smsButton.setVisible(True)
793                         self._dialButton.setVisible(True)
794                         self.close()
795                         self.destroy()
796
797         @QtCore.pyqtSlot()
798         @misc_utils.log_exception(_moduleLogger)
799         def _on_op_error(self, message):
800                 with qui_utils.notify_error(self._app.errorLog):
801                         self._smsEntry.setReadOnly(False)
802                         self._cancelButton.setVisible(False)
803                         self._smsButton.setVisible(True)
804                         self._dialButton.setVisible(True)
805
806                         self._errorLog.push_error(message)
807
808         @QtCore.pyqtSlot()
809         @misc_utils.log_exception(_moduleLogger)
810         def _on_letter_count_changed(self):
811                 with qui_utils.notify_error(self._app.errorLog):
812                         self._update_letter_count()
813                         self._update_button_state()
814
815         @QtCore.pyqtSlot()
816         @QtCore.pyqtSlot(bool)
817         @misc_utils.log_exception(_moduleLogger)
818         def _on_close_window(self, checked = True):
819                 with qui_utils.notify_error(self._app.errorLog):
820                         self.close()
821
822
823 def _index_number(numbers, default):
824         uglyDefault = misc_utils.make_ugly(default)
825         uglyContactNumbers = list(
826                 misc_utils.make_ugly(contactNumber)
827                 for (contactNumber, _) in numbers
828         )
829         defaultMatches = [
830                 misc_utils.similar_ugly_numbers(uglyDefault, contactNumber)
831                 for contactNumber in uglyContactNumbers
832         ]
833         try:
834                 defaultIndex = defaultMatches.index(True)
835         except ValueError:
836                 defaultIndex = -1
837                 _moduleLogger.warn(
838                         "Could not find contact number %s among %r" % (
839                                 default, numbers
840                         )
841                 )
842         return defaultIndex
843
844
845 def _get_contact_numbers(session, contactId, number, description):
846         contactPhoneNumbers = []
847         if contactId and contactId != "0":
848                 try:
849                         contactDetails = copy.deepcopy(session.get_contacts()[contactId])
850                         contactPhoneNumbers = contactDetails["numbers"]
851                 except KeyError:
852                         contactPhoneNumbers = []
853                 contactPhoneNumbers = [
854                         (contactPhoneNumber["phoneNumber"], contactPhoneNumber.get("phoneType", "Unknown"))
855                         for contactPhoneNumber in contactPhoneNumbers
856                 ]
857                 defaultIndex = _index_number(contactPhoneNumbers, number)
858
859         if not contactPhoneNumbers or defaultIndex == -1:
860                 contactPhoneNumbers += [(number, description)]
861                 defaultIndex = 0
862
863         return contactPhoneNumbers, defaultIndex