4 from __future__ import with_statement
14 from PyQt4 import QtGui
15 from PyQt4 import QtCore
18 from util import qui_utils
19 from util import qtpie
20 from util import misc as misc_utils
22 import backends.null_backend as null_backend
23 import backends.file_backend as file_backend
27 _moduleLogger = logging.getLogger(__name__)
33 class Dialcentral(object):
36 os.path.dirname(__file__),
37 os.path.join(os.path.dirname(__file__), "../data"),
38 os.path.join(os.path.dirname(__file__), "../lib"),
39 '/usr/share/%s' % constants.__app_name__,
40 '/usr/lib/%s' % constants.__app_name__,
43 def __init__(self, app):
46 self._hiddenCategories = set()
47 self._hiddenUnits = {}
48 self._clipboard = QtGui.QApplication.clipboard()
50 self._mainWindow = None
52 self._fullscreenAction = QtGui.QAction(None)
53 self._fullscreenAction.setText("Fullscreen")
54 self._fullscreenAction.setCheckable(True)
55 self._fullscreenAction.setShortcut(QtGui.QKeySequence("CTRL+Enter"))
56 self._fullscreenAction.toggled.connect(self._on_toggle_fullscreen)
58 self._logAction = QtGui.QAction(None)
59 self._logAction.setText("Log")
60 self._logAction.setShortcut(QtGui.QKeySequence("CTRL+l"))
61 self._logAction.triggered.connect(self._on_log)
63 self._quitAction = QtGui.QAction(None)
64 self._quitAction.setText("Quit")
65 self._quitAction.setShortcut(QtGui.QKeySequence("CTRL+q"))
66 self._quitAction.triggered.connect(self._on_quit)
68 self._app.lastWindowClosed.connect(self._on_app_quit)
71 self._mainWindow = MainWindow(None, self)
72 self._mainWindow.window.destroyed.connect(self._on_child_close)
74 def load_settings(self):
76 with open(constants._user_settings_, "r") as settingsFile:
77 settings = simplejson.load(settingsFile)
79 _moduleLogger.info("No settings")
82 _moduleLogger.info("Settings were corrupt")
85 self._fullscreenAction.setChecked(settings.get("isFullScreen", False))
87 def save_settings(self):
89 "isFullScreen": self._fullscreenAction.isChecked(),
91 with open(constants._user_settings_, "w") as settingsFile:
92 simplejson.dump(settings, settingsFile)
93 self._mainWindow.destroy()
96 def fsContactsPath(self):
97 return os.path.join(constants._data_path_, "contacts")
100 def fullscreenAction(self):
101 return self._fullscreenAction
105 return self._logAction
108 def quitAction(self):
109 return self._quitAction
111 def _close_windows(self):
112 if self._mainWindow is not None:
113 self._mainWindow.window.destroyed.disconnect(self._on_child_close)
114 self._mainWindow.close()
115 self._mainWindow = None
118 @QtCore.pyqtSlot(bool)
119 @misc_utils.log_exception(_moduleLogger)
120 def _on_app_quit(self, checked = False):
123 @QtCore.pyqtSlot(QtCore.QObject)
124 @misc_utils.log_exception(_moduleLogger)
125 def _on_child_close(self, obj = None):
126 self._mainWindow = None
129 @QtCore.pyqtSlot(bool)
130 @misc_utils.log_exception(_moduleLogger)
131 def _on_toggle_fullscreen(self, checked = False):
132 for window in self._walk_children():
133 window.set_fullscreen(checked)
136 @QtCore.pyqtSlot(bool)
137 @misc_utils.log_exception(_moduleLogger)
138 def _on_log(self, checked = False):
139 with open(constants._user_logpath_, "r") as f:
140 logLines = f.xreadlines()
141 log = "".join(logLines)
142 self._clipboard.setText(log)
145 @QtCore.pyqtSlot(bool)
146 @misc_utils.log_exception(_moduleLogger)
147 def _on_quit(self, checked = False):
148 self._close_windows()
151 class CredentialsDialog(object):
154 self._usernameField = QtGui.QLineEdit()
155 self._passwordField = QtGui.QLineEdit()
156 self._passwordField.setEchoMode(QtGui.QLineEdit.PasswordEchoOnEdit)
158 self._credLayout = QtGui.QGridLayout()
159 self._credLayout.addWidget(QtGui.QLabel("Username"), 0, 0)
160 self._credLayout.addWidget(self._usernameField, 0, 1)
161 self._credLayout.addWidget(QtGui.QLabel("Password"), 1, 0)
162 self._credLayout.addWidget(self._passwordField, 1, 1)
164 self._loginButton = QtGui.QPushButton("&Login")
165 self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
166 self._buttonLayout.addButton(self._loginButton, QtGui.QDialogButtonBox.AcceptRole)
168 self._layout = QtGui.QVBoxLayout()
169 self._layout.addLayout(self._credLayout)
170 self._layout.addWidget(self._buttonLayout)
172 self._dialog = QtGui.QDialog()
173 self._dialog.setWindowTitle("Login")
174 self._dialog.setLayout(self._layout)
175 self._dialog.setAttribute(QtCore.Qt.WA_DeleteOnClose, False)
176 qui_utils.set_autorient(self._dialog, True)
177 self._buttonLayout.accepted.connect(self._dialog.accept)
178 self._buttonLayout.rejected.connect(self._dialog.reject)
180 def run(self, defaultUsername, defaultPassword, parent=None):
181 self._dialog.setParent(parent, QtCore.Qt.Dialog)
183 self._usernameField.setText(defaultUsername)
184 self._passwordField.setText(defaultPassword)
186 response = self._dialog.exec_()
187 if response == QtGui.QDialog.Accepted:
188 return str(self._usernameField.text()), str(self._passwordField.text())
189 elif response == QtGui.QDialog.Rejected:
190 raise RuntimeError("Login Cancelled")
192 self._dialog.setParent(None, QtCore.Qt.Dialog)
195 class AccountDialog(object):
198 self._accountNumberLabel = QtGui.QLabel("NUMBER NOT SET")
199 self._clearButton = QtGui.QPushButton("Clear Account")
200 self._clearButton.clicked.connect(self._on_clear)
201 self._doClear = False
203 self._credLayout = QtGui.QGridLayout()
204 self._credLayout.addWidget(QtGui.QLabel("Account"), 0, 0)
205 self._credLayout.addWidget(self._accountNumberLabel, 0, 1)
206 self._credLayout.addWidget(QtGui.QLabel("Callback"), 1, 0)
207 self._credLayout.addWidget(self._clearButton, 2, 1)
209 self._loginButton = QtGui.QPushButton("&Login")
210 self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
211 self._buttonLayout.addButton(self._loginButton, QtGui.QDialogButtonBox.AcceptRole)
213 self._layout = QtGui.QVBoxLayout()
214 self._layout.addLayout(self._credLayout)
215 self._layout.addLayout(self._buttonLayout)
217 self._dialog = QtGui.QDialog()
218 self._dialog.setWindowTitle("Login")
219 self._dialog.setLayout(self._layout)
220 qui_utils.set_autorient(self._dialog, True)
221 self._buttonLayout.accepted.connect(self._dialog.accept)
222 self._buttonLayout.rejected.connect(self._dialog.reject)
228 accountNumber = property(
229 lambda self: str(self._accountNumberLabel.text()),
230 lambda self, num: self._accountNumberLabel.setText(num),
233 def run(self, defaultUsername, defaultPassword, parent=None):
234 self._doClear = False
235 self._dialog.setParent(parent)
236 self._usernameField.setText(defaultUsername)
237 self._passwordField.setText(defaultPassword)
239 response = self._dialog.exec_()
240 if response == QtGui.QDialog.Accepted:
241 return str(self._usernameField.text()), str(self._passwordField.text())
242 elif response == QtGui.QDialog.Rejected:
243 raise RuntimeError("Login Cancelled")
246 @QtCore.pyqtSlot(bool)
247 def _on_clear(self, checked = False):
249 self._dialog.accept()
252 class SMSEntryWindow(object):
254 def __init__(self, parent, app, session, errorLog):
257 self._session = session
258 self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
259 self._session.draft.called.connect(self._on_op_finished)
260 self._session.draft.sentMessage.connect(self._on_op_finished)
261 self._session.draft.cancelled.connect(self._on_op_finished)
262 self._errorLog = errorLog
264 self._targetLayout = QtGui.QVBoxLayout()
265 self._targetList = QtGui.QWidget()
266 self._targetList.setLayout(self._targetLayout)
267 self._history = QtGui.QTextEdit()
268 self._smsEntry = QtGui.QTextEdit()
269 self._smsEntry.textChanged.connect(self._on_letter_count_changed)
271 self._entryLayout = QtGui.QVBoxLayout()
272 self._entryLayout.addWidget(self._targetList)
273 self._entryLayout.addWidget(self._history)
274 self._entryLayout.addWidget(self._smsEntry)
275 self._entryWidget = QtGui.QWidget()
276 self._entryWidget.setLayout(self._entryLayout)
277 self._scrollEntry = QtGui.QScrollArea()
278 self._scrollEntry.setWidget(self._entryWidget)
279 self._scrollEntry.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignBottom)
280 self._scrollEntry.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
281 self._scrollEntry.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
283 self._characterCountLabel = QtGui.QLabel("Letters: %s" % 0)
284 self._singleNumberSelector = QtGui.QComboBox()
285 self._smsButton = QtGui.QPushButton("SMS")
286 self._smsButton.clicked.connect(self._on_sms_clicked)
287 self._dialButton = QtGui.QPushButton("Dial")
288 self._dialButton.clicked.connect(self._on_call_clicked)
290 self._buttonLayout = QtGui.QHBoxLayout()
291 self._buttonLayout.addWidget(self._characterCountLabel)
292 self._buttonLayout.addWidget(self._singleNumberSelector)
293 self._buttonLayout.addWidget(self._smsButton)
294 self._buttonLayout.addWidget(self._dialButton)
296 self._layout = QtGui.QVBoxLayout()
297 self._layout.addWidget(self._scrollEntry)
298 self._layout.addLayout(self._buttonLayout)
300 centralWidget = QtGui.QWidget()
301 centralWidget.setLayout(self._layout)
303 self._window = QtGui.QMainWindow(parent)
304 qui_utils.set_autorient(self._window, True)
305 qui_utils.set_stackable(self._window, True)
306 self._window.setWindowTitle("Contact")
307 self._window.setCentralWidget(centralWidget)
309 self._update_recipients()
311 def _update_letter_count(self):
312 count = self._smsEntry.toPlainText().size()
313 self._characterCountLabel.setText("Letters: %s" % count)
315 def _update_button_state(self):
316 if len(self._contacts) == 0:
317 self._dialButton.setEnabled(False)
318 self._smsButton.setEnabled(False)
319 elif len(self._contacts) == 1:
320 count = self._smsEntry.toPlainText().size()
322 self._dialButton.setEnabled(True)
323 self._smsButton.setEnabled(False)
325 self._dialButton.setEnabled(False)
326 self._smsButton.setEnabled(True)
328 self._dialButton.setEnabled(False)
329 self._smsButton.setEnabled(True)
331 def _update_recipients(self):
332 draftContactsCount = self._session.draft.get_num_contacts()
333 if draftContactsCount == 0:
335 elif draftContactsCount == 1:
336 (cid, ) = self._session.draft.get_contacts()
337 title = self._session.draft.get_title(cid)
338 description = self._session.draft.get_description(cid)
339 numbers = self._session.draft.get_numbers(cid)
341 self._targetList.setVisible(False)
343 self._history.setHtml(description)
344 self._history.setVisible(True)
346 self._history.setHtml("")
347 self._history.setVisible(False)
348 self._populate_number_selector(self._singleNumberSelector, cid, numbers)
350 self._scroll_to_bottom()
351 self._window.setWindowTitle(title)
354 self._targetList.setVisible(True)
355 while self._targetLayout.count():
356 removedLayoutItem = self._targetLayout.takeAt(self._targetLayout.count()-1)
357 removedWidget = removedLayoutItem.widget()
358 removedWidget.close()
359 for cid in self._session.draft.get_contacts():
360 title = self._session.draft.get_title(cid)
361 description = self._session.draft.get_description(cid)
362 numbers = self._session.draft.get_numbers(cid)
364 titleLabel = QtGui.QLabel(title)
365 numberSelector = QtGui.QComboBox()
366 self._populate_number_selector(numberSelector, cid, numbers)
367 deleteButton = QtGui.QPushButton("Delete")
368 callback = functools.partial(
369 self._on_remove_contact,
372 callback.__name__ = "b"
373 deleteButton.clicked.connect(
374 QtCore.pyqtSlot()(callback)
377 rowLayout = QtGui.QHBoxLayout()
378 rowLayout.addWidget(titleLabel)
379 rowLayout.addWidget(numberSelector)
380 rowLayout.addWidget(deleteButton)
381 rowWidget = QtGui.QWidget()
382 rowWidget.setLayout(rowLayout)
383 self._targetLayout.addWidget(rowWidget)
384 self._history.setHtml("")
385 self._history.setVisible(False)
386 self._singleNumberSelector.setVisible(False)
388 self._scroll_to_bottom()
389 self._window.setWindowTitle("Contacts")
392 def _populate_number_selector(self, selector, cid, numbers):
393 while 0 < selector.count():
394 selector.removeItem(0)
395 for number, description in numbers:
397 label = "%s - %s" % (number, description)
400 selector.addItem(label)
401 selector.setVisible(True)
403 selector.setEnabled(True)
405 selector.setEnabled(False)
406 callback = functools.partial(
407 self._on_change_number,
410 callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
411 selector.currentIndexChanged.connect(
412 QtCore.pyqtSlot(int)(callback)
415 def _scroll_to_bottom(self):
416 self._scrollEntry.ensureWidgetVisible(self._smsEntry)
419 @misc_utils.log_exception(_moduleLogger)
420 def _on_sms_clicked(self):
421 message = str(self._smsEntry.toPlainText())
422 self._session.draft.send(message)
423 self._smsEntry.setPlainText("")
426 @misc_utils.log_exception(_moduleLogger)
427 def _on_call_clicked(self):
428 self._session.draft.call()
429 self._smsEntry.setPlainText("")
431 @misc_utils.log_exception(_moduleLogger)
432 def _on_remove_contact(self, cid):
433 self._session.draft.remove_contact(cid)
435 @misc_utils.log_exception(_moduleLogger)
436 def _on_change_number(self, cid, index):
437 numbers = self._session.draft.get_numbers(cid)
438 number = numbers[index][0]
439 self._session.draft.set_selected_number(cid, number)
442 @misc_utils.log_exception(_moduleLogger)
443 def _on_recipients_changed(self):
444 self._update_recipients()
447 @misc_utils.log_exception(_moduleLogger)
448 def _on_op_finished(self):
452 @misc_utils.log_exception(_moduleLogger)
453 def _on_letter_count_changed(self):
454 self._update_letter_count()
455 self._update_button_state()
458 class DelayedWidget(object):
460 def __init__(self, app):
461 self._layout = QtGui.QVBoxLayout()
462 self._widget = QtGui.QWidget()
463 self._widget.setLayout(self._layout)
466 self._isEnabled = True
473 return self._child is not None
475 def set_child(self, child):
476 if self._child is not None:
477 self._layout.removeWidget(self._child.toplevel)
479 if self._child is not None:
480 self._layout.addWidget(self._child.toplevel)
485 self._child.disable()
488 self._isEnabled = True
489 if self._child is not None:
493 self._isEnabled = False
494 if self._child is not None:
495 self._child.disable()
498 if self._child is not None:
501 def refresh(self, force=True):
502 if self._child is not None:
503 self._child.refresh(force)
506 class Dialpad(object):
508 def __init__(self, app, session, errorLog):
510 self._session = session
511 self._errorLog = errorLog
513 self._plus = self._generate_key_button("+", "")
514 self._entry = QtGui.QLineEdit()
516 backAction = QtGui.QAction(None)
517 backAction.setText("Back")
518 backAction.triggered.connect(self._on_backspace)
519 backPieItem = qtpie.QActionPieItem(backAction)
520 clearAction = QtGui.QAction(None)
521 clearAction.setText("Clear")
522 clearAction.triggered.connect(self._on_clear_text)
523 clearPieItem = qtpie.QActionPieItem(clearAction)
524 self._back = qtpie.QPieButton(backPieItem)
525 self._back.set_center(backPieItem)
526 self._back.insertItem(qtpie.PieFiling.NULL_CENTER)
527 self._back.insertItem(clearPieItem)
528 self._back.insertItem(qtpie.PieFiling.NULL_CENTER)
529 self._back.insertItem(qtpie.PieFiling.NULL_CENTER)
531 self._entryLayout = QtGui.QHBoxLayout()
532 self._entryLayout.addWidget(self._plus, 0, QtCore.Qt.AlignCenter)
533 self._entryLayout.addWidget(self._entry, 10)
534 self._entryLayout.addWidget(self._back, 0, QtCore.Qt.AlignCenter)
536 self._smsButton = QtGui.QPushButton("SMS")
537 self._smsButton.clicked.connect(self._on_sms_clicked)
538 self._callButton = QtGui.QPushButton("Call")
539 self._callButton.clicked.connect(self._on_call_clicked)
541 self._padLayout = QtGui.QGridLayout()
542 rows = [0, 0, 0, 1, 1, 1, 2, 2, 2]
543 columns = [0, 1, 2] * 3
555 for (num, letters), (row, column) in zip(keys, zip(rows, columns)):
556 self._padLayout.addWidget(
557 self._generate_key_button(num, letters), row, column, QtCore.Qt.AlignCenter
559 self._padLayout.addWidget(self._smsButton, 3, 0)
560 self._padLayout.addWidget(
561 self._generate_key_button("0", ""), 3, 1, QtCore.Qt.AlignCenter
563 self._padLayout.addWidget(self._callButton, 3, 2)
565 self._layout = QtGui.QVBoxLayout()
566 self._layout.addLayout(self._entryLayout)
567 self._layout.addLayout(self._padLayout)
568 self._widget = QtGui.QWidget()
569 self._widget.setLayout(self._layout)
576 self._smsButton.setEnabled(True)
577 self._callButton.setEnabled(True)
580 self._smsButton.setEnabled(False)
581 self._callButton.setEnabled(False)
586 def refresh(self, force = True):
589 def _generate_key_button(self, center, letters):
590 centerPieItem = self._generate_button_slice(center)
591 button = qtpie.QPieButton(centerPieItem)
592 button.set_center(centerPieItem)
594 if len(letters) == 0:
596 pieItem = qtpie.PieFiling.NULL_CENTER
597 button.insertItem(pieItem)
598 elif len(letters) in [3, 4]:
599 for i in xrange(6 - len(letters)):
600 pieItem = qtpie.PieFiling.NULL_CENTER
601 button.insertItem(pieItem)
603 for letter in letters:
604 pieItem = self._generate_button_slice(letter)
605 button.insertItem(pieItem)
608 pieItem = qtpie.PieFiling.NULL_CENTER
609 button.insertItem(pieItem)
611 raise NotImplementedError("Cannot handle %r" % letters)
614 def _generate_button_slice(self, letter):
615 action = QtGui.QAction(None)
616 action.setText(letter)
617 action.triggered.connect(lambda: self._on_keypress(letter))
618 pieItem = qtpie.QActionPieItem(action)
621 @misc_utils.log_exception(_moduleLogger)
622 def _on_keypress(self, key):
623 self._entry.insert(key)
625 @misc_utils.log_exception(_moduleLogger)
626 def _on_backspace(self, toggled = False):
627 self._entry.backspace()
629 @misc_utils.log_exception(_moduleLogger)
630 def _on_clear_text(self, toggled = False):
634 @QtCore.pyqtSlot(bool)
635 @misc_utils.log_exception(_moduleLogger)
636 def _on_sms_clicked(self, checked = False):
637 number = str(self._entry.text())
643 numbersWithDescriptions = [(number, "")]
644 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
647 @QtCore.pyqtSlot(bool)
648 @misc_utils.log_exception(_moduleLogger)
649 def _on_call_clicked(self, checked = False):
650 number = str(self._entry.text())
656 numbersWithDescriptions = [(number, "")]
657 self._session.draft.clear()
658 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
659 self._session.draft.call()
662 class History(object):
670 HISTORY_ITEM_TYPES = ["Received", "Missed", "Placed", "All"]
671 HISTORY_COLUMNS = ["When", "What", "Number", "From"]
672 assert len(HISTORY_COLUMNS) == MAX_IDX
674 def __init__(self, app, session, errorLog):
675 self._selectedFilter = self.HISTORY_ITEM_TYPES[-1]
677 self._session = session
678 self._session.historyUpdated.connect(self._on_history_updated)
679 self._errorLog = errorLog
681 self._typeSelection = QtGui.QComboBox()
682 self._typeSelection.addItems(self.HISTORY_ITEM_TYPES)
683 self._typeSelection.setCurrentIndex(
684 self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
686 self._typeSelection.currentIndexChanged[str].connect(self._on_filter_changed)
688 self._itemStore = QtGui.QStandardItemModel()
689 self._itemStore.setHorizontalHeaderLabels(self.HISTORY_COLUMNS)
691 self._itemView = QtGui.QTreeView()
692 self._itemView.setModel(self._itemStore)
693 self._itemView.setUniformRowHeights(True)
694 self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
695 self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
696 self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
697 self._itemView.setHeaderHidden(True)
698 self._itemView.activated.connect(self._on_row_activated)
700 self._layout = QtGui.QVBoxLayout()
701 self._layout.addWidget(self._typeSelection)
702 self._layout.addWidget(self._itemView)
703 self._widget = QtGui.QWidget()
704 self._widget.setLayout(self._layout)
706 self._populate_items()
713 self._itemView.setEnabled(True)
716 self._itemView.setEnabled(False)
719 self._itemView.clear()
721 def refresh(self, force=True):
722 self._session.update_history(force)
724 def _populate_items(self):
725 self._itemStore.clear()
726 history = self._session.get_history()
727 history.sort(key=lambda item: item["time"], reverse=True)
728 for event in history:
729 if self._selectedFilter in [self.HISTORY_ITEM_TYPES[-1], event["action"]]:
730 relTime = abbrev_relative_date(event["relTime"])
731 action = event["action"]
732 number = event["number"]
733 prettyNumber = make_pretty(number)
735 if not name or name == number:
736 name = event["location"]
740 timeItem = QtGui.QStandardItem(relTime)
741 actionItem = QtGui.QStandardItem(action)
742 numberItem = QtGui.QStandardItem(prettyNumber)
743 nameItem = QtGui.QStandardItem(name)
744 row = timeItem, actionItem, numberItem, nameItem
746 item.setEditable(False)
747 item.setCheckable(False)
748 if item is not nameItem:
749 itemFont = item.font()
750 itemFont.setPointSize(max(itemFont.pointSize() - 3, 5))
751 item.setFont(itemFont)
752 numberItem.setData(event)
753 self._itemStore.appendRow(row)
755 @QtCore.pyqtSlot(str)
756 @misc_utils.log_exception(_moduleLogger)
757 def _on_filter_changed(self, newItem):
758 self._selectedFilter = str(newItem)
759 self._populate_items()
762 @misc_utils.log_exception(_moduleLogger)
763 def _on_history_updated(self):
764 self._populate_items()
766 @QtCore.pyqtSlot(QtCore.QModelIndex)
767 @misc_utils.log_exception(_moduleLogger)
768 def _on_row_activated(self, index):
769 rowIndex = index.row()
770 item = self._itemStore.item(rowIndex, self.NUMBER_IDX)
771 contactDetails = item.data().toPyObject()
773 title = str(self._itemStore.item(rowIndex, self.FROM_IDX).text())
774 number = str(contactDetails[QtCore.QString("number")])
775 contactId = number # ids don't seem too unique so using numbers
778 # @bug doesn't seem to print multiple entries
779 for i in xrange(self._itemStore.rowCount()):
780 iItem = self._itemStore.item(i, self.NUMBER_IDX)
781 iContactDetails = iItem.data().toPyObject()
782 iNumber = str(iContactDetails[QtCore.QString("number")])
783 if number != iNumber:
785 relTime = abbrev_relative_date(iContactDetails[QtCore.QString("relTime")])
786 action = str(iContactDetails[QtCore.QString("action")])
787 number = str(iContactDetails[QtCore.QString("number")])
788 prettyNumber = make_pretty(number)
789 rowItems = relTime, action, prettyNumber
790 descriptionRows.append("<tr><td>%s</td></tr>" % "</td><td>".join(rowItems))
791 description = "<table>%s</table>" % "".join(descriptionRows)
792 numbersWithDescriptions = [(str(contactDetails[QtCore.QString("number")]), "")]
793 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
796 class Messages(object):
799 VOICEMAIL_MESSAGES = "Voicemail"
800 TEXT_MESSAGES = "SMS"
801 ALL_TYPES = "All Messages"
802 MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
804 UNREAD_STATUS = "Unread"
805 UNARCHIVED_STATUS = "Inbox"
807 MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
809 _MIN_MESSAGES_SHOWN = 4
811 def __init__(self, app, session, errorLog):
812 self._selectedTypeFilter = self.ALL_TYPES
813 self._selectedStatusFilter = self.ALL_STATUS
815 self._session = session
816 self._session.messagesUpdated.connect(self._on_messages_updated)
817 self._errorLog = errorLog
819 self._typeSelection = QtGui.QComboBox()
820 self._typeSelection.addItems(self.MESSAGE_TYPES)
821 self._typeSelection.setCurrentIndex(
822 self.MESSAGE_TYPES.index(self._selectedTypeFilter)
824 self._typeSelection.currentIndexChanged[str].connect(self._on_type_filter_changed)
826 self._statusSelection = QtGui.QComboBox()
827 self._statusSelection.addItems(self.MESSAGE_STATUSES)
828 self._statusSelection.setCurrentIndex(
829 self.MESSAGE_STATUSES.index(self._selectedStatusFilter)
831 self._statusSelection.currentIndexChanged[str].connect(self._on_status_filter_changed)
833 self._selectionLayout = QtGui.QHBoxLayout()
834 self._selectionLayout.addWidget(self._typeSelection)
835 self._selectionLayout.addWidget(self._statusSelection)
837 self._itemStore = QtGui.QStandardItemModel()
838 self._itemStore.setHorizontalHeaderLabels(["Messages"])
840 self._htmlDelegate = qui_utils.QHtmlDelegate()
841 self._itemView = QtGui.QTreeView()
842 self._itemView.setModel(self._itemStore)
843 self._itemView.setUniformRowHeights(False)
844 self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
845 self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
846 self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
847 self._itemView.setHeaderHidden(True)
848 self._itemView.setItemDelegate(self._htmlDelegate)
849 self._itemView.activated.connect(self._on_row_activated)
851 self._layout = QtGui.QVBoxLayout()
852 self._layout.addLayout(self._selectionLayout)
853 self._layout.addWidget(self._itemView)
854 self._widget = QtGui.QWidget()
855 self._widget.setLayout(self._layout)
857 self._populate_items()
864 self._itemView.setEnabled(True)
867 self._itemView.setEnabled(False)
870 self._itemView.clear()
872 def refresh(self, force=True):
873 self._session.update_messages(force)
875 def _populate_items(self):
876 self._itemStore.clear()
877 rawMessages = self._session.get_messages()
878 rawMessages.sort(key=lambda item: item["time"], reverse=True)
879 for item in rawMessages:
880 isUnarchived = not item["isArchived"]
881 isUnread = not item["isRead"]
883 self.UNREAD_STATUS: isUnarchived and isUnread,
884 self.UNARCHIVED_STATUS: isUnarchived,
885 self.ALL_STATUS: True,
886 }[self._selectedStatusFilter]
888 visibleType = self._selectedTypeFilter in [item["type"], self.ALL_TYPES]
889 if visibleType and visibleStatus:
890 relTime = abbrev_relative_date(item["relTime"])
891 number = item["number"]
892 prettyNumber = make_pretty(number)
894 if not name or name == number:
895 name = item["location"]
899 messageParts = list(item["messageParts"])
900 if len(messageParts) == 0:
901 messages = ("No Transcription", )
902 elif len(messageParts) == 1:
903 if messageParts[0][1]:
904 messages = (messageParts[0][1], )
906 messages = ("No Transcription", )
909 "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
910 for messagePart in messageParts
913 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (name, prettyNumber, relTime)
915 expandedMessages = [firstMessage]
916 expandedMessages.extend(messages)
917 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
918 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
919 collapsedMessages = [firstMessage, secondMessage]
920 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
922 collapsedMessages = expandedMessages
924 item = dict(item.iteritems())
925 item["collapsedMessages"] = "<br/>\n".join(collapsedMessages)
926 item["expandedMessages"] = "<br/>\n".join(expandedMessages)
928 messageItem = QtGui.QStandardItem(item["collapsedMessages"])
929 # @bug Not showing all of a message
930 messageItem.setData(item)
931 messageItem.setEditable(False)
932 messageItem.setCheckable(False)
933 row = (messageItem, )
934 self._itemStore.appendRow(row)
936 @QtCore.pyqtSlot(str)
937 @misc_utils.log_exception(_moduleLogger)
938 def _on_type_filter_changed(self, newItem):
939 self._selectedTypeFilter = str(newItem)
940 self._populate_items()
942 @QtCore.pyqtSlot(str)
943 @misc_utils.log_exception(_moduleLogger)
944 def _on_status_filter_changed(self, newItem):
945 self._selectedStatusFilter = str(newItem)
946 self._populate_items()
949 @misc_utils.log_exception(_moduleLogger)
950 def _on_messages_updated(self):
951 self._populate_items()
953 @QtCore.pyqtSlot(QtCore.QModelIndex)
954 @misc_utils.log_exception(_moduleLogger)
955 def _on_row_activated(self, index):
956 rowIndex = index.row()
957 item = self._itemStore.item(rowIndex, 0)
958 contactDetails = item.data().toPyObject()
960 name = str(contactDetails[QtCore.QString("name")])
961 number = str(contactDetails[QtCore.QString("number")])
962 if not name or name == number:
963 name = str(contactDetails[QtCore.QString("location")])
967 contactId = str(contactDetails[QtCore.QString("id")])
969 description = str(contactDetails[QtCore.QString("expandedMessages")])
970 numbersWithDescriptions = [(number, "")]
971 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
974 class Contacts(object):
976 def __init__(self, app, session, errorLog):
978 self._session = session
979 self._session.contactsUpdated.connect(self._on_contacts_updated)
980 self._errorLog = errorLog
981 self._addressBookFactories = [
982 null_backend.NullAddressBookFactory(),
983 file_backend.FilesystemAddressBookFactory(app.fsContactsPath),
985 self._addressBooks = []
987 self._listSelection = QtGui.QComboBox()
988 self._listSelection.addItems([])
989 self._listSelection.currentIndexChanged[str].connect(self._on_filter_changed)
991 self._itemStore = QtGui.QStandardItemModel()
992 self._itemStore.setHorizontalHeaderLabels(["Contacts"])
994 self._itemView = QtGui.QTreeView()
995 self._itemView.setModel(self._itemStore)
996 self._itemView.setUniformRowHeights(True)
997 self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
998 self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
999 self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
1000 self._itemView.setHeaderHidden(True)
1001 self._itemView.activated.connect(self._on_row_activated)
1003 self._layout = QtGui.QVBoxLayout()
1004 self._layout.addWidget(self._listSelection)
1005 self._layout.addWidget(self._itemView)
1006 self._widget = QtGui.QWidget()
1007 self._widget.setLayout(self._layout)
1009 self.update_addressbooks()
1010 self._populate_items()
1017 self._itemView.setEnabled(True)
1020 self._itemView.setEnabled(False)
1023 self._itemView.clear()
1025 def refresh(self, force=True):
1026 self._backend.update_contacts(force)
1030 return self._addressBooks[self._listSelection.currentIndex()]["book"]
1032 def update_addressbooks(self):
1033 self._addressBooks = [
1034 {"book": book, "name": book.name}
1035 for factory in self._addressBookFactories
1036 for book in factory.get_addressbooks()
1038 self._addressBooks.append(
1040 "book": self._session,
1041 "name": "Google Voice",
1045 currentItem = str(self._listSelection.currentText())
1046 if currentItem == "":
1048 currentItem = "None"
1049 while 0 < self._listSelection.count():
1050 self._listSelection.removeItem(0)
1051 bookNames = [book["name"] for book in self._addressBooks]
1053 newIndex = bookNames.index(currentItem)
1055 # Switch over to None for the user
1057 self._itemStore.clear()
1058 _moduleLogger.info("Addressbook %r doesn't exist anymore, switching to None" % currentItem)
1059 self._listSelection.addItems(bookNames)
1060 self._listSelection.setCurrentIndex(newIndex)
1062 def _populate_items(self):
1063 self._itemStore.clear()
1065 contacts = list(self._backend.get_contacts().itervalues())
1066 contacts.sort(key=lambda contact: contact["name"].lower())
1067 for item in contacts:
1069 numbers = item["numbers"]
1070 nameItem = QtGui.QStandardItem(name)
1071 nameItem.setEditable(False)
1072 nameItem.setCheckable(False)
1073 nameItem.setData(item)
1075 self._itemStore.appendRow(row)
1077 @QtCore.pyqtSlot(str)
1078 @misc_utils.log_exception(_moduleLogger)
1079 def _on_filter_changed(self, newItem):
1080 self._populate_items()
1083 @misc_utils.log_exception(_moduleLogger)
1084 def _on_contacts_updated(self):
1085 self._populate_items()
1087 @QtCore.pyqtSlot(QtCore.QModelIndex)
1088 @misc_utils.log_exception(_moduleLogger)
1089 def _on_row_activated(self, index):
1090 rowIndex = index.row()
1091 item = self._itemStore.item(rowIndex, 0)
1092 contactDetails = item.data().toPyObject()
1094 name = str(contactDetails[QtCore.QString("name")])
1096 name = str(contactDetails[QtCore.QString("location")])
1100 contactId = str(contactDetails[QtCore.QString("contactId")])
1101 numbers = contactDetails[QtCore.QString("numbers")]
1105 for (k, v) in number.iteritems()
1107 for number in numbers
1109 numbersWithDescriptions = [
1111 number["phoneNumber"],
1112 self._choose_phonetype(number),
1114 for number in numbers
1118 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
1121 def _choose_phonetype(numberDetails):
1122 if "phoneTypeName" in numberDetails:
1123 return numberDetails["phoneTypeName"]
1124 elif "phoneType" in numberDetails:
1125 return numberDetails["phoneType"]
1130 class MainWindow(object):
1144 assert len(_TAB_TITLES) == MAX_TABS
1152 assert len(_TAB_CLASS) == MAX_TABS
1154 def __init__(self, parent, app):
1156 self._session = session.Session(constants._data_path_)
1157 self._session.error.connect(self._on_session_error)
1158 self._session.loggedIn.connect(self._on_login)
1159 self._session.loggedOut.connect(self._on_logout)
1160 self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
1162 self._credentialsDialog = None
1163 self._smsEntryDialog = None
1165 self._errorLog = qui_utils.QErrorLog()
1166 self._errorDisplay = qui_utils.ErrorDisplay(self._errorLog)
1168 self._tabsContents = [
1169 DelayedWidget(self._app)
1170 for i in xrange(self.MAX_TABS)
1172 for tab in self._tabsContents:
1175 self._tabWidget = QtGui.QTabWidget()
1176 if qui_utils.screen_orientation() == QtCore.Qt.Vertical:
1177 self._tabWidget.setTabPosition(QtGui.QTabWidget.South)
1179 self._tabWidget.setTabPosition(QtGui.QTabWidget.West)
1180 for tabIndex, tabTitle in enumerate(self._TAB_TITLES):
1181 self._tabWidget.addTab(self._tabsContents[tabIndex].toplevel, tabTitle)
1182 self._tabWidget.currentChanged.connect(self._on_tab_changed)
1184 self._layout = QtGui.QVBoxLayout()
1185 self._layout.addWidget(self._errorDisplay.toplevel)
1186 self._layout.addWidget(self._tabWidget)
1188 centralWidget = QtGui.QWidget()
1189 centralWidget.setLayout(self._layout)
1191 self._window = QtGui.QMainWindow(parent)
1192 self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
1193 qui_utils.set_autorient(self._window, True)
1194 qui_utils.set_stackable(self._window, True)
1195 self._window.setWindowTitle("%s" % constants.__pretty_app_name__)
1196 self._window.setCentralWidget(centralWidget)
1198 self._loginTabAction = QtGui.QAction(None)
1199 self._loginTabAction.setText("Login")
1200 self._loginTabAction.triggered.connect(self._on_login_requested)
1202 self._importTabAction = QtGui.QAction(None)
1203 self._importTabAction.setText("Import")
1204 self._importTabAction.triggered.connect(self._on_import)
1206 self._refreshTabAction = QtGui.QAction(None)
1207 self._refreshTabAction.setText("Refresh")
1208 self._refreshTabAction.setShortcut(QtGui.QKeySequence("CTRL+r"))
1209 self._refreshTabAction.triggered.connect(self._on_refresh)
1211 self._closeWindowAction = QtGui.QAction(None)
1212 self._closeWindowAction.setText("Close")
1213 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
1214 self._closeWindowAction.triggered.connect(self._on_close_window)
1217 fileMenu = self._window.menuBar().addMenu("&File")
1218 fileMenu.addAction(self._loginTabAction)
1219 fileMenu.addAction(self._refreshTabAction)
1221 toolsMenu = self._window.menuBar().addMenu("&Tools")
1222 toolsMenu.addAction(self._importTabAction)
1224 self._window.addAction(self._closeWindowAction)
1225 self._window.addAction(self._app.quitAction)
1226 self._window.addAction(self._app.fullscreenAction)
1228 fileMenu = self._window.menuBar().addMenu("&File")
1229 fileMenu.addAction(self._loginTabAction)
1230 fileMenu.addAction(self._refreshTabAction)
1231 fileMenu.addAction(self._closeWindowAction)
1232 fileMenu.addAction(self._app.quitAction)
1234 viewMenu = self._window.menuBar().addMenu("&View")
1235 viewMenu.addAction(self._app.fullscreenAction)
1237 toolsMenu = self._window.menuBar().addMenu("&Tools")
1238 toolsMenu.addAction(self._importTabAction)
1240 self._window.addAction(self._app.logAction)
1242 self._initialize_tab(self._tabWidget.currentIndex())
1243 self.set_fullscreen(self._app.fullscreenAction.isChecked())
1250 def walk_children(self):
1255 for child in self.walk_children():
1259 for child in self.walk_children():
1264 for child in self.walk_children():
1265 child.window.destroyed.disconnect(self._on_child_close)
1267 self._window.close()
1270 if self._session.state != self._session.LOGGEDOUT_STATE:
1271 self._session.logout()
1273 def set_fullscreen(self, isFullscreen):
1275 self._window.showFullScreen()
1277 self._window.showNormal()
1278 for child in self.walk_children():
1279 child.set_fullscreen(isFullscreen)
1281 def _initialize_tab(self, index):
1282 assert index < self.MAX_TABS
1283 if not self._tabsContents[index].has_child():
1284 tab = self._TAB_CLASS[index](self._app, self._session, self._errorLog)
1285 self._tabsContents[index].set_child(tab)
1286 self._tabsContents[index].refresh(force=False)
1288 @QtCore.pyqtSlot(str)
1289 @misc_utils.log_exception(_moduleLogger)
1290 def _on_session_error(self, message):
1291 self._errorLog.push_message(message)
1294 @misc_utils.log_exception(_moduleLogger)
1295 def _on_login(self):
1296 for tab in self._tabsContents:
1300 @misc_utils.log_exception(_moduleLogger)
1301 def _on_logout(self):
1302 for tab in self._tabsContents:
1306 @misc_utils.log_exception(_moduleLogger)
1307 def _on_recipients_changed(self):
1308 if self._session.draft.get_num_contacts() == 0:
1311 if self._smsEntryDialog is None:
1312 self._smsEntryDialog = SMSEntryWindow(self.window, self._app, self._session, self._errorLog)
1316 @QtCore.pyqtSlot(bool)
1317 @misc_utils.log_exception(_moduleLogger)
1318 def _on_login_requested(self, checked = True):
1319 if self._credentialsDialog is None:
1320 self._credentialsDialog = CredentialsDialog()
1321 username, password = self._credentialsDialog.run("", "", self.window)
1322 self._session.login(username, password)
1324 @QtCore.pyqtSlot(int)
1325 @misc_utils.log_exception(_moduleLogger)
1326 def _on_tab_changed(self, index):
1327 self._initialize_tab(index)
1330 @QtCore.pyqtSlot(bool)
1331 @misc_utils.log_exception(_moduleLogger)
1332 def _on_refresh(self, checked = True):
1333 index = self._tabWidget.currentIndex()
1334 self._tabsContents[index].refresh(force=True)
1337 @QtCore.pyqtSlot(bool)
1338 @misc_utils.log_exception(_moduleLogger)
1339 def _on_import(self, checked = True):
1340 csvName = QtGui.QFileDialog.getOpenFileName(self._window, caption="Import", filter="CSV Files (*.csv)")
1343 shutil.copy2(csvName, self._app.fsContactsPath)
1344 self._tabsContents[self.CONTACTS_TAB].update_addressbooks()
1347 @QtCore.pyqtSlot(bool)
1348 @misc_utils.log_exception(_moduleLogger)
1349 def _on_close_window(self, checked = True):
1353 def make_ugly(prettynumber):
1355 function to take a phone number and strip out all non-numeric
1358 >>> make_ugly("+012-(345)-678-90")
1361 return normalize_number(prettynumber)
1364 def normalize_number(prettynumber):
1366 function to take a phone number and strip out all non-numeric
1369 >>> normalize_number("+012-(345)-678-90")
1371 >>> normalize_number("1-(345)-678-9000")
1373 >>> normalize_number("+1-(345)-678-9000")
1376 uglynumber = re.sub('[^0-9+]', '', prettynumber)
1378 if uglynumber.startswith("+"):
1380 elif uglynumber.startswith("1"):
1381 uglynumber = "+"+uglynumber
1382 elif 10 <= len(uglynumber):
1383 assert uglynumber[0] not in ("+", "1")
1384 uglynumber = "+1"+uglynumber
1391 def _make_pretty_with_areacode(phonenumber):
1392 prettynumber = "(%s)" % (phonenumber[0:3], )
1393 if 3 < len(phonenumber):
1394 prettynumber += " %s" % (phonenumber[3:6], )
1395 if 6 < len(phonenumber):
1396 prettynumber += "-%s" % (phonenumber[6:], )
1400 def _make_pretty_local(phonenumber):
1401 prettynumber = "%s" % (phonenumber[0:3], )
1402 if 3 < len(phonenumber):
1403 prettynumber += "-%s" % (phonenumber[3:], )
1407 def _make_pretty_international(phonenumber):
1408 prettynumber = phonenumber
1409 if phonenumber.startswith("1"):
1411 prettynumber += _make_pretty_with_areacode(phonenumber[1:])
1415 def make_pretty(phonenumber):
1417 Function to take a phone number and return the pretty version
1419 if phonenumber begins with 0:
1421 if phonenumber begins with 1: ( for gizmo callback numbers )
1423 if phonenumber is 13 digits:
1425 if phonenumber is 10 digits:
1427 >>> make_pretty("12")
1429 >>> make_pretty("1234567")
1431 >>> make_pretty("2345678901")
1433 >>> make_pretty("12345678901")
1435 >>> make_pretty("01234567890")
1437 >>> make_pretty("+01234567890")
1439 >>> make_pretty("+12")
1441 >>> make_pretty("+123")
1443 >>> make_pretty("+1234")
1446 if phonenumber is None or phonenumber is "":
1449 phonenumber = normalize_number(phonenumber)
1451 if phonenumber[0] == "+":
1452 prettynumber = _make_pretty_international(phonenumber[1:])
1453 if not prettynumber.startswith("+"):
1454 prettynumber = "+"+prettynumber
1455 elif 8 < len(phonenumber) and phonenumber[0] in ("1", ):
1456 prettynumber = _make_pretty_international(phonenumber)
1457 elif 7 < len(phonenumber):
1458 prettynumber = _make_pretty_with_areacode(phonenumber)
1459 elif 3 < len(phonenumber):
1460 prettynumber = _make_pretty_local(phonenumber)
1462 prettynumber = phonenumber
1463 return prettynumber.strip()
1466 def abbrev_relative_date(date):
1468 >>> abbrev_relative_date("42 hours ago")
1470 >>> abbrev_relative_date("2 days ago")
1472 >>> abbrev_relative_date("4 weeks ago")
1475 parts = date.split(" ")
1476 return "%s %s" % (parts[0], parts[1][0])
1480 app = QtGui.QApplication([])
1481 handle = Dialcentral(app)
1486 if __name__ == "__main__":
1487 logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
1488 logging.basicConfig(level=logging.DEBUG, format=logFormat)
1490 os.makedirs(constants._data_path_)