4 DialCentral - Front end for Google's Grand Central service.
5 Copyright (C) 2008 Mark Bergman bergman AT merctech DOT com
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 Lesser General Public License for more details.
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21 @todo Touch selector for callback number
22 @todo Alternate UI for dialogs (stackables)
25 from __future__ import with_statement
39 def make_ugly(prettynumber):
41 function to take a phone number and strip out all non-numeric
44 >>> make_ugly("+012-(345)-678-90")
48 uglynumber = re.sub('\D', '', prettynumber)
52 def make_pretty(phonenumber):
54 Function to take a phone number and return the pretty version
56 if phonenumber begins with 0:
58 if phonenumber begins with 1: ( for gizmo callback numbers )
60 if phonenumber is 13 digits:
62 if phonenumber is 10 digits:
66 >>> make_pretty("1234567")
68 >>> make_pretty("2345678901")
70 >>> make_pretty("12345678901")
72 >>> make_pretty("01234567890")
75 if phonenumber is None or phonenumber is "":
78 phonenumber = make_ugly(phonenumber)
80 if len(phonenumber) < 3:
83 if phonenumber[0] == "0":
85 prettynumber += "+%s" % phonenumber[0:3]
86 if 3 < len(phonenumber):
87 prettynumber += "-(%s)" % phonenumber[3:6]
88 if 6 < len(phonenumber):
89 prettynumber += "-%s" % phonenumber[6:9]
90 if 9 < len(phonenumber):
91 prettynumber += "-%s" % phonenumber[9:]
93 elif len(phonenumber) <= 7:
94 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
95 elif len(phonenumber) > 8 and phonenumber[0] == "1":
96 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
97 elif len(phonenumber) > 7:
98 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
102 def abbrev_relative_date(date):
104 >>> abbrev_relative_date("42 hours ago")
106 >>> abbrev_relative_date("2 days ago")
108 >>> abbrev_relative_date("4 weeks ago")
111 parts = date.split(" ")
112 return "%s %s" % (parts[0], parts[1][0])
115 class MergedAddressBook(object):
117 Merger of all addressbooks
120 def __init__(self, addressbookFactories, sorter = None):
121 self.__addressbookFactories = addressbookFactories
122 self.__addressbooks = None
123 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
125 def clear_caches(self):
126 self.__addressbooks = None
127 for factory in self.__addressbookFactories:
128 factory.clear_caches()
130 def get_addressbooks(self):
132 @returns Iterable of (Address Book Factory, Book Id, Book Name)
136 def open_addressbook(self, bookId):
139 def contact_source_short_name(self, contactId):
140 if self.__addressbooks is None:
142 bookIndex, originalId = contactId.split("-", 1)
143 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
147 return "All Contacts"
149 def get_contacts(self):
151 @returns Iterable of (contact id, contact name)
153 if self.__addressbooks is None:
154 self.__addressbooks = list(
155 factory.open_addressbook(id)
156 for factory in self.__addressbookFactories
157 for (f, id, name) in factory.get_addressbooks()
160 ("-".join([str(bookIndex), contactId]), contactName)
161 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
162 for (contactId, contactName) in addressbook.get_contacts()
164 sortedContacts = self.__sort_contacts(contacts)
165 return sortedContacts
167 def get_contact_details(self, contactId):
169 @returns Iterable of (Phone Type, Phone Number)
171 if self.__addressbooks is None:
173 bookIndex, originalId = contactId.split("-", 1)
174 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
177 def null_sorter(contacts):
179 Good for speed/low memory
184 def basic_firtname_sorter(contacts):
186 Expects names in "First Last" format
189 (contactName.rsplit(" ", 1)[0], (contactId, contactName))
190 for (contactId, contactName) in contacts
192 contactsWithKey.sort()
193 return (contactData for (lastName, contactData) in contactsWithKey)
196 def basic_lastname_sorter(contacts):
198 Expects names in "First Last" format
201 (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
202 for (contactId, contactName) in contacts
204 contactsWithKey.sort()
205 return (contactData for (lastName, contactData) in contactsWithKey)
208 def reversed_firtname_sorter(contacts):
210 Expects names in "Last, First" format
213 (contactName.split(", ", 1)[-1], (contactId, contactName))
214 for (contactId, contactName) in contacts
216 contactsWithKey.sort()
217 return (contactData for (lastName, contactData) in contactsWithKey)
220 def reversed_lastname_sorter(contacts):
222 Expects names in "Last, First" format
225 (contactName.split(", ", 1)[0], (contactId, contactName))
226 for (contactId, contactName) in contacts
228 contactsWithKey.sort()
229 return (contactData for (lastName, contactData) in contactsWithKey)
232 def guess_firstname(name):
234 return name.split(", ", 1)[-1]
236 return name.rsplit(" ", 1)[0]
239 def guess_lastname(name):
241 return name.split(", ", 1)[0]
243 return name.rsplit(" ", 1)[-1]
246 def advanced_firstname_sorter(cls, contacts):
248 (cls.guess_firstname(contactName), (contactId, contactName))
249 for (contactId, contactName) in contacts
251 contactsWithKey.sort()
252 return (contactData for (lastName, contactData) in contactsWithKey)
255 def advanced_lastname_sorter(cls, contacts):
257 (cls.guess_lastname(contactName), (contactId, contactName))
258 for (contactId, contactName) in contacts
260 contactsWithKey.sort()
261 return (contactData for (lastName, contactData) in contactsWithKey)
264 class PhoneTypeSelector(object):
266 ACTION_CANCEL = "cancel"
267 ACTION_SELECT = "select"
269 ACTION_SEND_SMS = "sms"
271 def __init__(self, widgetTree, gcBackend):
272 self._gcBackend = gcBackend
273 self._widgetTree = widgetTree
275 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
276 self._smsDialog = SmsEntryDialog(self._widgetTree)
278 self._smsButton = self._widgetTree.get_widget("sms_button")
279 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
281 self._dialButton = self._widgetTree.get_widget("dial_button")
282 self._dialButton.connect("clicked", self._on_phonetype_dial)
284 self._selectButton = self._widgetTree.get_widget("select_button")
285 self._selectButton.connect("clicked", self._on_phonetype_select)
287 self._cancelButton = self._widgetTree.get_widget("cancel_button")
288 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
290 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
291 self._messagesView = self._widgetTree.get_widget("phoneSelectionMessages")
292 self._scrollWindow = self._messagesView.get_parent()
294 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
295 self._typeviewselection = None
296 self._typeview = self._widgetTree.get_widget("phonetypes")
297 self._typeview.connect("row-activated", self._on_phonetype_select)
299 self._action = self.ACTION_CANCEL
301 def run(self, contactDetails, messages = (), parent = None):
302 self._action = self.ACTION_CANCEL
304 # Add the column to the phone selection tree view
305 self._typemodel.clear()
306 self._typeview.set_model(self._typemodel)
308 textrenderer = gtk.CellRendererText()
309 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
310 self._typeview.append_column(numberColumn)
312 textrenderer = gtk.CellRendererText()
313 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
314 self._typeview.append_column(typeColumn)
316 for phoneType, phoneNumber in contactDetails:
317 display = " - ".join((phoneNumber, phoneType))
319 row = (phoneNumber, display)
320 self._typemodel.append(row)
322 self._typeviewselection = self._typeview.get_selection()
323 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
324 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
326 # Add the column to the messages tree view
327 self._messagemodel.clear()
328 self._messagesView.set_model(self._messagemodel)
330 textrenderer = gtk.CellRendererText()
331 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
332 textrenderer.set_property("wrap-width", 450)
333 messageColumn = gtk.TreeViewColumn("")
334 messageColumn.pack_start(textrenderer, expand=True)
335 messageColumn.add_attribute(textrenderer, "markup", 0)
336 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
337 self._messagesView.append_column(messageColumn)
338 self._messagesView.set_headers_visible(False)
341 for message in messages:
343 self._messagemodel.append(row)
344 self._messagesView.show()
345 self._scrollWindow.show()
346 messagesSelection = self._messagesView.get_selection()
347 messagesSelection.select_path((len(messages)-1, ))
349 self._messagesView.hide()
350 self._scrollWindow.hide()
352 if parent is not None:
353 self._dialog.set_transient_for(parent)
358 self._messagesView.scroll_to_cell((len(messages)-1, ))
360 userResponse = self._dialog.run()
364 if userResponse == gtk.RESPONSE_OK:
365 phoneNumber = self._get_number()
366 phoneNumber = make_ugly(phoneNumber)
370 self._action = self.ACTION_CANCEL
372 if self._action == self.ACTION_SEND_SMS:
373 smsMessage = self._smsDialog.run(phoneNumber, messages, parent)
376 self._action = self.ACTION_CANCEL
380 self._messagesView.remove_column(messageColumn)
381 self._messagesView.set_model(None)
383 self._typeviewselection.unselect_all()
384 self._typeview.remove_column(numberColumn)
385 self._typeview.remove_column(typeColumn)
386 self._typeview.set_model(None)
388 return self._action, phoneNumber, smsMessage
390 def _get_number(self):
391 model, itr = self._typeviewselection.get_selected()
395 phoneNumber = self._typemodel.get_value(itr, 0)
398 def _on_phonetype_dial(self, *args):
399 self._dialog.response(gtk.RESPONSE_OK)
400 self._action = self.ACTION_DIAL
402 def _on_phonetype_send_sms(self, *args):
403 self._dialog.response(gtk.RESPONSE_OK)
404 self._action = self.ACTION_SEND_SMS
406 def _on_phonetype_select(self, *args):
407 self._dialog.response(gtk.RESPONSE_OK)
408 self._action = self.ACTION_SELECT
410 def _on_phonetype_cancel(self, *args):
411 self._dialog.response(gtk.RESPONSE_CANCEL)
412 self._action = self.ACTION_CANCEL
415 class SmsEntryDialog(object):
417 @todo Add multi-SMS messages like GoogleVoice
422 def __init__(self, widgetTree):
423 self._widgetTree = widgetTree
424 self._dialog = self._widgetTree.get_widget("smsDialog")
426 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
427 self._smsButton.connect("clicked", self._on_send)
429 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
430 self._cancelButton.connect("clicked", self._on_cancel)
432 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
434 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
435 self._messagesView = self._widgetTree.get_widget("smsMessages")
436 self._scrollWindow = self._messagesView.get_parent()
438 self._smsEntry = self._widgetTree.get_widget("smsEntry")
439 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
441 def run(self, number, messages = (), parent = None):
442 # Add the column to the messages tree view
443 self._messagemodel.clear()
444 self._messagesView.set_model(self._messagemodel)
446 textrenderer = gtk.CellRendererText()
447 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
448 textrenderer.set_property("wrap-width", 450)
449 messageColumn = gtk.TreeViewColumn("")
450 messageColumn.pack_start(textrenderer, expand=True)
451 messageColumn.add_attribute(textrenderer, "markup", 0)
452 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
453 self._messagesView.append_column(messageColumn)
454 self._messagesView.set_headers_visible(False)
457 for message in messages:
459 self._messagemodel.append(row)
460 self._messagesView.show()
461 self._scrollWindow.show()
462 messagesSelection = self._messagesView.get_selection()
463 messagesSelection.select_path((len(messages)-1, ))
465 self._messagesView.hide()
466 self._scrollWindow.hide()
468 self._smsEntry.get_buffer().set_text("")
469 self._update_letter_count()
471 if parent is not None:
472 self._dialog.set_transient_for(parent)
477 self._messagesView.scroll_to_cell((len(messages)-1, ))
478 self._smsEntry.grab_focus()
480 userResponse = self._dialog.run()
484 if userResponse == gtk.RESPONSE_OK:
485 entryBuffer = self._smsEntry.get_buffer()
486 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
487 enteredMessage = enteredMessage[0:self.MAX_CHAR]
491 self._messagesView.remove_column(messageColumn)
492 self._messagesView.set_model(None)
494 return enteredMessage.strip()
496 def _update_letter_count(self, *args):
497 entryLength = self._smsEntry.get_buffer().get_char_count()
498 charsLeft = self.MAX_CHAR - entryLength
499 self._letterCountLabel.set_text(str(charsLeft))
501 self._smsButton.set_sensitive(False)
503 self._smsButton.set_sensitive(True)
505 def _on_entry_changed(self, *args):
506 self._update_letter_count()
508 def _on_send(self, *args):
509 self._dialog.response(gtk.RESPONSE_OK)
511 def _on_cancel(self, *args):
512 self._dialog.response(gtk.RESPONSE_CANCEL)
515 class Dialpad(object):
517 def __init__(self, widgetTree, errorDisplay):
518 self._clipboard = gtk.clipboard_get()
519 self._errorDisplay = errorDisplay
520 self._smsDialog = SmsEntryDialog(widgetTree)
522 self._numberdisplay = widgetTree.get_widget("numberdisplay")
523 self._smsButton = widgetTree.get_widget("sms")
524 self._dialButton = widgetTree.get_widget("dial")
525 self._backButton = widgetTree.get_widget("back")
526 self._phonenumber = ""
527 self._prettynumber = ""
530 "on_digit_clicked": self._on_digit_clicked,
532 widgetTree.signal_autoconnect(callbackMapping)
533 self._dialButton.connect("clicked", self._on_dial_clicked)
534 self._smsButton.connect("clicked", self._on_sms_clicked)
536 self._originalLabel = self._backButton.get_label()
537 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
538 self._backTapHandler.on_tap = self._on_backspace
539 self._backTapHandler.on_hold = self._on_clearall
540 self._backTapHandler.on_holding = self._set_clear_button
541 self._backTapHandler.on_cancel = self._reset_back_button
543 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
544 self._keyPressEventId = 0
547 self._dialButton.grab_focus()
548 self._backTapHandler.enable()
549 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
552 self._window.disconnect(self._keyPressEventId)
553 self._keyPressEventId = 0
554 self._reset_back_button()
555 self._backTapHandler.disable()
557 def number_selected(self, action, number, message):
559 @note Actual dial function is patched in later
561 raise NotImplementedError("Horrible unknown error has occurred")
563 def get_number(self):
564 return self._phonenumber
566 def set_number(self, number):
568 Set the number to dial
571 self._phonenumber = make_ugly(number)
572 self._prettynumber = make_pretty(self._phonenumber)
573 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
575 self._errorDisplay.push_exception()
584 def load_settings(self, config, section):
587 def save_settings(self, config, section):
589 @note Thread Agnostic
593 def _on_key_press(self, widget, event):
595 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
596 contents = self._clipboard.wait_for_text()
597 if contents is not None:
598 self.set_number(contents)
600 self._errorDisplay.push_exception()
602 def _on_sms_clicked(self, widget):
604 action = PhoneTypeSelector.ACTION_SEND_SMS
605 phoneNumber = self.get_number()
607 message = self._smsDialog.run(phoneNumber, (), self._window)
610 action = PhoneTypeSelector.ACTION_CANCEL
612 if action == PhoneTypeSelector.ACTION_CANCEL:
614 self.number_selected(action, phoneNumber, message)
616 self._errorDisplay.push_exception()
618 def _on_dial_clicked(self, widget):
620 action = PhoneTypeSelector.ACTION_DIAL
621 phoneNumber = self.get_number()
623 self.number_selected(action, phoneNumber, message)
625 self._errorDisplay.push_exception()
627 def _on_digit_clicked(self, widget):
629 self.set_number(self._phonenumber + widget.get_name()[-1])
631 self._errorDisplay.push_exception()
633 def _on_backspace(self, taps):
635 self.set_number(self._phonenumber[:-taps])
636 self._reset_back_button()
638 self._errorDisplay.push_exception()
640 def _on_clearall(self, taps):
643 self._reset_back_button()
645 self._errorDisplay.push_exception()
648 def _set_clear_button(self):
650 self._backButton.set_label("gtk-clear")
652 self._errorDisplay.push_exception()
654 def _reset_back_button(self):
656 self._backButton.set_label(self._originalLabel)
658 self._errorDisplay.push_exception()
661 class AccountInfo(object):
663 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
664 self._errorDisplay = errorDisplay
665 self._backend = backend
666 self._isPopulated = False
667 self._alarmHandler = alarmHandler
668 self._notifyOnMissed = False
669 self._notifyOnVoicemail = False
670 self._notifyOnSms = False
672 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
673 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
674 self._callbackCombo = widgetTree.get_widget("callbackcombo")
675 self._onCallbackentryChangedId = 0
677 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
678 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
679 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
680 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
681 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
682 self._onNotifyToggled = 0
683 self._onMinutesChanged = 0
684 self._onMissedToggled = 0
685 self._onVoicemailToggled = 0
686 self._onSmsToggled = 0
687 self._applyAlarmTimeoutId = None
689 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
690 self._defaultCallback = ""
693 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
695 self._accountViewNumberDisplay.set_use_markup(True)
696 self.set_account_number("")
698 self._callbackList.clear()
699 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
701 if self._alarmHandler is not None:
702 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
703 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
704 self._missedCheckbox.set_active(self._notifyOnMissed)
705 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
706 self._smsCheckbox.set_active(self._notifyOnSms)
708 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
709 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
710 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
711 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
712 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
714 self._notifyCheckbox.set_sensitive(False)
715 self._minutesEntryButton.set_sensitive(False)
716 self._missedCheckbox.set_sensitive(False)
717 self._voicemailCheckbox.set_sensitive(False)
718 self._smsCheckbox.set_sensitive(False)
720 self.update(force=True)
723 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
724 self._onCallbackentryChangedId = 0
726 if self._alarmHandler is not None:
727 self._notifyCheckbox.disconnect(self._onNotifyToggled)
728 self._minutesEntryButton.disconnect(self._onMinutesChanged)
729 self._missedCheckbox.disconnect(self._onNotifyToggled)
730 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
731 self._smsCheckbox.disconnect(self._onNotifyToggled)
732 self._onNotifyToggled = 0
733 self._onMinutesChanged = 0
734 self._onMissedToggled = 0
735 self._onVoicemailToggled = 0
736 self._onSmsToggled = 0
738 self._notifyCheckbox.set_sensitive(True)
739 self._minutesEntryButton.set_sensitive(True)
740 self._missedCheckbox.set_sensitive(True)
741 self._voicemailCheckbox.set_sensitive(True)
742 self._smsCheckbox.set_sensitive(True)
745 self._callbackList.clear()
747 def get_selected_callback_number(self):
748 return make_ugly(self._callbackCombo.get_child().get_text())
750 def set_account_number(self, number):
752 Displays current account number
754 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
756 def update(self, force = False):
757 if not force and self._isPopulated:
759 self._populate_callback_combo()
760 self.set_account_number(self._backend.get_account_number())
764 self._callbackCombo.get_child().set_text("")
765 self.set_account_number("")
766 self._isPopulated = False
768 def save_everything(self):
769 raise NotImplementedError
773 return "Account Info"
775 def load_settings(self, config, section):
776 self._defaultCallback = config.get(section, "callback")
777 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
778 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
779 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
781 def save_settings(self, config, section):
783 @note Thread Agnostic
785 callback = self.get_selected_callback_number()
786 config.set(section, "callback", callback)
787 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
788 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
789 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
791 def _populate_callback_combo(self):
792 self._isPopulated = True
793 self._callbackList.clear()
795 callbackNumbers = self._backend.get_callback_numbers()
797 self._errorDisplay.push_exception()
798 self._isPopulated = False
801 for number, description in callbackNumbers.iteritems():
802 self._callbackList.append((make_pretty(number),))
804 self._callbackCombo.set_model(self._callbackList)
805 self._callbackCombo.set_text_column(0)
806 #callbackNumber = self._backend.get_callback_number()
807 callbackNumber = self._defaultCallback
808 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
810 def _set_callback_number(self, number):
812 if not self._backend.is_valid_syntax(number) and 0 < len(number):
813 self._errorDisplay.push_message("%s is not a valid callback number" % number)
814 elif number == self._backend.get_callback_number():
816 "Callback number already is %s" % (
817 self._backend.get_callback_number(),
821 self._backend.set_callback_number(number)
822 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
823 make_pretty(number), make_pretty(self._backend.get_callback_number())
826 "Callback number set to %s" % (
827 self._backend.get_callback_number(),
831 self._errorDisplay.push_exception()
833 def _update_alarm_settings(self, recurrence):
835 isEnabled = self._notifyCheckbox.get_active()
836 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
837 self._alarmHandler.apply_settings(isEnabled, recurrence)
839 self.save_everything()
840 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
841 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
843 def _on_callbackentry_changed(self, *args):
845 text = self.get_selected_callback_number()
846 number = make_ugly(text)
847 self._set_callback_number(number)
849 self._errorDisplay.push_exception()
851 def _on_notify_toggled(self, *args):
853 if self._applyAlarmTimeoutId is not None:
854 gobject.source_remove(self._applyAlarmTimeoutId)
855 self._applyAlarmTimeoutId = None
856 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
858 self._errorDisplay.push_exception()
860 def _on_minutes_clicked(self, *args):
861 recurrenceChoices = [
873 actualSelection = self._alarmHandler.recurrence
875 closestSelectionIndex = 0
876 for i, possible in enumerate(recurrenceChoices):
877 if possible[0] <= actualSelection:
878 closestSelectionIndex = i
879 recurrenceIndex = hildonize.touch_selector(
882 (("%s" % m[1]) for m in recurrenceChoices),
883 closestSelectionIndex,
885 recurrence = recurrenceChoices[recurrenceIndex][0]
887 self._update_alarm_settings(recurrence)
889 self._errorDisplay.push_exception()
891 def _on_apply_timeout(self, *args):
893 self._applyAlarmTimeoutId = None
895 self._update_alarm_settings(self._alarmHandler.recurrence)
897 self._errorDisplay.push_exception()
900 def _on_missed_toggled(self, *args):
902 self._notifyOnMissed = self._missedCheckbox.get_active()
903 self.save_everything()
905 self._errorDisplay.push_exception()
907 def _on_voicemail_toggled(self, *args):
909 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
910 self.save_everything()
912 self._errorDisplay.push_exception()
914 def _on_sms_toggled(self, *args):
916 self._notifyOnSms = self._smsCheckbox.get_active()
917 self.save_everything()
919 self._errorDisplay.push_exception()
922 class RecentCallsView(object):
929 def __init__(self, widgetTree, backend, errorDisplay):
930 self._errorDisplay = errorDisplay
931 self._backend = backend
933 self._isPopulated = False
934 self._recentmodel = gtk.ListStore(
935 gobject.TYPE_STRING, # number
936 gobject.TYPE_STRING, # date
937 gobject.TYPE_STRING, # action
938 gobject.TYPE_STRING, # from
940 self._recentview = widgetTree.get_widget("recentview")
941 self._recentviewselection = None
942 self._onRecentviewRowActivatedId = 0
944 textrenderer = gtk.CellRendererText()
945 textrenderer.set_property("yalign", 0)
946 self._dateColumn = gtk.TreeViewColumn("Date")
947 self._dateColumn.pack_start(textrenderer, expand=True)
948 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
950 textrenderer = gtk.CellRendererText()
951 textrenderer.set_property("yalign", 0)
952 self._actionColumn = gtk.TreeViewColumn("Action")
953 self._actionColumn.pack_start(textrenderer, expand=True)
954 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
956 textrenderer = gtk.CellRendererText()
957 textrenderer.set_property("yalign", 0)
958 hildonize.set_cell_thumb_selectable(textrenderer)
959 self._nameColumn = gtk.TreeViewColumn("From")
960 self._nameColumn.pack_start(textrenderer, expand=True)
961 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
962 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
964 textrenderer = gtk.CellRendererText()
965 textrenderer.set_property("yalign", 0)
966 self._numberColumn = gtk.TreeViewColumn("Number")
967 self._numberColumn.pack_start(textrenderer, expand=True)
968 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
970 self._window = gtk_toolbox.find_parent_window(self._recentview)
971 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
973 self._updateSink = gtk_toolbox.threaded_stage(
975 self._idly_populate_recentview,
976 gtk_toolbox.null_sink(),
981 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
982 self._recentview.set_model(self._recentmodel)
984 self._recentview.append_column(self._dateColumn)
985 self._recentview.append_column(self._actionColumn)
986 self._recentview.append_column(self._numberColumn)
987 self._recentview.append_column(self._nameColumn)
988 self._recentviewselection = self._recentview.get_selection()
989 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
991 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
994 self._recentview.disconnect(self._onRecentviewRowActivatedId)
998 self._recentview.remove_column(self._dateColumn)
999 self._recentview.remove_column(self._actionColumn)
1000 self._recentview.remove_column(self._nameColumn)
1001 self._recentview.remove_column(self._numberColumn)
1002 self._recentview.set_model(None)
1004 def number_selected(self, action, number, message):
1006 @note Actual dial function is patched in later
1008 raise NotImplementedError("Horrible unknown error has occurred")
1010 def update(self, force = False):
1011 if not force and self._isPopulated:
1013 self._updateSink.send(())
1017 self._isPopulated = False
1018 self._recentmodel.clear()
1022 return "Recent Calls"
1024 def load_settings(self, config, section):
1027 def save_settings(self, config, section):
1029 @note Thread Agnostic
1033 def _idly_populate_recentview(self):
1034 with gtk_toolbox.gtk_lock():
1035 banner = hildonize.show_busy_banner_start(self._window, "Loading Recent History")
1037 self._recentmodel.clear()
1038 self._isPopulated = True
1041 recentItems = self._backend.get_recent()
1042 except Exception, e:
1043 self._errorDisplay.push_exception_with_lock()
1044 self._isPopulated = False
1047 for personName, phoneNumber, date, action in recentItems:
1049 personName = "Unknown"
1050 date = abbrev_relative_date(date)
1051 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1052 prettyNumber = make_pretty(prettyNumber)
1053 item = (prettyNumber, date, action.capitalize(), personName)
1054 with gtk_toolbox.gtk_lock():
1055 self._recentmodel.append(item)
1056 except Exception, e:
1057 self._errorDisplay.push_exception_with_lock()
1059 with gtk_toolbox.gtk_lock():
1060 hildonize.show_busy_banner_end(banner)
1064 def _on_recentview_row_activated(self, treeview, path, view_column):
1066 model, itr = self._recentviewselection.get_selected()
1070 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1071 number = make_ugly(number)
1072 contactPhoneNumbers = [("Phone", number)]
1073 description = self._recentmodel.get_value(itr, self.FROM_IDX)
1075 action, phoneNumber, message = self._phoneTypeSelector.run(
1076 contactPhoneNumbers,
1077 messages = (description, ),
1078 parent = self._window,
1080 if action == PhoneTypeSelector.ACTION_CANCEL:
1082 assert phoneNumber, "A lack of phone number exists"
1084 self.number_selected(action, phoneNumber, message)
1085 self._recentviewselection.unselect_all()
1086 except Exception, e:
1087 self._errorDisplay.push_exception()
1090 class MessagesView(object):
1098 def __init__(self, widgetTree, backend, errorDisplay):
1099 self._errorDisplay = errorDisplay
1100 self._backend = backend
1102 self._isPopulated = False
1103 self._messagemodel = gtk.ListStore(
1104 gobject.TYPE_STRING, # number
1105 gobject.TYPE_STRING, # date
1106 gobject.TYPE_STRING, # header
1107 gobject.TYPE_STRING, # message
1110 self._messageview = widgetTree.get_widget("messages_view")
1111 self._messageviewselection = None
1112 self._onMessageviewRowActivatedId = 0
1114 self._messageRenderer = gtk.CellRendererText()
1115 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1116 self._messageRenderer.set_property("wrap-width", 500)
1117 self._messageColumn = gtk.TreeViewColumn("Messages")
1118 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1119 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1120 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1122 self._window = gtk_toolbox.find_parent_window(self._messageview)
1123 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1125 self._updateSink = gtk_toolbox.threaded_stage(
1127 self._idly_populate_messageview,
1128 gtk_toolbox.null_sink(),
1133 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1134 self._messageview.set_model(self._messagemodel)
1136 self._messageview.append_column(self._messageColumn)
1137 self._messageviewselection = self._messageview.get_selection()
1138 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1140 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1143 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1147 self._messageview.remove_column(self._messageColumn)
1148 self._messageview.set_model(None)
1150 def number_selected(self, action, number, message):
1152 @note Actual dial function is patched in later
1154 raise NotImplementedError("Horrible unknown error has occurred")
1156 def update(self, force = False):
1157 if not force and self._isPopulated:
1159 self._updateSink.send(())
1163 self._isPopulated = False
1164 self._messagemodel.clear()
1170 def load_settings(self, config, section):
1173 def save_settings(self, config, section):
1175 @note Thread Agnostic
1179 def _idly_populate_messageview(self):
1180 with gtk_toolbox.gtk_lock():
1181 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1183 self._messagemodel.clear()
1184 self._isPopulated = True
1187 messageItems = self._backend.get_messages()
1188 except Exception, e:
1189 self._errorDisplay.push_exception_with_lock()
1190 self._isPopulated = False
1193 for header, number, relativeDate, messages in messageItems:
1194 prettyNumber = number[2:] if number.startswith("+1") else number
1195 prettyNumber = make_pretty(prettyNumber)
1197 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1198 newMessages = [firstMessage]
1199 newMessages.extend(messages)
1201 number = make_ugly(number)
1203 row = (number, relativeDate, header, "\n".join(newMessages), newMessages)
1204 with gtk_toolbox.gtk_lock():
1205 self._messagemodel.append(row)
1206 except Exception, e:
1207 self._errorDisplay.push_exception_with_lock()
1209 with gtk_toolbox.gtk_lock():
1210 hildonize.show_busy_banner_end(banner)
1214 def _on_messageview_row_activated(self, treeview, path, view_column):
1216 model, itr = self._messageviewselection.get_selected()
1220 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1221 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1223 action, phoneNumber, message = self._phoneTypeSelector.run(
1224 contactPhoneNumbers,
1225 messages = description,
1226 parent = self._window,
1228 if action == PhoneTypeSelector.ACTION_CANCEL:
1230 assert phoneNumber, "A lock of phone number exists"
1232 self.number_selected(action, phoneNumber, message)
1233 self._messageviewselection.unselect_all()
1234 except Exception, e:
1235 self._errorDisplay.push_exception()
1238 class ContactsView(object):
1240 def __init__(self, widgetTree, backend, errorDisplay):
1241 self._errorDisplay = errorDisplay
1242 self._backend = backend
1244 self._addressBook = None
1245 self._selectedComboIndex = 0
1246 self._addressBookFactories = [null_backend.NullAddressBook()]
1248 self._booksList = []
1249 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1251 self._isPopulated = False
1252 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1253 self._contactsviewselection = None
1254 self._contactsview = widgetTree.get_widget("contactsview")
1256 self._contactColumn = gtk.TreeViewColumn("Contact")
1257 displayContactSource = False
1258 if displayContactSource:
1259 textrenderer = gtk.CellRendererText()
1260 self._contactColumn.pack_start(textrenderer, expand=False)
1261 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1262 textrenderer = gtk.CellRendererText()
1263 hildonize.set_cell_thumb_selectable(textrenderer)
1264 self._contactColumn.pack_start(textrenderer, expand=True)
1265 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1266 textrenderer = gtk.CellRendererText()
1267 self._contactColumn.pack_start(textrenderer, expand=True)
1268 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1269 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1270 self._contactColumn.set_sort_column_id(1)
1271 self._contactColumn.set_visible(True)
1273 self._onContactsviewRowActivatedId = 0
1274 self._onAddressbookButtonChangedId = 0
1275 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1276 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1278 self._updateSink = gtk_toolbox.threaded_stage(
1280 self._idly_populate_contactsview,
1281 gtk_toolbox.null_sink(),
1286 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1288 self._contactsview.set_model(self._contactsmodel)
1289 self._contactsview.append_column(self._contactColumn)
1290 self._contactsviewselection = self._contactsview.get_selection()
1291 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1293 del self._booksList[:]
1294 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1295 if factoryName and bookName:
1296 entryName = "%s: %s" % (factoryName, bookName)
1298 entryName = factoryName
1300 entryName = bookName
1302 entryName = "Bad name (%d)" % factoryId
1303 row = (str(factoryId), bookId, entryName)
1304 self._booksList.append(row)
1306 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1307 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1309 if len(self._booksList) <= self._selectedComboIndex:
1310 self._selectedComboIndex = 0
1311 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1313 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1314 selectedBookId = self._booksList[self._selectedComboIndex][1]
1315 self.open_addressbook(selectedFactoryId, selectedBookId)
1318 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1319 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1323 self._bookSelectionButton.set_label("")
1324 self._contactsview.set_model(None)
1325 self._contactsview.remove_column(self._contactColumn)
1327 def number_selected(self, action, number, message):
1329 @note Actual dial function is patched in later
1331 raise NotImplementedError("Horrible unknown error has occurred")
1333 def get_addressbooks(self):
1335 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1337 for i, factory in enumerate(self._addressBookFactories):
1338 for bookFactory, bookId, bookName in factory.get_addressbooks():
1339 yield (str(i), bookId), (factory.factory_name(), bookName)
1341 def open_addressbook(self, bookFactoryId, bookId):
1342 bookFactoryIndex = int(bookFactoryId)
1343 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1345 forceUpdate = True if addressBook is not self._addressBook else False
1347 self._addressBook = addressBook
1348 self.update(force=forceUpdate)
1350 def update(self, force = False):
1351 if not force and self._isPopulated:
1353 self._updateSink.send(())
1357 self._isPopulated = False
1358 self._contactsmodel.clear()
1359 for factory in self._addressBookFactories:
1360 factory.clear_caches()
1361 self._addressBook.clear_caches()
1363 def append(self, book):
1364 self._addressBookFactories.append(book)
1366 def extend(self, books):
1367 self._addressBookFactories.extend(books)
1373 def load_settings(self, config, sectionName):
1375 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1376 except ConfigParser.NoOptionError:
1377 self._selectedComboIndex = 0
1379 def save_settings(self, config, sectionName):
1380 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1382 def _idly_populate_contactsview(self):
1383 with gtk_toolbox.gtk_lock():
1384 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1387 while addressBook is not self._addressBook:
1388 addressBook = self._addressBook
1389 with gtk_toolbox.gtk_lock():
1390 self._contactsview.set_model(None)
1394 contacts = addressBook.get_contacts()
1395 except Exception, e:
1397 self._isPopulated = False
1398 self._errorDisplay.push_exception_with_lock()
1399 for contactId, contactName in contacts:
1400 contactType = (addressBook.contact_source_short_name(contactId), )
1401 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1403 with gtk_toolbox.gtk_lock():
1404 self._contactsview.set_model(self._contactsmodel)
1406 self._isPopulated = True
1407 except Exception, e:
1408 self._errorDisplay.push_exception_with_lock()
1410 with gtk_toolbox.gtk_lock():
1411 hildonize.show_busy_banner_end(banner)
1414 def _on_addressbook_button_changed(self, *args, **kwds):
1417 newSelectedComboIndex = hildonize.touch_selector(
1420 (("%s" % m[2]) for m in self._booksList),
1421 self._selectedComboIndex,
1423 except RuntimeError:
1426 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1427 selectedBookId = self._booksList[newSelectedComboIndex][1]
1428 self.open_addressbook(selectedFactoryId, selectedBookId)
1429 self._selectedComboIndex = newSelectedComboIndex
1430 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1431 except Exception, e:
1432 self._errorDisplay.push_exception()
1434 def _on_contactsview_row_activated(self, treeview, path, view_column):
1436 model, itr = self._contactsviewselection.get_selected()
1440 contactId = self._contactsmodel.get_value(itr, 3)
1441 contactName = self._contactsmodel.get_value(itr, 1)
1443 contactDetails = self._addressBook.get_contact_details(contactId)
1444 except Exception, e:
1446 self._errorDisplay.push_exception()
1447 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1449 if len(contactPhoneNumbers) == 0:
1452 action, phoneNumber, message = self._phoneTypeSelector.run(
1453 contactPhoneNumbers,
1454 messages = (contactName, ),
1455 parent = self._window,
1457 if action == PhoneTypeSelector.ACTION_CANCEL:
1459 assert phoneNumber, "A lack of phone number exists"
1461 self.number_selected(action, phoneNumber, message)
1462 self._contactsviewselection.unselect_all()
1463 except Exception, e:
1464 self._errorDisplay.push_exception()