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