4215d3a95990073e57b6955ac6dffcd5c1937dd3
[gc-dialer] / src / gc_views.py
1 #!/usr/bin/python2.5
2
3 """
4 DialCentral - Front end for Google's Grand Central service.
5 Copyright (C) 2008  Mark Bergman bergman AT merctech DOT com
6
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.
11
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.
16
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
20
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
25 """
26
27 from __future__ import with_statement
28
29 import threading
30 import time
31 import warnings
32 import traceback
33
34 import gobject
35 import gtk
36
37 import gtk_toolbox
38
39
40 def make_ugly(prettynumber):
41         """
42         function to take a phone number and strip out all non-numeric
43         characters
44
45         >>> make_ugly("+012-(345)-678-90")
46         '01234567890'
47         """
48         import re
49         uglynumber = re.sub('\D', '', prettynumber)
50         return uglynumber
51
52
53 def make_pretty(phonenumber):
54         """
55         Function to take a phone number and return the pretty version
56         pretty numbers:
57                 if phonenumber begins with 0:
58                         ...-(...)-...-....
59                 if phonenumber begins with 1: ( for gizmo callback numbers )
60                         1 (...)-...-....
61                 if phonenumber is 13 digits:
62                         (...)-...-....
63                 if phonenumber is 10 digits:
64                         ...-....
65         >>> make_pretty("12")
66         '12'
67         >>> make_pretty("1234567")
68         '123-4567'
69         >>> make_pretty("2345678901")
70         '(234)-567-8901'
71         >>> make_pretty("12345678901")
72         '1 (234)-567-8901'
73         >>> make_pretty("01234567890")
74         '+012-(345)-678-90'
75         """
76         if phonenumber is None or phonenumber is "":
77                 return ""
78
79         phonenumber = make_ugly(phonenumber)
80
81         if len(phonenumber) < 3:
82                 return phonenumber
83
84         if phonenumber[0] == "0":
85                 prettynumber = ""
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:]
93                 return prettynumber
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:])
100         return prettynumber
101
102
103 def make_idler(func):
104         """
105         Decorator that makes a generator-function into a function that will continue execution on next call
106         """
107         a = []
108
109         def decorated_func(*args, **kwds):
110                 if not a:
111                         a.append(func(*args, **kwds))
112                 try:
113                         a[0].next()
114                         return True
115                 except StopIteration:
116                         del a[:]
117                         return False
118
119         decorated_func.__name__ = func.__name__
120         decorated_func.__doc__ = func.__doc__
121         decorated_func.__dict__.update(func.__dict__)
122
123         return decorated_func
124
125
126 class DummyAddressBook(object):
127         """
128         Minimal example of both an addressbook factory and an addressbook
129         """
130
131         def clear_caches(self):
132                 pass
133
134         def get_addressbooks(self):
135                 """
136                 @returns Iterable of (Address Book Factory, Book Id, Book Name)
137                 """
138                 yield self, "", "None"
139
140         def open_addressbook(self, bookId):
141                 return self
142
143         @staticmethod
144         def contact_source_short_name(contactId):
145                 return ""
146
147         @staticmethod
148         def factory_name():
149                 return ""
150
151         @staticmethod
152         def get_contacts():
153                 """
154                 @returns Iterable of (contact id, contact name)
155                 """
156                 return []
157
158         @staticmethod
159         def get_contact_details(contactId):
160                 """
161                 @returns Iterable of (Phone Type, Phone Number)
162                 """
163                 return []
164
165
166 class MergedAddressBook(object):
167         """
168         Merger of all addressbooks
169         """
170
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
175
176         def clear_caches(self):
177                 self.__addressbooks = None
178                 for factory in self.__addressbookFactories:
179                         factory.clear_caches()
180
181         def get_addressbooks(self):
182                 """
183                 @returns Iterable of (Address Book Factory, Book Id, Book Name)
184                 """
185                 yield self, "", ""
186
187         def open_addressbook(self, bookId):
188                 return self
189
190         def contact_source_short_name(self, contactId):
191                 if self.__addressbooks is None:
192                         return ""
193                 bookIndex, originalId = contactId.split("-", 1)
194                 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
195
196         @staticmethod
197         def factory_name():
198                 return "All Contacts"
199
200         def get_contacts(self):
201                 """
202                 @returns Iterable of (contact id, contact name)
203                 """
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()
209                         )
210                 contacts = (
211                         ("-".join([str(bookIndex), contactId]), contactName)
212                                 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
213                                         for (contactId, contactName) in addressbook.get_contacts()
214                 )
215                 sortedContacts = self.__sort_contacts(contacts)
216                 return sortedContacts
217
218         def get_contact_details(self, contactId):
219                 """
220                 @returns Iterable of (Phone Type, Phone Number)
221                 """
222                 if self.__addressbooks is None:
223                         return []
224                 bookIndex, originalId = contactId.split("-", 1)
225                 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
226
227         @staticmethod
228         def null_sorter(contacts):
229                 """
230                 Good for speed/low memory
231                 """
232                 return contacts
233
234         @staticmethod
235         def basic_firtname_sorter(contacts):
236                 """
237                 Expects names in "First Last" format
238                 """
239                 contactsWithKey = [
240                         (contactName.rsplit(" ", 1)[0], (contactId, contactName))
241                                 for (contactId, contactName) in contacts
242                 ]
243                 contactsWithKey.sort()
244                 return (contactData for (lastName, contactData) in contactsWithKey)
245
246         @staticmethod
247         def basic_lastname_sorter(contacts):
248                 """
249                 Expects names in "First Last" format
250                 """
251                 contactsWithKey = [
252                         (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
253                                 for (contactId, contactName) in contacts
254                 ]
255                 contactsWithKey.sort()
256                 return (contactData for (lastName, contactData) in contactsWithKey)
257
258         @staticmethod
259         def reversed_firtname_sorter(contacts):
260                 """
261                 Expects names in "Last, First" format
262                 """
263                 contactsWithKey = [
264                         (contactName.split(", ", 1)[-1], (contactId, contactName))
265                                 for (contactId, contactName) in contacts
266                 ]
267                 contactsWithKey.sort()
268                 return (contactData for (lastName, contactData) in contactsWithKey)
269
270         @staticmethod
271         def reversed_lastname_sorter(contacts):
272                 """
273                 Expects names in "Last, First" format
274                 """
275                 contactsWithKey = [
276                         (contactName.split(", ", 1)[0], (contactId, contactName))
277                                 for (contactId, contactName) in contacts
278                 ]
279                 contactsWithKey.sort()
280                 return (contactData for (lastName, contactData) in contactsWithKey)
281
282         @staticmethod
283         def guess_firstname(name):
284                 if ", " in name:
285                         return name.split(", ", 1)[-1]
286                 else:
287                         return name.rsplit(" ", 1)[0]
288
289         @staticmethod
290         def guess_lastname(name):
291                 if ", " in name:
292                         return name.split(", ", 1)[0]
293                 else:
294                         return name.rsplit(" ", 1)[-1]
295
296         @classmethod
297         def advanced_firstname_sorter(cls, contacts):
298                 contactsWithKey = [
299                         (cls.guess_firstname(contactName), (contactId, contactName))
300                                 for (contactId, contactName) in contacts
301                 ]
302                 contactsWithKey.sort()
303                 return (contactData for (lastName, contactData) in contactsWithKey)
304
305         @classmethod
306         def advanced_lastname_sorter(cls, contacts):
307                 contactsWithKey = [
308                         (cls.guess_lastname(contactName), (contactId, contactName))
309                                 for (contactId, contactName) in contacts
310                 ]
311                 contactsWithKey.sort()
312                 return (contactData for (lastName, contactData) in contactsWithKey)
313
314
315 class PhoneTypeSelector(object):
316
317         def __init__(self, widgetTree, gcBackend):
318                 self._gcBackend = gcBackend
319                 self._widgetTree = widgetTree
320                 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
321
322                 self._dialButton = self._widgetTree.get_widget("dial_button")
323                 self._dialButton.connect("clicked", self._on_phonetype_dial)
324
325                 self._selectButton = self._widgetTree.get_widget("select_button")
326                 self._selectButton.connect("clicked", self._on_phonetype_select)
327
328                 self._cancelButton = self._widgetTree.get_widget("cancel_button")
329                 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
330
331                 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
332                 self._typeviewselection = None
333
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()
339
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)
343
344                 typeview.append_column(column)
345
346                 self._typeviewselection = typeview.get_selection()
347                 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
348
349         def run(self, contactDetails, message = ""):
350                 self._typemodel.clear()
351
352                 for phoneType, phoneNumber in contactDetails:
353                         self._typemodel.append((phoneNumber, "%s - %s" % (make_pretty(phoneNumber), phoneType)))
354
355                 if message:
356                         self._message.show()
357                         self._message.set_text(message)
358                 else:
359                         self._message.hide()
360
361                 userResponse = self._dialog.run()
362
363                 if userResponse == gtk.RESPONSE_OK:
364                         phoneNumber = self._get_number()
365                 else:
366                         phoneNumber = ""
367
368                 self._typeviewselection.unselect_all()
369                 self._dialog.hide()
370                 return phoneNumber
371
372         def _get_number(self):
373                 model, itr = self._typeviewselection.get_selected()
374                 if not itr:
375                         return ""
376
377                 phoneNumber = self._typemodel.get_value(itr, 0)
378                 return phoneNumber
379
380         def _on_phonetype_dial(self, *args):
381                 self._gcBackend.dial(self._get_number())
382                 self._dialog.response(gtk.RESPONSE_CANCEL)
383
384         def _on_phonetype_select(self, *args):
385                 self._dialog.response(gtk.RESPONSE_OK)
386
387         def _on_phonetype_cancel(self, *args):
388                 self._dialog.response(gtk.RESPONSE_CANCEL)
389
390
391 class Dialpad(object):
392
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
400
401                 callbackMapping = {
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,
408                 }
409                 widgetTree.signal_autoconnect(callbackMapping)
410
411         def enable(self):
412                 self._dialButton.grab_focus()
413
414         def disable(self):
415                 pass
416
417         def dial(self, number):
418                 """
419                 @note Actual dial function is patched in later
420                 """
421                 raise NotImplementedError
422
423         def get_number(self):
424                 return self._phonenumber
425
426         def set_number(self, number):
427                 """
428                 Set the callback phonenumber
429                 """
430                 try:
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))
434                 except TypeError, e:
435                         self._errorDisplay.push_exception(e)
436
437         def clear(self):
438                 self.set_number("")
439
440         def _on_dial_clicked(self, widget):
441                 self.dial(self.get_number())
442
443         def _on_clear_number(self, *args):
444                 self.clear()
445
446         def _on_digit_clicked(self, widget):
447                 self.set_number(self._phonenumber + widget.get_name()[-1])
448
449         def _on_backspace(self, widget):
450                 self.set_number(self._phonenumber[:-1])
451
452         def _on_clearall(self):
453                 self.clear()
454                 return False
455
456         def _on_back_pressed(self, widget):
457                 self._clearall_id = gobject.timeout_add(1000, self._on_clearall)
458
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
463
464
465 class AccountInfo(object):
466
467         def __init__(self, widgetTree, backend, errorDisplay):
468                 self._errorDisplay = errorDisplay
469                 self._backend = backend
470
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
475
476         def enable(self):
477                 assert self._backend.is_authed()
478                 self._accountViewNumberDisplay.set_use_markup(True)
479                 self.set_account_number("")
480                 self._callbackList.clear()
481                 self.update()
482                 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
483
484         def disable(self):
485                 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
486                 self.clear()
487                 self._callbackList.clear()
488
489         def get_selected_callback_number(self):
490                 return make_ugly(self._callbackCombo.get_child().get_text())
491
492         def set_account_number(self, number):
493                 """
494                 Displays current account number
495                 """
496                 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
497
498         def update(self):
499                 self.populate_callback_combo()
500                 self.set_account_number(self._backend.get_account_number())
501
502         def clear(self):
503                 self._callbackCombo.get_child().set_text("")
504                 self.set_account_number("")
505
506         def populate_callback_combo(self):
507                 self._callbackList.clear()
508                 try:
509                         callbackNumbers = self._backend.get_callback_numbers()
510                 except RuntimeError, e:
511                         self._errorDisplay.push_exception(e)
512                         return
513
514                 for number, description in callbackNumbers.iteritems():
515                         self._callbackList.append((make_pretty(number),))
516
517                 self._callbackCombo.set_model(self._callbackList)
518                 self._callbackCombo.set_text_column(0)
519                 try:
520                         callbackNumber = self._backend.get_callback_number()
521                 except RuntimeError, e:
522                         self._errorDisplay.push_exception(e)
523                         return
524                 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
525
526         def _on_callbackentry_changed(self, *args):
527                 """
528                 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
529                 """
530                 try:
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)
536                         else:
537                                 self._backend.set_callback_number(text)
538                 except RuntimeError, e:
539                         self._errorDisplay.push_exception(e)
540
541
542 class RecentCallsView(object):
543
544         def __init__(self, widgetTree, backend, errorDisplay):
545                 self._errorDisplay = errorDisplay
546                 self._backend = backend
547
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
553
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)
559
560                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
561
562         def enable(self):
563                 assert self._backend.is_authed()
564                 self._recentview.set_model(self._recentmodel)
565
566                 self._recentview.append_column(self._recentviewColumn)
567                 self._recentviewselection = self._recentview.get_selection()
568                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
569
570                 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
571
572         def disable(self):
573                 self._recentview.disconnect(self._onRecentviewRowActivatedId)
574                 self._recentview.remove_column(self._recentviewColumn)
575                 self._recentview.set_model(None)
576
577         def number_selected(self, number):
578                 """
579                 @note Actual dial function is patched in later
580                 """
581                 raise NotImplementedError
582
583         def update(self):
584                 if (time.time() - self._recenttime) < 300:
585                         return
586                 backgroundPopulate = threading.Thread(target=self._idly_populate_recentview)
587                 backgroundPopulate.setDaemon(True)
588                 backgroundPopulate.start()
589
590         def clear(self):
591                 self._recenttime = 0.0
592                 self._recentmodel.clear()
593
594         def _idly_populate_recentview(self):
595                 self._recenttime = time.time()
596                 self._recentmodel.clear()
597
598                 try:
599                         recentItems = self._backend.get_recent()
600                 except RuntimeError, e:
601                         self._errorDisplay.push_exception_with_lock(e)
602                         self._recenttime = 0.0
603                         recentItems = []
604
605                 # @todo Sort these
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)
611
612                 return False
613
614         def _on_recentview_row_activated(self, treeview, path, view_column):
615                 model, itr = self._recentviewselection.get_selected()
616                 if not itr:
617                         return
618
619                 contactPhoneNumbers = [("Phone", self._recentmodel.get_value(itr, 0))]
620                 description = self._recentmodel.get_value(itr, 1)
621                 print repr(contactPhoneNumbers), repr(description)
622
623                 phoneNumber = self._phoneTypeSelector.run(contactPhoneNumbers, message = description)
624                 if 0 == len(phoneNumber):
625                         return
626
627                 self.number_selected(phoneNumber)
628                 self._recentviewselection.unselect_all()
629
630
631 class MessagesView(object):
632
633         def __init__(self, widgetTree, backend, errorDisplay):
634                 self._errorDisplay = errorDisplay
635                 self._backend = backend
636
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
642
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)
648
649                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
650
651         def enable(self):
652                 assert self._backend.is_authed()
653                 self._messageview.set_model(self._messagemodel)
654
655                 self._messageview.append_column(self._messageviewColumn)
656                 self._messageviewselection = self._messageview.get_selection()
657                 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
658
659                 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
660
661         def disable(self):
662                 self._messageview.disconnect(self._onMessageviewRowActivatedId)
663                 self._messageview.remove_column(self._messageviewColumn)
664                 self._messageview.set_model(None)
665
666         def number_selected(self, number):
667                 """
668                 @note Actual dial function is patched in later
669                 """
670                 raise NotImplementedError
671
672         def update(self):
673                 if (time.time() - self._messagetime) < 300:
674                         return
675                 backgroundPopulate = threading.Thread(target=self._idly_populate_messageview)
676                 backgroundPopulate.setDaemon(True)
677                 backgroundPopulate.start()
678
679         def clear(self):
680                 self._messagetime = 0.0
681                 self._messagemodel.clear()
682
683         def _idly_populate_messageview(self):
684                 self._messagetime = time.time()
685                 self._messagemodel.clear()
686
687                 try:
688                         messageItems = self._backend.get_messages()
689                 except RuntimeError, e:
690                         self._errorDisplay.push_exception_with_lock(e)
691                         self._messagetime = 0.0
692                         messageItems = []
693
694                 for phoneNumber, data in messageItems:
695                         item = (phoneNumber, data)
696                         with gtk_toolbox.gtk_lock():
697                                 self._messagemodel.append(item)
698
699                 return False
700
701         def _on_messageview_row_activated(self, treeview, path, view_column):
702                 model, itr = self._messageviewselection.get_selected()
703                 if not itr:
704                         return
705
706                 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, 0))]
707                 description = self._messagemodel.get_value(itr, 1)
708                 print repr(contactPhoneNumbers), repr(description)
709
710                 phoneNumber = self._phoneTypeSelector.run(contactPhoneNumbers, message = description)
711                 if 0 == len(phoneNumber):
712                         return
713
714                 self.number_selected(phoneNumber)
715                 self._messageviewselection.unselect_all()
716
717
718 class ContactsView(object):
719
720         def __init__(self, widgetTree, backend, errorDisplay):
721                 self._errorDisplay = errorDisplay
722                 self._backend = backend
723
724                 self._addressBook = None
725                 self._addressBookFactories = [DummyAddressBook()]
726
727                 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
728                 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
729
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")
734
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)
750
751                 self._onContactsviewRowActivatedId = 0
752                 self._onAddressbookComboChangedId = 0
753                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
754
755         def enable(self):
756                 assert self._backend.is_authed()
757
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)
762
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)
767                         elif factoryName:
768                                 entryName = factoryName
769                         elif bookName:
770                                 entryName = bookName
771                         else:
772                                 entryName = "Bad name (%d)" % factoryId
773                         row = (str(factoryId), bookId, entryName)
774                         self._booksList.append(row)
775
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)
781
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)
784
785         def disable(self):
786                 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
787                 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
788
789                 self._booksSelectionBox.clear()
790                 self._booksSelectionBox.set_model(None)
791                 self._contactsview.set_model(None)
792                 self._contactsview.remove_column(self._contactColumn)
793
794         def number_selected(self, number):
795                 """
796                 @note Actual dial function is patched in later
797                 """
798                 raise NotImplementedError
799
800         def get_addressbooks(self):
801                 """
802                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
803                 """
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)
807
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()
814
815         def update(self):
816                 if (time.time() - self._contactstime) < 300:
817                         return
818                 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
819                 backgroundPopulate.setDaemon(True)
820                 backgroundPopulate.start()
821
822         def clear(self):
823                 self._contactstime = 0.0
824                 self._contactsmodel.clear()
825
826         def clear_caches(self):
827                 for factory in self._addressBookFactories:
828                         factory.clear_caches()
829                 self._addressBook.clear_caches()
830
831         def append(self, book):
832                 self._addressBookFactories.append(book)
833
834         def extend(self, books):
835                 self._addressBookFactories.extend(books)
836
837         def _idly_populate_contactsview(self):
838                 #@todo Add a lock so only one code path can be in here at a time
839                 self.clear()
840
841                 # completely disable updating the treeview while we populate the data
842                 self._contactsview.freeze_child_notify()
843                 self._contactsview.set_model(None)
844
845                 addressBook = self._addressBook
846                 try:
847                         contacts = addressBook.get_contacts()
848                 except RuntimeError, e:
849                         contacts = []
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) + ("", ))
855
856                 # restart the treeview data rendering
857                 self._contactsview.set_model(self._contactsmodel)
858                 self._contactsview.thaw_child_notify()
859                 return False
860
861         def _on_addressbook_combo_changed(self, *args, **kwds):
862                 itr = self._booksSelectionBox.get_active_iter()
863                 if itr is None:
864                         return
865                 factoryId = int(self._booksList.get_value(itr, 0))
866                 bookId = self._booksList.get_value(itr, 1)
867                 self.open_addressbook(factoryId, bookId)
868
869         def _on_contactsview_row_activated(self, treeview, path, view_column):
870                 model, itr = self._contactsviewselection.get_selected()
871                 if not itr:
872                         return
873
874                 contactId = self._contactsmodel.get_value(itr, 3)
875                 contactName = self._contactsmodel.get_value(itr, 1)
876                 try:
877                         contactDetails = self._addressBook.get_contact_details(contactId)
878                 except RuntimeError, e:
879                         contactDetails = []
880                         self._contactstime = 0.0
881                         self._errorDisplay.push_exception(e)
882                 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
883
884                 if len(contactPhoneNumbers) == 0:
885                         phoneNumber = ""
886                 elif len(contactPhoneNumbers) == 1:
887                         phoneNumber = self._phoneTypeSelector.run(contactPhoneNumbers, message = contactName)
888
889                 if 0 == len(phoneNumber):
890                         return
891
892                 self.number_selected(phoneNumber)
893                 self._contactsviewselection.unselect_all()