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.QTextEdit()
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._singleNumberSelector = QtGui.QComboBox()
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._singleNumberSelector)
279 self._buttonLayout.addWidget(self._smsButton)
280 self._buttonLayout.addWidget(self._dialButton)
282 self._layout = QtGui.QVBoxLayout()
283 self._layout.addWidget(self._scrollEntry)
284 self._layout.addLayout(self._buttonLayout)
286 centralWidget = QtGui.QWidget()
287 centralWidget.setLayout(self._layout)
289 self._window = QtGui.QMainWindow(parent)
290 qui_utils.set_autorient(self._window, True)
291 qui_utils.set_stackable(self._window, True)
292 self._window.setWindowTitle("Contact")
293 self._window.setCentralWidget(centralWidget)
295 self._update_recipients()
297 def _update_letter_count(self):
298 count = self._smsEntry.toPlainText().size()
299 self._characterCountLabel.setText("Letters: %s" % count)
301 def _update_button_state(self):
302 if len(self._contacts) == 0:
303 self._dialButton.setEnabled(False)
304 self._smsButton.setEnabled(False)
305 elif len(self._contacts) == 1:
306 count = self._smsEntry.toPlainText().size()
308 self._dialButton.setEnabled(True)
309 self._smsButton.setEnabled(False)
311 self._dialButton.setEnabled(False)
312 self._smsButton.setEnabled(True)
314 self._dialButton.setEnabled(False)
315 self._smsButton.setEnabled(True)
317 def _update_recipients(self):
318 draftContacts = len(self._session.draft.get_contacts())
319 if draftContacts == 0:
321 elif draftContacts == 1:
322 title, description, numbers = list(
323 self._session.draft.get_contacts().itervalues()
325 self._window.setWindowTitle(title)
326 self._history.setHtml(description)
327 self._history.setVisible(True)
328 self._populate_number_selector(self._singleNumberSelector, numbers))
331 self._window.setWindowTitle("Contacts")
332 self._history.setHtml("")
333 self._history.setVisible(False)
334 self._singleNumberSelector.setVisible(False)
337 def _populate_number_selector(self, selector, numbers):
338 while 0 < selector.count():
339 selector.removeItem(0)
340 for number, description in numbers:
342 label = "%s - %s" % number, description
345 selector.addItem(label, numbers)
346 selector.setVisible(True)
348 selector.setEnabled(True)
350 selector.setEnabled(False)
353 @misc_utils.log_exception(_moduleLogger)
354 def _on_recipients_changed(self):
355 self._populate_recipients()
358 @misc_utils.log_exception(_moduleLogger)
359 def _on_op_finished(self):
363 @misc_utils.log_exception(_moduleLogger)
364 def _on_letter_count_changed(self):
365 self._update_letter_count()
366 self._update_button_state()
369 class DelayedWidget(object):
371 def __init__(self, app):
372 self._layout = QtGui.QVBoxLayout()
373 self._widget = QtGui.QWidget()
374 self._widget.setLayout(self._layout)
377 self._isEnabled = True
384 return self._child is not None
386 def set_child(self, child):
387 if self._child is not None:
388 self._layout.removeWidget(self._child.toplevel)
390 if self._child is not None:
391 self._layout.addWidget(self._child.toplevel)
396 self._child.disable()
399 self._isEnabled = True
400 if self._child is not None:
404 self._isEnabled = False
405 if self._child is not None:
406 self._child.disable()
409 if self._child is not None:
413 if self._child is not None:
414 self._child.refresh()
417 class Dialpad(object):
419 def __init__(self, app, session, errorLog):
421 self._session = session
422 self._errorLog = errorLog
424 self._plus = self._generate_key_button("+", "")
425 self._entry = QtGui.QLineEdit()
427 backAction = QtGui.QAction(None)
428 backAction.setText("Back")
429 backAction.triggered.connect(self._on_backspace)
430 backPieItem = qtpie.QActionPieItem(backAction)
431 clearAction = QtGui.QAction(None)
432 clearAction.setText("Clear")
433 clearAction.triggered.connect(self._on_clear_text)
434 clearPieItem = qtpie.QActionPieItem(clearAction)
435 self._back = qtpie.QPieButton(backPieItem)
436 self._back.set_center(backPieItem)
437 self._back.insertItem(qtpie.PieFiling.NULL_CENTER)
438 self._back.insertItem(clearPieItem)
439 self._back.insertItem(qtpie.PieFiling.NULL_CENTER)
440 self._back.insertItem(qtpie.PieFiling.NULL_CENTER)
442 self._entryLayout = QtGui.QHBoxLayout()
443 self._entryLayout.addWidget(self._plus, 0, QtCore.Qt.AlignCenter)
444 self._entryLayout.addWidget(self._entry, 10)
445 self._entryLayout.addWidget(self._back, 0, QtCore.Qt.AlignCenter)
447 self._smsButton = QtGui.QPushButton("SMS")
448 self._smsButton.clicked.connect(self._on_sms_clicked)
449 self._callButton = QtGui.QPushButton("Call")
450 self._callButton.clicked.connect(self._on_call_clicked)
452 self._padLayout = QtGui.QGridLayout()
453 rows = [0, 0, 0, 1, 1, 1, 2, 2, 2]
454 columns = [0, 1, 2] * 3
466 for (num, letters), (row, column) in zip(keys, zip(rows, columns)):
467 self._padLayout.addWidget(
468 self._generate_key_button(num, letters), row, column, QtCore.Qt.AlignCenter
470 self._padLayout.addWidget(self._smsButton, 3, 0)
471 self._padLayout.addWidget(
472 self._generate_key_button("0", ""), 3, 1, QtCore.Qt.AlignCenter
474 self._padLayout.addWidget(self._callButton, 3, 2)
476 self._layout = QtGui.QVBoxLayout()
477 self._layout.addLayout(self._entryLayout)
478 self._layout.addLayout(self._padLayout)
479 self._widget = QtGui.QWidget()
480 self._widget.setLayout(self._layout)
487 self._smsButton.setEnabled(True)
488 self._callButton.setEnabled(True)
491 self._smsButton.setEnabled(False)
492 self._callButton.setEnabled(False)
500 def _generate_key_button(self, center, letters):
501 centerPieItem = self._generate_button_slice(center)
502 button = qtpie.QPieButton(centerPieItem)
503 button.set_center(centerPieItem)
505 if len(letters) == 0:
507 pieItem = qtpie.PieFiling.NULL_CENTER
508 button.insertItem(pieItem)
509 elif len(letters) in [3, 4]:
510 for i in xrange(6 - len(letters)):
511 pieItem = qtpie.PieFiling.NULL_CENTER
512 button.insertItem(pieItem)
514 for letter in letters:
515 pieItem = self._generate_button_slice(letter)
516 button.insertItem(pieItem)
519 pieItem = qtpie.PieFiling.NULL_CENTER
520 button.insertItem(pieItem)
522 raise NotImplementedError("Cannot handle %r" % letters)
525 def _generate_button_slice(self, letter):
526 action = QtGui.QAction(None)
527 action.setText(letter)
528 action.triggered.connect(lambda: self._on_keypress(letter))
529 pieItem = qtpie.QActionPieItem(action)
532 @misc_utils.log_exception(_moduleLogger)
533 def _on_keypress(self, key):
534 self._entry.insert(key)
536 @misc_utils.log_exception(_moduleLogger)
537 def _on_backspace(self, toggled = False):
538 self._entry.backspace()
540 @misc_utils.log_exception(_moduleLogger)
541 def _on_clear_text(self, toggled = False):
545 @QtCore.pyqtSlot(bool)
546 @misc_utils.log_exception(_moduleLogger)
547 def _on_sms_clicked(self, checked = False):
548 number = str(self._entry.text())
554 numbersWithDescriptions = [(number, "")]
555 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
558 @QtCore.pyqtSlot(bool)
559 @misc_utils.log_exception(_moduleLogger)
560 def _on_call_clicked(self, checked = False):
561 number = str(self._entry.text())
567 numbersWithDescriptions = [(number, "")]
568 self._session.draft.clear()
569 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
570 self._session.draft.call()
573 class History(object):
581 HISTORY_ITEM_TYPES = ["Received", "Missed", "Placed", "All"]
582 HISTORY_COLUMNS = ["When", "What", "Number", "From"]
583 assert len(HISTORY_COLUMNS) == MAX_IDX
585 def __init__(self, app, session, errorLog):
586 self._selectedFilter = self.HISTORY_ITEM_TYPES[-1]
588 self._session = session
589 self._session.historyUpdated.connect(self._on_history_updated)
590 self._errorLog = errorLog
592 self._typeSelection = QtGui.QComboBox()
593 self._typeSelection.addItems(self.HISTORY_ITEM_TYPES)
594 self._typeSelection.setCurrentIndex(
595 self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
597 self._typeSelection.currentIndexChanged[str].connect(self._on_filter_changed)
599 self._itemStore = QtGui.QStandardItemModel()
600 self._itemStore.setHorizontalHeaderLabels(self.HISTORY_COLUMNS)
602 self._itemView = QtGui.QTreeView()
603 self._itemView.setModel(self._itemStore)
604 self._itemView.setUniformRowHeights(True)
605 self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
606 self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
607 self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
608 self._itemView.setHeaderHidden(True)
609 self._itemView.activated.connect(self._on_row_activated)
611 self._layout = QtGui.QVBoxLayout()
612 self._layout.addWidget(self._typeSelection)
613 self._layout.addWidget(self._itemView)
614 self._widget = QtGui.QWidget()
615 self._widget.setLayout(self._layout)
617 self._populate_items()
624 self._itemView.setEnabled(True)
627 self._itemView.setEnabled(False)
630 self._itemView.clear()
633 self._session.update_history()
635 def _populate_items(self):
636 self._itemStore.clear()
637 history = self._session.get_history()
638 history.sort(key=lambda item: item["time"], reverse=True)
639 for event in history:
640 if self._selectedFilter in [self.HISTORY_ITEM_TYPES[-1], event["action"]]:
641 relTime = abbrev_relative_date(event["relTime"])
642 action = event["action"]
643 number = event["number"]
644 prettyNumber = make_pretty(number)
646 if not name or name == number:
647 name = event["location"]
651 timeItem = QtGui.QStandardItem(relTime)
652 actionItem = QtGui.QStandardItem(action)
653 numberItem = QtGui.QStandardItem(prettyNumber)
654 nameItem = QtGui.QStandardItem(name)
655 row = timeItem, actionItem, numberItem, nameItem
657 item.setEditable(False)
658 item.setCheckable(False)
659 if item is not nameItem:
660 itemFont = item.font()
661 itemFont.setPointSize(max(itemFont.pointSize() - 3, 5))
662 item.setFont(itemFont)
663 numberItem.setData(event)
664 self._itemStore.appendRow(row)
666 @QtCore.pyqtSlot(str)
667 @misc_utils.log_exception(_moduleLogger)
668 def _on_filter_changed(self, newItem):
669 self._selectedFilter = str(newItem)
670 self._populate_items()
673 @misc_utils.log_exception(_moduleLogger)
674 def _on_history_updated(self):
675 self._populate_items()
677 @QtCore.pyqtSlot(QtCore.QModelIndex)
678 @misc_utils.log_exception(_moduleLogger)
679 def _on_row_activated(self, index):
680 rowIndex = index.row()
681 item = self._itemStore.item(rowIndex, self.NUMBER_IDX)
682 contactDetails = item.data().toPyObject()
684 title = str(self._itemStore.item(rowIndex, self.FROM_IDX).text())
685 number = str(contactDetails[QtCore.QString("number")])
686 contactId = number # ids don't seem too unique so using numbers
689 # @bug doesn't seem to print multiple entries
690 for i in xrange(self._itemStore.rowCount()):
691 iItem = self._itemStore.item(i, self.NUMBER_IDX)
692 iContactDetails = iItem.data().toPyObject()
693 iNumber = str(iContactDetails[QtCore.QString("number")])
694 if number != iNumber:
696 relTime = abbrev_relative_date(iContactDetails[QtCore.QString("relTime")])
697 action = str(iContactDetails[QtCore.QString("action")])
698 number = str(iContactDetails[QtCore.QString("number")])
699 prettyNumber = make_pretty(number)
700 rowItems = relTime, action, prettyNumber
701 descriptionRows.append("<tr><td>%s</td></tr>" % "</td><td>".join(rowItems))
702 description = "<table>%s</table>" % "".join(descriptionRows)
703 numbersWithDescriptions = [(str(contactDetails[QtCore.QString("number")]), "")]
704 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
707 class Messages(object):
710 VOICEMAIL_MESSAGES = "Voicemail"
711 TEXT_MESSAGES = "SMS"
712 ALL_TYPES = "All Messages"
713 MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
715 UNREAD_STATUS = "Unread"
716 UNARCHIVED_STATUS = "Inbox"
718 MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
720 _MIN_MESSAGES_SHOWN = 4
722 def __init__(self, app, session, errorLog):
723 self._selectedTypeFilter = self.ALL_TYPES
724 self._selectedStatusFilter = self.ALL_STATUS
726 self._session = session
727 self._session.messagesUpdated.connect(self._on_messages_updated)
728 self._errorLog = errorLog
730 self._typeSelection = QtGui.QComboBox()
731 self._typeSelection.addItems(self.MESSAGE_TYPES)
732 self._typeSelection.setCurrentIndex(
733 self.MESSAGE_TYPES.index(self._selectedTypeFilter)
735 self._typeSelection.currentIndexChanged[str].connect(self._on_type_filter_changed)
737 self._statusSelection = QtGui.QComboBox()
738 self._statusSelection.addItems(self.MESSAGE_STATUSES)
739 self._statusSelection.setCurrentIndex(
740 self.MESSAGE_STATUSES.index(self._selectedStatusFilter)
742 self._statusSelection.currentIndexChanged[str].connect(self._on_status_filter_changed)
744 self._selectionLayout = QtGui.QHBoxLayout()
745 self._selectionLayout.addWidget(self._typeSelection)
746 self._selectionLayout.addWidget(self._statusSelection)
748 self._itemStore = QtGui.QStandardItemModel()
749 self._itemStore.setHorizontalHeaderLabels(["Messages"])
751 self._htmlDelegate = qui_utils.QHtmlDelegate()
752 self._itemView = QtGui.QTreeView()
753 self._itemView.setModel(self._itemStore)
754 self._itemView.setUniformRowHeights(False)
755 self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
756 self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
757 self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
758 self._itemView.setHeaderHidden(True)
759 self._itemView.setItemDelegate(self._htmlDelegate)
760 self._itemView.activated.connect(self._on_row_activated)
762 self._layout = QtGui.QVBoxLayout()
763 self._layout.addLayout(self._selectionLayout)
764 self._layout.addWidget(self._itemView)
765 self._widget = QtGui.QWidget()
766 self._widget.setLayout(self._layout)
768 self._populate_items()
775 self._itemView.setEnabled(True)
778 self._itemView.setEnabled(False)
781 self._itemView.clear()
784 self._session.update_messages()
786 def _populate_items(self):
787 self._itemStore.clear()
788 rawMessages = self._session.get_messages()
789 rawMessages.sort(key=lambda item: item["time"], reverse=True)
790 for item in rawMessages:
791 isUnarchived = not item["isArchived"]
792 isUnread = not item["isRead"]
794 self.UNREAD_STATUS: isUnarchived and isUnread,
795 self.UNARCHIVED_STATUS: isUnarchived,
796 self.ALL_STATUS: True,
797 }[self._selectedStatusFilter]
799 visibleType = self._selectedTypeFilter in [item["type"], self.ALL_TYPES]
800 if visibleType and visibleStatus:
801 relTime = abbrev_relative_date(item["relTime"])
802 number = item["number"]
803 prettyNumber = make_pretty(number)
805 if not name or name == number:
806 name = item["location"]
810 messageParts = list(item["messageParts"])
811 if len(messageParts) == 0:
812 messages = ("No Transcription", )
813 elif len(messageParts) == 1:
814 if messageParts[0][1]:
815 messages = (messageParts[0][1], )
817 messages = ("No Transcription", )
820 "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
821 for messagePart in messageParts
824 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (name, prettyNumber, relTime)
826 expandedMessages = [firstMessage]
827 expandedMessages.extend(messages)
828 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
829 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
830 collapsedMessages = [firstMessage, secondMessage]
831 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
833 collapsedMessages = expandedMessages
835 item = dict(item.iteritems())
836 item["collapsedMessages"] = "<br/>\n".join(collapsedMessages)
837 item["expandedMessages"] = "<br/>\n".join(expandedMessages)
839 messageItem = QtGui.QStandardItem(item["collapsedMessages"])
840 # @bug Not showing all of a message
841 messageItem.setData(item)
842 messageItem.setEditable(False)
843 messageItem.setCheckable(False)
844 row = (messageItem, )
845 self._itemStore.appendRow(row)
847 @QtCore.pyqtSlot(str)
848 @misc_utils.log_exception(_moduleLogger)
849 def _on_type_filter_changed(self, newItem):
850 self._selectedTypeFilter = str(newItem)
851 self._populate_items()
853 @QtCore.pyqtSlot(str)
854 @misc_utils.log_exception(_moduleLogger)
855 def _on_status_filter_changed(self, newItem):
856 self._selectedStatusFilter = str(newItem)
857 self._populate_items()
860 @misc_utils.log_exception(_moduleLogger)
861 def _on_messages_updated(self):
862 self._populate_items()
864 @QtCore.pyqtSlot(QtCore.QModelIndex)
865 @misc_utils.log_exception(_moduleLogger)
866 def _on_row_activated(self, index):
867 rowIndex = index.row()
868 item = self._itemStore.item(rowIndex, 0)
869 contactDetails = item.data().toPyObject()
871 name = str(contactDetails[QtCore.QString("name")])
872 number = str(contactDetails[QtCore.QString("number")])
873 if not name or name == number:
874 name = str(contactDetails[QtCore.QString("location")])
878 contactId = str(contactDetails[QtCore.QString("id")])
880 description = str(contactDetails[QtCore.QString("expandedMessages")])
881 numbersWithDescriptions = [(number, "")]
882 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
885 class Contacts(object):
887 def __init__(self, app, session, errorLog):
888 self._selectedFilter = ""
890 self._session = session
891 self._session.contactsUpdated.connect(self._on_contacts_updated)
892 self._errorLog = errorLog
894 self._listSelection = QtGui.QComboBox()
895 self._listSelection.addItems([])
896 # @todo Implement more contact lists
897 #self._listSelection.setCurrentIndex(self.HISTORY_ITEM_TYPES.index(self._selectedFilter))
898 self._listSelection.currentIndexChanged[str].connect(self._on_filter_changed)
900 self._itemStore = QtGui.QStandardItemModel()
901 self._itemStore.setHorizontalHeaderLabels(["Contacts"])
903 self._itemView = QtGui.QTreeView()
904 self._itemView.setModel(self._itemStore)
905 self._itemView.setUniformRowHeights(True)
906 self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
907 self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
908 self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
909 self._itemView.setHeaderHidden(True)
910 self._itemView.activated.connect(self._on_row_activated)
912 self._layout = QtGui.QVBoxLayout()
913 self._layout.addWidget(self._listSelection)
914 self._layout.addWidget(self._itemView)
915 self._widget = QtGui.QWidget()
916 self._widget.setLayout(self._layout)
918 self._populate_items()
925 self._itemView.setEnabled(True)
928 self._itemView.setEnabled(False)
931 self._itemView.clear()
934 self._session.update_contacts()
936 def _populate_items(self):
937 self._itemStore.clear()
939 contacts = list(self._session.get_contacts().itervalues())
940 contacts.sort(key=lambda contact: contact["name"].lower())
941 for item in contacts:
943 numbers = item["numbers"]
944 nameItem = QtGui.QStandardItem(name)
945 nameItem.setEditable(False)
946 nameItem.setCheckable(False)
947 nameItem.setData(item)
949 self._itemStore.appendRow(row)
951 @QtCore.pyqtSlot(str)
952 @misc_utils.log_exception(_moduleLogger)
953 def _on_filter_changed(self, newItem):
954 self._selectedFilter = str(newItem)
957 @misc_utils.log_exception(_moduleLogger)
958 def _on_contacts_updated(self):
959 self._populate_items()
961 @QtCore.pyqtSlot(QtCore.QModelIndex)
962 @misc_utils.log_exception(_moduleLogger)
963 def _on_row_activated(self, index):
964 rowIndex = index.row()
965 item = self._itemStore.item(rowIndex, 0)
966 contactDetails = item.data().toPyObject()
968 name = str(contactDetails[QtCore.QString("name")])
970 name = str(contactDetails[QtCore.QString("location")])
974 contactId = str(contactDetails[QtCore.QString("contactId")])
975 numbers = contactDetails[QtCore.QString("numbers")]
979 for (k, v) in number.iteritems()
981 for number in numbers
983 numbersWithDescriptions = [
985 number["phoneNumber"],
986 self._choose_phonetype(number),
988 for number in numbers
992 self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions)
995 def _choose_phonetype(numberDetails):
996 if "phoneTypeName" in numberDetails:
997 return numberDetails["phoneTypeName"]
998 elif "phoneType" in numberDetails:
999 return numberDetails["phoneType"]
1004 class MainWindow(object):
1018 assert len(_TAB_TITLES) == MAX_TABS
1026 assert len(_TAB_CLASS) == MAX_TABS
1028 def __init__(self, parent, app):
1029 self._fsContactsPath = os.path.join(constants._data_path_, "contacts")
1031 self._session = session.Session()
1032 self._session.error.connect(self._on_session_error)
1033 self._session.loggedIn.connect(self._on_login)
1034 self._session.loggedOut.connect(self._on_logout)
1035 self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
1037 self._credentialsDialog = None
1038 self._smsEntryDialog = None
1040 self._errorLog = qui_utils.QErrorLog()
1041 self._errorDisplay = qui_utils.ErrorDisplay(self._errorLog)
1043 self._tabsContents = [
1044 DelayedWidget(self._app)
1045 for i in xrange(self.MAX_TABS)
1047 for tab in self._tabsContents:
1050 self._tabWidget = QtGui.QTabWidget()
1051 if qui_utils.screen_orientation() == QtCore.Qt.Vertical:
1052 self._tabWidget.setTabPosition(QtGui.QTabWidget.South)
1054 self._tabWidget.setTabPosition(QtGui.QTabWidget.West)
1055 for tabIndex, tabTitle in enumerate(self._TAB_TITLES):
1056 self._tabWidget.addTab(self._tabsContents[tabIndex].toplevel, tabTitle)
1057 self._tabWidget.currentChanged.connect(self._on_tab_changed)
1059 self._layout = QtGui.QVBoxLayout()
1060 self._layout.addWidget(self._errorDisplay.toplevel)
1061 self._layout.addWidget(self._tabWidget)
1063 centralWidget = QtGui.QWidget()
1064 centralWidget.setLayout(self._layout)
1066 self._window = QtGui.QMainWindow(parent)
1067 self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
1068 qui_utils.set_autorient(self._window, True)
1069 qui_utils.set_stackable(self._window, True)
1070 self._window.setWindowTitle("%s" % constants.__pretty_app_name__)
1071 self._window.setCentralWidget(centralWidget)
1073 self._loginTabAction = QtGui.QAction(None)
1074 self._loginTabAction.setText("Login")
1075 self._loginTabAction.triggered.connect(self._on_login_requested)
1077 self._importTabAction = QtGui.QAction(None)
1078 self._importTabAction.setText("Import")
1079 self._importTabAction.triggered.connect(self._on_import)
1081 self._refreshTabAction = QtGui.QAction(None)
1082 self._refreshTabAction.setText("Refresh")
1083 self._refreshTabAction.setShortcut(QtGui.QKeySequence("CTRL+r"))
1084 self._refreshTabAction.triggered.connect(self._on_refresh)
1086 self._closeWindowAction = QtGui.QAction(None)
1087 self._closeWindowAction.setText("Close")
1088 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
1089 self._closeWindowAction.triggered.connect(self._on_close_window)
1092 fileMenu = self._window.menuBar().addMenu("&File")
1093 fileMenu.addAction(self._loginTabAction)
1094 fileMenu.addAction(self._refreshTabAction)
1096 toolsMenu = self._window.menuBar().addMenu("&Tools")
1097 toolsMenu.addAction(self._importTabAction)
1099 self._window.addAction(self._closeWindowAction)
1100 self._window.addAction(self._app.quitAction)
1101 self._window.addAction(self._app.fullscreenAction)
1103 fileMenu = self._window.menuBar().addMenu("&File")
1104 fileMenu.addAction(self._loginTabAction)
1105 fileMenu.addAction(self._refreshTabAction)
1106 fileMenu.addAction(self._closeWindowAction)
1107 fileMenu.addAction(self._app.quitAction)
1109 viewMenu = self._window.menuBar().addMenu("&View")
1110 viewMenu.addAction(self._app.fullscreenAction)
1112 toolsMenu = self._window.menuBar().addMenu("&Tools")
1113 toolsMenu.addAction(self._importTabAction)
1115 self._window.addAction(self._app.logAction)
1117 self._initialize_tab(self._tabWidget.currentIndex())
1118 self.set_fullscreen(self._app.fullscreenAction.isChecked())
1125 def walk_children(self):
1130 for child in self.walk_children():
1134 for child in self.walk_children():
1139 for child in self.walk_children():
1140 child.window.destroyed.disconnect(self._on_child_close)
1142 self._window.close()
1144 def set_fullscreen(self, isFullscreen):
1146 self._window.showFullScreen()
1148 self._window.showNormal()
1149 for child in self.walk_children():
1150 child.set_fullscreen(isFullscreen)
1152 def _initialize_tab(self, index):
1153 assert index < self.MAX_TABS
1154 if not self._tabsContents[index].has_child():
1155 tab = self._TAB_CLASS[index](self._app, self._session, self._errorLog)
1156 self._tabsContents[index].set_child(tab)
1157 self._tabsContents[index].refresh()
1159 @QtCore.pyqtSlot(str)
1160 @misc_utils.log_exception(_moduleLogger)
1161 def _on_session_error(self, message):
1162 self._errorLog.push_message(message)
1165 @misc_utils.log_exception(_moduleLogger)
1166 def _on_login(self):
1167 for tab in self._tabsContents:
1171 @misc_utils.log_exception(_moduleLogger)
1172 def _on_logout(self):
1173 for tab in self._tabsContents:
1177 @misc_utils.log_exception(_moduleLogger)
1178 def _on_recipients_changed(self):
1179 if len(self._session.draft.get_contacts()) == 0:
1182 if self._smsEntryDialog is None:
1183 self._smsEntryDialog = SMSEntryWindow(self.window, self._app, self._session, self._errorLog)
1187 @QtCore.pyqtSlot(bool)
1188 @misc_utils.log_exception(_moduleLogger)
1189 def _on_login_requested(self, checked = True):
1190 if self._credentialsDialog is None:
1191 self._credentialsDialog = CredentialsDialog()
1192 username, password = self._credentialsDialog.run("", "", self.window)
1193 self._session.login(username, password)
1195 @QtCore.pyqtSlot(int)
1196 @misc_utils.log_exception(_moduleLogger)
1197 def _on_tab_changed(self, index):
1198 self._initialize_tab(index)
1201 @QtCore.pyqtSlot(bool)
1202 @misc_utils.log_exception(_moduleLogger)
1203 def _on_refresh(self, checked = True):
1204 index = self._tabWidget.currentIndex()
1205 self._tabsContents[index].refresh()
1208 @QtCore.pyqtSlot(bool)
1209 @misc_utils.log_exception(_moduleLogger)
1210 def _on_import(self, checked = True):
1211 csvName = QtGui.QFileDialog.getOpenFileName(self._window, caption="Import", filter="CSV Files (*.csv)")
1214 shutil.copy2(csvName, self._fsContactsPath)
1217 @QtCore.pyqtSlot(bool)
1218 @misc_utils.log_exception(_moduleLogger)
1219 def _on_close_window(self, checked = True):
1223 def make_ugly(prettynumber):
1225 function to take a phone number and strip out all non-numeric
1228 >>> make_ugly("+012-(345)-678-90")
1231 return normalize_number(prettynumber)
1234 def normalize_number(prettynumber):
1236 function to take a phone number and strip out all non-numeric
1239 >>> normalize_number("+012-(345)-678-90")
1241 >>> normalize_number("1-(345)-678-9000")
1243 >>> normalize_number("+1-(345)-678-9000")
1246 uglynumber = re.sub('[^0-9+]', '', prettynumber)
1248 if uglynumber.startswith("+"):
1250 elif uglynumber.startswith("1"):
1251 uglynumber = "+"+uglynumber
1252 elif 10 <= len(uglynumber):
1253 assert uglynumber[0] not in ("+", "1")
1254 uglynumber = "+1"+uglynumber
1261 def _make_pretty_with_areacode(phonenumber):
1262 prettynumber = "(%s)" % (phonenumber[0:3], )
1263 if 3 < len(phonenumber):
1264 prettynumber += " %s" % (phonenumber[3:6], )
1265 if 6 < len(phonenumber):
1266 prettynumber += "-%s" % (phonenumber[6:], )
1270 def _make_pretty_local(phonenumber):
1271 prettynumber = "%s" % (phonenumber[0:3], )
1272 if 3 < len(phonenumber):
1273 prettynumber += "-%s" % (phonenumber[3:], )
1277 def _make_pretty_international(phonenumber):
1278 prettynumber = phonenumber
1279 if phonenumber.startswith("1"):
1281 prettynumber += _make_pretty_with_areacode(phonenumber[1:])
1285 def make_pretty(phonenumber):
1287 Function to take a phone number and return the pretty version
1289 if phonenumber begins with 0:
1291 if phonenumber begins with 1: ( for gizmo callback numbers )
1293 if phonenumber is 13 digits:
1295 if phonenumber is 10 digits:
1297 >>> make_pretty("12")
1299 >>> make_pretty("1234567")
1301 >>> make_pretty("2345678901")
1303 >>> make_pretty("12345678901")
1305 >>> make_pretty("01234567890")
1307 >>> make_pretty("+01234567890")
1309 >>> make_pretty("+12")
1311 >>> make_pretty("+123")
1313 >>> make_pretty("+1234")
1316 if phonenumber is None or phonenumber is "":
1319 phonenumber = normalize_number(phonenumber)
1321 if phonenumber[0] == "+":
1322 prettynumber = _make_pretty_international(phonenumber[1:])
1323 if not prettynumber.startswith("+"):
1324 prettynumber = "+"+prettynumber
1325 elif 8 < len(phonenumber) and phonenumber[0] in ("1", ):
1326 prettynumber = _make_pretty_international(phonenumber)
1327 elif 7 < len(phonenumber):
1328 prettynumber = _make_pretty_with_areacode(phonenumber)
1329 elif 3 < len(phonenumber):
1330 prettynumber = _make_pretty_local(phonenumber)
1332 prettynumber = phonenumber
1333 return prettynumber.strip()
1336 def abbrev_relative_date(date):
1338 >>> abbrev_relative_date("42 hours ago")
1340 >>> abbrev_relative_date("2 days ago")
1342 >>> abbrev_relative_date("4 weeks ago")
1345 parts = date.split(" ")
1346 return "%s %s" % (parts[0], parts[1][0])
1350 app = QtGui.QApplication([])
1351 handle = Dialcentral(app)
1356 if __name__ == "__main__":
1357 logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
1358 logging.basicConfig(level=logging.DEBUG, format=logFormat)
1360 os.makedirs(constants._data_path_)