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 Collapse voicemails
22 @todo Alternate UI for dialogs (stackables)
25 from __future__ import with_statement
37 from backends import gv_backend
38 from backends import null_backend
41 _moduleLogger = logging.getLogger("gv_views")
44 def make_ugly(prettynumber):
46 function to take a phone number and strip out all non-numeric
49 >>> make_ugly("+012-(345)-678-90")
53 uglynumber = re.sub('\D', '', prettynumber)
57 def make_pretty(phonenumber):
59 Function to take a phone number and return the pretty version
61 if phonenumber begins with 0:
63 if phonenumber begins with 1: ( for gizmo callback numbers )
65 if phonenumber is 13 digits:
67 if phonenumber is 10 digits:
71 >>> make_pretty("1234567")
73 >>> make_pretty("2345678901")
75 >>> make_pretty("12345678901")
77 >>> make_pretty("01234567890")
80 if phonenumber is None or phonenumber is "":
83 phonenumber = make_ugly(phonenumber)
85 if len(phonenumber) < 3:
88 if phonenumber[0] == "0":
90 prettynumber += "+%s" % phonenumber[0:3]
91 if 3 < len(phonenumber):
92 prettynumber += "-(%s)" % phonenumber[3:6]
93 if 6 < len(phonenumber):
94 prettynumber += "-%s" % phonenumber[6:9]
95 if 9 < len(phonenumber):
96 prettynumber += "-%s" % phonenumber[9:]
98 elif len(phonenumber) <= 7:
99 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
100 elif len(phonenumber) > 8 and phonenumber[0] == "1":
101 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
102 elif len(phonenumber) > 7:
103 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
107 def abbrev_relative_date(date):
109 >>> abbrev_relative_date("42 hours ago")
111 >>> abbrev_relative_date("2 days ago")
113 >>> abbrev_relative_date("4 weeks ago")
116 parts = date.split(" ")
117 return "%s %s" % (parts[0], parts[1][0])
120 def _collapse_message(messageLines, maxCharsPerLine, maxLines):
123 numLines = len(messageLines)
124 for line in messageLines[0:min(maxLines, numLines)]:
125 linesPerLine = max(1, int(len(line) / maxCharsPerLine))
126 allowedLines = maxLines - lines
127 acceptedLines = min(allowedLines, linesPerLine)
128 acceptedChars = acceptedLines * maxCharsPerLine
130 if acceptedChars < (len(line) + 3):
133 acceptedChars = len(line) # eh, might as well complete the line
135 abbrevMessage = "%s%s" % (line[0:acceptedChars], suffix)
138 lines += acceptedLines
139 if maxLines <= lines:
143 def collapse_message(message, maxCharsPerLine, maxLines):
145 >>> collapse_message("Hello", 60, 2)
147 >>> collapse_message("Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789", 60, 2)
148 'Hello world how are you doing today? 01234567890123456789012...'
149 >>> collapse_message('''Hello world how are you doing today?
150 ... 01234567890123456789
151 ... 01234567890123456789
152 ... 01234567890123456789
153 ... 01234567890123456789''', 60, 2)
154 'Hello world how are you doing today?\n01234567890123456789'
155 >>> collapse_message('''
156 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
157 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
158 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
159 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
160 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
161 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789''', 60, 2)
162 '\nHello world how are you doing today? 01234567890123456789012...'
164 messageLines = message.split("\n")
165 return "\n".join(_collapse_message(messageLines, maxCharsPerLine, maxLines))
168 class SmsEntryDialog(object):
170 @todo Add multi-SMS messages like GoogleVoice
173 ACTION_CANCEL = "cancel"
175 ACTION_SEND_SMS = "sms"
179 def __init__(self, widgetTree):
180 self._clipboard = gtk.clipboard_get()
181 self._widgetTree = widgetTree
182 self._dialog = self._widgetTree.get_widget("smsDialog")
184 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
185 self._smsButton.connect("clicked", self._on_send)
186 self._dialButton = self._widgetTree.get_widget("dialButton")
187 self._dialButton.connect("clicked", self._on_dial)
188 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
189 self._cancelButton.connect("clicked", self._on_cancel)
191 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
193 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
194 self._messagesView = self._widgetTree.get_widget("smsMessages")
196 self._conversationView = self._messagesView.get_parent()
197 self._conversationViewPort = self._conversationView.get_parent()
198 self._scrollWindow = self._conversationViewPort.get_parent()
200 self._phoneButton = self._widgetTree.get_widget("phoneTypeSelection")
201 self._smsEntry = self._widgetTree.get_widget("smsEntry")
203 self._action = self.ACTION_CANCEL
205 self._numberIndex = -1
206 self._contactDetails = []
208 def run(self, contactDetails, messages = (), parent = None, defaultIndex = -1):
209 entryConnectId = self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
210 phoneConnectId = self._phoneButton.connect("clicked", self._on_phone)
211 keyConnectId = self._keyPressEventId = self._dialog.connect("key-press-event", self._on_key_press)
213 # Setup the phone selection button
214 del self._contactDetails[:]
215 for phoneType, phoneNumber in contactDetails:
216 display = " - ".join((make_pretty(phoneNumber), phoneType))
217 row = (phoneNumber, display)
218 self._contactDetails.append(row)
219 if 0 < len(self._contactDetails):
220 self._numberIndex = defaultIndex if defaultIndex != -1 else 0
221 self._phoneButton.set_label(self._contactDetails[self._numberIndex][1])
223 self._numberIndex = -1
224 self._phoneButton.set_label("Error: No Number Available")
226 # Add the column to the messages tree view
227 self._messagemodel.clear()
228 self._messagesView.set_model(self._messagemodel)
229 self._messagesView.set_fixed_height_mode(False)
231 textrenderer = gtk.CellRendererText()
232 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
233 textrenderer.set_property("wrap-width", 450)
234 messageColumn = gtk.TreeViewColumn("")
235 messageColumn.pack_start(textrenderer, expand=True)
236 messageColumn.add_attribute(textrenderer, "markup", 0)
237 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
238 self._messagesView.append_column(messageColumn)
239 self._messagesView.set_headers_visible(False)
242 for message in messages:
244 self._messagemodel.append(row)
245 self._messagesView.show()
246 self._scrollWindow.show()
247 messagesSelection = self._messagesView.get_selection()
248 messagesSelection.select_path((len(messages)-1, ))
250 self._messagesView.hide()
251 self._scrollWindow.hide()
253 self._smsEntry.get_buffer().set_text("")
254 self._update_letter_count()
256 if parent is not None:
257 self._dialog.set_transient_for(parent)
258 parentSize = parent.get_size()
259 self._dialog.resize(parentSize[0], max(parentSize[1]-10, 100))
263 self._dialog.show_all()
264 self._smsEntry.grab_focus()
265 adjustment = self._scrollWindow.get_vadjustment()
266 dx = self._conversationView.get_allocation().height - self._conversationViewPort.get_allocation().height
268 adjustment.value = dx
270 if 1 < len(self._contactDetails):
271 if defaultIndex == -1:
272 self._request_number()
273 self._phoneButton.set_sensitive(True)
275 self._phoneButton.set_sensitive(False)
277 userResponse = self._dialog.run()
279 self._dialog.hide_all()
281 # Process the users response
282 if userResponse == gtk.RESPONSE_OK and 0 <= self._numberIndex:
283 phoneNumber = self._contactDetails[self._numberIndex][0]
284 phoneNumber = make_ugly(phoneNumber)
288 self._action = self.ACTION_CANCEL
289 if self._action == self.ACTION_SEND_SMS:
290 entryBuffer = self._smsEntry.get_buffer()
291 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
292 enteredMessage = enteredMessage[0:self.MAX_CHAR].strip()
293 if not enteredMessage:
295 self._action = self.ACTION_CANCEL
299 self._messagesView.remove_column(messageColumn)
300 self._messagesView.set_model(None)
302 return self._action, phoneNumber, enteredMessage
304 self._smsEntry.get_buffer().disconnect(entryConnectId)
305 self._phoneButton.disconnect(phoneConnectId)
306 self._keyPressEventId = self._dialog.disconnect(keyConnectId)
308 def _update_letter_count(self, *args):
309 entryLength = self._smsEntry.get_buffer().get_char_count()
311 charsLeft = self.MAX_CHAR - entryLength
312 self._letterCountLabel.set_text(str(charsLeft))
313 if charsLeft < 0 or charsLeft == self.MAX_CHAR:
314 self._smsButton.set_sensitive(False)
316 self._smsButton.set_sensitive(True)
319 self._dialButton.set_sensitive(True)
321 self._dialButton.set_sensitive(False)
323 def _request_number(self):
325 assert 0 <= self._numberIndex, "%r" % self._numberIndex
327 self._numberIndex = hildonize.touch_selector(
330 (description for (number, description) in self._contactDetails),
333 self._phoneButton.set_label(self._contactDetails[self._numberIndex][1])
335 _moduleLogger.exception("%s" % str(e))
337 def _on_phone(self, *args):
338 self._request_number()
340 def _on_entry_changed(self, *args):
341 self._update_letter_count()
343 def _on_send(self, *args):
344 self._dialog.response(gtk.RESPONSE_OK)
345 self._action = self.ACTION_SEND_SMS
347 def _on_dial(self, *args):
348 self._dialog.response(gtk.RESPONSE_OK)
349 self._action = self.ACTION_DIAL
351 def _on_cancel(self, *args):
352 self._dialog.response(gtk.RESPONSE_CANCEL)
353 self._action = self.ACTION_CANCEL
355 def _on_key_press(self, widget, event):
357 if event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
360 for messagePart in self._messagemodel
362 self._clipboard.set_text(str(message))
364 _moduleLogger.exception(str(e))
367 class Dialpad(object):
369 def __init__(self, widgetTree, errorDisplay):
370 self._clipboard = gtk.clipboard_get()
371 self._errorDisplay = errorDisplay
372 self._smsDialog = SmsEntryDialog(widgetTree)
374 self._numberdisplay = widgetTree.get_widget("numberdisplay")
375 self._smsButton = widgetTree.get_widget("sms")
376 self._dialButton = widgetTree.get_widget("dial")
377 self._backButton = widgetTree.get_widget("back")
378 self._phonenumber = ""
379 self._prettynumber = ""
382 "on_digit_clicked": self._on_digit_clicked,
384 widgetTree.signal_autoconnect(callbackMapping)
385 self._dialButton.connect("clicked", self._on_dial_clicked)
386 self._smsButton.connect("clicked", self._on_sms_clicked)
388 self._originalLabel = self._backButton.get_label()
389 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
390 self._backTapHandler.on_tap = self._on_backspace
391 self._backTapHandler.on_hold = self._on_clearall
392 self._backTapHandler.on_holding = self._set_clear_button
393 self._backTapHandler.on_cancel = self._reset_back_button
395 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
396 self._keyPressEventId = 0
399 self._dialButton.grab_focus()
400 self._backTapHandler.enable()
401 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
404 self._window.disconnect(self._keyPressEventId)
405 self._keyPressEventId = 0
406 self._reset_back_button()
407 self._backTapHandler.disable()
409 def number_selected(self, action, number, message):
411 @note Actual dial function is patched in later
413 raise NotImplementedError("Horrible unknown error has occurred")
415 def get_number(self):
416 return self._phonenumber
418 def set_number(self, number):
420 Set the number to dial
423 self._phonenumber = make_ugly(number)
424 self._prettynumber = make_pretty(self._phonenumber)
425 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
427 self._errorDisplay.push_exception()
436 def load_settings(self, config, section):
439 def save_settings(self, config, section):
441 @note Thread Agnostic
445 def _on_key_press(self, widget, event):
447 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
448 contents = self._clipboard.wait_for_text()
449 if contents is not None:
450 self.set_number(contents)
452 self._errorDisplay.push_exception()
454 def _on_sms_clicked(self, widget):
456 phoneNumber = self.get_number()
457 action, phoneNumber, message = self._smsDialog.run([("Dialer", phoneNumber)], (), self._window)
459 if action == SmsEntryDialog.ACTION_CANCEL:
461 self.number_selected(action, phoneNumber, message)
463 self._errorDisplay.push_exception()
465 def _on_dial_clicked(self, widget):
467 action = SmsEntryDialog.ACTION_DIAL
468 phoneNumber = self.get_number()
470 self.number_selected(action, phoneNumber, message)
472 self._errorDisplay.push_exception()
474 def _on_digit_clicked(self, widget):
476 self.set_number(self._phonenumber + widget.get_name()[-1])
478 self._errorDisplay.push_exception()
480 def _on_backspace(self, taps):
482 self.set_number(self._phonenumber[:-taps])
483 self._reset_back_button()
485 self._errorDisplay.push_exception()
487 def _on_clearall(self, taps):
490 self._reset_back_button()
492 self._errorDisplay.push_exception()
495 def _set_clear_button(self):
497 self._backButton.set_label("gtk-clear")
499 self._errorDisplay.push_exception()
501 def _reset_back_button(self):
503 self._backButton.set_label(self._originalLabel)
505 self._errorDisplay.push_exception()
508 class AccountInfo(object):
510 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
511 self._errorDisplay = errorDisplay
512 self._backend = backend
513 self._isPopulated = False
514 self._alarmHandler = alarmHandler
515 self._notifyOnMissed = False
516 self._notifyOnVoicemail = False
517 self._notifyOnSms = False
519 self._callbackList = []
520 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
521 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
522 self._onCallbackSelectChangedId = 0
524 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
525 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
526 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
527 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
528 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
529 self._onNotifyToggled = 0
530 self._onMinutesChanged = 0
531 self._onMissedToggled = 0
532 self._onVoicemailToggled = 0
533 self._onSmsToggled = 0
534 self._applyAlarmTimeoutId = None
536 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
537 self._callbackNumber = ""
540 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
542 self._accountViewNumberDisplay.set_use_markup(True)
543 self.set_account_number("")
545 del self._callbackList[:]
546 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
547 self._set_callback_label("")
549 if self._alarmHandler is not None:
550 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
551 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
552 self._missedCheckbox.set_active(self._notifyOnMissed)
553 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
554 self._smsCheckbox.set_active(self._notifyOnSms)
556 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
557 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
558 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
559 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
560 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
562 self._notifyCheckbox.set_sensitive(False)
563 self._minutesEntryButton.set_sensitive(False)
564 self._missedCheckbox.set_sensitive(False)
565 self._voicemailCheckbox.set_sensitive(False)
566 self._smsCheckbox.set_sensitive(False)
568 self.update(force=True)
571 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
572 self._onCallbackSelectChangedId = 0
573 self._set_callback_label("")
575 if self._alarmHandler is not None:
576 self._notifyCheckbox.disconnect(self._onNotifyToggled)
577 self._minutesEntryButton.disconnect(self._onMinutesChanged)
578 self._missedCheckbox.disconnect(self._onNotifyToggled)
579 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
580 self._smsCheckbox.disconnect(self._onNotifyToggled)
581 self._onNotifyToggled = 0
582 self._onMinutesChanged = 0
583 self._onMissedToggled = 0
584 self._onVoicemailToggled = 0
585 self._onSmsToggled = 0
587 self._notifyCheckbox.set_sensitive(True)
588 self._minutesEntryButton.set_sensitive(True)
589 self._missedCheckbox.set_sensitive(True)
590 self._voicemailCheckbox.set_sensitive(True)
591 self._smsCheckbox.set_sensitive(True)
594 del self._callbackList[:]
596 def set_account_number(self, number):
598 Displays current account number
600 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
602 def update(self, force = False):
603 if not force and self._isPopulated:
605 self._populate_callback_combo()
606 self.set_account_number(self._backend.get_account_number())
610 self._set_callback_label("")
611 self.set_account_number("")
612 self._isPopulated = False
614 def save_everything(self):
615 raise NotImplementedError
619 return "Account Info"
621 def load_settings(self, config, section):
622 self._callbackNumber = make_ugly(config.get(section, "callback"))
623 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
624 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
625 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
627 def save_settings(self, config, section):
629 @note Thread Agnostic
631 config.set(section, "callback", self._callbackNumber)
632 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
633 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
634 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
636 def _populate_callback_combo(self):
637 self._isPopulated = True
638 del self._callbackList[:]
640 callbackNumbers = self._backend.get_callback_numbers()
642 self._errorDisplay.push_exception()
643 self._isPopulated = False
646 if len(callbackNumbers) == 0:
647 callbackNumbers = {"": "No callback numbers available"}
649 for number, description in callbackNumbers.iteritems():
650 self._callbackList.append((make_pretty(number), description))
652 self._set_callback_number(self._callbackNumber)
654 def _set_callback_number(self, number):
656 if not self._backend.is_valid_syntax(number) and 0 < len(number):
657 self._errorDisplay.push_message("%s is not a valid callback number" % number)
658 elif number == self._backend.get_callback_number() and 0 < len(number):
659 _moduleLogger.warning(
660 "Callback number already is %s" % (
661 self._backend.get_callback_number(),
664 self._set_callback_label(number)
666 if number.startswith("1747"): number = "+" + number
667 self._backend.set_callback_number(number)
668 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
669 make_pretty(number), make_pretty(self._backend.get_callback_number())
671 self._callbackNumber = make_ugly(number)
672 self._set_callback_label(number)
674 "Callback number set to %s" % (
675 self._backend.get_callback_number(),
679 self._errorDisplay.push_exception()
681 def _set_callback_label(self, uglyNumber):
682 prettyNumber = make_pretty(uglyNumber)
683 if len(prettyNumber) == 0:
684 prettyNumber = "No Callback Number"
685 self._callbackSelectButton.set_label(prettyNumber)
687 def _update_alarm_settings(self, recurrence):
689 isEnabled = self._notifyCheckbox.get_active()
690 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
691 self._alarmHandler.apply_settings(isEnabled, recurrence)
693 self.save_everything()
694 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
695 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
697 def _on_callbackentry_clicked(self, *args):
699 actualSelection = make_pretty(self._callbackNumber)
702 (number, "%s (%s)" % (number, description))
703 for (number, description) in self._callbackList
705 defaultSelection = userOptions.get(actualSelection, actualSelection)
707 userSelection = hildonize.touch_selector_entry(
710 list(userOptions.itervalues()),
713 reversedUserOptions = dict(
714 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
716 selectedNumber = reversedUserOptions.get(userSelection, userSelection)
718 number = make_ugly(selectedNumber)
719 self._set_callback_number(number)
720 except RuntimeError, e:
721 _moduleLogger.exception("%s" % str(e))
723 self._errorDisplay.push_exception()
725 def _on_notify_toggled(self, *args):
727 if self._applyAlarmTimeoutId is not None:
728 gobject.source_remove(self._applyAlarmTimeoutId)
729 self._applyAlarmTimeoutId = None
730 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
732 self._errorDisplay.push_exception()
734 def _on_minutes_clicked(self, *args):
735 recurrenceChoices = [
751 actualSelection = self._alarmHandler.recurrence
753 closestSelectionIndex = 0
754 for i, possible in enumerate(recurrenceChoices):
755 if possible[0] <= actualSelection:
756 closestSelectionIndex = i
757 recurrenceIndex = hildonize.touch_selector(
760 (("%s" % m[1]) for m in recurrenceChoices),
761 closestSelectionIndex,
763 recurrence = recurrenceChoices[recurrenceIndex][0]
765 self._update_alarm_settings(recurrence)
766 except RuntimeError, e:
767 _moduleLogger.exception("%s" % str(e))
769 self._errorDisplay.push_exception()
771 def _on_apply_timeout(self, *args):
773 self._applyAlarmTimeoutId = None
775 self._update_alarm_settings(self._alarmHandler.recurrence)
777 self._errorDisplay.push_exception()
780 def _on_missed_toggled(self, *args):
782 self._notifyOnMissed = self._missedCheckbox.get_active()
783 self.save_everything()
785 self._errorDisplay.push_exception()
787 def _on_voicemail_toggled(self, *args):
789 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
790 self.save_everything()
792 self._errorDisplay.push_exception()
794 def _on_sms_toggled(self, *args):
796 self._notifyOnSms = self._smsCheckbox.get_active()
797 self.save_everything()
799 self._errorDisplay.push_exception()
802 class CallHistoryView(object):
810 def __init__(self, widgetTree, backend, errorDisplay):
811 self._errorDisplay = errorDisplay
812 self._backend = backend
814 self._isPopulated = False
815 self._historymodel = gtk.ListStore(
816 gobject.TYPE_STRING, # number
817 gobject.TYPE_STRING, # date
818 gobject.TYPE_STRING, # action
819 gobject.TYPE_STRING, # from
820 gobject.TYPE_STRING, # from id
822 self._historyview = widgetTree.get_widget("historyview")
823 self._historyviewselection = None
824 self._onRecentviewRowActivatedId = 0
826 textrenderer = gtk.CellRendererText()
827 textrenderer.set_property("yalign", 0)
828 self._dateColumn = gtk.TreeViewColumn("Date")
829 self._dateColumn.pack_start(textrenderer, expand=True)
830 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
832 textrenderer = gtk.CellRendererText()
833 textrenderer.set_property("yalign", 0)
834 self._actionColumn = gtk.TreeViewColumn("Action")
835 self._actionColumn.pack_start(textrenderer, expand=True)
836 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
838 textrenderer = gtk.CellRendererText()
839 textrenderer.set_property("yalign", 0)
840 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
841 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
842 self._numberColumn = gtk.TreeViewColumn("Number")
843 self._numberColumn.pack_start(textrenderer, expand=True)
844 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
846 textrenderer = gtk.CellRendererText()
847 textrenderer.set_property("yalign", 0)
848 hildonize.set_cell_thumb_selectable(textrenderer)
849 self._nameColumn = gtk.TreeViewColumn("From")
850 self._nameColumn.pack_start(textrenderer, expand=True)
851 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
852 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
854 self._window = gtk_toolbox.find_parent_window(self._historyview)
855 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
857 self._updateSink = gtk_toolbox.threaded_stage(
859 self._idly_populate_historyview,
860 gtk_toolbox.null_sink(),
865 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
866 self._historyview.set_model(self._historymodel)
867 self._historyview.set_fixed_height_mode(False)
869 self._historyview.append_column(self._dateColumn)
870 self._historyview.append_column(self._actionColumn)
871 self._historyview.append_column(self._numberColumn)
872 self._historyview.append_column(self._nameColumn)
873 self._historyviewselection = self._historyview.get_selection()
874 self._historyviewselection.set_mode(gtk.SELECTION_SINGLE)
876 self._onRecentviewRowActivatedId = self._historyview.connect("row-activated", self._on_historyview_row_activated)
879 self._historyview.disconnect(self._onRecentviewRowActivatedId)
883 self._historyview.remove_column(self._dateColumn)
884 self._historyview.remove_column(self._actionColumn)
885 self._historyview.remove_column(self._nameColumn)
886 self._historyview.remove_column(self._numberColumn)
887 self._historyview.set_model(None)
889 def number_selected(self, action, number, message):
891 @note Actual dial function is patched in later
893 raise NotImplementedError("Horrible unknown error has occurred")
895 def update(self, force = False):
896 if not force and self._isPopulated:
898 self._updateSink.send(())
902 self._isPopulated = False
903 self._historymodel.clear()
907 return "Recent Calls"
909 def load_settings(self, config, section):
912 def save_settings(self, config, section):
914 @note Thread Agnostic
918 def _idly_populate_historyview(self):
919 with gtk_toolbox.gtk_lock():
920 banner = hildonize.show_busy_banner_start(self._window, "Loading Call History")
922 self._historymodel.clear()
923 self._isPopulated = True
926 historyItems = self._backend.get_recent()
928 self._errorDisplay.push_exception_with_lock()
929 self._isPopulated = False
933 gv_backend.decorate_recent(data)
934 for data in gv_backend.sort_messages(historyItems)
937 for contactId, personName, phoneNumber, date, action in historyItems:
939 personName = "Unknown"
940 date = abbrev_relative_date(date)
941 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
942 prettyNumber = make_pretty(prettyNumber)
943 item = (prettyNumber, date, action.capitalize(), personName, contactId)
944 with gtk_toolbox.gtk_lock():
945 self._historymodel.append(item)
947 self._errorDisplay.push_exception_with_lock()
949 with gtk_toolbox.gtk_lock():
950 hildonize.show_busy_banner_end(banner)
954 def _on_historyview_row_activated(self, treeview, path, view_column):
956 itr = self._historymodel.get_iter(path)
960 number = self._historymodel.get_value(itr, self.NUMBER_IDX)
961 number = make_ugly(number)
962 description = self._historymodel.get_value(itr, self.FROM_IDX)
963 contactId = self._historymodel.get_value(itr, self.FROM_ID_IDX)
965 contactPhoneNumbers = list(self._backend.get_contact_details(contactId))
967 (number == make_ugly(contactNumber) or number[1:] == make_ugly(contactNumber))
968 for (numberDescription, contactNumber) in contactPhoneNumbers
971 defaultIndex = defaultMatches.index(True)
973 contactPhoneNumbers.append(("Other", number))
974 defaultIndex = len(contactPhoneNumbers)-1
976 "Could not find contact %r's number %s among %r" % (
977 contactId, number, contactPhoneNumbers
981 contactPhoneNumbers = [("Phone", number)]
984 action, phoneNumber, message = self._phoneTypeSelector.run(
986 messages = (description, ),
987 parent = self._window,
988 defaultIndex = defaultIndex,
990 if action == SmsEntryDialog.ACTION_CANCEL:
992 assert phoneNumber, "A lack of phone number exists"
994 self.number_selected(action, phoneNumber, message)
995 self._historyviewselection.unselect_all()
997 self._errorDisplay.push_exception()
1000 class MessagesView(object):
1008 MESSAGE_DATA_IDX = 6
1010 NO_MESSAGES = "None"
1011 VOICEMAIL_MESSAGES = "Voicemail"
1012 TEXT_MESSAGES = "Texts"
1013 ALL_TYPES = "All Messages"
1014 MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
1016 UNREAD_STATUS = "Unread"
1017 UNARCHIVED_STATUS = "Inbox"
1019 MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
1021 def __init__(self, widgetTree, backend, errorDisplay):
1022 self._errorDisplay = errorDisplay
1023 self._backend = backend
1025 self._isPopulated = False
1026 self._messagemodel = gtk.ListStore(
1027 gobject.TYPE_STRING, # number
1028 gobject.TYPE_STRING, # date
1029 gobject.TYPE_STRING, # header
1030 gobject.TYPE_STRING, # message
1032 gobject.TYPE_STRING, # from id
1033 object, # message data
1035 self._messagemodelfiltered = self._messagemodel.filter_new()
1036 self._messagemodelfiltered.set_visible_func(self._is_message_visible)
1037 self._messageview = widgetTree.get_widget("messages_view")
1038 self._messageviewselection = None
1039 self._onMessageviewRowActivatedId = 0
1041 self._messageRenderer = gtk.CellRendererText()
1042 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1043 self._messageRenderer.set_property("wrap-width", 500)
1044 self._messageColumn = gtk.TreeViewColumn("Messages")
1045 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1046 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1047 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1049 self._window = gtk_toolbox.find_parent_window(self._messageview)
1050 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
1052 self._messageTypeButton = widgetTree.get_widget("messageTypeButton")
1053 self._onMessageTypeClickedId = 0
1054 self._messageType = self.ALL_TYPES
1055 self._messageStatusButton = widgetTree.get_widget("messageStatusButton")
1056 self._onMessageStatusClickedId = 0
1057 self._messageStatus = self.ALL_STATUS
1059 self._updateSink = gtk_toolbox.threaded_stage(
1061 self._idly_populate_messageview,
1062 gtk_toolbox.null_sink(),
1067 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1068 self._messageview.set_model(self._messagemodelfiltered)
1069 self._messageview.set_headers_visible(False)
1070 self._messageview.set_fixed_height_mode(False)
1072 self._messageview.append_column(self._messageColumn)
1073 self._messageviewselection = self._messageview.get_selection()
1074 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1076 self._messageTypeButton.set_label(self._messageType)
1077 self._messageStatusButton.set_label(self._messageStatus)
1079 self._onMessageviewRowActivatedId = self._messageview.connect(
1080 "row-activated", self._on_messageview_row_activated
1082 self._onMessageTypeClickedId = self._messageTypeButton.connect(
1083 "clicked", self._on_message_type_clicked
1085 self._onMessageStatusClickedId = self._messageStatusButton.connect(
1086 "clicked", self._on_message_status_clicked
1090 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1091 self._messageTypeButton.disconnect(self._onMessageTypeClickedId)
1092 self._messageStatusButton.disconnect(self._onMessageStatusClickedId)
1096 self._messageview.remove_column(self._messageColumn)
1097 self._messageview.set_model(None)
1099 def number_selected(self, action, number, message):
1101 @note Actual dial function is patched in later
1103 raise NotImplementedError("Horrible unknown error has occurred")
1105 def update(self, force = False):
1106 if not force and self._isPopulated:
1108 self._updateSink.send(())
1112 self._isPopulated = False
1113 self._messagemodel.clear()
1119 def load_settings(self, config, sectionName):
1121 self._messageType = config.get(sectionName, "type")
1122 if self._messageType not in self.MESSAGE_TYPES:
1123 self._messageType = self.ALL_TYPES
1124 self._messageStatus = config.get(sectionName, "status")
1125 if self._messageStatus not in self.MESSAGE_STATUSES:
1126 self._messageStatus = self.ALL_STATUS
1127 except ConfigParser.NoOptionError:
1130 def save_settings(self, config, sectionName):
1132 @note Thread Agnostic
1134 config.set(sectionName, "status", self._messageStatus)
1135 config.set(sectionName, "type", self._messageType)
1137 def _is_message_visible(self, model, iter):
1139 message = model.get_value(iter, self.MESSAGE_DATA_IDX)
1141 return False # this seems weird but oh well
1142 return self._filter_messages(message, self._messageType, self._messageStatus)
1143 except Exception, e:
1144 self._errorDisplay.push_exception()
1147 def _filter_messages(cls, message, type, status):
1148 if type == cls.ALL_TYPES:
1151 messageType = message["type"]
1152 isType = messageType == type
1154 if status == cls.ALL_STATUS:
1157 isUnarchived = not message["isArchived"]
1158 isUnread = not message["isRead"]
1159 if status == cls.UNREAD_STATUS:
1160 isStatus = isUnarchived and isUnread
1161 elif status == cls.UNARCHIVED_STATUS:
1162 isStatus = isUnarchived
1164 assert "Status %s is bad for %r" % (status, message)
1166 return isType and isStatus
1168 _MIN_MESSAGES_SHOWN = 4
1170 def _idly_populate_messageview(self):
1171 with gtk_toolbox.gtk_lock():
1172 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1174 self._messagemodel.clear()
1175 self._isPopulated = True
1177 if self._messageType == self.NO_MESSAGES:
1181 messageItems = self._backend.get_messages()
1182 except Exception, e:
1183 self._errorDisplay.push_exception_with_lock()
1184 self._isPopulated = False
1188 (gv_backend.decorate_message(message), message)
1189 for message in gv_backend.sort_messages(messageItems)
1192 for (contactId, header, number, relativeDate, messages), messageData in messageItems:
1193 prettyNumber = number[2:] if number.startswith("+1") else number
1194 prettyNumber = make_pretty(prettyNumber)
1196 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1197 expandedMessages = [firstMessage]
1198 expandedMessages.extend(messages)
1199 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1200 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1201 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1202 collapsedMessages = [firstMessage, secondMessage]
1203 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1205 collapsedMessages = expandedMessages
1206 #collapsedMessages = _collapse_message(collapsedMessages, 60, self._MIN_MESSAGES_SHOWN)
1208 number = make_ugly(number)
1210 row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId, messageData
1211 with gtk_toolbox.gtk_lock():
1212 self._messagemodel.append(row)
1213 except Exception, e:
1214 self._errorDisplay.push_exception_with_lock()
1216 with gtk_toolbox.gtk_lock():
1217 hildonize.show_busy_banner_end(banner)
1218 self._messagemodelfiltered.refilter()
1222 def _on_messageview_row_activated(self, treeview, path, view_column):
1224 itr = self._messagemodel.get_iter(path)
1228 number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX))
1229 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1231 contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX)
1233 contactPhoneNumbers = list(self._backend.get_contact_details(contactId))
1235 (number == make_ugly(contactNumber) or number[1:] == make_ugly(contactNumber))
1236 for (numberDescription, contactNumber) in contactPhoneNumbers
1239 defaultIndex = defaultMatches.index(True)
1241 contactPhoneNumbers.append(("Other", number))
1242 defaultIndex = len(contactPhoneNumbers)-1
1244 "Could not find contact %r's number %s among %r" % (
1245 contactId, number, contactPhoneNumbers
1249 contactPhoneNumbers = [("Phone", number)]
1252 action, phoneNumber, message = self._phoneTypeSelector.run(
1253 contactPhoneNumbers,
1254 messages = description,
1255 parent = self._window,
1256 defaultIndex = defaultIndex,
1258 if action == SmsEntryDialog.ACTION_CANCEL:
1260 assert phoneNumber, "A lock of phone number exists"
1262 self.number_selected(action, phoneNumber, message)
1263 self._messageviewselection.unselect_all()
1264 except Exception, e:
1265 self._errorDisplay.push_exception()
1267 def _on_message_type_clicked(self, *args, **kwds):
1269 selectedIndex = self.MESSAGE_TYPES.index(self._messageType)
1272 newSelectedIndex = hildonize.touch_selector(
1278 except RuntimeError:
1281 if selectedIndex != newSelectedIndex:
1282 self._messageType = self.MESSAGE_TYPES[newSelectedIndex]
1283 self._messageTypeButton.set_label(self._messageType)
1284 self._messagemodelfiltered.refilter()
1285 except Exception, e:
1286 self._errorDisplay.push_exception()
1288 def _on_message_status_clicked(self, *args, **kwds):
1290 selectedIndex = self.MESSAGE_STATUSES.index(self._messageStatus)
1293 newSelectedIndex = hildonize.touch_selector(
1296 self.MESSAGE_STATUSES,
1299 except RuntimeError:
1302 if selectedIndex != newSelectedIndex:
1303 self._messageStatus = self.MESSAGE_STATUSES[newSelectedIndex]
1304 self._messageStatusButton.set_label(self._messageStatus)
1305 self._messagemodelfiltered.refilter()
1306 except Exception, e:
1307 self._errorDisplay.push_exception()
1310 class ContactsView(object):
1312 CONTACT_TYPE_IDX = 0
1313 CONTACT_NAME_IDX = 1
1316 def __init__(self, widgetTree, backend, errorDisplay):
1317 self._errorDisplay = errorDisplay
1318 self._backend = backend
1320 self._addressBook = None
1321 self._selectedComboIndex = 0
1322 self._addressBookFactories = [null_backend.NullAddressBook()]
1324 self._booksList = []
1325 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1327 self._isPopulated = False
1328 self._contactsmodel = gtk.ListStore(
1329 gobject.TYPE_STRING, # Contact Type
1330 gobject.TYPE_STRING, # Contact Name
1331 gobject.TYPE_STRING, # Contact ID
1333 self._contactsviewselection = None
1334 self._contactsview = widgetTree.get_widget("contactsview")
1336 self._contactColumn = gtk.TreeViewColumn("Contact")
1337 displayContactSource = False
1338 if displayContactSource:
1339 textrenderer = gtk.CellRendererText()
1340 self._contactColumn.pack_start(textrenderer, expand=False)
1341 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_TYPE_IDX)
1342 textrenderer = gtk.CellRendererText()
1343 hildonize.set_cell_thumb_selectable(textrenderer)
1344 self._contactColumn.pack_start(textrenderer, expand=True)
1345 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_NAME_IDX)
1346 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1347 self._contactColumn.set_sort_column_id(1)
1348 self._contactColumn.set_visible(True)
1350 self._onContactsviewRowActivatedId = 0
1351 self._onAddressbookButtonChangedId = 0
1352 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1353 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
1355 self._updateSink = gtk_toolbox.threaded_stage(
1357 self._idly_populate_contactsview,
1358 gtk_toolbox.null_sink(),
1363 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1365 self._contactsview.set_model(self._contactsmodel)
1366 self._contactsview.set_fixed_height_mode(False)
1367 self._contactsview.append_column(self._contactColumn)
1368 self._contactsviewselection = self._contactsview.get_selection()
1369 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1371 del self._booksList[:]
1372 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1373 if factoryName and bookName:
1374 entryName = "%s: %s" % (factoryName, bookName)
1376 entryName = factoryName
1378 entryName = bookName
1380 entryName = "Bad name (%d)" % factoryId
1381 row = (str(factoryId), bookId, entryName)
1382 self._booksList.append(row)
1384 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1385 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1387 if len(self._booksList) <= self._selectedComboIndex:
1388 self._selectedComboIndex = 0
1389 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1391 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1392 selectedBookId = self._booksList[self._selectedComboIndex][1]
1393 self.open_addressbook(selectedFactoryId, selectedBookId)
1396 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1397 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1401 self._bookSelectionButton.set_label("")
1402 self._contactsview.set_model(None)
1403 self._contactsview.remove_column(self._contactColumn)
1405 def number_selected(self, action, number, message):
1407 @note Actual dial function is patched in later
1409 raise NotImplementedError("Horrible unknown error has occurred")
1411 def get_addressbooks(self):
1413 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1415 for i, factory in enumerate(self._addressBookFactories):
1416 for bookFactory, bookId, bookName in factory.get_addressbooks():
1417 yield (str(i), bookId), (factory.factory_name(), bookName)
1419 def open_addressbook(self, bookFactoryId, bookId):
1420 bookFactoryIndex = int(bookFactoryId)
1421 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1422 self._addressBook = addressBook
1424 def update(self, force = False):
1425 if not force and self._isPopulated:
1427 self._updateSink.send(())
1431 self._isPopulated = False
1432 self._contactsmodel.clear()
1433 for factory in self._addressBookFactories:
1434 factory.clear_caches()
1435 self._addressBook.clear_caches()
1437 def append(self, book):
1438 self._addressBookFactories.append(book)
1440 def extend(self, books):
1441 self._addressBookFactories.extend(books)
1447 def load_settings(self, config, sectionName):
1449 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1450 except ConfigParser.NoOptionError:
1451 self._selectedComboIndex = 0
1453 def save_settings(self, config, sectionName):
1454 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1456 def _idly_populate_contactsview(self):
1457 with gtk_toolbox.gtk_lock():
1458 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1461 while addressBook is not self._addressBook:
1462 addressBook = self._addressBook
1463 with gtk_toolbox.gtk_lock():
1464 self._contactsview.set_model(None)
1468 contacts = addressBook.get_contacts()
1469 except Exception, e:
1471 self._isPopulated = False
1472 self._errorDisplay.push_exception_with_lock()
1473 for contactId, contactName in contacts:
1474 contactType = addressBook.contact_source_short_name(contactId)
1475 row = contactType, contactName, contactId
1476 self._contactsmodel.append(row)
1478 with gtk_toolbox.gtk_lock():
1479 self._contactsview.set_model(self._contactsmodel)
1481 self._isPopulated = True
1482 except Exception, e:
1483 self._errorDisplay.push_exception_with_lock()
1485 with gtk_toolbox.gtk_lock():
1486 hildonize.show_busy_banner_end(banner)
1489 def _on_addressbook_button_changed(self, *args, **kwds):
1492 newSelectedComboIndex = hildonize.touch_selector(
1495 (("%s" % m[2]) for m in self._booksList),
1496 self._selectedComboIndex,
1498 except RuntimeError:
1501 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1502 selectedBookId = self._booksList[newSelectedComboIndex][1]
1504 oldAddressbook = self._addressBook
1505 self.open_addressbook(selectedFactoryId, selectedBookId)
1506 forceUpdate = True if oldAddressbook is not self._addressBook else False
1507 self.update(force=forceUpdate)
1509 self._selectedComboIndex = newSelectedComboIndex
1510 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1511 except Exception, e:
1512 self._errorDisplay.push_exception()
1514 def _on_contactsview_row_activated(self, treeview, path, view_column):
1516 itr = self._contactsmodel.get_iter(path)
1520 contactId = self._contactsmodel.get_value(itr, self.CONTACT_ID_IDX)
1521 contactName = self._contactsmodel.get_value(itr, self.CONTACT_NAME_IDX)
1523 contactDetails = self._addressBook.get_contact_details(contactId)
1524 except Exception, e:
1526 self._errorDisplay.push_exception()
1527 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1529 if len(contactPhoneNumbers) == 0:
1532 action, phoneNumber, message = self._phoneTypeSelector.run(
1533 contactPhoneNumbers,
1534 messages = (contactName, ),
1535 parent = self._window,
1537 if action == SmsEntryDialog.ACTION_CANCEL:
1539 assert phoneNumber, "A lack of phone number exists"
1541 self.number_selected(action, phoneNumber, message)
1542 self._contactsviewselection.unselect_all()
1543 except Exception, e:
1544 self._errorDisplay.push_exception()