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._callbackSelectButton.set_label("No Callback Number")
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(),
761 self._backend.set_callback_number(number)
762 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
763 make_pretty(number), make_pretty(self._backend.get_callback_number())
765 prettyNumber = make_pretty(number)
766 if len(prettyNumber) == 0:
767 prettyNumber = "No Callback Number"
768 self._callbackSelectButton.set_label(prettyNumber)
770 "Callback number set to %s" % (
771 self._backend.get_callback_number(),
775 self._errorDisplay.push_exception()
777 def _update_alarm_settings(self, recurrence):
779 isEnabled = self._notifyCheckbox.get_active()
780 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
781 self._alarmHandler.apply_settings(isEnabled, recurrence)
783 self.save_everything()
784 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
785 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
787 def _on_callbackentry_clicked(self, *args):
789 actualSelection = make_pretty(self.get_selected_callback_number())
792 (number, "%s (%s)" % (number, description))
793 for (number, description) in self._callbackList
795 defaultSelection = userOptions.get(actualSelection, actualSelection)
797 userSelection = hildonize.touch_selector_entry(
800 list(userOptions.itervalues()),
803 reversedUserOptions = dict(
804 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
806 selectedNumber = reversedUserOptions.get(userSelection, userSelection)
808 number = make_ugly(selectedNumber)
809 self._set_callback_number(number)
810 except RuntimeError, e:
811 _moduleLogger.exception("%s" % str(e))
813 self._errorDisplay.push_exception()
815 def _on_notify_toggled(self, *args):
817 if self._applyAlarmTimeoutId is not None:
818 gobject.source_remove(self._applyAlarmTimeoutId)
819 self._applyAlarmTimeoutId = None
820 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
822 self._errorDisplay.push_exception()
824 def _on_minutes_clicked(self, *args):
825 recurrenceChoices = [
841 actualSelection = self._alarmHandler.recurrence
843 closestSelectionIndex = 0
844 for i, possible in enumerate(recurrenceChoices):
845 if possible[0] <= actualSelection:
846 closestSelectionIndex = i
847 recurrenceIndex = hildonize.touch_selector(
850 (("%s" % m[1]) for m in recurrenceChoices),
851 closestSelectionIndex,
853 recurrence = recurrenceChoices[recurrenceIndex][0]
855 self._update_alarm_settings(recurrence)
856 except RuntimeError, e:
857 _moduleLogger.exception("%s" % str(e))
859 self._errorDisplay.push_exception()
861 def _on_apply_timeout(self, *args):
863 self._applyAlarmTimeoutId = None
865 self._update_alarm_settings(self._alarmHandler.recurrence)
867 self._errorDisplay.push_exception()
870 def _on_missed_toggled(self, *args):
872 self._notifyOnMissed = self._missedCheckbox.get_active()
873 self.save_everything()
875 self._errorDisplay.push_exception()
877 def _on_voicemail_toggled(self, *args):
879 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
880 self.save_everything()
882 self._errorDisplay.push_exception()
884 def _on_sms_toggled(self, *args):
886 self._notifyOnSms = self._smsCheckbox.get_active()
887 self.save_everything()
889 self._errorDisplay.push_exception()
892 class RecentCallsView(object):
900 def __init__(self, widgetTree, backend, errorDisplay):
901 self._errorDisplay = errorDisplay
902 self._backend = backend
904 self._isPopulated = False
905 self._recentmodel = gtk.ListStore(
906 gobject.TYPE_STRING, # number
907 gobject.TYPE_STRING, # date
908 gobject.TYPE_STRING, # action
909 gobject.TYPE_STRING, # from
910 gobject.TYPE_STRING, # from id
912 self._recentview = widgetTree.get_widget("recentview")
913 self._recentviewselection = None
914 self._onRecentviewRowActivatedId = 0
916 textrenderer = gtk.CellRendererText()
917 textrenderer.set_property("yalign", 0)
918 self._dateColumn = gtk.TreeViewColumn("Date")
919 self._dateColumn.pack_start(textrenderer, expand=True)
920 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
922 textrenderer = gtk.CellRendererText()
923 textrenderer.set_property("yalign", 0)
924 self._actionColumn = gtk.TreeViewColumn("Action")
925 self._actionColumn.pack_start(textrenderer, expand=True)
926 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
928 textrenderer = gtk.CellRendererText()
929 textrenderer.set_property("yalign", 0)
930 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
931 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
932 self._numberColumn = gtk.TreeViewColumn("Number")
933 self._numberColumn.pack_start(textrenderer, expand=True)
934 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
936 textrenderer = gtk.CellRendererText()
937 textrenderer.set_property("yalign", 0)
938 hildonize.set_cell_thumb_selectable(textrenderer)
939 self._nameColumn = gtk.TreeViewColumn("From")
940 self._nameColumn.pack_start(textrenderer, expand=True)
941 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
942 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
944 self._window = gtk_toolbox.find_parent_window(self._recentview)
945 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
947 self._updateSink = gtk_toolbox.threaded_stage(
949 self._idly_populate_recentview,
950 gtk_toolbox.null_sink(),
955 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
956 self._recentview.set_model(self._recentmodel)
957 self._recentview.set_fixed_height_mode(False)
959 self._recentview.append_column(self._dateColumn)
960 self._recentview.append_column(self._actionColumn)
961 self._recentview.append_column(self._numberColumn)
962 self._recentview.append_column(self._nameColumn)
963 self._recentviewselection = self._recentview.get_selection()
964 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
966 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
969 self._recentview.disconnect(self._onRecentviewRowActivatedId)
973 self._recentview.remove_column(self._dateColumn)
974 self._recentview.remove_column(self._actionColumn)
975 self._recentview.remove_column(self._nameColumn)
976 self._recentview.remove_column(self._numberColumn)
977 self._recentview.set_model(None)
979 def number_selected(self, action, number, message):
981 @note Actual dial function is patched in later
983 raise NotImplementedError("Horrible unknown error has occurred")
985 def update(self, force = False):
986 if not force and self._isPopulated:
988 self._updateSink.send(())
992 self._isPopulated = False
993 self._recentmodel.clear()
997 return "Recent Calls"
999 def load_settings(self, config, section):
1002 def save_settings(self, config, section):
1004 @note Thread Agnostic
1008 def _idly_populate_recentview(self):
1009 with gtk_toolbox.gtk_lock():
1010 banner = hildonize.show_busy_banner_start(self._window, "Loading Recent History")
1012 self._recentmodel.clear()
1013 self._isPopulated = True
1016 recentItems = self._backend.get_recent()
1017 except Exception, e:
1018 self._errorDisplay.push_exception_with_lock()
1019 self._isPopulated = False
1023 gv_backend.decorate_recent(data)
1024 for data in gv_backend.sort_messages(recentItems)
1027 for contactId, personName, phoneNumber, date, action in recentItems:
1029 personName = "Unknown"
1030 date = abbrev_relative_date(date)
1031 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1032 prettyNumber = make_pretty(prettyNumber)
1033 item = (prettyNumber, date, action.capitalize(), personName, contactId)
1034 with gtk_toolbox.gtk_lock():
1035 self._recentmodel.append(item)
1036 except Exception, e:
1037 self._errorDisplay.push_exception_with_lock()
1039 with gtk_toolbox.gtk_lock():
1040 hildonize.show_busy_banner_end(banner)
1044 def _on_recentview_row_activated(self, treeview, path, view_column):
1046 itr = self._recentmodel.get_iter(path)
1050 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1051 number = make_ugly(number)
1052 description = self._recentmodel.get_value(itr, self.FROM_IDX)
1053 contactId = self._recentmodel.get_value(itr, self.FROM_ID_IDX)
1055 contactPhoneNumbers = list(self._backend.get_contact_details(contactId))
1057 (number == make_ugly(contactNumber) or number[1:] == make_ugly(contactNumber))
1058 for (numberDescription, contactNumber) in contactPhoneNumbers
1061 defaultIndex = defaultMatches.index(True)
1063 contactPhoneNumbers.append(("Other", number))
1064 defaultIndex = len(contactPhoneNumbers)-1
1066 "Could not find contact %r's number %s among %r" % (
1067 contactId, number, contactPhoneNumbers
1071 contactPhoneNumbers = [("Phone", number)]
1074 action, phoneNumber, message = self._phoneTypeSelector.run(
1075 contactPhoneNumbers,
1076 messages = (description, ),
1077 parent = self._window,
1078 defaultIndex = defaultIndex,
1080 if action == SmsEntryDialog.ACTION_CANCEL:
1082 assert phoneNumber, "A lack of phone number exists"
1084 self.number_selected(action, phoneNumber, message)
1085 self._recentviewselection.unselect_all()
1086 except Exception, e:
1087 self._errorDisplay.push_exception()
1090 class MessagesView(object):
1099 def __init__(self, widgetTree, backend, errorDisplay):
1100 self._errorDisplay = errorDisplay
1101 self._backend = backend
1103 self._isPopulated = False
1104 self._messagemodel = gtk.ListStore(
1105 gobject.TYPE_STRING, # number
1106 gobject.TYPE_STRING, # date
1107 gobject.TYPE_STRING, # header
1108 gobject.TYPE_STRING, # message
1110 gobject.TYPE_STRING, # from id
1112 self._messageview = widgetTree.get_widget("messages_view")
1113 self._messageviewselection = None
1114 self._onMessageviewRowActivatedId = 0
1116 self._messageRenderer = gtk.CellRendererText()
1117 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1118 self._messageRenderer.set_property("wrap-width", 500)
1119 self._messageColumn = gtk.TreeViewColumn("Messages")
1120 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1121 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1122 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1124 self._window = gtk_toolbox.find_parent_window(self._messageview)
1125 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
1127 self._updateSink = gtk_toolbox.threaded_stage(
1129 self._idly_populate_messageview,
1130 gtk_toolbox.null_sink(),
1135 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1136 self._messageview.set_model(self._messagemodel)
1137 self._messageview.set_headers_visible(False)
1138 self._messageview.set_fixed_height_mode(False)
1140 self._messageview.append_column(self._messageColumn)
1141 self._messageviewselection = self._messageview.get_selection()
1142 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1144 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1147 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1151 self._messageview.remove_column(self._messageColumn)
1152 self._messageview.set_model(None)
1154 def number_selected(self, action, number, message):
1156 @note Actual dial function is patched in later
1158 raise NotImplementedError("Horrible unknown error has occurred")
1160 def update(self, force = False):
1161 if not force and self._isPopulated:
1163 self._updateSink.send(())
1167 self._isPopulated = False
1168 self._messagemodel.clear()
1174 def load_settings(self, config, section):
1177 def save_settings(self, config, section):
1179 @note Thread Agnostic
1183 _MIN_MESSAGES_SHOWN = 4
1185 def _idly_populate_messageview(self):
1186 with gtk_toolbox.gtk_lock():
1187 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1189 self._messagemodel.clear()
1190 self._isPopulated = True
1193 messageItems = self._backend.get_messages()
1194 except Exception, e:
1195 self._errorDisplay.push_exception_with_lock()
1196 self._isPopulated = False
1200 gv_backend.decorate_message(message)
1201 for message in gv_backend.sort_messages(messageItems)
1204 for contactId, header, number, relativeDate, messages in messageItems:
1205 prettyNumber = number[2:] if number.startswith("+1") else number
1206 prettyNumber = make_pretty(prettyNumber)
1208 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1209 expandedMessages = [firstMessage]
1210 expandedMessages.extend(messages)
1211 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1212 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1213 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1214 collapsedMessages = [firstMessage, secondMessage]
1215 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1217 collapsedMessages = expandedMessages
1219 number = make_ugly(number)
1221 row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId
1222 with gtk_toolbox.gtk_lock():
1223 self._messagemodel.append(row)
1224 except Exception, e:
1225 self._errorDisplay.push_exception_with_lock()
1227 with gtk_toolbox.gtk_lock():
1228 hildonize.show_busy_banner_end(banner)
1232 def _on_messageview_row_activated(self, treeview, path, view_column):
1234 itr = self._messagemodel.get_iter(path)
1238 number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX))
1239 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1241 contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX)
1243 contactPhoneNumbers = list(self._backend.get_contact_details(contactId))
1245 (number == make_ugly(contactNumber) or number[1:] == make_ugly(contactNumber))
1246 for (numberDescription, contactNumber) in contactPhoneNumbers
1249 defaultIndex = defaultMatches.index(True)
1251 contactPhoneNumbers.append(("Other", number))
1252 defaultIndex = len(contactPhoneNumbers)-1
1254 "Could not find contact %r's number %s among %r" % (
1255 contactId, number, contactPhoneNumbers
1259 contactPhoneNumbers = [("Phone", number)]
1262 action, phoneNumber, message = self._phoneTypeSelector.run(
1263 contactPhoneNumbers,
1264 messages = description,
1265 parent = self._window,
1266 defaultIndex = defaultIndex,
1268 if action == SmsEntryDialog.ACTION_CANCEL:
1270 assert phoneNumber, "A lock of phone number exists"
1272 self.number_selected(action, phoneNumber, message)
1273 self._messageviewselection.unselect_all()
1274 except Exception, e:
1275 self._errorDisplay.push_exception()
1278 class ContactsView(object):
1280 def __init__(self, widgetTree, backend, errorDisplay):
1281 self._errorDisplay = errorDisplay
1282 self._backend = backend
1284 self._addressBook = None
1285 self._selectedComboIndex = 0
1286 self._addressBookFactories = [null_backend.NullAddressBook()]
1288 self._booksList = []
1289 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1291 self._isPopulated = False
1292 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1293 self._contactsviewselection = None
1294 self._contactsview = widgetTree.get_widget("contactsview")
1296 self._contactColumn = gtk.TreeViewColumn("Contact")
1297 displayContactSource = False
1298 if displayContactSource:
1299 textrenderer = gtk.CellRendererText()
1300 self._contactColumn.pack_start(textrenderer, expand=False)
1301 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1302 textrenderer = gtk.CellRendererText()
1303 hildonize.set_cell_thumb_selectable(textrenderer)
1304 self._contactColumn.pack_start(textrenderer, expand=True)
1305 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1306 textrenderer = gtk.CellRendererText()
1307 self._contactColumn.pack_start(textrenderer, expand=True)
1308 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1309 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1310 self._contactColumn.set_sort_column_id(1)
1311 self._contactColumn.set_visible(True)
1313 self._onContactsviewRowActivatedId = 0
1314 self._onAddressbookButtonChangedId = 0
1315 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1316 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
1318 self._updateSink = gtk_toolbox.threaded_stage(
1320 self._idly_populate_contactsview,
1321 gtk_toolbox.null_sink(),
1326 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1328 self._contactsview.set_model(self._contactsmodel)
1329 self._contactsview.set_fixed_height_mode(True)
1330 self._contactsview.append_column(self._contactColumn)
1331 self._contactsviewselection = self._contactsview.get_selection()
1332 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1334 del self._booksList[:]
1335 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1336 if factoryName and bookName:
1337 entryName = "%s: %s" % (factoryName, bookName)
1339 entryName = factoryName
1341 entryName = bookName
1343 entryName = "Bad name (%d)" % factoryId
1344 row = (str(factoryId), bookId, entryName)
1345 self._booksList.append(row)
1347 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1348 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1350 if len(self._booksList) <= self._selectedComboIndex:
1351 self._selectedComboIndex = 0
1352 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1354 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1355 selectedBookId = self._booksList[self._selectedComboIndex][1]
1356 self.open_addressbook(selectedFactoryId, selectedBookId)
1359 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1360 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1364 self._bookSelectionButton.set_label("")
1365 self._contactsview.set_model(None)
1366 self._contactsview.remove_column(self._contactColumn)
1368 def number_selected(self, action, number, message):
1370 @note Actual dial function is patched in later
1372 raise NotImplementedError("Horrible unknown error has occurred")
1374 def get_addressbooks(self):
1376 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1378 for i, factory in enumerate(self._addressBookFactories):
1379 for bookFactory, bookId, bookName in factory.get_addressbooks():
1380 yield (str(i), bookId), (factory.factory_name(), bookName)
1382 def open_addressbook(self, bookFactoryId, bookId):
1383 bookFactoryIndex = int(bookFactoryId)
1384 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1386 forceUpdate = True if addressBook is not self._addressBook else False
1388 self._addressBook = addressBook
1389 self.update(force=forceUpdate)
1391 def update(self, force = False):
1392 if not force and self._isPopulated:
1394 self._updateSink.send(())
1398 self._isPopulated = False
1399 self._contactsmodel.clear()
1400 for factory in self._addressBookFactories:
1401 factory.clear_caches()
1402 self._addressBook.clear_caches()
1404 def append(self, book):
1405 self._addressBookFactories.append(book)
1407 def extend(self, books):
1408 self._addressBookFactories.extend(books)
1414 def load_settings(self, config, sectionName):
1416 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1417 except ConfigParser.NoOptionError:
1418 self._selectedComboIndex = 0
1420 def save_settings(self, config, sectionName):
1421 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1423 def _idly_populate_contactsview(self):
1424 with gtk_toolbox.gtk_lock():
1425 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1428 while addressBook is not self._addressBook:
1429 addressBook = self._addressBook
1430 with gtk_toolbox.gtk_lock():
1431 self._contactsview.set_model(None)
1435 contacts = addressBook.get_contacts()
1436 except Exception, e:
1438 self._isPopulated = False
1439 self._errorDisplay.push_exception_with_lock()
1440 for contactId, contactName in contacts:
1441 contactType = (addressBook.contact_source_short_name(contactId), )
1442 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1444 with gtk_toolbox.gtk_lock():
1445 self._contactsview.set_model(self._contactsmodel)
1447 self._isPopulated = True
1448 except Exception, e:
1449 self._errorDisplay.push_exception_with_lock()
1451 with gtk_toolbox.gtk_lock():
1452 hildonize.show_busy_banner_end(banner)
1455 def _on_addressbook_button_changed(self, *args, **kwds):
1458 newSelectedComboIndex = hildonize.touch_selector(
1461 (("%s" % m[2]) for m in self._booksList),
1462 self._selectedComboIndex,
1464 except RuntimeError:
1467 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1468 selectedBookId = self._booksList[newSelectedComboIndex][1]
1469 self.open_addressbook(selectedFactoryId, selectedBookId)
1470 self._selectedComboIndex = newSelectedComboIndex
1471 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1472 except Exception, e:
1473 self._errorDisplay.push_exception()
1475 def _on_contactsview_row_activated(self, treeview, path, view_column):
1477 itr = self._contactsmodel.get_iter(path)
1481 contactId = self._contactsmodel.get_value(itr, 3)
1482 contactName = self._contactsmodel.get_value(itr, 1)
1484 contactDetails = self._addressBook.get_contact_details(contactId)
1485 except Exception, e:
1487 self._errorDisplay.push_exception()
1488 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1490 if len(contactPhoneNumbers) == 0:
1493 action, phoneNumber, message = self._phoneTypeSelector.run(
1494 contactPhoneNumbers,
1495 messages = (contactName, ),
1496 parent = self._window,
1498 if action == SmsEntryDialog.ACTION_CANCEL:
1500 assert phoneNumber, "A lack of phone number exists"
1502 self.number_selected(action, phoneNumber, message)
1503 self._contactsviewselection.unselect_all()
1504 except Exception, e:
1505 self._errorDisplay.push_exception()