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)
22 @todo Get descriptions with the callback number
25 from __future__ import with_statement
41 _moduleLogger = logging.getLogger("gv_views")
44 def make_ugly(prettynumber):
46 function to take a phone number and strip out all non-numeric
49 >>> make_ugly("+012-(345)-678-90")
53 uglynumber = re.sub('\D', '', prettynumber)
57 def make_pretty(phonenumber):
59 Function to take a phone number and return the pretty version
61 if phonenumber begins with 0:
63 if phonenumber begins with 1: ( for gizmo callback numbers )
65 if phonenumber is 13 digits:
67 if phonenumber is 10 digits:
71 >>> make_pretty("1234567")
73 >>> make_pretty("2345678901")
75 >>> make_pretty("12345678901")
77 >>> make_pretty("01234567890")
80 if phonenumber is None or phonenumber is "":
83 phonenumber = make_ugly(phonenumber)
85 if len(phonenumber) < 3:
88 if phonenumber[0] == "0":
90 prettynumber += "+%s" % phonenumber[0:3]
91 if 3 < len(phonenumber):
92 prettynumber += "-(%s)" % phonenumber[3:6]
93 if 6 < len(phonenumber):
94 prettynumber += "-%s" % phonenumber[6:9]
95 if 9 < len(phonenumber):
96 prettynumber += "-%s" % phonenumber[9:]
98 elif len(phonenumber) <= 7:
99 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
100 elif len(phonenumber) > 8 and phonenumber[0] == "1":
101 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
102 elif len(phonenumber) > 7:
103 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
107 def abbrev_relative_date(date):
109 >>> abbrev_relative_date("42 hours ago")
111 >>> abbrev_relative_date("2 days ago")
113 >>> abbrev_relative_date("4 weeks ago")
116 parts = date.split(" ")
117 return "%s %s" % (parts[0], parts[1][0])
120 class MergedAddressBook(object):
122 Merger of all addressbooks
125 def __init__(self, addressbookFactories, sorter = None):
126 self.__addressbookFactories = addressbookFactories
127 self.__addressbooks = None
128 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
130 def clear_caches(self):
131 self.__addressbooks = None
132 for factory in self.__addressbookFactories:
133 factory.clear_caches()
135 def get_addressbooks(self):
137 @returns Iterable of (Address Book Factory, Book Id, Book Name)
141 def open_addressbook(self, bookId):
144 def contact_source_short_name(self, contactId):
145 if self.__addressbooks is None:
147 bookIndex, originalId = contactId.split("-", 1)
148 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
152 return "All Contacts"
154 def get_contacts(self):
156 @returns Iterable of (contact id, contact name)
158 if self.__addressbooks is None:
159 self.__addressbooks = list(
160 factory.open_addressbook(id)
161 for factory in self.__addressbookFactories
162 for (f, id, name) in factory.get_addressbooks()
165 ("-".join([str(bookIndex), contactId]), contactName)
166 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
167 for (contactId, contactName) in addressbook.get_contacts()
169 sortedContacts = self.__sort_contacts(contacts)
170 return sortedContacts
172 def get_contact_details(self, contactId):
174 @returns Iterable of (Phone Type, Phone Number)
176 if self.__addressbooks is None:
178 bookIndex, originalId = contactId.split("-", 1)
179 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
182 def null_sorter(contacts):
184 Good for speed/low memory
189 def basic_firtname_sorter(contacts):
191 Expects names in "First Last" format
194 (contactName.rsplit(" ", 1)[0], (contactId, contactName))
195 for (contactId, contactName) in contacts
197 contactsWithKey.sort()
198 return (contactData for (lastName, contactData) in contactsWithKey)
201 def basic_lastname_sorter(contacts):
203 Expects names in "First Last" format
206 (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
207 for (contactId, contactName) in contacts
209 contactsWithKey.sort()
210 return (contactData for (lastName, contactData) in contactsWithKey)
213 def reversed_firtname_sorter(contacts):
215 Expects names in "Last, First" format
218 (contactName.split(", ", 1)[-1], (contactId, contactName))
219 for (contactId, contactName) in contacts
221 contactsWithKey.sort()
222 return (contactData for (lastName, contactData) in contactsWithKey)
225 def reversed_lastname_sorter(contacts):
227 Expects names in "Last, First" format
230 (contactName.split(", ", 1)[0], (contactId, contactName))
231 for (contactId, contactName) in contacts
233 contactsWithKey.sort()
234 return (contactData for (lastName, contactData) in contactsWithKey)
237 def guess_firstname(name):
239 return name.split(", ", 1)[-1]
241 return name.rsplit(" ", 1)[0]
244 def guess_lastname(name):
246 return name.split(", ", 1)[0]
248 return name.rsplit(" ", 1)[-1]
251 def advanced_firstname_sorter(cls, contacts):
253 (cls.guess_firstname(contactName), (contactId, contactName))
254 for (contactId, contactName) in contacts
256 contactsWithKey.sort()
257 return (contactData for (lastName, contactData) in contactsWithKey)
260 def advanced_lastname_sorter(cls, contacts):
262 (cls.guess_lastname(contactName), (contactId, contactName))
263 for (contactId, contactName) in contacts
265 contactsWithKey.sort()
266 return (contactData for (lastName, contactData) in contactsWithKey)
269 class PhoneTypeSelector(object):
271 ACTION_CANCEL = "cancel"
272 ACTION_SELECT = "select"
274 ACTION_SEND_SMS = "sms"
276 def __init__(self, widgetTree, gcBackend):
277 self._gcBackend = gcBackend
278 self._widgetTree = widgetTree
280 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
281 self._smsDialog = SmsEntryDialog(self._widgetTree)
283 self._smsButton = self._widgetTree.get_widget("sms_button")
284 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
286 self._dialButton = self._widgetTree.get_widget("dial_button")
287 self._dialButton.connect("clicked", self._on_phonetype_dial)
289 self._selectButton = self._widgetTree.get_widget("select_button")
290 self._selectButton.connect("clicked", self._on_phonetype_select)
292 self._cancelButton = self._widgetTree.get_widget("cancel_button")
293 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
295 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
296 self._messagesView = self._widgetTree.get_widget("phoneSelectionMessages")
297 self._scrollWindow = self._messagesView.get_parent()
299 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
300 self._typeviewselection = None
301 self._typeview = self._widgetTree.get_widget("phonetypes")
302 self._typeview.connect("row-activated", self._on_phonetype_select)
304 self._action = self.ACTION_CANCEL
306 def run(self, contactDetails, messages = (), parent = None):
307 self._action = self.ACTION_CANCEL
309 # Add the column to the phone selection tree view
310 self._typemodel.clear()
311 self._typeview.set_model(self._typemodel)
313 textrenderer = gtk.CellRendererText()
314 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
315 self._typeview.append_column(numberColumn)
317 textrenderer = gtk.CellRendererText()
318 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
319 self._typeview.append_column(typeColumn)
321 for phoneType, phoneNumber in contactDetails:
322 display = " - ".join((phoneNumber, phoneType))
324 row = (phoneNumber, display)
325 self._typemodel.append(row)
327 self._typeviewselection = self._typeview.get_selection()
328 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
329 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
331 # Add the column to the messages tree view
332 self._messagemodel.clear()
333 self._messagesView.set_model(self._messagemodel)
335 textrenderer = gtk.CellRendererText()
336 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
337 textrenderer.set_property("wrap-width", 450)
338 messageColumn = gtk.TreeViewColumn("")
339 messageColumn.pack_start(textrenderer, expand=True)
340 messageColumn.add_attribute(textrenderer, "markup", 0)
341 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
342 self._messagesView.append_column(messageColumn)
343 self._messagesView.set_headers_visible(False)
346 for message in messages:
348 self._messagemodel.append(row)
349 self._messagesView.show()
350 self._scrollWindow.show()
351 messagesSelection = self._messagesView.get_selection()
352 messagesSelection.select_path((len(messages)-1, ))
354 self._messagesView.hide()
355 self._scrollWindow.hide()
357 if parent is not None:
358 self._dialog.set_transient_for(parent)
363 self._messagesView.scroll_to_cell((len(messages)-1, ))
365 userResponse = self._dialog.run()
369 if userResponse == gtk.RESPONSE_OK:
370 phoneNumber = self._get_number()
371 phoneNumber = make_ugly(phoneNumber)
375 self._action = self.ACTION_CANCEL
377 if self._action == self.ACTION_SEND_SMS:
378 smsMessage = self._smsDialog.run(phoneNumber, messages, parent)
381 self._action = self.ACTION_CANCEL
385 self._messagesView.remove_column(messageColumn)
386 self._messagesView.set_model(None)
388 self._typeviewselection.unselect_all()
389 self._typeview.remove_column(numberColumn)
390 self._typeview.remove_column(typeColumn)
391 self._typeview.set_model(None)
393 return self._action, phoneNumber, smsMessage
395 def _get_number(self):
396 model, itr = self._typeviewselection.get_selected()
400 phoneNumber = self._typemodel.get_value(itr, 0)
403 def _on_phonetype_dial(self, *args):
404 self._dialog.response(gtk.RESPONSE_OK)
405 self._action = self.ACTION_DIAL
407 def _on_phonetype_send_sms(self, *args):
408 self._dialog.response(gtk.RESPONSE_OK)
409 self._action = self.ACTION_SEND_SMS
411 def _on_phonetype_select(self, *args):
412 self._dialog.response(gtk.RESPONSE_OK)
413 self._action = self.ACTION_SELECT
415 def _on_phonetype_cancel(self, *args):
416 self._dialog.response(gtk.RESPONSE_CANCEL)
417 self._action = self.ACTION_CANCEL
420 class SmsEntryDialog(object):
422 @todo Add multi-SMS messages like GoogleVoice
427 def __init__(self, widgetTree):
428 self._widgetTree = widgetTree
429 self._dialog = self._widgetTree.get_widget("smsDialog")
431 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
432 self._smsButton.connect("clicked", self._on_send)
434 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
435 self._cancelButton.connect("clicked", self._on_cancel)
437 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
439 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
440 self._messagesView = self._widgetTree.get_widget("smsMessages")
441 self._scrollWindow = self._messagesView.get_parent()
443 self._smsEntry = self._widgetTree.get_widget("smsEntry")
444 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
446 def run(self, number, messages = (), parent = None):
447 # Add the column to the messages tree view
448 self._messagemodel.clear()
449 self._messagesView.set_model(self._messagemodel)
451 textrenderer = gtk.CellRendererText()
452 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
453 textrenderer.set_property("wrap-width", 450)
454 messageColumn = gtk.TreeViewColumn("")
455 messageColumn.pack_start(textrenderer, expand=True)
456 messageColumn.add_attribute(textrenderer, "markup", 0)
457 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
458 self._messagesView.append_column(messageColumn)
459 self._messagesView.set_headers_visible(False)
462 for message in messages:
464 self._messagemodel.append(row)
465 self._messagesView.show()
466 self._scrollWindow.show()
467 messagesSelection = self._messagesView.get_selection()
468 messagesSelection.select_path((len(messages)-1, ))
470 self._messagesView.hide()
471 self._scrollWindow.hide()
473 self._smsEntry.get_buffer().set_text("")
474 self._update_letter_count()
476 if parent is not None:
477 self._dialog.set_transient_for(parent)
482 self._messagesView.scroll_to_cell((len(messages)-1, ))
483 self._smsEntry.grab_focus()
485 userResponse = self._dialog.run()
489 if userResponse == gtk.RESPONSE_OK:
490 entryBuffer = self._smsEntry.get_buffer()
491 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
492 enteredMessage = enteredMessage[0:self.MAX_CHAR]
496 self._messagesView.remove_column(messageColumn)
497 self._messagesView.set_model(None)
499 return enteredMessage.strip()
501 def _update_letter_count(self, *args):
502 entryLength = self._smsEntry.get_buffer().get_char_count()
503 charsLeft = self.MAX_CHAR - entryLength
504 self._letterCountLabel.set_text(str(charsLeft))
506 self._smsButton.set_sensitive(False)
508 self._smsButton.set_sensitive(True)
510 def _on_entry_changed(self, *args):
511 self._update_letter_count()
513 def _on_send(self, *args):
514 self._dialog.response(gtk.RESPONSE_OK)
516 def _on_cancel(self, *args):
517 self._dialog.response(gtk.RESPONSE_CANCEL)
520 class Dialpad(object):
522 def __init__(self, widgetTree, errorDisplay):
523 self._clipboard = gtk.clipboard_get()
524 self._errorDisplay = errorDisplay
525 self._smsDialog = SmsEntryDialog(widgetTree)
527 self._numberdisplay = widgetTree.get_widget("numberdisplay")
528 self._smsButton = widgetTree.get_widget("sms")
529 self._dialButton = widgetTree.get_widget("dial")
530 self._backButton = widgetTree.get_widget("back")
531 self._phonenumber = ""
532 self._prettynumber = ""
535 "on_digit_clicked": self._on_digit_clicked,
537 widgetTree.signal_autoconnect(callbackMapping)
538 self._dialButton.connect("clicked", self._on_dial_clicked)
539 self._smsButton.connect("clicked", self._on_sms_clicked)
541 self._originalLabel = self._backButton.get_label()
542 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
543 self._backTapHandler.on_tap = self._on_backspace
544 self._backTapHandler.on_hold = self._on_clearall
545 self._backTapHandler.on_holding = self._set_clear_button
546 self._backTapHandler.on_cancel = self._reset_back_button
548 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
549 self._keyPressEventId = 0
552 self._dialButton.grab_focus()
553 self._backTapHandler.enable()
554 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
557 self._window.disconnect(self._keyPressEventId)
558 self._keyPressEventId = 0
559 self._reset_back_button()
560 self._backTapHandler.disable()
562 def number_selected(self, action, number, message):
564 @note Actual dial function is patched in later
566 raise NotImplementedError("Horrible unknown error has occurred")
568 def get_number(self):
569 return self._phonenumber
571 def set_number(self, number):
573 Set the number to dial
576 self._phonenumber = make_ugly(number)
577 self._prettynumber = make_pretty(self._phonenumber)
578 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
580 self._errorDisplay.push_exception()
589 def load_settings(self, config, section):
592 def save_settings(self, config, section):
594 @note Thread Agnostic
598 def _on_key_press(self, widget, event):
600 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
601 contents = self._clipboard.wait_for_text()
602 if contents is not None:
603 self.set_number(contents)
605 self._errorDisplay.push_exception()
607 def _on_sms_clicked(self, widget):
609 action = PhoneTypeSelector.ACTION_SEND_SMS
610 phoneNumber = self.get_number()
612 message = self._smsDialog.run(phoneNumber, (), self._window)
615 action = PhoneTypeSelector.ACTION_CANCEL
617 if action == PhoneTypeSelector.ACTION_CANCEL:
619 self.number_selected(action, phoneNumber, message)
621 self._errorDisplay.push_exception()
623 def _on_dial_clicked(self, widget):
625 action = PhoneTypeSelector.ACTION_DIAL
626 phoneNumber = self.get_number()
628 self.number_selected(action, phoneNumber, message)
630 self._errorDisplay.push_exception()
632 def _on_digit_clicked(self, widget):
634 self.set_number(self._phonenumber + widget.get_name()[-1])
636 self._errorDisplay.push_exception()
638 def _on_backspace(self, taps):
640 self.set_number(self._phonenumber[:-taps])
641 self._reset_back_button()
643 self._errorDisplay.push_exception()
645 def _on_clearall(self, taps):
648 self._reset_back_button()
650 self._errorDisplay.push_exception()
653 def _set_clear_button(self):
655 self._backButton.set_label("gtk-clear")
657 self._errorDisplay.push_exception()
659 def _reset_back_button(self):
661 self._backButton.set_label(self._originalLabel)
663 self._errorDisplay.push_exception()
666 class AccountInfo(object):
668 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
669 self._errorDisplay = errorDisplay
670 self._backend = backend
671 self._isPopulated = False
672 self._alarmHandler = alarmHandler
673 self._notifyOnMissed = False
674 self._notifyOnVoicemail = False
675 self._notifyOnSms = False
677 self._callbackList = []
678 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
679 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
680 self._onCallbackSelectChangedId = 0
682 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
683 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
684 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
685 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
686 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
687 self._onNotifyToggled = 0
688 self._onMinutesChanged = 0
689 self._onMissedToggled = 0
690 self._onVoicemailToggled = 0
691 self._onSmsToggled = 0
692 self._applyAlarmTimeoutId = None
694 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
695 self._defaultCallback = ""
698 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
700 self._accountViewNumberDisplay.set_use_markup(True)
701 self.set_account_number("")
703 del self._callbackList[:]
704 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
706 if self._alarmHandler is not None:
707 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
708 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
709 self._missedCheckbox.set_active(self._notifyOnMissed)
710 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
711 self._smsCheckbox.set_active(self._notifyOnSms)
713 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
714 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
715 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
716 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
717 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
719 self._notifyCheckbox.set_sensitive(False)
720 self._minutesEntryButton.set_sensitive(False)
721 self._missedCheckbox.set_sensitive(False)
722 self._voicemailCheckbox.set_sensitive(False)
723 self._smsCheckbox.set_sensitive(False)
725 self.update(force=True)
728 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
729 self._onCallbackSelectChangedId = 0
731 if self._alarmHandler is not None:
732 self._notifyCheckbox.disconnect(self._onNotifyToggled)
733 self._minutesEntryButton.disconnect(self._onMinutesChanged)
734 self._missedCheckbox.disconnect(self._onNotifyToggled)
735 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
736 self._smsCheckbox.disconnect(self._onNotifyToggled)
737 self._onNotifyToggled = 0
738 self._onMinutesChanged = 0
739 self._onMissedToggled = 0
740 self._onVoicemailToggled = 0
741 self._onSmsToggled = 0
743 self._notifyCheckbox.set_sensitive(True)
744 self._minutesEntryButton.set_sensitive(True)
745 self._missedCheckbox.set_sensitive(True)
746 self._voicemailCheckbox.set_sensitive(True)
747 self._smsCheckbox.set_sensitive(True)
750 del self._callbackList[:]
752 def get_selected_callback_number(self):
753 currentLabel = self._callbackSelectButton.get_label()
754 if currentLabel is not None:
755 return make_ugly(currentLabel)
759 def set_account_number(self, number):
761 Displays current account number
763 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
765 def update(self, force = False):
766 if not force and self._isPopulated:
768 self._populate_callback_combo()
769 self.set_account_number(self._backend.get_account_number())
773 self._callbackSelectButton.set_label("")
774 self.set_account_number("")
775 self._isPopulated = False
777 def save_everything(self):
778 raise NotImplementedError
782 return "Account Info"
784 def load_settings(self, config, section):
785 self._defaultCallback = config.get(section, "callback")
786 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
787 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
788 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
790 def save_settings(self, config, section):
792 @note Thread Agnostic
794 callback = self.get_selected_callback_number()
795 config.set(section, "callback", callback)
796 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
797 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
798 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
800 def _populate_callback_combo(self):
801 self._isPopulated = True
802 del self._callbackList[:]
804 callbackNumbers = self._backend.get_callback_numbers()
806 self._errorDisplay.push_exception()
807 self._isPopulated = False
810 for number, description in callbackNumbers.iteritems():
811 self._callbackList.append((make_pretty(number), description))
813 if not self.get_selected_callback_number():
814 self._set_callback_number(self._defaultCallback)
816 def _set_callback_number(self, number):
818 if not self._backend.is_valid_syntax(number) and 0 < len(number):
819 self._errorDisplay.push_message("%s is not a valid callback number" % number)
820 elif number == self._backend.get_callback_number():
821 _moduleLogger.warning(
822 "Callback number already is %s" % (
823 self._backend.get_callback_number(),
827 self._backend.set_callback_number(number)
828 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
829 make_pretty(number), make_pretty(self._backend.get_callback_number())
831 self._callbackSelectButton.set_label(make_pretty(number))
833 "Callback number set to %s" % (
834 self._backend.get_callback_number(),
838 self._errorDisplay.push_exception()
840 def _update_alarm_settings(self, recurrence):
842 isEnabled = self._notifyCheckbox.get_active()
843 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
844 self._alarmHandler.apply_settings(isEnabled, recurrence)
846 self.save_everything()
847 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
848 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
850 def _on_callbackentry_clicked(self, *args):
852 actualSelection = make_pretty(self.get_selected_callback_number())
855 (number, "%s (%s)" % (number, description))
856 for (number, description) in self._callbackList
858 defaultSelection = userOptions.get(actualSelection, actualSelection)
860 userSelection = hildonize.touch_selector_entry(
863 list(userOptions.itervalues()),
866 reversedUserOptions = dict(
867 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
869 selectedNumber = reversedUserOptions.get(userSelection, userSelection)
871 number = make_ugly(selectedNumber)
872 self._set_callback_number(number)
873 except RuntimeError, e:
874 _moduleLogger.exception("%s" % str(e))
876 self._errorDisplay.push_exception()
878 def _on_notify_toggled(self, *args):
880 if self._applyAlarmTimeoutId is not None:
881 gobject.source_remove(self._applyAlarmTimeoutId)
882 self._applyAlarmTimeoutId = None
883 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
885 self._errorDisplay.push_exception()
887 def _on_minutes_clicked(self, *args):
888 recurrenceChoices = [
904 actualSelection = self._alarmHandler.recurrence
906 closestSelectionIndex = 0
907 for i, possible in enumerate(recurrenceChoices):
908 if possible[0] <= actualSelection:
909 closestSelectionIndex = i
910 recurrenceIndex = hildonize.touch_selector(
913 (("%s" % m[1]) for m in recurrenceChoices),
914 closestSelectionIndex,
916 recurrence = recurrenceChoices[recurrenceIndex][0]
918 self._update_alarm_settings(recurrence)
919 except RuntimeError, e:
920 _moduleLogger.exception("%s" % str(e))
922 self._errorDisplay.push_exception()
924 def _on_apply_timeout(self, *args):
926 self._applyAlarmTimeoutId = None
928 self._update_alarm_settings(self._alarmHandler.recurrence)
930 self._errorDisplay.push_exception()
933 def _on_missed_toggled(self, *args):
935 self._notifyOnMissed = self._missedCheckbox.get_active()
936 self.save_everything()
938 self._errorDisplay.push_exception()
940 def _on_voicemail_toggled(self, *args):
942 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
943 self.save_everything()
945 self._errorDisplay.push_exception()
947 def _on_sms_toggled(self, *args):
949 self._notifyOnSms = self._smsCheckbox.get_active()
950 self.save_everything()
952 self._errorDisplay.push_exception()
955 class RecentCallsView(object):
962 def __init__(self, widgetTree, backend, errorDisplay):
963 self._errorDisplay = errorDisplay
964 self._backend = backend
966 self._isPopulated = False
967 self._recentmodel = gtk.ListStore(
968 gobject.TYPE_STRING, # number
969 gobject.TYPE_STRING, # date
970 gobject.TYPE_STRING, # action
971 gobject.TYPE_STRING, # from
973 self._recentview = widgetTree.get_widget("recentview")
974 self._recentviewselection = None
975 self._onRecentviewRowActivatedId = 0
977 textrenderer = gtk.CellRendererText()
978 textrenderer.set_property("yalign", 0)
979 self._dateColumn = gtk.TreeViewColumn("Date")
980 self._dateColumn.pack_start(textrenderer, expand=True)
981 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
983 textrenderer = gtk.CellRendererText()
984 textrenderer.set_property("yalign", 0)
985 self._actionColumn = gtk.TreeViewColumn("Action")
986 self._actionColumn.pack_start(textrenderer, expand=True)
987 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
989 textrenderer = gtk.CellRendererText()
990 textrenderer.set_property("yalign", 0)
991 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
992 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
993 self._numberColumn = gtk.TreeViewColumn("Number")
994 self._numberColumn.pack_start(textrenderer, expand=True)
995 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
997 textrenderer = gtk.CellRendererText()
998 textrenderer.set_property("yalign", 0)
999 hildonize.set_cell_thumb_selectable(textrenderer)
1000 self._nameColumn = gtk.TreeViewColumn("From")
1001 self._nameColumn.pack_start(textrenderer, expand=True)
1002 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
1003 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1005 self._window = gtk_toolbox.find_parent_window(self._recentview)
1006 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1008 self._updateSink = gtk_toolbox.threaded_stage(
1010 self._idly_populate_recentview,
1011 gtk_toolbox.null_sink(),
1016 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1017 self._recentview.set_model(self._recentmodel)
1019 self._recentview.append_column(self._dateColumn)
1020 self._recentview.append_column(self._actionColumn)
1021 self._recentview.append_column(self._numberColumn)
1022 self._recentview.append_column(self._nameColumn)
1023 self._recentviewselection = self._recentview.get_selection()
1024 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
1026 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
1029 self._recentview.disconnect(self._onRecentviewRowActivatedId)
1033 self._recentview.remove_column(self._dateColumn)
1034 self._recentview.remove_column(self._actionColumn)
1035 self._recentview.remove_column(self._nameColumn)
1036 self._recentview.remove_column(self._numberColumn)
1037 self._recentview.set_model(None)
1039 def number_selected(self, action, number, message):
1041 @note Actual dial function is patched in later
1043 raise NotImplementedError("Horrible unknown error has occurred")
1045 def update(self, force = False):
1046 if not force and self._isPopulated:
1048 self._updateSink.send(())
1052 self._isPopulated = False
1053 self._recentmodel.clear()
1057 return "Recent Calls"
1059 def load_settings(self, config, section):
1062 def save_settings(self, config, section):
1064 @note Thread Agnostic
1068 def _idly_populate_recentview(self):
1069 with gtk_toolbox.gtk_lock():
1070 banner = hildonize.show_busy_banner_start(self._window, "Loading Recent History")
1072 self._recentmodel.clear()
1073 self._isPopulated = True
1076 recentItems = self._backend.get_recent()
1077 except Exception, e:
1078 self._errorDisplay.push_exception_with_lock()
1079 self._isPopulated = False
1083 gv_backend.decorate_recent(data)
1084 for data in gv_backend.sort_messages(recentItems)
1087 for personName, phoneNumber, date, action in recentItems:
1089 personName = "Unknown"
1090 date = abbrev_relative_date(date)
1091 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1092 prettyNumber = make_pretty(prettyNumber)
1093 item = (prettyNumber, date, action.capitalize(), personName)
1094 with gtk_toolbox.gtk_lock():
1095 self._recentmodel.append(item)
1096 except Exception, e:
1097 self._errorDisplay.push_exception_with_lock()
1099 with gtk_toolbox.gtk_lock():
1100 hildonize.show_busy_banner_end(banner)
1104 def _on_recentview_row_activated(self, treeview, path, view_column):
1106 model, itr = self._recentviewselection.get_selected()
1110 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1111 number = make_ugly(number)
1112 contactPhoneNumbers = [("Phone", number)]
1113 description = self._recentmodel.get_value(itr, self.FROM_IDX)
1115 action, phoneNumber, message = self._phoneTypeSelector.run(
1116 contactPhoneNumbers,
1117 messages = (description, ),
1118 parent = self._window,
1120 if action == PhoneTypeSelector.ACTION_CANCEL:
1122 assert phoneNumber, "A lack of phone number exists"
1124 self.number_selected(action, phoneNumber, message)
1125 self._recentviewselection.unselect_all()
1126 except Exception, e:
1127 self._errorDisplay.push_exception()
1130 class MessagesView(object):
1138 def __init__(self, widgetTree, backend, errorDisplay):
1139 self._errorDisplay = errorDisplay
1140 self._backend = backend
1142 self._isPopulated = False
1143 self._messagemodel = gtk.ListStore(
1144 gobject.TYPE_STRING, # number
1145 gobject.TYPE_STRING, # date
1146 gobject.TYPE_STRING, # header
1147 gobject.TYPE_STRING, # message
1150 self._messageview = widgetTree.get_widget("messages_view")
1151 self._messageviewselection = None
1152 self._onMessageviewRowActivatedId = 0
1154 self._messageRenderer = gtk.CellRendererText()
1155 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1156 self._messageRenderer.set_property("wrap-width", 500)
1157 self._messageColumn = gtk.TreeViewColumn("Messages")
1158 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1159 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1160 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1162 self._window = gtk_toolbox.find_parent_window(self._messageview)
1163 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1165 self._updateSink = gtk_toolbox.threaded_stage(
1167 self._idly_populate_messageview,
1168 gtk_toolbox.null_sink(),
1173 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1174 self._messageview.set_model(self._messagemodel)
1175 self._messageview.set_headers_visible(False)
1177 self._messageview.append_column(self._messageColumn)
1178 self._messageviewselection = self._messageview.get_selection()
1179 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1181 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1184 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1188 self._messageview.remove_column(self._messageColumn)
1189 self._messageview.set_model(None)
1191 def number_selected(self, action, number, message):
1193 @note Actual dial function is patched in later
1195 raise NotImplementedError("Horrible unknown error has occurred")
1197 def update(self, force = False):
1198 if not force and self._isPopulated:
1200 self._updateSink.send(())
1204 self._isPopulated = False
1205 self._messagemodel.clear()
1211 def load_settings(self, config, section):
1214 def save_settings(self, config, section):
1216 @note Thread Agnostic
1220 def _idly_populate_messageview(self):
1221 with gtk_toolbox.gtk_lock():
1222 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1224 self._messagemodel.clear()
1225 self._isPopulated = True
1228 messageItems = self._backend.get_messages()
1229 except Exception, e:
1230 self._errorDisplay.push_exception_with_lock()
1231 self._isPopulated = False
1235 gv_backend.decorate_message(message)
1236 for message in gv_backend.sort_messages(messageItems)
1239 for header, number, relativeDate, messages in messageItems:
1240 prettyNumber = number[2:] if number.startswith("+1") else number
1241 prettyNumber = make_pretty(prettyNumber)
1243 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1244 newMessages = [firstMessage]
1245 newMessages.extend(messages)
1247 number = make_ugly(number)
1249 row = (number, relativeDate, header, "\n".join(newMessages), newMessages)
1250 with gtk_toolbox.gtk_lock():
1251 self._messagemodel.append(row)
1252 except Exception, e:
1253 self._errorDisplay.push_exception_with_lock()
1255 with gtk_toolbox.gtk_lock():
1256 hildonize.show_busy_banner_end(banner)
1260 def _on_messageview_row_activated(self, treeview, path, view_column):
1262 model, itr = self._messageviewselection.get_selected()
1266 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1267 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1269 action, phoneNumber, message = self._phoneTypeSelector.run(
1270 contactPhoneNumbers,
1271 messages = description,
1272 parent = self._window,
1274 if action == PhoneTypeSelector.ACTION_CANCEL:
1276 assert phoneNumber, "A lock of phone number exists"
1278 self.number_selected(action, phoneNumber, message)
1279 self._messageviewselection.unselect_all()
1280 except Exception, e:
1281 self._errorDisplay.push_exception()
1284 class ContactsView(object):
1286 def __init__(self, widgetTree, backend, errorDisplay):
1287 self._errorDisplay = errorDisplay
1288 self._backend = backend
1290 self._addressBook = None
1291 self._selectedComboIndex = 0
1292 self._addressBookFactories = [null_backend.NullAddressBook()]
1294 self._booksList = []
1295 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1297 self._isPopulated = False
1298 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1299 self._contactsviewselection = None
1300 self._contactsview = widgetTree.get_widget("contactsview")
1302 self._contactColumn = gtk.TreeViewColumn("Contact")
1303 displayContactSource = False
1304 if displayContactSource:
1305 textrenderer = gtk.CellRendererText()
1306 self._contactColumn.pack_start(textrenderer, expand=False)
1307 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1308 textrenderer = gtk.CellRendererText()
1309 hildonize.set_cell_thumb_selectable(textrenderer)
1310 self._contactColumn.pack_start(textrenderer, expand=True)
1311 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1312 textrenderer = gtk.CellRendererText()
1313 self._contactColumn.pack_start(textrenderer, expand=True)
1314 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1315 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1316 self._contactColumn.set_sort_column_id(1)
1317 self._contactColumn.set_visible(True)
1319 self._onContactsviewRowActivatedId = 0
1320 self._onAddressbookButtonChangedId = 0
1321 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1322 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1324 self._updateSink = gtk_toolbox.threaded_stage(
1326 self._idly_populate_contactsview,
1327 gtk_toolbox.null_sink(),
1332 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1334 self._contactsview.set_model(self._contactsmodel)
1335 self._contactsview.append_column(self._contactColumn)
1336 self._contactsviewselection = self._contactsview.get_selection()
1337 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1339 del self._booksList[:]
1340 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1341 if factoryName and bookName:
1342 entryName = "%s: %s" % (factoryName, bookName)
1344 entryName = factoryName
1346 entryName = bookName
1348 entryName = "Bad name (%d)" % factoryId
1349 row = (str(factoryId), bookId, entryName)
1350 self._booksList.append(row)
1352 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1353 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1355 if len(self._booksList) <= self._selectedComboIndex:
1356 self._selectedComboIndex = 0
1357 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1359 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1360 selectedBookId = self._booksList[self._selectedComboIndex][1]
1361 self.open_addressbook(selectedFactoryId, selectedBookId)
1364 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1365 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1369 self._bookSelectionButton.set_label("")
1370 self._contactsview.set_model(None)
1371 self._contactsview.remove_column(self._contactColumn)
1373 def number_selected(self, action, number, message):
1375 @note Actual dial function is patched in later
1377 raise NotImplementedError("Horrible unknown error has occurred")
1379 def get_addressbooks(self):
1381 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1383 for i, factory in enumerate(self._addressBookFactories):
1384 for bookFactory, bookId, bookName in factory.get_addressbooks():
1385 yield (str(i), bookId), (factory.factory_name(), bookName)
1387 def open_addressbook(self, bookFactoryId, bookId):
1388 bookFactoryIndex = int(bookFactoryId)
1389 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1391 forceUpdate = True if addressBook is not self._addressBook else False
1393 self._addressBook = addressBook
1394 self.update(force=forceUpdate)
1396 def update(self, force = False):
1397 if not force and self._isPopulated:
1399 self._updateSink.send(())
1403 self._isPopulated = False
1404 self._contactsmodel.clear()
1405 for factory in self._addressBookFactories:
1406 factory.clear_caches()
1407 self._addressBook.clear_caches()
1409 def append(self, book):
1410 self._addressBookFactories.append(book)
1412 def extend(self, books):
1413 self._addressBookFactories.extend(books)
1419 def load_settings(self, config, sectionName):
1421 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1422 except ConfigParser.NoOptionError:
1423 self._selectedComboIndex = 0
1425 def save_settings(self, config, sectionName):
1426 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1428 def _idly_populate_contactsview(self):
1429 with gtk_toolbox.gtk_lock():
1430 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1433 while addressBook is not self._addressBook:
1434 addressBook = self._addressBook
1435 with gtk_toolbox.gtk_lock():
1436 self._contactsview.set_model(None)
1440 contacts = addressBook.get_contacts()
1441 except Exception, e:
1443 self._isPopulated = False
1444 self._errorDisplay.push_exception_with_lock()
1445 for contactId, contactName in contacts:
1446 contactType = (addressBook.contact_source_short_name(contactId), )
1447 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1449 with gtk_toolbox.gtk_lock():
1450 self._contactsview.set_model(self._contactsmodel)
1452 self._isPopulated = True
1453 except Exception, e:
1454 self._errorDisplay.push_exception_with_lock()
1456 with gtk_toolbox.gtk_lock():
1457 hildonize.show_busy_banner_end(banner)
1460 def _on_addressbook_button_changed(self, *args, **kwds):
1463 newSelectedComboIndex = hildonize.touch_selector(
1466 (("%s" % m[2]) for m in self._booksList),
1467 self._selectedComboIndex,
1469 except RuntimeError:
1472 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1473 selectedBookId = self._booksList[newSelectedComboIndex][1]
1474 self.open_addressbook(selectedFactoryId, selectedBookId)
1475 self._selectedComboIndex = newSelectedComboIndex
1476 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1477 except Exception, e:
1478 self._errorDisplay.push_exception()
1480 def _on_contactsview_row_activated(self, treeview, path, view_column):
1482 model, itr = self._contactsviewselection.get_selected()
1486 contactId = self._contactsmodel.get_value(itr, 3)
1487 contactName = self._contactsmodel.get_value(itr, 1)
1489 contactDetails = self._addressBook.get_contact_details(contactId)
1490 except Exception, e:
1492 self._errorDisplay.push_exception()
1493 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1495 if len(contactPhoneNumbers) == 0:
1498 action, phoneNumber, message = self._phoneTypeSelector.run(
1499 contactPhoneNumbers,
1500 messages = (contactName, ),
1501 parent = self._window,
1503 if action == PhoneTypeSelector.ACTION_CANCEL:
1505 assert phoneNumber, "A lack of phone number exists"
1507 self.number_selected(action, phoneNumber, message)
1508 self._contactsviewselection.unselect_all()
1509 except Exception, e:
1510 self._errorDisplay.push_exception()