* Starting a messages view
[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                 phoneNumber = self._typemodel.get_value(itr, 0)
377                 return phonenumber
378
379         def _on_phonetype_dial(self, *args):
380                 self.dial(self._get_number())
381                 self._dialog.response(gtk.RESPONSE_CANCEL)
382
383         def _on_phonetype_select(self, *args):
384                 self._dialog.response(gtk.RESPONSE_OK)
385
386         def _on_phonetype_cancel(self, *args):
387                 self._dialog.response(gtk.RESPONSE_CANCEL)
388
389
390 class Dialpad(object):
391
392         def __init__(self, widgetTree, errorDisplay):
393                 self._errorDisplay = errorDisplay
394                 self._numberdisplay = widgetTree.get_widget("numberdisplay")
395                 self._dialButton = widgetTree.get_widget("dial")
396                 self._phonenumber = ""
397                 self._prettynumber = ""
398                 self._clearall_id = None
399
400                 callbackMapping = {
401                         "on_dial_clicked": self._on_dial_clicked,
402                         "on_digit_clicked": self._on_digit_clicked,
403                         "on_clear_number": self._on_clear_number,
404                         "on_back_clicked": self._on_backspace,
405                         "on_back_pressed": self._on_back_pressed,
406                         "on_back_released": self._on_back_released,
407                 }
408                 widgetTree.signal_autoconnect(callbackMapping)
409
410         def enable(self):
411                 self._dialButton.grab_focus()
412
413         def disable(self):
414                 pass
415
416         def dial(self, number):
417                 """
418                 @note Actual dial function is patched in later
419                 """
420                 raise NotImplementedError
421
422         def get_number(self):
423                 return self._phonenumber
424
425         def set_number(self, number):
426                 """
427                 Set the callback phonenumber
428                 """
429                 try:
430                         self._phonenumber = make_ugly(number)
431                         self._prettynumber = make_pretty(self._phonenumber)
432                         self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
433                 except TypeError, e:
434                         self._errorDisplay.push_exception(e)
435
436         def clear(self):
437                 self.set_number("")
438
439         def _on_dial_clicked(self, widget):
440                 self.dial(self.get_number())
441
442         def _on_clear_number(self, *args):
443                 self.clear()
444
445         def _on_digit_clicked(self, widget):
446                 self.set_number(self._phonenumber + widget.get_name()[-1])
447
448         def _on_backspace(self, widget):
449                 self.set_number(self._phonenumber[:-1])
450
451         def _on_clearall(self):
452                 self.clear()
453                 return False
454
455         def _on_back_pressed(self, widget):
456                 self._clearall_id = gobject.timeout_add(1000, self._on_clearall)
457
458         def _on_back_released(self, widget):
459                 if self._clearall_id is not None:
460                         gobject.source_remove(self._clearall_id)
461                 self._clearall_id = None
462
463
464 class AccountInfo(object):
465
466         def __init__(self, widgetTree, backend, errorDisplay):
467                 self._errorDisplay = errorDisplay
468                 self._backend = backend
469
470                 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
471                 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
472                 self._callbackCombo = widgetTree.get_widget("callbackcombo")
473                 self._onCallbackentryChangedId = 0
474
475         def enable(self):
476                 assert self._backend.is_authed()
477                 self._accountViewNumberDisplay.set_use_markup(True)
478                 self.set_account_number("")
479                 self._callbackList.clear()
480                 self.update()
481                 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
482
483         def disable(self):
484                 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
485                 self.clear()
486                 self._callbackList.clear()
487
488         def get_selected_callback_number(self):
489                 return make_ugly(self._callbackCombo.get_child().get_text())
490
491         def set_account_number(self, number):
492                 """
493                 Displays current account number
494                 """
495                 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
496
497         def update(self):
498                 self.populate_callback_combo()
499                 self.set_account_number(self._backend.get_account_number())
500
501         def clear(self):
502                 self._callbackCombo.get_child().set_text("")
503                 self.set_account_number("")
504
505         def populate_callback_combo(self):
506                 self._callbackList.clear()
507                 try:
508                         callbackNumbers = self._backend.get_callback_numbers()
509                 except RuntimeError, e:
510                         self._errorDisplay.push_exception(e)
511                         return
512
513                 for number, description in callbackNumbers.iteritems():
514                         self._callbackList.append((make_pretty(number),))
515
516                 self._callbackCombo.set_model(self._callbackList)
517                 self._callbackCombo.set_text_column(0)
518                 try:
519                         callbackNumber = self._backend.get_callback_number()
520                 except RuntimeError, e:
521                         self._errorDisplay.push_exception(e)
522                         return
523                 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
524
525         def _on_callbackentry_changed(self, *args):
526                 """
527                 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
528                 """
529                 try:
530                         text = self.get_selected_callback_number()
531                         if not self._backend.is_valid_syntax(text):
532                                 self._errorDisplay.push_message("%s is not a valid callback number" % text)
533                         elif text == self._backend.get_callback_number():
534                                 warnings.warn("Callback number already is %s" % self._backend.get_callback_number(), UserWarning, 2)
535                         else:
536                                 self._backend.set_callback_number(text)
537                 except RuntimeError, e:
538                         self._errorDisplay.push_exception(e)
539
540
541 class RecentCallsView(object):
542
543         def __init__(self, widgetTree, backend, errorDisplay):
544                 self._errorDisplay = errorDisplay
545                 self._backend = backend
546
547                 self._recenttime = 0.0
548                 self._recentmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
549                 self._recentview = widgetTree.get_widget("recentview")
550                 self._recentviewselection = None
551                 self._onRecentviewRowActivatedId = 0
552
553                 textrenderer = gtk.CellRendererText()
554                 self._recentviewColumn = gtk.TreeViewColumn("Calls")
555                 self._recentviewColumn.pack_start(textrenderer, expand=True)
556                 self._recentviewColumn.add_attribute(textrenderer, "text", 1)
557                 self._recentviewColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
558
559                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
560
561         def enable(self):
562                 assert self._backend.is_authed()
563                 self._recentview.set_model(self._recentmodel)
564
565                 self._recentview.append_column(self._recentviewColumn)
566                 self._recentviewselection = self._recentview.get_selection()
567                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
568
569                 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
570
571         def disable(self):
572                 self._recentview.disconnect(self._onRecentviewRowActivatedId)
573                 self._recentview.remove_column(self._recentviewColumn)
574                 self._recentview.set_model(None)
575
576         def number_selected(self, number):
577                 """
578                 @note Actual dial function is patched in later
579                 """
580                 raise NotImplementedError
581
582         def update(self):
583                 if (time.time() - self._recenttime) < 300:
584                         return
585                 backgroundPopulate = threading.Thread(target=self._idly_populate_recentview)
586                 backgroundPopulate.setDaemon(True)
587                 backgroundPopulate.start()
588
589         def clear(self):
590                 self._recenttime = 0.0
591                 self._recentmodel.clear()
592
593         def _idly_populate_recentview(self):
594                 self._recenttime = time.time()
595                 self._recentmodel.clear()
596
597                 try:
598                         recentItems = self._backend.get_recent()
599                 except RuntimeError, e:
600                         self._errorDisplay.push_exception_with_lock(e)
601                         self._recenttime = 0.0
602                         recentItems = []
603
604                 for personsName, phoneNumber, date, action in recentItems:
605                         description = "%s on %s from/to %s - %s" % (action.capitalize(), date, personsName, phoneNumber)
606                         item = (phoneNumber, description)
607                         with gtk_toolbox.gtk_lock():
608                                 self._recentmodel.append(item)
609
610                 return False
611
612         def _on_recentview_row_activated(self, treeview, path, view_column):
613                 model, itr = self._recentviewselection.get_selected()
614                 if not itr:
615                         return
616
617                 contactPhoneNumbers = [("Phone", self._recentmodel.get_value(itr, 0))]
618                 description = self._recentmodel.get_value(itr, 1)
619                 print repr(contactPhoneNumbers), repr(description)
620
621                 phoneNumber = self._phoneTypeSelector.run(contactPhoneNumbers, message = description)
622                 if 0 == len(phoneNumber):
623                         return
624
625                 self.number_selected(phoneNumber)
626                 self._recentviewselection.unselect_all()
627
628
629 class MessagesView(object):
630
631         def __init__(self, widgetTree, backend, errorDisplay):
632                 self._errorDisplay = errorDisplay
633                 self._backend = backend
634
635                 self._messagetime = 0.0
636                 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
637                 self._messageview = widgetTree.get_widget("messages_view")
638                 self._messageviewselection = None
639                 self._onRcentviewRowActivatedId = 0
640
641                 textrenderer = gtk.CellRendererText()
642                 self._messageviewColumn = gtk.TreeViewColumn("Messages")
643                 self._messageviewColumn.pack_start(textrenderer, expand=True)
644                 self._messageviewColumn.add_attribute(textrenderer, "text", 1)
645                 self._messageviewColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
646
647                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
648
649         def enable(self):
650                 assert self._backend.is_authed()
651                 self._messageview.set_model(self._messagemodel)
652
653                 self._messageview.append_column(self._messageviewColumn)
654                 self._messageviewselection = self._messageview.get_selection()
655                 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
656
657                 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
658
659         def disable(self):
660                 self._messageview.disconnect(self._onMessageviewRowActivatedId)
661                 self._messageview.remove_column(self._messageviewColumn)
662                 self._messageview.set_model(None)
663
664         def number_selected(self, number):
665                 """
666                 @note Actual dial function is patched in later
667                 """
668                 raise NotImplementedError
669
670         def update(self):
671                 if (time.time() - self._messagetime) < 300:
672                         return
673                 backgroundPopulate = threading.Thread(target=self._idly_populate_messageview)
674                 backgroundPopulate.setDaemon(True)
675                 backgroundPopulate.start()
676
677         def clear(self):
678                 self._messagetime = 0.0
679                 self._messagemodel.clear()
680
681         def _idly_populate_messageview(self):
682                 self._messagetime = time.time()
683                 self._messagemodel.clear()
684
685                 try:
686                         messageItems = self._backend.get_messages()
687                 except RuntimeError, e:
688                         self._errorDisplay.push_exception_with_lock(e)
689                         self._messagetime = 0.0
690                         messageItems = []
691
692                 for phoneNumber, date in messageItems:
693                         item = (phoneNumber, data)
694                         with gtk_toolbox.gtk_lock():
695                                 self._messagemodel.append(item)
696
697                 return False
698
699         def _on_messageview_row_activated(self, treeview, path, view_column):
700                 model, itr = self._messageviewselection.get_selected()
701                 if not itr:
702                         return
703
704                 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, 0))]
705                 description = self._messagemodel.get_value(itr, 1)
706                 print repr(contactPhoneNumbers), repr(description)
707
708                 phoneNumber = self._phoneTypeSelector.run(contactPhoneNumbers, message = description)
709                 if 0 == len(phoneNumber):
710                         return
711
712                 self.number_selected(phoneNumber)
713                 self._messageviewselection.unselect_all()
714
715
716 class ContactsView(object):
717
718         def __init__(self, widgetTree, backend, errorDisplay):
719                 self._errorDisplay = errorDisplay
720                 self._backend = backend
721
722                 self._addressBook = None
723                 self._addressBookFactories = [DummyAddressBook()]
724
725                 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
726                 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
727
728                 self._contactstime = 0.0
729                 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
730                 self._contactsviewselection = None
731                 self._contactsview = widgetTree.get_widget("contactsview")
732
733                 self._contactColumn = gtk.TreeViewColumn("Contact")
734                 displayContactSource = False
735                 if displayContactSource:
736                         textrenderer = gtk.CellRendererText()
737                         self._contactColumn.pack_start(textrenderer, expand=False)
738                         self._contactColumn.add_attribute(textrenderer, 'text', 0)
739                 textrenderer = gtk.CellRendererText()
740                 self._contactColumn.pack_start(textrenderer, expand=True)
741                 self._contactColumn.add_attribute(textrenderer, 'text', 1)
742                 textrenderer = gtk.CellRendererText()
743                 self._contactColumn.pack_start(textrenderer, expand=True)
744                 self._contactColumn.add_attribute(textrenderer, 'text', 4)
745                 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
746                 self._contactColumn.set_sort_column_id(1)
747                 self._contactColumn.set_visible(True)
748
749                 self._onContactsviewRowActivatedId = 0
750                 self._onAddressbookComboChangedId = 0
751                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
752
753         def enable(self):
754                 assert self._backend.is_authed()
755
756                 self._contactsview.set_model(self._contactsmodel)
757                 self._contactsview.append_column(self._contactColumn)
758                 self._contactsviewselection = self._contactsview.get_selection()
759                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
760
761                 self._booksList.clear()
762                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
763                         if factoryName and bookName:
764                                 entryName = "%s: %s" % (factoryName, bookName)
765                         elif factoryName:
766                                 entryName = factoryName
767                         elif bookName:
768                                 entryName = bookName
769                         else:
770                                 entryName = "Bad name (%d)" % factoryId
771                         row = (str(factoryId), bookId, entryName)
772                         self._booksList.append(row)
773
774                 self._booksSelectionBox.set_model(self._booksList)
775                 cell = gtk.CellRendererText()
776                 self._booksSelectionBox.pack_start(cell, True)
777                 self._booksSelectionBox.add_attribute(cell, 'text', 2)
778                 self._booksSelectionBox.set_active(0)
779
780                 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
781                 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
782
783         def disable(self):
784                 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
785                 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
786
787                 self._booksSelectionBox.clear()
788                 self._booksSelectionBox.set_model(None)
789                 self._contactsview.set_model(None)
790                 self._contactsview.remove_column(self._contactColumn)
791
792         def number_selected(self, number):
793                 """
794                 @note Actual dial function is patched in later
795                 """
796                 raise NotImplementedError
797
798         def get_addressbooks(self):
799                 """
800                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
801                 """
802                 for i, factory in enumerate(self._addressBookFactories):
803                         for bookFactory, bookId, bookName in factory.get_addressbooks():
804                                 yield (i, bookId), (factory.factory_name(), bookName)
805
806         def open_addressbook(self, bookFactoryId, bookId):
807                 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
808                 self._contactstime = 0
809                 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
810                 backgroundPopulate.setDaemon(True)
811                 backgroundPopulate.start()
812
813         def update(self):
814                 if (time.time() - self._contactstime) < 300:
815                         return
816                 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
817                 backgroundPopulate.setDaemon(True)
818                 backgroundPopulate.start()
819
820         def clear(self):
821                 self._contactstime = 0.0
822                 self._contactsmodel.clear()
823
824         def clear_caches(self):
825                 for factory in self._addressBookFactories:
826                         factory.clear_caches()
827                 self._addressBook.clear_caches()
828
829         def append(self, book):
830                 self._addressBookFactories.append(book)
831
832         def extend(self, books):
833                 self._addressBookFactories.extend(books)
834
835         def _idly_populate_contactsview(self):
836                 #@todo Add a lock so only one code path can be in here at a time
837                 self.clear()
838
839                 # completely disable updating the treeview while we populate the data
840                 self._contactsview.freeze_child_notify()
841                 self._contactsview.set_model(None)
842
843                 addressBook = self._addressBook
844                 try:
845                         contacts = addressBook.get_contacts()
846                 except RuntimeError, e:
847                         contacts = []
848                         self._contactstime = 0.0
849                         self._errorDisplay.push_exception_with_lock(e)
850                 for contactId, contactName in contacts:
851                         contactType = (addressBook.contact_source_short_name(contactId), )
852                         self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
853
854                 # restart the treeview data rendering
855                 self._contactsview.set_model(self._contactsmodel)
856                 self._contactsview.thaw_child_notify()
857                 return False
858
859         def _on_addressbook_combo_changed(self, *args, **kwds):
860                 itr = self._booksSelectionBox.get_active_iter()
861                 if itr is None:
862                         return
863                 factoryId = int(self._booksList.get_value(itr, 0))
864                 bookId = self._booksList.get_value(itr, 1)
865                 self.open_addressbook(factoryId, bookId)
866
867         def _on_contactsview_row_activated(self, treeview, path, view_column):
868                 model, itr = self._contactsviewselection.get_selected()
869                 if not itr:
870                         return
871
872                 contactId = self._contactsmodel.get_value(itr, 3)
873                 contactName = self._contactsmodel.get_value(itr, 1)
874                 try:
875                         contactDetails = self._addressBook.get_contact_details(contactId)
876                 except RuntimeError, e:
877                         contactDetails = []
878                         self._contactstime = 0.0
879                         self._errorDisplay.push_exception(e)
880                 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
881
882                 if len(contactPhoneNumbers) == 0:
883                         phoneNumber = ""
884                 elif len(contactPhoneNumbers) == 1:
885                         phoneNumber = self._phoneTypeSelector.run(contactPhoneNumbers, message = contactName)
886
887                 if 0 == len(phoneNumber):
888                         return
889
890                 self.number_selected(phoneNumber)
891                 self._contactsviewselection.unselect_all()