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")
295 self._scrollWindow = self._messagesView.get_parent()
297 self._phoneButton = self._widgetTree.get_widget("phoneTypeSelection")
298 self._smsEntry = self._widgetTree.get_widget("smsEntry")
300 self._action = self.ACTION_CANCEL
302 self._numberIndex = -1
303 self._contactDetails = []
305 def run(self, contactDetails, messages = (), parent = None, defaultIndex = -1):
306 entryConnectId = self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
307 phoneConnectId = self._phoneButton.connect("clicked", self._on_phone)
308 keyConnectId = self._keyPressEventId = self._dialog.connect("key-press-event", self._on_key_press)
310 # Setup the phone selection button
311 del self._contactDetails[:]
312 for phoneType, phoneNumber in contactDetails:
313 display = " - ".join((make_pretty(phoneNumber), phoneType))
314 row = (phoneNumber, display)
315 self._contactDetails.append(row)
316 if 0 < len(self._contactDetails):
317 self._numberIndex = defaultIndex if defaultIndex != -1 else 0
318 self._phoneButton.set_label(self._contactDetails[self._numberIndex][1])
320 self._numberIndex = -1
321 self._phoneButton.set_label("Error: No Number Available")
323 # Add the column to the messages tree view
324 self._messagemodel.clear()
325 self._messagesView.set_model(self._messagemodel)
326 self._messagesView.set_fixed_height_mode(False)
328 textrenderer = gtk.CellRendererText()
329 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
330 textrenderer.set_property("wrap-width", 450)
331 messageColumn = gtk.TreeViewColumn("")
332 messageColumn.pack_start(textrenderer, expand=True)
333 messageColumn.add_attribute(textrenderer, "markup", 0)
334 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
335 self._messagesView.append_column(messageColumn)
336 self._messagesView.set_headers_visible(False)
339 for message in messages:
341 self._messagemodel.append(row)
342 self._messagesView.show()
343 self._scrollWindow.show()
344 messagesSelection = self._messagesView.get_selection()
345 messagesSelection.select_path((len(messages)-1, ))
347 self._messagesView.hide()
348 self._scrollWindow.hide()
350 self._smsEntry.get_buffer().set_text("")
351 self._update_letter_count()
353 if parent is not None:
354 self._dialog.set_transient_for(parent)
355 parentSize = parent.get_size()
356 self._dialog.resize(parentSize[0], max(parentSize[1]-50, 100))
362 self._messagesView.scroll_to_cell((len(messages)-1, ))
363 self._smsEntry.grab_focus()
365 if 1 < len(self._contactDetails):
366 if defaultIndex == -1:
367 self._request_number()
368 self._phoneButton.set_sensitive(True)
370 self._phoneButton.set_sensitive(False)
372 userResponse = self._dialog.run()
376 # Process the users response
377 if userResponse == gtk.RESPONSE_OK and 0 <= self._numberIndex:
378 phoneNumber = self._contactDetails[self._numberIndex][0]
379 phoneNumber = make_ugly(phoneNumber)
383 self._action = self.ACTION_CANCEL
384 if self._action == self.ACTION_SEND_SMS:
385 entryBuffer = self._smsEntry.get_buffer()
386 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
387 enteredMessage = enteredMessage[0:self.MAX_CHAR].strip()
388 if not enteredMessage:
390 self._action = self.ACTION_CANCEL
394 self._messagesView.remove_column(messageColumn)
395 self._messagesView.set_model(None)
397 return self._action, phoneNumber, enteredMessage
399 self._smsEntry.get_buffer().disconnect(entryConnectId)
400 self._phoneButton.disconnect(phoneConnectId)
401 self._keyPressEventId = self._dialog.disconnect(keyConnectId)
403 def _update_letter_count(self, *args):
404 entryLength = self._smsEntry.get_buffer().get_char_count()
405 charsLeft = self.MAX_CHAR - entryLength
406 self._letterCountLabel.set_text(str(charsLeft))
407 if charsLeft < 0 or charsLeft == self.MAX_CHAR:
408 self._smsButton.set_sensitive(False)
410 self._smsButton.set_sensitive(True)
412 def _request_number(self):
414 assert 0 <= self._numberIndex, "%r" % self._numberIndex
416 self._numberIndex = hildonize.touch_selector(
419 (description for (number, description) in self._contactDetails),
422 self._phoneButton.set_label(self._contactDetails[self._numberIndex][1])
424 _moduleLogger.exception("%s" % str(e))
426 def _on_phone(self, *args):
427 self._request_number()
429 def _on_entry_changed(self, *args):
430 self._update_letter_count()
432 def _on_send(self, *args):
433 self._dialog.response(gtk.RESPONSE_OK)
434 self._action = self.ACTION_SEND_SMS
436 def _on_dial(self, *args):
437 self._dialog.response(gtk.RESPONSE_OK)
438 self._action = self.ACTION_DIAL
440 def _on_cancel(self, *args):
441 self._dialog.response(gtk.RESPONSE_CANCEL)
442 self._action = self.ACTION_CANCEL
444 def _on_key_press(self, widget, event):
446 if event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
449 for messagePart in self._messagemodel
451 # For some reason this kills clipboard stuff
452 #self._clipboard.set_text(message)
454 _moduleLogger.exception(str(e))
457 class Dialpad(object):
459 def __init__(self, widgetTree, errorDisplay):
460 self._clipboard = gtk.clipboard_get()
461 self._errorDisplay = errorDisplay
462 self._smsDialog = SmsEntryDialog(widgetTree)
464 self._numberdisplay = widgetTree.get_widget("numberdisplay")
465 self._smsButton = widgetTree.get_widget("sms")
466 self._dialButton = widgetTree.get_widget("dial")
467 self._backButton = widgetTree.get_widget("back")
468 self._phonenumber = ""
469 self._prettynumber = ""
472 "on_digit_clicked": self._on_digit_clicked,
474 widgetTree.signal_autoconnect(callbackMapping)
475 self._dialButton.connect("clicked", self._on_dial_clicked)
476 self._smsButton.connect("clicked", self._on_sms_clicked)
478 self._originalLabel = self._backButton.get_label()
479 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
480 self._backTapHandler.on_tap = self._on_backspace
481 self._backTapHandler.on_hold = self._on_clearall
482 self._backTapHandler.on_holding = self._set_clear_button
483 self._backTapHandler.on_cancel = self._reset_back_button
485 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
486 self._keyPressEventId = 0
489 self._dialButton.grab_focus()
490 self._backTapHandler.enable()
491 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
494 self._window.disconnect(self._keyPressEventId)
495 self._keyPressEventId = 0
496 self._reset_back_button()
497 self._backTapHandler.disable()
499 def number_selected(self, action, number, message):
501 @note Actual dial function is patched in later
503 raise NotImplementedError("Horrible unknown error has occurred")
505 def get_number(self):
506 return self._phonenumber
508 def set_number(self, number):
510 Set the number to dial
513 self._phonenumber = make_ugly(number)
514 self._prettynumber = make_pretty(self._phonenumber)
515 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
517 self._errorDisplay.push_exception()
526 def load_settings(self, config, section):
529 def save_settings(self, config, section):
531 @note Thread Agnostic
535 def _on_key_press(self, widget, event):
537 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
538 contents = self._clipboard.wait_for_text()
539 if contents is not None:
540 self.set_number(contents)
542 self._errorDisplay.push_exception()
544 def _on_sms_clicked(self, widget):
546 phoneNumber = self.get_number()
547 action, phoneNumber, message = self._smsDialog.run([("Dialer", phoneNumber)], (), self._window)
549 if action == SmsEntryDialog.ACTION_CANCEL:
551 self.number_selected(action, phoneNumber, message)
553 self._errorDisplay.push_exception()
555 def _on_dial_clicked(self, widget):
557 action = SmsEntryDialog.ACTION_DIAL
558 phoneNumber = self.get_number()
560 self.number_selected(action, phoneNumber, message)
562 self._errorDisplay.push_exception()
564 def _on_digit_clicked(self, widget):
566 self.set_number(self._phonenumber + widget.get_name()[-1])
568 self._errorDisplay.push_exception()
570 def _on_backspace(self, taps):
572 self.set_number(self._phonenumber[:-taps])
573 self._reset_back_button()
575 self._errorDisplay.push_exception()
577 def _on_clearall(self, taps):
580 self._reset_back_button()
582 self._errorDisplay.push_exception()
585 def _set_clear_button(self):
587 self._backButton.set_label("gtk-clear")
589 self._errorDisplay.push_exception()
591 def _reset_back_button(self):
593 self._backButton.set_label(self._originalLabel)
595 self._errorDisplay.push_exception()
598 class AccountInfo(object):
600 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
601 self._errorDisplay = errorDisplay
602 self._backend = backend
603 self._isPopulated = False
604 self._alarmHandler = alarmHandler
605 self._notifyOnMissed = False
606 self._notifyOnVoicemail = False
607 self._notifyOnSms = False
609 self._callbackList = []
610 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
611 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
612 self._onCallbackSelectChangedId = 0
614 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
615 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
616 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
617 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
618 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
619 self._onNotifyToggled = 0
620 self._onMinutesChanged = 0
621 self._onMissedToggled = 0
622 self._onVoicemailToggled = 0
623 self._onSmsToggled = 0
624 self._applyAlarmTimeoutId = None
626 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
627 self._defaultCallback = ""
630 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
632 self._accountViewNumberDisplay.set_use_markup(True)
633 self.set_account_number("")
635 del self._callbackList[:]
636 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
638 if self._alarmHandler is not None:
639 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
640 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
641 self._missedCheckbox.set_active(self._notifyOnMissed)
642 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
643 self._smsCheckbox.set_active(self._notifyOnSms)
645 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
646 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
647 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
648 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
649 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
651 self._notifyCheckbox.set_sensitive(False)
652 self._minutesEntryButton.set_sensitive(False)
653 self._missedCheckbox.set_sensitive(False)
654 self._voicemailCheckbox.set_sensitive(False)
655 self._smsCheckbox.set_sensitive(False)
657 self.update(force=True)
660 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
661 self._onCallbackSelectChangedId = 0
663 if self._alarmHandler is not None:
664 self._notifyCheckbox.disconnect(self._onNotifyToggled)
665 self._minutesEntryButton.disconnect(self._onMinutesChanged)
666 self._missedCheckbox.disconnect(self._onNotifyToggled)
667 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
668 self._smsCheckbox.disconnect(self._onNotifyToggled)
669 self._onNotifyToggled = 0
670 self._onMinutesChanged = 0
671 self._onMissedToggled = 0
672 self._onVoicemailToggled = 0
673 self._onSmsToggled = 0
675 self._notifyCheckbox.set_sensitive(True)
676 self._minutesEntryButton.set_sensitive(True)
677 self._missedCheckbox.set_sensitive(True)
678 self._voicemailCheckbox.set_sensitive(True)
679 self._smsCheckbox.set_sensitive(True)
682 del self._callbackList[:]
684 def get_selected_callback_number(self):
685 currentLabel = self._callbackSelectButton.get_label()
686 if currentLabel is not None:
687 return make_ugly(currentLabel)
691 def set_account_number(self, number):
693 Displays current account number
695 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
697 def update(self, force = False):
698 if not force and self._isPopulated:
700 self._populate_callback_combo()
701 self.set_account_number(self._backend.get_account_number())
705 self._set_callback_label("")
706 self.set_account_number("")
707 self._isPopulated = False
709 def save_everything(self):
710 raise NotImplementedError
714 return "Account Info"
716 def load_settings(self, config, section):
717 self._defaultCallback = config.get(section, "callback")
718 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
719 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
720 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
722 def save_settings(self, config, section):
724 @note Thread Agnostic
726 callback = self.get_selected_callback_number()
727 config.set(section, "callback", callback)
728 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
729 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
730 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
732 def _populate_callback_combo(self):
733 self._isPopulated = True
734 del self._callbackList[:]
736 callbackNumbers = self._backend.get_callback_numbers()
738 self._errorDisplay.push_exception()
739 self._isPopulated = False
742 if len(callbackNumbers) == 0:
743 callbackNumbers = {"": "No callback numbers available"}
745 for number, description in callbackNumbers.iteritems():
746 self._callbackList.append((make_pretty(number), description))
748 self._set_callback_number(self._defaultCallback)
750 def _set_callback_number(self, number):
752 if not self._backend.is_valid_syntax(number) and 0 < len(number):
753 self._errorDisplay.push_message("%s is not a valid callback number" % number)
754 elif number == self._backend.get_callback_number() and 0 < len(number):
755 _moduleLogger.warning(
756 "Callback number already is %s" % (
757 self._backend.get_callback_number(),
760 self._set_callback_label(number)
762 self._backend.set_callback_number(number)
763 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
764 make_pretty(number), make_pretty(self._backend.get_callback_number())
766 self._set_callback_label(number)
768 "Callback number set to %s" % (
769 self._backend.get_callback_number(),
773 self._errorDisplay.push_exception()
775 def _set_callback_label(self, uglyNumber):
776 prettyNumber = make_pretty(uglyNumber)
777 if len(prettyNumber) == 0:
778 prettyNumber = "No Callback Number"
779 self._callbackSelectButton.set_label(prettyNumber)
781 def _update_alarm_settings(self, recurrence):
783 isEnabled = self._notifyCheckbox.get_active()
784 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
785 self._alarmHandler.apply_settings(isEnabled, recurrence)
787 self.save_everything()
788 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
789 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
791 def _on_callbackentry_clicked(self, *args):
793 actualSelection = make_pretty(self.get_selected_callback_number())
796 (number, "%s (%s)" % (number, description))
797 for (number, description) in self._callbackList
799 defaultSelection = userOptions.get(actualSelection, actualSelection)
801 userSelection = hildonize.touch_selector_entry(
804 list(userOptions.itervalues()),
807 reversedUserOptions = dict(
808 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
810 selectedNumber = reversedUserOptions.get(userSelection, userSelection)
812 number = make_ugly(selectedNumber)
813 self._set_callback_number(number)
814 except RuntimeError, e:
815 _moduleLogger.exception("%s" % str(e))
817 self._errorDisplay.push_exception()
819 def _on_notify_toggled(self, *args):
821 if self._applyAlarmTimeoutId is not None:
822 gobject.source_remove(self._applyAlarmTimeoutId)
823 self._applyAlarmTimeoutId = None
824 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
826 self._errorDisplay.push_exception()
828 def _on_minutes_clicked(self, *args):
829 recurrenceChoices = [
845 actualSelection = self._alarmHandler.recurrence
847 closestSelectionIndex = 0
848 for i, possible in enumerate(recurrenceChoices):
849 if possible[0] <= actualSelection:
850 closestSelectionIndex = i
851 recurrenceIndex = hildonize.touch_selector(
854 (("%s" % m[1]) for m in recurrenceChoices),
855 closestSelectionIndex,
857 recurrence = recurrenceChoices[recurrenceIndex][0]
859 self._update_alarm_settings(recurrence)
860 except RuntimeError, e:
861 _moduleLogger.exception("%s" % str(e))
863 self._errorDisplay.push_exception()
865 def _on_apply_timeout(self, *args):
867 self._applyAlarmTimeoutId = None
869 self._update_alarm_settings(self._alarmHandler.recurrence)
871 self._errorDisplay.push_exception()
874 def _on_missed_toggled(self, *args):
876 self._notifyOnMissed = self._missedCheckbox.get_active()
877 self.save_everything()
879 self._errorDisplay.push_exception()
881 def _on_voicemail_toggled(self, *args):
883 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
884 self.save_everything()
886 self._errorDisplay.push_exception()
888 def _on_sms_toggled(self, *args):
890 self._notifyOnSms = self._smsCheckbox.get_active()
891 self.save_everything()
893 self._errorDisplay.push_exception()
896 class RecentCallsView(object):
904 def __init__(self, widgetTree, backend, errorDisplay):
905 self._errorDisplay = errorDisplay
906 self._backend = backend
908 self._isPopulated = False
909 self._recentmodel = gtk.ListStore(
910 gobject.TYPE_STRING, # number
911 gobject.TYPE_STRING, # date
912 gobject.TYPE_STRING, # action
913 gobject.TYPE_STRING, # from
914 gobject.TYPE_STRING, # from id
916 self._recentview = widgetTree.get_widget("recentview")
917 self._recentviewselection = None
918 self._onRecentviewRowActivatedId = 0
920 textrenderer = gtk.CellRendererText()
921 textrenderer.set_property("yalign", 0)
922 self._dateColumn = gtk.TreeViewColumn("Date")
923 self._dateColumn.pack_start(textrenderer, expand=True)
924 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
926 textrenderer = gtk.CellRendererText()
927 textrenderer.set_property("yalign", 0)
928 self._actionColumn = gtk.TreeViewColumn("Action")
929 self._actionColumn.pack_start(textrenderer, expand=True)
930 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
932 textrenderer = gtk.CellRendererText()
933 textrenderer.set_property("yalign", 0)
934 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
935 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
936 self._numberColumn = gtk.TreeViewColumn("Number")
937 self._numberColumn.pack_start(textrenderer, expand=True)
938 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
940 textrenderer = gtk.CellRendererText()
941 textrenderer.set_property("yalign", 0)
942 hildonize.set_cell_thumb_selectable(textrenderer)
943 self._nameColumn = gtk.TreeViewColumn("From")
944 self._nameColumn.pack_start(textrenderer, expand=True)
945 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
946 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
948 self._window = gtk_toolbox.find_parent_window(self._recentview)
949 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
951 self._updateSink = gtk_toolbox.threaded_stage(
953 self._idly_populate_recentview,
954 gtk_toolbox.null_sink(),
959 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
960 self._recentview.set_model(self._recentmodel)
961 self._recentview.set_fixed_height_mode(False)
963 self._recentview.append_column(self._dateColumn)
964 self._recentview.append_column(self._actionColumn)
965 self._recentview.append_column(self._numberColumn)
966 self._recentview.append_column(self._nameColumn)
967 self._recentviewselection = self._recentview.get_selection()
968 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
970 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
973 self._recentview.disconnect(self._onRecentviewRowActivatedId)
977 self._recentview.remove_column(self._dateColumn)
978 self._recentview.remove_column(self._actionColumn)
979 self._recentview.remove_column(self._nameColumn)
980 self._recentview.remove_column(self._numberColumn)
981 self._recentview.set_model(None)
983 def number_selected(self, action, number, message):
985 @note Actual dial function is patched in later
987 raise NotImplementedError("Horrible unknown error has occurred")
989 def update(self, force = False):
990 if not force and self._isPopulated:
992 self._updateSink.send(())
996 self._isPopulated = False
997 self._recentmodel.clear()
1001 return "Recent Calls"
1003 def load_settings(self, config, section):
1006 def save_settings(self, config, section):
1008 @note Thread Agnostic
1012 def _idly_populate_recentview(self):
1013 with gtk_toolbox.gtk_lock():
1014 banner = hildonize.show_busy_banner_start(self._window, "Loading Recent History")
1016 self._recentmodel.clear()
1017 self._isPopulated = True
1020 recentItems = self._backend.get_recent()
1021 except Exception, e:
1022 self._errorDisplay.push_exception_with_lock()
1023 self._isPopulated = False
1027 gv_backend.decorate_recent(data)
1028 for data in gv_backend.sort_messages(recentItems)
1031 for contactId, personName, phoneNumber, date, action in recentItems:
1033 personName = "Unknown"
1034 date = abbrev_relative_date(date)
1035 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1036 prettyNumber = make_pretty(prettyNumber)
1037 item = (prettyNumber, date, action.capitalize(), personName, contactId)
1038 with gtk_toolbox.gtk_lock():
1039 self._recentmodel.append(item)
1040 except Exception, e:
1041 self._errorDisplay.push_exception_with_lock()
1043 with gtk_toolbox.gtk_lock():
1044 hildonize.show_busy_banner_end(banner)
1048 def _on_recentview_row_activated(self, treeview, path, view_column):
1050 itr = self._recentmodel.get_iter(path)
1054 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1055 number = make_ugly(number)
1056 description = self._recentmodel.get_value(itr, self.FROM_IDX)
1057 contactId = self._recentmodel.get_value(itr, self.FROM_ID_IDX)
1059 contactPhoneNumbers = list(self._backend.get_contact_details(contactId))
1061 (number == make_ugly(contactNumber) or number[1:] == make_ugly(contactNumber))
1062 for (numberDescription, contactNumber) in contactPhoneNumbers
1065 defaultIndex = defaultMatches.index(True)
1067 contactPhoneNumbers.append(("Other", number))
1068 defaultIndex = len(contactPhoneNumbers)-1
1070 "Could not find contact %r's number %s among %r" % (
1071 contactId, number, contactPhoneNumbers
1075 contactPhoneNumbers = [("Phone", number)]
1078 action, phoneNumber, message = self._phoneTypeSelector.run(
1079 contactPhoneNumbers,
1080 messages = (description, ),
1081 parent = self._window,
1082 defaultIndex = defaultIndex,
1084 if action == SmsEntryDialog.ACTION_CANCEL:
1086 assert phoneNumber, "A lack of phone number exists"
1088 self.number_selected(action, phoneNumber, message)
1089 self._recentviewselection.unselect_all()
1090 except Exception, e:
1091 self._errorDisplay.push_exception()
1094 class MessagesView(object):
1103 def __init__(self, widgetTree, backend, errorDisplay):
1104 self._errorDisplay = errorDisplay
1105 self._backend = backend
1107 self._isPopulated = False
1108 self._messagemodel = gtk.ListStore(
1109 gobject.TYPE_STRING, # number
1110 gobject.TYPE_STRING, # date
1111 gobject.TYPE_STRING, # header
1112 gobject.TYPE_STRING, # message
1114 gobject.TYPE_STRING, # from id
1116 self._messageview = widgetTree.get_widget("messages_view")
1117 self._messageviewselection = None
1118 self._onMessageviewRowActivatedId = 0
1120 self._messageRenderer = gtk.CellRendererText()
1121 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1122 self._messageRenderer.set_property("wrap-width", 500)
1123 self._messageColumn = gtk.TreeViewColumn("Messages")
1124 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1125 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1126 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1128 self._window = gtk_toolbox.find_parent_window(self._messageview)
1129 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
1131 self._updateSink = gtk_toolbox.threaded_stage(
1133 self._idly_populate_messageview,
1134 gtk_toolbox.null_sink(),
1139 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1140 self._messageview.set_model(self._messagemodel)
1141 self._messageview.set_headers_visible(False)
1142 self._messageview.set_fixed_height_mode(False)
1144 self._messageview.append_column(self._messageColumn)
1145 self._messageviewselection = self._messageview.get_selection()
1146 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1148 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1151 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1155 self._messageview.remove_column(self._messageColumn)
1156 self._messageview.set_model(None)
1158 def number_selected(self, action, number, message):
1160 @note Actual dial function is patched in later
1162 raise NotImplementedError("Horrible unknown error has occurred")
1164 def update(self, force = False):
1165 if not force and self._isPopulated:
1167 self._updateSink.send(())
1171 self._isPopulated = False
1172 self._messagemodel.clear()
1178 def load_settings(self, config, section):
1181 def save_settings(self, config, section):
1183 @note Thread Agnostic
1187 _MIN_MESSAGES_SHOWN = 4
1189 def _idly_populate_messageview(self):
1190 with gtk_toolbox.gtk_lock():
1191 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1193 self._messagemodel.clear()
1194 self._isPopulated = True
1197 messageItems = self._backend.get_messages()
1198 except Exception, e:
1199 self._errorDisplay.push_exception_with_lock()
1200 self._isPopulated = False
1204 gv_backend.decorate_message(message)
1205 for message in gv_backend.sort_messages(messageItems)
1208 for contactId, header, number, relativeDate, messages in messageItems:
1209 prettyNumber = number[2:] if number.startswith("+1") else number
1210 prettyNumber = make_pretty(prettyNumber)
1212 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1213 expandedMessages = [firstMessage]
1214 expandedMessages.extend(messages)
1215 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1216 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1217 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1218 collapsedMessages = [firstMessage, secondMessage]
1219 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1221 collapsedMessages = expandedMessages
1223 number = make_ugly(number)
1225 row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId
1226 with gtk_toolbox.gtk_lock():
1227 self._messagemodel.append(row)
1228 except Exception, e:
1229 self._errorDisplay.push_exception_with_lock()
1231 with gtk_toolbox.gtk_lock():
1232 hildonize.show_busy_banner_end(banner)
1236 def _on_messageview_row_activated(self, treeview, path, view_column):
1238 itr = self._messagemodel.get_iter(path)
1242 number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX))
1243 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1245 contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX)
1247 contactPhoneNumbers = list(self._backend.get_contact_details(contactId))
1249 (number == make_ugly(contactNumber) or number[1:] == make_ugly(contactNumber))
1250 for (numberDescription, contactNumber) in contactPhoneNumbers
1253 defaultIndex = defaultMatches.index(True)
1255 contactPhoneNumbers.append(("Other", number))
1256 defaultIndex = len(contactPhoneNumbers)-1
1258 "Could not find contact %r's number %s among %r" % (
1259 contactId, number, contactPhoneNumbers
1263 contactPhoneNumbers = [("Phone", number)]
1266 action, phoneNumber, message = self._phoneTypeSelector.run(
1267 contactPhoneNumbers,
1268 messages = description,
1269 parent = self._window,
1270 defaultIndex = defaultIndex,
1272 if action == SmsEntryDialog.ACTION_CANCEL:
1274 assert phoneNumber, "A lock of phone number exists"
1276 self.number_selected(action, phoneNumber, message)
1277 self._messageviewselection.unselect_all()
1278 except Exception, e:
1279 self._errorDisplay.push_exception()
1282 class ContactsView(object):
1284 def __init__(self, widgetTree, backend, errorDisplay):
1285 self._errorDisplay = errorDisplay
1286 self._backend = backend
1288 self._addressBook = None
1289 self._selectedComboIndex = 0
1290 self._addressBookFactories = [null_backend.NullAddressBook()]
1292 self._booksList = []
1293 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1295 self._isPopulated = False
1296 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1297 self._contactsviewselection = None
1298 self._contactsview = widgetTree.get_widget("contactsview")
1300 self._contactColumn = gtk.TreeViewColumn("Contact")
1301 displayContactSource = False
1302 if displayContactSource:
1303 textrenderer = gtk.CellRendererText()
1304 self._contactColumn.pack_start(textrenderer, expand=False)
1305 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1306 textrenderer = gtk.CellRendererText()
1307 hildonize.set_cell_thumb_selectable(textrenderer)
1308 self._contactColumn.pack_start(textrenderer, expand=True)
1309 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1310 textrenderer = gtk.CellRendererText()
1311 self._contactColumn.pack_start(textrenderer, expand=True)
1312 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1313 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1314 self._contactColumn.set_sort_column_id(1)
1315 self._contactColumn.set_visible(True)
1317 self._onContactsviewRowActivatedId = 0
1318 self._onAddressbookButtonChangedId = 0
1319 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1320 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
1322 self._updateSink = gtk_toolbox.threaded_stage(
1324 self._idly_populate_contactsview,
1325 gtk_toolbox.null_sink(),
1330 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1332 self._contactsview.set_model(self._contactsmodel)
1333 self._contactsview.set_fixed_height_mode(True)
1334 self._contactsview.append_column(self._contactColumn)
1335 self._contactsviewselection = self._contactsview.get_selection()
1336 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1338 del self._booksList[:]
1339 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1340 if factoryName and bookName:
1341 entryName = "%s: %s" % (factoryName, bookName)
1343 entryName = factoryName
1345 entryName = bookName
1347 entryName = "Bad name (%d)" % factoryId
1348 row = (str(factoryId), bookId, entryName)
1349 self._booksList.append(row)
1351 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1352 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1354 if len(self._booksList) <= self._selectedComboIndex:
1355 self._selectedComboIndex = 0
1356 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1358 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1359 selectedBookId = self._booksList[self._selectedComboIndex][1]
1360 self.open_addressbook(selectedFactoryId, selectedBookId)
1363 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1364 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1368 self._bookSelectionButton.set_label("")
1369 self._contactsview.set_model(None)
1370 self._contactsview.remove_column(self._contactColumn)
1372 def number_selected(self, action, number, message):
1374 @note Actual dial function is patched in later
1376 raise NotImplementedError("Horrible unknown error has occurred")
1378 def get_addressbooks(self):
1380 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1382 for i, factory in enumerate(self._addressBookFactories):
1383 for bookFactory, bookId, bookName in factory.get_addressbooks():
1384 yield (str(i), bookId), (factory.factory_name(), bookName)
1386 def open_addressbook(self, bookFactoryId, bookId):
1387 bookFactoryIndex = int(bookFactoryId)
1388 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1390 forceUpdate = True if addressBook is not self._addressBook else False
1392 self._addressBook = addressBook
1393 self.update(force=forceUpdate)
1395 def update(self, force = False):
1396 if not force and self._isPopulated:
1398 self._updateSink.send(())
1402 self._isPopulated = False
1403 self._contactsmodel.clear()
1404 for factory in self._addressBookFactories:
1405 factory.clear_caches()
1406 self._addressBook.clear_caches()
1408 def append(self, book):
1409 self._addressBookFactories.append(book)
1411 def extend(self, books):
1412 self._addressBookFactories.extend(books)
1418 def load_settings(self, config, sectionName):
1420 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1421 except ConfigParser.NoOptionError:
1422 self._selectedComboIndex = 0
1424 def save_settings(self, config, sectionName):
1425 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1427 def _idly_populate_contactsview(self):
1428 with gtk_toolbox.gtk_lock():
1429 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1432 while addressBook is not self._addressBook:
1433 addressBook = self._addressBook
1434 with gtk_toolbox.gtk_lock():
1435 self._contactsview.set_model(None)
1439 contacts = addressBook.get_contacts()
1440 except Exception, e:
1442 self._isPopulated = False
1443 self._errorDisplay.push_exception_with_lock()
1444 for contactId, contactName in contacts:
1445 contactType = (addressBook.contact_source_short_name(contactId), )
1446 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1448 with gtk_toolbox.gtk_lock():
1449 self._contactsview.set_model(self._contactsmodel)
1451 self._isPopulated = True
1452 except Exception, e:
1453 self._errorDisplay.push_exception_with_lock()
1455 with gtk_toolbox.gtk_lock():
1456 hildonize.show_busy_banner_end(banner)
1459 def _on_addressbook_button_changed(self, *args, **kwds):
1462 newSelectedComboIndex = hildonize.touch_selector(
1465 (("%s" % m[2]) for m in self._booksList),
1466 self._selectedComboIndex,
1468 except RuntimeError:
1471 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1472 selectedBookId = self._booksList[newSelectedComboIndex][1]
1473 self.open_addressbook(selectedFactoryId, selectedBookId)
1474 self._selectedComboIndex = newSelectedComboIndex
1475 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1476 except Exception, e:
1477 self._errorDisplay.push_exception()
1479 def _on_contactsview_row_activated(self, treeview, path, view_column):
1481 itr = self._contactsmodel.get_iter(path)
1485 contactId = self._contactsmodel.get_value(itr, 3)
1486 contactName = self._contactsmodel.get_value(itr, 1)
1488 contactDetails = self._addressBook.get_contact_details(contactId)
1489 except Exception, e:
1491 self._errorDisplay.push_exception()
1492 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1494 if len(contactPhoneNumbers) == 0:
1497 action, phoneNumber, message = self._phoneTypeSelector.run(
1498 contactPhoneNumbers,
1499 messages = (contactName, ),
1500 parent = self._window,
1502 if action == SmsEntryDialog.ACTION_CANCEL:
1504 assert phoneNumber, "A lack of phone number exists"
1506 self.number_selected(action, phoneNumber, message)
1507 self._contactsviewselection.unselect_all()
1508 except Exception, e:
1509 self._errorDisplay.push_exception()