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._dialButton = QtGui.QPushButton("Dial")
281 self._buttonLayout = QtGui.QHBoxLayout()
282 self._buttonLayout.addWidget(self._characterCountLabel)
283 self._buttonLayout.addWidget(self._singleNumberSelector)
284 self._buttonLayout.addWidget(self._smsButton)
285 self._buttonLayout.addWidget(self._dialButton)
287 self._layout = QtGui.QVBoxLayout()
288 self._layout.addWidget(self._scrollEntry)
289 self._layout.addLayout(self._buttonLayout)
291 centralWidget = QtGui.QWidget()
292 centralWidget.setLayout(self._layout)
294 self._window = QtGui.QMainWindow(parent)
295 qui_utils.set_autorient(self._window, True)
296 qui_utils.set_stackable(self._window, True)
297 self._window.setWindowTitle("Contact")
298 self._window.setCentralWidget(centralWidget)
300 self._update_recipients()
302 def _update_letter_count(self):
303 count = self._smsEntry.toPlainText().size()
304 self._characterCountLabel.setText("Letters: %s" % count)
306 def _update_button_state(self):
307 if len(self._contacts) == 0:
308 self._dialButton.setEnabled(False)
309 self._smsButton.setEnabled(False)
310 elif len(self._contacts) == 1:
311 count = self._smsEntry.toPlainText().size()
313 self._dialButton.setEnabled(True)
314 self._smsButton.setEnabled(False)
316 self._dialButton.setEnabled(False)
317 self._smsButton.setEnabled(True)
319 self._dialButton.setEnabled(False)
320 self._smsButton.setEnabled(True)
322 def _update_recipients(self):
323 draftContactsCount = self._session.draft.get_num_contacts()
324 if draftContactsCount == 0:
326 elif draftContactsCount == 1:
327 (cid, ) = self._session.draft.get_contacts()
328 title = self._session.draft.get_title(cid)
329 description = self._session.draft.get_description(cid)
330 numbers = self._session.draft.get_numbers(cid)
332 self._targetList.setVisible(False)
334 self._history.setHtml(description)
335 self._history.setVisible(True)
337 self._history.setHtml("")
338 self._history.setVisible(False)
339 self._populate_number_selector(self._singleNumberSelector, cid, numbers)
341 self._scroll_to_bottom()
342 self._window.setWindowTitle(title)
345 self._targetList.setVisible(True)
346 while self._targetLayout.count():
347 removedLayoutItem = self._targetLayout.takeAt(self._targetLayout.count()-1)
348 removedWidget = removedLayoutItem.widget()
349 removedWidget.close()
350 for cid in self._session.draft.get_contacts():
351 title = self._session.draft.get_title(cid)
352 description = self._session.draft.get_description(cid)
353 numbers = self._session.draft.get_numbers(cid)
355 titleLabel = QtGui.QLabel(title)
356 numberSelector = QtGui.QComboBox()
357 self._populate_number_selector(numberSelector, cid, numbers)
358 deleteButton = QtGui.QPushButton("Delete")
359 callback = functools.partial(
360 self._on_remove_contact,
363 callback.__name__ = "b"
364 deleteButton.clicked.connect(
365 QtCore.pyqtSlot()(callback)
368 rowLayout = QtGui.QHBoxLayout()
369 rowLayout.addWidget(titleLabel)
370 rowLayout.addWidget(numberSelector)
371 rowLayout.addWidget(deleteButton)
372 rowWidget = QtGui.QWidget()
373 rowWidget.setLayout(rowLayout)
374 self._targetLayout.addWidget(rowWidget)
375 self._history.setHtml("")
376 self._history.setVisible(False)
377 self._singleNumberSelector.setVisible(False)
379 self._scroll_to_bottom()
380 self._window.setWindowTitle("Contacts")
383 def _populate_number_selector(self, selector, cid, numbers):
384 while 0 < selector.count():
385 selector.removeItem(0)
386 for number, description in numbers:
388 label = "%s - %s" % (number, description)
391 selector.addItem(label)
392 selector.setVisible(True)
394 selector.setEnabled(True)
396 selector.setEnabled(False)
397 callback = functools.partial(
398 self._on_change_number,
401 callback.__name__ = "b"
402 selector.currentIndexChanged.connect(
403 QtCore.pyqtSlot(int)(callback)
406 def _scroll_to_bottom(self):
407 self._scrollEntry.ensureWidgetVisible(self._smsEntry)
409 @misc_utils.log_exception(_moduleLogger)
410 def _on_remove_contact(self, cid):
411 self._session.draft.remove_contact(cid)
413 @misc_utils.log_exception(_moduleLogger)
414 def _on_change_number(self, cid, index):
415 numbers = self._session.draft.get_numbers(cid)
416 number = numbers[index][0]
417 self._session.draft.set_selected_number(cid, number)
420 @misc_utils.log_exception(_moduleLogger)
421 def _on_recipients_changed(self):
422 self._update_recipients()
425 @misc_utils.log_exception(_moduleLogger)
426 def _on_op_finished(self):
430 @misc_utils.log_exception(_moduleLogger)
431 def _on_letter_count_changed(self):
432 self._update_letter_count()
433 self._update_button_state()
436 class DelayedWidget(object):
438 def __init__(self, app):
439 self._layout = QtGui.QVBoxLayout()
440 self._widget = QtGui.QWidget()
441 self._widget.setLayout(self._layout)
444 self._isEnabled = True
451 return self._child is not None
453 def set_child(self, child):
454 if self._child is not None:
455 self._layout.removeWidget(self._child.toplevel)
457 if self._child is not None:
458 self._layout.addWidget(self._child.toplevel)
463 self._child.disable()
466 self._isEnabled = True
467 if self._child is not None:
471 self._isEnabled = False
472 if self._child is not None:
473 self._child.disable()
476 if self._child is not None:
480 if self._child is not None:
481 self._child.refresh()
484 class Dialpad(object):
486 def __init__(self, app, session, errorLog):
488 self._session = session
489 self._errorLog = errorLog
491 self._plus = self._generate_key_button("+", "")
492 self._entry = QtGui.QLineEdit()
494 backAction = QtGui.QAction(None)
495 backAction.setText("Back")
496 backAction.triggered.connect(self._on_backspace)
497 backPieItem = qtpie.QActionPieItem(backAction)
498 clearAction = QtGui.QAction(None)
499 clearAction.setText("Clear")
500 clearAction.triggered.connect(self._on_clear_text)
501 clearPieItem = qtpie.QActionPieItem(clearAction)
502 self._back = qtpie.QPieButton(backPieItem)
503 self._back.set_center(backPieItem)
504 self._back.insertItem(qtpie.PieFiling.NULL_CENTER)
505 self._back.insertItem(clearPieItem)
506 self._back.insertItem(qtpie.PieFiling.NULL_CENTER)
507 self._back.insertItem(qtpie.PieFiling.NULL_CENTER)
509 self._entryLayout = QtGui.QHBoxLayout()
510 self._entryLayout.addWidget(self._plus, 0, QtCore.Qt.AlignCenter)
511 self._entryLayout.addWidget(self._entry, 10)
512 self._entryLayout.addWidget(self._back, 0, QtCore.Qt.AlignCenter)
514 self._smsButton = QtGui.QPushButton("SMS")
515 self._smsButton.clicked.connect(self._on_sms_clicked)
516 self._callButton = QtGui.QPushButton("Call")
517 self._callButton.clicked.connect(self._on_call_clicked)
519 self._padLayout = QtGui.QGridLayout()
520 rows = [0, 0, 0, 1, 1, 1, 2, 2, 2]
521 columns = [0, 1, 2] * 3
533 for (num, letters), (row, column) in zip(keys, zip(rows, columns)):
534 self._padLayout.addWidget(
535 self._generate_key_button(num, letters), row, column, QtCore.Qt.AlignCenter
537 self._padLayout.addWidget(self._smsButton, 3, 0)
538 self._padLayout.addWidget(
539 self._generate_key_button("0", ""), 3, 1, QtCore.Qt.AlignCenter
541 self._padLayout.addWidget(self._callButton, 3, 2)
543 self._layout = QtGui.QVBoxLayout()
544 self._layout.addLayout(self._entryLayout)
545 self._layout.addLayout(self._padLayout)
546 self._widget = QtGui.QWidget()
547 self._widget.setLayout(self._layout)
554 self._smsButton.setEnabled(True)
555 self._callButton.setEnabled(True)
558 self._smsButton.setEnabled(False)
559 self._callButton.setEnabled(False)
567 def _generate_key_button(self, center, letters):
568 centerPieItem = self._generate_button_slice(center)
569 button = qtpie.QPieButton(centerPieItem)
570 button.set_center(centerPieItem)
572 if len(letters) == 0:
574 pieItem = qtpie.PieFiling.NULL_CENTER
575 button.insertItem(pieItem)
576 elif len(letters) in [3, 4]:
577 for i in xrange(6 - len(letters)):
578 pieItem = qtpie.PieFiling.NULL_CENTER
579 button.insertItem(pieItem)
581 for letter in letters:
582 pieItem = self._generate_button_slice(letter)
583 button.insertItem(pieItem)
586 pieItem = qtpie.PieFiling.NULL_CENTER
587 button.insertItem(pieItem)
589 raise NotImplementedError("Cannot handle %r" % letters)
592 def _generate_button_slice(self, letter):
593 action = QtGui.QAction(None)
594 action.setText(letter)
595 action.triggered.connect(lambda: self._on_keypress(letter))
596 pieItem = qtpie.QActionPieItem(action)
599 @misc_utils.log_exception(_moduleLogger)
600 def _on_keypress(self, key):
601 self._entry.insert(key)
603 @misc_utils.log_exception(_moduleLogger)
604 def _on_backspace(self, toggled = False):
605 self._entry.backspace()
607 @misc_utils.log_exception(_moduleLogger)
608 def _on_clear_text(self, toggled = False):
612 @QtCore.pyqtSlot(bool)
613 @misc_utils.log_exception(_moduleLogger)
614 def _on_sms_clicked(self, checked = False):
615 number = str(self._entry.text())
621 numbersWithDescriptions = [(number, "")]
622 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
625 @QtCore.pyqtSlot(bool)
626 @misc_utils.log_exception(_moduleLogger)
627 def _on_call_clicked(self, checked = False):
628 number = str(self._entry.text())
634 numbersWithDescriptions = [(number, "")]
635 self._session.draft.clear()
636 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
637 self._session.draft.call()
640 class History(object):
648 HISTORY_ITEM_TYPES = ["Received", "Missed", "Placed", "All"]
649 HISTORY_COLUMNS = ["When", "What", "Number", "From"]
650 assert len(HISTORY_COLUMNS) == MAX_IDX
652 def __init__(self, app, session, errorLog):
653 self._selectedFilter = self.HISTORY_ITEM_TYPES[-1]
655 self._session = session
656 self._session.historyUpdated.connect(self._on_history_updated)
657 self._errorLog = errorLog
659 self._typeSelection = QtGui.QComboBox()
660 self._typeSelection.addItems(self.HISTORY_ITEM_TYPES)
661 self._typeSelection.setCurrentIndex(
662 self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
664 self._typeSelection.currentIndexChanged[str].connect(self._on_filter_changed)
666 self._itemStore = QtGui.QStandardItemModel()
667 self._itemStore.setHorizontalHeaderLabels(self.HISTORY_COLUMNS)
669 self._itemView = QtGui.QTreeView()
670 self._itemView.setModel(self._itemStore)
671 self._itemView.setUniformRowHeights(True)
672 self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
673 self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
674 self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
675 self._itemView.setHeaderHidden(True)
676 self._itemView.activated.connect(self._on_row_activated)
678 self._layout = QtGui.QVBoxLayout()
679 self._layout.addWidget(self._typeSelection)
680 self._layout.addWidget(self._itemView)
681 self._widget = QtGui.QWidget()
682 self._widget.setLayout(self._layout)
684 self._populate_items()
691 self._itemView.setEnabled(True)
694 self._itemView.setEnabled(False)
697 self._itemView.clear()
700 self._session.update_history()
702 def _populate_items(self):
703 self._itemStore.clear()
704 history = self._session.get_history()
705 history.sort(key=lambda item: item["time"], reverse=True)
706 for event in history:
707 if self._selectedFilter in [self.HISTORY_ITEM_TYPES[-1], event["action"]]:
708 relTime = abbrev_relative_date(event["relTime"])
709 action = event["action"]
710 number = event["number"]
711 prettyNumber = make_pretty(number)
713 if not name or name == number:
714 name = event["location"]
718 timeItem = QtGui.QStandardItem(relTime)
719 actionItem = QtGui.QStandardItem(action)
720 numberItem = QtGui.QStandardItem(prettyNumber)
721 nameItem = QtGui.QStandardItem(name)
722 row = timeItem, actionItem, numberItem, nameItem
724 item.setEditable(False)
725 item.setCheckable(False)
726 if item is not nameItem:
727 itemFont = item.font()
728 itemFont.setPointSize(max(itemFont.pointSize() - 3, 5))
729 item.setFont(itemFont)
730 numberItem.setData(event)
731 self._itemStore.appendRow(row)
733 @QtCore.pyqtSlot(str)
734 @misc_utils.log_exception(_moduleLogger)
735 def _on_filter_changed(self, newItem):
736 self._selectedFilter = str(newItem)
737 self._populate_items()
740 @misc_utils.log_exception(_moduleLogger)
741 def _on_history_updated(self):
742 self._populate_items()
744 @QtCore.pyqtSlot(QtCore.QModelIndex)
745 @misc_utils.log_exception(_moduleLogger)
746 def _on_row_activated(self, index):
747 rowIndex = index.row()
748 item = self._itemStore.item(rowIndex, self.NUMBER_IDX)
749 contactDetails = item.data().toPyObject()
751 title = str(self._itemStore.item(rowIndex, self.FROM_IDX).text())
752 number = str(contactDetails[QtCore.QString("number")])
753 contactId = number # ids don't seem too unique so using numbers
756 # @bug doesn't seem to print multiple entries
757 for i in xrange(self._itemStore.rowCount()):
758 iItem = self._itemStore.item(i, self.NUMBER_IDX)
759 iContactDetails = iItem.data().toPyObject()
760 iNumber = str(iContactDetails[QtCore.QString("number")])
761 if number != iNumber:
763 relTime = abbrev_relative_date(iContactDetails[QtCore.QString("relTime")])
764 action = str(iContactDetails[QtCore.QString("action")])
765 number = str(iContactDetails[QtCore.QString("number")])
766 prettyNumber = make_pretty(number)
767 rowItems = relTime, action, prettyNumber
768 descriptionRows.append("<tr><td>%s</td></tr>" % "</td><td>".join(rowItems))
769 description = "<table>%s</table>" % "".join(descriptionRows)
770 numbersWithDescriptions = [(str(contactDetails[QtCore.QString("number")]), "")]
771 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
774 class Messages(object):
777 VOICEMAIL_MESSAGES = "Voicemail"
778 TEXT_MESSAGES = "SMS"
779 ALL_TYPES = "All Messages"
780 MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
782 UNREAD_STATUS = "Unread"
783 UNARCHIVED_STATUS = "Inbox"
785 MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
787 _MIN_MESSAGES_SHOWN = 4
789 def __init__(self, app, session, errorLog):
790 self._selectedTypeFilter = self.ALL_TYPES
791 self._selectedStatusFilter = self.ALL_STATUS
793 self._session = session
794 self._session.messagesUpdated.connect(self._on_messages_updated)
795 self._errorLog = errorLog
797 self._typeSelection = QtGui.QComboBox()
798 self._typeSelection.addItems(self.MESSAGE_TYPES)
799 self._typeSelection.setCurrentIndex(
800 self.MESSAGE_TYPES.index(self._selectedTypeFilter)
802 self._typeSelection.currentIndexChanged[str].connect(self._on_type_filter_changed)
804 self._statusSelection = QtGui.QComboBox()
805 self._statusSelection.addItems(self.MESSAGE_STATUSES)
806 self._statusSelection.setCurrentIndex(
807 self.MESSAGE_STATUSES.index(self._selectedStatusFilter)
809 self._statusSelection.currentIndexChanged[str].connect(self._on_status_filter_changed)
811 self._selectionLayout = QtGui.QHBoxLayout()
812 self._selectionLayout.addWidget(self._typeSelection)
813 self._selectionLayout.addWidget(self._statusSelection)
815 self._itemStore = QtGui.QStandardItemModel()
816 self._itemStore.setHorizontalHeaderLabels(["Messages"])
818 self._htmlDelegate = qui_utils.QHtmlDelegate()
819 self._itemView = QtGui.QTreeView()
820 self._itemView.setModel(self._itemStore)
821 self._itemView.setUniformRowHeights(False)
822 self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
823 self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
824 self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
825 self._itemView.setHeaderHidden(True)
826 self._itemView.setItemDelegate(self._htmlDelegate)
827 self._itemView.activated.connect(self._on_row_activated)
829 self._layout = QtGui.QVBoxLayout()
830 self._layout.addLayout(self._selectionLayout)
831 self._layout.addWidget(self._itemView)
832 self._widget = QtGui.QWidget()
833 self._widget.setLayout(self._layout)
835 self._populate_items()
842 self._itemView.setEnabled(True)
845 self._itemView.setEnabled(False)
848 self._itemView.clear()
851 self._session.update_messages()
853 def _populate_items(self):
854 self._itemStore.clear()
855 rawMessages = self._session.get_messages()
856 rawMessages.sort(key=lambda item: item["time"], reverse=True)
857 for item in rawMessages:
858 isUnarchived = not item["isArchived"]
859 isUnread = not item["isRead"]
861 self.UNREAD_STATUS: isUnarchived and isUnread,
862 self.UNARCHIVED_STATUS: isUnarchived,
863 self.ALL_STATUS: True,
864 }[self._selectedStatusFilter]
866 visibleType = self._selectedTypeFilter in [item["type"], self.ALL_TYPES]
867 if visibleType and visibleStatus:
868 relTime = abbrev_relative_date(item["relTime"])
869 number = item["number"]
870 prettyNumber = make_pretty(number)
872 if not name or name == number:
873 name = item["location"]
877 messageParts = list(item["messageParts"])
878 if len(messageParts) == 0:
879 messages = ("No Transcription", )
880 elif len(messageParts) == 1:
881 if messageParts[0][1]:
882 messages = (messageParts[0][1], )
884 messages = ("No Transcription", )
887 "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
888 for messagePart in messageParts
891 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (name, prettyNumber, relTime)
893 expandedMessages = [firstMessage]
894 expandedMessages.extend(messages)
895 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
896 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
897 collapsedMessages = [firstMessage, secondMessage]
898 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
900 collapsedMessages = expandedMessages
902 item = dict(item.iteritems())
903 item["collapsedMessages"] = "<br/>\n".join(collapsedMessages)
904 item["expandedMessages"] = "<br/>\n".join(expandedMessages)
906 messageItem = QtGui.QStandardItem(item["collapsedMessages"])
907 # @bug Not showing all of a message
908 messageItem.setData(item)
909 messageItem.setEditable(False)
910 messageItem.setCheckable(False)
911 row = (messageItem, )
912 self._itemStore.appendRow(row)
914 @QtCore.pyqtSlot(str)
915 @misc_utils.log_exception(_moduleLogger)
916 def _on_type_filter_changed(self, newItem):
917 self._selectedTypeFilter = str(newItem)
918 self._populate_items()
920 @QtCore.pyqtSlot(str)
921 @misc_utils.log_exception(_moduleLogger)
922 def _on_status_filter_changed(self, newItem):
923 self._selectedStatusFilter = str(newItem)
924 self._populate_items()
927 @misc_utils.log_exception(_moduleLogger)
928 def _on_messages_updated(self):
929 self._populate_items()
931 @QtCore.pyqtSlot(QtCore.QModelIndex)
932 @misc_utils.log_exception(_moduleLogger)
933 def _on_row_activated(self, index):
934 rowIndex = index.row()
935 item = self._itemStore.item(rowIndex, 0)
936 contactDetails = item.data().toPyObject()
938 name = str(contactDetails[QtCore.QString("name")])
939 number = str(contactDetails[QtCore.QString("number")])
940 if not name or name == number:
941 name = str(contactDetails[QtCore.QString("location")])
945 contactId = str(contactDetails[QtCore.QString("id")])
947 description = str(contactDetails[QtCore.QString("expandedMessages")])
948 numbersWithDescriptions = [(number, "")]
949 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
952 class Contacts(object):
954 def __init__(self, app, session, errorLog):
955 self._selectedFilter = ""
957 self._session = session
958 self._session.contactsUpdated.connect(self._on_contacts_updated)
959 self._errorLog = errorLog
961 self._listSelection = QtGui.QComboBox()
962 self._listSelection.addItems([])
963 # @todo Implement more contact lists
964 #self._listSelection.setCurrentIndex(self.HISTORY_ITEM_TYPES.index(self._selectedFilter))
965 self._listSelection.currentIndexChanged[str].connect(self._on_filter_changed)
967 self._itemStore = QtGui.QStandardItemModel()
968 self._itemStore.setHorizontalHeaderLabels(["Contacts"])
970 self._itemView = QtGui.QTreeView()
971 self._itemView.setModel(self._itemStore)
972 self._itemView.setUniformRowHeights(True)
973 self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
974 self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
975 self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
976 self._itemView.setHeaderHidden(True)
977 self._itemView.activated.connect(self._on_row_activated)
979 self._layout = QtGui.QVBoxLayout()
980 self._layout.addWidget(self._listSelection)
981 self._layout.addWidget(self._itemView)
982 self._widget = QtGui.QWidget()
983 self._widget.setLayout(self._layout)
985 self._populate_items()
992 self._itemView.setEnabled(True)
995 self._itemView.setEnabled(False)
998 self._itemView.clear()
1001 self._session.update_contacts()
1003 def _populate_items(self):
1004 self._itemStore.clear()
1006 contacts = list(self._session.get_contacts().itervalues())
1007 contacts.sort(key=lambda contact: contact["name"].lower())
1008 for item in contacts:
1010 numbers = item["numbers"]
1011 nameItem = QtGui.QStandardItem(name)
1012 nameItem.setEditable(False)
1013 nameItem.setCheckable(False)
1014 nameItem.setData(item)
1016 self._itemStore.appendRow(row)
1018 @QtCore.pyqtSlot(str)
1019 @misc_utils.log_exception(_moduleLogger)
1020 def _on_filter_changed(self, newItem):
1021 self._selectedFilter = str(newItem)
1024 @misc_utils.log_exception(_moduleLogger)
1025 def _on_contacts_updated(self):
1026 self._populate_items()
1028 @QtCore.pyqtSlot(QtCore.QModelIndex)
1029 @misc_utils.log_exception(_moduleLogger)
1030 def _on_row_activated(self, index):
1031 rowIndex = index.row()
1032 item = self._itemStore.item(rowIndex, 0)
1033 contactDetails = item.data().toPyObject()
1035 name = str(contactDetails[QtCore.QString("name")])
1037 name = str(contactDetails[QtCore.QString("location")])
1041 contactId = str(contactDetails[QtCore.QString("contactId")])
1042 numbers = contactDetails[QtCore.QString("numbers")]
1046 for (k, v) in number.iteritems()
1048 for number in numbers
1050 numbersWithDescriptions = [
1052 number["phoneNumber"],
1053 self._choose_phonetype(number),
1055 for number in numbers
1059 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
1062 def _choose_phonetype(numberDetails):
1063 if "phoneTypeName" in numberDetails:
1064 return numberDetails["phoneTypeName"]
1065 elif "phoneType" in numberDetails:
1066 return numberDetails["phoneType"]
1071 class MainWindow(object):
1085 assert len(_TAB_TITLES) == MAX_TABS
1093 assert len(_TAB_CLASS) == MAX_TABS
1095 def __init__(self, parent, app):
1096 self._fsContactsPath = os.path.join(constants._data_path_, "contacts")
1098 self._session = session.Session()
1099 self._session.error.connect(self._on_session_error)
1100 self._session.loggedIn.connect(self._on_login)
1101 self._session.loggedOut.connect(self._on_logout)
1102 self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
1104 self._credentialsDialog = None
1105 self._smsEntryDialog = None
1107 self._errorLog = qui_utils.QErrorLog()
1108 self._errorDisplay = qui_utils.ErrorDisplay(self._errorLog)
1110 self._tabsContents = [
1111 DelayedWidget(self._app)
1112 for i in xrange(self.MAX_TABS)
1114 for tab in self._tabsContents:
1117 self._tabWidget = QtGui.QTabWidget()
1118 if qui_utils.screen_orientation() == QtCore.Qt.Vertical:
1119 self._tabWidget.setTabPosition(QtGui.QTabWidget.South)
1121 self._tabWidget.setTabPosition(QtGui.QTabWidget.West)
1122 for tabIndex, tabTitle in enumerate(self._TAB_TITLES):
1123 self._tabWidget.addTab(self._tabsContents[tabIndex].toplevel, tabTitle)
1124 self._tabWidget.currentChanged.connect(self._on_tab_changed)
1126 self._layout = QtGui.QVBoxLayout()
1127 self._layout.addWidget(self._errorDisplay.toplevel)
1128 self._layout.addWidget(self._tabWidget)
1130 centralWidget = QtGui.QWidget()
1131 centralWidget.setLayout(self._layout)
1133 self._window = QtGui.QMainWindow(parent)
1134 self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
1135 qui_utils.set_autorient(self._window, True)
1136 qui_utils.set_stackable(self._window, True)
1137 self._window.setWindowTitle("%s" % constants.__pretty_app_name__)
1138 self._window.setCentralWidget(centralWidget)
1140 self._loginTabAction = QtGui.QAction(None)
1141 self._loginTabAction.setText("Login")
1142 self._loginTabAction.triggered.connect(self._on_login_requested)
1144 self._importTabAction = QtGui.QAction(None)
1145 self._importTabAction.setText("Import")
1146 self._importTabAction.triggered.connect(self._on_import)
1148 self._refreshTabAction = QtGui.QAction(None)
1149 self._refreshTabAction.setText("Refresh")
1150 self._refreshTabAction.setShortcut(QtGui.QKeySequence("CTRL+r"))
1151 self._refreshTabAction.triggered.connect(self._on_refresh)
1153 self._closeWindowAction = QtGui.QAction(None)
1154 self._closeWindowAction.setText("Close")
1155 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
1156 self._closeWindowAction.triggered.connect(self._on_close_window)
1159 fileMenu = self._window.menuBar().addMenu("&File")
1160 fileMenu.addAction(self._loginTabAction)
1161 fileMenu.addAction(self._refreshTabAction)
1163 toolsMenu = self._window.menuBar().addMenu("&Tools")
1164 toolsMenu.addAction(self._importTabAction)
1166 self._window.addAction(self._closeWindowAction)
1167 self._window.addAction(self._app.quitAction)
1168 self._window.addAction(self._app.fullscreenAction)
1170 fileMenu = self._window.menuBar().addMenu("&File")
1171 fileMenu.addAction(self._loginTabAction)
1172 fileMenu.addAction(self._refreshTabAction)
1173 fileMenu.addAction(self._closeWindowAction)
1174 fileMenu.addAction(self._app.quitAction)
1176 viewMenu = self._window.menuBar().addMenu("&View")
1177 viewMenu.addAction(self._app.fullscreenAction)
1179 toolsMenu = self._window.menuBar().addMenu("&Tools")
1180 toolsMenu.addAction(self._importTabAction)
1182 self._window.addAction(self._app.logAction)
1184 self._initialize_tab(self._tabWidget.currentIndex())
1185 self.set_fullscreen(self._app.fullscreenAction.isChecked())
1192 def walk_children(self):
1197 for child in self.walk_children():
1201 for child in self.walk_children():
1206 for child in self.walk_children():
1207 child.window.destroyed.disconnect(self._on_child_close)
1209 self._window.close()
1211 def set_fullscreen(self, isFullscreen):
1213 self._window.showFullScreen()
1215 self._window.showNormal()
1216 for child in self.walk_children():
1217 child.set_fullscreen(isFullscreen)
1219 def _initialize_tab(self, index):
1220 assert index < self.MAX_TABS
1221 if not self._tabsContents[index].has_child():
1222 tab = self._TAB_CLASS[index](self._app, self._session, self._errorLog)
1223 self._tabsContents[index].set_child(tab)
1224 self._tabsContents[index].refresh()
1226 @QtCore.pyqtSlot(str)
1227 @misc_utils.log_exception(_moduleLogger)
1228 def _on_session_error(self, message):
1229 self._errorLog.push_message(message)
1232 @misc_utils.log_exception(_moduleLogger)
1233 def _on_login(self):
1234 for tab in self._tabsContents:
1238 @misc_utils.log_exception(_moduleLogger)
1239 def _on_logout(self):
1240 for tab in self._tabsContents:
1244 @misc_utils.log_exception(_moduleLogger)
1245 def _on_recipients_changed(self):
1246 if self._session.draft.get_num_contacts() == 0:
1249 if self._smsEntryDialog is None:
1250 self._smsEntryDialog = SMSEntryWindow(self.window, self._app, self._session, self._errorLog)
1254 @QtCore.pyqtSlot(bool)
1255 @misc_utils.log_exception(_moduleLogger)
1256 def _on_login_requested(self, checked = True):
1257 if self._credentialsDialog is None:
1258 self._credentialsDialog = CredentialsDialog()
1259 username, password = self._credentialsDialog.run("", "", self.window)
1260 self._session.login(username, password)
1262 @QtCore.pyqtSlot(int)
1263 @misc_utils.log_exception(_moduleLogger)
1264 def _on_tab_changed(self, index):
1265 self._initialize_tab(index)
1268 @QtCore.pyqtSlot(bool)
1269 @misc_utils.log_exception(_moduleLogger)
1270 def _on_refresh(self, checked = True):
1271 index = self._tabWidget.currentIndex()
1272 self._tabsContents[index].refresh()
1275 @QtCore.pyqtSlot(bool)
1276 @misc_utils.log_exception(_moduleLogger)
1277 def _on_import(self, checked = True):
1278 csvName = QtGui.QFileDialog.getOpenFileName(self._window, caption="Import", filter="CSV Files (*.csv)")
1281 shutil.copy2(csvName, self._fsContactsPath)
1284 @QtCore.pyqtSlot(bool)
1285 @misc_utils.log_exception(_moduleLogger)
1286 def _on_close_window(self, checked = True):
1290 def make_ugly(prettynumber):
1292 function to take a phone number and strip out all non-numeric
1295 >>> make_ugly("+012-(345)-678-90")
1298 return normalize_number(prettynumber)
1301 def normalize_number(prettynumber):
1303 function to take a phone number and strip out all non-numeric
1306 >>> normalize_number("+012-(345)-678-90")
1308 >>> normalize_number("1-(345)-678-9000")
1310 >>> normalize_number("+1-(345)-678-9000")
1313 uglynumber = re.sub('[^0-9+]', '', prettynumber)
1315 if uglynumber.startswith("+"):
1317 elif uglynumber.startswith("1"):
1318 uglynumber = "+"+uglynumber
1319 elif 10 <= len(uglynumber):
1320 assert uglynumber[0] not in ("+", "1")
1321 uglynumber = "+1"+uglynumber
1328 def _make_pretty_with_areacode(phonenumber):
1329 prettynumber = "(%s)" % (phonenumber[0:3], )
1330 if 3 < len(phonenumber):
1331 prettynumber += " %s" % (phonenumber[3:6], )
1332 if 6 < len(phonenumber):
1333 prettynumber += "-%s" % (phonenumber[6:], )
1337 def _make_pretty_local(phonenumber):
1338 prettynumber = "%s" % (phonenumber[0:3], )
1339 if 3 < len(phonenumber):
1340 prettynumber += "-%s" % (phonenumber[3:], )
1344 def _make_pretty_international(phonenumber):
1345 prettynumber = phonenumber
1346 if phonenumber.startswith("1"):
1348 prettynumber += _make_pretty_with_areacode(phonenumber[1:])
1352 def make_pretty(phonenumber):
1354 Function to take a phone number and return the pretty version
1356 if phonenumber begins with 0:
1358 if phonenumber begins with 1: ( for gizmo callback numbers )
1360 if phonenumber is 13 digits:
1362 if phonenumber is 10 digits:
1364 >>> make_pretty("12")
1366 >>> make_pretty("1234567")
1368 >>> make_pretty("2345678901")
1370 >>> make_pretty("12345678901")
1372 >>> make_pretty("01234567890")
1374 >>> make_pretty("+01234567890")
1376 >>> make_pretty("+12")
1378 >>> make_pretty("+123")
1380 >>> make_pretty("+1234")
1383 if phonenumber is None or phonenumber is "":
1386 phonenumber = normalize_number(phonenumber)
1388 if phonenumber[0] == "+":
1389 prettynumber = _make_pretty_international(phonenumber[1:])
1390 if not prettynumber.startswith("+"):
1391 prettynumber = "+"+prettynumber
1392 elif 8 < len(phonenumber) and phonenumber[0] in ("1", ):
1393 prettynumber = _make_pretty_international(phonenumber)
1394 elif 7 < len(phonenumber):
1395 prettynumber = _make_pretty_with_areacode(phonenumber)
1396 elif 3 < len(phonenumber):
1397 prettynumber = _make_pretty_local(phonenumber)
1399 prettynumber = phonenumber
1400 return prettynumber.strip()
1403 def abbrev_relative_date(date):
1405 >>> abbrev_relative_date("42 hours ago")
1407 >>> abbrev_relative_date("2 days ago")
1409 >>> abbrev_relative_date("4 weeks ago")
1412 parts = date.split(" ")
1413 return "%s %s" % (parts[0], parts[1][0])
1417 app = QtGui.QApplication([])
1418 handle = Dialcentral(app)
1423 if __name__ == "__main__":
1424 logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
1425 logging.basicConfig(level=logging.DEBUG, format=logFormat)
1427 os.makedirs(constants._data_path_)