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