Enabling support for configuring voicemail check on missed call
[gc-dialer] / src / dialogs.py
1 #!/usr/bin/env python
2
3 from __future__ import with_statement
4 from __future__ import division
5
6 import functools
7 import copy
8 import logging
9
10 from PyQt4 import QtGui
11 from PyQt4 import QtCore
12
13 import constants
14 from util import qwrappers
15 from util import qui_utils
16 from util import misc as misc_utils
17
18
19 _moduleLogger = logging.getLogger(__name__)
20
21
22 class CredentialsDialog(object):
23
24         def __init__(self, app):
25                 self._app = app
26                 self._usernameField = QtGui.QLineEdit()
27                 self._passwordField = QtGui.QLineEdit()
28                 self._passwordField.setEchoMode(QtGui.QLineEdit.Password)
29
30                 self._credLayout = QtGui.QGridLayout()
31                 self._credLayout.addWidget(QtGui.QLabel("Username"), 0, 0)
32                 self._credLayout.addWidget(self._usernameField, 0, 1)
33                 self._credLayout.addWidget(QtGui.QLabel("Password"), 1, 0)
34                 self._credLayout.addWidget(self._passwordField, 1, 1)
35
36                 self._loginButton = QtGui.QPushButton("&Login")
37                 self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
38                 self._buttonLayout.addButton(self._loginButton, QtGui.QDialogButtonBox.AcceptRole)
39
40                 self._layout = QtGui.QVBoxLayout()
41                 self._layout.addLayout(self._credLayout)
42                 self._layout.addWidget(self._buttonLayout)
43
44                 self._dialog = QtGui.QDialog()
45                 self._dialog.setWindowTitle("Login")
46                 self._dialog.setLayout(self._layout)
47                 self._dialog.setAttribute(QtCore.Qt.WA_DeleteOnClose, False)
48                 self._buttonLayout.accepted.connect(self._dialog.accept)
49                 self._buttonLayout.rejected.connect(self._dialog.reject)
50
51                 self._closeWindowAction = QtGui.QAction(None)
52                 self._closeWindowAction.setText("Close")
53                 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
54                 self._closeWindowAction.triggered.connect(self._on_close_window)
55
56                 self._dialog.addAction(self._closeWindowAction)
57                 self._dialog.addAction(app.quitAction)
58                 self._dialog.addAction(app.fullscreenAction)
59
60         def run(self, defaultUsername, defaultPassword, parent=None):
61                 self._dialog.setParent(parent, QtCore.Qt.Dialog)
62                 try:
63                         self._usernameField.setText(defaultUsername)
64                         self._passwordField.setText(defaultPassword)
65
66                         response = self._dialog.exec_()
67                         if response == QtGui.QDialog.Accepted:
68                                 return str(self._usernameField.text()), str(self._passwordField.text())
69                         elif response == QtGui.QDialog.Rejected:
70                                 return None
71                         else:
72                                 _moduleLogger.error("Unknown response")
73                                 return None
74                 finally:
75                         self._dialog.setParent(None, QtCore.Qt.Dialog)
76
77         def close(self):
78                 try:
79                         self._dialog.reject()
80                 except RuntimeError:
81                         _moduleLogger.exception("Oh well")
82
83         @QtCore.pyqtSlot()
84         @QtCore.pyqtSlot(bool)
85         @misc_utils.log_exception(_moduleLogger)
86         def _on_close_window(self, checked = True):
87                 with qui_utils.notify_error(self._app.errorLog):
88                         self._dialog.reject()
89
90
91 class AboutDialog(object):
92
93         def __init__(self, app):
94                 self._app = app
95                 self._title = QtGui.QLabel(
96                         "<h1>%s</h1><h3>Version: %s</h3>" % (
97                                 constants.__pretty_app_name__, constants.__version__
98                         )
99                 )
100                 self._title.setTextFormat(QtCore.Qt.RichText)
101                 self._title.setAlignment(QtCore.Qt.AlignCenter)
102                 self._copyright = QtGui.QLabel("<h6>Developed by Ed Page<h6><h6>Icons: See website</h6>")
103                 self._copyright.setTextFormat(QtCore.Qt.RichText)
104                 self._copyright.setAlignment(QtCore.Qt.AlignCenter)
105                 self._link = QtGui.QLabel('<a href="http://gc-dialer.garage.maemo.org">DialCentral Website</a>')
106                 self._link.setTextFormat(QtCore.Qt.RichText)
107                 self._link.setAlignment(QtCore.Qt.AlignCenter)
108                 self._link.setOpenExternalLinks(True)
109
110                 self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
111
112                 self._layout = QtGui.QVBoxLayout()
113                 self._layout.addWidget(self._title)
114                 self._layout.addWidget(self._copyright)
115                 self._layout.addWidget(self._link)
116                 self._layout.addWidget(self._buttonLayout)
117
118                 self._dialog = QtGui.QDialog()
119                 self._dialog.setWindowTitle("About")
120                 self._dialog.setLayout(self._layout)
121                 self._buttonLayout.rejected.connect(self._dialog.reject)
122
123                 self._closeWindowAction = QtGui.QAction(None)
124                 self._closeWindowAction.setText("Close")
125                 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
126                 self._closeWindowAction.triggered.connect(self._on_close_window)
127
128                 self._dialog.addAction(self._closeWindowAction)
129                 self._dialog.addAction(app.quitAction)
130                 self._dialog.addAction(app.fullscreenAction)
131
132         def run(self, parent=None):
133                 self._dialog.setParent(parent, QtCore.Qt.Dialog)
134
135                 response = self._dialog.exec_()
136                 return response
137
138         def close(self):
139                 try:
140                         self._dialog.reject()
141                 except RuntimeError:
142                         _moduleLogger.exception("Oh well")
143
144         @QtCore.pyqtSlot()
145         @QtCore.pyqtSlot(bool)
146         @misc_utils.log_exception(_moduleLogger)
147         def _on_close_window(self, checked = True):
148                 with qui_utils.notify_error(self._app.errorLog):
149                         self._dialog.reject()
150
151
152 class AccountDialog(object):
153
154         # @bug Can't enter custom callback numbers
155
156         _RECURRENCE_CHOICES = [
157                 (1, "1 minute"),
158                 (2, "2 minutes"),
159                 (3, "3 minutes"),
160                 (5, "5 minutes"),
161                 (8, "8 minutes"),
162                 (10, "10 minutes"),
163                 (15, "15 minutes"),
164                 (30, "30 minutes"),
165                 (45, "45 minutes"),
166                 (60, "1 hour"),
167                 (3*60, "3 hours"),
168                 (6*60, "6 hours"),
169                 (12*60, "12 hours"),
170         ]
171
172         ALARM_NONE = "No Alert"
173         ALARM_BACKGROUND = "Background Alert"
174         ALARM_APPLICATION = "Application Alert"
175
176         VOICEMAIL_CHECK_NOT_SUPPORTED = "Not Supported"
177         VOICEMAIL_CHECK_DISABLED = "Disabled"
178         VOICEMAIL_CHECK_ENABLED = "Enabled"
179
180         def __init__(self, app):
181                 self._app = app
182                 self._doClear = False
183
184                 self._accountNumberLabel = QtGui.QLabel("NUMBER NOT SET")
185                 self._notificationSelecter = QtGui.QComboBox()
186                 self._notificationSelecter.currentIndexChanged.connect(self._on_notification_change)
187                 self._notificationTimeSelector = QtGui.QComboBox()
188                 #self._notificationTimeSelector.setEditable(True)
189                 self._notificationTimeSelector.setInsertPolicy(QtGui.QComboBox.InsertAtTop)
190                 for _, label in self._RECURRENCE_CHOICES:
191                         self._notificationTimeSelector.addItem(label)
192                 self._missedCallsNotificationButton = QtGui.QCheckBox("Missed Calls")
193                 self._voicemailNotificationButton = QtGui.QCheckBox("Voicemail")
194                 self._smsNotificationButton = QtGui.QCheckBox("SMS")
195                 self._voicemailOnMissedButton = QtGui.QCheckBox("Voicemail Update on Missed Calls")
196                 self._clearButton = QtGui.QPushButton("Clear Account")
197                 self._clearButton.clicked.connect(self._on_clear)
198                 self._callbackSelector = QtGui.QComboBox()
199                 #self._callbackSelector.setEditable(True)
200                 self._callbackSelector.setInsertPolicy(QtGui.QComboBox.InsertAtTop)
201
202                 self._update_notification_state()
203
204                 self._credLayout = QtGui.QGridLayout()
205                 self._credLayout.addWidget(QtGui.QLabel("Account"), 0, 0)
206                 self._credLayout.addWidget(self._accountNumberLabel, 0, 1)
207                 self._credLayout.addWidget(QtGui.QLabel("Callback"), 1, 0)
208                 self._credLayout.addWidget(self._callbackSelector, 1, 1)
209                 self._credLayout.addWidget(self._notificationSelecter, 2, 0)
210                 self._credLayout.addWidget(self._notificationTimeSelector, 2, 1)
211                 self._credLayout.addWidget(QtGui.QLabel(""), 3, 0)
212                 self._credLayout.addWidget(self._missedCallsNotificationButton, 3, 1)
213                 self._credLayout.addWidget(QtGui.QLabel(""), 4, 0)
214                 self._credLayout.addWidget(self._voicemailNotificationButton, 4, 1)
215                 self._credLayout.addWidget(QtGui.QLabel(""), 5, 0)
216                 self._credLayout.addWidget(self._smsNotificationButton, 5, 1)
217                 self._credLayout.addWidget(QtGui.QLabel(""), 6, 0)
218                 self._credLayout.addWidget(self._voicemailOnMissedButton, 6, 1)
219
220                 self._credLayout.addWidget(QtGui.QLabel(""), 7, 0)
221                 self._credLayout.addWidget(self._clearButton, 7, 1)
222
223                 self._loginButton = QtGui.QPushButton("&Apply")
224                 self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
225                 self._buttonLayout.addButton(self._loginButton, QtGui.QDialogButtonBox.AcceptRole)
226
227                 self._layout = QtGui.QVBoxLayout()
228                 self._layout.addLayout(self._credLayout)
229                 self._layout.addWidget(self._buttonLayout)
230
231                 self._dialog = QtGui.QDialog()
232                 self._dialog.setWindowTitle("Account")
233                 self._dialog.setLayout(self._layout)
234                 self._buttonLayout.accepted.connect(self._dialog.accept)
235                 self._buttonLayout.rejected.connect(self._dialog.reject)
236
237                 self._closeWindowAction = QtGui.QAction(None)
238                 self._closeWindowAction.setText("Close")
239                 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
240                 self._closeWindowAction.triggered.connect(self._on_close_window)
241
242                 self._dialog.addAction(self._closeWindowAction)
243                 self._dialog.addAction(app.quitAction)
244                 self._dialog.addAction(app.fullscreenAction)
245
246         @property
247         def doClear(self):
248                 return self._doClear
249
250         def setIfNotificationsSupported(self, isSupported):
251                 if isSupported:
252                         self._notificationSelecter.clear()
253                         self._notificationSelecter.addItems([self.ALARM_NONE, self.ALARM_APPLICATION, self.ALARM_BACKGROUND])
254                         self._notificationTimeSelector.setEnabled(False)
255                         self._missedCallsNotificationButton.setEnabled(False)
256                         self._voicemailNotificationButton.setEnabled(False)
257                         self._smsNotificationButton.setEnabled(False)
258                 else:
259                         self._notificationSelecter.clear()
260                         self._notificationSelecter.addItems([self.ALARM_NONE, self.ALARM_APPLICATION])
261                         self._notificationTimeSelector.setEnabled(False)
262                         self._missedCallsNotificationButton.setEnabled(False)
263                         self._voicemailNotificationButton.setEnabled(False)
264                         self._smsNotificationButton.setEnabled(False)
265
266         def set_account_number(self, num):
267                 self._accountNumberLabel.setText(num)
268
269         def _set_voicemail_on_missed(self, status):
270                 if status == self.VOICEMAIL_CHECK_NOT_SUPPORTED:
271                         self._voicemailOnMissedButton.setChecked(False)
272                         self._voicemailOnMissedButton.hide()
273                 elif status == self.VOICEMAIL_CHECK_DISABLED:
274                         self._voicemailOnMissedButton.setChecked(False)
275                         self._voicemailOnMissedButton.show()
276                 elif status == self.VOICEMAIL_CHECK_ENABLED:
277                         self._voicemailOnMissedButton.setChecked(True)
278                         self._voicemailOnMissedButton.show()
279                 else:
280                         raise RuntimeError("Unsupported option for updating voicemail on missed calls %r" % status)
281
282         def _get_voicemail_on_missed(self):
283                 if not self._voicemailOnMissedButton.isVisible():
284                         return self.VOICEMAIL_CHECK_NOT_SUPPORTED
285                 elif self._voicemailOnMissedButton.isChecked():
286                         return self.VOICEMAIL_CHECK_ENABLED
287                 else:
288                         return self.VOICEMAIL_CHECK_DISABLED
289
290         updateVMOnMissedCall = property(_get_voicemail_on_missed, _set_voicemail_on_missed)
291
292         def _set_notifications(self, enabled):
293                 for i in xrange(self._notificationSelecter.count()):
294                         if self._notificationSelecter.itemText(i) == enabled:
295                                 self._notificationSelecter.setCurrentIndex(i)
296                                 break
297                 else:
298                         self._notificationSelecter.setCurrentIndex(0)
299
300         notifications = property(
301                 lambda self: str(self._notificationSelecter.currentText()),
302                 _set_notifications,
303         )
304
305         notifyOnMissed = property(
306                 lambda self: self._missedCallsNotificationButton.isChecked(),
307                 lambda self, enabled: self._missedCallsNotificationButton.setChecked(enabled),
308         )
309
310         notifyOnVoicemail = property(
311                 lambda self: self._voicemailNotificationButton.isChecked(),
312                 lambda self, enabled: self._voicemailNotificationButton.setChecked(enabled),
313         )
314
315         notifyOnSms = property(
316                 lambda self: self._smsNotificationButton.isChecked(),
317                 lambda self, enabled: self._smsNotificationButton.setChecked(enabled),
318         )
319
320         def _get_notification_time(self):
321                 index = self._notificationTimeSelector.currentIndex()
322                 minutes = self._RECURRENCE_CHOICES[index][0]
323                 return minutes
324
325         def _set_notification_time(self, minutes):
326                 for i, (time, _) in enumerate(self._RECURRENCE_CHOICES):
327                         if time == minutes:
328                                 self._notificationTimeSelector.setCurrentIndex(i)
329                                 break
330                 else:
331                                 self._notificationTimeSelector.setCurrentIndex(0)
332
333         notificationTime = property(_get_notification_time, _set_notification_time)
334
335         @property
336         def selectedCallback(self):
337                 index = self._callbackSelector.currentIndex()
338                 data = str(self._callbackSelector.itemData(index).toPyObject())
339                 return data
340
341         def set_callbacks(self, choices, default):
342                 self._callbackSelector.clear()
343
344                 self._callbackSelector.addItem("Not Set", "")
345
346                 uglyDefault = misc_utils.make_ugly(default)
347                 for number, description in choices.iteritems():
348                         prettyNumber = misc_utils.make_pretty(number)
349                         uglyNumber = misc_utils.make_ugly(number)
350                         if not uglyNumber:
351                                 continue
352
353                         self._callbackSelector.addItem("%s - %s" % (prettyNumber, description), uglyNumber)
354                         if uglyNumber == uglyDefault:
355                                 self._callbackSelector.setCurrentIndex(self._callbackSelector.count() - 1)
356
357         def run(self, parent=None):
358                 self._doClear = False
359                 self._dialog.setParent(parent, QtCore.Qt.Dialog)
360
361                 response = self._dialog.exec_()
362                 return response
363
364         def close(self):
365                 try:
366                         self._dialog.reject()
367                 except RuntimeError:
368                         _moduleLogger.exception("Oh well")
369
370         def _update_notification_state(self):
371                 currentText = str(self._notificationSelecter.currentText())
372                 if currentText == self.ALARM_BACKGROUND:
373                         self._notificationTimeSelector.setEnabled(True)
374
375                         self._missedCallsNotificationButton.setEnabled(True)
376                         self._voicemailNotificationButton.setEnabled(True)
377                         self._smsNotificationButton.setEnabled(True)
378                 elif currentText == self.ALARM_APPLICATION:
379                         self._notificationTimeSelector.setEnabled(True)
380
381                         self._missedCallsNotificationButton.setEnabled(False)
382                         self._voicemailNotificationButton.setEnabled(True)
383                         self._smsNotificationButton.setEnabled(True)
384
385                         self._missedCallsNotificationButton.setChecked(False)
386                 else:
387                         self._notificationTimeSelector.setEnabled(False)
388
389                         self._missedCallsNotificationButton.setEnabled(False)
390                         self._voicemailNotificationButton.setEnabled(False)
391                         self._smsNotificationButton.setEnabled(False)
392
393                         self._missedCallsNotificationButton.setChecked(False)
394                         self._voicemailNotificationButton.setChecked(False)
395                         self._smsNotificationButton.setChecked(False)
396
397         @QtCore.pyqtSlot(int)
398         @misc_utils.log_exception(_moduleLogger)
399         def _on_notification_change(self, index):
400                 with qui_utils.notify_error(self._app.errorLog):
401                         self._update_notification_state()
402
403         @QtCore.pyqtSlot()
404         @QtCore.pyqtSlot(bool)
405         @misc_utils.log_exception(_moduleLogger)
406         def _on_clear(self, checked = False):
407                 with qui_utils.notify_error(self._app.errorLog):
408                         self._doClear = True
409                         self._dialog.accept()
410
411         @QtCore.pyqtSlot()
412         @QtCore.pyqtSlot(bool)
413         @misc_utils.log_exception(_moduleLogger)
414         def _on_close_window(self, checked = True):
415                 with qui_utils.notify_error(self._app.errorLog):
416                         self._dialog.reject()
417
418
419 class ContactList(object):
420
421         _SENTINEL_ICON = QtGui.QIcon()
422
423         def __init__(self, app, session):
424                 self._app = app
425                 self._session = session
426                 self._targetLayout = QtGui.QVBoxLayout()
427                 self._targetList = QtGui.QWidget()
428                 self._targetList.setLayout(self._targetLayout)
429                 self._uiItems = []
430                 self._closeIcon = qui_utils.get_theme_icon(("window-close", "general_close", "gtk-close"), self._SENTINEL_ICON)
431
432         @property
433         def toplevel(self):
434                 return self._targetList
435
436         def setVisible(self, isVisible):
437                 self._targetList.setVisible(isVisible)
438
439         def update(self):
440                 cids = list(self._session.draft.get_contacts())
441                 amountCommon = min(len(cids), len(self._uiItems))
442
443                 # Run through everything in common
444                 for i in xrange(0, amountCommon):
445                         cid = cids[i]
446                         uiItem = self._uiItems[i]
447                         title = self._session.draft.get_title(cid)
448                         description = self._session.draft.get_description(cid)
449                         numbers = self._session.draft.get_numbers(cid)
450                         uiItem["cid"] = cid
451                         uiItem["title"] = title
452                         uiItem["description"] = description
453                         uiItem["numbers"] = numbers
454                         uiItem["label"].setText(title)
455                         self._populate_number_selector(uiItem["selector"], cid, i, numbers)
456                         uiItem["rowWidget"].setVisible(True)
457
458                 # More contacts than ui items
459                 for i in xrange(amountCommon, len(cids)):
460                         cid = cids[i]
461                         title = self._session.draft.get_title(cid)
462                         description = self._session.draft.get_description(cid)
463                         numbers = self._session.draft.get_numbers(cid)
464
465                         titleLabel = QtGui.QLabel(title)
466                         titleLabel.setWordWrap(True)
467                         numberSelector = QtGui.QComboBox()
468                         self._populate_number_selector(numberSelector, cid, i, numbers)
469
470                         callback = functools.partial(
471                                 self._on_change_number,
472                                 i
473                         )
474                         callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
475                         numberSelector.activated.connect(
476                                 QtCore.pyqtSlot(int)(callback)
477                         )
478
479                         if self._closeIcon is self._SENTINEL_ICON:
480                                 deleteButton = QtGui.QPushButton("Delete")
481                         else:
482                                 deleteButton = QtGui.QPushButton(self._closeIcon, "")
483                         deleteButton.setSizePolicy(QtGui.QSizePolicy(
484                                 QtGui.QSizePolicy.Minimum,
485                                 QtGui.QSizePolicy.Minimum,
486                                 QtGui.QSizePolicy.PushButton,
487                         ))
488                         callback = functools.partial(
489                                 self._on_remove_contact,
490                                 i
491                         )
492                         callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
493                         deleteButton.clicked.connect(callback)
494
495                         rowLayout = QtGui.QHBoxLayout()
496                         rowLayout.addWidget(titleLabel, 1000)
497                         rowLayout.addWidget(numberSelector, 0)
498                         rowLayout.addWidget(deleteButton, 0)
499                         rowWidget = QtGui.QWidget()
500                         rowWidget.setLayout(rowLayout)
501                         self._targetLayout.addWidget(rowWidget)
502
503                         uiItem = {}
504                         uiItem["cid"] = cid
505                         uiItem["title"] = title
506                         uiItem["description"] = description
507                         uiItem["numbers"] = numbers
508                         uiItem["label"] = titleLabel
509                         uiItem["selector"] = numberSelector
510                         uiItem["rowWidget"] = rowWidget
511                         self._uiItems.append(uiItem)
512                         amountCommon = i+1
513
514                 # More UI items than contacts
515                 for i in xrange(amountCommon, len(self._uiItems)):
516                         uiItem = self._uiItems[i]
517                         uiItem["rowWidget"].setVisible(False)
518                         amountCommon = i+1
519
520         def _populate_number_selector(self, selector, cid, cidIndex, numbers):
521                 selector.clear()
522
523                 selectedNumber = self._session.draft.get_selected_number(cid)
524                 if len(numbers) == 1:
525                         # If no alt numbers available, check the address book
526                         numbers, defaultIndex = _get_contact_numbers(self._session, cid, selectedNumber, numbers[0][1])
527                 else:
528                         defaultIndex = _index_number(numbers, selectedNumber)
529
530                 for number, description in numbers:
531                         if description:
532                                 label = "%s - %s" % (number, description)
533                         else:
534                                 label = number
535                         selector.addItem(label)
536                 selector.setVisible(True)
537                 if 1 < len(numbers):
538                         selector.setEnabled(True)
539                         selector.setCurrentIndex(defaultIndex)
540                 else:
541                         selector.setEnabled(False)
542
543         @misc_utils.log_exception(_moduleLogger)
544         def _on_change_number(self, cidIndex, index):
545                 with qui_utils.notify_error(self._app.errorLog):
546                         # Exception thrown when the first item is removed
547                         try:
548                                 cid = self._uiItems[cidIndex]["cid"]
549                                 numbers = self._session.draft.get_numbers(cid)
550                         except IndexError:
551                                 _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
552                                 return
553                         except KeyError:
554                                 _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
555                                 return
556                         number = numbers[index][0]
557                         self._session.draft.set_selected_number(cid, number)
558
559         @misc_utils.log_exception(_moduleLogger)
560         def _on_remove_contact(self, index, toggled):
561                 with qui_utils.notify_error(self._app.errorLog):
562                         self._session.draft.remove_contact(self._uiItems[index]["cid"])
563
564
565 class VoicemailPlayer(object):
566
567         def __init__(self, app, session, errorLog):
568                 self._app = app
569                 self._session = session
570                 self._errorLog = errorLog
571                 self._token = None
572                 self._session.voicemailAvailable.connect(self._on_voicemail_downloaded)
573                 self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
574
575                 self._playButton = QtGui.QPushButton("Play")
576                 self._playButton.clicked.connect(self._on_voicemail_play)
577                 self._pauseButton = QtGui.QPushButton("Pause")
578                 self._pauseButton.clicked.connect(self._on_voicemail_pause)
579                 self._pauseButton.hide()
580                 self._resumeButton = QtGui.QPushButton("Resume")
581                 self._resumeButton.clicked.connect(self._on_voicemail_resume)
582                 self._resumeButton.hide()
583                 self._stopButton = QtGui.QPushButton("Stop")
584                 self._stopButton.clicked.connect(self._on_voicemail_stop)
585                 self._stopButton.hide()
586
587                 self._downloadButton = QtGui.QPushButton("Download Voicemail")
588                 self._downloadButton.clicked.connect(self._on_voicemail_download)
589                 self._downloadLayout = QtGui.QHBoxLayout()
590                 self._downloadLayout.addWidget(self._downloadButton)
591                 self._downloadWidget = QtGui.QWidget()
592                 self._downloadWidget.setLayout(self._downloadLayout)
593
594                 self._playLabel = QtGui.QLabel("Voicemail")
595                 self._saveButton = QtGui.QPushButton("Save")
596                 self._saveButton.clicked.connect(self._on_voicemail_save)
597                 self._playerLayout = QtGui.QHBoxLayout()
598                 self._playerLayout.addWidget(self._playLabel)
599                 self._playerLayout.addWidget(self._playButton)
600                 self._playerLayout.addWidget(self._pauseButton)
601                 self._playerLayout.addWidget(self._resumeButton)
602                 self._playerLayout.addWidget(self._stopButton)
603                 self._playerLayout.addWidget(self._saveButton)
604                 self._playerWidget = QtGui.QWidget()
605                 self._playerWidget.setLayout(self._playerLayout)
606
607                 self._visibleWidget = None
608                 self._layout = QtGui.QHBoxLayout()
609                 self._layout.setContentsMargins(0, 0, 0, 0)
610                 self._widget = QtGui.QWidget()
611                 self._widget.setLayout(self._layout)
612                 self._update_state()
613
614         @property
615         def toplevel(self):
616                 return self._widget
617
618         def destroy(self):
619                 self._session.voicemailAvailable.disconnect(self._on_voicemail_downloaded)
620                 self._session.draft.recipientsChanged.disconnect(self._on_recipients_changed)
621                 self._invalidate_token()
622
623         def _invalidate_token(self):
624                 if self._token is not None:
625                         self._token.invalidate()
626                         self._token.error.disconnect(self._on_play_error)
627                         self._token.stateChange.connect(self._on_play_state)
628                         self._token.invalidated.connect(self._on_play_invalidated)
629
630         def _show_download(self, messageId):
631                 if self._visibleWidget is self._downloadWidget:
632                         return
633                 self._hide()
634                 self._layout.addWidget(self._downloadWidget)
635                 self._visibleWidget = self._downloadWidget
636                 self._visibleWidget.show()
637
638         def _show_player(self, messageId):
639                 if self._visibleWidget is self._playerWidget:
640                         return
641                 self._hide()
642                 self._layout.addWidget(self._playerWidget)
643                 self._visibleWidget = self._playerWidget
644                 self._visibleWidget.show()
645
646         def _hide(self):
647                 if self._visibleWidget is None:
648                         return
649                 self._visibleWidget.hide()
650                 self._layout.removeWidget(self._visibleWidget)
651                 self._visibleWidget = None
652
653         def _update_play_state(self):
654                 if self._token is not None and self._token.isValid:
655                         self._playButton.setText("Stop")
656                 else:
657                         self._playButton.setText("Play")
658
659         def _update_state(self):
660                 if self._session.draft.get_num_contacts() != 1:
661                         self._hide()
662                         return
663
664                 (cid, ) = self._session.draft.get_contacts()
665                 messageId = self._session.draft.get_message_id(cid)
666                 if messageId is None:
667                         self._hide()
668                         return
669
670                 if self._session.is_available(messageId):
671                         self._show_player(messageId)
672                 else:
673                         self._show_download(messageId)
674                 if self._token is not None:
675                         self._token.invalidate()
676
677         @misc_utils.log_exception(_moduleLogger)
678         def _on_voicemail_save(self, arg):
679                 with qui_utils.notify_error(self._app.errorLog):
680                         targetPath = QtGui.QFileDialog.getSaveFileName(None, caption="Save Voicemail", filter="Audio File (*.mp3)")
681                         targetPath = unicode(targetPath)
682                         if not targetPath:
683                                 return
684
685                         (cid, ) = self._session.draft.get_contacts()
686                         messageId = self._session.draft.get_message_id(cid)
687                         sourcePath = self._session.voicemail_path(messageId)
688                         import shutil
689                         shutil.copy2(sourcePath, targetPath)
690
691         @misc_utils.log_exception(_moduleLogger)
692         def _on_play_error(self, error):
693                 with qui_utils.notify_error(self._app.errorLog):
694                         self._app.errorLog.push_error(error)
695
696         @misc_utils.log_exception(_moduleLogger)
697         def _on_play_invalidated(self):
698                 with qui_utils.notify_error(self._app.errorLog):
699                         self._playButton.show()
700                         self._pauseButton.hide()
701                         self._resumeButton.hide()
702                         self._stopButton.hide()
703                         self._invalidate_token()
704
705         @misc_utils.log_exception(_moduleLogger)
706         def _on_play_state(self, state):
707                 with qui_utils.notify_error(self._app.errorLog):
708                         if state == self._token.STATE_PLAY:
709                                 self._playButton.hide()
710                                 self._pauseButton.show()
711                                 self._resumeButton.hide()
712                                 self._stopButton.show()
713                         elif state == self._token.STATE_PAUSE:
714                                 self._playButton.hide()
715                                 self._pauseButton.hide()
716                                 self._resumeButton.show()
717                                 self._stopButton.show()
718                         elif state == self._token.STATE_STOP:
719                                 self._playButton.show()
720                                 self._pauseButton.hide()
721                                 self._resumeButton.hide()
722                                 self._stopButton.hide()
723
724         @misc_utils.log_exception(_moduleLogger)
725         def _on_voicemail_play(self, arg):
726                 with qui_utils.notify_error(self._app.errorLog):
727                         (cid, ) = self._session.draft.get_contacts()
728                         messageId = self._session.draft.get_message_id(cid)
729                         sourcePath = self._session.voicemail_path(messageId)
730
731                         self._invalidate_token()
732                         uri = "file://%s" % sourcePath
733                         self._token = self._app.streamHandler.set_file(uri)
734                         self._token.stateChange.connect(self._on_play_state)
735                         self._token.invalidated.connect(self._on_play_invalidated)
736                         self._token.error.connect(self._on_play_error)
737                         self._token.play()
738
739         @misc_utils.log_exception(_moduleLogger)
740         def _on_voicemail_pause(self, arg):
741                 with qui_utils.notify_error(self._app.errorLog):
742                         self._token.pause()
743
744         @misc_utils.log_exception(_moduleLogger)
745         def _on_voicemail_resume(self, arg):
746                 with qui_utils.notify_error(self._app.errorLog):
747                         self._token.play()
748
749         @misc_utils.log_exception(_moduleLogger)
750         def _on_voicemail_stop(self, arg):
751                 with qui_utils.notify_error(self._app.errorLog):
752                         self._token.stop()
753
754         @misc_utils.log_exception(_moduleLogger)
755         def _on_voicemail_download(self, arg):
756                 with qui_utils.notify_error(self._app.errorLog):
757                         (cid, ) = self._session.draft.get_contacts()
758                         messageId = self._session.draft.get_message_id(cid)
759                         self._session.download_voicemail(messageId)
760                         self._hide()
761
762         @QtCore.pyqtSlot()
763         @misc_utils.log_exception(_moduleLogger)
764         def _on_recipients_changed(self):
765                 with qui_utils.notify_error(self._app.errorLog):
766                         self._update_state()
767
768         @QtCore.pyqtSlot(str, str)
769         @misc_utils.log_exception(_moduleLogger)
770         def _on_voicemail_downloaded(self, messageId, filepath):
771                 with qui_utils.notify_error(self._app.errorLog):
772                         self._update_state()
773
774
775 class SMSEntryWindow(qwrappers.WindowWrapper):
776
777         MAX_CHAR = 160
778         # @bug Somehow a window is being destroyed on object creation which causes glitches on Maemo 5
779
780         def __init__(self, parent, app, session, errorLog):
781                 qwrappers.WindowWrapper.__init__(self, parent, app)
782                 self._session = session
783                 self._session.messagesUpdated.connect(self._on_refresh_history)
784                 self._session.historyUpdated.connect(self._on_refresh_history)
785                 self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
786
787                 self._session.draft.sendingMessage.connect(self._on_op_started)
788                 self._session.draft.calling.connect(self._on_op_started)
789                 self._session.draft.calling.connect(self._on_calling_started)
790                 self._session.draft.cancelling.connect(self._on_op_started)
791
792                 self._session.draft.sentMessage.connect(self._on_op_finished)
793                 self._session.draft.called.connect(self._on_op_finished)
794                 self._session.draft.cancelled.connect(self._on_op_finished)
795                 self._session.draft.error.connect(self._on_op_error)
796                 self._errorLog = errorLog
797
798                 self._errorDisplay = qui_utils.ErrorDisplay(self._errorLog)
799
800                 self._targetList = ContactList(self._app, self._session)
801                 self._history = QtGui.QLabel()
802                 self._history.setTextFormat(QtCore.Qt.RichText)
803                 self._history.setWordWrap(True)
804                 self._voicemailPlayer = VoicemailPlayer(self._app, self._session, self._errorLog)
805                 self._smsEntry = QtGui.QTextEdit()
806                 self._smsEntry.textChanged.connect(self._on_letter_count_changed)
807
808                 self._entryLayout = QtGui.QVBoxLayout()
809                 self._entryLayout.addWidget(self._targetList.toplevel)
810                 self._entryLayout.addWidget(self._history)
811                 self._entryLayout.addWidget(self._voicemailPlayer.toplevel, 0)
812                 self._entryLayout.addWidget(self._smsEntry)
813                 self._entryLayout.setContentsMargins(0, 0, 0, 0)
814                 self._entryWidget = QtGui.QWidget()
815                 self._entryWidget.setLayout(self._entryLayout)
816                 self._entryWidget.setContentsMargins(0, 0, 0, 0)
817                 self._scrollEntry = QtGui.QScrollArea()
818                 self._scrollEntry.setWidget(self._entryWidget)
819                 self._scrollEntry.setWidgetResizable(True)
820                 self._scrollEntry.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignBottom)
821                 self._scrollEntry.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
822                 self._scrollEntry.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
823
824                 self._characterCountLabel = QtGui.QLabel("")
825                 self._singleNumberSelector = QtGui.QComboBox()
826                 self._cids = []
827                 self._singleNumberSelector.activated.connect(self._on_single_change_number)
828                 self._smsButton = QtGui.QPushButton("SMS")
829                 self._smsButton.clicked.connect(self._on_sms_clicked)
830                 self._smsButton.setEnabled(False)
831                 self._dialButton = QtGui.QPushButton("Dial")
832                 self._dialButton.clicked.connect(self._on_call_clicked)
833                 self._cancelButton = QtGui.QPushButton("Cancel Call")
834                 self._cancelButton.clicked.connect(self._on_cancel_clicked)
835                 self._cancelButton.setVisible(False)
836
837                 self._buttonLayout = QtGui.QHBoxLayout()
838                 self._buttonLayout.addWidget(self._characterCountLabel)
839                 self._buttonLayout.addWidget(self._singleNumberSelector)
840                 self._buttonLayout.addWidget(self._smsButton)
841                 self._buttonLayout.addWidget(self._dialButton)
842                 self._buttonLayout.addWidget(self._cancelButton)
843
844                 self._layout.addWidget(self._errorDisplay.toplevel)
845                 self._layout.addWidget(self._scrollEntry)
846                 self._layout.addLayout(self._buttonLayout)
847                 self._layout.setDirection(QtGui.QBoxLayout.TopToBottom)
848
849                 self._window.setWindowTitle("Contact")
850                 self._window.closed.connect(self._on_close_window)
851                 self._window.hidden.connect(self._on_close_window)
852
853                 self._scrollTimer = QtCore.QTimer()
854                 self._scrollTimer.setInterval(100)
855                 self._scrollTimer.setSingleShot(True)
856                 self._scrollTimer.timeout.connect(self._on_delayed_scroll_to_bottom)
857
858                 self._smsEntry.setPlainText(self._session.draft.message)
859                 self._update_letter_count()
860                 self._update_target_fields()
861                 self.set_fullscreen(self._app.fullscreenAction.isChecked())
862                 self.set_orientation(self._app.orientationAction.isChecked())
863
864         def close(self):
865                 if self._window is None:
866                         # Already closed
867                         return
868                 window = self._window
869                 try:
870                         message = unicode(self._smsEntry.toPlainText())
871                         self._session.draft.message = message
872                         self.hide()
873                 except AttributeError:
874                         _moduleLogger.exception("Oh well")
875                 except RuntimeError:
876                         _moduleLogger.exception("Oh well")
877
878         def destroy(self):
879                 self._session.messagesUpdated.disconnect(self._on_refresh_history)
880                 self._session.historyUpdated.disconnect(self._on_refresh_history)
881                 self._session.draft.recipientsChanged.disconnect(self._on_recipients_changed)
882                 self._session.draft.sendingMessage.disconnect(self._on_op_started)
883                 self._session.draft.calling.disconnect(self._on_op_started)
884                 self._session.draft.calling.disconnect(self._on_calling_started)
885                 self._session.draft.cancelling.disconnect(self._on_op_started)
886                 self._session.draft.sentMessage.disconnect(self._on_op_finished)
887                 self._session.draft.called.disconnect(self._on_op_finished)
888                 self._session.draft.cancelled.disconnect(self._on_op_finished)
889                 self._session.draft.error.disconnect(self._on_op_error)
890                 self._voicemailPlayer.destroy()
891                 window = self._window
892                 self._window = None
893                 try:
894                         window.close()
895                         window.destroy()
896                 except AttributeError:
897                         _moduleLogger.exception("Oh well")
898                 except RuntimeError:
899                         _moduleLogger.exception("Oh well")
900
901         def set_orientation(self, isPortrait):
902                 qwrappers.WindowWrapper.set_orientation(self, isPortrait)
903                 self._scroll_to_bottom()
904
905         def _update_letter_count(self):
906                 count = self._smsEntry.toPlainText().size()
907                 numTexts, numCharInText = divmod(count, self.MAX_CHAR)
908                 numTexts += 1
909                 numCharsLeftInText = self.MAX_CHAR - numCharInText
910                 self._characterCountLabel.setText("%d (%d)" % (numCharsLeftInText, numTexts))
911
912         def _update_button_state(self):
913                 self._cancelButton.setEnabled(True)
914                 if self._session.draft.get_num_contacts() == 0:
915                         self._dialButton.setEnabled(False)
916                         self._smsButton.setEnabled(False)
917                 elif self._session.draft.get_num_contacts() == 1:
918                         count = self._smsEntry.toPlainText().size()
919                         if count == 0:
920                                 self._dialButton.setEnabled(True)
921                                 self._smsButton.setEnabled(False)
922                         else:
923                                 self._dialButton.setEnabled(False)
924                                 self._smsButton.setEnabled(True)
925                 else:
926                         self._dialButton.setEnabled(False)
927                         count = self._smsEntry.toPlainText().size()
928                         if count == 0:
929                                 self._smsButton.setEnabled(False)
930                         else:
931                                 self._smsButton.setEnabled(True)
932
933         def _update_history(self, cid):
934                 draftContactsCount = self._session.draft.get_num_contacts()
935                 if draftContactsCount != 1:
936                         self._history.setVisible(False)
937                 else:
938                         description = self._session.draft.get_description(cid)
939
940                         self._targetList.setVisible(False)
941                         if description:
942                                 self._history.setText(description)
943                                 self._history.setVisible(True)
944                         else:
945                                 self._history.setText("")
946                                 self._history.setVisible(False)
947
948         def _update_target_fields(self):
949                 draftContactsCount = self._session.draft.get_num_contacts()
950                 if draftContactsCount == 0:
951                         self.hide()
952                         del self._cids[:]
953                 elif draftContactsCount == 1:
954                         (cid, ) = self._session.draft.get_contacts()
955                         title = self._session.draft.get_title(cid)
956                         numbers = self._session.draft.get_numbers(cid)
957
958                         self._targetList.setVisible(False)
959                         self._update_history(cid)
960                         self._populate_number_selector(self._singleNumberSelector, cid, 0, numbers)
961                         self._cids = [cid]
962
963                         self._scroll_to_bottom()
964                         self._window.setWindowTitle(title)
965                         self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason)
966                         self.show()
967                         self._window.raise_()
968                 else:
969                         self._targetList.setVisible(True)
970                         self._targetList.update()
971                         self._history.setText("")
972                         self._history.setVisible(False)
973                         self._singleNumberSelector.setVisible(False)
974
975                         self._scroll_to_bottom()
976                         self._window.setWindowTitle("Contacts")
977                         self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason)
978                         self.show()
979                         self._window.raise_()
980
981         def _populate_number_selector(self, selector, cid, cidIndex, numbers):
982                 selector.clear()
983
984                 selectedNumber = self._session.draft.get_selected_number(cid)
985                 if len(numbers) == 1:
986                         # If no alt numbers available, check the address book
987                         numbers, defaultIndex = _get_contact_numbers(self._session, cid, selectedNumber, numbers[0][1])
988                 else:
989                         defaultIndex = _index_number(numbers, selectedNumber)
990
991                 for number, description in numbers:
992                         if description:
993                                 label = "%s - %s" % (number, description)
994                         else:
995                                 label = number
996                         selector.addItem(label)
997                 selector.setVisible(True)
998                 if 1 < len(numbers):
999                         selector.setEnabled(True)
1000                         selector.setCurrentIndex(defaultIndex)
1001                 else:
1002                         selector.setEnabled(False)
1003
1004         def _scroll_to_bottom(self):
1005                 self._scrollTimer.start()
1006
1007         @misc_utils.log_exception(_moduleLogger)
1008         def _on_delayed_scroll_to_bottom(self):
1009                 with qui_utils.notify_error(self._app.errorLog):
1010                         self._scrollEntry.ensureWidgetVisible(self._smsEntry)
1011
1012         @misc_utils.log_exception(_moduleLogger)
1013         def _on_sms_clicked(self, arg):
1014                 with qui_utils.notify_error(self._app.errorLog):
1015                         message = unicode(self._smsEntry.toPlainText())
1016                         self._session.draft.message = message
1017                         self._session.draft.send()
1018
1019         @misc_utils.log_exception(_moduleLogger)
1020         def _on_call_clicked(self, arg):
1021                 with qui_utils.notify_error(self._app.errorLog):
1022                         message = unicode(self._smsEntry.toPlainText())
1023                         self._session.draft.message = message
1024                         self._session.draft.call()
1025
1026         @QtCore.pyqtSlot()
1027         @misc_utils.log_exception(_moduleLogger)
1028         def _on_cancel_clicked(self, message):
1029                 with qui_utils.notify_error(self._app.errorLog):
1030                         self._session.draft.cancel()
1031
1032         @misc_utils.log_exception(_moduleLogger)
1033         def _on_single_change_number(self, index):
1034                 with qui_utils.notify_error(self._app.errorLog):
1035                         # Exception thrown when the first item is removed
1036                         cid = self._cids[0]
1037                         try:
1038                                 numbers = self._session.draft.get_numbers(cid)
1039                         except KeyError:
1040                                 _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
1041                                 return
1042                         number = numbers[index][0]
1043                         self._session.draft.set_selected_number(cid, number)
1044
1045         @QtCore.pyqtSlot()
1046         @misc_utils.log_exception(_moduleLogger)
1047         def _on_refresh_history(self):
1048                 with qui_utils.notify_error(self._app.errorLog):
1049                         draftContactsCount = self._session.draft.get_num_contacts()
1050                         if draftContactsCount != 1:
1051                                 # Changing contact count will automatically refresh it
1052                                 return
1053                         (cid, ) = self._session.draft.get_contacts()
1054                         self._update_history(cid)
1055
1056         @QtCore.pyqtSlot()
1057         @misc_utils.log_exception(_moduleLogger)
1058         def _on_recipients_changed(self):
1059                 with qui_utils.notify_error(self._app.errorLog):
1060                         self._update_target_fields()
1061                         self._update_button_state()
1062
1063         @QtCore.pyqtSlot()
1064         @misc_utils.log_exception(_moduleLogger)
1065         def _on_op_started(self):
1066                 with qui_utils.notify_error(self._app.errorLog):
1067                         self._smsEntry.setReadOnly(True)
1068                         self._smsButton.setVisible(False)
1069                         self._dialButton.setVisible(False)
1070                         self.show()
1071
1072         @QtCore.pyqtSlot()
1073         @misc_utils.log_exception(_moduleLogger)
1074         def _on_calling_started(self):
1075                 with qui_utils.notify_error(self._app.errorLog):
1076                         self._cancelButton.setVisible(True)
1077
1078         @QtCore.pyqtSlot()
1079         @misc_utils.log_exception(_moduleLogger)
1080         def _on_op_finished(self):
1081                 with qui_utils.notify_error(self._app.errorLog):
1082                         self._smsEntry.setPlainText("")
1083                         self._smsEntry.setReadOnly(False)
1084                         self._cancelButton.setVisible(False)
1085                         self._smsButton.setVisible(True)
1086                         self._dialButton.setVisible(True)
1087                         self.close()
1088                         self.destroy()
1089
1090         @QtCore.pyqtSlot()
1091         @misc_utils.log_exception(_moduleLogger)
1092         def _on_op_error(self, message):
1093                 with qui_utils.notify_error(self._app.errorLog):
1094                         self._smsEntry.setReadOnly(False)
1095                         self._cancelButton.setVisible(False)
1096                         self._smsButton.setVisible(True)
1097                         self._dialButton.setVisible(True)
1098
1099                         self._errorLog.push_error(message)
1100
1101         @QtCore.pyqtSlot()
1102         @misc_utils.log_exception(_moduleLogger)
1103         def _on_letter_count_changed(self):
1104                 with qui_utils.notify_error(self._app.errorLog):
1105                         self._update_letter_count()
1106                         self._update_button_state()
1107
1108         @QtCore.pyqtSlot()
1109         @QtCore.pyqtSlot(bool)
1110         @misc_utils.log_exception(_moduleLogger)
1111         def _on_close_window(self, checked = True):
1112                 with qui_utils.notify_error(self._app.errorLog):
1113                         self.close()
1114
1115
1116 def _index_number(numbers, default):
1117         uglyDefault = misc_utils.make_ugly(default)
1118         uglyContactNumbers = list(
1119                 misc_utils.make_ugly(contactNumber)
1120                 for (contactNumber, _) in numbers
1121         )
1122         defaultMatches = [
1123                 misc_utils.similar_ugly_numbers(uglyDefault, contactNumber)
1124                 for contactNumber in uglyContactNumbers
1125         ]
1126         try:
1127                 defaultIndex = defaultMatches.index(True)
1128         except ValueError:
1129                 defaultIndex = -1
1130                 _moduleLogger.warn(
1131                         "Could not find contact number %s among %r" % (
1132                                 default, numbers
1133                         )
1134                 )
1135         return defaultIndex
1136
1137
1138 def _get_contact_numbers(session, contactId, number, description):
1139         contactPhoneNumbers = []
1140         if contactId and contactId != "0":
1141                 try:
1142                         contactDetails = copy.deepcopy(session.get_contacts()[contactId])
1143                         contactPhoneNumbers = contactDetails["numbers"]
1144                 except KeyError:
1145                         contactPhoneNumbers = []
1146                 contactPhoneNumbers = [
1147                         (contactPhoneNumber["phoneNumber"], contactPhoneNumber.get("phoneType", "Unknown"))
1148                         for contactPhoneNumber in contactPhoneNumbers
1149                 ]
1150                 defaultIndex = _index_number(contactPhoneNumbers, number)
1151
1152         if not contactPhoneNumbers or defaultIndex == -1:
1153                 contactPhoneNumbers += [(number, description)]
1154                 defaultIndex = 0
1155
1156         return contactPhoneNumbers, defaultIndex