Various bug fixes
[gc-dialer] / src / gc_views.py
index 630e213..5f48097 100644 (file)
@@ -17,6 +17,8 @@ Lesser General Public License for more details.
 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 Feature request: The ability to go to relevant thing in web browser
 """
 
 from __future__ import with_statement
@@ -255,7 +257,7 @@ class PhoneTypeSelector(object):
                self._widgetTree = widgetTree
 
                self._dialog = self._widgetTree.get_widget("phonetype_dialog")
-               self._smsDialog = SmsEntryDialog(self._widgetTree, self._gcBackend)
+               self._smsDialog = SmsEntryDialog(self._widgetTree)
 
                self._smsButton = self._widgetTree.get_widget("sms_button")
                self._smsButton.connect("clicked", self._on_phonetype_send_sms)
@@ -367,10 +369,13 @@ class PhoneTypeSelector(object):
 
 class SmsEntryDialog(object):
 
+       """
+       @todo Add multi-SMS messages like GoogleVoice
+       """
+
        MAX_CHAR = 160
 
-       def __init__(self, widgetTree, gcBackend):
-               self._gcBackend = gcBackend
+       def __init__(self, widgetTree):
                self._widgetTree = widgetTree
                self._dialog = self._widgetTree.get_widget("smsDialog")
 
@@ -410,7 +415,7 @@ class SmsEntryDialog(object):
                else:
                        enteredMessage = ""
 
-               return enteredMessage
+               return enteredMessage.strip()
 
        def _update_letter_count(self, *args):
                entryLength = self._smsEntry.get_buffer().get_char_count()
@@ -435,33 +440,44 @@ class Dialpad(object):
 
        def __init__(self, widgetTree, errorDisplay):
                self._errorDisplay = errorDisplay
+               self._smsDialog = SmsEntryDialog(widgetTree)
+
                self._numberdisplay = widgetTree.get_widget("numberdisplay")
                self._dialButton = widgetTree.get_widget("dial")
+               self._backButton = widgetTree.get_widget("back")
                self._phonenumber = ""
                self._prettynumber = ""
-               self._clearall_id = None
 
                callbackMapping = {
                        "on_dial_clicked": self._on_dial_clicked,
+                       "on_sms_clicked": self._on_sms_clicked,
                        "on_digit_clicked": self._on_digit_clicked,
                        "on_clear_number": self._on_clear_number,
-                       "on_back_clicked": self._on_backspace,
-                       "on_back_pressed": self._on_back_pressed,
-                       "on_back_released": self._on_back_released,
                }
                widgetTree.signal_autoconnect(callbackMapping)
 
+               self._originalLabel = self._backButton.get_label()
+               self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
+               self._backTapHandler.on_tap = self._on_backspace
+               self._backTapHandler.on_hold = self._on_clearall
+               self._backTapHandler.on_holding = self._set_clear_button
+               self._backTapHandler.on_cancel = self._reset_back_button
+
+               self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
+
        def enable(self):
                self._dialButton.grab_focus()
+               self._backTapHandler.enable()
 
        def disable(self):
-               pass
+               self._reset_back_button()
+               self._backTapHandler.disable()
 
        def number_selected(self, action, number, message):
                """
                @note Actual dial function is patched in later
                """
-               raise NotImplementedError
+               raise NotImplementedError("Horrible unknown error has occurred")
 
        def get_number(self):
                return self._phonenumber
@@ -475,7 +491,7 @@ class Dialpad(object):
                        self._prettynumber = make_pretty(self._phonenumber)
                        self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
                except TypeError, e:
-                       self._errorDisplay.push_exception(e)
+                       self._errorDisplay.push_exception()
 
        def clear(self):
                self.set_number("")
@@ -493,6 +509,19 @@ class Dialpad(object):
                """
                pass
 
+       def _on_sms_clicked(self, widget):
+               action = PhoneTypeSelector.ACTION_SEND_SMS
+               phoneNumber = self.get_number()
+
+               message = self._smsDialog.run(phoneNumber, "", self._window)
+               if not message:
+                       phoneNumber = ""
+                       action = PhoneTypeSelector.ACTION_CANCEL
+
+               if action == PhoneTypeSelector.ACTION_CANCEL:
+                       return
+               self.number_selected(action, phoneNumber, message)
+
        def _on_dial_clicked(self, widget):
                action = PhoneTypeSelector.ACTION_DIAL
                phoneNumber = self.get_number()
@@ -505,49 +534,107 @@ class Dialpad(object):
        def _on_digit_clicked(self, widget):
                self.set_number(self._phonenumber + widget.get_name()[-1])
 
-       def _on_backspace(self, widget):
-               self.set_number(self._phonenumber[:-1])
+       def _on_backspace(self, taps):
+               self.set_number(self._phonenumber[:-taps])
+               self._reset_back_button()
 
