4 DialCentral - Front end for Google's Grand Central service.
5 Copyright (C) 2008 Mark Bergman bergman AT merctech DOT com
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 Lesser General Public License for more details.
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21 @todo Add CTRL-V support to Dialpad
22 @todo Touch selector for addressbook selection
23 @todo Touch selector for callback number
24 @todo Look into top half of dialogs being a treeview rather than a label
25 @todo Alternate UI for dialogs (stackables)
28 from __future__ import with_statement
42 def make_ugly(prettynumber):
44 function to take a phone number and strip out all non-numeric
47 >>> make_ugly("+012-(345)-678-90")
51 uglynumber = re.sub('\D', '', prettynumber)
55 def make_pretty(phonenumber):
57 Function to take a phone number and return the pretty version
59 if phonenumber begins with 0:
61 if phonenumber begins with 1: ( for gizmo callback numbers )
63 if phonenumber is 13 digits:
65 if phonenumber is 10 digits:
69 >>> make_pretty("1234567")
71 >>> make_pretty("2345678901")
73 >>> make_pretty("12345678901")
75 >>> make_pretty("01234567890")
78 if phonenumber is None or phonenumber is "":
81 phonenumber = make_ugly(phonenumber)
83 if len(phonenumber) < 3:
86 if phonenumber[0] == "0":
88 prettynumber += "+%s" % phonenumber[0:3]
89 if 3 < len(phonenumber):
90 prettynumber += "-(%s)" % phonenumber[3:6]
91 if 6 < len(phonenumber):
92 prettynumber += "-%s" % phonenumber[6:9]
93 if 9 < len(phonenumber):
94 prettynumber += "-%s" % phonenumber[9:]
96 elif len(phonenumber) <= 7:
97 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
98 elif len(phonenumber) > 8 and phonenumber[0] == "1":
99 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
100 elif len(phonenumber) > 7:
101 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
105 def abbrev_relative_date(date):
107 >>> abbrev_relative_date("42 hours ago")
109 >>> abbrev_relative_date("2 days ago")
111 >>> abbrev_relative_date("4 weeks ago")
114 parts = date.split(" ")
115 return "%s %s" % (parts[0], parts[1][0])
118 class MergedAddressBook(object):
120 Merger of all addressbooks
123 def __init__(self, addressbookFactories, sorter = None):
124 self.__addressbookFactories = addressbookFactories
125 self.__addressbooks = None
126 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
128 def clear_caches(self):
129 self.__addressbooks = None
130 for factory in self.__addressbookFactories:
131 factory.clear_caches()
133 def get_addressbooks(self):
135 @returns Iterable of (Address Book Factory, Book Id, Book Name)
139 def open_addressbook(self, bookId):
142 def contact_source_short_name(self, contactId):
143 if self.__addressbooks is None:
145 bookIndex, originalId = contactId.split("-", 1)
146 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
150 return "All Contacts"
152 def get_contacts(self):
154 @returns Iterable of (contact id, contact name)
156 if self.__addressbooks is None:
157 self.__addressbooks = list(
158 factory.open_addressbook(id)
159 for factory in self.__addressbookFactories
160 for (f, id, name) in factory.get_addressbooks()
163 ("-".join([str(bookIndex), contactId]), contactName)
164 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
165 for (contactId, contactName) in addressbook.get_contacts()
167 sortedContacts = self.__sort_contacts(contacts)
168 return sortedContacts
170 def get_contact_details(self, contactId):
172 @returns Iterable of (Phone Type, Phone Number)
174 if self.__addressbooks is None:
176 bookIndex, originalId = contactId.split("-", 1)
177 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
180 def null_sorter(contacts):
182 Good for speed/low memory
187 def basic_firtname_sorter(contacts):
189 Expects names in "First Last" format
192 (contactName.rsplit(" ", 1)[0], (contactId, contactName))
193 for (contactId, contactName) in contacts
195 contactsWithKey.sort()
196 return (contactData for (lastName, contactData) in contactsWithKey)
199 def basic_lastname_sorter(contacts):
201 Expects names in "First Last" format
204 (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
205 for (contactId, contactName) in contacts
207 contactsWithKey.sort()
208 return (contactData for (lastName, contactData) in contactsWithKey)
211 def reversed_firtname_sorter(contacts):
213 Expects names in "Last, First" format
216 (contactName.split(", ", 1)[-1], (contactId, contactName))
217 for (contactId, contactName) in contacts
219 contactsWithKey.sort()
220 return (contactData for (lastName, contactData) in contactsWithKey)
223 def reversed_lastname_sorter(contacts):
225 Expects names in "Last, First" format
228 (contactName.split(", ", 1)[0], (contactId, contactName))
229 for (contactId, contactName) in contacts
231 contactsWithKey.sort()
232 return (contactData for (lastName, contactData) in contactsWithKey)
235 def guess_firstname(name):
237 return name.split(", ", 1)[-1]
239 return name.rsplit(" ", 1)[0]
242 def guess_lastname(name):
244 return name.split(", ", 1)[0]
246 return name.rsplit(" ", 1)[-1]
249 def advanced_firstname_sorter(cls, contacts):
251 (cls.guess_firstname(contactName), (contactId, contactName))
252 for (contactId, contactName) in contacts
254 contactsWithKey.sort()
255 return (contactData for (lastName, contactData) in contactsWithKey)
258 def advanced_lastname_sorter(cls, contacts):
260 (cls.guess_lastname(contactName), (contactId, contactName))
261 for (contactId, contactName) in contacts
263 contactsWithKey.sort()
264 return (contactData for (lastName, contactData) in contactsWithKey)
267 class PhoneTypeSelector(object):
269 ACTION_CANCEL = "cancel"
270 ACTION_SELECT = "select"
272 ACTION_SEND_SMS = "sms"
274 def __init__(self, widgetTree, gcBackend):
275 self._gcBackend = gcBackend
276 self._widgetTree = widgetTree
278 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
279 self._smsDialog = SmsEntryDialog(self._widgetTree)
281 self._smsButton = self._widgetTree.get_widget("sms_button")
282 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
284 self._dialButton = self._widgetTree.get_widget("dial_button")
285 self._dialButton.connect("clicked", self._on_phonetype_dial)
287 self._selectButton = self._widgetTree.get_widget("select_button")
288 self._selectButton.connect("clicked", self._on_phonetype_select)
290 self._cancelButton = self._widgetTree.get_widget("cancel_button")
291 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
293 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
294 self._typeviewselection = None
296 self._message = self._widgetTree.get_widget("phoneSelectionMessage")
297 self._messageViewport = self._widgetTree.get_widget("phoneSelectionMessage_viewport")
298 self._scrollWindow = self._widgetTree.get_widget("phoneSelectionMessage_scrolledwindow")
299 self._typeview = self._widgetTree.get_widget("phonetypes")
300 self._typeview.connect("row-activated", self._on_phonetype_select)
302 self._action = self.ACTION_CANCEL
304 def run(self, contactDetails, message = "", parent = None):
305 self._action = self.ACTION_CANCEL
306 self._typemodel.clear()
307 self._typeview.set_model(self._typemodel)
309 # Add the column to the treeview
310 textrenderer = gtk.CellRendererText()
311 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
312 self._typeview.append_column(numberColumn)
314 textrenderer = gtk.CellRendererText()
315 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
316 self._typeview.append_column(typeColumn)
318 self._typeviewselection = self._typeview.get_selection()
319 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
321 for phoneType, phoneNumber in contactDetails:
322 display = " - ".join((phoneNumber, phoneType))
324 row = (phoneNumber, display)
325 self._typemodel.append(row)
327 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
329 self._message.set_markup(message)
332 self._message.set_markup("")
335 if parent is not None:
336 self._dialog.set_transient_for(parent)
340 adjustment = self._scrollWindow.get_vadjustment()
341 dx = self._message.get_allocation().height - self._messageViewport.get_allocation().height
343 adjustment.value = dx
345 userResponse = self._dialog.run()
349 if userResponse == gtk.RESPONSE_OK:
350 phoneNumber = self._get_number()
351 phoneNumber = make_ugly(phoneNumber)
355 self._action = self.ACTION_CANCEL
357 if self._action == self.ACTION_SEND_SMS:
358 smsMessage = self._smsDialog.run(phoneNumber, message, parent)
361 self._action = self.ACTION_CANCEL
365 self._typeviewselection.unselect_all()
366 self._typeview.remove_column(numberColumn)
367 self._typeview.remove_column(typeColumn)
368 self._typeview.set_model(None)
370 return self._action, phoneNumber, smsMessage
372 def _get_number(self):
373 model, itr = self._typeviewselection.get_selected()
377 phoneNumber = self._typemodel.get_value(itr, 0)
380 def _on_phonetype_dial(self, *args):
381 self._dialog.response(gtk.RESPONSE_OK)
382 self._action = self.ACTION_DIAL
384 def _on_phonetype_send_sms(self, *args):
385 self._dialog.response(gtk.RESPONSE_OK)
386 self._action = self.ACTION_SEND_SMS
388 def _on_phonetype_select(self, *args):
389 self._dialog.response(gtk.RESPONSE_OK)
390 self._action = self.ACTION_SELECT
392 def _on_phonetype_cancel(self, *args):
393 self._dialog.response(gtk.RESPONSE_CANCEL)
394 self._action = self.ACTION_CANCEL
397 class SmsEntryDialog(object):
400 @todo Add multi-SMS messages like GoogleVoice
405 def __init__(self, widgetTree):
406 self._widgetTree = widgetTree
407 self._dialog = self._widgetTree.get_widget("smsDialog")
409 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
410 self._smsButton.connect("clicked", self._on_send)
412 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
413 self._cancelButton.connect("clicked", self._on_cancel)
415 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
416 self._message = self._widgetTree.get_widget("smsMessage")
417 self._messageViewport = self._widgetTree.get_widget("smsMessage_viewport")
418 self._scrollWindow = self._widgetTree.get_widget("smsMessage_scrolledwindow")
419 self._smsEntry = self._widgetTree.get_widget("smsEntry")
420 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
422 def run(self, number, message = "", parent = None):
424 self._message.set_markup(message)
427 self._message.set_markup("")
429 self._smsEntry.get_buffer().set_text("")
430 self._update_letter_count()
432 if parent is not None:
433 self._dialog.set_transient_for(parent)
437 adjustment = self._scrollWindow.get_vadjustment()
438 dx = self._message.get_allocation().height - self._messageViewport.get_allocation().height
440 adjustment.value = dx
442 userResponse = self._dialog.run()
446 if userResponse == gtk.RESPONSE_OK:
447 entryBuffer = self._smsEntry.get_buffer()
448 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
449 enteredMessage = enteredMessage[0:self.MAX_CHAR]
453 return enteredMessage.strip()
455 def _update_letter_count(self, *args):
456 entryLength = self._smsEntry.get_buffer().get_char_count()
457 charsLeft = self.MAX_CHAR - entryLength
458 self._letterCountLabel.set_text(str(charsLeft))
460 self._smsButton.set_sensitive(False)
462 self._smsButton.set_sensitive(True)
464 def _on_entry_changed(self, *args):
465 self._update_letter_count()
467 def _on_send(self, *args):
468 self._dialog.response(gtk.RESPONSE_OK)
470 def _on_cancel(self, *args):
471 self._dialog.response(gtk.RESPONSE_CANCEL)
474 class Dialpad(object):
476 def __init__(self, widgetTree, errorDisplay):
477 self._errorDisplay = errorDisplay
478 self._smsDialog = SmsEntryDialog(widgetTree)
480 self._numberdisplay = widgetTree.get_widget("numberdisplay")
481 self._smsButton = widgetTree.get_widget("sms")
482 self._dialButton = widgetTree.get_widget("dial")
483 self._backButton = widgetTree.get_widget("back")
484 self._phonenumber = ""
485 self._prettynumber = ""
488 "on_digit_clicked": self._on_digit_clicked,
490 widgetTree.signal_autoconnect(callbackMapping)
491 self._dialButton.connect("clicked", self._on_dial_clicked)
492 self._smsButton.connect("clicked", self._on_sms_clicked)
494 self._originalLabel = self._backButton.get_label()
495 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
496 self._backTapHandler.on_tap = self._on_backspace
497 self._backTapHandler.on_hold = self._on_clearall
498 self._backTapHandler.on_holding = self._set_clear_button
499 self._backTapHandler.on_cancel = self._reset_back_button
501 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
504 self._dialButton.grab_focus()
505 self._backTapHandler.enable()
508 self._reset_back_button()
509 self._backTapHandler.disable()
511 def number_selected(self, action, number, message):
513 @note Actual dial function is patched in later
515 raise NotImplementedError("Horrible unknown error has occurred")
517 def get_number(self):
518 return self._phonenumber
520 def set_number(self, number):
522 Set the number to dial
525 self._phonenumber = make_ugly(number)
526 self._prettynumber = make_pretty(self._phonenumber)
527 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
529 self._errorDisplay.push_exception()
538 def load_settings(self, config, section):
541 def save_settings(self, config, section):
543 @note Thread Agnostic
547 def _on_sms_clicked(self, widget):
549 action = PhoneTypeSelector.ACTION_SEND_SMS
550 phoneNumber = self.get_number()
552 message = self._smsDialog.run(phoneNumber, "", self._window)
555 action = PhoneTypeSelector.ACTION_CANCEL
557 if action == PhoneTypeSelector.ACTION_CANCEL:
559 self.number_selected(action, phoneNumber, message)
561 self._errorDisplay.push_exception()
563 def _on_dial_clicked(self, widget):
565 action = PhoneTypeSelector.ACTION_DIAL
566 phoneNumber = self.get_number()
568 self.number_selected(action, phoneNumber, message)
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_clicked)
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)
664 self._minutesEntryButton.set_sensitive(True)
665 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
667 self.update(force=True)
670 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
671 self._onCallbackentryChangedId = 0
673 if self._alarmHandler is not None:
674 self._notifyCheckbox.disconnect(self._onNotifyToggled)
675 self._minutesEntryButton.disconnect(self._onMinutesChanged)
676 self._missedCheckbox.disconnect(self._onNotifyToggled)
677 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
678 self._smsCheckbox.disconnect(self._onNotifyToggled)
679 self._onNotifyToggled = 0
680 self._onMinutesChanged = 0
681 self._onMissedToggled = 0
682 self._onVoicemailToggled = 0
683 self._onSmsToggled = 0
685 self._notifyCheckbox.set_sensitive(True)
686 self._minutesEntryButton.set_sensitive(True)
687 self._missedCheckbox.set_sensitive(True)
688 self._voicemailCheckbox.set_sensitive(True)
689 self._smsCheckbox.set_sensitive(True)
692 self._callbackList.clear()
694 def get_selected_callback_number(self):
695 return make_ugly(self._callbackCombo.get_child().get_text())
697 def set_account_number(self, number):
699 Displays current account number
701 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
703 def update(self, force = False):
704 if not force and self._isPopulated:
706 self._populate_callback_combo()
707 self.set_account_number(self._backend.get_account_number())
711 self._callbackCombo.get_child().set_text("")
712 self.set_account_number("")
713 self._isPopulated = False
715 def save_everything(self):
716 raise NotImplementedError
720 return "Account Info"
722 def load_settings(self, config, section):
723 self._defaultCallback = config.get(section, "callback")
724 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
725 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
726 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
728 def save_settings(self, config, section):
730 @note Thread Agnostic
732 callback = self.get_selected_callback_number()
733 config.set(section, "callback", callback)
734 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
735 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
736 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
738 def _populate_callback_combo(self):
739 self._isPopulated = True
740 self._callbackList.clear()
742 callbackNumbers = self._backend.get_callback_numbers()
744 self._errorDisplay.push_exception()
745 self._isPopulated = False
748 for number, description in callbackNumbers.iteritems():
749 self._callbackList.append((make_pretty(number),))
751 self._callbackCombo.set_model(self._callbackList)
752 self._callbackCombo.set_text_column(0)
753 #callbackNumber = self._backend.get_callback_number()
754 callbackNumber = self._defaultCallback
755 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
757 def _set_callback_number(self, number):
759 if not self._backend.is_valid_syntax(number) and 0 < len(number):
760 self._errorDisplay.push_message("%s is not a valid callback number" % number)
761 elif number == self._backend.get_callback_number():
763 "Callback number already is %s" % (
764 self._backend.get_callback_number(),
768 self._backend.set_callback_number(number)
769 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
770 make_pretty(number), make_pretty(self._backend.get_callback_number())
773 "Callback number set to %s" % (
774 self._backend.get_callback_number(),
778 self._errorDisplay.push_exception()
780 def _update_alarm_settings(self, recurrence):
782 isEnabled = self._notifyCheckbox.get_active()
783 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
784 self._alarmHandler.apply_settings(isEnabled, recurrence)
786 self.save_everything()
787 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
788 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
790 def _on_callbackentry_changed(self, *args):
792 text = self.get_selected_callback_number()
793 number = make_ugly(text)
794 self._set_callback_number(number)
796 self._errorDisplay.push_exception()
798 def _on_notify_toggled(self, *args):
800 if self._applyAlarmTimeoutId is not None:
801 gobject.source_remove(self._applyAlarmTimeoutId)
802 self._applyAlarmTimeoutId = None
803 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
805 self._errorDisplay.push_exception()
807 def _on_minutes_clicked(self, *args):
808 recurrenceChoices = [
820 actualSelection = self._alarmHandler.recurrence
822 closestSelectionIndex = 0
823 for i, possible in enumerate(recurrenceChoices):
824 if possible[0] <= actualSelection:
825 closestSelectionIndex = i
826 recurrenceIndex = hildonize.touch_selector(
829 (("%s" % m[1]) for m in recurrenceChoices),
830 closestSelectionIndex,
832 recurrence = recurrenceChoices[recurrenceIndex][0]
834 self._update_alarm_settings(recurrence)
836 self._errorDisplay.push_exception()
838 def _on_apply_timeout(self, *args):
840 self._applyAlarmTimeoutId = None
842 self._update_alarm_settings(self._alarmHandler.recurrence)
844 self._errorDisplay.push_exception()
847 def _on_missed_toggled(self, *args):
849 self._notifyOnMissed = self._missedCheckbox.get_active()
850 self.save_everything()
852 self._errorDisplay.push_exception()
854 def _on_voicemail_toggled(self, *args):
856 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
857 self.save_everything()
859 self._errorDisplay.push_exception()
861 def _on_sms_toggled(self, *args):
863 self._notifyOnSms = self._smsCheckbox.get_active()
864 self.save_everything()
866 self._errorDisplay.push_exception()
869 class RecentCallsView(object):
876 def __init__(self, widgetTree, backend, errorDisplay):
877 self._errorDisplay = errorDisplay
878 self._backend = backend
880 self._isPopulated = False
881 self._recentmodel = gtk.ListStore(
882 gobject.TYPE_STRING, # number
883 gobject.TYPE_STRING, # date
884 gobject.TYPE_STRING, # action
885 gobject.TYPE_STRING, # from
887 self._recentview = widgetTree.get_widget("recentview")
888 self._recentviewselection = None
889 self._onRecentviewRowActivatedId = 0
891 textrenderer = gtk.CellRendererText()
892 textrenderer.set_property("yalign", 0)
893 self._dateColumn = gtk.TreeViewColumn("Date")
894 self._dateColumn.pack_start(textrenderer, expand=True)
895 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
897 textrenderer = gtk.CellRendererText()
898 textrenderer.set_property("yalign", 0)
899 self._actionColumn = gtk.TreeViewColumn("Action")
900 self._actionColumn.pack_start(textrenderer, expand=True)
901 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
903 textrenderer = gtk.CellRendererText()
904 textrenderer.set_property("yalign", 0)
905 hildonize.set_cell_thumb_selectable(textrenderer)
906 self._nameColumn = gtk.TreeViewColumn("From")
907 self._nameColumn.pack_start(textrenderer, expand=True)
908 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
909 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
911 textrenderer = gtk.CellRendererText()
912 textrenderer.set_property("yalign", 0)
913 self._numberColumn = gtk.TreeViewColumn("Number")
914 self._numberColumn.pack_start(textrenderer, expand=True)
915 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
917 self._window = gtk_toolbox.find_parent_window(self._recentview)
918 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
920 self._updateSink = gtk_toolbox.threaded_stage(
922 self._idly_populate_recentview,
923 gtk_toolbox.null_sink(),
928 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
929 self._recentview.set_model(self._recentmodel)
931 self._recentview.append_column(self._dateColumn)
932 self._recentview.append_column(self._actionColumn)
933 self._recentview.append_column(self._numberColumn)
934 self._recentview.append_column(self._nameColumn)
935 self._recentviewselection = self._recentview.get_selection()
936 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
938 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
941 self._recentview.disconnect(self._onRecentviewRowActivatedId)
945 self._recentview.remove_column(self._dateColumn)
946 self._recentview.remove_column(self._actionColumn)
947 self._recentview.remove_column(self._nameColumn)
948 self._recentview.remove_column(self._numberColumn)
949 self._recentview.set_model(None)
951 def number_selected(self, action, number, message):
953 @note Actual dial function is patched in later
955 raise NotImplementedError("Horrible unknown error has occurred")
957 def update(self, force = False):
958 if not force and self._isPopulated:
960 self._updateSink.send(())
964 self._isPopulated = False
965 self._recentmodel.clear()
969 return "Recent Calls"
971 def load_settings(self, config, section):
974 def save_settings(self, config, section):
976 @note Thread Agnostic
980 def _idly_populate_recentview(self):
982 self._recentmodel.clear()
983 self._isPopulated = True
986 recentItems = self._backend.get_recent()
988 self._errorDisplay.push_exception_with_lock()
989 self._isPopulated = False
992 for personName, phoneNumber, date, action in recentItems:
994 personName = "Unknown"
995 date = abbrev_relative_date(date)
996 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
997 prettyNumber = make_pretty(prettyNumber)
998 item = (prettyNumber, date, action.capitalize(), personName)
999 with gtk_toolbox.gtk_lock():
1000 self._recentmodel.append(item)
1001 except Exception, e:
1002 self._errorDisplay.push_exception_with_lock()
1006 def _on_recentview_row_activated(self, treeview, path, view_column):
1008 model, itr = self._recentviewselection.get_selected()
1012 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1013 number = make_ugly(number)
1014 contactPhoneNumbers = [("Phone", number)]
1015 description = self._recentmodel.get_value(itr, self.FROM_IDX)
1017 action, phoneNumber, message = self._phoneTypeSelector.run(
1018 contactPhoneNumbers,
1019 message = description,
1020 parent = self._window,
1022 if action == PhoneTypeSelector.ACTION_CANCEL:
1024 assert phoneNumber, "A lack of phone number exists"
1026 self.number_selected(action, phoneNumber, message)
1027 self._recentviewselection.unselect_all()
1028 except Exception, e:
1029 self._errorDisplay.push_exception()
1032 class MessagesView(object):
1039 def __init__(self, widgetTree, backend, errorDisplay):
1040 self._errorDisplay = errorDisplay
1041 self._backend = backend
1043 self._isPopulated = False
1044 self._messagemodel = gtk.ListStore(
1045 gobject.TYPE_STRING, # number
1046 gobject.TYPE_STRING, # date
1047 gobject.TYPE_STRING, # header
1048 gobject.TYPE_STRING, # message
1050 self._messageview = widgetTree.get_widget("messages_view")
1051 self._messageviewselection = None
1052 self._onMessageviewRowActivatedId = 0
1054 self._messageRenderer = gtk.CellRendererText()
1055 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1056 self._messageRenderer.set_property("wrap-width", 500)
1057 self._messageColumn = gtk.TreeViewColumn("Messages")
1058 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1059 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1060 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1062 self._window = gtk_toolbox.find_parent_window(self._messageview)
1063 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1065 self._updateSink = gtk_toolbox.threaded_stage(
1067 self._idly_populate_messageview,
1068 gtk_toolbox.null_sink(),
1073 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1074 self._messageview.set_model(self._messagemodel)
1076 self._messageview.append_column(self._messageColumn)
1077 self._messageviewselection = self._messageview.get_selection()
1078 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1080 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1083 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1087 self._messageview.remove_column(self._messageColumn)
1088 self._messageview.set_model(None)
1090 def number_selected(self, action, number, message):
1092 @note Actual dial function is patched in later
1094 raise NotImplementedError("Horrible unknown error has occurred")
1096 def update(self, force = False):
1097 if not force and self._isPopulated:
1099 self._updateSink.send(())
1103 self._isPopulated = False
1104 self._messagemodel.clear()
1110 def load_settings(self, config, section):
1113 def save_settings(self, config, section):
1115 @note Thread Agnostic
1119 def _idly_populate_messageview(self):
1121 self._messagemodel.clear()
1122 self._isPopulated = True
1125 messageItems = self._backend.get_messages()
1126 except Exception, e:
1127 self._errorDisplay.push_exception_with_lock()
1128 self._isPopulated = False
1131 for header, number, relativeDate, message in messageItems:
1132 prettyNumber = number[2:] if number.startswith("+1") else number
1133 prettyNumber = make_pretty(prettyNumber)
1134 message = "<b>%s - %s</b> <i>(%s)</i>\n\n%s" % (header, prettyNumber, relativeDate, message)
1135 number = make_ugly(number)
1136 row = (number, relativeDate, header, message)
1137 with gtk_toolbox.gtk_lock():
1138 self._messagemodel.append(row)
1139 except Exception, e:
1140 self._errorDisplay.push_exception_with_lock()
1144 def _on_messageview_row_activated(self, treeview, path, view_column):
1146 model, itr = self._messageviewselection.get_selected()
1150 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1151 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
1153 action, phoneNumber, message = self._phoneTypeSelector.run(
1154 contactPhoneNumbers,
1155 message = description,
1156 parent = self._window,
1158 if action == PhoneTypeSelector.ACTION_CANCEL:
1160 assert phoneNumber, "A lock of phone number exists"
1162 self.number_selected(action, phoneNumber, message)
1163 self._messageviewselection.unselect_all()
1164 except Exception, e:
1165 self._errorDisplay.push_exception()
1168 class ContactsView(object):
1170 def __init__(self, widgetTree, backend, errorDisplay):
1171 self._errorDisplay = errorDisplay
1172 self._backend = backend
1174 self._addressBook = None
1175 self._selectedComboIndex = 0
1176 self._addressBookFactories = [null_backend.NullAddressBook()]
1178 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1179 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
1181 self._isPopulated = False
1182 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1183 self._contactsviewselection = None
1184 self._contactsview = widgetTree.get_widget("contactsview")
1186 self._contactColumn = gtk.TreeViewColumn("Contact")
1187 displayContactSource = False
1188 if displayContactSource:
1189 textrenderer = gtk.CellRendererText()
1190 self._contactColumn.pack_start(textrenderer, expand=False)
1191 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1192 textrenderer = gtk.CellRendererText()
1193 hildonize.set_cell_thumb_selectable(textrenderer)
1194 self._contactColumn.pack_start(textrenderer, expand=True)
1195 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1196 textrenderer = gtk.CellRendererText()
1197 self._contactColumn.pack_start(textrenderer, expand=True)
1198 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1199 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1200 self._contactColumn.set_sort_column_id(1)
1201 self._contactColumn.set_visible(True)
1203 self._onContactsviewRowActivatedId = 0
1204 self._onAddressbookComboChangedId = 0
1205 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1206 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1208 self._updateSink = gtk_toolbox.threaded_stage(
1210 self._idly_populate_contactsview,
1211 gtk_toolbox.null_sink(),
1216 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1218 self._contactsview.set_model(self._contactsmodel)
1219 self._contactsview.append_column(self._contactColumn)
1220 self._contactsviewselection = self._contactsview.get_selection()
1221 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1223 self._booksList.clear()
1224 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1225 if factoryName and bookName:
1226 entryName = "%s: %s" % (factoryName, bookName)
1228 entryName = factoryName
1230 entryName = bookName
1232 entryName = "Bad name (%d)" % factoryId
1233 row = (str(factoryId), bookId, entryName)
1234 self._booksList.append(row)
1236 self._booksSelectionBox.set_model(self._booksList)
1237 cell = gtk.CellRendererText()
1238 self._booksSelectionBox.pack_start(cell, True)
1239 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1241 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1242 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1244 if len(self._booksList) <= self._selectedComboIndex:
1245 self._selectedComboIndex = 0
1246 self._booksSelectionBox.set_active(self._selectedComboIndex)
1249 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1250 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1254 self._booksSelectionBox.clear()
1255 self._booksSelectionBox.set_model(None)
1256 self._contactsview.set_model(None)
1257 self._contactsview.remove_column(self._contactColumn)
1259 def number_selected(self, action, number, message):
1261 @note Actual dial function is patched in later
1263 raise NotImplementedError("Horrible unknown error has occurred")
1265 def get_addressbooks(self):
1267 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1269 for i, factory in enumerate(self._addressBookFactories):
1270 for bookFactory, bookId, bookName in factory.get_addressbooks():
1271 yield (str(i), bookId), (factory.factory_name(), bookName)
1273 def open_addressbook(self, bookFactoryId, bookId):
1274 bookFactoryIndex = int(bookFactoryId)
1275 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1277 forceUpdate = True if addressBook is not self._addressBook else False
1279 self._addressBook = addressBook
1280 self.update(force=forceUpdate)
1282 def update(self, force = False):
1283 if not force and self._isPopulated:
1285 self._updateSink.send(())
1289 self._isPopulated = False
1290 self._contactsmodel.clear()
1291 for factory in self._addressBookFactories:
1292 factory.clear_caches()
1293 self._addressBook.clear_caches()
1295 def append(self, book):
1296 self._addressBookFactories.append(book)
1298 def extend(self, books):
1299 self._addressBookFactories.extend(books)
1305 def load_settings(self, config, sectionName):
1307 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1308 except ConfigParser.NoOptionError:
1309 self._selectedComboIndex = 0
1311 def save_settings(self, config, sectionName):
1312 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1314 def _idly_populate_contactsview(self):
1317 while addressBook is not self._addressBook:
1318 addressBook = self._addressBook
1319 with gtk_toolbox.gtk_lock():
1320 self._contactsview.set_model(None)
1324 contacts = addressBook.get_contacts()
1325 except Exception, e:
1327 self._isPopulated = False
1328 self._errorDisplay.push_exception_with_lock()
1329 for contactId, contactName in contacts:
1330 contactType = (addressBook.contact_source_short_name(contactId), )
1331 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1333 with gtk_toolbox.gtk_lock():
1334 self._contactsview.set_model(self._contactsmodel)
1336 self._isPopulated = True
1337 except Exception, e:
1338 self._errorDisplay.push_exception_with_lock()
1341 def _on_addressbook_combo_changed(self, *args, **kwds):
1343 itr = self._booksSelectionBox.get_active_iter()
1346 self._selectedComboIndex = self._booksSelectionBox.get_active()
1347 selectedFactoryId = self._booksList.get_value(itr, 0)
1348 selectedBookId = self._booksList.get_value(itr, 1)
1349 self.open_addressbook(selectedFactoryId, selectedBookId)
1350 except Exception, e:
1351 self._errorDisplay.push_exception()
1353 def _on_contactsview_row_activated(self, treeview, path, view_column):
1355 model, itr = self._contactsviewselection.get_selected()
1359 contactId = self._contactsmodel.get_value(itr, 3)
1360 contactName = self._contactsmodel.get_value(itr, 1)
1362 contactDetails = self._addressBook.get_contact_details(contactId)
1363 except Exception, e:
1365 self._errorDisplay.push_exception()
1366 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1368 if len(contactPhoneNumbers) == 0:
1371 action, phoneNumber, message = self._phoneTypeSelector.run(
1372 contactPhoneNumbers,
1373 message = contactName,
1374 parent = self._window,
1376 if action == PhoneTypeSelector.ACTION_CANCEL:
1378 assert phoneNumber, "A lack of phone number exists"
1380 self.number_selected(action, phoneNumber, message)
1381 self._contactsviewselection.unselect_all()
1382 except Exception, e:
1383 self._errorDisplay.push_exception()