4 DialCentral - Front end for Google's GoogleVoice service.
5 Copyright (C) 2008 Mark Bergman bergman AT merctech DOT com
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 Lesser General Public License for more details.
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21 @todo Alternate UI for dialogs (stackables)
24 from __future__ import with_statement
38 _moduleLogger = logging.getLogger("gv_views")
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._clipboard = gtk.clipboard_get()
521 self._errorDisplay = errorDisplay
522 self._smsDialog = SmsEntryDialog(widgetTree)
524 self._numberdisplay = widgetTree.get_widget("numberdisplay")
525 self._smsButton = widgetTree.get_widget("sms")
526 self._dialButton = widgetTree.get_widget("dial")
527 self._backButton = widgetTree.get_widget("back")
528 self._phonenumber = ""
529 self._prettynumber = ""
532 "on_digit_clicked": self._on_digit_clicked,
534 widgetTree.signal_autoconnect(callbackMapping)
535 self._dialButton.connect("clicked", self._on_dial_clicked)
536 self._smsButton.connect("clicked", self._on_sms_clicked)
538 self._originalLabel = self._backButton.get_label()
539 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
540 self._backTapHandler.on_tap = self._on_backspace
541 self._backTapHandler.on_hold = self._on_clearall
542 self._backTapHandler.on_holding = self._set_clear_button
543 self._backTapHandler.on_cancel = self._reset_back_button
545 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
546 self._keyPressEventId = 0
549 self._dialButton.grab_focus()
550 self._backTapHandler.enable()
551 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
554 self._window.disconnect(self._keyPressEventId)
555 self._keyPressEventId = 0
556 self._reset_back_button()
557 self._backTapHandler.disable()
559 def number_selected(self, action, number, message):
561 @note Actual dial function is patched in later
563 raise NotImplementedError("Horrible unknown error has occurred")
565 def get_number(self):
566 return self._phonenumber
568 def set_number(self, number):
570 Set the number to dial
573 self._phonenumber = make_ugly(number)
574 self._prettynumber = make_pretty(self._phonenumber)
575 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
577 self._errorDisplay.push_exception()
586 def load_settings(self, config, section):
589 def save_settings(self, config, section):
591 @note Thread Agnostic
595 def _on_key_press(self, widget, event):
597 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
598 contents = self._clipboard.wait_for_text()
599 if contents is not None:
600 self.set_number(contents)
602 self._errorDisplay.push_exception()
604 def _on_sms_clicked(self, widget):
606 action = PhoneTypeSelector.ACTION_SEND_SMS
607 phoneNumber = self.get_number()
609 message = self._smsDialog.run(phoneNumber, (), self._window)
612 action = PhoneTypeSelector.ACTION_CANCEL
614 if action == PhoneTypeSelector.ACTION_CANCEL:
616 self.number_selected(action, phoneNumber, message)
618 self._errorDisplay.push_exception()
620 def _on_dial_clicked(self, widget):
622 action = PhoneTypeSelector.ACTION_DIAL
623 phoneNumber = self.get_number()
625 self.number_selected(action, phoneNumber, message)
627 self._errorDisplay.push_exception()
629 def _on_digit_clicked(self, widget):
631 self.set_number(self._phonenumber + widget.get_name()[-1])
633 self._errorDisplay.push_exception()
635 def _on_backspace(self, taps):
637 self.set_number(self._phonenumber[:-taps])
638 self._reset_back_button()
640 self._errorDisplay.push_exception()
642 def _on_clearall(self, taps):
645 self._reset_back_button()
647 self._errorDisplay.push_exception()
650 def _set_clear_button(self):
652 self._backButton.set_label("gtk-clear")
654 self._errorDisplay.push_exception()
656 def _reset_back_button(self):
658 self._backButton.set_label(self._originalLabel)
660 self._errorDisplay.push_exception()
663 class AccountInfo(object):
665 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
666 self._errorDisplay = errorDisplay
667 self._backend = backend
668 self._isPopulated = False
669 self._alarmHandler = alarmHandler
670 self._notifyOnMissed = False
671 self._notifyOnVoicemail = False
672 self._notifyOnSms = False
674 self._callbackList = []
675 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
676 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
677 self._onCallbackSelectChangedId = 0
679 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
680 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
681 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
682 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
683 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
684 self._onNotifyToggled = 0
685 self._onMinutesChanged = 0
686 self._onMissedToggled = 0
687 self._onVoicemailToggled = 0
688 self._onSmsToggled = 0
689 self._applyAlarmTimeoutId = None
691 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
692 self._defaultCallback = ""
695 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
697 self._accountViewNumberDisplay.set_use_markup(True)
698 self.set_account_number("")
700 del self._callbackList[:]
701 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
703 if self._alarmHandler is not None:
704 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
705 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
706 self._missedCheckbox.set_active(self._notifyOnMissed)
707 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
708 self._smsCheckbox.set_active(self._notifyOnSms)
710 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
711 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
712 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
713 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
714 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
716 self._notifyCheckbox.set_sensitive(False)
717 self._minutesEntryButton.set_sensitive(False)
718 self._missedCheckbox.set_sensitive(False)
719 self._voicemailCheckbox.set_sensitive(False)
720 self._smsCheckbox.set_sensitive(False)
722 self.update(force=True)
725 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
726 self._onCallbackSelectChangedId = 0
728 if self._alarmHandler is not None:
729 self._notifyCheckbox.disconnect(self._onNotifyToggled)
730 self._minutesEntryButton.disconnect(self._onMinutesChanged)
731 self._missedCheckbox.disconnect(self._onNotifyToggled)
732 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
733 self._smsCheckbox.disconnect(self._onNotifyToggled)
734 self._onNotifyToggled = 0
735 self._onMinutesChanged = 0
736 self._onMissedToggled = 0
737 self._onVoicemailToggled = 0
738 self._onSmsToggled = 0
740 self._notifyCheckbox.set_sensitive(True)
741 self._minutesEntryButton.set_sensitive(True)
742 self._missedCheckbox.set_sensitive(True)
743 self._voicemailCheckbox.set_sensitive(True)
744 self._smsCheckbox.set_sensitive(True)
747 del self._callbackList[:]
749 def get_selected_callback_number(self):
750 currentLabel = self._callbackSelectButton.get_label()
751 if currentLabel is not None:
752 return make_ugly(currentLabel)
756 def set_account_number(self, number):
758 Displays current account number
760 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
762 def update(self, force = False):
763 if not force and self._isPopulated:
765 self._populate_callback_combo()
766 self.set_account_number(self._backend.get_account_number())
770 self._callbackSelectButton.set_label("")
771 self.set_account_number("")
772 self._isPopulated = False
774 def save_everything(self):
775 raise NotImplementedError
779 return "Account Info"
781 def load_settings(self, config, section):
782 self._defaultCallback = config.get(section, "callback")
783 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
784 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
785 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
787 def save_settings(self, config, section):
789 @note Thread Agnostic
791 callback = self.get_selected_callback_number()
792 config.set(section, "callback", callback)
793 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
794 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
795 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
797 def _populate_callback_combo(self):
798 self._isPopulated = True
799 del self._callbackList[:]
801 callbackNumbers = self._backend.get_callback_numbers()
803 self._errorDisplay.push_exception()
804 self._isPopulated = False
807 for number, description in callbackNumbers.iteritems():
808 self._callbackList.append(make_pretty(number))
810 if not self.get_selected_callback_number():
811 self._set_callback_number(self._defaultCallback)
813 def _set_callback_number(self, number):
815 if not self._backend.is_valid_syntax(number) and 0 < len(number):
816 self._errorDisplay.push_message("%s is not a valid callback number" % number)
817 elif number == self._backend.get_callback_number():
818 _moduleLogger.warning(
819 "Callback number already is %s" % (
820 self._backend.get_callback_number(),
824 self._backend.set_callback_number(number)
825 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
826 make_pretty(number), make_pretty(self._backend.get_callback_number())
828 self._callbackSelectButton.set_label(make_pretty(number))
830 "Callback number set to %s" % (
831 self._backend.get_callback_number(),
835 self._errorDisplay.push_exception()
837 def _update_alarm_settings(self, recurrence):
839 isEnabled = self._notifyCheckbox.get_active()
840 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
841 self._alarmHandler.apply_settings(isEnabled, recurrence)
843 self.save_everything()
844 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
845 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
847 def _on_callbackentry_clicked(self, *args):
849 actualSelection = make_pretty(self.get_selected_callback_number())
851 userSelection = hildonize.touch_selector_entry(
857 number = make_ugly(userSelection)
858 self._set_callback_number(number)
859 except RuntimeError, e:
860 _moduleLogger.exception("%s" % str(e))
862 self._errorDisplay.push_exception()
864 def _on_notify_toggled(self, *args):
866 if self._applyAlarmTimeoutId is not None:
867 gobject.source_remove(self._applyAlarmTimeoutId)
868 self._applyAlarmTimeoutId = None
869 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
871 self._errorDisplay.push_exception()
873 def _on_minutes_clicked(self, *args):
874 recurrenceChoices = [
890 actualSelection = self._alarmHandler.recurrence
892 closestSelectionIndex = 0
893 for i, possible in enumerate(recurrenceChoices):
894 if possible[0] <= actualSelection:
895 closestSelectionIndex = i
896 recurrenceIndex = hildonize.touch_selector(
899 (("%s" % m[1]) for m in recurrenceChoices),
900 closestSelectionIndex,
902 recurrence = recurrenceChoices[recurrenceIndex][0]
904 self._update_alarm_settings(recurrence)
905 except RuntimeError, e:
906 _moduleLogger.exception("%s" % str(e))
908 self._errorDisplay.push_exception()
910 def _on_apply_timeout(self, *args):
912 self._applyAlarmTimeoutId = None
914 self._update_alarm_settings(self._alarmHandler.recurrence)
916 self._errorDisplay.push_exception()
919 def _on_missed_toggled(self, *args):
921 self._notifyOnMissed = self._missedCheckbox.get_active()
922 self.save_everything()
924 self._errorDisplay.push_exception()
926 def _on_voicemail_toggled(self, *args):
928 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
929 self.save_everything()
931 self._errorDisplay.push_exception()
933 def _on_sms_toggled(self, *args):
935 self._notifyOnSms = self._smsCheckbox.get_active()
936 self.save_everything()
938 self._errorDisplay.push_exception()
941 class RecentCallsView(object):
948 def __init__(self, widgetTree, backend, errorDisplay):
949 self._errorDisplay = errorDisplay
950 self._backend = backend
952 self._isPopulated = False
953 self._recentmodel = gtk.ListStore(
954 gobject.TYPE_STRING, # number
955 gobject.TYPE_STRING, # date
956 gobject.TYPE_STRING, # action
957 gobject.TYPE_STRING, # from
959 self._recentview = widgetTree.get_widget("recentview")
960 self._recentviewselection = None
961 self._onRecentviewRowActivatedId = 0
963 textrenderer = gtk.CellRendererText()
964 textrenderer.set_property("yalign", 0)
965 self._dateColumn = gtk.TreeViewColumn("Date")
966 self._dateColumn.pack_start(textrenderer, expand=True)
967 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
969 textrenderer = gtk.CellRendererText()
970 textrenderer.set_property("yalign", 0)
971 self._actionColumn = gtk.TreeViewColumn("Action")
972 self._actionColumn.pack_start(textrenderer, expand=True)
973 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
975 textrenderer = gtk.CellRendererText()
976 textrenderer.set_property("yalign", 0)
977 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
978 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
979 self._numberColumn = gtk.TreeViewColumn("Number")
980 self._numberColumn.pack_start(textrenderer, expand=True)
981 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
983 textrenderer = gtk.CellRendererText()
984 textrenderer.set_property("yalign", 0)
985 hildonize.set_cell_thumb_selectable(textrenderer)
986 self._nameColumn = gtk.TreeViewColumn("From")
987 self._nameColumn.pack_start(textrenderer, expand=True)
988 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
989 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
991 self._window = gtk_toolbox.find_parent_window(self._recentview)
992 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
994 self._updateSink = gtk_toolbox.threaded_stage(
996 self._idly_populate_recentview,
997 gtk_toolbox.null_sink(),
1002 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1003 self._recentview.set_model(self._recentmodel)
1005 self._recentview.append_column(self._dateColumn)
1006 self._recentview.append_column(self._actionColumn)
1007 self._recentview.append_column(self._numberColumn)
1008 self._recentview.append_column(self._nameColumn)
1009 self._recentviewselection = self._recentview.get_selection()
1010 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
1012 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
1015 self._recentview.disconnect(self._onRecentviewRowActivatedId)
1019 self._recentview.remove_column(self._dateColumn)
1020 self._recentview.remove_column(self._actionColumn)
1021 self._recentview.remove_column(self._nameColumn)
1022 self._recentview.remove_column(self._numberColumn)
1023 self._recentview.set_model(None)
1025 def number_selected(self, action, number, message):
1027 @note Actual dial function is patched in later
1029 raise NotImplementedError("Horrible unknown error has occurred")
1031 def update(self, force = False):
1032 if not force and self._isPopulated:
1034 self._updateSink.send(())
1038 self._isPopulated = False
1039 self._recentmodel.clear()
1043 return "Recent Calls"
1045 def load_settings(self, config, section):
1048 def save_settings(self, config, section):
1050 @note Thread Agnostic
1054 def _idly_populate_recentview(self):
1055 with gtk_toolbox.gtk_lock():
1056 banner = hildonize.show_busy_banner_start(self._window, "Loading Recent History")
1058 self._recentmodel.clear()
1059 self._isPopulated = True
1062 recentItems = self._backend.get_recent()
1063 except Exception, e:
1064 self._errorDisplay.push_exception_with_lock()
1065 self._isPopulated = False
1068 for personName, phoneNumber, date, action in recentItems:
1070 personName = "Unknown"
1071 date = abbrev_relative_date(date)
1072 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1073 prettyNumber = make_pretty(prettyNumber)
1074 item = (prettyNumber, date, action.capitalize(), personName)
1075 with gtk_toolbox.gtk_lock():
1076 self._recentmodel.append(item)
1077 except Exception, e:
1078 self._errorDisplay.push_exception_with_lock()
1080 with gtk_toolbox.gtk_lock():
1081 hildonize.show_busy_banner_end(banner)
1085 def _on_recentview_row_activated(self, treeview, path, view_column):
1087 model, itr = self._recentviewselection.get_selected()
1091 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1092 number = make_ugly(number)
1093 contactPhoneNumbers = [("Phone", number)]
1094 description = self._recentmodel.get_value(itr, self.FROM_IDX)
1096 action, phoneNumber, message = self._phoneTypeSelector.run(
1097 contactPhoneNumbers,
1098 messages = (description, ),
1099 parent = self._window,
1101 if action == PhoneTypeSelector.ACTION_CANCEL:
1103 assert phoneNumber, "A lack of phone number exists"
1105 self.number_selected(action, phoneNumber, message)
1106 self._recentviewselection.unselect_all()
1107 except Exception, e:
1108 self._errorDisplay.push_exception()
1111 class MessagesView(object):
1119 def __init__(self, widgetTree, backend, errorDisplay):
1120 self._errorDisplay = errorDisplay
1121 self._backend = backend
1123 self._isPopulated = False
1124 self._messagemodel = gtk.ListStore(
1125 gobject.TYPE_STRING, # number
1126 gobject.TYPE_STRING, # date
1127 gobject.TYPE_STRING, # header
1128 gobject.TYPE_STRING, # message
1131 self._messageview = widgetTree.get_widget("messages_view")
1132 self._messageviewselection = None
1133 self._onMessageviewRowActivatedId = 0
1135 self._messageRenderer = gtk.CellRendererText()
1136 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1137 self._messageRenderer.set_property("wrap-width", 500)
1138 self._messageColumn = gtk.TreeViewColumn("Messages")
1139 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1140 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1141 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1143 self._window = gtk_toolbox.find_parent_window(self._messageview)
1144 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1146 self._updateSink = gtk_toolbox.threaded_stage(
1148 self._idly_populate_messageview,
1149 gtk_toolbox.null_sink(),
1154 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1155 self._messageview.set_model(self._messagemodel)
1156 self._messageview.set_headers_visible(False)
1158 self._messageview.append_column(self._messageColumn)
1159 self._messageviewselection = self._messageview.get_selection()
1160 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1162 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1165 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1169 self._messageview.remove_column(self._messageColumn)
1170 self._messageview.set_model(None)
1172 def number_selected(self, action, number, message):
1174 @note Actual dial function is patched in later
1176 raise NotImplementedError("Horrible unknown error has occurred")
1178 def update(self, force = False):
1179 if not force and self._isPopulated:
1181 self._updateSink.send(())
1185 self._isPopulated = False
1186 self._messagemodel.clear()
1192 def load_settings(self, config, section):
1195 def save_settings(self, config, section):
1197 @note Thread Agnostic
1201 def _idly_populate_messageview(self):
1202 with gtk_toolbox.gtk_lock():
1203 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1205 self._messagemodel.clear()
1206 self._isPopulated = True
1209 messageItems = self._backend.get_messages()
1210 except Exception, e:
1211 self._errorDisplay.push_exception_with_lock()
1212 self._isPopulated = False
1215 for header, number, relativeDate, messages in messageItems:
1216 prettyNumber = number[2:] if number.startswith("+1") else number
1217 prettyNumber = make_pretty(prettyNumber)
1219 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1220 newMessages = [firstMessage]
1221 newMessages.extend(messages)
1223 number = make_ugly(number)
1225 row = (number, relativeDate, header, "\n".join(newMessages), newMessages)
1226 with gtk_toolbox.gtk_lock():
1227 self._messagemodel.append(row)
1228 except Exception, e:
1229 self._errorDisplay.push_exception_with_lock()
1231 with gtk_toolbox.gtk_lock():
1232 hildonize.show_busy_banner_end(banner)
1236 def _on_messageview_row_activated(self, treeview, path, view_column):
1238 model, itr = self._messageviewselection.get_selected()
1242 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1243 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1245 action, phoneNumber, message = self._phoneTypeSelector.run(
1246 contactPhoneNumbers,
1247 messages = description,
1248 parent = self._window,
1250 if action == PhoneTypeSelector.ACTION_CANCEL:
1252 assert phoneNumber, "A lock of phone number exists"
1254 self.number_selected(action, phoneNumber, message)
1255 self._messageviewselection.unselect_all()
1256 except Exception, e:
1257 self._errorDisplay.push_exception()
1260 class ContactsView(object):
1262 def __init__(self, widgetTree, backend, errorDisplay):
1263 self._errorDisplay = errorDisplay
1264 self._backend = backend
1266 self._addressBook = None
1267 self._selectedComboIndex = 0
1268 self._addressBookFactories = [null_backend.NullAddressBook()]
1270 self._booksList = []
1271 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1273 self._isPopulated = False
1274 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1275 self._contactsviewselection = None
1276 self._contactsview = widgetTree.get_widget("contactsview")
1278 self._contactColumn = gtk.TreeViewColumn("Contact")
1279 displayContactSource = False
1280 if displayContactSource:
1281 textrenderer = gtk.CellRendererText()
1282 self._contactColumn.pack_start(textrenderer, expand=False)
1283 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1284 textrenderer = gtk.CellRendererText()
1285 hildonize.set_cell_thumb_selectable(textrenderer)
1286 self._contactColumn.pack_start(textrenderer, expand=True)
1287 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1288 textrenderer = gtk.CellRendererText()
1289 self._contactColumn.pack_start(textrenderer, expand=True)
1290 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1291 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1292 self._contactColumn.set_sort_column_id(1)
1293 self._contactColumn.set_visible(True)
1295 self._onContactsviewRowActivatedId = 0
1296 self._onAddressbookButtonChangedId = 0
1297 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1298 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1300 self._updateSink = gtk_toolbox.threaded_stage(
1302 self._idly_populate_contactsview,
1303 gtk_toolbox.null_sink(),
1308 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1310 self._contactsview.set_model(self._contactsmodel)
1311 self._contactsview.append_column(self._contactColumn)
1312 self._contactsviewselection = self._contactsview.get_selection()
1313 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1315 del self._booksList[:]
1316 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1317 if factoryName and bookName:
1318 entryName = "%s: %s" % (factoryName, bookName)
1320 entryName = factoryName
1322 entryName = bookName
1324 entryName = "Bad name (%d)" % factoryId
1325 row = (str(factoryId), bookId, entryName)
1326 self._booksList.append(row)
1328 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1329 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1331 if len(self._booksList) <= self._selectedComboIndex:
1332 self._selectedComboIndex = 0
1333 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1335 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1336 selectedBookId = self._booksList[self._selectedComboIndex][1]
1337 self.open_addressbook(selectedFactoryId, selectedBookId)
1340 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1341 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1345 self._bookSelectionButton.set_label("")
1346 self._contactsview.set_model(None)
1347 self._contactsview.remove_column(self._contactColumn)
1349 def number_selected(self, action, number, message):
1351 @note Actual dial function is patched in later
1353 raise NotImplementedError("Horrible unknown error has occurred")
1355 def get_addressbooks(self):
1357 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1359 for i, factory in enumerate(self._addressBookFactories):
1360 for bookFactory, bookId, bookName in factory.get_addressbooks():
1361 yield (str(i), bookId), (factory.factory_name(), bookName)
1363 def open_addressbook(self, bookFactoryId, bookId):
1364 bookFactoryIndex = int(bookFactoryId)
1365 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1367 forceUpdate = True if addressBook is not self._addressBook else False
1369 self._addressBook = addressBook
1370 self.update(force=forceUpdate)
1372 def update(self, force = False):
1373 if not force and self._isPopulated:
1375 self._updateSink.send(())
1379 self._isPopulated = False
1380 self._contactsmodel.clear()
1381 for factory in self._addressBookFactories:
1382 factory.clear_caches()
1383 self._addressBook.clear_caches()
1385 def append(self, book):
1386 self._addressBookFactories.append(book)
1388 def extend(self, books):
1389 self._addressBookFactories.extend(books)
1395 def load_settings(self, config, sectionName):
1397 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1398 except ConfigParser.NoOptionError:
1399 self._selectedComboIndex = 0
1401 def save_settings(self, config, sectionName):
1402 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1404 def _idly_populate_contactsview(self):
1405 with gtk_toolbox.gtk_lock():
1406 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1409 while addressBook is not self._addressBook:
1410 addressBook = self._addressBook
1411 with gtk_toolbox.gtk_lock():
1412 self._contactsview.set_model(None)
1416 contacts = addressBook.get_contacts()
1417 except Exception, e:
1419 self._isPopulated = False
1420 self._errorDisplay.push_exception_with_lock()
1421 for contactId, contactName in contacts:
1422 contactType = (addressBook.contact_source_short_name(contactId), )
1423 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1425 with gtk_toolbox.gtk_lock():
1426 self._contactsview.set_model(self._contactsmodel)
1428 self._isPopulated = True
1429 except Exception, e:
1430 self._errorDisplay.push_exception_with_lock()
1432 with gtk_toolbox.gtk_lock():
1433 hildonize.show_busy_banner_end(banner)
1436 def _on_addressbook_button_changed(self, *args, **kwds):
1439 newSelectedComboIndex = hildonize.touch_selector(
1442 (("%s" % m[2]) for m in self._booksList),
1443 self._selectedComboIndex,
1445 except RuntimeError:
1448 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1449 selectedBookId = self._booksList[newSelectedComboIndex][1]
1450 self.open_addressbook(selectedFactoryId, selectedBookId)
1451 self._selectedComboIndex = newSelectedComboIndex
1452 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1453 except Exception, e:
1454 self._errorDisplay.push_exception()
1456 def _on_contactsview_row_activated(self, treeview, path, view_column):
1458 model, itr = self._contactsviewselection.get_selected()
1462 contactId = self._contactsmodel.get_value(itr, 3)
1463 contactName = self._contactsmodel.get_value(itr, 1)
1465 contactDetails = self._addressBook.get_contact_details(contactId)
1466 except Exception, e:
1468 self._errorDisplay.push_exception()
1469 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1471 if len(contactPhoneNumbers) == 0:
1474 action, phoneNumber, message = self._phoneTypeSelector.run(
1475 contactPhoneNumbers,
1476 messages = (contactName, ),
1477 parent = self._window,
1479 if action == PhoneTypeSelector.ACTION_CANCEL:
1481 assert phoneNumber, "A lack of phone number exists"
1483 self.number_selected(action, phoneNumber, message)
1484 self._contactsviewselection.unselect_all()
1485 except Exception, e:
1486 self._errorDisplay.push_exception()