Removing contact caching from backend, moving it to session
authorEd Page <eopage@byu.net>
Sat, 18 Sep 2010 21:24:32 +0000 (16:24 -0500)
committerEd Page <eopage@byu.net>
Sat, 18 Sep 2010 21:24:32 +0000 (16:24 -0500)
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
src/backends/gvoice/browser_emu.py
src/backends/gvoice/gvoice.py
src/dialcentral_qt.py
src/session.py
src/util/qtpie.py
src/util/qtpieboard.py
src/util/qui_utils.py

index 49294e7..012be7e 100644 (file)
@@ -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": "<i>%s</i>",
index 5e9b678..3f3cc51 100644 (file)
@@ -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,
index 187c394..582c6f4 100755 (executable)
@@ -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()
index 809756c..88747f9 100755 (executable)
@@ -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("<tr><td>%s</td></tr>" % "</td><td>".join(rowItems))
+               description = "<table>%s</table>" % "".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 = [
+                                               "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
+                                               for messagePart in messageParts
+                                       ]
+
+                               firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (name, prettyNumber, relTime)
+
+                               expandedMessages = [firstMessage]
+                               expandedMessages.extend(messages)
+                               if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
+                                       secondMessage = "<i>%d Messages Hidden...</i>" % (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"] = "<br/>\n".join(collapsedMessages)
+                               item["expandedMessages"] = "<br/>\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)
index 0c650e0..28e56b1 100644 (file)
@@ -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)
 
index 4751ae3..d536038 100755 (executable)
@@ -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()
 
index c7094f4..0e3ac99 100755 (executable)
@@ -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)
index 47697db..dfbdcde 100644 (file)
@@ -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