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._clipboard = gtk.clipboard_get()
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._keyPressEventId = self._dialog.connect("key-press-event", self._on_key_press)
306 self._action = self.ACTION_CANCEL
308 def run(self, contactDetails, messages = (), parent = None):
309 self._action = self.ACTION_CANCEL
311 # Add the column to the phone selection tree view
312 self._typemodel.clear()
313 self._typeview.set_model(self._typemodel)
315 textrenderer = gtk.CellRendererText()
316 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
317 self._typeview.append_column(numberColumn)
319 textrenderer = gtk.CellRendererText()
320 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
321 self._typeview.append_column(typeColumn)
323 for phoneType, phoneNumber in contactDetails:
324 display = " - ".join((phoneNumber, phoneType))
326 row = (phoneNumber, display)
327 self._typemodel.append(row)
329 self._typeviewselection = self._typeview.get_selection()
330 self._typeviewselection.set_mode(gtk.SELECTION_NONE)
331 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
333 # Add the column to the messages tree view
334 self._messagemodel.clear()
335 self._messagesView.set_model(self._messagemodel)
337 textrenderer = gtk.CellRendererText()
338 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
339 textrenderer.set_property("wrap-width", 450)
340 messageColumn = gtk.TreeViewColumn("")
341 messageColumn.pack_start(textrenderer, expand=True)
342 messageColumn.add_attribute(textrenderer, "markup", 0)
343 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
344 self._messagesView.append_column(messageColumn)
345 self._messagesView.set_headers_visible(False)
348 for message in messages:
350 self._messagemodel.append(row)
351 self._messagesView.show()
352 self._scrollWindow.show()
353 messagesSelection = self._messagesView.get_selection()
354 messagesSelection.select_path((len(messages)-1, ))
356 self._messagesView.hide()
357 self._scrollWindow.hide()
359 if parent is not None:
360 self._dialog.set_transient_for(parent)
365 self._messagesView.scroll_to_cell((len(messages)-1, ))
367 userResponse = self._dialog.run()
371 if userResponse == gtk.RESPONSE_OK:
372 phoneNumber = self._get_number()
373 phoneNumber = make_ugly(phoneNumber)
377 self._action = self.ACTION_CANCEL
379 if self._action == self.ACTION_SEND_SMS:
380 smsMessage = self._smsDialog.run(phoneNumber, messages, parent)
383 self._action = self.ACTION_CANCEL
387 self._messagesView.remove_column(messageColumn)
388 self._messagesView.set_model(None)
390 self._typeviewselection.unselect_all()
391 self._typeview.remove_column(numberColumn)
392 self._typeview.remove_column(typeColumn)
393 self._typeview.set_model(None)
395 return self._action, phoneNumber, smsMessage
397 def _get_number(self):
398 model, itr = self._typeviewselection.get_selected()
402 phoneNumber = self._typemodel.get_value(itr, 0)
405 def _on_phonetype_dial(self, *args):
406 self._dialog.response(gtk.RESPONSE_OK)
407 self._action = self.ACTION_DIAL
409 def _on_phonetype_send_sms(self, *args):
410 self._dialog.response(gtk.RESPONSE_OK)
411 self._action = self.ACTION_SEND_SMS
413 def _on_phonetype_select(self, *args):
414 self._dialog.response(gtk.RESPONSE_OK)
415 self._action = self.ACTION_SELECT
417 def _on_phonetype_cancel(self, *args):
418 self._dialog.response(gtk.RESPONSE_CANCEL)
419 self._action = self.ACTION_CANCEL
421 def _on_key_press(self, widget, event):
423 if event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
426 for messagePart in self._messagemodel
428 # For some reason this kills clipboard stuff
429 #self._clipboard.set_text(message)
431 _moduleLogger.exception(str(e))
434 class SmsEntryDialog(object):
436 @todo Add multi-SMS messages like GoogleVoice
441 def __init__(self, widgetTree):
442 self._clipboard = gtk.clipboard_get()
443 self._widgetTree = widgetTree
444 self._dialog = self._widgetTree.get_widget("smsDialog")
446 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
447 self._smsButton.connect("clicked", self._on_send)
449 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
450 self._cancelButton.connect("clicked", self._on_cancel)
452 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
454 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
455 self._messagesView = self._widgetTree.get_widget("smsMessages")
456 self._scrollWindow = self._messagesView.get_parent()
458 self._smsEntry = self._widgetTree.get_widget("smsEntry")
459 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
461 self._keyPressEventId = self._dialog.connect("key-press-event", self._on_key_press)
463 def run(self, number, messages = (), parent = None):
464 # Add the column to the messages tree view
465 self._messagemodel.clear()
466 self._messagesView.set_model(self._messagemodel)
468 textrenderer = gtk.CellRendererText()
469 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
470 textrenderer.set_property("wrap-width", 450)
471 messageColumn = gtk.TreeViewColumn("")
472 messageColumn.pack_start(textrenderer, expand=True)
473 messageColumn.add_attribute(textrenderer, "markup", 0)
474 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
475 self._messagesView.append_column(messageColumn)
476 self._messagesView.set_headers_visible(False)
479 for message in messages:
481 self._messagemodel.append(row)
482 self._messagesView.show()
483 self._scrollWindow.show()
484 messagesSelection = self._messagesView.get_selection()
485 messagesSelection.select_path((len(messages)-1, ))
487 self._messagesView.hide()
488 self._scrollWindow.hide()
490 self._smsEntry.get_buffer().set_text("")
491 self._update_letter_count()
493 if parent is not None:
494 self._dialog.set_transient_for(parent)
499 self._messagesView.scroll_to_cell((len(messages)-1, ))
500 self._smsEntry.grab_focus()
502 userResponse = self._dialog.run()
506 if userResponse == gtk.RESPONSE_OK:
507 entryBuffer = self._smsEntry.get_buffer()
508 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
509 enteredMessage = enteredMessage[0:self.MAX_CHAR]
513 self._messagesView.remove_column(messageColumn)
514 self._messagesView.set_model(None)
516 return enteredMessage.strip()
518 def _update_letter_count(self, *args):
519 entryLength = self._smsEntry.get_buffer().get_char_count()
520 charsLeft = self.MAX_CHAR - entryLength
521 self._letterCountLabel.set_text(str(charsLeft))
522 if charsLeft < 0 or charsLeft == self.MAX_CHAR:
523 self._smsButton.set_sensitive(False)
525 self._smsButton.set_sensitive(True)
527 def _on_entry_changed(self, *args):
528 self._update_letter_count()
530 def _on_send(self, *args):
531 self._dialog.response(gtk.RESPONSE_OK)
533 def _on_cancel(self, *args):
534 self._dialog.response(gtk.RESPONSE_CANCEL)
536 def _on_key_press(self, widget, event):
538 if event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
541 for messagePart in self._messagemodel
543 # For some reason this kills clipboard stuff
544 #self._clipboard.set_text(message)
546 _moduleLogger.exception(str(e))
549 class Dialpad(object):
551 def __init__(self, widgetTree, errorDisplay):
552 self._clipboard = gtk.clipboard_get()
553 self._errorDisplay = errorDisplay
554 self._smsDialog = SmsEntryDialog(widgetTree)
556 self._numberdisplay = widgetTree.get_widget("numberdisplay")
557 self._smsButton = widgetTree.get_widget("sms")
558 self._dialButton = widgetTree.get_widget("dial")
559 self._backButton = widgetTree.get_widget("back")
560 self._phonenumber = ""
561 self._prettynumber = ""
564 "on_digit_clicked": self._on_digit_clicked,
566 widgetTree.signal_autoconnect(callbackMapping)
567 self._dialButton.connect("clicked", self._on_dial_clicked)
568 self._smsButton.connect("clicked", self._on_sms_clicked)
570 self._originalLabel = self._backButton.get_label()
571 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
572 self._backTapHandler.on_tap = self._on_backspace
573 self._backTapHandler.on_hold = self._on_clearall
574 self._backTapHandler.on_holding = self._set_clear_button
575 self._backTapHandler.on_cancel = self._reset_back_button
577 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
578 self._keyPressEventId = 0
581 self._dialButton.grab_focus()
582 self._backTapHandler.enable()
583 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
586 self._window.disconnect(self._keyPressEventId)
587 self._keyPressEventId = 0
588 self._reset_back_button()
589 self._backTapHandler.disable()
591 def number_selected(self, action, number, message):
593 @note Actual dial function is patched in later
595 raise NotImplementedError("Horrible unknown error has occurred")
597 def get_number(self):
598 return self._phonenumber
600 def set_number(self, number):
602 Set the number to dial
605 self._phonenumber = make_ugly(number)
606 self._prettynumber = make_pretty(self._phonenumber)
607 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
609 self._errorDisplay.push_exception()
618 def load_settings(self, config, section):
621 def save_settings(self, config, section):
623 @note Thread Agnostic
627 def _on_key_press(self, widget, event):
629 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
630 contents = self._clipboard.wait_for_text()
631 if contents is not None:
632 self.set_number(contents)
634 self._errorDisplay.push_exception()
636 def _on_sms_clicked(self, widget):
638 action = PhoneTypeSelector.ACTION_SEND_SMS
639 phoneNumber = self.get_number()
641 message = self._smsDialog.run(phoneNumber, (), self._window)
644 action = PhoneTypeSelector.ACTION_CANCEL
646 if action == PhoneTypeSelector.ACTION_CANCEL:
648 self.number_selected(action, phoneNumber, message)
650 self._errorDisplay.push_exception()
652 def _on_dial_clicked(self, widget):
654 action = PhoneTypeSelector.ACTION_DIAL
655 phoneNumber = self.get_number()
657 self.number_selected(action, phoneNumber, message)
659 self._errorDisplay.push_exception()
661 def _on_digit_clicked(self, widget):
663 self.set_number(self._phonenumber + widget.get_name()[-1])
665 self._errorDisplay.push_exception()
667 def _on_backspace(self, taps):
669 self.set_number(self._phonenumber[:-taps])
670 self._reset_back_button()
672 self._errorDisplay.push_exception()
674 def _on_clearall(self, taps):
677 self._reset_back_button()
679 self._errorDisplay.push_exception()
682 def _set_clear_button(self):
684 self._backButton.set_label("gtk-clear")
686 self._errorDisplay.push_exception()
688 def _reset_back_button(self):
690 self._backButton.set_label(self._originalLabel)
692 self._errorDisplay.push_exception()
695 class AccountInfo(object):
697 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
698 self._errorDisplay = errorDisplay
699 self._backend = backend
700 self._isPopulated = False
701 self._alarmHandler = alarmHandler
702 self._notifyOnMissed = False
703 self._notifyOnVoicemail = False
704 self._notifyOnSms = False
706 self._callbackList = []
707 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
708 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
709 self._onCallbackSelectChangedId = 0
711 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
712 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
713 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
714 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
715 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
716 self._onNotifyToggled = 0
717 self._onMinutesChanged = 0
718 self._onMissedToggled = 0
719 self._onVoicemailToggled = 0
720 self._onSmsToggled = 0
721 self._applyAlarmTimeoutId = None
723 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
724 self._defaultCallback = ""
727 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
729 self._accountViewNumberDisplay.set_use_markup(True)
730 self.set_account_number("")
732 del self._callbackList[:]
733 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
735 if self._alarmHandler is not None:
736 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
737 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
738 self._missedCheckbox.set_active(self._notifyOnMissed)
739 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
740 self._smsCheckbox.set_active(self._notifyOnSms)
742 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
743 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
744 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
745 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
746 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
748 self._notifyCheckbox.set_sensitive(False)
749 self._minutesEntryButton.set_sensitive(False)
750 self._missedCheckbox.set_sensitive(False)
751 self._voicemailCheckbox.set_sensitive(False)
752 self._smsCheckbox.set_sensitive(False)
754 self.update(force=True)
757 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
758 self._onCallbackSelectChangedId = 0
760 if self._alarmHandler is not None:
761 self._notifyCheckbox.disconnect(self._onNotifyToggled)
762 self._minutesEntryButton.disconnect(self._onMinutesChanged)
763 self._missedCheckbox.disconnect(self._onNotifyToggled)
764 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
765 self._smsCheckbox.disconnect(self._onNotifyToggled)
766 self._onNotifyToggled = 0
767 self._onMinutesChanged = 0
768 self._onMissedToggled = 0
769 self._onVoicemailToggled = 0
770 self._onSmsToggled = 0
772 self._notifyCheckbox.set_sensitive(True)
773 self._minutesEntryButton.set_sensitive(True)
774 self._missedCheckbox.set_sensitive(True)
775 self._voicemailCheckbox.set_sensitive(True)
776 self._smsCheckbox.set_sensitive(True)
779 del self._callbackList[:]
781 def get_selected_callback_number(self):
782 currentLabel = self._callbackSelectButton.get_label()
783 if currentLabel is not None:
784 return make_ugly(currentLabel)
788 def set_account_number(self, number):
790 Displays current account number
792 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
794 def update(self, force = False):
795 if not force and self._isPopulated:
797 self._populate_callback_combo()
798 self.set_account_number(self._backend.get_account_number())
802 self._callbackSelectButton.set_label("No Callback Number")
803 self.set_account_number("")
804 self._isPopulated = False
806 def save_everything(self):
807 raise NotImplementedError
811 return "Account Info"
813 def load_settings(self, config, section):
814 self._defaultCallback = config.get(section, "callback")
815 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
816 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
817 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
819 def save_settings(self, config, section):
821 @note Thread Agnostic
823 callback = self.get_selected_callback_number()
824 config.set(section, "callback", callback)
825 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
826 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
827 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
829 def _populate_callback_combo(self):
830 self._isPopulated = True
831 del self._callbackList[:]
833 callbackNumbers = self._backend.get_callback_numbers()
835 self._errorDisplay.push_exception()
836 self._isPopulated = False
839 if len(callbackNumbers) == 0:
840 callbackNumbers = {"": "No callback numbers available"}
842 for number, description in callbackNumbers.iteritems():
843 self._callbackList.append((make_pretty(number), description))
845 self._set_callback_number(self._defaultCallback)
847 def _set_callback_number(self, number):
849 if not self._backend.is_valid_syntax(number) and 0 < len(number):
850 self._errorDisplay.push_message("%s is not a valid callback number" % number)
851 elif number == self._backend.get_callback_number() and 0 < len(number):
852 _moduleLogger.warning(
853 "Callback number already is %s" % (
854 self._backend.get_callback_number(),
858 self._backend.set_callback_number(number)
859 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
860 make_pretty(number), make_pretty(self._backend.get_callback_number())
862 prettyNumber = make_pretty(number)
863 if len(prettyNumber) == 0:
864 prettyNumber = "No Callback Number"
865 self._callbackSelectButton.set_label(prettyNumber)
867 "Callback number set to %s" % (
868 self._backend.get_callback_number(),
872 self._errorDisplay.push_exception()
874 def _update_alarm_settings(self, recurrence):
876 isEnabled = self._notifyCheckbox.get_active()
877 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
878 self._alarmHandler.apply_settings(isEnabled, recurrence)
880 self.save_everything()
881 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
882 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
884 def _on_callbackentry_clicked(self, *args):
886 actualSelection = make_pretty(self.get_selected_callback_number())
889 (number, "%s (%s)" % (number, description))
890 for (number, description) in self._callbackList
892 defaultSelection = userOptions.get(actualSelection, actualSelection)
894 userSelection = hildonize.touch_selector_entry(
897 list(userOptions.itervalues()),
900 reversedUserOptions = dict(
901 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
903 selectedNumber = reversedUserOptions.get(userSelection, userSelection)
905 number = make_ugly(selectedNumber)
906 self._set_callback_number(number)
907 except RuntimeError, e:
908 _moduleLogger.exception("%s" % str(e))
910 self._errorDisplay.push_exception()
912 def _on_notify_toggled(self, *args):
914 if self._applyAlarmTimeoutId is not None:
915 gobject.source_remove(self._applyAlarmTimeoutId)
916 self._applyAlarmTimeoutId = None
917 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
919 self._errorDisplay.push_exception()
921 def _on_minutes_clicked(self, *args):
922 recurrenceChoices = [
938 actualSelection = self._alarmHandler.recurrence
940 closestSelectionIndex = 0
941 for i, possible in enumerate(recurrenceChoices):
942 if possible[0] <= actualSelection:
943 closestSelectionIndex = i
944 recurrenceIndex = hildonize.touch_selector(
947 (("%s" % m[1]) for m in recurrenceChoices),
948 closestSelectionIndex,
950 recurrence = recurrenceChoices[recurrenceIndex][0]
952 self._update_alarm_settings(recurrence)
953 except RuntimeError, e:
954 _moduleLogger.exception("%s" % str(e))
956 self._errorDisplay.push_exception()
958 def _on_apply_timeout(self, *args):
960 self._applyAlarmTimeoutId = None
962 self._update_alarm_settings(self._alarmHandler.recurrence)
964 self._errorDisplay.push_exception()
967 def _on_missed_toggled(self, *args):
969 self._notifyOnMissed = self._missedCheckbox.get_active()
970 self.save_everything()
972 self._errorDisplay.push_exception()
974 def _on_voicemail_toggled(self, *args):
976 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
977 self.save_everything()
979 self._errorDisplay.push_exception()
981 def _on_sms_toggled(self, *args):
983 self._notifyOnSms = self._smsCheckbox.get_active()
984 self.save_everything()
986 self._errorDisplay.push_exception()
989 class RecentCallsView(object):
996 def __init__(self, widgetTree, backend, errorDisplay):
997 self._errorDisplay = errorDisplay
998 self._backend = backend
1000 self._isPopulated = False
1001 self._recentmodel = gtk.ListStore(
1002 gobject.TYPE_STRING, # number
1003 gobject.TYPE_STRING, # date
1004 gobject.TYPE_STRING, # action
1005 gobject.TYPE_STRING, # from
1007 self._recentview = widgetTree.get_widget("recentview")
1008 self._recentviewselection = None
1009 self._onRecentviewRowActivatedId = 0
1011 textrenderer = gtk.CellRendererText()
1012 textrenderer.set_property("yalign", 0)
1013 self._dateColumn = gtk.TreeViewColumn("Date")
1014 self._dateColumn.pack_start(textrenderer, expand=True)
1015 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
1017 textrenderer = gtk.CellRendererText()
1018 textrenderer.set_property("yalign", 0)
1019 self._actionColumn = gtk.TreeViewColumn("Action")
1020 self._actionColumn.pack_start(textrenderer, expand=True)
1021 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
1023 textrenderer = gtk.CellRendererText()
1024 textrenderer.set_property("yalign", 0)
1025 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
1026 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
1027 self._numberColumn = gtk.TreeViewColumn("Number")
1028 self._numberColumn.pack_start(textrenderer, expand=True)
1029 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
1031 textrenderer = gtk.CellRendererText()
1032 textrenderer.set_property("yalign", 0)
1033 hildonize.set_cell_thumb_selectable(textrenderer)
1034 self._nameColumn = gtk.TreeViewColumn("From")
1035 self._nameColumn.pack_start(textrenderer, expand=True)
1036 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
1037 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1039 self._window = gtk_toolbox.find_parent_window(self._recentview)
1040 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1042 self._updateSink = gtk_toolbox.threaded_stage(
1044 self._idly_populate_recentview,
1045 gtk_toolbox.null_sink(),
1050 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1051 self._recentview.set_model(self._recentmodel)
1053 self._recentview.append_column(self._dateColumn)
1054 self._recentview.append_column(self._actionColumn)
1055 self._recentview.append_column(self._numberColumn)
1056 self._recentview.append_column(self._nameColumn)
1057 self._recentviewselection = self._recentview.get_selection()
1058 self._recentviewselection.set_mode(gtk.SELECTION_NONE)
1060 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
1063 self._recentview.disconnect(self._onRecentviewRowActivatedId)
1067 self._recentview.remove_column(self._dateColumn)
1068 self._recentview.remove_column(self._actionColumn)
1069 self._recentview.remove_column(self._nameColumn)
1070 self._recentview.remove_column(self._numberColumn)
1071 self._recentview.set_model(None)
1073 def number_selected(self, action, number, message):
1075 @note Actual dial function is patched in later
1077 raise NotImplementedError("Horrible unknown error has occurred")
1079 def update(self, force = False):
1080 if not force and self._isPopulated:
1082 self._updateSink.send(())
1086 self._isPopulated = False
1087 self._recentmodel.clear()
1091 return "Recent Calls"
1093 def load_settings(self, config, section):
1096 def save_settings(self, config, section):
1098 @note Thread Agnostic
1102 def _idly_populate_recentview(self):
1103 with gtk_toolbox.gtk_lock():
1104 banner = hildonize.show_busy_banner_start(self._window, "Loading Recent History")
1106 self._recentmodel.clear()
1107 self._isPopulated = True
1110 recentItems = self._backend.get_recent()
1111 except Exception, e:
1112 self._errorDisplay.push_exception_with_lock()
1113 self._isPopulated = False
1117 gv_backend.decorate_recent(data)
1118 for data in gv_backend.sort_messages(recentItems)
1121 for personName, phoneNumber, date, action in recentItems:
1123 personName = "Unknown"
1124 date = abbrev_relative_date(date)
1125 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1126 prettyNumber = make_pretty(prettyNumber)
1127 item = (prettyNumber, date, action.capitalize(), personName)
1128 with gtk_toolbox.gtk_lock():
1129 self._recentmodel.append(item)
1130 except Exception, e:
1131 self._errorDisplay.push_exception_with_lock()
1133 with gtk_toolbox.gtk_lock():
1134 hildonize.show_busy_banner_end(banner)
1138 def _on_recentview_row_activated(self, treeview, path, view_column):
1140 itr = self._recentmodel.get_iter(path)
1144 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1145 number = make_ugly(number)
1146 contactPhoneNumbers = [("Phone", number)]
1147 description = self._recentmodel.get_value(itr, self.FROM_IDX)
1149 action, phoneNumber, message = self._phoneTypeSelector.run(
1150 contactPhoneNumbers,
1151 messages = (description, ),
1152 parent = self._window,
1154 if action == PhoneTypeSelector.ACTION_CANCEL:
1156 assert phoneNumber, "A lack of phone number exists"
1158 self.number_selected(action, phoneNumber, message)
1159 self._recentviewselection.unselect_all()
1160 except Exception, e:
1161 self._errorDisplay.push_exception()
1164 class MessagesView(object):
1172 def __init__(self, widgetTree, backend, errorDisplay):
1173 self._errorDisplay = errorDisplay
1174 self._backend = backend
1176 self._isPopulated = False
1177 self._messagemodel = gtk.ListStore(
1178 gobject.TYPE_STRING, # number
1179 gobject.TYPE_STRING, # date
1180 gobject.TYPE_STRING, # header
1181 gobject.TYPE_STRING, # message
1184 self._messageview = widgetTree.get_widget("messages_view")
1185 self._messageviewselection = None
1186 self._onMessageviewRowActivatedId = 0
1188 self._messageRenderer = gtk.CellRendererText()
1189 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1190 self._messageRenderer.set_property("wrap-width", 500)
1191 self._messageColumn = gtk.TreeViewColumn("Messages")
1192 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1193 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1194 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1196 self._window = gtk_toolbox.find_parent_window(self._messageview)
1197 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1199 self._updateSink = gtk_toolbox.threaded_stage(
1201 self._idly_populate_messageview,
1202 gtk_toolbox.null_sink(),
1207 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1208 self._messageview.set_model(self._messagemodel)
1209 self._messageview.set_headers_visible(False)
1211 self._messageview.append_column(self._messageColumn)
1212 self._messageviewselection = self._messageview.get_selection()
1213 self._messageviewselection.set_mode(gtk.SELECTION_NONE)
1215 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1218 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1222 self._messageview.remove_column(self._messageColumn)
1223 self._messageview.set_model(None)
1225 def number_selected(self, action, number, message):
1227 @note Actual dial function is patched in later
1229 raise NotImplementedError("Horrible unknown error has occurred")
1231 def update(self, force = False):
1232 if not force and self._isPopulated:
1234 self._updateSink.send(())
1238 self._isPopulated = False
1239 self._messagemodel.clear()
1245 def load_settings(self, config, section):
1248 def save_settings(self, config, section):
1250 @note Thread Agnostic
1254 _MIN_MESSAGES_SHOWN = 4
1256 def _idly_populate_messageview(self):
1257 with gtk_toolbox.gtk_lock():
1258 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1260 self._messagemodel.clear()
1261 self._isPopulated = True
1264 messageItems = self._backend.get_messages()
1265 except Exception, e:
1266 self._errorDisplay.push_exception_with_lock()
1267 self._isPopulated = False
1271 gv_backend.decorate_message(message)
1272 for message in gv_backend.sort_messages(messageItems)
1275 for header, number, relativeDate, messages in messageItems:
1276 prettyNumber = number[2:] if number.startswith("+1") else number
1277 prettyNumber = make_pretty(prettyNumber)
1279 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1280 expandedMessages = [firstMessage]
1281 expandedMessages.extend(messages)
1282 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1283 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1284 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1285 collapsedMessages = [firstMessage, secondMessage]
1286 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1288 collapsedMessages = expandedMessages
1290 number = make_ugly(number)
1292 row = (number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages)
1293 with gtk_toolbox.gtk_lock():
1294 self._messagemodel.append(row)
1295 except Exception, e:
1296 self._errorDisplay.push_exception_with_lock()
1298 with gtk_toolbox.gtk_lock():
1299 hildonize.show_busy_banner_end(banner)
1303 def _on_messageview_row_activated(self, treeview, path, view_column):
1305 itr = self._messagemodel.get_iter(path)
1309 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1310 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1312 action, phoneNumber, message = self._phoneTypeSelector.run(
1313 contactPhoneNumbers,
1314 messages = description,
1315 parent = self._window,
1317 if action == PhoneTypeSelector.ACTION_CANCEL:
1319 assert phoneNumber, "A lock of phone number exists"
1321 self.number_selected(action, phoneNumber, message)
1322 self._messageviewselection.unselect_all()
1323 except Exception, e:
1324 self._errorDisplay.push_exception()
1327 class ContactsView(object):
1329 def __init__(self, widgetTree, backend, errorDisplay):
1330 self._errorDisplay = errorDisplay
1331 self._backend = backend
1333 self._addressBook = None
1334 self._selectedComboIndex = 0
1335 self._addressBookFactories = [null_backend.NullAddressBook()]
1337 self._booksList = []
1338 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1340 self._isPopulated = False
1341 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1342 self._contactsviewselection = None
1343 self._contactsview = widgetTree.get_widget("contactsview")
1345 self._contactColumn = gtk.TreeViewColumn("Contact")
1346 displayContactSource = False
1347 if displayContactSource:
1348 textrenderer = gtk.CellRendererText()
1349 self._contactColumn.pack_start(textrenderer, expand=False)
1350 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1351 textrenderer = gtk.CellRendererText()
1352 hildonize.set_cell_thumb_selectable(textrenderer)
1353 self._contactColumn.pack_start(textrenderer, expand=True)
1354 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1355 textrenderer = gtk.CellRendererText()
1356 self._contactColumn.pack_start(textrenderer, expand=True)
1357 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1358 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1359 self._contactColumn.set_sort_column_id(1)
1360 self._contactColumn.set_visible(True)
1362 self._onContactsviewRowActivatedId = 0
1363 self._onAddressbookButtonChangedId = 0
1364 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1365 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1367 self._updateSink = gtk_toolbox.threaded_stage(
1369 self._idly_populate_contactsview,
1370 gtk_toolbox.null_sink(),
1375 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1377 self._contactsview.set_model(self._contactsmodel)
1378 self._contactsview.append_column(self._contactColumn)
1379 self._contactsviewselection = self._contactsview.get_selection()
1380 self._contactsviewselection.set_mode(gtk.SELECTION_NONE)
1382 del self._booksList[:]
1383 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1384 if factoryName and bookName:
1385 entryName = "%s: %s" % (factoryName, bookName)
1387 entryName = factoryName
1389 entryName = bookName
1391 entryName = "Bad name (%d)" % factoryId
1392 row = (str(factoryId), bookId, entryName)
1393 self._booksList.append(row)
1395 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1396 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1398 if len(self._booksList) <= self._selectedComboIndex:
1399 self._selectedComboIndex = 0
1400 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1402 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1403 selectedBookId = self._booksList[self._selectedComboIndex][1]
1404 self.open_addressbook(selectedFactoryId, selectedBookId)
1407 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1408 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1412 self._bookSelectionButton.set_label("")
1413 self._contactsview.set_model(None)
1414 self._contactsview.remove_column(self._contactColumn)
1416 def number_selected(self, action, number, message):
1418 @note Actual dial function is patched in later
1420 raise NotImplementedError("Horrible unknown error has occurred")
1422 def get_addressbooks(self):
1424 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1426 for i, factory in enumerate(self._addressBookFactories):
1427 for bookFactory, bookId, bookName in factory.get_addressbooks():
1428 yield (str(i), bookId), (factory.factory_name(), bookName)
1430 def open_addressbook(self, bookFactoryId, bookId):
1431 bookFactoryIndex = int(bookFactoryId)
1432 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1434 forceUpdate = True if addressBook is not self._addressBook else False
1436 self._addressBook = addressBook
1437 self.update(force=forceUpdate)
1439 def update(self, force = False):
1440 if not force and self._isPopulated:
1442 self._updateSink.send(())
1446 self._isPopulated = False
1447 self._contactsmodel.clear()
1448 for factory in self._addressBookFactories:
1449 factory.clear_caches()
1450 self._addressBook.clear_caches()
1452 def append(self, book):
1453 self._addressBookFactories.append(book)
1455 def extend(self, books):
1456 self._addressBookFactories.extend(books)
1462 def load_settings(self, config, sectionName):
1464 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1465 except ConfigParser.NoOptionError:
1466 self._selectedComboIndex = 0
1468 def save_settings(self, config, sectionName):
1469 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1471 def _idly_populate_contactsview(self):
1472 with gtk_toolbox.gtk_lock():
1473 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1476 while addressBook is not self._addressBook:
1477 addressBook = self._addressBook
1478 with gtk_toolbox.gtk_lock():
1479 self._contactsview.set_model(None)
1483 contacts = addressBook.get_contacts()
1484 except Exception, e:
1486 self._isPopulated = False
1487 self._errorDisplay.push_exception_with_lock()
1488 for contactId, contactName in contacts:
1489 contactType = (addressBook.contact_source_short_name(contactId), )
1490 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1492 with gtk_toolbox.gtk_lock():
1493 self._contactsview.set_model(self._contactsmodel)
1495 self._isPopulated = True
1496 except Exception, e:
1497 self._errorDisplay.push_exception_with_lock()
1499 with gtk_toolbox.gtk_lock():
1500 hildonize.show_busy_banner_end(banner)
1503 def _on_addressbook_button_changed(self, *args, **kwds):
1506 newSelectedComboIndex = hildonize.touch_selector(
1509 (("%s" % m[2]) for m in self._booksList),
1510 self._selectedComboIndex,
1512 except RuntimeError:
1515 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1516 selectedBookId = self._booksList[newSelectedComboIndex][1]
1517 self.open_addressbook(selectedFactoryId, selectedBookId)
1518 self._selectedComboIndex = newSelectedComboIndex
1519 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1520 except Exception, e:
1521 self._errorDisplay.push_exception()
1523 def _on_contactsview_row_activated(self, treeview, path, view_column):
1525 itr = self._contactsmodel.get_iter(path)
1529 contactId = self._contactsmodel.get_value(itr, 3)
1530 contactName = self._contactsmodel.get_value(itr, 1)
1532 contactDetails = self._addressBook.get_contact_details(contactId)
1533 except Exception, e:
1535 self._errorDisplay.push_exception()
1536 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1538 if len(contactPhoneNumbers) == 0:
1541 action, phoneNumber, message = self._phoneTypeSelector.run(
1542 contactPhoneNumbers,
1543 messages = (contactName, ),
1544 parent = self._window,
1546 if action == PhoneTypeSelector.ACTION_CANCEL:
1548 assert phoneNumber, "A lack of phone number exists"
1550 self.number_selected(action, phoneNumber, message)
1551 self._contactsviewselection.unselect_all()
1552 except Exception, e:
1553 self._errorDisplay.push_exception()