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)
1108 self._messageview.set_fixed_height_mode(False)
1110 self._messageview.append_column(self._messageColumn)
1111 self._messageviewselection = self._messageview.get_selection()
1112 self._messageviewselection.set_mode(gtk.SELECTION_NONE)
1114 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1117 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1121 self._messageview.remove_column(self._messageColumn)
1122 self._messageview.set_model(None)
1124 def number_selected(self, action, number, message):
1126 @note Actual dial function is patched in later
1128 raise NotImplementedError("Horrible unknown error has occurred")
1130 def update(self, force = False):
1131 if not force and self._isPopulated:
1133 self._updateSink.send(())
1137 self._isPopulated = False
1138 self._messagemodel.clear()
1144 def load_settings(self, config, section):
1147 def save_settings(self, config, section):
1149 @note Thread Agnostic
1153 _MIN_MESSAGES_SHOWN = 4
1155 def _idly_populate_messageview(self):
1156 with gtk_toolbox.gtk_lock():
1157 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1159 self._messagemodel.clear()
1160 self._isPopulated = True
1163 messageItems = self._backend.get_messages()
1164 except Exception, e:
1165 self._errorDisplay.push_exception_with_lock()
1166 self._isPopulated = False
1170 gv_backend.decorate_message(message)
1171 for message in gv_backend.sort_messages(messageItems)
1174 for header, number, relativeDate, messages in messageItems:
1175 prettyNumber = number[2:] if number.startswith("+1") else number
1176 prettyNumber = make_pretty(prettyNumber)
1178 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1179 expandedMessages = [firstMessage]
1180 expandedMessages.extend(messages)
1181 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1182 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1183 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1184 collapsedMessages = [firstMessage, secondMessage]
1185 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1187 collapsedMessages = expandedMessages
1189 number = make_ugly(number)
1191 row = (number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages)
1192 with gtk_toolbox.gtk_lock():
1193 self._messagemodel.append(row)
1194 except Exception, e:
1195 self._errorDisplay.push_exception_with_lock()
1197 with gtk_toolbox.gtk_lock():
1198 hildonize.show_busy_banner_end(banner)
1202 def _on_messageview_row_activated(self, treeview, path, view_column):
1204 itr = self._messagemodel.get_iter(path)
1208 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1209 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1211 action, phoneNumber, message = self._phoneTypeSelector.run(
1212 contactPhoneNumbers,
1213 messages = description,
1214 parent = self._window,
1216 if action == SmsEntryDialog.ACTION_CANCEL:
1218 assert phoneNumber, "A lock of phone number exists"
1220 self.number_selected(action, phoneNumber, message)
1221 self._messageviewselection.unselect_all()
1222 except Exception, e:
1223 self._errorDisplay.push_exception()
1226 class ContactsView(object):
1228 def __init__(self, widgetTree, backend, errorDisplay):
1229 self._errorDisplay = errorDisplay
1230 self._backend = backend
1232 self._addressBook = None
1233 self._selectedComboIndex = 0
1234 self._addressBookFactories = [null_backend.NullAddressBook()]
1236 self._booksList = []
1237 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1239 self._isPopulated = False
1240 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1241 self._contactsviewselection = None
1242 self._contactsview = widgetTree.get_widget("contactsview")
1244 self._contactColumn = gtk.TreeViewColumn("Contact")
1245 displayContactSource = False
1246 if displayContactSource:
1247 textrenderer = gtk.CellRendererText()
1248 self._contactColumn.pack_start(textrenderer, expand=False)
1249 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1250 textrenderer = gtk.CellRendererText()
1251 hildonize.set_cell_thumb_selectable(textrenderer)
1252 self._contactColumn.pack_start(textrenderer, expand=True)
1253 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1254 textrenderer = gtk.CellRendererText()
1255 self._contactColumn.pack_start(textrenderer, expand=True)
1256 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1257 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1258 self._contactColumn.set_sort_column_id(1)
1259 self._contactColumn.set_visible(True)
1261 self._onContactsviewRowActivatedId = 0
1262 self._onAddressbookButtonChangedId = 0
1263 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1264 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
1266 self._updateSink = gtk_toolbox.threaded_stage(
1268 self._idly_populate_contactsview,
1269 gtk_toolbox.null_sink(),
1274 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1276 self._contactsview.set_model(self._contactsmodel)
1277 self._contactsview.append_column(self._contactColumn)
1278 self._contactsviewselection = self._contactsview.get_selection()
1279 self._contactsviewselection.set_mode(gtk.SELECTION_NONE)
1281 del self._booksList[:]
1282 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1283 if factoryName and bookName:
1284 entryName = "%s: %s" % (factoryName, bookName)
1286 entryName = factoryName
1288 entryName = bookName
1290 entryName = "Bad name (%d)" % factoryId
1291 row = (str(factoryId), bookId, entryName)
1292 self._booksList.append(row)
1294 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1295 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1297 if len(self._booksList) <= self._selectedComboIndex:
1298 self._selectedComboIndex = 0
1299 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1301 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1302 selectedBookId = self._booksList[self._selectedComboIndex][1]
1303 self.open_addressbook(selectedFactoryId, selectedBookId)
1306 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1307 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1311 self._bookSelectionButton.set_label("")
1312 self._contactsview.set_model(None)
1313 self._contactsview.remove_column(self._contactColumn)
1315 def number_selected(self, action, number, message):
1317 @note Actual dial function is patched in later
1319 raise NotImplementedError("Horrible unknown error has occurred")
1321 def get_addressbooks(self):
1323 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1325 for i, factory in enumerate(self._addressBookFactories):
1326 for bookFactory, bookId, bookName in factory.get_addressbooks():
1327 yield (str(i), bookId), (factory.factory_name(), bookName)
1329 def open_addressbook(self, bookFactoryId, bookId):
1330 bookFactoryIndex = int(bookFactoryId)
1331 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1333 forceUpdate = True if addressBook is not self._addressBook else False
1335 self._addressBook = addressBook
1336 self.update(force=forceUpdate)
1338 def update(self, force = False):
1339 if not force and self._isPopulated:
1341 self._updateSink.send(())
1345 self._isPopulated = False
1346 self._contactsmodel.clear()
1347 for factory in self._addressBookFactories:
1348 factory.clear_caches()
1349 self._addressBook.clear_caches()
1351 def append(self, book):
1352 self._addressBookFactories.append(book)
1354 def extend(self, books):
1355 self._addressBookFactories.extend(books)
1361 def load_settings(self, config, sectionName):
1363 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1364 except ConfigParser.NoOptionError:
1365 self._selectedComboIndex = 0
1367 def save_settings(self, config, sectionName):
1368 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1370 def _idly_populate_contactsview(self):
1371 with gtk_toolbox.gtk_lock():
1372 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1375 while addressBook is not self._addressBook:
1376 addressBook = self._addressBook
1377 with gtk_toolbox.gtk_lock():
1378 self._contactsview.set_model(None)
1382 contacts = addressBook.get_contacts()
1383 except Exception, e:
1385 self._isPopulated = False
1386 self._errorDisplay.push_exception_with_lock()
1387 for contactId, contactName in contacts:
1388 contactType = (addressBook.contact_source_short_name(contactId), )
1389 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1391 with gtk_toolbox.gtk_lock():
1392 self._contactsview.set_model(self._contactsmodel)
1394 self._isPopulated = True
1395 except Exception, e:
1396 self._errorDisplay.push_exception_with_lock()
1398 with gtk_toolbox.gtk_lock():
1399 hildonize.show_busy_banner_end(banner)
1402 def _on_addressbook_button_changed(self, *args, **kwds):
1405 newSelectedComboIndex = hildonize.touch_selector(
1408 (("%s" % m[2]) for m in self._booksList),
1409 self._selectedComboIndex,
1411 except RuntimeError:
1414 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1415 selectedBookId = self._booksList[newSelectedComboIndex][1]
1416 self.open_addressbook(selectedFactoryId, selectedBookId)
1417 self._selectedComboIndex = newSelectedComboIndex
1418 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1419 except Exception, e:
1420 self._errorDisplay.push_exception()
1422 def _on_contactsview_row_activated(self, treeview, path, view_column):
1424 itr = self._contactsmodel.get_iter(path)
1428 contactId = self._contactsmodel.get_value(itr, 3)
1429 contactName = self._contactsmodel.get_value(itr, 1)
1431 contactDetails = self._addressBook.get_contact_details(contactId)
1432 except Exception, e:
1434 self._errorDisplay.push_exception()
1435 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1437 if len(contactPhoneNumbers) == 0:
1440 action, phoneNumber, message = self._phoneTypeSelector.run(
1441 contactPhoneNumbers,
1442 messages = (contactName, ),
1443 parent = self._window,
1445 if action == SmsEntryDialog.ACTION_CANCEL:
1447 assert phoneNumber, "A lack of phone number exists"
1449 self.number_selected(action, phoneNumber, message)
1450 self._contactsviewselection.unselect_all()
1451 except Exception, e:
1452 self._errorDisplay.push_exception()