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
25 _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)
93 def fullscreenAction(self):
94 return self._fullscreenAction
98 return self._logAction
101 def quitAction(self):
102 return self._quitAction
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
111 @QtCore.pyqtSlot(bool)
112 @misc_utils.log_exception(_moduleLogger)
113 def _on_app_quit(self, checked = False):
116 @QtCore.pyqtSlot(QtCore.QObject)
117 @misc_utils.log_exception(_moduleLogger)
118 def _on_child_close(self, obj = None):
119 self._mainWindow = None
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)
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)
138 @QtCore.pyqtSlot(bool)
139 @misc_utils.log_exception(_moduleLogger)
140 def _on_quit(self, checked = False):
141 self._close_windows()
144 class CredentialsDialog(object):
147 self._usernameField = QtGui.QLineEdit()
148 self._passwordField = QtGui.QLineEdit()
149 self._passwordField.setEchoMode(QtGui.QLineEdit.PasswordEchoOnEdit)
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)
157 self._loginButton = QtGui.QPushButton("&Login")
158 self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
159 self._buttonLayout.addButton(self._loginButton, QtGui.QDialogButtonBox.AcceptRole)
161 self._layout = QtGui.QVBoxLayout()
162 self._layout.addLayout(self._credLayout)
163 self._layout.addWidget(self._buttonLayout)
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)
173 def run(self, defaultUsername, defaultPassword, parent=None):
174 self._dialog.setParent(parent, QtCore.Qt.Dialog)
176 self._usernameField.setText(defaultUsername)
177 self._passwordField.setText(defaultPassword)
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")
185 self._dialog.setParent(None, QtCore.Qt.Dialog)
188 class AccountDialog(object):
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
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)
202 self._loginButton = QtGui.QPushButton("&Login")
203 self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
204 self._buttonLayout.addButton(self._loginButton, QtGui.QDialogButtonBox.AcceptRole)
206 self._layout = QtGui.QVBoxLayout()
207 self._layout.addLayout(self._credLayout)
208 self._layout.addLayout(self._buttonLayout)
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)
221 accountNumber = property(
222 lambda self: str(self._accountNumberLabel.text()),
223 lambda self, num: self._accountNumberLabel.setText(num),
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)
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")
239 @QtCore.pyqtSlot(bool)
240 def _on_clear(self, checked = False):
242 self._dialog.accept()
245 class SMSEntryWindow(object):
247 def __init__(self, parent, app, session, errorLog):
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
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)
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)
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)
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)
289 self._layout = QtGui.QVBoxLayout()
290 self._layout.addWidget(self._scrollEntry)
291 self._layout.addLayout(self._buttonLayout)
293 centralWidget = QtGui.QWidget()
294 centralWidget.setLayout(self._layout)
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)
302 self._update_recipients()
304 def _update_letter_count(self):
305 count = self._smsEntry.toPlainText().size()
306 self._characterCountLabel.setText("Letters: %s" % count)
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()
315 self._dialButton.setEnabled(True)
316 self._smsButton.setEnabled(False)
318 self._dialButton.setEnabled(False)
319 self._smsButton.setEnabled(True)
321 self._dialButton.setEnabled(False)
322 self._smsButton.setEnabled(True)
324 def _update_recipients(self):
325 draftContactsCount = self._session.draft.get_num_contacts()
326 if draftContactsCount == 0:
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)
334 self._targetList.setVisible(False)
336 self._history.setHtml(description)
337 self._history.setVisible(True)
339 self._history.setHtml("")
340 self._history.setVisible(False)
341 self._populate_number_selector(self._singleNumberSelector, cid, numbers)
343 self._scroll_to_bottom()
344 self._window.setWindowTitle(title)
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)
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,
365 callback.__name__ = "b"
366 deleteButton.clicked.connect(
367 QtCore.pyqtSlot()(callback)
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)
381 self._scroll_to_bottom()
382 self._window.setWindowTitle("Contacts")
385 def _populate_number_selector(self, selector, cid, numbers):
386 while 0 < selector.count():
387 selector.removeItem(0)
388 for number, description in numbers:
390 label = "%s - %s" % (number, description)
393 selector.addItem(label)
394 selector.setVisible(True)
396 selector.setEnabled(True)
398 selector.setEnabled(False)
399 callback = functools.partial(
400 self._on_change_number,
403 callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
404 selector.currentIndexChanged.connect(
405 QtCore.pyqtSlot(int)(callback)
408 def _scroll_to_bottom(self):
409 self._scrollEntry.ensureWidgetVisible(self._smsEntry)
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("")
419 @misc_utils.log_exception(_moduleLogger)
420 def _on_call_clicked(self):
421 self._session.draft.call()
422 self._smsEntry.setPlainText("")
424 @misc_utils.log_exception(_moduleLogger)
425 def _on_remove_contact(self, cid):
426 self._session.draft.remove_contact(cid)
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)
435 @misc_utils.log_exception(_moduleLogger)
436 def _on_recipients_changed(self):
437 self._update_recipients()
440 @misc_utils.log_exception(_moduleLogger)
441 def _on_op_finished(self):
445 @misc_utils.log_exception(_moduleLogger)
446 def _on_letter_count_changed(self):
447 self._update_letter_count()
448 self._update_button_state()
451 class DelayedWidget(object):
453 def __init__(self, app):
454 self._layout = QtGui.QVBoxLayout()
455 self._widget = QtGui.QWidget()
456 self._widget.setLayout(self._layout)
459 self._isEnabled = True
466 return self._child is not None
468 def set_child(self, child):
469 if self._child is not None:
470 self._layout.removeWidget(self._child.toplevel)
472 if self._child is not None:
473 self._layout.addWidget(self._child.toplevel)
478 self._child.disable()
481 self._isEnabled = True
482 if self._child is not None:
486 self._isEnabled = False
487 if self._child is not None:
488 self._child.disable()
491 if self._child is not None:
495 if self._child is not None:
496 self._child.refresh()
499 class Dialpad(object):
501 def __init__(self, app, session, errorLog):
503 self._session = session
504 self._errorLog = errorLog
506 self._plus = self._generate_key_button("+", "")
507 self._entry = QtGui.QLineEdit()
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)
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)
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)
534 self._padLayout = QtGui.QGridLayout()
535 rows = [0, 0, 0, 1, 1, 1, 2, 2, 2]
536 columns = [0, 1, 2] * 3
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
552 self._padLayout.addWidget(self._smsButton, 3, 0)
553 self._padLayout.addWidget(
554 self._generate_key_button("0", ""), 3, 1, QtCore.Qt.AlignCenter
556 self._padLayout.addWidget(self._callButton, 3, 2)
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)
569 self._smsButton.setEnabled(True)
570 self._callButton.setEnabled(True)
573 self._smsButton.setEnabled(False)
574 self._callButton.setEnabled(False)
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)
587 if len(letters) == 0:
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)
596 for letter in letters:
597 pieItem = self._generate_button_slice(letter)
598 button.insertItem(pieItem)
601 pieItem = qtpie.PieFiling.NULL_CENTER
602 button.insertItem(pieItem)
604 raise NotImplementedError("Cannot handle %r" % letters)
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)
614 @misc_utils.log_exception(_moduleLogger)
615 def _on_keypress(self, key):
616 self._entry.insert(key)
618 @misc_utils.log_exception(_moduleLogger)
619 def _on_backspace(self, toggled = False):
620 self._entry.backspace()
622 @misc_utils.log_exception(_moduleLogger)
623 def _on_clear_text(self, toggled = False):
627 @QtCore.pyqtSlot(bool)
628 @misc_utils.log_exception(_moduleLogger)
629 def _on_sms_clicked(self, checked = False):
630 number = str(self._entry.text())
636 numbersWithDescriptions = [(number, "")]
637 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
640 @QtCore.pyqtSlot(bool)
641 @misc_utils.log_exception(_moduleLogger)
642 def _on_call_clicked(self, checked = False):
643 number = str(self._entry.text())
649 numbersWithDescriptions = [(number, "")]
650 self._session.draft.clear()
651 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
652 self._session.draft.call()
655 class History(object):
663 HISTORY_ITEM_TYPES = ["Received", "Missed", "Placed", "All"]
664 HISTORY_COLUMNS = ["When", "What", "Number", "From"]
665 assert len(HISTORY_COLUMNS) == MAX_IDX
667 def __init__(self, app, session, errorLog):
668 self._selectedFilter = self.HISTORY_ITEM_TYPES[-1]
670 self._session = session
671 self._session.historyUpdated.connect(self._on_history_updated)
672 self._errorLog = errorLog
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)
679 self._typeSelection.currentIndexChanged[str].connect(self._on_filter_changed)
681 self._itemStore = QtGui.QStandardItemModel()
682 self._itemStore.setHorizontalHeaderLabels(self.HISTORY_COLUMNS)
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)
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)
699 self._populate_items()
706 self._itemView.setEnabled(True)
709 self._itemView.setEnabled(False)
712 self._itemView.clear()
715 self._session.update_history()
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)
728 if not name or name == number:
729 name = event["location"]
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
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)
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()
755 @misc_utils.log_exception(_moduleLogger)
756 def _on_history_updated(self):
757 self._populate_items()
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()
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
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:
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)
789 class Messages(object):
792 VOICEMAIL_MESSAGES = "Voicemail"
793 TEXT_MESSAGES = "SMS"
794 ALL_TYPES = "All Messages"
795 MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
797 UNREAD_STATUS = "Unread"
798 UNARCHIVED_STATUS = "Inbox"
800 MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
802 _MIN_MESSAGES_SHOWN = 4
804 def __init__(self, app, session, errorLog):
805 self._selectedTypeFilter = self.ALL_TYPES
806 self._selectedStatusFilter = self.ALL_STATUS
808 self._session = session
809 self._session.messagesUpdated.connect(self._on_messages_updated)
810 self._errorLog = errorLog
812 self._typeSelection = QtGui.QComboBox()
813 self._typeSelection.addItems(self.MESSAGE_TYPES)
814 self._typeSelection.setCurrentIndex(
815 self.MESSAGE_TYPES.index(self._selectedTypeFilter)
817 self._typeSelection.currentIndexChanged[str].connect(self._on_type_filter_changed)
819 self._statusSelection = QtGui.QComboBox()
820 self._statusSelection.addItems(self.MESSAGE_STATUSES)
821 self._statusSelection.setCurrentIndex(
822 self.MESSAGE_STATUSES.index(self._selectedStatusFilter)
824 self._statusSelection.currentIndexChanged[str].connect(self._on_status_filter_changed)
826 self._selectionLayout = QtGui.QHBoxLayout()
827 self._selectionLayout.addWidget(self._typeSelection)
828 self._selectionLayout.addWidget(self._statusSelection)
830 self._itemStore = QtGui.QStandardItemModel()
831 self._itemStore.setHorizontalHeaderLabels(["Messages"])
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)
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)
850 self._populate_items()
857 self._itemView.setEnabled(True)
860 self._itemView.setEnabled(False)
863 self._itemView.clear()
866 self._session.update_messages()
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"]
876 self.UNREAD_STATUS: isUnarchived and isUnread,
877 self.UNARCHIVED_STATUS: isUnarchived,
878 self.ALL_STATUS: True,
879 }[self._selectedStatusFilter]
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)
887 if not name or name == number:
888 name = item["location"]
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], )
899 messages = ("No Transcription", )
902 "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
903 for messagePart in messageParts
906 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (name, prettyNumber, relTime)
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):])
915 collapsedMessages = expandedMessages
917 item = dict(item.iteritems())
918 item["collapsedMessages"] = "<br/>\n".join(collapsedMessages)
919 item["expandedMessages"] = "<br/>\n".join(expandedMessages)
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)
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()
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()
942 @misc_utils.log_exception(_moduleLogger)
943 def _on_messages_updated(self):
944 self._populate_items()
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()
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")])
960 contactId = str(contactDetails[QtCore.QString("id")])
962 description = str(contactDetails[QtCore.QString("expandedMessages")])
963 numbersWithDescriptions = [(number, "")]
964 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
967 class Contacts(object):
969 def __init__(self, app, session, errorLog):
970 self._selectedFilter = ""
972 self._session = session
973 self._session.contactsUpdated.connect(self._on_contacts_updated)
974 self._errorLog = errorLog
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)
982 self._itemStore = QtGui.QStandardItemModel()
983 self._itemStore.setHorizontalHeaderLabels(["Contacts"])
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)
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)
1000 self._populate_items()
1007 self._itemView.setEnabled(True)
1010 self._itemView.setEnabled(False)
1013 self._itemView.clear()
1016 self._session.update_contacts()
1018 def _populate_items(self):
1019 self._itemStore.clear()
1021 contacts = list(self._session.get_contacts().itervalues())
1022 contacts.sort(key=lambda contact: contact["name"].lower())
1023 for item in contacts:
1025 numbers = item["numbers"]
1026 nameItem = QtGui.QStandardItem(name)
1027 nameItem.setEditable(False)
1028 nameItem.setCheckable(False)
1029 nameItem.setData(item)
1031 self._itemStore.appendRow(row)
1033 @QtCore.pyqtSlot(str)
1034 @misc_utils.log_exception(_moduleLogger)
1035 def _on_filter_changed(self, newItem):
1036 self._selectedFilter = str(newItem)
1039 @misc_utils.log_exception(_moduleLogger)
1040 def _on_contacts_updated(self):
1041 self._populate_items()
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()
1050 name = str(contactDetails[QtCore.QString("name")])
1052 name = str(contactDetails[QtCore.QString("location")])
1056 contactId = str(contactDetails[QtCore.QString("contactId")])
1057 numbers = contactDetails[QtCore.QString("numbers")]
1061 for (k, v) in number.iteritems()
1063 for number in numbers
1065 numbersWithDescriptions = [
1067 number["phoneNumber"],
1068 self._choose_phonetype(number),
1070 for number in numbers
1074 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
1077 def _choose_phonetype(numberDetails):
1078 if "phoneTypeName" in numberDetails:
1079 return numberDetails["phoneTypeName"]
1080 elif "phoneType" in numberDetails:
1081 return numberDetails["phoneType"]
1086 class MainWindow(object):
1100 assert len(_TAB_TITLES) == MAX_TABS
1108 assert len(_TAB_CLASS) == MAX_TABS
1110 def __init__(self, parent, app):
1111 self._fsContactsPath = os.path.join(constants._data_path_, "contacts")
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)
1119 self._credentialsDialog = None
1120 self._smsEntryDialog = None
1122 self._errorLog = qui_utils.QErrorLog()
1123 self._errorDisplay = qui_utils.ErrorDisplay(self._errorLog)
1125 self._tabsContents = [
1126 DelayedWidget(self._app)
1127 for i in xrange(self.MAX_TABS)
1129 for tab in self._tabsContents:
1132 self._tabWidget = QtGui.QTabWidget()
1133 if qui_utils.screen_orientation() == QtCore.Qt.Vertical:
1134 self._tabWidget.setTabPosition(QtGui.QTabWidget.South)
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)
1141 self._layout = QtGui.QVBoxLayout()
1142 self._layout.addWidget(self._errorDisplay.toplevel)
1143 self._layout.addWidget(self._tabWidget)
1145 centralWidget = QtGui.QWidget()
1146 centralWidget.setLayout(self._layout)
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)
1155 self._loginTabAction = QtGui.QAction(None)
1156 self._loginTabAction.setText("Login")
1157 self._loginTabAction.triggered.connect(self._on_login_requested)
1159 self._importTabAction = QtGui.QAction(None)
1160 self._importTabAction.setText("Import")
1161 self._importTabAction.triggered.connect(self._on_import)
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)
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)
1174 fileMenu = self._window.menuBar().addMenu("&File")
1175 fileMenu.addAction(self._loginTabAction)
1176 fileMenu.addAction(self._refreshTabAction)
1178 toolsMenu = self._window.menuBar().addMenu("&Tools")
1179 toolsMenu.addAction(self._importTabAction)
1181 self._window.addAction(self._closeWindowAction)
1182 self._window.addAction(self._app.quitAction)
1183 self._window.addAction(self._app.fullscreenAction)
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)
1191 viewMenu = self._window.menuBar().addMenu("&View")
1192 viewMenu.addAction(self._app.fullscreenAction)
1194 toolsMenu = self._window.menuBar().addMenu("&Tools")
1195 toolsMenu.addAction(self._importTabAction)
1197 self._window.addAction(self._app.logAction)
1199 self._initialize_tab(self._tabWidget.currentIndex())
1200 self.set_fullscreen(self._app.fullscreenAction.isChecked())
1207 def walk_children(self):
1212 for child in self.walk_children():
1216 for child in self.walk_children():
1221 for child in self.walk_children():
1222 child.window.destroyed.disconnect(self._on_child_close)
1224 self._window.close()
1226 def set_fullscreen(self, isFullscreen):
1228 self._window.showFullScreen()
1230 self._window.showNormal()
1231 for child in self.walk_children():
1232 child.set_fullscreen(isFullscreen)
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()
1241 @QtCore.pyqtSlot(str)
1242 @misc_utils.log_exception(_moduleLogger)
1243 def _on_session_error(self, message):
1244 self._errorLog.push_message(message)
1247 @misc_utils.log_exception(_moduleLogger)
1248 def _on_login(self):
1249 for tab in self._tabsContents:
1253 @misc_utils.log_exception(_moduleLogger)
1254 def _on_logout(self):
1255 for tab in self._tabsContents:
1259 @misc_utils.log_exception(_moduleLogger)
1260 def _on_recipients_changed(self):
1261 if self._session.draft.get_num_contacts() == 0:
1264 if self._smsEntryDialog is None:
1265 self._smsEntryDialog = SMSEntryWindow(self.window, self._app, self._session, self._errorLog)
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)
1277 @QtCore.pyqtSlot(int)
1278 @misc_utils.log_exception(_moduleLogger)
1279 def _on_tab_changed(self, index):
1280 self._initialize_tab(index)
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()
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)")
1296 shutil.copy2(csvName, self._fsContactsPath)
1299 @QtCore.pyqtSlot(bool)
1300 @misc_utils.log_exception(_moduleLogger)
1301 def _on_close_window(self, checked = True):
1305 def make_ugly(prettynumber):
1307 function to take a phone number and strip out all non-numeric
1310 >>> make_ugly("+012-(345)-678-90")
1313 return normalize_number(prettynumber)
1316 def normalize_number(prettynumber):
1318 function to take a phone number and strip out all non-numeric
1321 >>> normalize_number("+012-(345)-678-90")
1323 >>> normalize_number("1-(345)-678-9000")
1325 >>> normalize_number("+1-(345)-678-9000")
1328 uglynumber = re.sub('[^0-9+]', '', prettynumber)
1330 if uglynumber.startswith("+"):
1332 elif uglynumber.startswith("1"):
1333 uglynumber = "+"+uglynumber
1334 elif 10 <= len(uglynumber):
1335 assert uglynumber[0] not in ("+", "1")
1336 uglynumber = "+1"+uglynumber
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:], )
1352 def _make_pretty_local(phonenumber):
1353 prettynumber = "%s" % (phonenumber[0:3], )
1354 if 3 < len(phonenumber):
1355 prettynumber += "-%s" % (phonenumber[3:], )
1359 def _make_pretty_international(phonenumber):
1360 prettynumber = phonenumber
1361 if phonenumber.startswith("1"):
1363 prettynumber += _make_pretty_with_areacode(phonenumber[1:])
1367 def make_pretty(phonenumber):
1369 Function to take a phone number and return the pretty version
1371 if phonenumber begins with 0:
1373 if phonenumber begins with 1: ( for gizmo callback numbers )
1375 if phonenumber is 13 digits:
1377 if phonenumber is 10 digits:
1379 >>> make_pretty("12")
1381 >>> make_pretty("1234567")
1383 >>> make_pretty("2345678901")
1385 >>> make_pretty("12345678901")
1387 >>> make_pretty("01234567890")
1389 >>> make_pretty("+01234567890")
1391 >>> make_pretty("+12")
1393 >>> make_pretty("+123")
1395 >>> make_pretty("+1234")
1398 if phonenumber is None or phonenumber is "":
1401 phonenumber = normalize_number(phonenumber)
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)
1414 prettynumber = phonenumber
1415 return prettynumber.strip()
1418 def abbrev_relative_date(date):
1420 >>> abbrev_relative_date("42 hours ago")
1422 >>> abbrev_relative_date("2 days ago")
1424 >>> abbrev_relative_date("4 weeks ago")
1427 parts = date.split(" ")
1428 return "%s %s" % (parts[0], parts[1][0])
1432 app = QtGui.QApplication([])
1433 handle = Dialcentral(app)
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)
1442 os.makedirs(constants._data_path_)