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