X-Git-Url: http://git.maemo.org/git/?p=gc-dialer;a=blobdiff_plain;f=src%2Fgv_views.py;h=83a0707a81ae3d7cd98eeb4017dedab90684c558;hp=7d1e8ef60d1a5e4c3f744ede61a0aa8e161b5f13;hb=0320d6d2086ce89fe554d8f902531f5b00dbec94;hpb=2a3af9da766de1bf65d2ac626bc9147172e962fe diff --git a/src/gv_views.py b/src/gv_views.py index 7d1e8ef..83a0707 100644 --- a/src/gv_views.py +++ b/src/gv_views.py @@ -1,4 +1,4 @@ -#!/usr/bin/python2.5 +#!/usr/bin/env python """ DialCentral - Front end for Google's GoogleVoice service. @@ -18,14 +18,16 @@ You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +@todo Collapse voicemails @todo Alternate UI for dialogs (stackables) """ from __future__ import with_statement +import re import ConfigParser -import logging import itertools +import logging import gobject import pango @@ -33,8 +35,8 @@ import gtk import gtk_toolbox import hildonize -import gv_backend -import null_backend +from backends import gv_backend +from backends import null_backend _moduleLogger = logging.getLogger("gv_views") @@ -46,13 +48,67 @@ def make_ugly(prettynumber): characters >>> make_ugly("+012-(345)-678-90") - '01234567890' + '+01234567890' + """ + return normalize_number(prettynumber) + + +def normalize_number(prettynumber): """ - import re - uglynumber = re.sub('\D', '', 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") and len(uglynumber) == 11: + uglynumber = "+"+uglynumber + elif len(uglynumber) == 10: + uglynumber = "+1"+uglynumber + else: + pass + + #validateRe = re.compile("^\+?[0-9]{10,}$") + #assert validateRe.match(uglynumber) is not None + return uglynumber +def _make_pretty_with_areacodde(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("0"): + prettynumber = "+%s " % (phonenumber[0:3], ) + if 3 < len(phonenumber): + prettynumber += _make_pretty_with_areacodde(phonenumber[3:]) + if phonenumber.startswith("1"): + prettynumber = "1 " + prettynumber += _make_pretty_with_areacodde(phonenumber[1:]) + return prettynumber + + def make_pretty(phonenumber): """ Function to take a phone number and return the pretty version @@ -70,37 +126,38 @@ def make_pretty(phonenumber): >>> make_pretty("1234567") '123-4567' >>> make_pretty("2345678901") - '(234)-567-8901' + '+1 (234) 567-8901' >>> make_pretty("12345678901") - '1 (234)-567-8901' + '+1 (234) 567-8901' >>> make_pretty("01234567890") - '+012-(345)-678-90' + '+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 = make_ugly(phonenumber) + phonenumber = normalize_number(phonenumber) - if len(phonenumber) < 3: - return phonenumber - - if phonenumber[0] == "0": - prettynumber = "" - prettynumber += "+%s" % phonenumber[0:3] - if 3 < len(phonenumber): - prettynumber += "-(%s)" % phonenumber[3:6] - if 6 < len(phonenumber): - prettynumber += "-%s" % phonenumber[6:9] - if 9 < len(phonenumber): - prettynumber += "-%s" % phonenumber[9:] - return prettynumber - elif len(phonenumber) <= 7: - prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:]) - elif len(phonenumber) > 8 and phonenumber[0] == "1": - prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:]) - elif len(phonenumber) > 7: - prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:]) - return prettynumber + if phonenumber[0] == "+": + prettynumber = _make_pretty_international(phonenumber[1:]) + if not prettynumber.startswith("+"): + prettynumber = "+"+prettynumber + elif 8 < len(phonenumber) and phonenumber[0] in ("0", "1"): + prettynumber = _make_pretty_international(phonenumber) + elif 7 < len(phonenumber): + prettynumber = _make_pretty_with_areacodde(phonenumber) + elif 3 < len(phonenumber): + prettynumber = _make_pretty_local(phonenumber) + else: + prettynumber = phonenumber + return prettynumber.strip() def abbrev_relative_date(date): @@ -116,304 +173,52 @@ def abbrev_relative_date(date): return "%s %s" % (parts[0], parts[1][0]) -class MergedAddressBook(object): - """ - Merger of all addressbooks - """ +def _collapse_message(messageLines, maxCharsPerLine, maxLines): + lines = 0 - def __init__(self, addressbookFactories, sorter = None): - self.__addressbookFactories = addressbookFactories - self.__addressbooks = None - self.__sort_contacts = sorter if sorter is not None else self.null_sorter - - def clear_caches(self): - self.__addressbooks = None - for factory in self.__addressbookFactories: - factory.clear_caches() + numLines = len(messageLines) + for line in messageLines[0:min(maxLines, numLines)]: + linesPerLine = max(1, int(len(line) / maxCharsPerLine)) + allowedLines = maxLines - lines + acceptedLines = min(allowedLines, linesPerLine) + acceptedChars = acceptedLines * maxCharsPerLine - def get_addressbooks(self): - """ - @returns Iterable of (Address Book Factory, Book Id, Book Name) - """ - yield self, "", "" - - def open_addressbook(self, bookId): - return self - - def contact_source_short_name(self, contactId): - if self.__addressbooks is None: - return "" - bookIndex, originalId = contactId.split("-", 1) - return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId) - - @staticmethod - def factory_name(): - return "All Contacts" - - def get_contacts(self): - """ - @returns Iterable of (contact id, contact name) - """ - if self.__addressbooks is None: - self.__addressbooks = list( - factory.open_addressbook(id) - for factory in self.__addressbookFactories - for (f, id, name) in factory.get_addressbooks() - ) - contacts = ( - ("-".join([str(bookIndex), contactId]), contactName) - for (bookIndex, addressbook) in enumerate(self.__addressbooks) - for (contactId, contactName) in addressbook.get_contacts() - ) - sortedContacts = self.__sort_contacts(contacts) - return sortedContacts - - def get_contact_details(self, contactId): - """ - @returns Iterable of (Phone Type, Phone Number) - """ - if self.__addressbooks is None: - return [] - bookIndex, originalId = contactId.split("-", 1) - return self.__addressbooks[int(bookIndex)].get_contact_details(originalId) - - @staticmethod - def null_sorter(contacts): - """ - Good for speed/low memory - """ - return contacts - - @staticmethod - def basic_firtname_sorter(contacts): - """ - Expects names in "First Last" format - """ - contactsWithKey = [ - (contactName.rsplit(" ", 1)[0], (contactId, contactName)) - for (contactId, contactName) in contacts - ] - contactsWithKey.sort() - return (contactData for (lastName, contactData) in contactsWithKey) - - @staticmethod - def basic_lastname_sorter(contacts): - """ - Expects names in "First Last" format - """ - contactsWithKey = [ - (contactName.rsplit(" ", 1)[-1], (contactId, contactName)) - for (contactId, contactName) in contacts - ] - contactsWithKey.sort() - return (contactData for (lastName, contactData) in contactsWithKey) - - @staticmethod - def reversed_firtname_sorter(contacts): - """ - Expects names in "Last, First" format - """ - contactsWithKey = [ - (contactName.split(", ", 1)[-1], (contactId, contactName)) - for (contactId, contactName) in contacts - ] - contactsWithKey.sort() - return (contactData for (lastName, contactData) in contactsWithKey) - - @staticmethod - def reversed_lastname_sorter(contacts): - """ - Expects names in "Last, First" format - """ - contactsWithKey = [ - (contactName.split(", ", 1)[0], (contactId, contactName)) - for (contactId, contactName) in contacts - ] - contactsWithKey.sort() - return (contactData for (lastName, contactData) in contactsWithKey) - - @staticmethod - def guess_firstname(name): - if ", " in name: - return name.split(", ", 1)[-1] + if acceptedChars < (len(line) + 3): + suffix = "..." else: - return name.rsplit(" ", 1)[0] - - @staticmethod - def guess_lastname(name): - if ", " in name: - return name.split(", ", 1)[0] - else: - return name.rsplit(" ", 1)[-1] - - @classmethod - def advanced_firstname_sorter(cls, contacts): - contactsWithKey = [ - (cls.guess_firstname(contactName), (contactId, contactName)) - for (contactId, contactName) in contacts - ] - contactsWithKey.sort() - return (contactData for (lastName, contactData) in contactsWithKey) - - @classmethod - def advanced_lastname_sorter(cls, contacts): - contactsWithKey = [ - (cls.guess_lastname(contactName), (contactId, contactName)) - for (contactId, contactName) in contacts - ] - contactsWithKey.sort() - return (contactData for (lastName, contactData) in contactsWithKey) - - -class PhoneTypeSelector(object): - - ACTION_CANCEL = "cancel" - ACTION_SELECT = "select" - ACTION_DIAL = "dial" - ACTION_SEND_SMS = "sms" - - def __init__(self, widgetTree, gcBackend): - self._gcBackend = gcBackend - self._widgetTree = widgetTree - - self._dialog = self._widgetTree.get_widget("phonetype_dialog") - self._smsDialog = SmsEntryDialog(self._widgetTree) - - self._smsButton = self._widgetTree.get_widget("sms_button") - self._smsButton.connect("clicked", self._on_phonetype_send_sms) - - self._dialButton = self._widgetTree.get_widget("dial_button") - self._dialButton.connect("clicked", self._on_phonetype_dial) - - self._selectButton = self._widgetTree.get_widget("select_button") - self._selectButton.connect("clicked", self._on_phonetype_select) - - self._cancelButton = self._widgetTree.get_widget("cancel_button") - self._cancelButton.connect("clicked", self._on_phonetype_cancel) - - self._messagemodel = gtk.ListStore(gobject.TYPE_STRING) - self._messagesView = self._widgetTree.get_widget("phoneSelectionMessages") - self._scrollWindow = self._messagesView.get_parent() - - self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING) - self._typeviewselection = None - self._typeview = self._widgetTree.get_widget("phonetypes") - self._typeview.connect("row-activated", self._on_phonetype_select) - - self._action = self.ACTION_CANCEL - - def run(self, contactDetails, messages = (), parent = None): - self._action = self.ACTION_CANCEL - - # Add the column to the phone selection tree view - self._typemodel.clear() - self._typeview.set_model(self._typemodel) - - textrenderer = gtk.CellRendererText() - numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0) - self._typeview.append_column(numberColumn) - - textrenderer = gtk.CellRendererText() - typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1) - self._typeview.append_column(typeColumn) - - for phoneType, phoneNumber in contactDetails: - display = " - ".join((phoneNumber, phoneType)) - display = phoneType - row = (phoneNumber, display) - self._typemodel.append(row) - - self._typeviewselection = self._typeview.get_selection() - self._typeviewselection.set_mode(gtk.SELECTION_SINGLE) - self._typeviewselection.select_iter(self._typemodel.get_iter_first()) - - # Add the column to the messages tree view - self._messagemodel.clear() - self._messagesView.set_model(self._messagemodel) - - textrenderer = gtk.CellRendererText() - textrenderer.set_property("wrap-mode", pango.WRAP_WORD) - textrenderer.set_property("wrap-width", 450) - messageColumn = gtk.TreeViewColumn("") - messageColumn.pack_start(textrenderer, expand=True) - messageColumn.add_attribute(textrenderer, "markup", 0) - messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) - self._messagesView.append_column(messageColumn) - self._messagesView.set_headers_visible(False) - - if messages: - for message in messages: - row = (message, ) - self._messagemodel.append(row) - self._messagesView.show() - self._scrollWindow.show() - messagesSelection = self._messagesView.get_selection() - messagesSelection.select_path((len(messages)-1, )) - else: - self._messagesView.hide() - self._scrollWindow.hide() - - if parent is not None: - self._dialog.set_transient_for(parent) - - try: - self._dialog.show() - if messages: - self._messagesView.scroll_to_cell((len(messages)-1, )) - - userResponse = self._dialog.run() - finally: - self._dialog.hide() - - if userResponse == gtk.RESPONSE_OK: - phoneNumber = self._get_number() - phoneNumber = make_ugly(phoneNumber) - else: - phoneNumber = "" - if not phoneNumber: - self._action = self.ACTION_CANCEL - - if self._action == self.ACTION_SEND_SMS: - smsMessage = self._smsDialog.run(phoneNumber, messages, parent) - if not smsMessage: - phoneNumber = "" - self._action = self.ACTION_CANCEL - else: - smsMessage = "" - - self._messagesView.remove_column(messageColumn) - self._messagesView.set_model(None) - - self._typeviewselection.unselect_all() - self._typeview.remove_column(numberColumn) - self._typeview.remove_column(typeColumn) - self._typeview.set_model(None) - - return self._action, phoneNumber, smsMessage - - def _get_number(self): - model, itr = self._typeviewselection.get_selected() - if not itr: - return "" - - phoneNumber = self._typemodel.get_value(itr, 0) - return phoneNumber - - def _on_phonetype_dial(self, *args): - self._dialog.response(gtk.RESPONSE_OK) - self._action = self.ACTION_DIAL - - def _on_phonetype_send_sms(self, *args): - self._dialog.response(gtk.RESPONSE_OK) - self._action = self.ACTION_SEND_SMS - - def _on_phonetype_select(self, *args): - self._dialog.response(gtk.RESPONSE_OK) - self._action = self.ACTION_SELECT - - def _on_phonetype_cancel(self, *args): - self._dialog.response(gtk.RESPONSE_CANCEL) - self._action = self.ACTION_CANCEL + acceptedChars = len(line) # eh, might as well complete the line + suffix = "" + abbrevMessage = "%s%s" % (line[0:acceptedChars], suffix) + yield abbrevMessage + + lines += acceptedLines + if maxLines <= lines: + break + + +def collapse_message(message, maxCharsPerLine, maxLines): + r""" + >>> collapse_message("Hello", 60, 2) + 'Hello' + >>> collapse_message("Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789", 60, 2) + 'Hello world how are you doing today? 01234567890123456789012...' + >>> collapse_message('''Hello world how are you doing today? + ... 01234567890123456789 + ... 01234567890123456789 + ... 01234567890123456789 + ... 01234567890123456789''', 60, 2) + 'Hello world how are you doing today?\n01234567890123456789' + >>> collapse_message(''' + ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789 + ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789 + ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789 + ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789 + ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789 + ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789''', 60, 2) + '\nHello world how are you doing today? 01234567890123456789012...' + """ + messageLines = message.split("\n") + return "\n".join(_collapse_message(messageLines, maxCharsPerLine, maxLines)) class SmsEntryDialog(object): @@ -421,15 +226,21 @@ class SmsEntryDialog(object): @todo Add multi-SMS messages like GoogleVoice """ + ACTION_CANCEL = "cancel" + ACTION_DIAL = "dial" + ACTION_SEND_SMS = "sms" + MAX_CHAR = 160 def __init__(self, widgetTree): + self._clipboard = gtk.clipboard_get() self._widgetTree = widgetTree self._dialog = self._widgetTree.get_widget("smsDialog") self._smsButton = self._widgetTree.get_widget("sendSmsButton") self._smsButton.connect("clicked", self._on_send) - + self._dialButton = self._widgetTree.get_widget("dialButton") + self._dialButton.connect("clicked", self._on_dial) self._cancelButton = self._widgetTree.get_widget("cancelSmsButton") self._cancelButton.connect("clicked", self._on_cancel) @@ -437,83 +248,183 @@ class SmsEntryDialog(object): self._messagemodel = gtk.ListStore(gobject.TYPE_STRING) self._messagesView = self._widgetTree.get_widget("smsMessages") - self._scrollWindow = self._messagesView.get_parent() + self._conversationView = self._messagesView.get_parent() + self._conversationViewPort = self._conversationView.get_parent() + self._scrollWindow = self._conversationViewPort.get_parent() + + self._phoneButton = self._widgetTree.get_widget("phoneTypeSelection") self._smsEntry = self._widgetTree.get_widget("smsEntry") - self._smsEntry.get_buffer().connect("changed", self._on_entry_changed) + self._smsEntrySize = None - def run(self, number, messages = (), parent = None): - # Add the column to the messages tree view - self._messagemodel.clear() - self._messagesView.set_model(self._messagemodel) + self._action = self.ACTION_CANCEL - textrenderer = gtk.CellRendererText() - textrenderer.set_property("wrap-mode", pango.WRAP_WORD) - textrenderer.set_property("wrap-width", 450) - messageColumn = gtk.TreeViewColumn("") - messageColumn.pack_start(textrenderer, expand=True) - messageColumn.add_attribute(textrenderer, "markup", 0) - messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) - self._messagesView.append_column(messageColumn) - self._messagesView.set_headers_visible(False) - - if messages: - for message in messages: - row = (message, ) - self._messagemodel.append(row) - self._messagesView.show() - self._scrollWindow.show() - messagesSelection = self._messagesView.get_selection() - messagesSelection.select_path((len(messages)-1, )) - else: - self._messagesView.hide() - self._scrollWindow.hide() + self._numberIndex = -1 + self._contactDetails = [] - self._smsEntry.get_buffer().set_text("") - self._update_letter_count() + def run(self, contactDetails, messages = (), parent = None, defaultIndex = -1): + entryConnectId = self._smsEntry.get_buffer().connect("changed", self._on_entry_changed) + phoneConnectId = self._phoneButton.connect("clicked", self._on_phone) + keyConnectId = self._keyPressEventId = self._dialog.connect("key-press-event", self._on_key_press) + try: + # Setup the phone selection button + del self._contactDetails[:] + for phoneType, phoneNumber in contactDetails: + display = " - ".join((make_pretty(phoneNumber), phoneType)) + row = (phoneNumber, display) + self._contactDetails.append(row) + if 0 < len(self._contactDetails): + self._numberIndex = defaultIndex if defaultIndex != -1 else 0 + self._phoneButton.set_label(self._contactDetails[self._numberIndex][1]) + else: + self._numberIndex = -1 + self._phoneButton.set_label("Error: No Number Available") - if parent is not None: - self._dialog.set_transient_for(parent) + # Add the column to the messages tree view + self._messagemodel.clear() + self._messagesView.set_model(self._messagemodel) + self._messagesView.set_fixed_height_mode(False) + + textrenderer = gtk.CellRendererText() + textrenderer.set_property("wrap-mode", pango.WRAP_WORD) + textrenderer.set_property("wrap-width", 450) + messageColumn = gtk.TreeViewColumn("") + messageColumn.pack_start(textrenderer, expand=True) + messageColumn.add_attribute(textrenderer, "markup", 0) + messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) + self._messagesView.append_column(messageColumn) + self._messagesView.set_headers_visible(False) - try: - self._dialog.show() if messages: - self._messagesView.scroll_to_cell((len(messages)-1, )) - self._smsEntry.grab_focus() + for message in messages: + row = (message, ) + self._messagemodel.append(row) + self._messagesView.show() + self._scrollWindow.show() + messagesSelection = self._messagesView.get_selection() + messagesSelection.select_path((len(messages)-1, )) + else: + self._messagesView.hide() + self._scrollWindow.hide() - userResponse = self._dialog.run() - finally: - self._dialog.hide() + self._smsEntry.get_buffer().set_text("") + self._update_letter_count() - if userResponse == gtk.RESPONSE_OK: - entryBuffer = self._smsEntry.get_buffer() - enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter()) - enteredMessage = enteredMessage[0:self.MAX_CHAR] - else: - enteredMessage = "" + if parent is not None: + self._dialog.set_transient_for(parent) + parentSize = parent.get_size() + self._dialog.resize(parentSize[0], max(parentSize[1]-10, 100)) + + # Run + try: + self._dialog.show_all() + self._smsEntry.grab_focus() + adjustment = self._scrollWindow.get_vadjustment() + dx = self._conversationView.get_allocation().height - self._conversationViewPort.get_allocation().height + dx = max(dx, 0) + adjustment.value = dx + + if 1 < len(self._contactDetails): + if defaultIndex == -1: + self._request_number() + self._phoneButton.set_sensitive(True) + else: + self._phoneButton.set_sensitive(False) + + userResponse = self._dialog.run() + finally: + self._dialog.hide_all() + + # Process the users response + if userResponse == gtk.RESPONSE_OK and 0 <= self._numberIndex: + phoneNumber = self._contactDetails[self._numberIndex][0] + phoneNumbers = [make_ugly(phoneNumber)] + else: + phoneNumbers = [] + if not phoneNumbers: + self._action = self.ACTION_CANCEL + if self._action == self.ACTION_SEND_SMS: + entryBuffer = self._smsEntry.get_buffer() + enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter()) + enteredMessage = enteredMessage.strip() + if not enteredMessage: + phoneNumbers = [] + self._action = self.ACTION_CANCEL + else: + enteredMessage = "" - self._messagesView.remove_column(messageColumn) - self._messagesView.set_model(None) + self._messagesView.remove_column(messageColumn) + self._messagesView.set_model(None) - return enteredMessage.strip() + return self._action, phoneNumbers, enteredMessage + finally: + self._smsEntry.get_buffer().disconnect(entryConnectId) + self._phoneButton.disconnect(phoneConnectId) + self._keyPressEventId = self._dialog.disconnect(keyConnectId) def _update_letter_count(self, *args): + if self._smsEntrySize is None: + self._smsEntrySize = self._smsEntry.size_request() + else: + self._smsEntry.set_size_request(*self._smsEntrySize) entryLength = self._smsEntry.get_buffer().get_char_count() + charsLeft = self.MAX_CHAR - entryLength - self._letterCountLabel.set_text(str(charsLeft)) - if charsLeft < 0: + numTexts, numCharInText = divmod(entryLength, self.MAX_CHAR) + if numTexts: + self._letterCountLabel.set_text("%s.%s" % (numTexts, numCharInText)) + else: + self._letterCountLabel.set_text("%s" % (numCharInText, )) + + if entryLength == 0: + self._dialButton.set_sensitive(True) self._smsButton.set_sensitive(False) else: + self._dialButton.set_sensitive(False) self._smsButton.set_sensitive(True) + def _request_number(self): + try: + assert 0 <= self._numberIndex, "%r" % self._numberIndex + + self._numberIndex = hildonize.touch_selector( + self._dialog, + "Phone Numbers", + (description for (number, description) in self._contactDetails), + self._numberIndex, + ) + self._phoneButton.set_label(self._contactDetails[self._numberIndex][1]) + except Exception, e: + _moduleLogger.exception("%s" % str(e)) + + def _on_phone(self, *args): + self._request_number() + def _on_entry_changed(self, *args): self._update_letter_count() def _on_send(self, *args): self._dialog.response(gtk.RESPONSE_OK) + self._action = self.ACTION_SEND_SMS + + def _on_dial(self, *args): + self._dialog.response(gtk.RESPONSE_OK) + self._action = self.ACTION_DIAL def _on_cancel(self, *args): self._dialog.response(gtk.RESPONSE_CANCEL) + self._action = self.ACTION_CANCEL + + def _on_key_press(self, widget, event): + try: + if event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK: + message = "\n".join( + messagePart[0] + for messagePart in self._messagemodel + ) + self._clipboard.set_text(str(message)) + except Exception, e: + _moduleLogger.exception(str(e)) class Dialpad(object): @@ -527,6 +438,7 @@ class Dialpad(object): self._smsButton = widgetTree.get_widget("sms") self._dialButton = widgetTree.get_widget("dial") self._backButton = widgetTree.get_widget("back") + self._zeroOrPlusButton = widgetTree.get_widget("digit0") self._phonenumber = "" self._prettynumber = "" @@ -543,6 +455,9 @@ class Dialpad(object): self._backTapHandler.on_hold = self._on_clearall self._backTapHandler.on_holding = self._set_clear_button self._backTapHandler.on_cancel = self._reset_back_button + self._zeroOrPlusTapHandler = gtk_toolbox.TapOrHold(self._zeroOrPlusButton) + self._zeroOrPlusTapHandler.on_tap = self._on_zero + self._zeroOrPlusTapHandler.on_hold = self._on_plus self._window = gtk_toolbox.find_parent_window(self._numberdisplay) self._keyPressEventId = 0 @@ -550,6 +465,7 @@ class Dialpad(object): def enable(self): self._dialButton.grab_focus() self._backTapHandler.enable() + self._zeroOrPlusTapHandler.enable() self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press) def disable(self): @@ -557,8 +473,9 @@ class Dialpad(object): self._keyPressEventId = 0 self._reset_back_button() self._backTapHandler.disable() + self._zeroOrPlusTapHandler.disable() - def number_selected(self, action, number, message): + def number_selected(self, action, numbers, message): """ @note Actual dial function is patched in later """ @@ -605,26 +522,21 @@ class Dialpad(object): def _on_sms_clicked(self, widget): try: - action = PhoneTypeSelector.ACTION_SEND_SMS phoneNumber = self.get_number() + action, phoneNumbers, message = self._smsDialog.run([("Dialer", phoneNumber)], (), self._window) - message = self._smsDialog.run(phoneNumber, (), self._window) - if not message: - phoneNumber = "" - action = PhoneTypeSelector.ACTION_CANCEL - - if action == PhoneTypeSelector.ACTION_CANCEL: + if action == SmsEntryDialog.ACTION_CANCEL: return - self.number_selected(action, phoneNumber, message) + self.number_selected(action, phoneNumbers, message) except Exception, e: self._errorDisplay.push_exception() def _on_dial_clicked(self, widget): try: - action = PhoneTypeSelector.ACTION_DIAL - phoneNumber = self.get_number() + action = SmsEntryDialog.ACTION_DIAL + phoneNumbers = [self.get_number()] message = "" - self.number_selected(action, phoneNumber, message) + self.number_selected(action, phoneNumbers, message) except Exception, e: self._errorDisplay.push_exception() @@ -634,6 +546,18 @@ class Dialpad(object): except Exception, e: self._errorDisplay.push_exception() + def _on_zero(self, *args): + try: + self.set_number(self._phonenumber + "0") + except Exception, e: + self._errorDisplay.push_exception() + + def _on_plus(self, *args): + try: + self.set_number(self._phonenumber + "+") + except Exception, e: + self._errorDisplay.push_exception() + def _on_backspace(self, taps): try: self.set_number(self._phonenumber[:-taps]) @@ -691,7 +615,7 @@ class AccountInfo(object): self._applyAlarmTimeoutId = None self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton) - self._defaultCallback = "" + self._callbackNumber = "" def enable(self): assert self._backend.is_authed(), "Attempting to enable backend while not logged in" @@ -701,6 +625,7 @@ class AccountInfo(object): del self._callbackList[:] self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked) + self._set_callback_label("") if self._alarmHandler is not None: self._notifyCheckbox.set_active(self._alarmHandler.isEnabled) @@ -726,6 +651,7 @@ class AccountInfo(object): def disable(self): self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId) self._onCallbackSelectChangedId = 0 + self._set_callback_label("") if self._alarmHandler is not None: self._notifyCheckbox.disconnect(self._onNotifyToggled) @@ -748,13 +674,6 @@ class AccountInfo(object): self.clear() del self._callbackList[:] - def get_selected_callback_number(self): - currentLabel = self._callbackSelectButton.get_label() - if currentLabel is not None: - return make_ugly(currentLabel) - else: - return "" - def set_account_number(self, number): """ Displays current account number @@ -769,7 +688,7 @@ class AccountInfo(object): return True def clear(self): - self._callbackSelectButton.set_label("No Callback Number") + self._set_callback_label("") self.set_account_number("") self._isPopulated = False @@ -781,7 +700,7 @@ class AccountInfo(object): return "Account Info" def load_settings(self, config, section): - self._defaultCallback = config.get(section, "callback") + self._callbackNumber = make_ugly(config.get(section, "callback")) self._notifyOnMissed = config.getboolean(section, "notifyOnMissed") self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail") self._notifyOnSms = config.getboolean(section, "notifyOnSms") @@ -790,8 +709,7 @@ class AccountInfo(object): """ @note Thread Agnostic """ - callback = self.get_selected_callback_number() - config.set(section, "callback", callback) + config.set(section, "callback", self._callbackNumber) config.set(section, "notifyOnMissed", repr(self._notifyOnMissed)) config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail)) config.set(section, "notifyOnSms", repr(self._notifyOnSms)) @@ -812,7 +730,7 @@ class AccountInfo(object): for number, description in callbackNumbers.iteritems(): self._callbackList.append((make_pretty(number), description)) - self._set_callback_number(self._defaultCallback) + self._set_callback_number(self._callbackNumber) def _set_callback_number(self, number): try: @@ -824,15 +742,15 @@ class AccountInfo(object): self._backend.get_callback_number(), ), ) + self._set_callback_label(number) else: + if number.startswith("1747"): number = "+" + number self._backend.set_callback_number(number) assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % ( make_pretty(number), make_pretty(self._backend.get_callback_number()) ) - prettyNumber = make_pretty(number) - if len(prettyNumber) == 0: - prettyNumber = "No Callback Number" - self._callbackSelectButton.set_label(prettyNumber) + self._callbackNumber = make_ugly(number) + self._set_callback_label(number) _moduleLogger.info( "Callback number set to %s" % ( self._backend.get_callback_number(), @@ -841,6 +759,12 @@ class AccountInfo(object): except Exception, e: self._errorDisplay.push_exception() + def _set_callback_label(self, uglyNumber): + prettyNumber = make_pretty(uglyNumber) + if len(prettyNumber) == 0: + prettyNumber = "No Callback Number" + self._callbackSelectButton.set_label(prettyNumber) + def _update_alarm_settings(self, recurrence): try: isEnabled = self._notifyCheckbox.get_active() @@ -853,7 +777,7 @@ class AccountInfo(object): def _on_callbackentry_clicked(self, *args): try: - actualSelection = make_pretty(self.get_selected_callback_number()) + actualSelection = make_pretty(self._callbackNumber) userOptions = dict( (number, "%s (%s)" % (number, description)) @@ -956,26 +880,32 @@ class AccountInfo(object): self._errorDisplay.push_exception() -class RecentCallsView(object): +class CallHistoryView(object): NUMBER_IDX = 0 DATE_IDX = 1 ACTION_IDX = 2 FROM_IDX = 3 + FROM_ID_IDX = 4 + + HISTORY_ITEM_TYPES = ["All", "Received", "Missed", "Placed"] def __init__(self, widgetTree, backend, errorDisplay): self._errorDisplay = errorDisplay self._backend = backend self._isPopulated = False - self._recentmodel = gtk.ListStore( + self._historymodel = gtk.ListStore( gobject.TYPE_STRING, # number gobject.TYPE_STRING, # date gobject.TYPE_STRING, # action gobject.TYPE_STRING, # from + gobject.TYPE_STRING, # from id ) - self._recentview = widgetTree.get_widget("recentview") - self._recentviewselection = None + self._historymodelfiltered = self._historymodel.filter_new() + self._historymodelfiltered.set_visible_func(self._is_history_visible) + self._historyview = widgetTree.get_widget("historyview") + self._historyviewselection = None self._onRecentviewRowActivatedId = 0 textrenderer = gtk.CellRendererText() @@ -1006,41 +936,48 @@ class RecentCallsView(object): self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX) self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) - self._window = gtk_toolbox.find_parent_window(self._recentview) - self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend) + self._window = gtk_toolbox.find_parent_window(self._historyview) + self._smsDialog = SmsEntryDialog(widgetTree) + + self._historyFilterSelector = widgetTree.get_widget("historyFilterSelector") + self._historyFilterSelector.connect("clicked", self._on_history_filter_clicked) + self._selectedFilter = "All" self._updateSink = gtk_toolbox.threaded_stage( gtk_toolbox.comap( - self._idly_populate_recentview, + self._idly_populate_historyview, gtk_toolbox.null_sink(), ) ) def enable(self): assert self._backend.is_authed(), "Attempting to enable backend while not logged in" - self._recentview.set_model(self._recentmodel) + self._historyFilterSelector.set_label(self._selectedFilter) - self._recentview.append_column(self._dateColumn) - self._recentview.append_column(self._actionColumn) - self._recentview.append_column(self._numberColumn) - self._recentview.append_column(self._nameColumn) - self._recentviewselection = self._recentview.get_selection() - self._recentviewselection.set_mode(gtk.SELECTION_SINGLE) + self._historyview.set_model(self._historymodelfiltered) + self._historyview.set_fixed_height_mode(False) - self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated) + self._historyview.append_column(self._dateColumn) + self._historyview.append_column(self._actionColumn) + self._historyview.append_column(self._numberColumn) + self._historyview.append_column(self._nameColumn) + self._historyviewselection = self._historyview.get_selection() + self._historyviewselection.set_mode(gtk.SELECTION_SINGLE) + + self._onRecentviewRowActivatedId = self._historyview.connect("row-activated", self._on_historyview_row_activated) def disable(self): - self._recentview.disconnect(self._onRecentviewRowActivatedId) + self._historyview.disconnect(self._onRecentviewRowActivatedId) self.clear() - self._recentview.remove_column(self._dateColumn) - self._recentview.remove_column(self._actionColumn) - self._recentview.remove_column(self._nameColumn) - self._recentview.remove_column(self._numberColumn) - self._recentview.set_model(None) + self._historyview.remove_column(self._dateColumn) + self._historyview.remove_column(self._actionColumn) + self._historyview.remove_column(self._nameColumn) + self._historyview.remove_column(self._numberColumn) + self._historyview.set_model(None) - def number_selected(self, action, number, message): + def number_selected(self, action, numbers, message): """ @note Actual dial function is patched in later """ @@ -1054,49 +991,67 @@ class RecentCallsView(object): def clear(self): self._isPopulated = False - self._recentmodel.clear() + self._historymodel.clear() @staticmethod def name(): return "Recent Calls" - def load_settings(self, config, section): - pass + def load_settings(self, config, sectionName): + try: + self._selectedFilter = config.get(sectionName, "filter") + if self._selectedFilter not in self.HISTORY_ITEM_TYPES: + self._messageType = self.HISTORY_ITEM_TYPES[0] + except ConfigParser.NoOptionError: + pass - def save_settings(self, config, section): + def save_settings(self, config, sectionName): """ @note Thread Agnostic """ - pass + config.set(sectionName, "filter", self._selectedFilter) - def _idly_populate_recentview(self): + def _is_history_visible(self, model, iter): + try: + action = model.get_value(iter, self.ACTION_IDX) + if action is None: + return False # this seems weird but oh well + + if self._selectedFilter in [action, "All"]: + return True + else: + return False + except Exception, e: + self._errorDisplay.push_exception() + + def _idly_populate_historyview(self): with gtk_toolbox.gtk_lock(): - banner = hildonize.show_busy_banner_start(self._window, "Loading Recent History") + banner = hildonize.show_busy_banner_start(self._window, "Loading Call History") try: - self._recentmodel.clear() + self._historymodel.clear() self._isPopulated = True try: - recentItems = self._backend.get_recent() + historyItems = self._backend.get_recent() except Exception, e: self._errorDisplay.push_exception_with_lock() self._isPopulated = False - recentItems = [] + historyItems = [] - recentItems = ( + historyItems = ( gv_backend.decorate_recent(data) - for data in gv_backend.sort_messages(recentItems) + for data in gv_backend.sort_messages(historyItems) ) - for personName, phoneNumber, date, action in recentItems: + for contactId, personName, phoneNumber, date, action in historyItems: if not personName: personName = "Unknown" date = abbrev_relative_date(date) prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber prettyNumber = make_pretty(prettyNumber) - item = (prettyNumber, date, action.capitalize(), personName) + item = (prettyNumber, date, action.capitalize(), personName, contactId) with gtk_toolbox.gtk_lock(): - self._recentmodel.append(item) + self._historymodel.append(item) except Exception, e: self._errorDisplay.push_exception_with_lock() finally: @@ -1105,28 +1060,70 @@ class RecentCallsView(object): return False - def _on_recentview_row_activated(self, treeview, path, view_column): + def _on_history_filter_clicked(self, *args, **kwds): try: - model, itr = self._recentviewselection.get_selected() + selectedComboIndex = self.HISTORY_ITEM_TYPES.index(self._selectedFilter) + + try: + newSelectedComboIndex = hildonize.touch_selector( + self._window, + "History", + self.HISTORY_ITEM_TYPES, + selectedComboIndex, + ) + except RuntimeError: + return + + option = self.HISTORY_ITEM_TYPES[newSelectedComboIndex] + self._selectedFilter = option + self._historyFilterSelector.set_label(self._selectedFilter) + self._historymodelfiltered.refilter() + except Exception, e: + self._errorDisplay.push_exception() + + def _on_historyview_row_activated(self, treeview, path, view_column): + try: + childPath = self._historymodelfiltered.convert_path_to_child_path(path) + itr = self._historymodel.get_iter(childPath) if not itr: return - number = self._recentmodel.get_value(itr, self.NUMBER_IDX) + number = self._historymodel.get_value(itr, self.NUMBER_IDX) number = make_ugly(number) - contactPhoneNumbers = [("Phone", number)] - description = self._recentmodel.get_value(itr, self.FROM_IDX) + description = self._historymodel.get_value(itr, self.FROM_IDX) + contactId = self._historymodel.get_value(itr, self.FROM_ID_IDX) + if contactId: + contactPhoneNumbers = list(self._backend.get_contact_details(contactId)) + defaultMatches = [ + (number == make_ugly(contactNumber) or number[1:] == make_ugly(contactNumber)) + for (numberDescription, contactNumber) in contactPhoneNumbers + ] + try: + defaultIndex = defaultMatches.index(True) + except ValueError: + contactPhoneNumbers.append(("Other", number)) + defaultIndex = len(contactPhoneNumbers)-1 + _moduleLogger.warn( + "Could not find contact %r's number %s among %r" % ( + contactId, number, contactPhoneNumbers + ) + ) + else: + contactPhoneNumbers = [("Phone", number)] + defaultIndex = -1 - action, phoneNumber, message = self._phoneTypeSelector.run( + action, phoneNumbers, message = self._smsDialog.run( contactPhoneNumbers, messages = (description, ), parent = self._window, + defaultIndex = defaultIndex, ) - if action == PhoneTypeSelector.ACTION_CANCEL: + if action == SmsEntryDialog.ACTION_CANCEL: return - assert phoneNumber, "A lack of phone number exists" + assert phoneNumbers, "A lack of phone number exists" - self.number_selected(action, phoneNumber, message) - self._recentviewselection.unselect_all() + self.number_selected(action, phoneNumbers, message) + self._historyviewselection.unselect_all() except Exception, e: self._errorDisplay.push_exception() @@ -1138,6 +1135,19 @@ class MessagesView(object): HEADER_IDX = 2 MESSAGE_IDX = 3 MESSAGES_IDX = 4 + FROM_ID_IDX = 5 + MESSAGE_DATA_IDX = 6 + + NO_MESSAGES = "None" + VOICEMAIL_MESSAGES = "Voicemail" + TEXT_MESSAGES = "Texts" + ALL_TYPES = "All Messages" + MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES] + + UNREAD_STATUS = "Unread" + UNARCHIVED_STATUS = "Inbox" + ALL_STATUS = "Any" + MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS] def __init__(self, widgetTree, backend, errorDisplay): self._errorDisplay = errorDisplay @@ -1150,7 +1160,11 @@ class MessagesView(object): gobject.TYPE_STRING, # header gobject.TYPE_STRING, # message object, # messages + gobject.TYPE_STRING, # from id + object, # message data ) + self._messagemodelfiltered = self._messagemodel.filter_new() + self._messagemodelfiltered.set_visible_func(self._is_message_visible) self._messageview = widgetTree.get_widget("messages_view") self._messageviewselection = None self._onMessageviewRowActivatedId = 0 @@ -1164,7 +1178,14 @@ class MessagesView(object): self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) self._window = gtk_toolbox.find_parent_window(self._messageview) - self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend) + self._smsDialog = SmsEntryDialog(widgetTree) + + self._messageTypeButton = widgetTree.get_widget("messageTypeButton") + self._onMessageTypeClickedId = 0 + self._messageType = self.ALL_TYPES + self._messageStatusButton = widgetTree.get_widget("messageStatusButton") + self._onMessageStatusClickedId = 0 + self._messageStatus = self.ALL_STATUS self._updateSink = gtk_toolbox.threaded_stage( gtk_toolbox.comap( @@ -1175,24 +1196,38 @@ class MessagesView(object): def enable(self): assert self._backend.is_authed(), "Attempting to enable backend while not logged in" - self._messageview.set_model(self._messagemodel) + self._messageview.set_model(self._messagemodelfiltered) self._messageview.set_headers_visible(False) + self._messageview.set_fixed_height_mode(False) self._messageview.append_column(self._messageColumn) self._messageviewselection = self._messageview.get_selection() self._messageviewselection.set_mode(gtk.SELECTION_SINGLE) - self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated) + self._messageTypeButton.set_label(self._messageType) + self._messageStatusButton.set_label(self._messageStatus) + + self._onMessageviewRowActivatedId = self._messageview.connect( + "row-activated", self._on_messageview_row_activated + ) + self._onMessageTypeClickedId = self._messageTypeButton.connect( + "clicked", self._on_message_type_clicked + ) + self._onMessageStatusClickedId = self._messageStatusButton.connect( + "clicked", self._on_message_status_clicked + ) def disable(self): self._messageview.disconnect(self._onMessageviewRowActivatedId) + self._messageTypeButton.disconnect(self._onMessageTypeClickedId) + self._messageStatusButton.disconnect(self._onMessageStatusClickedId) self.clear() self._messageview.remove_column(self._messageColumn) self._messageview.set_model(None) - def number_selected(self, action, number, message): + def number_selected(self, action, numbers, message): """ @note Actual dial function is patched in later """ @@ -1212,14 +1247,54 @@ class MessagesView(object): def name(): return "Messages" - def load_settings(self, config, section): - pass + def load_settings(self, config, sectionName): + try: + self._messageType = config.get(sectionName, "type") + if self._messageType not in self.MESSAGE_TYPES: + self._messageType = self.ALL_TYPES + self._messageStatus = config.get(sectionName, "status") + if self._messageStatus not in self.MESSAGE_STATUSES: + self._messageStatus = self.ALL_STATUS + except ConfigParser.NoOptionError: + pass - def save_settings(self, config, section): + def save_settings(self, config, sectionName): """ @note Thread Agnostic """ - pass + config.set(sectionName, "status", self._messageStatus) + config.set(sectionName, "type", self._messageType) + + def _is_message_visible(self, model, iter): + try: + message = model.get_value(iter, self.MESSAGE_DATA_IDX) + if message is None: + return False # this seems weird but oh well + return self._filter_messages(message, self._messageType, self._messageStatus) + except Exception, e: + self._errorDisplay.push_exception() + + @classmethod + def _filter_messages(cls, message, type, status): + if type == cls.ALL_TYPES: + isType = True + else: + messageType = message["type"] + isType = messageType == type + + if status == cls.ALL_STATUS: + isStatus = True + else: + isUnarchived = not message["isArchived"] + isUnread = not message["isRead"] + if status == cls.UNREAD_STATUS: + isStatus = isUnarchived and isUnread + elif status == cls.UNARCHIVED_STATUS: + isStatus = isUnarchived + else: + assert "Status %s is bad for %r" % (status, message) + + return isType and isStatus _MIN_MESSAGES_SHOWN = 4 @@ -1230,19 +1305,22 @@ class MessagesView(object): self._messagemodel.clear() self._isPopulated = True - try: - messageItems = self._backend.get_messages() - except Exception, e: - self._errorDisplay.push_exception_with_lock() - self._isPopulated = False + if self._messageType == self.NO_MESSAGES: messageItems = [] + else: + try: + messageItems = self._backend.get_messages() + except Exception, e: + self._errorDisplay.push_exception_with_lock() + self._isPopulated = False + messageItems = [] messageItems = ( - gv_backend.decorate_message(message) + (gv_backend.decorate_message(message), message) for message in gv_backend.sort_messages(messageItems) ) - for header, number, relativeDate, messages in messageItems: + for (contactId, header, number, relativeDate, messages), messageData in messageItems: prettyNumber = number[2:] if number.startswith("+1") else number prettyNumber = make_pretty(prettyNumber) @@ -1253,13 +1331,14 @@ class MessagesView(object): firstMessage = "%s - %s (%s)" % (header, prettyNumber, relativeDate) secondMessage = "%d Messages Hidden..." % (len(messages) - self._MIN_MESSAGES_SHOWN, ) collapsedMessages = [firstMessage, secondMessage] - collapsedMessages.extend(messages[-self._MIN_MESSAGES_SHOWN-1:-1]) + collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):]) else: collapsedMessages = expandedMessages + #collapsedMessages = _collapse_message(collapsedMessages, 60, self._MIN_MESSAGES_SHOWN) number = make_ugly(number) - row = (number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages) + row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId, messageData with gtk_toolbox.gtk_lock(): self._messagemodel.append(row) except Exception, e: @@ -1267,35 +1346,105 @@ class MessagesView(object): finally: with gtk_toolbox.gtk_lock(): hildonize.show_busy_banner_end(banner) + self._messagemodelfiltered.refilter() return False def _on_messageview_row_activated(self, treeview, path, view_column): try: - model, itr = self._messageviewselection.get_selected() + childPath = self._messagemodelfiltered.convert_path_to_child_path(path) + itr = self._messagemodel.get_iter(childPath) if not itr: return - contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))] + number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX)) description = self._messagemodel.get_value(itr, self.MESSAGES_IDX) - action, phoneNumber, message = self._phoneTypeSelector.run( + contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX) + if contactId: + contactPhoneNumbers = list(self._backend.get_contact_details(contactId)) + defaultMatches = [ + (number == make_ugly(contactNumber) or number[1:] == make_ugly(contactNumber)) + for (numberDescription, contactNumber) in contactPhoneNumbers + ] + try: + defaultIndex = defaultMatches.index(True) + except ValueError: + contactPhoneNumbers.append(("Other", number)) + defaultIndex = len(contactPhoneNumbers)-1 + _moduleLogger.warn( + "Could not find contact %r's number %s among %r" % ( + contactId, number, contactPhoneNumbers + ) + ) + else: + contactPhoneNumbers = [("Phone", number)] + defaultIndex = -1 + + action, phoneNumbers, message = self._smsDialog.run( contactPhoneNumbers, messages = description, parent = self._window, + defaultIndex = defaultIndex, ) - if action == PhoneTypeSelector.ACTION_CANCEL: + if action == SmsEntryDialog.ACTION_CANCEL: return - assert phoneNumber, "A lock of phone number exists" + assert phoneNumbers, "A lock of phone number exists" - self.number_selected(action, phoneNumber, message) + self.number_selected(action, phoneNumbers, message) self._messageviewselection.unselect_all() except Exception, e: self._errorDisplay.push_exception() + def _on_message_type_clicked(self, *args, **kwds): + try: + selectedIndex = self.MESSAGE_TYPES.index(self._messageType) + + try: + newSelectedIndex = hildonize.touch_selector( + self._window, + "Message Type", + self.MESSAGE_TYPES, + selectedIndex, + ) + except RuntimeError: + return + + if selectedIndex != newSelectedIndex: + self._messageType = self.MESSAGE_TYPES[newSelectedIndex] + self._messageTypeButton.set_label(self._messageType) + self._messagemodelfiltered.refilter() + except Exception, e: + self._errorDisplay.push_exception() + + def _on_message_status_clicked(self, *args, **kwds): + try: + selectedIndex = self.MESSAGE_STATUSES.index(self._messageStatus) + + try: + newSelectedIndex = hildonize.touch_selector( + self._window, + "Message Status", + self.MESSAGE_STATUSES, + selectedIndex, + ) + except RuntimeError: + return + + if selectedIndex != newSelectedIndex: + self._messageStatus = self.MESSAGE_STATUSES[newSelectedIndex] + self._messageStatusButton.set_label(self._messageStatus) + self._messagemodelfiltered.refilter() + except Exception, e: + self._errorDisplay.push_exception() + class ContactsView(object): + CONTACT_TYPE_IDX = 0 + CONTACT_NAME_IDX = 1 + CONTACT_ID_IDX = 2 + def __init__(self, widgetTree, backend, errorDisplay): self._errorDisplay = errorDisplay self._backend = backend @@ -1308,7 +1457,11 @@ class ContactsView(object): self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton") self._isPopulated = False - self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING) + self._contactsmodel = gtk.ListStore( + gobject.TYPE_STRING, # Contact Type + gobject.TYPE_STRING, # Contact Name + gobject.TYPE_STRING, # Contact ID + ) self._contactsviewselection = None self._contactsview = widgetTree.get_widget("contactsview") @@ -1317,14 +1470,11 @@ class ContactsView(object): if displayContactSource: textrenderer = gtk.CellRendererText() self._contactColumn.pack_start(textrenderer, expand=False) - self._contactColumn.add_attribute(textrenderer, 'text', 0) + self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_TYPE_IDX) textrenderer = gtk.CellRendererText() hildonize.set_cell_thumb_selectable(textrenderer) self._contactColumn.pack_start(textrenderer, expand=True) - self._contactColumn.add_attribute(textrenderer, 'text', 1) - textrenderer = gtk.CellRendererText() - self._contactColumn.pack_start(textrenderer, expand=True) - self._contactColumn.add_attribute(textrenderer, 'text', 4) + self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_NAME_IDX) self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) self._contactColumn.set_sort_column_id(1) self._contactColumn.set_visible(True) @@ -1332,7 +1482,7 @@ class ContactsView(object): self._onContactsviewRowActivatedId = 0 self._onAddressbookButtonChangedId = 0 self._window = gtk_toolbox.find_parent_window(self._contactsview) - self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend) + self._smsDialog = SmsEntryDialog(widgetTree) self._updateSink = gtk_toolbox.threaded_stage( gtk_toolbox.comap( @@ -1345,6 +1495,7 @@ class ContactsView(object): assert self._backend.is_authed(), "Attempting to enable backend while not logged in" self._contactsview.set_model(self._contactsmodel) + self._contactsview.set_fixed_height_mode(False) self._contactsview.append_column(self._contactColumn) self._contactsviewselection = self._contactsview.get_selection() self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE) @@ -1383,7 +1534,7 @@ class ContactsView(object): self._contactsview.set_model(None) self._contactsview.remove_column(self._contactColumn) - def number_selected(self, action, number, message): + def number_selected(self, action, numbers, message): """ @note Actual dial function is patched in later """ @@ -1400,11 +1551,7 @@ class ContactsView(object): def open_addressbook(self, bookFactoryId, bookId): bookFactoryIndex = int(bookFactoryId) addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId) - - forceUpdate = True if addressBook is not self._addressBook else False - self._addressBook = addressBook - self.update(force=forceUpdate) def update(self, force = False): if not force and self._isPopulated: @@ -1456,8 +1603,9 @@ class ContactsView(object): self._isPopulated = False self._errorDisplay.push_exception_with_lock() for contactId, contactName in contacts: - contactType = (addressBook.contact_source_short_name(contactId), ) - self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", )) + contactType = addressBook.contact_source_short_name(contactId) + row = contactType, contactName, contactId + self._contactsmodel.append(row) with gtk_toolbox.gtk_lock(): self._contactsview.set_model(self._contactsmodel) @@ -1484,7 +1632,12 @@ class ContactsView(object): selectedFactoryId = self._booksList[newSelectedComboIndex][0] selectedBookId = self._booksList[newSelectedComboIndex][1] + + oldAddressbook = self._addressBook self.open_addressbook(selectedFactoryId, selectedBookId) + forceUpdate = True if oldAddressbook is not self._addressBook else False + self.update(force=forceUpdate) + self._selectedComboIndex = newSelectedComboIndex self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2]) except Exception, e: @@ -1492,12 +1645,12 @@ class ContactsView(object): def _on_contactsview_row_activated(self, treeview, path, view_column): try: - model, itr = self._contactsviewselection.get_selected() + itr = self._contactsmodel.get_iter(path) if not itr: return - contactId = self._contactsmodel.get_value(itr, 3) - contactName = self._contactsmodel.get_value(itr, 1) + contactId = self._contactsmodel.get_value(itr, self.CONTACT_ID_IDX) + contactName = self._contactsmodel.get_value(itr, self.CONTACT_NAME_IDX) try: contactDetails = self._addressBook.get_contact_details(contactId) except Exception, e: @@ -1508,16 +1661,16 @@ class ContactsView(object): if len(contactPhoneNumbers) == 0: return - action, phoneNumber, message = self._phoneTypeSelector.run( + action, phoneNumbers, message = self._smsDialog.run( contactPhoneNumbers, messages = (contactName, ), parent = self._window, ) - if action == PhoneTypeSelector.ACTION_CANCEL: + if action == SmsEntryDialog.ACTION_CANCEL: return - assert phoneNumber, "A lack of phone number exists" + assert phoneNumbers, "A lack of phone number exists" - self.number_selected(action, phoneNumber, message) + self.number_selected(action, phoneNumbers, message) self._contactsviewselection.unselect_all() except Exception, e: self._errorDisplay.push_exception()