Rounding out the contacts implementation
[gc-dialer] / src / dialcentral_qt.py
1 #!/usr/bin/env python
2 # -*- coding: UTF8 -*-
3
4 from __future__ import with_statement
5
6 import sys
7 import os
8 import shutil
9 import simplejson
10 import re
11 import functools
12 import logging
13
14 from PyQt4 import QtGui
15 from PyQt4 import QtCore
16
17 import constants
18 from util import qui_utils
19 from util import qtpie
20 from util import misc as misc_utils
21
22 import backends.null_backend as null_backend
23 import backends.file_backend as file_backend
24 import session
25
26
27 _moduleLogger = logging.getLogger(__name__)
28
29
30 IS_MAEMO = True
31
32
33 class Dialcentral(object):
34
35         _DATA_PATHS = [
36                 os.path.dirname(__file__),
37                 os.path.join(os.path.dirname(__file__), "../data"),
38                 os.path.join(os.path.dirname(__file__), "../lib"),
39                 '/usr/share/%s' % constants.__app_name__,
40                 '/usr/lib/%s' % constants.__app_name__,
41         ]
42
43         def __init__(self, app):
44                 self._app = app
45                 self._recent = []
46                 self._hiddenCategories = set()
47                 self._hiddenUnits = {}
48                 self._clipboard = QtGui.QApplication.clipboard()
49
50                 self._mainWindow = None
51
52                 self._fullscreenAction = QtGui.QAction(None)
53                 self._fullscreenAction.setText("Fullscreen")
54                 self._fullscreenAction.setCheckable(True)
55                 self._fullscreenAction.setShortcut(QtGui.QKeySequence("CTRL+Enter"))
56                 self._fullscreenAction.toggled.connect(self._on_toggle_fullscreen)
57
58                 self._logAction = QtGui.QAction(None)
59                 self._logAction.setText("Log")
60                 self._logAction.setShortcut(QtGui.QKeySequence("CTRL+l"))
61                 self._logAction.triggered.connect(self._on_log)
62
63                 self._quitAction = QtGui.QAction(None)
64                 self._quitAction.setText("Quit")
65                 self._quitAction.setShortcut(QtGui.QKeySequence("CTRL+q"))
66                 self._quitAction.triggered.connect(self._on_quit)
67
68                 self._app.lastWindowClosed.connect(self._on_app_quit)
69                 self.load_settings()
70
71                 self._mainWindow = MainWindow(None, self)
72                 self._mainWindow.window.destroyed.connect(self._on_child_close)
73
74         def load_settings(self):
75                 try:
76                         with open(constants._user_settings_, "r") as settingsFile:
77                                 settings = simplejson.load(settingsFile)
78                 except IOError, e:
79                         _moduleLogger.info("No settings")
80                         settings = {}
81                 except ValueError:
82                         _moduleLogger.info("Settings were corrupt")
83                         settings = {}
84
85                 self._fullscreenAction.setChecked(settings.get("isFullScreen", False))
86
87         def save_settings(self):
88                 settings = {
89                         "isFullScreen": self._fullscreenAction.isChecked(),
90                 }
91                 with open(constants._user_settings_, "w") as settingsFile:
92                         simplejson.dump(settings, settingsFile)
93                 self._mainWindow.destroy()
94
95         @property
96         def fsContactsPath(self):
97                 return os.path.join(constants._data_path_, "contacts")
98
99         @property
100         def fullscreenAction(self):
101                 return self._fullscreenAction
102
103         @property
104         def logAction(self):
105                 return self._logAction
106
107         @property
108         def quitAction(self):
109                 return self._quitAction
110
111         def _close_windows(self):
112                 if self._mainWindow is not None:
113                         self._mainWindow.window.destroyed.disconnect(self._on_child_close)
114                         self._mainWindow.close()
115                         self._mainWindow = None
116
117         @QtCore.pyqtSlot()
118         @QtCore.pyqtSlot(bool)
119         @misc_utils.log_exception(_moduleLogger)
120         def _on_app_quit(self, checked = False):
121                 self.save_settings()
122
123         @QtCore.pyqtSlot(QtCore.QObject)
124         @misc_utils.log_exception(_moduleLogger)
125         def _on_child_close(self, obj = None):
126                 self._mainWindow = None
127
128         @QtCore.pyqtSlot()
129         @QtCore.pyqtSlot(bool)
130         @misc_utils.log_exception(_moduleLogger)
131         def _on_toggle_fullscreen(self, checked = False):
132                 for window in self._walk_children():
133                         window.set_fullscreen(checked)
134
135         @QtCore.pyqtSlot()
136         @QtCore.pyqtSlot(bool)
137         @misc_utils.log_exception(_moduleLogger)
138         def _on_log(self, checked = False):
139                 with open(constants._user_logpath_, "r") as f:
140                         logLines = f.xreadlines()
141                         log = "".join(logLines)
142                         self._clipboard.setText(log)
143
144         @QtCore.pyqtSlot()
145         @QtCore.pyqtSlot(bool)
146         @misc_utils.log_exception(_moduleLogger)
147         def _on_quit(self, checked = False):
148                 self._close_windows()
149
150
151 class CredentialsDialog(object):
152
153         def __init__(self):
154                 self._usernameField = QtGui.QLineEdit()
155                 self._passwordField = QtGui.QLineEdit()
156                 self._passwordField.setEchoMode(QtGui.QLineEdit.PasswordEchoOnEdit)
157
158                 self._credLayout = QtGui.QGridLayout()
159                 self._credLayout.addWidget(QtGui.QLabel("Username"), 0, 0)
160                 self._credLayout.addWidget(self._usernameField, 0, 1)
161                 self._credLayout.addWidget(QtGui.QLabel("Password"), 1, 0)
162                 self._credLayout.addWidget(self._passwordField, 1, 1)
163
164                 self._loginButton = QtGui.QPushButton("&Login")
165                 self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
166                 self._buttonLayout.addButton(self._loginButton, QtGui.QDialogButtonBox.AcceptRole)
167
168                 self._layout = QtGui.QVBoxLayout()
169                 self._layout.addLayout(self._credLayout)
170                 self._layout.addWidget(self._buttonLayout)
171
172                 self._dialog = QtGui.QDialog()
173                 self._dialog.setWindowTitle("Login")
174                 self._dialog.setLayout(self._layout)
175                 self._dialog.setAttribute(QtCore.Qt.WA_DeleteOnClose, False)
176                 qui_utils.set_autorient(self._dialog, True)
177                 self._buttonLayout.accepted.connect(self._dialog.accept)
178                 self._buttonLayout.rejected.connect(self._dialog.reject)
179
180         def run(self, defaultUsername, defaultPassword, parent=None):
181                 self._dialog.setParent(parent, QtCore.Qt.Dialog)
182                 try:
183                         self._usernameField.setText(defaultUsername)
184                         self._passwordField.setText(defaultPassword)
185
186                         response = self._dialog.exec_()
187                         if response == QtGui.QDialog.Accepted:
188                                 return str(self._usernameField.text()), str(self._passwordField.text())
189                         elif response == QtGui.QDialog.Rejected:
190                                 raise RuntimeError("Login Cancelled")
191                 finally:
192                         self._dialog.setParent(None, QtCore.Qt.Dialog)
193
194
195 class AccountDialog(object):
196
197         def __init__(self):
198                 self._accountNumberLabel = QtGui.QLabel("NUMBER NOT SET")
199                 self._clearButton = QtGui.QPushButton("Clear Account")
200                 self._clearButton.clicked.connect(self._on_clear)
201                 self._doClear = False
202
203                 self._credLayout = QtGui.QGridLayout()
204                 self._credLayout.addWidget(QtGui.QLabel("Account"), 0, 0)
205                 self._credLayout.addWidget(self._accountNumberLabel, 0, 1)
206                 self._credLayout.addWidget(QtGui.QLabel("Callback"), 1, 0)
207                 self._credLayout.addWidget(self._clearButton, 2, 1)
208
209                 self._loginButton = QtGui.QPushButton("&Login")
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.addLayout(self._buttonLayout)
216
217                 self._dialog = QtGui.QDialog()
218                 self._dialog.setWindowTitle("Login")
219                 self._dialog.setLayout(self._layout)
220                 qui_utils.set_autorient(self._dialog, True)
221                 self._buttonLayout.accepted.connect(self._dialog.accept)
222                 self._buttonLayout.rejected.connect(self._dialog.reject)
223
224         @property
225         def doClear(self):
226                 return self._doClear
227
228         accountNumber = property(
229                 lambda self: str(self._accountNumberLabel.text()),
230                 lambda self, num: self._accountNumberLabel.setText(num),
231         )
232
233         def run(self, defaultUsername, defaultPassword, parent=None):
234                 self._doClear = False
235                 self._dialog.setParent(parent)
236                 self._usernameField.setText(defaultUsername)
237                 self._passwordField.setText(defaultPassword)
238
239                 response = self._dialog.exec_()
240                 if response == QtGui.QDialog.Accepted:
241                         return str(self._usernameField.text()), str(self._passwordField.text())
242                 elif response == QtGui.QDialog.Rejected:
243                         raise RuntimeError("Login Cancelled")
244
245         @QtCore.pyqtSlot()
246         @QtCore.pyqtSlot(bool)
247         def _on_clear(self, checked = False):
248                 self._doClear = True
249                 self._dialog.accept()
250
251
252 class SMSEntryWindow(object):
253
254         def __init__(self, parent, app, session, errorLog):
255                 self._contacts = []
256                 self._app = app
257                 self._session = session
258                 self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
259                 self._session.draft.called.connect(self._on_op_finished)
260                 self._session.draft.sentMessage.connect(self._on_op_finished)
261                 self._session.draft.cancelled.connect(self._on_op_finished)
262                 self._errorLog = errorLog
263
264                 self._targetLayout = QtGui.QVBoxLayout()
265                 self._targetList = QtGui.QWidget()
266                 self._targetList.setLayout(self._targetLayout)
267                 self._history = QtGui.QTextEdit()
268                 self._smsEntry = QtGui.QTextEdit()
269                 self._smsEntry.textChanged.connect(self._on_letter_count_changed)
270
271                 self._entryLayout = QtGui.QVBoxLayout()
272                 self._entryLayout.addWidget(self._targetList)
273                 self._entryLayout.addWidget(self._history)
274                 self._entryLayout.addWidget(self._smsEntry)
275                 self._entryWidget = QtGui.QWidget()
276                 self._entryWidget.setLayout(self._entryLayout)
277                 self._scrollEntry = QtGui.QScrollArea()
278                 self._scrollEntry.setWidget(self._entryWidget)
279                 self._scrollEntry.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignBottom)
280                 self._scrollEntry.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
281                 self._scrollEntry.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
282
283                 self._characterCountLabel = QtGui.QLabel("Letters: %s" % 0)
284                 self._singleNumberSelector = QtGui.QComboBox()
285                 self._smsButton = QtGui.QPushButton("SMS")
286                 self._smsButton.clicked.connect(self._on_sms_clicked)
287                 self._dialButton = QtGui.QPushButton("Dial")
288                 self._dialButton.clicked.connect(self._on_call_clicked)
289
290                 self._buttonLayout = QtGui.QHBoxLayout()
291                 self._buttonLayout.addWidget(self._characterCountLabel)
292                 self._buttonLayout.addWidget(self._singleNumberSelector)
293                 self._buttonLayout.addWidget(self._smsButton)
294                 self._buttonLayout.addWidget(self._dialButton)
295
296                 self._layout = QtGui.QVBoxLayout()
297                 self._layout.addWidget(self._scrollEntry)
298                 self._layout.addLayout(self._buttonLayout)
299
300                 centralWidget = QtGui.QWidget()
301                 centralWidget.setLayout(self._layout)
302
303                 self._window = QtGui.QMainWindow(parent)
304                 qui_utils.set_autorient(self._window, True)
305                 qui_utils.set_stackable(self._window, True)
306                 self._window.setWindowTitle("Contact")
307                 self._window.setCentralWidget(centralWidget)
308                 self._window.show()
309                 self._update_recipients()
310
311         def _update_letter_count(self):
312                 count = self._smsEntry.toPlainText().size()
313                 self._characterCountLabel.setText("Letters: %s" % count)
314
315         def _update_button_state(self):
316                 if len(self._contacts) == 0:
317                         self._dialButton.setEnabled(False)
318                         self._smsButton.setEnabled(False)
319                 elif len(self._contacts) == 1:
320                         count = self._smsEntry.toPlainText().size()
321                         if count == 0:
322                                 self._dialButton.setEnabled(True)
323                                 self._smsButton.setEnabled(False)
324                         else:
325                                 self._dialButton.setEnabled(False)
326                                 self._smsButton.setEnabled(True)
327                 else:
328                         self._dialButton.setEnabled(False)
329                         self._smsButton.setEnabled(True)
330
331         def _update_recipients(self):
332                 draftContactsCount = self._session.draft.get_num_contacts()
333                 if draftContactsCount == 0:
334                         self._window.hide()
335                 elif draftContactsCount == 1:
336                         (cid, ) = self._session.draft.get_contacts()
337                         title = self._session.draft.get_title(cid)
338                         description = self._session.draft.get_description(cid)
339                         numbers = self._session.draft.get_numbers(cid)
340
341                         self._targetList.setVisible(False)
342                         if description:
343                                 self._history.setHtml(description)
344                                 self._history.setVisible(True)
345                         else:
346                                 self._history.setHtml("")
347                                 self._history.setVisible(False)
348                         self._populate_number_selector(self._singleNumberSelector, cid, numbers)
349
350                         self._scroll_to_bottom()
351                         self._window.setWindowTitle(title)
352                         self._window.show()
353                 else:
354                         self._targetList.setVisible(True)
355                         while self._targetLayout.count():
356                                 removedLayoutItem = self._targetLayout.takeAt(self._targetLayout.count()-1)
357                                 removedWidget = removedLayoutItem.widget()
358                                 removedWidget.close()
359                         for cid in self._session.draft.get_contacts():
360                                 title = self._session.draft.get_title(cid)
361                                 description = self._session.draft.get_description(cid)
362                                 numbers = self._session.draft.get_numbers(cid)
363
364                                 titleLabel = QtGui.QLabel(title)
365                                 numberSelector = QtGui.QComboBox()
366                                 self._populate_number_selector(numberSelector, cid, numbers)
367                                 deleteButton = QtGui.QPushButton("Delete")
368                                 callback = functools.partial(
369                                         self._on_remove_contact,
370                                         cid
371                                 )
372                                 callback.__name__ = "b"
373                                 deleteButton.clicked.connect(
374                                         QtCore.pyqtSlot()(callback)
375                                 )
376
377                                 rowLayout = QtGui.QHBoxLayout()
378                                 rowLayout.addWidget(titleLabel)
379                                 rowLayout.addWidget(numberSelector)
380                                 rowLayout.addWidget(deleteButton)
381                                 rowWidget = QtGui.QWidget()
382                                 rowWidget.setLayout(rowLayout)
383                                 self._targetLayout.addWidget(rowWidget)
384                         self._history.setHtml("")
385                         self._history.setVisible(False)
386                         self._singleNumberSelector.setVisible(False)
387
388                         self._scroll_to_bottom()
389                         self._window.setWindowTitle("Contacts")
390                         self._window.show()
391
392         def _populate_number_selector(self, selector, cid, numbers):
393                 while 0 < selector.count():
394                         selector.removeItem(0)
395                 for number, description in numbers:
396                         if description:
397                                 label = "%s - %s" % (number, description)
398                         else:
399                                 label = number
400                         selector.addItem(label)
401                 selector.setVisible(True)
402                 if 1 < len(numbers):
403                         selector.setEnabled(True)
404                 else:
405                         selector.setEnabled(False)
406                 callback = functools.partial(
407                         self._on_change_number,
408                         cid
409                 )
410                 callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
411                 selector.currentIndexChanged.connect(
412                         QtCore.pyqtSlot(int)(callback)
413                 )
414
415         def _scroll_to_bottom(self):
416                 self._scrollEntry.ensureWidgetVisible(self._smsEntry)
417
418         @QtCore.pyqtSlot()
419         @misc_utils.log_exception(_moduleLogger)
420         def _on_sms_clicked(self):
421                 message = str(self._smsEntry.toPlainText())
422                 self._session.draft.send(message)
423                 self._smsEntry.setPlainText("")
424
425         @QtCore.pyqtSlot()
426         @misc_utils.log_exception(_moduleLogger)
427         def _on_call_clicked(self):
428                 self._session.draft.call()
429                 self._smsEntry.setPlainText("")
430
431         @misc_utils.log_exception(_moduleLogger)
432         def _on_remove_contact(self, cid):
433                 self._session.draft.remove_contact(cid)
434
435         @misc_utils.log_exception(_moduleLogger)
436         def _on_change_number(self, cid, index):
437                 numbers = self._session.draft.get_numbers(cid)
438                 number = numbers[index][0]
439                 self._session.draft.set_selected_number(cid, number)
440
441         @QtCore.pyqtSlot()
442         @misc_utils.log_exception(_moduleLogger)
443         def _on_recipients_changed(self):
444                 self._update_recipients()
445
446         @QtCore.pyqtSlot()
447         @misc_utils.log_exception(_moduleLogger)
448         def _on_op_finished(self):
449                 self._window.hide()
450
451         @QtCore.pyqtSlot()
452         @misc_utils.log_exception(_moduleLogger)
453         def _on_letter_count_changed(self):
454                 self._update_letter_count()
455                 self._update_button_state()
456
457
458 class DelayedWidget(object):
459
460         def __init__(self, app):
461                 self._layout = QtGui.QVBoxLayout()
462                 self._widget = QtGui.QWidget()
463                 self._widget.setLayout(self._layout)
464
465                 self._child = None
466                 self._isEnabled = True
467
468         @property
469         def toplevel(self):
470                 return self._widget
471
472         def has_child(self):
473                 return self._child is not None
474
475         def set_child(self, child):
476                 if self._child is not None:
477                         self._layout.removeWidget(self._child.toplevel)
478                 self._child = child
479                 if self._child is not None:
480                         self._layout.addWidget(self._child.toplevel)
481
482                 if self._isEnabled:
483                         self._child.enable()
484                 else:
485                         self._child.disable()
486
487         def enable(self):
488                 self._isEnabled = True
489                 if self._child is not None:
490                         self._child.enable()
491
492         def disable(self):
493                 self._isEnabled = False
494                 if self._child is not None:
495                         self._child.disable()
496
497         def clear(self):
498                 if self._child is not None:
499                         self._child.clear()
500
501         def refresh(self, force=True):
502                 if self._child is not None:
503                         self._child.refresh(force)
504
505
506 class Dialpad(object):
507
508         def __init__(self, app, session, errorLog):
509                 self._app = app
510                 self._session = session
511                 self._errorLog = errorLog
512
513                 self._plus = self._generate_key_button("+", "")
514                 self._entry = QtGui.QLineEdit()
515
516                 backAction = QtGui.QAction(None)
517                 backAction.setText("Back")
518                 backAction.triggered.connect(self._on_backspace)
519                 backPieItem = qtpie.QActionPieItem(backAction)
520                 clearAction = QtGui.QAction(None)
521                 clearAction.setText("Clear")
522                 clearAction.triggered.connect(self._on_clear_text)
523                 clearPieItem = qtpie.QActionPieItem(clearAction)
524                 self._back = qtpie.QPieButton(backPieItem)
525                 self._back.set_center(backPieItem)
526                 self._back.insertItem(qtpie.PieFiling.NULL_CENTER)
527                 self._back.insertItem(clearPieItem)
528                 self._back.insertItem(qtpie.PieFiling.NULL_CENTER)
529                 self._back.insertItem(qtpie.PieFiling.NULL_CENTER)
530
531                 self._entryLayout = QtGui.QHBoxLayout()
532                 self._entryLayout.addWidget(self._plus, 0, QtCore.Qt.AlignCenter)
533                 self._entryLayout.addWidget(self._entry, 10)
534                 self._entryLayout.addWidget(self._back, 0, QtCore.Qt.AlignCenter)
535
536                 self._smsButton = QtGui.QPushButton("SMS")
537                 self._smsButton.clicked.connect(self._on_sms_clicked)
538                 self._callButton = QtGui.QPushButton("Call")
539                 self._callButton.clicked.connect(self._on_call_clicked)
540
541                 self._padLayout = QtGui.QGridLayout()
542                 rows = [0, 0, 0, 1, 1, 1, 2, 2, 2]
543                 columns = [0, 1, 2] * 3
544                 keys = [
545                         ("1", ""),
546                         ("2", "ABC"),
547                         ("3", "DEF"),
548                         ("4", "GHI"),
549                         ("5", "JKL"),
550                         ("6", "MNO"),
551                         ("7", "PQRS"),
552                         ("8", "TUV"),
553                         ("9", "WXYZ"),
554                 ]
555                 for (num, letters), (row, column) in zip(keys, zip(rows, columns)):
556                         self._padLayout.addWidget(
557                                 self._generate_key_button(num, letters), row, column, QtCore.Qt.AlignCenter
558                         )
559                 self._padLayout.addWidget(self._smsButton, 3, 0)
560                 self._padLayout.addWidget(
561                         self._generate_key_button("0", ""), 3, 1, QtCore.Qt.AlignCenter
562                 )
563                 self._padLayout.addWidget(self._callButton, 3, 2)
564
565                 self._layout = QtGui.QVBoxLayout()
566                 self._layout.addLayout(self._entryLayout)
567                 self._layout.addLayout(self._padLayout)
568                 self._widget = QtGui.QWidget()
569                 self._widget.setLayout(self._layout)
570
571         @property
572         def toplevel(self):
573                 return self._widget
574
575         def enable(self):
576                 self._smsButton.setEnabled(True)
577                 self._callButton.setEnabled(True)
578
579         def disable(self):
580                 self._smsButton.setEnabled(False)
581                 self._callButton.setEnabled(False)
582
583         def clear(self):
584                 pass
585
586         def refresh(self, force = True):
587                 pass
588
589         def _generate_key_button(self, center, letters):
590                 centerPieItem = self._generate_button_slice(center)
591                 button = qtpie.QPieButton(centerPieItem)
592                 button.set_center(centerPieItem)
593
594                 if len(letters) == 0:
595                         for i in xrange(8):
596                                 pieItem = qtpie.PieFiling.NULL_CENTER
597                                 button.insertItem(pieItem)
598                 elif len(letters) in [3, 4]:
599                         for i in xrange(6 - len(letters)):
600                                 pieItem = qtpie.PieFiling.NULL_CENTER
601                                 button.insertItem(pieItem)
602
603                         for letter in letters:
604                                 pieItem = self._generate_button_slice(letter)
605                                 button.insertItem(pieItem)
606
607                         for i in xrange(2):
608                                 pieItem = qtpie.PieFiling.NULL_CENTER
609                                 button.insertItem(pieItem)
610                 else:
611                         raise NotImplementedError("Cannot handle %r" % letters)
612                 return button
613
614         def _generate_button_slice(self, letter):
615                 action = QtGui.QAction(None)
616                 action.setText(letter)
617                 action.triggered.connect(lambda: self._on_keypress(letter))
618                 pieItem = qtpie.QActionPieItem(action)
619                 return pieItem
620
621         @misc_utils.log_exception(_moduleLogger)
622         def _on_keypress(self, key):
623                 self._entry.insert(key)
624
625         @misc_utils.log_exception(_moduleLogger)
626         def _on_backspace(self, toggled = False):
627                 self._entry.backspace()
628
629         @misc_utils.log_exception(_moduleLogger)
630         def _on_clear_text(self, toggled = False):
631                 self._entry.clear()
632
633         @QtCore.pyqtSlot()
634         @QtCore.pyqtSlot(bool)
635         @misc_utils.log_exception(_moduleLogger)
636         def _on_sms_clicked(self, checked = False):
637                 number = str(self._entry.text())
638                 self._entry.clear()
639
640                 contactId = number
641                 title = number
642                 description = number
643                 numbersWithDescriptions = [(number, "")]
644                 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
645
646         @QtCore.pyqtSlot()
647         @QtCore.pyqtSlot(bool)
648         @misc_utils.log_exception(_moduleLogger)
649         def _on_call_clicked(self, checked = False):
650                 number = str(self._entry.text())
651                 self._entry.clear()
652
653                 contactId = number
654                 title = number
655                 description = number
656                 numbersWithDescriptions = [(number, "")]
657                 self._session.draft.clear()
658                 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
659                 self._session.draft.call()
660
661
662 class History(object):
663
664         DATE_IDX = 0
665         ACTION_IDX = 1
666         NUMBER_IDX = 2
667         FROM_IDX = 3
668         MAX_IDX = 4
669
670         HISTORY_ITEM_TYPES = ["Received", "Missed", "Placed", "All"]
671         HISTORY_COLUMNS = ["When", "What", "Number", "From"]
672         assert len(HISTORY_COLUMNS) == MAX_IDX
673
674         def __init__(self, app, session, errorLog):
675                 self._selectedFilter = self.HISTORY_ITEM_TYPES[-1]
676                 self._app = app
677                 self._session = session
678                 self._session.historyUpdated.connect(self._on_history_updated)
679                 self._errorLog = errorLog
680
681                 self._typeSelection = QtGui.QComboBox()
682                 self._typeSelection.addItems(self.HISTORY_ITEM_TYPES)
683                 self._typeSelection.setCurrentIndex(
684                         self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
685                 )
686                 self._typeSelection.currentIndexChanged[str].connect(self._on_filter_changed)
687
688                 self._itemStore = QtGui.QStandardItemModel()
689                 self._itemStore.setHorizontalHeaderLabels(self.HISTORY_COLUMNS)
690
691                 self._itemView = QtGui.QTreeView()
692                 self._itemView.setModel(self._itemStore)
693                 self._itemView.setUniformRowHeights(True)
694                 self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
695                 self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
696                 self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
697                 self._itemView.setHeaderHidden(True)
698                 self._itemView.activated.connect(self._on_row_activated)
699
700                 self._layout = QtGui.QVBoxLayout()
701                 self._layout.addWidget(self._typeSelection)
702                 self._layout.addWidget(self._itemView)
703                 self._widget = QtGui.QWidget()
704                 self._widget.setLayout(self._layout)
705
706                 self._populate_items()
707
708         @property
709         def toplevel(self):
710                 return self._widget
711
712         def enable(self):
713                 self._itemView.setEnabled(True)
714
715         def disable(self):
716                 self._itemView.setEnabled(False)
717
718         def clear(self):
719                 self._itemView.clear()
720
721         def refresh(self, force=True):
722                 self._session.update_history(force)
723
724         def _populate_items(self):
725                 self._itemStore.clear()
726                 history = self._session.get_history()
727                 history.sort(key=lambda item: item["time"], reverse=True)
728                 for event in history:
729                         if self._selectedFilter in [self.HISTORY_ITEM_TYPES[-1], event["action"]]:
730                                 relTime = abbrev_relative_date(event["relTime"])
731                                 action = event["action"]
732                                 number = event["number"]
733                                 prettyNumber = make_pretty(number)
734                                 name = event["name"]
735                                 if not name or name == number:
736                                         name = event["location"]
737                                 if not name:
738                                         name = "Unknown"
739
740                                 timeItem = QtGui.QStandardItem(relTime)
741                                 actionItem = QtGui.QStandardItem(action)
742                                 numberItem = QtGui.QStandardItem(prettyNumber)
743                                 nameItem = QtGui.QStandardItem(name)
744                                 row = timeItem, actionItem, numberItem, nameItem
745                                 for item in row:
746                                         item.setEditable(False)
747                                         item.setCheckable(False)
748                                         if item is not nameItem:
749                                                 itemFont = item.font()
750                                                 itemFont.setPointSize(max(itemFont.pointSize() - 3, 5))
751                                                 item.setFont(itemFont)
752                                 numberItem.setData(event)
753                                 self._itemStore.appendRow(row)
754
755         @QtCore.pyqtSlot(str)
756         @misc_utils.log_exception(_moduleLogger)
757         def _on_filter_changed(self, newItem):
758                 self._selectedFilter = str(newItem)
759                 self._populate_items()
760
761         @QtCore.pyqtSlot()
762         @misc_utils.log_exception(_moduleLogger)
763         def _on_history_updated(self):
764                 self._populate_items()
765
766         @QtCore.pyqtSlot(QtCore.QModelIndex)
767         @misc_utils.log_exception(_moduleLogger)
768         def _on_row_activated(self, index):
769                 rowIndex = index.row()
770                 item = self._itemStore.item(rowIndex, self.NUMBER_IDX)
771                 contactDetails = item.data().toPyObject()
772
773                 title = str(self._itemStore.item(rowIndex, self.FROM_IDX).text())
774                 number = str(contactDetails[QtCore.QString("number")])
775                 contactId = number # ids don't seem too unique so using numbers
776
777                 descriptionRows = []
778                 # @bug doesn't seem to print multiple entries
779                 for i in xrange(self._itemStore.rowCount()):
780                         iItem = self._itemStore.item(i, self.NUMBER_IDX)
781                         iContactDetails = iItem.data().toPyObject()
782                         iNumber = str(iContactDetails[QtCore.QString("number")])
783                         if number != iNumber:
784                                 continue
785                         relTime = abbrev_relative_date(iContactDetails[QtCore.QString("relTime")])
786                         action = str(iContactDetails[QtCore.QString("action")])
787                         number = str(iContactDetails[QtCore.QString("number")])
788                         prettyNumber = make_pretty(number)
789                         rowItems = relTime, action, prettyNumber
790                         descriptionRows.append("<tr><td>%s</td></tr>" % "</td><td>".join(rowItems))
791                 description = "<table>%s</table>" % "".join(descriptionRows)
792                 numbersWithDescriptions = [(str(contactDetails[QtCore.QString("number")]), "")]
793                 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
794
795
796 class Messages(object):
797
798         NO_MESSAGES = "None"
799         VOICEMAIL_MESSAGES = "Voicemail"
800         TEXT_MESSAGES = "SMS"
801         ALL_TYPES = "All Messages"
802         MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
803
804         UNREAD_STATUS = "Unread"
805         UNARCHIVED_STATUS = "Inbox"
806         ALL_STATUS = "Any"
807         MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
808
809         _MIN_MESSAGES_SHOWN = 4
810
811         def __init__(self, app, session, errorLog):
812                 self._selectedTypeFilter = self.ALL_TYPES
813                 self._selectedStatusFilter = self.ALL_STATUS
814                 self._app = app
815                 self._session = session
816                 self._session.messagesUpdated.connect(self._on_messages_updated)
817                 self._errorLog = errorLog
818
819                 self._typeSelection = QtGui.QComboBox()
820                 self._typeSelection.addItems(self.MESSAGE_TYPES)
821                 self._typeSelection.setCurrentIndex(
822                         self.MESSAGE_TYPES.index(self._selectedTypeFilter)
823                 )
824                 self._typeSelection.currentIndexChanged[str].connect(self._on_type_filter_changed)
825
826                 self._statusSelection = QtGui.QComboBox()
827                 self._statusSelection.addItems(self.MESSAGE_STATUSES)
828                 self._statusSelection.setCurrentIndex(
829                         self.MESSAGE_STATUSES.index(self._selectedStatusFilter)
830                 )
831                 self._statusSelection.currentIndexChanged[str].connect(self._on_status_filter_changed)
832
833                 self._selectionLayout = QtGui.QHBoxLayout()
834                 self._selectionLayout.addWidget(self._typeSelection)
835                 self._selectionLayout.addWidget(self._statusSelection)
836
837                 self._itemStore = QtGui.QStandardItemModel()
838                 self._itemStore.setHorizontalHeaderLabels(["Messages"])
839
840                 self._htmlDelegate = qui_utils.QHtmlDelegate()
841                 self._itemView = QtGui.QTreeView()
842                 self._itemView.setModel(self._itemStore)
843                 self._itemView.setUniformRowHeights(False)
844                 self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
845                 self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
846                 self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
847                 self._itemView.setHeaderHidden(True)
848                 self._itemView.setItemDelegate(self._htmlDelegate)
849                 self._itemView.activated.connect(self._on_row_activated)
850
851                 self._layout = QtGui.QVBoxLayout()
852                 self._layout.addLayout(self._selectionLayout)
853                 self._layout.addWidget(self._itemView)
854                 self._widget = QtGui.QWidget()
855                 self._widget.setLayout(self._layout)
856
857                 self._populate_items()
858
859         @property
860         def toplevel(self):
861                 return self._widget
862
863         def enable(self):
864                 self._itemView.setEnabled(True)
865
866         def disable(self):
867                 self._itemView.setEnabled(False)
868
869         def clear(self):
870                 self._itemView.clear()
871
872         def refresh(self, force=True):
873                 self._session.update_messages(force)
874
875         def _populate_items(self):
876                 self._itemStore.clear()
877                 rawMessages = self._session.get_messages()
878                 rawMessages.sort(key=lambda item: item["time"], reverse=True)
879                 for item in rawMessages:
880                         isUnarchived = not item["isArchived"]
881                         isUnread = not item["isRead"]
882                         visibleStatus = {
883                                 self.UNREAD_STATUS: isUnarchived and isUnread,
884                                 self.UNARCHIVED_STATUS: isUnarchived,
885                                 self.ALL_STATUS: True,
886                         }[self._selectedStatusFilter]
887
888                         visibleType = self._selectedTypeFilter in [item["type"], self.ALL_TYPES]
889                         if visibleType and visibleStatus:
890                                 relTime = abbrev_relative_date(item["relTime"])
891                                 number = item["number"]
892                                 prettyNumber = make_pretty(number)
893                                 name = item["name"]
894                                 if not name or name == number:
895                                         name = item["location"]
896                                 if not name:
897                                         name = "Unknown"
898
899                                 messageParts = list(item["messageParts"])
900                                 if len(messageParts) == 0:
901                                         messages = ("No Transcription", )
902                                 elif len(messageParts) == 1:
903                                         if messageParts[0][1]:
904                                                 messages = (messageParts[0][1], )
905                                         else:
906                                                 messages = ("No Transcription", )
907                                 else:
908                                         messages = [
909                                                 "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
910                                                 for messagePart in messageParts
911                                         ]
912
913                                 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (name, prettyNumber, relTime)
914
915                                 expandedMessages = [firstMessage]
916                                 expandedMessages.extend(messages)
917                                 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
918                                         secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
919                                         collapsedMessages = [firstMessage, secondMessage]
920                                         collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
921                                 else:
922                                         collapsedMessages = expandedMessages
923
924                                 item = dict(item.iteritems())
925                                 item["collapsedMessages"] = "<br/>\n".join(collapsedMessages)
926                                 item["expandedMessages"] = "<br/>\n".join(expandedMessages)
927
928                                 messageItem = QtGui.QStandardItem(item["collapsedMessages"])
929                                 # @bug Not showing all of a message
930                                 messageItem.setData(item)
931                                 messageItem.setEditable(False)
932                                 messageItem.setCheckable(False)
933                                 row = (messageItem, )
934                                 self._itemStore.appendRow(row)
935
936         @QtCore.pyqtSlot(str)
937         @misc_utils.log_exception(_moduleLogger)
938         def _on_type_filter_changed(self, newItem):
939                 self._selectedTypeFilter = str(newItem)
940                 self._populate_items()
941
942         @QtCore.pyqtSlot(str)
943         @misc_utils.log_exception(_moduleLogger)
944         def _on_status_filter_changed(self, newItem):
945                 self._selectedStatusFilter = str(newItem)
946                 self._populate_items()
947
948         @QtCore.pyqtSlot()
949         @misc_utils.log_exception(_moduleLogger)
950         def _on_messages_updated(self):
951                 self._populate_items()
952
953         @QtCore.pyqtSlot(QtCore.QModelIndex)
954         @misc_utils.log_exception(_moduleLogger)
955         def _on_row_activated(self, index):
956                 rowIndex = index.row()
957                 item = self._itemStore.item(rowIndex, 0)
958                 contactDetails = item.data().toPyObject()
959
960                 name = str(contactDetails[QtCore.QString("name")])
961                 number = str(contactDetails[QtCore.QString("number")])
962                 if not name or name == number:
963                         name = str(contactDetails[QtCore.QString("location")])
964                 if not name:
965                         name = "Unknown"
966
967                 contactId = str(contactDetails[QtCore.QString("id")])
968                 title = name
969                 description = str(contactDetails[QtCore.QString("expandedMessages")])
970                 numbersWithDescriptions = [(number, "")]
971                 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
972
973
974 class Contacts(object):
975
976         def __init__(self, app, session, errorLog):
977                 self._app = app
978                 self._session = session
979                 self._session.contactsUpdated.connect(self._on_contacts_updated)
980                 self._errorLog = errorLog
981                 self._addressBookFactories = [
982                         null_backend.NullAddressBookFactory(),
983                         file_backend.FilesystemAddressBookFactory(app.fsContactsPath),
984                 ]
985                 self._addressBooks = []
986
987                 self._listSelection = QtGui.QComboBox()
988                 self._listSelection.addItems([])
989                 self._listSelection.currentIndexChanged[str].connect(self._on_filter_changed)
990
991                 self._itemStore = QtGui.QStandardItemModel()
992                 self._itemStore.setHorizontalHeaderLabels(["Contacts"])
993
994                 self._itemView = QtGui.QTreeView()
995                 self._itemView.setModel(self._itemStore)
996                 self._itemView.setUniformRowHeights(True)
997                 self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
998                 self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
999                 self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
1000                 self._itemView.setHeaderHidden(True)
1001                 self._itemView.activated.connect(self._on_row_activated)
1002
1003                 self._layout = QtGui.QVBoxLayout()
1004                 self._layout.addWidget(self._listSelection)
1005                 self._layout.addWidget(self._itemView)
1006                 self._widget = QtGui.QWidget()
1007                 self._widget.setLayout(self._layout)
1008
1009                 self.update_addressbooks()
1010                 self._populate_items()
1011
1012         @property
1013         def toplevel(self):
1014                 return self._widget
1015
1016         def enable(self):
1017                 self._itemView.setEnabled(True)
1018
1019         def disable(self):
1020                 self._itemView.setEnabled(False)
1021
1022         def clear(self):
1023                 self._itemView.clear()
1024
1025         def refresh(self, force=True):
1026                 self._backend.update_contacts(force)
1027
1028         @property
1029         def _backend(self):
1030                 return self._addressBooks[self._listSelection.currentIndex()]["book"]
1031
1032         def update_addressbooks(self):
1033                 self._addressBooks = [
1034                         {"book": book, "name": book.name}
1035                         for factory in self._addressBookFactories
1036                         for book in factory.get_addressbooks()
1037                 ]
1038                 self._addressBooks.append(
1039                         {
1040                                 "book": self._session,
1041                                 "name": "Google Voice",
1042                         }
1043                 )
1044
1045                 currentItem = str(self._listSelection.currentText())
1046                 if currentItem == "":
1047                         # Not loaded yet
1048                         currentItem = "None"
1049                 while 0 < self._listSelection.count():
1050                         self._listSelection.removeItem(0)
1051                 bookNames = [book["name"] for book in self._addressBooks]
1052                 try:
1053                         newIndex = bookNames.index(currentItem)
1054                 except ValueError:
1055                         # Switch over to None for the user
1056                         newIndex = 0
1057                         self._itemStore.clear()
1058                         _moduleLogger.info("Addressbook %r doesn't exist anymore, switching to None" % currentItem)
1059                 self._listSelection.addItems(bookNames)
1060                 self._listSelection.setCurrentIndex(newIndex)
1061
1062         def _populate_items(self):
1063                 self._itemStore.clear()
1064
1065                 contacts = list(self._backend.get_contacts().itervalues())
1066                 contacts.sort(key=lambda contact: contact["name"].lower())
1067                 for item in contacts:
1068                         name = item["name"]
1069                         numbers = item["numbers"]
1070                         nameItem = QtGui.QStandardItem(name)
1071                         nameItem.setEditable(False)
1072                         nameItem.setCheckable(False)
1073                         nameItem.setData(item)
1074                         row = (nameItem, )
1075                         self._itemStore.appendRow(row)
1076
1077         @QtCore.pyqtSlot(str)
1078         @misc_utils.log_exception(_moduleLogger)
1079         def _on_filter_changed(self, newItem):
1080                 self._populate_items()
1081
1082         @QtCore.pyqtSlot()
1083         @misc_utils.log_exception(_moduleLogger)
1084         def _on_contacts_updated(self):
1085                 self._populate_items()
1086
1087         @QtCore.pyqtSlot(QtCore.QModelIndex)
1088         @misc_utils.log_exception(_moduleLogger)
1089         def _on_row_activated(self, index):
1090                 rowIndex = index.row()
1091                 item = self._itemStore.item(rowIndex, 0)
1092                 contactDetails = item.data().toPyObject()
1093
1094                 name = str(contactDetails[QtCore.QString("name")])
1095                 if not name:
1096                         name = str(contactDetails[QtCore.QString("location")])
1097                 if not name:
1098                         name = "Unknown"
1099
1100                 contactId = str(contactDetails[QtCore.QString("contactId")])
1101                 numbers = contactDetails[QtCore.QString("numbers")]
1102                 numbers = [
1103                         dict(
1104                                 (str(k), str(v))
1105                                 for (k, v) in number.iteritems()
1106                         )
1107                         for number in numbers
1108                 ]
1109                 numbersWithDescriptions = [
1110                         (
1111                                 number["phoneNumber"],
1112                                 self._choose_phonetype(number),
1113                         )
1114                         for number in numbers
1115                 ]
1116                 title = name
1117                 description = name
1118                 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
1119
1120         @staticmethod
1121         def _choose_phonetype(numberDetails):
1122                 if "phoneTypeName" in numberDetails:
1123                         return numberDetails["phoneTypeName"]
1124                 elif "phoneType" in numberDetails:
1125                         return numberDetails["phoneType"]
1126                 else:
1127                         return ""
1128
1129
1130 class MainWindow(object):
1131
1132         KEYPAD_TAB = 0
1133         RECENT_TAB = 1
1134         MESSAGES_TAB = 2
1135         CONTACTS_TAB = 3
1136         MAX_TABS = 4
1137
1138         _TAB_TITLES = [
1139                 "Dialpad",
1140                 "History",
1141                 "Messages",
1142                 "Contacts",
1143         ]
1144         assert len(_TAB_TITLES) == MAX_TABS
1145
1146         _TAB_CLASS = [
1147                 Dialpad,
1148                 History,
1149                 Messages,
1150                 Contacts,
1151         ]
1152         assert len(_TAB_CLASS) == MAX_TABS
1153
1154         def __init__(self, parent, app):
1155                 self._app = app
1156                 self._session = session.Session(constants._data_path_)
1157                 self._session.error.connect(self._on_session_error)
1158                 self._session.loggedIn.connect(self._on_login)
1159                 self._session.loggedOut.connect(self._on_logout)
1160                 self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
1161
1162                 self._credentialsDialog = None
1163                 self._smsEntryDialog = None
1164
1165                 self._errorLog = qui_utils.QErrorLog()
1166                 self._errorDisplay = qui_utils.ErrorDisplay(self._errorLog)
1167
1168                 self._tabsContents = [
1169                         DelayedWidget(self._app)
1170                         for i in xrange(self.MAX_TABS)
1171                 ]
1172                 for tab in self._tabsContents:
1173                         tab.disable()
1174
1175                 self._tabWidget = QtGui.QTabWidget()
1176                 if qui_utils.screen_orientation() == QtCore.Qt.Vertical:
1177                         self._tabWidget.setTabPosition(QtGui.QTabWidget.South)
1178                 else:
1179                         self._tabWidget.setTabPosition(QtGui.QTabWidget.West)
1180                 for tabIndex, tabTitle in enumerate(self._TAB_TITLES):
1181                         self._tabWidget.addTab(self._tabsContents[tabIndex].toplevel, tabTitle)
1182                 self._tabWidget.currentChanged.connect(self._on_tab_changed)
1183
1184                 self._layout = QtGui.QVBoxLayout()
1185                 self._layout.addWidget(self._errorDisplay.toplevel)
1186                 self._layout.addWidget(self._tabWidget)
1187
1188                 centralWidget = QtGui.QWidget()
1189                 centralWidget.setLayout(self._layout)
1190
1191                 self._window = QtGui.QMainWindow(parent)
1192                 self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
1193                 qui_utils.set_autorient(self._window, True)
1194                 qui_utils.set_stackable(self._window, True)
1195                 self._window.setWindowTitle("%s" % constants.__pretty_app_name__)
1196                 self._window.setCentralWidget(centralWidget)
1197
1198                 self._loginTabAction = QtGui.QAction(None)
1199                 self._loginTabAction.setText("Login")
1200                 self._loginTabAction.triggered.connect(self._on_login_requested)
1201
1202                 self._importTabAction = QtGui.QAction(None)
1203                 self._importTabAction.setText("Import")
1204                 self._importTabAction.triggered.connect(self._on_import)
1205
1206                 self._refreshTabAction = QtGui.QAction(None)
1207                 self._refreshTabAction.setText("Refresh")
1208                 self._refreshTabAction.setShortcut(QtGui.QKeySequence("CTRL+r"))
1209                 self._refreshTabAction.triggered.connect(self._on_refresh)
1210
1211                 self._closeWindowAction = QtGui.QAction(None)
1212                 self._closeWindowAction.setText("Close")
1213                 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
1214                 self._closeWindowAction.triggered.connect(self._on_close_window)
1215
1216                 if IS_MAEMO:
1217                         fileMenu = self._window.menuBar().addMenu("&File")
1218                         fileMenu.addAction(self._loginTabAction)
1219                         fileMenu.addAction(self._refreshTabAction)
1220
1221                         toolsMenu = self._window.menuBar().addMenu("&Tools")
1222                         toolsMenu.addAction(self._importTabAction)
1223
1224                         self._window.addAction(self._closeWindowAction)
1225                         self._window.addAction(self._app.quitAction)
1226                         self._window.addAction(self._app.fullscreenAction)
1227                 else:
1228                         fileMenu = self._window.menuBar().addMenu("&File")
1229                         fileMenu.addAction(self._loginTabAction)
1230                         fileMenu.addAction(self._refreshTabAction)
1231                         fileMenu.addAction(self._closeWindowAction)
1232                         fileMenu.addAction(self._app.quitAction)
1233
1234                         viewMenu = self._window.menuBar().addMenu("&View")
1235                         viewMenu.addAction(self._app.fullscreenAction)
1236
1237                         toolsMenu = self._window.menuBar().addMenu("&Tools")
1238                         toolsMenu.addAction(self._importTabAction)
1239
1240                 self._window.addAction(self._app.logAction)
1241
1242                 self._initialize_tab(self._tabWidget.currentIndex())
1243                 self.set_fullscreen(self._app.fullscreenAction.isChecked())
1244                 self._window.show()
1245
1246         @property
1247         def window(self):
1248                 return self._window
1249
1250         def walk_children(self):
1251                 return ()
1252
1253         def show(self):
1254                 self._window.show()
1255                 for child in self.walk_children():
1256                         child.show()
1257
1258         def hide(self):
1259                 for child in self.walk_children():
1260                         child.hide()
1261                 self._window.hide()
1262
1263         def close(self):
1264                 for child in self.walk_children():
1265                         child.window.destroyed.disconnect(self._on_child_close)
1266                         child.close()
1267                 self._window.close()
1268
1269         def destroy(self):
1270                 if self._session.state != self._session.LOGGEDOUT_STATE:
1271                         self._session.logout()
1272
1273         def set_fullscreen(self, isFullscreen):
1274                 if isFullscreen:
1275                         self._window.showFullScreen()
1276                 else:
1277                         self._window.showNormal()
1278                 for child in self.walk_children():
1279                         child.set_fullscreen(isFullscreen)
1280
1281         def _initialize_tab(self, index):
1282                 assert index < self.MAX_TABS
1283                 if not self._tabsContents[index].has_child():
1284                         tab = self._TAB_CLASS[index](self._app, self._session, self._errorLog)
1285                         self._tabsContents[index].set_child(tab)
1286                         self._tabsContents[index].refresh(force=False)
1287
1288         @QtCore.pyqtSlot(str)
1289         @misc_utils.log_exception(_moduleLogger)
1290         def _on_session_error(self, message):
1291                 self._errorLog.push_message(message)
1292
1293         @QtCore.pyqtSlot()
1294         @misc_utils.log_exception(_moduleLogger)
1295         def _on_login(self):
1296                 for tab in self._tabsContents:
1297                         tab.enable()
1298
1299         @QtCore.pyqtSlot()
1300         @misc_utils.log_exception(_moduleLogger)
1301         def _on_logout(self):
1302                 for tab in self._tabsContents:
1303                         tab.disable()
1304
1305         @QtCore.pyqtSlot()
1306         @misc_utils.log_exception(_moduleLogger)
1307         def _on_recipients_changed(self):
1308                 if self._session.draft.get_num_contacts() == 0:
1309                         return
1310
1311                 if self._smsEntryDialog is None:
1312                         self._smsEntryDialog = SMSEntryWindow(self.window, self._app, self._session, self._errorLog)
1313                 pass
1314
1315         @QtCore.pyqtSlot()
1316         @QtCore.pyqtSlot(bool)
1317         @misc_utils.log_exception(_moduleLogger)
1318         def _on_login_requested(self, checked = True):
1319                 if self._credentialsDialog is None:
1320                         self._credentialsDialog = CredentialsDialog()
1321                 username, password = self._credentialsDialog.run("", "", self.window)
1322                 self._session.login(username, password)
1323
1324         @QtCore.pyqtSlot(int)
1325         @misc_utils.log_exception(_moduleLogger)
1326         def _on_tab_changed(self, index):
1327                 self._initialize_tab(index)
1328
1329         @QtCore.pyqtSlot()
1330         @QtCore.pyqtSlot(bool)
1331         @misc_utils.log_exception(_moduleLogger)
1332         def _on_refresh(self, checked = True):
1333                 index = self._tabWidget.currentIndex()
1334                 self._tabsContents[index].refresh(force=True)
1335
1336         @QtCore.pyqtSlot()
1337         @QtCore.pyqtSlot(bool)
1338         @misc_utils.log_exception(_moduleLogger)
1339         def _on_import(self, checked = True):
1340                 csvName = QtGui.QFileDialog.getOpenFileName(self._window, caption="Import", filter="CSV Files (*.csv)")
1341                 if not csvName:
1342                         return
1343                 shutil.copy2(csvName, self._app.fsContactsPath)
1344                 self._tabsContents[self.CONTACTS_TAB].update_addressbooks()
1345
1346         @QtCore.pyqtSlot()
1347         @QtCore.pyqtSlot(bool)
1348         @misc_utils.log_exception(_moduleLogger)
1349         def _on_close_window(self, checked = True):
1350                 self.close()
1351
1352
1353 def make_ugly(prettynumber):
1354         """
1355         function to take a phone number and strip out all non-numeric
1356         characters
1357
1358         >>> make_ugly("+012-(345)-678-90")
1359         '+01234567890'
1360         """
1361         return normalize_number(prettynumber)
1362
1363
1364 def normalize_number(prettynumber):
1365         """
1366         function to take a phone number and strip out all non-numeric
1367         characters
1368
1369         >>> normalize_number("+012-(345)-678-90")
1370         '+01234567890'
1371         >>> normalize_number("1-(345)-678-9000")
1372         '+13456789000'
1373         >>> normalize_number("+1-(345)-678-9000")
1374         '+13456789000'
1375         """
1376         uglynumber = re.sub('[^0-9+]', '', prettynumber)
1377
1378         if uglynumber.startswith("+"):
1379                 pass
1380         elif uglynumber.startswith("1"):
1381                 uglynumber = "+"+uglynumber
1382         elif 10 <= len(uglynumber):
1383                 assert uglynumber[0] not in ("+", "1")
1384                 uglynumber = "+1"+uglynumber
1385         else:
1386                 pass
1387
1388         return uglynumber
1389
1390
1391 def _make_pretty_with_areacode(phonenumber):
1392         prettynumber = "(%s)" % (phonenumber[0:3], )
1393         if 3 < len(phonenumber):
1394                 prettynumber += " %s" % (phonenumber[3:6], )
1395                 if 6 < len(phonenumber):
1396                         prettynumber += "-%s" % (phonenumber[6:], )
1397         return prettynumber
1398
1399
1400 def _make_pretty_local(phonenumber):
1401         prettynumber = "%s" % (phonenumber[0:3], )
1402         if 3 < len(phonenumber):
1403                 prettynumber += "-%s" % (phonenumber[3:], )
1404         return prettynumber
1405
1406
1407 def _make_pretty_international(phonenumber):
1408         prettynumber = phonenumber
1409         if phonenumber.startswith("1"):
1410                 prettynumber = "1 "
1411                 prettynumber += _make_pretty_with_areacode(phonenumber[1:])
1412         return prettynumber
1413
1414
1415 def make_pretty(phonenumber):
1416         """
1417         Function to take a phone number and return the pretty version
1418         pretty numbers:
1419                 if phonenumber begins with 0:
1420                         ...-(...)-...-....
1421                 if phonenumber begins with 1: ( for gizmo callback numbers )
1422                         1 (...)-...-....
1423                 if phonenumber is 13 digits:
1424                         (...)-...-....
1425                 if phonenumber is 10 digits:
1426                         ...-....
1427         >>> make_pretty("12")
1428         '12'
1429         >>> make_pretty("1234567")
1430         '123-4567'
1431         >>> make_pretty("2345678901")
1432         '+1 (234) 567-8901'
1433         >>> make_pretty("12345678901")
1434         '+1 (234) 567-8901'
1435         >>> make_pretty("01234567890")
1436         '+012 (345) 678-90'
1437         >>> make_pretty("+01234567890")
1438         '+012 (345) 678-90'
1439         >>> make_pretty("+12")
1440         '+1 (2)'
1441         >>> make_pretty("+123")
1442         '+1 (23)'
1443         >>> make_pretty("+1234")
1444         '+1 (234)'
1445         """
1446         if phonenumber is None or phonenumber is "":
1447                 return ""
1448
1449         phonenumber = normalize_number(phonenumber)
1450
1451         if phonenumber[0] == "+":
1452                 prettynumber = _make_pretty_international(phonenumber[1:])
1453                 if not prettynumber.startswith("+"):
1454                         prettynumber = "+"+prettynumber
1455         elif 8 < len(phonenumber) and phonenumber[0] in ("1", ):
1456                 prettynumber = _make_pretty_international(phonenumber)
1457         elif 7 < len(phonenumber):
1458                 prettynumber = _make_pretty_with_areacode(phonenumber)
1459         elif 3 < len(phonenumber):
1460                 prettynumber = _make_pretty_local(phonenumber)
1461         else:
1462                 prettynumber = phonenumber
1463         return prettynumber.strip()
1464
1465
1466 def abbrev_relative_date(date):
1467         """
1468         >>> abbrev_relative_date("42 hours ago")
1469         '42 h'
1470         >>> abbrev_relative_date("2 days ago")
1471         '2 d'
1472         >>> abbrev_relative_date("4 weeks ago")
1473         '4 w'
1474         """
1475         parts = date.split(" ")
1476         return "%s %s" % (parts[0], parts[1][0])
1477
1478
1479 def run():
1480         app = QtGui.QApplication([])
1481         handle = Dialcentral(app)
1482         qtpie.init_pies()
1483         return app.exec_()
1484
1485
1486 if __name__ == "__main__":
1487         logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
1488         logging.basicConfig(level=logging.DEBUG, format=logFormat)
1489         try:
1490                 os.makedirs(constants._data_path_)
1491         except OSError, e:
1492                 if e.errno != 17:
1493                         raise
1494
1495         val = run()
1496         sys.exit(val)