X-Git-Url: http://git.maemo.org/git/?a=blobdiff_plain;f=src%2Fgc_views.py;h=6c2411ec8a669d663c1eb4a44d63fcf69e51bb0f;hb=c956ce7444b465157aa47b17683db77e15fbfca1;hp=98d6acf7050132926fe6799fb49716d6adeb98d4;hpb=29ac74586577fd627388a6f6b01d9d6a3a3e6b60;p=gc-dialer diff --git a/src/gc_views.py b/src/gc_views.py index 98d6acf..6c2411e 100644 --- a/src/gc_views.py +++ b/src/gc_views.py @@ -22,13 +22,13 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from __future__ import with_statement import threading -import time import warnings import gobject import gtk import gtk_toolbox +import null_backend def make_ugly(prettynumber): @@ -94,69 +94,6 @@ def make_pretty(phonenumber): return prettynumber -def make_idler(func): - """ - Decorator that makes a generator-function into a function that will continue execution on next call - """ - a = [] - - def decorated_func(*args, **kwds): - if not a: - a.append(func(*args, **kwds)) - try: - a[0].next() - return True - except StopIteration: - del a[:] - return False - - decorated_func.__name__ = func.__name__ - decorated_func.__doc__ = func.__doc__ - decorated_func.__dict__.update(func.__dict__) - - return decorated_func - - -class DummyAddressBook(object): - """ - Minimal example of both an addressbook factory and an addressbook - """ - - def clear_caches(self): - pass - - def get_addressbooks(self): - """ - @returns Iterable of (Address Book Factory, Book Id, Book Name) - """ - yield self, "", "None" - - def open_addressbook(self, bookId): - return self - - @staticmethod - def contact_source_short_name(contactId): - return "" - - @staticmethod - def factory_name(): - return "" - - @staticmethod - def get_contacts(): - """ - @returns Iterable of (contact id, contact name) - """ - return [] - - @staticmethod - def get_contact_details(contactId): - """ - @returns Iterable of (Phone Type, Phone Number) - """ - return [] - - class MergedAddressBook(object): """ Merger of all addressbooks @@ -341,7 +278,7 @@ class PhoneTypeSelector(object): self._action = self.ACTION_CANCEL - def run(self, contactDetails, message = ""): + def run(self, contactDetails, message = "", parent = None): self._action = self.ACTION_CANCEL self._typemodel.clear() self._typeview.set_model(self._typemodel) @@ -364,7 +301,7 @@ class PhoneTypeSelector(object): row = (phoneNumber, display) self._typemodel.append(row) - # @todo Need to decide how how to handle the single phone number case + self._typeviewselection.select_iter(self._typemodel.get_iter_first()) if message: self._message.set_markup(message) self._message.show() @@ -372,28 +309,35 @@ class PhoneTypeSelector(object): self._message.set_markup("") self._message.hide() - userResponse = self._dialog.run() + if parent is not None: + self._dialog.set_transient_for(parent) + + try: + 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, message) + smsMessage = self._smsDialog.run(phoneNumber, message, parent) + if not smsMessage: + phoneNumber = "" + self._action = self.ACTION_CANCEL else: smsMessage = "" - if not smsMessage: - phoneNumber = "" - self._action = self.ACTION_CANCEL self._typeviewselection.unselect_all() self._typeview.remove_column(numberColumn) self._typeview.remove_column(typeColumn) self._typeview.set_model(None) - self._dialog.hide() + return self._action, phoneNumber, smsMessage def _get_number(self): @@ -441,7 +385,7 @@ class SmsEntryDialog(object): self._smsEntry = self._widgetTree.get_widget("smsEntry") self._smsEntry.get_buffer().connect("changed", self._on_entry_changed) - def run(self, number, message = ""): + def run(self, number, message = "", parent = None): if message: self._message.set_markup(message) self._message.show() @@ -451,7 +395,14 @@ class SmsEntryDialog(object): self._smsEntry.get_buffer().set_text("") self._update_letter_count() - userResponse = self._dialog.run() + if parent is not None: + self._dialog.set_transient_for(parent) + + try: + userResponse = self._dialog.run() + finally: + self._dialog.hide() + if userResponse == gtk.RESPONSE_OK: entryBuffer = self._smsEntry.get_buffer() enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter()) @@ -459,12 +410,16 @@ class SmsEntryDialog(object): else: enteredMessage = "" - self._dialog.hide() return enteredMessage def _update_letter_count(self, *args): entryLength = self._smsEntry.get_buffer().get_char_count() - self._letterCountLabel.set_text(str(self.MAX_CHAR - entryLength)) + charsLeft = self.MAX_CHAR - entryLength + self._letterCountLabel.set_text(str(charsLeft)) + if charsLeft < 0: + self._smsButton.set_sensitive(False) + else: + self._smsButton.set_sensitive(True) def _on_entry_changed(self, *args): self._update_letter_count() @@ -502,7 +457,7 @@ class Dialpad(object): def disable(self): pass - def dial(self, number): + def number_selected(self, action, number, message): """ @note Actual dial function is patched in later """ @@ -525,8 +480,24 @@ class Dialpad(object): def clear(self): self.set_number("") + @staticmethod + def name(): + return "Dialpad" + + def load_settings(self, config, section): + pass + + def save_settings(self, config, section): + """ + @note Thread Agnostic + """ + pass + def _on_dial_clicked(self, widget): - self.dial(self.get_number()) + action = PhoneTypeSelector.ACTION_DIAL + phoneNumber = self.get_number() + message = "" + self.number_selected(action, phoneNumber, message) def _on_clear_number(self, *args): self.clear() @@ -555,23 +526,28 @@ class AccountInfo(object): def __init__(self, widgetTree, backend, errorDisplay): self._errorDisplay = errorDisplay self._backend = backend + self._isPopulated = False self._callbackList = gtk.ListStore(gobject.TYPE_STRING) self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display") self._callbackCombo = widgetTree.get_widget("callbackcombo") self._onCallbackentryChangedId = 0 + self._defaultCallback = "" + def enable(self): assert self._backend.is_authed() self._accountViewNumberDisplay.set_use_markup(True) self.set_account_number("") self._callbackList.clear() - self.update() self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed) + self.update(force=True) def disable(self): self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId) + self.clear() + self._callbackList.clear() def get_selected_callback_number(self): @@ -583,20 +559,39 @@ class AccountInfo(object): """ self._accountViewNumberDisplay.set_label("%s" % (number)) - def update(self): - self.populate_callback_combo() + def update(self, force = False): + if not force and self._isPopulated: + return + self._populate_callback_combo() self.set_account_number(self._backend.get_account_number()) def clear(self): self._callbackCombo.get_child().set_text("") self.set_account_number("") + self._isPopulated = False + + @staticmethod + def name(): + return "Account Info" + + def load_settings(self, config, section): + self._defaultCallback = config.get(section, "callback") + + def save_settings(self, config, section): + """ + @note Thread Agnostic + """ + callback = self.get_selected_callback_number() + config.set(section, "callback", callback) - def populate_callback_combo(self): + def _populate_callback_combo(self): + self._isPopulated = True self._callbackList.clear() try: callbackNumbers = self._backend.get_callback_numbers() except RuntimeError, e: self._errorDisplay.push_exception(e) + self._isPopulated = False return for number, description in callbackNumbers.iteritems(): @@ -604,54 +599,78 @@ class AccountInfo(object): self._callbackCombo.set_model(self._callbackList) self._callbackCombo.set_text_column(0) - try: - callbackNumber = self._backend.get_callback_number() - except RuntimeError, e: - self._errorDisplay.push_exception(e) - return + #callbackNumber = self._backend.get_callback_number() + callbackNumber = self._defaultCallback self._callbackCombo.get_child().set_text(make_pretty(callbackNumber)) - def _on_callbackentry_changed(self, *args): - """ - @todo Potential blocking on web access, maybe we should defer this or put up a dialog? - """ + def _set_callback_number(self, number): try: - text = self.get_selected_callback_number() - if not self._backend.is_valid_syntax(text): - self._errorDisplay.push_message("%s is not a valid callback number" % text) - elif text == self._backend.get_callback_number(): + if not self._backend.is_valid_syntax(number): + self._errorDisplay.push_message("%s is not a valid callback number" % number) + elif number == self._backend.get_callback_number(): warnings.warn("Callback number already is %s" % self._backend.get_callback_number(), UserWarning, 2) else: - self._backend.set_callback_number(text) + self._backend.set_callback_number(number) + warnings.warn("Callback number set to %s" % self._backend.get_callback_number(), UserWarning, 2) except RuntimeError, e: self._errorDisplay.push_exception(e) + def _on_callbackentry_changed(self, *args): + text = self.get_selected_callback_number() + self._set_callback_number(text) + class RecentCallsView(object): + NUMBER_IDX = 0 + DATE_IDX = 1 + ACTION_IDX = 2 + FROM_IDX = 3 + def __init__(self, widgetTree, backend, errorDisplay): self._errorDisplay = errorDisplay self._backend = backend - self._recenttime = 0.0 - self._recentmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING) + self._isPopulated = False + self._recentmodel = gtk.ListStore( + gobject.TYPE_STRING, # number + gobject.TYPE_STRING, # date + gobject.TYPE_STRING, # action + gobject.TYPE_STRING, # from + ) self._recentview = widgetTree.get_widget("recentview") self._recentviewselection = None self._onRecentviewRowActivatedId = 0 - # @todo Make seperate columns for each item in recent item payload textrenderer = gtk.CellRendererText() - self._recentviewColumn = gtk.TreeViewColumn("Calls") - self._recentviewColumn.pack_start(textrenderer, expand=True) - self._recentviewColumn.add_attribute(textrenderer, "text", 1) + textrenderer.set_property("yalign", 0) + self._dateColumn = gtk.TreeViewColumn("Date") + self._dateColumn.pack_start(textrenderer, expand=True) + self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX) + textrenderer = gtk.CellRendererText() + textrenderer.set_property("yalign", 0) + self._actionColumn = gtk.TreeViewColumn("Action") + self._actionColumn.pack_start(textrenderer, expand=True) + self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX) + + textrenderer = gtk.CellRendererText() + textrenderer.set_property("yalign", 0) + self._fromColumn = gtk.TreeViewColumn("From") + self._fromColumn.pack_start(textrenderer, expand=True) + self._fromColumn.add_attribute(textrenderer, "text", self.FROM_IDX) + self._fromColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) + + self._window = gtk_toolbox.find_parent_window(self._recentview) self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend) def enable(self): assert self._backend.is_authed() self._recentview.set_model(self._recentmodel) - self._recentview.append_column(self._recentviewColumn) + self._recentview.append_column(self._dateColumn) + self._recentview.append_column(self._actionColumn) + self._recentview.append_column(self._fromColumn) self._recentviewselection = self._recentview.get_selection() self._recentviewselection.set_mode(gtk.SELECTION_SINGLE) @@ -659,7 +678,12 @@ class RecentCallsView(object): def disable(self): self._recentview.disconnect(self._onRecentviewRowActivatedId) - self._recentview.remove_column(self._recentviewColumn) + + self.clear() + + self._recentview.remove_column(self._dateColumn) + self._recentview.remove_column(self._actionColumn) + self._recentview.remove_column(self._fromColumn) self._recentview.set_model(None) def number_selected(self, action, number, message): @@ -668,31 +692,46 @@ class RecentCallsView(object): """ raise NotImplementedError - def update(self): - if (time.time() - self._recenttime) < 300: + def update(self, force = False): + if not force and self._isPopulated: return backgroundPopulate = threading.Thread(target=self._idly_populate_recentview) backgroundPopulate.setDaemon(True) backgroundPopulate.start() def clear(self): - self._recenttime = 0.0 + self._isPopulated = False self._recentmodel.clear() + @staticmethod + def name(): + return "Recent Calls" + + def load_settings(self, config, section): + pass + + def save_settings(self, config, section): + """ + @note Thread Agnostic + """ + pass + def _idly_populate_recentview(self): - self._recenttime = time.time() + self._isPopulated = True self._recentmodel.clear() try: recentItems = self._backend.get_recent() except RuntimeError, e: self._errorDisplay.push_exception_with_lock(e) - self._recenttime = 0.0 + self._isPopulated = False recentItems = [] - for personsName, phoneNumber, date, action in recentItems: - description = "%s on %s from/to %s - %s" % (action.capitalize(), date, personsName, phoneNumber) - item = (phoneNumber, description) + for personName, phoneNumber, date, action in recentItems: + if not personName: + personName = "Unknown" + description = "%s (%s)" % (phoneNumber, personName) + item = (phoneNumber, date, action.capitalize(), description) with gtk_toolbox.gtk_lock(): self._recentmodel.append(item) @@ -703,12 +742,16 @@ class RecentCallsView(object): if not itr: return - number = self._recentmodel.get_value(itr, 0) + number = self._recentmodel.get_value(itr, self.NUMBER_IDX) number = make_ugly(number) contactPhoneNumbers = [("Phone", number)] - description = self._recentmodel.get_value(itr, 1) + description = self._recentmodel.get_value(itr, self.FROM_IDX) - action, phoneNumber, message = self._phoneTypeSelector.run(contactPhoneNumbers, message = description) + action, phoneNumber, message = self._phoneTypeSelector.run( + contactPhoneNumbers, + message = description, + parent = self._window, + ) if action == PhoneTypeSelector.ACTION_CANCEL: return assert phoneNumber @@ -719,30 +762,55 @@ class RecentCallsView(object): class MessagesView(object): + NUMBER_IDX = 0 + DATE_IDX = 1 + HEADER_IDX = 2 + MESSAGE_IDX = 3 + def __init__(self, widgetTree, backend, errorDisplay): self._errorDisplay = errorDisplay self._backend = backend - self._messagetime = 0.0 - self._messagemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING) + self._isPopulated = False + self._messagemodel = gtk.ListStore( + gobject.TYPE_STRING, # number + gobject.TYPE_STRING, # date + gobject.TYPE_STRING, # header + gobject.TYPE_STRING, # message + ) self._messageview = widgetTree.get_widget("messages_view") self._messageviewselection = None self._onMessageviewRowActivatedId = 0 textrenderer = gtk.CellRendererText() - # @todo Make seperate columns for each item in message payload - self._messageviewColumn = gtk.TreeViewColumn("Messages") - self._messageviewColumn.pack_start(textrenderer, expand=True) - self._messageviewColumn.add_attribute(textrenderer, "markup", 1) - self._messageviewColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) + textrenderer.set_property("yalign", 0) + self._dateColumn = gtk.TreeViewColumn("Date") + self._dateColumn.pack_start(textrenderer, expand=True) + self._dateColumn.add_attribute(textrenderer, "markup", self.DATE_IDX) + + textrenderer = gtk.CellRendererText() + textrenderer.set_property("yalign", 0) + self._headerColumn = gtk.TreeViewColumn("From") + self._headerColumn.pack_start(textrenderer, expand=True) + self._headerColumn.add_attribute(textrenderer, "markup", self.HEADER_IDX) + textrenderer = gtk.CellRendererText() + textrenderer.set_property("yalign", 0) + self._messageColumn = gtk.TreeViewColumn("Messages") + self._messageColumn.pack_start(textrenderer, expand=True) + self._messageColumn.add_attribute(textrenderer, "markup", self.MESSAGE_IDX) + self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) + + self._window = gtk_toolbox.find_parent_window(self._messageview) self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend) def enable(self): assert self._backend.is_authed() self._messageview.set_model(self._messagemodel) - self._messageview.append_column(self._messageviewColumn) + self._messageview.append_column(self._dateColumn) + self._messageview.append_column(self._headerColumn) + self._messageview.append_column(self._messageColumn) self._messageviewselection = self._messageview.get_selection() self._messageviewselection.set_mode(gtk.SELECTION_SINGLE) @@ -750,7 +818,12 @@ class MessagesView(object): def disable(self): self._messageview.disconnect(self._onMessageviewRowActivatedId) - self._messageview.remove_column(self._messageviewColumn) + + self.clear() + + self._messageview.remove_column(self._dateColumn) + self._messageview.remove_column(self._headerColumn) + self._messageview.remove_column(self._messageColumn) self._messageview.set_model(None) def number_selected(self, action, number, message): @@ -759,33 +832,46 @@ class MessagesView(object): """ raise NotImplementedError - def update(self): - if (time.time() - self._messagetime) < 300: + def update(self, force = False): + if not force and self._isPopulated: return backgroundPopulate = threading.Thread(target=self._idly_populate_messageview) backgroundPopulate.setDaemon(True) backgroundPopulate.start() def clear(self): - self._messagetime = 0.0 + self._isPopulated = False self._messagemodel.clear() + @staticmethod + def name(): + return "Messages" + + def load_settings(self, config, section): + pass + + def save_settings(self, config, section): + """ + @note Thread Agnostic + """ + pass + def _idly_populate_messageview(self): - self._messagetime = time.time() + self._isPopulated = True self._messagemodel.clear() try: messageItems = self._backend.get_messages() except RuntimeError, e: self._errorDisplay.push_exception_with_lock(e) - self._messagetime = 0.0 + self._isPopulated = False messageItems = [] for header, number, relativeDate, message in messageItems: number = make_ugly(number) - item = (number, message) + row = (number, relativeDate, header, message) with gtk_toolbox.gtk_lock(): - self._messagemodel.append(item) + self._messagemodel.append(row) return False @@ -794,10 +880,14 @@ class MessagesView(object): if not itr: return - contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, 0))] - description = self._messagemodel.get_value(itr, 1) + contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))] + description = self._messagemodel.get_value(itr, self.MESSAGE_IDX) - action, phoneNumber, message = self._phoneTypeSelector.run(contactPhoneNumbers, message = description) + action, phoneNumber, message = self._phoneTypeSelector.run( + contactPhoneNumbers, + message = description, + parent = self._window, + ) if action == PhoneTypeSelector.ACTION_CANCEL: return assert phoneNumber @@ -813,12 +903,12 @@ class ContactsView(object): self._backend = backend self._addressBook = None - self._addressBookFactories = [DummyAddressBook()] + self._addressBookFactories = [null_backend.NullAddressBook()] self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING) self._booksSelectionBox = widgetTree.get_widget("addressbook_combo") - self._contactstime = 0.0 + self._isPopulated = False self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING) self._contactsviewselection = None self._contactsview = widgetTree.get_widget("contactsview") @@ -841,6 +931,7 @@ class ContactsView(object): self._onContactsviewRowActivatedId = 0 self._onAddressbookComboChangedId = 0 + self._window = gtk_toolbox.find_parent_window(self._contactsview) self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend) def enable(self): @@ -877,6 +968,8 @@ class ContactsView(object): self._contactsview.disconnect(self._onContactsviewRowActivatedId) self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId) + self.clear() + self._booksSelectionBox.clear() self._booksSelectionBox.set_model(None) self._contactsview.set_model(None) @@ -898,20 +991,17 @@ class ContactsView(object): def open_addressbook(self, bookFactoryId, bookId): self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId) - self._contactstime = 0 - backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview) - backgroundPopulate.setDaemon(True) - backgroundPopulate.start() + self.update(force=True) - def update(self): - if (time.time() - self._contactstime) < 300: + def update(self, force = False): + if not force and self._isPopulated: return backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview) backgroundPopulate.setDaemon(True) backgroundPopulate.start() def clear(self): - self._contactstime = 0.0 + self._isPopulated = False self._contactsmodel.clear() def clear_caches(self): @@ -925,8 +1015,21 @@ class ContactsView(object): def extend(self, books): self._addressBookFactories.extend(books) + @staticmethod + def name(): + return "Contacts" + + def load_settings(self, config, section): + pass + + def save_settings(self, config, section): + """ + @note Thread Agnostic + """ + pass + def _idly_populate_contactsview(self): - #@todo Add a lock so only one code path can be in here at a time + self._isPopulated = True self.clear() # completely disable updating the treeview while we populate the data @@ -938,7 +1041,7 @@ class ContactsView(object): contacts = addressBook.get_contacts() except RuntimeError, e: contacts = [] - self._contactstime = 0.0 + self._isPopulated = False self._errorDisplay.push_exception_with_lock(e) for contactId, contactName in contacts: contactType = (addressBook.contact_source_short_name(contactId), ) @@ -968,14 +1071,17 @@ class ContactsView(object): contactDetails = self._addressBook.get_contact_details(contactId) except RuntimeError, e: contactDetails = [] - self._contactstime = 0.0 self._errorDisplay.push_exception(e) contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails] if len(contactPhoneNumbers) == 0: return - action, phoneNumber, message = self._phoneTypeSelector.run(contactPhoneNumbers, message = contactName) + action, phoneNumber, message = self._phoneTypeSelector.run( + contactPhoneNumbers, + message = contactName, + parent = self._window, + ) if action == PhoneTypeSelector.ACTION_CANCEL: return assert phoneNumber