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