Minor cleanups
[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
33 import gobject
34 import gtk
35
36 import gtk_toolbox
37
38
39 def make_ugly(prettynumber):
40         """
41         function to take a phone number and strip out all non-numeric
42         characters
43
44         >>> make_ugly("+012-(345)-678-90")
45         '01234567890'
46         """
47         import re
48         uglynumber = re.sub('\D', '', prettynumber)
49         return uglynumber
50
51
52 def make_pretty(phonenumber):
53         """
54         Function to take a phone number and return the pretty version
55         pretty numbers:
56                 if phonenumber begins with 0:
57                         ...-(...)-...-....
58                 if phonenumber begins with 1: ( for gizmo callback numbers )
59                         1 (...)-...-....
60                 if phonenumber is 13 digits:
61                         (...)-...-....
62                 if phonenumber is 10 digits:
63                         ...-....
64         >>> make_pretty("12")
65         '12'
66         >>> make_pretty("1234567")
67         '123-4567'
68         >>> make_pretty("2345678901")
69         '(234)-567-8901'
70         >>> make_pretty("12345678901")
71         '1 (234)-567-8901'
72         >>> make_pretty("01234567890")
73         '+012-(345)-678-90'
74         """
75         if phonenumber is None or phonenumber is "":
76                 return ""
77
78         phonenumber = make_ugly(phonenumber)
79
80         if len(phonenumber) < 3:
81                 return phonenumber
82
83         if phonenumber[0] == "0":
84                 prettynumber = ""
85                 prettynumber += "+%s" % phonenumber[0:3]
86                 if 3 < len(phonenumber):
87                         prettynumber += "-(%s)" % phonenumber[3:6]
88                         if 6 < len(phonenumber):
89                                 prettynumber += "-%s" % phonenumber[6:9]
90                                 if 9 < len(phonenumber):
91                                         prettynumber += "-%s" % phonenumber[9:]
92                 return prettynumber
93         elif len(phonenumber) <= 7:
94                 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
95         elif len(phonenumber) > 8 and phonenumber[0] == "1":
96                 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
97         elif len(phonenumber) > 7:
98                 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
99         return prettynumber
100
101
102 def make_idler(func):
103         """
104         Decorator that makes a generator-function into a function that will continue execution on next call
105         """
106         a = []
107
108         def decorated_func(*args, **kwds):
109                 if not a:
110                         a.append(func(*args, **kwds))
111                 try:
112                         a[0].next()
113                         return True
114                 except StopIteration:
115                         del a[:]
116                         return False
117
118         decorated_func.__name__ = func.__name__
119         decorated_func.__doc__ = func.__doc__
120         decorated_func.__dict__.update(func.__dict__)
121
122         return decorated_func
123
124
125 class DummyAddressBook(object):
126         """
127         Minimal example of both an addressbook factory and an addressbook
128         """
129
130         def clear_caches(self):
131                 pass
132
133         def get_addressbooks(self):
134                 """
135                 @returns Iterable of (Address Book Factory, Book Id, Book Name)
136                 """
137                 yield self, "", "None"
138
139         def open_addressbook(self, bookId):
140                 return self
141
142         @staticmethod
143         def contact_source_short_name(contactId):
144                 return ""
145
146         @staticmethod
147         def factory_name():
148                 return ""
149
150         @staticmethod
151         def get_contacts():
152                 """
153                 @returns Iterable of (contact id, contact name)
154                 """
155                 return []
156
157         @staticmethod
158         def get_contact_details(contactId):
159                 """
160                 @returns Iterable of (Phone Type, Phone Number)
161                 """
162                 return []
163
164
165 class MergedAddressBook(object):
166         """
167         Merger of all addressbooks
168         """
169
170         def __init__(self, addressbookFactories, sorter = None):
171                 self.__addressbookFactories = addressbookFactories
172                 self.__addressbooks = None
173                 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
174
175         def clear_caches(self):
176                 self.__addressbooks = None
177                 for factory in self.__addressbookFactories:
178                         factory.clear_caches()
179
180         def get_addressbooks(self):
181                 """
182                 @returns Iterable of (Address Book Factory, Book Id, Book Name)
183                 """
184                 yield self, "", ""
185
186         def open_addressbook(self, bookId):
187                 return self
188
189         def contact_source_short_name(self, contactId):
190                 if self.__addressbooks is None:
191                         return ""
192                 bookIndex, originalId = contactId.split("-", 1)
193                 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
194
195         @staticmethod
196         def factory_name():
197                 return "All Contacts"
198
199         def get_contacts(self):
200                 """
201                 @returns Iterable of (contact id, contact name)
202                 """
203                 if self.__addressbooks is None:
204                         self.__addressbooks = list(
205                                 factory.open_addressbook(id)
206                                 for factory in self.__addressbookFactories
207                                 for (f, id, name) in factory.get_addressbooks()
208                         )
209                 contacts = (
210                         ("-".join([str(bookIndex), contactId]), contactName)
211                                 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
212                                         for (contactId, contactName) in addressbook.get_contacts()
213                 )
214                 sortedContacts = self.__sort_contacts(contacts)
215                 return sortedContacts
216
217         def get_contact_details(self, contactId):
218                 """
219                 @returns Iterable of (Phone Type, Phone Number)
220                 """
221                 if self.__addressbooks is None:
222                         return []
223                 bookIndex, originalId = contactId.split("-", 1)
224                 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
225
226         @staticmethod
227         def null_sorter(contacts):
228                 """
229                 Good for speed/low memory
230                 """
231                 return contacts
232
233         @staticmethod
234         def basic_firtname_sorter(contacts):
235                 """
236                 Expects names in "First Last" format
237                 """
238                 contactsWithKey = [
239                         (contactName.rsplit(" ", 1)[0], (contactId, contactName))
240                                 for (contactId, contactName) in contacts
241                 ]
242                 contactsWithKey.sort()
243                 return (contactData for (lastName, contactData) in contactsWithKey)
244
245         @staticmethod
246         def basic_lastname_sorter(contacts):
247                 """
248                 Expects names in "First Last" format
249                 """
250                 contactsWithKey = [
251                         (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
252                                 for (contactId, contactName) in contacts
253                 ]
254                 contactsWithKey.sort()
255                 return (contactData for (lastName, contactData) in contactsWithKey)
256
257         @staticmethod
258         def reversed_firtname_sorter(contacts):
259                 """
260                 Expects names in "Last, First" format
261                 """
262                 contactsWithKey = [
263                         (contactName.split(", ", 1)[-1], (contactId, contactName))
264                                 for (contactId, contactName) in contacts
265                 ]
266                 contactsWithKey.sort()
267                 return (contactData for (lastName, contactData) in contactsWithKey)
268
269         @staticmethod
270         def reversed_lastname_sorter(contacts):
271                 """
272                 Expects names in "Last, First" format
273                 """
274                 contactsWithKey = [
275                         (contactName.split(", ", 1)[0], (contactId, contactName))
276                                 for (contactId, contactName) in contacts
277                 ]
278                 contactsWithKey.sort()
279                 return (contactData for (lastName, contactData) in contactsWithKey)
280
281         @staticmethod
282         def guess_firstname(name):
283                 if ", " in name:
284                         return name.split(", ", 1)[-1]
285                 else:
286                         return name.rsplit(" ", 1)[0]
287
288         @staticmethod
289         def guess_lastname(name):
290                 if ", " in name:
291                         return name.split(", ", 1)[0]
292                 else:
293                         return name.rsplit(" ", 1)[-1]
294
295         @classmethod
296         def advanced_firstname_sorter(cls, contacts):
297                 contactsWithKey = [
298                         (cls.guess_firstname(contactName), (contactId, contactName))
299                                 for (contactId, contactName) in contacts
300                 ]
301                 contactsWithKey.sort()
302                 return (contactData for (lastName, contactData) in contactsWithKey)
303
304         @classmethod
305         def advanced_lastname_sorter(cls, contacts):
306                 contactsWithKey = [
307                         (cls.guess_lastname(contactName), (contactId, contactName))
308                                 for (contactId, contactName) in contacts
309                 ]
310                 contactsWithKey.sort()
311                 return (contactData for (lastName, contactData) in contactsWithKey)
312
313
314 class PhoneTypeSelector(object):
315
316         def __init__(self, widgetTree, gcBackend):
317                 self._gcBackend = gcBackend
318                 self._widgetTree = widgetTree
319
320                 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
321                 self._smsDialog = SmsEntryDialog(self._widgetTree, self._gcBackend)
322
323                 self._smsButton = self._widgetTree.get_widget("sms_button")
324                 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
325
326                 self._dialButton = self._widgetTree.get_widget("dial_button")
327                 self._dialButton.connect("clicked", self._on_phonetype_dial)
328
329                 self._selectButton = self._widgetTree.get_widget("select_button")
330                 self._selectButton.connect("clicked", self._on_phonetype_select)
331
332                 self._cancelButton = self._widgetTree.get_widget("cancel_button")
333                 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
334
335                 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
336                 self._typeviewselection = None
337
338                 self._message = self._widgetTree.get_widget("phoneSelectionMessage")
339                 typeview = self._widgetTree.get_widget("phonetypes")
340                 typeview.connect("row-activated", self._on_phonetype_select)
341                 typeview.set_model(self._typemodel)
342                 textrenderer = gtk.CellRendererText()
343
344                 # Add the column to the treeview
345                 column = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=1)
346                 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
347
348                 typeview.append_column(column)
349
350                 self._typeviewselection = typeview.get_selection()
351                 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
352
353         def run(self, contactDetails, message = ""):
354                 self._typemodel.clear()
355
356                 for phoneType, phoneNumber in contactDetails:
357                         self._typemodel.append((phoneNumber, "%s - %s" % (make_pretty(phoneNumber), phoneType)))
358
359                 if message:
360                         self._message.show()
361                         self._message.set_text(message)
362                 else:
363                         self._message.hide()
364
365                 userResponse = self._dialog.run()
366
367                 if userResponse == gtk.RESPONSE_OK:
368                         phoneNumber = self._get_number()
369                 else:
370                         phoneNumber = ""
371
372                 self._typeviewselection.unselect_all()
373                 self._dialog.hide()
374                 return phoneNumber
375
376         def _get_number(self):
377                 model, itr = self._typeviewselection.get_selected()
378                 if not itr:
379                         return ""
380
381                 phoneNumber = self._typemodel.get_value(itr, 0)
382                 return phoneNumber
383
384         def _on_phonetype_dial(self, *args):
385                 self._gcBackend.dial(self._get_number())
386                 self._dialog.response(gtk.RESPONSE_CANCEL)
387
388         def _on_phonetype_send_sms(self, *args):
389                 self._dialog.response(gtk.RESPONSE_CANCEL)
390                 idly_run = gtk_toolbox.asynchronous_gtk_message(self._smsDialog.run)
391                 idly_run(self._get_number(), self._message.get_text())
392
393         def _on_phonetype_select(self, *args):
394                 self._dialog.response(gtk.RESPONSE_OK)
395
396         def _on_phonetype_cancel(self, *args):
397                 self._dialog.response(gtk.RESPONSE_CANCEL)
398
399
400 class SmsEntryDialog(object):
401
402         MAX_CHAR = 160
403
404         def __init__(self, widgetTree, gcBackend):
405                 self._gcBackend = gcBackend
406                 self._widgetTree = widgetTree
407                 self._dialog = self._widgetTree.get_widget("smsDialog")
408
409                 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
410                 self._smsButton.connect("clicked", self._on_send)
411
412                 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
413                 self._cancelButton.connect("clicked", self._on_cancel)
414
415                 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
416                 self._message = self._widgetTree.get_widget("smsMessage")
417                 self._smsEntry = self._widgetTree.get_widget("smsEntry")
418                 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
419
420         def run(self, number, message = ""):
421                 if message:
422                         self._message.show()
423                         self._message.set_text(message)
424                 else:
425                         self._message.hide()
426                 self._smsEntry.get_buffer().set_text("")
427                 self._update_letter_count()
428
429                 userResponse = self._dialog.run()
430                 if userResponse == gtk.RESPONSE_OK:
431                         entryBuffer = self._smsEntry.get_buffer()
432                         enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
433                         enteredMessage = enteredMessage[0:self.MAX_CHAR]
434                         self._gcBackend.send_sms(number, enteredMessage)
435
436                 self._dialog.hide()
437
438         def _update_letter_count(self, *args):
439                 entryLength = self._smsEntry.get_buffer().get_char_count()
440                 self._letterCountLabel.set_text(str(self.MAX_CHAR - entryLength))
441
442         def _on_entry_changed(self, *args):
443                 self._update_letter_count()
444
445         def _on_send(self, *args):
446                 self._dialog.response(gtk.RESPONSE_OK)
447
448         def _on_cancel(self, *args):
449                 self._dialog.response(gtk.RESPONSE_CANCEL)
450
451
452 class Dialpad(object):
453
454         def __init__(self, widgetTree, errorDisplay):
455                 self._errorDisplay = errorDisplay
456                 self._numberdisplay = widgetTree.get_widget("numberdisplay")
457                 self._dialButton = widgetTree.get_widget("dial")
458                 self._phonenumber = ""
459                 self._prettynumber = ""
460                 self._clearall_id = None
461
462                 callbackMapping = {
463                         "on_dial_clicked": self._on_dial_clicked,
464                         "on_digit_clicked": self._on_digit_clicked,
465                         "on_clear_number": self._on_clear_number,
466                         "on_back_clicked": self._on_backspace,
467                         "on_back_pressed": self._on_back_pressed,
468                         "on_back_released": self._on_back_released,
469                 }
470                 widgetTree.signal_autoconnect(callbackMapping)
471
472         def enable(self):
473                 self._dialButton.grab_focus()
474
475         def disable(self):
476                 pass
477
478         def dial(self, number):
479                 """
480                 @note Actual dial function is patched in later
481                 """
482                 raise NotImplementedError
483
484         def get_number(self):
485                 return self._phonenumber
486
487         def set_number(self, number):
488                 """
489                 Set the callback phonenumber
490                 """
491                 try:
492                         self._phonenumber = make_ugly(number)
493                         self._prettynumber = make_pretty(self._phonenumber)
494                         self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
495                 except TypeError, e:
496                         self._errorDisplay.push_exception(e)
497
498         def clear(self):
499                 self.set_number("")
500
501         def _on_dial_clicked(self, widget):
502                 self.dial(self.get_number())
503
504         def _on_clear_number(self, *args):
505                 self.clear()
506
507         def _on_digit_clicked(self, widget):
508                 self.set_number(self._phonenumber + widget.get_name()[-1])
509
510         def _on_backspace(self, widget):
511                 self.set_number(self._phonenumber[:-1])
512
513         def _on_clearall(self):
514                 self.clear()
515                 return False
516
517         def _on_back_pressed(self, widget):
518                 self._clearall_id = gobject.timeout_add(1000, self._on_clearall)
519
520         def _on_back_released(self, widget):
521                 if self._clearall_id is not None:
522                         gobject.source_remove(self._clearall_id)
523                 self._clearall_id = None
524
525
526 class AccountInfo(object):
527
528         def __init__(self, widgetTree, backend, errorDisplay):
529                 self._errorDisplay = errorDisplay
530                 self._backend = backend
531
532                 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
533                 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
534                 self._callbackCombo = widgetTree.get_widget("callbackcombo")
535                 self._onCallbackentryChangedId = 0
536
537         def enable(self):
538                 assert self._backend.is_authed()
539                 self._accountViewNumberDisplay.set_use_markup(True)
540                 self.set_account_number("")
541                 self._callbackList.clear()
542                 self.update()
543                 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
544
545         def disable(self):
546                 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
547                 self.clear()
548                 self._callbackList.clear()
549
550         def get_selected_callback_number(self):
551                 return make_ugly(self._callbackCombo.get_child().get_text())
552
553         def set_account_number(self, number):
554                 """
555                 Displays current account number
556                 """
557                 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
558
559         def update(self):
560                 self.populate_callback_combo()
561                 self.set_account_number(self._backend.get_account_number())
562
563         def clear(self):
564                 self._callbackCombo.get_child().set_text("")
565                 self.set_account_number("")
566
567         def populate_callback_combo(self):
568                 self._callbackList.clear()
569                 try:
570                         callbackNumbers = self._backend.get_callback_numbers()
571                 except RuntimeError, e:
572                         self._errorDisplay.push_exception(e)
573                         return
574
575                 for number, description in callbackNumbers.iteritems():
576                         self._callbackList.append((make_pretty(number),))
577
578                 self._callbackCombo.set_model(self._callbackList)
579                 self._callbackCombo.set_text_column(0)
580                 try:
581                         callbackNumber = self._backend.get_callback_number()
582                 except RuntimeError, e:
583                         self._errorDisplay.push_exception(e)
584                         return
585                 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
586
587         def _on_callbackentry_changed(self, *args):
588                 """
589                 @todo Potential blocking on web access, maybe we should defer this or put up a dialog?
590                 """
591                 try:
592                         text = self.get_selected_callback_number()
593                         if not self._backend.is_valid_syntax(text):
594                                 self._errorDisplay.push_message("%s is not a valid callback number" % text)
595                         elif text == self._backend.get_callback_number():
596                                 warnings.warn("Callback number already is %s" % self._backend.get_callback_number(), UserWarning, 2)
597                         else:
598                                 self._backend.set_callback_number(text)
599                 except RuntimeError, e:
600                         self._errorDisplay.push_exception(e)
601
602
603 class RecentCallsView(object):
604
605         def __init__(self, widgetTree, backend, errorDisplay):
606                 self._errorDisplay = errorDisplay
607                 self._backend = backend
608
609                 self._recenttime = 0.0
610                 self._recentmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
611                 self._recentview = widgetTree.get_widget("recentview")
612                 self._recentviewselection = None
613                 self._onRecentviewRowActivatedId = 0
614
615                 textrenderer = gtk.CellRendererText()
616                 self._recentviewColumn = gtk.TreeViewColumn("Calls")
617                 self._recentviewColumn.pack_start(textrenderer, expand=True)
618                 self._recentviewColumn.add_attribute(textrenderer, "text", 1)
619                 self._recentviewColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
620
621                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
622
623         def enable(self):
624                 assert self._backend.is_authed()
625                 self._recentview.set_model(self._recentmodel)
626
627                 self._recentview.append_column(self._recentviewColumn)
628                 self._recentviewselection = self._recentview.get_selection()
629                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
630
631                 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
632
633         def disable(self):
634                 self._recentview.disconnect(self._onRecentviewRowActivatedId)
635                 self._recentview.remove_column(self._recentviewColumn)
636                 self._recentview.set_model(None)
637
638         def number_selected(self, number):
639                 """
640                 @note Actual dial function is patched in later
641                 """
642                 raise NotImplementedError
643
644         def update(self):
645                 if (time.time() - self._recenttime) < 300:
646                         return
647                 backgroundPopulate = threading.Thread(target=self._idly_populate_recentview)
648                 backgroundPopulate.setDaemon(True)
649                 backgroundPopulate.start()
650
651         def clear(self):
652                 self._recenttime = 0.0
653                 self._recentmodel.clear()
654
655         def _idly_populate_recentview(self):
656                 self._recenttime = time.time()
657                 self._recentmodel.clear()
658
659                 try:
660                         recentItems = self._backend.get_recent()
661                 except RuntimeError, e:
662                         self._errorDisplay.push_exception_with_lock(e)
663                         self._recenttime = 0.0
664                         recentItems = []
665
666                 for personsName, phoneNumber, date, action in recentItems:
667                         description = "%s on %s from/to %s - %s" % (action.capitalize(), date, personsName, phoneNumber)
668                         item = (phoneNumber, description)
669                         with gtk_toolbox.gtk_lock():
670                                 self._recentmodel.append(item)
671
672                 return False
673
674         def _on_recentview_row_activated(self, treeview, path, view_column):
675                 model, itr = self._recentviewselection.get_selected()
676                 if not itr:
677                         return
678
679                 contactPhoneNumbers = [("Phone", self._recentmodel.get_value(itr, 0))]
680                 description = self._recentmodel.get_value(itr, 1)
681                 print repr(contactPhoneNumbers), repr(description)
682
683                 phoneNumber = self._phoneTypeSelector.run(contactPhoneNumbers, message = description)
684                 if 0 == len(phoneNumber):
685                         return
686
687                 self.number_selected(phoneNumber)
688                 self._recentviewselection.unselect_all()
689
690
691 class MessagesView(object):
692
693         def __init__(self, widgetTree, backend, errorDisplay):
694                 self._errorDisplay = errorDisplay
695                 self._backend = backend
696
697                 self._messagetime = 0.0
698                 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
699                 self._messageview = widgetTree.get_widget("messages_view")
700                 self._messageviewselection = None
701                 self._onMessageviewRowActivatedId = 0
702
703                 textrenderer = gtk.CellRendererText()
704                 self._messageviewColumn = gtk.TreeViewColumn("Messages")
705                 self._messageviewColumn.pack_start(textrenderer, expand=True)
706                 self._messageviewColumn.add_attribute(textrenderer, "text", 1)
707                 self._messageviewColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
708
709                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
710
711         def enable(self):
712                 assert self._backend.is_authed()
713                 self._messageview.set_model(self._messagemodel)
714
715                 self._messageview.append_column(self._messageviewColumn)
716                 self._messageviewselection = self._messageview.get_selection()
717                 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
718
719                 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
720
721         def disable(self):
722                 self._messageview.disconnect(self._onMessageviewRowActivatedId)
723                 self._messageview.remove_column(self._messageviewColumn)
724                 self._messageview.set_model(None)
725
726         def number_selected(self, number):
727                 """
728                 @note Actual dial function is patched in later
729                 """
730                 raise NotImplementedError
731
732         def update(self):
733                 if (time.time() - self._messagetime) < 300:
734                         return
735                 backgroundPopulate = threading.Thread(target=self._idly_populate_messageview)
736                 backgroundPopulate.setDaemon(True)
737                 backgroundPopulate.start()
738
739         def clear(self):
740                 self._messagetime = 0.0
741                 self._messagemodel.clear()
742
743         def _idly_populate_messageview(self):
744                 self._messagetime = time.time()
745                 self._messagemodel.clear()
746
747                 try:
748                         messageItems = self._backend.get_messages()
749                 except RuntimeError, e:
750                         self._errorDisplay.push_exception_with_lock(e)
751                         self._messagetime = 0.0
752                         messageItems = []
753
754                 for phoneNumber, data in messageItems:
755                         item = (phoneNumber, data)
756                         with gtk_toolbox.gtk_lock():
757                                 self._messagemodel.append(item)
758
759                 return False
760
761         def _on_messageview_row_activated(self, treeview, path, view_column):
762                 model, itr = self._messageviewselection.get_selected()
763                 if not itr:
764                         return
765
766                 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, 0))]
767                 description = self._messagemodel.get_value(itr, 1)
768                 print repr(contactPhoneNumbers), repr(description)
769
770                 phoneNumber = self._phoneTypeSelector.run(contactPhoneNumbers, message = description)
771                 if 0 == len(phoneNumber):
772                         return
773
774                 self.number_selected(phoneNumber)
775                 self._messageviewselection.unselect_all()
776
777
778 class ContactsView(object):
779
780         def __init__(self, widgetTree, backend, errorDisplay):
781                 self._errorDisplay = errorDisplay
782                 self._backend = backend
783
784                 self._addressBook = None
785                 self._addressBookFactories = [DummyAddressBook()]
786
787                 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
788                 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
789
790                 self._contactstime = 0.0
791                 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
792                 self._contactsviewselection = None
793                 self._contactsview = widgetTree.get_widget("contactsview")
794
795                 self._contactColumn = gtk.TreeViewColumn("Contact")
796                 displayContactSource = False
797                 if displayContactSource:
798                         textrenderer = gtk.CellRendererText()
799                         self._contactColumn.pack_start(textrenderer, expand=False)
800                         self._contactColumn.add_attribute(textrenderer, 'text', 0)
801                 textrenderer = gtk.CellRendererText()
802                 self._contactColumn.pack_start(textrenderer, expand=True)
803                 self._contactColumn.add_attribute(textrenderer, 'text', 1)
804                 textrenderer = gtk.CellRendererText()
805                 self._contactColumn.pack_start(textrenderer, expand=True)
806                 self._contactColumn.add_attribute(textrenderer, 'text', 4)
807                 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
808                 self._contactColumn.set_sort_column_id(1)
809                 self._contactColumn.set_visible(True)
810
811                 self._onContactsviewRowActivatedId = 0
812                 self._onAddressbookComboChangedId = 0
813                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
814
815         def enable(self):
816                 assert self._backend.is_authed()
817
818                 self._contactsview.set_model(self._contactsmodel)
819                 self._contactsview.append_column(self._contactColumn)
820                 self._contactsviewselection = self._contactsview.get_selection()
821                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
822
823                 self._booksList.clear()
824                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
825                         if factoryName and bookName:
826                                 entryName = "%s: %s" % (factoryName, bookName)
827                         elif factoryName:
828                                 entryName = factoryName
829                         elif bookName:
830                                 entryName = bookName
831                         else:
832                                 entryName = "Bad name (%d)" % factoryId
833                         row = (str(factoryId), bookId, entryName)
834                         self._booksList.append(row)
835
836                 self._booksSelectionBox.set_model(self._booksList)
837                 cell = gtk.CellRendererText()
838                 self._booksSelectionBox.pack_start(cell, True)
839                 self._booksSelectionBox.add_attribute(cell, 'text', 2)
840                 self._booksSelectionBox.set_active(0)
841
842                 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
843                 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
844
845         def disable(self):
846                 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
847                 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
848
849                 self._booksSelectionBox.clear()
850                 self._booksSelectionBox.set_model(None)
851                 self._contactsview.set_model(None)
852                 self._contactsview.remove_column(self._contactColumn)
853
854         def number_selected(self, number):
855                 """
856                 @note Actual dial function is patched in later
857                 """
858                 raise NotImplementedError
859
860         def get_addressbooks(self):
861                 """
862                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
863                 """
864                 for i, factory in enumerate(self._addressBookFactories):
865                         for bookFactory, bookId, bookName in factory.get_addressbooks():
866                                 yield (i, bookId), (factory.factory_name(), bookName)
867
868         def open_addressbook(self, bookFactoryId, bookId):
869                 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
870                 self._contactstime = 0
871                 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
872                 backgroundPopulate.setDaemon(True)
873                 backgroundPopulate.start()
874
875         def update(self):
876                 if (time.time() - self._contactstime) < 300:
877                         return
878                 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
879                 backgroundPopulate.setDaemon(True)
880                 backgroundPopulate.start()
881
882         def clear(self):
883                 self._contactstime = 0.0
884                 self._contactsmodel.clear()
885
886         def clear_caches(self):
887                 for factory in self._addressBookFactories:
888                         factory.clear_caches()
889                 self._addressBook.clear_caches()
890
891         def append(self, book):
892                 self._addressBookFactories.append(book)
893
894         def extend(self, books):
895                 self._addressBookFactories.extend(books)
896
897         def _idly_populate_contactsview(self):
898                 #@todo Add a lock so only one code path can be in here at a time
899                 self.clear()
900
901                 # completely disable updating the treeview while we populate the data
902                 self._contactsview.freeze_child_notify()
903                 self._contactsview.set_model(None)
904
905                 addressBook = self._addressBook
906                 try:
907                         contacts = addressBook.get_contacts()
908                 except RuntimeError, e:
909                         contacts = []
910                         self._contactstime = 0.0
911                         self._errorDisplay.push_exception_with_lock(e)
912                 for contactId, contactName in contacts:
913                         contactType = (addressBook.contact_source_short_name(contactId), )
914                         self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
915
916                 # restart the treeview data rendering
917                 self._contactsview.set_model(self._contactsmodel)
918                 self._contactsview.thaw_child_notify()
919                 return False
920
921         def _on_addressbook_combo_changed(self, *args, **kwds):
922                 itr = self._booksSelectionBox.get_active_iter()
923                 if itr is None:
924                         return
925                 factoryId = int(self._booksList.get_value(itr, 0))
926                 bookId = self._booksList.get_value(itr, 1)
927                 self.open_addressbook(factoryId, bookId)
928
929         def _on_contactsview_row_activated(self, treeview, path, view_column):
930                 model, itr = self._contactsviewselection.get_selected()
931                 if not itr:
932                         return
933
934                 contactId = self._contactsmodel.get_value(itr, 3)
935                 contactName = self._contactsmodel.get_value(itr, 1)
936                 try:
937                         contactDetails = self._addressBook.get_contact_details(contactId)
938                 except RuntimeError, e:
939                         contactDetails = []
940                         self._contactstime = 0.0
941                         self._errorDisplay.push_exception(e)
942                 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
943
944                 if len(contactPhoneNumbers) == 0:
945                         phoneNumber = ""
946                 elif len(contactPhoneNumbers) == 1:
947                         phoneNumber = self._phoneTypeSelector.run(contactPhoneNumbers, message = contactName)
948
949                 if 0 == len(phoneNumber):
950                         return
951
952                 self.number_selected(phoneNumber)
953                 self._contactsviewselection.unselect_all()