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