From 55fc43115d653fbaec52ecf59815b14f6064e05b Mon Sep 17 00:00:00 2001 From: Ed Page Date: Sat, 18 Sep 2010 16:24:32 -0500 Subject: [PATCH] Removing contact caching from backend, moving it to session Improving tracking of storing of cookies Lots of implementation work on session object Modified sizing for pies Populate at least something for each tab Adding HTML Delegate for displaying of messages Make each tab manipulate the drafts object Starting on SMS Window --- src/backends/gv_backend.py | 36 +--- src/backends/gvoice/browser_emu.py | 9 +- src/backends/gvoice/gvoice.py | 3 + src/dialcentral_qt.py | 389 +++++++++++++++++++++++++++++++++--- src/session.py | 250 +++++++++++++++++------ src/util/qtpie.py | 9 + src/util/qtpieboard.py | 2 +- src/util/qui_utils.py | 46 +++++ 8 files changed, 622 insertions(+), 122 deletions(-) diff --git a/src/backends/gv_backend.py b/src/backends/gv_backend.py index 49294e7..012be7e 100644 --- a/src/backends/gv_backend.py +++ b/src/backends/gv_backend.py @@ -41,8 +41,6 @@ class GVDialer(object): def __init__(self, cookieFile = None): self._gvoice = gvoice.GVoiceBackend(cookieFile) - self._contacts = None - def is_quick_login_possible(self): """ @returns True then is_authed might be enough to login, else full login is required @@ -145,37 +143,18 @@ class GVDialer(object): """ @returns Iterable of (personsName, phoneNumber, exact date, relative date, action) """ - return self._gvoice.get_recent() + return list(self._gvoice.get_recent()) def get_contacts(self): """ - @returns Iterable of (contact id, contact name) + @returns Fresh dictionary of items """ - self._update_contacts_cache() - contactsToSort = [ - (contactDetails["name"], contactId) - for contactId, contactDetails in self._contacts.iteritems() - ] - contactsToSort.sort() - return ( - (contactId, contactName) - for (contactName, contactId) in contactsToSort - ) - - def get_contact_details(self, contactId): - """ - @returns Iterable of (Phone Type, Phone Number) - """ - if self._contacts is None: - self._update_contacts_cache() - contactDetails = self._contacts[contactId] - # Defaulting phoneTypes because those are just things like faxes - return ( - (number.get("phoneType", ""), number["phoneNumber"]) - for number in contactDetails["numbers"] - ) + return dict(self._gvoice.get_contacts()) def get_messages(self): + return list(self._get_messages()) + + def _get_messages(self): voicemails = self._gvoice.get_voicemails() smss = self._gvoice.get_texts() conversations = itertools.chain(voicemails, smss) @@ -224,9 +203,6 @@ class GVDialer(object): def factory_name(): return "Google Voice" - def _update_contacts_cache(self): - self._contacts = dict(self._gvoice.get_contacts()) - def _format_message(self, message): messagePartFormat = { "med1": "%s", diff --git a/src/backends/gvoice/browser_emu.py b/src/backends/gvoice/browser_emu.py index 5e9b678..3f3cc51 100644 --- a/src/backends/gvoice/browser_emu.py +++ b/src/backends/gvoice/browser_emu.py @@ -59,6 +59,7 @@ class MozillaEmulator(object): self.trycount = trycount self._cookies = cookielib.LWPCookieJar() self._loadedFromCookies = False + self._storeCookies = False def load_cookies(self, path): assert not self._loadedFromCookies, "Load cookies only once" @@ -74,16 +75,18 @@ class MozillaEmulator(object): _moduleLogger.exception("No cookie file") except Exception, e: _moduleLogger.exception("Unknown error with cookies") - self._loadedFromCookies = True + else: + self._loadedFromCookies = True + self._storeCookies = True return self._loadedFromCookies def save_cookies(self): - if self._loadedFromCookies: + if self._storeCookies: self._cookies.save() def clear_cookies(self): - if self._loadedFromCookies: + if self._storeCookies: self._cookies.clear() def download(self, url, diff --git a/src/backends/gvoice/gvoice.py b/src/backends/gvoice/gvoice.py index 187c394..582c6f4 100755 --- a/src/backends/gvoice/gvoice.py +++ b/src/backends/gvoice/gvoice.py @@ -303,6 +303,9 @@ class GVoiceBackend(object): self._lastAuthed = time.time() return True + def persist(self): + self._browser.save_cookies() + def logout(self): self._browser.clear_cookies() self._browser.save_cookies() diff --git a/src/dialcentral_qt.py b/src/dialcentral_qt.py index 809756c..88747f9 100755 --- a/src/dialcentral_qt.py +++ b/src/dialcentral_qt.py @@ -7,6 +7,7 @@ import sys import os import shutil import simplejson +import re import logging from PyQt4 import QtGui @@ -246,6 +247,10 @@ class SMSEntryWindow(object): self._contacts = [] self._app = app self._session = session + self._session.draft.recipientsChanged.connect(self._on_recipients_changed) + self._session.draft.called.connect(self._on_op_finished) + self._session.draft.sentMessage.connect(self._on_op_finished) + self._session.draft.cancelled.connect(self._on_op_finished) self._errorLog = errorLog self._history = QtGui.QListView() @@ -274,18 +279,18 @@ class SMSEntryWindow(object): self._buttonLayout.addWidget(self._dialButton) self._layout = QtGui.QVBoxLayout() - self._layout.addLayout(self._entryLayout) + self._layout.addWidget(self._scrollEntry) self._layout.addLayout(self._buttonLayout) centralWidget = QtGui.QWidget() centralWidget.setLayout(self._layout) self._window = QtGui.QMainWindow(parent) - self._window.setAttribute(QtCore.Qt.WA_DeleteOnClose, False) qui_utils.set_autorient(self._window, True) qui_utils.set_stackable(self._window, True) self._window.setWindowTitle("Contact") self._window.setCentralWidget(centralWidget) + self._window.show() def _update_letter_count(self): count = self._smsEntry.toPlainText().size() @@ -309,6 +314,20 @@ class SMSEntryWindow(object): @QtCore.pyqtSlot() @misc_utils.log_exception(_moduleLogger) + def _on_recipients_changed(self): + draftContacts = len(self._session.draft.get_contacts()) + if draftContacts == 0: + self._window.hide() + else: + self._window.show() + + @QtCore.pyqtSlot() + @misc_utils.log_exception(_moduleLogger) + def _on_op_finished(self): + self._window.hide() + + @QtCore.pyqtSlot() + @misc_utils.log_exception(_moduleLogger) def _on_letter_count_changed(self): self._update_letter_count() self._update_button_state() @@ -495,7 +514,12 @@ class Dialpad(object): def _on_sms_clicked(self, checked = False): number = str(self._entry.text()) self._entry.clear() - self._session.draft.add_contact(number, []) + + contactId = number + title = number + description = number + numbersWithDescriptions = [(number, "")] + self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions) @QtCore.pyqtSlot() @QtCore.pyqtSlot(bool) @@ -503,7 +527,12 @@ class Dialpad(object): def _on_call_clicked(self, checked = False): number = str(self._entry.text()) self._entry.clear() - self._session.draft.add_contact(number, []) + + contactId = number + title = number + description = number + numbersWithDescriptions = [(number, "")] + self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions) self._session.draft.call() @@ -515,12 +544,12 @@ class History(object): FROM_IDX = 3 MAX_IDX = 4 - HISTORY_ITEM_TYPES = ["All", "Received", "Missed", "Placed"] + HISTORY_ITEM_TYPES = ["Received", "Missed", "Placed", "All"] HISTORY_COLUMNS = ["When", "What", "Number", "From"] assert len(HISTORY_COLUMNS) == MAX_IDX def __init__(self, app, session, errorLog): - self._selectedFilter = self.HISTORY_ITEM_TYPES[0] + self._selectedFilter = self.HISTORY_ITEM_TYPES[-1] self._app = app self._session = session self._session.historyUpdated.connect(self._on_history_updated) @@ -531,7 +560,7 @@ class History(object): self._typeSelection.setCurrentIndex( self.HISTORY_ITEM_TYPES.index(self._selectedFilter) ) - self._typeSelection.currentIndexChanged.connect(self._on_filter_changed) + self._typeSelection.currentIndexChanged[str].connect(self._on_filter_changed) self._itemStore = QtGui.QStandardItemModel() self._itemStore.setHorizontalHeaderLabels(self.HISTORY_COLUMNS) @@ -567,15 +596,44 @@ class History(object): self._itemView.clear() def refresh(self): - pass + self._session.update_history() def _populate_items(self): - self._errorLog.push_message("Not supported") + self._itemStore.clear() + history = self._session.get_history() + history.sort(key=lambda item: item["time"], reverse=True) + for event in history: + if self._selectedFilter in [self.HISTORY_ITEM_TYPES[-1], event["action"]]: + relTime = abbrev_relative_date(event["relTime"]) + action = event["action"] + number = event["number"] + prettyNumber = make_pretty(number) + name = event["name"] + if not name or name == number: + name = event["location"] + if not name: + name = "Unknown" + + timeItem = QtGui.QStandardItem(relTime) + actionItem = QtGui.QStandardItem(action) + numberItem = QtGui.QStandardItem(prettyNumber) + nameItem = QtGui.QStandardItem(name) + row = timeItem, actionItem, numberItem, nameItem + for item in row: + item.setEditable(False) + item.setCheckable(False) + if item is not nameItem: + itemFont = item.font() + itemFont.setPointSize(max(itemFont.pointSize() - 3, 5)) + item.setFont(itemFont) + numberItem.setData(event) + self._itemStore.appendRow(row) @QtCore.pyqtSlot(str) @misc_utils.log_exception(_moduleLogger) def _on_filter_changed(self, newItem): self._selectedFilter = str(newItem) + self._populate_items() @QtCore.pyqtSlot() @misc_utils.log_exception(_moduleLogger) @@ -586,7 +644,30 @@ class History(object): @misc_utils.log_exception(_moduleLogger) def _on_row_activated(self, index): rowIndex = index.row() - #self._session.draft.add_contact(number, details) + item = self._itemStore.item(rowIndex, self.NUMBER_IDX) + contactDetails = item.data().toPyObject() + + title = str(self._itemStore.item(rowIndex, self.FROM_IDX).text()) + number = str(contactDetails[QtCore.QString("number")]) + contactId = number # ids don't seem too unique so using numbers + + descriptionRows = [] + # @bug doesn't seem to print multiple entries + for i in xrange(self._itemStore.rowCount()): + iItem = self._itemStore.item(i, self.NUMBER_IDX) + iContactDetails = iItem.data().toPyObject() + iNumber = str(iContactDetails[QtCore.QString("number")]) + if number != iNumber: + continue + relTime = abbrev_relative_date(iContactDetails[QtCore.QString("relTime")]) + action = str(iContactDetails[QtCore.QString("action")]) + number = str(iContactDetails[QtCore.QString("number")]) + prettyNumber = make_pretty(number) + rowItems = relTime, action, prettyNumber + descriptionRows.append("%s" % "".join(rowItems)) + description = "%s
" % "".join(descriptionRows) + numbersWithDescriptions = [(str(contactDetails[QtCore.QString("number")]), "")] + self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions) class Messages(object): @@ -602,6 +683,8 @@ class Messages(object): ALL_STATUS = "Any" MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS] + _MIN_MESSAGES_SHOWN = 4 + def __init__(self, app, session, errorLog): self._selectedTypeFilter = self.ALL_TYPES self._selectedStatusFilter = self.ALL_STATUS @@ -615,14 +698,14 @@ class Messages(object): self._typeSelection.setCurrentIndex( self.MESSAGE_TYPES.index(self._selectedTypeFilter) ) - self._typeSelection.currentIndexChanged.connect(self._on_type_filter_changed) + self._typeSelection.currentIndexChanged[str].connect(self._on_type_filter_changed) self._statusSelection = QtGui.QComboBox() self._statusSelection.addItems(self.MESSAGE_STATUSES) self._statusSelection.setCurrentIndex( self.MESSAGE_STATUSES.index(self._selectedStatusFilter) ) - self._statusSelection.currentIndexChanged.connect(self._on_status_filter_changed) + self._statusSelection.currentIndexChanged[str].connect(self._on_status_filter_changed) self._selectionLayout = QtGui.QHBoxLayout() self._selectionLayout.addWidget(self._typeSelection) @@ -631,13 +714,15 @@ class Messages(object): self._itemStore = QtGui.QStandardItemModel() self._itemStore.setHorizontalHeaderLabels(["Messages"]) + self._htmlDelegate = qui_utils.QHtmlDelegate() self._itemView = QtGui.QTreeView() self._itemView.setModel(self._itemStore) - self._itemView.setUniformRowHeights(True) + self._itemView.setUniformRowHeights(False) self._itemView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self._itemView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) self._itemView.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) self._itemView.setHeaderHidden(True) + self._itemView.setItemDelegate(self._htmlDelegate) self._itemView.activated.connect(self._on_row_activated) self._layout = QtGui.QVBoxLayout() @@ -662,20 +747,80 @@ class Messages(object): self._itemView.clear() def refresh(self): - pass + self._session.update_messages() def _populate_items(self): - self._errorLog.push_message("Not supported") + self._itemStore.clear() + rawMessages = self._session.get_messages() + rawMessages.sort(key=lambda item: item["time"], reverse=True) + for item in rawMessages: + isUnarchived = not item["isArchived"] + isUnread = not item["isRead"] + visibleStatus = { + self.UNREAD_STATUS: isUnarchived and isUnread, + self.UNARCHIVED_STATUS: isUnarchived, + self.ALL_STATUS: True, + }[self._selectedStatusFilter] + + visibleType = self._selectedTypeFilter in [item["type"], self.ALL_TYPES] + if visibleType and visibleStatus: + relTime = abbrev_relative_date(item["relTime"]) + number = item["number"] + prettyNumber = make_pretty(number) + name = item["name"] + if not name or name == number: + name = item["location"] + if not name: + name = "Unknown" + + messageParts = list(item["messageParts"]) + if len(messageParts) == 0: + messages = ("No Transcription", ) + elif len(messageParts) == 1: + if messageParts[0][1]: + messages = (messageParts[0][1], ) + else: + messages = ("No Transcription", ) + else: + messages = [ + "%s: %s" % (messagePart[0], messagePart[1]) + for messagePart in messageParts + ] + + firstMessage = "%s - %s (%s)" % (name, prettyNumber, relTime) + + expandedMessages = [firstMessage] + expandedMessages.extend(messages) + if (self._MIN_MESSAGES_SHOWN + 1) < len(messages): + secondMessage = "%d Messages Hidden..." % (len(messages) - self._MIN_MESSAGES_SHOWN, ) + collapsedMessages = [firstMessage, secondMessage] + collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):]) + else: + collapsedMessages = expandedMessages + + item = dict(item.iteritems()) + item["collapsedMessages"] = "
\n".join(collapsedMessages) + item["expandedMessages"] = "
\n".join(expandedMessages) + + messageItem = QtGui.QStandardItem(item["collapsedMessages"]) + # @bug Not showing all of a message + messageItem.setData(item) + messageItem.setEditable(False) + messageItem.setCheckable(False) + row = (messageItem, ) + self._itemStore.appendRow(row) @QtCore.pyqtSlot(str) @misc_utils.log_exception(_moduleLogger) def _on_type_filter_changed(self, newItem): self._selectedTypeFilter = str(newItem) + self._populate_items() @QtCore.pyqtSlot(str) @misc_utils.log_exception(_moduleLogger) def _on_status_filter_changed(self, newItem): self._selectedStatusFilter = str(newItem) + self._populate_items() @QtCore.pyqtSlot() @misc_utils.log_exception(_moduleLogger) @@ -686,7 +831,21 @@ class Messages(object): @misc_utils.log_exception(_moduleLogger) def _on_row_activated(self, index): rowIndex = index.row() - #self._session.draft.add_contact(number, details) + item = self._itemStore.item(rowIndex, 0) + contactDetails = item.data().toPyObject() + + name = str(contactDetails[QtCore.QString("name")]) + number = str(contactDetails[QtCore.QString("number")]) + if not name or name == number: + name = str(contactDetails[QtCore.QString("location")]) + if not name: + name = "Unknown" + + contactId = str(contactDetails[QtCore.QString("id")]) + title = name + description = str(contactDetails[QtCore.QString("expandedMessages")]) + numbersWithDescriptions = [(number, "")] + self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions) class Contacts(object): @@ -700,8 +859,9 @@ class Contacts(object): self._listSelection = QtGui.QComboBox() self._listSelection.addItems([]) + # @todo Implement more contact lists #self._listSelection.setCurrentIndex(self.HISTORY_ITEM_TYPES.index(self._selectedFilter)) - self._listSelection.currentIndexChanged.connect(self._on_filter_changed) + self._listSelection.currentIndexChanged[str].connect(self._on_filter_changed) self._itemStore = QtGui.QStandardItemModel() self._itemStore.setHorizontalHeaderLabels(["Contacts"]) @@ -737,10 +897,22 @@ class Contacts(object): self._itemView.clear() def refresh(self): - pass + self._session.update_contacts() def _populate_items(self): - self._errorLog.push_message("Not supported") + self._itemStore.clear() + + contacts = list(self._session.get_contacts().itervalues()) + contacts.sort(key=lambda contact: contact["name"].lower()) + for item in contacts: + name = item["name"] + numbers = item["numbers"] + nameItem = QtGui.QStandardItem(name) + nameItem.setEditable(False) + nameItem.setCheckable(False) + nameItem.setData(item) + row = (nameItem, ) + self._itemStore.appendRow(row) @QtCore.pyqtSlot(str) @misc_utils.log_exception(_moduleLogger) @@ -756,7 +928,43 @@ class Contacts(object): @misc_utils.log_exception(_moduleLogger) def _on_row_activated(self, index): rowIndex = index.row() - #self._session.draft.add_contact(number, details) + item = self._itemStore.item(rowIndex, 0) + contactDetails = item.data().toPyObject() + + name = str(contactDetails[QtCore.QString("name")]) + if not name: + name = str(contactDetails[QtCore.QString("location")]) + if not name: + name = "Unknown" + + contactId = str(contactDetails[QtCore.QString("contactId")]) + numbers = contactDetails[QtCore.QString("numbers")] + numbers = [ + dict( + (str(k), str(v)) + for (k, v) in number.iteritems() + ) + for number in numbers + ] + numbersWithDescriptions = [ + ( + number["phoneNumber"], + self._choose_phonetype(number), + ) + for number in numbers + ] + title = name + description = name + self._session.draft.add_contact(contactId, title, description, numbersWithDescriptions) + + @staticmethod + def _choose_phonetype(numberDetails): + if "phoneTypeName" in numberDetails: + return numberDetails["phoneTypeName"] + elif "phoneType" in numberDetails: + return numberDetails["phoneType"] + else: + return "" class MainWindow(object): @@ -790,8 +998,10 @@ class MainWindow(object): self._session.error.connect(self._on_session_error) self._session.loggedIn.connect(self._on_login) self._session.loggedOut.connect(self._on_logout) + self._session.draft.recipientsChanged.connect(self._on_recipients_changed) self._credentialsDialog = None + self._smsEntryDialog = None self._errorLog = qui_utils.QErrorLog() self._errorDisplay = qui_utils.ErrorDisplay(self._errorLog) @@ -908,9 +1118,9 @@ class MainWindow(object): def _initialize_tab(self, index): assert index < self.MAX_TABS if not self._tabsContents[index].has_child(): - self._tabsContents[index].set_child( - self._TAB_CLASS[index](self._app, self._session, self._errorLog) - ) + tab = self._TAB_CLASS[index](self._app, self._session, self._errorLog) + self._tabsContents[index].set_child(tab) + self._tabsContents[index].refresh() @QtCore.pyqtSlot(str) @misc_utils.log_exception(_moduleLogger) @@ -930,6 +1140,13 @@ class MainWindow(object): tab.disable() @QtCore.pyqtSlot() + @misc_utils.log_exception(_moduleLogger) + def _on_recipients_changed(self): + if self._smsEntryDialog is None: + self._smsEntryDialog = SMSEntryWindow(self.window, self._app, self._session, self._errorLog) + pass + + @QtCore.pyqtSlot() @QtCore.pyqtSlot(bool) @misc_utils.log_exception(_moduleLogger) def _on_login_requested(self, checked = True): @@ -966,6 +1183,132 @@ class MainWindow(object): self.close() +def make_ugly(prettynumber): + """ + function to take a phone number and strip out all non-numeric + characters + + >>> make_ugly("+012-(345)-678-90") + '+01234567890' + """ + return normalize_number(prettynumber) + + +def normalize_number(prettynumber): + """ + function to take a phone number and strip out all non-numeric + characters + + >>> normalize_number("+012-(345)-678-90") + '+01234567890' + >>> normalize_number("1-(345)-678-9000") + '+13456789000' + >>> normalize_number("+1-(345)-678-9000") + '+13456789000' + """ + uglynumber = re.sub('[^0-9+]', '', prettynumber) + + if uglynumber.startswith("+"): + pass + elif uglynumber.startswith("1"): + uglynumber = "+"+uglynumber + elif 10 <= len(uglynumber): + assert uglynumber[0] not in ("+", "1") + uglynumber = "+1"+uglynumber + else: + pass + + return uglynumber + + +def _make_pretty_with_areacode(phonenumber): + prettynumber = "(%s)" % (phonenumber[0:3], ) + if 3 < len(phonenumber): + prettynumber += " %s" % (phonenumber[3:6], ) + if 6 < len(phonenumber): + prettynumber += "-%s" % (phonenumber[6:], ) + return prettynumber + + +def _make_pretty_local(phonenumber): + prettynumber = "%s" % (phonenumber[0:3], ) + if 3 < len(phonenumber): + prettynumber += "-%s" % (phonenumber[3:], ) + return prettynumber + + +def _make_pretty_international(phonenumber): + prettynumber = phonenumber + if phonenumber.startswith("1"): + prettynumber = "1 " + prettynumber += _make_pretty_with_areacode(phonenumber[1:]) + return prettynumber + + +def make_pretty(phonenumber): + """ + Function to take a phone number and return the pretty version + pretty numbers: + if phonenumber begins with 0: + ...-(...)-...-.... + if phonenumber begins with 1: ( for gizmo callback numbers ) + 1 (...)-...-.... + if phonenumber is 13 digits: + (...)-...-.... + if phonenumber is 10 digits: + ...-.... + >>> make_pretty("12") + '12' + >>> make_pretty("1234567") + '123-4567' + >>> make_pretty("2345678901") + '+1 (234) 567-8901' + >>> make_pretty("12345678901") + '+1 (234) 567-8901' + >>> make_pretty("01234567890") + '+012 (345) 678-90' + >>> make_pretty("+01234567890") + '+012 (345) 678-90' + >>> make_pretty("+12") + '+1 (2)' + >>> make_pretty("+123") + '+1 (23)' + >>> make_pretty("+1234") + '+1 (234)' + """ + if phonenumber is None or phonenumber is "": + return "" + + phonenumber = normalize_number(phonenumber) + + if phonenumber[0] == "+": + prettynumber = _make_pretty_international(phonenumber[1:]) + if not prettynumber.startswith("+"): + prettynumber = "+"+prettynumber + elif 8 < len(phonenumber) and phonenumber[0] in ("1", ): + prettynumber = _make_pretty_international(phonenumber) + elif 7 < len(phonenumber): + prettynumber = _make_pretty_with_areacode(phonenumber) + elif 3 < len(phonenumber): + prettynumber = _make_pretty_local(phonenumber) + else: + prettynumber = phonenumber + return prettynumber.strip() + + +def abbrev_relative_date(date): + """ + >>> abbrev_relative_date("42 hours ago") + '42 h' + >>> abbrev_relative_date("2 days ago") + '2 d' + >>> abbrev_relative_date("4 weeks ago") + '4 w' + """ + parts = date.split(" ") + return "%s %s" % (parts[0], parts[1][0]) + + def run(): app = QtGui.QApplication([]) handle = Dialcentral(app) diff --git a/src/session.py b/src/session.py index 0c650e0..28e56b1 100644 --- a/src/session.py +++ b/src/session.py @@ -32,37 +32,66 @@ class Draft(QtCore.QObject): def send(self, text): assert 0 < len(self._contacts) - self.sendingMessage.emit() - self.error.emit("Not Implemented") - # self.clear() + le = concurrent.AsyncLinearExecution(self._pool, self._send) + le.start(text) def call(self): assert len(self._contacts) == 1 - self.calling.emit() - self.error.emit("Not Implemented") - # self.clear() + le = concurrent.AsyncLinearExecution(self._pool, self._call) + le.start() def cancel(self): - self.cancelling.emit() - self.error.emit("Not Implemented") + le = concurrent.AsyncLinearExecution(self._pool, self._cancel) + le.start() - def add_contact(self, contact, details): - assert contact not in self._contacts - self._contacts[contact] = details + def add_contact(self, contactId, title, description, numbersWithDescriptions): + assert contactId not in self._contacts + contactDetails = title, description, numbersWithDescriptions + self._contacts[contactId] = contactDetails self.recipientsChanged.emit() - def remove_contact(self, contact): - assert contact not in self._contacts - del self._contacts[contact] + def remove_contact(self, contactId): + assert contactId in self._contacts + del self._contacts[contactId] self.recipientsChanged.emit() - def get_contacts(self, contact): + def get_contacts(self): return self._contacts def clear(self): self._contacts = {} self.recipientsChanged.emit() + def _send(self, text): + self.sendingMessage.emit() + try: + self.error.emit("Not Implemented") + self.sentMessage.emit() + self.clear() + except Exception, e: + self.error.emit(str(e)) + + def _call(self): + self.calling.emit() + try: + self.error.emit("Not Implemented") + self.called.emit() + self.clear() + except Exception, e: + self.error.emit(str(e)) + + def _cancel(self): + self.cancelling.emit() + try: + yield ( + self._backend.cancel, + (), + {}, + ) + self.cancelled.emit() + except Exception, e: + self.error.emit(str(e)) + class Session(QtCore.QObject): @@ -95,10 +124,11 @@ class Session(QtCore.QObject): self._username = None self._draft = Draft(self._pool) - self._contacts = [] + self._contacts = {} self._messages = [] self._history = [] self._dnd = False + self._callback = "" @property def state(self): @@ -113,6 +143,7 @@ class Session(QtCore.QObject): def login(self, username, password): assert self.state == self.LOGGEDOUT_STATE + assert username != "" if self._cachePath is not None: cookiePath = os.path.join(self._cachePath, "%s.cookies" % username) else: @@ -128,20 +159,22 @@ class Session(QtCore.QObject): def logout(self): assert self.state != self.LOGGEDOUT_STATE self._pool.stop() - self.error.emit("Not Implemented") + self._loggedInTime = self._LOGGEDOUT_TIME + self._backend.persist() + self._save_to_cache() def clear(self): assert self.state == self.LOGGEDOUT_STATE + self._backend.logout() self._backend = None + self._clear_cache() self._draft.clear() - self._contacts = [] - self.contactsUpdated.emit() - self._messages = [] - self.messagesUpdated.emit() - self._history = [] - self.historyUpdated.emit() - self._dnd = False - self.dndStateChange.emit(self._dnd) + + def logout_and_clear(self): + assert self.state != self.LOGGEDOUT_STATE + self._pool.stop() + self._loggedInTime = self._LOGGEDOUT_TIME + self.clear() def update_contacts(self): le = concurrent.AsyncLinearExecution(self._pool, self._update_contacts) @@ -169,21 +202,50 @@ class Session(QtCore.QObject): self._perform_op_while_loggedin(le) def set_dnd(self, dnd): + # I'm paranoid about our state geting out of sync so we set no matter + # what but act as if we have the cannonical state assert self.state == self.LOGGEDIN_STATE - self.error.emit("Not Implemented") + oldDnd = self._dnd + try: + yield ( + self._backend.set_dnd, + (dnd), + {}, + ) + except Exception, e: + self.error.emit(str(e)) + return + self._dnd = dnd + if oldDnd != self._dnd: + self.dndStateChange.emit(self._dnd) def get_dnd(self): return self._dnd def get_callback_numbers(self): - return [] + # @todo Remove evilness + return self._backend.get_callback_numbers() def get_callback_number(self): - return "" + return self._callback - def set_callback_number(self): + def set_callback_number(self, callback): + # I'm paranoid about our state geting out of sync so we set no matter + # what but act as if we have the cannonical state assert self.state == self.LOGGEDIN_STATE - self.error.emit("Not Implemented") + oldCallback = self._callback + try: + yield ( + self._backend.set_callback_number, + (callback), + {}, + ) + except Exception, e: + self.error.emit(str(e)) + return + self._callback = callback + if oldCallback != self._callback: + self.callbackNumberChanged.emit(self._callback) def _login(self, username, password): self._loggedInTime = self._LOGGINGIN_TIME @@ -193,96 +255,154 @@ class Session(QtCore.QObject): isLoggedIn = False if not isLoggedIn and self._backend.is_quick_login_possible(): - try: - isLoggedIn = yield ( - self._backend.is_authed, + isLoggedIn = yield ( + self._backend.is_authed, + (), + {}, + ) + if isLoggedIn: + _moduleLogger.info("Logged in through cookies") + else: + # Force a clearing of the cookies + yield ( + self._backend.logout, (), {}, ) - except Exception, e: - self.error.emit(str(e)) - return - if isLoggedIn: - _moduleLogger.info("Logged in through cookies") if not isLoggedIn: - try: - isLoggedIn = yield ( - self._backend.login, - (username, password), - {}, - ) - except Exception, e: - self.error.emit(str(e)) - return + isLoggedIn = yield ( + self._backend.login, + (username, password), + {}, + ) if isLoggedIn: _moduleLogger.info("Logged in through credentials") if isLoggedIn: - self._loggedInTime = time.time() + self._loggedInTime = int(time.time()) + oldUsername = self._username self._username = username finalState = self.LOGGEDIN_STATE self.loggedIn.emit() - # if the username is the same, do nothing - # else clear the in-memory caches and attempt to load from file-caches - # If caches went from empty to something, fire signals - # Fire off queued async ops + if oldUsername != self._username: + self._load_from_cache() + loginOps = self._loginOps[:] + del self._loginOps[:] + for asyncOp in loginOps: + asyncOp.start() except Exception, e: self.error.emit(str(e)) finally: self.stateChange.emit(finalState) + def _load_from_cache(self): + updateContacts = len(self._contacts) != 0 + updateMessages = len(self._messages) != 0 + updateHistory = len(self._history) != 0 + oldDnd = self._dnd + oldCallback = self._callback + + self._contacts = {} + self._messages = [] + self._history = [] + self._dnd = False + self._callback = "" + + if updateContacts: + self.contactsUpdated.emit() + if updateMessages: + self.messagesUpdated.emit() + if updateHistory: + self.historyUpdated.emit() + if oldDnd != self._dnd: + self.dndStateChange.emit(self._dnd) + if oldCallback != self._callback: + self.callbackNumberChanged.emit(self._callback) + + def _save_to_cache(self): + # @todo + pass + + def _clear_cache(self): + updateContacts = len(self._contacts) != 0 + updateMessages = len(self._messages) != 0 + updateHistory = len(self._history) != 0 + oldDnd = self._dnd + oldCallback = self._callback + + self._contacts = {} + self._messages = [] + self._history = [] + self._dnd = False + self._callback = "" + + if updateContacts: + self.contactsUpdated.emit() + if updateMessages: + self.messagesUpdated.emit() + if updateHistory: + self.historyUpdated.emit() + if oldDnd != self._dnd: + self.dndStateChange.emit(self._dnd) + if oldCallback != self._callback: + self.callbackNumberChanged.emit(self._callback) + + self._save_to_cache() + def _update_contacts(self): - self.error.emit("Not Implemented") try: - isLoggedIn = yield ( - self._backend.is_authed, + self._contacts = yield ( + self._backend.get_contacts, (), {}, ) except Exception, e: self.error.emit(str(e)) return + self.contactsUpdated.emit() def _update_messages(self): - self.error.emit("Not Implemented") try: - isLoggedIn = yield ( - self._backend.is_authed, + self._messages = yield ( + self._backend.get_messages, (), {}, ) except Exception, e: self.error.emit(str(e)) return + self.messagesUpdated.emit() def _update_history(self): - self.error.emit("Not Implemented") try: - isLoggedIn = yield ( - self._backend.is_authed, + self._history = yield ( + self._backend.get_recent, (), {}, ) except Exception, e: self.error.emit(str(e)) return + self.historyUpdated.emit() def _update_dnd(self): - self.error.emit("Not Implemented") + oldDnd = self._dnd try: - isLoggedIn = yield ( - self._backend.is_authed, + self._dnd = yield ( + self._backend.is_dnd, (), {}, ) except Exception, e: self.error.emit(str(e)) return + if oldDnd != self._dnd: + self.dndStateChange(self._dnd) def _perform_op_while_loggedin(self, op): if self.state == self.LOGGEDIN_STATE: - op() + op.start() else: self._push_login_op(op) diff --git a/src/util/qtpie.py b/src/util/qtpie.py index 4751ae3..d536038 100755 --- a/src/util/qtpie.py +++ b/src/util/qtpie.py @@ -526,6 +526,12 @@ class QPieButton(QtGui.QWidget): self._mousePosition = None self.setFocusPolicy(QtCore.Qt.StrongFocus) + self.setSizePolicy( + QtGui.QSizePolicy( + QtGui.QSizePolicy.MinimumExpanding, + QtGui.QSizePolicy.MinimumExpanding, + ) + ) def insertItem(self, item, index = -1): self._filing.insertItem(item, index) @@ -567,6 +573,9 @@ class QPieButton(QtGui.QWidget): self._buttonFiling.setOuterRadius(radius) self._buttonArtist.show(self.palette()) + def sizeHint(self): + return self._buttonArtist.pieSize() + def minimumSizeHint(self): return self._buttonArtist.centerSize() diff --git a/src/util/qtpieboard.py b/src/util/qtpieboard.py index c7094f4..0e3ac99 100755 --- a/src/util/qtpieboard.py +++ b/src/util/qtpieboard.py @@ -49,7 +49,7 @@ class PieKeyboard(object): "NORTH_EAST", ] - def __init__(self): + def __init__(self, rows, columns): self._layout = QtGui.QGridLayout() self._widget = QtGui.QWidget() self._widget.setLayout(self._layout) diff --git a/src/util/qui_utils.py b/src/util/qui_utils.py index 47697db..dfbdcde 100644 --- a/src/util/qui_utils.py +++ b/src/util/qui_utils.py @@ -96,6 +96,52 @@ class ErrorDisplay(object): self._message.setText(self._errorLog.peek_message()) +class QHtmlDelegate(QtGui.QStyledItemDelegate): + + def paint(self, painter, option, index): + newOption = QtGui.QStyleOptionViewItemV4(option) + self.initStyleOption(newOption, index) + + doc = QtGui.QTextDocument() + doc.setHtml(newOption.text) + doc.setTextWidth(newOption.rect.width()) + + if newOption.widget is not None: + style = newOption.widget.style() + else: + style = QtGui.QApplication.style() + + newOption.text = "" + style.drawControl(QtGui.QStyle.CE_ItemViewItem, newOption, painter) + + ctx = QtGui.QAbstractTextDocumentLayout.PaintContext() + if newOption.state & QtGui.QStyle.State_Selected: + ctx.palette.setColor( + QtGui.QPalette.Text, + newOption.palette.color( + QtGui.QPalette.Active, + QtGui.QPalette.HighlightedText + ) + ) + + textRect = style.subElementRect(QtGui.QStyle.SE_ItemViewItemText, newOption) + painter.save() + painter.translate(textRect.topLeft()) + painter.setClipRect(textRect.translated(-textRect.topLeft())) + doc.documentLayout().draw(painter, ctx) + painter.restore() + + def sizeHint(self, option, index): + newOption = QtGui.QStyleOptionViewItemV4(option) + self.initStyleOption(newOption, index) + + doc = QtGui.QTextDocument() + doc.setHtml(newOption.text) + doc.setTextWidth(newOption.rect.width()) + size = QtCore.QSize(doc.idealWidth(), doc.size().height()) + return size + + def _null_set_stackable(window, isStackable): pass -- 1.7.9.5