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 return make_ugly(self._callbackSelectButton.get_label())
749 def set_account_number(self, number):
751 Displays current account number
753 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
755 def update(self, force = False):
756 if not force and self._isPopulated:
758 self._populate_callback_combo()
759 self.set_account_number(self._backend.get_account_number())
763 self._callbackSelectButton.set_label("")
764 self.set_account_number("")
765 self._isPopulated = False
767 def save_everything(self):
768 raise NotImplementedError
772 return "Account Info"
774 def load_settings(self, config, section):
775 self._defaultCallback = config.get(section, "callback")
776 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
777 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
778 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
780 def save_settings(self, config, section):
782 @note Thread Agnostic
784 callback = self.get_selected_callback_number()
785 config.set(section, "callback", callback)
786 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
787 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
788 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
790 def _populate_callback_combo(self):
791 self._isPopulated = True
792 del self._callbackList[:]
794 callbackNumbers = self._backend.get_callback_numbers()
796 self._errorDisplay.push_exception()
797 self._isPopulated = False
800 for number, description in callbackNumbers.iteritems():
801 self._callbackList.append(make_pretty(number))
803 callbackNumber = self._defaultCallback
804 self._callbackSelectButton.set_label(make_pretty(callbackNumber))
806 def _set_callback_number(self, number):
808 if not self._backend.is_valid_syntax(number) and 0 < len(number):
809 self._errorDisplay.push_message("%s is not a valid callback number" % number)
810 elif number == self._backend.get_callback_number():
812 "Callback number already is %s" % (
813 self._backend.get_callback_number(),
817 self._backend.set_callback_number(number)
818 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
819 make_pretty(number), make_pretty(self._backend.get_callback_number())
822 "Callback number set to %s" % (
823 self._backend.get_callback_number(),
827 self._errorDisplay.push_exception()
829 def _update_alarm_settings(self, recurrence):
831 isEnabled = self._notifyCheckbox.get_active()
832 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
833 self._alarmHandler.apply_settings(isEnabled, recurrence)
835 self.save_everything()
836 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
837 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
839 def _on_callbackentry_clicked(self, *args):
841 actualSelection = make_pretty(self.get_selected_callback_number())
843 userSelection = hildonize.touch_selector_entry(
849 number = make_ugly(userSelection)
850 self._set_callback_number(number)
851 except RuntimeError, e:
852 logging.exception("%s" % str(e))
854 self._errorDisplay.push_exception()
856 def _on_notify_toggled(self, *args):
858 if self._applyAlarmTimeoutId is not None:
859 gobject.source_remove(self._applyAlarmTimeoutId)
860 self._applyAlarmTimeoutId = None
861 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
863 self._errorDisplay.push_exception()
865 def _on_minutes_clicked(self, *args):
866 recurrenceChoices = [
882 actualSelection = self._alarmHandler.recurrence
884 closestSelectionIndex = 0
885 for i, possible in enumerate(recurrenceChoices):
886 if possible[0] <= actualSelection:
887 closestSelectionIndex = i
888 recurrenceIndex = hildonize.touch_selector(
891 (("%s" % m[1]) for m in recurrenceChoices),
892 closestSelectionIndex,
894 recurrence = recurrenceChoices[recurrenceIndex][0]
896 self._update_alarm_settings(recurrence)
897 except RuntimeError, e:
898 logging.exception("%s" % str(e))
900 self._errorDisplay.push_exception()
902 def _on_apply_timeout(self, *args):
904 self._applyAlarmTimeoutId = None
906 self._update_alarm_settings(self._alarmHandler.recurrence)
908 self._errorDisplay.push_exception()
911 def _on_missed_toggled(self, *args):
913 self._notifyOnMissed = self._missedCheckbox.get_active()
914 self.save_everything()
916 self._errorDisplay.push_exception()
918 def _on_voicemail_toggled(self, *args):
920 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
921 self.save_everything()
923 self._errorDisplay.push_exception()
925 def _on_sms_toggled(self, *args):
927 self._notifyOnSms = self._smsCheckbox.get_active()
928 self.save_everything()
930 self._errorDisplay.push_exception()
933 class RecentCallsView(object):
940 def __init__(self, widgetTree, backend, errorDisplay):
941 self._errorDisplay = errorDisplay
942 self._backend = backend
944 self._isPopulated = False
945 self._recentmodel = gtk.ListStore(
946 gobject.TYPE_STRING, # number
947 gobject.TYPE_STRING, # date
948 gobject.TYPE_STRING, # action
949 gobject.TYPE_STRING, # from
951 self._recentview = widgetTree.get_widget("recentview")
952 self._recentviewselection = None
953 self._onRecentviewRowActivatedId = 0
955 textrenderer = gtk.CellRendererText()
956 textrenderer.set_property("yalign", 0)
957 self._dateColumn = gtk.TreeViewColumn("Date")
958 self._dateColumn.pack_start(textrenderer, expand=True)
959 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
961 textrenderer = gtk.CellRendererText()
962 textrenderer.set_property("yalign", 0)
963 self._actionColumn = gtk.TreeViewColumn("Action")
964 self._actionColumn.pack_start(textrenderer, expand=True)
965 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
967 textrenderer = gtk.CellRendererText()
968 textrenderer.set_property("yalign", 0)
969 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
970 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
971 self._numberColumn = gtk.TreeViewColumn("Number")
972 self._numberColumn.pack_start(textrenderer, expand=True)
973 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
975 textrenderer = gtk.CellRendererText()
976 textrenderer.set_property("yalign", 0)
977 hildonize.set_cell_thumb_selectable(textrenderer)
978 self._nameColumn = gtk.TreeViewColumn("From")
979 self._nameColumn.pack_start(textrenderer, expand=True)
980 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
981 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
983 self._window = gtk_toolbox.find_parent_window(self._recentview)
984 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
986 self._updateSink = gtk_toolbox.threaded_stage(
988 self._idly_populate_recentview,
989 gtk_toolbox.null_sink(),
994 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
995 self._recentview.set_model(self._recentmodel)
997 self._recentview.append_column(self._dateColumn)
998 self._recentview.append_column(self._actionColumn)
999 self._recentview.append_column(self._numberColumn)
1000 self._recentview.append_column(self._nameColumn)
1001 self._recentviewselection = self._recentview.get_selection()
1002 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
1004 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
1007 self._recentview.disconnect(self._onRecentviewRowActivatedId)
1011 self._recentview.remove_column(self._dateColumn)
1012 self._recentview.remove_column(self._actionColumn)
1013 self._recentview.remove_column(self._nameColumn)
1014 self._recentview.remove_column(self._numberColumn)
1015 self._recentview.set_model(None)
1017 def number_selected(self, action, number, message):
1019 @note Actual dial function is patched in later
1021 raise NotImplementedError("Horrible unknown error has occurred")
1023 def update(self, force = False):
1024 if not force and self._isPopulated:
1026 self._updateSink.send(())
1030 self._isPopulated = False
1031 self._recentmodel.clear()
1035 return "Recent Calls"
1037 def load_settings(self, config, section):
1040 def save_settings(self, config, section):
1042 @note Thread Agnostic
1046 def _idly_populate_recentview(self):
1047 with gtk_toolbox.gtk_lock():
1048 banner = hildonize.show_busy_banner_start(self._window, "Loading Recent History")
1050 self._recentmodel.clear()
1051 self._isPopulated = True
1054 recentItems = self._backend.get_recent()
1055 except Exception, e:
1056 self._errorDisplay.push_exception_with_lock()
1057 self._isPopulated = False
1060 for personName, phoneNumber, date, action in recentItems:
1062 personName = "Unknown"
1063 date = abbrev_relative_date(date)
1064 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1065 prettyNumber = make_pretty(prettyNumber)
1066 item = (prettyNumber, date, action.capitalize(), personName)
1067 with gtk_toolbox.gtk_lock():
1068 self._recentmodel.append(item)
1069 except Exception, e:
1070 self._errorDisplay.push_exception_with_lock()
1072 with gtk_toolbox.gtk_lock():
1073 hildonize.show_busy_banner_end(banner)
1077 def _on_recentview_row_activated(self, treeview, path, view_column):
1079 model, itr = self._recentviewselection.get_selected()
1083 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1084 number = make_ugly(number)
1085 contactPhoneNumbers = [("Phone", number)]
1086 description = self._recentmodel.get_value(itr, self.FROM_IDX)
1088 action, phoneNumber, message = self._phoneTypeSelector.run(
1089 contactPhoneNumbers,
1090 messages = (description, ),
1091 parent = self._window,
1093 if action == PhoneTypeSelector.ACTION_CANCEL:
1095 assert phoneNumber, "A lack of phone number exists"
1097 self.number_selected(action, phoneNumber, message)
1098 self._recentviewselection.unselect_all()
1099 except Exception, e:
1100 self._errorDisplay.push_exception()
1103 class MessagesView(object):
1111 def __init__(self, widgetTree, backend, errorDisplay):
1112 self._errorDisplay = errorDisplay
1113 self._backend = backend
1115 self._isPopulated = False
1116 self._messagemodel = gtk.ListStore(
1117 gobject.TYPE_STRING, # number
1118 gobject.TYPE_STRING, # date
1119 gobject.TYPE_STRING, # header
1120 gobject.TYPE_STRING, # message
1123 self._messageview = widgetTree.get_widget("messages_view")
1124 self._messageviewselection = None
1125 self._onMessageviewRowActivatedId = 0
1127 self._messageRenderer = gtk.CellRendererText()
1128 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1129 self._messageRenderer.set_property("wrap-width", 500)
1130 self._messageColumn = gtk.TreeViewColumn("Messages")
1131 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1132 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1133 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1135 self._window = gtk_toolbox.find_parent_window(self._messageview)
1136 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1138 self._updateSink = gtk_toolbox.threaded_stage(
1140 self._idly_populate_messageview,
1141 gtk_toolbox.null_sink(),
1146 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1147 self._messageview.set_model(self._messagemodel)
1149 self._messageview.append_column(self._messageColumn)
1150 self._messageviewselection = self._messageview.get_selection()
1151 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1153 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1156 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1160 self._messageview.remove_column(self._messageColumn)
1161 self._messageview.set_model(None)
1163 def number_selected(self, action, number, message):
1165 @note Actual dial function is patched in later
1167 raise NotImplementedError("Horrible unknown error has occurred")
1169 def update(self, force = False):
1170 if not force and self._isPopulated:
1172 self._updateSink.send(())
1176 self._isPopulated = False
1177 self._messagemodel.clear()
1183 def load_settings(self, config, section):
1186 def save_settings(self, config, section):
1188 @note Thread Agnostic
1192 def _idly_populate_messageview(self):
1193 with gtk_toolbox.gtk_lock():
1194 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1196 self._messagemodel.clear()
1197 self._isPopulated = True
1200 messageItems = self._backend.get_messages()
1201 except Exception, e:
1202 self._errorDisplay.push_exception_with_lock()
1203 self._isPopulated = False
1206 for header, number, relativeDate, messages in messageItems:
1207 prettyNumber = number[2:] if number.startswith("+1") else number
1208 prettyNumber = make_pretty(prettyNumber)
1210 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1211 newMessages = [firstMessage]
1212 newMessages.extend(messages)
1214 number = make_ugly(number)
1216 row = (number, relativeDate, header, "\n".join(newMessages), newMessages)
1217 with gtk_toolbox.gtk_lock():
1218 self._messagemodel.append(row)
1219 except Exception, e:
1220 self._errorDisplay.push_exception_with_lock()
1222 with gtk_toolbox.gtk_lock():
1223 hildonize.show_busy_banner_end(banner)
1227 def _on_messageview_row_activated(self, treeview, path, view_column):
1229 model, itr = self._messageviewselection.get_selected()
1233 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1234 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1236 action, phoneNumber, message = self._phoneTypeSelector.run(
1237 contactPhoneNumbers,
1238 messages = description,
1239 parent = self._window,
1241 if action == PhoneTypeSelector.ACTION_CANCEL:
1243 assert phoneNumber, "A lock of phone number exists"
1245 self.number_selected(action, phoneNumber, message)
1246 self._messageviewselection.unselect_all()
1247 except Exception, e:
1248 self._errorDisplay.push_exception()
1251 class ContactsView(object):
1253 def __init__(self, widgetTree, backend, errorDisplay):
1254 self._errorDisplay = errorDisplay
1255 self._backend = backend
1257 self._addressBook = None
1258 self._selectedComboIndex = 0
1259 self._addressBookFactories = [null_backend.NullAddressBook()]
1261 self._booksList = []
1262 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1264 self._isPopulated = False
1265 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1266 self._contactsviewselection = None
1267 self._contactsview = widgetTree.get_widget("contactsview")
1269 self._contactColumn = gtk.TreeViewColumn("Contact")
1270 displayContactSource = False
1271 if displayContactSource:
1272 textrenderer = gtk.CellRendererText()
1273 self._contactColumn.pack_start(textrenderer, expand=False)
1274 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1275 textrenderer = gtk.CellRendererText()
1276 hildonize.set_cell_thumb_selectable(textrenderer)
1277 self._contactColumn.pack_start(textrenderer, expand=True)
1278 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1279 textrenderer = gtk.CellRendererText()
1280 self._contactColumn.pack_start(textrenderer, expand=True)
1281 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1282 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1283 self._contactColumn.set_sort_column_id(1)
1284 self._contactColumn.set_visible(True)
1286 self._onContactsviewRowActivatedId = 0
1287 self._onAddressbookButtonChangedId = 0
1288 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1289 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1291 self._updateSink = gtk_toolbox.threaded_stage(
1293 self._idly_populate_contactsview,
1294 gtk_toolbox.null_sink(),
1299 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1301 self._contactsview.set_model(self._contactsmodel)
1302 self._contactsview.append_column(self._contactColumn)
1303 self._contactsviewselection = self._contactsview.get_selection()
1304 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1306 del self._booksList[:]
1307 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1308 if factoryName and bookName:
1309 entryName = "%s: %s" % (factoryName, bookName)
1311 entryName = factoryName
1313 entryName = bookName
1315 entryName = "Bad name (%d)" % factoryId
1316 row = (str(factoryId), bookId, entryName)
1317 self._booksList.append(row)
1319 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1320 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1322 if len(self._booksList) <= self._selectedComboIndex:
1323 self._selectedComboIndex = 0
1324 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1326 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1327 selectedBookId = self._booksList[self._selectedComboIndex][1]
1328 self.open_addressbook(selectedFactoryId, selectedBookId)
1331 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1332 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1336 self._bookSelectionButton.set_label("")
1337 self._contactsview.set_model(None)
1338 self._contactsview.remove_column(self._contactColumn)
1340 def number_selected(self, action, number, message):
1342 @note Actual dial function is patched in later
1344 raise NotImplementedError("Horrible unknown error has occurred")
1346 def get_addressbooks(self):
1348 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1350 for i, factory in enumerate(self._addressBookFactories):
1351 for bookFactory, bookId, bookName in factory.get_addressbooks():
1352 yield (str(i), bookId), (factory.factory_name(), bookName)
1354 def open_addressbook(self, bookFactoryId, bookId):
1355 bookFactoryIndex = int(bookFactoryId)
1356 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1358 forceUpdate = True if addressBook is not self._addressBook else False
1360 self._addressBook = addressBook
1361 self.update(force=forceUpdate)
1363 def update(self, force = False):
1364 if not force and self._isPopulated:
1366 self._updateSink.send(())
1370 self._isPopulated = False
1371 self._contactsmodel.clear()
1372 for factory in self._addressBookFactories:
1373 factory.clear_caches()
1374 self._addressBook.clear_caches()
1376 def append(self, book):
1377 self._addressBookFactories.append(book)
1379 def extend(self, books):
1380 self._addressBookFactories.extend(books)
1386 def load_settings(self, config, sectionName):
1388 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1389 except ConfigParser.NoOptionError:
1390 self._selectedComboIndex = 0
1392 def save_settings(self, config, sectionName):
1393 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1395 def _idly_populate_contactsview(self):
1396 with gtk_toolbox.gtk_lock():
1397 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1400 while addressBook is not self._addressBook:
1401 addressBook = self._addressBook
1402 with gtk_toolbox.gtk_lock():
1403 self._contactsview.set_model(None)
1407 contacts = addressBook.get_contacts()
1408 except Exception, e:
1410 self._isPopulated = False
1411 self._errorDisplay.push_exception_with_lock()
1412 for contactId, contactName in contacts:
1413 contactType = (addressBook.contact_source_short_name(contactId), )
1414 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1416 with gtk_toolbox.gtk_lock():
1417 self._contactsview.set_model(self._contactsmodel)
1419 self._isPopulated = True
1420 except Exception, e:
1421 self._errorDisplay.push_exception_with_lock()
1423 with gtk_toolbox.gtk_lock():
1424 hildonize.show_busy_banner_end(banner)
1427 def _on_addressbook_button_changed(self, *args, **kwds):
1430 newSelectedComboIndex = hildonize.touch_selector(
1433 (("%s" % m[2]) for m in self._booksList),
1434 self._selectedComboIndex,
1436 except RuntimeError:
1439 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1440 selectedBookId = self._booksList[newSelectedComboIndex][1]
1441 self.open_addressbook(selectedFactoryId, selectedBookId)
1442 self._selectedComboIndex = newSelectedComboIndex
1443 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1444 except Exception, e:
1445 self._errorDisplay.push_exception()
1447 def _on_contactsview_row_activated(self, treeview, path, view_column):
1449 model, itr = self._contactsviewselection.get_selected()
1453 contactId = self._contactsmodel.get_value(itr, 3)
1454 contactName = self._contactsmodel.get_value(itr, 1)
1456 contactDetails = self._addressBook.get_contact_details(contactId)
1457 except Exception, e:
1459 self._errorDisplay.push_exception()
1460 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1462 if len(contactPhoneNumbers) == 0:
1465 action, phoneNumber, message = self._phoneTypeSelector.run(
1466 contactPhoneNumbers,
1467 messages = (contactName, ),
1468 parent = self._window,
1470 if action == PhoneTypeSelector.ACTION_CANCEL:
1472 assert phoneNumber, "A lack of phone number exists"
1474 self.number_selected(action, phoneNumber, message)
1475 self._contactsviewselection.unselect_all()
1476 except Exception, e:
1477 self._errorDisplay.push_exception()