-       def _on_clearall(self):
+       def _on_clearall(self, taps):
                self.clear()
+               self._reset_back_button()
                return False
 
-       def _on_back_pressed(self, widget):
-               self._clearall_id = gobject.timeout_add(1000, self._on_clearall)
+       def _set_clear_button(self):
+               self._backButton.set_label("gtk-clear")
 
-       def _on_back_released(self, widget):
-               if self._clearall_id is not None:
-                       gobject.source_remove(self._clearall_id)
-               self._clearall_id = None
+       def _reset_back_button(self):
+               self._backButton.set_label(self._originalLabel)
 
 
 class AccountInfo(object):
 
-       def __init__(self, widgetTree, backend, errorDisplay):
+       def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
                self._errorDisplay = errorDisplay
                self._backend = backend
                self._isPopulated = False
+               self._alarmHandler = alarmHandler
+               self._notifyOnMissed = False
+               self._notifyOnVoicemail = False
+               self._notifyOnSms = 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._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
+               self._minutesEntry = widgetTree.get_widget("minutesEntry")
+               self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
+               self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
+               self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
+               self._onNotifyToggled = 0
+               self._onMinutesChanged = 0
+               self._onMissedToggled = 0
+               self._onVoicemailToggled = 0
+               self._onSmsToggled = 0
+
                self._defaultCallback = ""
 
        def enable(self):
-               assert self._backend.is_authed()
+               assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
+
                self._accountViewNumberDisplay.set_use_markup(True)
                self.set_account_number("")
+
                self._callbackList.clear()
                self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
+
+               if self._alarmHandler is not None:
+                       self._minutesEntry.set_range(0, 60)
+                       self._minutesEntry.set_increments(1, 5)
+
+                       self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
+                       self._minutesEntry.set_value(self._alarmHandler.recurrence)
+                       self._missedCheckbox.set_active(self._notifyOnMissed)
+                       self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
+                       self._smsCheckbox.set_active(self._notifyOnSms)
+
+                       self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
+                       self._onMinutesChanged = self._minutesEntry.connect("value-changed", self._on_minutes_changed)
+                       self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
+                       self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
+                       self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
+               else:
+                       self._notifyCheckbox.set_sensitive(False)
+                       self._minutesEntry.set_sensitive(False)
+                       self._missedCheckbox.set_sensitive(False)
+                       self._voicemailCheckbox.set_sensitive(False)
+                       self._smsCheckbox.set_sensitive(False)
+
                self.update(force=True)
 
        def disable(self):
                self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
+               self._onCallbackentryChangedId = 0
 
-               self.clear()
+               if self._alarmHandler is not None:
+                       self._notifyCheckbox.disconnect(self._onNotifyToggled)
+                       self._minutesEntry.disconnect(self._onMinutesChanged)
+                       self._missedCheckbox.disconnect(self._onNotifyToggled)
+                       self._voicemailCheckbox.disconnect(self._onNotifyToggled)
+                       self._smsCheckbox.disconnect(self._onNotifyToggled)
+                       self._onNotifyToggled = 0
+                       self._onMinutesChanged = 0
+                       self._onMissedToggled = 0
+                       self._onVoicemailToggled = 0
+                       self._onSmsToggled = 0
+               else:
+                       self._notifyCheckbox.set_sensitive(True)
+                       self._minutesEntry.set_sensitive(True)
+                       self._missedCheckbox.set_sensitive(True)
+                       self._voicemailCheckbox.set_sensitive(True)
+                       self._smsCheckbox.set_sensitive(True)
 
+               self.clear()
                self._callbackList.clear()
 
        def get_selected_callback_number(self):
@@ -561,21 +648,28 @@ class AccountInfo(object):
 
        def update(self, force = False):
                if not force and self._isPopulated:
-                       return
+                       return False
                self._populate_callback_combo()
                self.set_account_number(self._backend.get_account_number())
+               return True
 
        def clear(self):
                self._callbackCombo.get_child().set_text("")
                self.set_account_number("")
                self._isPopulated = False
 
+       def save_everything(self):
+               raise NotImplementedError
+
        @staticmethod
        def name():
                return "Account Info"
 
        def load_settings(self, config, section):
                self._defaultCallback = config.get(section, "callback")
+               self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
+               self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
+               self._notifyOnSms = config.getboolean(section, "notifyOnSms")
 
        def save_settings(self, config, section):
                """
@@ -583,14 +677,17 @@ class AccountInfo(object):
                """
                callback = self.get_selected_callback_number()
                config.set(section, "callback", callback)
+               config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
+               config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
+               config.set(section, "notifyOnSms", repr(self._notifyOnSms))
 
        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)
