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)
1153 self._messageview.set_headers_visible(False)
1155 self._messageview.append_column(self._messageColumn)
1156 self._messageviewselection = self._messageview.get_selection()
1157 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1159 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1162 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1166 self._messageview.remove_column(self._messageColumn)
1167 self._messageview.set_model(None)
1169 def number_selected(self, action, number, message):
1171 @note Actual dial function is patched in later
1173 raise NotImplementedError("Horrible unknown error has occurred")
1175 def update(self, force = False):
1176 if not force and self._isPopulated:
1178 self._updateSink.send(())
1182 self._isPopulated = False
1183 self._messagemodel.clear()
1189 def load_settings(self, config, section):
1192 def save_settings(self, config, section):
1194 @note Thread Agnostic
1198 def _idly_populate_messageview(self):
1199 with gtk_toolbox.gtk_lock():
1200 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1202 self._messagemodel.clear()
1203 self._isPopulated = True
1206 messageItems = self._backend.get_messages()
1207 except Exception, e:
1208 self._errorDisplay.push_exception_with_lock()
1209 self._isPopulated = False
1212 for header, number, relativeDate, messages in messageItems:
1213 prettyNumber = number[2:] if number.startswith("+1") else number
1214 prettyNumber = make_pretty(prettyNumber)
1216 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1217 newMessages = [firstMessage]
1218 newMessages.extend(messages)
1220 number = make_ugly(number)
1222 row = (number, relativeDate, header, "\n".join(newMessages), newMessages)
1223 with gtk_toolbox.gtk_lock():
1224 self._messagemodel.append(row)
1225 except Exception, e:
1226 self._errorDisplay.push_exception_with_lock()
1228 with gtk_toolbox.gtk_lock():
1229 hildonize.show_busy_banner_end(banner)
1233 def _on_messageview_row_activated(self, treeview, path, view_column):
1235 model, itr = self._messageviewselection.get_selected()
1239 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1240 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1242 action, phoneNumber, message = self._phoneTypeSelector.run(
1243 contactPhoneNumbers,
1244 messages = description,
1245 parent = self._window,
1247 if action == PhoneTypeSelector.ACTION_CANCEL:
1249 assert phoneNumber, "A lock of phone number exists"
1251 self.number_selected(action, phoneNumber, message)
1252 self._messageviewselection.unselect_all()
1253 except Exception, e:
1254 self._errorDisplay.push_exception()
1257 class ContactsView(object):
1259 def __init__(self, widgetTree, backend, errorDisplay):
1260 self._errorDisplay = errorDisplay
1261 self._backend = backend
1263 self._addressBook = None
1264 self._selectedComboIndex = 0
1265 self._addressBookFactories = [null_backend.NullAddressBook()]
1267 self._booksList = []
1268 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1270 self._isPopulated = False
1271 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1272 self._contactsviewselection = None
1273 self._contactsview = widgetTree.get_widget("contactsview")
1275 self._contactColumn = gtk.TreeViewColumn("Contact")
1276 displayContactSource = False
1277 if displayContactSource:
1278 textrenderer = gtk.CellRendererText()
1279 self._contactColumn.pack_start(textrenderer, expand=False)
1280 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1281 textrenderer = gtk.CellRendererText()
1282 hildonize.set_cell_thumb_selectable(textrenderer)
1283 self._contactColumn.pack_start(textrenderer, expand=True)
1284 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1285 textrenderer = gtk.CellRendererText()
1286 self._contactColumn.pack_start(textrenderer, expand=True)
1287 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1288 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1289 self._contactColumn.set_sort_column_id(1)
1290 self._contactColumn.set_visible(True)
1292 self._onContactsviewRowActivatedId = 0
1293 self._onAddressbookButtonChangedId = 0
1294 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1295 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1297 self._updateSink = gtk_toolbox.threaded_stage(
1299 self._idly_populate_contactsview,
1300 gtk_toolbox.null_sink(),
1305 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1307 self._contactsview.set_model(self._contactsmodel)
1308 self._contactsview.append_column(self._contactColumn)
1309 self._contactsviewselection = self._contactsview.get_selection()
1310 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1312 del self._booksList[:]
1313 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1314 if factoryName and bookName:
1315 entryName = "%s: %s" % (factoryName, bookName)
1317 entryName = factoryName
1319 entryName = bookName
1321 entryName = "Bad name (%d)" % factoryId
1322 row = (str(factoryId), bookId, entryName)
1323 self._booksList.append(row)
1325 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1326 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1328 if len(self._booksList) <= self._selectedComboIndex:
1329 self._selectedComboIndex = 0
1330 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1332 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1333 selectedBookId = self._booksList[self._selectedComboIndex][1]
1334 self.open_addressbook(selectedFactoryId, selectedBookId)
1337 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1338 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1342 self._bookSelectionButton.set_label("")
1343 self._contactsview.set_model(None)
1344 self._contactsview.remove_column(self._contactColumn)
1346 def number_selected(self, action, number, message):
1348 @note Actual dial function is patched in later
1350 raise NotImplementedError("Horrible unknown error has occurred")
1352 def get_addressbooks(self):
1354 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1356 for i, factory in enumerate(self._addressBookFactories):
1357 for bookFactory, bookId, bookName in factory.get_addressbooks():
1358 yield (str(i), bookId), (factory.factory_name(), bookName)
1360 def open_addressbook(self, bookFactoryId, bookId):
1361 bookFactoryIndex = int(bookFactoryId)
1362 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1364 forceUpdate = True if addressBook is not self._addressBook else False
1366 self._addressBook = addressBook
1367 self.update(force=forceUpdate)
1369 def update(self, force = False):
1370 if not force and self._isPopulated:
1372 self._updateSink.send(())
1376 self._isPopulated = False
1377 self._contactsmodel.clear()
1378 for factory in self._addressBookFactories:
1379 factory.clear_caches()
1380 self._addressBook.clear_caches()
1382 def append(self, book):
1383 self._addressBookFactories.append(book)
1385 def extend(self, books):
1386 self._addressBookFactories.extend(books)
1392 def load_settings(self, config, sectionName):
1394 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1395 except ConfigParser.NoOptionError:
1396 self._selectedComboIndex = 0
1398 def save_settings(self, config, sectionName):
1399 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1401 def _idly_populate_contactsview(self):
1402 with gtk_toolbox.gtk_lock():
1403 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1406 while addressBook is not self._addressBook:
1407 addressBook = self._addressBook
1408 with gtk_toolbox.gtk_lock():
1409 self._contactsview.set_model(None)
1413 contacts = addressBook.get_contacts()
1414 except Exception, e:
1416 self._isPopulated = False
1417 self._errorDisplay.push_exception_with_lock()
1418 for contactId, contactName in contacts:
1419 contactType = (addressBook.contact_source_short_name(contactId), )
1420 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1422 with gtk_toolbox.gtk_lock():
1423 self._contactsview.set_model(self._contactsmodel)
1425 self._isPopulated = True
1426 except Exception, e:
1427 self._errorDisplay.push_exception_with_lock()
1429 with gtk_toolbox.gtk_lock():
1430 hildonize.show_busy_banner_end(banner)
1433 def _on_addressbook_button_changed(self, *args, **kwds):
1436 newSelectedComboIndex = hildonize.touch_selector(
1439 (("%s" % m[2]) for m in self._booksList),
1440 self._selectedComboIndex,
1442 except RuntimeError:
1445 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1446 selectedBookId = self._booksList[newSelectedComboIndex][1]
1447 self.open_addressbook(selectedFactoryId, selectedBookId)
1448 self._selectedComboIndex = newSelectedComboIndex
1449 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1450 except Exception, e:
1451 self._errorDisplay.push_exception()
1453 def _on_contactsview_row_activated(self, treeview, path, view_column):
1455 model, itr = self._contactsviewselection.get_selected()
1459 contactId = self._contactsmodel.get_value(itr, 3)
1460 contactName = self._contactsmodel.get_value(itr, 1)
1462 contactDetails = self._addressBook.get_contact_details(contactId)
1463 except Exception, e:
1465 self._errorDisplay.push_exception()
1466 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1468 if len(contactPhoneNumbers) == 0:
1471 action, phoneNumber, message = self._phoneTypeSelector.run(
1472 contactPhoneNumbers,
1473 messages = (contactName, ),
1474 parent = self._window,
1476 if action == PhoneTypeSelector.ACTION_CANCEL:
1478 assert phoneNumber, "A lack of phone number exists"
1480 self.number_selected(action, phoneNumber, message)
1481 self._contactsviewselection.unselect_all()
1482 except Exception, e:
1483 self._errorDisplay.push_exception()