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 @bug Contacts don't populate
24 @bug large scrollbars aren't working
25 @bug exception when killing banner on contacts
26 @bug No showing of any banners
29 from __future__ import with_statement
43 def make_ugly(prettynumber):
45 function to take a phone number and strip out all non-numeric
48 >>> make_ugly("+012-(345)-678-90")
52 uglynumber = re.sub('\D', '', prettynumber)
56 def make_pretty(phonenumber):
58 Function to take a phone number and return the pretty version
60 if phonenumber begins with 0:
62 if phonenumber begins with 1: ( for gizmo callback numbers )
64 if phonenumber is 13 digits:
66 if phonenumber is 10 digits:
70 >>> make_pretty("1234567")
72 >>> make_pretty("2345678901")
74 >>> make_pretty("12345678901")
76 >>> make_pretty("01234567890")
79 if phonenumber is None or phonenumber is "":
82 phonenumber = make_ugly(phonenumber)
84 if len(phonenumber) < 3:
87 if phonenumber[0] == "0":
89 prettynumber += "+%s" % phonenumber[0:3]
90 if 3 < len(phonenumber):
91 prettynumber += "-(%s)" % phonenumber[3:6]
92 if 6 < len(phonenumber):
93 prettynumber += "-%s" % phonenumber[6:9]
94 if 9 < len(phonenumber):
95 prettynumber += "-%s" % phonenumber[9:]
97 elif len(phonenumber) <= 7:
98 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
99 elif len(phonenumber) > 8 and phonenumber[0] == "1":
100 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
101 elif len(phonenumber) > 7:
102 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
106 def abbrev_relative_date(date):
108 >>> abbrev_relative_date("42 hours ago")
110 >>> abbrev_relative_date("2 days ago")
112 >>> abbrev_relative_date("4 weeks ago")
115 parts = date.split(" ")
116 return "%s %s" % (parts[0], parts[1][0])
119 class MergedAddressBook(object):
121 Merger of all addressbooks
124 def __init__(self, addressbookFactories, sorter = None):
125 self.__addressbookFactories = addressbookFactories
126 self.__addressbooks = None
127 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
129 def clear_caches(self):
130 self.__addressbooks = None
131 for factory in self.__addressbookFactories:
132 factory.clear_caches()
134 def get_addressbooks(self):
136 @returns Iterable of (Address Book Factory, Book Id, Book Name)
140 def open_addressbook(self, bookId):
143 def contact_source_short_name(self, contactId):
144 if self.__addressbooks is None:
146 bookIndex, originalId = contactId.split("-", 1)
147 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
151 return "All Contacts"
153 def get_contacts(self):
155 @returns Iterable of (contact id, contact name)
157 if self.__addressbooks is None:
158 self.__addressbooks = list(
159 factory.open_addressbook(id)
160 for factory in self.__addressbookFactories
161 for (f, id, name) in factory.get_addressbooks()
164 ("-".join([str(bookIndex), contactId]), contactName)
165 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
166 for (contactId, contactName) in addressbook.get_contacts()
168 sortedContacts = self.__sort_contacts(contacts)
169 return sortedContacts
171 def get_contact_details(self, contactId):
173 @returns Iterable of (Phone Type, Phone Number)
175 if self.__addressbooks is None:
177 bookIndex, originalId = contactId.split("-", 1)
178 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
181 def null_sorter(contacts):
183 Good for speed/low memory
188 def basic_firtname_sorter(contacts):
190 Expects names in "First Last" format
193 (contactName.rsplit(" ", 1)[0], (contactId, contactName))
194 for (contactId, contactName) in contacts
196 contactsWithKey.sort()
197 return (contactData for (lastName, contactData) in contactsWithKey)
200 def basic_lastname_sorter(contacts):
202 Expects names in "First Last" format
205 (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
206 for (contactId, contactName) in contacts
208 contactsWithKey.sort()
209 return (contactData for (lastName, contactData) in contactsWithKey)
212 def reversed_firtname_sorter(contacts):
214 Expects names in "Last, First" format
217 (contactName.split(", ", 1)[-1], (contactId, contactName))
218 for (contactId, contactName) in contacts
220 contactsWithKey.sort()
221 return (contactData for (lastName, contactData) in contactsWithKey)
224 def reversed_lastname_sorter(contacts):
226 Expects names in "Last, First" format
229 (contactName.split(", ", 1)[0], (contactId, contactName))
230 for (contactId, contactName) in contacts
232 contactsWithKey.sort()
233 return (contactData for (lastName, contactData) in contactsWithKey)
236 def guess_firstname(name):
238 return name.split(", ", 1)[-1]
240 return name.rsplit(" ", 1)[0]
243 def guess_lastname(name):
245 return name.split(", ", 1)[0]
247 return name.rsplit(" ", 1)[-1]
250 def advanced_firstname_sorter(cls, contacts):
252 (cls.guess_firstname(contactName), (contactId, contactName))
253 for (contactId, contactName) in contacts
255 contactsWithKey.sort()
256 return (contactData for (lastName, contactData) in contactsWithKey)
259 def advanced_lastname_sorter(cls, contacts):
261 (cls.guess_lastname(contactName), (contactId, contactName))
262 for (contactId, contactName) in contacts
264 contactsWithKey.sort()
265 return (contactData for (lastName, contactData) in contactsWithKey)
268 class PhoneTypeSelector(object):
270 ACTION_CANCEL = "cancel"
271 ACTION_SELECT = "select"
273 ACTION_SEND_SMS = "sms"
275 def __init__(self, widgetTree, gcBackend):
276 self._gcBackend = gcBackend
277 self._widgetTree = widgetTree
279 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
280 self._smsDialog = SmsEntryDialog(self._widgetTree)
282 self._smsButton = self._widgetTree.get_widget("sms_button")
283 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
285 self._dialButton = self._widgetTree.get_widget("dial_button")
286 self._dialButton.connect("clicked", self._on_phonetype_dial)
288 self._selectButton = self._widgetTree.get_widget("select_button")
289 self._selectButton.connect("clicked", self._on_phonetype_select)
291 self._cancelButton = self._widgetTree.get_widget("cancel_button")
292 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
294 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
295 self._messagesView = self._widgetTree.get_widget("phoneSelectionMessages")
296 self._scrollWindow = self._messagesView.get_parent()
298 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
299 self._typeviewselection = None
300 self._typeview = self._widgetTree.get_widget("phonetypes")
301 self._typeview.connect("row-activated", self._on_phonetype_select)
303 self._action = self.ACTION_CANCEL
305 def run(self, contactDetails, messages = (), parent = None):
306 self._action = self.ACTION_CANCEL
308 # Add the column to the phone selection tree view
309 self._typemodel.clear()
310 self._typeview.set_model(self._typemodel)
312 textrenderer = gtk.CellRendererText()
313 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
314 self._typeview.append_column(numberColumn)
316 textrenderer = gtk.CellRendererText()
317 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
318 self._typeview.append_column(typeColumn)
320 for phoneType, phoneNumber in contactDetails:
321 display = " - ".join((phoneNumber, phoneType))
323 row = (phoneNumber, display)
324 self._typemodel.append(row)
326 self._typeviewselection = self._typeview.get_selection()
327 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
328 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
330 # Add the column to the messages tree view
331 self._messagemodel.clear()
332 self._messagesView.set_model(self._messagemodel)
334 textrenderer = gtk.CellRendererText()
335 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
336 textrenderer.set_property("wrap-width", 450)
337 messageColumn = gtk.TreeViewColumn("")
338 messageColumn.pack_start(textrenderer, expand=True)
339 messageColumn.add_attribute(textrenderer, "markup", 0)
340 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
341 self._messagesView.append_column(messageColumn)
342 self._messagesView.set_headers_visible(False)
345 for message in messages:
347 self._messagemodel.append(row)
348 self._messagesView.show()
349 self._scrollWindow.show()
350 messagesSelection = self._messagesView.get_selection()
351 messagesSelection.select_path((len(messages)-1, ))
353 self._messagesView.hide()
354 self._scrollWindow.hide()
356 if parent is not None:
357 self._dialog.set_transient_for(parent)
362 self._messagesView.scroll_to_cell((len(messages)-1, ))
364 userResponse = self._dialog.run()
368 if userResponse == gtk.RESPONSE_OK:
369 phoneNumber = self._get_number()
370 phoneNumber = make_ugly(phoneNumber)
374 self._action = self.ACTION_CANCEL
376 if self._action == self.ACTION_SEND_SMS:
377 smsMessage = self._smsDialog.run(phoneNumber, messages, parent)
380 self._action = self.ACTION_CANCEL
384 self._messagesView.remove_column(messageColumn)
385 self._messagesView.set_model(None)
387 self._typeviewselection.unselect_all()
388 self._typeview.remove_column(numberColumn)
389 self._typeview.remove_column(typeColumn)
390 self._typeview.set_model(None)
392 return self._action, phoneNumber, smsMessage
394 def _get_number(self):
395 model, itr = self._typeviewselection.get_selected()
399 phoneNumber = self._typemodel.get_value(itr, 0)
402 def _on_phonetype_dial(self, *args):
403 self._dialog.response(gtk.RESPONSE_OK)
404 self._action = self.ACTION_DIAL
406 def _on_phonetype_send_sms(self, *args):
407 self._dialog.response(gtk.RESPONSE_OK)
408 self._action = self.ACTION_SEND_SMS
410 def _on_phonetype_select(self, *args):
411 self._dialog.response(gtk.RESPONSE_OK)
412 self._action = self.ACTION_SELECT
414 def _on_phonetype_cancel(self, *args):
415 self._dialog.response(gtk.RESPONSE_CANCEL)
416 self._action = self.ACTION_CANCEL
419 class SmsEntryDialog(object):
421 @todo Add multi-SMS messages like GoogleVoice
426 def __init__(self, widgetTree):
427 self._widgetTree = widgetTree
428 self._dialog = self._widgetTree.get_widget("smsDialog")
430 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
431 self._smsButton.connect("clicked", self._on_send)
433 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
434 self._cancelButton.connect("clicked", self._on_cancel)
436 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
438 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
439 self._messagesView = self._widgetTree.get_widget("smsMessages")
440 self._scrollWindow = self._messagesView.get_parent()
442 self._smsEntry = self._widgetTree.get_widget("smsEntry")
443 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
445 def run(self, number, messages = (), parent = None):
446 # Add the column to the messages tree view
447 self._messagemodel.clear()
448 self._messagesView.set_model(self._messagemodel)
450 textrenderer = gtk.CellRendererText()
451 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
452 textrenderer.set_property("wrap-width", 450)
453 messageColumn = gtk.TreeViewColumn("")
454 messageColumn.pack_start(textrenderer, expand=True)
455 messageColumn.add_attribute(textrenderer, "markup", 0)
456 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
457 self._messagesView.append_column(messageColumn)
458 self._messagesView.set_headers_visible(False)
461 for message in messages:
463 self._messagemodel.append(row)
464 self._messagesView.show()
465 self._scrollWindow.show()
466 messagesSelection = self._messagesView.get_selection()
467 messagesSelection.select_path((len(messages)-1, ))
469 self._messagesView.hide()
470 self._scrollWindow.hide()
472 self._smsEntry.get_buffer().set_text("")
473 self._update_letter_count()
475 if parent is not None:
476 self._dialog.set_transient_for(parent)
481 self._messagesView.scroll_to_cell((len(messages)-1, ))
482 self._smsEntry.grab_focus()
484 userResponse = self._dialog.run()
488 if userResponse == gtk.RESPONSE_OK:
489 entryBuffer = self._smsEntry.get_buffer()
490 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
491 enteredMessage = enteredMessage[0:self.MAX_CHAR]
495 self._messagesView.remove_column(messageColumn)
496 self._messagesView.set_model(None)
498 return enteredMessage.strip()
500 def _update_letter_count(self, *args):
501 entryLength = self._smsEntry.get_buffer().get_char_count()
502 charsLeft = self.MAX_CHAR - entryLength
503 self._letterCountLabel.set_text(str(charsLeft))
505 self._smsButton.set_sensitive(False)
507 self._smsButton.set_sensitive(True)
509 def _on_entry_changed(self, *args):
510 self._update_letter_count()
512 def _on_send(self, *args):
513 self._dialog.response(gtk.RESPONSE_OK)
515 def _on_cancel(self, *args):
516 self._dialog.response(gtk.RESPONSE_CANCEL)
519 class Dialpad(object):
521 def __init__(self, widgetTree, errorDisplay):
522 self._clipboard = gtk.clipboard_get()
523 self._errorDisplay = errorDisplay
524 self._smsDialog = SmsEntryDialog(widgetTree)
526 self._numberdisplay = widgetTree.get_widget("numberdisplay")
527 self._smsButton = widgetTree.get_widget("sms")
528 self._dialButton = widgetTree.get_widget("dial")
529 self._backButton = widgetTree.get_widget("back")
530 self._phonenumber = ""
531 self._prettynumber = ""
534 "on_digit_clicked": self._on_digit_clicked,
536 widgetTree.signal_autoconnect(callbackMapping)
537 self._dialButton.connect("clicked", self._on_dial_clicked)
538 self._smsButton.connect("clicked", self._on_sms_clicked)
540 self._originalLabel = self._backButton.get_label()
541 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
542 self._backTapHandler.on_tap = self._on_backspace
543 self._backTapHandler.on_hold = self._on_clearall
544 self._backTapHandler.on_holding = self._set_clear_button
545 self._backTapHandler.on_cancel = self._reset_back_button
547 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
548 self._keyPressEventId = 0
551 self._dialButton.grab_focus()
552 self._backTapHandler.enable()
553 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
556 self._window.disconnect(self._keyPressEventId)
557 self._keyPressEventId = 0
558 self._reset_back_button()
559 self._backTapHandler.disable()
561 def number_selected(self, action, number, message):
563 @note Actual dial function is patched in later
565 raise NotImplementedError("Horrible unknown error has occurred")
567 def get_number(self):
568 return self._phonenumber
570 def set_number(self, number):
572 Set the number to dial
575 self._phonenumber = make_ugly(number)
576 self._prettynumber = make_pretty(self._phonenumber)
577 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
579 self._errorDisplay.push_exception()
588 def load_settings(self, config, section):
591 def save_settings(self, config, section):
593 @note Thread Agnostic
597 def _on_key_press(self, widget, event):
599 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
600 contents = self._clipboard.wait_for_text()
601 if contents is not None:
602 self.set_number(contents)
604 self._errorDisplay.push_exception()
606 def _on_sms_clicked(self, widget):
608 action = PhoneTypeSelector.ACTION_SEND_SMS
609 phoneNumber = self.get_number()
611 message = self._smsDialog.run(phoneNumber, (), self._window)
614 action = PhoneTypeSelector.ACTION_CANCEL
616 if action == PhoneTypeSelector.ACTION_CANCEL:
618 self.number_selected(action, phoneNumber, message)
620 self._errorDisplay.push_exception()
622 def _on_dial_clicked(self, widget):
624 action = PhoneTypeSelector.ACTION_DIAL
625 phoneNumber = self.get_number()
627 self.number_selected(action, phoneNumber, message)
629 self._errorDisplay.push_exception()
631 def _on_digit_clicked(self, widget):
633 self.set_number(self._phonenumber + widget.get_name()[-1])
635 self._errorDisplay.push_exception()
637 def _on_backspace(self, taps):
639 self.set_number(self._phonenumber[:-taps])
640 self._reset_back_button()
642 self._errorDisplay.push_exception()
644 def _on_clearall(self, taps):
647 self._reset_back_button()
649 self._errorDisplay.push_exception()
652 def _set_clear_button(self):
654 self._backButton.set_label("gtk-clear")
656 self._errorDisplay.push_exception()
658 def _reset_back_button(self):
660 self._backButton.set_label(self._originalLabel)
662 self._errorDisplay.push_exception()
665 class AccountInfo(object):
667 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
668 self._errorDisplay = errorDisplay
669 self._backend = backend
670 self._isPopulated = False
671 self._alarmHandler = alarmHandler
672 self._notifyOnMissed = False
673 self._notifyOnVoicemail = False
674 self._notifyOnSms = False
676 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
677 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
678 self._callbackCombo = widgetTree.get_widget("callbackcombo")
679 self._onCallbackentryChangedId = 0
681 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
682 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
683 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
684 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
685 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
686 self._onNotifyToggled = 0
687 self._onMinutesChanged = 0
688 self._onMissedToggled = 0
689 self._onVoicemailToggled = 0
690 self._onSmsToggled = 0
691 self._applyAlarmTimeoutId = None
693 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
694 self._defaultCallback = ""
697 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
699 self._accountViewNumberDisplay.set_use_markup(True)
700 self.set_account_number("")
702 self._callbackList.clear()
703 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
705 if self._alarmHandler is not None:
706 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
707 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
708 self._missedCheckbox.set_active(self._notifyOnMissed)
709 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
710 self._smsCheckbox.set_active(self._notifyOnSms)
712 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
713 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
714 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
715 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
716 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
718 self._notifyCheckbox.set_sensitive(False)
719 self._minutesEntryButton.set_sensitive(False)
720 self._missedCheckbox.set_sensitive(False)
721 self._voicemailCheckbox.set_sensitive(False)
722 self._smsCheckbox.set_sensitive(False)
724 self.update(force=True)
727 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
728 self._onCallbackentryChangedId = 0
730 if self._alarmHandler is not None:
731 self._notifyCheckbox.disconnect(self._onNotifyToggled)
732 self._minutesEntryButton.disconnect(self._onMinutesChanged)
733 self._missedCheckbox.disconnect(self._onNotifyToggled)
734 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
735 self._smsCheckbox.disconnect(self._onNotifyToggled)
736 self._onNotifyToggled = 0
737 self._onMinutesChanged = 0
738 self._onMissedToggled = 0
739 self._onVoicemailToggled = 0
740 self._onSmsToggled = 0
742 self._notifyCheckbox.set_sensitive(True)
743 self._minutesEntryButton.set_sensitive(True)
744 self._missedCheckbox.set_sensitive(True)
745 self._voicemailCheckbox.set_sensitive(True)
746 self._smsCheckbox.set_sensitive(True)
749 self._callbackList.clear()
751 def get_selected_callback_number(self):
752 return make_ugly(self._callbackCombo.get_child().get_text())
754 def set_account_number(self, number):
756 Displays current account number
758 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
760 def update(self, force = False):
761 if not force and self._isPopulated:
763 self._populate_callback_combo()
764 self.set_account_number(self._backend.get_account_number())
768 self._callbackCombo.get_child().set_text("")
769 self.set_account_number("")
770 self._isPopulated = False
772 def save_everything(self):
773 raise NotImplementedError
777 return "Account Info"
779 def load_settings(self, config, section):
780 self._defaultCallback = config.get(section, "callback")
781 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
782 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
783 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
785 def save_settings(self, config, section):
787 @note Thread Agnostic
789 callback = self.get_selected_callback_number()
790 config.set(section, "callback", callback)
791 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
792 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
793 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
795 def _populate_callback_combo(self):
796 self._isPopulated = True
797 self._callbackList.clear()
799 callbackNumbers = self._backend.get_callback_numbers()
801 self._errorDisplay.push_exception()
802 self._isPopulated = False
805 for number, description in callbackNumbers.iteritems():
806 self._callbackList.append((make_pretty(number),))
808 self._callbackCombo.set_model(self._callbackList)
809 self._callbackCombo.set_text_column(0)
810 #callbackNumber = self._backend.get_callback_number()
811 callbackNumber = self._defaultCallback
812 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
814 def _set_callback_number(self, number):
816 if not self._backend.is_valid_syntax(number) and 0 < len(number):
817 self._errorDisplay.push_message("%s is not a valid callback number" % number)
818 elif number == self._backend.get_callback_number():
820 "Callback number already is %s" % (
821 self._backend.get_callback_number(),
825 self._backend.set_callback_number(number)
826 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
827 make_pretty(number), make_pretty(self._backend.get_callback_number())
830 "Callback number set to %s" % (
831 self._backend.get_callback_number(),
835 self._errorDisplay.push_exception()
837 def _update_alarm_settings(self, recurrence):
839 isEnabled = self._notifyCheckbox.get_active()
840 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
841 self._alarmHandler.apply_settings(isEnabled, recurrence)
843 self.save_everything()
844 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
845 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
847 def _on_callbackentry_changed(self, *args):
849 text = self.get_selected_callback_number()
850 number = make_ugly(text)
851 self._set_callback_number(number)
853 self._errorDisplay.push_exception()
855 def _on_notify_toggled(self, *args):
857 if self._applyAlarmTimeoutId is not None:
858 gobject.source_remove(self._applyAlarmTimeoutId)
859 self._applyAlarmTimeoutId = None
860 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
862 self._errorDisplay.push_exception()
864 def _on_minutes_clicked(self, *args):
865 recurrenceChoices = [
877 actualSelection = self._alarmHandler.recurrence
879 closestSelectionIndex = 0
880 for i, possible in enumerate(recurrenceChoices):
881 if possible[0] <= actualSelection:
882 closestSelectionIndex = i
883 recurrenceIndex = hildonize.touch_selector(
886 (("%s" % m[1]) for m in recurrenceChoices),
887 closestSelectionIndex,
889 recurrence = recurrenceChoices[recurrenceIndex][0]
891 self._update_alarm_settings(recurrence)
893 self._errorDisplay.push_exception()
895 def _on_apply_timeout(self, *args):
897 self._applyAlarmTimeoutId = None
899 self._update_alarm_settings(self._alarmHandler.recurrence)
901 self._errorDisplay.push_exception()
904 def _on_missed_toggled(self, *args):
906 self._notifyOnMissed = self._missedCheckbox.get_active()
907 self.save_everything()
909 self._errorDisplay.push_exception()
911 def _on_voicemail_toggled(self, *args):
913 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
914 self.save_everything()
916 self._errorDisplay.push_exception()
918 def _on_sms_toggled(self, *args):
920 self._notifyOnSms = self._smsCheckbox.get_active()
921 self.save_everything()
923 self._errorDisplay.push_exception()
926 class RecentCallsView(object):
933 def __init__(self, widgetTree, backend, errorDisplay):
934 self._errorDisplay = errorDisplay
935 self._backend = backend
937 self._isPopulated = False
938 self._recentmodel = gtk.ListStore(
939 gobject.TYPE_STRING, # number
940 gobject.TYPE_STRING, # date
941 gobject.TYPE_STRING, # action
942 gobject.TYPE_STRING, # from
944 self._recentview = widgetTree.get_widget("recentview")
945 self._recentviewselection = None
946 self._onRecentviewRowActivatedId = 0
948 textrenderer = gtk.CellRendererText()
949 textrenderer.set_property("yalign", 0)
950 self._dateColumn = gtk.TreeViewColumn("Date")
951 self._dateColumn.pack_start(textrenderer, expand=True)
952 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
954 textrenderer = gtk.CellRendererText()
955 textrenderer.set_property("yalign", 0)
956 self._actionColumn = gtk.TreeViewColumn("Action")
957 self._actionColumn.pack_start(textrenderer, expand=True)
958 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
960 textrenderer = gtk.CellRendererText()
961 textrenderer.set_property("yalign", 0)
962 hildonize.set_cell_thumb_selectable(textrenderer)
963 self._nameColumn = gtk.TreeViewColumn("From")
964 self._nameColumn.pack_start(textrenderer, expand=True)
965 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
966 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
968 textrenderer = gtk.CellRendererText()
969 textrenderer.set_property("yalign", 0)
970 self._numberColumn = gtk.TreeViewColumn("Number")
971 self._numberColumn.pack_start(textrenderer, expand=True)
972 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
974 self._window = gtk_toolbox.find_parent_window(self._recentview)
975 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
977 self._updateSink = gtk_toolbox.threaded_stage(
979 self._idly_populate_recentview,
980 gtk_toolbox.null_sink(),
985 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
986 self._recentview.set_model(self._recentmodel)
988 self._recentview.append_column(self._dateColumn)
989 self._recentview.append_column(self._actionColumn)
990 self._recentview.append_column(self._numberColumn)
991 self._recentview.append_column(self._nameColumn)
992 self._recentviewselection = self._recentview.get_selection()
993 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
995 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
998 self._recentview.disconnect(self._onRecentviewRowActivatedId)
1002 self._recentview.remove_column(self._dateColumn)
1003 self._recentview.remove_column(self._actionColumn)
1004 self._recentview.remove_column(self._nameColumn)
1005 self._recentview.remove_column(self._numberColumn)
1006 self._recentview.set_model(None)
1008 def number_selected(self, action, number, message):
1010 @note Actual dial function is patched in later
1012 raise NotImplementedError("Horrible unknown error has occurred")
1014 def update(self, force = False):
1015 if not force and self._isPopulated:
1017 self._updateSink.send(())
1021 self._isPopulated = False
1022 self._recentmodel.clear()
1026 return "Recent Calls"
1028 def load_settings(self, config, section):
1031 def save_settings(self, config, section):
1033 @note Thread Agnostic
1037 def _idly_populate_recentview(self):
1038 with gtk_toolbox.gtk_lock():
1039 banner = hildonize.show_busy_banner_start(self._window, "Loading Recent History")
1041 self._recentmodel.clear()
1042 self._isPopulated = True
1045 recentItems = self._backend.get_recent()
1046 except Exception, e:
1047 self._errorDisplay.push_exception_with_lock()
1048 self._isPopulated = False
1051 for personName, phoneNumber, date, action in recentItems:
1053 personName = "Unknown"
1054 date = abbrev_relative_date(date)
1055 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1056 prettyNumber = make_pretty(prettyNumber)
1057 item = (prettyNumber, date, action.capitalize(), personName)
1058 with gtk_toolbox.gtk_lock():
1059 self._recentmodel.append(item)
1060 except Exception, e:
1061 self._errorDisplay.push_exception_with_lock()
1063 with gtk_toolbox.gtk_lock():
1064 hildonize.show_busy_banner_end(banner)
1068 def _on_recentview_row_activated(self, treeview, path, view_column):
1070 model, itr = self._recentviewselection.get_selected()
1074 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1075 number = make_ugly(number)
1076 contactPhoneNumbers = [("Phone", number)]
1077 description = self._recentmodel.get_value(itr, self.FROM_IDX)
1079 action, phoneNumber, message = self._phoneTypeSelector.run(
1080 contactPhoneNumbers,
1081 messages = (description, ),
1082 parent = self._window,
1084 if action == PhoneTypeSelector.ACTION_CANCEL:
1086 assert phoneNumber, "A lack of phone number exists"
1088 self.number_selected(action, phoneNumber, message)
1089 self._recentviewselection.unselect_all()
1090 except Exception, e:
1091 self._errorDisplay.push_exception()
1094 class MessagesView(object):
1102 def __init__(self, widgetTree, backend, errorDisplay):
1103 self._errorDisplay = errorDisplay
1104 self._backend = backend
1106 self._isPopulated = False
1107 self._messagemodel = gtk.ListStore(
1108 gobject.TYPE_STRING, # number
1109 gobject.TYPE_STRING, # date
1110 gobject.TYPE_STRING, # header
1111 gobject.TYPE_STRING, # message
1114 self._messageview = widgetTree.get_widget("messages_view")
1115 self._messageviewselection = None
1116 self._onMessageviewRowActivatedId = 0
1118 self._messageRenderer = gtk.CellRendererText()
1119 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1120 self._messageRenderer.set_property("wrap-width", 500)
1121 self._messageColumn = gtk.TreeViewColumn("Messages")
1122 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1123 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1124 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1126 self._window = gtk_toolbox.find_parent_window(self._messageview)
1127 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1129 self._updateSink = gtk_toolbox.threaded_stage(
1131 self._idly_populate_messageview,
1132 gtk_toolbox.null_sink(),
1137 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1138 self._messageview.set_model(self._messagemodel)
1140 self._messageview.append_column(self._messageColumn)
1141 self._messageviewselection = self._messageview.get_selection()
1142 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1144 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1147 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1151 self._messageview.remove_column(self._messageColumn)
1152 self._messageview.set_model(None)
1154 def number_selected(self, action, number, message):
1156 @note Actual dial function is patched in later
1158 raise NotImplementedError("Horrible unknown error has occurred")
1160 def update(self, force = False):
1161 if not force and self._isPopulated:
1163 self._updateSink.send(())
1167 self._isPopulated = False
1168 self._messagemodel.clear()
1174 def load_settings(self, config, section):
1177 def save_settings(self, config, section):
1179 @note Thread Agnostic
1183 def _idly_populate_messageview(self):
1184 with gtk_toolbox.gtk_lock():
1185 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1187 self._messagemodel.clear()
1188 self._isPopulated = True
1191 messageItems = self._backend.get_messages()
1192 except Exception, e:
1193 self._errorDisplay.push_exception_with_lock()
1194 self._isPopulated = False
1197 for header, number, relativeDate, messages in messageItems:
1198 prettyNumber = number[2:] if number.startswith("+1") else number
1199 prettyNumber = make_pretty(prettyNumber)
1201 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1202 newMessages = [firstMessage]
1203 newMessages.extend(messages)
1205 number = make_ugly(number)
1207 row = (number, relativeDate, header, "\n".join(newMessages), newMessages)
1208 with gtk_toolbox.gtk_lock():
1209 self._messagemodel.append(row)
1210 except Exception, e:
1211 self._errorDisplay.push_exception_with_lock()
1213 with gtk_toolbox.gtk_lock():
1214 hildonize.show_busy_banner_end(banner)
1218 def _on_messageview_row_activated(self, treeview, path, view_column):
1220 model, itr = self._messageviewselection.get_selected()
1224 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1225 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1227 action, phoneNumber, message = self._phoneTypeSelector.run(
1228 contactPhoneNumbers,
1229 messages = description,
1230 parent = self._window,
1232 if action == PhoneTypeSelector.ACTION_CANCEL:
1234 assert phoneNumber, "A lock of phone number exists"
1236 self.number_selected(action, phoneNumber, message)
1237 self._messageviewselection.unselect_all()
1238 except Exception, e:
1239 self._errorDisplay.push_exception()
1242 class ContactsView(object):
1244 def __init__(self, widgetTree, backend, errorDisplay):
1245 self._errorDisplay = errorDisplay
1246 self._backend = backend
1248 self._addressBook = None
1249 self._selectedComboIndex = 0
1250 self._addressBookFactories = [null_backend.NullAddressBook()]
1252 self._booksList = []
1253 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1255 self._isPopulated = False
1256 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1257 self._contactsviewselection = None
1258 self._contactsview = widgetTree.get_widget("contactsview")
1260 self._contactColumn = gtk.TreeViewColumn("Contact")
1261 displayContactSource = False
1262 if displayContactSource:
1263 textrenderer = gtk.CellRendererText()
1264 self._contactColumn.pack_start(textrenderer, expand=False)
1265 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1266 textrenderer = gtk.CellRendererText()
1267 hildonize.set_cell_thumb_selectable(textrenderer)
1268 self._contactColumn.pack_start(textrenderer, expand=True)
1269 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1270 textrenderer = gtk.CellRendererText()
1271 self._contactColumn.pack_start(textrenderer, expand=True)
1272 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1273 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1274 self._contactColumn.set_sort_column_id(1)
1275 self._contactColumn.set_visible(True)
1277 self._onContactsviewRowActivatedId = 0
1278 self._onAddressbookButtonChangedId = 0
1279 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1280 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1282 self._updateSink = gtk_toolbox.threaded_stage(
1284 self._idly_populate_contactsview,
1285 gtk_toolbox.null_sink(),
1290 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1292 self._contactsview.set_model(self._contactsmodel)
1293 self._contactsview.append_column(self._contactColumn)
1294 self._contactsviewselection = self._contactsview.get_selection()
1295 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1297 del self._booksList[:]
1298 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1299 if factoryName and bookName:
1300 entryName = "%s: %s" % (factoryName, bookName)
1302 entryName = factoryName
1304 entryName = bookName
1306 entryName = "Bad name (%d)" % factoryId
1307 row = (str(factoryId), bookId, entryName)
1308 self._booksList.append(row)
1310 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1311 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1313 if len(self._booksList) <= self._selectedComboIndex:
1314 self._selectedComboIndex = 0
1315 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1317 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1318 selectedBookId = self._booksList[self._selectedComboIndex][1]
1319 self.open_addressbook(selectedFactoryId, selectedBookId)
1322 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1323 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1327 self._bookSelectionButton.set_label("")
1328 self._contactsview.set_model(None)
1329 self._contactsview.remove_column(self._contactColumn)
1331 def number_selected(self, action, number, message):
1333 @note Actual dial function is patched in later
1335 raise NotImplementedError("Horrible unknown error has occurred")
1337 def get_addressbooks(self):
1339 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1341 for i, factory in enumerate(self._addressBookFactories):
1342 for bookFactory, bookId, bookName in factory.get_addressbooks():
1343 yield (str(i), bookId), (factory.factory_name(), bookName)
1345 def open_addressbook(self, bookFactoryId, bookId):
1346 bookFactoryIndex = int(bookFactoryId)
1347 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1349 forceUpdate = True if addressBook is not self._addressBook else False
1351 self._addressBook = addressBook
1352 self.update(force=forceUpdate)
1354 def update(self, force = False):
1355 if not force and self._isPopulated:
1357 self._updateSink.send(())
1361 self._isPopulated = False
1362 self._contactsmodel.clear()
1363 for factory in self._addressBookFactories:
1364 factory.clear_caches()
1365 self._addressBook.clear_caches()
1367 def append(self, book):
1368 self._addressBookFactories.append(book)
1370 def extend(self, books):
1371 self._addressBookFactories.extend(books)
1377 def load_settings(self, config, sectionName):
1379 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1380 except ConfigParser.NoOptionError:
1381 self._selectedComboIndex = 0
1383 def save_settings(self, config, sectionName):
1384 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1386 def _idly_populate_contactsview(self):
1387 with gtk_toolbox.gtk_lock():
1388 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1391 while addressBook is not self._addressBook:
1392 addressBook = self._addressBook
1393 with gtk_toolbox.gtk_lock():
1394 self._contactsview.set_model(None)
1398 contacts = addressBook.get_contacts()
1399 except Exception, e:
1401 self._isPopulated = False
1402 self._errorDisplay.push_exception_with_lock()
1403 for contactId, contactName in contacts:
1404 contactType = (addressBook.contact_source_short_name(contactId), )
1405 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1407 with gtk_toolbox.gtk_lock():
1408 self._contactsview.set_model(self._contactsmodel)
1410 self._isPopulated = True
1411 except Exception, e:
1412 self._errorDisplay.push_exception_with_lock()
1414 with gtk_toolbox.gtk_lock():
1415 hildonize.show_busy_banner_end(banner)
1418 def _on_addressbook_button_changed(self, *args, **kwds):
1421 newSelectedComboIndex = hildonize.touch_selector(
1424 (("%s" % m[2]) for m in self._booksList),
1425 self._selectedComboIndex,
1427 except RuntimeError:
1430 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1431 selectedBookId = self._booksList[newSelectedComboIndex][1]
1432 self.open_addressbook(selectedFactoryId, selectedBookId)
1433 self._selectedComboIndex = newSelectedComboIndex
1434 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1435 except Exception, e:
1436 self._errorDisplay.push_exception()
1438 def _on_contactsview_row_activated(self, treeview, path, view_column):
1440 model, itr = self._contactsviewselection.get_selected()
1444 contactId = self._contactsmodel.get_value(itr, 3)
1445 contactName = self._contactsmodel.get_value(itr, 1)
1447 contactDetails = self._addressBook.get_contact_details(contactId)
1448 except Exception, e:
1450 self._errorDisplay.push_exception()
1451 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1453 if len(contactPhoneNumbers) == 0:
1456 action, phoneNumber, message = self._phoneTypeSelector.run(
1457 contactPhoneNumbers,
1458 messages = (contactName, ),
1459 parent = self._window,
1461 if action == PhoneTypeSelector.ACTION_CANCEL:
1463 assert phoneNumber, "A lack of phone number exists"
1465 self.number_selected(action, phoneNumber, message)
1466 self._contactsviewselection.unselect_all()
1467 except Exception, e:
1468 self._errorDisplay.push_exception()