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 def _idly_populate_messageview(self):
1220 with gtk_toolbox.gtk_lock():
1221 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1223 self._messagemodel.clear()
1224 self._isPopulated = True
1227 messageItems = self._backend.get_messages()
1228 except Exception, e:
1229 self._errorDisplay.push_exception_with_lock()
1230 self._isPopulated = False
1234 gv_backend.decorate_message(message)
1235 for message in gv_backend.sort_messages(messageItems)
1238 for header, number, relativeDate, messages in messageItems:
1239 prettyNumber = number[2:] if number.startswith("+1") else number
1240 prettyNumber = make_pretty(prettyNumber)
1242 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1243 newMessages = [firstMessage]
1244 newMessages.extend(messages)
1246 number = make_ugly(number)
1248 row = (number, relativeDate, header, "\n".join(newMessages), newMessages)
1249 with gtk_toolbox.gtk_lock():
1250 self._messagemodel.append(row)
1251 except Exception, e:
1252 self._errorDisplay.push_exception_with_lock()
1254 with gtk_toolbox.gtk_lock():
1255 hildonize.show_busy_banner_end(banner)
1259 def _on_messageview_row_activated(self, treeview, path, view_column):
1261 model, itr = self._messageviewselection.get_selected()
1265 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1266 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1268 action, phoneNumber, message = self._phoneTypeSelector.run(
1269 contactPhoneNumbers,
1270 messages = description,
1271 parent = self._window,
1273 if action == PhoneTypeSelector.ACTION_CANCEL:
1275 assert phoneNumber, "A lock of phone number exists"
1277 self.number_selected(action, phoneNumber, message)
1278 self._messageviewselection.unselect_all()
1279 except Exception, e:
1280 self._errorDisplay.push_exception()
1283 class ContactsView(object):
1285 def __init__(self, widgetTree, backend, errorDisplay):
1286 self._errorDisplay = errorDisplay
1287 self._backend = backend
1289 self._addressBook = None
1290 self._selectedComboIndex = 0
1291 self._addressBookFactories = [null_backend.NullAddressBook()]
1293 self._booksList = []
1294 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1296 self._isPopulated = False
1297 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1298 self._contactsviewselection = None
1299 self._contactsview = widgetTree.get_widget("contactsview")
1301 self._contactColumn = gtk.TreeViewColumn("Contact")
1302 displayContactSource = False
1303 if displayContactSource:
1304 textrenderer = gtk.CellRendererText()
1305 self._contactColumn.pack_start(textrenderer, expand=False)
1306 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1307 textrenderer = gtk.CellRendererText()
1308 hildonize.set_cell_thumb_selectable(textrenderer)
1309 self._contactColumn.pack_start(textrenderer, expand=True)
1310 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1311 textrenderer = gtk.CellRendererText()
1312 self._contactColumn.pack_start(textrenderer, expand=True)
1313 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1314 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1315 self._contactColumn.set_sort_column_id(1)
1316 self._contactColumn.set_visible(True)
1318 self._onContactsviewRowActivatedId = 0
1319 self._onAddressbookButtonChangedId = 0
1320 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1321 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1323 self._updateSink = gtk_toolbox.threaded_stage(
1325 self._idly_populate_contactsview,
1326 gtk_toolbox.null_sink(),
1331 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1333 self._contactsview.set_model(self._contactsmodel)
1334 self._contactsview.append_column(self._contactColumn)
1335 self._contactsviewselection = self._contactsview.get_selection()
1336 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1338 del self._booksList[:]
1339 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1340 if factoryName and bookName:
1341 entryName = "%s: %s" % (factoryName, bookName)
1343 entryName = factoryName
1345 entryName = bookName
1347 entryName = "Bad name (%d)" % factoryId
1348 row = (str(factoryId), bookId, entryName)
1349 self._booksList.append(row)
1351 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1352 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1354 if len(self._booksList) <= self._selectedComboIndex:
1355 self._selectedComboIndex = 0
1356 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1358 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1359 selectedBookId = self._booksList[self._selectedComboIndex][1]
1360 self.open_addressbook(selectedFactoryId, selectedBookId)
1363 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1364 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1368 self._bookSelectionButton.set_label("")
1369 self._contactsview.set_model(None)
1370 self._contactsview.remove_column(self._contactColumn)
1372 def number_selected(self, action, number, message):
1374 @note Actual dial function is patched in later
1376 raise NotImplementedError("Horrible unknown error has occurred")
1378 def get_addressbooks(self):
1380 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1382 for i, factory in enumerate(self._addressBookFactories):
1383 for bookFactory, bookId, bookName in factory.get_addressbooks():
1384 yield (str(i), bookId), (factory.factory_name(), bookName)
1386 def open_addressbook(self, bookFactoryId, bookId):
1387 bookFactoryIndex = int(bookFactoryId)
1388 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1390 forceUpdate = True if addressBook is not self._addressBook else False
1392 self._addressBook = addressBook
1393 self.update(force=forceUpdate)
1395 def update(self, force = False):
1396 if not force and self._isPopulated:
1398 self._updateSink.send(())
1402 self._isPopulated = False
1403 self._contactsmodel.clear()
1404 for factory in self._addressBookFactories:
1405 factory.clear_caches()
1406 self._addressBook.clear_caches()
1408 def append(self, book):
1409 self._addressBookFactories.append(book)
1411 def extend(self, books):
1412 self._addressBookFactories.extend(books)
1418 def load_settings(self, config, sectionName):
1420 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1421 except ConfigParser.NoOptionError:
1422 self._selectedComboIndex = 0
1424 def save_settings(self, config, sectionName):
1425 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1427 def _idly_populate_contactsview(self):
1428 with gtk_toolbox.gtk_lock():
1429 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1432 while addressBook is not self._addressBook:
1433 addressBook = self._addressBook
1434 with gtk_toolbox.gtk_lock():
1435 self._contactsview.set_model(None)
1439 contacts = addressBook.get_contacts()
1440 except Exception, e:
1442 self._isPopulated = False
1443 self._errorDisplay.push_exception_with_lock()
1444 for contactId, contactName in contacts:
1445 contactType = (addressBook.contact_source_short_name(contactId), )
1446 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1448 with gtk_toolbox.gtk_lock():
1449 self._contactsview.set_model(self._contactsmodel)
1451 self._isPopulated = True
1452 except Exception, e:
1453 self._errorDisplay.push_exception_with_lock()
1455 with gtk_toolbox.gtk_lock():
1456 hildonize.show_busy_banner_end(banner)
1459 def _on_addressbook_button_changed(self, *args, **kwds):
1462 newSelectedComboIndex = hildonize.touch_selector(
1465 (("%s" % m[2]) for m in self._booksList),
1466 self._selectedComboIndex,
1468 except RuntimeError:
1471 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1472 selectedBookId = self._booksList[newSelectedComboIndex][1]
1473 self.open_addressbook(selectedFactoryId, selectedBookId)
1474 self._selectedComboIndex = newSelectedComboIndex
1475 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1476 except Exception, e:
1477 self._errorDisplay.push_exception()
1479 def _on_contactsview_row_activated(self, treeview, path, view_column):
1481 model, itr = self._contactsviewselection.get_selected()
1485 contactId = self._contactsmodel.get_value(itr, 3)
1486 contactName = self._contactsmodel.get_value(itr, 1)
1488 contactDetails = self._addressBook.get_contact_details(contactId)
1489 except Exception, e:
1491 self._errorDisplay.push_exception()
1492 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1494 if len(contactPhoneNumbers) == 0:
1497 action, phoneNumber, message = self._phoneTypeSelector.run(
1498 contactPhoneNumbers,
1499 messages = (contactName, ),
1500 parent = self._window,
1502 if action == PhoneTypeSelector.ACTION_CANCEL:
1504 assert phoneNumber, "A lack of phone number exists"
1506 self.number_selected(action, phoneNumber, message)
1507 self._contactsviewselection.unselect_all()
1508 except Exception, e:
1509 self._errorDisplay.push_exception()