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._dialButton = widgetTree.get_widget("dial")
476 self._backButton = widgetTree.get_widget("back")
477 self._phonenumber = ""
478 self._prettynumber = ""
481 "on_dial_clicked": self._on_dial_clicked,
482 "on_sms_clicked": self._on_sms_clicked,
483 "on_digit_clicked": self._on_digit_clicked,
484 "on_clear_number": self._on_clear_number,
486 widgetTree.signal_autoconnect(callbackMapping)
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_clear_number(self, *args):
570 self._errorDisplay.push_exception()
572 def _on_digit_clicked(self, widget):
574 self.set_number(self._phonenumber + widget.get_name()[-1])
576 self._errorDisplay.push_exception()
578 def _on_backspace(self, taps):
580 self.set_number(self._phonenumber[:-taps])
581 self._reset_back_button()
583 self._errorDisplay.push_exception()
585 def _on_clearall(self, taps):
588 self._reset_back_button()
590 self._errorDisplay.push_exception()
593 def _set_clear_button(self):
595 self._backButton.set_label("gtk-clear")
597 self._errorDisplay.push_exception()
599 def _reset_back_button(self):
601 self._backButton.set_label(self._originalLabel)
603 self._errorDisplay.push_exception()
606 class AccountInfo(object):
608 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
609 self._errorDisplay = errorDisplay
610 self._backend = backend
611 self._isPopulated = False
612 self._alarmHandler = alarmHandler
613 self._notifyOnMissed = False
614 self._notifyOnVoicemail = False
615 self._notifyOnSms = False
617 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
618 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
619 self._callbackCombo = widgetTree.get_widget("callbackcombo")
620 self._onCallbackentryChangedId = 0
622 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
623 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
624 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
625 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
626 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
627 self._onNotifyToggled = 0
628 self._onMinutesChanged = 0
629 self._onMissedToggled = 0
630 self._onVoicemailToggled = 0
631 self._onSmsToggled = 0
632 self._applyAlarmTimeoutId = None
634 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
635 self._defaultCallback = ""
638 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
640 self._accountViewNumberDisplay.set_use_markup(True)
641 self.set_account_number("")
643 self._callbackList.clear()
644 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
646 if self._alarmHandler is not None:
647 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
648 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
649 self._missedCheckbox.set_active(self._notifyOnMissed)
650 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
651 self._smsCheckbox.set_active(self._notifyOnSms)
653 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
654 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_changed)
655 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
656 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
657 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
659 self._notifyCheckbox.set_sensitive(False)
660 self._minutesEntryButton.set_sensitive(False)
661 self._missedCheckbox.set_sensitive(False)
662 self._voicemailCheckbox.set_sensitive(False)
663 self._smsCheckbox.set_sensitive(False)
665 self.update(force=True)
668 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
669 self._onCallbackentryChangedId = 0
671 if self._alarmHandler is not None:
672 self._notifyCheckbox.disconnect(self._onNotifyToggled)
673 self._minutesEntryButton.disconnect(self._onMinutesChanged)
674 self._missedCheckbox.disconnect(self._onNotifyToggled)
675 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
676 self._smsCheckbox.disconnect(self._onNotifyToggled)
677 self._onNotifyToggled = 0
678 self._onMinutesChanged = 0
679 self._onMissedToggled = 0
680 self._onVoicemailToggled = 0
681 self._onSmsToggled = 0
683 self._notifyCheckbox.set_sensitive(True)
684 self._minutesEntryButton.set_sensitive(True)
685 self._missedCheckbox.set_sensitive(True)
686 self._voicemailCheckbox.set_sensitive(True)
687 self._smsCheckbox.set_sensitive(True)
690 self._callbackList.clear()
692 def get_selected_callback_number(self):
693 return make_ugly(self._callbackCombo.get_child().get_text())
695 def set_account_number(self, number):
697 Displays current account number
699 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
701 def update(self, force = False):
702 if not force and self._isPopulated:
704 self._populate_callback_combo()
705 self.set_account_number(self._backend.get_account_number())
709 self._callbackCombo.get_child().set_text("")
710 self.set_account_number("")
711 self._isPopulated = False
713 def save_everything(self):
714 raise NotImplementedError
718 return "Account Info"
720 def load_settings(self, config, section):
721 self._defaultCallback = config.get(section, "callback")
722 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
723 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
724 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
726 def save_settings(self, config, section):
728 @note Thread Agnostic
730 callback = self.get_selected_callback_number()
731 config.set(section, "callback", callback)
732 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
733 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
734 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
736 def _populate_callback_combo(self):
737 self._isPopulated = True
738 self._callbackList.clear()
740 callbackNumbers = self._backend.get_callback_numbers()
742 self._errorDisplay.push_exception()
743 self._isPopulated = False
746 for number, description in callbackNumbers.iteritems():
747 self._callbackList.append((make_pretty(number),))
749 self._callbackCombo.set_model(self._callbackList)
750 self._callbackCombo.set_text_column(0)
751 #callbackNumber = self._backend.get_callback_number()
752 callbackNumber = self._defaultCallback
753 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
755 def _set_callback_number(self, number):
757 if not self._backend.is_valid_syntax(number):
758 self._errorDisplay.push_message("%s is not a valid callback number" % number)
759 elif number == self._backend.get_callback_number():
761 "Callback number already is %s" % (
762 self._backend.get_callback_number(),
766 self._backend.set_callback_number(number)
767 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
768 make_pretty(number), make_pretty(self._backend.get_callback_number())
771 "Callback number set to %s" % (
772 self._backend.get_callback_number(),
776 self._errorDisplay.push_exception()
778 def _update_alarm_settings(self, recurrence):
780 isEnabled = self._notifyCheckbox.get_active()
781 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
782 self._alarmHandler.apply_settings(isEnabled, recurrence)
784 self.save_everything()
785 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
786 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
788 def _on_callbackentry_changed(self, *args):
790 text = self.get_selected_callback_number()
791 number = make_ugly(text)
792 self._set_callback_number(number)
794 self._errorDisplay.push_exception()
796 def _on_notify_toggled(self, *args):
798 if self._applyAlarmTimeoutId is not None:
799 gobject.source_remove(self._applyAlarmTimeoutId)
800 self._applyAlarmTimeoutId = None
801 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
803 self._errorDisplay.push_exception()
805 def _on_minutes_changed(self, *args):
807 recurrence = hildonize.request_number(
808 self._window, "Minutes", (1, 50), self._alarmHandler.recurrence
810 self._update_alarm_settings(recurrence)
812 self._errorDisplay.push_exception()
814 def _on_apply_timeout(self, *args):
816 self._applyAlarmTimeoutId = None
818 self._update_alarm_settings(self._alarmHandler.recurrence)
820 self._errorDisplay.push_exception()
823 def _on_missed_toggled(self, *args):
825 self._notifyOnMissed = self._missedCheckbox.get_active()
826 self.save_everything()
828 self._errorDisplay.push_exception()
830 def _on_voicemail_toggled(self, *args):
832 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
833 self.save_everything()
835 self._errorDisplay.push_exception()
837 def _on_sms_toggled(self, *args):
839 self._notifyOnSms = self._smsCheckbox.get_active()
840 self.save_everything()
842 self._errorDisplay.push_exception()
845 class RecentCallsView(object):
852 def __init__(self, widgetTree, backend, errorDisplay):
853 self._errorDisplay = errorDisplay
854 self._backend = backend
856 self._isPopulated = False
857 self._recentmodel = gtk.ListStore(
858 gobject.TYPE_STRING, # number
859 gobject.TYPE_STRING, # date
860 gobject.TYPE_STRING, # action
861 gobject.TYPE_STRING, # from
863 self._recentview = widgetTree.get_widget("recentview")
864 self._recentviewselection = None
865 self._onRecentviewRowActivatedId = 0
867 textrenderer = gtk.CellRendererText()
868 textrenderer.set_property("yalign", 0)
869 self._dateColumn = gtk.TreeViewColumn("Date")
870 self._dateColumn.pack_start(textrenderer, expand=True)
871 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
873 textrenderer = gtk.CellRendererText()
874 textrenderer.set_property("yalign", 0)
875 self._actionColumn = gtk.TreeViewColumn("Action")
876 self._actionColumn.pack_start(textrenderer, expand=True)
877 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
879 textrenderer = gtk.CellRendererText()
880 textrenderer.set_property("yalign", 0)
881 hildonize.set_cell_thumb_selectable(textrenderer)
882 self._nameColumn = gtk.TreeViewColumn("From")
883 self._nameColumn.pack_start(textrenderer, expand=True)
884 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
885 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
887 textrenderer = gtk.CellRendererText()
888 textrenderer.set_property("yalign", 0)
889 self._numberColumn = gtk.TreeViewColumn("Number")
890 self._numberColumn.pack_start(textrenderer, expand=True)
891 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
893 self._window = gtk_toolbox.find_parent_window(self._recentview)
894 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
896 self._updateSink = gtk_toolbox.threaded_stage(
898 self._idly_populate_recentview,
899 gtk_toolbox.null_sink(),
904 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
905 self._recentview.set_model(self._recentmodel)
907 self._recentview.append_column(self._dateColumn)
908 self._recentview.append_column(self._actionColumn)
909 self._recentview.append_column(self._numberColumn)
910 self._recentview.append_column(self._nameColumn)
911 self._recentviewselection = self._recentview.get_selection()
912 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
914 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
917 self._recentview.disconnect(self._onRecentviewRowActivatedId)
921 self._recentview.remove_column(self._dateColumn)
922 self._recentview.remove_column(self._actionColumn)
923 self._recentview.remove_column(self._nameColumn)
924 self._recentview.remove_column(self._numberColumn)
925 self._recentview.set_model(None)
927 def number_selected(self, action, number, message):
929 @note Actual dial function is patched in later
931 raise NotImplementedError("Horrible unknown error has occurred")
933 def update(self, force = False):
934 if not force and self._isPopulated:
936 self._updateSink.send(())
940 self._isPopulated = False
941 self._recentmodel.clear()
945 return "Recent Calls"
947 def load_settings(self, config, section):
950 def save_settings(self, config, section):
952 @note Thread Agnostic
956 def _idly_populate_recentview(self):
958 self._recentmodel.clear()
959 self._isPopulated = True
962 recentItems = self._backend.get_recent()
964 self._errorDisplay.push_exception_with_lock()
965 self._isPopulated = False
968 for personName, phoneNumber, date, action in recentItems:
970 personName = "Unknown"
971 date = abbrev_relative_date(date)
972 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
973 prettyNumber = make_pretty(prettyNumber)
974 item = (prettyNumber, date, action.capitalize(), personName)
975 with gtk_toolbox.gtk_lock():
976 self._recentmodel.append(item)
978 self._errorDisplay.push_exception_with_lock()
982 def _on_recentview_row_activated(self, treeview, path, view_column):
984 model, itr = self._recentviewselection.get_selected()
988 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
989 number = make_ugly(number)
990 contactPhoneNumbers = [("Phone", number)]
991 description = self._recentmodel.get_value(itr, self.FROM_IDX)
993 action, phoneNumber, message = self._phoneTypeSelector.run(
995 message = description,
996 parent = self._window,
998 if action == PhoneTypeSelector.ACTION_CANCEL:
1000 assert phoneNumber, "A lack of phone number exists"
1002 self.number_selected(action, phoneNumber, message)
1003 self._recentviewselection.unselect_all()
1004 except Exception, e:
1005 self._errorDisplay.push_exception()
1008 class MessagesView(object):
1015 def __init__(self, widgetTree, backend, errorDisplay):
1016 self._errorDisplay = errorDisplay
1017 self._backend = backend
1019 self._isPopulated = False
1020 self._messagemodel = gtk.ListStore(
1021 gobject.TYPE_STRING, # number
1022 gobject.TYPE_STRING, # date
1023 gobject.TYPE_STRING, # header
1024 gobject.TYPE_STRING, # message
1026 self._messageview = widgetTree.get_widget("messages_view")
1027 self._messageviewselection = None
1028 self._onMessageviewRowActivatedId = 0
1030 self._messageRenderer = gtk.CellRendererText()
1031 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1032 self._messageRenderer.set_property("wrap-width", 500)
1033 self._messageColumn = gtk.TreeViewColumn("Messages")
1034 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1035 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1036 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1038 self._window = gtk_toolbox.find_parent_window(self._messageview)
1039 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1041 self._updateSink = gtk_toolbox.threaded_stage(
1043 self._idly_populate_messageview,
1044 gtk_toolbox.null_sink(),
1049 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1050 self._messageview.set_model(self._messagemodel)
1052 self._messageview.append_column(self._messageColumn)
1053 self._messageviewselection = self._messageview.get_selection()
1054 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1056 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1059 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1063 self._messageview.remove_column(self._messageColumn)
1064 self._messageview.set_model(None)
1066 def number_selected(self, action, number, message):
1068 @note Actual dial function is patched in later
1070 raise NotImplementedError("Horrible unknown error has occurred")
1072 def update(self, force = False):
1073 if not force and self._isPopulated:
1075 self._updateSink.send(())
1079 self._isPopulated = False
1080 self._messagemodel.clear()
1086 def load_settings(self, config, section):
1089 def save_settings(self, config, section):
1091 @note Thread Agnostic
1095 def _idly_populate_messageview(self):
1097 self._messagemodel.clear()
1098 self._isPopulated = True
1101 messageItems = self._backend.get_messages()
1102 except Exception, e:
1103 self._errorDisplay.push_exception_with_lock()
1104 self._isPopulated = False
1107 for header, number, relativeDate, message in messageItems:
1108 prettyNumber = number[2:] if number.startswith("+1") else number
1109 prettyNumber = make_pretty(prettyNumber)
1110 message = "<b>%s - %s</b> <i>(%s)</i>\n\n%s" % (header, prettyNumber, relativeDate, message)
1111 number = make_ugly(number)
1112 row = (number, relativeDate, header, message)
1113 with gtk_toolbox.gtk_lock():
1114 self._messagemodel.append(row)
1115 except Exception, e:
1116 self._errorDisplay.push_exception_with_lock()
1120 def _on_messageview_row_activated(self, treeview, path, view_column):
1122 model, itr = self._messageviewselection.get_selected()
1126 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1127 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
1129 action, phoneNumber, message = self._phoneTypeSelector.run(
1130 contactPhoneNumbers,
1131 message = description,
1132 parent = self._window,
1134 if action == PhoneTypeSelector.ACTION_CANCEL:
1136 assert phoneNumber, "A lock of phone number exists"
1138 self.number_selected(action, phoneNumber, message)
1139 self._messageviewselection.unselect_all()
1140 except Exception, e:
1141 self._errorDisplay.push_exception()
1144 class ContactsView(object):
1146 def __init__(self, widgetTree, backend, errorDisplay):
1147 self._errorDisplay = errorDisplay
1148 self._backend = backend
1150 self._addressBook = None
1151 self._selectedComboIndex = 0
1152 self._addressBookFactories = [null_backend.NullAddressBook()]
1154 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1155 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
1157 self._isPopulated = False
1158 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1159 self._contactsviewselection = None
1160 self._contactsview = widgetTree.get_widget("contactsview")
1162 self._contactColumn = gtk.TreeViewColumn("Contact")
1163 displayContactSource = False
1164 if displayContactSource:
1165 textrenderer = gtk.CellRendererText()
1166 self._contactColumn.pack_start(textrenderer, expand=False)
1167 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1168 textrenderer = gtk.CellRendererText()
1169 hildonize.set_cell_thumb_selectable(textrenderer)
1170 self._contactColumn.pack_start(textrenderer, expand=True)
1171 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1172 textrenderer = gtk.CellRendererText()
1173 self._contactColumn.pack_start(textrenderer, expand=True)
1174 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1175 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1176 self._contactColumn.set_sort_column_id(1)
1177 self._contactColumn.set_visible(True)
1179 self._onContactsviewRowActivatedId = 0
1180 self._onAddressbookComboChangedId = 0
1181 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1182 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1184 self._updateSink = gtk_toolbox.threaded_stage(
1186 self._idly_populate_contactsview,
1187 gtk_toolbox.null_sink(),
1192 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1194 self._contactsview.set_model(self._contactsmodel)
1195 self._contactsview.append_column(self._contactColumn)
1196 self._contactsviewselection = self._contactsview.get_selection()
1197 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1199 self._booksList.clear()
1200 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1201 if factoryName and bookName:
1202 entryName = "%s: %s" % (factoryName, bookName)
1204 entryName = factoryName
1206 entryName = bookName
1208 entryName = "Bad name (%d)" % factoryId
1209 row = (str(factoryId), bookId, entryName)
1210 self._booksList.append(row)
1212 self._booksSelectionBox.set_model(self._booksList)
1213 cell = gtk.CellRendererText()
1214 self._booksSelectionBox.pack_start(cell, True)
1215 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1217 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1218 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1220 if len(self._booksList) <= self._selectedComboIndex:
1221 self._selectedComboIndex = 0
1222 self._booksSelectionBox.set_active(self._selectedComboIndex)
1225 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1226 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1230 self._booksSelectionBox.clear()
1231 self._booksSelectionBox.set_model(None)
1232 self._contactsview.set_model(None)
1233 self._contactsview.remove_column(self._contactColumn)
1235 def number_selected(self, action, number, message):
1237 @note Actual dial function is patched in later
1239 raise NotImplementedError("Horrible unknown error has occurred")
1241 def get_addressbooks(self):
1243 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1245 for i, factory in enumerate(self._addressBookFactories):
1246 for bookFactory, bookId, bookName in factory.get_addressbooks():
1247 yield (str(i), bookId), (factory.factory_name(), bookName)
1249 def open_addressbook(self, bookFactoryId, bookId):
1250 bookFactoryIndex = int(bookFactoryId)
1251 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1253 forceUpdate = True if addressBook is not self._addressBook else False
1255 self._addressBook = addressBook
1256 self.update(force=forceUpdate)
1258 def update(self, force = False):
1259 if not force and self._isPopulated:
1261 self._updateSink.send(())
1265 self._isPopulated = False
1266 self._contactsmodel.clear()
1267 for factory in self._addressBookFactories:
1268 factory.clear_caches()
1269 self._addressBook.clear_caches()
1271 def append(self, book):
1272 self._addressBookFactories.append(book)
1274 def extend(self, books):
1275 self._addressBookFactories.extend(books)
1281 def load_settings(self, config, sectionName):
1283 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1284 except ConfigParser.NoOptionError:
1285 self._selectedComboIndex = 0
1287 def save_settings(self, config, sectionName):
1288 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1290 def _idly_populate_contactsview(self):
1293 while addressBook is not self._addressBook:
1294 addressBook = self._addressBook
1295 with gtk_toolbox.gtk_lock():
1296 self._contactsview.set_model(None)
1300 contacts = addressBook.get_contacts()
1301 except Exception, e:
1303 self._isPopulated = False
1304 self._errorDisplay.push_exception_with_lock()
1305 for contactId, contactName in contacts:
1306 contactType = (addressBook.contact_source_short_name(contactId), )
1307 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1309 with gtk_toolbox.gtk_lock():
1310 self._contactsview.set_model(self._contactsmodel)
1312 self._isPopulated = True
1313 except Exception, e:
1314 self._errorDisplay.push_exception_with_lock()
1317 def _on_addressbook_combo_changed(self, *args, **kwds):
1319 itr = self._booksSelectionBox.get_active_iter()
1322 self._selectedComboIndex = self._booksSelectionBox.get_active()
1323 selectedFactoryId = self._booksList.get_value(itr, 0)
1324 selectedBookId = self._booksList.get_value(itr, 1)
1325 self.open_addressbook(selectedFactoryId, selectedBookId)
1326 except Exception, e:
1327 self._errorDisplay.push_exception()
1329 def _on_contactsview_row_activated(self, treeview, path, view_column):
1331 model, itr = self._contactsviewselection.get_selected()
1335 contactId = self._contactsmodel.get_value(itr, 3)
1336 contactName = self._contactsmodel.get_value(itr, 1)
1338 contactDetails = self._addressBook.get_contact_details(contactId)
1339 except Exception, e:
1341 self._errorDisplay.push_exception()
1342 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1344 if len(contactPhoneNumbers) == 0:
1347 action, phoneNumber, message = self._phoneTypeSelector.run(
1348 contactPhoneNumbers,
1349 message = contactName,
1350 parent = self._window,
1352 if action == PhoneTypeSelector.ACTION_CANCEL:
1354 assert phoneNumber, "A lack of phone number exists"
1356 self.number_selected(action, phoneNumber, message)
1357 self._contactsviewselection.unselect_all()
1358 except Exception, e:
1359 self._errorDisplay.push_exception()