4 from __future__ import with_statement
13 from PyQt4 import QtGui
14 from PyQt4 import QtCore
17 from util import qui_utils
18 from util import qtpie
19 from util import misc as misc_utils
24 _moduleLogger = logging.getLogger(__name__)
30 class Dialcentral(object):
33 os.path.dirname(__file__),
34 os.path.join(os.path.dirname(__file__), "../data"),
35 os.path.join(os.path.dirname(__file__), "../lib"),
36 '/usr/share/%s' % constants.__app_name__,
37 '/usr/lib/%s' % constants.__app_name__,
40 def __init__(self, app):
43 self._hiddenCategories = set()
44 self._hiddenUnits = {}
45 self._clipboard = QtGui.QApplication.clipboard()
47 self._mainWindow = None
49 self._fullscreenAction = QtGui.QAction(None)
50 self._fullscreenAction.setText("Fullscreen")
51 self._fullscreenAction.setCheckable(True)
52 self._fullscreenAction.setShortcut(QtGui.QKeySequence("CTRL+Enter"))
53 self._fullscreenAction.toggled.connect(self._on_toggle_fullscreen)
55 self._logAction = QtGui.QAction(None)
56 self._logAction.setText("Log")
57 self._logAction.setShortcut(QtGui.QKeySequence("CTRL+l"))
58 self._logAction.triggered.connect(self._on_log)
60 self._quitAction = QtGui.QAction(None)
61 self._quitAction.setText("Quit")
62 self._quitAction.setShortcut(QtGui.QKeySequence("CTRL+q"))
63 self._quitAction.triggered.connect(self._on_quit)
65 self._app.lastWindowClosed.connect(self._on_app_quit)
68 self._mainWindow = MainWindow(None, self)
69 self._mainWindow.window.destroyed.connect(self._on_child_close)
71 def load_settings(self):
73 with open(constants._user_settings_, "r") as settingsFile:
74 settings = simplejson.load(settingsFile)
76 _moduleLogger.info("No settings")
79 _moduleLogger.info("Settings were corrupt")
82 self._fullscreenAction.setChecked(settings.get("isFullScreen", False))
84 def save_settings(self):
86 "isFullScreen": self._fullscreenAction.isChecked(),
88 with open(constants._user_settings_, "w") as settingsFile:
89 simplejson.dump(settings, settingsFile)
92 def fullscreenAction(self):
93 return self._fullscreenAction
97 return self._logAction
100 def quitAction(self):
101 return self._quitAction
103 def _close_windows(self):
104 if self._mainWindow is not None:
105 self._mainWindow.window.destroyed.disconnect(self._on_child_close)
106 self._mainWindow.close()
107 self._mainWindow = None
110 @QtCore.pyqtSlot(bool)
111 @misc_utils.log_exception(_moduleLogger)
112 def _on_app_quit(self, checked = False):
115 @QtCore.pyqtSlot(QtCore.QObject)
116 @misc_utils.log_exception(_moduleLogger)
117 def _on_child_close(self, obj = None):
118 self._mainWindow = None
121 @QtCore.pyqtSlot(bool)
122 @misc_utils.log_exception(_moduleLogger)
123 def _on_toggle_fullscreen(self, checked = False):
124 for window in self._walk_children():
125 window.set_fullscreen(checked)
128 @QtCore.pyqtSlot(bool)
129 @misc_utils.log_exception(_moduleLogger)
130 def _on_log(self, checked = False):
131 with open(constants._user_logpath_, "r") as f:
132 logLines = f.xreadlines()
133 log = "".join(logLines)
134 self._clipboard.setText(log)
137 @QtCore.pyqtSlot(bool)
138 @misc_utils.log_exception(_moduleLogger)
139 def _on_quit(self, checked = False):
140 self._close_windows()
143 class CredentialsDialog(object):
146 self._usernameField = QtGui.QLineEdit()
147 self._passwordField = QtGui.QLineEdit()
148 self._passwordField.setEchoMode(QtGui.QLineEdit.PasswordEchoOnEdit)
150 self._credLayout = QtGui.QGridLayout()
151 self._credLayout.addWidget(QtGui.QLabel("Username"), 0, 0)
152 self._credLayout.addWidget(self._usernameField, 0, 1)
153 self._credLayout.addWidget(QtGui.QLabel("Password"), 1, 0)
154 self._credLayout.addWidget(self._passwordField, 1, 1)
156 self._loginButton = QtGui.QPushButton("&Login")
157 self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
158 self._buttonLayout.addButton(self._loginButton, QtGui.QDialogButtonBox.AcceptRole)
160 self._layout = QtGui.QVBoxLayout()
161 self._layout.addLayout(self._credLayout)
162 self._layout.addWidget(self._buttonLayout)
164 self._dialog = QtGui.QDialog()
165 self._dialog.setWindowTitle("Login")
166 self._dialog.setLayout(self._layout)
167 self._dialog.setAttribute(QtCore.Qt.WA_DeleteOnClose, False)
168 qui_utils.set_autorient(self._dialog, True)
169 self._buttonLayout.accepted.connect(self._dialog.accept)
170 self._buttonLayout.rejected.connect(self._dialog.reject)
172 def run(self, defaultUsername, defaultPassword, parent=None):
173 self._dialog.setParent(parent, QtCore.Qt.Dialog)
175 self._usernameField.setText(defaultUsername)
176 self._passwordField.setText(defaultPassword)
178 response = self._dialog.exec_()
179 if response == QtGui.QDialog.Accepted:
180 return str(self._usernameField.text()), str(self._passwordField.text())
181 elif response == QtGui.QDialog.Rejected:
182 raise RuntimeError("Login Cancelled")
184 self._dialog.setParent(None, QtCore.Qt.Dialog)
187 class AccountDialog(object):
190 self._accountNumberLabel = QtGui.QLabel("NUMBER NOT SET")
191 self._clearButton = QtGui.QPushButton("Clear Account")
192 self._clearButton.clicked.connect(self._on_clear)
193 self._doClear = False
195 self._credLayout = QtGui.QGridLayout()
196 self._credLayout.addWidget(QtGui.QLabel("Account"), 0, 0)
197 self._credLayout.addWidget(self._accountNumberLabel, 0, 1)
198 self._credLayout.addWidget(QtGui.QLabel("Callback"), 1, 0)
199 self._credLayout.addWidget(self._clearButton, 2, 1)
201 self._loginButton = QtGui.QPushButton("&Login")
202 self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
203 self._buttonLayout.addButton(self._loginButton, QtGui.QDialogButtonBox.AcceptRole)
205 self._layout = QtGui.QVBoxLayout()
206 self._layout.addLayout(self._credLayout)
207 self._layout.addLayout(self._buttonLayout)
209 self._dialog = QtGui.QDialog()
210 self._dialog.setWindowTitle("Login")
211 self._dialog.setLayout(self._layout)
212 qui_utils.set_autorient(self._dialog, True)
213 self._buttonLayout.accepted.connect(self._dialog.accept)
214 self._buttonLayout.rejected.connect(self._dialog.reject)
220 accountNumber = property(
221 lambda self: str(self._accountNumberLabel.text()),
222 lambda self, num: self._accountNumberLabel.setText(num),
225 def run(self, defaultUsername, defaultPassword, parent=None):
226 self._doClear = False
227 self._dialog.setParent(parent)
228 self._usernameField.setText(defaultUsername)
229 self._passwordField.setText(defaultPassword)
231 response = self._dialog.exec_()
232 if response == QtGui.QDialog.Accepted:
233 return str(self._usernameField.text()), str(self._passwordField.text())
234 elif response == QtGui.QDialog.Rejected:
235 raise RuntimeError("Login Cancelled")
238 @QtCore.pyqtSlot(bool)
239 def _on_clear(self, checked = False):
241 self._dialog.accept()
244 class SMSEntryWindow(object):
246 def __init__(self, parent, app, session, errorLog):
249 self._session = session
250 self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
251 self._session.draft.called.connect(self._on_op_finished)
252 self._session.draft.sentMessage.connect(self._on_op_finished)
253 self._session.draft.cancelled.connect(self._on_op_finished)
254 self._errorLog = errorLog
256 self._history = QtGui.QListView()
257 self._smsEntry = QtGui.QTextEdit()
258 self._smsEntry.textChanged.connect(self._on_letter_count_changed)
260 self._entryLayout = QtGui.QVBoxLayout()
261 self._entryLayout.addWidget(self._history)
262 self._entryLayout.addWidget(self._smsEntry)
263 self._entryWidget = QtGui.QWidget()
264 self._entryWidget.setLayout(self._entryLayout)
265 self._scrollEntry = QtGui.QScrollArea()
266 self._scrollEntry.setWidget(self._entryWidget)
267 self._scrollEntry.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignBottom)
268 self._scrollEntry.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
269 self._scrollEntry.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
271 self._characterCountLabel = QtGui.QLabel("Letters: %s" % 0)
272 self._numberSelector = None
273 self._smsButton = QtGui.QPushButton("SMS")
274 self._dialButton = QtGui.QPushButton("Dial")
276 self._buttonLayout = QtGui.QHBoxLayout()
277 self._buttonLayout.addWidget(self._characterCountLabel)
278 self._buttonLayout.addWidget(self._smsButton)
279 self._buttonLayout.addWidget(self._dialButton)
281 self._layout = QtGui.QVBoxLayout()
282 self._layout.addWidget(self._scrollEntry)
283 self._layout.addLayout(self._buttonLayout)
285 centralWidget = QtGui.QWidget()
286 centralWidget.setLayout(self._layout)
288 self._window = QtGui.QMainWindow(parent)
289 qui_utils.set_autorient(self._window, True)
290 qui_utils.set_stackable(self._window, True)
291 self._window.setWindowTitle("Contact")
292 self._window.setCentralWidget(centralWidget)
295 def _update_letter_count(self):
296 count = self._smsEntry.toPlainText().size()
297 self._characterCountLabel.setText("Letters: %s" % count)
299 def _update_button_state(self):
300 if len(self._contacts) == 0:
301 self._dialButton.setEnabled(False)
302 self._smsButton.setEnabled(False)
303 elif len(self._contacts) == 1:
304 count = self._smsEntry.toPlainText().size()
306 self._dialButton.setEnabled(True)
307 self._smsButton.setEnabled(False)
309 self._dialButton.setEnabled(False)
310 self._smsButton.setEnabled(True)
312 self._dialButton.setEnabled(False)
313 self._smsButton.setEnabled(True)
316 @misc_utils.log_exception(_moduleLogger)
317 def _on_recipients_changed(self):
318 draftContacts = len(self._session.draft.get_contacts())
319 if draftContacts == 0:
325 @misc_utils.log_exception(_moduleLogger)
326 def _on_op_finished(self):
330 @misc_utils.log_exception(_moduleLogger)
331 def _on_letter_count_changed(self):
332 self._update_letter_count()
333 self._update_button_state()
336 class DelayedWidget(object):
338 def __init__(self, app):
339 self._layout = QtGui.QVBoxLayout()
340 self._widget = QtGui.QWidget()
341 self._widget.setLayout(self._layout)
344 self._isEnabled = True
351 return self._child is not None
353 def set_child(self, child):
354 if self._child is not None:
355 self._layout.removeWidget(self._child.toplevel)
357 if self._child is not None:
358 self._layout.addWidget(self._child.toplevel)
363 self._child.disable()
366 self._isEnabled = True
367 if self._child is not None:
371 self._isEnabled = False
372 if self._child is not None:
373 self._child.disable()
376 if self._child is not None:
380 if self._child is not None:
381 self._child.refresh()
384 class Dialpad(object):
386 def __init__(self, app, session, errorLog):
388 self._session = session
389 self._errorLog = errorLog
391 self._plus = self._generate_key_button("+", "")
392 self._entry = QtGui.QLineEdit()
394 backAction = QtGui.QAction(None)
395 backAction.setText("Back")
396 backAction.triggered.connect(self._on_backspace)
397 backPieItem = qtpie.QActionPieItem(backAction)
398 clearAction = QtGui.QAction(None)
399 clearAction.setText("Clear")
400 clearAction.triggered.connect(self._on_clear_text)
401 clearPieItem = qtpie.QActionPieItem(clearAction)
402 self._back = qtpie.QPieButton(backPieItem)
403 self._back.set_center(backPieItem)
404 self._back.insertItem(qtpie.PieFiling.NULL_CENTER)
405 self._back.insertItem(clearPieItem)
406 self._back.insertItem(qtpie.PieFiling.NULL_CENTER)
407 self._back.insertItem(qtpie.PieFiling.NULL_CENTER)
409 self._entryLayout = QtGui.QHBoxLayout()
410 self._entryLayout.addWidget(self._plus, 0, QtCore.Qt.AlignCenter)
411 self._entryLayout.addWidget(self._entry, 10)
412 self._entryLayout.addWidget(self._back, 0, QtCore.Qt.AlignCenter)
414 self._smsButton = QtGui.QPushButton("SMS")
415 self._smsButton.clicked.connect(self._on_sms_clicked)
416 self._callButton = QtGui.QPushButton("Call")
417 self._callButton.clicked.connect(self._on_call_clicked)
419 self._padLayout = QtGui.QGridLayout()
420 rows = [0, 0, 0, 1, 1, 1, 2, 2, 2]
421 columns = [0, 1, 2] * 3
433 for (num, letters), (row, column) in zip(keys, zip(rows, columns)):
434 self._padLayout.addWidget(
435 self._generate_key_button(num, letters), row, column, QtCore.Qt.AlignCenter
437 self._padLayout.addWidget(self._smsButton, 3, 0)
438 self._padLayout.addWidget(
439 self._generate_key_button("0", ""), 3, 1, QtCore.Qt.AlignCenter
441 self._padLayout.addWidget(self._callButton, 3, 2)
443 self._layout = QtGui.QVBoxLayout()
444 self._layout.addLayout(self._entryLayout)
445 self._layout.addLayout(self._padLayout)
446 self._widget = QtGui.QWidget()
447 self._widget.setLayout(self._layout)
454 self._smsButton.setEnabled(True)
455 self._callButton.setEnabled(True)
458 self._smsButton.setEnabled(False)
459 self._callButton.setEnabled(False)
467 def _generate_key_button(self, center, letters):
468 centerPieItem = self._generate_button_slice(center)
469 button = qtpie.QPieButton(centerPieItem)
470 button.set_center(centerPieItem)
472 if len(letters) == 0:
474 pieItem = qtpie.PieFiling.NULL_CENTER
475 button.insertItem(pieItem)
476 elif len(letters) in [3, 4]:
477 for i in xrange(6 - len(letters)):
478 pieItem = qtpie.PieFiling.NULL_CENTER
479 button.insertItem(pieItem)
481 for letter in letters:
482 pieItem = self._generate_button_slice(letter)
483 button.insertItem(pieItem)
486 pieItem = qtpie.PieFiling.NULL_CENTER
487 button.insertItem(pieItem)
489 raise NotImplementedError("Cannot handle %r" % letters)
492 def _generate_button_slice(self, letter):
493 action = QtGui.QAction(None)
494 action.setText(letter)
495 action.triggered.connect(lambda: self._on_keypress(letter))
496 pieItem = qtpie.QActionPieItem(action)
499 @misc_utils.log_exception(_moduleLogger)
500 def _on_keypress(self, key):
501 self._entry.insert(key)
503 @misc_utils.log_exception(_moduleLogger)
504 def _on_backspace(self, toggled = False):
505 self._entry.backspace()
507 @misc_utils.log_exception(_moduleLogger)
508 def _on_clear_text(self, toggled = False):
512 @QtCore.pyqtSlot(bool)
513 @misc_utils.log_exception(_moduleLogger)
514 def _on_sms_clicked(self, checked = False):
515 number = str(self._entry.text())
521 numbersWithDescriptions = [(number, "")]
522 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
525 @QtCore.pyqtSlot(bool)
526 @misc_utils.log_exception(_moduleLogger)
527 def _on_call_clicked(self, checked = False):
528 number = str(self._entry.text())
534 numbersWithDescriptions = [(number, "")]
535 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
536 self._session.draft.call()
539 class History(object):
547 HISTORY_ITEM_TYPES = ["Received", "Missed", "Placed", "All"]
548 HISTORY_COLUMNS = ["When", "What", "Number", "From"]
549 assert len(HISTORY_COLUMNS) == MAX_IDX
551 def __init__(self, app, session, errorLog):
552 self._selectedFilter = self.HISTORY_ITEM_TYPES[-1]
554 self._session = session
555 self._session.historyUpdated.connect(self._on_history_updated)
556 self._errorLog = errorLog
558 self._typeSelection = QtGui.QComboBox()
559 self._typeSelection.addItems(self.HISTORY_ITEM_TYPES)
560 self._typeSelection.setCurrentIndex(
561 self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
563 self._typeSelection.currentIndexChanged[str].connect(self._on_filter_changed)
565 self._itemStore = QtGui.QStandardItemModel()
566 self._itemStore.setHorizontalHeaderLabels(self.HISTORY_COLUMNS)
568 self._itemView = QtGui.QTreeView()
569 self._itemView.setModel(self._itemStore)
570 self._itemView.setUniformRowHeights(True)
571 self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
572 self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
573 self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
574 self._itemView.setHeaderHidden(True)
575 self._itemView.activated.connect(self._on_row_activated)
577 self._layout = QtGui.QVBoxLayout()
578 self._layout.addWidget(self._typeSelection)
579 self._layout.addWidget(self._itemView)
580 self._widget = QtGui.QWidget()
581 self._widget.setLayout(self._layout)
583 self._populate_items()
590 self._itemView.setEnabled(True)
593 self._itemView.setEnabled(False)
596 self._itemView.clear()
599 self._session.update_history()
601 def _populate_items(self):
602 self._itemStore.clear()
603 history = self._session.get_history()
604 history.sort(key=lambda item: item["time"], reverse=True)
605 for event in history:
606 if self._selectedFilter in [self.HISTORY_ITEM_TYPES[-1], event["action"]]:
607 relTime = abbrev_relative_date(event["relTime"])
608 action = event["action"]
609 number = event["number"]
610 prettyNumber = make_pretty(number)
612 if not name or name == number:
613 name = event["location"]
617 timeItem = QtGui.QStandardItem(relTime)
618 actionItem = QtGui.QStandardItem(action)
619 numberItem = QtGui.QStandardItem(prettyNumber)
620 nameItem = QtGui.QStandardItem(name)
621 row = timeItem, actionItem, numberItem, nameItem
623 item.setEditable(False)
624 item.setCheckable(False)
625 if item is not nameItem:
626 itemFont = item.font()
627 itemFont.setPointSize(max(itemFont.pointSize() - 3, 5))
628 item.setFont(itemFont)
629 numberItem.setData(event)
630 self._itemStore.appendRow(row)
632 @QtCore.pyqtSlot(str)
633 @misc_utils.log_exception(_moduleLogger)
634 def _on_filter_changed(self, newItem):
635 self._selectedFilter = str(newItem)
636 self._populate_items()
639 @misc_utils.log_exception(_moduleLogger)
640 def _on_history_updated(self):
641 self._populate_items()
643 @QtCore.pyqtSlot(QtCore.QModelIndex)
644 @misc_utils.log_exception(_moduleLogger)
645 def _on_row_activated(self, index):
646 rowIndex = index.row()
647 item = self._itemStore.item(rowIndex, self.NUMBER_IDX)
648 contactDetails = item.data().toPyObject()
650 title = str(self._itemStore.item(rowIndex, self.FROM_IDX).text())
651 number = str(contactDetails[QtCore.QString("number")])
652 contactId = number # ids don't seem too unique so using numbers
655 # @bug doesn't seem to print multiple entries
656 for i in xrange(self._itemStore.rowCount()):
657 iItem = self._itemStore.item(i, self.NUMBER_IDX)
658 iContactDetails = iItem.data().toPyObject()
659 iNumber = str(iContactDetails[QtCore.QString("number")])
660 if number != iNumber:
662 relTime = abbrev_relative_date(iContactDetails[QtCore.QString("relTime")])
663 action = str(iContactDetails[QtCore.QString("action")])
664 number = str(iContactDetails[QtCore.QString("number")])
665 prettyNumber = make_pretty(number)
666 rowItems = relTime, action, prettyNumber
667 descriptionRows.append("<tr><td>%s</td></tr>" % "</td><td>".join(rowItems))
668 description = "<table>%s</table>" % "".join(descriptionRows)
669 numbersWithDescriptions = [(str(contactDetails[QtCore.QString("number")]), "")]
670 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
673 class Messages(object):
676 VOICEMAIL_MESSAGES = "Voicemail"
677 TEXT_MESSAGES = "SMS"
678 ALL_TYPES = "All Messages"
679 MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
681 UNREAD_STATUS = "Unread"
682 UNARCHIVED_STATUS = "Inbox"
684 MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
686 _MIN_MESSAGES_SHOWN = 4
688 def __init__(self, app, session, errorLog):
689 self._selectedTypeFilter = self.ALL_TYPES
690 self._selectedStatusFilter = self.ALL_STATUS
692 self._session = session
693 self._session.messagesUpdated.connect(self._on_messages_updated)
694 self._errorLog = errorLog
696 self._typeSelection = QtGui.QComboBox()
697 self._typeSelection.addItems(self.MESSAGE_TYPES)
698 self._typeSelection.setCurrentIndex(
699 self.MESSAGE_TYPES.index(self._selectedTypeFilter)
701 self._typeSelection.currentIndexChanged[str].connect(self._on_type_filter_changed)
703 self._statusSelection = QtGui.QComboBox()
704 self._statusSelection.addItems(self.MESSAGE_STATUSES)
705 self._statusSelection.setCurrentIndex(
706 self.MESSAGE_STATUSES.index(self._selectedStatusFilter)
708 self._statusSelection.currentIndexChanged[str].connect(self._on_status_filter_changed)
710 self._selectionLayout = QtGui.QHBoxLayout()
711 self._selectionLayout.addWidget(self._typeSelection)
712 self._selectionLayout.addWidget(self._statusSelection)
714 self._itemStore = QtGui.QStandardItemModel()
715 self._itemStore.setHorizontalHeaderLabels(["Messages"])
717 self._htmlDelegate = qui_utils.QHtmlDelegate()
718 self._itemView = QtGui.QTreeView()
719 self._itemView.setModel(self._itemStore)
720 self._itemView.setUniformRowHeights(False)
721 self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
722 self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
723 self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
724 self._itemView.setHeaderHidden(True)
725 self._itemView.setItemDelegate(self._htmlDelegate)
726 self._itemView.activated.connect(self._on_row_activated)
728 self._layout = QtGui.QVBoxLayout()
729 self._layout.addLayout(self._selectionLayout)
730 self._layout.addWidget(self._itemView)
731 self._widget = QtGui.QWidget()
732 self._widget.setLayout(self._layout)
734 self._populate_items()
741 self._itemView.setEnabled(True)
744 self._itemView.setEnabled(False)
747 self._itemView.clear()
750 self._session.update_messages()
752 def _populate_items(self):
753 self._itemStore.clear()
754 rawMessages = self._session.get_messages()
755 rawMessages.sort(key=lambda item: item["time"], reverse=True)
756 for item in rawMessages:
757 isUnarchived = not item["isArchived"]
758 isUnread = not item["isRead"]
760 self.UNREAD_STATUS: isUnarchived and isUnread,
761 self.UNARCHIVED_STATUS: isUnarchived,
762 self.ALL_STATUS: True,
763 }[self._selectedStatusFilter]
765 visibleType = self._selectedTypeFilter in [item["type"], self.ALL_TYPES]
766 if visibleType and visibleStatus:
767 relTime = abbrev_relative_date(item["relTime"])
768 number = item["number"]
769 prettyNumber = make_pretty(number)
771 if not name or name == number:
772 name = item["location"]
776 messageParts = list(item["messageParts"])
777 if len(messageParts) == 0:
778 messages = ("No Transcription", )
779 elif len(messageParts) == 1:
780 if messageParts[0][1]:
781 messages = (messageParts[0][1], )
783 messages = ("No Transcription", )
786 "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
787 for messagePart in messageParts
790 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (name, prettyNumber, relTime)
792 expandedMessages = [firstMessage]
793 expandedMessages.extend(messages)
794 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
795 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
796 collapsedMessages = [firstMessage, secondMessage]
797 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
799 collapsedMessages = expandedMessages
801 item = dict(item.iteritems())
802 item["collapsedMessages"] = "<br/>\n".join(collapsedMessages)
803 item["expandedMessages"] = "<br/>\n".join(expandedMessages)
805 messageItem = QtGui.QStandardItem(item["collapsedMessages"])
806 # @bug Not showing all of a message
807 messageItem.setData(item)
808 messageItem.setEditable(False)
809 messageItem.setCheckable(False)
810 row = (messageItem, )
811 self._itemStore.appendRow(row)
813 @QtCore.pyqtSlot(str)
814 @misc_utils.log_exception(_moduleLogger)
815 def _on_type_filter_changed(self, newItem):
816 self._selectedTypeFilter = str(newItem)
817 self._populate_items()
819 @QtCore.pyqtSlot(str)
820 @misc_utils.log_exception(_moduleLogger)
821 def _on_status_filter_changed(self, newItem):
822 self._selectedStatusFilter = str(newItem)
823 self._populate_items()
826 @misc_utils.log_exception(_moduleLogger)
827 def _on_messages_updated(self):
828 self._populate_items()
830 @QtCore.pyqtSlot(QtCore.QModelIndex)
831 @misc_utils.log_exception(_moduleLogger)
832 def _on_row_activated(self, index):
833 rowIndex = index.row()
834 item = self._itemStore.item(rowIndex, 0)
835 contactDetails = item.data().toPyObject()
837 name = str(contactDetails[QtCore.QString("name")])
838 number = str(contactDetails[QtCore.QString("number")])
839 if not name or name == number:
840 name = str(contactDetails[QtCore.QString("location")])
844 contactId = str(contactDetails[QtCore.QString("id")])
846 description = str(contactDetails[QtCore.QString("expandedMessages")])
847 numbersWithDescriptions = [(number, "")]
848 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
851 class Contacts(object):
853 def __init__(self, app, session, errorLog):
854 self._selectedFilter = ""
856 self._session = session
857 self._session.contactsUpdated.connect(self._on_contacts_updated)
858 self._errorLog = errorLog
860 self._listSelection = QtGui.QComboBox()
861 self._listSelection.addItems([])
862 # @todo Implement more contact lists
863 #self._listSelection.setCurrentIndex(self.HISTORY_ITEM_TYPES.index(self._selectedFilter))
864 self._listSelection.currentIndexChanged[str].connect(self._on_filter_changed)
866 self._itemStore = QtGui.QStandardItemModel()
867 self._itemStore.setHorizontalHeaderLabels(["Contacts"])
869 self._itemView = QtGui.QTreeView()
870 self._itemView.setModel(self._itemStore)
871 self._itemView.setUniformRowHeights(True)
872 self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
873 self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
874 self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
875 self._itemView.setHeaderHidden(True)
876 self._itemView.activated.connect(self._on_row_activated)
878 self._layout = QtGui.QVBoxLayout()
879 self._layout.addWidget(self._listSelection)
880 self._layout.addWidget(self._itemView)
881 self._widget = QtGui.QWidget()
882 self._widget.setLayout(self._layout)
884 self._populate_items()
891 self._itemView.setEnabled(True)
894 self._itemView.setEnabled(False)
897 self._itemView.clear()
900 self._session.update_contacts()
902 def _populate_items(self):
903 self._itemStore.clear()
905 contacts = list(self._session.get_contacts().itervalues())
906 contacts.sort(key=lambda contact: contact["name"].lower())
907 for item in contacts:
909 numbers = item["numbers"]
910 nameItem = QtGui.QStandardItem(name)
911 nameItem.setEditable(False)
912 nameItem.setCheckable(False)
913 nameItem.setData(item)
915 self._itemStore.appendRow(row)
917 @QtCore.pyqtSlot(str)
918 @misc_utils.log_exception(_moduleLogger)
919 def _on_filter_changed(self, newItem):
920 self._selectedFilter = str(newItem)
923 @misc_utils.log_exception(_moduleLogger)
924 def _on_contacts_updated(self):
925 self._populate_items()
927 @QtCore.pyqtSlot(QtCore.QModelIndex)
928 @misc_utils.log_exception(_moduleLogger)
929 def _on_row_activated(self, index):
930 rowIndex = index.row()
931 item = self._itemStore.item(rowIndex, 0)
932 contactDetails = item.data().toPyObject()
934 name = str(contactDetails[QtCore.QString("name")])
936 name = str(contactDetails[QtCore.QString("location")])
940 contactId = str(contactDetails[QtCore.QString("contactId")])
941 numbers = contactDetails[QtCore.QString("numbers")]
945 for (k, v) in number.iteritems()
947 for number in numbers
949 numbersWithDescriptions = [
951 number["phoneNumber"],
952 self._choose_phonetype(number),
954 for number in numbers
958 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
961 def _choose_phonetype(numberDetails):
962 if "phoneTypeName" in numberDetails:
963 return numberDetails["phoneTypeName"]
964 elif "phoneType" in numberDetails:
965 return numberDetails["phoneType"]
970 class MainWindow(object):
984 assert len(_TAB_TITLES) == MAX_TABS
992 assert len(_TAB_CLASS) == MAX_TABS
994 def __init__(self, parent, app):
995 self._fsContactsPath = os.path.join(constants._data_path_, "contacts")
997 self._session = session.Session()
998 self._session.error.connect(self._on_session_error)
999 self._session.loggedIn.connect(self._on_login)
1000 self._session.loggedOut.connect(self._on_logout)
1001 self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
1003 self._credentialsDialog = None
1004 self._smsEntryDialog = None
1006 self._errorLog = qui_utils.QErrorLog()
1007 self._errorDisplay = qui_utils.ErrorDisplay(self._errorLog)
1009 self._tabsContents = [
1010 DelayedWidget(self._app)
1011 for i in xrange(self.MAX_TABS)
1013 for tab in self._tabsContents:
1016 self._tabWidget = QtGui.QTabWidget()
1017 if qui_utils.screen_orientation() == QtCore.Qt.Vertical:
1018 self._tabWidget.setTabPosition(QtGui.QTabWidget.South)
1020 self._tabWidget.setTabPosition(QtGui.QTabWidget.West)
1021 for tabIndex, tabTitle in enumerate(self._TAB_TITLES):
1022 self._tabWidget.addTab(self._tabsContents[tabIndex].toplevel, tabTitle)
1023 self._tabWidget.currentChanged.connect(self._on_tab_changed)
1025 self._layout = QtGui.QVBoxLayout()
1026 self._layout.addWidget(self._errorDisplay.toplevel)
1027 self._layout.addWidget(self._tabWidget)
1029 centralWidget = QtGui.QWidget()
1030 centralWidget.setLayout(self._layout)
1032 self._window = QtGui.QMainWindow(parent)
1033 self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
1034 qui_utils.set_autorient(self._window, True)
1035 qui_utils.set_stackable(self._window, True)
1036 self._window.setWindowTitle("%s" % constants.__pretty_app_name__)
1037 self._window.setCentralWidget(centralWidget)
1039 self._loginTabAction = QtGui.QAction(None)
1040 self._loginTabAction.setText("Login")
1041 self._loginTabAction.triggered.connect(self._on_login_requested)
1043 self._importTabAction = QtGui.QAction(None)
1044 self._importTabAction.setText("Import")
1045 self._importTabAction.triggered.connect(self._on_import)
1047 self._refreshTabAction = QtGui.QAction(None)
1048 self._refreshTabAction.setText("Refresh")
1049 self._refreshTabAction.setShortcut(QtGui.QKeySequence("CTRL+r"))
1050 self._refreshTabAction.triggered.connect(self._on_refresh)
1052 self._closeWindowAction = QtGui.QAction(None)
1053 self._closeWindowAction.setText("Close")
1054 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
1055 self._closeWindowAction.triggered.connect(self._on_close_window)
1058 fileMenu = self._window.menuBar().addMenu("&File")
1059 fileMenu.addAction(self._loginTabAction)
1060 fileMenu.addAction(self._refreshTabAction)
1062 toolsMenu = self._window.menuBar().addMenu("&Tools")
1063 toolsMenu.addAction(self._importTabAction)
1065 self._window.addAction(self._closeWindowAction)
1066 self._window.addAction(self._app.quitAction)
1067 self._window.addAction(self._app.fullscreenAction)
1069 fileMenu = self._window.menuBar().addMenu("&File")
1070 fileMenu.addAction(self._loginTabAction)
1071 fileMenu.addAction(self._refreshTabAction)
1072 fileMenu.addAction(self._closeWindowAction)
1073 fileMenu.addAction(self._app.quitAction)
1075 viewMenu = self._window.menuBar().addMenu("&View")
1076 viewMenu.addAction(self._app.fullscreenAction)
1078 toolsMenu = self._window.menuBar().addMenu("&Tools")
1079 toolsMenu.addAction(self._importTabAction)
1081 self._window.addAction(self._app.logAction)
1083 self._initialize_tab(self._tabWidget.currentIndex())
1084 self.set_fullscreen(self._app.fullscreenAction.isChecked())
1091 def walk_children(self):
1096 for child in self.walk_children():
1100 for child in self.walk_children():
1105 for child in self.walk_children():
1106 child.window.destroyed.disconnect(self._on_child_close)
1108 self._window.close()
1110 def set_fullscreen(self, isFullscreen):
1112 self._window.showFullScreen()
1114 self._window.showNormal()
1115 for child in self.walk_children():
1116 child.set_fullscreen(isFullscreen)
1118 def _initialize_tab(self, index):
1119 assert index < self.MAX_TABS
1120 if not self._tabsContents[index].has_child():
1121 tab = self._TAB_CLASS[index](self._app, self._session, self._errorLog)
1122 self._tabsContents[index].set_child(tab)
1123 self._tabsContents[index].refresh()
1125 @QtCore.pyqtSlot(str)
1126 @misc_utils.log_exception(_moduleLogger)
1127 def _on_session_error(self, message):
1128 self._errorLog.push_message(message)
1131 @misc_utils.log_exception(_moduleLogger)
1132 def _on_login(self):
1133 for tab in self._tabsContents:
1137 @misc_utils.log_exception(_moduleLogger)
1138 def _on_logout(self):
1139 for tab in self._tabsContents:
1143 @misc_utils.log_exception(_moduleLogger)
1144 def _on_recipients_changed(self):
1145 if self._smsEntryDialog is None:
1146 self._smsEntryDialog = SMSEntryWindow(self.window, self._app, self._session, self._errorLog)
1150 @QtCore.pyqtSlot(bool)
1151 @misc_utils.log_exception(_moduleLogger)
1152 def _on_login_requested(self, checked = True):
1153 if self._credentialsDialog is None:
1154 self._credentialsDialog = CredentialsDialog()
1155 username, password = self._credentialsDialog.run("", "", self.window)
1156 self._session.login(username, password)
1158 @QtCore.pyqtSlot(int)
1159 @misc_utils.log_exception(_moduleLogger)
1160 def _on_tab_changed(self, index):
1161 self._initialize_tab(index)
1164 @QtCore.pyqtSlot(bool)
1165 @misc_utils.log_exception(_moduleLogger)
1166 def _on_refresh(self, checked = True):
1167 index = self._tabWidget.currentIndex()
1168 self._tabsContents[index].refresh()
1171 @QtCore.pyqtSlot(bool)
1172 @misc_utils.log_exception(_moduleLogger)
1173 def _on_import(self, checked = True):
1174 csvName = QtGui.QFileDialog.getOpenFileName(self._window, caption="Import", filter="CSV Files (*.csv)")
1177 shutil.copy2(csvName, self._fsContactsPath)
1180 @QtCore.pyqtSlot(bool)
1181 @misc_utils.log_exception(_moduleLogger)
1182 def _on_close_window(self, checked = True):
1186 def make_ugly(prettynumber):
1188 function to take a phone number and strip out all non-numeric
1191 >>> make_ugly("+012-(345)-678-90")
1194 return normalize_number(prettynumber)
1197 def normalize_number(prettynumber):
1199 function to take a phone number and strip out all non-numeric
1202 >>> normalize_number("+012-(345)-678-90")
1204 >>> normalize_number("1-(345)-678-9000")
1206 >>> normalize_number("+1-(345)-678-9000")
1209 uglynumber = re.sub('[^0-9+]', '', prettynumber)
1211 if uglynumber.startswith("+"):
1213 elif uglynumber.startswith("1"):
1214 uglynumber = "+"+uglynumber
1215 elif 10 <= len(uglynumber):
1216 assert uglynumber[0] not in ("+", "1")
1217 uglynumber = "+1"+uglynumber
1224 def _make_pretty_with_areacode(phonenumber):
1225 prettynumber = "(%s)" % (phonenumber[0:3], )
1226 if 3 < len(phonenumber):
1227 prettynumber += " %s" % (phonenumber[3:6], )
1228 if 6 < len(phonenumber):
1229 prettynumber += "-%s" % (phonenumber[6:], )
1233 def _make_pretty_local(phonenumber):
1234 prettynumber = "%s" % (phonenumber[0:3], )
1235 if 3 < len(phonenumber):
1236 prettynumber += "-%s" % (phonenumber[3:], )
1240 def _make_pretty_international(phonenumber):
1241 prettynumber = phonenumber
1242 if phonenumber.startswith("1"):
1244 prettynumber += _make_pretty_with_areacode(phonenumber[1:])
1248 def make_pretty(phonenumber):
1250 Function to take a phone number and return the pretty version
1252 if phonenumber begins with 0:
1254 if phonenumber begins with 1: ( for gizmo callback numbers )
1256 if phonenumber is 13 digits:
1258 if phonenumber is 10 digits:
1260 >>> make_pretty("12")
1262 >>> make_pretty("1234567")
1264 >>> make_pretty("2345678901")
1266 >>> make_pretty("12345678901")
1268 >>> make_pretty("01234567890")
1270 >>> make_pretty("+01234567890")
1272 >>> make_pretty("+12")
1274 >>> make_pretty("+123")
1276 >>> make_pretty("+1234")
1279 if phonenumber is None or phonenumber is "":
1282 phonenumber = normalize_number(phonenumber)
1284 if phonenumber[0] == "+":
1285 prettynumber = _make_pretty_international(phonenumber[1:])
1286 if not prettynumber.startswith("+"):
1287 prettynumber = "+"+prettynumber
1288 elif 8 < len(phonenumber) and phonenumber[0] in ("1", ):
1289 prettynumber = _make_pretty_international(phonenumber)
1290 elif 7 < len(phonenumber):
1291 prettynumber = _make_pretty_with_areacode(phonenumber)
1292 elif 3 < len(phonenumber):
1293 prettynumber = _make_pretty_local(phonenumber)
1295 prettynumber = phonenumber
1296 return prettynumber.strip()
1299 def abbrev_relative_date(date):
1301 >>> abbrev_relative_date("42 hours ago")
1303 >>> abbrev_relative_date("2 days ago")
1305 >>> abbrev_relative_date("4 weeks ago")
1308 parts = date.split(" ")
1309 return "%s %s" % (parts[0], parts[1][0])
1313 app = QtGui.QApplication([])
1314 handle = Dialcentral(app)
1319 if __name__ == "__main__":
1320 logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
1321 logging.basicConfig(level=logging.DEBUG, format=logFormat)
1323 os.makedirs(constants._data_path_)