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