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 hildonize.set_cell_thumb_selectable(textrenderer)
890 self._numberColumn = gtk.TreeViewColumn("Number")
891 self._numberColumn.pack_start(textrenderer, expand=True)
892 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
894 self._window = gtk_toolbox.find_parent_window(self._recentview)
895 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
897 self._updateSink = gtk_toolbox.threaded_stage(
899 self._idly_populate_recentview,
900 gtk_toolbox.null_sink(),
905 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
906 self._recentview.set_model(self._recentmodel)
908 self._recentview.append_column(self._dateColumn)
909 self._recentview.append_column(self._actionColumn)
910 self._recentview.append_column(self._numberColumn)
911 self._recentview.append_column(self._nameColumn)
912 self._recentviewselection = self._recentview.get_selection()
913 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
915 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
918 self._recentview.disconnect(self._onRecentviewRowActivatedId)
922 self._recentview.remove_column(self._dateColumn)
923 self._recentview.remove_column(self._actionColumn)
924 self._recentview.remove_column(self._nameColumn)
925 self._recentview.remove_column(self._numberColumn)
926 self._recentview.set_model(None)
928 def number_selected(self, action, number, message):
930 @note Actual dial function is patched in later
932 raise NotImplementedError("Horrible unknown error has occurred")
934 def update(self, force = False):
935 if not force and self._isPopulated:
937 self._updateSink.send(())
941 self._isPopulated = False
942 self._recentmodel.clear()
946 return "Recent Calls"
948 def load_settings(self, config, section):
951 def save_settings(self, config, section):
953 @note Thread Agnostic
957 def _idly_populate_recentview(self):
959 self._recentmodel.clear()
960 self._isPopulated = True
963 recentItems = self._backend.get_recent()
965 self._errorDisplay.push_exception_with_lock()
966 self._isPopulated = False
969 for personName, phoneNumber, date, action in recentItems:
971 personName = "Unknown"
972 date = abbrev_relative_date(date)
973 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
974 prettyNumber = make_pretty(prettyNumber)
975 item = (prettyNumber, date, action.capitalize(), personName)
976 with gtk_toolbox.gtk_lock():
977 self._recentmodel.append(item)
979 self._errorDisplay.push_exception_with_lock()
983 def _on_recentview_row_activated(self, treeview, path, view_column):
985 model, itr = self._recentviewselection.get_selected()
989 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
990 number = make_ugly(number)
991 contactPhoneNumbers = [("Phone", number)]
992 description = self._recentmodel.get_value(itr, self.FROM_IDX)
994 action, phoneNumber, message = self._phoneTypeSelector.run(
996 message = description,
997 parent = self._window,
999 if action == PhoneTypeSelector.ACTION_CANCEL:
1001 assert phoneNumber, "A lack of phone number exists"
1003 self.number_selected(action, phoneNumber, message)
1004 self._recentviewselection.unselect_all()
1005 except Exception, e:
1006 self._errorDisplay.push_exception()
1009 class MessagesView(object):
1016 def __init__(self, widgetTree, backend, errorDisplay):
1017 self._errorDisplay = errorDisplay
1018 self._backend = backend
1020 self._isPopulated = False
1021 self._messagemodel = gtk.ListStore(
1022 gobject.TYPE_STRING, # number
1023 gobject.TYPE_STRING, # date
1024 gobject.TYPE_STRING, # header
1025 gobject.TYPE_STRING, # message
1027 self._messageview = widgetTree.get_widget("messages_view")
1028 self._messageviewselection = None
1029 self._onMessageviewRowActivatedId = 0
1031 self._messageRenderer = gtk.CellRendererText()
1032 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1033 self._messageRenderer.set_property("wrap-width", 500)
1034 self._messageColumn = gtk.TreeViewColumn("Messages")
1035 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1036 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1037 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1039 self._window = gtk_toolbox.find_parent_window(self._messageview)
1040 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1042 self._updateSink = gtk_toolbox.threaded_stage(
1044 self._idly_populate_messageview,
1045 gtk_toolbox.null_sink(),
1050 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1051 self._messageview.set_model(self._messagemodel)
1053 self._messageview.append_column(self._messageColumn)
1054 self._messageviewselection = self._messageview.get_selection()
1055 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1057 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1060 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1064 self._messageview.remove_column(self._messageColumn)
1065 self._messageview.set_model(None)
1067 def number_selected(self, action, number, message):
1069 @note Actual dial function is patched in later
1071 raise NotImplementedError("Horrible unknown error has occurred")
1073 def update(self, force = False):
1074 if not force and self._isPopulated:
1076 self._updateSink.send(())
1080 self._isPopulated = False
1081 self._messagemodel.clear()
1087 def load_settings(self, config, section):
1090 def save_settings(self, config, section):
1092 @note Thread Agnostic
1096 def _idly_populate_messageview(self):
1098 self._messagemodel.clear()
1099 self._isPopulated = True
1102 messageItems = self._backend.get_messages()
1103 except Exception, e:
1104 self._errorDisplay.push_exception_with_lock()
1105 self._isPopulated = False
1108 for header, number, relativeDate, message in messageItems:
1109 prettyNumber = number[2:] if number.startswith("+1") else number
1110 prettyNumber = make_pretty(prettyNumber)
1111 message = "<b>%s - %s</b> <i>(%s)</i>\n\n%s" % (header, prettyNumber, relativeDate, message)
1112 number = make_ugly(number)
1113 row = (number, relativeDate, header, message)
1114 with gtk_toolbox.gtk_lock():
1115 self._messagemodel.append(row)
1116 except Exception, e:
1117 self._errorDisplay.push_exception_with_lock()
1121 def _on_messageview_row_activated(self, treeview, path, view_column):
1123 model, itr = self._messageviewselection.get_selected()
1127 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1128 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
1130 action, phoneNumber, message = self._phoneTypeSelector.run(
1131 contactPhoneNumbers,
1132 message = description,
1133 parent = self._window,
1135 if action == PhoneTypeSelector.ACTION_CANCEL:
1137 assert phoneNumber, "A lock of phone number exists"
1139 self.number_selected(action, phoneNumber, message)
1140 self._messageviewselection.unselect_all()
1141 except Exception, e:
1142 self._errorDisplay.push_exception()
1145 class ContactsView(object):
1147 def __init__(self, widgetTree, backend, errorDisplay):
1148 self._errorDisplay = errorDisplay
1149 self._backend = backend
1151 self._addressBook = None
1152 self._selectedComboIndex = 0
1153 self._addressBookFactories = [null_backend.NullAddressBook()]
1155 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1156 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
1158 self._isPopulated = False
1159 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1160 self._contactsviewselection = None
1161 self._contactsview = widgetTree.get_widget("contactsview")
1163 self._contactColumn = gtk.TreeViewColumn("Contact")
1164 displayContactSource = False
1165 if displayContactSource:
1166 textrenderer = gtk.CellRendererText()
1167 self._contactColumn.pack_start(textrenderer, expand=False)
1168 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1169 textrenderer = gtk.CellRendererText()
1170 hildonize.set_cell_thumb_selectable(textrenderer)
1171 self._contactColumn.pack_start(textrenderer, expand=True)
1172 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1173 textrenderer = gtk.CellRendererText()
1174 self._contactColumn.pack_start(textrenderer, expand=True)
1175 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1176 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1177 self._contactColumn.set_sort_column_id(1)
1178 self._contactColumn.set_visible(True)
1180 self._onContactsviewRowActivatedId = 0
1181 self._onAddressbookComboChangedId = 0
1182 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1183 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1185 self._updateSink = gtk_toolbox.threaded_stage(
1187 self._idly_populate_contactsview,
1188 gtk_toolbox.null_sink(),
1193 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1195 self._contactsview.set_model(self._contactsmodel)
1196 self._contactsview.append_column(self._contactColumn)
1197 self._contactsviewselection = self._contactsview.get_selection()
1198 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1200 self._booksList.clear()
1201 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1202 if factoryName and bookName:
1203 entryName = "%s: %s" % (factoryName, bookName)
1205 entryName = factoryName
1207 entryName = bookName
1209 entryName = "Bad name (%d)" % factoryId
1210 row = (str(factoryId), bookId, entryName)
1211 self._booksList.append(row)
1213 self._booksSelectionBox.set_model(self._booksList)
1214 cell = gtk.CellRendererText()
1215 self._booksSelectionBox.pack_start(cell, True)
1216 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1218 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1219 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1221 if len(self._booksList) <= self._selectedComboIndex:
1222 self._selectedComboIndex = 0
1223 self._booksSelectionBox.set_active(self._selectedComboIndex)
1226 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1227 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1231 self._booksSelectionBox.clear()
1232 self._booksSelectionBox.set_model(None)
1233 self._contactsview.set_model(None)
1234 self._contactsview.remove_column(self._contactColumn)
1236 def number_selected(self, action, number, message):
1238 @note Actual dial function is patched in later
1240 raise NotImplementedError("Horrible unknown error has occurred")
1242 def get_addressbooks(self):
1244 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1246 for i, factory in enumerate(self._addressBookFactories):
1247 for bookFactory, bookId, bookName in factory.get_addressbooks():
1248 yield (str(i), bookId), (factory.factory_name(), bookName)
1250 def open_addressbook(self, bookFactoryId, bookId):
1251 bookFactoryIndex = int(bookFactoryId)
1252 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1254 forceUpdate = True if addressBook is not self._addressBook else False
1256 self._addressBook = addressBook
1257 self.update(force=forceUpdate)
1259 def update(self, force = False):
1260 if not force and self._isPopulated:
1262 self._updateSink.send(())
1266 self._isPopulated = False
1267 self._contactsmodel.clear()
1268 for factory in self._addressBookFactories:
1269 factory.clear_caches()
1270 self._addressBook.clear_caches()
1272 def append(self, book):
1273 self._addressBookFactories.append(book)
1275 def extend(self, books):
1276 self._addressBookFactories.extend(books)
1282 def load_settings(self, config, sectionName):
1284 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1285 except ConfigParser.NoOptionError:
1286 self._selectedComboIndex = 0
1288 def save_settings(self, config, sectionName):
1289 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1291 def _idly_populate_contactsview(self):
1294 while addressBook is not self._addressBook:
1295 addressBook = self._addressBook
1296 with gtk_toolbox.gtk_lock():
1297 self._contactsview.set_model(None)
1301 contacts = addressBook.get_contacts()
1302 except Exception, e:
1304 self._isPopulated = False
1305 self._errorDisplay.push_exception_with_lock()
1306 for contactId, contactName in contacts:
1307 contactType = (addressBook.contact_source_short_name(contactId), )
1308 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1310 with gtk_toolbox.gtk_lock():
1311 self._contactsview.set_model(self._contactsmodel)
1313 self._isPopulated = True
1314 except Exception, e:
1315 self._errorDisplay.push_exception_with_lock()
1318 def _on_addressbook_combo_changed(self, *args, **kwds):
1320 itr = self._booksSelectionBox.get_active_iter()
1323 self._selectedComboIndex = self._booksSelectionBox.get_active()
1324 selectedFactoryId = self._booksList.get_value(itr, 0)
1325 selectedBookId = self._booksList.get_value(itr, 1)
1326 self.open_addressbook(selectedFactoryId, selectedBookId)
1327 except Exception, e:
1328 self._errorDisplay.push_exception()
1330 def _on_contactsview_row_activated(self, treeview, path, view_column):
1332 model, itr = self._contactsviewselection.get_selected()
1336 contactId = self._contactsmodel.get_value(itr, 3)
1337 contactName = self._contactsmodel.get_value(itr, 1)
1339 contactDetails = self._addressBook.get_contact_details(contactId)
1340 except Exception, e:
1342 self._errorDisplay.push_exception()
1343 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1345 if len(contactPhoneNumbers) == 0:
1348 action, phoneNumber, message = self._phoneTypeSelector.run(
1349 contactPhoneNumbers,
1350 message = contactName,
1351 parent = self._window,
1353 if action == PhoneTypeSelector.ACTION_CANCEL:
1355 assert phoneNumber, "A lack of phone number exists"
1357 self.number_selected(action, phoneNumber, message)
1358 self._contactsviewselection.unselect_all()
1359 except Exception, e:
1360 self._errorDisplay.push_exception()