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
22 from __future__ import with_statement
36 def make_ugly(prettynumber):
38 function to take a phone number and strip out all non-numeric
41 >>> make_ugly("+012-(345)-678-90")
45 uglynumber = re.sub('\D', '', prettynumber)
49 def make_pretty(phonenumber):
51 Function to take a phone number and return the pretty version
53 if phonenumber begins with 0:
55 if phonenumber begins with 1: ( for gizmo callback numbers )
57 if phonenumber is 13 digits:
59 if phonenumber is 10 digits:
63 >>> make_pretty("1234567")
65 >>> make_pretty("2345678901")
67 >>> make_pretty("12345678901")
69 >>> make_pretty("01234567890")
72 if phonenumber is None or phonenumber is "":
75 phonenumber = make_ugly(phonenumber)
77 if len(phonenumber) < 3:
80 if phonenumber[0] == "0":
82 prettynumber += "+%s" % phonenumber[0:3]
83 if 3 < len(phonenumber):
84 prettynumber += "-(%s)" % phonenumber[3:6]
85 if 6 < len(phonenumber):
86 prettynumber += "-%s" % phonenumber[6:9]
87 if 9 < len(phonenumber):
88 prettynumber += "-%s" % phonenumber[9:]
90 elif len(phonenumber) <= 7:
91 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
92 elif len(phonenumber) > 8 and phonenumber[0] == "1":
93 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
94 elif len(phonenumber) > 7:
95 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
99 def abbrev_relative_date(date):
101 >>> abbrev_relative_date("42 hours ago")
103 >>> abbrev_relative_date("2 days ago")
105 >>> abbrev_relative_date("4 weeks ago")
108 parts = date.split(" ")
109 return "%s %s" % (parts[0], parts[1][0])
112 class MergedAddressBook(object):
114 Merger of all addressbooks
117 def __init__(self, addressbookFactories, sorter = None):
118 self.__addressbookFactories = addressbookFactories
119 self.__addressbooks = None
120 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
122 def clear_caches(self):
123 self.__addressbooks = None
124 for factory in self.__addressbookFactories:
125 factory.clear_caches()
127 def get_addressbooks(self):
129 @returns Iterable of (Address Book Factory, Book Id, Book Name)
133 def open_addressbook(self, bookId):
136 def contact_source_short_name(self, contactId):
137 if self.__addressbooks is None:
139 bookIndex, originalId = contactId.split("-", 1)
140 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
144 return "All Contacts"
146 def get_contacts(self):
148 @returns Iterable of (contact id, contact name)
150 if self.__addressbooks is None:
151 self.__addressbooks = list(
152 factory.open_addressbook(id)
153 for factory in self.__addressbookFactories
154 for (f, id, name) in factory.get_addressbooks()
157 ("-".join([str(bookIndex), contactId]), contactName)
158 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
159 for (contactId, contactName) in addressbook.get_contacts()
161 sortedContacts = self.__sort_contacts(contacts)
162 return sortedContacts
164 def get_contact_details(self, contactId):
166 @returns Iterable of (Phone Type, Phone Number)
168 if self.__addressbooks is None:
170 bookIndex, originalId = contactId.split("-", 1)
171 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
174 def null_sorter(contacts):
176 Good for speed/low memory
181 def basic_firtname_sorter(contacts):
183 Expects names in "First Last" format
186 (contactName.rsplit(" ", 1)[0], (contactId, contactName))
187 for (contactId, contactName) in contacts
189 contactsWithKey.sort()
190 return (contactData for (lastName, contactData) in contactsWithKey)
193 def basic_lastname_sorter(contacts):
195 Expects names in "First Last" format
198 (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
199 for (contactId, contactName) in contacts
201 contactsWithKey.sort()
202 return (contactData for (lastName, contactData) in contactsWithKey)
205 def reversed_firtname_sorter(contacts):
207 Expects names in "Last, First" format
210 (contactName.split(", ", 1)[-1], (contactId, contactName))
211 for (contactId, contactName) in contacts
213 contactsWithKey.sort()
214 return (contactData for (lastName, contactData) in contactsWithKey)
217 def reversed_lastname_sorter(contacts):
219 Expects names in "Last, First" format
222 (contactName.split(", ", 1)[0], (contactId, contactName))
223 for (contactId, contactName) in contacts
225 contactsWithKey.sort()
226 return (contactData for (lastName, contactData) in contactsWithKey)
229 def guess_firstname(name):
231 return name.split(", ", 1)[-1]
233 return name.rsplit(" ", 1)[0]
236 def guess_lastname(name):
238 return name.split(", ", 1)[0]
240 return name.rsplit(" ", 1)[-1]
243 def advanced_firstname_sorter(cls, contacts):
245 (cls.guess_firstname(contactName), (contactId, contactName))
246 for (contactId, contactName) in contacts
248 contactsWithKey.sort()
249 return (contactData for (lastName, contactData) in contactsWithKey)
252 def advanced_lastname_sorter(cls, contacts):
254 (cls.guess_lastname(contactName), (contactId, contactName))
255 for (contactId, contactName) in contacts
257 contactsWithKey.sort()
258 return (contactData for (lastName, contactData) in contactsWithKey)
261 class PhoneTypeSelector(object):
263 ACTION_CANCEL = "cancel"
264 ACTION_SELECT = "select"
266 ACTION_SEND_SMS = "sms"
268 def __init__(self, widgetTree, gcBackend):
269 self._gcBackend = gcBackend
270 self._widgetTree = widgetTree
272 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
273 self._smsDialog = SmsEntryDialog(self._widgetTree)
275 self._smsButton = self._widgetTree.get_widget("sms_button")
276 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
278 self._dialButton = self._widgetTree.get_widget("dial_button")
279 self._dialButton.connect("clicked", self._on_phonetype_dial)
281 self._selectButton = self._widgetTree.get_widget("select_button")
282 self._selectButton.connect("clicked", self._on_phonetype_select)
284 self._cancelButton = self._widgetTree.get_widget("cancel_button")
285 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
287 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
288 self._typeviewselection = None
290 self._message = self._widgetTree.get_widget("phoneSelectionMessage")
291 self._messageViewport = self._widgetTree.get_widget("phoneSelectionMessage_viewport")
292 self._scrollWindow = self._widgetTree.get_widget("phoneSelectionMessage_scrolledwindow")
293 self._typeview = self._widgetTree.get_widget("phonetypes")
294 self._typeview.connect("row-activated", self._on_phonetype_select)
296 self._action = self.ACTION_CANCEL
298 def run(self, contactDetails, message = "", parent = None):
299 self._action = self.ACTION_CANCEL
300 self._typemodel.clear()
301 self._typeview.set_model(self._typemodel)
303 # Add the column to the treeview
304 textrenderer = gtk.CellRendererText()
305 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
306 self._typeview.append_column(numberColumn)
308 textrenderer = gtk.CellRendererText()
309 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
310 self._typeview.append_column(typeColumn)
312 self._typeviewselection = self._typeview.get_selection()
313 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
315 for phoneType, phoneNumber in contactDetails:
316 display = " - ".join((phoneNumber, phoneType))
318 row = (phoneNumber, display)
319 self._typemodel.append(row)
321 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
323 self._message.set_markup(message)
326 self._message.set_markup("")
329 if parent is not None:
330 self._dialog.set_transient_for(parent)
334 adjustment = self._scrollWindow.get_vadjustment()
335 dx = self._message.get_allocation().height - self._messageViewport.get_allocation().height
337 adjustment.value = dx
339 userResponse = self._dialog.run()
343 if userResponse == gtk.RESPONSE_OK:
344 phoneNumber = self._get_number()
345 phoneNumber = make_ugly(phoneNumber)
349 self._action = self.ACTION_CANCEL
351 if self._action == self.ACTION_SEND_SMS:
352 smsMessage = self._smsDialog.run(phoneNumber, message, parent)
355 self._action = self.ACTION_CANCEL
359 self._typeviewselection.unselect_all()
360 self._typeview.remove_column(numberColumn)
361 self._typeview.remove_column(typeColumn)
362 self._typeview.set_model(None)
364 return self._action, phoneNumber, smsMessage
366 def _get_number(self):
367 model, itr = self._typeviewselection.get_selected()
371 phoneNumber = self._typemodel.get_value(itr, 0)
374 def _on_phonetype_dial(self, *args):
375 self._dialog.response(gtk.RESPONSE_OK)
376 self._action = self.ACTION_DIAL
378 def _on_phonetype_send_sms(self, *args):
379 self._dialog.response(gtk.RESPONSE_OK)
380 self._action = self.ACTION_SEND_SMS
382 def _on_phonetype_select(self, *args):
383 self._dialog.response(gtk.RESPONSE_OK)
384 self._action = self.ACTION_SELECT
386 def _on_phonetype_cancel(self, *args):
387 self._dialog.response(gtk.RESPONSE_CANCEL)
388 self._action = self.ACTION_CANCEL
391 class SmsEntryDialog(object):
394 @todo Add multi-SMS messages like GoogleVoice
399 def __init__(self, widgetTree):
400 self._widgetTree = widgetTree
401 self._dialog = self._widgetTree.get_widget("smsDialog")
403 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
404 self._smsButton.connect("clicked", self._on_send)
406 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
407 self._cancelButton.connect("clicked", self._on_cancel)
409 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
410 self._message = self._widgetTree.get_widget("smsMessage")
411 self._messageViewport = self._widgetTree.get_widget("smsMessage_viewport")
412 self._scrollWindow = self._widgetTree.get_widget("smsMessage_scrolledwindow")
413 self._smsEntry = self._widgetTree.get_widget("smsEntry")
414 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
416 def run(self, number, message = "", parent = None):
418 self._message.set_markup(message)
421 self._message.set_markup("")
423 self._smsEntry.get_buffer().set_text("")
424 self._update_letter_count()
426 if parent is not None:
427 self._dialog.set_transient_for(parent)
431 adjustment = self._scrollWindow.get_vadjustment()
432 dx = self._message.get_allocation().height - self._messageViewport.get_allocation().height
434 adjustment.value = dx
436 userResponse = self._dialog.run()
440 if userResponse == gtk.RESPONSE_OK:
441 entryBuffer = self._smsEntry.get_buffer()
442 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
443 enteredMessage = enteredMessage[0:self.MAX_CHAR]
447 return enteredMessage.strip()
449 def _update_letter_count(self, *args):
450 entryLength = self._smsEntry.get_buffer().get_char_count()
451 charsLeft = self.MAX_CHAR - entryLength
452 self._letterCountLabel.set_text(str(charsLeft))
454 self._smsButton.set_sensitive(False)
456 self._smsButton.set_sensitive(True)
458 def _on_entry_changed(self, *args):
459 self._update_letter_count()
461 def _on_send(self, *args):
462 self._dialog.response(gtk.RESPONSE_OK)
464 def _on_cancel(self, *args):
465 self._dialog.response(gtk.RESPONSE_CANCEL)
468 class Dialpad(object):
470 def __init__(self, widgetTree, errorDisplay):
471 self._errorDisplay = errorDisplay
472 self._smsDialog = SmsEntryDialog(widgetTree)
474 self._numberdisplay = widgetTree.get_widget("numberdisplay")
475 self._smsButton = widgetTree.get_widget("sms")
476 self._dialButton = widgetTree.get_widget("dial")
477 self._backButton = widgetTree.get_widget("back")
478 self._phonenumber = ""
479 self._prettynumber = ""
482 "on_digit_clicked": self._on_digit_clicked,
484 widgetTree.signal_autoconnect(callbackMapping)
485 self._dialButton.connect("clicked", self._on_dial_clicked)
486 self._smsButton.connect("clicked", self._on_sms_clicked)
488 self._originalLabel = self._backButton.get_label()
489 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
490 self._backTapHandler.on_tap = self._on_backspace
491 self._backTapHandler.on_hold = self._on_clearall
492 self._backTapHandler.on_holding = self._set_clear_button
493 self._backTapHandler.on_cancel = self._reset_back_button
495 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
498 self._dialButton.grab_focus()
499 self._backTapHandler.enable()
502 self._reset_back_button()
503 self._backTapHandler.disable()
505 def number_selected(self, action, number, message):
507 @note Actual dial function is patched in later
509 raise NotImplementedError("Horrible unknown error has occurred")
511 def get_number(self):
512 return self._phonenumber
514 def set_number(self, number):
516 Set the number to dial
519 self._phonenumber = make_ugly(number)
520 self._prettynumber = make_pretty(self._phonenumber)
521 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
523 self._errorDisplay.push_exception()
532 def load_settings(self, config, section):
535 def save_settings(self, config, section):
537 @note Thread Agnostic
541 def _on_sms_clicked(self, widget):
543 action = PhoneTypeSelector.ACTION_SEND_SMS
544 phoneNumber = self.get_number()
546 message = self._smsDialog.run(phoneNumber, "", self._window)
549 action = PhoneTypeSelector.ACTION_CANCEL
551 if action == PhoneTypeSelector.ACTION_CANCEL:
553 self.number_selected(action, phoneNumber, message)
555 self._errorDisplay.push_exception()
557 def _on_dial_clicked(self, widget):
559 action = PhoneTypeSelector.ACTION_DIAL
560 phoneNumber = self.get_number()
562 self.number_selected(action, phoneNumber, message)
564 self._errorDisplay.push_exception()
566 def _on_digit_clicked(self, widget):
568 self.set_number(self._phonenumber + widget.get_name()[-1])
570 self._errorDisplay.push_exception()
572 def _on_backspace(self, taps):
574 self.set_number(self._phonenumber[:-taps])
575 self._reset_back_button()
577 self._errorDisplay.push_exception()
579 def _on_clearall(self, taps):
582 self._reset_back_button()
584 self._errorDisplay.push_exception()
587 def _set_clear_button(self):
589 self._backButton.set_label("gtk-clear")
591 self._errorDisplay.push_exception()
593 def _reset_back_button(self):
595 self._backButton.set_label(self._originalLabel)
597 self._errorDisplay.push_exception()
600 class AccountInfo(object):
602 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
603 self._errorDisplay = errorDisplay
604 self._backend = backend
605 self._isPopulated = False
606 self._alarmHandler = alarmHandler
607 self._notifyOnMissed = False
608 self._notifyOnVoicemail = False
609 self._notifyOnSms = False
611 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
612 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
613 self._callbackCombo = widgetTree.get_widget("callbackcombo")
614 self._onCallbackentryChangedId = 0
616 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
617 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
618 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
619 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
620 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
621 self._onNotifyToggled = 0
622 self._onMinutesChanged = 0
623 self._onMissedToggled = 0
624 self._onVoicemailToggled = 0
625 self._onSmsToggled = 0
626 self._applyAlarmTimeoutId = None
628 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
629 self._defaultCallback = ""
632 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
634 self._accountViewNumberDisplay.set_use_markup(True)
635 self.set_account_number("")
637 self._callbackList.clear()
638 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
640 if self._alarmHandler is not None:
641 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
642 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
643 self._missedCheckbox.set_active(self._notifyOnMissed)
644 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
645 self._smsCheckbox.set_active(self._notifyOnSms)
647 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
648 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_changed)
649 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
650 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
651 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
653 self._notifyCheckbox.set_sensitive(False)
654 self._minutesEntryButton.set_sensitive(False)
655 self._missedCheckbox.set_sensitive(False)
656 self._voicemailCheckbox.set_sensitive(False)
657 self._smsCheckbox.set_sensitive(False)
659 self.update(force=True)
662 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
663 self._onCallbackentryChangedId = 0
665 if self._alarmHandler is not None:
666 self._notifyCheckbox.disconnect(self._onNotifyToggled)
667 self._minutesEntryButton.disconnect(self._onMinutesChanged)
668 self._missedCheckbox.disconnect(self._onNotifyToggled)
669 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
670 self._smsCheckbox.disconnect(self._onNotifyToggled)
671 self._onNotifyToggled = 0
672 self._onMinutesChanged = 0
673 self._onMissedToggled = 0
674 self._onVoicemailToggled = 0
675 self._onSmsToggled = 0
677 self._notifyCheckbox.set_sensitive(True)
678 self._minutesEntryButton.set_sensitive(True)
679 self._missedCheckbox.set_sensitive(True)
680 self._voicemailCheckbox.set_sensitive(True)
681 self._smsCheckbox.set_sensitive(True)
684 self._callbackList.clear()
686 def get_selected_callback_number(self):
687 return make_ugly(self._callbackCombo.get_child().get_text())
689 def set_account_number(self, number):
691 Displays current account number
693 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
695 def update(self, force = False):
696 if not force and self._isPopulated:
698 self._populate_callback_combo()
699 self.set_account_number(self._backend.get_account_number())
703 self._callbackCombo.get_child().set_text("")
704 self.set_account_number("")
705 self._isPopulated = False
707 def save_everything(self):
708 raise NotImplementedError
712 return "Account Info"
714 def load_settings(self, config, section):
715 self._defaultCallback = config.get(section, "callback")
716 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
717 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
718 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
720 def save_settings(self, config, section):
722 @note Thread Agnostic
724 callback = self.get_selected_callback_number()
725 config.set(section, "callback", callback)
726 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
727 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
728 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
730 def _populate_callback_combo(self):
731 self._isPopulated = True
732 self._callbackList.clear()
734 callbackNumbers = self._backend.get_callback_numbers()
736 self._errorDisplay.push_exception()
737 self._isPopulated = False
740 for number, description in callbackNumbers.iteritems():
741 self._callbackList.append((make_pretty(number),))
743 self._callbackCombo.set_model(self._callbackList)
744 self._callbackCombo.set_text_column(0)
745 #callbackNumber = self._backend.get_callback_number()
746 callbackNumber = self._defaultCallback
747 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
749 def _set_callback_number(self, number):
751 if not self._backend.is_valid_syntax(number):
752 self._errorDisplay.push_message("%s is not a valid callback number" % number)
753 elif number == self._backend.get_callback_number():
755 "Callback number already is %s" % (
756 self._backend.get_callback_number(),
760 self._backend.set_callback_number(number)
761 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
762 make_pretty(number), make_pretty(self._backend.get_callback_number())
765 "Callback number set to %s" % (
766 self._backend.get_callback_number(),
770 self._errorDisplay.push_exception()
772 def _update_alarm_settings(self, recurrence):
774 isEnabled = self._notifyCheckbox.get_active()
775 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
776 self._alarmHandler.apply_settings(isEnabled, recurrence)
778 self.save_everything()
779 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
780 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
782 def _on_callbackentry_changed(self, *args):
784 text = self.get_selected_callback_number()
785 number = make_ugly(text)
786 self._set_callback_number(number)
788 self._errorDisplay.push_exception()
790 def _on_notify_toggled(self, *args):
792 if self._applyAlarmTimeoutId is not None:
793 gobject.source_remove(self._applyAlarmTimeoutId)
794 self._applyAlarmTimeoutId = None
795 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
797 self._errorDisplay.push_exception()
799 def _on_minutes_changed(self, *args):
801 recurrence = hildonize.request_number(
802 self._window, "Minutes", (1, 50), self._alarmHandler.recurrence
804 self._update_alarm_settings(recurrence)
806 self._errorDisplay.push_exception()
808 def _on_apply_timeout(self, *args):
810 self._applyAlarmTimeoutId = None
812 self._update_alarm_settings(self._alarmHandler.recurrence)
814 self._errorDisplay.push_exception()
817 def _on_missed_toggled(self, *args):
819 self._notifyOnMissed = self._missedCheckbox.get_active()
820 self.save_everything()
822 self._errorDisplay.push_exception()
824 def _on_voicemail_toggled(self, *args):
826 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
827 self.save_everything()
829 self._errorDisplay.push_exception()
831 def _on_sms_toggled(self, *args):
833 self._notifyOnSms = self._smsCheckbox.get_active()
834 self.save_everything()
836 self._errorDisplay.push_exception()
839 class RecentCallsView(object):
846 def __init__(self, widgetTree, backend, errorDisplay):
847 self._errorDisplay = errorDisplay
848 self._backend = backend
850 self._isPopulated = False
851 self._recentmodel = gtk.ListStore(
852 gobject.TYPE_STRING, # number
853 gobject.TYPE_STRING, # date
854 gobject.TYPE_STRING, # action
855 gobject.TYPE_STRING, # from
857 self._recentview = widgetTree.get_widget("recentview")
858 self._recentviewselection = None
859 self._onRecentviewRowActivatedId = 0
861 textrenderer = gtk.CellRendererText()
862 textrenderer.set_property("yalign", 0)
863 self._dateColumn = gtk.TreeViewColumn("Date")
864 self._dateColumn.pack_start(textrenderer, expand=True)
865 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
867 textrenderer = gtk.CellRendererText()
868 textrenderer.set_property("yalign", 0)
869 self._actionColumn = gtk.TreeViewColumn("Action")
870 self._actionColumn.pack_start(textrenderer, expand=True)
871 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
873 textrenderer = gtk.CellRendererText()
874 textrenderer.set_property("yalign", 0)
875 hildonize.set_cell_thumb_selectable(textrenderer)
876 self._nameColumn = gtk.TreeViewColumn("From")
877 self._nameColumn.pack_start(textrenderer, expand=True)
878 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
879 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
881 textrenderer = gtk.CellRendererText()
882 textrenderer.set_property("yalign", 0)
883 self._numberColumn = gtk.TreeViewColumn("Number")
884 self._numberColumn.pack_start(textrenderer, expand=True)
885 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
887 self._window = gtk_toolbox.find_parent_window(self._recentview)
888 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
890 self._updateSink = gtk_toolbox.threaded_stage(
892 self._idly_populate_recentview,
893 gtk_toolbox.null_sink(),
898 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
899 self._recentview.set_model(self._recentmodel)
901 self._recentview.append_column(self._dateColumn)
902 self._recentview.append_column(self._actionColumn)
903 self._recentview.append_column(self._numberColumn)
904 self._recentview.append_column(self._nameColumn)
905 self._recentviewselection = self._recentview.get_selection()
906 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
908 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
911 self._recentview.disconnect(self._onRecentviewRowActivatedId)
915 self._recentview.remove_column(self._dateColumn)
916 self._recentview.remove_column(self._actionColumn)
917 self._recentview.remove_column(self._nameColumn)
918 self._recentview.remove_column(self._numberColumn)
919 self._recentview.set_model(None)
921 def number_selected(self, action, number, message):
923 @note Actual dial function is patched in later
925 raise NotImplementedError("Horrible unknown error has occurred")
927 def update(self, force = False):
928 if not force and self._isPopulated:
930 self._updateSink.send(())
934 self._isPopulated = False
935 self._recentmodel.clear()
939 return "Recent Calls"
941 def load_settings(self, config, section):
944 def save_settings(self, config, section):
946 @note Thread Agnostic
950 def _idly_populate_recentview(self):
952 self._recentmodel.clear()
953 self._isPopulated = True
956 recentItems = self._backend.get_recent()
958 self._errorDisplay.push_exception_with_lock()
959 self._isPopulated = False
962 for personName, phoneNumber, date, action in recentItems:
964 personName = "Unknown"
965 date = abbrev_relative_date(date)
966 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
967 prettyNumber = make_pretty(prettyNumber)
968 item = (prettyNumber, date, action.capitalize(), personName)
969 with gtk_toolbox.gtk_lock():
970 self._recentmodel.append(item)
972 self._errorDisplay.push_exception_with_lock()
976 def _on_recentview_row_activated(self, treeview, path, view_column):
978 model, itr = self._recentviewselection.get_selected()
982 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
983 number = make_ugly(number)
984 contactPhoneNumbers = [("Phone", number)]
985 description = self._recentmodel.get_value(itr, self.FROM_IDX)
987 action, phoneNumber, message = self._phoneTypeSelector.run(
989 message = description,
990 parent = self._window,
992 if action == PhoneTypeSelector.ACTION_CANCEL:
994 assert phoneNumber, "A lack of phone number exists"
996 self.number_selected(action, phoneNumber, message)
997 self._recentviewselection.unselect_all()
999 self._errorDisplay.push_exception()
1002 class MessagesView(object):
1009 def __init__(self, widgetTree, backend, errorDisplay):
1010 self._errorDisplay = errorDisplay
1011 self._backend = backend
1013 self._isPopulated = False
1014 self._messagemodel = gtk.ListStore(
1015 gobject.TYPE_STRING, # number
1016 gobject.TYPE_STRING, # date
1017 gobject.TYPE_STRING, # header
1018 gobject.TYPE_STRING, # message
1020 self._messageview = widgetTree.get_widget("messages_view")
1021 self._messageviewselection = None
1022 self._onMessageviewRowActivatedId = 0
1024 self._messageRenderer = gtk.CellRendererText()
1025 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1026 self._messageRenderer.set_property("wrap-width", 500)
1027 self._messageColumn = gtk.TreeViewColumn("Messages")
1028 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1029 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1030 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1032 self._window = gtk_toolbox.find_parent_window(self._messageview)
1033 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1035 self._updateSink = gtk_toolbox.threaded_stage(
1037 self._idly_populate_messageview,
1038 gtk_toolbox.null_sink(),
1043 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1044 self._messageview.set_model(self._messagemodel)
1046 self._messageview.append_column(self._messageColumn)
1047 self._messageviewselection = self._messageview.get_selection()
1048 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1050 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1053 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1057 self._messageview.remove_column(self._messageColumn)
1058 self._messageview.set_model(None)
1060 def number_selected(self, action, number, message):
1062 @note Actual dial function is patched in later
1064 raise NotImplementedError("Horrible unknown error has occurred")
1066 def update(self, force = False):
1067 if not force and self._isPopulated:
1069 self._updateSink.send(())
1073 self._isPopulated = False
1074 self._messagemodel.clear()
1080 def load_settings(self, config, section):
1083 def save_settings(self, config, section):
1085 @note Thread Agnostic
1089 def _idly_populate_messageview(self):
1091 self._messagemodel.clear()
1092 self._isPopulated = True
1095 messageItems = self._backend.get_messages()
1096 except Exception, e:
1097 self._errorDisplay.push_exception_with_lock()
1098 self._isPopulated = False
1101 for header, number, relativeDate, message in messageItems:
1102 prettyNumber = number[2:] if number.startswith("+1") else number
1103 prettyNumber = make_pretty(prettyNumber)
1104 message = "<b>%s - %s</b> <i>(%s)</i>\n\n%s" % (header, prettyNumber, relativeDate, message)
1105 number = make_ugly(number)
1106 row = (number, relativeDate, header, message)
1107 with gtk_toolbox.gtk_lock():
1108 self._messagemodel.append(row)
1109 except Exception, e:
1110 self._errorDisplay.push_exception_with_lock()
1114 def _on_messageview_row_activated(self, treeview, path, view_column):
1116 model, itr = self._messageviewselection.get_selected()
1120 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1121 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
1123 action, phoneNumber, message = self._phoneTypeSelector.run(
1124 contactPhoneNumbers,
1125 message = description,
1126 parent = self._window,
1128 if action == PhoneTypeSelector.ACTION_CANCEL:
1130 assert phoneNumber, "A lock of phone number exists"
1132 self.number_selected(action, phoneNumber, message)
1133 self._messageviewselection.unselect_all()
1134 except Exception, e:
1135 self._errorDisplay.push_exception()
1138 class ContactsView(object):
1140 def __init__(self, widgetTree, backend, errorDisplay):
1141 self._errorDisplay = errorDisplay
1142 self._backend = backend
1144 self._addressBook = None
1145 self._selectedComboIndex = 0
1146 self._addressBookFactories = [null_backend.NullAddressBook()]
1148 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1149 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
1151 self._isPopulated = False
1152 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1153 self._contactsviewselection = None
1154 self._contactsview = widgetTree.get_widget("contactsview")
1156 self._contactColumn = gtk.TreeViewColumn("Contact")
1157 displayContactSource = False
1158 if displayContactSource:
1159 textrenderer = gtk.CellRendererText()
1160 self._contactColumn.pack_start(textrenderer, expand=False)
1161 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1162 textrenderer = gtk.CellRendererText()
1163 hildonize.set_cell_thumb_selectable(textrenderer)
1164 self._contactColumn.pack_start(textrenderer, expand=True)
1165 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1166 textrenderer = gtk.CellRendererText()
1167 self._contactColumn.pack_start(textrenderer, expand=True)
1168 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1169 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1170 self._contactColumn.set_sort_column_id(1)
1171 self._contactColumn.set_visible(True)
1173 self._onContactsviewRowActivatedId = 0
1174 self._onAddressbookComboChangedId = 0
1175 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1176 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1178 self._updateSink = gtk_toolbox.threaded_stage(
1180 self._idly_populate_contactsview,
1181 gtk_toolbox.null_sink(),
1186 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1188 self._contactsview.set_model(self._contactsmodel)
1189 self._contactsview.append_column(self._contactColumn)
1190 self._contactsviewselection = self._contactsview.get_selection()
1191 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1193 self._booksList.clear()
1194 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1195 if factoryName and bookName:
1196 entryName = "%s: %s" % (factoryName, bookName)
1198 entryName = factoryName
1200 entryName = bookName
1202 entryName = "Bad name (%d)" % factoryId
1203 row = (str(factoryId), bookId, entryName)
1204 self._booksList.append(row)
1206 self._booksSelectionBox.set_model(self._booksList)
1207 cell = gtk.CellRendererText()
1208 self._booksSelectionBox.pack_start(cell, True)
1209 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1211 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1212 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1214 if len(self._booksList) <= self._selectedComboIndex:
1215 self._selectedComboIndex = 0
1216 self._booksSelectionBox.set_active(self._selectedComboIndex)
1219 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1220 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1224 self._booksSelectionBox.clear()
1225 self._booksSelectionBox.set_model(None)
1226 self._contactsview.set_model(None)
1227 self._contactsview.remove_column(self._contactColumn)
1229 def number_selected(self, action, number, message):
1231 @note Actual dial function is patched in later
1233 raise NotImplementedError("Horrible unknown error has occurred")
1235 def get_addressbooks(self):
1237 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1239 for i, factory in enumerate(self._addressBookFactories):
1240 for bookFactory, bookId, bookName in factory.get_addressbooks():
1241 yield (str(i), bookId), (factory.factory_name(), bookName)
1243 def open_addressbook(self, bookFactoryId, bookId):
1244 bookFactoryIndex = int(bookFactoryId)
1245 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1247 forceUpdate = True if addressBook is not self._addressBook else False
1249 self._addressBook = addressBook
1250 self.update(force=forceUpdate)
1252 def update(self, force = False):
1253 if not force and self._isPopulated:
1255 self._updateSink.send(())
1259 self._isPopulated = False
1260 self._contactsmodel.clear()
1261 for factory in self._addressBookFactories:
1262 factory.clear_caches()
1263 self._addressBook.clear_caches()
1265 def append(self, book):
1266 self._addressBookFactories.append(book)
1268 def extend(self, books):
1269 self._addressBookFactories.extend(books)
1275 def load_settings(self, config, sectionName):
1277 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1278 except ConfigParser.NoOptionError:
1279 self._selectedComboIndex = 0
1281 def save_settings(self, config, sectionName):
1282 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1284 def _idly_populate_contactsview(self):
1287 while addressBook is not self._addressBook:
1288 addressBook = self._addressBook
1289 with gtk_toolbox.gtk_lock():
1290 self._contactsview.set_model(None)
1294 contacts = addressBook.get_contacts()
1295 except Exception, e:
1297 self._isPopulated = False
1298 self._errorDisplay.push_exception_with_lock()
1299 for contactId, contactName in contacts:
1300 contactType = (addressBook.contact_source_short_name(contactId), )
1301 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1303 with gtk_toolbox.gtk_lock():
1304 self._contactsview.set_model(self._contactsmodel)
1306 self._isPopulated = True
1307 except Exception, e:
1308 self._errorDisplay.push_exception_with_lock()
1311 def _on_addressbook_combo_changed(self, *args, **kwds):
1313 itr = self._booksSelectionBox.get_active_iter()
1316 self._selectedComboIndex = self._booksSelectionBox.get_active()
1317 selectedFactoryId = self._booksList.get_value(itr, 0)
1318 selectedBookId = self._booksList.get_value(itr, 1)
1319 self.open_addressbook(selectedFactoryId, selectedBookId)
1320 except Exception, e:
1321 self._errorDisplay.push_exception()
1323 def _on_contactsview_row_activated(self, treeview, path, view_column):
1325 model, itr = self._contactsviewselection.get_selected()
1329 contactId = self._contactsmodel.get_value(itr, 3)
1330 contactName = self._contactsmodel.get_value(itr, 1)
1332 contactDetails = self._addressBook.get_contact_details(contactId)
1333 except Exception, e:
1335 self._errorDisplay.push_exception()
1336 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1338 if len(contactPhoneNumbers) == 0:
1341 action, phoneNumber, message = self._phoneTypeSelector.run(
1342 contactPhoneNumbers,
1343 message = contactName,
1344 parent = self._window,
1346 if action == PhoneTypeSelector.ACTION_CANCEL:
1348 assert phoneNumber, "A lack of phone number exists"
1350 self.number_selected(action, phoneNumber, message)
1351 self._contactsviewselection.unselect_all()
1352 except Exception, e:
1353 self._errorDisplay.push_exception()