be4a17753c5ab8067df373be836f2d2eee04e93d
[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(True)
354                         self._smsNotificationButton.setEnabled(True)
355
356                         self._missedCallsNotificationButton.setChecked(False)
357                 else:
358                         self._notificationTimeSelector.setEnabled(False)
359
360                         self._missedCallsNotificationButton.setEnabled(False)
361                         self._voicemailNotificationButton.setEnabled(False)
362                         self._smsNotificationButton.setEnabled(False)
363
364                         self._missedCallsNotificationButton.setChecked(False)
365                         self._voicemailNotificationButton.setChecked(False)
366                         self._smsNotificationButton.setChecked(False)
367
368         @QtCore.pyqtSlot(int)
369         @misc_utils.log_exception(_moduleLogger)
370         def _on_notification_change(self, index):
371                 with qui_utils.notify_error(self._app.errorLog):
372                         self._update_notification_state()
373
374         @QtCore.pyqtSlot()
375         @QtCore.pyqtSlot(bool)
376         @misc_utils.log_exception(_moduleLogger)
377         def _on_clear(self, checked = False):
378                 with qui_utils.notify_error(self._app.errorLog):
379                         self._doClear = True
380                         self._dialog.accept()
381
382         @QtCore.pyqtSlot()
383         @QtCore.pyqtSlot(bool)
384         @misc_utils.log_exception(_moduleLogger)
385         def _on_close_window(self, checked = True):
386                 with qui_utils.notify_error(self._app.errorLog):
387                         self._dialog.reject()
388
389
390 class ContactList(object):
391
392         _SENTINEL_ICON = QtGui.QIcon()
393
394         def __init__(self, app, session):
395                 self._app = app
396                 self._session = session
397                 self._targetLayout = QtGui.QVBoxLayout()
398                 self._targetList = QtGui.QWidget()
399                 self._targetList.setLayout(self._targetLayout)
400                 self._uiItems = []
401                 self._closeIcon = qui_utils.get_theme_icon(("window-close", "general_close", "gtk-close"), self._SENTINEL_ICON)
402
403         @property
404         def toplevel(self):
405                 return self._targetList
406
407         def setVisible(self, isVisible):
408                 self._targetList.setVisible(isVisible)
409
410         def update(self):
411                 cids = list(self._session.draft.get_contacts())
412                 amountCommon = min(len(cids), len(self._uiItems))
413
414                 # Run through everything in common
415                 for i in xrange(0, amountCommon):
416                         cid = cids[i]
417                         uiItem = self._uiItems[i]
418                         title = self._session.draft.get_title(cid)
419                         description = self._session.draft.get_description(cid)
420                         numbers = self._session.draft.get_numbers(cid)
421                         uiItem["cid"] = cid
422                         uiItem["title"] = title
423                         uiItem["description"] = description
424                         uiItem["numbers"] = numbers
425                         uiItem["label"].setText(title)
426                         self._populate_number_selector(uiItem["selector"], cid, i, numbers)
427                         uiItem["rowWidget"].setVisible(True)
428
429                 # More contacts than ui items
430                 for i in xrange(amountCommon, len(cids)):
431                         cid = cids[i]
432                         title = self._session.draft.get_title(cid)
433                         description = self._session.draft.get_description(cid)
434                         numbers = self._session.draft.get_numbers(cid)
435
436                         titleLabel = QtGui.QLabel(title)
437                         titleLabel.setWordWrap(True)
438                         numberSelector = QtGui.QComboBox()
439                         self._populate_number_selector(numberSelector, cid, i, numbers)
440
441                         callback = functools.partial(
442                                 self._on_change_number,
443                                 i
444                         )
445                         callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
446                         numberSelector.activated.connect(
447                                 QtCore.pyqtSlot(int)(callback)
448                         )
449
450                         if self._closeIcon is self._SENTINEL_ICON:
451                                 deleteButton = QtGui.QPushButton("Delete")
452                         else:
453                                 deleteButton = QtGui.QPushButton(self._closeIcon, "")
454                         deleteButton.setSizePolicy(QtGui.QSizePolicy(
455                                 QtGui.QSizePolicy.Minimum,
456                                 QtGui.QSizePolicy.Minimum,
457                                 QtGui.QSizePolicy.PushButton,
458                         ))
459                         callback = functools.partial(
460                                 self._on_remove_contact,
461                                 i
462                         )
463                         callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
464                         deleteButton.clicked.connect(callback)
465
466                         rowLayout = QtGui.QHBoxLayout()
467                         rowLayout.addWidget(titleLabel, 1000)
468                         rowLayout.addWidget(numberSelector, 0)
469                         rowLayout.addWidget(deleteButton, 0)
470                         rowWidget = QtGui.QWidget()
471                         rowWidget.setLayout(rowLayout)
472                         self._targetLayout.addWidget(rowWidget)
473
474                         uiItem = {}
475                         uiItem["cid"] = cid
476                         uiItem["title"] = title
477                         uiItem["description"] = description
478                         uiItem["numbers"] = numbers
479                         uiItem["label"] = titleLabel
480                         uiItem["selector"] = numberSelector
481                         uiItem["rowWidget"] = rowWidget
482                         self._uiItems.append(uiItem)
483                         amountCommon = i+1
484
485                 # More UI items than contacts
486                 for i in xrange(amountCommon, len(self._uiItems)):
487                         uiItem = self._uiItems[i]
488                         uiItem["rowWidget"].setVisible(False)
489                         amountCommon = i+1
490
491         def _populate_number_selector(self, selector, cid, cidIndex, numbers):
492                 selector.clear()
493
494                 selectedNumber = self._session.draft.get_selected_number(cid)
495                 if len(numbers) == 1:
496                         # If no alt numbers available, check the address book
497                         numbers, defaultIndex = _get_contact_numbers(self._session, cid, selectedNumber, numbers[0][1])
498                 else:
499                         defaultIndex = _index_number(numbers, selectedNumber)
500
501                 for number, description in numbers:
502                         if description:
503                                 label = "%s - %s" % (number, description)
504                         else:
505                                 label = number
506                         selector.addItem(label)
507                 selector.setVisible(True)
508                 if 1 < len(numbers):
509                         selector.setEnabled(True)
510                         selector.setCurrentIndex(defaultIndex)
511                 else:
512                         selector.setEnabled(False)
513
514         @misc_utils.log_exception(_moduleLogger)
515         def _on_change_number(self, cidIndex, index):
516                 with qui_utils.notify_error(self._app.errorLog):
517                         # Exception thrown when the first item is removed
518                         try:
519                                 cid = self._uiItems[cidIndex]["cid"]
520                                 numbers = self._session.draft.get_numbers(cid)
521                         except IndexError:
522                                 _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
523                                 return
524                         except KeyError:
525                                 _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
526                                 return
527                         number = numbers[index][0]
528                         self._session.draft.set_selected_number(cid, number)
529
530         @misc_utils.log_exception(_moduleLogger)
531         def _on_remove_contact(self, index, toggled):
532                 with qui_utils.notify_error(self._app.errorLog):
533                         self._session.draft.remove_contact(self._uiItems[index]["cid"])
534
535
536 class SMSEntryWindow(qwrappers.WindowWrapper):
537
538         MAX_CHAR = 160
539         # @bug Somehow a window is being destroyed on object creation which causes glitches on Maemo 5
540
541         def __init__(self, parent, app, session, errorLog):
542                 qwrappers.WindowWrapper.__init__(self, parent, app)
543                 self._session = session
544                 self._session.messagesUpdated.connect(self._on_refresh_history)
545                 self._session.historyUpdated.connect(self._on_refresh_history)
546                 self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
547
548                 self._session.draft.sendingMessage.connect(self._on_op_started)
549                 self._session.draft.calling.connect(self._on_op_started)
550                 self._session.draft.calling.connect(self._on_calling_started)
551                 self._session.draft.cancelling.connect(self._on_op_started)
552
553                 self._session.draft.sentMessage.connect(self._on_op_finished)
554                 self._session.draft.called.connect(self._on_op_finished)
555                 self._session.draft.cancelled.connect(self._on_op_finished)
556                 self._session.draft.error.connect(self._on_op_error)
557                 self._errorLog = errorLog
558
559                 self._errorDisplay = qui_utils.ErrorDisplay(self._errorLog)
560
561                 self._targetList = ContactList(self._app, self._session)
562                 self._history = QtGui.QLabel()
563                 self._history.setTextFormat(QtCore.Qt.RichText)
564                 self._history.setWordWrap(True)
565                 self._smsEntry = QtGui.QTextEdit()
566                 self._smsEntry.textChanged.connect(self._on_letter_count_changed)
567
568                 self._entryLayout = QtGui.QVBoxLayout()
569                 self._entryLayout.addWidget(self._targetList.toplevel)
570                 self._entryLayout.addWidget(self._history)
571                 self._entryLayout.addWidget(self._smsEntry)
572                 self._entryLayout.setContentsMargins(0, 0, 0, 0)
573                 self._entryWidget = QtGui.QWidget()
574                 self._entryWidget.setLayout(self._entryLayout)
575                 self._entryWidget.setContentsMargins(0, 0, 0, 0)
576                 self._scrollEntry = QtGui.QScrollArea()
577                 self._scrollEntry.setWidget(self._entryWidget)
578                 self._scrollEntry.setWidgetResizable(True)
579                 self._scrollEntry.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignBottom)
580                 self._scrollEntry.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
581                 self._scrollEntry.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
582
583                 self._characterCountLabel = QtGui.QLabel("")
584                 self._singleNumberSelector = QtGui.QComboBox()
585                 self._cids = []
586                 self._singleNumberSelector.activated.connect(self._on_single_change_number)
587                 self._smsButton = QtGui.QPushButton("SMS")
588                 self._smsButton.clicked.connect(self._on_sms_clicked)
589                 self._smsButton.setEnabled(False)
590                 self._dialButton = QtGui.QPushButton("Dial")
591                 self._dialButton.clicked.connect(self._on_call_clicked)
592                 self._cancelButton = QtGui.QPushButton("Cancel Call")
593                 self._cancelButton.clicked.connect(self._on_cancel_clicked)
594                 self._cancelButton.setVisible(False)
595
596                 self._buttonLayout = QtGui.QHBoxLayout()
597                 self._buttonLayout.addWidget(self._characterCountLabel)
598                 self._buttonLayout.addWidget(self._singleNumberSelector)
599                 self._buttonLayout.addWidget(self._smsButton)
600                 self._buttonLayout.addWidget(self._dialButton)
601                 self._buttonLayout.addWidget(self._cancelButton)
602
603                 self._layout.addWidget(self._errorDisplay.toplevel)
604                 self._layout.addWidget(self._scrollEntry)
605                 self._layout.addLayout(self._buttonLayout)
606                 self._layout.setDirection(QtGui.QBoxLayout.TopToBottom)
607
608                 self._window.setWindowTitle("Contact")
609                 self._window.closed.connect(self._on_close_window)
610                 self._window.hidden.connect(self._on_close_window)
611
612                 self._scrollTimer = QtCore.QTimer()
613                 self._scrollTimer.setInterval(100)
614                 self._scrollTimer.setSingleShot(True)
615                 self._scrollTimer.timeout.connect(self._on_delayed_scroll_to_bottom)
616
617                 self._smsEntry.setPlainText(self._session.draft.message)
618                 self._update_letter_count()
619                 self._update_target_fields()
620                 self.set_fullscreen(self._app.fullscreenAction.isChecked())
621                 self.set_orientation(self._app.orientationAction.isChecked())
622
623         def close(self):
624                 if self._window is None:
625                         # Already closed
626                         return
627                 window = self._window
628                 try:
629                         message = unicode(self._smsEntry.toPlainText())
630                         self._session.draft.message = message
631                         self.hide()
632                 except AttributeError:
633                         _moduleLogger.exception("Oh well")
634                 except RuntimeError:
635                         _moduleLogger.exception("Oh well")
636
637         def destroy(self):
638                 self._session.messagesUpdated.disconnect(self._on_refresh_history)
639                 self._session.historyUpdated.disconnect(self._on_refresh_history)
640                 self._session.draft.recipientsChanged.disconnect(self._on_recipients_changed)
641                 self._session.draft.sendingMessage.disconnect(self._on_op_started)
642                 self._session.draft.calling.disconnect(self._on_op_started)
643                 self._session.draft.calling.disconnect(self._on_calling_started)
644                 self._session.draft.cancelling.disconnect(self._on_op_started)
645                 self._session.draft.sentMessage.disconnect(self._on_op_finished)
646                 self._session.draft.called.disconnect(self._on_op_finished)
647                 self._session.draft.cancelled.disconnect(self._on_op_finished)
648                 self._session.draft.error.disconnect(self._on_op_error)
649                 window = self._window
650                 self._window = None
651                 try:
652                         window.close()
653                         window.destroy()
654                 except AttributeError:
655                         _moduleLogger.exception("Oh well")
656                 except RuntimeError:
657                         _moduleLogger.exception("Oh well")
658
659         def set_orientation(self, isPortrait):
660                 qwrappers.WindowWrapper.set_orientation(self, isPortrait)
661                 self._scroll_to_bottom()
662
663         def _update_letter_count(self):
664                 count = self._smsEntry.toPlainText().size()
665                 numTexts, numCharInText = divmod(count, self.MAX_CHAR)
666                 numTexts += 1
667                 numCharsLeftInText = self.MAX_CHAR - numCharInText
668                 self._characterCountLabel.setText("%d (%d)" % (numCharsLeftInText, numTexts))
669
670         def _update_button_state(self):
671                 self._cancelButton.setEnabled(True)
672                 if self._session.draft.get_num_contacts() == 0:
673                         self._dialButton.setEnabled(False)
674                         self._smsButton.setEnabled(False)
675                 elif self._session.draft.get_num_contacts() == 1:
676                         count = self._smsEntry.toPlainText().size()
677                         if count == 0:
678                                 self._dialButton.setEnabled(True)
679                                 self._smsButton.setEnabled(False)
680                         else:
681                                 self._dialButton.setEnabled(False)
682                                 self._smsButton.setEnabled(True)
683                 else:
684                         self._dialButton.setEnabled(False)
685                         count = self._smsEntry.toPlainText().size()
686                         if count == 0:
687                                 self._smsButton.setEnabled(False)
688                         else:
689                                 self._smsButton.setEnabled(True)
690
691         def _update_history(self, cid):
692                 draftContactsCount = self._session.draft.get_num_contacts()
693                 if draftContactsCount != 1:
694                         self._history.setVisible(False)
695                 else:
696                         description = self._session.draft.get_description(cid)
697
698                         self._targetList.setVisible(False)
699                         if description:
700                                 self._history.setText(description)
701                                 self._history.setVisible(True)
702                         else:
703                                 self._history.setText("")
704                                 self._history.setVisible(False)
705
706         def _update_target_fields(self):
707                 draftContactsCount = self._session.draft.get_num_contacts()
708                 if draftContactsCount == 0:
709                         self.hide()
710                         del self._cids[:]
711                 elif draftContactsCount == 1:
712                         (cid, ) = self._session.draft.get_contacts()
713                         title = self._session.draft.get_title(cid)
714                         numbers = self._session.draft.get_numbers(cid)
715
716                         self._targetList.setVisible(False)
717                         self._update_history(cid)
718                         self._populate_number_selector(self._singleNumberSelector, cid, 0, numbers)
719                         self._cids = [cid]
720
721                         self._scroll_to_bottom()
722                         self._window.setWindowTitle(title)
723                         self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason)
724                         self.show()
725                         self._window.raise_()
726                 else:
727                         self._targetList.setVisible(True)
728                         self._targetList.update()
729                         self._history.setText("")
730                         self._history.setVisible(False)
731                         self._singleNumberSelector.setVisible(False)
732
733                         self._scroll_to_bottom()
734                         self._window.setWindowTitle("Contacts")
735                         self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason)
736                         self.show()
737                         self._window.raise_()
738
739         def _populate_number_selector(self, selector, cid, cidIndex, numbers):
740                 selector.clear()
741
742                 selectedNumber = self._session.draft.get_selected_number(cid)
743                 if len(numbers) == 1:
744                         # If no alt numbers available, check the address book
745                         numbers, defaultIndex = _get_contact_numbers(self._session, cid, selectedNumber, numbers[0][1])
746                 else:
747                         defaultIndex = _index_number(numbers, selectedNumber)
748
749                 for number, description in numbers:
750                         if description:
751                                 label = "%s - %s" % (number, description)
752                         else:
753                                 label = number
754                         selector.addItem(label)
755                 selector.setVisible(True)
756                 if 1 < len(numbers):
757                         selector.setEnabled(True)
758                         selector.setCurrentIndex(defaultIndex)
759                 else:
760                         selector.setEnabled(False)
761
762         def _scroll_to_bottom(self):
763                 self._scrollTimer.start()
764
765         @misc_utils.log_exception(_moduleLogger)
766         def _on_delayed_scroll_to_bottom(self):
767                 with qui_utils.notify_error(self._app.errorLog):
768                         self._scrollEntry.ensureWidgetVisible(self._smsEntry)
769
770         @misc_utils.log_exception(_moduleLogger)
771         def _on_sms_clicked(self, arg):
772                 with qui_utils.notify_error(self._app.errorLog):
773                         message = unicode(self._smsEntry.toPlainText())
774                         self._session.draft.message = message
775                         self._session.draft.send()
776
777         @misc_utils.log_exception(_moduleLogger)
778         def _on_call_clicked(self, arg):
779                 with qui_utils.notify_error(self._app.errorLog):
780                         message = unicode(self._smsEntry.toPlainText())
781                         self._session.draft.message = message
782                         self._session.draft.call()
783
784         @QtCore.pyqtSlot()
785         @misc_utils.log_exception(_moduleLogger)
786         def _on_cancel_clicked(self, message):
787                 with qui_utils.notify_error(self._app.errorLog):
788                         self._session.draft.cancel()
789
790         @misc_utils.log_exception(_moduleLogger)
791         def _on_single_change_number(self, index):
792                 with qui_utils.notify_error(self._app.errorLog):
793                         # Exception thrown when the first item is removed
794                         cid = self._cids[0]
795                         try:
796                                 numbers = self._session.draft.get_numbers(cid)
797                         except KeyError:
798                                 _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
799                                 return
800                         number = numbers[index][0]
801                         self._session.draft.set_selected_number(cid, number)
802
803         @QtCore.pyqtSlot()
804         @misc_utils.log_exception(_moduleLogger)
805         def _on_refresh_history(self):
806                 draftContactsCount = self._session.draft.get_num_contacts()
807                 if draftContactsCount != 1:
808                         # Changing contact count will automatically refresh it
809                         return
810                 (cid, ) = self._session.draft.get_contacts()
811                 self._update_history(cid)
812
813         @QtCore.pyqtSlot()
814         @misc_utils.log_exception(_moduleLogger)
815         def _on_recipients_changed(self):
816                 with qui_utils.notify_error(self._app.errorLog):
817                         self._update_target_fields()
818                         self._update_button_state()
819
820         @QtCore.pyqtSlot()
821         @misc_utils.log_exception(_moduleLogger)
822         def _on_op_started(self):
823                 with qui_utils.notify_error(self._app.errorLog):
824                         self._smsEntry.setReadOnly(True)
825                         self._smsButton.setVisible(False)
826                         self._dialButton.setVisible(False)
827                         self.show()
828
829         @QtCore.pyqtSlot()
830         @misc_utils.log_exception(_moduleLogger)
831         def _on_calling_started(self):
832                 with qui_utils.notify_error(self._app.errorLog):
833                         self._cancelButton.setVisible(True)
834
835         @QtCore.pyqtSlot()
836         @misc_utils.log_exception(_moduleLogger)
837         def _on_op_finished(self):
838                 with qui_utils.notify_error(self._app.errorLog):
839                         self._smsEntry.setPlainText("")
840                         self._smsEntry.setReadOnly(False)
841                         self._cancelButton.setVisible(False)
842                         self._smsButton.setVisible(True)
843                         self._dialButton.setVisible(True)
844                         self.close()
845                         self.destroy()
846
847         @QtCore.pyqtSlot()
848         @misc_utils.log_exception(_moduleLogger)
849         def _on_op_error(self, message):
850                 with qui_utils.notify_error(self._app.errorLog):
851                         self._smsEntry.setReadOnly(False)
852                         self._cancelButton.setVisible(False)
853                         self._smsButton.setVisible(True)
854                         self._dialButton.setVisible(True)
855
856                         self._errorLog.push_error(message)
857
858         @QtCore.pyqtSlot()
859         @misc_utils.log_exception(_moduleLogger)
860         def _on_letter_count_changed(self):
861                 with qui_utils.notify_error(self._app.errorLog):
862                         self._update_letter_count()
863                         self._update_button_state()
864
865         @QtCore.pyqtSlot()
866         @QtCore.pyqtSlot(bool)
867         @misc_utils.log_exception(_moduleLogger)
868         def _on_close_window(self, checked = True):
869                 with qui_utils.notify_error(self._app.errorLog):
870                         self.close()
871
872
873 def _index_number(numbers, default):
874         uglyDefault = misc_utils.make_ugly(default)
875         uglyContactNumbers = list(
876                 misc_utils.make_ugly(contactNumber)
877                 for (contactNumber, _) in numbers
878         )
879         defaultMatches = [
880                 misc_utils.similar_ugly_numbers(uglyDefault, contactNumber)
881                 for contactNumber in uglyContactNumbers
882         ]
883         try:
884                 defaultIndex = defaultMatches.index(True)
885         except ValueError:
886                 defaultIndex = -1
887                 _moduleLogger.warn(
888                         "Could not find contact number %s among %r" % (
889                                 default, numbers
890                         )
891                 )
892         return defaultIndex
893
894
895 def _get_contact_numbers(session, contactId, number, description):
896         contactPhoneNumbers = []
897         if contactId and contactId != "0":
898                 try:
899                         contactDetails = copy.deepcopy(session.get_contacts()[contactId])
900                         contactPhoneNumbers = contactDetails["numbers"]
901                 except KeyError:
902                         contactPhoneNumbers = []
903                 contactPhoneNumbers = [
904                         (contactPhoneNumber["phoneNumber"], contactPhoneNumber.get("phoneType", "Unknown"))
905                         for contactPhoneNumber in contactPhoneNumbers
906                 ]
907                 defaultIndex = _index_number(contactPhoneNumbers, number)
908
909         if not contactPhoneNumbers or defaultIndex == -1:
910                 contactPhoneNumbers += [(number, description)]
911                 defaultIndex = 0
912
913         return contactPhoneNumbers, defaultIndex