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 Look into top half of dialogs being a treeview rather than a label
24 @todo Alternate UI for dialogs (stackables)
27 from __future__ import with_statement
41 def make_ugly(prettynumber):
43 function to take a phone number and strip out all non-numeric
46 >>> make_ugly("+012-(345)-678-90")
50 uglynumber = re.sub('\D', '', prettynumber)
54 def make_pretty(phonenumber):
56 Function to take a phone number and return the pretty version
58 if phonenumber begins with 0:
60 if phonenumber begins with 1: ( for gizmo callback numbers )
62 if phonenumber is 13 digits:
64 if phonenumber is 10 digits:
68 >>> make_pretty("1234567")
70 >>> make_pretty("2345678901")
72 >>> make_pretty("12345678901")
74 >>> make_pretty("01234567890")
77 if phonenumber is None or phonenumber is "":
80 phonenumber = make_ugly(phonenumber)
82 if len(phonenumber) < 3:
85 if phonenumber[0] == "0":
87 prettynumber += "+%s" % phonenumber[0:3]
88 if 3 < len(phonenumber):
89 prettynumber += "-(%s)" % phonenumber[3:6]
90 if 6 < len(phonenumber):
91 prettynumber += "-%s" % phonenumber[6:9]
92 if 9 < len(phonenumber):
93 prettynumber += "-%s" % phonenumber[9:]
95 elif len(phonenumber) <= 7:
96 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
97 elif len(phonenumber) > 8 and phonenumber[0] == "1":
98 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
99 elif len(phonenumber) > 7:
100 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
104 def abbrev_relative_date(date):
106 >>> abbrev_relative_date("42 hours ago")
108 >>> abbrev_relative_date("2 days ago")
110 >>> abbrev_relative_date("4 weeks ago")
113 parts = date.split(" ")
114 return "%s %s" % (parts[0], parts[1][0])
117 class MergedAddressBook(object):
119 Merger of all addressbooks
122 def __init__(self, addressbookFactories, sorter = None):
123 self.__addressbookFactories = addressbookFactories
124 self.__addressbooks = None
125 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
127 def clear_caches(self):
128 self.__addressbooks = None
129 for factory in self.__addressbookFactories:
130 factory.clear_caches()
132 def get_addressbooks(self):
134 @returns Iterable of (Address Book Factory, Book Id, Book Name)
138 def open_addressbook(self, bookId):
141 def contact_source_short_name(self, contactId):
142 if self.__addressbooks is None:
144 bookIndex, originalId = contactId.split("-", 1)
145 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
149 return "All Contacts"
151 def get_contacts(self):
153 @returns Iterable of (contact id, contact name)
155 if self.__addressbooks is None:
156 self.__addressbooks = list(
157 factory.open_addressbook(id)
158 for factory in self.__addressbookFactories
159 for (f, id, name) in factory.get_addressbooks()
162 ("-".join([str(bookIndex), contactId]), contactName)
163 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
164 for (contactId, contactName) in addressbook.get_contacts()
166 sortedContacts = self.__sort_contacts(contacts)
167 return sortedContacts
169 def get_contact_details(self, contactId):
171 @returns Iterable of (Phone Type, Phone Number)
173 if self.__addressbooks is None:
175 bookIndex, originalId = contactId.split("-", 1)
176 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
179 def null_sorter(contacts):
181 Good for speed/low memory
186 def basic_firtname_sorter(contacts):
188 Expects names in "First Last" format
191 (contactName.rsplit(" ", 1)[0], (contactId, contactName))
192 for (contactId, contactName) in contacts
194 contactsWithKey.sort()
195 return (contactData for (lastName, contactData) in contactsWithKey)
198 def basic_lastname_sorter(contacts):
200 Expects names in "First Last" format
203 (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
204 for (contactId, contactName) in contacts
206 contactsWithKey.sort()
207 return (contactData for (lastName, contactData) in contactsWithKey)
210 def reversed_firtname_sorter(contacts):
212 Expects names in "Last, First" format
215 (contactName.split(", ", 1)[-1], (contactId, contactName))
216 for (contactId, contactName) in contacts
218 contactsWithKey.sort()
219 return (contactData for (lastName, contactData) in contactsWithKey)
222 def reversed_lastname_sorter(contacts):
224 Expects names in "Last, First" format
227 (contactName.split(", ", 1)[0], (contactId, contactName))
228 for (contactId, contactName) in contacts
230 contactsWithKey.sort()
231 return (contactData for (lastName, contactData) in contactsWithKey)
234 def guess_firstname(name):
236 return name.split(", ", 1)[-1]
238 return name.rsplit(" ", 1)[0]
241 def guess_lastname(name):
243 return name.split(", ", 1)[0]
245 return name.rsplit(" ", 1)[-1]
248 def advanced_firstname_sorter(cls, contacts):
250 (cls.guess_firstname(contactName), (contactId, contactName))
251 for (contactId, contactName) in contacts
253 contactsWithKey.sort()
254 return (contactData for (lastName, contactData) in contactsWithKey)
257 def advanced_lastname_sorter(cls, contacts):
259 (cls.guess_lastname(contactName), (contactId, contactName))
260 for (contactId, contactName) in contacts
262 contactsWithKey.sort()
263 return (contactData for (lastName, contactData) in contactsWithKey)
266 class PhoneTypeSelector(object):
268 ACTION_CANCEL = "cancel"
269 ACTION_SELECT = "select"
271 ACTION_SEND_SMS = "sms"
273 def __init__(self, widgetTree, gcBackend):
274 self._gcBackend = gcBackend
275 self._widgetTree = widgetTree
277 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
278 self._smsDialog = SmsEntryDialog(self._widgetTree)
280 self._smsButton = self._widgetTree.get_widget("sms_button")
281 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
283 self._dialButton = self._widgetTree.get_widget("dial_button")
284 self._dialButton.connect("clicked", self._on_phonetype_dial)
286 self._selectButton = self._widgetTree.get_widget("select_button")
287 self._selectButton.connect("clicked", self._on_phonetype_select)
289 self._cancelButton = self._widgetTree.get_widget("cancel_button")
290 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
292 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
293 self._messagesView = self._widgetTree.get_widget("phoneSelectionMessages")
294 self._scrollWindow = self._messagesView.get_parent()
296 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
297 self._typeviewselection = None
298 self._typeview = self._widgetTree.get_widget("phonetypes")
299 self._typeview.connect("row-activated", self._on_phonetype_select)
301 self._action = self.ACTION_CANCEL
303 def run(self, contactDetails, messages = (), parent = None):
304 self._action = self.ACTION_CANCEL
306 # Add the column to the phone selection tree view
307 self._typemodel.clear()
308 self._typeview.set_model(self._typemodel)
310 textrenderer = gtk.CellRendererText()
311 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
312 self._typeview.append_column(numberColumn)
314 textrenderer = gtk.CellRendererText()
315 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
316 self._typeview.append_column(typeColumn)
318 for phoneType, phoneNumber in contactDetails:
319 display = " - ".join((phoneNumber, phoneType))
321 row = (phoneNumber, display)
322 self._typemodel.append(row)
324 self._typeviewselection = self._typeview.get_selection()
325 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
326 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
328 # Add the column to the messages tree view
329 self._messagemodel.clear()
330 self._messagesView.set_model(self._messagemodel)
332 textrenderer = gtk.CellRendererText()
333 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
334 textrenderer.set_property("wrap-width", 450)
335 messageColumn = gtk.TreeViewColumn("")
336 messageColumn.pack_start(textrenderer, expand=True)
337 messageColumn.add_attribute(textrenderer, "markup", 0)
338 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
339 self._messagesView.append_column(messageColumn)
340 self._messagesView.set_headers_visible(False)
343 for message in messages:
345 self._messagemodel.append(row)
346 self._messagesView.show()
347 self._scrollWindow.show()
348 messagesSelection = self._messagesView.get_selection()
349 messagesSelection.select_path((len(messages)-1, ))
351 self._messagesView.hide()
352 self._scrollWindow.hide()
354 if parent is not None:
355 self._dialog.set_transient_for(parent)
360 self._messagesView.scroll_to_cell((len(messages)-1, ))
362 userResponse = self._dialog.run()
366 if userResponse == gtk.RESPONSE_OK:
367 phoneNumber = self._get_number()
368 phoneNumber = make_ugly(phoneNumber)
372 self._action = self.ACTION_CANCEL
374 if self._action == self.ACTION_SEND_SMS:
375 smsMessage = self._smsDialog.run(phoneNumber, messages, parent)
378 self._action = self.ACTION_CANCEL
382 self._messagesView.remove_column(messageColumn)
383 self._messagesView.set_model(None)
385 self._typeviewselection.unselect_all()
386 self._typeview.remove_column(numberColumn)
387 self._typeview.remove_column(typeColumn)
388 self._typeview.set_model(None)
390 return self._action, phoneNumber, smsMessage
392 def _get_number(self):
393 model, itr = self._typeviewselection.get_selected()
397 phoneNumber = self._typemodel.get_value(itr, 0)
400 def _on_phonetype_dial(self, *args):
401 self._dialog.response(gtk.RESPONSE_OK)
402 self._action = self.ACTION_DIAL
404 def _on_phonetype_send_sms(self, *args):
405 self._dialog.response(gtk.RESPONSE_OK)
406 self._action = self.ACTION_SEND_SMS
408 def _on_phonetype_select(self, *args):
409 self._dialog.response(gtk.RESPONSE_OK)
410 self._action = self.ACTION_SELECT
412 def _on_phonetype_cancel(self, *args):
413 self._dialog.response(gtk.RESPONSE_CANCEL)
414 self._action = self.ACTION_CANCEL
417 class SmsEntryDialog(object):
419 @todo Add multi-SMS messages like GoogleVoice
424 def __init__(self, widgetTree):
425 self._widgetTree = widgetTree
426 self._dialog = self._widgetTree.get_widget("smsDialog")
428 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
429 self._smsButton.connect("clicked", self._on_send)
431 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
432 self._cancelButton.connect("clicked", self._on_cancel)
434 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
436 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
437 self._messagesView = self._widgetTree.get_widget("smsMessages")
438 self._scrollWindow = self._messagesView.get_parent()
440 self._smsEntry = self._widgetTree.get_widget("smsEntry")
441 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
443 def run(self, number, messages = (), parent = None):
444 # Add the column to the messages tree view
445 self._messagemodel.clear()
446 self._messagesView.set_model(self._messagemodel)
448 textrenderer = gtk.CellRendererText()
449 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
450 textrenderer.set_property("wrap-width", 450)
451 messageColumn = gtk.TreeViewColumn("")
452 messageColumn.pack_start(textrenderer, expand=True)
453 messageColumn.add_attribute(textrenderer, "markup", 0)
454 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
455 self._messagesView.append_column(messageColumn)
456 self._messagesView.set_headers_visible(False)
459 for message in messages:
461 self._messagemodel.append(row)
462 self._messagesView.show()
463 self._scrollWindow.show()
464 messagesSelection = self._messagesView.get_selection()
465 messagesSelection.select_path((len(messages)-1, ))
467 self._messagesView.hide()
468 self._scrollWindow.hide()
470 self._smsEntry.get_buffer().set_text("")
471 self._update_letter_count()
473 if parent is not None:
474 self._dialog.set_transient_for(parent)
479 self._messagesView.scroll_to_cell((len(messages)-1, ))
480 self._smsEntry.grab_focus()
482 userResponse = self._dialog.run()
486 if userResponse == gtk.RESPONSE_OK:
487 entryBuffer = self._smsEntry.get_buffer()
488 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
489 enteredMessage = enteredMessage[0:self.MAX_CHAR]
493 self._messagesView.remove_column(messageColumn)
494 self._messagesView.set_model(None)
496 return enteredMessage.strip()
498 def _update_letter_count(self, *args):
499 entryLength = self._smsEntry.get_buffer().get_char_count()
500 charsLeft = self.MAX_CHAR - entryLength
501 self._letterCountLabel.set_text(str(charsLeft))
503 self._smsButton.set_sensitive(False)
505 self._smsButton.set_sensitive(True)
507 def _on_entry_changed(self, *args):
508 self._update_letter_count()
510 def _on_send(self, *args):
511 self._dialog.response(gtk.RESPONSE_OK)
513 def _on_cancel(self, *args):
514 self._dialog.response(gtk.RESPONSE_CANCEL)
517 class Dialpad(object):
519 def __init__(self, widgetTree, errorDisplay):
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)
547 self._dialButton.grab_focus()
548 self._backTapHandler.enable()
551 self._reset_back_button()
552 self._backTapHandler.disable()
554 def number_selected(self, action, number, message):
556 @note Actual dial function is patched in later
558 raise NotImplementedError("Horrible unknown error has occurred")
560 def get_number(self):
561 return self._phonenumber
563 def set_number(self, number):
565 Set the number to dial
568 self._phonenumber = make_ugly(number)
569 self._prettynumber = make_pretty(self._phonenumber)
570 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
572 self._errorDisplay.push_exception()
581 def load_settings(self, config, section):
584 def save_settings(self, config, section):
586 @note Thread Agnostic
590 def _on_sms_clicked(self, widget):
592 action = PhoneTypeSelector.ACTION_SEND_SMS
593 phoneNumber = self.get_number()
595 message = self._smsDialog.run(phoneNumber, (), self._window)
598 action = PhoneTypeSelector.ACTION_CANCEL
600 if action == PhoneTypeSelector.ACTION_CANCEL:
602 self.number_selected(action, phoneNumber, message)
604 self._errorDisplay.push_exception()
606 def _on_dial_clicked(self, widget):
608 action = PhoneTypeSelector.ACTION_DIAL
609 phoneNumber = self.get_number()
611 self.number_selected(action, phoneNumber, message)
613 self._errorDisplay.push_exception()
615 def _on_digit_clicked(self, widget):
617 self.set_number(self._phonenumber + widget.get_name()[-1])
619 self._errorDisplay.push_exception()
621 def _on_backspace(self, taps):
623 self.set_number(self._phonenumber[:-taps])
624 self._reset_back_button()
626 self._errorDisplay.push_exception()
628 def _on_clearall(self, taps):
631 self._reset_back_button()
633 self._errorDisplay.push_exception()
636 def _set_clear_button(self):
638 self._backButton.set_label("gtk-clear")
640 self._errorDisplay.push_exception()
642 def _reset_back_button(self):
644 self._backButton.set_label(self._originalLabel)
646 self._errorDisplay.push_exception()
649 class AccountInfo(object):
651 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
652 self._errorDisplay = errorDisplay
653 self._backend = backend
654 self._isPopulated = False
655 self._alarmHandler = alarmHandler
656 self._notifyOnMissed = False
657 self._notifyOnVoicemail = False
658 self._notifyOnSms = False
660 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
661 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
662 self._callbackCombo = widgetTree.get_widget("callbackcombo")
663 self._onCallbackentryChangedId = 0
665 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
666 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
667 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
668 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
669 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
670 self._onNotifyToggled = 0
671 self._onMinutesChanged = 0
672 self._onMissedToggled = 0
673 self._onVoicemailToggled = 0
674 self._onSmsToggled = 0
675 self._applyAlarmTimeoutId = None
677 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
678 self._defaultCallback = ""
681 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
683 self._accountViewNumberDisplay.set_use_markup(True)
684 self.set_account_number("")
686 self._callbackList.clear()
687 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
689 if self._alarmHandler is not None:
690 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
691 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
692 self._missedCheckbox.set_active(self._notifyOnMissed)
693 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
694 self._smsCheckbox.set_active(self._notifyOnSms)
696 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
697 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
698 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
699 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
700 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
702 self._notifyCheckbox.set_sensitive(False)
703 self._minutesEntryButton.set_sensitive(False)
704 self._missedCheckbox.set_sensitive(False)
705 self._voicemailCheckbox.set_sensitive(False)
706 self._smsCheckbox.set_sensitive(False)
708 self.update(force=True)
711 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
712 self._onCallbackentryChangedId = 0
714 if self._alarmHandler is not None:
715 self._notifyCheckbox.disconnect(self._onNotifyToggled)
716 self._minutesEntryButton.disconnect(self._onMinutesChanged)
717 self._missedCheckbox.disconnect(self._onNotifyToggled)
718 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
719 self._smsCheckbox.disconnect(self._onNotifyToggled)
720 self._onNotifyToggled = 0
721 self._onMinutesChanged = 0
722 self._onMissedToggled = 0
723 self._onVoicemailToggled = 0
724 self._onSmsToggled = 0
726 self._notifyCheckbox.set_sensitive(True)
727 self._minutesEntryButton.set_sensitive(True)
728 self._missedCheckbox.set_sensitive(True)
729 self._voicemailCheckbox.set_sensitive(True)
730 self._smsCheckbox.set_sensitive(True)
733 self._callbackList.clear()
735 def get_selected_callback_number(self):
736 return make_ugly(self._callbackCombo.get_child().get_text())
738 def set_account_number(self, number):
740 Displays current account number
742 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
744 def update(self, force = False):
745 if not force and self._isPopulated:
747 self._populate_callback_combo()
748 self.set_account_number(self._backend.get_account_number())
752 self._callbackCombo.get_child().set_text("")
753 self.set_account_number("")
754 self._isPopulated = False
756 def save_everything(self):
757 raise NotImplementedError
761 return "Account Info"
763 def load_settings(self, config, section):
764 self._defaultCallback = config.get(section, "callback")
765 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
766 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
767 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
769 def save_settings(self, config, section):
771 @note Thread Agnostic
773 callback = self.get_selected_callback_number()
774 config.set(section, "callback", callback)
775 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
776 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
777 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
779 def _populate_callback_combo(self):
780 self._isPopulated = True
781 self._callbackList.clear()
783 callbackNumbers = self._backend.get_callback_numbers()
785 self._errorDisplay.push_exception()
786 self._isPopulated = False
789 for number, description in callbackNumbers.iteritems():
790 self._callbackList.append((make_pretty(number),))
792 self._callbackCombo.set_model(self._callbackList)
793 self._callbackCombo.set_text_column(0)
794 #callbackNumber = self._backend.get_callback_number()
795 callbackNumber = self._defaultCallback
796 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
798 def _set_callback_number(self, number):
800 if not self._backend.is_valid_syntax(number) and 0 < len(number):
801 self._errorDisplay.push_message("%s is not a valid callback number" % number)
802 elif number == self._backend.get_callback_number():
804 "Callback number already is %s" % (
805 self._backend.get_callback_number(),
809 self._backend.set_callback_number(number)
810 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
811 make_pretty(number), make_pretty(self._backend.get_callback_number())
814 "Callback number set to %s" % (
815 self._backend.get_callback_number(),
819 self._errorDisplay.push_exception()
821 def _update_alarm_settings(self, recurrence):
823 isEnabled = self._notifyCheckbox.get_active()
824 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
825 self._alarmHandler.apply_settings(isEnabled, recurrence)
827 self.save_everything()
828 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
829 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
831 def _on_callbackentry_changed(self, *args):
833 text = self.get_selected_callback_number()
834 number = make_ugly(text)
835 self._set_callback_number(number)
837 self._errorDisplay.push_exception()
839 def _on_notify_toggled(self, *args):
841 if self._applyAlarmTimeoutId is not None:
842 gobject.source_remove(self._applyAlarmTimeoutId)
843 self._applyAlarmTimeoutId = None
844 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
846 self._errorDisplay.push_exception()
848 def _on_minutes_clicked(self, *args):
849 recurrenceChoices = [
861 actualSelection = self._alarmHandler.recurrence
863 closestSelectionIndex = 0
864 for i, possible in enumerate(recurrenceChoices):
865 if possible[0] <= actualSelection:
866 closestSelectionIndex = i
867 recurrenceIndex = hildonize.touch_selector(
870 (("%s" % m[1]) for m in recurrenceChoices),
871 closestSelectionIndex,
873 recurrence = recurrenceChoices[recurrenceIndex][0]
875 self._update_alarm_settings(recurrence)
877 self._errorDisplay.push_exception()
879 def _on_apply_timeout(self, *args):
881 self._applyAlarmTimeoutId = None
883 self._update_alarm_settings(self._alarmHandler.recurrence)
885 self._errorDisplay.push_exception()
888 def _on_missed_toggled(self, *args):
890 self._notifyOnMissed = self._missedCheckbox.get_active()
891 self.save_everything()
893 self._errorDisplay.push_exception()
895 def _on_voicemail_toggled(self, *args):
897 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
898 self.save_everything()
900 self._errorDisplay.push_exception()
902 def _on_sms_toggled(self, *args):
904 self._notifyOnSms = self._smsCheckbox.get_active()
905 self.save_everything()
907 self._errorDisplay.push_exception()
910 class RecentCallsView(object):
917 def __init__(self, widgetTree, backend, errorDisplay):
918 self._errorDisplay = errorDisplay
919 self._backend = backend
921 self._isPopulated = False
922 self._recentmodel = gtk.ListStore(
923 gobject.TYPE_STRING, # number
924 gobject.TYPE_STRING, # date
925 gobject.TYPE_STRING, # action
926 gobject.TYPE_STRING, # from
928 self._recentview = widgetTree.get_widget("recentview")
929 self._recentviewselection = None
930 self._onRecentviewRowActivatedId = 0
932 textrenderer = gtk.CellRendererText()
933 textrenderer.set_property("yalign", 0)
934 self._dateColumn = gtk.TreeViewColumn("Date")
935 self._dateColumn.pack_start(textrenderer, expand=True)
936 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
938 textrenderer = gtk.CellRendererText()
939 textrenderer.set_property("yalign", 0)
940 self._actionColumn = gtk.TreeViewColumn("Action")
941 self._actionColumn.pack_start(textrenderer, expand=True)
942 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
944 textrenderer = gtk.CellRendererText()
945 textrenderer.set_property("yalign", 0)
946 hildonize.set_cell_thumb_selectable(textrenderer)
947 self._nameColumn = gtk.TreeViewColumn("From")
948 self._nameColumn.pack_start(textrenderer, expand=True)
949 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
950 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
952 textrenderer = gtk.CellRendererText()
953 textrenderer.set_property("yalign", 0)
954 self._numberColumn = gtk.TreeViewColumn("Number")
955 self._numberColumn.pack_start(textrenderer, expand=True)
956 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
958 self._window = gtk_toolbox.find_parent_window(self._recentview)
959 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
961 self._updateSink = gtk_toolbox.threaded_stage(
963 self._idly_populate_recentview,
964 gtk_toolbox.null_sink(),
969 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
970 self._recentview.set_model(self._recentmodel)
972 self._recentview.append_column(self._dateColumn)
973 self._recentview.append_column(self._actionColumn)
974 self._recentview.append_column(self._numberColumn)
975 self._recentview.append_column(self._nameColumn)
976 self._recentviewselection = self._recentview.get_selection()
977 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
979 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
982 self._recentview.disconnect(self._onRecentviewRowActivatedId)
986 self._recentview.remove_column(self._dateColumn)
987 self._recentview.remove_column(self._actionColumn)
988 self._recentview.remove_column(self._nameColumn)
989 self._recentview.remove_column(self._numberColumn)
990 self._recentview.set_model(None)
992 def number_selected(self, action, number, message):
994 @note Actual dial function is patched in later
996 raise NotImplementedError("Horrible unknown error has occurred")
998 def update(self, force = False):
999 if not force and self._isPopulated:
1001 self._updateSink.send(())
1005 self._isPopulated = False
1006 self._recentmodel.clear()
1010 return "Recent Calls"
1012 def load_settings(self, config, section):
1015 def save_settings(self, config, section):
1017 @note Thread Agnostic
1021 def _idly_populate_recentview(self):
1023 self._recentmodel.clear()
1024 self._isPopulated = True
1027 recentItems = self._backend.get_recent()
1028 except Exception, e:
1029 self._errorDisplay.push_exception_with_lock()
1030 self._isPopulated = False
1033 for personName, phoneNumber, date, action in recentItems:
1035 personName = "Unknown"
1036 date = abbrev_relative_date(date)
1037 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1038 prettyNumber = make_pretty(prettyNumber)
1039 item = (prettyNumber, date, action.capitalize(), personName)
1040 with gtk_toolbox.gtk_lock():
1041 self._recentmodel.append(item)
1042 except Exception, e:
1043 self._errorDisplay.push_exception_with_lock()
1047 def _on_recentview_row_activated(self, treeview, path, view_column):
1049 model, itr = self._recentviewselection.get_selected()
1053 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1054 number = make_ugly(number)
1055 contactPhoneNumbers = [("Phone", number)]
1056 description = self._recentmodel.get_value(itr, self.FROM_IDX)
1058 action, phoneNumber, message = self._phoneTypeSelector.run(
1059 contactPhoneNumbers,
1060 messages = (description, ),
1061 parent = self._window,
1063 if action == PhoneTypeSelector.ACTION_CANCEL:
1065 assert phoneNumber, "A lack of phone number exists"
1067 self.number_selected(action, phoneNumber, message)
1068 self._recentviewselection.unselect_all()
1069 except Exception, e:
1070 self._errorDisplay.push_exception()
1073 class MessagesView(object):
1081 def __init__(self, widgetTree, backend, errorDisplay):
1082 self._errorDisplay = errorDisplay
1083 self._backend = backend
1085 self._isPopulated = False
1086 self._messagemodel = gtk.ListStore(
1087 gobject.TYPE_STRING, # number
1088 gobject.TYPE_STRING, # date
1089 gobject.TYPE_STRING, # header
1090 gobject.TYPE_STRING, # message
1093 self._messageview = widgetTree.get_widget("messages_view")
1094 self._messageviewselection = None
1095 self._onMessageviewRowActivatedId = 0
1097 self._messageRenderer = gtk.CellRendererText()
1098 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1099 self._messageRenderer.set_property("wrap-width", 500)
1100 self._messageColumn = gtk.TreeViewColumn("Messages")
1101 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1102 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1103 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1105 self._window = gtk_toolbox.find_parent_window(self._messageview)
1106 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1108 self._updateSink = gtk_toolbox.threaded_stage(
1110 self._idly_populate_messageview,
1111 gtk_toolbox.null_sink(),
1116 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1117 self._messageview.set_model(self._messagemodel)
1119 self._messageview.append_column(self._messageColumn)
1120 self._messageviewselection = self._messageview.get_selection()
1121 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1123 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1126 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1130 self._messageview.remove_column(self._messageColumn)
1131 self._messageview.set_model(None)
1133 def number_selected(self, action, number, message):
1135 @note Actual dial function is patched in later
1137 raise NotImplementedError("Horrible unknown error has occurred")
1139 def update(self, force = False):
1140 if not force and self._isPopulated:
1142 self._updateSink.send(())
1146 self._isPopulated = False
1147 self._messagemodel.clear()
1153 def load_settings(self, config, section):
1156 def save_settings(self, config, section):
1158 @note Thread Agnostic
1162 def _idly_populate_messageview(self):
1164 self._messagemodel.clear()
1165 self._isPopulated = True
1168 messageItems = self._backend.get_messages()
1169 except Exception, e:
1170 self._errorDisplay.push_exception_with_lock()
1171 self._isPopulated = False
1174 for header, number, relativeDate, messages in messageItems:
1175 prettyNumber = number[2:] if number.startswith("+1") else number
1176 prettyNumber = make_pretty(prettyNumber)
1178 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1179 newMessages = [firstMessage]
1180 newMessages.extend(messages)
1182 number = make_ugly(number)
1184 row = (number, relativeDate, header, "\n".join(newMessages), newMessages)
1185 with gtk_toolbox.gtk_lock():
1186 self._messagemodel.append(row)
1187 except Exception, e:
1188 self._errorDisplay.push_exception_with_lock()
1192 def _on_messageview_row_activated(self, treeview, path, view_column):
1194 model, itr = self._messageviewselection.get_selected()
1198 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1199 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1201 action, phoneNumber, message = self._phoneTypeSelector.run(
1202 contactPhoneNumbers,
1203 messages = description,
1204 parent = self._window,
1206 if action == PhoneTypeSelector.ACTION_CANCEL:
1208 assert phoneNumber, "A lock of phone number exists"
1210 self.number_selected(action, phoneNumber, message)
1211 self._messageviewselection.unselect_all()
1212 except Exception, e:
1213 self._errorDisplay.push_exception()
1216 class ContactsView(object):
1218 def __init__(self, widgetTree, backend, errorDisplay):
1219 self._errorDisplay = errorDisplay
1220 self._backend = backend
1222 self._addressBook = None
1223 self._selectedComboIndex = 0
1224 self._addressBookFactories = [null_backend.NullAddressBook()]
1226 self._booksList = []
1227 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1229 self._isPopulated = False
1230 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1231 self._contactsviewselection = None
1232 self._contactsview = widgetTree.get_widget("contactsview")
1234 self._contactColumn = gtk.TreeViewColumn("Contact")
1235 displayContactSource = False
1236 if displayContactSource:
1237 textrenderer = gtk.CellRendererText()
1238 self._contactColumn.pack_start(textrenderer, expand=False)
1239 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1240 textrenderer = gtk.CellRendererText()
1241 hildonize.set_cell_thumb_selectable(textrenderer)
1242 self._contactColumn.pack_start(textrenderer, expand=True)
1243 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1244 textrenderer = gtk.CellRendererText()
1245 self._contactColumn.pack_start(textrenderer, expand=True)
1246 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1247 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1248 self._contactColumn.set_sort_column_id(1)
1249 self._contactColumn.set_visible(True)
1251 self._onContactsviewRowActivatedId = 0
1252 self._onAddressbookButtonChangedId = 0
1253 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1254 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1256 self._updateSink = gtk_toolbox.threaded_stage(
1258 self._idly_populate_contactsview,
1259 gtk_toolbox.null_sink(),
1264 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1266 self._contactsview.set_model(self._contactsmodel)
1267 self._contactsview.append_column(self._contactColumn)
1268 self._contactsviewselection = self._contactsview.get_selection()
1269 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1271 del self._booksList[:]
1272 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1273 if factoryName and bookName:
1274 entryName = "%s: %s" % (factoryName, bookName)
1276 entryName = factoryName
1278 entryName = bookName
1280 entryName = "Bad name (%d)" % factoryId
1281 row = (str(factoryId), bookId, entryName)
1282 self._booksList.append(row)
1284 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1285 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1287 if len(self._booksList) <= self._selectedComboIndex:
1288 self._selectedComboIndex = 0
1289 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1291 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1292 selectedBookId = self._booksList[self._selectedComboIndex][1]
1293 self.open_addressbook(selectedFactoryId, selectedBookId)
1296 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1297 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1301 self._bookSelectionButton.set_label("")
1302 self._contactsview.set_model(None)
1303 self._contactsview.remove_column(self._contactColumn)
1305 def number_selected(self, action, number, message):
1307 @note Actual dial function is patched in later
1309 raise NotImplementedError("Horrible unknown error has occurred")
1311 def get_addressbooks(self):
1313 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1315 for i, factory in enumerate(self._addressBookFactories):
1316 for bookFactory, bookId, bookName in factory.get_addressbooks():
1317 yield (str(i), bookId), (factory.factory_name(), bookName)
1319 def open_addressbook(self, bookFactoryId, bookId):
1320 bookFactoryIndex = int(bookFactoryId)
1321 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1323 forceUpdate = True if addressBook is not self._addressBook else False
1325 self._addressBook = addressBook
1326 self.update(force=forceUpdate)
1328 def update(self, force = False):
1329 if not force and self._isPopulated:
1331 self._updateSink.send(())
1335 self._isPopulated = False
1336 self._contactsmodel.clear()
1337 for factory in self._addressBookFactories:
1338 factory.clear_caches()
1339 self._addressBook.clear_caches()
1341 def append(self, book):
1342 self._addressBookFactories.append(book)
1344 def extend(self, books):
1345 self._addressBookFactories.extend(books)
1351 def load_settings(self, config, sectionName):
1353 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1354 except ConfigParser.NoOptionError:
1355 self._selectedComboIndex = 0
1357 def save_settings(self, config, sectionName):
1358 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1360 def _idly_populate_contactsview(self):
1363 while addressBook is not self._addressBook:
1364 addressBook = self._addressBook
1365 with gtk_toolbox.gtk_lock():
1366 self._contactsview.set_model(None)
1370 contacts = addressBook.get_contacts()
1371 except Exception, e:
1373 self._isPopulated = False
1374 self._errorDisplay.push_exception_with_lock()
1375 for contactId, contactName in contacts:
1376 contactType = (addressBook.contact_source_short_name(contactId), )
1377 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1379 with gtk_toolbox.gtk_lock():
1380 self._contactsview.set_model(self._contactsmodel)
1382 self._isPopulated = True
1383 except Exception, e:
1384 self._errorDisplay.push_exception_with_lock()
1387 def _on_addressbook_button_changed(self, *args, **kwds):
1390 newSelectedComboIndex = hildonize.touch_selector(
1393 (("%s" % m[2]) for m in self._booksList),
1394 self._selectedComboIndex,
1396 except RuntimeError:
1399 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1400 selectedBookId = self._booksList[newSelectedComboIndex][1]
1401 self.open_addressbook(selectedFactoryId, selectedBookId)
1402 self._selectedComboIndex = newSelectedComboIndex
1403 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1404 except Exception, e:
1405 self._errorDisplay.push_exception()
1407 def _on_contactsview_row_activated(self, treeview, path, view_column):
1409 model, itr = self._contactsviewselection.get_selected()
1413 contactId = self._contactsmodel.get_value(itr, 3)
1414 contactName = self._contactsmodel.get_value(itr, 1)
1416 contactDetails = self._addressBook.get_contact_details(contactId)
1417 except Exception, e:
1419 self._errorDisplay.push_exception()
1420 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1422 if len(contactPhoneNumbers) == 0:
1425 action, phoneNumber, message = self._phoneTypeSelector.run(
1426 contactPhoneNumbers,
1427 messages = (contactName, ),
1428 parent = self._window,
1430 if action == PhoneTypeSelector.ACTION_CANCEL:
1432 assert phoneNumber, "A lack of phone number exists"
1434 self.number_selected(action, phoneNumber, message)
1435 self._contactsviewselection.unselect_all()
1436 except Exception, e:
1437 self._errorDisplay.push_exception()