78941036ab14bc4d8c5fd09cc178b0897f766365
[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._session.voicemailAvailable.connect(self._on_voicemail_downloaded)
543                 self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
544
545                 self._downloadButton = QtGui.QPushButton("Download Voicemail")
546                 self._downloadButton.clicked.connect(self._on_voicemail_download)
547                 self._downloadLayout = QtGui.QHBoxLayout()
548                 self._downloadLayout.addWidget(self._downloadButton)
549                 self._downloadWidget = QtGui.QWidget()
550                 self._downloadWidget.setLayout(self._downloadLayout)
551
552                 self._playLabel = QtGui.QLabel("Voicemail")
553                 self._saveButton = QtGui.QPushButton("Save")
554                 self._saveButton.clicked.connect(self._on_voicemail_save)
555                 self._playerLayout = QtGui.QHBoxLayout()
556                 self._playerLayout.addWidget(self._playLabel)
557                 self._playerLayout.addWidget(self._saveButton)
558                 self._playerWidget = QtGui.QWidget()
559                 self._playerWidget.setLayout(self._playerLayout)
560
561                 self._visibleWidget = None
562                 self._layout = QtGui.QHBoxLayout()
563                 self._layout.setContentsMargins(0, 0, 0, 0)
564                 self._widget = QtGui.QWidget()
565                 self._widget.setLayout(self._layout)
566                 self._update_state()
567
568         @property
569         def toplevel(self):
570                 return self._widget
571
572         def destroy(self):
573                 self._session.voicemailAvailable.disconnect(self._on_voicemail_downloaded)
574                 self._session.draft.recipientsChanged.disconnect(self._on_recipients_changed)
575
576         def _show_download(self, messageId):
577                 if self._visibleWidget is self._downloadWidget:
578                         return
579                 self._hide()
580                 self._layout.addWidget(self._downloadWidget)
581                 self._visibleWidget = self._downloadWidget
582                 self._visibleWidget.show()
583
584         def _show_player(self, messageId):
585                 if self._visibleWidget is self._playerWidget:
586                         return
587                 self._hide()
588                 self._layout.addWidget(self._playerWidget)
589                 self._visibleWidget = self._playerWidget
590                 self._visibleWidget.show()
591
592         def _hide(self):
593                 if self._visibleWidget is None:
594                         return
595                 self._visibleWidget.hide()
596                 self._layout.removeWidget(self._visibleWidget)
597                 self._visibleWidget = None
598
599         def _update_state(self):
600                 if self._session.draft.get_num_contacts() != 1:
601                         self._hide()
602                         return
603
604                 (cid, ) = self._session.draft.get_contacts()
605                 messageId = self._session.draft.get_message_id(cid)
606                 if messageId is None:
607                         self._hide()
608                         return
609
610                 if self._session.is_available(messageId):
611                         self._show_player(messageId)
612                 else:
613                         self._show_download(messageId)
614
615         @misc_utils.log_exception(_moduleLogger)
616         def _on_voicemail_save(self, arg):
617                 with qui_utils.notify_error(self._app.errorLog):
618                         targetPath = QtGui.QFileDialog.getSaveFileName(None, caption="Save Voicemail", filter="Audio File (*.mp3)")
619                         targetPath = unicode(targetPath)
620                         if not targetPath:
621                                 return
622
623                         (cid, ) = self._session.draft.get_contacts()
624                         messageId = self._session.draft.get_message_id(cid)
625                         sourcePath = self._session.voicemail_path(messageId)
626                         import shutil
627                         shutil.copy2(sourcePath, targetPath)
628
629         @misc_utils.log_exception(_moduleLogger)
630         def _on_voicemail_download(self, arg):
631                 with qui_utils.notify_error(self._app.errorLog):
632                         (cid, ) = self._session.draft.get_contacts()
633                         messageId = self._session.draft.get_message_id(cid)
634                         self._session.download_voicemail(messageId)
635                         self._hide()
636
637         @QtCore.pyqtSlot()
638         @misc_utils.log_exception(_moduleLogger)
639         def _on_recipients_changed(self):
640                 with qui_utils.notify_error(self._app.errorLog):
641                         self._update_state()
642
643         @QtCore.pyqtSlot(str, str)
644         @misc_utils.log_exception(_moduleLogger)
645         def _on_voicemail_downloaded(self, messageId, filepath):
646                 with qui_utils.notify_error(self._app.errorLog):
647                         self._update_state()
648
649
650 class SMSEntryWindow(qwrappers.WindowWrapper):
651
652         MAX_CHAR = 160
653         # @bug Somehow a window is being destroyed on object creation which causes glitches on Maemo 5
654
655         def __init__(self, parent, app, session, errorLog):
656                 qwrappers.WindowWrapper.__init__(self, parent, app)
657                 self._session = session
658                 self._session.messagesUpdated.connect(self._on_refresh_history)
659                 self._session.historyUpdated.connect(self._on_refresh_history)
660                 self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
661
662                 self._session.draft.sendingMessage.connect(self._on_op_started)
663                 self._session.draft.calling.connect(self._on_op_started)
664                 self._session.draft.calling.connect(self._on_calling_started)
665                 self._session.draft.cancelling.connect(self._on_op_started)
666
667                 self._session.draft.sentMessage.connect(self._on_op_finished)
668                 self._session.draft.called.connect(self._on_op_finished)
669                 self._session.draft.cancelled.connect(self._on_op_finished)
670                 self._session.draft.error.connect(self._on_op_error)
671                 self._errorLog = errorLog
672
673                 self._errorDisplay = qui_utils.ErrorDisplay(self._errorLog)
674
675                 self._targetList = ContactList(self._app, self._session)
676                 self._history = QtGui.QLabel()
677                 self._history.setTextFormat(QtCore.Qt.RichText)
678                 self._history.setWordWrap(True)
679                 self._voicemailPlayer = VoicemailPlayer(self._app, self._session, self._errorLog)
680                 self._smsEntry = QtGui.QTextEdit()
681                 self._smsEntry.textChanged.connect(self._on_letter_count_changed)
682
683                 self._entryLayout = QtGui.QVBoxLayout()
684                 self._entryLayout.addWidget(self._targetList.toplevel)
685                 self._entryLayout.addWidget(self._history)
686                 self._entryLayout.addWidget(self._voicemailPlayer.toplevel, 0)
687                 self._entryLayout.addWidget(self._smsEntry)
688                 self._entryLayout.setContentsMargins(0, 0, 0, 0)
689                 self._entryWidget = QtGui.QWidget()
690                 self._entryWidget.setLayout(self._entryLayout)
691                 self._entryWidget.setContentsMargins(0, 0, 0, 0)
692                 self._scrollEntry = QtGui.QScrollArea()
693                 self._scrollEntry.setWidget(self._entryWidget)
694                 self._scrollEntry.setWidgetResizable(True)
695                 self._scrollEntry.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignBottom)
696                 self._scrollEntry.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
697                 self._scrollEntry.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
698
699                 self._characterCountLabel = QtGui.QLabel("")
700                 self._singleNumberSelector = QtGui.QComboBox()
701                 self._cids = []
702                 self._singleNumberSelector.activated.connect(self._on_single_change_number)
703                 self._smsButton = QtGui.QPushButton("SMS")
704                 self._smsButton.clicked.connect(self._on_sms_clicked)
705                 self._smsButton.setEnabled(False)
706                 self._dialButton = QtGui.QPushButton("Dial")
707                 self._dialButton.clicked.connect(self._on_call_clicked)
708                 self._cancelButton = QtGui.QPushButton("Cancel Call")
709                 self._cancelButton.clicked.connect(self._on_cancel_clicked)
710                 self._cancelButton.setVisible(False)
711
712                 self._buttonLayout = QtGui.QHBoxLayout()
713                 self._buttonLayout.addWidget(self._characterCountLabel)
714                 self._buttonLayout.addWidget(self._singleNumberSelector)
715                 self._buttonLayout.addWidget(self._smsButton)
716                 self._buttonLayout.addWidget(self._dialButton)
717                 self._buttonLayout.addWidget(self._cancelButton)
718
719                 self._layout.addWidget(self._errorDisplay.toplevel)
720                 self._layout.addWidget(self._scrollEntry)
721                 self._layout.addLayout(self._buttonLayout)
722                 self._layout.setDirection(QtGui.QBoxLayout.TopToBottom)
723
724                 self._window.setWindowTitle("Contact")
725                 self._window.closed.connect(self._on_close_window)
726                 self._window.hidden.connect(self._on_close_window)
727
728                 self._scrollTimer = QtCore.QTimer()
729                 self._scrollTimer.setInterval(100)
730                 self._scrollTimer.setSingleShot(True)
731                 self._scrollTimer.timeout.connect(self._on_delayed_scroll_to_bottom)
732
733                 self._smsEntry.setPlainText(self._session.draft.message)
734                 self._update_letter_count()
735                 self._update_target_fields()
736                 self.set_fullscreen(self._app.fullscreenAction.isChecked())
737                 self.set_orientation(self._app.orientationAction.isChecked())
738
739         def close(self):
740                 if self._window is None:
741                         # Already closed
742                         return
743                 window = self._window
744                 try:
745                         message = unicode(self._smsEntry.toPlainText())
746                         self._session.draft.message = message
747                         self.hide()
748                 except AttributeError:
749                         _moduleLogger.exception("Oh well")
750                 except RuntimeError:
751                         _moduleLogger.exception("Oh well")
752
753         def destroy(self):
754                 self._session.messagesUpdated.disconnect(self._on_refresh_history)
755                 self._session.historyUpdated.disconnect(self._on_refresh_history)
756                 self._session.draft.recipientsChanged.disconnect(self._on_recipients_changed)
757                 self._session.draft.sendingMessage.disconnect(self._on_op_started)
758                 self._session.draft.calling.disconnect(self._on_op_started)
759                 self._session.draft.calling.disconnect(self._on_calling_started)
760                 self._session.draft.cancelling.disconnect(self._on_op_started)
761                 self._session.draft.sentMessage.disconnect(self._on_op_finished)
762                 self._session.draft.called.disconnect(self._on_op_finished)
763                 self._session.draft.cancelled.disconnect(self._on_op_finished)
764                 self._session.draft.error.disconnect(self._on_op_error)
765                 self._voicemailPlayer.destroy()
766                 window = self._window
767                 self._window = None
768                 try:
769                         window.close()
770                         window.destroy()
771                 except AttributeError:
772                         _moduleLogger.exception("Oh well")
773                 except RuntimeError:
774                         _moduleLogger.exception("Oh well")
775
776         def set_orientation(self, isPortrait):
777                 qwrappers.WindowWrapper.set_orientation(self, isPortrait)
778                 self._scroll_to_bottom()
779
780         def _update_letter_count(self):
781                 count = self._smsEntry.toPlainText().size()
782                 numTexts, numCharInText = divmod(count, self.MAX_CHAR)
783                 numTexts += 1
784                 numCharsLeftInText = self.MAX_CHAR - numCharInText
785                 self._characterCountLabel.setText("%d (%d)" % (numCharsLeftInText, numTexts))
786
787         def _update_button_state(self):
788                 self._cancelButton.setEnabled(True)
789                 if self._session.draft.get_num_contacts() == 0:
790                         self._dialButton.setEnabled(False)
791                         self._smsButton.setEnabled(False)
792                 elif self._session.draft.get_num_contacts() == 1:
793                         count = self._smsEntry.toPlainText().size()
794                         if count == 0:
795                                 self._dialButton.setEnabled(True)
796                                 self._smsButton.setEnabled(False)
797                         else:
798                                 self._dialButton.setEnabled(False)
799                                 self._smsButton.setEnabled(True)
800                 else:
801                         self._dialButton.setEnabled(False)
802                         count = self._smsEntry.toPlainText().size()
803                         if count == 0:
804                                 self._smsButton.setEnabled(False)
805                         else:
806                                 self._smsButton.setEnabled(True)
807
808         def _update_history(self, cid):
809                 draftContactsCount = self._session.draft.get_num_contacts()
810                 if draftContactsCount != 1:
811                         self._history.setVisible(False)
812                 else:
813                         description = self._session.draft.get_description(cid)
814
815                         self._targetList.setVisible(False)
816                         if description:
817                                 self._history.setText(description)
818                                 self._history.setVisible(True)
819                         else:
820                                 self._history.setText("")
821                                 self._history.setVisible(False)
822
823         def _update_target_fields(self):
824                 draftContactsCount = self._session.draft.get_num_contacts()
825                 if draftContactsCount == 0:
826                         self.hide()
827                         del self._cids[:]
828                 elif draftContactsCount == 1:
829                         (cid, ) = self._session.draft.get_contacts()
830                         title = self._session.draft.get_title(cid)
831                         numbers = self._session.draft.get_numbers(cid)
832
833                         self._targetList.setVisible(False)
834                         self._update_history(cid)
835                         self._populate_number_selector(self._singleNumberSelector, cid, 0, numbers)
836                         self._cids = [cid]
837
838                         self._scroll_to_bottom()
839                         self._window.setWindowTitle(title)
840                         self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason)
841                         self.show()
842                         self._window.raise_()
843                 else:
844                         self._targetList.setVisible(True)
845                         self._targetList.update()
846                         self._history.setText("")
847                         self._history.setVisible(False)
848                         self._singleNumberSelector.setVisible(False)
849
850                         self._scroll_to_bottom()
851                         self._window.setWindowTitle("Contacts")
852                         self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason)
853                         self.show()
854                         self._window.raise_()
855
856         def _populate_number_selector(self, selector, cid, cidIndex, numbers):
857                 selector.clear()
858
859                 selectedNumber = self._session.draft.get_selected_number(cid)
860                 if len(numbers) == 1:
861                         # If no alt numbers available, check the address book
862                         numbers, defaultIndex = _get_contact_numbers(self._session, cid, selectedNumber, numbers[0][1])
863                 else:
864                         defaultIndex = _index_number(numbers, selectedNumber)
865
866                 for number, description in numbers:
867                         if description:
868                                 label = "%s - %s" % (number, description)
869                         else:
870                                 label = number
871                         selector.addItem(label)
872                 selector.setVisible(True)
873                 if 1 < len(numbers):
874                         selector.setEnabled(True)
875                         selector.setCurrentIndex(defaultIndex)
876                 else:
877                         selector.setEnabled(False)
878
879         def _scroll_to_bottom(self):
880                 self._scrollTimer.start()
881
882         @misc_utils.log_exception(_moduleLogger)
883         def _on_delayed_scroll_to_bottom(self):
884                 with qui_utils.notify_error(self._app.errorLog):
885                         self._scrollEntry.ensureWidgetVisible(self._smsEntry)
886
887         @misc_utils.log_exception(_moduleLogger)
888         def _on_sms_clicked(self, arg):
889                 with qui_utils.notify_error(self._app.errorLog):
890                         message = unicode(self._smsEntry.toPlainText())
891                         self._session.draft.message = message
892                         self._session.draft.send()
893
894         @misc_utils.log_exception(_moduleLogger)
895         def _on_call_clicked(self, arg):
896                 with qui_utils.notify_error(self._app.errorLog):
897                         message = unicode(self._smsEntry.toPlainText())
898                         self._session.draft.message = message
899                         self._session.draft.call()
900
901         @QtCore.pyqtSlot()
902         @misc_utils.log_exception(_moduleLogger)
903         def _on_cancel_clicked(self, message):
904                 with qui_utils.notify_error(self._app.errorLog):
905                         self._session.draft.cancel()
906
907         @misc_utils.log_exception(_moduleLogger)
908         def _on_single_change_number(self, index):
909                 with qui_utils.notify_error(self._app.errorLog):
910                         # Exception thrown when the first item is removed
911                         cid = self._cids[0]
912                         try:
913                                 numbers = self._session.draft.get_numbers(cid)
914                         except KeyError:
915                                 _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
916                                 return
917                         number = numbers[index][0]
918                         self._session.draft.set_selected_number(cid, number)
919
920         @QtCore.pyqtSlot()
921         @misc_utils.log_exception(_moduleLogger)
922         def _on_refresh_history(self):
923                 with qui_utils.notify_error(self._app.errorLog):
924                         draftContactsCount = self._session.draft.get_num_contacts()
925                         if draftContactsCount != 1:
926                                 # Changing contact count will automatically refresh it
927                                 return
928                         (cid, ) = self._session.draft.get_contacts()
929                         self._update_history(cid)
930
931         @QtCore.pyqtSlot()
932         @misc_utils.log_exception(_moduleLogger)
933         def _on_recipients_changed(self):
934                 with qui_utils.notify_error(self._app.errorLog):
935                         self._update_target_fields()
936                         self._update_button_state()
937
938         @QtCore.pyqtSlot()
939         @misc_utils.log_exception(_moduleLogger)
940         def _on_op_started(self):
941                 with qui_utils.notify_error(self._app.errorLog):
942                         self._smsEntry.setReadOnly(True)
943                         self._smsButton.setVisible(False)
944                         self._dialButton.setVisible(False)
945                         self.show()
946
947         @QtCore.pyqtSlot()
948         @misc_utils.log_exception(_moduleLogger)
949         def _on_calling_started(self):
950                 with qui_utils.notify_error(self._app.errorLog):
951                         self._cancelButton.setVisible(True)
952
953         @QtCore.pyqtSlot()
954         @misc_utils.log_exception(_moduleLogger)
955         def _on_op_finished(self):
956                 with qui_utils.notify_error(self._app.errorLog):
957                         self._smsEntry.setPlainText("")
958                         self._smsEntry.setReadOnly(False)
959                         self._cancelButton.setVisible(False)
960                         self._smsButton.setVisible(True)
961                         self._dialButton.setVisible(True)
962                         self.close()
963                         self.destroy()
964
965         @QtCore.pyqtSlot()
966         @misc_utils.log_exception(_moduleLogger)
967         def _on_op_error(self, message):
968                 with qui_utils.notify_error(self._app.errorLog):
969                         self._smsEntry.setReadOnly(False)
970                         self._cancelButton.setVisible(False)
971                         self._smsButton.setVisible(True)
972                         self._dialButton.setVisible(True)
973
974                         self._errorLog.push_error(message)
975
976         @QtCore.pyqtSlot()
977         @misc_utils.log_exception(_moduleLogger)
978         def _on_letter_count_changed(self):
979                 with qui_utils.notify_error(self._app.errorLog):
980                         self._update_letter_count()
981                         self._update_button_state()
982
983         @QtCore.pyqtSlot()
984         @QtCore.pyqtSlot(bool)
985         @misc_utils.log_exception(_moduleLogger)
986         def _on_close_window(self, checked = True):
987                 with qui_utils.notify_error(self._app.errorLog):
988                         self.close()
989
990
991 def _index_number(numbers, default):
992         uglyDefault = misc_utils.make_ugly(default)
993         uglyContactNumbers = list(
994                 misc_utils.make_ugly(contactNumber)
995                 for (contactNumber, _) in numbers
996         )
997         defaultMatches = [
998                 misc_utils.similar_ugly_numbers(uglyDefault, contactNumber)
999                 for contactNumber in uglyContactNumbers
1000         ]
1001         try:
1002                 defaultIndex = defaultMatches.index(True)
1003         except ValueError:
1004                 defaultIndex = -1
1005                 _moduleLogger.warn(
1006                         "Could not find contact number %s among %r" % (
1007                                 default, numbers
1008                         )
1009                 )
1010         return defaultIndex
1011
1012
1013 def _get_contact_numbers(session, contactId, number, description):
1014         contactPhoneNumbers = []
1015         if contactId and contactId != "0":
1016                 try:
1017                         contactDetails = copy.deepcopy(session.get_contacts()[contactId])
1018                         contactPhoneNumbers = contactDetails["numbers"]
1019                 except KeyError:
1020                         contactPhoneNumbers = []
1021                 contactPhoneNumbers = [
1022                         (contactPhoneNumber["phoneNumber"], contactPhoneNumber.get("phoneType", "Unknown"))
1023                         for contactPhoneNumber in contactPhoneNumbers
1024                 ]
1025                 defaultIndex = _index_number(contactPhoneNumbers, number)
1026
1027         if not contactPhoneNumbers or defaultIndex == -1:
1028                 contactPhoneNumbers += [(number, description)]
1029                 defaultIndex = 0
1030
1031         return contactPhoneNumbers, defaultIndex