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__)
31 class Dialcentral(object):
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__,
41 def __init__(self, app):
44 self._hiddenCategories = set()
45 self._hiddenUnits = {}
46 self._clipboard = QtGui.QApplication.clipboard()
48 self._mainWindow = None
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)
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)
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)
66 self._app.lastWindowClosed.connect(self._on_app_quit)
69 self._mainWindow = MainWindow(None, self)
70 self._mainWindow.window.destroyed.connect(self._on_child_close)
72 def load_settings(self):
74 with open(constants._user_settings_, "r") as settingsFile:
75 settings = simplejson.load(settingsFile)
77 _moduleLogger.info("No settings")
80 _moduleLogger.info("Settings were corrupt")
83 self._fullscreenAction.setChecked(settings.get("isFullScreen", False))
85 def save_settings(self):
87 "isFullScreen": self._fullscreenAction.isChecked(),
89 with open(constants._user_settings_, "w") as settingsFile:
90 simplejson.dump(settings, settingsFile)
91 self._mainWindow.destroy()
94 def fsContactsPath(self):
95 return os.path.join(constants._data_path_, "contacts")
98 def fullscreenAction(self):
99 return self._fullscreenAction
103 return self._logAction
106 def quitAction(self):
107 return self._quitAction
109 def _close_windows(self):
110 if self._mainWindow is not None:
111 self._mainWindow.window.destroyed.disconnect(self._on_child_close)
112 self._mainWindow.close()
113 self._mainWindow = None
116 @QtCore.pyqtSlot(bool)
117 @misc_utils.log_exception(_moduleLogger)
118 def _on_app_quit(self, checked = False):
121 @QtCore.pyqtSlot(QtCore.QObject)
122 @misc_utils.log_exception(_moduleLogger)
123 def _on_child_close(self, obj = None):
124 self._mainWindow = None
127 @QtCore.pyqtSlot(bool)
128 @misc_utils.log_exception(_moduleLogger)
129 def _on_toggle_fullscreen(self, checked = False):
130 for window in self._walk_children():
131 window.set_fullscreen(checked)
134 @QtCore.pyqtSlot(bool)
135 @misc_utils.log_exception(_moduleLogger)
136 def _on_log(self, checked = False):
137 with open(constants._user_logpath_, "r") as f:
138 logLines = f.xreadlines()
139 log = "".join(logLines)
140 self._clipboard.setText(log)
143 @QtCore.pyqtSlot(bool)
144 @misc_utils.log_exception(_moduleLogger)
145 def _on_quit(self, checked = False):
146 self._close_windows()
149 class CredentialsDialog(object):
152 self._usernameField = QtGui.QLineEdit()
153 self._passwordField = QtGui.QLineEdit()
154 self._passwordField.setEchoMode(QtGui.QLineEdit.PasswordEchoOnEdit)
156 self._credLayout = QtGui.QGridLayout()
157 self._credLayout.addWidget(QtGui.QLabel("Username"), 0, 0)
158 self._credLayout.addWidget(self._usernameField, 0, 1)
159 self._credLayout.addWidget(QtGui.QLabel("Password"), 1, 0)
160 self._credLayout.addWidget(self._passwordField, 1, 1)
162 self._loginButton = QtGui.QPushButton("&Login")
163 self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
164 self._buttonLayout.addButton(self._loginButton, QtGui.QDialogButtonBox.AcceptRole)
166 self._layout = QtGui.QVBoxLayout()
167 self._layout.addLayout(self._credLayout)
168 self._layout.addWidget(self._buttonLayout)
170 self._dialog = QtGui.QDialog()
171 self._dialog.setWindowTitle("Login")
172 self._dialog.setLayout(self._layout)
173 self._dialog.setAttribute(QtCore.Qt.WA_DeleteOnClose, False)
174 qui_utils.set_autorient(self._dialog, True)
175 self._buttonLayout.accepted.connect(self._dialog.accept)
176 self._buttonLayout.rejected.connect(self._dialog.reject)
178 def run(self, defaultUsername, defaultPassword, parent=None):
179 self._dialog.setParent(parent, QtCore.Qt.Dialog)
181 self._usernameField.setText(defaultUsername)
182 self._passwordField.setText(defaultPassword)
184 response = self._dialog.exec_()
185 if response == QtGui.QDialog.Accepted:
186 return str(self._usernameField.text()), str(self._passwordField.text())
187 elif response == QtGui.QDialog.Rejected:
188 raise RuntimeError("Login Cancelled")
190 raise RuntimeError("Unknown Response")
192 self._dialog.setParent(None, QtCore.Qt.Dialog)
195 class AccountDialog(object):
198 self._doClear = False
200 self._accountNumberLabel = QtGui.QLabel("NUMBER NOT SET")
201 self._clearButton = QtGui.QPushButton("Clear Account")
202 self._clearButton.clicked.connect(self._on_clear)
204 self._credLayout = QtGui.QGridLayout()
205 self._credLayout.addWidget(QtGui.QLabel("Account"), 0, 0)
206 self._credLayout.addWidget(self._accountNumberLabel, 0, 1)
207 self._credLayout.addWidget(QtGui.QLabel("Callback"), 1, 0)
208 self._credLayout.addWidget(QtGui.QLabel(""), 2, 0)
209 self._credLayout.addWidget(self._clearButton, 2, 1)
210 self._credLayout.addWidget(QtGui.QLabel(""), 3, 0)
212 self._loginButton = QtGui.QPushButton("&Apply")
213 self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
214 self._buttonLayout.addButton(self._loginButton, QtGui.QDialogButtonBox.AcceptRole)
216 self._layout = QtGui.QVBoxLayout()
217 self._layout.addLayout(self._credLayout)
218 self._layout.addWidget(self._buttonLayout)
220 self._dialog = QtGui.QDialog()
221 self._dialog.setWindowTitle("Login")
222 self._dialog.setLayout(self._layout)
223 qui_utils.set_autorient(self._dialog, True)
224 self._buttonLayout.accepted.connect(self._dialog.accept)
225 self._buttonLayout.rejected.connect(self._dialog.reject)
231 accountNumber = property(
232 lambda self: str(self._accountNumberLabel.text()),
233 lambda self, num: self._accountNumberLabel.setText(num),
236 def run(self, parent=None):
237 self._doClear = False
238 self._dialog.setParent(parent)
240 response = self._dialog.exec_()
244 @QtCore.pyqtSlot(bool)
245 def _on_clear(self, checked = False):
247 self._dialog.accept()
250 class SMSEntryWindow(object):
252 def __init__(self, parent, app, session, errorLog):
255 self._session = session
256 self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
257 self._session.draft.called.connect(self._on_op_finished)
258 self._session.draft.sentMessage.connect(self._on_op_finished)
259 self._session.draft.cancelled.connect(self._on_op_finished)
260 self._errorLog = errorLog
262 self._targetLayout = QtGui.QVBoxLayout()
263 self._targetList = QtGui.QWidget()
264 self._targetList.setLayout(self._targetLayout)
265 self._history = QtGui.QTextEdit()
266 self._smsEntry = QtGui.QTextEdit()
267 self._smsEntry.textChanged.connect(self._on_letter_count_changed)
269 self._entryLayout = QtGui.QVBoxLayout()
270 self._entryLayout.addWidget(self._targetList)
271 self._entryLayout.addWidget(self._history)
272 self._entryLayout.addWidget(self._smsEntry)
273 self._entryWidget = QtGui.QWidget()
274 self._entryWidget.setLayout(self._entryLayout)
275 self._scrollEntry = QtGui.QScrollArea()
276 self._scrollEntry.setWidget(self._entryWidget)
277 self._scrollEntry.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignBottom)
278 self._scrollEntry.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
279 self._scrollEntry.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
281 self._characterCountLabel = QtGui.QLabel("Letters: %s" % 0)
282 self._singleNumberSelector = QtGui.QComboBox()
283 self._smsButton = QtGui.QPushButton("SMS")
284 self._smsButton.clicked.connect(self._on_sms_clicked)
285 self._dialButton = QtGui.QPushButton("Dial")
286 self._dialButton.clicked.connect(self._on_call_clicked)
288 self._buttonLayout = QtGui.QHBoxLayout()
289 self._buttonLayout.addWidget(self._characterCountLabel)
290 self._buttonLayout.addWidget(self._singleNumberSelector)
291 self._buttonLayout.addWidget(self._smsButton)
292 self._buttonLayout.addWidget(self._dialButton)
294 self._layout = QtGui.QVBoxLayout()
295 self._layout.addWidget(self._scrollEntry)
296 self._layout.addLayout(self._buttonLayout)
298 centralWidget = QtGui.QWidget()
299 centralWidget.setLayout(self._layout)
301 self._window = QtGui.QMainWindow(parent)
302 qui_utils.set_autorient(self._window, True)
303 qui_utils.set_stackable(self._window, True)
304 self._window.setWindowTitle("Contact")
305 self._window.setCentralWidget(centralWidget)
307 self._update_recipients()
309 def _update_letter_count(self):
310 count = self._smsEntry.toPlainText().size()
311 self._characterCountLabel.setText("Letters: %s" % count)
313 def _update_button_state(self):
314 if len(self._contacts) == 0:
315 self._dialButton.setEnabled(False)
316 self._smsButton.setEnabled(False)
317 elif len(self._contacts) == 1:
318 count = self._smsEntry.toPlainText().size()
320 self._dialButton.setEnabled(True)
321 self._smsButton.setEnabled(False)
323 self._dialButton.setEnabled(False)
324 self._smsButton.setEnabled(True)
326 self._dialButton.setEnabled(False)
327 self._smsButton.setEnabled(True)
329 def _update_recipients(self):
330 draftContactsCount = self._session.draft.get_num_contacts()
331 if draftContactsCount == 0:
333 elif draftContactsCount == 1:
334 (cid, ) = self._session.draft.get_contacts()
335 title = self._session.draft.get_title(cid)
336 description = self._session.draft.get_description(cid)
337 numbers = self._session.draft.get_numbers(cid)
339 self._targetList.setVisible(False)
341 self._history.setHtml(description)
342 self._history.setVisible(True)
344 self._history.setHtml("")
345 self._history.setVisible(False)
346 self._populate_number_selector(self._singleNumberSelector, cid, numbers)
348 self._scroll_to_bottom()
349 self._window.setWindowTitle(title)
352 self._targetList.setVisible(True)
353 while self._targetLayout.count():
354 removedLayoutItem = self._targetLayout.takeAt(self._targetLayout.count()-1)
355 removedWidget = removedLayoutItem.widget()
356 removedWidget.close()
357 for cid in self._session.draft.get_contacts():
358 title = self._session.draft.get_title(cid)
359 description = self._session.draft.get_description(cid)
360 numbers = self._session.draft.get_numbers(cid)
362 titleLabel = QtGui.QLabel(title)
363 numberSelector = QtGui.QComboBox()
364 self._populate_number_selector(numberSelector, cid, numbers)
365 deleteButton = QtGui.QPushButton("Delete")
366 callback = functools.partial(
367 self._on_remove_contact,
370 callback.__name__ = "b"
371 deleteButton.clicked.connect(
372 QtCore.pyqtSlot()(callback)
375 rowLayout = QtGui.QHBoxLayout()
376 rowLayout.addWidget(titleLabel)
377 rowLayout.addWidget(numberSelector)
378 rowLayout.addWidget(deleteButton)
379 rowWidget = QtGui.QWidget()
380 rowWidget.setLayout(rowLayout)
381 self._targetLayout.addWidget(rowWidget)
382 self._history.setHtml("")
383 self._history.setVisible(False)
384 self._singleNumberSelector.setVisible(False)
386 self._scroll_to_bottom()
387 self._window.setWindowTitle("Contacts")
390 def _populate_number_selector(self, selector, cid, numbers):
391 while 0 < selector.count():
392 selector.removeItem(0)
393 for number, description in numbers:
395 label = "%s - %s" % (number, description)
398 selector.addItem(label)
399 selector.setVisible(True)
401 selector.setEnabled(True)
403 selector.setEnabled(False)
404 callback = functools.partial(
405 self._on_change_number,
408 callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
409 selector.currentIndexChanged.connect(
410 QtCore.pyqtSlot(int)(callback)
413 def _scroll_to_bottom(self):
414 self._scrollEntry.ensureWidgetVisible(self._smsEntry)
417 @misc_utils.log_exception(_moduleLogger)
418 def _on_sms_clicked(self):
419 message = str(self._smsEntry.toPlainText())
420 self._session.draft.send(message)
421 self._smsEntry.setPlainText("")
424 @misc_utils.log_exception(_moduleLogger)
425 def _on_call_clicked(self):
426 self._session.draft.call()
427 self._smsEntry.setPlainText("")
429 @misc_utils.log_exception(_moduleLogger)
430 def _on_remove_contact(self, cid):
431 self._session.draft.remove_contact(cid)
433 @misc_utils.log_exception(_moduleLogger)
434 def _on_change_number(self, cid, index):
435 numbers = self._session.draft.get_numbers(cid)
436 number = numbers[index][0]
437 self._session.draft.set_selected_number(cid, number)
440 @misc_utils.log_exception(_moduleLogger)
441 def _on_recipients_changed(self):
442 self._update_recipients()
445 @misc_utils.log_exception(_moduleLogger)
446 def _on_op_finished(self):
450 @misc_utils.log_exception(_moduleLogger)
451 def _on_letter_count_changed(self):
452 self._update_letter_count()
453 self._update_button_state()
456 class DelayedWidget(object):
458 def __init__(self, app):
459 self._layout = QtGui.QVBoxLayout()
460 self._widget = QtGui.QWidget()
461 self._widget.setLayout(self._layout)
464 self._isEnabled = True
471 return self._child is not None
473 def set_child(self, child):
474 if self._child is not None:
475 self._layout.removeWidget(self._child.toplevel)
477 if self._child is not None:
478 self._layout.addWidget(self._child.toplevel)
483 self._child.disable()
486 self._isEnabled = True
487 if self._child is not None:
491 self._isEnabled = False
492 if self._child is not None:
493 self._child.disable()
496 if self._child is not None:
499 def refresh(self, force=True):
500 if self._child is not None:
501 self._child.refresh(force)
504 class Dialpad(object):
506 def __init__(self, app, session, errorLog):
508 self._session = session
509 self._errorLog = errorLog
511 self._plus = self._generate_key_button("+", "")
512 self._entry = QtGui.QLineEdit()
514 backAction = QtGui.QAction(None)
515 backAction.setText("Back")
516 backAction.triggered.connect(self._on_backspace)
517 backPieItem = qtpie.QActionPieItem(backAction)
518 clearAction = QtGui.QAction(None)
519 clearAction.setText("Clear")
520 clearAction.triggered.connect(self._on_clear_text)
521 clearPieItem = qtpie.QActionPieItem(clearAction)
522 self._back = qtpie.QPieButton(backPieItem)
523 self._back.set_center(backPieItem)
524 self._back.insertItem(qtpie.PieFiling.NULL_CENTER)
525 self._back.insertItem(clearPieItem)
526 self._back.insertItem(qtpie.PieFiling.NULL_CENTER)
527 self._back.insertItem(qtpie.PieFiling.NULL_CENTER)
529 self._entryLayout = QtGui.QHBoxLayout()
530 self._entryLayout.addWidget(self._plus, 0, QtCore.Qt.AlignCenter)
531 self._entryLayout.addWidget(self._entry, 10)
532 self._entryLayout.addWidget(self._back, 0, QtCore.Qt.AlignCenter)
534 self._smsButton = QtGui.QPushButton("SMS")
535 self._smsButton.clicked.connect(self._on_sms_clicked)
536 self._callButton = QtGui.QPushButton("Call")
537 self._callButton.clicked.connect(self._on_call_clicked)
539 self._padLayout = QtGui.QGridLayout()
540 rows = [0, 0, 0, 1, 1, 1, 2, 2, 2]
541 columns = [0, 1, 2] * 3
553 for (num, letters), (row, column) in zip(keys, zip(rows, columns)):
554 self._padLayout.addWidget(
555 self._generate_key_button(num, letters), row, column, QtCore.Qt.AlignCenter
557 self._padLayout.addWidget(self._smsButton, 3, 0)
558 self._padLayout.addWidget(
559 self._generate_key_button("0", ""), 3, 1, QtCore.Qt.AlignCenter
561 self._padLayout.addWidget(self._callButton, 3, 2)
563 self._layout = QtGui.QVBoxLayout()
564 self._layout.addLayout(self._entryLayout)
565 self._layout.addLayout(self._padLayout)
566 self._widget = QtGui.QWidget()
567 self._widget.setLayout(self._layout)
574 self._smsButton.setEnabled(True)
575 self._callButton.setEnabled(True)
578 self._smsButton.setEnabled(False)
579 self._callButton.setEnabled(False)
584 def refresh(self, force = True):
587 def _generate_key_button(self, center, letters):
588 centerPieItem = self._generate_button_slice(center)
589 button = qtpie.QPieButton(centerPieItem)
590 button.set_center(centerPieItem)
592 if len(letters) == 0:
594 pieItem = qtpie.PieFiling.NULL_CENTER
595 button.insertItem(pieItem)
596 elif len(letters) in [3, 4]:
597 for i in xrange(6 - len(letters)):
598 pieItem = qtpie.PieFiling.NULL_CENTER
599 button.insertItem(pieItem)
601 for letter in letters:
602 pieItem = self._generate_button_slice(letter)
603 button.insertItem(pieItem)
606 pieItem = qtpie.PieFiling.NULL_CENTER
607 button.insertItem(pieItem)
609 raise NotImplementedError("Cannot handle %r" % letters)
612 def _generate_button_slice(self, letter):
613 action = QtGui.QAction(None)
614 action.setText(letter)
615 action.triggered.connect(lambda: self._on_keypress(letter))
616 pieItem = qtpie.QActionPieItem(action)
619 @misc_utils.log_exception(_moduleLogger)
620 def _on_keypress(self, key):
621 self._entry.insert(key)
623 @misc_utils.log_exception(_moduleLogger)
624 def _on_backspace(self, toggled = False):
625 self._entry.backspace()
627 @misc_utils.log_exception(_moduleLogger)
628 def _on_clear_text(self, toggled = False):
632 @QtCore.pyqtSlot(bool)
633 @misc_utils.log_exception(_moduleLogger)
634 def _on_sms_clicked(self, checked = False):
635 number = str(self._entry.text())
641 numbersWithDescriptions = [(number, "")]
642 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
645 @QtCore.pyqtSlot(bool)
646 @misc_utils.log_exception(_moduleLogger)
647 def _on_call_clicked(self, checked = False):
648 number = str(self._entry.text())
654 numbersWithDescriptions = [(number, "")]
655 self._session.draft.clear()
656 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
657 self._session.draft.call()
660 class History(object):
668 HISTORY_ITEM_TYPES = ["Received", "Missed", "Placed", "All"]
669 HISTORY_COLUMNS = ["When", "What", "Number", "From"]
670 assert len(HISTORY_COLUMNS) == MAX_IDX
672 def __init__(self, app, session, errorLog):
673 self._selectedFilter = self.HISTORY_ITEM_TYPES[-1]
675 self._session = session
676 self._session.historyUpdated.connect(self._on_history_updated)
677 self._errorLog = errorLog
679 self._typeSelection = QtGui.QComboBox()
680 self._typeSelection.addItems(self.HISTORY_ITEM_TYPES)
681 self._typeSelection.setCurrentIndex(
682 self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
684 self._typeSelection.currentIndexChanged[str].connect(self._on_filter_changed)
686 self._itemStore = QtGui.QStandardItemModel()
687 self._itemStore.setHorizontalHeaderLabels(self.HISTORY_COLUMNS)
689 self._itemView = QtGui.QTreeView()
690 self._itemView.setModel(self._itemStore)
691 self._itemView.setUniformRowHeights(True)
692 self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
693 self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
694 self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
695 self._itemView.setHeaderHidden(True)
696 self._itemView.activated.connect(self._on_row_activated)
698 self._layout = QtGui.QVBoxLayout()
699 self._layout.addWidget(self._typeSelection)
700 self._layout.addWidget(self._itemView)
701 self._widget = QtGui.QWidget()
702 self._widget.setLayout(self._layout)
704 self._populate_items()
711 self._itemView.setEnabled(True)
714 self._itemView.setEnabled(False)
717 self._itemView.clear()
719 def refresh(self, force=True):
720 self._session.update_history(force)
722 def _populate_items(self):
723 self._itemStore.clear()
724 history = self._session.get_history()
725 history.sort(key=lambda item: item["time"], reverse=True)
726 for event in history:
727 if self._selectedFilter in [self.HISTORY_ITEM_TYPES[-1], event["action"]]:
728 relTime = abbrev_relative_date(event["relTime"])
729 action = event["action"]
730 number = event["number"]
731 prettyNumber = make_pretty(number)
733 if not name or name == number:
734 name = event["location"]
738 timeItem = QtGui.QStandardItem(relTime)
739 actionItem = QtGui.QStandardItem(action)
740 numberItem = QtGui.QStandardItem(prettyNumber)
741 nameItem = QtGui.QStandardItem(name)
742 row = timeItem, actionItem, numberItem, nameItem
744 item.setEditable(False)
745 item.setCheckable(False)
746 if item is not nameItem:
747 itemFont = item.font()
748 itemFont.setPointSize(max(itemFont.pointSize() - 3, 5))
749 item.setFont(itemFont)
750 numberItem.setData(event)
751 self._itemStore.appendRow(row)
753 @QtCore.pyqtSlot(str)
754 @misc_utils.log_exception(_moduleLogger)
755 def _on_filter_changed(self, newItem):
756 self._selectedFilter = str(newItem)
757 self._populate_items()
760 @misc_utils.log_exception(_moduleLogger)
761 def _on_history_updated(self):
762 self._populate_items()
764 @QtCore.pyqtSlot(QtCore.QModelIndex)
765 @misc_utils.log_exception(_moduleLogger)
766 def _on_row_activated(self, index):
767 rowIndex = index.row()
768 item = self._itemStore.item(rowIndex, self.NUMBER_IDX)
769 contactDetails = item.data().toPyObject()
771 title = str(self._itemStore.item(rowIndex, self.FROM_IDX).text())
772 number = str(contactDetails[QtCore.QString("number")])
773 contactId = number # ids don't seem too unique so using numbers
776 # @bug doesn't seem to print multiple entries
777 for i in xrange(self._itemStore.rowCount()):
778 iItem = self._itemStore.item(i, self.NUMBER_IDX)
779 iContactDetails = iItem.data().toPyObject()
780 iNumber = str(iContactDetails[QtCore.QString("number")])
781 if number != iNumber:
783 relTime = abbrev_relative_date(iContactDetails[QtCore.QString("relTime")])
784 action = str(iContactDetails[QtCore.QString("action")])
785 number = str(iContactDetails[QtCore.QString("number")])
786 prettyNumber = make_pretty(number)
787 rowItems = relTime, action, prettyNumber
788 descriptionRows.append("<tr><td>%s</td></tr>" % "</td><td>".join(rowItems))
789 description = "<table>%s</table>" % "".join(descriptionRows)
790 numbersWithDescriptions = [(str(contactDetails[QtCore.QString("number")]), "")]
791 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
794 class Messages(object):
797 VOICEMAIL_MESSAGES = "Voicemail"
798 TEXT_MESSAGES = "SMS"
799 ALL_TYPES = "All Messages"
800 MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
802 UNREAD_STATUS = "Unread"
803 UNARCHIVED_STATUS = "Inbox"
805 MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
807 _MIN_MESSAGES_SHOWN = 4
809 def __init__(self, app, session, errorLog):
810 self._selectedTypeFilter = self.ALL_TYPES
811 self._selectedStatusFilter = self.ALL_STATUS
813 self._session = session
814 self._session.messagesUpdated.connect(self._on_messages_updated)
815 self._errorLog = errorLog
817 self._typeSelection = QtGui.QComboBox()
818 self._typeSelection.addItems(self.MESSAGE_TYPES)
819 self._typeSelection.setCurrentIndex(
820 self.MESSAGE_TYPES.index(self._selectedTypeFilter)
822 self._typeSelection.currentIndexChanged[str].connect(self._on_type_filter_changed)
824 self._statusSelection = QtGui.QComboBox()
825 self._statusSelection.addItems(self.MESSAGE_STATUSES)
826 self._statusSelection.setCurrentIndex(
827 self.MESSAGE_STATUSES.index(self._selectedStatusFilter)
829 self._statusSelection.currentIndexChanged[str].connect(self._on_status_filter_changed)
831 self._selectionLayout = QtGui.QHBoxLayout()
832 self._selectionLayout.addWidget(self._typeSelection)
833 self._selectionLayout.addWidget(self._statusSelection)
835 self._itemStore = QtGui.QStandardItemModel()
836 self._itemStore.setHorizontalHeaderLabels(["Messages"])
838 self._htmlDelegate = qui_utils.QHtmlDelegate()
839 self._itemView = QtGui.QTreeView()
840 self._itemView.setModel(self._itemStore)
841 self._itemView.setUniformRowHeights(False)
842 self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
843 self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
844 self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
845 self._itemView.setHeaderHidden(True)
846 self._itemView.setItemDelegate(self._htmlDelegate)
847 self._itemView.activated.connect(self._on_row_activated)
849 self._layout = QtGui.QVBoxLayout()
850 self._layout.addLayout(self._selectionLayout)
851 self._layout.addWidget(self._itemView)
852 self._widget = QtGui.QWidget()
853 self._widget.setLayout(self._layout)
855 self._populate_items()
862 self._itemView.setEnabled(True)
865 self._itemView.setEnabled(False)
868 self._itemView.clear()
870 def refresh(self, force=True):
871 self._session.update_messages(force)
873 def _populate_items(self):
874 self._itemStore.clear()
875 rawMessages = self._session.get_messages()
876 rawMessages.sort(key=lambda item: item["time"], reverse=True)
877 for item in rawMessages:
878 isUnarchived = not item["isArchived"]
879 isUnread = not item["isRead"]
881 self.UNREAD_STATUS: isUnarchived and isUnread,
882 self.UNARCHIVED_STATUS: isUnarchived,
883 self.ALL_STATUS: True,
884 }[self._selectedStatusFilter]
886 visibleType = self._selectedTypeFilter in [item["type"], self.ALL_TYPES]
887 if visibleType and visibleStatus:
888 relTime = abbrev_relative_date(item["relTime"])
889 number = item["number"]
890 prettyNumber = make_pretty(number)
892 if not name or name == number:
893 name = item["location"]
897 messageParts = list(item["messageParts"])
898 if len(messageParts) == 0:
899 messages = ("No Transcription", )
900 elif len(messageParts) == 1:
901 if messageParts[0][1]:
902 messages = (messageParts[0][1], )
904 messages = ("No Transcription", )
907 "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
908 for messagePart in messageParts
911 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (name, prettyNumber, relTime)
913 expandedMessages = [firstMessage]
914 expandedMessages.extend(messages)
915 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
916 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
917 collapsedMessages = [firstMessage, secondMessage]
918 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
920 collapsedMessages = expandedMessages
922 item = dict(item.iteritems())
923 item["collapsedMessages"] = "<br/>\n".join(collapsedMessages)
924 item["expandedMessages"] = "<br/>\n".join(expandedMessages)
926 messageItem = QtGui.QStandardItem(item["collapsedMessages"])
927 # @bug Not showing all of a message
928 messageItem.setData(item)
929 messageItem.setEditable(False)
930 messageItem.setCheckable(False)
931 row = (messageItem, )
932 self._itemStore.appendRow(row)
934 @QtCore.pyqtSlot(str)
935 @misc_utils.log_exception(_moduleLogger)
936 def _on_type_filter_changed(self, newItem):
937 self._selectedTypeFilter = str(newItem)
938 self._populate_items()
940 @QtCore.pyqtSlot(str)
941 @misc_utils.log_exception(_moduleLogger)
942 def _on_status_filter_changed(self, newItem):
943 self._selectedStatusFilter = str(newItem)
944 self._populate_items()
947 @misc_utils.log_exception(_moduleLogger)
948 def _on_messages_updated(self):
949 self._populate_items()
951 @QtCore.pyqtSlot(QtCore.QModelIndex)
952 @misc_utils.log_exception(_moduleLogger)
953 def _on_row_activated(self, index):
954 rowIndex = index.row()
955 item = self._itemStore.item(rowIndex, 0)
956 contactDetails = item.data().toPyObject()
958 name = str(contactDetails[QtCore.QString("name")])
959 number = str(contactDetails[QtCore.QString("number")])
960 if not name or name == number:
961 name = str(contactDetails[QtCore.QString("location")])
965 contactId = str(contactDetails[QtCore.QString("id")])
967 description = str(contactDetails[QtCore.QString("expandedMessages")])
968 numbersWithDescriptions = [(number, "")]
969 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
972 class Contacts(object):
974 def __init__(self, app, session, errorLog):
976 self._session = session
977 self._session.contactsUpdated.connect(self._on_contacts_updated)
978 self._errorLog = errorLog
979 self._addressBookFactories = [
980 null_backend.NullAddressBookFactory(),
981 file_backend.FilesystemAddressBookFactory(app.fsContactsPath),
983 self._addressBooks = []
985 self._listSelection = QtGui.QComboBox()
986 self._listSelection.addItems([])
987 self._listSelection.currentIndexChanged[str].connect(self._on_filter_changed)
989 self._itemStore = QtGui.QStandardItemModel()
990 self._itemStore.setHorizontalHeaderLabels(["Contacts"])
992 self._itemView = QtGui.QTreeView()
993 self._itemView.setModel(self._itemStore)
994 self._itemView.setUniformRowHeights(True)
995 self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
996 self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
997 self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
998 self._itemView.setHeaderHidden(True)
999 self._itemView.activated.connect(self._on_row_activated)
1001 self._layout = QtGui.QVBoxLayout()
1002 self._layout.addWidget(self._listSelection)
1003 self._layout.addWidget(self._itemView)
1004 self._widget = QtGui.QWidget()
1005 self._widget.setLayout(self._layout)
1007 self.update_addressbooks()
1008 self._populate_items()
1015 self._itemView.setEnabled(True)
1018 self._itemView.setEnabled(False)
1021 self._itemView.clear()
1023 def refresh(self, force=True):
1024 self._backend.update_contacts(force)
1028 return self._addressBooks[self._listSelection.currentIndex()]["book"]
1030 def update_addressbooks(self):
1031 self._addressBooks = [
1032 {"book": book, "name": book.name}
1033 for factory in self._addressBookFactories
1034 for book in factory.get_addressbooks()
1036 self._addressBooks.append(
1038 "book": self._session,
1039 "name": "Google Voice",
1043 currentItem = str(self._listSelection.currentText())
1044 if currentItem == "":
1046 currentItem = "None"
1047 while 0 < self._listSelection.count():
1048 self._listSelection.removeItem(0)
1049 bookNames = [book["name"] for book in self._addressBooks]
1051 newIndex = bookNames.index(currentItem)
1053 # Switch over to None for the user
1055 self._itemStore.clear()
1056 _moduleLogger.info("Addressbook %r doesn't exist anymore, switching to None" % currentItem)
1057 self._listSelection.addItems(bookNames)
1058 self._listSelection.setCurrentIndex(newIndex)
1060 def _populate_items(self):
1061 self._itemStore.clear()
1063 contacts = list(self._backend.get_contacts().itervalues())
1064 contacts.sort(key=lambda contact: contact["name"].lower())
1065 for item in contacts:
1067 numbers = item["numbers"]
1068 nameItem = QtGui.QStandardItem(name)
1069 nameItem.setEditable(False)
1070 nameItem.setCheckable(False)
1071 nameItem.setData(item)
1073 self._itemStore.appendRow(row)
1075 @QtCore.pyqtSlot(str)
1076 @misc_utils.log_exception(_moduleLogger)
1077 def _on_filter_changed(self, newItem):
1078 self._populate_items()
1081 @misc_utils.log_exception(_moduleLogger)
1082 def _on_contacts_updated(self):
1083 self._populate_items()
1085 @QtCore.pyqtSlot(QtCore.QModelIndex)
1086 @misc_utils.log_exception(_moduleLogger)
1087 def _on_row_activated(self, index):
1088 rowIndex = index.row()
1089 item = self._itemStore.item(rowIndex, 0)
1090 contactDetails = item.data().toPyObject()
1092 name = str(contactDetails[QtCore.QString("name")])
1094 name = str(contactDetails[QtCore.QString("location")])
1098 contactId = str(contactDetails[QtCore.QString("contactId")])
1099 numbers = contactDetails[QtCore.QString("numbers")]
1103 for (k, v) in number.iteritems()
1105 for number in numbers
1107 numbersWithDescriptions = [
1109 number["phoneNumber"],
1110 self._choose_phonetype(number),
1112 for number in numbers
1116 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
1119 def _choose_phonetype(numberDetails):
1120 if "phoneTypeName" in numberDetails:
1121 return numberDetails["phoneTypeName"]
1122 elif "phoneType" in numberDetails:
1123 return numberDetails["phoneType"]
1128 class MainWindow(object):
1142 assert len(_TAB_TITLES) == MAX_TABS
1150 assert len(_TAB_CLASS) == MAX_TABS
1152 def __init__(self, parent, app):
1154 self._session = session.Session(constants._data_path_)
1155 self._session.error.connect(self._on_session_error)
1156 self._session.loggedIn.connect(self._on_login)
1157 self._session.loggedOut.connect(self._on_logout)
1158 self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
1160 self._credentialsDialog = None
1161 self._smsEntryDialog = None
1162 self._accountDialog = None
1164 self._errorLog = qui_utils.QErrorLog()
1165 self._errorDisplay = qui_utils.ErrorDisplay(self._errorLog)
1167 self._tabsContents = [
1168 DelayedWidget(self._app)
1169 for i in xrange(self.MAX_TABS)
1171 for tab in self._tabsContents:
1174 self._tabWidget = QtGui.QTabWidget()
1175 if qui_utils.screen_orientation() == QtCore.Qt.Vertical:
1176 self._tabWidget.setTabPosition(QtGui.QTabWidget.South)
1178 self._tabWidget.setTabPosition(QtGui.QTabWidget.West)
1179 for tabIndex, tabTitle in enumerate(self._TAB_TITLES):
1180 self._tabWidget.addTab(self._tabsContents[tabIndex].toplevel, tabTitle)
1181 self._tabWidget.currentChanged.connect(self._on_tab_changed)
1183 self._layout = QtGui.QVBoxLayout()
1184 self._layout.addWidget(self._errorDisplay.toplevel)
1185 self._layout.addWidget(self._tabWidget)
1187 centralWidget = QtGui.QWidget()
1188 centralWidget.setLayout(self._layout)
1190 self._window = QtGui.QMainWindow(parent)
1191 self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
1192 qui_utils.set_autorient(self._window, True)
1193 qui_utils.set_stackable(self._window, True)
1194 self._window.setWindowTitle("%s" % constants.__pretty_app_name__)
1195 self._window.setCentralWidget(centralWidget)
1197 self._loginTabAction = QtGui.QAction(None)
1198 self._loginTabAction.setText("Login")
1199 self._loginTabAction.triggered.connect(self._on_login_requested)
1201 self._importTabAction = QtGui.QAction(None)
1202 self._importTabAction.setText("Import")
1203 self._importTabAction.triggered.connect(self._on_import)
1205 self._accountTabAction = QtGui.QAction(None)
1206 self._accountTabAction.setText("Account")
1207 self._accountTabAction.triggered.connect(self._on_account)
1209 self._refreshTabAction = QtGui.QAction(None)
1210 self._refreshTabAction.setText("Refresh")
1211 self._refreshTabAction.setShortcut(QtGui.QKeySequence("CTRL+r"))
1212 self._refreshTabAction.triggered.connect(self._on_refresh)
1214 self._closeWindowAction = QtGui.QAction(None)
1215 self._closeWindowAction.setText("Close")
1216 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
1217 self._closeWindowAction.triggered.connect(self._on_close_window)
1220 fileMenu = self._window.menuBar().addMenu("&File")
1221 fileMenu.addAction(self._loginTabAction)
1222 fileMenu.addAction(self._refreshTabAction)
1224 toolsMenu = self._window.menuBar().addMenu("&Tools")
1225 toolsMenu.addAction(self._accountTabAction)
1226 toolsMenu.addAction(self._importTabAction)
1228 self._window.addAction(self._closeWindowAction)
1229 self._window.addAction(self._app.quitAction)
1230 self._window.addAction(self._app.fullscreenAction)
1232 fileMenu = self._window.menuBar().addMenu("&File")
1233 fileMenu.addAction(self._loginTabAction)
1234 fileMenu.addAction(self._refreshTabAction)
1235 fileMenu.addAction(self._closeWindowAction)
1236 fileMenu.addAction(self._app.quitAction)
1238 viewMenu = self._window.menuBar().addMenu("&View")
1239 viewMenu.addAction(self._app.fullscreenAction)
1241 toolsMenu = self._window.menuBar().addMenu("&Tools")
1242 toolsMenu.addAction(self._accountTabAction)
1243 toolsMenu.addAction(self._importTabAction)
1245 self._window.addAction(self._app.logAction)
1247 self._initialize_tab(self._tabWidget.currentIndex())
1248 self.set_fullscreen(self._app.fullscreenAction.isChecked())
1255 def walk_children(self):
1260 for child in self.walk_children():
1264 for child in self.walk_children():
1269 for child in self.walk_children():
1270 child.window.destroyed.disconnect(self._on_child_close)
1272 self._window.close()
1275 if self._session.state != self._session.LOGGEDOUT_STATE:
1276 self._session.logout()
1278 def set_fullscreen(self, isFullscreen):
1280 self._window.showFullScreen()
1282 self._window.showNormal()
1283 for child in self.walk_children():
1284 child.set_fullscreen(isFullscreen)
1286 def _initialize_tab(self, index):
1287 assert index < self.MAX_TABS
1288 if not self._tabsContents[index].has_child():
1289 tab = self._TAB_CLASS[index](self._app, self._session, self._errorLog)
1290 self._tabsContents[index].set_child(tab)
1291 self._tabsContents[index].refresh(force=False)
1293 @QtCore.pyqtSlot(str)
1294 @misc_utils.log_exception(_moduleLogger)
1295 def _on_session_error(self, message):
1296 self._errorLog.push_message(message)
1299 @misc_utils.log_exception(_moduleLogger)
1300 def _on_login(self):
1301 for tab in self._tabsContents:
1305 @misc_utils.log_exception(_moduleLogger)
1306 def _on_logout(self):
1307 for tab in self._tabsContents:
1311 @misc_utils.log_exception(_moduleLogger)
1312 def _on_recipients_changed(self):
1313 if self._session.draft.get_num_contacts() == 0:
1316 if self._smsEntryDialog is None:
1317 self._smsEntryDialog = SMSEntryWindow(self.window, self._app, self._session, self._errorLog)
1321 @QtCore.pyqtSlot(bool)
1322 @misc_utils.log_exception(_moduleLogger)
1323 def _on_login_requested(self, checked = True):
1324 if self._credentialsDialog is None:
1325 self._credentialsDialog = CredentialsDialog()
1326 username, password = self._credentialsDialog.run("", "", self.window)
1327 self._session.login(username, password)
1329 @QtCore.pyqtSlot(int)
1330 @misc_utils.log_exception(_moduleLogger)
1331 def _on_tab_changed(self, index):
1332 self._initialize_tab(index)
1335 @QtCore.pyqtSlot(bool)
1336 @misc_utils.log_exception(_moduleLogger)
1337 def _on_refresh(self, checked = True):
1338 index = self._tabWidget.currentIndex()
1339 self._tabsContents[index].refresh(force=True)
1342 @QtCore.pyqtSlot(bool)
1343 @misc_utils.log_exception(_moduleLogger)
1344 def _on_import(self, checked = True):
1345 csvName = QtGui.QFileDialog.getOpenFileName(self._window, caption="Import", filter="CSV Files (*.csv)")
1348 shutil.copy2(csvName, self._app.fsContactsPath)
1349 self._tabsContents[self.CONTACTS_TAB].update_addressbooks()
1352 @QtCore.pyqtSlot(bool)
1353 @misc_utils.log_exception(_moduleLogger)
1354 def _on_account(self, checked = True):
1355 if self._accountDialog is None:
1356 self._accountDialog = AccountDialog()
1357 self._accountDialog.accountNumber = self._session.get_account_number()
1358 response = self._accountDialog.run()
1359 if response == QtGui.QDialog.Accepted:
1360 if self._accountDialog.doClear():
1361 self._session.logout_and_clear()
1362 elif response == QtGui.QDialog.Rejected:
1363 _moduleLogger.info("Cancelled")
1365 _moduleLogger.info("Unknown response")
1368 @QtCore.pyqtSlot(bool)
1369 @misc_utils.log_exception(_moduleLogger)
1370 def _on_close_window(self, checked = True):
1374 def make_ugly(prettynumber):
1376 function to take a phone number and strip out all non-numeric
1379 >>> make_ugly("+012-(345)-678-90")
1382 return normalize_number(prettynumber)
1385 def normalize_number(prettynumber):
1387 function to take a phone number and strip out all non-numeric
1390 >>> normalize_number("+012-(345)-678-90")
1392 >>> normalize_number("1-(345)-678-9000")
1394 >>> normalize_number("+1-(345)-678-9000")
1397 uglynumber = re.sub('[^0-9+]', '', prettynumber)
1399 if uglynumber.startswith("+"):
1401 elif uglynumber.startswith("1"):
1402 uglynumber = "+"+uglynumber
1403 elif 10 <= len(uglynumber):
1404 assert uglynumber[0] not in ("+", "1")
1405 uglynumber = "+1"+uglynumber
1412 def _make_pretty_with_areacode(phonenumber):
1413 prettynumber = "(%s)" % (phonenumber[0:3], )
1414 if 3 < len(phonenumber):
1415 prettynumber += " %s" % (phonenumber[3:6], )
1416 if 6 < len(phonenumber):
1417 prettynumber += "-%s" % (phonenumber[6:], )
1421 def _make_pretty_local(phonenumber):
1422 prettynumber = "%s" % (phonenumber[0:3], )
1423 if 3 < len(phonenumber):
1424 prettynumber += "-%s" % (phonenumber[3:], )
1428 def _make_pretty_international(phonenumber):
1429 prettynumber = phonenumber
1430 if phonenumber.startswith("1"):
1432 prettynumber += _make_pretty_with_areacode(phonenumber[1:])
1436 def make_pretty(phonenumber):
1438 Function to take a phone number and return the pretty version
1440 if phonenumber begins with 0:
1442 if phonenumber begins with 1: ( for gizmo callback numbers )
1444 if phonenumber is 13 digits:
1446 if phonenumber is 10 digits:
1448 >>> make_pretty("12")
1450 >>> make_pretty("1234567")
1452 >>> make_pretty("2345678901")
1454 >>> make_pretty("12345678901")
1456 >>> make_pretty("01234567890")
1458 >>> make_pretty("+01234567890")
1460 >>> make_pretty("+12")
1462 >>> make_pretty("+123")
1464 >>> make_pretty("+1234")
1467 if phonenumber is None or phonenumber is "":
1470 phonenumber = normalize_number(phonenumber)
1472 if phonenumber[0] == "+":
1473 prettynumber = _make_pretty_international(phonenumber[1:])
1474 if not prettynumber.startswith("+"):
1475 prettynumber = "+"+prettynumber
1476 elif 8 < len(phonenumber) and phonenumber[0] in ("1", ):
1477 prettynumber = _make_pretty_international(phonenumber)
1478 elif 7 < len(phonenumber):
1479 prettynumber = _make_pretty_with_areacode(phonenumber)
1480 elif 3 < len(phonenumber):
1481 prettynumber = _make_pretty_local(phonenumber)
1483 prettynumber = phonenumber
1484 return prettynumber.strip()
1487 def abbrev_relative_date(date):
1489 >>> abbrev_relative_date("42 hours ago")
1491 >>> abbrev_relative_date("2 days ago")
1493 >>> abbrev_relative_date("4 weeks ago")
1496 parts = date.split(" ")
1497 return "%s %s" % (parts[0], parts[1][0])
1501 app = QtGui.QApplication([])
1502 handle = Dialcentral(app)
1507 if __name__ == "__main__":
1508 logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
1509 logging.basicConfig(level=logging.DEBUG, format=logFormat)
1511 os.makedirs(constants._data_path_)