+               except StandardError, e:
+                       self._errorDisplay.push_exception()
                        self._isPopulated = False
                        return
 
@@ -604,26 +701,66 @@ class AccountInfo(object):
                self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
 
        def _set_callback_number(self, number):
-               """
-               @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
-               """
                try:
                        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)
+                               warnings.warn(
+                                       "Callback number already is %s" % (
+                                               self._backend.get_callback_number(),
+                                       ),
+                                       UserWarning,
+                                       2
+                               )
                        else:
                                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)
+                               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())
+                               )
+                               warnings.warn(
+                                       "Callback number set to %s" % (
+                                               self._backend.get_callback_number(),
+                                       ),
+                                       UserWarning, 2
+                               )
+               except StandardError, e:
+                       self._errorDisplay.push_exception()
+
+       def _update_alarm_settings(self):
+               try:
+                       isEnabled = self._notifyCheckbox.get_active()
+                       recurrence = self._minutesEntry.get_value_as_int()
+                       if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
+                               self._alarmHandler.apply_settings(isEnabled, recurrence)
+               finally:
+                       self.save_everything()
+                       self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
+                       self._minutesEntry.set_value(self._alarmHandler.recurrence)
 
        def _on_callbackentry_changed(self, *args):
-               """
-               @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
-               """
                text = self.get_selected_callback_number()
-               self._set_callback_number(text)
+               number = make_ugly(text)
+               self._set_callback_number(number)
+
+               self.save_everything()
+
+       def _on_notify_toggled(self, *args):
+               self._update_alarm_settings()
+
+       def _on_minutes_changed(self, *args):
+               self._update_alarm_settings()
+
+       def _on_missed_toggled(self, *args):
+               self._notifyOnMissed = self._missedCheckbox.get_active()
+               self.save_everything()
+
+       def _on_voicemail_toggled(self, *args):
+               self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
+               self.save_everything()
+
+       def _on_sms_toggled(self, *args):
+               self._notifyOnSms = self._smsCheckbox.get_active()
+               self.save_everything()
 
 
 class RecentCallsView(object):
@@ -670,8 +807,15 @@ class RecentCallsView(object):
                self._window = gtk_toolbox.find_parent_window(self._recentview)
                self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
 
+               self._updateSink = gtk_toolbox.threaded_stage(
+                       gtk_toolbox.comap(
+                               self._idly_populate_recentview,
+                               gtk_toolbox.null_sink(),
+                       )
+               )
+
        def enable(self):
-               assert self._backend.is_authed()
+               assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
                self._recentview.set_model(self._recentmodel)
 
                self._recentview.append_column(self._dateColumn)
@@ -696,14 +840,13 @@ class RecentCallsView(object):
                """
                @note Actual dial function is patched in later
                """
-               raise NotImplementedError
+               raise NotImplementedError("Horrible unknown error has occurred")
 
        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()
+                       return False
+               self._updateSink.send(())
+               return True
 
        def clear(self):
                self._isPopulated = False
@@ -723,20 +866,22 @@ class RecentCallsView(object):
                pass
 
        def _idly_populate_recentview(self):
-               self._isPopulated = True
                self._recentmodel.clear()
+               self._isPopulated = True
 
                try:
                        recentItems = self._backend.get_recent()
-               except RuntimeError, e:
-                       self._errorDisplay.push_exception_with_lock(e)
+               except StandardError, e:
+                       self._errorDisplay.push_exception_with_lock()
                        self._isPopulated = False
                        recentItems = []
 
                for personName, phoneNumber, date, action in recentItems:
                        if not personName:
                                personName = "Unknown"
-                       description = "%s (%s)" % (phoneNumber, personName)
+                       prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
+                       prettyNumber = make_pretty(prettyNumber)
+                       description = "%s - %s" % (personName, prettyNumber)
                        item = (phoneNumber, date, action.capitalize(), description)
                        with gtk_toolbox.gtk_lock():
                                self._recentmodel.append(item)
@@ -760,7 +905,7 @@ class RecentCallsView(object):
                )
                if action == PhoneTypeSelector.ACTION_CANCEL:
                        return
-               assert phoneNumber
+               assert phoneNumber, "A lack of phone number exists"
 
                self.number_selected(action, phoneNumber, message)
                self._recentviewselection.unselect_all()
@@ -810,8 +955,15 @@ class MessagesView(object):
                self._window = gtk_toolbox.find_parent_window(self._messageview)
                self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
 
+               self._updateSink = gtk_toolbox.threaded_stage(
+                       gtk_toolbox.comap(
+                               self._idly_populate_messageview,
+                               gtk_toolbox.null_sink(),
+                       )
+               )
+
        def enable(self):
