Tweaks to error reporting and other things
[gc-dialer] / src / gc_views.py
1 #!/usr/bin/python2.5
2
3 # DialCentral - Front end for Google's Grand Central service.
4 # Copyright (C) 2008  Mark Bergman bergman AT merctech DOT com
5
6 # This library is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU Lesser General Public
8 # License as published by the Free Software Foundation; either
9 # version 2.1 of the License, or (at your option) any later version.
10
11 # This library is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # Lesser General Public License for more details.
15
16 # You should have received a copy of the GNU Lesser General Public
17 # License along with this library; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
19
20 from __future__ import with_statement
21
22 import threading
23 import time
24 import warnings
25 import traceback
26
27 import gobject
28 import gtk
29
30 import gtk_toolbox
31
32
33 def make_ugly(prettynumber):
34         """
35         function to take a phone number and strip out all non-numeric
36         characters
37
38         >>> make_ugly("+012-(345)-678-90")
39         '01234567890'
40         """
41         import re
42         uglynumber = re.sub('\D', '', prettynumber)
43         return uglynumber
44
45
46 def make_pretty(phonenumber):
47         """
48         Function to take a phone number and return the pretty version
49         pretty numbers:
50                 if phonenumber begins with 0:
51                         ...-(...)-...-....
52                 if phonenumber begins with 1: ( for gizmo callback numbers )
53                         1 (...)-...-....
54                 if phonenumber is 13 digits:
55                         (...)-...-....
56                 if phonenumber is 10 digits:
57                         ...-....
58         >>> make_pretty("12")
59         '12'
60         >>> make_pretty("1234567")
61         '123-4567'
62         >>> make_pretty("2345678901")
63         '(234)-567-8901'
64         >>> make_pretty("12345678901")
65         '1 (234)-567-8901'
66         >>> make_pretty("01234567890")
67         '+012-(345)-678-90'
68         """
69         if phonenumber is None or phonenumber is "":
70                 return ""
71
72         phonenumber = make_ugly(phonenumber)
73
74         if len(phonenumber) < 3:
75                 return phonenumber
76
77         if phonenumber[0] == "0":
78                 prettynumber = ""
79                 prettynumber += "+%s" % phonenumber[0:3]
80                 if 3 < len(phonenumber):
81                         prettynumber += "-(%s)" % phonenumber[3:6]
82                         if 6 < len(phonenumber):
83                                 prettynumber += "-%s" % phonenumber[6:9]
84                                 if 9 < len(phonenumber):
85                                         prettynumber += "-%s" % phonenumber[9:]
86                 return prettynumber
87         elif len(phonenumber) <= 7:
88                 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
89         elif len(phonenumber) > 8 and phonenumber[0] == "1":
90                 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
91         elif len(phonenumber) > 7:
92                 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
93         return prettynumber
94
95
96 def make_idler(func):
97         """
98         Decorator that makes a generator-function into a function that will continue execution on next call
99         """
100         a = []
101
102         def decorated_func(*args, **kwds):
103                 if not a:
104                         a.append(func(*args, **kwds))
105                 try:
106                         a[0].next()
107                         return True
108                 except StopIteration:
109                         del a[:]
110                         return False
111
112         decorated_func.__name__ = func.__name__
113         decorated_func.__doc__ = func.__doc__
114         decorated_func.__dict__.update(func.__dict__)
115
116         return decorated_func
117
118
119 class DummyAddressBook(object):
120         """
121         Minimal example of both an addressbook factory and an addressbook
122         """
123
124         def clear_caches(self):
125                 pass
126
127         def get_addressbooks(self):
128                 """
129                 @returns Iterable of (Address Book Factory, Book Id, Book Name)
130                 """
131                 yield self, "", "None"
132
133         def open_addressbook(self, bookId):
134                 return self
135
136         @staticmethod
137         def contact_source_short_name(contactId):
138                 return ""
139
140         @staticmethod
141         def factory_name():
142                 return ""
143
144         @staticmethod
145         def get_contacts():
146                 """
147                 @returns Iterable of (contact id, contact name)
148                 """
149                 return []
150
151         @staticmethod
152         def get_contact_details(contactId):
153                 """
154                 @returns Iterable of (Phone Type, Phone Number)
155                 """
156                 return []
157
158
159 class MergedAddressBook(object):
160         """
161         Merger of all addressbooks
162         """
163
164         def __init__(self, addressbookFactories, sorter = None):
165                 self.__addressbookFactories = addressbookFactories
166                 self.__addressbooks = None
167                 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
168
169         def clear_caches(self):
170                 self.__addressbooks = None
171                 for factory in self.__addressbookFactories:
172                         factory.clear_caches()
173
174         def get_addressbooks(self):
175                 """
176                 @returns Iterable of (Address Book Factory, Book Id, Book Name)
177                 """
178                 yield self, "", ""
179
180         def open_addressbook(self, bookId):
181                 return self
182
183         def contact_source_short_name(self, contactId):
184                 if self.__addressbooks is None:
185                         return ""
186                 bookIndex, originalId = contactId.split("-", 1)
187                 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
188
189         @staticmethod
190         def factory_name():
191                 return "All Contacts"
192
193         def get_contacts(self):
194                 """
195                 @returns Iterable of (contact id, contact name)
196                 """
197                 if self.__addressbooks is None:
198                         self.__addressbooks = list(
199                                 factory.open_addressbook(id)
200                                 for factory in self.__addressbookFactories
201                                 for (f, id, name) in factory.get_addressbooks()
202                         )
203                 contacts = (
204                         ("-".join([str(bookIndex), contactId]), contactName)
205                                 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
206                                         for (contactId, contactName) in addressbook.get_contacts()
207                 )
208                 sortedContacts = self.__sort_contacts(contacts)
209                 return sortedContacts
210
211         def get_contact_details(self, contactId):
212                 """
213                 @returns Iterable of (Phone Type, Phone Number)
214                 """
215                 if self.__addressbooks is None:
216                         return []
217                 bookIndex, originalId = contactId.split("-", 1)
218                 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
219
220         @staticmethod
221         def null_sorter(contacts):
222                 """
223                 Good for speed/low memory
224                 """
225                 return contacts
226
227         @staticmethod
228         def basic_firtname_sorter(contacts):
229                 """
230                 Expects names in "First Last" format
231                 """
232                 contactsWithKey = [
233                         (contactName.rsplit(" ", 1)[0], (contactId, contactName))
234                                 for (contactId, contactName) in contacts
235                 ]
236                 contactsWithKey.sort()
237                 return (contactData for (lastName, contactData) in contactsWithKey)
238
239         @staticmethod
240         def basic_lastname_sorter(contacts):
241                 """
242                 Expects names in "First Last" format
243                 """
244                 contactsWithKey = [
245                         (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
246                                 for (contactId, contactName) in contacts
247                 ]
248                 contactsWithKey.sort()
249                 return (contactData for (lastName, contactData) in contactsWithKey)
250
251         @staticmethod
252         def reversed_firtname_sorter(contacts):
253                 """
254                 Expects names in "Last, First" format
255                 """
256                 contactsWithKey = [
257                         (contactName.split(", ", 1)[-1], (contactId, contactName))
258                                 for (contactId, contactName) in contacts
259                 ]
260                 contactsWithKey.sort()
261                 return (contactData for (lastName, contactData) in contactsWithKey)
262
263         @staticmethod
264         def reversed_lastname_sorter(contacts):
265                 """
266                 Expects names in "Last, First" format
267                 """
268                 contactsWithKey = [
269                         (contactName.split(", ", 1)[0], (contactId, contactName))
270                                 for (contactId, contactName) in contacts
271                 ]
272                 contactsWithKey.sort()
273                 return (contactData for (lastName, contactData) in contactsWithKey)
274
275         @staticmethod
276         def guess_firstname(name):
277                 if ", " in name:
278                         return name.split(", ", 1)[-1]
279                 else:
280                         return name.rsplit(" ", 1)[0]
281
282         @staticmethod
283         def guess_lastname(name):
284                 if ", " in name:
285                         return name.split(", ", 1)[0]
286                 else:
287                         return name.rsplit(" ", 1)[-1]
288
289         @classmethod
290         def advanced_firstname_sorter(cls, contacts):
291                 contactsWithKey = [
292                         (cls.guess_firstname(contactName), (contactId, contactName))
293                                 for (contactId, contactName) in contacts
294                 ]
295                 contactsWithKey.sort()
296                 return (contactData for (lastName, contactData) in contactsWithKey)
297
298         @classmethod
299         def advanced_lastname_sorter(cls, contacts):
300                 contactsWithKey = [
301                         (cls.guess_lastname(contactName), (contactId, contactName))
302                                 for (contactId, contactName) in contacts
303                 ]
304                 contactsWithKey.sort()
305                 return (contactData for (lastName, contactData) in contactsWithKey)
306
307
308 class PhoneTypeSelector(object):
309
310         def __init__(self, widgetTree, gcBackend):
311                 self._gcBackend = gcBackend
312                 self._widgetTree = widgetTree
313                 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
314
315                 self._selectButton = self._widgetTree.get_widget("select_button")
316                 self._selectButton.connect("clicked", self._on_phonetype_select)
317
318                 self._cancelButton = self._widgetTree.get_widget("cancel_button")
319                 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
320
321                 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
322                 self._typeviewselection = None
323
324                 typeview = self._widgetTree.get_widget("phonetypes")
325                 typeview.connect("row-activated", self._on_phonetype_select)
326                 typeview.set_model(self._typemodel)
327                 textrenderer = gtk.CellRendererText()
328
329                 # Add the column to the treeview
330                 column = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=1)
331                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
332
333                 typeview.append_column(column)
334
335                 self._typeviewselection = typeview.get_selection()
336                 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
337
338         def run(self, contactDetails):
339                 self._typemodel.clear()
340
341                 for phoneType, phoneNumber in contactDetails:
342                         self._typemodel.append((phoneNumber, "%s - %s" % (make_pretty(phoneNumber), phoneType)))
343
344                 userResponse = self._dialog.run()
345
346                 if userResponse == gtk.RESPONSE_OK:
347                         model, itr = self._typeviewselection.get_selected()
348                         if itr:
349                                 phoneNumber = self._typemodel.get_value(itr, 0)
350                 else:
351                         phoneNumber = ""
352
353                 self._typeviewselection.unselect_all()
354                 self._dialog.hide()
355                 return phoneNumber
356
357         def _on_phonetype_select(self, *args):
358                 self._dialog.response(gtk.RESPONSE_OK)
359
360         def _on_phonetype_cancel(self, *args):
361                 self._dialog.response(gtk.RESPONSE_CANCEL)
362
363
364 class Dialpad(object):
365
366         def __init__(self, widgetTree, errorDisplay):
367                 self._errorDisplay = errorDisplay
368                 self._numberdisplay = widgetTree.get_widget("numberdisplay")
369                 self._dialButton = widgetTree.get_widget("dial")
370                 self._phonenumber = ""
371                 self._prettynumber = ""
372                 self._clearall_id = None
373
374                 callbackMapping = {
375                         "on_dial_clicked": self._on_dial_clicked,
376                         "on_digit_clicked": self._on_digit_clicked,
377                         "on_clear_number": self._on_clear_number,
378                         "on_back_clicked": self._on_backspace,
379                         "on_back_pressed": self._on_back_pressed,
380                         "on_back_released": self._on_back_released,
381                 }
382                 widgetTree.signal_autoconnect(callbackMapping)
383
384         def enable(self):
385                 self._dialButton.grab_focus()
386
387         def disable(self):
388                 pass
389
390         def dial(self, number):
391                 """
392                 @note Actual dial function is patched in later
393                 """
394                 raise NotImplementedError
395
396         def get_number(self):
397                 return self._phonenumber
398
399         def set_number(self, number):
400                 """
401                 Set the callback phonenumber
402                 """
403                 try:
404                         self._phonenumber = make_ugly(number)
405                         self._prettynumber = make_pretty(self._phonenumber)
406                         self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
407                 except TypeError, e:
408                         self._errorDisplay.push_exception(e)
409
410         def clear(self):
411                 self.set_number("")
412
413         def _on_dial_clicked(self, widget):
414                 self.dial(self.get_number())
415
416         def _on_clear_number(self, *args):
417                 self.clear()
418
419         def _on_digit_clicked(self, widget):
420                 self.set_number(self._phonenumber + widget.get_name()[-1])
421
422         def _on_backspace(self, widget):
423                 self.set_number(self._phonenumber[:-1])
424
425         def _on_clearall(self):
426                 self.clear()
427                 return False
428
429         def _on_back_pressed(self, widget):
430                 self._clearall_id = gobject.timeout_add(1000, self._on_clearall)
431
432         def _on_back_released(self, widget):
433                 if self._clearall_id is not None:
434                         gobject.source_remove(self._clearall_id)
435                 self._clearall_id = None
436
437
438 class AccountInfo(object):
439
440         def __init__(self, widgetTree, backend, errorDisplay):
441                 self._errorDisplay = errorDisplay
442                 self._backend = backend
443
444                 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
445                 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
446                 self._callbackCombo = widgetTree.get_widget("callbackcombo")
447                 self._onCallbackentryChangedId = 0
448
449         def enable(self):
450                 assert self._backend.is_authed()
451                 self._accountViewNumberDisplay.set_use_markup(True)
452                 self.set_account_number("")
453                 self._callbackList.clear()
454                 self.update()
455                 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
456
457         def disable(self):
458                 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
459                 self.clear()
460                 self._callbackList.clear()
461
462         def get_selected_callback_number(self):
463                 return make_ugly(self._callbackCombo.get_child().get_text())
464
465         def set_account_number(self, number):
466                 """
467                 Displays current account number
468                 """
469                 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
470
471         def update(self):
472                 self.populate_callback_combo()
473                 self.set_account_number(self._backend.get_account_number())
474
475         def clear(self):
476                 self._callbackCombo.get_child().set_text("")
477                 self.set_account_number("")
478
479         def populate_callback_combo(self):
480                 self._callbackList.clear()
481                 try:
482                         callbackNumbers = self._backend.get_callback_numbers()
483                 except RuntimeError, e:
484                         self._errorDisplay.push_exception(e)
485                         return
486
487                 for number, description in callbackNumbers.iteritems():
488                         self._callbackList.append((make_pretty(number),))
489
490                 self._callbackCombo.set_model(self._callbackList)
491                 self._callbackCombo.set_text_column(0)
492                 try:
493                         callbackNumber = self._backend.get_callback_number()
494                 except RuntimeError, e:
495                         self._errorDisplay.push_exception(e)
496                         return
497                 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
498
499         def _on_callbackentry_changed(self, *args):
500                 """
501                 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
502                 """
503                 try:
504                         text = self.get_selected_callback_number()
505                         if not self._backend.is_valid_syntax(text):
506                                 self._errorDisplay.push_message("%s is not a valid callback number" % text)
507                         elif text == self._backend.get_callback_number():
508                                 warnings.warn("Callback number already is %s" % self._backend.get_callback_number(), UserWarning, 2)
509                         else:
510                                 self._backend.set_callback_number(text)
511                 except RuntimeError, e:
512                         self._errorDisplay.push_exception(e)
513
514
515 class RecentCallsView(object):
516
517         def __init__(self, widgetTree, backend, errorDisplay):
518                 self._errorDisplay = errorDisplay
519                 self._backend = backend
520
521                 self._recenttime = 0.0
522                 self._recentmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
523                 self._recentview = widgetTree.get_widget("recentview")
524                 self._recentviewselection = None
525                 self._onRecentviewRowActivatedId = 0
526
527                 textrenderer = gtk.CellRendererText()
528                 self._recentviewColumn = gtk.TreeViewColumn("Calls")
529                 self._recentviewColumn.pack_start(textrenderer, expand=True)
530                 self._recentviewColumn.add_attribute(textrenderer, "text", 1)
531                 self._recentviewColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
532
533         def enable(self):
534                 assert self._backend.is_authed()
535                 self._recentview.set_model(self._recentmodel)
536
537                 self._recentview.append_column(self._recentviewColumn)
538                 self._recentviewselection = self._recentview.get_selection()
539                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
540
541                 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
542
543         def disable(self):
544                 self._recentview.disconnect(self._onRecentviewRowActivatedId)
545                 self._recentview.remove_column(self._recentviewColumn)
546                 self._recentview.set_model(None)
547
548         def number_selected(self, number):
549                 """
550                 @note Actual dial function is patched in later
551                 """
552                 raise NotImplementedError
553
554         def update(self):
555                 if (time.time() - self._recenttime) < 300:
556                         return
557                 backgroundPopulate = threading.Thread(target=self._idly_populate_recentview)
558                 backgroundPopulate.setDaemon(True)
559                 backgroundPopulate.start()
560
561         def clear(self):
562                 self._recenttime = 0.0
563                 self._recentmodel.clear()
564
565         def _idly_populate_recentview(self):
566                 self._recenttime = time.time()
567                 self._recentmodel.clear()
568
569                 try:
570                         recentItems = self._backend.get_recent()
571                 except RuntimeError, e:
572                         self._errorDisplay.push_exception_with_lock(e)
573                         self._recenttime = 0.0
574                         recentItems = []
575
576                 for personsName, phoneNumber, date, action in recentItems:
577                         description = "%s on %s from/to %s - %s" % (action.capitalize(), date, personsName, phoneNumber)
578                         item = (phoneNumber, description)
579                         with gtk_toolbox.gtk_lock():
580                                 self._recentmodel.append(item)
581
582                 return False
583
584         def _on_recentview_row_activated(self, treeview, path, view_column):
585                 model, itr = self._recentviewselection.get_selected()
586                 if not itr:
587                         return
588
589                 self.number_selected(self._recentmodel.get_value(itr, 0))
590                 self._recentviewselection.unselect_all()
591
592
593 class ContactsView(object):
594
595         def __init__(self, widgetTree, backend, errorDisplay):
596                 self._errorDisplay = errorDisplay
597                 self._backend = backend
598
599                 self._addressBook = None
600                 self._addressBookFactories = [DummyAddressBook()]
601
602                 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
603                 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
604
605                 self._contactstime = 0.0
606                 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
607                 self._contactsviewselection = None
608                 self._contactsview = widgetTree.get_widget("contactsview")
609
610                 self._contactColumn = gtk.TreeViewColumn("Contact")
611                 displayContactSource = False
612                 if displayContactSource:
613                         textrenderer = gtk.CellRendererText()
614                         self._contactColumn.pack_start(textrenderer, expand=False)
615                         self._contactColumn.add_attribute(textrenderer, 'text', 0)
616                 textrenderer = gtk.CellRendererText()
617                 self._contactColumn.pack_start(textrenderer, expand=True)
618                 self._contactColumn.add_attribute(textrenderer, 'text', 1)
619                 textrenderer = gtk.CellRendererText()
620                 self._contactColumn.pack_start(textrenderer, expand=True)
621                 self._contactColumn.add_attribute(textrenderer, 'text', 4)
622                 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
623                 self._contactColumn.set_sort_column_id(1)
624                 self._contactColumn.set_visible(True)
625
626                 self._onContactsviewRowActivatedId = 0
627                 self._onAddressbookComboChangedId = 0
628                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
629
630         def enable(self):
631                 assert self._backend.is_authed()
632
633                 self._contactsview.set_model(self._contactsmodel)
634                 self._contactsview.append_column(self._contactColumn)
635                 self._contactsviewselection = self._contactsview.get_selection()
636                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
637
638                 self._booksList.clear()
639                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
640                         if factoryName and bookName:
641                                 entryName = "%s: %s" % (factoryName, bookName)
642                         elif factoryName:
643                                 entryName = factoryName
644                         elif bookName:
645                                 entryName = bookName
646                         else:
647                                 entryName = "Bad name (%d)" % factoryId
648                         row = (str(factoryId), bookId, entryName)
649                         self._booksList.append(row)
650
651                 self._booksSelectionBox.set_model(self._booksList)
652                 cell = gtk.CellRendererText()
653                 self._booksSelectionBox.pack_start(cell, True)
654                 self._booksSelectionBox.add_attribute(cell, 'text', 2)
655                 self._booksSelectionBox.set_active(0)
656
657                 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
658                 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
659
660         def disable(self):
661                 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
662                 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
663
664                 self._booksSelectionBox.clear()
665                 self._booksSelectionBox.set_model(None)
666                 self._contactsview.set_model(None)
667                 self._contactsview.remove_column(self._contactColumn)
668
669         def number_selected(self, number):
670                 """
671                 @note Actual dial function is patched in later
672                 """
673                 raise NotImplementedError
674
675         def get_addressbooks(self):
676                 """
677                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
678                 """
679                 for i, factory in enumerate(self._addressBookFactories):
680                         for bookFactory, bookId, bookName in factory.get_addressbooks():
681                                 yield (i, bookId), (factory.factory_name(), bookName)
682
683         def open_addressbook(self, bookFactoryId, bookId):
684                 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
685                 self._contactstime = 0
686                 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
687                 backgroundPopulate.setDaemon(True)
688                 backgroundPopulate.start()
689
690         def update(self):
691                 if (time.time() - self._contactstime) < 300:
692                         return
693                 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
694                 backgroundPopulate.setDaemon(True)
695                 backgroundPopulate.start()
696
697         def clear(self):
698                 self._contactstime = 0.0
699                 self._contactsmodel.clear()
700
701         def clear_caches(self):
702                 for factory in self._addressBookFactories:
703                         factory.clear_caches()
704                 self._addressBook.clear_caches()
705
706         def append(self, book):
707                 self._addressBookFactories.append(book)
708
709         def extend(self, books):
710                 self._addressBookFactories.extend(books)
711
712         def _idly_populate_contactsview(self):
713                 #@todo Add a lock so only one code path can be in here at a time
714                 self.clear()
715
716                 # completely disable updating the treeview while we populate the data
717                 self._contactsview.freeze_child_notify()
718                 self._contactsview.set_model(None)
719
720                 addressBook = self._addressBook
721                 try:
722                         contacts = addressBook.get_contacts()
723                 except RuntimeError, e:
724                         contacts = []
725                         self._contactstime = 0.0
726                         self._errorDisplay.push_exception_with_lock(e)
727                 for contactId, contactName in contacts:
728                         contactType = (addressBook.contact_source_short_name(contactId), )
729                         self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
730
731                 # restart the treeview data rendering
732                 self._contactsview.set_model(self._contactsmodel)
733                 self._contactsview.thaw_child_notify()
734                 return False
735
736         def _on_addressbook_combo_changed(self, *args, **kwds):
737                 itr = self._booksSelectionBox.get_active_iter()
738                 if itr is None:
739                         return
740                 factoryId = int(self._booksList.get_value(itr, 0))
741                 bookId = self._booksList.get_value(itr, 1)
742                 self.open_addressbook(factoryId, bookId)
743
744         def _on_contactsview_row_activated(self, treeview, path, view_column):
745                 model, itr = self._contactsviewselection.get_selected()
746                 if not itr:
747                         return
748
749                 contactId = self._contactsmodel.get_value(itr, 3)
750                 try:
751                         contactDetails = self._addressBook.get_contact_details(contactId)
752                 except RuntimeError, e:
753                         contactDetails = []
754                         self._contactstime = 0.0
755                         self._errorDisplay.push_exception(e)
756                 contactDetails = [phoneNumber for phoneNumber in contactDetails]
757
758                 if len(contactDetails) == 0:
759                         phoneNumber = ""
760                 elif len(contactDetails) == 1:
761                         phoneNumber = contactDetails[0][1]
762                 else:
763                         phoneNumber = self._phoneTypeSelector.run(contactDetails)
764
765                 if 0 < len(phoneNumber):
766                         self.number_selected(phoneNumber)
767
768                 self._contactsviewselection.unselect_all()