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