-               assert self._backend.is_authed()
+               assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
                self._messageview.set_model(self._messagemodel)
 
                self._messageview.append_column(self._dateColumn)
@@ -836,14 +988,13 @@ class MessagesView(object):
                """
                @note Actual dial function is patched in later
                """
-               raise NotImplementedError
+               raise NotImplementedError("Horrible unknown error has occurred")
 
        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()
+                       return False
+               self._updateSink.send(())
+               return True
 
        def clear(self):
                self._isPopulated = False
@@ -863,13 +1014,13 @@ class MessagesView(object):
                pass
 
        def _idly_populate_messageview(self):
-               self._isPopulated = True
                self._messagemodel.clear()
+               self._isPopulated = True
 
                try:
                        messageItems = self._backend.get_messages()
-               except RuntimeError, e:
-                       self._errorDisplay.push_exception_with_lock(e)
+               except StandardError, e:
+                       self._errorDisplay.push_exception_with_lock()
                        self._isPopulated = False
                        messageItems = []
 
@@ -896,7 +1047,7 @@ class MessagesView(object):
                )
                if action == PhoneTypeSelector.ACTION_CANCEL:
                        return
-               assert phoneNumber
+               assert phoneNumber, "A lock of phone number exists"
 
                self.number_selected(action, phoneNumber, message)
                self._messageviewselection.unselect_all()
@@ -940,8 +1091,15 @@ class ContactsView(object):
                self._window = gtk_toolbox.find_parent_window(self._contactsview)
                self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
 
+               self._updateSink = gtk_toolbox.threaded_stage(
+                       gtk_toolbox.comap(
+                               self._idly_populate_contactsview,
+                               gtk_toolbox.null_sink(),
+                       )
+               )
+
        def enable(self):
-               assert self._backend.is_authed()
+               assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
 
                self._contactsview.set_model(self._contactsmodel)
                self._contactsview.append_column(self._contactColumn)
@@ -985,7 +1143,7 @@ class ContactsView(object):
                """
                @note Actual dial function is patched in later
                """
-               raise NotImplementedError
+               raise NotImplementedError("Horrible unknown error has occurred")
 
        def get_addressbooks(self):
                """
@@ -1001,16 +1159,13 @@ class ContactsView(object):
 
        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()
+                       return False
+               self._updateSink.send(())
+               return True
 
        def clear(self):
                self._isPopulated = False
                self._contactsmodel.clear()
-
-       def clear_caches(self):
                for factory in self._addressBookFactories:
                        factory.clear_caches()
                self._addressBook.clear_caches()
@@ -1035,27 +1190,29 @@ class ContactsView(object):
                pass
 
        def _idly_populate_contactsview(self):
-               self._isPopulated = True
                self.clear()
+               self._isPopulated = True
 
                # completely disable updating the treeview while we populate the data
                self._contactsview.freeze_child_notify()
-               self._contactsview.set_model(None)
-
-               addressBook = self._addressBook
                try:
-                       contacts = addressBook.get_contacts()
-               except RuntimeError, e:
-                       contacts = []
-                       self._isPopulated = False
-                       self._errorDisplay.push_exception_with_lock(e)
-               for contactId, contactName in contacts:
-                       contactType = (addressBook.contact_source_short_name(contactId), )
-                       self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
-
-               # restart the treeview data rendering
-               self._contactsview.set_model(self._contactsmodel)
-               self._contactsview.thaw_child_notify()
+                       self._contactsview.set_model(None)
+
+                       addressBook = self._addressBook
+                       try:
+                               contacts = addressBook.get_contacts()
+                       except StandardError, e:
+                               contacts = []
+                               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) + ("", ))
+
+                       # restart the treeview data rendering
+                       self._contactsview.set_model(self._contactsmodel)
+               finally:
+                       self._contactsview.thaw_child_notify()
                return False
 
        def _on_addressbook_combo_changed(self, *args, **kwds):
@@ -1075,9 +1232,9 @@ class ContactsView(object):
                contactName = self._contactsmodel.get_value(itr, 1)
                try:
                        contactDetails = self._addressBook.get_contact_details(contactId)
-               except RuntimeError, e:
+               except StandardError, e:
                        contactDetails = []
-                       self._errorDisplay.push_exception(e)
+                       self._errorDisplay.push_exception()
                contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
 
                if len(contactPhoneNumbers) == 0:
@@ -1090,7 +1247,7 @@ class ContactsView(object):
                )
                if action == PhoneTypeSelector.ACTION_CANCEL:
                        return
-               assert phoneNumber
+               assert phoneNumber, "A lack of phone number exists"
 
                self.number_selected(action, phoneNumber, message)
                self._contactsviewselection.unselect_all()