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
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 MergedAddressBook(object):
170 Merger of all addressbooks
173 def __init__(self, addressbookFactories, sorter = None):
174 self.__addressbookFactories = addressbookFactories
175 self.__addressbooks = None
176 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
178 def clear_caches(self):
179 self.__addressbooks = None
180 for factory in self.__addressbookFactories:
181 factory.clear_caches()
183 def get_addressbooks(self):
185 @returns Iterable of (Address Book Factory, Book Id, Book Name)
189 def open_addressbook(self, bookId):
192 def contact_source_short_name(self, contactId):
193 if self.__addressbooks is None:
195 bookIndex, originalId = contactId.split("-", 1)
196 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
200 return "All Contacts"
202 def get_contacts(self):
204 @returns Iterable of (contact id, contact name)
206 if self.__addressbooks is None:
207 self.__addressbooks = list(
208 factory.open_addressbook(id)
209 for factory in self.__addressbookFactories
210 for (f, id, name) in factory.get_addressbooks()
213 ("-".join([str(bookIndex), contactId]), contactName)
214 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
215 for (contactId, contactName) in addressbook.get_contacts()
217 sortedContacts = self.__sort_contacts(contacts)
218 return sortedContacts
220 def get_contact_details(self, contactId):
222 @returns Iterable of (Phone Type, Phone Number)
224 if self.__addressbooks is None:
226 bookIndex, originalId = contactId.split("-", 1)
227 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
230 def null_sorter(contacts):
232 Good for speed/low memory
237 def basic_firtname_sorter(contacts):
239 Expects names in "First Last" format
242 (contactName.rsplit(" ", 1)[0], (contactId, contactName))
243 for (contactId, contactName) in contacts
245 contactsWithKey.sort()
246 return (contactData for (lastName, contactData) in contactsWithKey)
249 def basic_lastname_sorter(contacts):
251 Expects names in "First Last" format
254 (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
255 for (contactId, contactName) in contacts
257 contactsWithKey.sort()
258 return (contactData for (lastName, contactData) in contactsWithKey)
261 def reversed_firtname_sorter(contacts):
263 Expects names in "Last, First" format
266 (contactName.split(", ", 1)[-1], (contactId, contactName))
267 for (contactId, contactName) in contacts
269 contactsWithKey.sort()
270 return (contactData for (lastName, contactData) in contactsWithKey)
273 def reversed_lastname_sorter(contacts):
275 Expects names in "Last, First" format
278 (contactName.split(", ", 1)[0], (contactId, contactName))
279 for (contactId, contactName) in contacts
281 contactsWithKey.sort()
282 return (contactData for (lastName, contactData) in contactsWithKey)
285 def guess_firstname(name):
287 return name.split(", ", 1)[-1]
289 return name.rsplit(" ", 1)[0]
292 def guess_lastname(name):
294 return name.split(", ", 1)[0]
296 return name.rsplit(" ", 1)[-1]
299 def advanced_firstname_sorter(cls, contacts):
301 (cls.guess_firstname(contactName), (contactId, contactName))
302 for (contactId, contactName) in contacts
304 contactsWithKey.sort()
305 return (contactData for (lastName, contactData) in contactsWithKey)
308 def advanced_lastname_sorter(cls, contacts):
310 (cls.guess_lastname(contactName), (contactId, contactName))
311 for (contactId, contactName) in contacts
313 contactsWithKey.sort()
314 return (contactData for (lastName, contactData) in contactsWithKey)
317 class SmsEntryDialog(object):
319 @todo Add multi-SMS messages like GoogleVoice
322 ACTION_CANCEL = "cancel"
324 ACTION_SEND_SMS = "sms"
328 def __init__(self, widgetTree):
329 self._clipboard = gtk.clipboard_get()
330 self._widgetTree = widgetTree
331 self._dialog = self._widgetTree.get_widget("smsDialog")
333 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
334 self._smsButton.connect("clicked", self._on_send)
335 self._dialButton = self._widgetTree.get_widget("dialButton")
336 self._dialButton.connect("clicked", self._on_dial)
337 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
338 self._cancelButton.connect("clicked", self._on_cancel)
340 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
342 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
343 self._messagesView = self._widgetTree.get_widget("smsMessages")
345 self._conversationView = self._messagesView.get_parent()
346 self._conversationViewPort = self._conversationView.get_parent()
347 self._scrollWindow = self._conversationViewPort.get_parent()
349 self._phoneButton = self._widgetTree.get_widget("phoneTypeSelection")
350 self._smsEntry = self._widgetTree.get_widget("smsEntry")
352 self._action = self.ACTION_CANCEL
354 self._numberIndex = -1
355 self._contactDetails = []
357 def run(self, contactDetails, messages = (), parent = None, defaultIndex = -1):
358 entryConnectId = self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
359 phoneConnectId = self._phoneButton.connect("clicked", self._on_phone)
360 keyConnectId = self._keyPressEventId = self._dialog.connect("key-press-event", self._on_key_press)
362 # Setup the phone selection button
363 del self._contactDetails[:]
364 for phoneType, phoneNumber in contactDetails:
365 display = " - ".join((make_pretty(phoneNumber), phoneType))
366 row = (phoneNumber, display)
367 self._contactDetails.append(row)
368 if 0 < len(self._contactDetails):
369 self._numberIndex = defaultIndex if defaultIndex != -1 else 0
370 self._phoneButton.set_label(self._contactDetails[self._numberIndex][1])
372 self._numberIndex = -1
373 self._phoneButton.set_label("Error: No Number Available")
375 # Add the column to the messages tree view
376 self._messagemodel.clear()
377 self._messagesView.set_model(self._messagemodel)
378 self._messagesView.set_fixed_height_mode(False)
380 textrenderer = gtk.CellRendererText()
381 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
382 textrenderer.set_property("wrap-width", 450)
383 messageColumn = gtk.TreeViewColumn("")
384 messageColumn.pack_start(textrenderer, expand=True)
385 messageColumn.add_attribute(textrenderer, "markup", 0)
386 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
387 self._messagesView.append_column(messageColumn)
388 self._messagesView.set_headers_visible(False)
391 for message in messages:
393 self._messagemodel.append(row)
394 self._messagesView.show()
395 self._scrollWindow.show()
396 messagesSelection = self._messagesView.get_selection()
397 messagesSelection.select_path((len(messages)-1, ))
399 self._messagesView.hide()
400 self._scrollWindow.hide()
402 self._smsEntry.get_buffer().set_text("")
403 self._update_letter_count()
405 if parent is not None:
406 self._dialog.set_transient_for(parent)
407 parentSize = parent.get_size()
408 self._dialog.resize(parentSize[0], max(parentSize[1]-10, 100))
412 self._dialog.show_all()
413 self._smsEntry.grab_focus()
414 adjustment = self._scrollWindow.get_vadjustment()
415 dx = self._conversationView.get_allocation().height - self._conversationViewPort.get_allocation().height
417 adjustment.value = dx
419 if 1 < len(self._contactDetails):
420 if defaultIndex == -1:
421 self._request_number()
422 self._phoneButton.set_sensitive(True)
424 self._phoneButton.set_sensitive(False)
426 userResponse = self._dialog.run()
428 self._dialog.hide_all()
430 # Process the users response
431 if userResponse == gtk.RESPONSE_OK and 0 <= self._numberIndex:
432 phoneNumber = self._contactDetails[self._numberIndex][0]
433 phoneNumber = make_ugly(phoneNumber)
437 self._action = self.ACTION_CANCEL
438 if self._action == self.ACTION_SEND_SMS:
439 entryBuffer = self._smsEntry.get_buffer()
440 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
441 enteredMessage = enteredMessage[0:self.MAX_CHAR].strip()
442 if not enteredMessage:
444 self._action = self.ACTION_CANCEL
448 self._messagesView.remove_column(messageColumn)
449 self._messagesView.set_model(None)
451 return self._action, phoneNumber, enteredMessage
453 self._smsEntry.get_buffer().disconnect(entryConnectId)
454 self._phoneButton.disconnect(phoneConnectId)
455 self._keyPressEventId = self._dialog.disconnect(keyConnectId)
457 def _update_letter_count(self, *args):
458 entryLength = self._smsEntry.get_buffer().get_char_count()
460 charsLeft = self.MAX_CHAR - entryLength
461 self._letterCountLabel.set_text(str(charsLeft))
462 if charsLeft < 0 or charsLeft == self.MAX_CHAR:
463 self._smsButton.set_sensitive(False)
465 self._smsButton.set_sensitive(True)
468 self._dialButton.set_sensitive(True)
470 self._dialButton.set_sensitive(False)
472 def _request_number(self):
474 assert 0 <= self._numberIndex, "%r" % self._numberIndex
476 self._numberIndex = hildonize.touch_selector(
479 (description for (number, description) in self._contactDetails),
482 self._phoneButton.set_label(self._contactDetails[self._numberIndex][1])
484 _moduleLogger.exception("%s" % str(e))
486 def _on_phone(self, *args):
487 self._request_number()
489 def _on_entry_changed(self, *args):
490 self._update_letter_count()
492 def _on_send(self, *args):
493 self._dialog.response(gtk.RESPONSE_OK)
494 self._action = self.ACTION_SEND_SMS
496 def _on_dial(self, *args):
497 self._dialog.response(gtk.RESPONSE_OK)
498 self._action = self.ACTION_DIAL
500 def _on_cancel(self, *args):
501 self._dialog.response(gtk.RESPONSE_CANCEL)
502 self._action = self.ACTION_CANCEL
504 def _on_key_press(self, widget, event):
506 if event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
509 for messagePart in self._messagemodel
511 # For some reason this kills clipboard stuff
512 #self._clipboard.set_text(message)
514 _moduleLogger.exception(str(e))
517 class Dialpad(object):
519 def __init__(self, widgetTree, errorDisplay):
520 self._clipboard = gtk.clipboard_get()
521 self._errorDisplay = errorDisplay
522 self._smsDialog = SmsEntryDialog(widgetTree)
524 self._numberdisplay = widgetTree.get_widget("numberdisplay")
525 self._smsButton = widgetTree.get_widget("sms")
526 self._dialButton = widgetTree.get_widget("dial")
527 self._backButton = widgetTree.get_widget("back")
528 self._phonenumber = ""
529 self._prettynumber = ""
532 "on_digit_clicked": self._on_digit_clicked,
534 widgetTree.signal_autoconnect(callbackMapping)
535 self._dialButton.connect("clicked", self._on_dial_clicked)
536 self._smsButton.connect("clicked", self._on_sms_clicked)
538 self._originalLabel = self._backButton.get_label()
539 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
540 self._backTapHandler.on_tap = self._on_backspace
541 self._backTapHandler.on_hold = self._on_clearall
542 self._backTapHandler.on_holding = self._set_clear_button
543 self._backTapHandler.on_cancel = self._reset_back_button
545 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
546 self._keyPressEventId = 0
549 self._dialButton.grab_focus()
550 self._backTapHandler.enable()
551 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
554 self._window.disconnect(self._keyPressEventId)
555 self._keyPressEventId = 0
556 self._reset_back_button()
557 self._backTapHandler.disable()
559 def number_selected(self, action, number, message):
561 @note Actual dial function is patched in later
563 raise NotImplementedError("Horrible unknown error has occurred")
565 def get_number(self):
566 return self._phonenumber
568 def set_number(self, number):
570 Set the number to dial
573 self._phonenumber = make_ugly(number)
574 self._prettynumber = make_pretty(self._phonenumber)
575 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
577 self._errorDisplay.push_exception()
586 def load_settings(self, config, section):
589 def save_settings(self, config, section):
591 @note Thread Agnostic
595 def _on_key_press(self, widget, event):
597 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
598 contents = self._clipboard.wait_for_text()
599 if contents is not None:
600 self.set_number(contents)
602 self._errorDisplay.push_exception()
604 def _on_sms_clicked(self, widget):
606 phoneNumber = self.get_number()
607 action, phoneNumber, message = self._smsDialog.run([("Dialer", phoneNumber)], (), self._window)
609 if action == SmsEntryDialog.ACTION_CANCEL:
611 self.number_selected(action, phoneNumber, message)
613 self._errorDisplay.push_exception()
615 def _on_dial_clicked(self, widget):
617 action = SmsEntryDialog.ACTION_DIAL
618 phoneNumber = self.get_number()
620 self.number_selected(action, phoneNumber, message)
622 self._errorDisplay.push_exception()
624 def _on_digit_clicked(self, widget):
626 self.set_number(self._phonenumber + widget.get_name()[-1])
628 self._errorDisplay.push_exception()
630 def _on_backspace(self, taps):
632 self.set_number(self._phonenumber[:-taps])
633 self._reset_back_button()
635 self._errorDisplay.push_exception()
637 def _on_clearall(self, taps):
640 self._reset_back_button()
642 self._errorDisplay.push_exception()
645 def _set_clear_button(self):
647 self._backButton.set_label("gtk-clear")
649 self._errorDisplay.push_exception()
651 def _reset_back_button(self):
653 self._backButton.set_label(self._originalLabel)
655 self._errorDisplay.push_exception()
658 class AccountInfo(object):
660 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
661 self._errorDisplay = errorDisplay
662 self._backend = backend
663 self._isPopulated = False
664 self._alarmHandler = alarmHandler
665 self._notifyOnMissed = False
666 self._notifyOnVoicemail = False
667 self._notifyOnSms = False
669 self._callbackList = []
670 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
671 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
672 self._onCallbackSelectChangedId = 0
674 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
675 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
676 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
677 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
678 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
679 self._onNotifyToggled = 0
680 self._onMinutesChanged = 0
681 self._onMissedToggled = 0
682 self._onVoicemailToggled = 0
683 self._onSmsToggled = 0
684 self._applyAlarmTimeoutId = None
686 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
687 self._defaultCallback = ""
690 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
692 self._accountViewNumberDisplay.set_use_markup(True)
693 self.set_account_number("")
695 del self._callbackList[:]
696 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
698 if self._alarmHandler is not None:
699 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
700 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
701 self._missedCheckbox.set_active(self._notifyOnMissed)
702 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
703 self._smsCheckbox.set_active(self._notifyOnSms)
705 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
706 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
707 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
708 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
709 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
711 self._notifyCheckbox.set_sensitive(False)
712 self._minutesEntryButton.set_sensitive(False)
713 self._missedCheckbox.set_sensitive(False)
714 self._voicemailCheckbox.set_sensitive(False)
715 self._smsCheckbox.set_sensitive(False)
717 self.update(force=True)
720 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
721 self._onCallbackSelectChangedId = 0
723 if self._alarmHandler is not None:
724 self._notifyCheckbox.disconnect(self._onNotifyToggled)
725 self._minutesEntryButton.disconnect(self._onMinutesChanged)
726 self._missedCheckbox.disconnect(self._onNotifyToggled)
727 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
728 self._smsCheckbox.disconnect(self._onNotifyToggled)
729 self._onNotifyToggled = 0
730 self._onMinutesChanged = 0
731 self._onMissedToggled = 0
732 self._onVoicemailToggled = 0
733 self._onSmsToggled = 0
735 self._notifyCheckbox.set_sensitive(True)
736 self._minutesEntryButton.set_sensitive(True)
737 self._missedCheckbox.set_sensitive(True)
738 self._voicemailCheckbox.set_sensitive(True)
739 self._smsCheckbox.set_sensitive(True)
742 del self._callbackList[:]
744 def get_selected_callback_number(self):
745 currentLabel = self._callbackSelectButton.get_label()
746 if currentLabel is not None:
747 return make_ugly(currentLabel)
751 def set_account_number(self, number):
753 Displays current account number
755 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
757 def update(self, force = False):
758 if not force and self._isPopulated:
760 self._populate_callback_combo()
761 self.set_account_number(self._backend.get_account_number())
765 self._set_callback_label("")
766 self.set_account_number("")
767 self._isPopulated = False
769 def save_everything(self):
770 raise NotImplementedError
774 return "Account Info"
776 def load_settings(self, config, section):
777 self._defaultCallback = config.get(section, "callback")
778 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
779 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
780 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
782 def save_settings(self, config, section):
784 @note Thread Agnostic
786 callback = self.get_selected_callback_number()
787 config.set(section, "callback", callback)
788 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
789 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
790 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
792 def _populate_callback_combo(self):
793 self._isPopulated = True
794 del self._callbackList[:]
796 callbackNumbers = self._backend.get_callback_numbers()
798 self._errorDisplay.push_exception()
799 self._isPopulated = False
802 if len(callbackNumbers) == 0:
803 callbackNumbers = {"": "No callback numbers available"}
805 for number, description in callbackNumbers.iteritems():
806 self._callbackList.append((make_pretty(number), description))
808 self._set_callback_number(self._defaultCallback)
810 def _set_callback_number(self, number):
812 if not self._backend.is_valid_syntax(number) and 0 < len(number):
813 self._errorDisplay.push_message("%s is not a valid callback number" % number)
814 elif number == self._backend.get_callback_number() and 0 < len(number):
815 _moduleLogger.warning(
816 "Callback number already is %s" % (
817 self._backend.get_callback_number(),
820 self._set_callback_label(number)
822 self._backend.set_callback_number(number)
823 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
824 make_pretty(number), make_pretty(self._backend.get_callback_number())
826 self._set_callback_label(number)
828 "Callback number set to %s" % (
829 self._backend.get_callback_number(),
833 self._errorDisplay.push_exception()
835 def _set_callback_label(self, uglyNumber):
836 prettyNumber = make_pretty(uglyNumber)
837 if len(prettyNumber) == 0:
838 prettyNumber = "No Callback Number"
839 self._callbackSelectButton.set_label(prettyNumber)
841 def _update_alarm_settings(self, recurrence):
843 isEnabled = self._notifyCheckbox.get_active()
844 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
845 self._alarmHandler.apply_settings(isEnabled, recurrence)
847 self.save_everything()
848 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
849 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
851 def _on_callbackentry_clicked(self, *args):
853 actualSelection = make_pretty(self.get_selected_callback_number())
856 (number, "%s (%s)" % (number, description))
857 for (number, description) in self._callbackList
859 defaultSelection = userOptions.get(actualSelection, actualSelection)
861 userSelection = hildonize.touch_selector_entry(
864 list(userOptions.itervalues()),
867 reversedUserOptions = dict(
868 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
870 selectedNumber = reversedUserOptions.get(userSelection, userSelection)
872 number = make_ugly(selectedNumber)
873 self._set_callback_number(number)
874 except RuntimeError, e:
875 _moduleLogger.exception("%s" % str(e))
877 self._errorDisplay.push_exception()
879 def _on_notify_toggled(self, *args):
881 if self._applyAlarmTimeoutId is not None:
882 gobject.source_remove(self._applyAlarmTimeoutId)
883 self._applyAlarmTimeoutId = None
884 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
886 self._errorDisplay.push_exception()
888 def _on_minutes_clicked(self, *args):
889 recurrenceChoices = [
905 actualSelection = self._alarmHandler.recurrence
907 closestSelectionIndex = 0
908 for i, possible in enumerate(recurrenceChoices):
909 if possible[0] <= actualSelection:
910 closestSelectionIndex = i
911 recurrenceIndex = hildonize.touch_selector(
914 (("%s" % m[1]) for m in recurrenceChoices),
915 closestSelectionIndex,
917 recurrence = recurrenceChoices[recurrenceIndex][0]
919 self._update_alarm_settings(recurrence)
920 except RuntimeError, e:
921 _moduleLogger.exception("%s" % str(e))
923 self._errorDisplay.push_exception()
925 def _on_apply_timeout(self, *args):
927 self._applyAlarmTimeoutId = None
929 self._update_alarm_settings(self._alarmHandler.recurrence)
931 self._errorDisplay.push_exception()
934 def _on_missed_toggled(self, *args):
936 self._notifyOnMissed = self._missedCheckbox.get_active()
937 self.save_everything()
939 self._errorDisplay.push_exception()
941 def _on_voicemail_toggled(self, *args):
943 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
944 self.save_everything()
946 self._errorDisplay.push_exception()
948 def _on_sms_toggled(self, *args):
950 self._notifyOnSms = self._smsCheckbox.get_active()
951 self.save_everything()
953 self._errorDisplay.push_exception()
956 class RecentCallsView(object):
964 def __init__(self, widgetTree, backend, errorDisplay):
965 self._errorDisplay = errorDisplay
966 self._backend = backend
968 self._isPopulated = False
969 self._recentmodel = gtk.ListStore(
970 gobject.TYPE_STRING, # number
971 gobject.TYPE_STRING, # date
972 gobject.TYPE_STRING, # action
973 gobject.TYPE_STRING, # from
974 gobject.TYPE_STRING, # from id
976 self._recentview = widgetTree.get_widget("recentview")
977 self._recentviewselection = None
978 self._onRecentviewRowActivatedId = 0
980 textrenderer = gtk.CellRendererText()
981 textrenderer.set_property("yalign", 0)
982 self._dateColumn = gtk.TreeViewColumn("Date")
983 self._dateColumn.pack_start(textrenderer, expand=True)
984 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
986 textrenderer = gtk.CellRendererText()
987 textrenderer.set_property("yalign", 0)
988 self._actionColumn = gtk.TreeViewColumn("Action")
989 self._actionColumn.pack_start(textrenderer, expand=True)
990 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
992 textrenderer = gtk.CellRendererText()
993 textrenderer.set_property("yalign", 0)
994 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
995 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
996 self._numberColumn = gtk.TreeViewColumn("Number")
997 self._numberColumn.pack_start(textrenderer, expand=True)
998 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
1000 textrenderer = gtk.CellRendererText()
1001 textrenderer.set_property("yalign", 0)
1002 hildonize.set_cell_thumb_selectable(textrenderer)
1003 self._nameColumn = gtk.TreeViewColumn("From")
1004 self._nameColumn.pack_start(textrenderer, expand=True)
1005 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
1006 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1008 self._window = gtk_toolbox.find_parent_window(self._recentview)
1009 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
1011 self._updateSink = gtk_toolbox.threaded_stage(
1013 self._idly_populate_recentview,
1014 gtk_toolbox.null_sink(),
1019 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1020 self._recentview.set_model(self._recentmodel)
1021 self._recentview.set_fixed_height_mode(False)
1023 self._recentview.append_column(self._dateColumn)
1024 self._recentview.append_column(self._actionColumn)
1025 self._recentview.append_column(self._numberColumn)
1026 self._recentview.append_column(self._nameColumn)
1027 self._recentviewselection = self._recentview.get_selection()
1028 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
1030 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
1033 self._recentview.disconnect(self._onRecentviewRowActivatedId)
1037 self._recentview.remove_column(self._dateColumn)
1038 self._recentview.remove_column(self._actionColumn)
1039 self._recentview.remove_column(self._nameColumn)
1040 self._recentview.remove_column(self._numberColumn)
1041 self._recentview.set_model(None)
1043 def number_selected(self, action, number, message):
1045 @note Actual dial function is patched in later
1047 raise NotImplementedError("Horrible unknown error has occurred")
1049 def update(self, force = False):
1050 if not force and self._isPopulated:
1052 self._updateSink.send(())
1056 self._isPopulated = False
1057 self._recentmodel.clear()
1061 return "Recent Calls"
1063 def load_settings(self, config, section):
1066 def save_settings(self, config, section):
1068 @note Thread Agnostic
1072 def _idly_populate_recentview(self):
1073 with gtk_toolbox.gtk_lock():
1074 banner = hildonize.show_busy_banner_start(self._window, "Loading Recent History")
1076 self._recentmodel.clear()
1077 self._isPopulated = True
1080 recentItems = self._backend.get_recent()
1081 except Exception, e:
1082 self._errorDisplay.push_exception_with_lock()
1083 self._isPopulated = False
1087 gv_backend.decorate_recent(data)
1088 for data in gv_backend.sort_messages(recentItems)
1091 for contactId, personName, phoneNumber, date, action in recentItems:
1093 personName = "Unknown"
1094 date = abbrev_relative_date(date)
1095 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1096 prettyNumber = make_pretty(prettyNumber)
1097 item = (prettyNumber, date, action.capitalize(), personName, contactId)
1098 with gtk_toolbox.gtk_lock():
1099 self._recentmodel.append(item)
1100 except Exception, e:
1101 self._errorDisplay.push_exception_with_lock()
1103 with gtk_toolbox.gtk_lock():
1104 hildonize.show_busy_banner_end(banner)
1108 def _on_recentview_row_activated(self, treeview, path, view_column):
1110 itr = self._recentmodel.get_iter(path)
1114 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1115 number = make_ugly(number)
1116 description = self._recentmodel.get_value(itr, self.FROM_IDX)
1117 contactId = self._recentmodel.get_value(itr, self.FROM_ID_IDX)
1119 contactPhoneNumbers = list(self._backend.get_contact_details(contactId))
1121 (number == make_ugly(contactNumber) or number[1:] == make_ugly(contactNumber))
1122 for (numberDescription, contactNumber) in contactPhoneNumbers
1125 defaultIndex = defaultMatches.index(True)
1127 contactPhoneNumbers.append(("Other", number))
1128 defaultIndex = len(contactPhoneNumbers)-1
1130 "Could not find contact %r's number %s among %r" % (
1131 contactId, number, contactPhoneNumbers
1135 contactPhoneNumbers = [("Phone", number)]
1138 action, phoneNumber, message = self._phoneTypeSelector.run(
1139 contactPhoneNumbers,
1140 messages = (description, ),
1141 parent = self._window,
1142 defaultIndex = defaultIndex,
1144 if action == SmsEntryDialog.ACTION_CANCEL:
1146 assert phoneNumber, "A lack of phone number exists"
1148 self.number_selected(action, phoneNumber, message)
1149 self._recentviewselection.unselect_all()
1150 except Exception, e:
1151 self._errorDisplay.push_exception()
1154 class MessagesView(object):
1163 def __init__(self, widgetTree, backend, errorDisplay):
1164 self._errorDisplay = errorDisplay
1165 self._backend = backend
1167 self._isPopulated = False
1168 self._messagemodel = gtk.ListStore(
1169 gobject.TYPE_STRING, # number
1170 gobject.TYPE_STRING, # date
1171 gobject.TYPE_STRING, # header
1172 gobject.TYPE_STRING, # message
1174 gobject.TYPE_STRING, # from id
1176 self._messageview = widgetTree.get_widget("messages_view")
1177 self._messageviewselection = None
1178 self._onMessageviewRowActivatedId = 0
1180 self._messageRenderer = gtk.CellRendererText()
1181 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1182 self._messageRenderer.set_property("wrap-width", 500)
1183 self._messageColumn = gtk.TreeViewColumn("Messages")
1184 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1185 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1186 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1188 self._window = gtk_toolbox.find_parent_window(self._messageview)
1189 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
1191 self._updateSink = gtk_toolbox.threaded_stage(
1193 self._idly_populate_messageview,
1194 gtk_toolbox.null_sink(),
1199 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1200 self._messageview.set_model(self._messagemodel)
1201 self._messageview.set_headers_visible(False)
1202 self._messageview.set_fixed_height_mode(False)
1204 self._messageview.append_column(self._messageColumn)
1205 self._messageviewselection = self._messageview.get_selection()
1206 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1208 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1211 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1215 self._messageview.remove_column(self._messageColumn)
1216 self._messageview.set_model(None)
1218 def number_selected(self, action, number, message):
1220 @note Actual dial function is patched in later
1222 raise NotImplementedError("Horrible unknown error has occurred")
1224 def update(self, force = False):
1225 if not force and self._isPopulated:
1227 self._updateSink.send(())
1231 self._isPopulated = False
1232 self._messagemodel.clear()
1238 def load_settings(self, config, section):
1241 def save_settings(self, config, section):
1243 @note Thread Agnostic
1247 _MIN_MESSAGES_SHOWN = 4
1249 def _idly_populate_messageview(self):
1250 with gtk_toolbox.gtk_lock():
1251 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1253 self._messagemodel.clear()
1254 self._isPopulated = True
1257 messageItems = self._backend.get_messages()
1258 except Exception, e:
1259 self._errorDisplay.push_exception_with_lock()
1260 self._isPopulated = False
1264 gv_backend.decorate_message(message)
1265 for message in gv_backend.sort_messages(messageItems)
1268 for contactId, header, number, relativeDate, messages in messageItems:
1269 prettyNumber = number[2:] if number.startswith("+1") else number
1270 prettyNumber = make_pretty(prettyNumber)
1272 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1273 expandedMessages = [firstMessage]
1274 expandedMessages.extend(messages)
1275 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1276 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1277 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1278 collapsedMessages = [firstMessage, secondMessage]
1279 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1281 collapsedMessages = expandedMessages
1282 #collapsedMessages = _collapse_message(collapsedMessages, 60, self._MIN_MESSAGES_SHOWN)
1284 number = make_ugly(number)
1286 row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId
1287 with gtk_toolbox.gtk_lock():
1288 self._messagemodel.append(row)
1289 except Exception, e:
1290 self._errorDisplay.push_exception_with_lock()
1292 with gtk_toolbox.gtk_lock():
1293 hildonize.show_busy_banner_end(banner)
1297 def _on_messageview_row_activated(self, treeview, path, view_column):
1299 itr = self._messagemodel.get_iter(path)
1303 number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX))
1304 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1306 contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX)
1308 contactPhoneNumbers = list(self._backend.get_contact_details(contactId))
1310 (number == make_ugly(contactNumber) or number[1:] == make_ugly(contactNumber))
1311 for (numberDescription, contactNumber) in contactPhoneNumbers
1314 defaultIndex = defaultMatches.index(True)
1316 contactPhoneNumbers.append(("Other", number))
1317 defaultIndex = len(contactPhoneNumbers)-1
1319 "Could not find contact %r's number %s among %r" % (
1320 contactId, number, contactPhoneNumbers
1324 contactPhoneNumbers = [("Phone", number)]
1327 action, phoneNumber, message = self._phoneTypeSelector.run(
1328 contactPhoneNumbers,
1329 messages = description,
1330 parent = self._window,
1331 defaultIndex = defaultIndex,
1333 if action == SmsEntryDialog.ACTION_CANCEL:
1335 assert phoneNumber, "A lock of phone number exists"
1337 self.number_selected(action, phoneNumber, message)
1338 self._messageviewselection.unselect_all()
1339 except Exception, e:
1340 self._errorDisplay.push_exception()
1343 class ContactsView(object):
1345 def __init__(self, widgetTree, backend, errorDisplay):
1346 self._errorDisplay = errorDisplay
1347 self._backend = backend
1349 self._addressBook = None
1350 self._selectedComboIndex = 0
1351 self._addressBookFactories = [null_backend.NullAddressBook()]
1353 self._booksList = []
1354 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1356 self._isPopulated = False
1357 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1358 self._contactsviewselection = None
1359 self._contactsview = widgetTree.get_widget("contactsview")
1361 self._contactColumn = gtk.TreeViewColumn("Contact")
1362 displayContactSource = False
1363 if displayContactSource:
1364 textrenderer = gtk.CellRendererText()
1365 self._contactColumn.pack_start(textrenderer, expand=False)
1366 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1367 textrenderer = gtk.CellRendererText()
1368 hildonize.set_cell_thumb_selectable(textrenderer)
1369 self._contactColumn.pack_start(textrenderer, expand=True)
1370 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1371 textrenderer = gtk.CellRendererText()
1372 self._contactColumn.pack_start(textrenderer, expand=True)
1373 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1374 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1375 self._contactColumn.set_sort_column_id(1)
1376 self._contactColumn.set_visible(True)
1378 self._onContactsviewRowActivatedId = 0
1379 self._onAddressbookButtonChangedId = 0
1380 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1381 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
1383 self._updateSink = gtk_toolbox.threaded_stage(
1385 self._idly_populate_contactsview,
1386 gtk_toolbox.null_sink(),
1391 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1393 self._contactsview.set_model(self._contactsmodel)
1394 self._contactsview.set_fixed_height_mode(False)
1395 self._contactsview.append_column(self._contactColumn)
1396 self._contactsviewselection = self._contactsview.get_selection()
1397 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1399 del self._booksList[:]
1400 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1401 if factoryName and bookName:
1402 entryName = "%s: %s" % (factoryName, bookName)
1404 entryName = factoryName
1406 entryName = bookName
1408 entryName = "Bad name (%d)" % factoryId
1409 row = (str(factoryId), bookId, entryName)
1410 self._booksList.append(row)
1412 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1413 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1415 if len(self._booksList) <= self._selectedComboIndex:
1416 self._selectedComboIndex = 0
1417 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1419 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1420 selectedBookId = self._booksList[self._selectedComboIndex][1]
1421 self.open_addressbook(selectedFactoryId, selectedBookId)
1424 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1425 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1429 self._bookSelectionButton.set_label("")
1430 self._contactsview.set_model(None)
1431 self._contactsview.remove_column(self._contactColumn)
1433 def number_selected(self, action, number, message):
1435 @note Actual dial function is patched in later
1437 raise NotImplementedError("Horrible unknown error has occurred")
1439 def get_addressbooks(self):
1441 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1443 for i, factory in enumerate(self._addressBookFactories):
1444 for bookFactory, bookId, bookName in factory.get_addressbooks():
1445 yield (str(i), bookId), (factory.factory_name(), bookName)
1447 def open_addressbook(self, bookFactoryId, bookId):
1448 bookFactoryIndex = int(bookFactoryId)
1449 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1450 self._addressBook = addressBook
1452 def update(self, force = False):
1453 if not force and self._isPopulated:
1455 self._updateSink.send(())
1459 self._isPopulated = False
1460 self._contactsmodel.clear()
1461 for factory in self._addressBookFactories:
1462 factory.clear_caches()
1463 self._addressBook.clear_caches()
1465 def append(self, book):
1466 self._addressBookFactories.append(book)
1468 def extend(self, books):
1469 self._addressBookFactories.extend(books)
1475 def load_settings(self, config, sectionName):
1477 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1478 except ConfigParser.NoOptionError:
1479 self._selectedComboIndex = 0
1481 def save_settings(self, config, sectionName):
1482 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1484 def _idly_populate_contactsview(self):
1485 with gtk_toolbox.gtk_lock():
1486 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1489 while addressBook is not self._addressBook:
1490 addressBook = self._addressBook
1491 with gtk_toolbox.gtk_lock():
1492 self._contactsview.set_model(None)
1496 contacts = addressBook.get_contacts()
1497 except Exception, e:
1499 self._isPopulated = False
1500 self._errorDisplay.push_exception_with_lock()
1501 for contactId, contactName in contacts:
1502 contactType = (addressBook.contact_source_short_name(contactId), )
1503 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1505 with gtk_toolbox.gtk_lock():
1506 self._contactsview.set_model(self._contactsmodel)
1508 self._isPopulated = True
1509 except Exception, e:
1510 self._errorDisplay.push_exception_with_lock()
1512 with gtk_toolbox.gtk_lock():
1513 hildonize.show_busy_banner_end(banner)
1516 def _on_addressbook_button_changed(self, *args, **kwds):
1519 newSelectedComboIndex = hildonize.touch_selector(
1522 (("%s" % m[2]) for m in self._booksList),
1523 self._selectedComboIndex,
1525 except RuntimeError:
1528 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1529 selectedBookId = self._booksList[newSelectedComboIndex][1]
1531 oldAddressbook = self._addressBook
1532 self.open_addressbook(selectedFactoryId, selectedBookId)
1533 forceUpdate = True if oldAddressbook is not self._addressBook else False
1534 self.update(force=forceUpdate)
1536 self._selectedComboIndex = newSelectedComboIndex
1537 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1538 except Exception, e:
1539 self._errorDisplay.push_exception()
1541 def _on_contactsview_row_activated(self, treeview, path, view_column):
1543 itr = self._contactsmodel.get_iter(path)
1547 contactId = self._contactsmodel.get_value(itr, 3)
1548 contactName = self._contactsmodel.get_value(itr, 1)
1550 contactDetails = self._addressBook.get_contact_details(contactId)
1551 except Exception, e:
1553 self._errorDisplay.push_exception()
1554 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1556 if len(contactPhoneNumbers) == 0:
1559 action, phoneNumber, message = self._phoneTypeSelector.run(
1560 contactPhoneNumbers,
1561 messages = (contactName, ),
1562 parent = self._window,
1564 if action == SmsEntryDialog.ACTION_CANCEL:
1566 assert phoneNumber, "A lack of phone number exists"
1568 self.number_selected(action, phoneNumber, message)
1569 self._contactsviewselection.unselect_all()
1570 except Exception, e:
1571 self._errorDisplay.push_exception()