4 DialCentral - Front end for Google's GoogleVoice service.
5 Copyright (C) 2008 Mark Bergman bergman AT merctech DOT com
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 Lesser General Public License for more details.
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21 @todo Alternate UI for dialogs (stackables)
24 from __future__ import with_statement
40 _moduleLogger = logging.getLogger("gv_views")
43 def make_ugly(prettynumber):
45 function to take a phone number and strip out all non-numeric
48 >>> make_ugly("+012-(345)-678-90")
52 uglynumber = re.sub('\D', '', prettynumber)
56 def make_pretty(phonenumber):
58 Function to take a phone number and return the pretty version
60 if phonenumber begins with 0:
62 if phonenumber begins with 1: ( for gizmo callback numbers )
64 if phonenumber is 13 digits:
66 if phonenumber is 10 digits:
70 >>> make_pretty("1234567")
72 >>> make_pretty("2345678901")
74 >>> make_pretty("12345678901")
76 >>> make_pretty("01234567890")
79 if phonenumber is None or phonenumber is "":
82 phonenumber = make_ugly(phonenumber)
84 if len(phonenumber) < 3:
87 if phonenumber[0] == "0":
89 prettynumber += "+%s" % phonenumber[0:3]
90 if 3 < len(phonenumber):
91 prettynumber += "-(%s)" % phonenumber[3:6]
92 if 6 < len(phonenumber):
93 prettynumber += "-%s" % phonenumber[6:9]
94 if 9 < len(phonenumber):
95 prettynumber += "-%s" % phonenumber[9:]
97 elif len(phonenumber) <= 7:
98 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
99 elif len(phonenumber) > 8 and phonenumber[0] == "1":
100 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
101 elif len(phonenumber) > 7:
102 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
106 def abbrev_relative_date(date):
108 >>> abbrev_relative_date("42 hours ago")
110 >>> abbrev_relative_date("2 days ago")
112 >>> abbrev_relative_date("4 weeks ago")
115 parts = date.split(" ")
116 return "%s %s" % (parts[0], parts[1][0])
119 class MergedAddressBook(object):
121 Merger of all addressbooks
124 def __init__(self, addressbookFactories, sorter = None):
125 self.__addressbookFactories = addressbookFactories
126 self.__addressbooks = None
127 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
129 def clear_caches(self):
130 self.__addressbooks = None
131 for factory in self.__addressbookFactories:
132 factory.clear_caches()
134 def get_addressbooks(self):
136 @returns Iterable of (Address Book Factory, Book Id, Book Name)
140 def open_addressbook(self, bookId):
143 def contact_source_short_name(self, contactId):
144 if self.__addressbooks is None:
146 bookIndex, originalId = contactId.split("-", 1)
147 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
151 return "All Contacts"
153 def get_contacts(self):
155 @returns Iterable of (contact id, contact name)
157 if self.__addressbooks is None:
158 self.__addressbooks = list(
159 factory.open_addressbook(id)
160 for factory in self.__addressbookFactories
161 for (f, id, name) in factory.get_addressbooks()
164 ("-".join([str(bookIndex), contactId]), contactName)
165 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
166 for (contactId, contactName) in addressbook.get_contacts()
168 sortedContacts = self.__sort_contacts(contacts)
169 return sortedContacts
171 def get_contact_details(self, contactId):
173 @returns Iterable of (Phone Type, Phone Number)
175 if self.__addressbooks is None:
177 bookIndex, originalId = contactId.split("-", 1)
178 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
181 def null_sorter(contacts):
183 Good for speed/low memory
188 def basic_firtname_sorter(contacts):
190 Expects names in "First Last" format
193 (contactName.rsplit(" ", 1)[0], (contactId, contactName))
194 for (contactId, contactName) in contacts
196 contactsWithKey.sort()
197 return (contactData for (lastName, contactData) in contactsWithKey)
200 def basic_lastname_sorter(contacts):
202 Expects names in "First Last" format
205 (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
206 for (contactId, contactName) in contacts
208 contactsWithKey.sort()
209 return (contactData for (lastName, contactData) in contactsWithKey)
212 def reversed_firtname_sorter(contacts):
214 Expects names in "Last, First" format
217 (contactName.split(", ", 1)[-1], (contactId, contactName))
218 for (contactId, contactName) in contacts
220 contactsWithKey.sort()
221 return (contactData for (lastName, contactData) in contactsWithKey)
224 def reversed_lastname_sorter(contacts):
226 Expects names in "Last, First" format
229 (contactName.split(", ", 1)[0], (contactId, contactName))
230 for (contactId, contactName) in contacts
232 contactsWithKey.sort()
233 return (contactData for (lastName, contactData) in contactsWithKey)
236 def guess_firstname(name):
238 return name.split(", ", 1)[-1]
240 return name.rsplit(" ", 1)[0]
243 def guess_lastname(name):
245 return name.split(", ", 1)[0]
247 return name.rsplit(" ", 1)[-1]
250 def advanced_firstname_sorter(cls, contacts):
252 (cls.guess_firstname(contactName), (contactId, contactName))
253 for (contactId, contactName) in contacts
255 contactsWithKey.sort()
256 return (contactData for (lastName, contactData) in contactsWithKey)
259 def advanced_lastname_sorter(cls, contacts):
261 (cls.guess_lastname(contactName), (contactId, contactName))
262 for (contactId, contactName) in contacts
264 contactsWithKey.sort()
265 return (contactData for (lastName, contactData) in contactsWithKey)
268 class SmsEntryDialog(object):
270 @todo Add multi-SMS messages like GoogleVoice
273 ACTION_CANCEL = "cancel"
275 ACTION_SEND_SMS = "sms"
279 def __init__(self, widgetTree):
280 self._clipboard = gtk.clipboard_get()
281 self._widgetTree = widgetTree
282 self._dialog = self._widgetTree.get_widget("smsDialog")
284 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
285 self._smsButton.connect("clicked", self._on_send)
286 self._dialButton = self._widgetTree.get_widget("dialButton")
287 self._dialButton.connect("clicked", self._on_dial)
288 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
289 self._cancelButton.connect("clicked", self._on_cancel)
291 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
293 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
294 self._messagesView = self._widgetTree.get_widget("smsMessages")
296 self._conversationView = self._messagesView.get_parent()
297 self._conversationViewPort = self._conversationView.get_parent()
298 self._scrollWindow = self._conversationViewPort.get_parent()
300 self._phoneButton = self._widgetTree.get_widget("phoneTypeSelection")
301 self._smsEntry = self._widgetTree.get_widget("smsEntry")
303 self._action = self.ACTION_CANCEL
305 self._numberIndex = -1
306 self._contactDetails = []
308 def run(self, contactDetails, messages = (), parent = None, defaultIndex = -1):
309 entryConnectId = self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
310 phoneConnectId = self._phoneButton.connect("clicked", self._on_phone)
311 keyConnectId = self._keyPressEventId = self._dialog.connect("key-press-event", self._on_key_press)
313 # Setup the phone selection button
314 del self._contactDetails[:]
315 for phoneType, phoneNumber in contactDetails:
316 display = " - ".join((make_pretty(phoneNumber), phoneType))
317 row = (phoneNumber, display)
318 self._contactDetails.append(row)
319 if 0 < len(self._contactDetails):
320 self._numberIndex = defaultIndex if defaultIndex != -1 else 0
321 self._phoneButton.set_label(self._contactDetails[self._numberIndex][1])
323 self._numberIndex = -1
324 self._phoneButton.set_label("Error: No Number Available")
326 # Add the column to the messages tree view
327 self._messagemodel.clear()
328 self._messagesView.set_model(self._messagemodel)
329 self._messagesView.set_fixed_height_mode(False)
331 textrenderer = gtk.CellRendererText()
332 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
333 textrenderer.set_property("wrap-width", 450)
334 messageColumn = gtk.TreeViewColumn("")
335 messageColumn.pack_start(textrenderer, expand=True)
336 messageColumn.add_attribute(textrenderer, "markup", 0)
337 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
338 self._messagesView.append_column(messageColumn)
339 self._messagesView.set_headers_visible(False)
342 for message in messages:
344 self._messagemodel.append(row)
345 self._messagesView.show()
346 self._scrollWindow.show()
347 messagesSelection = self._messagesView.get_selection()
348 messagesSelection.select_path((len(messages)-1, ))
350 self._messagesView.hide()
351 self._scrollWindow.hide()
353 self._smsEntry.get_buffer().set_text("")
354 self._update_letter_count()
356 if parent is not None:
357 self._dialog.set_transient_for(parent)
358 parentSize = parent.get_size()
359 self._dialog.resize(parentSize[0], max(parentSize[1]-10, 100))
363 self._dialog.show_all()
364 self._smsEntry.grab_focus()
365 adjustment = self._scrollWindow.get_vadjustment()
366 dx = self._conversationView.get_allocation().height - self._conversationViewPort.get_allocation().height
368 adjustment.value = dx
370 if 1 < len(self._contactDetails):
371 if defaultIndex == -1:
372 self._request_number()
373 self._phoneButton.set_sensitive(True)
375 self._phoneButton.set_sensitive(False)
377 userResponse = self._dialog.run()
379 self._dialog.hide_all()
381 # Process the users response
382 if userResponse == gtk.RESPONSE_OK and 0 <= self._numberIndex:
383 phoneNumber = self._contactDetails[self._numberIndex][0]
384 phoneNumber = make_ugly(phoneNumber)
388 self._action = self.ACTION_CANCEL
389 if self._action == self.ACTION_SEND_SMS:
390 entryBuffer = self._smsEntry.get_buffer()
391 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
392 enteredMessage = enteredMessage[0:self.MAX_CHAR].strip()
393 if not enteredMessage:
395 self._action = self.ACTION_CANCEL
399 self._messagesView.remove_column(messageColumn)
400 self._messagesView.set_model(None)
402 return self._action, phoneNumber, enteredMessage
404 self._smsEntry.get_buffer().disconnect(entryConnectId)
405 self._phoneButton.disconnect(phoneConnectId)
406 self._keyPressEventId = self._dialog.disconnect(keyConnectId)
408 def _update_letter_count(self, *args):
409 entryLength = self._smsEntry.get_buffer().get_char_count()
410 charsLeft = self.MAX_CHAR - entryLength
411 self._letterCountLabel.set_text(str(charsLeft))
412 if charsLeft < 0 or charsLeft == self.MAX_CHAR:
413 self._smsButton.set_sensitive(False)
414 self._dialButton.set_sensitive(True)
416 self._smsButton.set_sensitive(True)
417 self._dialButton.set_sensitive(False)
419 def _request_number(self):
421 assert 0 <= self._numberIndex, "%r" % self._numberIndex
423 self._numberIndex = hildonize.touch_selector(
426 (description for (number, description) in self._contactDetails),
429 self._phoneButton.set_label(self._contactDetails[self._numberIndex][1])
431 _moduleLogger.exception("%s" % str(e))
433 def _on_phone(self, *args):
434 self._request_number()
436 def _on_entry_changed(self, *args):
437 self._update_letter_count()
439 def _on_send(self, *args):
440 self._dialog.response(gtk.RESPONSE_OK)
441 self._action = self.ACTION_SEND_SMS
443 def _on_dial(self, *args):
444 self._dialog.response(gtk.RESPONSE_OK)
445 self._action = self.ACTION_DIAL
447 def _on_cancel(self, *args):
448 self._dialog.response(gtk.RESPONSE_CANCEL)
449 self._action = self.ACTION_CANCEL
451 def _on_key_press(self, widget, event):
453 if event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
456 for messagePart in self._messagemodel
458 # For some reason this kills clipboard stuff
459 #self._clipboard.set_text(message)
461 _moduleLogger.exception(str(e))
464 class Dialpad(object):
466 def __init__(self, widgetTree, errorDisplay):
467 self._clipboard = gtk.clipboard_get()
468 self._errorDisplay = errorDisplay
469 self._smsDialog = SmsEntryDialog(widgetTree)
471 self._numberdisplay = widgetTree.get_widget("numberdisplay")
472 self._smsButton = widgetTree.get_widget("sms")
473 self._dialButton = widgetTree.get_widget("dial")
474 self._backButton = widgetTree.get_widget("back")
475 self._phonenumber = ""
476 self._prettynumber = ""
479 "on_digit_clicked": self._on_digit_clicked,
481 widgetTree.signal_autoconnect(callbackMapping)
482 self._dialButton.connect("clicked", self._on_dial_clicked)
483 self._smsButton.connect("clicked", self._on_sms_clicked)
485 self._originalLabel = self._backButton.get_label()
486 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
487 self._backTapHandler.on_tap = self._on_backspace
488 self._backTapHandler.on_hold = self._on_clearall
489 self._backTapHandler.on_holding = self._set_clear_button
490 self._backTapHandler.on_cancel = self._reset_back_button
492 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
493 self._keyPressEventId = 0
496 self._dialButton.grab_focus()
497 self._backTapHandler.enable()
498 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
501 self._window.disconnect(self._keyPressEventId)
502 self._keyPressEventId = 0
503 self._reset_back_button()
504 self._backTapHandler.disable()
506 def number_selected(self, action, number, message):
508 @note Actual dial function is patched in later
510 raise NotImplementedError("Horrible unknown error has occurred")
512 def get_number(self):
513 return self._phonenumber
515 def set_number(self, number):
517 Set the number to dial
520 self._phonenumber = make_ugly(number)
521 self._prettynumber = make_pretty(self._phonenumber)
522 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
524 self._errorDisplay.push_exception()
533 def load_settings(self, config, section):
536 def save_settings(self, config, section):
538 @note Thread Agnostic
542 def _on_key_press(self, widget, event):
544 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
545 contents = self._clipboard.wait_for_text()
546 if contents is not None:
547 self.set_number(contents)
549 self._errorDisplay.push_exception()
551 def _on_sms_clicked(self, widget):
553 phoneNumber = self.get_number()
554 action, phoneNumber, message = self._smsDialog.run([("Dialer", phoneNumber)], (), self._window)
556 if action == SmsEntryDialog.ACTION_CANCEL:
558 self.number_selected(action, phoneNumber, message)
560 self._errorDisplay.push_exception()
562 def _on_dial_clicked(self, widget):
564 action = SmsEntryDialog.ACTION_DIAL
565 phoneNumber = self.get_number()
567 self.number_selected(action, phoneNumber, message)
569 self._errorDisplay.push_exception()
571 def _on_digit_clicked(self, widget):
573 self.set_number(self._phonenumber + widget.get_name()[-1])
575 self._errorDisplay.push_exception()
577 def _on_backspace(self, taps):
579 self.set_number(self._phonenumber[:-taps])
580 self._reset_back_button()
582 self._errorDisplay.push_exception()
584 def _on_clearall(self, taps):
587 self._reset_back_button()
589 self._errorDisplay.push_exception()
592 def _set_clear_button(self):
594 self._backButton.set_label("gtk-clear")
596 self._errorDisplay.push_exception()
598 def _reset_back_button(self):
600 self._backButton.set_label(self._originalLabel)
602 self._errorDisplay.push_exception()
605 class AccountInfo(object):
607 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
608 self._errorDisplay = errorDisplay
609 self._backend = backend
610 self._isPopulated = False
611 self._alarmHandler = alarmHandler
612 self._notifyOnMissed = False
613 self._notifyOnVoicemail = False
614 self._notifyOnSms = False
616 self._callbackList = []
617 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
618 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
619 self._onCallbackSelectChangedId = 0
621 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
622 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
623 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
624 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
625 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
626 self._onNotifyToggled = 0
627 self._onMinutesChanged = 0
628 self._onMissedToggled = 0
629 self._onVoicemailToggled = 0
630 self._onSmsToggled = 0
631 self._applyAlarmTimeoutId = None
633 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
634 self._defaultCallback = ""
637 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
639 self._accountViewNumberDisplay.set_use_markup(True)
640 self.set_account_number("")
642 del self._callbackList[:]
643 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
645 if self._alarmHandler is not None:
646 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
647 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
648 self._missedCheckbox.set_active(self._notifyOnMissed)
649 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
650 self._smsCheckbox.set_active(self._notifyOnSms)
652 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
653 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
654 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
655 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
656 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
658 self._notifyCheckbox.set_sensitive(False)
659 self._minutesEntryButton.set_sensitive(False)
660 self._missedCheckbox.set_sensitive(False)
661 self._voicemailCheckbox.set_sensitive(False)
662 self._smsCheckbox.set_sensitive(False)
664 self.update(force=True)
667 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
668 self._onCallbackSelectChangedId = 0
670 if self._alarmHandler is not None:
671 self._notifyCheckbox.disconnect(self._onNotifyToggled)
672 self._minutesEntryButton.disconnect(self._onMinutesChanged)
673 self._missedCheckbox.disconnect(self._onNotifyToggled)
674 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
675 self._smsCheckbox.disconnect(self._onNotifyToggled)
676 self._onNotifyToggled = 0
677 self._onMinutesChanged = 0
678 self._onMissedToggled = 0
679 self._onVoicemailToggled = 0
680 self._onSmsToggled = 0
682 self._notifyCheckbox.set_sensitive(True)
683 self._minutesEntryButton.set_sensitive(True)
684 self._missedCheckbox.set_sensitive(True)
685 self._voicemailCheckbox.set_sensitive(True)
686 self._smsCheckbox.set_sensitive(True)
689 del self._callbackList[:]
691 def get_selected_callback_number(self):
692 currentLabel = self._callbackSelectButton.get_label()
693 if currentLabel is not None:
694 return make_ugly(currentLabel)
698 def set_account_number(self, number):
700 Displays current account number
702 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
704 def update(self, force = False):
705 if not force and self._isPopulated:
707 self._populate_callback_combo()
708 self.set_account_number(self._backend.get_account_number())
712 self._set_callback_label("")
713 self.set_account_number("")
714 self._isPopulated = False
716 def save_everything(self):
717 raise NotImplementedError
721 return "Account Info"
723 def load_settings(self, config, section):
724 self._defaultCallback = config.get(section, "callback")
725 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
726 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
727 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
729 def save_settings(self, config, section):
731 @note Thread Agnostic
733 callback = self.get_selected_callback_number()
734 config.set(section, "callback", callback)
735 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
736 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
737 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
739 def _populate_callback_combo(self):
740 self._isPopulated = True
741 del self._callbackList[:]
743 callbackNumbers = self._backend.get_callback_numbers()
745 self._errorDisplay.push_exception()
746 self._isPopulated = False
749 if len(callbackNumbers) == 0:
750 callbackNumbers = {"": "No callback numbers available"}
752 for number, description in callbackNumbers.iteritems():
753 self._callbackList.append((make_pretty(number), description))
755 self._set_callback_number(self._defaultCallback)
757 def _set_callback_number(self, number):
759 if not self._backend.is_valid_syntax(number) and 0 < len(number):
760 self._errorDisplay.push_message("%s is not a valid callback number" % number)
761 elif number == self._backend.get_callback_number() and 0 < len(number):
762 _moduleLogger.warning(
763 "Callback number already is %s" % (
764 self._backend.get_callback_number(),
767 self._set_callback_label(number)
769 self._backend.set_callback_number(number)
770 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
771 make_pretty(number), make_pretty(self._backend.get_callback_number())
773 self._set_callback_label(number)
775 "Callback number set to %s" % (
776 self._backend.get_callback_number(),
780 self._errorDisplay.push_exception()
782 def _set_callback_label(self, uglyNumber):
783 prettyNumber = make_pretty(uglyNumber)
784 if len(prettyNumber) == 0:
785 prettyNumber = "No Callback Number"
786 self._callbackSelectButton.set_label(prettyNumber)
788 def _update_alarm_settings(self, recurrence):
790 isEnabled = self._notifyCheckbox.get_active()
791 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
792 self._alarmHandler.apply_settings(isEnabled, recurrence)
794 self.save_everything()
795 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
796 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
798 def _on_callbackentry_clicked(self, *args):
800 actualSelection = make_pretty(self.get_selected_callback_number())
803 (number, "%s (%s)" % (number, description))
804 for (number, description) in self._callbackList
806 defaultSelection = userOptions.get(actualSelection, actualSelection)
808 userSelection = hildonize.touch_selector_entry(
811 list(userOptions.itervalues()),
814 reversedUserOptions = dict(
815 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
817 selectedNumber = reversedUserOptions.get(userSelection, userSelection)
819 number = make_ugly(selectedNumber)
820 self._set_callback_number(number)
821 except RuntimeError, e:
822 _moduleLogger.exception("%s" % str(e))
824 self._errorDisplay.push_exception()
826 def _on_notify_toggled(self, *args):
828 if self._applyAlarmTimeoutId is not None:
829 gobject.source_remove(self._applyAlarmTimeoutId)
830 self._applyAlarmTimeoutId = None
831 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
833 self._errorDisplay.push_exception()
835 def _on_minutes_clicked(self, *args):
836 recurrenceChoices = [
852 actualSelection = self._alarmHandler.recurrence
854 closestSelectionIndex = 0
855 for i, possible in enumerate(recurrenceChoices):
856 if possible[0] <= actualSelection:
857 closestSelectionIndex = i
858 recurrenceIndex = hildonize.touch_selector(
861 (("%s" % m[1]) for m in recurrenceChoices),
862 closestSelectionIndex,
864 recurrence = recurrenceChoices[recurrenceIndex][0]
866 self._update_alarm_settings(recurrence)
867 except RuntimeError, e:
868 _moduleLogger.exception("%s" % str(e))
870 self._errorDisplay.push_exception()
872 def _on_apply_timeout(self, *args):
874 self._applyAlarmTimeoutId = None
876 self._update_alarm_settings(self._alarmHandler.recurrence)
878 self._errorDisplay.push_exception()
881 def _on_missed_toggled(self, *args):
883 self._notifyOnMissed = self._missedCheckbox.get_active()
884 self.save_everything()
886 self._errorDisplay.push_exception()
888 def _on_voicemail_toggled(self, *args):
890 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
891 self.save_everything()
893 self._errorDisplay.push_exception()
895 def _on_sms_toggled(self, *args):
897 self._notifyOnSms = self._smsCheckbox.get_active()
898 self.save_everything()
900 self._errorDisplay.push_exception()
903 class RecentCallsView(object):
911 def __init__(self, widgetTree, backend, errorDisplay):
912 self._errorDisplay = errorDisplay
913 self._backend = backend
915 self._isPopulated = False
916 self._recentmodel = gtk.ListStore(
917 gobject.TYPE_STRING, # number
918 gobject.TYPE_STRING, # date
919 gobject.TYPE_STRING, # action
920 gobject.TYPE_STRING, # from
921 gobject.TYPE_STRING, # from id
923 self._recentview = widgetTree.get_widget("recentview")
924 self._recentviewselection = None
925 self._onRecentviewRowActivatedId = 0
927 textrenderer = gtk.CellRendererText()
928 textrenderer.set_property("yalign", 0)
929 self._dateColumn = gtk.TreeViewColumn("Date")
930 self._dateColumn.pack_start(textrenderer, expand=True)
931 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
933 textrenderer = gtk.CellRendererText()
934 textrenderer.set_property("yalign", 0)
935 self._actionColumn = gtk.TreeViewColumn("Action")
936 self._actionColumn.pack_start(textrenderer, expand=True)
937 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
939 textrenderer = gtk.CellRendererText()
940 textrenderer.set_property("yalign", 0)
941 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
942 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
943 self._numberColumn = gtk.TreeViewColumn("Number")
944 self._numberColumn.pack_start(textrenderer, expand=True)
945 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
947 textrenderer = gtk.CellRendererText()
948 textrenderer.set_property("yalign", 0)
949 hildonize.set_cell_thumb_selectable(textrenderer)
950 self._nameColumn = gtk.TreeViewColumn("From")
951 self._nameColumn.pack_start(textrenderer, expand=True)
952 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
953 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
955 self._window = gtk_toolbox.find_parent_window(self._recentview)
956 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
958 self._updateSink = gtk_toolbox.threaded_stage(
960 self._idly_populate_recentview,
961 gtk_toolbox.null_sink(),
966 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
967 self._recentview.set_model(self._recentmodel)
968 self._recentview.set_fixed_height_mode(False)
970 self._recentview.append_column(self._dateColumn)
971 self._recentview.append_column(self._actionColumn)
972 self._recentview.append_column(self._numberColumn)
973 self._recentview.append_column(self._nameColumn)
974 self._recentviewselection = self._recentview.get_selection()
975 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
977 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
980 self._recentview.disconnect(self._onRecentviewRowActivatedId)
984 self._recentview.remove_column(self._dateColumn)
985 self._recentview.remove_column(self._actionColumn)
986 self._recentview.remove_column(self._nameColumn)
987 self._recentview.remove_column(self._numberColumn)
988 self._recentview.set_model(None)
990 def number_selected(self, action, number, message):
992 @note Actual dial function is patched in later
994 raise NotImplementedError("Horrible unknown error has occurred")
996 def update(self, force = False):
997 if not force and self._isPopulated:
999 self._updateSink.send(())
1003 self._isPopulated = False
1004 self._recentmodel.clear()
1008 return "Recent Calls"
1010 def load_settings(self, config, section):
1013 def save_settings(self, config, section):
1015 @note Thread Agnostic
1019 def _idly_populate_recentview(self):
1020 with gtk_toolbox.gtk_lock():
1021 banner = hildonize.show_busy_banner_start(self._window, "Loading Recent History")
1023 self._recentmodel.clear()
1024 self._isPopulated = True
1027 recentItems = self._backend.get_recent()
1028 except Exception, e:
1029 self._errorDisplay.push_exception_with_lock()
1030 self._isPopulated = False
1034 gv_backend.decorate_recent(data)
1035 for data in gv_backend.sort_messages(recentItems)
1038 for contactId, personName, phoneNumber, date, action in recentItems:
1040 personName = "Unknown"
1041 date = abbrev_relative_date(date)
1042 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1043 prettyNumber = make_pretty(prettyNumber)
1044 item = (prettyNumber, date, action.capitalize(), personName, contactId)
1045 with gtk_toolbox.gtk_lock():
1046 self._recentmodel.append(item)
1047 except Exception, e:
1048 self._errorDisplay.push_exception_with_lock()
1050 with gtk_toolbox.gtk_lock():
1051 hildonize.show_busy_banner_end(banner)
1055 def _on_recentview_row_activated(self, treeview, path, view_column):
1057 itr = self._recentmodel.get_iter(path)
1061 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1062 number = make_ugly(number)
1063 description = self._recentmodel.get_value(itr, self.FROM_IDX)
1064 contactId = self._recentmodel.get_value(itr, self.FROM_ID_IDX)
1066 contactPhoneNumbers = list(self._backend.get_contact_details(contactId))
1068 (number == make_ugly(contactNumber) or number[1:] == make_ugly(contactNumber))
1069 for (numberDescription, contactNumber) in contactPhoneNumbers
1072 defaultIndex = defaultMatches.index(True)
1074 contactPhoneNumbers.append(("Other", number))
1075 defaultIndex = len(contactPhoneNumbers)-1
1077 "Could not find contact %r's number %s among %r" % (
1078 contactId, number, contactPhoneNumbers
1082 contactPhoneNumbers = [("Phone", number)]
1085 action, phoneNumber, message = self._phoneTypeSelector.run(
1086 contactPhoneNumbers,
1087 messages = (description, ),
1088 parent = self._window,
1089 defaultIndex = defaultIndex,
1091 if action == SmsEntryDialog.ACTION_CANCEL:
1093 assert phoneNumber, "A lack of phone number exists"
1095 self.number_selected(action, phoneNumber, message)
1096 self._recentviewselection.unselect_all()
1097 except Exception, e:
1098 self._errorDisplay.push_exception()
1101 class MessagesView(object):
1110 def __init__(self, widgetTree, backend, errorDisplay):
1111 self._errorDisplay = errorDisplay
1112 self._backend = backend
1114 self._isPopulated = False
1115 self._messagemodel = gtk.ListStore(
1116 gobject.TYPE_STRING, # number
1117 gobject.TYPE_STRING, # date
1118 gobject.TYPE_STRING, # header
1119 gobject.TYPE_STRING, # message
1121 gobject.TYPE_STRING, # from id
1123 self._messageview = widgetTree.get_widget("messages_view")
1124 self._messageviewselection = None
1125 self._onMessageviewRowActivatedId = 0
1127 self._messageRenderer = gtk.CellRendererText()
1128 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1129 self._messageRenderer.set_property("wrap-width", 500)
1130 self._messageColumn = gtk.TreeViewColumn("Messages")
1131 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1132 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1133 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1135 self._window = gtk_toolbox.find_parent_window(self._messageview)
1136 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
1138 self._updateSink = gtk_toolbox.threaded_stage(
1140 self._idly_populate_messageview,
1141 gtk_toolbox.null_sink(),
1146 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1147 self._messageview.set_model(self._messagemodel)
1148 self._messageview.set_headers_visible(False)
1149 self._messageview.set_fixed_height_mode(False)
1151 self._messageview.append_column(self._messageColumn)
1152 self._messageviewselection = self._messageview.get_selection()
1153 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1155 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1158 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1162 self._messageview.remove_column(self._messageColumn)
1163 self._messageview.set_model(None)
1165 def number_selected(self, action, number, message):
1167 @note Actual dial function is patched in later
1169 raise NotImplementedError("Horrible unknown error has occurred")
1171 def update(self, force = False):
1172 if not force and self._isPopulated:
1174 self._updateSink.send(())
1178 self._isPopulated = False
1179 self._messagemodel.clear()
1185 def load_settings(self, config, section):
1188 def save_settings(self, config, section):
1190 @note Thread Agnostic
1194 _MIN_MESSAGES_SHOWN = 4
1196 def _idly_populate_messageview(self):
1197 with gtk_toolbox.gtk_lock():
1198 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1200 self._messagemodel.clear()
1201 self._isPopulated = True
1204 messageItems = self._backend.get_messages()
1205 except Exception, e:
1206 self._errorDisplay.push_exception_with_lock()
1207 self._isPopulated = False
1211 gv_backend.decorate_message(message)
1212 for message in gv_backend.sort_messages(messageItems)
1215 for contactId, header, number, relativeDate, messages in messageItems:
1216 prettyNumber = number[2:] if number.startswith("+1") else number
1217 prettyNumber = make_pretty(prettyNumber)
1219 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1220 expandedMessages = [firstMessage]
1221 expandedMessages.extend(messages)
1222 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1223 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1224 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1225 collapsedMessages = [firstMessage, secondMessage]
1226 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1228 collapsedMessages = expandedMessages
1230 number = make_ugly(number)
1232 row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId
1233 with gtk_toolbox.gtk_lock():
1234 self._messagemodel.append(row)
1235 except Exception, e:
1236 self._errorDisplay.push_exception_with_lock()
1238 with gtk_toolbox.gtk_lock():
1239 hildonize.show_busy_banner_end(banner)
1243 def _on_messageview_row_activated(self, treeview, path, view_column):
1245 itr = self._messagemodel.get_iter(path)
1249 number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX))
1250 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1252 contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX)
1254 contactPhoneNumbers = list(self._backend.get_contact_details(contactId))
1256 (number == make_ugly(contactNumber) or number[1:] == make_ugly(contactNumber))
1257 for (numberDescription, contactNumber) in contactPhoneNumbers
1260 defaultIndex = defaultMatches.index(True)
1262 contactPhoneNumbers.append(("Other", number))
1263 defaultIndex = len(contactPhoneNumbers)-1
1265 "Could not find contact %r's number %s among %r" % (
1266 contactId, number, contactPhoneNumbers
1270 contactPhoneNumbers = [("Phone", number)]
1273 action, phoneNumber, message = self._phoneTypeSelector.run(
1274 contactPhoneNumbers,
1275 messages = description,
1276 parent = self._window,
1277 defaultIndex = defaultIndex,
1279 if action == SmsEntryDialog.ACTION_CANCEL:
1281 assert phoneNumber, "A lock of phone number exists"
1283 self.number_selected(action, phoneNumber, message)
1284 self._messageviewselection.unselect_all()
1285 except Exception, e:
1286 self._errorDisplay.push_exception()
1289 class ContactsView(object):
1291 def __init__(self, widgetTree, backend, errorDisplay):
1292 self._errorDisplay = errorDisplay
1293 self._backend = backend
1295 self._addressBook = None
1296 self._selectedComboIndex = 0
1297 self._addressBookFactories = [null_backend.NullAddressBook()]
1299 self._booksList = []
1300 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1302 self._isPopulated = False
1303 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1304 self._contactsviewselection = None
1305 self._contactsview = widgetTree.get_widget("contactsview")
1307 self._contactColumn = gtk.TreeViewColumn("Contact")
1308 displayContactSource = False
1309 if displayContactSource:
1310 textrenderer = gtk.CellRendererText()
1311 self._contactColumn.pack_start(textrenderer, expand=False)
1312 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1313 textrenderer = gtk.CellRendererText()
1314 hildonize.set_cell_thumb_selectable(textrenderer)
1315 self._contactColumn.pack_start(textrenderer, expand=True)
1316 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1317 textrenderer = gtk.CellRendererText()
1318 self._contactColumn.pack_start(textrenderer, expand=True)
1319 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1320 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1321 self._contactColumn.set_sort_column_id(1)
1322 self._contactColumn.set_visible(True)
1324 self._onContactsviewRowActivatedId = 0
1325 self._onAddressbookButtonChangedId = 0
1326 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1327 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
1329 self._updateSink = gtk_toolbox.threaded_stage(
1331 self._idly_populate_contactsview,
1332 gtk_toolbox.null_sink(),
1337 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1339 self._contactsview.set_model(self._contactsmodel)
1340 self._contactsview.set_fixed_height_mode(False)
1341 self._contactsview.append_column(self._contactColumn)
1342 self._contactsviewselection = self._contactsview.get_selection()
1343 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1345 del self._booksList[:]
1346 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1347 if factoryName and bookName:
1348 entryName = "%s: %s" % (factoryName, bookName)
1350 entryName = factoryName
1352 entryName = bookName
1354 entryName = "Bad name (%d)" % factoryId
1355 row = (str(factoryId), bookId, entryName)
1356 self._booksList.append(row)
1358 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1359 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1361 if len(self._booksList) <= self._selectedComboIndex:
1362 self._selectedComboIndex = 0
1363 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1365 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1366 selectedBookId = self._booksList[self._selectedComboIndex][1]
1367 self.open_addressbook(selectedFactoryId, selectedBookId)
1370 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1371 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1375 self._bookSelectionButton.set_label("")
1376 self._contactsview.set_model(None)
1377 self._contactsview.remove_column(self._contactColumn)
1379 def number_selected(self, action, number, message):
1381 @note Actual dial function is patched in later
1383 raise NotImplementedError("Horrible unknown error has occurred")
1385 def get_addressbooks(self):
1387 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1389 for i, factory in enumerate(self._addressBookFactories):
1390 for bookFactory, bookId, bookName in factory.get_addressbooks():
1391 yield (str(i), bookId), (factory.factory_name(), bookName)
1393 def open_addressbook(self, bookFactoryId, bookId):
1394 bookFactoryIndex = int(bookFactoryId)
1395 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1397 forceUpdate = True if addressBook is not self._addressBook else False
1399 self._addressBook = addressBook
1400 self.update(force=forceUpdate)
1402 def update(self, force = False):
1403 if not force and self._isPopulated:
1405 self._updateSink.send(())
1409 self._isPopulated = False
1410 self._contactsmodel.clear()
1411 for factory in self._addressBookFactories:
1412 factory.clear_caches()
1413 self._addressBook.clear_caches()
1415 def append(self, book):
1416 self._addressBookFactories.append(book)
1418 def extend(self, books):
1419 self._addressBookFactories.extend(books)
1425 def load_settings(self, config, sectionName):
1427 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1428 except ConfigParser.NoOptionError:
1429 self._selectedComboIndex = 0
1431 def save_settings(self, config, sectionName):
1432 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1434 def _idly_populate_contactsview(self):
1435 with gtk_toolbox.gtk_lock():
1436 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1439 while addressBook is not self._addressBook:
1440 addressBook = self._addressBook
1441 with gtk_toolbox.gtk_lock():
1442 self._contactsview.set_model(None)
1446 contacts = addressBook.get_contacts()
1447 except Exception, e:
1449 self._isPopulated = False
1450 self._errorDisplay.push_exception_with_lock()
1451 for contactId, contactName in contacts:
1452 contactType = (addressBook.contact_source_short_name(contactId), )
1453 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1455 with gtk_toolbox.gtk_lock():
1456 self._contactsview.set_model(self._contactsmodel)
1458 self._isPopulated = True
1459 except Exception, e:
1460 self._errorDisplay.push_exception_with_lock()
1462 with gtk_toolbox.gtk_lock():
1463 hildonize.show_busy_banner_end(banner)
1466 def _on_addressbook_button_changed(self, *args, **kwds):
1469 newSelectedComboIndex = hildonize.touch_selector(
1472 (("%s" % m[2]) for m in self._booksList),
1473 self._selectedComboIndex,
1475 except RuntimeError:
1478 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1479 selectedBookId = self._booksList[newSelectedComboIndex][1]
1480 self.open_addressbook(selectedFactoryId, selectedBookId)
1481 self._selectedComboIndex = newSelectedComboIndex
1482 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1483 except Exception, e:
1484 self._errorDisplay.push_exception()
1486 def _on_contactsview_row_activated(self, treeview, path, view_column):
1488 itr = self._contactsmodel.get_iter(path)
1492 contactId = self._contactsmodel.get_value(itr, 3)
1493 contactName = self._contactsmodel.get_value(itr, 1)
1495 contactDetails = self._addressBook.get_contact_details(contactId)
1496 except Exception, e:
1498 self._errorDisplay.push_exception()
1499 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1501 if len(contactPhoneNumbers) == 0:
1504 action, phoneNumber, message = self._phoneTypeSelector.run(
1505 contactPhoneNumbers,
1506 messages = (contactName, ),
1507 parent = self._window,
1509 if action == SmsEntryDialog.ACTION_CANCEL:
1511 assert phoneNumber, "A lack of phone number exists"
1513 self.number_selected(action, phoneNumber, message)
1514 self._contactsviewselection.unselect_all()
1515 except Exception, e:
1516 self._errorDisplay.push_exception()