4 DialCentral - Front end for Google's Grand Central 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 Look into a messages view
22 @li https://www.google.com/voice/inbox/recent/voicemail/
23 @li https://www.google.com/voice/inbox/recent/sms/
24 Would need to either use both json and html or just html
27 from __future__ import with_statement
40 def make_ugly(prettynumber):
42 function to take a phone number and strip out all non-numeric
45 >>> make_ugly("+012-(345)-678-90")
49 uglynumber = re.sub('\D', '', prettynumber)
53 def make_pretty(phonenumber):
55 Function to take a phone number and return the pretty version
57 if phonenumber begins with 0:
59 if phonenumber begins with 1: ( for gizmo callback numbers )
61 if phonenumber is 13 digits:
63 if phonenumber is 10 digits:
67 >>> make_pretty("1234567")
69 >>> make_pretty("2345678901")
71 >>> make_pretty("12345678901")
73 >>> make_pretty("01234567890")
76 if phonenumber is None or phonenumber is "":
79 phonenumber = make_ugly(phonenumber)
81 if len(phonenumber) < 3:
84 if phonenumber[0] == "0":
86 prettynumber += "+%s" % phonenumber[0:3]
87 if 3 < len(phonenumber):
88 prettynumber += "-(%s)" % phonenumber[3:6]
89 if 6 < len(phonenumber):
90 prettynumber += "-%s" % phonenumber[6:9]
91 if 9 < len(phonenumber):
92 prettynumber += "-%s" % phonenumber[9:]
94 elif len(phonenumber) <= 7:
95 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
96 elif len(phonenumber) > 8 and phonenumber[0] == "1":
97 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
98 elif len(phonenumber) > 7:
99 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
103 def make_idler(func):
105 Decorator that makes a generator-function into a function that will continue execution on next call
109 def decorated_func(*args, **kwds):
111 a.append(func(*args, **kwds))
115 except StopIteration:
119 decorated_func.__name__ = func.__name__
120 decorated_func.__doc__ = func.__doc__
121 decorated_func.__dict__.update(func.__dict__)
123 return decorated_func
126 class DummyAddressBook(object):
128 Minimal example of both an addressbook factory and an addressbook
131 def clear_caches(self):
134 def get_addressbooks(self):
136 @returns Iterable of (Address Book Factory, Book Id, Book Name)
138 yield self, "", "None"
140 def open_addressbook(self, bookId):
144 def contact_source_short_name(contactId):
154 @returns Iterable of (contact id, contact name)
159 def get_contact_details(contactId):
161 @returns Iterable of (Phone Type, Phone Number)
166 class MergedAddressBook(object):
168 Merger of all addressbooks
171 def __init__(self, addressbookFactories, sorter = None):
172 self.__addressbookFactories = addressbookFactories
173 self.__addressbooks = None
174 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
176 def clear_caches(self):
177 self.__addressbooks = None
178 for factory in self.__addressbookFactories:
179 factory.clear_caches()
181 def get_addressbooks(self):
183 @returns Iterable of (Address Book Factory, Book Id, Book Name)
187 def open_addressbook(self, bookId):
190 def contact_source_short_name(self, contactId):
191 if self.__addressbooks is None:
193 bookIndex, originalId = contactId.split("-", 1)
194 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
198 return "All Contacts"
200 def get_contacts(self):
202 @returns Iterable of (contact id, contact name)
204 if self.__addressbooks is None:
205 self.__addressbooks = list(
206 factory.open_addressbook(id)
207 for factory in self.__addressbookFactories
208 for (f, id, name) in factory.get_addressbooks()
211 ("-".join([str(bookIndex), contactId]), contactName)
212 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
213 for (contactId, contactName) in addressbook.get_contacts()
215 sortedContacts = self.__sort_contacts(contacts)
216 return sortedContacts
218 def get_contact_details(self, contactId):
220 @returns Iterable of (Phone Type, Phone Number)
222 if self.__addressbooks is None:
224 bookIndex, originalId = contactId.split("-", 1)
225 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
228 def null_sorter(contacts):
230 Good for speed/low memory
235 def basic_firtname_sorter(contacts):
237 Expects names in "First Last" format
240 (contactName.rsplit(" ", 1)[0], (contactId, contactName))
241 for (contactId, contactName) in contacts
243 contactsWithKey.sort()
244 return (contactData for (lastName, contactData) in contactsWithKey)
247 def basic_lastname_sorter(contacts):
249 Expects names in "First Last" format
252 (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
253 for (contactId, contactName) in contacts
255 contactsWithKey.sort()
256 return (contactData for (lastName, contactData) in contactsWithKey)
259 def reversed_firtname_sorter(contacts):
261 Expects names in "Last, First" format
264 (contactName.split(", ", 1)[-1], (contactId, contactName))
265 for (contactId, contactName) in contacts
267 contactsWithKey.sort()
268 return (contactData for (lastName, contactData) in contactsWithKey)
271 def reversed_lastname_sorter(contacts):
273 Expects names in "Last, First" format
276 (contactName.split(", ", 1)[0], (contactId, contactName))
277 for (contactId, contactName) in contacts
279 contactsWithKey.sort()
280 return (contactData for (lastName, contactData) in contactsWithKey)
283 def guess_firstname(name):
285 return name.split(", ", 1)[-1]
287 return name.rsplit(" ", 1)[0]
290 def guess_lastname(name):
292 return name.split(", ", 1)[0]
294 return name.rsplit(" ", 1)[-1]
297 def advanced_firstname_sorter(cls, contacts):
299 (cls.guess_firstname(contactName), (contactId, contactName))
300 for (contactId, contactName) in contacts
302 contactsWithKey.sort()
303 return (contactData for (lastName, contactData) in contactsWithKey)
306 def advanced_lastname_sorter(cls, contacts):
308 (cls.guess_lastname(contactName), (contactId, contactName))
309 for (contactId, contactName) in contacts
311 contactsWithKey.sort()
312 return (contactData for (lastName, contactData) in contactsWithKey)
315 class PhoneTypeSelector(object):
317 def __init__(self, widgetTree, gcBackend):
318 self._gcBackend = gcBackend
319 self._widgetTree = widgetTree
320 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
322 self._dialButton = self._widgetTree.get_widget("dial_button")
323 self._dialButton.connect("clicked", self._on_phonetype_dial)
325 self._selectButton = self._widgetTree.get_widget("select_button")
326 self._selectButton.connect("clicked", self._on_phonetype_select)
328 self._cancelButton = self._widgetTree.get_widget("cancel_button")
329 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
331 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
332 self._typeviewselection = None
334 self._message = self._widgetTree.get_widget("phoneSelectionMessage")
335 typeview = self._widgetTree.get_widget("phonetypes")
336 typeview.connect("row-activated", self._on_phonetype_select)
337 typeview.set_model(self._typemodel)
338 textrenderer = gtk.CellRendererText()
340 # Add the column to the treeview
341 column = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=1)
342 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
344 typeview.append_column(column)
346 self._typeviewselection = typeview.get_selection()
347 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
349 def run(self, contactDetails, message = ""):
350 self._typemodel.clear()
352 for phoneType, phoneNumber in contactDetails:
353 self._typemodel.append((phoneNumber, "%s - %s" % (make_pretty(phoneNumber), phoneType)))
357 self._message.set_text(message)
361 userResponse = self._dialog.run()
363 if userResponse == gtk.RESPONSE_OK:
364 phoneNumber = self._get_number()
368 self._typeviewselection.unselect_all()
372 def _get_number(self):
373 model, itr = self._typeviewselection.get_selected()
376 phoneNumber = self._typemodel.get_value(itr, 0)
379 def _on_phonetype_dial(self, *args):
380 self.dial(self._get_number())
381 self._dialog.response(gtk.RESPONSE_CANCEL)
383 def _on_phonetype_select(self, *args):
384 self._dialog.response(gtk.RESPONSE_OK)
386 def _on_phonetype_cancel(self, *args):
387 self._dialog.response(gtk.RESPONSE_CANCEL)
390 class Dialpad(object):
392 def __init__(self, widgetTree, errorDisplay):
393 self._errorDisplay = errorDisplay
394 self._numberdisplay = widgetTree.get_widget("numberdisplay")
395 self._dialButton = widgetTree.get_widget("dial")
396 self._phonenumber = ""
397 self._prettynumber = ""
398 self._clearall_id = None
401 "on_dial_clicked": self._on_dial_clicked,
402 "on_digit_clicked": self._on_digit_clicked,
403 "on_clear_number": self._on_clear_number,
404 "on_back_clicked": self._on_backspace,
405 "on_back_pressed": self._on_back_pressed,
406 "on_back_released": self._on_back_released,
408 widgetTree.signal_autoconnect(callbackMapping)
411 self._dialButton.grab_focus()
416 def dial(self, number):
418 @note Actual dial function is patched in later
420 raise NotImplementedError
422 def get_number(self):
423 return self._phonenumber
425 def set_number(self, number):
427 Set the callback phonenumber
430 self._phonenumber = make_ugly(number)
431 self._prettynumber = make_pretty(self._phonenumber)
432 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
434 self._errorDisplay.push_exception(e)
439 def _on_dial_clicked(self, widget):
440 self.dial(self.get_number())
442 def _on_clear_number(self, *args):
445 def _on_digit_clicked(self, widget):
446 self.set_number(self._phonenumber + widget.get_name()[-1])
448 def _on_backspace(self, widget):
449 self.set_number(self._phonenumber[:-1])
451 def _on_clearall(self):
455 def _on_back_pressed(self, widget):
456 self._clearall_id = gobject.timeout_add(1000, self._on_clearall)
458 def _on_back_released(self, widget):
459 if self._clearall_id is not None:
460 gobject.source_remove(self._clearall_id)
461 self._clearall_id = None
464 class AccountInfo(object):
466 def __init__(self, widgetTree, backend, errorDisplay):
467 self._errorDisplay = errorDisplay
468 self._backend = backend
470 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
471 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
472 self._callbackCombo = widgetTree.get_widget("callbackcombo")
473 self._onCallbackentryChangedId = 0
476 assert self._backend.is_authed()
477 self._accountViewNumberDisplay.set_use_markup(True)
478 self.set_account_number("")
479 self._callbackList.clear()
481 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
484 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
486 self._callbackList.clear()
488 def get_selected_callback_number(self):
489 return make_ugly(self._callbackCombo.get_child().get_text())
491 def set_account_number(self, number):
493 Displays current account number
495 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
498 self.populate_callback_combo()
499 self.set_account_number(self._backend.get_account_number())
502 self._callbackCombo.get_child().set_text("")
503 self.set_account_number("")
505 def populate_callback_combo(self):
506 self._callbackList.clear()
508 callbackNumbers = self._backend.get_callback_numbers()
509 except RuntimeError, e:
510 self._errorDisplay.push_exception(e)
513 for number, description in callbackNumbers.iteritems():
514 self._callbackList.append((make_pretty(number),))
516 self._callbackCombo.set_model(self._callbackList)
517 self._callbackCombo.set_text_column(0)
519 callbackNumber = self._backend.get_callback_number()
520 except RuntimeError, e:
521 self._errorDisplay.push_exception(e)
523 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
525 def _on_callbackentry_changed(self, *args):
527 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
530 text = self.get_selected_callback_number()
531 if not self._backend.is_valid_syntax(text):
532 self._errorDisplay.push_message("%s is not a valid callback number" % text)
533 elif text == self._backend.get_callback_number():
534 warnings.warn("Callback number already is %s" % self._backend.get_callback_number(), UserWarning, 2)
536 self._backend.set_callback_number(text)
537 except RuntimeError, e:
538 self._errorDisplay.push_exception(e)
541 class RecentCallsView(object):
543 def __init__(self, widgetTree, backend, errorDisplay):
544 self._errorDisplay = errorDisplay
545 self._backend = backend
547 self._recenttime = 0.0
548 self._recentmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
549 self._recentview = widgetTree.get_widget("recentview")
550 self._recentviewselection = None
551 self._onRecentviewRowActivatedId = 0
553 textrenderer = gtk.CellRendererText()
554 self._recentviewColumn = gtk.TreeViewColumn("Calls")
555 self._recentviewColumn.pack_start(textrenderer, expand=True)
556 self._recentviewColumn.add_attribute(textrenderer, "text", 1)
557 self._recentviewColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
559 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
562 assert self._backend.is_authed()
563 self._recentview.set_model(self._recentmodel)
565 self._recentview.append_column(self._recentviewColumn)
566 self._recentviewselection = self._recentview.get_selection()
567 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
569 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
572 self._recentview.disconnect(self._onRecentviewRowActivatedId)
573 self._recentview.remove_column(self._recentviewColumn)
574 self._recentview.set_model(None)
576 def number_selected(self, number):
578 @note Actual dial function is patched in later
580 raise NotImplementedError
583 if (time.time() - self._recenttime) < 300:
585 backgroundPopulate = threading.Thread(target=self._idly_populate_recentview)
586 backgroundPopulate.setDaemon(True)
587 backgroundPopulate.start()
590 self._recenttime = 0.0
591 self._recentmodel.clear()
593 def _idly_populate_recentview(self):
594 self._recenttime = time.time()
595 self._recentmodel.clear()
598 recentItems = self._backend.get_recent()
599 except RuntimeError, e:
600 self._errorDisplay.push_exception_with_lock(e)
601 self._recenttime = 0.0
604 for personsName, phoneNumber, date, action in recentItems:
605 description = "%s on %s from/to %s - %s" % (action.capitalize(), date, personsName, phoneNumber)
606 item = (phoneNumber, description)
607 with gtk_toolbox.gtk_lock():
608 self._recentmodel.append(item)
612 def _on_recentview_row_activated(self, treeview, path, view_column):
613 model, itr = self._recentviewselection.get_selected()
617 contactPhoneNumbers = [("Phone", self._recentmodel.get_value(itr, 0))]
618 description = self._recentmodel.get_value(itr, 1)
619 print repr(contactPhoneNumbers), repr(description)
621 phoneNumber = self._phoneTypeSelector.run(contactPhoneNumbers, message = description)
622 if 0 == len(phoneNumber):
625 self.number_selected(phoneNumber)
626 self._recentviewselection.unselect_all()
629 class MessagesView(object):
631 def __init__(self, widgetTree, backend, errorDisplay):
632 self._errorDisplay = errorDisplay
633 self._backend = backend
635 self._messagetime = 0.0
636 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
637 self._messageview = widgetTree.get_widget("messages_view")
638 self._messageviewselection = None
639 self._onRcentviewRowActivatedId = 0
641 textrenderer = gtk.CellRendererText()
642 self._messageviewColumn = gtk.TreeViewColumn("Messages")
643 self._messageviewColumn.pack_start(textrenderer, expand=True)
644 self._messageviewColumn.add_attribute(textrenderer, "text", 1)
645 self._messageviewColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
647 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
650 assert self._backend.is_authed()
651 self._messageview.set_model(self._messagemodel)
653 self._messageview.append_column(self._messageviewColumn)
654 self._messageviewselection = self._messageview.get_selection()
655 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
657 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
660 self._messageview.disconnect(self._onMessageviewRowActivatedId)
661 self._messageview.remove_column(self._messageviewColumn)
662 self._messageview.set_model(None)
664 def number_selected(self, number):
666 @note Actual dial function is patched in later
668 raise NotImplementedError
671 if (time.time() - self._messagetime) < 300:
673 backgroundPopulate = threading.Thread(target=self._idly_populate_messageview)
674 backgroundPopulate.setDaemon(True)
675 backgroundPopulate.start()
678 self._messagetime = 0.0
679 self._messagemodel.clear()
681 def _idly_populate_messageview(self):
682 self._messagetime = time.time()
683 self._messagemodel.clear()
686 messageItems = self._backend.get_messages()
687 except RuntimeError, e:
688 self._errorDisplay.push_exception_with_lock(e)
689 self._messagetime = 0.0
692 for phoneNumber, date in messageItems:
693 item = (phoneNumber, data)
694 with gtk_toolbox.gtk_lock():
695 self._messagemodel.append(item)
699 def _on_messageview_row_activated(self, treeview, path, view_column):
700 model, itr = self._messageviewselection.get_selected()
704 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, 0))]
705 description = self._messagemodel.get_value(itr, 1)
706 print repr(contactPhoneNumbers), repr(description)
708 phoneNumber = self._phoneTypeSelector.run(contactPhoneNumbers, message = description)
709 if 0 == len(phoneNumber):
712 self.number_selected(phoneNumber)
713 self._messageviewselection.unselect_all()
716 class ContactsView(object):
718 def __init__(self, widgetTree, backend, errorDisplay):
719 self._errorDisplay = errorDisplay
720 self._backend = backend
722 self._addressBook = None
723 self._addressBookFactories = [DummyAddressBook()]
725 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
726 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
728 self._contactstime = 0.0
729 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
730 self._contactsviewselection = None
731 self._contactsview = widgetTree.get_widget("contactsview")
733 self._contactColumn = gtk.TreeViewColumn("Contact")
734 displayContactSource = False
735 if displayContactSource:
736 textrenderer = gtk.CellRendererText()
737 self._contactColumn.pack_start(textrenderer, expand=False)
738 self._contactColumn.add_attribute(textrenderer, 'text', 0)
739 textrenderer = gtk.CellRendererText()
740 self._contactColumn.pack_start(textrenderer, expand=True)
741 self._contactColumn.add_attribute(textrenderer, 'text', 1)
742 textrenderer = gtk.CellRendererText()
743 self._contactColumn.pack_start(textrenderer, expand=True)
744 self._contactColumn.add_attribute(textrenderer, 'text', 4)
745 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
746 self._contactColumn.set_sort_column_id(1)
747 self._contactColumn.set_visible(True)
749 self._onContactsviewRowActivatedId = 0
750 self._onAddressbookComboChangedId = 0
751 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
754 assert self._backend.is_authed()
756 self._contactsview.set_model(self._contactsmodel)
757 self._contactsview.append_column(self._contactColumn)
758 self._contactsviewselection = self._contactsview.get_selection()
759 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
761 self._booksList.clear()
762 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
763 if factoryName and bookName:
764 entryName = "%s: %s" % (factoryName, bookName)
766 entryName = factoryName
770 entryName = "Bad name (%d)" % factoryId
771 row = (str(factoryId), bookId, entryName)
772 self._booksList.append(row)
774 self._booksSelectionBox.set_model(self._booksList)
775 cell = gtk.CellRendererText()
776 self._booksSelectionBox.pack_start(cell, True)
777 self._booksSelectionBox.add_attribute(cell, 'text', 2)
778 self._booksSelectionBox.set_active(0)
780 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
781 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
784 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
785 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
787 self._booksSelectionBox.clear()
788 self._booksSelectionBox.set_model(None)
789 self._contactsview.set_model(None)
790 self._contactsview.remove_column(self._contactColumn)
792 def number_selected(self, number):
794 @note Actual dial function is patched in later
796 raise NotImplementedError
798 def get_addressbooks(self):
800 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
802 for i, factory in enumerate(self._addressBookFactories):
803 for bookFactory, bookId, bookName in factory.get_addressbooks():
804 yield (i, bookId), (factory.factory_name(), bookName)
806 def open_addressbook(self, bookFactoryId, bookId):
807 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
808 self._contactstime = 0
809 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
810 backgroundPopulate.setDaemon(True)
811 backgroundPopulate.start()
814 if (time.time() - self._contactstime) < 300:
816 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
817 backgroundPopulate.setDaemon(True)
818 backgroundPopulate.start()
821 self._contactstime = 0.0
822 self._contactsmodel.clear()
824 def clear_caches(self):
825 for factory in self._addressBookFactories:
826 factory.clear_caches()
827 self._addressBook.clear_caches()
829 def append(self, book):
830 self._addressBookFactories.append(book)
832 def extend(self, books):
833 self._addressBookFactories.extend(books)
835 def _idly_populate_contactsview(self):
836 #@todo Add a lock so only one code path can be in here at a time
839 # completely disable updating the treeview while we populate the data
840 self._contactsview.freeze_child_notify()
841 self._contactsview.set_model(None)
843 addressBook = self._addressBook
845 contacts = addressBook.get_contacts()
846 except RuntimeError, e:
848 self._contactstime = 0.0
849 self._errorDisplay.push_exception_with_lock(e)
850 for contactId, contactName in contacts:
851 contactType = (addressBook.contact_source_short_name(contactId), )
852 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
854 # restart the treeview data rendering
855 self._contactsview.set_model(self._contactsmodel)
856 self._contactsview.thaw_child_notify()
859 def _on_addressbook_combo_changed(self, *args, **kwds):
860 itr = self._booksSelectionBox.get_active_iter()
863 factoryId = int(self._booksList.get_value(itr, 0))
864 bookId = self._booksList.get_value(itr, 1)
865 self.open_addressbook(factoryId, bookId)
867 def _on_contactsview_row_activated(self, treeview, path, view_column):
868 model, itr = self._contactsviewselection.get_selected()
872 contactId = self._contactsmodel.get_value(itr, 3)
873 contactName = self._contactsmodel.get_value(itr, 1)
875 contactDetails = self._addressBook.get_contact_details(contactId)
876 except RuntimeError, e:
878 self._contactstime = 0.0
879 self._errorDisplay.push_exception(e)
880 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
882 if len(contactPhoneNumbers) == 0:
884 elif len(contactPhoneNumbers) == 1:
885 phoneNumber = self._phoneTypeSelector.run(contactPhoneNumbers, message = contactName)
887 if 0 == len(phoneNumber):
890 self.number_selected(phoneNumber)
891 self._contactsviewselection.unselect_all()