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