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