4 DialCentral - Front end for Google's Grand Central 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 Add CTRL-V support to Dialpad
22 @todo Touch selector for callback number
23 @todo Alternate UI for dialogs (stackables)
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 = [
874 actualSelection = self._alarmHandler.recurrence
876 closestSelectionIndex = 0
877 for i, possible in enumerate(recurrenceChoices):
878 if possible[0] <= actualSelection:
879 closestSelectionIndex = i
880 recurrenceIndex = hildonize.touch_selector(
883 (("%s" % m[1]) for m in recurrenceChoices),
884 closestSelectionIndex,
886 recurrence = recurrenceChoices[recurrenceIndex][0]
888 self._update_alarm_settings(recurrence)
890 self._errorDisplay.push_exception()
892 def _on_apply_timeout(self, *args):
894 self._applyAlarmTimeoutId = None
896 self._update_alarm_settings(self._alarmHandler.recurrence)
898 self._errorDisplay.push_exception()
901 def _on_missed_toggled(self, *args):
903 self._notifyOnMissed = self._missedCheckbox.get_active()
904 self.save_everything()
906 self._errorDisplay.push_exception()
908 def _on_voicemail_toggled(self, *args):
910 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
911 self.save_everything()
913 self._errorDisplay.push_exception()
915 def _on_sms_toggled(self, *args):
917 self._notifyOnSms = self._smsCheckbox.get_active()
918 self.save_everything()
920 self._errorDisplay.push_exception()
923 class RecentCallsView(object):
930 def __init__(self, widgetTree, backend, errorDisplay):
931 self._errorDisplay = errorDisplay
932 self._backend = backend
934 self._isPopulated = False
935 self._recentmodel = gtk.ListStore(
936 gobject.TYPE_STRING, # number
937 gobject.TYPE_STRING, # date
938 gobject.TYPE_STRING, # action
939 gobject.TYPE_STRING, # from
941 self._recentview = widgetTree.get_widget("recentview")
942 self._recentviewselection = None
943 self._onRecentviewRowActivatedId = 0
945 textrenderer = gtk.CellRendererText()
946 textrenderer.set_property("yalign", 0)
947 self._dateColumn = gtk.TreeViewColumn("Date")
948 self._dateColumn.pack_start(textrenderer, expand=True)
949 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
951 textrenderer = gtk.CellRendererText()
952 textrenderer.set_property("yalign", 0)
953 self._actionColumn = gtk.TreeViewColumn("Action")
954 self._actionColumn.pack_start(textrenderer, expand=True)
955 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
957 textrenderer = gtk.CellRendererText()
958 textrenderer.set_property("yalign", 0)
959 hildonize.set_cell_thumb_selectable(textrenderer)
960 self._nameColumn = gtk.TreeViewColumn("From")
961 self._nameColumn.pack_start(textrenderer, expand=True)
962 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
963 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
965 textrenderer = gtk.CellRendererText()
966 textrenderer.set_property("yalign", 0)
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 self._window = gtk_toolbox.find_parent_window(self._recentview)
972 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
974 self._updateSink = gtk_toolbox.threaded_stage(
976 self._idly_populate_recentview,
977 gtk_toolbox.null_sink(),
982 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
983 self._recentview.set_model(self._recentmodel)
985 self._recentview.append_column(self._dateColumn)
986 self._recentview.append_column(self._actionColumn)
987 self._recentview.append_column(self._numberColumn)
988 self._recentview.append_column(self._nameColumn)
989 self._recentviewselection = self._recentview.get_selection()
990 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
992 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
995 self._recentview.disconnect(self._onRecentviewRowActivatedId)
999 self._recentview.remove_column(self._dateColumn)
1000 self._recentview.remove_column(self._actionColumn)
1001 self._recentview.remove_column(self._nameColumn)
1002 self._recentview.remove_column(self._numberColumn)
1003 self._recentview.set_model(None)
1005 def number_selected(self, action, number, message):
1007 @note Actual dial function is patched in later
1009 raise NotImplementedError("Horrible unknown error has occurred")
1011 def update(self, force = False):
1012 if not force and self._isPopulated:
1014 self._updateSink.send(())
1018 self._isPopulated = False
1019 self._recentmodel.clear()
1023 return "Recent Calls"
1025 def load_settings(self, config, section):
1028 def save_settings(self, config, section):
1030 @note Thread Agnostic
1034 def _idly_populate_recentview(self):
1036 self._recentmodel.clear()
1037 self._isPopulated = True
1040 recentItems = self._backend.get_recent()
1041 except Exception, e:
1042 self._errorDisplay.push_exception_with_lock()
1043 self._isPopulated = False
1046 for personName, phoneNumber, date, action in recentItems:
1048 personName = "Unknown"
1049 date = abbrev_relative_date(date)
1050 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1051 prettyNumber = make_pretty(prettyNumber)
1052 item = (prettyNumber, date, action.capitalize(), personName)
1053 with gtk_toolbox.gtk_lock():
1054 self._recentmodel.append(item)
1055 except Exception, e:
1056 self._errorDisplay.push_exception_with_lock()
1060 def _on_recentview_row_activated(self, treeview, path, view_column):
1062 model, itr = self._recentviewselection.get_selected()
1066 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1067 number = make_ugly(number)
1068 contactPhoneNumbers = [("Phone", number)]
1069 description = self._recentmodel.get_value(itr, self.FROM_IDX)
1071 action, phoneNumber, message = self._phoneTypeSelector.run(
1072 contactPhoneNumbers,
1073 messages = (description, ),
1074 parent = self._window,
1076 if action == PhoneTypeSelector.ACTION_CANCEL:
1078 assert phoneNumber, "A lack of phone number exists"
1080 self.number_selected(action, phoneNumber, message)
1081 self._recentviewselection.unselect_all()
1082 except Exception, e:
1083 self._errorDisplay.push_exception()
1086 class MessagesView(object):
1094 def __init__(self, widgetTree, backend, errorDisplay):
1095 self._errorDisplay = errorDisplay
1096 self._backend = backend
1098 self._isPopulated = False
1099 self._messagemodel = gtk.ListStore(
1100 gobject.TYPE_STRING, # number
1101 gobject.TYPE_STRING, # date
1102 gobject.TYPE_STRING, # header
1103 gobject.TYPE_STRING, # message
1106 self._messageview = widgetTree.get_widget("messages_view")
1107 self._messageviewselection = None
1108 self._onMessageviewRowActivatedId = 0
1110 self._messageRenderer = gtk.CellRendererText()
1111 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1112 self._messageRenderer.set_property("wrap-width", 500)
1113 self._messageColumn = gtk.TreeViewColumn("Messages")
1114 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1115 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1116 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1118 self._window = gtk_toolbox.find_parent_window(self._messageview)
1119 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1121 self._updateSink = gtk_toolbox.threaded_stage(
1123 self._idly_populate_messageview,
1124 gtk_toolbox.null_sink(),
1129 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1130 self._messageview.set_model(self._messagemodel)
1132 self._messageview.append_column(self._messageColumn)
1133 self._messageviewselection = self._messageview.get_selection()
1134 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1136 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1139 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1143 self._messageview.remove_column(self._messageColumn)
1144 self._messageview.set_model(None)
1146 def number_selected(self, action, number, message):
1148 @note Actual dial function is patched in later
1150 raise NotImplementedError("Horrible unknown error has occurred")
1152 def update(self, force = False):
1153 if not force and self._isPopulated:
1155 self._updateSink.send(())
1159 self._isPopulated = False
1160 self._messagemodel.clear()
1166 def load_settings(self, config, section):
1169 def save_settings(self, config, section):
1171 @note Thread Agnostic
1175 def _idly_populate_messageview(self):
1177 self._messagemodel.clear()
1178 self._isPopulated = True
1181 messageItems = self._backend.get_messages()
1182 except Exception, e:
1183 self._errorDisplay.push_exception_with_lock()
1184 self._isPopulated = False
1187 for header, number, relativeDate, messages in messageItems:
1188 prettyNumber = number[2:] if number.startswith("+1") else number
1189 prettyNumber = make_pretty(prettyNumber)
1191 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1192 newMessages = [firstMessage]
1193 newMessages.extend(messages)
1195 number = make_ugly(number)
1197 row = (number, relativeDate, header, "\n".join(newMessages), newMessages)
1198 with gtk_toolbox.gtk_lock():
1199 self._messagemodel.append(row)
1200 except Exception, e:
1201 self._errorDisplay.push_exception_with_lock()
1205 def _on_messageview_row_activated(self, treeview, path, view_column):
1207 model, itr = self._messageviewselection.get_selected()
1211 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1212 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1214 action, phoneNumber, message = self._phoneTypeSelector.run(
1215 contactPhoneNumbers,
1216 messages = description,
1217 parent = self._window,
1219 if action == PhoneTypeSelector.ACTION_CANCEL:
1221 assert phoneNumber, "A lock of phone number exists"
1223 self.number_selected(action, phoneNumber, message)
1224 self._messageviewselection.unselect_all()
1225 except Exception, e:
1226 self._errorDisplay.push_exception()
1229 class ContactsView(object):
1231 def __init__(self, widgetTree, backend, errorDisplay):
1232 self._errorDisplay = errorDisplay
1233 self._backend = backend
1235 self._addressBook = None
1236 self._selectedComboIndex = 0
1237 self._addressBookFactories = [null_backend.NullAddressBook()]
1239 self._booksList = []
1240 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1242 self._isPopulated = False
1243 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1244 self._contactsviewselection = None
1245 self._contactsview = widgetTree.get_widget("contactsview")
1247 self._contactColumn = gtk.TreeViewColumn("Contact")
1248 displayContactSource = False
1249 if displayContactSource:
1250 textrenderer = gtk.CellRendererText()
1251 self._contactColumn.pack_start(textrenderer, expand=False)
1252 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1253 textrenderer = gtk.CellRendererText()
1254 hildonize.set_cell_thumb_selectable(textrenderer)
1255 self._contactColumn.pack_start(textrenderer, expand=True)
1256 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1257 textrenderer = gtk.CellRendererText()
1258 self._contactColumn.pack_start(textrenderer, expand=True)
1259 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1260 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1261 self._contactColumn.set_sort_column_id(1)
1262 self._contactColumn.set_visible(True)
1264 self._onContactsviewRowActivatedId = 0
1265 self._onAddressbookButtonChangedId = 0
1266 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1267 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1269 self._updateSink = gtk_toolbox.threaded_stage(
1271 self._idly_populate_contactsview,
1272 gtk_toolbox.null_sink(),
1277 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1279 self._contactsview.set_model(self._contactsmodel)
1280 self._contactsview.append_column(self._contactColumn)
1281 self._contactsviewselection = self._contactsview.get_selection()
1282 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1284 del self._booksList[:]
1285 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1286 if factoryName and bookName:
1287 entryName = "%s: %s" % (factoryName, bookName)
1289 entryName = factoryName
1291 entryName = bookName
1293 entryName = "Bad name (%d)" % factoryId
1294 row = (str(factoryId), bookId, entryName)
1295 self._booksList.append(row)
1297 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1298 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1300 if len(self._booksList) <= self._selectedComboIndex:
1301 self._selectedComboIndex = 0
1302 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1304 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1305 selectedBookId = self._booksList[self._selectedComboIndex][1]
1306 self.open_addressbook(selectedFactoryId, selectedBookId)
1309 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1310 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1314 self._bookSelectionButton.set_label("")
1315 self._contactsview.set_model(None)
1316 self._contactsview.remove_column(self._contactColumn)
1318 def number_selected(self, action, number, message):
1320 @note Actual dial function is patched in later
1322 raise NotImplementedError("Horrible unknown error has occurred")
1324 def get_addressbooks(self):
1326 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1328 for i, factory in enumerate(self._addressBookFactories):
1329 for bookFactory, bookId, bookName in factory.get_addressbooks():
1330 yield (str(i), bookId), (factory.factory_name(), bookName)
1332 def open_addressbook(self, bookFactoryId, bookId):
1333 bookFactoryIndex = int(bookFactoryId)
1334 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1336 forceUpdate = True if addressBook is not self._addressBook else False
1338 self._addressBook = addressBook
1339 self.update(force=forceUpdate)
1341 def update(self, force = False):
1342 if not force and self._isPopulated:
1344 self._updateSink.send(())
1348 self._isPopulated = False
1349 self._contactsmodel.clear()
1350 for factory in self._addressBookFactories:
1351 factory.clear_caches()
1352 self._addressBook.clear_caches()
1354 def append(self, book):
1355 self._addressBookFactories.append(book)
1357 def extend(self, books):
1358 self._addressBookFactories.extend(books)
1364 def load_settings(self, config, sectionName):
1366 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1367 except ConfigParser.NoOptionError:
1368 self._selectedComboIndex = 0
1370 def save_settings(self, config, sectionName):
1371 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1373 def _idly_populate_contactsview(self):
1376 while addressBook is not self._addressBook:
1377 addressBook = self._addressBook
1378 with gtk_toolbox.gtk_lock():
1379 self._contactsview.set_model(None)
1383 contacts = addressBook.get_contacts()
1384 except Exception, e:
1386 self._isPopulated = False
1387 self._errorDisplay.push_exception_with_lock()
1388 for contactId, contactName in contacts:
1389 contactType = (addressBook.contact_source_short_name(contactId), )
1390 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1392 with gtk_toolbox.gtk_lock():
1393 self._contactsview.set_model(self._contactsmodel)
1395 self._isPopulated = True
1396 except Exception, e:
1397 self._errorDisplay.push_exception_with_lock()
1400 def _on_addressbook_button_changed(self, *args, **kwds):
1403 newSelectedComboIndex = hildonize.touch_selector(
1406 (("%s" % m[2]) for m in self._booksList),
1407 self._selectedComboIndex,
1409 except RuntimeError:
1412 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1413 selectedBookId = self._booksList[newSelectedComboIndex][1]
1414 self.open_addressbook(selectedFactoryId, selectedBookId)
1415 self._selectedComboIndex = newSelectedComboIndex
1416 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1417 except Exception, e:
1418 self._errorDisplay.push_exception()
1420 def _on_contactsview_row_activated(self, treeview, path, view_column):
1422 model, itr = self._contactsviewselection.get_selected()
1426 contactId = self._contactsmodel.get_value(itr, 3)
1427 contactName = self._contactsmodel.get_value(itr, 1)
1429 contactDetails = self._addressBook.get_contact_details(contactId)
1430 except Exception, e:
1432 self._errorDisplay.push_exception()
1433 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1435 if len(contactPhoneNumbers) == 0:
1438 action, phoneNumber, message = self._phoneTypeSelector.run(
1439 contactPhoneNumbers,
1440 messages = (contactName, ),
1441 parent = self._window,
1443 if action == PhoneTypeSelector.ACTION_CANCEL:
1445 assert phoneNumber, "A lack of phone number exists"
1447 self.number_selected(action, phoneNumber, message)
1448 self._contactsviewselection.unselect_all()
1449 except Exception, e:
1450 self._errorDisplay.push_exception()