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