4 DialCentral - Front end for Google's Grand Central service.
5 Copyright (C) 2008 Mark Bergman bergman AT merctech DOT com
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 Lesser General Public License for more details.
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21 @todo Add CTRL-V support to Dialpad
22 @todo Touch selector for notification time
23 @todo Test if hildonize should do stackables by default
24 @todo Alternate UI for dialogs
27 from __future__ import with_statement
41 def make_ugly(prettynumber):
43 function to take a phone number and strip out all non-numeric
46 >>> make_ugly("+012-(345)-678-90")
50 uglynumber = re.sub('\D', '', prettynumber)
54 def make_pretty(phonenumber):
56 Function to take a phone number and return the pretty version
58 if phonenumber begins with 0:
60 if phonenumber begins with 1: ( for gizmo callback numbers )
62 if phonenumber is 13 digits:
64 if phonenumber is 10 digits:
68 >>> make_pretty("1234567")
70 >>> make_pretty("2345678901")
72 >>> make_pretty("12345678901")
74 >>> make_pretty("01234567890")
77 if phonenumber is None or phonenumber is "":
80 phonenumber = make_ugly(phonenumber)
82 if len(phonenumber) < 3:
85 if phonenumber[0] == "0":
87 prettynumber += "+%s" % phonenumber[0:3]
88 if 3 < len(phonenumber):
89 prettynumber += "-(%s)" % phonenumber[3:6]
90 if 6 < len(phonenumber):
91 prettynumber += "-%s" % phonenumber[6:9]
92 if 9 < len(phonenumber):
93 prettynumber += "-%s" % phonenumber[9:]
95 elif len(phonenumber) <= 7:
96 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
97 elif len(phonenumber) > 8 and phonenumber[0] == "1":
98 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
99 elif len(phonenumber) > 7:
100 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
104 def abbrev_relative_date(date):
106 >>> abbrev_relative_date("42 hours ago")
108 >>> abbrev_relative_date("2 days ago")
110 >>> abbrev_relative_date("4 weeks ago")
113 parts = date.split(" ")
114 return "%s %s" % (parts[0], parts[1][0])
117 class MergedAddressBook(object):
119 Merger of all addressbooks
122 def __init__(self, addressbookFactories, sorter = None):
123 self.__addressbookFactories = addressbookFactories
124 self.__addressbooks = None
125 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
127 def clear_caches(self):
128 self.__addressbooks = None
129 for factory in self.__addressbookFactories:
130 factory.clear_caches()
132 def get_addressbooks(self):
134 @returns Iterable of (Address Book Factory, Book Id, Book Name)
138 def open_addressbook(self, bookId):
141 def contact_source_short_name(self, contactId):
142 if self.__addressbooks is None:
144 bookIndex, originalId = contactId.split("-", 1)
145 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
149 return "All Contacts"
151 def get_contacts(self):
153 @returns Iterable of (contact id, contact name)
155 if self.__addressbooks is None:
156 self.__addressbooks = list(
157 factory.open_addressbook(id)
158 for factory in self.__addressbookFactories
159 for (f, id, name) in factory.get_addressbooks()
162 ("-".join([str(bookIndex), contactId]), contactName)
163 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
164 for (contactId, contactName) in addressbook.get_contacts()
166 sortedContacts = self.__sort_contacts(contacts)
167 return sortedContacts
169 def get_contact_details(self, contactId):
171 @returns Iterable of (Phone Type, Phone Number)
173 if self.__addressbooks is None:
175 bookIndex, originalId = contactId.split("-", 1)
176 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
179 def null_sorter(contacts):
181 Good for speed/low memory
186 def basic_firtname_sorter(contacts):
188 Expects names in "First Last" format
191 (contactName.rsplit(" ", 1)[0], (contactId, contactName))
192 for (contactId, contactName) in contacts
194 contactsWithKey.sort()
195 return (contactData for (lastName, contactData) in contactsWithKey)
198 def basic_lastname_sorter(contacts):
200 Expects names in "First Last" format
203 (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
204 for (contactId, contactName) in contacts
206 contactsWithKey.sort()
207 return (contactData for (lastName, contactData) in contactsWithKey)
210 def reversed_firtname_sorter(contacts):
212 Expects names in "Last, First" format
215 (contactName.split(", ", 1)[-1], (contactId, contactName))
216 for (contactId, contactName) in contacts
218 contactsWithKey.sort()
219 return (contactData for (lastName, contactData) in contactsWithKey)
222 def reversed_lastname_sorter(contacts):
224 Expects names in "Last, First" format
227 (contactName.split(", ", 1)[0], (contactId, contactName))
228 for (contactId, contactName) in contacts
230 contactsWithKey.sort()
231 return (contactData for (lastName, contactData) in contactsWithKey)
234 def guess_firstname(name):
236 return name.split(", ", 1)[-1]
238 return name.rsplit(" ", 1)[0]
241 def guess_lastname(name):
243 return name.split(", ", 1)[0]
245 return name.rsplit(" ", 1)[-1]
248 def advanced_firstname_sorter(cls, contacts):
250 (cls.guess_firstname(contactName), (contactId, contactName))
251 for (contactId, contactName) in contacts
253 contactsWithKey.sort()
254 return (contactData for (lastName, contactData) in contactsWithKey)
257 def advanced_lastname_sorter(cls, contacts):
259 (cls.guess_lastname(contactName), (contactId, contactName))
260 for (contactId, contactName) in contacts
262 contactsWithKey.sort()
263 return (contactData for (lastName, contactData) in contactsWithKey)
266 class PhoneTypeSelector(object):
268 ACTION_CANCEL = "cancel"
269 ACTION_SELECT = "select"
271 ACTION_SEND_SMS = "sms"
273 def __init__(self, widgetTree, gcBackend):
274 self._gcBackend = gcBackend
275 self._widgetTree = widgetTree
277 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
278 self._smsDialog = SmsEntryDialog(self._widgetTree)
280 self._smsButton = self._widgetTree.get_widget("sms_button")
281 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
283 self._dialButton = self._widgetTree.get_widget("dial_button")
284 self._dialButton.connect("clicked", self._on_phonetype_dial)
286 self._selectButton = self._widgetTree.get_widget("select_button")
287 self._selectButton.connect("clicked", self._on_phonetype_select)
289 self._cancelButton = self._widgetTree.get_widget("cancel_button")
290 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
292 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
293 self._typeviewselection = None
295 self._message = self._widgetTree.get_widget("phoneSelectionMessage")
296 self._messageViewport = self._widgetTree.get_widget("phoneSelectionMessage_viewport")
297 self._scrollWindow = self._widgetTree.get_widget("phoneSelectionMessage_scrolledwindow")
298 self._typeview = self._widgetTree.get_widget("phonetypes")
299 self._typeview.connect("row-activated", self._on_phonetype_select)
301 self._action = self.ACTION_CANCEL
303 def run(self, contactDetails, message = "", parent = None):
304 self._action = self.ACTION_CANCEL
305 self._typemodel.clear()
306 self._typeview.set_model(self._typemodel)
308 # Add the column to the treeview
309 textrenderer = gtk.CellRendererText()
310 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
311 self._typeview.append_column(numberColumn)
313 textrenderer = gtk.CellRendererText()
314 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
315 self._typeview.append_column(typeColumn)
317 self._typeviewselection = self._typeview.get_selection()
318 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
320 for phoneType, phoneNumber in contactDetails:
321 display = " - ".join((phoneNumber, phoneType))
323 row = (phoneNumber, display)
324 self._typemodel.append(row)
326 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
328 self._message.set_markup(message)
331 self._message.set_markup("")
334 if parent is not None:
335 self._dialog.set_transient_for(parent)
339 adjustment = self._scrollWindow.get_vadjustment()
340 dx = self._message.get_allocation().height - self._messageViewport.get_allocation().height
342 adjustment.value = dx
344 userResponse = self._dialog.run()
348 if userResponse == gtk.RESPONSE_OK:
349 phoneNumber = self._get_number()
350 phoneNumber = make_ugly(phoneNumber)
354 self._action = self.ACTION_CANCEL
356 if self._action == self.ACTION_SEND_SMS:
357 smsMessage = self._smsDialog.run(phoneNumber, message, parent)
360 self._action = self.ACTION_CANCEL
364 self._typeviewselection.unselect_all()
365 self._typeview.remove_column(numberColumn)
366 self._typeview.remove_column(typeColumn)
367 self._typeview.set_model(None)
369 return self._action, phoneNumber, smsMessage
371 def _get_number(self):
372 model, itr = self._typeviewselection.get_selected()
376 phoneNumber = self._typemodel.get_value(itr, 0)
379 def _on_phonetype_dial(self, *args):
380 self._dialog.response(gtk.RESPONSE_OK)
381 self._action = self.ACTION_DIAL
383 def _on_phonetype_send_sms(self, *args):
384 self._dialog.response(gtk.RESPONSE_OK)
385 self._action = self.ACTION_SEND_SMS
387 def _on_phonetype_select(self, *args):
388 self._dialog.response(gtk.RESPONSE_OK)
389 self._action = self.ACTION_SELECT
391 def _on_phonetype_cancel(self, *args):
392 self._dialog.response(gtk.RESPONSE_CANCEL)
393 self._action = self.ACTION_CANCEL
396 class SmsEntryDialog(object):
399 @todo Add multi-SMS messages like GoogleVoice
404 def __init__(self, widgetTree):
405 self._widgetTree = widgetTree
406 self._dialog = self._widgetTree.get_widget("smsDialog")
408 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
409 self._smsButton.connect("clicked", self._on_send)
411 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
412 self._cancelButton.connect("clicked", self._on_cancel)
414 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
415 self._message = self._widgetTree.get_widget("smsMessage")
416 self._messageViewport = self._widgetTree.get_widget("smsMessage_viewport")
417 self._scrollWindow = self._widgetTree.get_widget("smsMessage_scrolledwindow")
418 self._smsEntry = self._widgetTree.get_widget("smsEntry")
419 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
421 def run(self, number, message = "", parent = None):
423 self._message.set_markup(message)
426 self._message.set_markup("")
428 self._smsEntry.get_buffer().set_text("")
429 self._update_letter_count()
431 if parent is not None:
432 self._dialog.set_transient_for(parent)
436 adjustment = self._scrollWindow.get_vadjustment()
437 dx = self._message.get_allocation().height - self._messageViewport.get_allocation().height
439 adjustment.value = dx
441 userResponse = self._dialog.run()
445 if userResponse == gtk.RESPONSE_OK:
446 entryBuffer = self._smsEntry.get_buffer()
447 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
448 enteredMessage = enteredMessage[0:self.MAX_CHAR]
452 return enteredMessage.strip()
454 def _update_letter_count(self, *args):
455 entryLength = self._smsEntry.get_buffer().get_char_count()
456 charsLeft = self.MAX_CHAR - entryLength
457 self._letterCountLabel.set_text(str(charsLeft))
459 self._smsButton.set_sensitive(False)
461 self._smsButton.set_sensitive(True)
463 def _on_entry_changed(self, *args):
464 self._update_letter_count()
466 def _on_send(self, *args):
467 self._dialog.response(gtk.RESPONSE_OK)
469 def _on_cancel(self, *args):
470 self._dialog.response(gtk.RESPONSE_CANCEL)
473 class Dialpad(object):
475 def __init__(self, widgetTree, errorDisplay):
476 self._errorDisplay = errorDisplay
477 self._smsDialog = SmsEntryDialog(widgetTree)
479 self._numberdisplay = widgetTree.get_widget("numberdisplay")
480 self._smsButton = widgetTree.get_widget("sms")
481 self._dialButton = widgetTree.get_widget("dial")
482 self._backButton = widgetTree.get_widget("back")
483 self._phonenumber = ""
484 self._prettynumber = ""
487 "on_digit_clicked": self._on_digit_clicked,
489 widgetTree.signal_autoconnect(callbackMapping)
490 self._dialButton.connect("clicked", self._on_dial_clicked)
491 self._smsButton.connect("clicked", self._on_sms_clicked)
493 self._originalLabel = self._backButton.get_label()
494 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
495 self._backTapHandler.on_tap = self._on_backspace
496 self._backTapHandler.on_hold = self._on_clearall
497 self._backTapHandler.on_holding = self._set_clear_button
498 self._backTapHandler.on_cancel = self._reset_back_button
500 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
503 self._dialButton.grab_focus()
504 self._backTapHandler.enable()
507 self._reset_back_button()
508 self._backTapHandler.disable()
510 def number_selected(self, action, number, message):
512 @note Actual dial function is patched in later
514 raise NotImplementedError("Horrible unknown error has occurred")
516 def get_number(self):
517 return self._phonenumber
519 def set_number(self, number):
521 Set the number to dial
524 self._phonenumber = make_ugly(number)
525 self._prettynumber = make_pretty(self._phonenumber)
526 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
528 self._errorDisplay.push_exception()
537 def load_settings(self, config, section):
540 def save_settings(self, config, section):
542 @note Thread Agnostic
546 def _on_sms_clicked(self, widget):
548 action = PhoneTypeSelector.ACTION_SEND_SMS
549 phoneNumber = self.get_number()
551 message = self._smsDialog.run(phoneNumber, "", self._window)
554 action = PhoneTypeSelector.ACTION_CANCEL
556 if action == PhoneTypeSelector.ACTION_CANCEL:
558 self.number_selected(action, phoneNumber, message)
560 self._errorDisplay.push_exception()
562 def _on_dial_clicked(self, widget):
564 action = PhoneTypeSelector.ACTION_DIAL
565 phoneNumber = self.get_number()
567 self.number_selected(action, phoneNumber, message)
569 self._errorDisplay.push_exception()
571 def _on_digit_clicked(self, widget):
573 self.set_number(self._phonenumber + widget.get_name()[-1])
575 self._errorDisplay.push_exception()
577 def _on_backspace(self, taps):
579 self.set_number(self._phonenumber[:-taps])
580 self._reset_back_button()
582 self._errorDisplay.push_exception()
584 def _on_clearall(self, taps):
587 self._reset_back_button()
589 self._errorDisplay.push_exception()
592 def _set_clear_button(self):
594 self._backButton.set_label("gtk-clear")
596 self._errorDisplay.push_exception()
598 def _reset_back_button(self):
600 self._backButton.set_label(self._originalLabel)
602 self._errorDisplay.push_exception()
605 class AccountInfo(object):
607 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
608 self._errorDisplay = errorDisplay
609 self._backend = backend
610 self._isPopulated = False
611 self._alarmHandler = alarmHandler
612 self._notifyOnMissed = False
613 self._notifyOnVoicemail = False
614 self._notifyOnSms = False
616 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
617 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
618 self._callbackCombo = widgetTree.get_widget("callbackcombo")
619 self._onCallbackentryChangedId = 0
621 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
622 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
623 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
624 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
625 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
626 self._onNotifyToggled = 0
627 self._onMinutesChanged = 0
628 self._onMissedToggled = 0
629 self._onVoicemailToggled = 0
630 self._onSmsToggled = 0
631 self._applyAlarmTimeoutId = None
633 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
634 self._defaultCallback = ""
637 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
639 self._accountViewNumberDisplay.set_use_markup(True)
640 self.set_account_number("")
642 self._callbackList.clear()
643 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
645 if self._alarmHandler is not None:
646 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
647 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
648 self._missedCheckbox.set_active(self._notifyOnMissed)
649 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
650 self._smsCheckbox.set_active(self._notifyOnSms)
652 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
653 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
654 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
655 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
656 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
658 self._notifyCheckbox.set_sensitive(False)
659 self._minutesEntryButton.set_sensitive(False)
660 self._missedCheckbox.set_sensitive(False)
661 self._voicemailCheckbox.set_sensitive(False)
662 self._smsCheckbox.set_sensitive(False)
663 self._minutesEntryButton.set_sensitive(True)
664 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
666 self.update(force=True)
669 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
670 self._onCallbackentryChangedId = 0
672 if self._alarmHandler is not None:
673 self._notifyCheckbox.disconnect(self._onNotifyToggled)
674 self._minutesEntryButton.disconnect(self._onMinutesChanged)
675 self._missedCheckbox.disconnect(self._onNotifyToggled)
676 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
677 self._smsCheckbox.disconnect(self._onNotifyToggled)
678 self._onNotifyToggled = 0
679 self._onMinutesChanged = 0
680 self._onMissedToggled = 0
681 self._onVoicemailToggled = 0
682 self._onSmsToggled = 0
684 self._notifyCheckbox.set_sensitive(True)
685 self._minutesEntryButton.set_sensitive(True)
686 self._missedCheckbox.set_sensitive(True)
687 self._voicemailCheckbox.set_sensitive(True)
688 self._smsCheckbox.set_sensitive(True)
691 self._callbackList.clear()
693 def get_selected_callback_number(self):
694 return make_ugly(self._callbackCombo.get_child().get_text())
696 def set_account_number(self, number):
698 Displays current account number
700 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
702 def update(self, force = False):
703 if not force and self._isPopulated:
705 self._populate_callback_combo()
706 self.set_account_number(self._backend.get_account_number())
710 self._callbackCombo.get_child().set_text("")
711 self.set_account_number("")
712 self._isPopulated = False
714 def save_everything(self):
715 raise NotImplementedError
719 return "Account Info"
721 def load_settings(self, config, section):
722 self._defaultCallback = config.get(section, "callback")
723 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
724 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
725 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
727 def save_settings(self, config, section):
729 @note Thread Agnostic
731 callback = self.get_selected_callback_number()
732 config.set(section, "callback", callback)
733 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
734 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
735 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
737 def _populate_callback_combo(self):
738 self._isPopulated = True
739 self._callbackList.clear()
741 callbackNumbers = self._backend.get_callback_numbers()
743 self._errorDisplay.push_exception()
744 self._isPopulated = False
747 for number, description in callbackNumbers.iteritems():
748 self._callbackList.append((make_pretty(number),))
750 self._callbackCombo.set_model(self._callbackList)
751 self._callbackCombo.set_text_column(0)
752 #callbackNumber = self._backend.get_callback_number()
753 callbackNumber = self._defaultCallback
754 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
756 def _set_callback_number(self, number):
758 if not self._backend.is_valid_syntax(number) and 0 < len(number):
759 self._errorDisplay.push_message("%s is not a valid callback number" % number)
760 elif number == self._backend.get_callback_number():
762 "Callback number already is %s" % (
763 self._backend.get_callback_number(),
767 self._backend.set_callback_number(number)
768 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
769 make_pretty(number), make_pretty(self._backend.get_callback_number())
772 "Callback number set to %s" % (
773 self._backend.get_callback_number(),
777 self._errorDisplay.push_exception()
779 def _update_alarm_settings(self, recurrence):
781 isEnabled = self._notifyCheckbox.get_active()
782 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
783 self._alarmHandler.apply_settings(isEnabled, recurrence)
785 self.save_everything()
786 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
787 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
789 def _on_callbackentry_changed(self, *args):
791 text = self.get_selected_callback_number()
792 number = make_ugly(text)
793 self._set_callback_number(number)
795 self._errorDisplay.push_exception()
797 def _on_notify_toggled(self, *args):
799 if self._applyAlarmTimeoutId is not None:
800 gobject.source_remove(self._applyAlarmTimeoutId)
801 self._applyAlarmTimeoutId = None
802 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
804 self._errorDisplay.push_exception()
806 def _on_minutes_clicked(self, *args):
807 recurrenceChoices = [
819 actualSelection = self._alarmHandler.recurrence
821 closestSelectionIndex = 0
822 for i, possible in enumerate(recurrenceChoices):
823 if possible[0] <= actualSelection:
824 closestSelectionIndex = i
825 recurrenceIndex = hildonize.touch_selector(
828 (("%s" % m[1]) for m in recurrenceChoices),
829 closestSelectionIndex,
831 recurrence = recurrenceChoices[recurrenceIndex][0]
833 self._update_alarm_settings(recurrence)
835 self._errorDisplay.push_exception()
837 def _on_apply_timeout(self, *args):
839 self._applyAlarmTimeoutId = None
841 self._update_alarm_settings(self._alarmHandler.recurrence)
843 self._errorDisplay.push_exception()
846 def _on_missed_toggled(self, *args):
848 self._notifyOnMissed = self._missedCheckbox.get_active()
849 self.save_everything()
851 self._errorDisplay.push_exception()
853 def _on_voicemail_toggled(self, *args):
855 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
856 self.save_everything()
858 self._errorDisplay.push_exception()
860 def _on_sms_toggled(self, *args):
862 self._notifyOnSms = self._smsCheckbox.get_active()
863 self.save_everything()
865 self._errorDisplay.push_exception()
868 class RecentCallsView(object):
875 def __init__(self, widgetTree, backend, errorDisplay):
876 self._errorDisplay = errorDisplay
877 self._backend = backend
879 self._isPopulated = False
880 self._recentmodel = gtk.ListStore(
881 gobject.TYPE_STRING, # number
882 gobject.TYPE_STRING, # date
883 gobject.TYPE_STRING, # action
884 gobject.TYPE_STRING, # from
886 self._recentview = widgetTree.get_widget("recentview")
887 self._recentviewselection = None
888 self._onRecentviewRowActivatedId = 0
890 textrenderer = gtk.CellRendererText()
891 textrenderer.set_property("yalign", 0)
892 self._dateColumn = gtk.TreeViewColumn("Date")
893 self._dateColumn.pack_start(textrenderer, expand=True)
894 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
896 textrenderer = gtk.CellRendererText()
897 textrenderer.set_property("yalign", 0)
898 self._actionColumn = gtk.TreeViewColumn("Action")
899 self._actionColumn.pack_start(textrenderer, expand=True)
900 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
902 textrenderer = gtk.CellRendererText()
903 textrenderer.set_property("yalign", 0)
904 hildonize.set_cell_thumb_selectable(textrenderer)
905 self._nameColumn = gtk.TreeViewColumn("From")
906 self._nameColumn.pack_start(textrenderer, expand=True)
907 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
908 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
910 textrenderer = gtk.CellRendererText()
911 textrenderer.set_property("yalign", 0)
912 self._numberColumn = gtk.TreeViewColumn("Number")
913 self._numberColumn.pack_start(textrenderer, expand=True)
914 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
916 self._window = gtk_toolbox.find_parent_window(self._recentview)
917 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
919 self._updateSink = gtk_toolbox.threaded_stage(
921 self._idly_populate_recentview,
922 gtk_toolbox.null_sink(),
927 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
928 self._recentview.set_model(self._recentmodel)
930 self._recentview.append_column(self._dateColumn)
931 self._recentview.append_column(self._actionColumn)
932 self._recentview.append_column(self._numberColumn)
933 self._recentview.append_column(self._nameColumn)
934 self._recentviewselection = self._recentview.get_selection()
935 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
937 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
940 self._recentview.disconnect(self._onRecentviewRowActivatedId)
944 self._recentview.remove_column(self._dateColumn)
945 self._recentview.remove_column(self._actionColumn)
946 self._recentview.remove_column(self._nameColumn)
947 self._recentview.remove_column(self._numberColumn)
948 self._recentview.set_model(None)
950 def number_selected(self, action, number, message):
952 @note Actual dial function is patched in later
954 raise NotImplementedError("Horrible unknown error has occurred")
956 def update(self, force = False):
957 if not force and self._isPopulated:
959 self._updateSink.send(())
963 self._isPopulated = False
964 self._recentmodel.clear()
968 return "Recent Calls"
970 def load_settings(self, config, section):
973 def save_settings(self, config, section):
975 @note Thread Agnostic
979 def _idly_populate_recentview(self):
981 self._recentmodel.clear()
982 self._isPopulated = True
985 recentItems = self._backend.get_recent()
987 self._errorDisplay.push_exception_with_lock()
988 self._isPopulated = False
991 for personName, phoneNumber, date, action in recentItems:
993 personName = "Unknown"
994 date = abbrev_relative_date(date)
995 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
996 prettyNumber = make_pretty(prettyNumber)
997 item = (prettyNumber, date, action.capitalize(), personName)
998 with gtk_toolbox.gtk_lock():
999 self._recentmodel.append(item)
1000 except Exception, e:
1001 self._errorDisplay.push_exception_with_lock()
1005 def _on_recentview_row_activated(self, treeview, path, view_column):
1007 model, itr = self._recentviewselection.get_selected()
1011 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1012 number = make_ugly(number)
1013 contactPhoneNumbers = [("Phone", number)]
1014 description = self._recentmodel.get_value(itr, self.FROM_IDX)
1016 action, phoneNumber, message = self._phoneTypeSelector.run(
1017 contactPhoneNumbers,
1018 message = description,
1019 parent = self._window,
1021 if action == PhoneTypeSelector.ACTION_CANCEL:
1023 assert phoneNumber, "A lack of phone number exists"
1025 self.number_selected(action, phoneNumber, message)
1026 self._recentviewselection.unselect_all()
1027 except Exception, e:
1028 self._errorDisplay.push_exception()
1031 class MessagesView(object):
1038 def __init__(self, widgetTree, backend, errorDisplay):
1039 self._errorDisplay = errorDisplay
1040 self._backend = backend
1042 self._isPopulated = False
1043 self._messagemodel = gtk.ListStore(
1044 gobject.TYPE_STRING, # number
1045 gobject.TYPE_STRING, # date
1046 gobject.TYPE_STRING, # header
1047 gobject.TYPE_STRING, # message
1049 self._messageview = widgetTree.get_widget("messages_view")
1050 self._messageviewselection = None
1051 self._onMessageviewRowActivatedId = 0
1053 self._messageRenderer = gtk.CellRendererText()
1054 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1055 self._messageRenderer.set_property("wrap-width", 500)
1056 self._messageColumn = gtk.TreeViewColumn("Messages")
1057 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1058 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1059 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1061 self._window = gtk_toolbox.find_parent_window(self._messageview)
1062 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1064 self._updateSink = gtk_toolbox.threaded_stage(
1066 self._idly_populate_messageview,
1067 gtk_toolbox.null_sink(),
1072 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1073 self._messageview.set_model(self._messagemodel)
1075 self._messageview.append_column(self._messageColumn)
1076 self._messageviewselection = self._messageview.get_selection()
1077 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1079 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1082 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1086 self._messageview.remove_column(self._messageColumn)
1087 self._messageview.set_model(None)
1089 def number_selected(self, action, number, message):
1091 @note Actual dial function is patched in later
1093 raise NotImplementedError("Horrible unknown error has occurred")
1095 def update(self, force = False):
1096 if not force and self._isPopulated:
1098 self._updateSink.send(())
1102 self._isPopulated = False
1103 self._messagemodel.clear()
1109 def load_settings(self, config, section):
1112 def save_settings(self, config, section):
1114 @note Thread Agnostic
1118 def _idly_populate_messageview(self):
1120 self._messagemodel.clear()
1121 self._isPopulated = True
1124 messageItems = self._backend.get_messages()
1125 except Exception, e:
1126 self._errorDisplay.push_exception_with_lock()
1127 self._isPopulated = False
1130 for header, number, relativeDate, message in messageItems:
1131 prettyNumber = number[2:] if number.startswith("+1") else number
1132 prettyNumber = make_pretty(prettyNumber)
1133 message = "<b>%s - %s</b> <i>(%s)</i>\n\n%s" % (header, prettyNumber, relativeDate, message)
1134 number = make_ugly(number)
1135 row = (number, relativeDate, header, message)
1136 with gtk_toolbox.gtk_lock():
1137 self._messagemodel.append(row)
1138 except Exception, e:
1139 self._errorDisplay.push_exception_with_lock()
1143 def _on_messageview_row_activated(self, treeview, path, view_column):
1145 model, itr = self._messageviewselection.get_selected()
1149 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1150 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
1152 action, phoneNumber, message = self._phoneTypeSelector.run(
1153 contactPhoneNumbers,
1154 message = description,
1155 parent = self._window,
1157 if action == PhoneTypeSelector.ACTION_CANCEL:
1159 assert phoneNumber, "A lock of phone number exists"
1161 self.number_selected(action, phoneNumber, message)
1162 self._messageviewselection.unselect_all()
1163 except Exception, e:
1164 self._errorDisplay.push_exception()
1167 class ContactsView(object):
1169 def __init__(self, widgetTree, backend, errorDisplay):
1170 self._errorDisplay = errorDisplay
1171 self._backend = backend
1173 self._addressBook = None
1174 self._selectedComboIndex = 0
1175 self._addressBookFactories = [null_backend.NullAddressBook()]
1177 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1178 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
1180 self._isPopulated = False
1181 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1182 self._contactsviewselection = None
1183 self._contactsview = widgetTree.get_widget("contactsview")
1185 self._contactColumn = gtk.TreeViewColumn("Contact")
1186 displayContactSource = False
1187 if displayContactSource:
1188 textrenderer = gtk.CellRendererText()
1189 self._contactColumn.pack_start(textrenderer, expand=False)
1190 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1191 textrenderer = gtk.CellRendererText()
1192 hildonize.set_cell_thumb_selectable(textrenderer)
1193 self._contactColumn.pack_start(textrenderer, expand=True)
1194 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1195 textrenderer = gtk.CellRendererText()
1196 self._contactColumn.pack_start(textrenderer, expand=True)
1197 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1198 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1199 self._contactColumn.set_sort_column_id(1)
1200 self._contactColumn.set_visible(True)
1202 self._onContactsviewRowActivatedId = 0
1203 self._onAddressbookComboChangedId = 0
1204 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1205 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1207 self._updateSink = gtk_toolbox.threaded_stage(
1209 self._idly_populate_contactsview,
1210 gtk_toolbox.null_sink(),
1215 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1217 self._contactsview.set_model(self._contactsmodel)
1218 self._contactsview.append_column(self._contactColumn)
1219 self._contactsviewselection = self._contactsview.get_selection()
1220 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1222 self._booksList.clear()
1223 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1224 if factoryName and bookName:
1225 entryName = "%s: %s" % (factoryName, bookName)
1227 entryName = factoryName
1229 entryName = bookName
1231 entryName = "Bad name (%d)" % factoryId
1232 row = (str(factoryId), bookId, entryName)
1233 self._booksList.append(row)
1235 self._booksSelectionBox.set_model(self._booksList)
1236 cell = gtk.CellRendererText()
1237 self._booksSelectionBox.pack_start(cell, True)
1238 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1240 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1241 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1243 if len(self._booksList) <= self._selectedComboIndex:
1244 self._selectedComboIndex = 0
1245 self._booksSelectionBox.set_active(self._selectedComboIndex)
1248 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1249 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1253 self._booksSelectionBox.clear()
1254 self._booksSelectionBox.set_model(None)
1255 self._contactsview.set_model(None)
1256 self._contactsview.remove_column(self._contactColumn)
1258 def number_selected(self, action, number, message):
1260 @note Actual dial function is patched in later
1262 raise NotImplementedError("Horrible unknown error has occurred")
1264 def get_addressbooks(self):
1266 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1268 for i, factory in enumerate(self._addressBookFactories):
1269 for bookFactory, bookId, bookName in factory.get_addressbooks():
1270 yield (str(i), bookId), (factory.factory_name(), bookName)
1272 def open_addressbook(self, bookFactoryId, bookId):
1273 bookFactoryIndex = int(bookFactoryId)
1274 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1276 forceUpdate = True if addressBook is not self._addressBook else False
1278 self._addressBook = addressBook
1279 self.update(force=forceUpdate)
1281 def update(self, force = False):
1282 if not force and self._isPopulated:
1284 self._updateSink.send(())
1288 self._isPopulated = False
1289 self._contactsmodel.clear()
1290 for factory in self._addressBookFactories:
1291 factory.clear_caches()
1292 self._addressBook.clear_caches()
1294 def append(self, book):
1295 self._addressBookFactories.append(book)
1297 def extend(self, books):
1298 self._addressBookFactories.extend(books)
1304 def load_settings(self, config, sectionName):
1306 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1307 except ConfigParser.NoOptionError:
1308 self._selectedComboIndex = 0
1310 def save_settings(self, config, sectionName):
1311 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1313 def _idly_populate_contactsview(self):
1316 while addressBook is not self._addressBook:
1317 addressBook = self._addressBook
1318 with gtk_toolbox.gtk_lock():
1319 self._contactsview.set_model(None)
1323 contacts = addressBook.get_contacts()
1324 except Exception, e:
1326 self._isPopulated = False
1327 self._errorDisplay.push_exception_with_lock()
1328 for contactId, contactName in contacts:
1329 contactType = (addressBook.contact_source_short_name(contactId), )
1330 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1332 with gtk_toolbox.gtk_lock():
1333 self._contactsview.set_model(self._contactsmodel)
1335 self._isPopulated = True
1336 except Exception, e:
1337 self._errorDisplay.push_exception_with_lock()
1340 def _on_addressbook_combo_changed(self, *args, **kwds):
1342 itr = self._booksSelectionBox.get_active_iter()
1345 self._selectedComboIndex = self._booksSelectionBox.get_active()
1346 selectedFactoryId = self._booksList.get_value(itr, 0)
1347 selectedBookId = self._booksList.get_value(itr, 1)
1348 self.open_addressbook(selectedFactoryId, selectedBookId)
1349 except Exception, e:
1350 self._errorDisplay.push_exception()
1352 def _on_contactsview_row_activated(self, treeview, path, view_column):
1354 model, itr = self._contactsviewselection.get_selected()
1358 contactId = self._contactsmodel.get_value(itr, 3)
1359 contactName = self._contactsmodel.get_value(itr, 1)
1361 contactDetails = self._addressBook.get_contact_details(contactId)
1362 except Exception, e:
1364 self._errorDisplay.push_exception()
1365 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1367 if len(contactPhoneNumbers) == 0:
1370 action, phoneNumber, message = self._phoneTypeSelector.run(
1371 contactPhoneNumbers,
1372 message = contactName,
1373 parent = self._window,
1375 if action == PhoneTypeSelector.ACTION_CANCEL:
1377 assert phoneNumber, "A lack of phone number exists"
1379 self.number_selected(action, phoneNumber, message)
1380 self._contactsviewselection.unselect_all()
1381 except Exception, e:
1382 self._errorDisplay.push_exception()