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()
377 phoneNumber = self._typemodel.get_value(itr, 0)
380 def _on_phonetype_dial(self, *args):
381 self._gcBackend.dial(self._get_number())
382 self._dialog.response(gtk.RESPONSE_CANCEL)
384 def _on_phonetype_select(self, *args):
385 self._dialog.response(gtk.RESPONSE_OK)
387 def _on_phonetype_cancel(self, *args):
388 self._dialog.response(gtk.RESPONSE_CANCEL)
391 class Dialpad(object):
393 def __init__(self, widgetTree, errorDisplay):
394 self._errorDisplay = errorDisplay
395 self._numberdisplay = widgetTree.get_widget("numberdisplay")
396 self._dialButton = widgetTree.get_widget("dial")
397 self._phonenumber = ""
398 self._prettynumber = ""
399 self._clearall_id = None
402 "on_dial_clicked": self._on_dial_clicked,
403 "on_digit_clicked": self._on_digit_clicked,
404 "on_clear_number": self._on_clear_number,
405 "on_back_clicked": self._on_backspace,
406 "on_back_pressed": self._on_back_pressed,
407 "on_back_released": self._on_back_released,
409 widgetTree.signal_autoconnect(callbackMapping)
412 self._dialButton.grab_focus()
417 def dial(self, number):
419 @note Actual dial function is patched in later
421 raise NotImplementedError
423 def get_number(self):
424 return self._phonenumber
426 def set_number(self, number):
428 Set the callback phonenumber
431 self._phonenumber = make_ugly(number)
432 self._prettynumber = make_pretty(self._phonenumber)
433 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
435 self._errorDisplay.push_exception(e)
440 def _on_dial_clicked(self, widget):
441 self.dial(self.get_number())
443 def _on_clear_number(self, *args):
446 def _on_digit_clicked(self, widget):
447 self.set_number(self._phonenumber + widget.get_name()[-1])
449 def _on_backspace(self, widget):
450 self.set_number(self._phonenumber[:-1])
452 def _on_clearall(self):
456 def _on_back_pressed(self, widget):
457 self._clearall_id = gobject.timeout_add(1000, self._on_clearall)
459 def _on_back_released(self, widget):
460 if self._clearall_id is not None:
461 gobject.source_remove(self._clearall_id)
462 self._clearall_id = None
465 class AccountInfo(object):
467 def __init__(self, widgetTree, backend, errorDisplay):
468 self._errorDisplay = errorDisplay
469 self._backend = backend
471 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
472 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
473 self._callbackCombo = widgetTree.get_widget("callbackcombo")
474 self._onCallbackentryChangedId = 0
477 assert self._backend.is_authed()
478 self._accountViewNumberDisplay.set_use_markup(True)
479 self.set_account_number("")
480 self._callbackList.clear()
482 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
485 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
487 self._callbackList.clear()
489 def get_selected_callback_number(self):
490 return make_ugly(self._callbackCombo.get_child().get_text())
492 def set_account_number(self, number):
494 Displays current account number
496 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
499 self.populate_callback_combo()
500 self.set_account_number(self._backend.get_account_number())
503 self._callbackCombo.get_child().set_text("")
504 self.set_account_number("")
506 def populate_callback_combo(self):
507 self._callbackList.clear()
509 callbackNumbers = self._backend.get_callback_numbers()
510 except RuntimeError, e:
511 self._errorDisplay.push_exception(e)
514 for number, description in callbackNumbers.iteritems():
515 self._callbackList.append((make_pretty(number),))
517 self._callbackCombo.set_model(self._callbackList)
518 self._callbackCombo.set_text_column(0)
520 callbackNumber = self._backend.get_callback_number()
521 except RuntimeError, e:
522 self._errorDisplay.push_exception(e)
524 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
526 def _on_callbackentry_changed(self, *args):
528 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
531 text = self.get_selected_callback_number()
532 if not self._backend.is_valid_syntax(text):
533 self._errorDisplay.push_message("%s is not a valid callback number" % text)
534 elif text == self._backend.get_callback_number():
535 warnings.warn("Callback number already is %s" % self._backend.get_callback_number(), UserWarning, 2)
537 self._backend.set_callback_number(text)
538 except RuntimeError, e:
539 self._errorDisplay.push_exception(e)
542 class RecentCallsView(object):
544 def __init__(self, widgetTree, backend, errorDisplay):
545 self._errorDisplay = errorDisplay
546 self._backend = backend
548 self._recenttime = 0.0
549 self._recentmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
550 self._recentview = widgetTree.get_widget("recentview")
551 self._recentviewselection = None
552 self._onRecentviewRowActivatedId = 0
554 textrenderer = gtk.CellRendererText()
555 self._recentviewColumn = gtk.TreeViewColumn("Calls")
556 self._recentviewColumn.pack_start(textrenderer, expand=True)
557 self._recentviewColumn.add_attribute(textrenderer, "text", 1)
558 self._recentviewColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
560 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
563 assert self._backend.is_authed()
564 self._recentview.set_model(self._recentmodel)
566 self._recentview.append_column(self._recentviewColumn)
567 self._recentviewselection = self._recentview.get_selection()
568 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
570 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
573 self._recentview.disconnect(self._onRecentviewRowActivatedId)
574 self._recentview.remove_column(self._recentviewColumn)
575 self._recentview.set_model(None)
577 def number_selected(self, number):
579 @note Actual dial function is patched in later
581 raise NotImplementedError
584 if (time.time() - self._recenttime) < 300:
586 backgroundPopulate = threading.Thread(target=self._idly_populate_recentview)
587 backgroundPopulate.setDaemon(True)
588 backgroundPopulate.start()
591 self._recenttime = 0.0
592 self._recentmodel.clear()
594 def _idly_populate_recentview(self):
595 self._recenttime = time.time()
596 self._recentmodel.clear()
599 recentItems = self._backend.get_recent()
600 except RuntimeError, e:
601 self._errorDisplay.push_exception_with_lock(e)
602 self._recenttime = 0.0
606 for personsName, phoneNumber, date, action in recentItems:
607 description = "%s on %s from/to %s - %s" % (action.capitalize(), date, personsName, phoneNumber)
608 item = (phoneNumber, description)
609 with gtk_toolbox.gtk_lock():
610 self._recentmodel.append(item)
614 def _on_recentview_row_activated(self, treeview, path, view_column):
615 model, itr = self._recentviewselection.get_selected()
619 contactPhoneNumbers = [("Phone", self._recentmodel.get_value(itr, 0))]
620 description = self._recentmodel.get_value(itr, 1)
621 print repr(contactPhoneNumbers), repr(description)
623 phoneNumber = self._phoneTypeSelector.run(contactPhoneNumbers, message = description)
624 if 0 == len(phoneNumber):
627 self.number_selected(phoneNumber)
628 self._recentviewselection.unselect_all()
631 class MessagesView(object):
633 def __init__(self, widgetTree, backend, errorDisplay):
634 self._errorDisplay = errorDisplay
635 self._backend = backend
637 self._messagetime = 0.0
638 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
639 self._messageview = widgetTree.get_widget("messages_view")
640 self._messageviewselection = None
641 self._onRcentviewRowActivatedId = 0
643 textrenderer = gtk.CellRendererText()
644 self._messageviewColumn = gtk.TreeViewColumn("Messages")
645 self._messageviewColumn.pack_start(textrenderer, expand=True)
646 self._messageviewColumn.add_attribute(textrenderer, "text", 1)
647 self._messageviewColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
649 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
652 assert self._backend.is_authed()
653 self._messageview.set_model(self._messagemodel)
655 self._messageview.append_column(self._messageviewColumn)
656 self._messageviewselection = self._messageview.get_selection()
657 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
659 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
662 self._messageview.disconnect(self._onMessageviewRowActivatedId)
663 self._messageview.remove_column(self._messageviewColumn)
664 self._messageview.set_model(None)
666 def number_selected(self, number):
668 @note Actual dial function is patched in later
670 raise NotImplementedError
673 if (time.time() - self._messagetime) < 300:
675 backgroundPopulate = threading.Thread(target=self._idly_populate_messageview)
676 backgroundPopulate.setDaemon(True)
677 backgroundPopulate.start()
680 self._messagetime = 0.0
681 self._messagemodel.clear()
683 def _idly_populate_messageview(self):
684 self._messagetime = time.time()
685 self._messagemodel.clear()
688 messageItems = self._backend.get_messages()
689 except RuntimeError, e:
690 self._errorDisplay.push_exception_with_lock(e)
691 self._messagetime = 0.0
694 for phoneNumber, data in messageItems:
695 item = (phoneNumber, data)
696 with gtk_toolbox.gtk_lock():
697 self._messagemodel.append(item)
701 def _on_messageview_row_activated(self, treeview, path, view_column):
702 model, itr = self._messageviewselection.get_selected()
706 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, 0))]
707 description = self._messagemodel.get_value(itr, 1)
708 print repr(contactPhoneNumbers), repr(description)
710 phoneNumber = self._phoneTypeSelector.run(contactPhoneNumbers, message = description)
711 if 0 == len(phoneNumber):
714 self.number_selected(phoneNumber)
715 self._messageviewselection.unselect_all()
718 class ContactsView(object):
720 def __init__(self, widgetTree, backend, errorDisplay):
721 self._errorDisplay = errorDisplay
722 self._backend = backend
724 self._addressBook = None
725 self._addressBookFactories = [DummyAddressBook()]
727 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
728 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
730 self._contactstime = 0.0
731 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
732 self._contactsviewselection = None
733 self._contactsview = widgetTree.get_widget("contactsview")
735 self._contactColumn = gtk.TreeViewColumn("Contact")
736 displayContactSource = False
737 if displayContactSource:
738 textrenderer = gtk.CellRendererText()
739 self._contactColumn.pack_start(textrenderer, expand=False)
740 self._contactColumn.add_attribute(textrenderer, 'text', 0)
741 textrenderer = gtk.CellRendererText()
742 self._contactColumn.pack_start(textrenderer, expand=True)
743 self._contactColumn.add_attribute(textrenderer, 'text', 1)
744 textrenderer = gtk.CellRendererText()
745 self._contactColumn.pack_start(textrenderer, expand=True)
746 self._contactColumn.add_attribute(textrenderer, 'text', 4)
747 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
748 self._contactColumn.set_sort_column_id(1)
749 self._contactColumn.set_visible(True)
751 self._onContactsviewRowActivatedId = 0
752 self._onAddressbookComboChangedId = 0
753 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
756 assert self._backend.is_authed()
758 self._contactsview.set_model(self._contactsmodel)
759 self._contactsview.append_column(self._contactColumn)
760 self._contactsviewselection = self._contactsview.get_selection()
761 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
763 self._booksList.clear()
764 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
765 if factoryName and bookName:
766 entryName = "%s: %s" % (factoryName, bookName)
768 entryName = factoryName
772 entryName = "Bad name (%d)" % factoryId
773 row = (str(factoryId), bookId, entryName)
774 self._booksList.append(row)
776 self._booksSelectionBox.set_model(self._booksList)
777 cell = gtk.CellRendererText()
778 self._booksSelectionBox.pack_start(cell, True)
779 self._booksSelectionBox.add_attribute(cell, 'text', 2)
780 self._booksSelectionBox.set_active(0)
782 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
783 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
786 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
787 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
789 self._booksSelectionBox.clear()
790 self._booksSelectionBox.set_model(None)
791 self._contactsview.set_model(None)
792 self._contactsview.remove_column(self._contactColumn)
794 def number_selected(self, number):
796 @note Actual dial function is patched in later
798 raise NotImplementedError
800 def get_addressbooks(self):
802 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
804 for i, factory in enumerate(self._addressBookFactories):
805 for bookFactory, bookId, bookName in factory.get_addressbooks():
806 yield (i, bookId), (factory.factory_name(), bookName)
808 def open_addressbook(self, bookFactoryId, bookId):
809 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
810 self._contactstime = 0
811 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
812 backgroundPopulate.setDaemon(True)
813 backgroundPopulate.start()
816 if (time.time() - self._contactstime) < 300:
818 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
819 backgroundPopulate.setDaemon(True)
820 backgroundPopulate.start()
823 self._contactstime = 0.0
824 self._contactsmodel.clear()
826 def clear_caches(self):
827 for factory in self._addressBookFactories:
828 factory.clear_caches()
829 self._addressBook.clear_caches()
831 def append(self, book):
832 self._addressBookFactories.append(book)
834 def extend(self, books):
835 self._addressBookFactories.extend(books)
837 def _idly_populate_contactsview(self):
838 #@todo Add a lock so only one code path can be in here at a time
841 # completely disable updating the treeview while we populate the data
842 self._contactsview.freeze_child_notify()
843 self._contactsview.set_model(None)
845 addressBook = self._addressBook
847 contacts = addressBook.get_contacts()
848 except RuntimeError, e:
850 self._contactstime = 0.0
851 self._errorDisplay.push_exception_with_lock(e)
852 for contactId, contactName in contacts:
853 contactType = (addressBook.contact_source_short_name(contactId), )
854 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
856 # restart the treeview data rendering
857 self._contactsview.set_model(self._contactsmodel)
858 self._contactsview.thaw_child_notify()
861 def _on_addressbook_combo_changed(self, *args, **kwds):
862 itr = self._booksSelectionBox.get_active_iter()
865 factoryId = int(self._booksList.get_value(itr, 0))
866 bookId = self._booksList.get_value(itr, 1)
867 self.open_addressbook(factoryId, bookId)
869 def _on_contactsview_row_activated(self, treeview, path, view_column):
870 model, itr = self._contactsviewselection.get_selected()
874 contactId = self._contactsmodel.get_value(itr, 3)
875 contactName = self._contactsmodel.get_value(itr, 1)
877 contactDetails = self._addressBook.get_contact_details(contactId)
878 except RuntimeError, e:
880 self._contactstime = 0.0
881 self._errorDisplay.push_exception(e)
882 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
884 if len(contactPhoneNumbers) == 0:
886 elif len(contactPhoneNumbers) == 1:
887 phoneNumber = self._phoneTypeSelector.run(contactPhoneNumbers, message = contactName)
889 if 0 == len(phoneNumber):
892 self.number_selected(phoneNumber)
893 self._contactsviewselection.unselect_all()