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 Touch selector for callback number
22 @todo Alternate UI for dialogs (stackables)
25 from __future__ import with_statement
39 def make_ugly(prettynumber):
41 function to take a phone number and strip out all non-numeric
44 >>> make_ugly("+012-(345)-678-90")
48 uglynumber = re.sub('\D', '', prettynumber)
52 def make_pretty(phonenumber):
54 Function to take a phone number and return the pretty version
56 if phonenumber begins with 0:
58 if phonenumber begins with 1: ( for gizmo callback numbers )
60 if phonenumber is 13 digits:
62 if phonenumber is 10 digits:
66 >>> make_pretty("1234567")
68 >>> make_pretty("2345678901")
70 >>> make_pretty("12345678901")
72 >>> make_pretty("01234567890")
75 if phonenumber is None or phonenumber is "":
78 phonenumber = make_ugly(phonenumber)
80 if len(phonenumber) < 3:
83 if phonenumber[0] == "0":
85 prettynumber += "+%s" % phonenumber[0:3]
86 if 3 < len(phonenumber):
87 prettynumber += "-(%s)" % phonenumber[3:6]
88 if 6 < len(phonenumber):
89 prettynumber += "-%s" % phonenumber[6:9]
90 if 9 < len(phonenumber):
91 prettynumber += "-%s" % phonenumber[9:]
93 elif len(phonenumber) <= 7:
94 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
95 elif len(phonenumber) > 8 and phonenumber[0] == "1":
96 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
97 elif len(phonenumber) > 7:
98 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
102 def abbrev_relative_date(date):
104 >>> abbrev_relative_date("42 hours ago")
106 >>> abbrev_relative_date("2 days ago")
108 >>> abbrev_relative_date("4 weeks ago")
111 parts = date.split(" ")
112 return "%s %s" % (parts[0], parts[1][0])
115 class MergedAddressBook(object):
117 Merger of all addressbooks
120 def __init__(self, addressbookFactories, sorter = None):
121 self.__addressbookFactories = addressbookFactories
122 self.__addressbooks = None
123 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
125 def clear_caches(self):
126 self.__addressbooks = None
127 for factory in self.__addressbookFactories:
128 factory.clear_caches()
130 def get_addressbooks(self):
132 @returns Iterable of (Address Book Factory, Book Id, Book Name)
136 def open_addressbook(self, bookId):
139 def contact_source_short_name(self, contactId):
140 if self.__addressbooks is None:
142 bookIndex, originalId = contactId.split("-", 1)
143 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
147 return "All Contacts"
149 def get_contacts(self):
151 @returns Iterable of (contact id, contact name)
153 if self.__addressbooks is None:
154 self.__addressbooks = list(
155 factory.open_addressbook(id)
156 for factory in self.__addressbookFactories
157 for (f, id, name) in factory.get_addressbooks()
160 ("-".join([str(bookIndex), contactId]), contactName)
161 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
162 for (contactId, contactName) in addressbook.get_contacts()
164 sortedContacts = self.__sort_contacts(contacts)
165 return sortedContacts
167 def get_contact_details(self, contactId):
169 @returns Iterable of (Phone Type, Phone Number)
171 if self.__addressbooks is None:
173 bookIndex, originalId = contactId.split("-", 1)
174 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
177 def null_sorter(contacts):
179 Good for speed/low memory
184 def basic_firtname_sorter(contacts):
186 Expects names in "First Last" format
189 (contactName.rsplit(" ", 1)[0], (contactId, contactName))
190 for (contactId, contactName) in contacts
192 contactsWithKey.sort()
193 return (contactData for (lastName, contactData) in contactsWithKey)
196 def basic_lastname_sorter(contacts):
198 Expects names in "First Last" format
201 (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
202 for (contactId, contactName) in contacts
204 contactsWithKey.sort()
205 return (contactData for (lastName, contactData) in contactsWithKey)
208 def reversed_firtname_sorter(contacts):
210 Expects names in "Last, First" format
213 (contactName.split(", ", 1)[-1], (contactId, contactName))
214 for (contactId, contactName) in contacts
216 contactsWithKey.sort()
217 return (contactData for (lastName, contactData) in contactsWithKey)
220 def reversed_lastname_sorter(contacts):
222 Expects names in "Last, First" format
225 (contactName.split(", ", 1)[0], (contactId, contactName))
226 for (contactId, contactName) in contacts
228 contactsWithKey.sort()
229 return (contactData for (lastName, contactData) in contactsWithKey)
232 def guess_firstname(name):
234 return name.split(", ", 1)[-1]
236 return name.rsplit(" ", 1)[0]
239 def guess_lastname(name):
241 return name.split(", ", 1)[0]
243 return name.rsplit(" ", 1)[-1]
246 def advanced_firstname_sorter(cls, contacts):
248 (cls.guess_firstname(contactName), (contactId, contactName))
249 for (contactId, contactName) in contacts
251 contactsWithKey.sort()
252 return (contactData for (lastName, contactData) in contactsWithKey)
255 def advanced_lastname_sorter(cls, contacts):
257 (cls.guess_lastname(contactName), (contactId, contactName))
258 for (contactId, contactName) in contacts
260 contactsWithKey.sort()
261 return (contactData for (lastName, contactData) in contactsWithKey)
264 class PhoneTypeSelector(object):
266 ACTION_CANCEL = "cancel"
267 ACTION_SELECT = "select"
269 ACTION_SEND_SMS = "sms"
271 def __init__(self, widgetTree, gcBackend):
272 self._gcBackend = gcBackend
273 self._widgetTree = widgetTree
275 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
276 self._smsDialog = SmsEntryDialog(self._widgetTree)
278 self._smsButton = self._widgetTree.get_widget("sms_button")
279 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
281 self._dialButton = self._widgetTree.get_widget("dial_button")
282 self._dialButton.connect("clicked", self._on_phonetype_dial)
284 self._selectButton = self._widgetTree.get_widget("select_button")
285 self._selectButton.connect("clicked", self._on_phonetype_select)
287 self._cancelButton = self._widgetTree.get_widget("cancel_button")
288 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
290 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
291 self._messagesView = self._widgetTree.get_widget("phoneSelectionMessages")
292 self._scrollWindow = self._messagesView.get_parent()
294 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
295 self._typeviewselection = None
296 self._typeview = self._widgetTree.get_widget("phonetypes")
297 self._typeview.connect("row-activated", self._on_phonetype_select)
299 self._action = self.ACTION_CANCEL
301 def run(self, contactDetails, messages = (), parent = None):
302 self._action = self.ACTION_CANCEL
304 # Add the column to the phone selection tree view
305 self._typemodel.clear()
306 self._typeview.set_model(self._typemodel)
308 textrenderer = gtk.CellRendererText()
309 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
310 self._typeview.append_column(numberColumn)
312 textrenderer = gtk.CellRendererText()
313 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
314 self._typeview.append_column(typeColumn)
316 for phoneType, phoneNumber in contactDetails:
317 display = " - ".join((phoneNumber, phoneType))
319 row = (phoneNumber, display)
320 self._typemodel.append(row)
322 self._typeviewselection = self._typeview.get_selection()
323 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
324 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
326 # Add the column to the messages tree view
327 self._messagemodel.clear()
328 self._messagesView.set_model(self._messagemodel)
330 textrenderer = gtk.CellRendererText()
331 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
332 textrenderer.set_property("wrap-width", 450)
333 messageColumn = gtk.TreeViewColumn("")
334 messageColumn.pack_start(textrenderer, expand=True)
335 messageColumn.add_attribute(textrenderer, "markup", 0)
336 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
337 self._messagesView.append_column(messageColumn)
338 self._messagesView.set_headers_visible(False)
341 for message in messages:
343 self._messagemodel.append(row)
344 self._messagesView.show()
345 self._scrollWindow.show()
346 messagesSelection = self._messagesView.get_selection()
347 messagesSelection.select_path((len(messages)-1, ))
349 self._messagesView.hide()
350 self._scrollWindow.hide()
352 if parent is not None:
353 self._dialog.set_transient_for(parent)
358 self._messagesView.scroll_to_cell((len(messages)-1, ))
360 userResponse = self._dialog.run()
364 if userResponse == gtk.RESPONSE_OK:
365 phoneNumber = self._get_number()
366 phoneNumber = make_ugly(phoneNumber)
370 self._action = self.ACTION_CANCEL
372 if self._action == self.ACTION_SEND_SMS:
373 smsMessage = self._smsDialog.run(phoneNumber, messages, parent)
376 self._action = self.ACTION_CANCEL
380 self._messagesView.remove_column(messageColumn)
381 self._messagesView.set_model(None)
383 self._typeviewselection.unselect_all()
384 self._typeview.remove_column(numberColumn)
385 self._typeview.remove_column(typeColumn)
386 self._typeview.set_model(None)
388 return self._action, phoneNumber, smsMessage
390 def _get_number(self):
391 model, itr = self._typeviewselection.get_selected()
395 phoneNumber = self._typemodel.get_value(itr, 0)
398 def _on_phonetype_dial(self, *args):
399 self._dialog.response(gtk.RESPONSE_OK)
400 self._action = self.ACTION_DIAL
402 def _on_phonetype_send_sms(self, *args):
403 self._dialog.response(gtk.RESPONSE_OK)
404 self._action = self.ACTION_SEND_SMS
406 def _on_phonetype_select(self, *args):
407 self._dialog.response(gtk.RESPONSE_OK)
408 self._action = self.ACTION_SELECT
410 def _on_phonetype_cancel(self, *args):
411 self._dialog.response(gtk.RESPONSE_CANCEL)
412 self._action = self.ACTION_CANCEL
415 class SmsEntryDialog(object):
417 @todo Add multi-SMS messages like GoogleVoice
422 def __init__(self, widgetTree):
423 self._widgetTree = widgetTree
424 self._dialog = self._widgetTree.get_widget("smsDialog")
426 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
427 self._smsButton.connect("clicked", self._on_send)
429 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
430 self._cancelButton.connect("clicked", self._on_cancel)
432 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
434 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
435 self._messagesView = self._widgetTree.get_widget("smsMessages")
436 self._scrollWindow = self._messagesView.get_parent()
438 self._smsEntry = self._widgetTree.get_widget("smsEntry")
439 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
441 def run(self, number, messages = (), parent = None):
442 # Add the column to the messages tree view
443 self._messagemodel.clear()
444 self._messagesView.set_model(self._messagemodel)
446 textrenderer = gtk.CellRendererText()
447 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
448 textrenderer.set_property("wrap-width", 450)
449 messageColumn = gtk.TreeViewColumn("")
450 messageColumn.pack_start(textrenderer, expand=True)
451 messageColumn.add_attribute(textrenderer, "markup", 0)
452 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
453 self._messagesView.append_column(messageColumn)
454 self._messagesView.set_headers_visible(False)
457 for message in messages:
459 self._messagemodel.append(row)
460 self._messagesView.show()
461 self._scrollWindow.show()
462 messagesSelection = self._messagesView.get_selection()
463 messagesSelection.select_path((len(messages)-1, ))
465 self._messagesView.hide()
466 self._scrollWindow.hide()
468 self._smsEntry.get_buffer().set_text("")
469 self._update_letter_count()
471 if parent is not None:
472 self._dialog.set_transient_for(parent)
477 self._messagesView.scroll_to_cell((len(messages)-1, ))
478 self._smsEntry.grab_focus()
480 userResponse = self._dialog.run()
484 if userResponse == gtk.RESPONSE_OK:
485 entryBuffer = self._smsEntry.get_buffer()
486 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
487 enteredMessage = enteredMessage[0:self.MAX_CHAR]
491 self._messagesView.remove_column(messageColumn)
492 self._messagesView.set_model(None)
494 return enteredMessage.strip()
496 def _update_letter_count(self, *args):
497 entryLength = self._smsEntry.get_buffer().get_char_count()
498 charsLeft = self.MAX_CHAR - entryLength
499 self._letterCountLabel.set_text(str(charsLeft))
501 self._smsButton.set_sensitive(False)
503 self._smsButton.set_sensitive(True)
505 def _on_entry_changed(self, *args):
506 self._update_letter_count()
508 def _on_send(self, *args):
509 self._dialog.response(gtk.RESPONSE_OK)
511 def _on_cancel(self, *args):
512 self._dialog.response(gtk.RESPONSE_CANCEL)
515 class Dialpad(object):
517 def __init__(self, widgetTree, errorDisplay):
518 self._clipboard = gtk.clipboard_get()
519 self._errorDisplay = errorDisplay
520 self._smsDialog = SmsEntryDialog(widgetTree)
522 self._numberdisplay = widgetTree.get_widget("numberdisplay")
523 self._smsButton = widgetTree.get_widget("sms")
524 self._dialButton = widgetTree.get_widget("dial")
525 self._backButton = widgetTree.get_widget("back")
526 self._phonenumber = ""
527 self._prettynumber = ""
530 "on_digit_clicked": self._on_digit_clicked,
532 widgetTree.signal_autoconnect(callbackMapping)
533 self._dialButton.connect("clicked", self._on_dial_clicked)
534 self._smsButton.connect("clicked", self._on_sms_clicked)
536 self._originalLabel = self._backButton.get_label()
537 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
538 self._backTapHandler.on_tap = self._on_backspace
539 self._backTapHandler.on_hold = self._on_clearall
540 self._backTapHandler.on_holding = self._set_clear_button
541 self._backTapHandler.on_cancel = self._reset_back_button
543 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
544 self._keyPressEventId = 0
547 self._dialButton.grab_focus()
548 self._backTapHandler.enable()
549 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
552 self._window.disconnect(self._keyPressEventId)
553 self._keyPressEventId = 0
554 self._reset_back_button()
555 self._backTapHandler.disable()
557 def number_selected(self, action, number, message):
559 @note Actual dial function is patched in later
561 raise NotImplementedError("Horrible unknown error has occurred")
563 def get_number(self):
564 return self._phonenumber
566 def set_number(self, number):
568 Set the number to dial
571 self._phonenumber = make_ugly(number)
572 self._prettynumber = make_pretty(self._phonenumber)
573 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
575 self._errorDisplay.push_exception()
584 def load_settings(self, config, section):
587 def save_settings(self, config, section):
589 @note Thread Agnostic
593 def _on_key_press(self, widget, event):
595 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
596 contents = self._clipboard.wait_for_text()
597 if contents is not None:
598 self.set_number(contents)
600 self._errorDisplay.push_exception()
602 def _on_sms_clicked(self, widget):
604 action = PhoneTypeSelector.ACTION_SEND_SMS
605 phoneNumber = self.get_number()
607 message = self._smsDialog.run(phoneNumber, (), self._window)
610 action = PhoneTypeSelector.ACTION_CANCEL
612 if action == PhoneTypeSelector.ACTION_CANCEL:
614 self.number_selected(action, phoneNumber, message)
616 self._errorDisplay.push_exception()
618 def _on_dial_clicked(self, widget):
620 action = PhoneTypeSelector.ACTION_DIAL
621 phoneNumber = self.get_number()
623 self.number_selected(action, phoneNumber, message)
625 self._errorDisplay.push_exception()
627 def _on_digit_clicked(self, widget):
629 self.set_number(self._phonenumber + widget.get_name()[-1])
631 self._errorDisplay.push_exception()
633 def _on_backspace(self, taps):
635 self.set_number(self._phonenumber[:-taps])
636 self._reset_back_button()
638 self._errorDisplay.push_exception()
640 def _on_clearall(self, taps):
643 self._reset_back_button()
645 self._errorDisplay.push_exception()
648 def _set_clear_button(self):
650 self._backButton.set_label("gtk-clear")
652 self._errorDisplay.push_exception()
654 def _reset_back_button(self):
656 self._backButton.set_label(self._originalLabel)
658 self._errorDisplay.push_exception()
661 class AccountInfo(object):
663 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
664 self._errorDisplay = errorDisplay
665 self._backend = backend
666 self._isPopulated = False
667 self._alarmHandler = alarmHandler
668 self._notifyOnMissed = False
669 self._notifyOnVoicemail = False
670 self._notifyOnSms = False
672 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
673 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
674 self._callbackCombo = widgetTree.get_widget("callbackcombo")
675 self._onCallbackentryChangedId = 0
677 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
678 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
679 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
680 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
681 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
682 self._onNotifyToggled = 0
683 self._onMinutesChanged = 0
684 self._onMissedToggled = 0
685 self._onVoicemailToggled = 0
686 self._onSmsToggled = 0
687 self._applyAlarmTimeoutId = None
689 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
690 self._defaultCallback = ""
693 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
695 self._accountViewNumberDisplay.set_use_markup(True)
696 self.set_account_number("")
698 self._callbackList.clear()
699 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
701 if self._alarmHandler is not None:
702 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
703 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
704 self._missedCheckbox.set_active(self._notifyOnMissed)
705 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
706 self._smsCheckbox.set_active(self._notifyOnSms)
708 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
709 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
710 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
711 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
712 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
714 self._notifyCheckbox.set_sensitive(False)
715 self._minutesEntryButton.set_sensitive(False)
716 self._missedCheckbox.set_sensitive(False)
717 self._voicemailCheckbox.set_sensitive(False)
718 self._smsCheckbox.set_sensitive(False)
720 self.update(force=True)
723 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
724 self._onCallbackentryChangedId = 0
726 if self._alarmHandler is not None:
727 self._notifyCheckbox.disconnect(self._onNotifyToggled)
728 self._minutesEntryButton.disconnect(self._onMinutesChanged)
729 self._missedCheckbox.disconnect(self._onNotifyToggled)
730 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
731 self._smsCheckbox.disconnect(self._onNotifyToggled)
732 self._onNotifyToggled = 0
733 self._onMinutesChanged = 0
734 self._onMissedToggled = 0
735 self._onVoicemailToggled = 0
736 self._onSmsToggled = 0
738 self._notifyCheckbox.set_sensitive(True)
739 self._minutesEntryButton.set_sensitive(True)
740 self._missedCheckbox.set_sensitive(True)
741 self._voicemailCheckbox.set_sensitive(True)
742 self._smsCheckbox.set_sensitive(True)
745 self._callbackList.clear()
747 def get_selected_callback_number(self):
748 return make_ugly(self._callbackCombo.get_child().get_text())
750 def set_account_number(self, number):
752 Displays current account number
754 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
756 def update(self, force = False):
757 if not force and self._isPopulated:
759 self._populate_callback_combo()
760 self.set_account_number(self._backend.get_account_number())
764 self._callbackCombo.get_child().set_text("")
765 self.set_account_number("")
766 self._isPopulated = False
768 def save_everything(self):
769 raise NotImplementedError
773 return "Account Info"
775 def load_settings(self, config, section):
776 self._defaultCallback = config.get(section, "callback")
777 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
778 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
779 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
781 def save_settings(self, config, section):
783 @note Thread Agnostic
785 callback = self.get_selected_callback_number()
786 config.set(section, "callback", callback)
787 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
788 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
789 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
791 def _populate_callback_combo(self):
792 self._isPopulated = True
793 self._callbackList.clear()
795 callbackNumbers = self._backend.get_callback_numbers()
797 self._errorDisplay.push_exception()
798 self._isPopulated = False
801 for number, description in callbackNumbers.iteritems():
802 self._callbackList.append((make_pretty(number),))
804 self._callbackCombo.set_model(self._callbackList)
805 self._callbackCombo.set_text_column(0)
806 #callbackNumber = self._backend.get_callback_number()
807 callbackNumber = self._defaultCallback
808 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
810 def _set_callback_number(self, number):
812 if not self._backend.is_valid_syntax(number) and 0 < len(number):
813 self._errorDisplay.push_message("%s is not a valid callback number" % number)
814 elif number == self._backend.get_callback_number():
816 "Callback number already is %s" % (
817 self._backend.get_callback_number(),
821 self._backend.set_callback_number(number)
822 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
823 make_pretty(number), make_pretty(self._backend.get_callback_number())
826 "Callback number set to %s" % (
827 self._backend.get_callback_number(),
831 self._errorDisplay.push_exception()
833 def _update_alarm_settings(self, recurrence):
835 isEnabled = self._notifyCheckbox.get_active()
836 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
837 self._alarmHandler.apply_settings(isEnabled, recurrence)
839 self.save_everything()
840 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
841 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
843 def _on_callbackentry_changed(self, *args):
845 text = self.get_selected_callback_number()
846 number = make_ugly(text)
847 self._set_callback_number(number)
849 self._errorDisplay.push_exception()
851 def _on_notify_toggled(self, *args):
853 if self._applyAlarmTimeoutId is not None:
854 gobject.source_remove(self._applyAlarmTimeoutId)
855 self._applyAlarmTimeoutId = None
856 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
858 self._errorDisplay.push_exception()
860 def _on_minutes_clicked(self, *args):
861 recurrenceChoices = [
873 actualSelection = self._alarmHandler.recurrence
875 closestSelectionIndex = 0
876 for i, possible in enumerate(recurrenceChoices):
877 if possible[0] <= actualSelection:
878 closestSelectionIndex = i
879 recurrenceIndex = hildonize.touch_selector(
882 (("%s" % m[1]) for m in recurrenceChoices),
883 closestSelectionIndex,
885 recurrence = recurrenceChoices[recurrenceIndex][0]
887 self._update_alarm_settings(recurrence)
889 self._errorDisplay.push_exception()
891 def _on_apply_timeout(self, *args):
893 self._applyAlarmTimeoutId = None
895 self._update_alarm_settings(self._alarmHandler.recurrence)
897 self._errorDisplay.push_exception()
900 def _on_missed_toggled(self, *args):
902 self._notifyOnMissed = self._missedCheckbox.get_active()
903 self.save_everything()
905 self._errorDisplay.push_exception()
907 def _on_voicemail_toggled(self, *args):
909 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
910 self.save_everything()
912 self._errorDisplay.push_exception()
914 def _on_sms_toggled(self, *args):
916 self._notifyOnSms = self._smsCheckbox.get_active()
917 self.save_everything()
919 self._errorDisplay.push_exception()
922 class RecentCallsView(object):
929 def __init__(self, widgetTree, backend, errorDisplay):
930 self._errorDisplay = errorDisplay
931 self._backend = backend
933 self._isPopulated = False
934 self._recentmodel = gtk.ListStore(
935 gobject.TYPE_STRING, # number
936 gobject.TYPE_STRING, # date
937 gobject.TYPE_STRING, # action
938 gobject.TYPE_STRING, # from
940 self._recentview = widgetTree.get_widget("recentview")
941 self._recentviewselection = None
942 self._onRecentviewRowActivatedId = 0
944 textrenderer = gtk.CellRendererText()
945 textrenderer.set_property("yalign", 0)
946 self._dateColumn = gtk.TreeViewColumn("Date")
947 self._dateColumn.pack_start(textrenderer, expand=True)
948 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
950 textrenderer = gtk.CellRendererText()
951 textrenderer.set_property("yalign", 0)
952 self._actionColumn = gtk.TreeViewColumn("Action")
953 self._actionColumn.pack_start(textrenderer, expand=True)
954 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
956 textrenderer = gtk.CellRendererText()
957 textrenderer.set_property("yalign", 0)
958 hildonize.set_cell_thumb_selectable(textrenderer)
959 self._nameColumn = gtk.TreeViewColumn("From")
960 self._nameColumn.pack_start(textrenderer, expand=True)
961 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
962 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
964 textrenderer = gtk.CellRendererText()
965 textrenderer.set_property("yalign", 0)
966 self._numberColumn = gtk.TreeViewColumn("Number")
967 self._numberColumn.pack_start(textrenderer, expand=True)
968 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
970 self._window = gtk_toolbox.find_parent_window(self._recentview)
971 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
973 self._updateSink = gtk_toolbox.threaded_stage(
975 self._idly_populate_recentview,
976 gtk_toolbox.null_sink(),
981 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
982 self._recentview.set_model(self._recentmodel)
984 self._recentview.append_column(self._dateColumn)
985 self._recentview.append_column(self._actionColumn)
986 self._recentview.append_column(self._numberColumn)
987 self._recentview.append_column(self._nameColumn)
988 self._recentviewselection = self._recentview.get_selection()
989 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
991 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
994 self._recentview.disconnect(self._onRecentviewRowActivatedId)
998 self._recentview.remove_column(self._dateColumn)
999 self._recentview.remove_column(self._actionColumn)
1000 self._recentview.remove_column(self._nameColumn)
1001 self._recentview.remove_column(self._numberColumn)
1002 self._recentview.set_model(None)
1004 def number_selected(self, action, number, message):
1006 @note Actual dial function is patched in later
1008 raise NotImplementedError("Horrible unknown error has occurred")
1010 def update(self, force = False):
1011 if not force and self._isPopulated:
1013 self._updateSink.send(())
1017 self._isPopulated = False
1018 self._recentmodel.clear()
1022 return "Recent Calls"
1024 def load_settings(self, config, section):
1027 def save_settings(self, config, section):
1029 @note Thread Agnostic
1033 def _idly_populate_recentview(self):
1035 self._recentmodel.clear()
1036 self._isPopulated = True
1039 recentItems = self._backend.get_recent()
1040 except Exception, e:
1041 self._errorDisplay.push_exception_with_lock()
1042 self._isPopulated = False
1045 for personName, phoneNumber, date, action in recentItems:
1047 personName = "Unknown"
1048 date = abbrev_relative_date(date)
1049 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1050 prettyNumber = make_pretty(prettyNumber)
1051 item = (prettyNumber, date, action.capitalize(), personName)
1052 with gtk_toolbox.gtk_lock():
1053 self._recentmodel.append(item)
1054 except Exception, e:
1055 self._errorDisplay.push_exception_with_lock()
1059 def _on_recentview_row_activated(self, treeview, path, view_column):
1061 model, itr = self._recentviewselection.get_selected()
1065 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1066 number = make_ugly(number)
1067 contactPhoneNumbers = [("Phone", number)]
1068 description = self._recentmodel.get_value(itr, self.FROM_IDX)
1070 action, phoneNumber, message = self._phoneTypeSelector.run(
1071 contactPhoneNumbers,
1072 messages = (description, ),
1073 parent = self._window,
1075 if action == PhoneTypeSelector.ACTION_CANCEL:
1077 assert phoneNumber, "A lack of phone number exists"
1079 self.number_selected(action, phoneNumber, message)
1080 self._recentviewselection.unselect_all()
1081 except Exception, e:
1082 self._errorDisplay.push_exception()
1085 class MessagesView(object):
1093 def __init__(self, widgetTree, backend, errorDisplay):
1094 self._errorDisplay = errorDisplay
1095 self._backend = backend
1097 self._isPopulated = False
1098 self._messagemodel = gtk.ListStore(
1099 gobject.TYPE_STRING, # number
1100 gobject.TYPE_STRING, # date
1101 gobject.TYPE_STRING, # header
1102 gobject.TYPE_STRING, # message
1105 self._messageview = widgetTree.get_widget("messages_view")
1106 self._messageviewselection = None
1107 self._onMessageviewRowActivatedId = 0
1109 self._messageRenderer = gtk.CellRendererText()
1110 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1111 self._messageRenderer.set_property("wrap-width", 500)
1112 self._messageColumn = gtk.TreeViewColumn("Messages")
1113 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1114 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1115 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1117 self._window = gtk_toolbox.find_parent_window(self._messageview)
1118 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1120 self._updateSink = gtk_toolbox.threaded_stage(
1122 self._idly_populate_messageview,
1123 gtk_toolbox.null_sink(),
1128 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1129 self._messageview.set_model(self._messagemodel)
1131 self._messageview.append_column(self._messageColumn)
1132 self._messageviewselection = self._messageview.get_selection()
1133 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1135 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1138 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1142 self._messageview.remove_column(self._messageColumn)
1143 self._messageview.set_model(None)
1145 def number_selected(self, action, number, message):
1147 @note Actual dial function is patched in later
1149 raise NotImplementedError("Horrible unknown error has occurred")
1151 def update(self, force = False):
1152 if not force and self._isPopulated:
1154 self._updateSink.send(())
1158 self._isPopulated = False
1159 self._messagemodel.clear()
1165 def load_settings(self, config, section):
1168 def save_settings(self, config, section):
1170 @note Thread Agnostic
1174 def _idly_populate_messageview(self):
1176 self._messagemodel.clear()
1177 self._isPopulated = True
1180 messageItems = self._backend.get_messages()
1181 except Exception, e:
1182 self._errorDisplay.push_exception_with_lock()
1183 self._isPopulated = False
1186 for header, number, relativeDate, messages in messageItems:
1187 prettyNumber = number[2:] if number.startswith("+1") else number
1188 prettyNumber = make_pretty(prettyNumber)
1190 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1191 newMessages = [firstMessage]
1192 newMessages.extend(messages)
1194 number = make_ugly(number)
1196 row = (number, relativeDate, header, "\n".join(newMessages), newMessages)
1197 with gtk_toolbox.gtk_lock():
1198 self._messagemodel.append(row)
1199 except Exception, e:
1200 self._errorDisplay.push_exception_with_lock()
1204 def _on_messageview_row_activated(self, treeview, path, view_column):
1206 model, itr = self._messageviewselection.get_selected()
1210 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1211 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1213 action, phoneNumber, message = self._phoneTypeSelector.run(
1214 contactPhoneNumbers,
1215 messages = description,
1216 parent = self._window,
1218 if action == PhoneTypeSelector.ACTION_CANCEL:
1220 assert phoneNumber, "A lock of phone number exists"
1222 self.number_selected(action, phoneNumber, message)
1223 self._messageviewselection.unselect_all()
1224 except Exception, e:
1225 self._errorDisplay.push_exception()
1228 class ContactsView(object):
1230 def __init__(self, widgetTree, backend, errorDisplay):
1231 self._errorDisplay = errorDisplay
1232 self._backend = backend
1234 self._addressBook = None
1235 self._selectedComboIndex = 0
1236 self._addressBookFactories = [null_backend.NullAddressBook()]
1238 self._booksList = []
1239 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1241 self._isPopulated = False
1242 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1243 self._contactsviewselection = None
1244 self._contactsview = widgetTree.get_widget("contactsview")
1246 self._contactColumn = gtk.TreeViewColumn("Contact")
1247 displayContactSource = False
1248 if displayContactSource:
1249 textrenderer = gtk.CellRendererText()
1250 self._contactColumn.pack_start(textrenderer, expand=False)
1251 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1252 textrenderer = gtk.CellRendererText()
1253 hildonize.set_cell_thumb_selectable(textrenderer)
1254 self._contactColumn.pack_start(textrenderer, expand=True)
1255 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1256 textrenderer = gtk.CellRendererText()
1257 self._contactColumn.pack_start(textrenderer, expand=True)
1258 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1259 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1260 self._contactColumn.set_sort_column_id(1)
1261 self._contactColumn.set_visible(True)
1263 self._onContactsviewRowActivatedId = 0
1264 self._onAddressbookButtonChangedId = 0
1265 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1266 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1268 self._updateSink = gtk_toolbox.threaded_stage(
1270 self._idly_populate_contactsview,
1271 gtk_toolbox.null_sink(),
1276 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1278 self._contactsview.set_model(self._contactsmodel)
1279 self._contactsview.append_column(self._contactColumn)
1280 self._contactsviewselection = self._contactsview.get_selection()
1281 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1283 del self._booksList[:]
1284 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1285 if factoryName and bookName:
1286 entryName = "%s: %s" % (factoryName, bookName)
1288 entryName = factoryName
1290 entryName = bookName
1292 entryName = "Bad name (%d)" % factoryId
1293 row = (str(factoryId), bookId, entryName)
1294 self._booksList.append(row)
1296 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1297 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1299 if len(self._booksList) <= self._selectedComboIndex:
1300 self._selectedComboIndex = 0
1301 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1303 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1304 selectedBookId = self._booksList[self._selectedComboIndex][1]
1305 self.open_addressbook(selectedFactoryId, selectedBookId)
1308 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1309 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1313 self._bookSelectionButton.set_label("")
1314 self._contactsview.set_model(None)
1315 self._contactsview.remove_column(self._contactColumn)
1317 def number_selected(self, action, number, message):
1319 @note Actual dial function is patched in later
1321 raise NotImplementedError("Horrible unknown error has occurred")
1323 def get_addressbooks(self):
1325 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1327 for i, factory in enumerate(self._addressBookFactories):
1328 for bookFactory, bookId, bookName in factory.get_addressbooks():
1329 yield (str(i), bookId), (factory.factory_name(), bookName)
1331 def open_addressbook(self, bookFactoryId, bookId):
1332 bookFactoryIndex = int(bookFactoryId)
1333 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1335 forceUpdate = True if addressBook is not self._addressBook else False
1337 self._addressBook = addressBook
1338 self.update(force=forceUpdate)
1340 def update(self, force = False):
1341 if not force and self._isPopulated:
1343 self._updateSink.send(())
1347 self._isPopulated = False
1348 self._contactsmodel.clear()
1349 for factory in self._addressBookFactories:
1350 factory.clear_caches()
1351 self._addressBook.clear_caches()
1353 def append(self, book):
1354 self._addressBookFactories.append(book)
1356 def extend(self, books):
1357 self._addressBookFactories.extend(books)
1363 def load_settings(self, config, sectionName):
1365 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1366 except ConfigParser.NoOptionError:
1367 self._selectedComboIndex = 0
1369 def save_settings(self, config, sectionName):
1370 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1372 def _idly_populate_contactsview(self):
1375 while addressBook is not self._addressBook:
1376 addressBook = self._addressBook
1377 with gtk_toolbox.gtk_lock():
1378 self._contactsview.set_model(None)
1382 contacts = addressBook.get_contacts()
1383 except Exception, e:
1385 self._isPopulated = False
1386 self._errorDisplay.push_exception_with_lock()
1387 for contactId, contactName in contacts:
1388 contactType = (addressBook.contact_source_short_name(contactId), )
1389 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1391 with gtk_toolbox.gtk_lock():
1392 self._contactsview.set_model(self._contactsmodel)
1394 self._isPopulated = True
1395 except Exception, e:
1396 self._errorDisplay.push_exception_with_lock()
1399 def _on_addressbook_button_changed(self, *args, **kwds):
1402 newSelectedComboIndex = hildonize.touch_selector(
1405 (("%s" % m[2]) for m in self._booksList),
1406 self._selectedComboIndex,
1408 except RuntimeError:
1411 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1412 selectedBookId = self._booksList[newSelectedComboIndex][1]
1413 self.open_addressbook(selectedFactoryId, selectedBookId)
1414 self._selectedComboIndex = newSelectedComboIndex
1415 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1416 except Exception, e:
1417 self._errorDisplay.push_exception()
1419 def _on_contactsview_row_activated(self, treeview, path, view_column):
1421 model, itr = self._contactsviewselection.get_selected()
1425 contactId = self._contactsmodel.get_value(itr, 3)
1426 contactName = self._contactsmodel.get_value(itr, 1)
1428 contactDetails = self._addressBook.get_contact_details(contactId)
1429 except Exception, e:
1431 self._errorDisplay.push_exception()
1432 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1434 if len(contactPhoneNumbers) == 0:
1437 action, phoneNumber, message = self._phoneTypeSelector.run(
1438 contactPhoneNumbers,
1439 messages = (contactName, ),
1440 parent = self._window,
1442 if action == PhoneTypeSelector.ACTION_CANCEL:
1444 assert phoneNumber, "A lack of phone number exists"
1446 self.number_selected(action, phoneNumber, message)
1447 self._contactsviewselection.unselect_all()
1448 except Exception, e:
1449 self._errorDisplay.push_exception()