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):
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 = 0
318 self._phoneButton.set_label(self._contactDetails[0][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)
360 self._messagesView.scroll_to_cell((len(messages)-1, ))
361 self._smsEntry.grab_focus()
363 if 1 < len(self._contactDetails):
364 self._request_number()
365 self._phoneButton.set_sensitive(True)
367 self._phoneButton.set_sensitive(False)
369 userResponse = self._dialog.run()
373 # Process the users response
374 if userResponse == gtk.RESPONSE_OK and 0 <= self._numberIndex:
375 phoneNumber = self._contactDetails[self._numberIndex][0]
376 phoneNumber = make_ugly(phoneNumber)
380 self._action = self.ACTION_CANCEL
381 if self._action == self.ACTION_SEND_SMS:
382 entryBuffer = self._smsEntry.get_buffer()
383 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
384 enteredMessage = enteredMessage[0:self.MAX_CHAR].strip()
385 if not enteredMessage:
387 self._action = self.ACTION_CANCEL
391 self._messagesView.remove_column(messageColumn)
392 self._messagesView.set_model(None)
394 return self._action, phoneNumber, enteredMessage
396 self._smsEntry.get_buffer().disconnect(entryConnectId)
397 self._phoneButton.disconnect(phoneConnectId)
398 self._keyPressEventId = self._dialog.disconnect(keyConnectId)
400 def _update_letter_count(self, *args):
401 entryLength = self._smsEntry.get_buffer().get_char_count()
402 charsLeft = self.MAX_CHAR - entryLength
403 self._letterCountLabel.set_text(str(charsLeft))
404 if charsLeft < 0 or charsLeft == self.MAX_CHAR:
405 self._smsButton.set_sensitive(False)
407 self._smsButton.set_sensitive(True)
409 def _request_number(self):
411 assert 0 <= self._numberIndex, "%r" % self._numberIndex
413 self._numberIndex = hildonize.touch_selector(
416 (description for (number, description) in self._contactDetails),
419 self._phoneButton.set_label(self._contactDetails[self._numberIndex][1])
421 _moduleLogger.exception("%s" % str(e))
423 def _on_phone(self, *args):
424 self._request_number()
426 def _on_entry_changed(self, *args):
427 self._update_letter_count()
429 def _on_send(self, *args):
430 self._dialog.response(gtk.RESPONSE_OK)
432 def _on_dial(self, *args):
433 self._dialog.response(gtk.RESPONSE_OK)
434 self._action = self.ACTION_DIAL
436 def _on_cancel(self, *args):
437 self._dialog.response(gtk.RESPONSE_CANCEL)
438 self._action = self.ACTION_CANCEL
440 def _on_key_press(self, widget, event):
442 if event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
445 for messagePart in self._messagemodel
447 # For some reason this kills clipboard stuff
448 #self._clipboard.set_text(message)
450 _moduleLogger.exception(str(e))
453 class Dialpad(object):
455 def __init__(self, widgetTree, errorDisplay):
456 self._clipboard = gtk.clipboard_get()
457 self._errorDisplay = errorDisplay
458 self._smsDialog = SmsEntryDialog(widgetTree)
460 self._numberdisplay = widgetTree.get_widget("numberdisplay")
461 self._smsButton = widgetTree.get_widget("sms")
462 self._dialButton = widgetTree.get_widget("dial")
463 self._backButton = widgetTree.get_widget("back")
464 self._phonenumber = ""
465 self._prettynumber = ""
468 "on_digit_clicked": self._on_digit_clicked,
470 widgetTree.signal_autoconnect(callbackMapping)
471 self._dialButton.connect("clicked", self._on_dial_clicked)
472 self._smsButton.connect("clicked", self._on_sms_clicked)
474 self._originalLabel = self._backButton.get_label()
475 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
476 self._backTapHandler.on_tap = self._on_backspace
477 self._backTapHandler.on_hold = self._on_clearall
478 self._backTapHandler.on_holding = self._set_clear_button
479 self._backTapHandler.on_cancel = self._reset_back_button
481 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
482 self._keyPressEventId = 0
485 self._dialButton.grab_focus()
486 self._backTapHandler.enable()
487 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
490 self._window.disconnect(self._keyPressEventId)
491 self._keyPressEventId = 0
492 self._reset_back_button()
493 self._backTapHandler.disable()
495 def number_selected(self, action, number, message):
497 @note Actual dial function is patched in later
499 raise NotImplementedError("Horrible unknown error has occurred")
501 def get_number(self):
502 return self._phonenumber
504 def set_number(self, number):
506 Set the number to dial
509 self._phonenumber = make_ugly(number)
510 self._prettynumber = make_pretty(self._phonenumber)
511 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
513 self._errorDisplay.push_exception()
522 def load_settings(self, config, section):
525 def save_settings(self, config, section):
527 @note Thread Agnostic
531 def _on_key_press(self, widget, event):
533 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
534 contents = self._clipboard.wait_for_text()
535 if contents is not None:
536 self.set_number(contents)
538 self._errorDisplay.push_exception()
540 def _on_sms_clicked(self, widget):
542 phoneNumber = self.get_number()
543 action, phoneNumber, message = self._smsDialog.run([("Dialer", phoneNumber)], (), self._window)
545 if action == SmsEntryDialog.ACTION_CANCEL:
547 self.number_selected(action, phoneNumber, message)
549 self._errorDisplay.push_exception()
551 def _on_dial_clicked(self, widget):
553 action = SmsEntryDialog.ACTION_DIAL
554 phoneNumber = self.get_number()
556 self.number_selected(action, phoneNumber, message)
558 self._errorDisplay.push_exception()
560 def _on_digit_clicked(self, widget):
562 self.set_number(self._phonenumber + widget.get_name()[-1])
564 self._errorDisplay.push_exception()
566 def _on_backspace(self, taps):
568 self.set_number(self._phonenumber[:-taps])
569 self._reset_back_button()
571 self._errorDisplay.push_exception()
573 def _on_clearall(self, taps):
576 self._reset_back_button()
578 self._errorDisplay.push_exception()
581 def _set_clear_button(self):
583 self._backButton.set_label("gtk-clear")
585 self._errorDisplay.push_exception()
587 def _reset_back_button(self):
589 self._backButton.set_label(self._originalLabel)
591 self._errorDisplay.push_exception()
594 class AccountInfo(object):
596 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
597 self._errorDisplay = errorDisplay
598 self._backend = backend
599 self._isPopulated = False
600 self._alarmHandler = alarmHandler
601 self._notifyOnMissed = False
602 self._notifyOnVoicemail = False
603 self._notifyOnSms = False
605 self._callbackList = []
606 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
607 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
608 self._onCallbackSelectChangedId = 0
610 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
611 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
612 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
613 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
614 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
615 self._onNotifyToggled = 0
616 self._onMinutesChanged = 0
617 self._onMissedToggled = 0
618 self._onVoicemailToggled = 0
619 self._onSmsToggled = 0
620 self._applyAlarmTimeoutId = None
622 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
623 self._defaultCallback = ""
626 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
628 self._accountViewNumberDisplay.set_use_markup(True)
629 self.set_account_number("")
631 del self._callbackList[:]
632 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
634 if self._alarmHandler is not None:
635 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
636 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
637 self._missedCheckbox.set_active(self._notifyOnMissed)
638 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
639 self._smsCheckbox.set_active(self._notifyOnSms)
641 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
642 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
643 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
644 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
645 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
647 self._notifyCheckbox.set_sensitive(False)
648 self._minutesEntryButton.set_sensitive(False)
649 self._missedCheckbox.set_sensitive(False)
650 self._voicemailCheckbox.set_sensitive(False)
651 self._smsCheckbox.set_sensitive(False)
653 self.update(force=True)
656 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
657 self._onCallbackSelectChangedId = 0
659 if self._alarmHandler is not None:
660 self._notifyCheckbox.disconnect(self._onNotifyToggled)
661 self._minutesEntryButton.disconnect(self._onMinutesChanged)
662 self._missedCheckbox.disconnect(self._onNotifyToggled)
663 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
664 self._smsCheckbox.disconnect(self._onNotifyToggled)
665 self._onNotifyToggled = 0
666 self._onMinutesChanged = 0
667 self._onMissedToggled = 0
668 self._onVoicemailToggled = 0
669 self._onSmsToggled = 0
671 self._notifyCheckbox.set_sensitive(True)
672 self._minutesEntryButton.set_sensitive(True)
673 self._missedCheckbox.set_sensitive(True)
674 self._voicemailCheckbox.set_sensitive(True)
675 self._smsCheckbox.set_sensitive(True)
678 del self._callbackList[:]
680 def get_selected_callback_number(self):
681 currentLabel = self._callbackSelectButton.get_label()
682 if currentLabel is not None:
683 return make_ugly(currentLabel)
687 def set_account_number(self, number):
689 Displays current account number
691 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
693 def update(self, force = False):
694 if not force and self._isPopulated:
696 self._populate_callback_combo()
697 self.set_account_number(self._backend.get_account_number())
701 self._callbackSelectButton.set_label("No Callback Number")
702 self.set_account_number("")
703 self._isPopulated = False
705 def save_everything(self):
706 raise NotImplementedError
710 return "Account Info"
712 def load_settings(self, config, section):
713 self._defaultCallback = config.get(section, "callback")
714 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
715 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
716 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
718 def save_settings(self, config, section):
720 @note Thread Agnostic
722 callback = self.get_selected_callback_number()
723 config.set(section, "callback", callback)
724 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
725 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
726 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
728 def _populate_callback_combo(self):
729 self._isPopulated = True
730 del self._callbackList[:]
732 callbackNumbers = self._backend.get_callback_numbers()
734 self._errorDisplay.push_exception()
735 self._isPopulated = False
738 if len(callbackNumbers) == 0:
739 callbackNumbers = {"": "No callback numbers available"}
741 for number, description in callbackNumbers.iteritems():
742 self._callbackList.append((make_pretty(number), description))
744 self._set_callback_number(self._defaultCallback)
746 def _set_callback_number(self, number):
748 if not self._backend.is_valid_syntax(number) and 0 < len(number):
749 self._errorDisplay.push_message("%s is not a valid callback number" % number)
750 elif number == self._backend.get_callback_number() and 0 < len(number):
751 _moduleLogger.warning(
752 "Callback number already is %s" % (
753 self._backend.get_callback_number(),
757 self._backend.set_callback_number(number)
758 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
759 make_pretty(number), make_pretty(self._backend.get_callback_number())
761 prettyNumber = make_pretty(number)
762 if len(prettyNumber) == 0:
763 prettyNumber = "No Callback Number"
764 self._callbackSelectButton.set_label(prettyNumber)
766 "Callback number set to %s" % (
767 self._backend.get_callback_number(),
771 self._errorDisplay.push_exception()
773 def _update_alarm_settings(self, recurrence):
775 isEnabled = self._notifyCheckbox.get_active()
776 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
777 self._alarmHandler.apply_settings(isEnabled, recurrence)
779 self.save_everything()
780 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
781 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
783 def _on_callbackentry_clicked(self, *args):
785 actualSelection = make_pretty(self.get_selected_callback_number())
788 (number, "%s (%s)" % (number, description))
789 for (number, description) in self._callbackList
791 defaultSelection = userOptions.get(actualSelection, actualSelection)
793 userSelection = hildonize.touch_selector_entry(
796 list(userOptions.itervalues()),
799 reversedUserOptions = dict(
800 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
802 selectedNumber = reversedUserOptions.get(userSelection, userSelection)
804 number = make_ugly(selectedNumber)
805 self._set_callback_number(number)
806 except RuntimeError, e:
807 _moduleLogger.exception("%s" % str(e))
809 self._errorDisplay.push_exception()
811 def _on_notify_toggled(self, *args):
813 if self._applyAlarmTimeoutId is not None:
814 gobject.source_remove(self._applyAlarmTimeoutId)
815 self._applyAlarmTimeoutId = None
816 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
818 self._errorDisplay.push_exception()
820 def _on_minutes_clicked(self, *args):
821 recurrenceChoices = [
837 actualSelection = self._alarmHandler.recurrence
839 closestSelectionIndex = 0
840 for i, possible in enumerate(recurrenceChoices):
841 if possible[0] <= actualSelection:
842 closestSelectionIndex = i
843 recurrenceIndex = hildonize.touch_selector(
846 (("%s" % m[1]) for m in recurrenceChoices),
847 closestSelectionIndex,
849 recurrence = recurrenceChoices[recurrenceIndex][0]
851 self._update_alarm_settings(recurrence)
852 except RuntimeError, e:
853 _moduleLogger.exception("%s" % str(e))
855 self._errorDisplay.push_exception()
857 def _on_apply_timeout(self, *args):
859 self._applyAlarmTimeoutId = None
861 self._update_alarm_settings(self._alarmHandler.recurrence)
863 self._errorDisplay.push_exception()
866 def _on_missed_toggled(self, *args):
868 self._notifyOnMissed = self._missedCheckbox.get_active()
869 self.save_everything()
871 self._errorDisplay.push_exception()
873 def _on_voicemail_toggled(self, *args):
875 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
876 self.save_everything()
878 self._errorDisplay.push_exception()
880 def _on_sms_toggled(self, *args):
882 self._notifyOnSms = self._smsCheckbox.get_active()
883 self.save_everything()
885 self._errorDisplay.push_exception()
888 class RecentCallsView(object):
895 def __init__(self, widgetTree, backend, errorDisplay):
896 self._errorDisplay = errorDisplay
897 self._backend = backend
899 self._isPopulated = False
900 self._recentmodel = gtk.ListStore(
901 gobject.TYPE_STRING, # number
902 gobject.TYPE_STRING, # date
903 gobject.TYPE_STRING, # action
904 gobject.TYPE_STRING, # from
906 self._recentview = widgetTree.get_widget("recentview")
907 self._recentviewselection = None
908 self._onRecentviewRowActivatedId = 0
910 textrenderer = gtk.CellRendererText()
911 textrenderer.set_property("yalign", 0)
912 self._dateColumn = gtk.TreeViewColumn("Date")
913 self._dateColumn.pack_start(textrenderer, expand=True)
914 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
916 textrenderer = gtk.CellRendererText()
917 textrenderer.set_property("yalign", 0)
918 self._actionColumn = gtk.TreeViewColumn("Action")
919 self._actionColumn.pack_start(textrenderer, expand=True)
920 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
922 textrenderer = gtk.CellRendererText()
923 textrenderer.set_property("yalign", 0)
924 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
925 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
926 self._numberColumn = gtk.TreeViewColumn("Number")
927 self._numberColumn.pack_start(textrenderer, expand=True)
928 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
930 textrenderer = gtk.CellRendererText()
931 textrenderer.set_property("yalign", 0)
932 hildonize.set_cell_thumb_selectable(textrenderer)
933 self._nameColumn = gtk.TreeViewColumn("From")
934 self._nameColumn.pack_start(textrenderer, expand=True)
935 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
936 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
938 self._window = gtk_toolbox.find_parent_window(self._recentview)
939 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
941 self._updateSink = gtk_toolbox.threaded_stage(
943 self._idly_populate_recentview,
944 gtk_toolbox.null_sink(),
949 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
950 self._recentview.set_model(self._recentmodel)
951 self._recentview.set_fixed_height_mode(False)
953 self._recentview.append_column(self._dateColumn)
954 self._recentview.append_column(self._actionColumn)
955 self._recentview.append_column(self._numberColumn)
956 self._recentview.append_column(self._nameColumn)
957 self._recentviewselection = self._recentview.get_selection()
958 self._recentviewselection.set_mode(gtk.SELECTION_NONE)
960 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
963 self._recentview.disconnect(self._onRecentviewRowActivatedId)
967 self._recentview.remove_column(self._dateColumn)
968 self._recentview.remove_column(self._actionColumn)
969 self._recentview.remove_column(self._nameColumn)
970 self._recentview.remove_column(self._numberColumn)
971 self._recentview.set_model(None)
973 def number_selected(self, action, number, message):
975 @note Actual dial function is patched in later
977 raise NotImplementedError("Horrible unknown error has occurred")
979 def update(self, force = False):
980 if not force and self._isPopulated:
982 self._updateSink.send(())
986 self._isPopulated = False
987 self._recentmodel.clear()
991 return "Recent Calls"
993 def load_settings(self, config, section):
996 def save_settings(self, config, section):
998 @note Thread Agnostic
1002 def _idly_populate_recentview(self):
1003 with gtk_toolbox.gtk_lock():
1004 banner = hildonize.show_busy_banner_start(self._window, "Loading Recent History")
1006 self._recentmodel.clear()
1007 self._isPopulated = True
1010 recentItems = self._backend.get_recent()
1011 except Exception, e:
1012 self._errorDisplay.push_exception_with_lock()
1013 self._isPopulated = False
1017 gv_backend.decorate_recent(data)
1018 for data in gv_backend.sort_messages(recentItems)
1021 for personName, phoneNumber, date, action in recentItems:
1023 personName = "Unknown"
1024 date = abbrev_relative_date(date)
1025 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1026 prettyNumber = make_pretty(prettyNumber)
1027 item = (prettyNumber, date, action.capitalize(), personName)
1028 with gtk_toolbox.gtk_lock():
1029 self._recentmodel.append(item)
1030 except Exception, e:
1031 self._errorDisplay.push_exception_with_lock()
1033 with gtk_toolbox.gtk_lock():
1034 hildonize.show_busy_banner_end(banner)
1038 def _on_recentview_row_activated(self, treeview, path, view_column):
1040 itr = self._recentmodel.get_iter(path)
1044 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1045 number = make_ugly(number)
1046 contactPhoneNumbers = [("Phone", number)]
1047 description = self._recentmodel.get_value(itr, self.FROM_IDX)
1049 action, phoneNumber, message = self._phoneTypeSelector.run(
1050 contactPhoneNumbers,
1051 messages = (description, ),
1052 parent = self._window,
1054 if action == SmsEntryDialog.ACTION_CANCEL:
1056 assert phoneNumber, "A lack of phone number exists"
1058 self.number_selected(action, phoneNumber, message)
1059 self._recentviewselection.unselect_all()
1060 except Exception, e:
1061 self._errorDisplay.push_exception()
1064 class MessagesView(object):
1072 def __init__(self, widgetTree, backend, errorDisplay):
1073 self._errorDisplay = errorDisplay
1074 self._backend = backend
1076 self._isPopulated = False
1077 self._messagemodel = gtk.ListStore(
1078 gobject.TYPE_STRING, # number
1079 gobject.TYPE_STRING, # date
1080 gobject.TYPE_STRING, # header
1081 gobject.TYPE_STRING, # message
1084 self._messageview = widgetTree.get_widget("messages_view")
1085 self._messageviewselection = None
1086 self._onMessageviewRowActivatedId = 0
1088 self._messageRenderer = gtk.CellRendererText()
1089 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1090 self._messageRenderer.set_property("wrap-width", 500)
1091 self._messageColumn = gtk.TreeViewColumn("Messages")
1092 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1093 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1094 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1096 self._window = gtk_toolbox.find_parent_window(self._messageview)
1097 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
1099 self._updateSink = gtk_toolbox.threaded_stage(
1101 self._idly_populate_messageview,
1102 gtk_toolbox.null_sink(),
1107 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1108 self._messageview.set_model(self._messagemodel)
1109 self._messageview.set_headers_visible(False)
1110 self._messageview.set_fixed_height_mode(False)
1112 self._messageview.append_column(self._messageColumn)
1113 self._messageviewselection = self._messageview.get_selection()
1114 self._messageviewselection.set_mode(gtk.SELECTION_NONE)
1116 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1119 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1123 self._messageview.remove_column(self._messageColumn)
1124 self._messageview.set_model(None)
1126 def number_selected(self, action, number, message):
1128 @note Actual dial function is patched in later
1130 raise NotImplementedError("Horrible unknown error has occurred")
1132 def update(self, force = False):
1133 if not force and self._isPopulated:
1135 self._updateSink.send(())
1139 self._isPopulated = False
1140 self._messagemodel.clear()
1146 def load_settings(self, config, section):
1149 def save_settings(self, config, section):
1151 @note Thread Agnostic
1155 _MIN_MESSAGES_SHOWN = 4
1157 def _idly_populate_messageview(self):
1158 with gtk_toolbox.gtk_lock():
1159 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1161 self._messagemodel.clear()
1162 self._isPopulated = True
1165 messageItems = self._backend.get_messages()
1166 except Exception, e:
1167 self._errorDisplay.push_exception_with_lock()
1168 self._isPopulated = False
1172 gv_backend.decorate_message(message)
1173 for message in gv_backend.sort_messages(messageItems)
1176 for header, number, relativeDate, messages in messageItems:
1177 prettyNumber = number[2:] if number.startswith("+1") else number
1178 prettyNumber = make_pretty(prettyNumber)
1180 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1181 expandedMessages = [firstMessage]
1182 expandedMessages.extend(messages)
1183 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1184 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1185 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1186 collapsedMessages = [firstMessage, secondMessage]
1187 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1189 collapsedMessages = expandedMessages
1191 number = make_ugly(number)
1193 row = (number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages)
1194 with gtk_toolbox.gtk_lock():
1195 self._messagemodel.append(row)
1196 except Exception, e:
1197 self._errorDisplay.push_exception_with_lock()
1199 with gtk_toolbox.gtk_lock():
1200 hildonize.show_busy_banner_end(banner)
1204 def _on_messageview_row_activated(self, treeview, path, view_column):
1206 itr = self._messagemodel.get_iter(path)
1210 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1211 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1213 action, phoneNumber, message = self._phoneTypeSelector.run(
1214 contactPhoneNumbers,
1215 messages = description,
1216 parent = self._window,
1218 if action == SmsEntryDialog.ACTION_CANCEL:
1220 assert phoneNumber, "A lock of phone number exists"
1222 self.number_selected(action, phoneNumber, message)
1223 self._messageviewselection.unselect_all()
1224 except Exception, e:
1225 self._errorDisplay.push_exception()
1228 class ContactsView(object):
1230 def __init__(self, widgetTree, backend, errorDisplay):
1231 self._errorDisplay = errorDisplay
1232 self._backend = backend
1234 self._addressBook = None
1235 self._selectedComboIndex = 0
1236 self._addressBookFactories = [null_backend.NullAddressBook()]
1238 self._booksList = []
1239 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1241 self._isPopulated = False
1242 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1243 self._contactsviewselection = None
1244 self._contactsview = widgetTree.get_widget("contactsview")
1246 self._contactColumn = gtk.TreeViewColumn("Contact")
1247 displayContactSource = False
1248 if displayContactSource:
1249 textrenderer = gtk.CellRendererText()
1250 self._contactColumn.pack_start(textrenderer, expand=False)
1251 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1252 textrenderer = gtk.CellRendererText()
1253 hildonize.set_cell_thumb_selectable(textrenderer)
1254 self._contactColumn.pack_start(textrenderer, expand=True)
1255 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1256 textrenderer = gtk.CellRendererText()
1257 self._contactColumn.pack_start(textrenderer, expand=True)
1258 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1259 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1260 self._contactColumn.set_sort_column_id(1)
1261 self._contactColumn.set_visible(True)
1263 self._onContactsviewRowActivatedId = 0
1264 self._onAddressbookButtonChangedId = 0
1265 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1266 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
1268 self._updateSink = gtk_toolbox.threaded_stage(
1270 self._idly_populate_contactsview,
1271 gtk_toolbox.null_sink(),
1276 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1278 self._contactsview.set_model(self._contactsmodel)
1279 self._contactsview.set_fixed_height_mode(True)
1280 self._contactsview.append_column(self._contactColumn)
1281 self._contactsviewselection = self._contactsview.get_selection()
1282 self._contactsviewselection.set_mode(gtk.SELECTION_NONE)
1284 del self._booksList[:]
1285 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1286 if factoryName and bookName:
1287 entryName = "%s: %s" % (factoryName, bookName)
1289 entryName = factoryName
1291 entryName = bookName
1293 entryName = "Bad name (%d)" % factoryId
1294 row = (str(factoryId), bookId, entryName)
1295 self._booksList.append(row)
1297 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1298 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1300 if len(self._booksList) <= self._selectedComboIndex:
1301 self._selectedComboIndex = 0
1302 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1304 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1305 selectedBookId = self._booksList[self._selectedComboIndex][1]
1306 self.open_addressbook(selectedFactoryId, selectedBookId)
1309 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1310 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1314 self._bookSelectionButton.set_label("")
1315 self._contactsview.set_model(None)
1316 self._contactsview.remove_column(self._contactColumn)
1318 def number_selected(self, action, number, message):
1320 @note Actual dial function is patched in later
1322 raise NotImplementedError("Horrible unknown error has occurred")
1324 def get_addressbooks(self):
1326 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1328 for i, factory in enumerate(self._addressBookFactories):
1329 for bookFactory, bookId, bookName in factory.get_addressbooks():
1330 yield (str(i), bookId), (factory.factory_name(), bookName)
1332 def open_addressbook(self, bookFactoryId, bookId):
1333 bookFactoryIndex = int(bookFactoryId)
1334 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1336 forceUpdate = True if addressBook is not self._addressBook else False
1338 self._addressBook = addressBook
1339 self.update(force=forceUpdate)
1341 def update(self, force = False):
1342 if not force and self._isPopulated:
1344 self._updateSink.send(())
1348 self._isPopulated = False
1349 self._contactsmodel.clear()
1350 for factory in self._addressBookFactories:
1351 factory.clear_caches()
1352 self._addressBook.clear_caches()
1354 def append(self, book):
1355 self._addressBookFactories.append(book)
1357 def extend(self, books):
1358 self._addressBookFactories.extend(books)
1364 def load_settings(self, config, sectionName):
1366 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1367 except ConfigParser.NoOptionError:
1368 self._selectedComboIndex = 0
1370 def save_settings(self, config, sectionName):
1371 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1373 def _idly_populate_contactsview(self):
1374 with gtk_toolbox.gtk_lock():
1375 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1378 while addressBook is not self._addressBook:
1379 addressBook = self._addressBook
1380 with gtk_toolbox.gtk_lock():
1381 self._contactsview.set_model(None)
1385 contacts = addressBook.get_contacts()
1386 except Exception, e:
1388 self._isPopulated = False
1389 self._errorDisplay.push_exception_with_lock()
1390 for contactId, contactName in contacts:
1391 contactType = (addressBook.contact_source_short_name(contactId), )
1392 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1394 with gtk_toolbox.gtk_lock():
1395 self._contactsview.set_model(self._contactsmodel)
1397 self._isPopulated = True
1398 except Exception, e:
1399 self._errorDisplay.push_exception_with_lock()
1401 with gtk_toolbox.gtk_lock():
1402 hildonize.show_busy_banner_end(banner)
1405 def _on_addressbook_button_changed(self, *args, **kwds):
1408 newSelectedComboIndex = hildonize.touch_selector(
1411 (("%s" % m[2]) for m in self._booksList),
1412 self._selectedComboIndex,
1414 except RuntimeError:
1417 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1418 selectedBookId = self._booksList[newSelectedComboIndex][1]
1419 self.open_addressbook(selectedFactoryId, selectedBookId)
1420 self._selectedComboIndex = newSelectedComboIndex
1421 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1422 except Exception, e:
1423 self._errorDisplay.push_exception()
1425 def _on_contactsview_row_activated(self, treeview, path, view_column):
1427 itr = self._contactsmodel.get_iter(path)
1431 contactId = self._contactsmodel.get_value(itr, 3)
1432 contactName = self._contactsmodel.get_value(itr, 1)
1434 contactDetails = self._addressBook.get_contact_details(contactId)
1435 except Exception, e:
1437 self._errorDisplay.push_exception()
1438 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1440 if len(contactPhoneNumbers) == 0:
1443 action, phoneNumber, message = self._phoneTypeSelector.run(
1444 contactPhoneNumbers,
1445 messages = (contactName, ),
1446 parent = self._window,
1448 if action == SmsEntryDialog.ACTION_CANCEL:
1450 assert phoneNumber, "A lack of phone number exists"
1452 self.number_selected(action, phoneNumber, message)
1453 self._contactsviewselection.unselect_all()
1454 except Exception, e:
1455 self._errorDisplay.push_exception()