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