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 SmsEntryDialog(object):
270 @todo Add multi-SMS messages like GoogleVoice
273 ACTION_CANCEL = "cancel"
274 ACTION_SELECT = "select"
276 ACTION_SEND_SMS = "sms"
280 def __init__(self, widgetTree):
281 self._clipboard = gtk.clipboard_get()
282 self._widgetTree = widgetTree
283 self._dialog = self._widgetTree.get_widget("smsDialog")
285 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
286 self._smsButton.connect("clicked", self._on_send)
287 self._dialButton = self._widgetTree.get_widget("dialButton")
288 self._dialButton.connect("clicked", self._on_dial)
289 self._selectButton = self._widgetTree.get_widget("selectButton")
290 self._selectButton.connect("clicked", self._on_select)
291 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
292 self._cancelButton.connect("clicked", self._on_cancel)
294 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
296 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
297 self._messagesView = self._widgetTree.get_widget("smsMessages")
298 self._scrollWindow = self._messagesView.get_parent()
300 self._phoneButton = self._widgetTree.get_widget("phoneTypeSelection")
301 self._smsEntry = self._widgetTree.get_widget("smsEntry")
303 self._action = self.ACTION_CANCEL
305 self._numberIndex = -1
306 self._contactDetails = []
308 def run(self, contactDetails, messages = (), parent = None):
309 entryConnectId = self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
310 phoneConnectId = self._phoneButton.connect("clicked", self._on_phone)
311 keyConnectId = self._keyPressEventId = self._dialog.connect("key-press-event", self._on_key_press)
313 # Setup the phone selection button
314 del self._contactDetails[:]
315 for phoneType, phoneNumber in contactDetails:
316 display = " - ".join((make_pretty(phoneNumber), phoneType))
317 row = (phoneNumber, display)
318 self._contactDetails.append(row)
319 if 0 < len(self._contactDetails):
320 self._numberIndex = 0
321 self._phoneButton.set_label(self._contactDetails[0][1])
322 self._phoneButton.set_sensitive(True)
324 self._numberIndex = -1
325 self._phoneButton.set_label("Error: No Number Available")
326 self._phoneButton.set_sensitive(False)
328 # Add the column to the messages tree view
329 self._messagemodel.clear()
330 self._messagesView.set_model(self._messagemodel)
332 textrenderer = gtk.CellRendererText()
333 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
334 textrenderer.set_property("wrap-width", 450)
335 messageColumn = gtk.TreeViewColumn("")
336 messageColumn.pack_start(textrenderer, expand=True)
337 messageColumn.add_attribute(textrenderer, "markup", 0)
338 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
339 self._messagesView.append_column(messageColumn)
340 self._messagesView.set_headers_visible(False)
343 for message in messages:
345 self._messagemodel.append(row)
346 self._messagesView.show()
347 self._scrollWindow.show()
348 messagesSelection = self._messagesView.get_selection()
349 messagesSelection.select_path((len(messages)-1, ))
351 self._messagesView.hide()
352 self._scrollWindow.hide()
354 self._smsEntry.get_buffer().set_text("")
355 self._update_letter_count()
357 if parent is not None:
358 self._dialog.set_transient_for(parent)
364 self._messagesView.scroll_to_cell((len(messages)-1, ))
365 self._smsEntry.grab_focus()
367 userResponse = self._dialog.run()
371 # Process the users response
372 if userResponse == gtk.RESPONSE_OK and 0 <= self._numberIndex:
373 phoneNumber = self._contactDetails[self._numberIndex][0]
374 phoneNumber = make_ugly(phoneNumber)
378 self._action = self.ACTION_CANCEL
379 if self._action == self.ACTION_SEND_SMS:
380 entryBuffer = self._smsEntry.get_buffer()
381 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
382 enteredMessage = enteredMessage[0:self.MAX_CHAR].strip()
383 if not enteredMessage:
385 self._action = self.ACTION_CANCEL
389 self._messagesView.remove_column(messageColumn)
390 self._messagesView.set_model(None)
392 return self._action, phoneNumber, enteredMessage
394 self._smsEntry.get_buffer().disconnect(entryConnectId)
395 self._phoneButton.disconnect(phoneConnectId)
396 self._keyPressEventId = self._dialog.disconnect(keyConnectId)
398 def _update_letter_count(self, *args):
399 entryLength = self._smsEntry.get_buffer().get_char_count()
400 charsLeft = self.MAX_CHAR - entryLength
401 self._letterCountLabel.set_text(str(charsLeft))
402 if charsLeft < 0 or charsLeft == self.MAX_CHAR:
403 self._smsButton.set_sensitive(False)
405 self._smsButton.set_sensitive(True)
407 def _on_phone(self, *args):
409 assert 0 <= self._numberIndex, "%r" % self._numberIndex
411 self._numberIndex = hildonize.touch_selector(
414 (description for (number, description) in self._contactDetails),
417 self._phoneButton.set_label(self._contactDetails[self._numberIndex][1])
419 _moduleLogger.exception("%s" % str(e))
421 def _on_entry_changed(self, *args):
422 self._update_letter_count()
424 def _on_send(self, *args):
425 self._dialog.response(gtk.RESPONSE_OK)
427 def _on_dial(self, *args):
428 self._dialog.response(gtk.RESPONSE_OK)
429 self._action = self.ACTION_DIAL
431 def _on_select(self, *args):
432 self._dialog.response(gtk.RESPONSE_OK)
433 self._action = self.ACTION_SELECT
435 def _on_cancel(self, *args):
436 self._dialog.response(gtk.RESPONSE_CANCEL)
437 self._action = self.ACTION_CANCEL
439 def _on_key_press(self, widget, event):
441 if event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
444 for messagePart in self._messagemodel
446 # For some reason this kills clipboard stuff
447 #self._clipboard.set_text(message)
449 _moduleLogger.exception(str(e))
452 class Dialpad(object):
454 def __init__(self, widgetTree, errorDisplay):
455 self._clipboard = gtk.clipboard_get()
456 self._errorDisplay = errorDisplay
457 self._smsDialog = SmsEntryDialog(widgetTree)
459 self._numberdisplay = widgetTree.get_widget("numberdisplay")
460 self._smsButton = widgetTree.get_widget("sms")
461 self._dialButton = widgetTree.get_widget("dial")
462 self._backButton = widgetTree.get_widget("back")
463 self._phonenumber = ""
464 self._prettynumber = ""
467 "on_digit_clicked": self._on_digit_clicked,
469 widgetTree.signal_autoconnect(callbackMapping)
470 self._dialButton.connect("clicked", self._on_dial_clicked)
471 self._smsButton.connect("clicked", self._on_sms_clicked)
473 self._originalLabel = self._backButton.get_label()
474 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
475 self._backTapHandler.on_tap = self._on_backspace
476 self._backTapHandler.on_hold = self._on_clearall
477 self._backTapHandler.on_holding = self._set_clear_button
478 self._backTapHandler.on_cancel = self._reset_back_button
480 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
481 self._keyPressEventId = 0
484 self._dialButton.grab_focus()
485 self._backTapHandler.enable()
486 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
489 self._window.disconnect(self._keyPressEventId)
490 self._keyPressEventId = 0
491 self._reset_back_button()
492 self._backTapHandler.disable()
494 def number_selected(self, action, number, message):
496 @note Actual dial function is patched in later
498 raise NotImplementedError("Horrible unknown error has occurred")
500 def get_number(self):
501 return self._phonenumber
503 def set_number(self, number):
505 Set the number to dial
508 self._phonenumber = make_ugly(number)
509 self._prettynumber = make_pretty(self._phonenumber)
510 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
512 self._errorDisplay.push_exception()
521 def load_settings(self, config, section):
524 def save_settings(self, config, section):
526 @note Thread Agnostic
530 def _on_key_press(self, widget, event):
532 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
533 contents = self._clipboard.wait_for_text()
534 if contents is not None:
535 self.set_number(contents)
537 self._errorDisplay.push_exception()
539 def _on_sms_clicked(self, widget):
541 phoneNumber = self.get_number()
542 action, phoneNumber, message = self._smsDialog.run([("Dialer", phoneNumber)], (), self._window)
544 if action == SmsEntryDialog.ACTION_CANCEL:
546 self.number_selected(action, phoneNumber, message)
548 self._errorDisplay.push_exception()
550 def _on_dial_clicked(self, widget):
552 action = SmsEntryDialog.ACTION_DIAL
553 phoneNumber = self.get_number()
555 self.number_selected(action, phoneNumber, message)
557 self._errorDisplay.push_exception()
559 def _on_digit_clicked(self, widget):
561 self.set_number(self._phonenumber + widget.get_name()[-1])
563 self._errorDisplay.push_exception()
565 def _on_backspace(self, taps):
567 self.set_number(self._phonenumber[:-taps])
568 self._reset_back_button()
570 self._errorDisplay.push_exception()
572 def _on_clearall(self, taps):
575 self._reset_back_button()
577 self._errorDisplay.push_exception()
580 def _set_clear_button(self):
582 self._backButton.set_label("gtk-clear")
584 self._errorDisplay.push_exception()
586 def _reset_back_button(self):
588 self._backButton.set_label(self._originalLabel)
590 self._errorDisplay.push_exception()
593 class AccountInfo(object):
595 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
596 self._errorDisplay = errorDisplay
597 self._backend = backend
598 self._isPopulated = False
599 self._alarmHandler = alarmHandler
600 self._notifyOnMissed = False
601 self._notifyOnVoicemail = False
602 self._notifyOnSms = False
604 self._callbackList = []
605 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
606 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
607 self._onCallbackSelectChangedId = 0
609 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
610 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
611 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
612 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
613 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
614 self._onNotifyToggled = 0
615 self._onMinutesChanged = 0
616 self._onMissedToggled = 0
617 self._onVoicemailToggled = 0
618 self._onSmsToggled = 0
619 self._applyAlarmTimeoutId = None
621 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
622 self._defaultCallback = ""
625 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
627 self._accountViewNumberDisplay.set_use_markup(True)
628 self.set_account_number("")
630 del self._callbackList[:]
631 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
633 if self._alarmHandler is not None:
634 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
635 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
636 self._missedCheckbox.set_active(self._notifyOnMissed)
637 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
638 self._smsCheckbox.set_active(self._notifyOnSms)
640 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
641 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
642 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
643 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
644 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
646 self._notifyCheckbox.set_sensitive(False)
647 self._minutesEntryButton.set_sensitive(False)
648 self._missedCheckbox.set_sensitive(False)
649 self._voicemailCheckbox.set_sensitive(False)
650 self._smsCheckbox.set_sensitive(False)
652 self.update(force=True)
655 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
656 self._onCallbackSelectChangedId = 0
658 if self._alarmHandler is not None:
659 self._notifyCheckbox.disconnect(self._onNotifyToggled)
660 self._minutesEntryButton.disconnect(self._onMinutesChanged)
661 self._missedCheckbox.disconnect(self._onNotifyToggled)
662 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
663 self._smsCheckbox.disconnect(self._onNotifyToggled)
664 self._onNotifyToggled = 0
665 self._onMinutesChanged = 0
666 self._onMissedToggled = 0
667 self._onVoicemailToggled = 0
668 self._onSmsToggled = 0
670 self._notifyCheckbox.set_sensitive(True)
671 self._minutesEntryButton.set_sensitive(True)
672 self._missedCheckbox.set_sensitive(True)
673 self._voicemailCheckbox.set_sensitive(True)
674 self._smsCheckbox.set_sensitive(True)
677 del self._callbackList[:]
679 def get_selected_callback_number(self):
680 currentLabel = self._callbackSelectButton.get_label()
681 if currentLabel is not None:
682 return make_ugly(currentLabel)
686 def set_account_number(self, number):
688 Displays current account number
690 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
692 def update(self, force = False):
693 if not force and self._isPopulated:
695 self._populate_callback_combo()
696 self.set_account_number(self._backend.get_account_number())
700 self._callbackSelectButton.set_label("No Callback Number")
701 self.set_account_number("")
702 self._isPopulated = False
704 def save_everything(self):
705 raise NotImplementedError
709 return "Account Info"
711 def load_settings(self, config, section):
712 self._defaultCallback = config.get(section, "callback")
713 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
714 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
715 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
717 def save_settings(self, config, section):
719 @note Thread Agnostic
721 callback = self.get_selected_callback_number()
722 config.set(section, "callback", callback)
723 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
724 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
725 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
727 def _populate_callback_combo(self):
728 self._isPopulated = True
729 del self._callbackList[:]
731 callbackNumbers = self._backend.get_callback_numbers()
733 self._errorDisplay.push_exception()
734 self._isPopulated = False
737 if len(callbackNumbers) == 0:
738 callbackNumbers = {"": "No callback numbers available"}
740 for number, description in callbackNumbers.iteritems():
741 self._callbackList.append((make_pretty(number), description))
743 self._set_callback_number(self._defaultCallback)
745 def _set_callback_number(self, number):
747 if not self._backend.is_valid_syntax(number) and 0 < len(number):
748 self._errorDisplay.push_message("%s is not a valid callback number" % number)
749 elif number == self._backend.get_callback_number() and 0 < len(number):
750 _moduleLogger.warning(
751 "Callback number already is %s" % (
752 self._backend.get_callback_number(),
756 self._backend.set_callback_number(number)
757 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
758 make_pretty(number), make_pretty(self._backend.get_callback_number())
760 prettyNumber = make_pretty(number)
761 if len(prettyNumber) == 0:
762 prettyNumber = "No Callback Number"
763 self._callbackSelectButton.set_label(prettyNumber)
765 "Callback number set to %s" % (
766 self._backend.get_callback_number(),
770 self._errorDisplay.push_exception()
772 def _update_alarm_settings(self, recurrence):
774 isEnabled = self._notifyCheckbox.get_active()
775 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
776 self._alarmHandler.apply_settings(isEnabled, recurrence)
778 self.save_everything()
779 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
780 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
782 def _on_callbackentry_clicked(self, *args):
784 actualSelection = make_pretty(self.get_selected_callback_number())
787 (number, "%s (%s)" % (number, description))
788 for (number, description) in self._callbackList
790 defaultSelection = userOptions.get(actualSelection, actualSelection)
792 userSelection = hildonize.touch_selector_entry(
795 list(userOptions.itervalues()),
798 reversedUserOptions = dict(
799 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
801 selectedNumber = reversedUserOptions.get(userSelection, userSelection)
803 number = make_ugly(selectedNumber)
804 self._set_callback_number(number)
805 except RuntimeError, e:
806 _moduleLogger.exception("%s" % str(e))
808 self._errorDisplay.push_exception()
810 def _on_notify_toggled(self, *args):
812 if self._applyAlarmTimeoutId is not None:
813 gobject.source_remove(self._applyAlarmTimeoutId)
814 self._applyAlarmTimeoutId = None
815 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
817 self._errorDisplay.push_exception()
819 def _on_minutes_clicked(self, *args):
820 recurrenceChoices = [
836 actualSelection = self._alarmHandler.recurrence
838 closestSelectionIndex = 0
839 for i, possible in enumerate(recurrenceChoices):
840 if possible[0] <= actualSelection:
841 closestSelectionIndex = i
842 recurrenceIndex = hildonize.touch_selector(
845 (("%s" % m[1]) for m in recurrenceChoices),
846 closestSelectionIndex,
848 recurrence = recurrenceChoices[recurrenceIndex][0]
850 self._update_alarm_settings(recurrence)
851 except RuntimeError, e:
852 _moduleLogger.exception("%s" % str(e))
854 self._errorDisplay.push_exception()
856 def _on_apply_timeout(self, *args):
858 self._applyAlarmTimeoutId = None
860 self._update_alarm_settings(self._alarmHandler.recurrence)
862 self._errorDisplay.push_exception()
865 def _on_missed_toggled(self, *args):
867 self._notifyOnMissed = self._missedCheckbox.get_active()
868 self.save_everything()
870 self._errorDisplay.push_exception()
872 def _on_voicemail_toggled(self, *args):
874 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
875 self.save_everything()
877 self._errorDisplay.push_exception()
879 def _on_sms_toggled(self, *args):
881 self._notifyOnSms = self._smsCheckbox.get_active()
882 self.save_everything()
884 self._errorDisplay.push_exception()
887 class RecentCallsView(object):
894 def __init__(self, widgetTree, backend, errorDisplay):
895 self._errorDisplay = errorDisplay
896 self._backend = backend
898 self._isPopulated = False
899 self._recentmodel = gtk.ListStore(
900 gobject.TYPE_STRING, # number
901 gobject.TYPE_STRING, # date
902 gobject.TYPE_STRING, # action
903 gobject.TYPE_STRING, # from
905 self._recentview = widgetTree.get_widget("recentview")
906 self._recentviewselection = None
907 self._onRecentviewRowActivatedId = 0
909 textrenderer = gtk.CellRendererText()
910 textrenderer.set_property("yalign", 0)
911 self._dateColumn = gtk.TreeViewColumn("Date")
912 self._dateColumn.pack_start(textrenderer, expand=True)
913 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
915 textrenderer = gtk.CellRendererText()
916 textrenderer.set_property("yalign", 0)
917 self._actionColumn = gtk.TreeViewColumn("Action")
918 self._actionColumn.pack_start(textrenderer, expand=True)
919 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
921 textrenderer = gtk.CellRendererText()
922 textrenderer.set_property("yalign", 0)
923 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
924 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
925 self._numberColumn = gtk.TreeViewColumn("Number")
926 self._numberColumn.pack_start(textrenderer, expand=True)
927 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
929 textrenderer = gtk.CellRendererText()
930 textrenderer.set_property("yalign", 0)
931 hildonize.set_cell_thumb_selectable(textrenderer)
932 self._nameColumn = gtk.TreeViewColumn("From")
933 self._nameColumn.pack_start(textrenderer, expand=True)
934 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
935 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
937 self._window = gtk_toolbox.find_parent_window(self._recentview)
938 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
940 self._updateSink = gtk_toolbox.threaded_stage(
942 self._idly_populate_recentview,
943 gtk_toolbox.null_sink(),
948 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
949 self._recentview.set_model(self._recentmodel)
951 self._recentview.append_column(self._dateColumn)
952 self._recentview.append_column(self._actionColumn)
953 self._recentview.append_column(self._numberColumn)
954 self._recentview.append_column(self._nameColumn)
955 self._recentviewselection = self._recentview.get_selection()
956 self._recentviewselection.set_mode(gtk.SELECTION_NONE)
958 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
961 self._recentview.disconnect(self._onRecentviewRowActivatedId)
965 self._recentview.remove_column(self._dateColumn)
966 self._recentview.remove_column(self._actionColumn)
967 self._recentview.remove_column(self._nameColumn)
968 self._recentview.remove_column(self._numberColumn)
969 self._recentview.set_model(None)
971 def number_selected(self, action, number, message):
973 @note Actual dial function is patched in later
975 raise NotImplementedError("Horrible unknown error has occurred")
977 def update(self, force = False):
978 if not force and self._isPopulated:
980 self._updateSink.send(())
984 self._isPopulated = False
985 self._recentmodel.clear()
989 return "Recent Calls"
991 def load_settings(self, config, section):
994 def save_settings(self, config, section):
996 @note Thread Agnostic
1000 def _idly_populate_recentview(self):
1001 with gtk_toolbox.gtk_lock():
1002 banner = hildonize.show_busy_banner_start(self._window, "Loading Recent History")
1004 self._recentmodel.clear()
1005 self._isPopulated = True
1008 recentItems = self._backend.get_recent()
1009 except Exception, e:
1010 self._errorDisplay.push_exception_with_lock()
1011 self._isPopulated = False
1015 gv_backend.decorate_recent(data)
1016 for data in gv_backend.sort_messages(recentItems)
1019 for personName, phoneNumber, date, action in recentItems:
1021 personName = "Unknown"
1022 date = abbrev_relative_date(date)
1023 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1024 prettyNumber = make_pretty(prettyNumber)
1025 item = (prettyNumber, date, action.capitalize(), personName)
1026 with gtk_toolbox.gtk_lock():
1027 self._recentmodel.append(item)
1028 except Exception, e:
1029 self._errorDisplay.push_exception_with_lock()
1031 with gtk_toolbox.gtk_lock():
1032 hildonize.show_busy_banner_end(banner)
1036 def _on_recentview_row_activated(self, treeview, path, view_column):
1038 itr = self._recentmodel.get_iter(path)
1042 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1043 number = make_ugly(number)
1044 contactPhoneNumbers = [("Phone", number)]
1045 description = self._recentmodel.get_value(itr, self.FROM_IDX)
1047 action, phoneNumber, message = self._phoneTypeSelector.run(
1048 contactPhoneNumbers,
1049 messages = (description, ),
1050 parent = self._window,
1052 if action == SmsEntryDialog.ACTION_CANCEL:
1054 assert phoneNumber, "A lack of phone number exists"
1056 self.number_selected(action, phoneNumber, message)
1057 self._recentviewselection.unselect_all()
1058 except Exception, e:
1059 self._errorDisplay.push_exception()
1062 class MessagesView(object):
1070 def __init__(self, widgetTree, backend, errorDisplay):
1071 self._errorDisplay = errorDisplay
1072 self._backend = backend
1074 self._isPopulated = False
1075 self._messagemodel = gtk.ListStore(
1076 gobject.TYPE_STRING, # number
1077 gobject.TYPE_STRING, # date
1078 gobject.TYPE_STRING, # header
1079 gobject.TYPE_STRING, # message
1082 self._messageview = widgetTree.get_widget("messages_view")
1083 self._messageviewselection = None
1084 self._onMessageviewRowActivatedId = 0
1086 self._messageRenderer = gtk.CellRendererText()
1087 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1088 self._messageRenderer.set_property("wrap-width", 500)
1089 self._messageColumn = gtk.TreeViewColumn("Messages")
1090 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1091 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1092 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1094 self._window = gtk_toolbox.find_parent_window(self._messageview)
1095 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
1097 self._updateSink = gtk_toolbox.threaded_stage(
1099 self._idly_populate_messageview,
1100 gtk_toolbox.null_sink(),
1105 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1106 self._messageview.set_model(self._messagemodel)
1107 self._messageview.set_headers_visible(False)
1109 self._messageview.append_column(self._messageColumn)
1110 self._messageviewselection = self._messageview.get_selection()
1111 self._messageviewselection.set_mode(gtk.SELECTION_NONE)
1113 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1116 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1120 self._messageview.remove_column(self._messageColumn)
1121 self._messageview.set_model(None)
1123 def number_selected(self, action, number, message):
1125 @note Actual dial function is patched in later
1127 raise NotImplementedError("Horrible unknown error has occurred")
1129 def update(self, force = False):
1130 if not force and self._isPopulated:
1132 self._updateSink.send(())
1136 self._isPopulated = False
1137 self._messagemodel.clear()
1143 def load_settings(self, config, section):
1146 def save_settings(self, config, section):
1148 @note Thread Agnostic
1152 _MIN_MESSAGES_SHOWN = 4
1154 def _idly_populate_messageview(self):
1155 with gtk_toolbox.gtk_lock():
1156 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1158 self._messagemodel.clear()
1159 self._isPopulated = True
1162 messageItems = self._backend.get_messages()
1163 except Exception, e:
1164 self._errorDisplay.push_exception_with_lock()
1165 self._isPopulated = False
1169 gv_backend.decorate_message(message)
1170 for message in gv_backend.sort_messages(messageItems)
1173 for header, number, relativeDate, messages in messageItems:
1174 prettyNumber = number[2:] if number.startswith("+1") else number
1175 prettyNumber = make_pretty(prettyNumber)
1177 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1178 expandedMessages = [firstMessage]
1179 expandedMessages.extend(messages)
1180 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1181 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1182 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1183 collapsedMessages = [firstMessage, secondMessage]
1184 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1186 collapsedMessages = expandedMessages
1188 number = make_ugly(number)
1190 row = (number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages)
1191 with gtk_toolbox.gtk_lock():
1192 self._messagemodel.append(row)
1193 except Exception, e:
1194 self._errorDisplay.push_exception_with_lock()
1196 with gtk_toolbox.gtk_lock():
1197 hildonize.show_busy_banner_end(banner)
1201 def _on_messageview_row_activated(self, treeview, path, view_column):
1203 itr = self._messagemodel.get_iter(path)
1207 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1208 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1210 action, phoneNumber, message = self._phoneTypeSelector.run(
1211 contactPhoneNumbers,
1212 messages = description,
1213 parent = self._window,
1215 if action == SmsEntryDialog.ACTION_CANCEL:
1217 assert phoneNumber, "A lock of phone number exists"
1219 self.number_selected(action, phoneNumber, message)
1220 self._messageviewselection.unselect_all()
1221 except Exception, e:
1222 self._errorDisplay.push_exception()
1225 class ContactsView(object):
1227 def __init__(self, widgetTree, backend, errorDisplay):
1228 self._errorDisplay = errorDisplay
1229 self._backend = backend
1231 self._addressBook = None
1232 self._selectedComboIndex = 0
1233 self._addressBookFactories = [null_backend.NullAddressBook()]
1235 self._booksList = []
1236 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1238 self._isPopulated = False
1239 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1240 self._contactsviewselection = None
1241 self._contactsview = widgetTree.get_widget("contactsview")
1243 self._contactColumn = gtk.TreeViewColumn("Contact")
1244 displayContactSource = False
1245 if displayContactSource:
1246 textrenderer = gtk.CellRendererText()
1247 self._contactColumn.pack_start(textrenderer, expand=False)
1248 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1249 textrenderer = gtk.CellRendererText()
1250 hildonize.set_cell_thumb_selectable(textrenderer)
1251 self._contactColumn.pack_start(textrenderer, expand=True)
1252 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1253 textrenderer = gtk.CellRendererText()
1254 self._contactColumn.pack_start(textrenderer, expand=True)
1255 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1256 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1257 self._contactColumn.set_sort_column_id(1)
1258 self._contactColumn.set_visible(True)
1260 self._onContactsviewRowActivatedId = 0
1261 self._onAddressbookButtonChangedId = 0
1262 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1263 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
1265 self._updateSink = gtk_toolbox.threaded_stage(
1267 self._idly_populate_contactsview,
1268 gtk_toolbox.null_sink(),
1273 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1275 self._contactsview.set_model(self._contactsmodel)
1276 self._contactsview.append_column(self._contactColumn)
1277 self._contactsviewselection = self._contactsview.get_selection()
1278 self._contactsviewselection.set_mode(gtk.SELECTION_NONE)
1280 del self._booksList[:]
1281 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1282 if factoryName and bookName:
1283 entryName = "%s: %s" % (factoryName, bookName)
1285 entryName = factoryName
1287 entryName = bookName
1289 entryName = "Bad name (%d)" % factoryId
1290 row = (str(factoryId), bookId, entryName)
1291 self._booksList.append(row)
1293 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1294 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1296 if len(self._booksList) <= self._selectedComboIndex:
1297 self._selectedComboIndex = 0
1298 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1300 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1301 selectedBookId = self._booksList[self._selectedComboIndex][1]
1302 self.open_addressbook(selectedFactoryId, selectedBookId)
1305 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1306 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1310 self._bookSelectionButton.set_label("")
1311 self._contactsview.set_model(None)
1312 self._contactsview.remove_column(self._contactColumn)
1314 def number_selected(self, action, number, message):
1316 @note Actual dial function is patched in later
1318 raise NotImplementedError("Horrible unknown error has occurred")
1320 def get_addressbooks(self):
1322 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1324 for i, factory in enumerate(self._addressBookFactories):
1325 for bookFactory, bookId, bookName in factory.get_addressbooks():
1326 yield (str(i), bookId), (factory.factory_name(), bookName)
1328 def open_addressbook(self, bookFactoryId, bookId):
1329 bookFactoryIndex = int(bookFactoryId)
1330 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1332 forceUpdate = True if addressBook is not self._addressBook else False
1334 self._addressBook = addressBook
1335 self.update(force=forceUpdate)
1337 def update(self, force = False):
1338 if not force and self._isPopulated:
1340 self._updateSink.send(())
1344 self._isPopulated = False
1345 self._contactsmodel.clear()
1346 for factory in self._addressBookFactories:
1347 factory.clear_caches()
1348 self._addressBook.clear_caches()
1350 def append(self, book):
1351 self._addressBookFactories.append(book)
1353 def extend(self, books):
1354 self._addressBookFactories.extend(books)
1360 def load_settings(self, config, sectionName):
1362 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1363 except ConfigParser.NoOptionError:
1364 self._selectedComboIndex = 0
1366 def save_settings(self, config, sectionName):
1367 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1369 def _idly_populate_contactsview(self):
1370 with gtk_toolbox.gtk_lock():
1371 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1374 while addressBook is not self._addressBook:
1375 addressBook = self._addressBook
1376 with gtk_toolbox.gtk_lock():
1377 self._contactsview.set_model(None)
1381 contacts = addressBook.get_contacts()
1382 except Exception, e:
1384 self._isPopulated = False
1385 self._errorDisplay.push_exception_with_lock()
1386 for contactId, contactName in contacts:
1387 contactType = (addressBook.contact_source_short_name(contactId), )
1388 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1390 with gtk_toolbox.gtk_lock():
1391 self._contactsview.set_model(self._contactsmodel)
1393 self._isPopulated = True
1394 except Exception, e:
1395 self._errorDisplay.push_exception_with_lock()
1397 with gtk_toolbox.gtk_lock():
1398 hildonize.show_busy_banner_end(banner)
1401 def _on_addressbook_button_changed(self, *args, **kwds):
1404 newSelectedComboIndex = hildonize.touch_selector(
1407 (("%s" % m[2]) for m in self._booksList),
1408 self._selectedComboIndex,
1410 except RuntimeError:
1413 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1414 selectedBookId = self._booksList[newSelectedComboIndex][1]
1415 self.open_addressbook(selectedFactoryId, selectedBookId)
1416 self._selectedComboIndex = newSelectedComboIndex
1417 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1418 except Exception, e:
1419 self._errorDisplay.push_exception()
1421 def _on_contactsview_row_activated(self, treeview, path, view_column):
1423 itr = self._contactsmodel.get_iter(path)
1427 contactId = self._contactsmodel.get_value(itr, 3)
1428 contactName = self._contactsmodel.get_value(itr, 1)
1430 contactDetails = self._addressBook.get_contact_details(contactId)
1431 except Exception, e:
1433 self._errorDisplay.push_exception()
1434 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1436 if len(contactPhoneNumbers) == 0:
1439 action, phoneNumber, message = self._phoneTypeSelector.run(
1440 contactPhoneNumbers,
1441 messages = (contactName, ),
1442 parent = self._window,
1444 if action == SmsEntryDialog.ACTION_CANCEL:
1446 assert phoneNumber, "A lack of phone number exists"
1448 self.number_selected(action, phoneNumber, message)
1449 self._contactsviewselection.unselect_all()
1450 except Exception, e:
1451 self._errorDisplay.push_exception()