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