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"
275 ACTION_SEND_SMS = "sms"
279 def __init__(self, widgetTree):
280 self._clipboard = gtk.clipboard_get()
281 self._widgetTree = widgetTree
282 self._dialog = self._widgetTree.get_widget("smsDialog")
284 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
285 self._smsButton.connect("clicked", self._on_send)
286 self._dialButton = self._widgetTree.get_widget("dialButton")
287 self._dialButton.connect("clicked", self._on_dial)
288 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
289 self._cancelButton.connect("clicked", self._on_cancel)
291 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
293 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
294 self._messagesView = self._widgetTree.get_widget("smsMessages")
296 self._conversationView = self._messagesView.get_parent()
297 self._conversationViewPort = self._conversationView.get_parent()
298 self._scrollWindow = self._conversationViewPort.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, defaultIndex = -1):
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 = defaultIndex if defaultIndex != -1 else 0
321 self._phoneButton.set_label(self._contactDetails[self._numberIndex][1])
323 self._numberIndex = -1
324 self._phoneButton.set_label("Error: No Number Available")
326 # Add the column to the messages tree view
327 self._messagemodel.clear()
328 self._messagesView.set_model(self._messagemodel)
329 self._messagesView.set_fixed_height_mode(False)
331 textrenderer = gtk.CellRendererText()
332 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
333 textrenderer.set_property("wrap-width", 450)
334 messageColumn = gtk.TreeViewColumn("")
335 messageColumn.pack_start(textrenderer, expand=True)
336 messageColumn.add_attribute(textrenderer, "markup", 0)
337 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
338 self._messagesView.append_column(messageColumn)
339 self._messagesView.set_headers_visible(False)
342 for message in messages:
344 self._messagemodel.append(row)
345 self._messagesView.show()
346 self._scrollWindow.show()
347 messagesSelection = self._messagesView.get_selection()
348 messagesSelection.select_path((len(messages)-1, ))
350 self._messagesView.hide()
351 self._scrollWindow.hide()
353 self._smsEntry.get_buffer().set_text("")
354 self._update_letter_count()
356 if parent is not None:
357 self._dialog.set_transient_for(parent)
358 parentSize = parent.get_size()
359 self._dialog.resize(parentSize[0], max(parentSize[1]-10, 100))
363 self._dialog.show_all()
364 self._smsEntry.grab_focus()
365 adjustment = self._scrollWindow.get_vadjustment()
366 dx = self._conversationView.get_allocation().height - self._conversationViewPort.get_allocation().height
368 adjustment.value = dx
370 if 1 < len(self._contactDetails):
371 if defaultIndex == -1:
372 self._request_number()
373 self._phoneButton.set_sensitive(True)
375 self._phoneButton.set_sensitive(False)
377 userResponse = self._dialog.run()
379 self._dialog.hide_all()
381 # Process the users response
382 if userResponse == gtk.RESPONSE_OK and 0 <= self._numberIndex:
383 phoneNumber = self._contactDetails[self._numberIndex][0]
384 phoneNumber = make_ugly(phoneNumber)
388 self._action = self.ACTION_CANCEL
389 if self._action == self.ACTION_SEND_SMS:
390 entryBuffer = self._smsEntry.get_buffer()
391 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
392 enteredMessage = enteredMessage[0:self.MAX_CHAR].strip()
393 if not enteredMessage:
395 self._action = self.ACTION_CANCEL
399 self._messagesView.remove_column(messageColumn)
400 self._messagesView.set_model(None)
402 return self._action, phoneNumber, enteredMessage
404 self._smsEntry.get_buffer().disconnect(entryConnectId)
405 self._phoneButton.disconnect(phoneConnectId)
406 self._keyPressEventId = self._dialog.disconnect(keyConnectId)
408 def _update_letter_count(self, *args):
409 entryLength = self._smsEntry.get_buffer().get_char_count()
411 charsLeft = self.MAX_CHAR - entryLength
412 self._letterCountLabel.set_text(str(charsLeft))
413 if charsLeft < 0 or charsLeft == self.MAX_CHAR:
414 self._smsButton.set_sensitive(False)
416 self._smsButton.set_sensitive(True)
419 self._dialButton.set_sensitive(True)
421 self._dialButton.set_sensitive(False)
423 def _request_number(self):
425 assert 0 <= self._numberIndex, "%r" % self._numberIndex
427 self._numberIndex = hildonize.touch_selector(
430 (description for (number, description) in self._contactDetails),
433 self._phoneButton.set_label(self._contactDetails[self._numberIndex][1])
435 _moduleLogger.exception("%s" % str(e))
437 def _on_phone(self, *args):
438 self._request_number()
440 def _on_entry_changed(self, *args):
441 self._update_letter_count()
443 def _on_send(self, *args):
444 self._dialog.response(gtk.RESPONSE_OK)
445 self._action = self.ACTION_SEND_SMS
447 def _on_dial(self, *args):
448 self._dialog.response(gtk.RESPONSE_OK)
449 self._action = self.ACTION_DIAL
451 def _on_cancel(self, *args):
452 self._dialog.response(gtk.RESPONSE_CANCEL)
453 self._action = self.ACTION_CANCEL
455 def _on_key_press(self, widget, event):
457 if event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
460 for messagePart in self._messagemodel
462 # For some reason this kills clipboard stuff
463 #self._clipboard.set_text(message)
465 _moduleLogger.exception(str(e))
468 class Dialpad(object):
470 def __init__(self, widgetTree, errorDisplay):
471 self._clipboard = gtk.clipboard_get()
472 self._errorDisplay = errorDisplay
473 self._smsDialog = SmsEntryDialog(widgetTree)
475 self._numberdisplay = widgetTree.get_widget("numberdisplay")
476 self._smsButton = widgetTree.get_widget("sms")
477 self._dialButton = widgetTree.get_widget("dial")
478 self._backButton = widgetTree.get_widget("back")
479 self._phonenumber = ""
480 self._prettynumber = ""
483 "on_digit_clicked": self._on_digit_clicked,
485 widgetTree.signal_autoconnect(callbackMapping)
486 self._dialButton.connect("clicked", self._on_dial_clicked)
487 self._smsButton.connect("clicked", self._on_sms_clicked)
489 self._originalLabel = self._backButton.get_label()
490 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
491 self._backTapHandler.on_tap = self._on_backspace
492 self._backTapHandler.on_hold = self._on_clearall
493 self._backTapHandler.on_holding = self._set_clear_button
494 self._backTapHandler.on_cancel = self._reset_back_button
496 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
497 self._keyPressEventId = 0
500 self._dialButton.grab_focus()
501 self._backTapHandler.enable()
502 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
505 self._window.disconnect(self._keyPressEventId)
506 self._keyPressEventId = 0
507 self._reset_back_button()
508 self._backTapHandler.disable()
510 def number_selected(self, action, number, message):
512 @note Actual dial function is patched in later
514 raise NotImplementedError("Horrible unknown error has occurred")
516 def get_number(self):
517 return self._phonenumber
519 def set_number(self, number):
521 Set the number to dial
524 self._phonenumber = make_ugly(number)
525 self._prettynumber = make_pretty(self._phonenumber)
526 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
528 self._errorDisplay.push_exception()
537 def load_settings(self, config, section):
540 def save_settings(self, config, section):
542 @note Thread Agnostic
546 def _on_key_press(self, widget, event):
548 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
549 contents = self._clipboard.wait_for_text()
550 if contents is not None:
551 self.set_number(contents)
553 self._errorDisplay.push_exception()
555 def _on_sms_clicked(self, widget):
557 phoneNumber = self.get_number()
558 action, phoneNumber, message = self._smsDialog.run([("Dialer", phoneNumber)], (), self._window)
560 if action == SmsEntryDialog.ACTION_CANCEL:
562 self.number_selected(action, phoneNumber, message)
564 self._errorDisplay.push_exception()
566 def _on_dial_clicked(self, widget):
568 action = SmsEntryDialog.ACTION_DIAL
569 phoneNumber = self.get_number()
571 self.number_selected(action, phoneNumber, message)
573 self._errorDisplay.push_exception()
575 def _on_digit_clicked(self, widget):
577 self.set_number(self._phonenumber + widget.get_name()[-1])
579 self._errorDisplay.push_exception()
581 def _on_backspace(self, taps):
583 self.set_number(self._phonenumber[:-taps])
584 self._reset_back_button()
586 self._errorDisplay.push_exception()
588 def _on_clearall(self, taps):
591 self._reset_back_button()
593 self._errorDisplay.push_exception()
596 def _set_clear_button(self):
598 self._backButton.set_label("gtk-clear")
600 self._errorDisplay.push_exception()
602 def _reset_back_button(self):
604 self._backButton.set_label(self._originalLabel)
606 self._errorDisplay.push_exception()
609 class AccountInfo(object):
611 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
612 self._errorDisplay = errorDisplay
613 self._backend = backend
614 self._isPopulated = False
615 self._alarmHandler = alarmHandler
616 self._notifyOnMissed = False
617 self._notifyOnVoicemail = False
618 self._notifyOnSms = False
620 self._callbackList = []
621 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
622 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
623 self._onCallbackSelectChangedId = 0
625 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
626 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
627 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
628 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
629 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
630 self._onNotifyToggled = 0
631 self._onMinutesChanged = 0
632 self._onMissedToggled = 0
633 self._onVoicemailToggled = 0
634 self._onSmsToggled = 0
635 self._applyAlarmTimeoutId = None
637 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
638 self._defaultCallback = ""
641 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
643 self._accountViewNumberDisplay.set_use_markup(True)
644 self.set_account_number("")
646 del self._callbackList[:]
647 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
649 if self._alarmHandler is not None:
650 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
651 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
652 self._missedCheckbox.set_active(self._notifyOnMissed)
653 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
654 self._smsCheckbox.set_active(self._notifyOnSms)
656 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
657 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
658 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
659 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
660 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
662 self._notifyCheckbox.set_sensitive(False)
663 self._minutesEntryButton.set_sensitive(False)
664 self._missedCheckbox.set_sensitive(False)
665 self._voicemailCheckbox.set_sensitive(False)
666 self._smsCheckbox.set_sensitive(False)
668 self.update(force=True)
671 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
672 self._onCallbackSelectChangedId = 0
674 if self._alarmHandler is not None:
675 self._notifyCheckbox.disconnect(self._onNotifyToggled)
676 self._minutesEntryButton.disconnect(self._onMinutesChanged)
677 self._missedCheckbox.disconnect(self._onNotifyToggled)
678 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
679 self._smsCheckbox.disconnect(self._onNotifyToggled)
680 self._onNotifyToggled = 0
681 self._onMinutesChanged = 0
682 self._onMissedToggled = 0
683 self._onVoicemailToggled = 0
684 self._onSmsToggled = 0
686 self._notifyCheckbox.set_sensitive(True)
687 self._minutesEntryButton.set_sensitive(True)
688 self._missedCheckbox.set_sensitive(True)
689 self._voicemailCheckbox.set_sensitive(True)
690 self._smsCheckbox.set_sensitive(True)
693 del self._callbackList[:]
695 def get_selected_callback_number(self):
696 currentLabel = self._callbackSelectButton.get_label()
697 if currentLabel is not None:
698 return make_ugly(currentLabel)
702 def set_account_number(self, number):
704 Displays current account number
706 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
708 def update(self, force = False):
709 if not force and self._isPopulated:
711 self._populate_callback_combo()
712 self.set_account_number(self._backend.get_account_number())
716 self._set_callback_label("")
717 self.set_account_number("")
718 self._isPopulated = False
720 def save_everything(self):
721 raise NotImplementedError
725 return "Account Info"
727 def load_settings(self, config, section):
728 self._defaultCallback = config.get(section, "callback")
729 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
730 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
731 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
733 def save_settings(self, config, section):
735 @note Thread Agnostic
737 callback = self.get_selected_callback_number()
738 config.set(section, "callback", callback)
739 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
740 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
741 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
743 def _populate_callback_combo(self):
744 self._isPopulated = True
745 del self._callbackList[:]
747 callbackNumbers = self._backend.get_callback_numbers()
749 self._errorDisplay.push_exception()
750 self._isPopulated = False
753 if len(callbackNumbers) == 0:
754 callbackNumbers = {"": "No callback numbers available"}
756 for number, description in callbackNumbers.iteritems():
757 self._callbackList.append((make_pretty(number), description))
759 self._set_callback_number(self._defaultCallback)
761 def _set_callback_number(self, number):
763 if not self._backend.is_valid_syntax(number) and 0 < len(number):
764 self._errorDisplay.push_message("%s is not a valid callback number" % number)
765 elif number == self._backend.get_callback_number() and 0 < len(number):
766 _moduleLogger.warning(
767 "Callback number already is %s" % (
768 self._backend.get_callback_number(),
771 self._set_callback_label(number)
773 self._backend.set_callback_number(number)
774 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
775 make_pretty(number), make_pretty(self._backend.get_callback_number())
777 self._set_callback_label(number)
779 "Callback number set to %s" % (
780 self._backend.get_callback_number(),
784 self._errorDisplay.push_exception()
786 def _set_callback_label(self, uglyNumber):
787 prettyNumber = make_pretty(uglyNumber)
788 if len(prettyNumber) == 0:
789 prettyNumber = "No Callback Number"
790 self._callbackSelectButton.set_label(prettyNumber)
792 def _update_alarm_settings(self, recurrence):
794 isEnabled = self._notifyCheckbox.get_active()
795 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
796 self._alarmHandler.apply_settings(isEnabled, recurrence)
798 self.save_everything()
799 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
800 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
802 def _on_callbackentry_clicked(self, *args):
804 actualSelection = make_pretty(self.get_selected_callback_number())
807 (number, "%s (%s)" % (number, description))
808 for (number, description) in self._callbackList
810 defaultSelection = userOptions.get(actualSelection, actualSelection)
812 userSelection = hildonize.touch_selector_entry(
815 list(userOptions.itervalues()),
818 reversedUserOptions = dict(
819 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
821 selectedNumber = reversedUserOptions.get(userSelection, userSelection)
823 number = make_ugly(selectedNumber)
824 self._set_callback_number(number)
825 except RuntimeError, e:
826 _moduleLogger.exception("%s" % str(e))
828 self._errorDisplay.push_exception()
830 def _on_notify_toggled(self, *args):
832 if self._applyAlarmTimeoutId is not None:
833 gobject.source_remove(self._applyAlarmTimeoutId)
834 self._applyAlarmTimeoutId = None
835 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
837 self._errorDisplay.push_exception()
839 def _on_minutes_clicked(self, *args):
840 recurrenceChoices = [
856 actualSelection = self._alarmHandler.recurrence
858 closestSelectionIndex = 0
859 for i, possible in enumerate(recurrenceChoices):
860 if possible[0] <= actualSelection:
861 closestSelectionIndex = i
862 recurrenceIndex = hildonize.touch_selector(
865 (("%s" % m[1]) for m in recurrenceChoices),
866 closestSelectionIndex,
868 recurrence = recurrenceChoices[recurrenceIndex][0]
870 self._update_alarm_settings(recurrence)
871 except RuntimeError, e:
872 _moduleLogger.exception("%s" % str(e))
874 self._errorDisplay.push_exception()
876 def _on_apply_timeout(self, *args):
878 self._applyAlarmTimeoutId = None
880 self._update_alarm_settings(self._alarmHandler.recurrence)
882 self._errorDisplay.push_exception()
885 def _on_missed_toggled(self, *args):
887 self._notifyOnMissed = self._missedCheckbox.get_active()
888 self.save_everything()
890 self._errorDisplay.push_exception()
892 def _on_voicemail_toggled(self, *args):
894 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
895 self.save_everything()
897 self._errorDisplay.push_exception()
899 def _on_sms_toggled(self, *args):
901 self._notifyOnSms = self._smsCheckbox.get_active()
902 self.save_everything()
904 self._errorDisplay.push_exception()
907 class RecentCallsView(object):
915 def __init__(self, widgetTree, backend, errorDisplay):
916 self._errorDisplay = errorDisplay
917 self._backend = backend
919 self._isPopulated = False
920 self._recentmodel = gtk.ListStore(
921 gobject.TYPE_STRING, # number
922 gobject.TYPE_STRING, # date
923 gobject.TYPE_STRING, # action
924 gobject.TYPE_STRING, # from
925 gobject.TYPE_STRING, # from id
927 self._recentview = widgetTree.get_widget("recentview")
928 self._recentviewselection = None
929 self._onRecentviewRowActivatedId = 0
931 textrenderer = gtk.CellRendererText()
932 textrenderer.set_property("yalign", 0)
933 self._dateColumn = gtk.TreeViewColumn("Date")
934 self._dateColumn.pack_start(textrenderer, expand=True)
935 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
937 textrenderer = gtk.CellRendererText()
938 textrenderer.set_property("yalign", 0)
939 self._actionColumn = gtk.TreeViewColumn("Action")
940 self._actionColumn.pack_start(textrenderer, expand=True)
941 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
943 textrenderer = gtk.CellRendererText()
944 textrenderer.set_property("yalign", 0)
945 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
946 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
947 self._numberColumn = gtk.TreeViewColumn("Number")
948 self._numberColumn.pack_start(textrenderer, expand=True)
949 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
951 textrenderer = gtk.CellRendererText()
952 textrenderer.set_property("yalign", 0)
953 hildonize.set_cell_thumb_selectable(textrenderer)
954 self._nameColumn = gtk.TreeViewColumn("From")
955 self._nameColumn.pack_start(textrenderer, expand=True)
956 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
957 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
959 self._window = gtk_toolbox.find_parent_window(self._recentview)
960 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
962 self._updateSink = gtk_toolbox.threaded_stage(
964 self._idly_populate_recentview,
965 gtk_toolbox.null_sink(),
970 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
971 self._recentview.set_model(self._recentmodel)
972 self._recentview.set_fixed_height_mode(False)
974 self._recentview.append_column(self._dateColumn)
975 self._recentview.append_column(self._actionColumn)
976 self._recentview.append_column(self._numberColumn)
977 self._recentview.append_column(self._nameColumn)
978 self._recentviewselection = self._recentview.get_selection()
979 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
981 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
984 self._recentview.disconnect(self._onRecentviewRowActivatedId)
988 self._recentview.remove_column(self._dateColumn)
989 self._recentview.remove_column(self._actionColumn)
990 self._recentview.remove_column(self._nameColumn)
991 self._recentview.remove_column(self._numberColumn)
992 self._recentview.set_model(None)
994 def number_selected(self, action, number, message):
996 @note Actual dial function is patched in later
998 raise NotImplementedError("Horrible unknown error has occurred")
1000 def update(self, force = False):
1001 if not force and self._isPopulated:
1003 self._updateSink.send(())
1007 self._isPopulated = False
1008 self._recentmodel.clear()
1012 return "Recent Calls"
1014 def load_settings(self, config, section):
1017 def save_settings(self, config, section):
1019 @note Thread Agnostic
1023 def _idly_populate_recentview(self):
1024 with gtk_toolbox.gtk_lock():
1025 banner = hildonize.show_busy_banner_start(self._window, "Loading Recent History")
1027 self._recentmodel.clear()
1028 self._isPopulated = True
1031 recentItems = self._backend.get_recent()
1032 except Exception, e:
1033 self._errorDisplay.push_exception_with_lock()
1034 self._isPopulated = False
1038 gv_backend.decorate_recent(data)
1039 for data in gv_backend.sort_messages(recentItems)
1042 for contactId, personName, phoneNumber, date, action in recentItems:
1044 personName = "Unknown"
1045 date = abbrev_relative_date(date)
1046 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1047 prettyNumber = make_pretty(prettyNumber)
1048 item = (prettyNumber, date, action.capitalize(), personName, contactId)
1049 with gtk_toolbox.gtk_lock():
1050 self._recentmodel.append(item)
1051 except Exception, e:
1052 self._errorDisplay.push_exception_with_lock()
1054 with gtk_toolbox.gtk_lock():
1055 hildonize.show_busy_banner_end(banner)
1059 def _on_recentview_row_activated(self, treeview, path, view_column):
1061 itr = self._recentmodel.get_iter(path)
1065 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1066 number = make_ugly(number)
1067 description = self._recentmodel.get_value(itr, self.FROM_IDX)
1068 contactId = self._recentmodel.get_value(itr, self.FROM_ID_IDX)
1070 contactPhoneNumbers = list(self._backend.get_contact_details(contactId))
1072 (number == make_ugly(contactNumber) or number[1:] == make_ugly(contactNumber))
1073 for (numberDescription, contactNumber) in contactPhoneNumbers
1076 defaultIndex = defaultMatches.index(True)
1078 contactPhoneNumbers.append(("Other", number))
1079 defaultIndex = len(contactPhoneNumbers)-1
1081 "Could not find contact %r's number %s among %r" % (
1082 contactId, number, contactPhoneNumbers
1086 contactPhoneNumbers = [("Phone", number)]
1089 action, phoneNumber, message = self._phoneTypeSelector.run(
1090 contactPhoneNumbers,
1091 messages = (description, ),
1092 parent = self._window,
1093 defaultIndex = defaultIndex,
1095 if action == SmsEntryDialog.ACTION_CANCEL:
1097 assert phoneNumber, "A lack of phone number exists"
1099 self.number_selected(action, phoneNumber, message)
1100 self._recentviewselection.unselect_all()
1101 except Exception, e:
1102 self._errorDisplay.push_exception()
1105 class MessagesView(object):
1114 def __init__(self, widgetTree, backend, errorDisplay):
1115 self._errorDisplay = errorDisplay
1116 self._backend = backend
1118 self._isPopulated = False
1119 self._messagemodel = gtk.ListStore(
1120 gobject.TYPE_STRING, # number
1121 gobject.TYPE_STRING, # date
1122 gobject.TYPE_STRING, # header
1123 gobject.TYPE_STRING, # message
1125 gobject.TYPE_STRING, # from id
1127 self._messageview = widgetTree.get_widget("messages_view")
1128 self._messageviewselection = None
1129 self._onMessageviewRowActivatedId = 0
1131 self._messageRenderer = gtk.CellRendererText()
1132 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1133 self._messageRenderer.set_property("wrap-width", 500)
1134 self._messageColumn = gtk.TreeViewColumn("Messages")
1135 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1136 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1137 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1139 self._window = gtk_toolbox.find_parent_window(self._messageview)
1140 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
1142 self._updateSink = gtk_toolbox.threaded_stage(
1144 self._idly_populate_messageview,
1145 gtk_toolbox.null_sink(),
1150 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1151 self._messageview.set_model(self._messagemodel)
1152 self._messageview.set_headers_visible(False)
1153 self._messageview.set_fixed_height_mode(False)
1155 self._messageview.append_column(self._messageColumn)
1156 self._messageviewselection = self._messageview.get_selection()
1157 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1159 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1162 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1166 self._messageview.remove_column(self._messageColumn)
1167 self._messageview.set_model(None)
1169 def number_selected(self, action, number, message):
1171 @note Actual dial function is patched in later
1173 raise NotImplementedError("Horrible unknown error has occurred")
1175 def update(self, force = False):
1176 if not force and self._isPopulated:
1178 self._updateSink.send(())
1182 self._isPopulated = False
1183 self._messagemodel.clear()
1189 def load_settings(self, config, section):
1192 def save_settings(self, config, section):
1194 @note Thread Agnostic
1198 _MIN_MESSAGES_SHOWN = 4
1200 def _idly_populate_messageview(self):
1201 with gtk_toolbox.gtk_lock():
1202 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1204 self._messagemodel.clear()
1205 self._isPopulated = True
1208 messageItems = self._backend.get_messages()
1209 except Exception, e:
1210 self._errorDisplay.push_exception_with_lock()
1211 self._isPopulated = False
1215 gv_backend.decorate_message(message)
1216 for message in gv_backend.sort_messages(messageItems)
1219 for contactId, header, number, relativeDate, messages in messageItems:
1220 prettyNumber = number[2:] if number.startswith("+1") else number
1221 prettyNumber = make_pretty(prettyNumber)
1223 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1224 expandedMessages = [firstMessage]
1225 expandedMessages.extend(messages)
1226 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1227 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1228 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1229 collapsedMessages = [firstMessage, secondMessage]
1230 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1232 collapsedMessages = expandedMessages
1234 number = make_ugly(number)
1236 row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId
1237 with gtk_toolbox.gtk_lock():
1238 self._messagemodel.append(row)
1239 except Exception, e:
1240 self._errorDisplay.push_exception_with_lock()
1242 with gtk_toolbox.gtk_lock():
1243 hildonize.show_busy_banner_end(banner)
1247 def _on_messageview_row_activated(self, treeview, path, view_column):
1249 itr = self._messagemodel.get_iter(path)
1253 number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX))
1254 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1256 contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX)
1258 contactPhoneNumbers = list(self._backend.get_contact_details(contactId))
1260 (number == make_ugly(contactNumber) or number[1:] == make_ugly(contactNumber))
1261 for (numberDescription, contactNumber) in contactPhoneNumbers
1264 defaultIndex = defaultMatches.index(True)
1266 contactPhoneNumbers.append(("Other", number))
1267 defaultIndex = len(contactPhoneNumbers)-1
1269 "Could not find contact %r's number %s among %r" % (
1270 contactId, number, contactPhoneNumbers
1274 contactPhoneNumbers = [("Phone", number)]
1277 action, phoneNumber, message = self._phoneTypeSelector.run(
1278 contactPhoneNumbers,
1279 messages = description,
1280 parent = self._window,
1281 defaultIndex = defaultIndex,
1283 if action == SmsEntryDialog.ACTION_CANCEL:
1285 assert phoneNumber, "A lock of phone number exists"
1287 self.number_selected(action, phoneNumber, message)
1288 self._messageviewselection.unselect_all()
1289 except Exception, e:
1290 self._errorDisplay.push_exception()
1293 class ContactsView(object):
1295 def __init__(self, widgetTree, backend, errorDisplay):
1296 self._errorDisplay = errorDisplay
1297 self._backend = backend
1299 self._addressBook = None
1300 self._selectedComboIndex = 0
1301 self._addressBookFactories = [null_backend.NullAddressBook()]
1303 self._booksList = []
1304 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1306 self._isPopulated = False
1307 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1308 self._contactsviewselection = None
1309 self._contactsview = widgetTree.get_widget("contactsview")
1311 self._contactColumn = gtk.TreeViewColumn("Contact")
1312 displayContactSource = False
1313 if displayContactSource:
1314 textrenderer = gtk.CellRendererText()
1315 self._contactColumn.pack_start(textrenderer, expand=False)
1316 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1317 textrenderer = gtk.CellRendererText()
1318 hildonize.set_cell_thumb_selectable(textrenderer)
1319 self._contactColumn.pack_start(textrenderer, expand=True)
1320 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1321 textrenderer = gtk.CellRendererText()
1322 self._contactColumn.pack_start(textrenderer, expand=True)
1323 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1324 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1325 self._contactColumn.set_sort_column_id(1)
1326 self._contactColumn.set_visible(True)
1328 self._onContactsviewRowActivatedId = 0
1329 self._onAddressbookButtonChangedId = 0
1330 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1331 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
1333 self._updateSink = gtk_toolbox.threaded_stage(
1335 self._idly_populate_contactsview,
1336 gtk_toolbox.null_sink(),
1341 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1343 self._contactsview.set_model(self._contactsmodel)
1344 self._contactsview.set_fixed_height_mode(False)
1345 self._contactsview.append_column(self._contactColumn)
1346 self._contactsviewselection = self._contactsview.get_selection()
1347 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1349 del self._booksList[:]
1350 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1351 if factoryName and bookName:
1352 entryName = "%s: %s" % (factoryName, bookName)
1354 entryName = factoryName
1356 entryName = bookName
1358 entryName = "Bad name (%d)" % factoryId
1359 row = (str(factoryId), bookId, entryName)
1360 self._booksList.append(row)
1362 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1363 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1365 if len(self._booksList) <= self._selectedComboIndex:
1366 self._selectedComboIndex = 0
1367 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1369 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1370 selectedBookId = self._booksList[self._selectedComboIndex][1]
1371 self.open_addressbook(selectedFactoryId, selectedBookId)
1374 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1375 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1379 self._bookSelectionButton.set_label("")
1380 self._contactsview.set_model(None)
1381 self._contactsview.remove_column(self._contactColumn)
1383 def number_selected(self, action, number, message):
1385 @note Actual dial function is patched in later
1387 raise NotImplementedError("Horrible unknown error has occurred")
1389 def get_addressbooks(self):
1391 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1393 for i, factory in enumerate(self._addressBookFactories):
1394 for bookFactory, bookId, bookName in factory.get_addressbooks():
1395 yield (str(i), bookId), (factory.factory_name(), bookName)
1397 def open_addressbook(self, bookFactoryId, bookId):
1398 bookFactoryIndex = int(bookFactoryId)
1399 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1401 forceUpdate = True if addressBook is not self._addressBook else False
1403 self._addressBook = addressBook
1404 self.update(force=forceUpdate)
1406 def update(self, force = False):
1407 if not force and self._isPopulated:
1409 self._updateSink.send(())
1413 self._isPopulated = False
1414 self._contactsmodel.clear()
1415 for factory in self._addressBookFactories:
1416 factory.clear_caches()
1417 self._addressBook.clear_caches()
1419 def append(self, book):
1420 self._addressBookFactories.append(book)
1422 def extend(self, books):
1423 self._addressBookFactories.extend(books)
1429 def load_settings(self, config, sectionName):
1431 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1432 except ConfigParser.NoOptionError:
1433 self._selectedComboIndex = 0
1435 def save_settings(self, config, sectionName):
1436 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1438 def _idly_populate_contactsview(self):
1439 with gtk_toolbox.gtk_lock():
1440 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1443 while addressBook is not self._addressBook:
1444 addressBook = self._addressBook
1445 with gtk_toolbox.gtk_lock():
1446 self._contactsview.set_model(None)
1450 contacts = addressBook.get_contacts()
1451 except Exception, e:
1453 self._isPopulated = False
1454 self._errorDisplay.push_exception_with_lock()
1455 for contactId, contactName in contacts:
1456 contactType = (addressBook.contact_source_short_name(contactId), )
1457 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1459 with gtk_toolbox.gtk_lock():
1460 self._contactsview.set_model(self._contactsmodel)
1462 self._isPopulated = True
1463 except Exception, e:
1464 self._errorDisplay.push_exception_with_lock()
1466 with gtk_toolbox.gtk_lock():
1467 hildonize.show_busy_banner_end(banner)
1470 def _on_addressbook_button_changed(self, *args, **kwds):
1473 newSelectedComboIndex = hildonize.touch_selector(
1476 (("%s" % m[2]) for m in self._booksList),
1477 self._selectedComboIndex,
1479 except RuntimeError:
1482 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1483 selectedBookId = self._booksList[newSelectedComboIndex][1]
1484 self.open_addressbook(selectedFactoryId, selectedBookId)
1485 self._selectedComboIndex = newSelectedComboIndex
1486 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1487 except Exception, e:
1488 self._errorDisplay.push_exception()
1490 def _on_contactsview_row_activated(self, treeview, path, view_column):
1492 itr = self._contactsmodel.get_iter(path)
1496 contactId = self._contactsmodel.get_value(itr, 3)
1497 contactName = self._contactsmodel.get_value(itr, 1)
1499 contactDetails = self._addressBook.get_contact_details(contactId)
1500 except Exception, e:
1502 self._errorDisplay.push_exception()
1503 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1505 if len(contactPhoneNumbers) == 0:
1508 action, phoneNumber, message = self._phoneTypeSelector.run(
1509 contactPhoneNumbers,
1510 messages = (contactName, ),
1511 parent = self._window,
1513 if action == SmsEntryDialog.ACTION_CANCEL:
1515 assert phoneNumber, "A lack of phone number exists"
1517 self.number_selected(action, phoneNumber, message)
1518 self._contactsviewselection.unselect_all()
1519 except Exception, e:
1520 self._errorDisplay.push_exception()