4 DialCentral - Front end for Google's GoogleVoice 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 Alternate UI for dialogs (stackables)
24 from __future__ import with_statement
38 def make_ugly(prettynumber):
40 function to take a phone number and strip out all non-numeric
43 >>> make_ugly("+012-(345)-678-90")
47 uglynumber = re.sub('\D', '', prettynumber)
51 def make_pretty(phonenumber):
53 Function to take a phone number and return the pretty version
55 if phonenumber begins with 0:
57 if phonenumber begins with 1: ( for gizmo callback numbers )
59 if phonenumber is 13 digits:
61 if phonenumber is 10 digits:
65 >>> make_pretty("1234567")
67 >>> make_pretty("2345678901")
69 >>> make_pretty("12345678901")
71 >>> make_pretty("01234567890")
74 if phonenumber is None or phonenumber is "":
77 phonenumber = make_ugly(phonenumber)
79 if len(phonenumber) < 3:
82 if phonenumber[0] == "0":
84 prettynumber += "+%s" % phonenumber[0:3]
85 if 3 < len(phonenumber):
86 prettynumber += "-(%s)" % phonenumber[3:6]
87 if 6 < len(phonenumber):
88 prettynumber += "-%s" % phonenumber[6:9]
89 if 9 < len(phonenumber):
90 prettynumber += "-%s" % phonenumber[9:]
92 elif len(phonenumber) <= 7:
93 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
94 elif len(phonenumber) > 8 and phonenumber[0] == "1":
95 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
96 elif len(phonenumber) > 7:
97 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
101 def abbrev_relative_date(date):
103 >>> abbrev_relative_date("42 hours ago")
105 >>> abbrev_relative_date("2 days ago")
107 >>> abbrev_relative_date("4 weeks ago")
110 parts = date.split(" ")
111 return "%s %s" % (parts[0], parts[1][0])
114 class MergedAddressBook(object):
116 Merger of all addressbooks
119 def __init__(self, addressbookFactories, sorter = None):
120 self.__addressbookFactories = addressbookFactories
121 self.__addressbooks = None
122 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
124 def clear_caches(self):
125 self.__addressbooks = None
126 for factory in self.__addressbookFactories:
127 factory.clear_caches()
129 def get_addressbooks(self):
131 @returns Iterable of (Address Book Factory, Book Id, Book Name)
135 def open_addressbook(self, bookId):
138 def contact_source_short_name(self, contactId):
139 if self.__addressbooks is None:
141 bookIndex, originalId = contactId.split("-", 1)
142 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
146 return "All Contacts"
148 def get_contacts(self):
150 @returns Iterable of (contact id, contact name)
152 if self.__addressbooks is None:
153 self.__addressbooks = list(
154 factory.open_addressbook(id)
155 for factory in self.__addressbookFactories
156 for (f, id, name) in factory.get_addressbooks()
159 ("-".join([str(bookIndex), contactId]), contactName)
160 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
161 for (contactId, contactName) in addressbook.get_contacts()
163 sortedContacts = self.__sort_contacts(contacts)
164 return sortedContacts
166 def get_contact_details(self, contactId):
168 @returns Iterable of (Phone Type, Phone Number)
170 if self.__addressbooks is None:
172 bookIndex, originalId = contactId.split("-", 1)
173 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
176 def null_sorter(contacts):
178 Good for speed/low memory
183 def basic_firtname_sorter(contacts):
185 Expects names in "First Last" format
188 (contactName.rsplit(" ", 1)[0], (contactId, contactName))
189 for (contactId, contactName) in contacts
191 contactsWithKey.sort()
192 return (contactData for (lastName, contactData) in contactsWithKey)
195 def basic_lastname_sorter(contacts):
197 Expects names in "First Last" format
200 (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
201 for (contactId, contactName) in contacts
203 contactsWithKey.sort()
204 return (contactData for (lastName, contactData) in contactsWithKey)
207 def reversed_firtname_sorter(contacts):
209 Expects names in "Last, First" format
212 (contactName.split(", ", 1)[-1], (contactId, contactName))
213 for (contactId, contactName) in contacts
215 contactsWithKey.sort()
216 return (contactData for (lastName, contactData) in contactsWithKey)
219 def reversed_lastname_sorter(contacts):
221 Expects names in "Last, First" format
224 (contactName.split(", ", 1)[0], (contactId, contactName))
225 for (contactId, contactName) in contacts
227 contactsWithKey.sort()
228 return (contactData for (lastName, contactData) in contactsWithKey)
231 def guess_firstname(name):
233 return name.split(", ", 1)[-1]
235 return name.rsplit(" ", 1)[0]
238 def guess_lastname(name):
240 return name.split(", ", 1)[0]
242 return name.rsplit(" ", 1)[-1]
245 def advanced_firstname_sorter(cls, contacts):
247 (cls.guess_firstname(contactName), (contactId, contactName))
248 for (contactId, contactName) in contacts
250 contactsWithKey.sort()
251 return (contactData for (lastName, contactData) in contactsWithKey)
254 def advanced_lastname_sorter(cls, contacts):
256 (cls.guess_lastname(contactName), (contactId, contactName))
257 for (contactId, contactName) in contacts
259 contactsWithKey.sort()
260 return (contactData for (lastName, contactData) in contactsWithKey)
263 class PhoneTypeSelector(object):
265 ACTION_CANCEL = "cancel"
266 ACTION_SELECT = "select"
268 ACTION_SEND_SMS = "sms"
270 def __init__(self, widgetTree, gcBackend):
271 self._gcBackend = gcBackend
272 self._widgetTree = widgetTree
274 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
275 self._smsDialog = SmsEntryDialog(self._widgetTree)
277 self._smsButton = self._widgetTree.get_widget("sms_button")
278 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
280 self._dialButton = self._widgetTree.get_widget("dial_button")
281 self._dialButton.connect("clicked", self._on_phonetype_dial)
283 self._selectButton = self._widgetTree.get_widget("select_button")
284 self._selectButton.connect("clicked", self._on_phonetype_select)
286 self._cancelButton = self._widgetTree.get_widget("cancel_button")
287 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
289 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
290 self._messagesView = self._widgetTree.get_widget("phoneSelectionMessages")
291 self._scrollWindow = self._messagesView.get_parent()
293 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
294 self._typeviewselection = None
295 self._typeview = self._widgetTree.get_widget("phonetypes")
296 self._typeview.connect("row-activated", self._on_phonetype_select)
298 self._action = self.ACTION_CANCEL
300 def run(self, contactDetails, messages = (), parent = None):
301 self._action = self.ACTION_CANCEL
303 # Add the column to the phone selection tree view
304 self._typemodel.clear()
305 self._typeview.set_model(self._typemodel)
307 textrenderer = gtk.CellRendererText()
308 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
309 self._typeview.append_column(numberColumn)
311 textrenderer = gtk.CellRendererText()
312 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
313 self._typeview.append_column(typeColumn)
315 for phoneType, phoneNumber in contactDetails:
316 display = " - ".join((phoneNumber, phoneType))
318 row = (phoneNumber, display)
319 self._typemodel.append(row)
321 self._typeviewselection = self._typeview.get_selection()
322 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
323 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
325 # Add the column to the messages tree view
326 self._messagemodel.clear()
327 self._messagesView.set_model(self._messagemodel)
329 textrenderer = gtk.CellRendererText()
330 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
331 textrenderer.set_property("wrap-width", 450)
332 messageColumn = gtk.TreeViewColumn("")
333 messageColumn.pack_start(textrenderer, expand=True)
334 messageColumn.add_attribute(textrenderer, "markup", 0)
335 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
336 self._messagesView.append_column(messageColumn)
337 self._messagesView.set_headers_visible(False)
340 for message in messages:
342 self._messagemodel.append(row)
343 self._messagesView.show()
344 self._scrollWindow.show()
345 messagesSelection = self._messagesView.get_selection()
346 messagesSelection.select_path((len(messages)-1, ))
348 self._messagesView.hide()
349 self._scrollWindow.hide()
351 if parent is not None:
352 self._dialog.set_transient_for(parent)
357 self._messagesView.scroll_to_cell((len(messages)-1, ))
359 userResponse = self._dialog.run()
363 if userResponse == gtk.RESPONSE_OK:
364 phoneNumber = self._get_number()
365 phoneNumber = make_ugly(phoneNumber)
369 self._action = self.ACTION_CANCEL
371 if self._action == self.ACTION_SEND_SMS:
372 smsMessage = self._smsDialog.run(phoneNumber, messages, parent)
375 self._action = self.ACTION_CANCEL
379 self._messagesView.remove_column(messageColumn)
380 self._messagesView.set_model(None)
382 self._typeviewselection.unselect_all()
383 self._typeview.remove_column(numberColumn)
384 self._typeview.remove_column(typeColumn)
385 self._typeview.set_model(None)
387 return self._action, phoneNumber, smsMessage
389 def _get_number(self):
390 model, itr = self._typeviewselection.get_selected()
394 phoneNumber = self._typemodel.get_value(itr, 0)
397 def _on_phonetype_dial(self, *args):
398 self._dialog.response(gtk.RESPONSE_OK)
399 self._action = self.ACTION_DIAL
401 def _on_phonetype_send_sms(self, *args):
402 self._dialog.response(gtk.RESPONSE_OK)
403 self._action = self.ACTION_SEND_SMS
405 def _on_phonetype_select(self, *args):
406 self._dialog.response(gtk.RESPONSE_OK)
407 self._action = self.ACTION_SELECT
409 def _on_phonetype_cancel(self, *args):
410 self._dialog.response(gtk.RESPONSE_CANCEL)
411 self._action = self.ACTION_CANCEL
414 class SmsEntryDialog(object):
416 @todo Add multi-SMS messages like GoogleVoice
421 def __init__(self, widgetTree):
422 self._widgetTree = widgetTree
423 self._dialog = self._widgetTree.get_widget("smsDialog")
425 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
426 self._smsButton.connect("clicked", self._on_send)
428 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
429 self._cancelButton.connect("clicked", self._on_cancel)
431 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
433 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
434 self._messagesView = self._widgetTree.get_widget("smsMessages")
435 self._scrollWindow = self._messagesView.get_parent()
437 self._smsEntry = self._widgetTree.get_widget("smsEntry")
438 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
440 def run(self, number, messages = (), parent = None):
441 # Add the column to the messages tree view
442 self._messagemodel.clear()
443 self._messagesView.set_model(self._messagemodel)
445 textrenderer = gtk.CellRendererText()
446 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
447 textrenderer.set_property("wrap-width", 450)
448 messageColumn = gtk.TreeViewColumn("")
449 messageColumn.pack_start(textrenderer, expand=True)
450 messageColumn.add_attribute(textrenderer, "markup", 0)
451 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
452 self._messagesView.append_column(messageColumn)
453 self._messagesView.set_headers_visible(False)
456 for message in messages:
458 self._messagemodel.append(row)
459 self._messagesView.show()
460 self._scrollWindow.show()
461 messagesSelection = self._messagesView.get_selection()
462 messagesSelection.select_path((len(messages)-1, ))
464 self._messagesView.hide()
465 self._scrollWindow.hide()
467 self._smsEntry.get_buffer().set_text("")
468 self._update_letter_count()
470 if parent is not None:
471 self._dialog.set_transient_for(parent)
476 self._messagesView.scroll_to_cell((len(messages)-1, ))
477 self._smsEntry.grab_focus()
479 userResponse = self._dialog.run()
483 if userResponse == gtk.RESPONSE_OK:
484 entryBuffer = self._smsEntry.get_buffer()
485 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
486 enteredMessage = enteredMessage[0:self.MAX_CHAR]
490 self._messagesView.remove_column(messageColumn)
491 self._messagesView.set_model(None)
493 return enteredMessage.strip()
495 def _update_letter_count(self, *args):
496 entryLength = self._smsEntry.get_buffer().get_char_count()
497 charsLeft = self.MAX_CHAR - entryLength
498 self._letterCountLabel.set_text(str(charsLeft))
500 self._smsButton.set_sensitive(False)
502 self._smsButton.set_sensitive(True)
504 def _on_entry_changed(self, *args):
505 self._update_letter_count()
507 def _on_send(self, *args):
508 self._dialog.response(gtk.RESPONSE_OK)
510 def _on_cancel(self, *args):
511 self._dialog.response(gtk.RESPONSE_CANCEL)
514 class Dialpad(object):
516 def __init__(self, widgetTree, errorDisplay):
517 self._clipboard = gtk.clipboard_get()
518 self._errorDisplay = errorDisplay
519 self._smsDialog = SmsEntryDialog(widgetTree)
521 self._numberdisplay = widgetTree.get_widget("numberdisplay")
522 self._smsButton = widgetTree.get_widget("sms")
523 self._dialButton = widgetTree.get_widget("dial")
524 self._backButton = widgetTree.get_widget("back")
525 self._phonenumber = ""
526 self._prettynumber = ""
529 "on_digit_clicked": self._on_digit_clicked,
531 widgetTree.signal_autoconnect(callbackMapping)
532 self._dialButton.connect("clicked", self._on_dial_clicked)
533 self._smsButton.connect("clicked", self._on_sms_clicked)
535 self._originalLabel = self._backButton.get_label()
536 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
537 self._backTapHandler.on_tap = self._on_backspace
538 self._backTapHandler.on_hold = self._on_clearall
539 self._backTapHandler.on_holding = self._set_clear_button
540 self._backTapHandler.on_cancel = self._reset_back_button
542 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
543 self._keyPressEventId = 0
546 self._dialButton.grab_focus()
547 self._backTapHandler.enable()
548 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
551 self._window.disconnect(self._keyPressEventId)
552 self._keyPressEventId = 0
553 self._reset_back_button()
554 self._backTapHandler.disable()
556 def number_selected(self, action, number, message):
558 @note Actual dial function is patched in later
560 raise NotImplementedError("Horrible unknown error has occurred")
562 def get_number(self):
563 return self._phonenumber
565 def set_number(self, number):
567 Set the number to dial
570 self._phonenumber = make_ugly(number)
571 self._prettynumber = make_pretty(self._phonenumber)
572 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
574 self._errorDisplay.push_exception()
583 def load_settings(self, config, section):
586 def save_settings(self, config, section):
588 @note Thread Agnostic
592 def _on_key_press(self, widget, event):
594 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
595 contents = self._clipboard.wait_for_text()
596 if contents is not None:
597 self.set_number(contents)
599 self._errorDisplay.push_exception()
601 def _on_sms_clicked(self, widget):
603 action = PhoneTypeSelector.ACTION_SEND_SMS
604 phoneNumber = self.get_number()
606 message = self._smsDialog.run(phoneNumber, (), self._window)
609 action = PhoneTypeSelector.ACTION_CANCEL
611 if action == PhoneTypeSelector.ACTION_CANCEL:
613 self.number_selected(action, phoneNumber, message)
615 self._errorDisplay.push_exception()
617 def _on_dial_clicked(self, widget):
619 action = PhoneTypeSelector.ACTION_DIAL
620 phoneNumber = self.get_number()
622 self.number_selected(action, phoneNumber, message)
624 self._errorDisplay.push_exception()
626 def _on_digit_clicked(self, widget):
628 self.set_number(self._phonenumber + widget.get_name()[-1])
630 self._errorDisplay.push_exception()
632 def _on_backspace(self, taps):
634 self.set_number(self._phonenumber[:-taps])
635 self._reset_back_button()
637 self._errorDisplay.push_exception()
639 def _on_clearall(self, taps):
642 self._reset_back_button()
644 self._errorDisplay.push_exception()
647 def _set_clear_button(self):
649 self._backButton.set_label("gtk-clear")
651 self._errorDisplay.push_exception()
653 def _reset_back_button(self):
655 self._backButton.set_label(self._originalLabel)
657 self._errorDisplay.push_exception()
660 class AccountInfo(object):
662 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
663 self._errorDisplay = errorDisplay
664 self._backend = backend
665 self._isPopulated = False
666 self._alarmHandler = alarmHandler
667 self._notifyOnMissed = False
668 self._notifyOnVoicemail = False
669 self._notifyOnSms = False
671 self._callbackList = []
672 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
673 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
674 self._onCallbackSelectChangedId = 0
676 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
677 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
678 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
679 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
680 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
681 self._onNotifyToggled = 0
682 self._onMinutesChanged = 0
683 self._onMissedToggled = 0
684 self._onVoicemailToggled = 0
685 self._onSmsToggled = 0
686 self._applyAlarmTimeoutId = None
688 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
689 self._defaultCallback = ""
692 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
694 self._accountViewNumberDisplay.set_use_markup(True)
695 self.set_account_number("")
697 del self._callbackList[:]
698 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
700 if self._alarmHandler is not None:
701 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
702 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
703 self._missedCheckbox.set_active(self._notifyOnMissed)
704 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
705 self._smsCheckbox.set_active(self._notifyOnSms)
707 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
708 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
709 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
710 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
711 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
713 self._notifyCheckbox.set_sensitive(False)
714 self._minutesEntryButton.set_sensitive(False)
715 self._missedCheckbox.set_sensitive(False)
716 self._voicemailCheckbox.set_sensitive(False)
717 self._smsCheckbox.set_sensitive(False)
719 self.update(force=True)
722 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
723 self._onCallbackSelectChangedId = 0
725 if self._alarmHandler is not None:
726 self._notifyCheckbox.disconnect(self._onNotifyToggled)
727 self._minutesEntryButton.disconnect(self._onMinutesChanged)
728 self._missedCheckbox.disconnect(self._onNotifyToggled)
729 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
730 self._smsCheckbox.disconnect(self._onNotifyToggled)
731 self._onNotifyToggled = 0
732 self._onMinutesChanged = 0
733 self._onMissedToggled = 0
734 self._onVoicemailToggled = 0
735 self._onSmsToggled = 0
737 self._notifyCheckbox.set_sensitive(True)
738 self._minutesEntryButton.set_sensitive(True)
739 self._missedCheckbox.set_sensitive(True)
740 self._voicemailCheckbox.set_sensitive(True)
741 self._smsCheckbox.set_sensitive(True)
744 del self._callbackList[:]
746 def get_selected_callback_number(self):
747 currentLabel = self._callbackSelectButton.get_label()
748 if currentLabel is not None:
749 return make_ugly(currentLabel)
753 def set_account_number(self, number):
755 Displays current account number
757 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
759 def update(self, force = False):
760 if not force and self._isPopulated:
762 self._populate_callback_combo()
763 self.set_account_number(self._backend.get_account_number())
767 self._callbackSelectButton.set_label("")
768 self.set_account_number("")
769 self._isPopulated = False
771 def save_everything(self):
772 raise NotImplementedError
776 return "Account Info"
778 def load_settings(self, config, section):
779 self._defaultCallback = config.get(section, "callback")
780 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
781 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
782 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
784 def save_settings(self, config, section):
786 @note Thread Agnostic
788 callback = self.get_selected_callback_number()
789 config.set(section, "callback", callback)
790 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
791 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
792 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
794 def _populate_callback_combo(self):
795 self._isPopulated = True
796 del self._callbackList[:]
798 callbackNumbers = self._backend.get_callback_numbers()
800 self._errorDisplay.push_exception()
801 self._isPopulated = False
804 for number, description in callbackNumbers.iteritems():
805 self._callbackList.append(make_pretty(number))
807 if not self.get_selected_callback_number():
808 self._set_callback_number(self._defaultCallback)
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())
825 self._callbackSelectButton.set_label(make_pretty(number))
827 "Callback number set to %s" % (
828 self._backend.get_callback_number(),
832 self._errorDisplay.push_exception()
834 def _update_alarm_settings(self, recurrence):
836 isEnabled = self._notifyCheckbox.get_active()
837 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
838 self._alarmHandler.apply_settings(isEnabled, recurrence)
840 self.save_everything()
841 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
842 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
844 def _on_callbackentry_clicked(self, *args):
846 actualSelection = make_pretty(self.get_selected_callback_number())
848 userSelection = hildonize.touch_selector_entry(
854 number = make_ugly(userSelection)
855 self._set_callback_number(number)
856 except RuntimeError, e:
857 logging.exception("%s" % str(e))
859 self._errorDisplay.push_exception()
861 def _on_notify_toggled(self, *args):
863 if self._applyAlarmTimeoutId is not None:
864 gobject.source_remove(self._applyAlarmTimeoutId)
865 self._applyAlarmTimeoutId = None
866 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
868 self._errorDisplay.push_exception()
870 def _on_minutes_clicked(self, *args):
871 recurrenceChoices = [
887 actualSelection = self._alarmHandler.recurrence
889 closestSelectionIndex = 0
890 for i, possible in enumerate(recurrenceChoices):
891 if possible[0] <= actualSelection:
892 closestSelectionIndex = i
893 recurrenceIndex = hildonize.touch_selector(
896 (("%s" % m[1]) for m in recurrenceChoices),
897 closestSelectionIndex,
899 recurrence = recurrenceChoices[recurrenceIndex][0]
901 self._update_alarm_settings(recurrence)
902 except RuntimeError, e:
903 logging.exception("%s" % str(e))
905 self._errorDisplay.push_exception()
907 def _on_apply_timeout(self, *args):
909 self._applyAlarmTimeoutId = None
911 self._update_alarm_settings(self._alarmHandler.recurrence)
913 self._errorDisplay.push_exception()
916 def _on_missed_toggled(self, *args):
918 self._notifyOnMissed = self._missedCheckbox.get_active()
919 self.save_everything()
921 self._errorDisplay.push_exception()
923 def _on_voicemail_toggled(self, *args):
925 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
926 self.save_everything()
928 self._errorDisplay.push_exception()
930 def _on_sms_toggled(self, *args):
932 self._notifyOnSms = self._smsCheckbox.get_active()
933 self.save_everything()
935 self._errorDisplay.push_exception()
938 class RecentCallsView(object):
945 def __init__(self, widgetTree, backend, errorDisplay):
946 self._errorDisplay = errorDisplay
947 self._backend = backend
949 self._isPopulated = False
950 self._recentmodel = gtk.ListStore(
951 gobject.TYPE_STRING, # number
952 gobject.TYPE_STRING, # date
953 gobject.TYPE_STRING, # action
954 gobject.TYPE_STRING, # from
956 self._recentview = widgetTree.get_widget("recentview")
957 self._recentviewselection = None
958 self._onRecentviewRowActivatedId = 0
960 textrenderer = gtk.CellRendererText()
961 textrenderer.set_property("yalign", 0)
962 self._dateColumn = gtk.TreeViewColumn("Date")
963 self._dateColumn.pack_start(textrenderer, expand=True)
964 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
966 textrenderer = gtk.CellRendererText()
967 textrenderer.set_property("yalign", 0)
968 self._actionColumn = gtk.TreeViewColumn("Action")
969 self._actionColumn.pack_start(textrenderer, expand=True)
970 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
972 textrenderer = gtk.CellRendererText()
973 textrenderer.set_property("yalign", 0)
974 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
975 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
976 self._numberColumn = gtk.TreeViewColumn("Number")
977 self._numberColumn.pack_start(textrenderer, expand=True)
978 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
980 textrenderer = gtk.CellRendererText()
981 textrenderer.set_property("yalign", 0)
982 hildonize.set_cell_thumb_selectable(textrenderer)
983 self._nameColumn = gtk.TreeViewColumn("From")
984 self._nameColumn.pack_start(textrenderer, expand=True)
985 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
986 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
988 self._window = gtk_toolbox.find_parent_window(self._recentview)
989 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
991 self._updateSink = gtk_toolbox.threaded_stage(
993 self._idly_populate_recentview,
994 gtk_toolbox.null_sink(),
999 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1000 self._recentview.set_model(self._recentmodel)
1002 self._recentview.append_column(self._dateColumn)
1003 self._recentview.append_column(self._actionColumn)
1004 self._recentview.append_column(self._numberColumn)
1005 self._recentview.append_column(self._nameColumn)
1006 self._recentviewselection = self._recentview.get_selection()
1007 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
1009 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
1012 self._recentview.disconnect(self._onRecentviewRowActivatedId)
1016 self._recentview.remove_column(self._dateColumn)
1017 self._recentview.remove_column(self._actionColumn)
1018 self._recentview.remove_column(self._nameColumn)
1019 self._recentview.remove_column(self._numberColumn)
1020 self._recentview.set_model(None)
1022 def number_selected(self, action, number, message):
1024 @note Actual dial function is patched in later
1026 raise NotImplementedError("Horrible unknown error has occurred")
1028 def update(self, force = False):
1029 if not force and self._isPopulated:
1031 self._updateSink.send(())
1035 self._isPopulated = False
1036 self._recentmodel.clear()
1040 return "Recent Calls"
1042 def load_settings(self, config, section):
1045 def save_settings(self, config, section):
1047 @note Thread Agnostic
1051 def _idly_populate_recentview(self):
1052 with gtk_toolbox.gtk_lock():
1053 banner = hildonize.show_busy_banner_start(self._window, "Loading Recent History")
1055 self._recentmodel.clear()
1056 self._isPopulated = True
1059 recentItems = self._backend.get_recent()
1060 except Exception, e:
1061 self._errorDisplay.push_exception_with_lock()
1062 self._isPopulated = False
1065 for personName, phoneNumber, date, action in recentItems:
1067 personName = "Unknown"
1068 date = abbrev_relative_date(date)
1069 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1070 prettyNumber = make_pretty(prettyNumber)
1071 item = (prettyNumber, date, action.capitalize(), personName)
1072 with gtk_toolbox.gtk_lock():
1073 self._recentmodel.append(item)
1074 except Exception, e:
1075 self._errorDisplay.push_exception_with_lock()
1077 with gtk_toolbox.gtk_lock():
1078 hildonize.show_busy_banner_end(banner)
1082 def _on_recentview_row_activated(self, treeview, path, view_column):
1084 model, itr = self._recentviewselection.get_selected()
1088 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1089 number = make_ugly(number)
1090 contactPhoneNumbers = [("Phone", number)]
1091 description = self._recentmodel.get_value(itr, self.FROM_IDX)
1093 action, phoneNumber, message = self._phoneTypeSelector.run(
1094 contactPhoneNumbers,
1095 messages = (description, ),
1096 parent = self._window,
1098 if action == PhoneTypeSelector.ACTION_CANCEL:
1100 assert phoneNumber, "A lack of phone number exists"
1102 self.number_selected(action, phoneNumber, message)
1103 self._recentviewselection.unselect_all()
1104 except Exception, e:
1105 self._errorDisplay.push_exception()
1108 class MessagesView(object):
1116 def __init__(self, widgetTree, backend, errorDisplay):
1117 self._errorDisplay = errorDisplay
1118 self._backend = backend
1120 self._isPopulated = False
1121 self._messagemodel = gtk.ListStore(
1122 gobject.TYPE_STRING, # number
1123 gobject.TYPE_STRING, # date
1124 gobject.TYPE_STRING, # header
1125 gobject.TYPE_STRING, # message
1128 self._messageview = widgetTree.get_widget("messages_view")
1129 self._messageviewselection = None
1130 self._onMessageviewRowActivatedId = 0
1132 self._messageRenderer = gtk.CellRendererText()
1133 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1134 self._messageRenderer.set_property("wrap-width", 500)
1135 self._messageColumn = gtk.TreeViewColumn("Messages")
1136 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1137 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1138 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1140 self._window = gtk_toolbox.find_parent_window(self._messageview)
1141 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1143 self._updateSink = gtk_toolbox.threaded_stage(
1145 self._idly_populate_messageview,
1146 gtk_toolbox.null_sink(),
1151 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1152 self._messageview.set_model(self._messagemodel)
1154 self._messageview.append_column(self._messageColumn)
1155 self._messageviewselection = self._messageview.get_selection()
1156 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1158 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1161 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1165 self._messageview.remove_column(self._messageColumn)
1166 self._messageview.set_model(None)
1168 def number_selected(self, action, number, message):
1170 @note Actual dial function is patched in later
1172 raise NotImplementedError("Horrible unknown error has occurred")
1174 def update(self, force = False):
1175 if not force and self._isPopulated:
1177 self._updateSink.send(())
1181 self._isPopulated = False
1182 self._messagemodel.clear()
1188 def load_settings(self, config, section):
1191 def save_settings(self, config, section):
1193 @note Thread Agnostic
1197 def _idly_populate_messageview(self):
1198 with gtk_toolbox.gtk_lock():
1199 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1201 self._messagemodel.clear()
1202 self._isPopulated = True
1205 messageItems = self._backend.get_messages()
1206 except Exception, e:
1207 self._errorDisplay.push_exception_with_lock()
1208 self._isPopulated = False
1211 for header, number, relativeDate, messages in messageItems:
1212 prettyNumber = number[2:] if number.startswith("+1") else number
1213 prettyNumber = make_pretty(prettyNumber)
1215 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1216 newMessages = [firstMessage]
1217 newMessages.extend(messages)
1219 number = make_ugly(number)
1221 row = (number, relativeDate, header, "\n".join(newMessages), newMessages)
1222 with gtk_toolbox.gtk_lock():
1223 self._messagemodel.append(row)
1224 except Exception, e:
1225 self._errorDisplay.push_exception_with_lock()
1227 with gtk_toolbox.gtk_lock():
1228 hildonize.show_busy_banner_end(banner)
1232 def _on_messageview_row_activated(self, treeview, path, view_column):
1234 model, itr = self._messageviewselection.get_selected()
1238 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1239 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1241 action, phoneNumber, message = self._phoneTypeSelector.run(
1242 contactPhoneNumbers,
1243 messages = description,
1244 parent = self._window,
1246 if action == PhoneTypeSelector.ACTION_CANCEL:
1248 assert phoneNumber, "A lock of phone number exists"
1250 self.number_selected(action, phoneNumber, message)
1251 self._messageviewselection.unselect_all()
1252 except Exception, e:
1253 self._errorDisplay.push_exception()
1256 class ContactsView(object):
1258 def __init__(self, widgetTree, backend, errorDisplay):
1259 self._errorDisplay = errorDisplay
1260 self._backend = backend
1262 self._addressBook = None
1263 self._selectedComboIndex = 0
1264 self._addressBookFactories = [null_backend.NullAddressBook()]
1266 self._booksList = []
1267 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1269 self._isPopulated = False
1270 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1271 self._contactsviewselection = None
1272 self._contactsview = widgetTree.get_widget("contactsview")
1274 self._contactColumn = gtk.TreeViewColumn("Contact")
1275 displayContactSource = False
1276 if displayContactSource:
1277 textrenderer = gtk.CellRendererText()
1278 self._contactColumn.pack_start(textrenderer, expand=False)
1279 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1280 textrenderer = gtk.CellRendererText()
1281 hildonize.set_cell_thumb_selectable(textrenderer)
1282 self._contactColumn.pack_start(textrenderer, expand=True)
1283 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1284 textrenderer = gtk.CellRendererText()
1285 self._contactColumn.pack_start(textrenderer, expand=True)
1286 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1287 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1288 self._contactColumn.set_sort_column_id(1)
1289 self._contactColumn.set_visible(True)
1291 self._onContactsviewRowActivatedId = 0
1292 self._onAddressbookButtonChangedId = 0
1293 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1294 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1296 self._updateSink = gtk_toolbox.threaded_stage(
1298 self._idly_populate_contactsview,
1299 gtk_toolbox.null_sink(),
1304 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1306 self._contactsview.set_model(self._contactsmodel)
1307 self._contactsview.append_column(self._contactColumn)
1308 self._contactsviewselection = self._contactsview.get_selection()
1309 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1311 del self._booksList[:]
1312 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1313 if factoryName and bookName:
1314 entryName = "%s: %s" % (factoryName, bookName)
1316 entryName = factoryName
1318 entryName = bookName
1320 entryName = "Bad name (%d)" % factoryId
1321 row = (str(factoryId), bookId, entryName)
1322 self._booksList.append(row)
1324 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1325 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1327 if len(self._booksList) <= self._selectedComboIndex:
1328 self._selectedComboIndex = 0
1329 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1331 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1332 selectedBookId = self._booksList[self._selectedComboIndex][1]
1333 self.open_addressbook(selectedFactoryId, selectedBookId)
1336 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1337 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1341 self._bookSelectionButton.set_label("")
1342 self._contactsview.set_model(None)
1343 self._contactsview.remove_column(self._contactColumn)
1345 def number_selected(self, action, number, message):
1347 @note Actual dial function is patched in later
1349 raise NotImplementedError("Horrible unknown error has occurred")
1351 def get_addressbooks(self):
1353 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1355 for i, factory in enumerate(self._addressBookFactories):
1356 for bookFactory, bookId, bookName in factory.get_addressbooks():
1357 yield (str(i), bookId), (factory.factory_name(), bookName)
1359 def open_addressbook(self, bookFactoryId, bookId):
1360 bookFactoryIndex = int(bookFactoryId)
1361 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1363 forceUpdate = True if addressBook is not self._addressBook else False
1365 self._addressBook = addressBook
1366 self.update(force=forceUpdate)
1368 def update(self, force = False):
1369 if not force and self._isPopulated:
1371 self._updateSink.send(())
1375 self._isPopulated = False
1376 self._contactsmodel.clear()
1377 for factory in self._addressBookFactories:
1378 factory.clear_caches()
1379 self._addressBook.clear_caches()
1381 def append(self, book):
1382 self._addressBookFactories.append(book)
1384 def extend(self, books):
1385 self._addressBookFactories.extend(books)
1391 def load_settings(self, config, sectionName):
1393 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1394 except ConfigParser.NoOptionError:
1395 self._selectedComboIndex = 0
1397 def save_settings(self, config, sectionName):
1398 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1400 def _idly_populate_contactsview(self):
1401 with gtk_toolbox.gtk_lock():
1402 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1405 while addressBook is not self._addressBook:
1406 addressBook = self._addressBook
1407 with gtk_toolbox.gtk_lock():
1408 self._contactsview.set_model(None)
1412 contacts = addressBook.get_contacts()
1413 except Exception, e:
1415 self._isPopulated = False
1416 self._errorDisplay.push_exception_with_lock()
1417 for contactId, contactName in contacts:
1418 contactType = (addressBook.contact_source_short_name(contactId), )
1419 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1421 with gtk_toolbox.gtk_lock():
1422 self._contactsview.set_model(self._contactsmodel)
1424 self._isPopulated = True
1425 except Exception, e:
1426 self._errorDisplay.push_exception_with_lock()
1428 with gtk_toolbox.gtk_lock():
1429 hildonize.show_busy_banner_end(banner)
1432 def _on_addressbook_button_changed(self, *args, **kwds):
1435 newSelectedComboIndex = hildonize.touch_selector(
1438 (("%s" % m[2]) for m in self._booksList),
1439 self._selectedComboIndex,
1441 except RuntimeError:
1444 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1445 selectedBookId = self._booksList[newSelectedComboIndex][1]
1446 self.open_addressbook(selectedFactoryId, selectedBookId)
1447 self._selectedComboIndex = newSelectedComboIndex
1448 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1449 except Exception, e:
1450 self._errorDisplay.push_exception()
1452 def _on_contactsview_row_activated(self, treeview, path, view_column):
1454 model, itr = self._contactsviewselection.get_selected()
1458 contactId = self._contactsmodel.get_value(itr, 3)
1459 contactName = self._contactsmodel.get_value(itr, 1)
1461 contactDetails = self._addressBook.get_contact_details(contactId)
1462 except Exception, e:
1464 self._errorDisplay.push_exception()
1465 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1467 if len(contactPhoneNumbers) == 0:
1470 action, phoneNumber, message = self._phoneTypeSelector.run(
1471 contactPhoneNumbers,
1472 messages = (contactName, ),
1473 parent = self._window,
1475 if action == PhoneTypeSelector.ACTION_CANCEL:
1477 assert phoneNumber, "A lack of phone number exists"
1479 self.number_selected(action, phoneNumber, message)
1480 self._contactsviewselection.unselect_all()
1481 except Exception, e:
1482 self._errorDisplay.push_exception()