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 Touch selector for notification time
22 @todo Test if hildonize should do stackables by default
23 @todo Alternate UI for dialogs
26 from __future__ import with_statement
40 def make_ugly(prettynumber):
42 function to take a phone number and strip out all non-numeric
45 >>> make_ugly("+012-(345)-678-90")
49 uglynumber = re.sub('\D', '', prettynumber)
53 def make_pretty(phonenumber):
55 Function to take a phone number and return the pretty version
57 if phonenumber begins with 0:
59 if phonenumber begins with 1: ( for gizmo callback numbers )
61 if phonenumber is 13 digits:
63 if phonenumber is 10 digits:
67 >>> make_pretty("1234567")
69 >>> make_pretty("2345678901")
71 >>> make_pretty("12345678901")
73 >>> make_pretty("01234567890")
76 if phonenumber is None or phonenumber is "":
79 phonenumber = make_ugly(phonenumber)
81 if len(phonenumber) < 3:
84 if phonenumber[0] == "0":
86 prettynumber += "+%s" % phonenumber[0:3]
87 if 3 < len(phonenumber):
88 prettynumber += "-(%s)" % phonenumber[3:6]
89 if 6 < len(phonenumber):
90 prettynumber += "-%s" % phonenumber[6:9]
91 if 9 < len(phonenumber):
92 prettynumber += "-%s" % phonenumber[9:]
94 elif len(phonenumber) <= 7:
95 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
96 elif len(phonenumber) > 8 and phonenumber[0] == "1":
97 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
98 elif len(phonenumber) > 7:
99 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
103 def abbrev_relative_date(date):
105 >>> abbrev_relative_date("42 hours ago")
107 >>> abbrev_relative_date("2 days ago")
109 >>> abbrev_relative_date("4 weeks ago")
112 parts = date.split(" ")
113 return "%s %s" % (parts[0], parts[1][0])
116 class MergedAddressBook(object):
118 Merger of all addressbooks
121 def __init__(self, addressbookFactories, sorter = None):
122 self.__addressbookFactories = addressbookFactories
123 self.__addressbooks = None
124 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
126 def clear_caches(self):
127 self.__addressbooks = None
128 for factory in self.__addressbookFactories:
129 factory.clear_caches()
131 def get_addressbooks(self):
133 @returns Iterable of (Address Book Factory, Book Id, Book Name)
137 def open_addressbook(self, bookId):
140 def contact_source_short_name(self, contactId):
141 if self.__addressbooks is None:
143 bookIndex, originalId = contactId.split("-", 1)
144 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
148 return "All Contacts"
150 def get_contacts(self):
152 @returns Iterable of (contact id, contact name)
154 if self.__addressbooks is None:
155 self.__addressbooks = list(
156 factory.open_addressbook(id)
157 for factory in self.__addressbookFactories
158 for (f, id, name) in factory.get_addressbooks()
161 ("-".join([str(bookIndex), contactId]), contactName)
162 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
163 for (contactId, contactName) in addressbook.get_contacts()
165 sortedContacts = self.__sort_contacts(contacts)
166 return sortedContacts
168 def get_contact_details(self, contactId):
170 @returns Iterable of (Phone Type, Phone Number)
172 if self.__addressbooks is None:
174 bookIndex, originalId = contactId.split("-", 1)
175 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
178 def null_sorter(contacts):
180 Good for speed/low memory
185 def basic_firtname_sorter(contacts):
187 Expects names in "First Last" format
190 (contactName.rsplit(" ", 1)[0], (contactId, contactName))
191 for (contactId, contactName) in contacts
193 contactsWithKey.sort()
194 return (contactData for (lastName, contactData) in contactsWithKey)
197 def basic_lastname_sorter(contacts):
199 Expects names in "First Last" format
202 (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
203 for (contactId, contactName) in contacts
205 contactsWithKey.sort()
206 return (contactData for (lastName, contactData) in contactsWithKey)
209 def reversed_firtname_sorter(contacts):
211 Expects names in "Last, First" format
214 (contactName.split(", ", 1)[-1], (contactId, contactName))
215 for (contactId, contactName) in contacts
217 contactsWithKey.sort()
218 return (contactData for (lastName, contactData) in contactsWithKey)
221 def reversed_lastname_sorter(contacts):
223 Expects names in "Last, First" format
226 (contactName.split(", ", 1)[0], (contactId, contactName))
227 for (contactId, contactName) in contacts
229 contactsWithKey.sort()
230 return (contactData for (lastName, contactData) in contactsWithKey)
233 def guess_firstname(name):
235 return name.split(", ", 1)[-1]
237 return name.rsplit(" ", 1)[0]
240 def guess_lastname(name):
242 return name.split(", ", 1)[0]
244 return name.rsplit(" ", 1)[-1]
247 def advanced_firstname_sorter(cls, contacts):
249 (cls.guess_firstname(contactName), (contactId, contactName))
250 for (contactId, contactName) in contacts
252 contactsWithKey.sort()
253 return (contactData for (lastName, contactData) in contactsWithKey)
256 def advanced_lastname_sorter(cls, contacts):
258 (cls.guess_lastname(contactName), (contactId, contactName))
259 for (contactId, contactName) in contacts
261 contactsWithKey.sort()
262 return (contactData for (lastName, contactData) in contactsWithKey)
265 class PhoneTypeSelector(object):
267 ACTION_CANCEL = "cancel"
268 ACTION_SELECT = "select"
270 ACTION_SEND_SMS = "sms"
272 def __init__(self, widgetTree, gcBackend):
273 self._gcBackend = gcBackend
274 self._widgetTree = widgetTree
276 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
277 self._smsDialog = SmsEntryDialog(self._widgetTree)
279 self._smsButton = self._widgetTree.get_widget("sms_button")
280 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
282 self._dialButton = self._widgetTree.get_widget("dial_button")
283 self._dialButton.connect("clicked", self._on_phonetype_dial)
285 self._selectButton = self._widgetTree.get_widget("select_button")
286 self._selectButton.connect("clicked", self._on_phonetype_select)
288 self._cancelButton = self._widgetTree.get_widget("cancel_button")
289 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
291 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
292 self._typeviewselection = None
294 self._message = self._widgetTree.get_widget("phoneSelectionMessage")
295 self._messageViewport = self._widgetTree.get_widget("phoneSelectionMessage_viewport")
296 self._scrollWindow = self._widgetTree.get_widget("phoneSelectionMessage_scrolledwindow")
297 self._typeview = self._widgetTree.get_widget("phonetypes")
298 self._typeview.connect("row-activated", self._on_phonetype_select)
300 self._action = self.ACTION_CANCEL
302 def run(self, contactDetails, message = "", parent = None):
303 self._action = self.ACTION_CANCEL
304 self._typemodel.clear()
305 self._typeview.set_model(self._typemodel)
307 # Add the column to the treeview
308 textrenderer = gtk.CellRendererText()
309 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
310 self._typeview.append_column(numberColumn)
312 textrenderer = gtk.CellRendererText()
313 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
314 self._typeview.append_column(typeColumn)
316 self._typeviewselection = self._typeview.get_selection()
317 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
319 for phoneType, phoneNumber in contactDetails:
320 display = " - ".join((phoneNumber, phoneType))
322 row = (phoneNumber, display)
323 self._typemodel.append(row)
325 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
327 self._message.set_markup(message)
330 self._message.set_markup("")
333 if parent is not None:
334 self._dialog.set_transient_for(parent)
338 adjustment = self._scrollWindow.get_vadjustment()
339 dx = self._message.get_allocation().height - self._messageViewport.get_allocation().height
341 adjustment.value = dx
343 userResponse = self._dialog.run()
347 if userResponse == gtk.RESPONSE_OK:
348 phoneNumber = self._get_number()
349 phoneNumber = make_ugly(phoneNumber)
353 self._action = self.ACTION_CANCEL
355 if self._action == self.ACTION_SEND_SMS:
356 smsMessage = self._smsDialog.run(phoneNumber, message, parent)
359 self._action = self.ACTION_CANCEL
363 self._typeviewselection.unselect_all()
364 self._typeview.remove_column(numberColumn)
365 self._typeview.remove_column(typeColumn)
366 self._typeview.set_model(None)
368 return self._action, phoneNumber, smsMessage
370 def _get_number(self):
371 model, itr = self._typeviewselection.get_selected()
375 phoneNumber = self._typemodel.get_value(itr, 0)
378 def _on_phonetype_dial(self, *args):
379 self._dialog.response(gtk.RESPONSE_OK)
380 self._action = self.ACTION_DIAL
382 def _on_phonetype_send_sms(self, *args):
383 self._dialog.response(gtk.RESPONSE_OK)
384 self._action = self.ACTION_SEND_SMS
386 def _on_phonetype_select(self, *args):
387 self._dialog.response(gtk.RESPONSE_OK)
388 self._action = self.ACTION_SELECT
390 def _on_phonetype_cancel(self, *args):
391 self._dialog.response(gtk.RESPONSE_CANCEL)
392 self._action = self.ACTION_CANCEL
395 class SmsEntryDialog(object):
398 @todo Add multi-SMS messages like GoogleVoice
403 def __init__(self, widgetTree):
404 self._widgetTree = widgetTree
405 self._dialog = self._widgetTree.get_widget("smsDialog")
407 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
408 self._smsButton.connect("clicked", self._on_send)
410 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
411 self._cancelButton.connect("clicked", self._on_cancel)
413 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
414 self._message = self._widgetTree.get_widget("smsMessage")
415 self._messageViewport = self._widgetTree.get_widget("smsMessage_viewport")
416 self._scrollWindow = self._widgetTree.get_widget("smsMessage_scrolledwindow")
417 self._smsEntry = self._widgetTree.get_widget("smsEntry")
418 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
420 def run(self, number, message = "", parent = None):
422 self._message.set_markup(message)
425 self._message.set_markup("")
427 self._smsEntry.get_buffer().set_text("")
428 self._update_letter_count()
430 if parent is not None:
431 self._dialog.set_transient_for(parent)
435 adjustment = self._scrollWindow.get_vadjustment()
436 dx = self._message.get_allocation().height - self._messageViewport.get_allocation().height
438 adjustment.value = dx
440 userResponse = self._dialog.run()
444 if userResponse == gtk.RESPONSE_OK:
445 entryBuffer = self._smsEntry.get_buffer()
446 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
447 enteredMessage = enteredMessage[0:self.MAX_CHAR]
451 return enteredMessage.strip()
453 def _update_letter_count(self, *args):
454 entryLength = self._smsEntry.get_buffer().get_char_count()
455 charsLeft = self.MAX_CHAR - entryLength
456 self._letterCountLabel.set_text(str(charsLeft))
458 self._smsButton.set_sensitive(False)
460 self._smsButton.set_sensitive(True)
462 def _on_entry_changed(self, *args):
463 self._update_letter_count()
465 def _on_send(self, *args):
466 self._dialog.response(gtk.RESPONSE_OK)
468 def _on_cancel(self, *args):
469 self._dialog.response(gtk.RESPONSE_CANCEL)
472 class Dialpad(object):
474 def __init__(self, widgetTree, errorDisplay):
475 self._errorDisplay = errorDisplay
476 self._smsDialog = SmsEntryDialog(widgetTree)
478 self._numberdisplay = widgetTree.get_widget("numberdisplay")
479 self._smsButton = widgetTree.get_widget("sms")
480 self._dialButton = widgetTree.get_widget("dial")
481 self._backButton = widgetTree.get_widget("back")
482 self._phonenumber = ""
483 self._prettynumber = ""
486 "on_digit_clicked": self._on_digit_clicked,
488 widgetTree.signal_autoconnect(callbackMapping)
489 self._dialButton.connect("clicked", self._on_dial_clicked)
490 self._smsButton.connect("clicked", self._on_sms_clicked)
492 self._originalLabel = self._backButton.get_label()
493 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
494 self._backTapHandler.on_tap = self._on_backspace
495 self._backTapHandler.on_hold = self._on_clearall
496 self._backTapHandler.on_holding = self._set_clear_button
497 self._backTapHandler.on_cancel = self._reset_back_button
499 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
502 self._dialButton.grab_focus()
503 self._backTapHandler.enable()
506 self._reset_back_button()
507 self._backTapHandler.disable()
509 def number_selected(self, action, number, message):
511 @note Actual dial function is patched in later
513 raise NotImplementedError("Horrible unknown error has occurred")
515 def get_number(self):
516 return self._phonenumber
518 def set_number(self, number):
520 Set the number to dial
523 self._phonenumber = make_ugly(number)
524 self._prettynumber = make_pretty(self._phonenumber)
525 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
527 self._errorDisplay.push_exception()
536 def load_settings(self, config, section):
539 def save_settings(self, config, section):
541 @note Thread Agnostic
545 def _on_sms_clicked(self, widget):
547 action = PhoneTypeSelector.ACTION_SEND_SMS
548 phoneNumber = self.get_number()
550 message = self._smsDialog.run(phoneNumber, "", self._window)
553 action = PhoneTypeSelector.ACTION_CANCEL
555 if action == PhoneTypeSelector.ACTION_CANCEL:
557 self.number_selected(action, phoneNumber, message)
559 self._errorDisplay.push_exception()
561 def _on_dial_clicked(self, widget):
563 action = PhoneTypeSelector.ACTION_DIAL
564 phoneNumber = self.get_number()
566 self.number_selected(action, phoneNumber, message)
568 self._errorDisplay.push_exception()
570 def _on_digit_clicked(self, widget):
572 self.set_number(self._phonenumber + widget.get_name()[-1])
574 self._errorDisplay.push_exception()
576 def _on_backspace(self, taps):
578 self.set_number(self._phonenumber[:-taps])
579 self._reset_back_button()
581 self._errorDisplay.push_exception()
583 def _on_clearall(self, taps):
586 self._reset_back_button()
588 self._errorDisplay.push_exception()
591 def _set_clear_button(self):
593 self._backButton.set_label("gtk-clear")
595 self._errorDisplay.push_exception()
597 def _reset_back_button(self):
599 self._backButton.set_label(self._originalLabel)
601 self._errorDisplay.push_exception()
604 class AccountInfo(object):
606 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
607 self._errorDisplay = errorDisplay
608 self._backend = backend
609 self._isPopulated = False
610 self._alarmHandler = alarmHandler
611 self._notifyOnMissed = False
612 self._notifyOnVoicemail = False
613 self._notifyOnSms = False
615 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
616 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
617 self._callbackCombo = widgetTree.get_widget("callbackcombo")
618 self._onCallbackentryChangedId = 0
620 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
621 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
622 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
623 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
624 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
625 self._onNotifyToggled = 0
626 self._onMinutesChanged = 0
627 self._onMissedToggled = 0
628 self._onVoicemailToggled = 0
629 self._onSmsToggled = 0
630 self._applyAlarmTimeoutId = None
632 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
633 self._defaultCallback = ""
636 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
638 self._accountViewNumberDisplay.set_use_markup(True)
639 self.set_account_number("")
641 self._callbackList.clear()
642 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
644 if self._alarmHandler is not None:
645 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
646 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
647 self._missedCheckbox.set_active(self._notifyOnMissed)
648 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
649 self._smsCheckbox.set_active(self._notifyOnSms)
651 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
652 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_changed)
653 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
654 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
655 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
657 self._notifyCheckbox.set_sensitive(False)
658 self._minutesEntryButton.set_sensitive(False)
659 self._missedCheckbox.set_sensitive(False)
660 self._voicemailCheckbox.set_sensitive(False)
661 self._smsCheckbox.set_sensitive(False)
663 self.update(force=True)
666 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
667 self._onCallbackentryChangedId = 0
669 if self._alarmHandler is not None:
670 self._notifyCheckbox.disconnect(self._onNotifyToggled)
671 self._minutesEntryButton.disconnect(self._onMinutesChanged)
672 self._missedCheckbox.disconnect(self._onNotifyToggled)
673 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
674 self._smsCheckbox.disconnect(self._onNotifyToggled)
675 self._onNotifyToggled = 0
676 self._onMinutesChanged = 0
677 self._onMissedToggled = 0
678 self._onVoicemailToggled = 0
679 self._onSmsToggled = 0
681 self._notifyCheckbox.set_sensitive(True)
682 self._minutesEntryButton.set_sensitive(True)
683 self._missedCheckbox.set_sensitive(True)
684 self._voicemailCheckbox.set_sensitive(True)
685 self._smsCheckbox.set_sensitive(True)
688 self._callbackList.clear()
690 def get_selected_callback_number(self):
691 return make_ugly(self._callbackCombo.get_child().get_text())
693 def set_account_number(self, number):
695 Displays current account number
697 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
699 def update(self, force = False):
700 if not force and self._isPopulated:
702 self._populate_callback_combo()
703 self.set_account_number(self._backend.get_account_number())
707 self._callbackCombo.get_child().set_text("")
708 self.set_account_number("")
709 self._isPopulated = False
711 def save_everything(self):
712 raise NotImplementedError
716 return "Account Info"
718 def load_settings(self, config, section):
719 self._defaultCallback = config.get(section, "callback")
720 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
721 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
722 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
724 def save_settings(self, config, section):
726 @note Thread Agnostic
728 callback = self.get_selected_callback_number()
729 config.set(section, "callback", callback)
730 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
731 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
732 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
734 def _populate_callback_combo(self):
735 self._isPopulated = True
736 self._callbackList.clear()
738 callbackNumbers = self._backend.get_callback_numbers()
740 self._errorDisplay.push_exception()
741 self._isPopulated = False
744 for number, description in callbackNumbers.iteritems():
745 self._callbackList.append((make_pretty(number),))
747 self._callbackCombo.set_model(self._callbackList)
748 self._callbackCombo.set_text_column(0)
749 #callbackNumber = self._backend.get_callback_number()
750 callbackNumber = self._defaultCallback
751 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
753 def _set_callback_number(self, number):
755 if not self._backend.is_valid_syntax(number) and 0 < len(number):
756 self._errorDisplay.push_message("%s is not a valid callback number" % number)
757 elif number == self._backend.get_callback_number():
759 "Callback number already is %s" % (
760 self._backend.get_callback_number(),
764 self._backend.set_callback_number(number)
765 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
766 make_pretty(number), make_pretty(self._backend.get_callback_number())
769 "Callback number set to %s" % (
770 self._backend.get_callback_number(),
774 self._errorDisplay.push_exception()
776 def _update_alarm_settings(self, recurrence):
778 isEnabled = self._notifyCheckbox.get_active()
779 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
780 self._alarmHandler.apply_settings(isEnabled, recurrence)
782 self.save_everything()
783 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
784 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
786 def _on_callbackentry_changed(self, *args):
788 text = self.get_selected_callback_number()
789 number = make_ugly(text)
790 self._set_callback_number(number)
792 self._errorDisplay.push_exception()
794 def _on_notify_toggled(self, *args):
796 if self._applyAlarmTimeoutId is not None:
797 gobject.source_remove(self._applyAlarmTimeoutId)
798 self._applyAlarmTimeoutId = None
799 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
801 self._errorDisplay.push_exception()
803 def _on_minutes_changed(self, *args):
805 recurrence = hildonize.request_number(
806 self._window, "Minutes", (1, 50), self._alarmHandler.recurrence
808 self._update_alarm_settings(recurrence)
810 self._errorDisplay.push_exception()
812 def _on_apply_timeout(self, *args):
814 self._applyAlarmTimeoutId = None
816 self._update_alarm_settings(self._alarmHandler.recurrence)
818 self._errorDisplay.push_exception()
821 def _on_missed_toggled(self, *args):
823 self._notifyOnMissed = self._missedCheckbox.get_active()
824 self.save_everything()
826 self._errorDisplay.push_exception()
828 def _on_voicemail_toggled(self, *args):
830 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
831 self.save_everything()
833 self._errorDisplay.push_exception()
835 def _on_sms_toggled(self, *args):
837 self._notifyOnSms = self._smsCheckbox.get_active()
838 self.save_everything()
840 self._errorDisplay.push_exception()
843 class RecentCallsView(object):
850 def __init__(self, widgetTree, backend, errorDisplay):
851 self._errorDisplay = errorDisplay
852 self._backend = backend
854 self._isPopulated = False
855 self._recentmodel = gtk.ListStore(
856 gobject.TYPE_STRING, # number
857 gobject.TYPE_STRING, # date
858 gobject.TYPE_STRING, # action
859 gobject.TYPE_STRING, # from
861 self._recentview = widgetTree.get_widget("recentview")
862 self._recentviewselection = None
863 self._onRecentviewRowActivatedId = 0
865 textrenderer = gtk.CellRendererText()
866 textrenderer.set_property("yalign", 0)
867 self._dateColumn = gtk.TreeViewColumn("Date")
868 self._dateColumn.pack_start(textrenderer, expand=True)
869 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
871 textrenderer = gtk.CellRendererText()
872 textrenderer.set_property("yalign", 0)
873 self._actionColumn = gtk.TreeViewColumn("Action")
874 self._actionColumn.pack_start(textrenderer, expand=True)
875 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
877 textrenderer = gtk.CellRendererText()
878 textrenderer.set_property("yalign", 0)
879 hildonize.set_cell_thumb_selectable(textrenderer)
880 self._nameColumn = gtk.TreeViewColumn("From")
881 self._nameColumn.pack_start(textrenderer, expand=True)
882 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
883 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
885 textrenderer = gtk.CellRendererText()
886 textrenderer.set_property("yalign", 0)
887 self._numberColumn = gtk.TreeViewColumn("Number")
888 self._numberColumn.pack_start(textrenderer, expand=True)
889 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
891 self._window = gtk_toolbox.find_parent_window(self._recentview)
892 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
894 self._updateSink = gtk_toolbox.threaded_stage(
896 self._idly_populate_recentview,
897 gtk_toolbox.null_sink(),
902 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
903 self._recentview.set_model(self._recentmodel)
905 self._recentview.append_column(self._dateColumn)
906 self._recentview.append_column(self._actionColumn)
907 self._recentview.append_column(self._numberColumn)
908 self._recentview.append_column(self._nameColumn)
909 self._recentviewselection = self._recentview.get_selection()
910 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
912 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
915 self._recentview.disconnect(self._onRecentviewRowActivatedId)
919 self._recentview.remove_column(self._dateColumn)
920 self._recentview.remove_column(self._actionColumn)
921 self._recentview.remove_column(self._nameColumn)
922 self._recentview.remove_column(self._numberColumn)
923 self._recentview.set_model(None)
925 def number_selected(self, action, number, message):
927 @note Actual dial function is patched in later
929 raise NotImplementedError("Horrible unknown error has occurred")
931 def update(self, force = False):
932 if not force and self._isPopulated:
934 self._updateSink.send(())
938 self._isPopulated = False
939 self._recentmodel.clear()
943 return "Recent Calls"
945 def load_settings(self, config, section):
948 def save_settings(self, config, section):
950 @note Thread Agnostic
954 def _idly_populate_recentview(self):
956 self._recentmodel.clear()
957 self._isPopulated = True
960 recentItems = self._backend.get_recent()
962 self._errorDisplay.push_exception_with_lock()
963 self._isPopulated = False
966 for personName, phoneNumber, date, action in recentItems:
968 personName = "Unknown"
969 date = abbrev_relative_date(date)
970 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
971 prettyNumber = make_pretty(prettyNumber)
972 item = (prettyNumber, date, action.capitalize(), personName)
973 with gtk_toolbox.gtk_lock():
974 self._recentmodel.append(item)
976 self._errorDisplay.push_exception_with_lock()
980 def _on_recentview_row_activated(self, treeview, path, view_column):
982 model, itr = self._recentviewselection.get_selected()
986 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
987 number = make_ugly(number)
988 contactPhoneNumbers = [("Phone", number)]
989 description = self._recentmodel.get_value(itr, self.FROM_IDX)
991 action, phoneNumber, message = self._phoneTypeSelector.run(
993 message = description,
994 parent = self._window,
996 if action == PhoneTypeSelector.ACTION_CANCEL:
998 assert phoneNumber, "A lack of phone number exists"
1000 self.number_selected(action, phoneNumber, message)
1001 self._recentviewselection.unselect_all()
1002 except Exception, e:
1003 self._errorDisplay.push_exception()
1006 class MessagesView(object):
1013 def __init__(self, widgetTree, backend, errorDisplay):
1014 self._errorDisplay = errorDisplay
1015 self._backend = backend
1017 self._isPopulated = False
1018 self._messagemodel = gtk.ListStore(
1019 gobject.TYPE_STRING, # number
1020 gobject.TYPE_STRING, # date
1021 gobject.TYPE_STRING, # header
1022 gobject.TYPE_STRING, # message
1024 self._messageview = widgetTree.get_widget("messages_view")
1025 self._messageviewselection = None
1026 self._onMessageviewRowActivatedId = 0
1028 self._messageRenderer = gtk.CellRendererText()
1029 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1030 self._messageRenderer.set_property("wrap-width", 500)
1031 self._messageColumn = gtk.TreeViewColumn("Messages")
1032 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1033 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1034 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1036 self._window = gtk_toolbox.find_parent_window(self._messageview)
1037 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1039 self._updateSink = gtk_toolbox.threaded_stage(
1041 self._idly_populate_messageview,
1042 gtk_toolbox.null_sink(),
1047 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1048 self._messageview.set_model(self._messagemodel)
1050 self._messageview.append_column(self._messageColumn)
1051 self._messageviewselection = self._messageview.get_selection()
1052 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1054 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1057 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1061 self._messageview.remove_column(self._messageColumn)
1062 self._messageview.set_model(None)
1064 def number_selected(self, action, number, message):
1066 @note Actual dial function is patched in later
1068 raise NotImplementedError("Horrible unknown error has occurred")
1070 def update(self, force = False):
1071 if not force and self._isPopulated:
1073 self._updateSink.send(())
1077 self._isPopulated = False
1078 self._messagemodel.clear()
1084 def load_settings(self, config, section):
1087 def save_settings(self, config, section):
1089 @note Thread Agnostic
1093 def _idly_populate_messageview(self):
1095 self._messagemodel.clear()
1096 self._isPopulated = True
1099 messageItems = self._backend.get_messages()
1100 except Exception, e:
1101 self._errorDisplay.push_exception_with_lock()
1102 self._isPopulated = False
1105 for header, number, relativeDate, message in messageItems:
1106 prettyNumber = number[2:] if number.startswith("+1") else number
1107 prettyNumber = make_pretty(prettyNumber)
1108 message = "<b>%s - %s</b> <i>(%s)</i>\n\n%s" % (header, prettyNumber, relativeDate, message)
1109 number = make_ugly(number)
1110 row = (number, relativeDate, header, message)
1111 with gtk_toolbox.gtk_lock():
1112 self._messagemodel.append(row)
1113 except Exception, e:
1114 self._errorDisplay.push_exception_with_lock()
1118 def _on_messageview_row_activated(self, treeview, path, view_column):
1120 model, itr = self._messageviewselection.get_selected()
1124 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1125 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
1127 action, phoneNumber, message = self._phoneTypeSelector.run(
1128 contactPhoneNumbers,
1129 message = description,
1130 parent = self._window,
1132 if action == PhoneTypeSelector.ACTION_CANCEL:
1134 assert phoneNumber, "A lock of phone number exists"
1136 self.number_selected(action, phoneNumber, message)
1137 self._messageviewselection.unselect_all()
1138 except Exception, e:
1139 self._errorDisplay.push_exception()
1142 class ContactsView(object):
1144 def __init__(self, widgetTree, backend, errorDisplay):
1145 self._errorDisplay = errorDisplay
1146 self._backend = backend
1148 self._addressBook = None
1149 self._selectedComboIndex = 0
1150 self._addressBookFactories = [null_backend.NullAddressBook()]
1152 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1153 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
1155 self._isPopulated = False
1156 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1157 self._contactsviewselection = None
1158 self._contactsview = widgetTree.get_widget("contactsview")
1160 self._contactColumn = gtk.TreeViewColumn("Contact")
1161 displayContactSource = False
1162 if displayContactSource:
1163 textrenderer = gtk.CellRendererText()
1164 self._contactColumn.pack_start(textrenderer, expand=False)
1165 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1166 textrenderer = gtk.CellRendererText()
1167 hildonize.set_cell_thumb_selectable(textrenderer)
1168 self._contactColumn.pack_start(textrenderer, expand=True)
1169 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1170 textrenderer = gtk.CellRendererText()
1171 self._contactColumn.pack_start(textrenderer, expand=True)
1172 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1173 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1174 self._contactColumn.set_sort_column_id(1)
1175 self._contactColumn.set_visible(True)
1177 self._onContactsviewRowActivatedId = 0
1178 self._onAddressbookComboChangedId = 0
1179 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1180 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1182 self._updateSink = gtk_toolbox.threaded_stage(
1184 self._idly_populate_contactsview,
1185 gtk_toolbox.null_sink(),
1190 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1192 self._contactsview.set_model(self._contactsmodel)
1193 self._contactsview.append_column(self._contactColumn)
1194 self._contactsviewselection = self._contactsview.get_selection()
1195 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1197 self._booksList.clear()
1198 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1199 if factoryName and bookName:
1200 entryName = "%s: %s" % (factoryName, bookName)
1202 entryName = factoryName
1204 entryName = bookName
1206 entryName = "Bad name (%d)" % factoryId
1207 row = (str(factoryId), bookId, entryName)
1208 self._booksList.append(row)
1210 self._booksSelectionBox.set_model(self._booksList)
1211 cell = gtk.CellRendererText()
1212 self._booksSelectionBox.pack_start(cell, True)
1213 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1215 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1216 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1218 if len(self._booksList) <= self._selectedComboIndex:
1219 self._selectedComboIndex = 0
1220 self._booksSelectionBox.set_active(self._selectedComboIndex)
1223 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1224 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1228 self._booksSelectionBox.clear()
1229 self._booksSelectionBox.set_model(None)
1230 self._contactsview.set_model(None)
1231 self._contactsview.remove_column(self._contactColumn)
1233 def number_selected(self, action, number, message):
1235 @note Actual dial function is patched in later
1237 raise NotImplementedError("Horrible unknown error has occurred")
1239 def get_addressbooks(self):
1241 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1243 for i, factory in enumerate(self._addressBookFactories):
1244 for bookFactory, bookId, bookName in factory.get_addressbooks():
1245 yield (str(i), bookId), (factory.factory_name(), bookName)
1247 def open_addressbook(self, bookFactoryId, bookId):
1248 bookFactoryIndex = int(bookFactoryId)
1249 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1251 forceUpdate = True if addressBook is not self._addressBook else False
1253 self._addressBook = addressBook
1254 self.update(force=forceUpdate)
1256 def update(self, force = False):
1257 if not force and self._isPopulated:
1259 self._updateSink.send(())
1263 self._isPopulated = False
1264 self._contactsmodel.clear()
1265 for factory in self._addressBookFactories:
1266 factory.clear_caches()
1267 self._addressBook.clear_caches()
1269 def append(self, book):
1270 self._addressBookFactories.append(book)
1272 def extend(self, books):
1273 self._addressBookFactories.extend(books)
1279 def load_settings(self, config, sectionName):
1281 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1282 except ConfigParser.NoOptionError:
1283 self._selectedComboIndex = 0
1285 def save_settings(self, config, sectionName):
1286 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1288 def _idly_populate_contactsview(self):
1291 while addressBook is not self._addressBook:
1292 addressBook = self._addressBook
1293 with gtk_toolbox.gtk_lock():
1294 self._contactsview.set_model(None)
1298 contacts = addressBook.get_contacts()
1299 except Exception, e:
1301 self._isPopulated = False
1302 self._errorDisplay.push_exception_with_lock()
1303 for contactId, contactName in contacts:
1304 contactType = (addressBook.contact_source_short_name(contactId), )
1305 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1307 with gtk_toolbox.gtk_lock():
1308 self._contactsview.set_model(self._contactsmodel)
1310 self._isPopulated = True
1311 except Exception, e:
1312 self._errorDisplay.push_exception_with_lock()
1315 def _on_addressbook_combo_changed(self, *args, **kwds):
1317 itr = self._booksSelectionBox.get_active_iter()
1320 self._selectedComboIndex = self._booksSelectionBox.get_active()
1321 selectedFactoryId = self._booksList.get_value(itr, 0)
1322 selectedBookId = self._booksList.get_value(itr, 1)
1323 self.open_addressbook(selectedFactoryId, selectedBookId)
1324 except Exception, e:
1325 self._errorDisplay.push_exception()
1327 def _on_contactsview_row_activated(self, treeview, path, view_column):
1329 model, itr = self._contactsviewselection.get_selected()
1333 contactId = self._contactsmodel.get_value(itr, 3)
1334 contactName = self._contactsmodel.get_value(itr, 1)
1336 contactDetails = self._addressBook.get_contact_details(contactId)
1337 except Exception, e:
1339 self._errorDisplay.push_exception()
1340 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1342 if len(contactPhoneNumbers) == 0:
1345 action, phoneNumber, message = self._phoneTypeSelector.run(
1346 contactPhoneNumbers,
1347 message = contactName,
1348 parent = self._window,
1350 if action == PhoneTypeSelector.ACTION_CANCEL:
1352 assert phoneNumber, "A lack of phone number exists"
1354 self.number_selected(action, phoneNumber, message)
1355 self._contactsviewselection.unselect_all()
1356 except Exception, e:
1357 self._errorDisplay.push_exception()