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
40 _moduleLogger = logging.getLogger("gv_views")
43 def make_ugly(prettynumber):
45 function to take a phone number and strip out all non-numeric
48 >>> make_ugly("+012-(345)-678-90")
52 uglynumber = re.sub('\D', '', prettynumber)
56 def make_pretty(phonenumber):
58 Function to take a phone number and return the pretty version
60 if phonenumber begins with 0:
62 if phonenumber begins with 1: ( for gizmo callback numbers )
64 if phonenumber is 13 digits:
66 if phonenumber is 10 digits:
70 >>> make_pretty("1234567")
72 >>> make_pretty("2345678901")
74 >>> make_pretty("12345678901")
76 >>> make_pretty("01234567890")
79 if phonenumber is None or phonenumber is "":
82 phonenumber = make_ugly(phonenumber)
84 if len(phonenumber) < 3:
87 if phonenumber[0] == "0":
89 prettynumber += "+%s" % phonenumber[0:3]
90 if 3 < len(phonenumber):
91 prettynumber += "-(%s)" % phonenumber[3:6]
92 if 6 < len(phonenumber):
93 prettynumber += "-%s" % phonenumber[6:9]
94 if 9 < len(phonenumber):
95 prettynumber += "-%s" % phonenumber[9:]
97 elif len(phonenumber) <= 7:
98 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
99 elif len(phonenumber) > 8 and phonenumber[0] == "1":
100 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
101 elif len(phonenumber) > 7:
102 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
106 def abbrev_relative_date(date):
108 >>> abbrev_relative_date("42 hours ago")
110 >>> abbrev_relative_date("2 days ago")
112 >>> abbrev_relative_date("4 weeks ago")
115 parts = date.split(" ")
116 return "%s %s" % (parts[0], parts[1][0])
119 class MergedAddressBook(object):
121 Merger of all addressbooks
124 def __init__(self, addressbookFactories, sorter = None):
125 self.__addressbookFactories = addressbookFactories
126 self.__addressbooks = None
127 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
129 def clear_caches(self):
130 self.__addressbooks = None
131 for factory in self.__addressbookFactories:
132 factory.clear_caches()
134 def get_addressbooks(self):
136 @returns Iterable of (Address Book Factory, Book Id, Book Name)
140 def open_addressbook(self, bookId):
143 def contact_source_short_name(self, contactId):
144 if self.__addressbooks is None:
146 bookIndex, originalId = contactId.split("-", 1)
147 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
151 return "All Contacts"
153 def get_contacts(self):
155 @returns Iterable of (contact id, contact name)
157 if self.__addressbooks is None:
158 self.__addressbooks = list(
159 factory.open_addressbook(id)
160 for factory in self.__addressbookFactories
161 for (f, id, name) in factory.get_addressbooks()
164 ("-".join([str(bookIndex), contactId]), contactName)
165 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
166 for (contactId, contactName) in addressbook.get_contacts()
168 sortedContacts = self.__sort_contacts(contacts)
169 return sortedContacts
171 def get_contact_details(self, contactId):
173 @returns Iterable of (Phone Type, Phone Number)
175 if self.__addressbooks is None:
177 bookIndex, originalId = contactId.split("-", 1)
178 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
181 def null_sorter(contacts):
183 Good for speed/low memory
188 def basic_firtname_sorter(contacts):
190 Expects names in "First Last" format
193 (contactName.rsplit(" ", 1)[0], (contactId, contactName))
194 for (contactId, contactName) in contacts
196 contactsWithKey.sort()
197 return (contactData for (lastName, contactData) in contactsWithKey)
200 def basic_lastname_sorter(contacts):
202 Expects names in "First Last" format
205 (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
206 for (contactId, contactName) in contacts
208 contactsWithKey.sort()
209 return (contactData for (lastName, contactData) in contactsWithKey)
212 def reversed_firtname_sorter(contacts):
214 Expects names in "Last, First" format
217 (contactName.split(", ", 1)[-1], (contactId, contactName))
218 for (contactId, contactName) in contacts
220 contactsWithKey.sort()
221 return (contactData for (lastName, contactData) in contactsWithKey)
224 def reversed_lastname_sorter(contacts):
226 Expects names in "Last, First" format
229 (contactName.split(", ", 1)[0], (contactId, contactName))
230 for (contactId, contactName) in contacts
232 contactsWithKey.sort()
233 return (contactData for (lastName, contactData) in contactsWithKey)
236 def guess_firstname(name):
238 return name.split(", ", 1)[-1]
240 return name.rsplit(" ", 1)[0]
243 def guess_lastname(name):
245 return name.split(", ", 1)[0]
247 return name.rsplit(" ", 1)[-1]
250 def advanced_firstname_sorter(cls, contacts):
252 (cls.guess_firstname(contactName), (contactId, contactName))
253 for (contactId, contactName) in contacts
255 contactsWithKey.sort()
256 return (contactData for (lastName, contactData) in contactsWithKey)
259 def advanced_lastname_sorter(cls, contacts):
261 (cls.guess_lastname(contactName), (contactId, contactName))
262 for (contactId, contactName) in contacts
264 contactsWithKey.sort()
265 return (contactData for (lastName, contactData) in contactsWithKey)
268 class PhoneTypeSelector(object):
270 ACTION_CANCEL = "cancel"
271 ACTION_SELECT = "select"
273 ACTION_SEND_SMS = "sms"
275 def __init__(self, widgetTree, gcBackend):
276 self._gcBackend = gcBackend
277 self._widgetTree = widgetTree
279 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
280 self._smsDialog = SmsEntryDialog(self._widgetTree)
282 self._smsButton = self._widgetTree.get_widget("sms_button")
283 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
285 self._dialButton = self._widgetTree.get_widget("dial_button")
286 self._dialButton.connect("clicked", self._on_phonetype_dial)
288 self._selectButton = self._widgetTree.get_widget("select_button")
289 self._selectButton.connect("clicked", self._on_phonetype_select)
291 self._cancelButton = self._widgetTree.get_widget("cancel_button")
292 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
294 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
295 self._messagesView = self._widgetTree.get_widget("phoneSelectionMessages")
296 self._scrollWindow = self._messagesView.get_parent()
298 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
299 self._typeviewselection = None
300 self._typeview = self._widgetTree.get_widget("phonetypes")
301 self._typeview.connect("row-activated", self._on_phonetype_select)
303 self._action = self.ACTION_CANCEL
305 def run(self, contactDetails, messages = (), parent = None):
306 self._action = self.ACTION_CANCEL
308 # Add the column to the phone selection tree view
309 self._typemodel.clear()
310 self._typeview.set_model(self._typemodel)
312 textrenderer = gtk.CellRendererText()
313 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
314 self._typeview.append_column(numberColumn)
316 textrenderer = gtk.CellRendererText()
317 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
318 self._typeview.append_column(typeColumn)
320 for phoneType, phoneNumber in contactDetails:
321 display = " - ".join((phoneNumber, phoneType))
323 row = (phoneNumber, display)
324 self._typemodel.append(row)
326 self._typeviewselection = self._typeview.get_selection()
327 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
328 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
330 # Add the column to the messages tree view
331 self._messagemodel.clear()
332 self._messagesView.set_model(self._messagemodel)
334 textrenderer = gtk.CellRendererText()
335 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
336 textrenderer.set_property("wrap-width", 450)
337 messageColumn = gtk.TreeViewColumn("")
338 messageColumn.pack_start(textrenderer, expand=True)
339 messageColumn.add_attribute(textrenderer, "markup", 0)
340 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
341 self._messagesView.append_column(messageColumn)
342 self._messagesView.set_headers_visible(False)
345 for message in messages:
347 self._messagemodel.append(row)
348 self._messagesView.show()
349 self._scrollWindow.show()
350 messagesSelection = self._messagesView.get_selection()
351 messagesSelection.select_path((len(messages)-1, ))
353 self._messagesView.hide()
354 self._scrollWindow.hide()
356 if parent is not None:
357 self._dialog.set_transient_for(parent)
362 self._messagesView.scroll_to_cell((len(messages)-1, ))
364 userResponse = self._dialog.run()
368 if userResponse == gtk.RESPONSE_OK:
369 phoneNumber = self._get_number()
370 phoneNumber = make_ugly(phoneNumber)
374 self._action = self.ACTION_CANCEL
376 if self._action == self.ACTION_SEND_SMS:
377 smsMessage = self._smsDialog.run(phoneNumber, messages, parent)
380 self._action = self.ACTION_CANCEL
384 self._messagesView.remove_column(messageColumn)
385 self._messagesView.set_model(None)
387 self._typeviewselection.unselect_all()
388 self._typeview.remove_column(numberColumn)
389 self._typeview.remove_column(typeColumn)
390 self._typeview.set_model(None)
392 return self._action, phoneNumber, smsMessage
394 def _get_number(self):
395 model, itr = self._typeviewselection.get_selected()
399 phoneNumber = self._typemodel.get_value(itr, 0)
402 def _on_phonetype_dial(self, *args):
403 self._dialog.response(gtk.RESPONSE_OK)
404 self._action = self.ACTION_DIAL
406 def _on_phonetype_send_sms(self, *args):
407 self._dialog.response(gtk.RESPONSE_OK)
408 self._action = self.ACTION_SEND_SMS
410 def _on_phonetype_select(self, *args):
411 self._dialog.response(gtk.RESPONSE_OK)
412 self._action = self.ACTION_SELECT
414 def _on_phonetype_cancel(self, *args):
415 self._dialog.response(gtk.RESPONSE_CANCEL)
416 self._action = self.ACTION_CANCEL
419 class SmsEntryDialog(object):
421 @todo Add multi-SMS messages like GoogleVoice
426 def __init__(self, widgetTree):
427 self._widgetTree = widgetTree
428 self._dialog = self._widgetTree.get_widget("smsDialog")
430 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
431 self._smsButton.connect("clicked", self._on_send)
433 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
434 self._cancelButton.connect("clicked", self._on_cancel)
436 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
438 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
439 self._messagesView = self._widgetTree.get_widget("smsMessages")
440 self._scrollWindow = self._messagesView.get_parent()
442 self._smsEntry = self._widgetTree.get_widget("smsEntry")
443 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
445 def run(self, number, messages = (), parent = None):
446 # Add the column to the messages tree view
447 self._messagemodel.clear()
448 self._messagesView.set_model(self._messagemodel)
450 textrenderer = gtk.CellRendererText()
451 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
452 textrenderer.set_property("wrap-width", 450)
453 messageColumn = gtk.TreeViewColumn("")
454 messageColumn.pack_start(textrenderer, expand=True)
455 messageColumn.add_attribute(textrenderer, "markup", 0)
456 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
457 self._messagesView.append_column(messageColumn)
458 self._messagesView.set_headers_visible(False)
461 for message in messages:
463 self._messagemodel.append(row)
464 self._messagesView.show()
465 self._scrollWindow.show()
466 messagesSelection = self._messagesView.get_selection()
467 messagesSelection.select_path((len(messages)-1, ))
469 self._messagesView.hide()
470 self._scrollWindow.hide()
472 self._smsEntry.get_buffer().set_text("")
473 self._update_letter_count()
475 if parent is not None:
476 self._dialog.set_transient_for(parent)
481 self._messagesView.scroll_to_cell((len(messages)-1, ))
482 self._smsEntry.grab_focus()
484 userResponse = self._dialog.run()
488 if userResponse == gtk.RESPONSE_OK:
489 entryBuffer = self._smsEntry.get_buffer()
490 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
491 enteredMessage = enteredMessage[0:self.MAX_CHAR]
495 self._messagesView.remove_column(messageColumn)
496 self._messagesView.set_model(None)
498 return enteredMessage.strip()
500 def _update_letter_count(self, *args):
501 entryLength = self._smsEntry.get_buffer().get_char_count()
502 charsLeft = self.MAX_CHAR - entryLength
503 self._letterCountLabel.set_text(str(charsLeft))
505 self._smsButton.set_sensitive(False)
507 self._smsButton.set_sensitive(True)
509 def _on_entry_changed(self, *args):
510 self._update_letter_count()
512 def _on_send(self, *args):
513 self._dialog.response(gtk.RESPONSE_OK)
515 def _on_cancel(self, *args):
516 self._dialog.response(gtk.RESPONSE_CANCEL)
519 class Dialpad(object):
521 def __init__(self, widgetTree, errorDisplay):
522 self._clipboard = gtk.clipboard_get()
523 self._errorDisplay = errorDisplay
524 self._smsDialog = SmsEntryDialog(widgetTree)
526 self._numberdisplay = widgetTree.get_widget("numberdisplay")
527 self._smsButton = widgetTree.get_widget("sms")
528 self._dialButton = widgetTree.get_widget("dial")
529 self._backButton = widgetTree.get_widget("back")
530 self._phonenumber = ""
531 self._prettynumber = ""
534 "on_digit_clicked": self._on_digit_clicked,
536 widgetTree.signal_autoconnect(callbackMapping)
537 self._dialButton.connect("clicked", self._on_dial_clicked)
538 self._smsButton.connect("clicked", self._on_sms_clicked)
540 self._originalLabel = self._backButton.get_label()
541 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
542 self._backTapHandler.on_tap = self._on_backspace
543 self._backTapHandler.on_hold = self._on_clearall
544 self._backTapHandler.on_holding = self._set_clear_button
545 self._backTapHandler.on_cancel = self._reset_back_button
547 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
548 self._keyPressEventId = 0
551 self._dialButton.grab_focus()
552 self._backTapHandler.enable()
553 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
556 self._window.disconnect(self._keyPressEventId)
557 self._keyPressEventId = 0
558 self._reset_back_button()
559 self._backTapHandler.disable()
561 def number_selected(self, action, number, message):
563 @note Actual dial function is patched in later
565 raise NotImplementedError("Horrible unknown error has occurred")
567 def get_number(self):
568 return self._phonenumber
570 def set_number(self, number):
572 Set the number to dial
575 self._phonenumber = make_ugly(number)
576 self._prettynumber = make_pretty(self._phonenumber)
577 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
579 self._errorDisplay.push_exception()
588 def load_settings(self, config, section):
591 def save_settings(self, config, section):
593 @note Thread Agnostic
597 def _on_key_press(self, widget, event):
599 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
600 contents = self._clipboard.wait_for_text()
601 if contents is not None:
602 self.set_number(contents)
604 self._errorDisplay.push_exception()
606 def _on_sms_clicked(self, widget):
608 action = PhoneTypeSelector.ACTION_SEND_SMS
609 phoneNumber = self.get_number()
611 message = self._smsDialog.run(phoneNumber, (), self._window)
614 action = PhoneTypeSelector.ACTION_CANCEL
616 if action == PhoneTypeSelector.ACTION_CANCEL:
618 self.number_selected(action, phoneNumber, message)
620 self._errorDisplay.push_exception()
622 def _on_dial_clicked(self, widget):
624 action = PhoneTypeSelector.ACTION_DIAL
625 phoneNumber = self.get_number()
627 self.number_selected(action, phoneNumber, message)
629 self._errorDisplay.push_exception()
631 def _on_digit_clicked(self, widget):
633 self.set_number(self._phonenumber + widget.get_name()[-1])
635 self._errorDisplay.push_exception()
637 def _on_backspace(self, taps):
639 self.set_number(self._phonenumber[:-taps])
640 self._reset_back_button()
642 self._errorDisplay.push_exception()
644 def _on_clearall(self, taps):
647 self._reset_back_button()
649 self._errorDisplay.push_exception()
652 def _set_clear_button(self):
654 self._backButton.set_label("gtk-clear")
656 self._errorDisplay.push_exception()
658 def _reset_back_button(self):
660 self._backButton.set_label(self._originalLabel)
662 self._errorDisplay.push_exception()
665 class AccountInfo(object):
667 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
668 self._errorDisplay = errorDisplay
669 self._backend = backend
670 self._isPopulated = False
671 self._alarmHandler = alarmHandler
672 self._notifyOnMissed = False
673 self._notifyOnVoicemail = False
674 self._notifyOnSms = False
676 self._callbackList = []
677 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
678 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
679 self._onCallbackSelectChangedId = 0
681 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
682 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
683 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
684 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
685 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
686 self._onNotifyToggled = 0
687 self._onMinutesChanged = 0
688 self._onMissedToggled = 0
689 self._onVoicemailToggled = 0
690 self._onSmsToggled = 0
691 self._applyAlarmTimeoutId = None
693 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
694 self._defaultCallback = ""
697 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
699 self._accountViewNumberDisplay.set_use_markup(True)
700 self.set_account_number("")
702 del self._callbackList[:]
703 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
705 if self._alarmHandler is not None:
706 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
707 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
708 self._missedCheckbox.set_active(self._notifyOnMissed)
709 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
710 self._smsCheckbox.set_active(self._notifyOnSms)
712 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
713 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
714 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
715 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
716 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
718 self._notifyCheckbox.set_sensitive(False)
719 self._minutesEntryButton.set_sensitive(False)
720 self._missedCheckbox.set_sensitive(False)
721 self._voicemailCheckbox.set_sensitive(False)
722 self._smsCheckbox.set_sensitive(False)
724 self.update(force=True)
727 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
728 self._onCallbackSelectChangedId = 0
730 if self._alarmHandler is not None:
731 self._notifyCheckbox.disconnect(self._onNotifyToggled)
732 self._minutesEntryButton.disconnect(self._onMinutesChanged)
733 self._missedCheckbox.disconnect(self._onNotifyToggled)
734 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
735 self._smsCheckbox.disconnect(self._onNotifyToggled)
736 self._onNotifyToggled = 0
737 self._onMinutesChanged = 0
738 self._onMissedToggled = 0
739 self._onVoicemailToggled = 0
740 self._onSmsToggled = 0
742 self._notifyCheckbox.set_sensitive(True)
743 self._minutesEntryButton.set_sensitive(True)
744 self._missedCheckbox.set_sensitive(True)
745 self._voicemailCheckbox.set_sensitive(True)
746 self._smsCheckbox.set_sensitive(True)
749 del self._callbackList[:]
751 def get_selected_callback_number(self):
752 currentLabel = self._callbackSelectButton.get_label()
753 if currentLabel is not None:
754 return make_ugly(currentLabel)
758 def set_account_number(self, number):
760 Displays current account number
762 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
764 def update(self, force = False):
765 if not force and self._isPopulated:
767 self._populate_callback_combo()
768 self.set_account_number(self._backend.get_account_number())
772 self._callbackSelectButton.set_label("")
773 self.set_account_number("")
774 self._isPopulated = False
776 def save_everything(self):
777 raise NotImplementedError
781 return "Account Info"
783 def load_settings(self, config, section):
784 self._defaultCallback = config.get(section, "callback")
785 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
786 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
787 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
789 def save_settings(self, config, section):
791 @note Thread Agnostic
793 callback = self.get_selected_callback_number()
794 config.set(section, "callback", callback)
795 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
796 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
797 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
799 def _populate_callback_combo(self):
800 self._isPopulated = True
801 del self._callbackList[:]
803 callbackNumbers = self._backend.get_callback_numbers()
805 self._errorDisplay.push_exception()
806 self._isPopulated = False
809 for number, description in callbackNumbers.iteritems():
810 self._callbackList.append((make_pretty(number), description))
812 if not self.get_selected_callback_number():
813 self._set_callback_number(self._defaultCallback)
815 def _set_callback_number(self, number):
817 if not self._backend.is_valid_syntax(number) and 0 < len(number):
818 self._errorDisplay.push_message("%s is not a valid callback number" % number)
819 elif number == self._backend.get_callback_number():
820 _moduleLogger.warning(
821 "Callback number already is %s" % (
822 self._backend.get_callback_number(),
826 self._backend.set_callback_number(number)
827 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
828 make_pretty(number), make_pretty(self._backend.get_callback_number())
830 self._callbackSelectButton.set_label(make_pretty(number))
832 "Callback number set to %s" % (
833 self._backend.get_callback_number(),
837 self._errorDisplay.push_exception()
839 def _update_alarm_settings(self, recurrence):
841 isEnabled = self._notifyCheckbox.get_active()
842 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
843 self._alarmHandler.apply_settings(isEnabled, recurrence)
845 self.save_everything()
846 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
847 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
849 def _on_callbackentry_clicked(self, *args):
851 actualSelection = make_pretty(self.get_selected_callback_number())
854 (number, "%s (%s)" % (number, description))
855 for (number, description) in self._callbackList
857 defaultSelection = userOptions.get(actualSelection, actualSelection)
859 userSelection = hildonize.touch_selector_entry(
862 list(userOptions.itervalues()),
865 reversedUserOptions = dict(
866 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
868 selectedNumber = reversedUserOptions.get(userSelection, userSelection)
870 number = make_ugly(selectedNumber)
871 self._set_callback_number(number)
872 except RuntimeError, e:
873 _moduleLogger.exception("%s" % str(e))
875 self._errorDisplay.push_exception()
877 def _on_notify_toggled(self, *args):
879 if self._applyAlarmTimeoutId is not None:
880 gobject.source_remove(self._applyAlarmTimeoutId)
881 self._applyAlarmTimeoutId = None
882 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
884 self._errorDisplay.push_exception()
886 def _on_minutes_clicked(self, *args):
887 recurrenceChoices = [
903 actualSelection = self._alarmHandler.recurrence
905 closestSelectionIndex = 0
906 for i, possible in enumerate(recurrenceChoices):
907 if possible[0] <= actualSelection:
908 closestSelectionIndex = i
909 recurrenceIndex = hildonize.touch_selector(
912 (("%s" % m[1]) for m in recurrenceChoices),
913 closestSelectionIndex,
915 recurrence = recurrenceChoices[recurrenceIndex][0]
917 self._update_alarm_settings(recurrence)
918 except RuntimeError, e:
919 _moduleLogger.exception("%s" % str(e))
921 self._errorDisplay.push_exception()
923 def _on_apply_timeout(self, *args):
925 self._applyAlarmTimeoutId = None
927 self._update_alarm_settings(self._alarmHandler.recurrence)
929 self._errorDisplay.push_exception()
932 def _on_missed_toggled(self, *args):
934 self._notifyOnMissed = self._missedCheckbox.get_active()
935 self.save_everything()
937 self._errorDisplay.push_exception()
939 def _on_voicemail_toggled(self, *args):
941 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
942 self.save_everything()
944 self._errorDisplay.push_exception()
946 def _on_sms_toggled(self, *args):
948 self._notifyOnSms = self._smsCheckbox.get_active()
949 self.save_everything()
951 self._errorDisplay.push_exception()
954 class RecentCallsView(object):
961 def __init__(self, widgetTree, backend, errorDisplay):
962 self._errorDisplay = errorDisplay
963 self._backend = backend
965 self._isPopulated = False
966 self._recentmodel = gtk.ListStore(
967 gobject.TYPE_STRING, # number
968 gobject.TYPE_STRING, # date
969 gobject.TYPE_STRING, # action
970 gobject.TYPE_STRING, # from
972 self._recentview = widgetTree.get_widget("recentview")
973 self._recentviewselection = None
974 self._onRecentviewRowActivatedId = 0
976 textrenderer = gtk.CellRendererText()
977 textrenderer.set_property("yalign", 0)
978 self._dateColumn = gtk.TreeViewColumn("Date")
979 self._dateColumn.pack_start(textrenderer, expand=True)
980 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
982 textrenderer = gtk.CellRendererText()
983 textrenderer.set_property("yalign", 0)
984 self._actionColumn = gtk.TreeViewColumn("Action")
985 self._actionColumn.pack_start(textrenderer, expand=True)
986 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
988 textrenderer = gtk.CellRendererText()
989 textrenderer.set_property("yalign", 0)
990 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
991 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
992 self._numberColumn = gtk.TreeViewColumn("Number")
993 self._numberColumn.pack_start(textrenderer, expand=True)
994 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
996 textrenderer = gtk.CellRendererText()
997 textrenderer.set_property("yalign", 0)
998 hildonize.set_cell_thumb_selectable(textrenderer)
999 self._nameColumn = gtk.TreeViewColumn("From")
1000 self._nameColumn.pack_start(textrenderer, expand=True)
1001 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
1002 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1004 self._window = gtk_toolbox.find_parent_window(self._recentview)
1005 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1007 self._updateSink = gtk_toolbox.threaded_stage(
1009 self._idly_populate_recentview,
1010 gtk_toolbox.null_sink(),
1015 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1016 self._recentview.set_model(self._recentmodel)
1018 self._recentview.append_column(self._dateColumn)
1019 self._recentview.append_column(self._actionColumn)
1020 self._recentview.append_column(self._numberColumn)
1021 self._recentview.append_column(self._nameColumn)
1022 self._recentviewselection = self._recentview.get_selection()
1023 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
1025 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
1028 self._recentview.disconnect(self._onRecentviewRowActivatedId)
1032 self._recentview.remove_column(self._dateColumn)
1033 self._recentview.remove_column(self._actionColumn)
1034 self._recentview.remove_column(self._nameColumn)
1035 self._recentview.remove_column(self._numberColumn)
1036 self._recentview.set_model(None)
1038 def number_selected(self, action, number, message):
1040 @note Actual dial function is patched in later
1042 raise NotImplementedError("Horrible unknown error has occurred")
1044 def update(self, force = False):
1045 if not force and self._isPopulated:
1047 self._updateSink.send(())
1051 self._isPopulated = False
1052 self._recentmodel.clear()
1056 return "Recent Calls"
1058 def load_settings(self, config, section):
1061 def save_settings(self, config, section):
1063 @note Thread Agnostic
1067 def _idly_populate_recentview(self):
1068 with gtk_toolbox.gtk_lock():
1069 banner = hildonize.show_busy_banner_start(self._window, "Loading Recent History")
1071 self._recentmodel.clear()
1072 self._isPopulated = True
1075 recentItems = self._backend.get_recent()
1076 except Exception, e:
1077 self._errorDisplay.push_exception_with_lock()
1078 self._isPopulated = False
1082 gv_backend.decorate_recent(data)
1083 for data in gv_backend.sort_messages(recentItems)
1086 for personName, phoneNumber, date, action in recentItems:
1088 personName = "Unknown"
1089 date = abbrev_relative_date(date)
1090 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1091 prettyNumber = make_pretty(prettyNumber)
1092 item = (prettyNumber, date, action.capitalize(), personName)
1093 with gtk_toolbox.gtk_lock():
1094 self._recentmodel.append(item)
1095 except Exception, e:
1096 self._errorDisplay.push_exception_with_lock()
1098 with gtk_toolbox.gtk_lock():
1099 hildonize.show_busy_banner_end(banner)
1103 def _on_recentview_row_activated(self, treeview, path, view_column):
1105 model, itr = self._recentviewselection.get_selected()
1109 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1110 number = make_ugly(number)
1111 contactPhoneNumbers = [("Phone", number)]
1112 description = self._recentmodel.get_value(itr, self.FROM_IDX)
1114 action, phoneNumber, message = self._phoneTypeSelector.run(
1115 contactPhoneNumbers,
1116 messages = (description, ),
1117 parent = self._window,
1119 if action == PhoneTypeSelector.ACTION_CANCEL:
1121 assert phoneNumber, "A lack of phone number exists"
1123 self.number_selected(action, phoneNumber, message)
1124 self._recentviewselection.unselect_all()
1125 except Exception, e:
1126 self._errorDisplay.push_exception()
1129 class MessagesView(object):
1137 def __init__(self, widgetTree, backend, errorDisplay):
1138 self._errorDisplay = errorDisplay
1139 self._backend = backend
1141 self._isPopulated = False
1142 self._messagemodel = gtk.ListStore(
1143 gobject.TYPE_STRING, # number
1144 gobject.TYPE_STRING, # date
1145 gobject.TYPE_STRING, # header
1146 gobject.TYPE_STRING, # message
1149 self._messageview = widgetTree.get_widget("messages_view")
1150 self._messageviewselection = None
1151 self._onMessageviewRowActivatedId = 0
1153 self._messageRenderer = gtk.CellRendererText()
1154 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1155 self._messageRenderer.set_property("wrap-width", 500)
1156 self._messageColumn = gtk.TreeViewColumn("Messages")
1157 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1158 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1159 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1161 self._window = gtk_toolbox.find_parent_window(self._messageview)
1162 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1164 self._updateSink = gtk_toolbox.threaded_stage(
1166 self._idly_populate_messageview,
1167 gtk_toolbox.null_sink(),
1172 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1173 self._messageview.set_model(self._messagemodel)
1174 self._messageview.set_headers_visible(False)
1176 self._messageview.append_column(self._messageColumn)
1177 self._messageviewselection = self._messageview.get_selection()
1178 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1180 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1183 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1187 self._messageview.remove_column(self._messageColumn)
1188 self._messageview.set_model(None)
1190 def number_selected(self, action, number, message):
1192 @note Actual dial function is patched in later
1194 raise NotImplementedError("Horrible unknown error has occurred")
1196 def update(self, force = False):
1197 if not force and self._isPopulated:
1199 self._updateSink.send(())
1203 self._isPopulated = False
1204 self._messagemodel.clear()
1210 def load_settings(self, config, section):
1213 def save_settings(self, config, section):
1215 @note Thread Agnostic
1219 _MIN_MESSAGES_SHOWN = 4
1221 def _idly_populate_messageview(self):
1222 with gtk_toolbox.gtk_lock():
1223 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1225 self._messagemodel.clear()
1226 self._isPopulated = True
1229 messageItems = self._backend.get_messages()
1230 except Exception, e:
1231 self._errorDisplay.push_exception_with_lock()
1232 self._isPopulated = False
1236 gv_backend.decorate_message(message)
1237 for message in gv_backend.sort_messages(messageItems)
1240 for header, number, relativeDate, messages in messageItems:
1241 prettyNumber = number[2:] if number.startswith("+1") else number
1242 prettyNumber = make_pretty(prettyNumber)
1244 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1245 expandedMessages = [firstMessage]
1246 expandedMessages.extend(messages)
1247 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1248 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1249 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1250 collapsedMessages = [firstMessage, secondMessage]
1251 collapsedMessages.extend(messages[-self._MIN_MESSAGES_SHOWN-1:-1])
1253 collapsedMessages = expandedMessages
1255 number = make_ugly(number)
1257 row = (number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages)
1258 with gtk_toolbox.gtk_lock():
1259 self._messagemodel.append(row)
1260 except Exception, e:
1261 self._errorDisplay.push_exception_with_lock()
1263 with gtk_toolbox.gtk_lock():
1264 hildonize.show_busy_banner_end(banner)
1268 def _on_messageview_row_activated(self, treeview, path, view_column):
1270 model, itr = self._messageviewselection.get_selected()
1274 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1275 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1277 action, phoneNumber, message = self._phoneTypeSelector.run(
1278 contactPhoneNumbers,
1279 messages = description,
1280 parent = self._window,
1282 if action == PhoneTypeSelector.ACTION_CANCEL:
1284 assert phoneNumber, "A lock of phone number exists"
1286 self.number_selected(action, phoneNumber, message)
1287 self._messageviewselection.unselect_all()
1288 except Exception, e:
1289 self._errorDisplay.push_exception()
1292 class ContactsView(object):
1294 def __init__(self, widgetTree, backend, errorDisplay):
1295 self._errorDisplay = errorDisplay
1296 self._backend = backend
1298 self._addressBook = None
1299 self._selectedComboIndex = 0
1300 self._addressBookFactories = [null_backend.NullAddressBook()]
1302 self._booksList = []
1303 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1305 self._isPopulated = False
1306 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1307 self._contactsviewselection = None
1308 self._contactsview = widgetTree.get_widget("contactsview")
1310 self._contactColumn = gtk.TreeViewColumn("Contact")
1311 displayContactSource = False
1312 if displayContactSource:
1313 textrenderer = gtk.CellRendererText()
1314 self._contactColumn.pack_start(textrenderer, expand=False)
1315 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1316 textrenderer = gtk.CellRendererText()
1317 hildonize.set_cell_thumb_selectable(textrenderer)
1318 self._contactColumn.pack_start(textrenderer, expand=True)
1319 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1320 textrenderer = gtk.CellRendererText()
1321 self._contactColumn.pack_start(textrenderer, expand=True)
1322 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1323 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1324 self._contactColumn.set_sort_column_id(1)
1325 self._contactColumn.set_visible(True)
1327 self._onContactsviewRowActivatedId = 0
1328 self._onAddressbookButtonChangedId = 0
1329 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1330 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1332 self._updateSink = gtk_toolbox.threaded_stage(
1334 self._idly_populate_contactsview,
1335 gtk_toolbox.null_sink(),
1340 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1342 self._contactsview.set_model(self._contactsmodel)
1343 self._contactsview.append_column(self._contactColumn)
1344 self._contactsviewselection = self._contactsview.get_selection()
1345 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1347 del self._booksList[:]
1348 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1349 if factoryName and bookName:
1350 entryName = "%s: %s" % (factoryName, bookName)
1352 entryName = factoryName
1354 entryName = bookName
1356 entryName = "Bad name (%d)" % factoryId
1357 row = (str(factoryId), bookId, entryName)
1358 self._booksList.append(row)
1360 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1361 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1363 if len(self._booksList) <= self._selectedComboIndex:
1364 self._selectedComboIndex = 0
1365 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1367 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1368 selectedBookId = self._booksList[self._selectedComboIndex][1]
1369 self.open_addressbook(selectedFactoryId, selectedBookId)
1372 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1373 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1377 self._bookSelectionButton.set_label("")
1378 self._contactsview.set_model(None)
1379 self._contactsview.remove_column(self._contactColumn)
1381 def number_selected(self, action, number, message):
1383 @note Actual dial function is patched in later
1385 raise NotImplementedError("Horrible unknown error has occurred")
1387 def get_addressbooks(self):
1389 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1391 for i, factory in enumerate(self._addressBookFactories):
1392 for bookFactory, bookId, bookName in factory.get_addressbooks():
1393 yield (str(i), bookId), (factory.factory_name(), bookName)
1395 def open_addressbook(self, bookFactoryId, bookId):
1396 bookFactoryIndex = int(bookFactoryId)
1397 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1399 forceUpdate = True if addressBook is not self._addressBook else False
1401 self._addressBook = addressBook
1402 self.update(force=forceUpdate)
1404 def update(self, force = False):
1405 if not force and self._isPopulated:
1407 self._updateSink.send(())
1411 self._isPopulated = False
1412 self._contactsmodel.clear()
1413 for factory in self._addressBookFactories:
1414 factory.clear_caches()
1415 self._addressBook.clear_caches()
1417 def append(self, book):
1418 self._addressBookFactories.append(book)
1420 def extend(self, books):
1421 self._addressBookFactories.extend(books)
1427 def load_settings(self, config, sectionName):
1429 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1430 except ConfigParser.NoOptionError:
1431 self._selectedComboIndex = 0
1433 def save_settings(self, config, sectionName):
1434 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1436 def _idly_populate_contactsview(self):
1437 with gtk_toolbox.gtk_lock():
1438 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1441 while addressBook is not self._addressBook:
1442 addressBook = self._addressBook
1443 with gtk_toolbox.gtk_lock():
1444 self._contactsview.set_model(None)
1448 contacts = addressBook.get_contacts()
1449 except Exception, e:
1451 self._isPopulated = False
1452 self._errorDisplay.push_exception_with_lock()
1453 for contactId, contactName in contacts:
1454 contactType = (addressBook.contact_source_short_name(contactId), )
1455 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1457 with gtk_toolbox.gtk_lock():
1458 self._contactsview.set_model(self._contactsmodel)
1460 self._isPopulated = True
1461 except Exception, e:
1462 self._errorDisplay.push_exception_with_lock()
1464 with gtk_toolbox.gtk_lock():
1465 hildonize.show_busy_banner_end(banner)
1468 def _on_addressbook_button_changed(self, *args, **kwds):
1471 newSelectedComboIndex = hildonize.touch_selector(
1474 (("%s" % m[2]) for m in self._booksList),
1475 self._selectedComboIndex,
1477 except RuntimeError:
1480 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1481 selectedBookId = self._booksList[newSelectedComboIndex][1]
1482 self.open_addressbook(selectedFactoryId, selectedBookId)
1483 self._selectedComboIndex = newSelectedComboIndex
1484 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1485 except Exception, e:
1486 self._errorDisplay.push_exception()
1488 def _on_contactsview_row_activated(self, treeview, path, view_column):
1490 model, itr = self._contactsviewselection.get_selected()
1494 contactId = self._contactsmodel.get_value(itr, 3)
1495 contactName = self._contactsmodel.get_value(itr, 1)
1497 contactDetails = self._addressBook.get_contact_details(contactId)
1498 except Exception, e:
1500 self._errorDisplay.push_exception()
1501 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1503 if len(contactPhoneNumbers) == 0:
1506 action, phoneNumber, message = self._phoneTypeSelector.run(
1507 contactPhoneNumbers,
1508 messages = (contactName, ),
1509 parent = self._window,
1511 if action == PhoneTypeSelector.ACTION_CANCEL:
1513 assert phoneNumber, "A lack of phone number exists"
1515 self.number_selected(action, phoneNumber, message)
1516 self._contactsviewselection.unselect_all()
1517 except Exception, e:
1518 self._errorDisplay.push_exception()