Hopefully fixing scrolling on first load
[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                         if self._closeIcon is self._SENTINEL_ICON:
410                                 deleteButton = QtGui.QPushButton("Delete")
411                         else:
412                                 deleteButton = QtGui.QPushButton(self._closeIcon, "")
413                         deleteButton.setSizePolicy(QtGui.QSizePolicy(
414                                 QtGui.QSizePolicy.Minimum,
415                                 QtGui.QSizePolicy.Minimum,
416                                 QtGui.QSizePolicy.PushButton,
417                         ))
418                         callback = functools.partial(
419                                 self._on_remove_contact,
420                                 i
421                         )
422                         callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
423                         deleteButton.clicked.connect(callback)
424
425                         rowLayout = QtGui.QHBoxLayout()
426                         rowLayout.addWidget(titleLabel, 1000)
427                         rowLayout.addWidget(numberSelector, 0)
428                         rowLayout.addWidget(deleteButton, 0)
429                         rowWidget = QtGui.QWidget()
430                         rowWidget.setLayout(rowLayout)
431                         self._targetLayout.addWidget(rowWidget)
432
433                         uiItem = {}
434                         uiItem["cid"] = cid
435                         uiItem["title"] = title
436                         uiItem["description"] = description
437                         uiItem["numbers"] = numbers
438                         uiItem["label"] = titleLabel
439                         uiItem["selector"] = numberSelector
440                         uiItem["rowWidget"] = rowWidget
441                         self._uiItems.append(uiItem)
442                         amountCommon = i+1
443
444                 # More UI items than contacts
445                 for i in xrange(amountCommon, len(self._uiItems)):
446                         uiItem = self._uiItems[i]
447                         uiItem["rowWidget"].setVisible(False)
448                         amountCommon = i+1
449
450         def _populate_number_selector(self, selector, cid, cidIndex, numbers):
451                 selector.clear()
452
453                 selectedNumber = self._session.draft.get_selected_number(cid)
454                 if len(numbers) == 1:
455                         # If no alt numbers available, check the address book
456                         numbers, defaultIndex = _get_contact_numbers(self._session, cid, selectedNumber)
457                 else:
458                         defaultIndex = _index_number(numbers, selectedNumber)
459
460                 for number, description in numbers:
461                         if description:
462                                 label = "%s - %s" % (number, description)
463                         else:
464                                 label = number
465                         selector.addItem(label)
466                 selector.setVisible(True)
467                 if 1 < len(numbers):
468                         selector.setEnabled(True)
469                         selector.setCurrentIndex(defaultIndex)
470                 else:
471                         selector.setEnabled(False)
472
473                 callback = functools.partial(
474                         self._on_change_number,
475                         cidIndex
476                 )
477                 callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
478                 selector.activated.connect(
479                         QtCore.pyqtSlot(int)(callback)
480                 )
481
482         @misc_utils.log_exception(_moduleLogger)
483         def _on_change_number(self, cidIndex, index):
484                 with qui_utils.notify_error(self._app.errorLog):
485                         # Exception thrown when the first item is removed
486                         try:
487                                 cid = self._uiItems[cidIndex]["cid"]
488                                 numbers = self._session.draft.get_numbers(cid)
489                         except IndexError:
490                                 _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
491                                 return
492                         except KeyError:
493                                 _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
494                                 return
495                         number = numbers[index][0]
496                         self._session.draft.set_selected_number(cid, number)
497
498         @misc_utils.log_exception(_moduleLogger)
499         def _on_remove_contact(self, index, toggled):
500                 with qui_utils.notify_error(self._app.errorLog):
501                         self._session.draft.remove_contact(self._uiItems[index]["cid"])
502
503
504 class SMSEntryWindow(object):
505
506         MAX_CHAR = 160
507
508         def __init__(self, parent, app, session, errorLog):
509                 self._app = app
510                 self._session = session
511                 self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
512
513                 self._session.draft.sendingMessage.connect(self._on_op_started)
514                 self._session.draft.calling.connect(self._on_op_started)
515                 self._session.draft.calling.connect(self._on_calling_started)
516                 self._session.draft.cancelling.connect(self._on_op_started)
517
518                 self._session.draft.sentMessage.connect(self._on_op_finished)
519                 self._session.draft.called.connect(self._on_op_finished)
520                 self._session.draft.cancelled.connect(self._on_op_finished)
521                 self._session.draft.error.connect(self._on_op_error)
522                 self._errorLog = errorLog
523
524                 self._errorDisplay = qui_utils.ErrorDisplay(self._errorLog)
525
526                 self._targetList = ContactList(self._app, self._session)
527                 self._history = QtGui.QLabel()
528                 self._history.setTextFormat(QtCore.Qt.RichText)
529                 self._history.setWordWrap(True)
530                 self._smsEntry = QtGui.QTextEdit()
531                 self._smsEntry.textChanged.connect(self._on_letter_count_changed)
532
533                 self._entryLayout = QtGui.QVBoxLayout()
534                 self._entryLayout.addWidget(self._targetList.toplevel)
535                 self._entryLayout.addWidget(self._history)
536                 self._entryLayout.addWidget(self._smsEntry)
537                 self._entryLayout.setContentsMargins(0, 0, 0, 0)
538                 self._entryWidget = QtGui.QWidget()
539                 self._entryWidget.setLayout(self._entryLayout)
540                 self._entryWidget.setContentsMargins(0, 0, 0, 0)
541                 self._scrollEntry = QtGui.QScrollArea()
542                 self._scrollEntry.setWidget(self._entryWidget)
543                 self._scrollEntry.setWidgetResizable(True)
544                 self._scrollEntry.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignBottom)
545                 self._scrollEntry.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
546                 self._scrollEntry.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
547
548                 self._characterCountLabel = QtGui.QLabel("")
549                 self._singleNumberSelector = QtGui.QComboBox()
550                 self._cids = []
551                 self._singleNumberSelector.activated.connect(self._on_single_change_number)
552                 self._smsButton = QtGui.QPushButton("SMS")
553                 self._smsButton.clicked.connect(self._on_sms_clicked)
554                 self._smsButton.setEnabled(False)
555                 self._dialButton = QtGui.QPushButton("Dial")
556                 self._dialButton.clicked.connect(self._on_call_clicked)
557                 self._cancelButton = QtGui.QPushButton("Cancel Call")
558                 self._cancelButton.clicked.connect(self._on_cancel_clicked)
559                 self._cancelButton.setVisible(False)
560
561                 self._buttonLayout = QtGui.QHBoxLayout()
562                 self._buttonLayout.addWidget(self._characterCountLabel)
563                 self._buttonLayout.addWidget(self._singleNumberSelector)
564                 self._buttonLayout.addWidget(self._smsButton)
565                 self._buttonLayout.addWidget(self._dialButton)
566                 self._buttonLayout.addWidget(self._cancelButton)
567
568                 self._layout = QtGui.QVBoxLayout()
569                 self._layout.addWidget(self._errorDisplay.toplevel)
570                 self._layout.addWidget(self._scrollEntry)
571                 self._layout.addLayout(self._buttonLayout)
572
573                 centralWidget = QtGui.QWidget()
574                 centralWidget.setLayout(self._layout)
575
576                 self._window = qui_utils.QSignalingMainWindow(parent)
577                 qui_utils.set_window_orientation(self._window, QtCore.Qt.Horizontal)
578                 qui_utils.set_stackable(self._window, True)
579                 self._window.setWindowTitle("Contact")
580                 self._window.setCentralWidget(centralWidget)
581                 self._window.addAction(self._app.orientationAction)
582                 self._window.closed.connect(self._on_close_window)
583                 self._window.hidden.connect(self._on_close_window)
584
585                 self._closeWindowAction = QtGui.QAction(None)
586                 self._closeWindowAction.setText("Close")
587                 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
588                 self._closeWindowAction.triggered.connect(self._on_close_window)
589
590                 fileMenu = self._window.menuBar().addMenu("&File")
591                 fileMenu.addAction(self._closeWindowAction)
592                 fileMenu.addAction(app.quitAction)
593                 viewMenu = self._window.menuBar().addMenu("&View")
594                 viewMenu.addAction(app.fullscreenAction)
595
596                 self._scrollTimer = QtCore.QTimer()
597                 self._scrollTimer.setInterval(100)
598                 self._scrollTimer.setSingleShot(True)
599                 self._scrollTimer.timeout.connect(self._on_delayed_scroll_to_bottom)
600
601                 self._window.show()
602                 self._smsEntry.setPlainText(self._session.draft.message)
603                 self._update_letter_count()
604                 self._update_target_fields()
605
606         @property
607         def window(self):
608                 return self._window
609
610         def close(self):
611                 if self._window is None:
612                         # Already closed
613                         return
614                 self._session.draft.recipientsChanged.disconnect(self._on_recipients_changed)
615                 self._session.draft.sendingMessage.disconnect(self._on_op_started)
616                 self._session.draft.calling.disconnect(self._on_op_started)
617                 self._session.draft.calling.disconnect(self._on_calling_started)
618                 self._session.draft.cancelling.disconnect(self._on_op_started)
619                 self._session.draft.sentMessage.disconnect(self._on_op_finished)
620                 self._session.draft.called.disconnect(self._on_op_finished)
621                 self._session.draft.cancelled.disconnect(self._on_op_finished)
622                 self._session.draft.error.disconnect(self._on_op_error)
623                 window = self._window
624                 self._window = None
625                 try:
626                         message = unicode(self._smsEntry.toPlainText())
627                         self._session.draft.message = message
628                         window.hide()
629                         window.close()
630                         window.destroy()
631                 except AttributeError:
632                         _moduleLogger.exception("Oh well")
633                 except RuntimeError:
634                         _moduleLogger.exception("Oh well")
635
636         def set_orientation(self, isPortrait):
637                 if isPortrait:
638                         qui_utils.set_window_orientation(self._window, QtCore.Qt.Vertical)
639                 else:
640                         qui_utils.set_window_orientation(self._window, QtCore.Qt.Horizontal)
641                 self._scroll_to_bottom()
642
643         def _update_letter_count(self):
644                 count = self._smsEntry.toPlainText().size()
645                 numTexts, numCharInText = divmod(count, self.MAX_CHAR)
646                 numTexts += 1
647                 numCharsLeftInText = self.MAX_CHAR - numCharInText
648                 self._characterCountLabel.setText("%d (%d)" % (numCharsLeftInText, numTexts))
649
650         def _update_button_state(self):
651                 self._cancelButton.setEnabled(True)
652                 if self._session.draft.get_num_contacts() == 0:
653                         self._dialButton.setEnabled(False)
654                         self._smsButton.setEnabled(False)
655                 elif self._session.draft.get_num_contacts() == 1:
656                         count = self._smsEntry.toPlainText().size()
657                         if count == 0:
658                                 self._dialButton.setEnabled(True)
659                                 self._smsButton.setEnabled(False)
660                         else:
661                                 self._dialButton.setEnabled(False)
662                                 self._smsButton.setEnabled(True)
663                 else:
664                         self._dialButton.setEnabled(False)
665                         count = self._smsEntry.toPlainText().size()
666                         if count == 0:
667                                 self._smsButton.setEnabled(False)
668                         else:
669                                 self._smsButton.setEnabled(True)
670
671         def _update_target_fields(self):
672                 draftContactsCount = self._session.draft.get_num_contacts()
673                 if draftContactsCount == 0:
674                         self._window.hide()
675                         del self._cids[:]
676                 elif draftContactsCount == 1:
677                         (cid, ) = self._session.draft.get_contacts()
678                         title = self._session.draft.get_title(cid)
679                         description = self._session.draft.get_description(cid)
680                         numbers = self._session.draft.get_numbers(cid)
681
682                         self._targetList.setVisible(False)
683                         if description:
684                                 self._history.setText(description)
685                                 self._history.setVisible(True)
686                         else:
687                                 self._history.setText("")
688                                 self._history.setVisible(False)
689                         self._populate_number_selector(self._singleNumberSelector, cid, 0, numbers)
690                         self._cids = [cid]
691
692                         self._scroll_to_bottom()
693                         self._window.setWindowTitle(title)
694                         self._window.show()
695                         self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason)
696                 else:
697                         self._targetList.setVisible(True)
698                         self._targetList.update()
699                         self._history.setText("")
700                         self._history.setVisible(False)
701                         self._singleNumberSelector.setVisible(False)
702
703                         self._scroll_to_bottom()
704                         self._window.setWindowTitle("Contacts")
705                         self._window.show()
706                         self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason)
707
708         def _populate_number_selector(self, selector, cid, cidIndex, numbers):
709                 selector.clear()
710
711                 selectedNumber = self._session.draft.get_selected_number(cid)
712                 if len(numbers) == 1:
713                         # If no alt numbers available, check the address book
714                         numbers, defaultIndex = _get_contact_numbers(self._session, cid, selectedNumber)
715                 else:
716                         defaultIndex = _index_number(numbers, selectedNumber)
717
718                 for number, description in numbers:
719                         if description:
720                                 label = "%s - %s" % (number, description)
721                         else:
722                                 label = number
723                         selector.addItem(label)
724                 selector.setVisible(True)
725                 if 1 < len(numbers):
726                         selector.setEnabled(True)
727                         selector.setCurrentIndex(defaultIndex)
728                 else:
729                         selector.setEnabled(False)
730
731         def _scroll_to_bottom(self):
732                 self._scrollTimer.start()
733
734         @misc_utils.log_exception(_moduleLogger)
735         def _on_delayed_scroll_to_bottom(self):
736                 with qui_utils.notify_error(self._app.errorLog):
737                         self._scrollEntry.ensureWidgetVisible(self._smsEntry)
738
739         @misc_utils.log_exception(_moduleLogger)
740         def _on_sms_clicked(self, arg):
741                 with qui_utils.notify_error(self._app.errorLog):
742                         message = unicode(self._smsEntry.toPlainText())
743                         self._session.draft.message = message
744                         self._session.draft.send()
745
746         @misc_utils.log_exception(_moduleLogger)
747         def _on_call_clicked(self, arg):
748                 with qui_utils.notify_error(self._app.errorLog):
749                         message = unicode(self._smsEntry.toPlainText())
750                         self._session.draft.message = message
751                         self._session.draft.call()
752
753         @QtCore.pyqtSlot()
754         @misc_utils.log_exception(_moduleLogger)
755         def _on_cancel_clicked(self, message):
756                 with qui_utils.notify_error(self._app.errorLog):
757                         self._session.draft.cancel()
758
759         @misc_utils.log_exception(_moduleLogger)
760         def _on_single_change_number(self, index):
761                 with qui_utils.notify_error(self._app.errorLog):
762                         # Exception thrown when the first item is removed
763                         cid = self._cids[0]
764                         try:
765                                 numbers = self._session.draft.get_numbers(cid)
766                         except KeyError:
767                                 _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
768                                 return
769                         number = numbers[index][0]
770                         self._session.draft.set_selected_number(cid, number)
771
772         @QtCore.pyqtSlot()
773         @misc_utils.log_exception(_moduleLogger)
774         def _on_recipients_changed(self):
775                 with qui_utils.notify_error(self._app.errorLog):
776                         self._update_target_fields()
777                         self._update_button_state()
778
779         @QtCore.pyqtSlot()
780         @misc_utils.log_exception(_moduleLogger)
781         def _on_op_started(self):
782                 with qui_utils.notify_error(self._app.errorLog):
783                         self._smsEntry.setReadOnly(True)
784                         self._smsButton.setVisible(False)
785                         self._dialButton.setVisible(False)
786                         self._window.show()
787
788         @QtCore.pyqtSlot()
789         @misc_utils.log_exception(_moduleLogger)
790         def _on_calling_started(self):
791                 with qui_utils.notify_error(self._app.errorLog):
792                         self._cancelButton.setVisible(True)
793
794         @QtCore.pyqtSlot()
795         @misc_utils.log_exception(_moduleLogger)
796         def _on_op_finished(self):
797                 with qui_utils.notify_error(self._app.errorLog):
798                         self._smsEntry.setPlainText("")
799                         self._smsEntry.setReadOnly(False)
800                         self._cancelButton.setVisible(False)
801                         self._smsButton.setVisible(True)
802                         self._dialButton.setVisible(True)
803                         self._window.hide()
804
805         @QtCore.pyqtSlot()
806         @misc_utils.log_exception(_moduleLogger)
807         def _on_op_error(self, message):
808                 with qui_utils.notify_error(self._app.errorLog):
809                         self._smsEntry.setReadOnly(False)
810                         self._cancelButton.setVisible(False)
811                         self._smsButton.setVisible(True)
812                         self._dialButton.setVisible(True)
813
814                         self._errorLog.push_error(message)
815
816         @QtCore.pyqtSlot()
817         @misc_utils.log_exception(_moduleLogger)
818         def _on_letter_count_changed(self):
819                 with qui_utils.notify_error(self._app.errorLog):
820                         self._update_letter_count()
821                         self._update_button_state()
822
823         @QtCore.pyqtSlot()
824         @QtCore.pyqtSlot(bool)
825         @misc_utils.log_exception(_moduleLogger)
826         def _on_close_window(self, checked = True):
827                 with qui_utils.notify_error(self._app.errorLog):
828                         self.close()
829
830
831 def _index_number(numbers, default):
832         uglyContactNumbers = (
833                 misc_utils.make_ugly(contactNumber)
834                 for (contactNumber, _) in numbers
835         )
836         defaultMatches = [
837                 misc_utils.similar_ugly_numbers(default, contactNumber)
838                 for contactNumber in uglyContactNumbers
839         ]
840         try:
841                 defaultIndex = defaultMatches.index(True)
842         except ValueError:
843                 defaultIndex = 0
844                 _moduleLogger.warn(
845                         "Could not find contact number %s among %r" % (
846                                 default, numbers
847                         )
848                 )
849         return defaultIndex
850
851
852 def _get_contact_numbers(session, contactId, number):
853         contactPhoneNumbers = []
854         if contactId and contactId != "0":
855                 try:
856                         contactDetails = copy.deepcopy(session.get_contacts()[contactId])
857                         contactPhoneNumbers = contactDetails["numbers"]
858                 except KeyError:
859                         contactPhoneNumbers = []
860                 contactPhoneNumbers = [
861                         (contactPhoneNumber["phoneNumber"], contactPhoneNumber.get("phoneType", "Unknown"))
862                         for contactPhoneNumber in contactPhoneNumbers
863                 ]
864                 defaultIndex = _index_number(contactPhoneNumbers, number)
865
866         if not contactPhoneNumbers:
867                 contactPhoneNumbers = [(number, "Unknown")]
868                 defaultIndex = 0
869
870         return contactPhoneNumbers, defaultIndex