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("No Callback Number")
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 if len(callbackNumbers) == 0:
810 callbackNumbers = {"": "No callback numbers available"}
812 for number, description in callbackNumbers.iteritems():
813 self._callbackList.append((make_pretty(number), description))
815 self._set_callback_number(self._defaultCallback)
817 def _set_callback_number(self, number):
819 if not self._backend.is_valid_syntax(number) and 0 < len(number):
820 self._errorDisplay.push_message("%s is not a valid callback number" % number)
821 elif number == self._backend.get_callback_number() and 0 < len(number):
822 _moduleLogger.warning(
823 "Callback number already is %s" % (
824 self._backend.get_callback_number(),
828 self._backend.set_callback_number(number)
829 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
830 make_pretty(number), make_pretty(self._backend.get_callback_number())
832 prettyNumber = make_pretty(number)
833 if len(prettyNumber) == 0:
834 prettyNumber = "No Callback Number"
835 self._callbackSelectButton.set_label(prettyNumber)
837 "Callback number set to %s" % (
838 self._backend.get_callback_number(),
842 self._errorDisplay.push_exception()
844 def _update_alarm_settings(self, recurrence):
846 isEnabled = self._notifyCheckbox.get_active()
847 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
848 self._alarmHandler.apply_settings(isEnabled, recurrence)
850 self.save_everything()
851 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
852 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
854 def _on_callbackentry_clicked(self, *args):
856 actualSelection = make_pretty(self.get_selected_callback_number())
859 (number, "%s (%s)" % (number, description))
860 for (number, description) in self._callbackList
862 defaultSelection = userOptions.get(actualSelection, actualSelection)
864 userSelection = hildonize.touch_selector_entry(
867 list(userOptions.itervalues()),
870 reversedUserOptions = dict(
871 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
873 selectedNumber = reversedUserOptions.get(userSelection, userSelection)
875 number = make_ugly(selectedNumber)
876 self._set_callback_number(number)
877 except RuntimeError, e:
878 _moduleLogger.exception("%s" % str(e))
880 self._errorDisplay.push_exception()
882 def _on_notify_toggled(self, *args):
884 if self._applyAlarmTimeoutId is not None:
885 gobject.source_remove(self._applyAlarmTimeoutId)
886 self._applyAlarmTimeoutId = None
887 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
889 self._errorDisplay.push_exception()
891 def _on_minutes_clicked(self, *args):
892 recurrenceChoices = [
908 actualSelection = self._alarmHandler.recurrence
910 closestSelectionIndex = 0
911 for i, possible in enumerate(recurrenceChoices):
912 if possible[0] <= actualSelection:
913 closestSelectionIndex = i
914 recurrenceIndex = hildonize.touch_selector(
917 (("%s" % m[1]) for m in recurrenceChoices),
918 closestSelectionIndex,
920 recurrence = recurrenceChoices[recurrenceIndex][0]
922 self._update_alarm_settings(recurrence)
923 except RuntimeError, e:
924 _moduleLogger.exception("%s" % str(e))
926 self._errorDisplay.push_exception()
928 def _on_apply_timeout(self, *args):
930 self._applyAlarmTimeoutId = None
932 self._update_alarm_settings(self._alarmHandler.recurrence)
934 self._errorDisplay.push_exception()
937 def _on_missed_toggled(self, *args):
939 self._notifyOnMissed = self._missedCheckbox.get_active()
940 self.save_everything()
942 self._errorDisplay.push_exception()
944 def _on_voicemail_toggled(self, *args):
946 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
947 self.save_everything()
949 self._errorDisplay.push_exception()
951 def _on_sms_toggled(self, *args):
953 self._notifyOnSms = self._smsCheckbox.get_active()
954 self.save_everything()
956 self._errorDisplay.push_exception()
959 class RecentCallsView(object):
966 def __init__(self, widgetTree, backend, errorDisplay):
967 self._errorDisplay = errorDisplay
968 self._backend = backend
970 self._isPopulated = False
971 self._recentmodel = gtk.ListStore(
972 gobject.TYPE_STRING, # number
973 gobject.TYPE_STRING, # date
974 gobject.TYPE_STRING, # action
975 gobject.TYPE_STRING, # from
977 self._recentview = widgetTree.get_widget("recentview")
978 self._recentviewselection = None
979 self._onRecentviewRowActivatedId = 0
981 textrenderer = gtk.CellRendererText()
982 textrenderer.set_property("yalign", 0)
983 self._dateColumn = gtk.TreeViewColumn("Date")
984 self._dateColumn.pack_start(textrenderer, expand=True)
985 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
987 textrenderer = gtk.CellRendererText()
988 textrenderer.set_property("yalign", 0)
989 self._actionColumn = gtk.TreeViewColumn("Action")
990 self._actionColumn.pack_start(textrenderer, expand=True)
991 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
993 textrenderer = gtk.CellRendererText()
994 textrenderer.set_property("yalign", 0)
995 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
996 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
997 self._numberColumn = gtk.TreeViewColumn("Number")
998 self._numberColumn.pack_start(textrenderer, expand=True)
999 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
1001 textrenderer = gtk.CellRendererText()
1002 textrenderer.set_property("yalign", 0)
1003 hildonize.set_cell_thumb_selectable(textrenderer)
1004 self._nameColumn = gtk.TreeViewColumn("From")
1005 self._nameColumn.pack_start(textrenderer, expand=True)
1006 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
1007 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1009 self._window = gtk_toolbox.find_parent_window(self._recentview)
1010 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1012 self._updateSink = gtk_toolbox.threaded_stage(
1014 self._idly_populate_recentview,
1015 gtk_toolbox.null_sink(),
1020 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1021 self._recentview.set_model(self._recentmodel)
1023 self._recentview.append_column(self._dateColumn)
1024 self._recentview.append_column(self._actionColumn)
1025 self._recentview.append_column(self._numberColumn)
1026 self._recentview.append_column(self._nameColumn)
1027 self._recentviewselection = self._recentview.get_selection()
1028 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
1030 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
1033 self._recentview.disconnect(self._onRecentviewRowActivatedId)
1037 self._recentview.remove_column(self._dateColumn)
1038 self._recentview.remove_column(self._actionColumn)
1039 self._recentview.remove_column(self._nameColumn)
1040 self._recentview.remove_column(self._numberColumn)
1041 self._recentview.set_model(None)
1043 def number_selected(self, action, number, message):
1045 @note Actual dial function is patched in later
1047 raise NotImplementedError("Horrible unknown error has occurred")
1049 def update(self, force = False):
1050 if not force and self._isPopulated:
1052 self._updateSink.send(())
1056 self._isPopulated = False
1057 self._recentmodel.clear()
1061 return "Recent Calls"
1063 def load_settings(self, config, section):
1066 def save_settings(self, config, section):
1068 @note Thread Agnostic
1072 def _idly_populate_recentview(self):
1073 with gtk_toolbox.gtk_lock():
1074 banner = hildonize.show_busy_banner_start(self._window, "Loading Recent History")
1076 self._recentmodel.clear()
1077 self._isPopulated = True
1080 recentItems = self._backend.get_recent()
1081 except Exception, e:
1082 self._errorDisplay.push_exception_with_lock()
1083 self._isPopulated = False
1087 gv_backend.decorate_recent(data)
1088 for data in gv_backend.sort_messages(recentItems)
1091 for personName, phoneNumber, date, action in recentItems:
1093 personName = "Unknown"
1094 date = abbrev_relative_date(date)
1095 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1096 prettyNumber = make_pretty(prettyNumber)
1097 item = (prettyNumber, date, action.capitalize(), personName)
1098 with gtk_toolbox.gtk_lock():
1099 self._recentmodel.append(item)
1100 except Exception, e:
1101 self._errorDisplay.push_exception_with_lock()
1103 with gtk_toolbox.gtk_lock():
1104 hildonize.show_busy_banner_end(banner)
1108 def _on_recentview_row_activated(self, treeview, path, view_column):
1110 model, itr = self._recentviewselection.get_selected()
1114 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1115 number = make_ugly(number)
1116 contactPhoneNumbers = [("Phone", number)]
1117 description = self._recentmodel.get_value(itr, self.FROM_IDX)
1119 action, phoneNumber, message = self._phoneTypeSelector.run(
1120 contactPhoneNumbers,
1121 messages = (description, ),
1122 parent = self._window,
1124 if action == PhoneTypeSelector.ACTION_CANCEL:
1126 assert phoneNumber, "A lack of phone number exists"
1128 self.number_selected(action, phoneNumber, message)
1129 self._recentviewselection.unselect_all()
1130 except Exception, e:
1131 self._errorDisplay.push_exception()
1134 class MessagesView(object):
1142 def __init__(self, widgetTree, backend, errorDisplay):
1143 self._errorDisplay = errorDisplay
1144 self._backend = backend
1146 self._isPopulated = False
1147 self._messagemodel = gtk.ListStore(
1148 gobject.TYPE_STRING, # number
1149 gobject.TYPE_STRING, # date
1150 gobject.TYPE_STRING, # header
1151 gobject.TYPE_STRING, # message
1154 self._messageview = widgetTree.get_widget("messages_view")
1155 self._messageviewselection = None
1156 self._onMessageviewRowActivatedId = 0
1158 self._messageRenderer = gtk.CellRendererText()
1159 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1160 self._messageRenderer.set_property("wrap-width", 500)
1161 self._messageColumn = gtk.TreeViewColumn("Messages")
1162 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1163 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1164 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1166 self._window = gtk_toolbox.find_parent_window(self._messageview)
1167 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1169 self._updateSink = gtk_toolbox.threaded_stage(
1171 self._idly_populate_messageview,
1172 gtk_toolbox.null_sink(),
1177 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1178 self._messageview.set_model(self._messagemodel)
1179 self._messageview.set_headers_visible(False)
1181 self._messageview.append_column(self._messageColumn)
1182 self._messageviewselection = self._messageview.get_selection()
1183 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1185 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1188 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1192 self._messageview.remove_column(self._messageColumn)
1193 self._messageview.set_model(None)
1195 def number_selected(self, action, number, message):
1197 @note Actual dial function is patched in later
1199 raise NotImplementedError("Horrible unknown error has occurred")
1201 def update(self, force = False):
1202 if not force and self._isPopulated:
1204 self._updateSink.send(())
1208 self._isPopulated = False
1209 self._messagemodel.clear()
1215 def load_settings(self, config, section):
1218 def save_settings(self, config, section):
1220 @note Thread Agnostic
1224 _MIN_MESSAGES_SHOWN = 4
1226 def _idly_populate_messageview(self):
1227 with gtk_toolbox.gtk_lock():
1228 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1230 self._messagemodel.clear()
1231 self._isPopulated = True
1234 messageItems = self._backend.get_messages()
1235 except Exception, e:
1236 self._errorDisplay.push_exception_with_lock()
1237 self._isPopulated = False
1241 gv_backend.decorate_message(message)
1242 for message in gv_backend.sort_messages(messageItems)
1245 for header, number, relativeDate, messages in messageItems:
1246 prettyNumber = number[2:] if number.startswith("+1") else number
1247 prettyNumber = make_pretty(prettyNumber)
1249 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1250 expandedMessages = [firstMessage]
1251 expandedMessages.extend(messages)
1252 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1253 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1254 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1255 collapsedMessages = [firstMessage, secondMessage]
1256 collapsedMessages.extend(messages[-self._MIN_MESSAGES_SHOWN-1:-1])
1258 collapsedMessages = expandedMessages
1260 number = make_ugly(number)
1262 row = (number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages)
1263 with gtk_toolbox.gtk_lock():
1264 self._messagemodel.append(row)
1265 except Exception, e:
1266 self._errorDisplay.push_exception_with_lock()
1268 with gtk_toolbox.gtk_lock():
1269 hildonize.show_busy_banner_end(banner)
1273 def _on_messageview_row_activated(self, treeview, path, view_column):
1275 model, itr = self._messageviewselection.get_selected()
1279 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1280 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1282 action, phoneNumber, message = self._phoneTypeSelector.run(
1283 contactPhoneNumbers,
1284 messages = description,
1285 parent = self._window,
1287 if action == PhoneTypeSelector.ACTION_CANCEL:
1289 assert phoneNumber, "A lock of phone number exists"
1291 self.number_selected(action, phoneNumber, message)
1292 self._messageviewselection.unselect_all()
1293 except Exception, e:
1294 self._errorDisplay.push_exception()
1297 class ContactsView(object):
1299 def __init__(self, widgetTree, backend, errorDisplay):
1300 self._errorDisplay = errorDisplay
1301 self._backend = backend
1303 self._addressBook = None
1304 self._selectedComboIndex = 0
1305 self._addressBookFactories = [null_backend.NullAddressBook()]
1307 self._booksList = []
1308 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1310 self._isPopulated = False
1311 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1312 self._contactsviewselection = None
1313 self._contactsview = widgetTree.get_widget("contactsview")
1315 self._contactColumn = gtk.TreeViewColumn("Contact")
1316 displayContactSource = False
1317 if displayContactSource:
1318 textrenderer = gtk.CellRendererText()
1319 self._contactColumn.pack_start(textrenderer, expand=False)
1320 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1321 textrenderer = gtk.CellRendererText()
1322 hildonize.set_cell_thumb_selectable(textrenderer)
1323 self._contactColumn.pack_start(textrenderer, expand=True)
1324 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1325 textrenderer = gtk.CellRendererText()
1326 self._contactColumn.pack_start(textrenderer, expand=True)
1327 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1328 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1329 self._contactColumn.set_sort_column_id(1)
1330 self._contactColumn.set_visible(True)
1332 self._onContactsviewRowActivatedId = 0
1333 self._onAddressbookButtonChangedId = 0
1334 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1335 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1337 self._updateSink = gtk_toolbox.threaded_stage(
1339 self._idly_populate_contactsview,
1340 gtk_toolbox.null_sink(),
1345 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1347 self._contactsview.set_model(self._contactsmodel)
1348 self._contactsview.append_column(self._contactColumn)
1349 self._contactsviewselection = self._contactsview.get_selection()
1350 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1352 del self._booksList[:]
1353 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1354 if factoryName and bookName:
1355 entryName = "%s: %s" % (factoryName, bookName)
1357 entryName = factoryName
1359 entryName = bookName
1361 entryName = "Bad name (%d)" % factoryId
1362 row = (str(factoryId), bookId, entryName)
1363 self._booksList.append(row)
1365 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1366 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1368 if len(self._booksList) <= self._selectedComboIndex:
1369 self._selectedComboIndex = 0
1370 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1372 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1373 selectedBookId = self._booksList[self._selectedComboIndex][1]
1374 self.open_addressbook(selectedFactoryId, selectedBookId)
1377 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1378 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1382 self._bookSelectionButton.set_label("")
1383 self._contactsview.set_model(None)
1384 self._contactsview.remove_column(self._contactColumn)
1386 def number_selected(self, action, number, message):
1388 @note Actual dial function is patched in later
1390 raise NotImplementedError("Horrible unknown error has occurred")
1392 def get_addressbooks(self):
1394 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1396 for i, factory in enumerate(self._addressBookFactories):
1397 for bookFactory, bookId, bookName in factory.get_addressbooks():
1398 yield (str(i), bookId), (factory.factory_name(), bookName)
1400 def open_addressbook(self, bookFactoryId, bookId):
1401 bookFactoryIndex = int(bookFactoryId)
1402 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1404 forceUpdate = True if addressBook is not self._addressBook else False
1406 self._addressBook = addressBook
1407 self.update(force=forceUpdate)
1409 def update(self, force = False):
1410 if not force and self._isPopulated:
1412 self._updateSink.send(())
1416 self._isPopulated = False
1417 self._contactsmodel.clear()
1418 for factory in self._addressBookFactories:
1419 factory.clear_caches()
1420 self._addressBook.clear_caches()
1422 def append(self, book):
1423 self._addressBookFactories.append(book)
1425 def extend(self, books):
1426 self._addressBookFactories.extend(books)
1432 def load_settings(self, config, sectionName):
1434 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1435 except ConfigParser.NoOptionError:
1436 self._selectedComboIndex = 0
1438 def save_settings(self, config, sectionName):
1439 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1441 def _idly_populate_contactsview(self):
1442 with gtk_toolbox.gtk_lock():
1443 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1446 while addressBook is not self._addressBook:
1447 addressBook = self._addressBook
1448 with gtk_toolbox.gtk_lock():
1449 self._contactsview.set_model(None)
1453 contacts = addressBook.get_contacts()
1454 except Exception, e:
1456 self._isPopulated = False
1457 self._errorDisplay.push_exception_with_lock()
1458 for contactId, contactName in contacts:
1459 contactType = (addressBook.contact_source_short_name(contactId), )
1460 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1462 with gtk_toolbox.gtk_lock():
1463 self._contactsview.set_model(self._contactsmodel)
1465 self._isPopulated = True
1466 except Exception, e:
1467 self._errorDisplay.push_exception_with_lock()
1469 with gtk_toolbox.gtk_lock():
1470 hildonize.show_busy_banner_end(banner)
1473 def _on_addressbook_button_changed(self, *args, **kwds):
1476 newSelectedComboIndex = hildonize.touch_selector(
1479 (("%s" % m[2]) for m in self._booksList),
1480 self._selectedComboIndex,
1482 except RuntimeError:
1485 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1486 selectedBookId = self._booksList[newSelectedComboIndex][1]
1487 self.open_addressbook(selectedFactoryId, selectedBookId)
1488 self._selectedComboIndex = newSelectedComboIndex
1489 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1490 except Exception, e:
1491 self._errorDisplay.push_exception()
1493 def _on_contactsview_row_activated(self, treeview, path, view_column):
1495 model, itr = self._contactsviewselection.get_selected()
1499 contactId = self._contactsmodel.get_value(itr, 3)
1500 contactName = self._contactsmodel.get_value(itr, 1)
1502 contactDetails = self._addressBook.get_contact_details(contactId)
1503 except Exception, e:
1505 self._errorDisplay.push_exception()
1506 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1508 if len(contactPhoneNumbers) == 0:
1511 action, phoneNumber, message = self._phoneTypeSelector.run(
1512 contactPhoneNumbers,
1513 messages = (contactName, ),
1514 parent = self._window,
1516 if action == PhoneTypeSelector.ACTION_CANCEL:
1518 assert phoneNumber, "A lack of phone number exists"
1520 self.number_selected(action, phoneNumber, message)
1521 self._contactsviewselection.unselect_all()
1522 except Exception, e:
1523 self._errorDisplay.push_exception()