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