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 Touch selector for callback number
22 @todo Alternate UI for dialogs (stackables)
23 @todo Switch to a selector with entry for notification time
26 from __future__ import with_statement
40 def make_ugly(prettynumber):
42 function to take a phone number and strip out all non-numeric
45 >>> make_ugly("+012-(345)-678-90")
49 uglynumber = re.sub('\D', '', prettynumber)
53 def make_pretty(phonenumber):
55 Function to take a phone number and return the pretty version
57 if phonenumber begins with 0:
59 if phonenumber begins with 1: ( for gizmo callback numbers )
61 if phonenumber is 13 digits:
63 if phonenumber is 10 digits:
67 >>> make_pretty("1234567")
69 >>> make_pretty("2345678901")
71 >>> make_pretty("12345678901")
73 >>> make_pretty("01234567890")
76 if phonenumber is None or phonenumber is "":
79 phonenumber = make_ugly(phonenumber)
81 if len(phonenumber) < 3:
84 if phonenumber[0] == "0":
86 prettynumber += "+%s" % phonenumber[0:3]
87 if 3 < len(phonenumber):
88 prettynumber += "-(%s)" % phonenumber[3:6]
89 if 6 < len(phonenumber):
90 prettynumber += "-%s" % phonenumber[6:9]
91 if 9 < len(phonenumber):
92 prettynumber += "-%s" % phonenumber[9:]
94 elif len(phonenumber) <= 7:
95 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
96 elif len(phonenumber) > 8 and phonenumber[0] == "1":
97 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
98 elif len(phonenumber) > 7:
99 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
103 def abbrev_relative_date(date):
105 >>> abbrev_relative_date("42 hours ago")
107 >>> abbrev_relative_date("2 days ago")
109 >>> abbrev_relative_date("4 weeks ago")
112 parts = date.split(" ")
113 return "%s %s" % (parts[0], parts[1][0])
116 class MergedAddressBook(object):
118 Merger of all addressbooks
121 def __init__(self, addressbookFactories, sorter = None):
122 self.__addressbookFactories = addressbookFactories
123 self.__addressbooks = None
124 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
126 def clear_caches(self):
127 self.__addressbooks = None
128 for factory in self.__addressbookFactories:
129 factory.clear_caches()
131 def get_addressbooks(self):
133 @returns Iterable of (Address Book Factory, Book Id, Book Name)
137 def open_addressbook(self, bookId):
140 def contact_source_short_name(self, contactId):
141 if self.__addressbooks is None:
143 bookIndex, originalId = contactId.split("-", 1)
144 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
148 return "All Contacts"
150 def get_contacts(self):
152 @returns Iterable of (contact id, contact name)
154 if self.__addressbooks is None:
155 self.__addressbooks = list(
156 factory.open_addressbook(id)
157 for factory in self.__addressbookFactories
158 for (f, id, name) in factory.get_addressbooks()
161 ("-".join([str(bookIndex), contactId]), contactName)
162 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
163 for (contactId, contactName) in addressbook.get_contacts()
165 sortedContacts = self.__sort_contacts(contacts)
166 return sortedContacts
168 def get_contact_details(self, contactId):
170 @returns Iterable of (Phone Type, Phone Number)
172 if self.__addressbooks is None:
174 bookIndex, originalId = contactId.split("-", 1)
175 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
178 def null_sorter(contacts):
180 Good for speed/low memory
185 def basic_firtname_sorter(contacts):
187 Expects names in "First Last" format
190 (contactName.rsplit(" ", 1)[0], (contactId, contactName))
191 for (contactId, contactName) in contacts
193 contactsWithKey.sort()
194 return (contactData for (lastName, contactData) in contactsWithKey)
197 def basic_lastname_sorter(contacts):
199 Expects names in "First Last" format
202 (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
203 for (contactId, contactName) in contacts
205 contactsWithKey.sort()
206 return (contactData for (lastName, contactData) in contactsWithKey)
209 def reversed_firtname_sorter(contacts):
211 Expects names in "Last, First" format
214 (contactName.split(", ", 1)[-1], (contactId, contactName))
215 for (contactId, contactName) in contacts
217 contactsWithKey.sort()
218 return (contactData for (lastName, contactData) in contactsWithKey)
221 def reversed_lastname_sorter(contacts):
223 Expects names in "Last, First" format
226 (contactName.split(", ", 1)[0], (contactId, contactName))
227 for (contactId, contactName) in contacts
229 contactsWithKey.sort()
230 return (contactData for (lastName, contactData) in contactsWithKey)
233 def guess_firstname(name):
235 return name.split(", ", 1)[-1]
237 return name.rsplit(" ", 1)[0]
240 def guess_lastname(name):
242 return name.split(", ", 1)[0]
244 return name.rsplit(" ", 1)[-1]
247 def advanced_firstname_sorter(cls, contacts):
249 (cls.guess_firstname(contactName), (contactId, contactName))
250 for (contactId, contactName) in contacts
252 contactsWithKey.sort()
253 return (contactData for (lastName, contactData) in contactsWithKey)
256 def advanced_lastname_sorter(cls, contacts):
258 (cls.guess_lastname(contactName), (contactId, contactName))
259 for (contactId, contactName) in contacts
261 contactsWithKey.sort()
262 return (contactData for (lastName, contactData) in contactsWithKey)
265 class PhoneTypeSelector(object):
267 ACTION_CANCEL = "cancel"
268 ACTION_SELECT = "select"
270 ACTION_SEND_SMS = "sms"
272 def __init__(self, widgetTree, gcBackend):
273 self._gcBackend = gcBackend
274 self._widgetTree = widgetTree
276 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
277 self._smsDialog = SmsEntryDialog(self._widgetTree)
279 self._smsButton = self._widgetTree.get_widget("sms_button")
280 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
282 self._dialButton = self._widgetTree.get_widget("dial_button")
283 self._dialButton.connect("clicked", self._on_phonetype_dial)
285 self._selectButton = self._widgetTree.get_widget("select_button")
286 self._selectButton.connect("clicked", self._on_phonetype_select)
288 self._cancelButton = self._widgetTree.get_widget("cancel_button")
289 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
291 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
292 self._messagesView = self._widgetTree.get_widget("phoneSelectionMessages")
293 self._scrollWindow = self._messagesView.get_parent()
295 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
296 self._typeviewselection = None
297 self._typeview = self._widgetTree.get_widget("phonetypes")
298 self._typeview.connect("row-activated", self._on_phonetype_select)
300 self._action = self.ACTION_CANCEL
302 def run(self, contactDetails, messages = (), parent = None):
303 self._action = self.ACTION_CANCEL
305 # Add the column to the phone selection tree view
306 self._typemodel.clear()
307 self._typeview.set_model(self._typemodel)
309 textrenderer = gtk.CellRendererText()
310 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
311 self._typeview.append_column(numberColumn)
313 textrenderer = gtk.CellRendererText()
314 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
315 self._typeview.append_column(typeColumn)
317 for phoneType, phoneNumber in contactDetails:
318 display = " - ".join((phoneNumber, phoneType))
320 row = (phoneNumber, display)
321 self._typemodel.append(row)
323 self._typeviewselection = self._typeview.get_selection()
324 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
325 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
327 # Add the column to the messages tree view
328 self._messagemodel.clear()
329 self._messagesView.set_model(self._messagemodel)
331 textrenderer = gtk.CellRendererText()
332 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
333 textrenderer.set_property("wrap-width", 450)
334 messageColumn = gtk.TreeViewColumn("")
335 messageColumn.pack_start(textrenderer, expand=True)
336 messageColumn.add_attribute(textrenderer, "markup", 0)
337 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
338 self._messagesView.append_column(messageColumn)
339 self._messagesView.set_headers_visible(False)
342 for message in messages:
344 self._messagemodel.append(row)
345 self._messagesView.show()
346 self._scrollWindow.show()
347 messagesSelection = self._messagesView.get_selection()
348 messagesSelection.select_path((len(messages)-1, ))
350 self._messagesView.hide()
351 self._scrollWindow.hide()
353 if parent is not None:
354 self._dialog.set_transient_for(parent)
359 self._messagesView.scroll_to_cell((len(messages)-1, ))
361 userResponse = self._dialog.run()
365 if userResponse == gtk.RESPONSE_OK:
366 phoneNumber = self._get_number()
367 phoneNumber = make_ugly(phoneNumber)
371 self._action = self.ACTION_CANCEL
373 if self._action == self.ACTION_SEND_SMS:
374 smsMessage = self._smsDialog.run(phoneNumber, messages, parent)
377 self._action = self.ACTION_CANCEL
381 self._messagesView.remove_column(messageColumn)
382 self._messagesView.set_model(None)
384 self._typeviewselection.unselect_all()
385 self._typeview.remove_column(numberColumn)
386 self._typeview.remove_column(typeColumn)
387 self._typeview.set_model(None)
389 return self._action, phoneNumber, smsMessage
391 def _get_number(self):
392 model, itr = self._typeviewselection.get_selected()
396 phoneNumber = self._typemodel.get_value(itr, 0)
399 def _on_phonetype_dial(self, *args):
400 self._dialog.response(gtk.RESPONSE_OK)
401 self._action = self.ACTION_DIAL
403 def _on_phonetype_send_sms(self, *args):
404 self._dialog.response(gtk.RESPONSE_OK)
405 self._action = self.ACTION_SEND_SMS
407 def _on_phonetype_select(self, *args):
408 self._dialog.response(gtk.RESPONSE_OK)
409 self._action = self.ACTION_SELECT
411 def _on_phonetype_cancel(self, *args):
412 self._dialog.response(gtk.RESPONSE_CANCEL)
413 self._action = self.ACTION_CANCEL
416 class SmsEntryDialog(object):
418 @todo Add multi-SMS messages like GoogleVoice
423 def __init__(self, widgetTree):
424 self._widgetTree = widgetTree
425 self._dialog = self._widgetTree.get_widget("smsDialog")
427 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
428 self._smsButton.connect("clicked", self._on_send)
430 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
431 self._cancelButton.connect("clicked", self._on_cancel)
433 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
435 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
436 self._messagesView = self._widgetTree.get_widget("smsMessages")
437 self._scrollWindow = self._messagesView.get_parent()
439 self._smsEntry = self._widgetTree.get_widget("smsEntry")
440 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
442 def run(self, number, messages = (), parent = None):
443 # Add the column to the messages tree view
444 self._messagemodel.clear()
445 self._messagesView.set_model(self._messagemodel)
447 textrenderer = gtk.CellRendererText()
448 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
449 textrenderer.set_property("wrap-width", 450)
450 messageColumn = gtk.TreeViewColumn("")
451 messageColumn.pack_start(textrenderer, expand=True)
452 messageColumn.add_attribute(textrenderer, "markup", 0)
453 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
454 self._messagesView.append_column(messageColumn)
455 self._messagesView.set_headers_visible(False)
458 for message in messages:
460 self._messagemodel.append(row)
461 self._messagesView.show()
462 self._scrollWindow.show()
463 messagesSelection = self._messagesView.get_selection()
464 messagesSelection.select_path((len(messages)-1, ))
466 self._messagesView.hide()
467 self._scrollWindow.hide()
469 self._smsEntry.get_buffer().set_text("")
470 self._update_letter_count()
472 if parent is not None:
473 self._dialog.set_transient_for(parent)
478 self._messagesView.scroll_to_cell((len(messages)-1, ))
479 self._smsEntry.grab_focus()
481 userResponse = self._dialog.run()
485 if userResponse == gtk.RESPONSE_OK:
486 entryBuffer = self._smsEntry.get_buffer()
487 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
488 enteredMessage = enteredMessage[0:self.MAX_CHAR]
492 self._messagesView.remove_column(messageColumn)
493 self._messagesView.set_model(None)
495 return enteredMessage.strip()
497 def _update_letter_count(self, *args):
498 entryLength = self._smsEntry.get_buffer().get_char_count()
499 charsLeft = self.MAX_CHAR - entryLength
500 self._letterCountLabel.set_text(str(charsLeft))
502 self._smsButton.set_sensitive(False)
504 self._smsButton.set_sensitive(True)
506 def _on_entry_changed(self, *args):
507 self._update_letter_count()
509 def _on_send(self, *args):
510 self._dialog.response(gtk.RESPONSE_OK)
512 def _on_cancel(self, *args):
513 self._dialog.response(gtk.RESPONSE_CANCEL)
516 class Dialpad(object):
518 def __init__(self, widgetTree, errorDisplay):
519 self._clipboard = gtk.clipboard_get()
520 self._errorDisplay = errorDisplay
521 self._smsDialog = SmsEntryDialog(widgetTree)
523 self._numberdisplay = widgetTree.get_widget("numberdisplay")
524 self._smsButton = widgetTree.get_widget("sms")
525 self._dialButton = widgetTree.get_widget("dial")
526 self._backButton = widgetTree.get_widget("back")
527 self._phonenumber = ""
528 self._prettynumber = ""
531 "on_digit_clicked": self._on_digit_clicked,
533 widgetTree.signal_autoconnect(callbackMapping)
534 self._dialButton.connect("clicked", self._on_dial_clicked)
535 self._smsButton.connect("clicked", self._on_sms_clicked)
537 self._originalLabel = self._backButton.get_label()
538 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
539 self._backTapHandler.on_tap = self._on_backspace
540 self._backTapHandler.on_hold = self._on_clearall
541 self._backTapHandler.on_holding = self._set_clear_button
542 self._backTapHandler.on_cancel = self._reset_back_button
544 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
545 self._keyPressEventId = 0
548 self._dialButton.grab_focus()
549 self._backTapHandler.enable()
550 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
553 self._window.disconnect(self._keyPressEventId)
554 self._keyPressEventId = 0
555 self._reset_back_button()
556 self._backTapHandler.disable()
558 def number_selected(self, action, number, message):
560 @note Actual dial function is patched in later
562 raise NotImplementedError("Horrible unknown error has occurred")
564 def get_number(self):
565 return self._phonenumber
567 def set_number(self, number):
569 Set the number to dial
572 self._phonenumber = make_ugly(number)
573 self._prettynumber = make_pretty(self._phonenumber)
574 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
576 self._errorDisplay.push_exception()
585 def load_settings(self, config, section):
588 def save_settings(self, config, section):
590 @note Thread Agnostic
594 def _on_key_press(self, widget, event):
596 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
597 contents = self._clipboard.wait_for_text()
598 if contents is not None:
599 self.set_number(contents)
601 self._errorDisplay.push_exception()
603 def _on_sms_clicked(self, widget):
605 action = PhoneTypeSelector.ACTION_SEND_SMS
606 phoneNumber = self.get_number()
608 message = self._smsDialog.run(phoneNumber, (), self._window)
611 action = PhoneTypeSelector.ACTION_CANCEL
613 if action == PhoneTypeSelector.ACTION_CANCEL:
615 self.number_selected(action, phoneNumber, message)
617 self._errorDisplay.push_exception()
619 def _on_dial_clicked(self, widget):
621 action = PhoneTypeSelector.ACTION_DIAL
622 phoneNumber = self.get_number()
624 self.number_selected(action, phoneNumber, message)
626 self._errorDisplay.push_exception()
628 def _on_digit_clicked(self, widget):
630 self.set_number(self._phonenumber + widget.get_name()[-1])
632 self._errorDisplay.push_exception()
634 def _on_backspace(self, taps):
636 self.set_number(self._phonenumber[:-taps])
637 self._reset_back_button()
639 self._errorDisplay.push_exception()
641 def _on_clearall(self, taps):
644 self._reset_back_button()
646 self._errorDisplay.push_exception()
649 def _set_clear_button(self):
651 self._backButton.set_label("gtk-clear")
653 self._errorDisplay.push_exception()
655 def _reset_back_button(self):
657 self._backButton.set_label(self._originalLabel)
659 self._errorDisplay.push_exception()
662 class AccountInfo(object):
664 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
665 self._errorDisplay = errorDisplay
666 self._backend = backend
667 self._isPopulated = False
668 self._alarmHandler = alarmHandler
669 self._notifyOnMissed = False
670 self._notifyOnVoicemail = False
671 self._notifyOnSms = False
673 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
674 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
675 self._callbackCombo = widgetTree.get_widget("callbackcombo")
676 self._onCallbackentryChangedId = 0
678 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
679 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
680 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
681 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
682 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
683 self._onNotifyToggled = 0
684 self._onMinutesChanged = 0
685 self._onMissedToggled = 0
686 self._onVoicemailToggled = 0
687 self._onSmsToggled = 0
688 self._applyAlarmTimeoutId = None
690 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
691 self._defaultCallback = ""
694 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
696 self._accountViewNumberDisplay.set_use_markup(True)
697 self.set_account_number("")
699 self._callbackList.clear()
700 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
702 if self._alarmHandler is not None:
703 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
704 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
705 self._missedCheckbox.set_active(self._notifyOnMissed)
706 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
707 self._smsCheckbox.set_active(self._notifyOnSms)
709 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
710 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
711 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
712 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
713 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
715 self._notifyCheckbox.set_sensitive(False)
716 self._minutesEntryButton.set_sensitive(False)
717 self._missedCheckbox.set_sensitive(False)
718 self._voicemailCheckbox.set_sensitive(False)
719 self._smsCheckbox.set_sensitive(False)
721 self.update(force=True)
724 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
725 self._onCallbackentryChangedId = 0
727 if self._alarmHandler is not None:
728 self._notifyCheckbox.disconnect(self._onNotifyToggled)
729 self._minutesEntryButton.disconnect(self._onMinutesChanged)
730 self._missedCheckbox.disconnect(self._onNotifyToggled)
731 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
732 self._smsCheckbox.disconnect(self._onNotifyToggled)
733 self._onNotifyToggled = 0
734 self._onMinutesChanged = 0
735 self._onMissedToggled = 0
736 self._onVoicemailToggled = 0
737 self._onSmsToggled = 0
739 self._notifyCheckbox.set_sensitive(True)
740 self._minutesEntryButton.set_sensitive(True)
741 self._missedCheckbox.set_sensitive(True)
742 self._voicemailCheckbox.set_sensitive(True)
743 self._smsCheckbox.set_sensitive(True)
746 self._callbackList.clear()
748 def get_selected_callback_number(self):
749 return make_ugly(self._callbackCombo.get_child().get_text())
751 def set_account_number(self, number):
753 Displays current account number
755 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
757 def update(self, force = False):
758 if not force and self._isPopulated:
760 self._populate_callback_combo()
761 self.set_account_number(self._backend.get_account_number())
765 self._callbackCombo.get_child().set_text("")
766 self.set_account_number("")
767 self._isPopulated = False
769 def save_everything(self):
770 raise NotImplementedError
774 return "Account Info"
776 def load_settings(self, config, section):
777 self._defaultCallback = config.get(section, "callback")
778 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
779 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
780 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
782 def save_settings(self, config, section):
784 @note Thread Agnostic
786 callback = self.get_selected_callback_number()
787 config.set(section, "callback", callback)
788 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
789 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
790 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
792 def _populate_callback_combo(self):
793 self._isPopulated = True
794 self._callbackList.clear()
796 callbackNumbers = self._backend.get_callback_numbers()
798 self._errorDisplay.push_exception()
799 self._isPopulated = False
802 for number, description in callbackNumbers.iteritems():
803 self._callbackList.append((make_pretty(number),))
805 self._callbackCombo.set_model(self._callbackList)
806 self._callbackCombo.set_text_column(0)
807 #callbackNumber = self._backend.get_callback_number()
808 callbackNumber = self._defaultCallback
809 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
811 def _set_callback_number(self, number):
813 if not self._backend.is_valid_syntax(number) and 0 < len(number):
814 self._errorDisplay.push_message("%s is not a valid callback number" % number)
815 elif number == self._backend.get_callback_number():
817 "Callback number already is %s" % (
818 self._backend.get_callback_number(),
822 self._backend.set_callback_number(number)
823 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
824 make_pretty(number), make_pretty(self._backend.get_callback_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_changed(self, *args):
846 text = self.get_selected_callback_number()
847 number = make_ugly(text)
848 self._set_callback_number(number)
850 self._errorDisplay.push_exception()
852 def _on_notify_toggled(self, *args):
854 if self._applyAlarmTimeoutId is not None:
855 gobject.source_remove(self._applyAlarmTimeoutId)
856 self._applyAlarmTimeoutId = None
857 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
859 self._errorDisplay.push_exception()
861 def _on_minutes_clicked(self, *args):
862 recurrenceChoices = [
878 actualSelection = self._alarmHandler.recurrence
880 closestSelectionIndex = 0
881 for i, possible in enumerate(recurrenceChoices):
882 if possible[0] <= actualSelection:
883 closestSelectionIndex = i
884 recurrenceIndex = hildonize.touch_selector(
887 (("%s" % m[1]) for m in recurrenceChoices),
888 closestSelectionIndex,
890 recurrence = recurrenceChoices[recurrenceIndex][0]
892 self._update_alarm_settings(recurrence)
893 except RuntimeError, e:
894 logging.exception("%s" % str(e))
896 self._errorDisplay.push_exception()
898 def _on_apply_timeout(self, *args):
900 self._applyAlarmTimeoutId = None
902 self._update_alarm_settings(self._alarmHandler.recurrence)
904 self._errorDisplay.push_exception()
907 def _on_missed_toggled(self, *args):
909 self._notifyOnMissed = self._missedCheckbox.get_active()
910 self.save_everything()
912 self._errorDisplay.push_exception()
914 def _on_voicemail_toggled(self, *args):
916 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
917 self.save_everything()
919 self._errorDisplay.push_exception()
921 def _on_sms_toggled(self, *args):
923 self._notifyOnSms = self._smsCheckbox.get_active()
924 self.save_everything()
926 self._errorDisplay.push_exception()
929 class RecentCallsView(object):
936 def __init__(self, widgetTree, backend, errorDisplay):
937 self._errorDisplay = errorDisplay
938 self._backend = backend
940 self._isPopulated = False
941 self._recentmodel = gtk.ListStore(
942 gobject.TYPE_STRING, # number
943 gobject.TYPE_STRING, # date
944 gobject.TYPE_STRING, # action
945 gobject.TYPE_STRING, # from
947 self._recentview = widgetTree.get_widget("recentview")
948 self._recentviewselection = None
949 self._onRecentviewRowActivatedId = 0
951 textrenderer = gtk.CellRendererText()
952 textrenderer.set_property("yalign", 0)
953 self._dateColumn = gtk.TreeViewColumn("Date")
954 self._dateColumn.pack_start(textrenderer, expand=True)
955 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
957 textrenderer = gtk.CellRendererText()
958 textrenderer.set_property("yalign", 0)
959 self._actionColumn = gtk.TreeViewColumn("Action")
960 self._actionColumn.pack_start(textrenderer, expand=True)
961 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
963 textrenderer = gtk.CellRendererText()
964 textrenderer.set_property("yalign", 0)
965 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
966 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
967 self._numberColumn = gtk.TreeViewColumn("Number")
968 self._numberColumn.pack_start(textrenderer, expand=True)
969 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
971 textrenderer = gtk.CellRendererText()
972 textrenderer.set_property("yalign", 0)
973 hildonize.set_cell_thumb_selectable(textrenderer)
974 self._nameColumn = gtk.TreeViewColumn("From")
975 self._nameColumn.pack_start(textrenderer, expand=True)
976 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
977 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
979 self._window = gtk_toolbox.find_parent_window(self._recentview)
980 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
982 self._updateSink = gtk_toolbox.threaded_stage(
984 self._idly_populate_recentview,
985 gtk_toolbox.null_sink(),
990 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
991 self._recentview.set_model(self._recentmodel)
993 self._recentview.append_column(self._dateColumn)
994 self._recentview.append_column(self._actionColumn)
995 self._recentview.append_column(self._numberColumn)
996 self._recentview.append_column(self._nameColumn)
997 self._recentviewselection = self._recentview.get_selection()
998 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
1000 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
1003 self._recentview.disconnect(self._onRecentviewRowActivatedId)
1007 self._recentview.remove_column(self._dateColumn)
1008 self._recentview.remove_column(self._actionColumn)
1009 self._recentview.remove_column(self._nameColumn)
1010 self._recentview.remove_column(self._numberColumn)
1011 self._recentview.set_model(None)
1013 def number_selected(self, action, number, message):
1015 @note Actual dial function is patched in later
1017 raise NotImplementedError("Horrible unknown error has occurred")
1019 def update(self, force = False):
1020 if not force and self._isPopulated:
1022 self._updateSink.send(())
1026 self._isPopulated = False
1027 self._recentmodel.clear()
1031 return "Recent Calls"
1033 def load_settings(self, config, section):
1036 def save_settings(self, config, section):
1038 @note Thread Agnostic
1042 def _idly_populate_recentview(self):
1043 with gtk_toolbox.gtk_lock():
1044 banner = hildonize.show_busy_banner_start(self._window, "Loading Recent History")
1046 self._recentmodel.clear()
1047 self._isPopulated = True
1050 recentItems = self._backend.get_recent()
1051 except Exception, e:
1052 self._errorDisplay.push_exception_with_lock()
1053 self._isPopulated = False
1056 for personName, phoneNumber, date, action in recentItems:
1058 personName = "Unknown"
1059 date = abbrev_relative_date(date)
1060 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1061 prettyNumber = make_pretty(prettyNumber)
1062 item = (prettyNumber, date, action.capitalize(), personName)
1063 with gtk_toolbox.gtk_lock():
1064 self._recentmodel.append(item)
1065 except Exception, e:
1066 self._errorDisplay.push_exception_with_lock()
1068 with gtk_toolbox.gtk_lock():
1069 hildonize.show_busy_banner_end(banner)
1073 def _on_recentview_row_activated(self, treeview, path, view_column):
1075 model, itr = self._recentviewselection.get_selected()
1079 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1080 number = make_ugly(number)
1081 contactPhoneNumbers = [("Phone", number)]
1082 description = self._recentmodel.get_value(itr, self.FROM_IDX)
1084 action, phoneNumber, message = self._phoneTypeSelector.run(
1085 contactPhoneNumbers,
1086 messages = (description, ),
1087 parent = self._window,
1089 if action == PhoneTypeSelector.ACTION_CANCEL:
1091 assert phoneNumber, "A lack of phone number exists"
1093 self.number_selected(action, phoneNumber, message)
1094 self._recentviewselection.unselect_all()
1095 except Exception, e:
1096 self._errorDisplay.push_exception()
1099 class MessagesView(object):
1107 def __init__(self, widgetTree, backend, errorDisplay):
1108 self._errorDisplay = errorDisplay
1109 self._backend = backend
1111 self._isPopulated = False
1112 self._messagemodel = gtk.ListStore(
1113 gobject.TYPE_STRING, # number
1114 gobject.TYPE_STRING, # date
1115 gobject.TYPE_STRING, # header
1116 gobject.TYPE_STRING, # message
1119 self._messageview = widgetTree.get_widget("messages_view")
1120 self._messageviewselection = None
1121 self._onMessageviewRowActivatedId = 0
1123 self._messageRenderer = gtk.CellRendererText()
1124 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1125 self._messageRenderer.set_property("wrap-width", 500)
1126 self._messageColumn = gtk.TreeViewColumn("Messages")
1127 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1128 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1129 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1131 self._window = gtk_toolbox.find_parent_window(self._messageview)
1132 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1134 self._updateSink = gtk_toolbox.threaded_stage(
1136 self._idly_populate_messageview,
1137 gtk_toolbox.null_sink(),
1142 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1143 self._messageview.set_model(self._messagemodel)
1145 self._messageview.append_column(self._messageColumn)
1146 self._messageviewselection = self._messageview.get_selection()
1147 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1149 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1152 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1156 self._messageview.remove_column(self._messageColumn)
1157 self._messageview.set_model(None)
1159 def number_selected(self, action, number, message):
1161 @note Actual dial function is patched in later
1163 raise NotImplementedError("Horrible unknown error has occurred")
1165 def update(self, force = False):
1166 if not force and self._isPopulated:
1168 self._updateSink.send(())
1172 self._isPopulated = False
1173 self._messagemodel.clear()
1179 def load_settings(self, config, section):
1182 def save_settings(self, config, section):
1184 @note Thread Agnostic
1188 def _idly_populate_messageview(self):
1189 with gtk_toolbox.gtk_lock():
1190 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1192 self._messagemodel.clear()
1193 self._isPopulated = True
1196 messageItems = self._backend.get_messages()
1197 except Exception, e:
1198 self._errorDisplay.push_exception_with_lock()
1199 self._isPopulated = False
1202 for header, number, relativeDate, messages in messageItems:
1203 prettyNumber = number[2:] if number.startswith("+1") else number
1204 prettyNumber = make_pretty(prettyNumber)
1206 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1207 newMessages = [firstMessage]
1208 newMessages.extend(messages)
1210 number = make_ugly(number)
1212 row = (number, relativeDate, header, "\n".join(newMessages), newMessages)
1213 with gtk_toolbox.gtk_lock():
1214 self._messagemodel.append(row)
1215 except Exception, e:
1216 self._errorDisplay.push_exception_with_lock()
1218 with gtk_toolbox.gtk_lock():
1219 hildonize.show_busy_banner_end(banner)
1223 def _on_messageview_row_activated(self, treeview, path, view_column):
1225 model, itr = self._messageviewselection.get_selected()
1229 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1230 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1232 action, phoneNumber, message = self._phoneTypeSelector.run(
1233 contactPhoneNumbers,
1234 messages = description,
1235 parent = self._window,
1237 if action == PhoneTypeSelector.ACTION_CANCEL:
1239 assert phoneNumber, "A lock of phone number exists"
1241 self.number_selected(action, phoneNumber, message)
1242 self._messageviewselection.unselect_all()
1243 except Exception, e:
1244 self._errorDisplay.push_exception()
1247 class ContactsView(object):
1249 def __init__(self, widgetTree, backend, errorDisplay):
1250 self._errorDisplay = errorDisplay
1251 self._backend = backend
1253 self._addressBook = None
1254 self._selectedComboIndex = 0
1255 self._addressBookFactories = [null_backend.NullAddressBook()]
1257 self._booksList = []
1258 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1260 self._isPopulated = False
1261 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1262 self._contactsviewselection = None
1263 self._contactsview = widgetTree.get_widget("contactsview")
1265 self._contactColumn = gtk.TreeViewColumn("Contact")
1266 displayContactSource = False
1267 if displayContactSource:
1268 textrenderer = gtk.CellRendererText()
1269 self._contactColumn.pack_start(textrenderer, expand=False)
1270 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1271 textrenderer = gtk.CellRendererText()
1272 hildonize.set_cell_thumb_selectable(textrenderer)
1273 self._contactColumn.pack_start(textrenderer, expand=True)
1274 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1275 textrenderer = gtk.CellRendererText()
1276 self._contactColumn.pack_start(textrenderer, expand=True)
1277 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1278 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1279 self._contactColumn.set_sort_column_id(1)
1280 self._contactColumn.set_visible(True)
1282 self._onContactsviewRowActivatedId = 0
1283 self._onAddressbookButtonChangedId = 0
1284 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1285 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1287 self._updateSink = gtk_toolbox.threaded_stage(
1289 self._idly_populate_contactsview,
1290 gtk_toolbox.null_sink(),
1295 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1297 self._contactsview.set_model(self._contactsmodel)
1298 self._contactsview.append_column(self._contactColumn)
1299 self._contactsviewselection = self._contactsview.get_selection()
1300 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1302 del self._booksList[:]
1303 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1304 if factoryName and bookName:
1305 entryName = "%s: %s" % (factoryName, bookName)
1307 entryName = factoryName
1309 entryName = bookName
1311 entryName = "Bad name (%d)" % factoryId
1312 row = (str(factoryId), bookId, entryName)
1313 self._booksList.append(row)
1315 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1316 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1318 if len(self._booksList) <= self._selectedComboIndex:
1319 self._selectedComboIndex = 0
1320 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1322 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1323 selectedBookId = self._booksList[self._selectedComboIndex][1]
1324 self.open_addressbook(selectedFactoryId, selectedBookId)
1327 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1328 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1332 self._bookSelectionButton.set_label("")
1333 self._contactsview.set_model(None)
1334 self._contactsview.remove_column(self._contactColumn)
1336 def number_selected(self, action, number, message):
1338 @note Actual dial function is patched in later
1340 raise NotImplementedError("Horrible unknown error has occurred")
1342 def get_addressbooks(self):
1344 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1346 for i, factory in enumerate(self._addressBookFactories):
1347 for bookFactory, bookId, bookName in factory.get_addressbooks():
1348 yield (str(i), bookId), (factory.factory_name(), bookName)
1350 def open_addressbook(self, bookFactoryId, bookId):
1351 bookFactoryIndex = int(bookFactoryId)
1352 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1354 forceUpdate = True if addressBook is not self._addressBook else False
1356 self._addressBook = addressBook
1357 self.update(force=forceUpdate)
1359 def update(self, force = False):
1360 if not force and self._isPopulated:
1362 self._updateSink.send(())
1366 self._isPopulated = False
1367 self._contactsmodel.clear()
1368 for factory in self._addressBookFactories:
1369 factory.clear_caches()
1370 self._addressBook.clear_caches()
1372 def append(self, book):
1373 self._addressBookFactories.append(book)
1375 def extend(self, books):
1376 self._addressBookFactories.extend(books)
1382 def load_settings(self, config, sectionName):
1384 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1385 except ConfigParser.NoOptionError:
1386 self._selectedComboIndex = 0
1388 def save_settings(self, config, sectionName):
1389 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1391 def _idly_populate_contactsview(self):
1392 with gtk_toolbox.gtk_lock():
1393 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1396 while addressBook is not self._addressBook:
1397 addressBook = self._addressBook
1398 with gtk_toolbox.gtk_lock():
1399 self._contactsview.set_model(None)
1403 contacts = addressBook.get_contacts()
1404 except Exception, e:
1406 self._isPopulated = False
1407 self._errorDisplay.push_exception_with_lock()
1408 for contactId, contactName in contacts:
1409 contactType = (addressBook.contact_source_short_name(contactId), )
1410 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1412 with gtk_toolbox.gtk_lock():
1413 self._contactsview.set_model(self._contactsmodel)
1415 self._isPopulated = True
1416 except Exception, e:
1417 self._errorDisplay.push_exception_with_lock()
1419 with gtk_toolbox.gtk_lock():
1420 hildonize.show_busy_banner_end(banner)
1423 def _on_addressbook_button_changed(self, *args, **kwds):
1426 newSelectedComboIndex = hildonize.touch_selector(
1429 (("%s" % m[2]) for m in self._booksList),
1430 self._selectedComboIndex,
1432 except RuntimeError:
1435 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1436 selectedBookId = self._booksList[newSelectedComboIndex][1]
1437 self.open_addressbook(selectedFactoryId, selectedBookId)
1438 self._selectedComboIndex = newSelectedComboIndex
1439 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1440 except Exception, e:
1441 self._errorDisplay.push_exception()
1443 def _on_contactsview_row_activated(self, treeview, path, view_column):
1445 model, itr = self._contactsviewselection.get_selected()
1449 contactId = self._contactsmodel.get_value(itr, 3)
1450 contactName = self._contactsmodel.get_value(itr, 1)
1452 contactDetails = self._addressBook.get_contact_details(contactId)
1453 except Exception, e:
1455 self._errorDisplay.push_exception()
1456 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1458 if len(contactPhoneNumbers) == 0:
1461 action, phoneNumber, message = self._phoneTypeSelector.run(
1462 contactPhoneNumbers,
1463 messages = (contactName, ),
1464 parent = self._window,
1466 if action == PhoneTypeSelector.ACTION_CANCEL:
1468 assert phoneNumber, "A lack of phone number exists"
1470 self.number_selected(action, phoneNumber, message)
1471 self._contactsviewselection.unselect_all()
1472 except Exception, e:
1473 self._errorDisplay.push_exception()