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