Added messages
[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         NUMBER_IDX = 0
691         DATE_IDX = 1
692         ACTION_IDX = 2
693         FROM_IDX = 3
694
695         def __init__(self, widgetTree, backend, errorDisplay):
696                 self._errorDisplay = errorDisplay
697                 self._backend = backend
698
699                 self._isPopulated = False
700                 self._recentmodel = gtk.ListStore(
701                         gobject.TYPE_STRING, # number
702                         gobject.TYPE_STRING, # date
703                         gobject.TYPE_STRING, # action
704                         gobject.TYPE_STRING, # from
705                 )
706                 self._recentview = widgetTree.get_widget("recentview")
707                 self._recentviewselection = None
708                 self._onRecentviewRowActivatedId = 0
709
710                 textrenderer = gtk.CellRendererText()
711                 textrenderer.set_property("yalign", 0)
712                 self._dateColumn = gtk.TreeViewColumn("Date")
713                 self._dateColumn.pack_start(textrenderer, expand=True)
714                 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
715
716                 textrenderer = gtk.CellRendererText()
717                 textrenderer.set_property("yalign", 0)
718                 self._actionColumn = gtk.TreeViewColumn("Action")
719                 self._actionColumn.pack_start(textrenderer, expand=True)
720                 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
721
722                 textrenderer = gtk.CellRendererText()
723                 textrenderer.set_property("yalign", 0)
724                 self._fromColumn = gtk.TreeViewColumn("From")
725                 self._fromColumn.pack_start(textrenderer, expand=True)
726                 self._fromColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
727                 self._fromColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
728
729                 self._window = gtk_toolbox.find_parent_window(self._recentview)
730                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
731
732         def enable(self):
733                 assert self._backend.is_authed()
734                 self._recentview.set_model(self._recentmodel)
735
736                 self._recentview.append_column(self._dateColumn)
737                 self._recentview.append_column(self._actionColumn)
738                 self._recentview.append_column(self._fromColumn)
739                 self._recentviewselection = self._recentview.get_selection()
740                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
741
742                 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
743
744         def disable(self):
745                 self._recentview.disconnect(self._onRecentviewRowActivatedId)
746
747                 self.clear()
748
749                 self._recentview.remove_column(self._dateColumn)
750                 self._recentview.remove_column(self._actionColumn)
751                 self._recentview.remove_column(self._fromColumn)
752                 self._recentview.set_model(None)
753
754         def number_selected(self, action, number, message):
755                 """
756                 @note Actual dial function is patched in later
757                 """
758                 raise NotImplementedError
759
760         def update(self, force = False):
761                 if not force and self._isPopulated:
762                         return
763                 backgroundPopulate = threading.Thread(target=self._idly_populate_recentview)
764                 backgroundPopulate.setDaemon(True)
765                 backgroundPopulate.start()
766
767         def clear(self):
768                 self._isPopulated = False
769                 self._recentmodel.clear()
770
771         @staticmethod
772         def name():
773                 return "Recent Calls"
774
775         def load_settings(self, config, section):
776                 pass
777
778         def save_settings(self, config, section):
779                 """
780                 @note Thread Agnostic
781                 """
782                 pass
783
784         def _idly_populate_recentview(self):
785                 self._isPopulated = True
786                 self._recentmodel.clear()
787
788                 try:
789                         recentItems = self._backend.get_recent()
790                 except RuntimeError, e:
791                         self._errorDisplay.push_exception_with_lock(e)
792                         self._isPopulated = False
793                         recentItems = []
794
795                 for personName, phoneNumber, date, action in recentItems:
796                         if not personName:
797                                 personName = "Unknown"
798                         description = "%s (%s)" % (phoneNumber, personName)
799                         item = (phoneNumber, date, action.capitalize(), description)
800                         with gtk_toolbox.gtk_lock():
801                                 self._recentmodel.append(item)
802
803                 return False
804
805         def _on_recentview_row_activated(self, treeview, path, view_column):
806                 model, itr = self._recentviewselection.get_selected()
807                 if not itr:
808                         return
809
810                 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
811                 number = make_ugly(number)
812                 contactPhoneNumbers = [("Phone", number)]
813                 description = self._recentmodel.get_value(itr, self.FROM_IDX)
814
815                 action, phoneNumber, message = self._phoneTypeSelector.run(
816                         contactPhoneNumbers,
817                         message = description,
818                         parent = self._window,
819                 )
820                 if action == PhoneTypeSelector.ACTION_CANCEL:
821                         return
822                 assert phoneNumber
823
824                 self.number_selected(action, phoneNumber, message)
825                 self._recentviewselection.unselect_all()
826
827
828 class MessagesView(object):
829
830         NUMBER_IDX = 0
831         DATE_IDX = 1
832         HEADER_IDX = 2
833         MESSAGE_IDX = 3
834
835         def __init__(self, widgetTree, backend, errorDisplay):
836                 self._errorDisplay = errorDisplay
837                 self._backend = backend
838
839                 self._isPopulated = False
840                 self._messagemodel = gtk.ListStore(
841                         gobject.TYPE_STRING, # number
842                         gobject.TYPE_STRING, # date
843                         gobject.TYPE_STRING, # header
844                         gobject.TYPE_STRING, # message
845                 )
846                 self._messageview = widgetTree.get_widget("messages_view")
847                 self._messageviewselection = None
848                 self._onMessageviewRowActivatedId = 0
849
850                 textrenderer = gtk.CellRendererText()
851                 textrenderer.set_property("yalign", 0)
852                 self._dateColumn = gtk.TreeViewColumn("Date")
853                 self._dateColumn.pack_start(textrenderer, expand=True)
854                 self._dateColumn.add_attribute(textrenderer, "markup", self.DATE_IDX)
855
856                 textrenderer = gtk.CellRendererText()
857                 textrenderer.set_property("yalign", 0)
858                 self._headerColumn = gtk.TreeViewColumn("From")
859                 self._headerColumn.pack_start(textrenderer, expand=True)
860                 self._headerColumn.add_attribute(textrenderer, "markup", self.HEADER_IDX)
861
862                 textrenderer = gtk.CellRendererText()
863                 textrenderer.set_property("yalign", 0)
864                 self._messageColumn = gtk.TreeViewColumn("Messages")
865                 self._messageColumn.pack_start(textrenderer, expand=True)
866                 self._messageColumn.add_attribute(textrenderer, "markup", self.MESSAGE_IDX)
867                 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
868
869                 self._window = gtk_toolbox.find_parent_window(self._messageview)
870                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
871
872         def enable(self):
873                 assert self._backend.is_authed()
874                 self._messageview.set_model(self._messagemodel)
875
876                 self._messageview.append_column(self._dateColumn)
877                 self._messageview.append_column(self._headerColumn)
878                 self._messageview.append_column(self._messageColumn)
879                 self._messageviewselection = self._messageview.get_selection()
880                 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
881
882                 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
883
884         def disable(self):
885                 self._messageview.disconnect(self._onMessageviewRowActivatedId)
886
887                 self.clear()
888
889                 self._messageview.remove_column(self._dateColumn)
890                 self._messageview.remove_column(self._headerColumn)
891                 self._messageview.remove_column(self._messageColumn)
892                 self._messageview.set_model(None)
893
894         def number_selected(self, action, number, message):
895                 """
896                 @note Actual dial function is patched in later
897                 """
898                 raise NotImplementedError
899
900         def update(self, force = False):
901                 if not force and self._isPopulated:
902                         return
903                 backgroundPopulate = threading.Thread(target=self._idly_populate_messageview)
904                 backgroundPopulate.setDaemon(True)
905                 backgroundPopulate.start()
906
907         def clear(self):
908                 self._isPopulated = False
909                 self._messagemodel.clear()
910
911         @staticmethod
912         def name():
913                 return "Messages"
914
915         def load_settings(self, config, section):
916                 pass
917
918         def save_settings(self, config, section):
919                 """
920                 @note Thread Agnostic
921                 """
922                 pass
923
924         def _idly_populate_messageview(self):
925                 self._isPopulated = True
926                 self._messagemodel.clear()
927
928                 try:
929                         messageItems = self._backend.get_messages()
930                 except RuntimeError, e:
931                         self._errorDisplay.push_exception_with_lock(e)
932                         self._isPopulated = False
933                         messageItems = []
934
935                 for header, number, relativeDate, message in messageItems:
936                         number = make_ugly(number)
937                         row = (number, relativeDate, header, message)
938                         with gtk_toolbox.gtk_lock():
939                                 self._messagemodel.append(row)
940
941                 return False
942
943         def _on_messageview_row_activated(self, treeview, path, view_column):
944                 model, itr = self._messageviewselection.get_selected()
945                 if not itr:
946                         return
947
948                 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
949                 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
950
951                 action, phoneNumber, message = self._phoneTypeSelector.run(
952                         contactPhoneNumbers,
953                         message = description,
954                         parent = self._window,
955                 )
956                 if action == PhoneTypeSelector.ACTION_CANCEL:
957                         return
958                 assert phoneNumber
959
960                 self.number_selected(action, phoneNumber, message)
961                 self._messageviewselection.unselect_all()
962
963
964 class ContactsView(object):
965
966         def __init__(self, widgetTree, backend, errorDisplay):
967                 self._errorDisplay = errorDisplay
968                 self._backend = backend
969
970                 self._addressBook = None
971                 self._addressBookFactories = [DummyAddressBook()]
972
973                 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
974                 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
975
976                 self._isPopulated = False
977                 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
978                 self._contactsviewselection = None
979                 self._contactsview = widgetTree.get_widget("contactsview")
980
981                 self._contactColumn = gtk.TreeViewColumn("Contact")
982                 displayContactSource = False
983                 if displayContactSource:
984                         textrenderer = gtk.CellRendererText()
985                         self._contactColumn.pack_start(textrenderer, expand=False)
986                         self._contactColumn.add_attribute(textrenderer, 'text', 0)
987                 textrenderer = gtk.CellRendererText()
988                 self._contactColumn.pack_start(textrenderer, expand=True)
989                 self._contactColumn.add_attribute(textrenderer, 'text', 1)
990                 textrenderer = gtk.CellRendererText()
991                 self._contactColumn.pack_start(textrenderer, expand=True)
992                 self._contactColumn.add_attribute(textrenderer, 'text', 4)
993                 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
994                 self._contactColumn.set_sort_column_id(1)
995                 self._contactColumn.set_visible(True)
996
997                 self._onContactsviewRowActivatedId = 0
998                 self._onAddressbookComboChangedId = 0
999                 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1000                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1001
1002         def enable(self):
1003                 assert self._backend.is_authed()
1004
1005                 self._contactsview.set_model(self._contactsmodel)
1006                 self._contactsview.append_column(self._contactColumn)
1007                 self._contactsviewselection = self._contactsview.get_selection()
1008                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1009
1010                 self._booksList.clear()
1011                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1012                         if factoryName and bookName:
1013                                 entryName = "%s: %s" % (factoryName, bookName)
1014                         elif factoryName:
1015                                 entryName = factoryName
1016                         elif bookName:
1017                                 entryName = bookName
1018                         else:
1019                                 entryName = "Bad name (%d)" % factoryId
1020                         row = (str(factoryId), bookId, entryName)
1021                         self._booksList.append(row)
1022
1023                 self._booksSelectionBox.set_model(self._booksList)
1024                 cell = gtk.CellRendererText()
1025                 self._booksSelectionBox.pack_start(cell, True)
1026                 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1027                 self._booksSelectionBox.set_active(0)
1028
1029                 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1030                 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1031
1032         def disable(self):
1033                 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1034                 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1035
1036                 self.clear()
1037
1038                 self._booksSelectionBox.clear()
1039                 self._booksSelectionBox.set_model(None)
1040                 self._contactsview.set_model(None)
1041                 self._contactsview.remove_column(self._contactColumn)
1042
1043         def number_selected(self, action, number, message):
1044                 """
1045                 @note Actual dial function is patched in later
1046                 """
1047                 raise NotImplementedError
1048
1049         def get_addressbooks(self):
1050                 """
1051                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1052                 """
1053                 for i, factory in enumerate(self._addressBookFactories):
1054                         for bookFactory, bookId, bookName in factory.get_addressbooks():
1055                                 yield (i, bookId), (factory.factory_name(), bookName)
1056
1057         def open_addressbook(self, bookFactoryId, bookId):
1058                 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
1059                 self.update(force=True)
1060
1061         def update(self, force = False):
1062                 if not force and self._isPopulated:
1063                         return
1064                 backgroundPopulate = threading.Thread(target=self._idly_populate_contactsview)
1065                 backgroundPopulate.setDaemon(True)
1066                 backgroundPopulate.start()
1067
1068         def clear(self):
1069                 self._isPopulated = False
1070                 self._contactsmodel.clear()
1071
1072         def clear_caches(self):
1073                 for factory in self._addressBookFactories:
1074                         factory.clear_caches()
1075                 self._addressBook.clear_caches()
1076
1077         def append(self, book):
1078                 self._addressBookFactories.append(book)
1079
1080         def extend(self, books):
1081                 self._addressBookFactories.extend(books)
1082
1083         @staticmethod
1084         def name():
1085                 return "Contacts"
1086
1087         def load_settings(self, config, section):
1088                 pass
1089
1090         def save_settings(self, config, section):
1091                 """
1092                 @note Thread Agnostic
1093                 """
1094                 pass
1095
1096         def _idly_populate_contactsview(self):
1097                 self._isPopulated = True
1098                 self.clear()
1099
1100                 # completely disable updating the treeview while we populate the data
1101                 self._contactsview.freeze_child_notify()
1102                 self._contactsview.set_model(None)
1103
1104                 addressBook = self._addressBook
1105                 try:
1106                         contacts = addressBook.get_contacts()
1107                 except RuntimeError, e:
1108                         contacts = []
1109                         self._isPopulated = False
1110                         self._errorDisplay.push_exception_with_lock(e)
1111                 for contactId, contactName in contacts:
1112                         contactType = (addressBook.contact_source_short_name(contactId), )
1113                         self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1114
1115                 # restart the treeview data rendering
1116                 self._contactsview.set_model(self._contactsmodel)
1117                 self._contactsview.thaw_child_notify()
1118                 return False
1119
1120         def _on_addressbook_combo_changed(self, *args, **kwds):
1121                 itr = self._booksSelectionBox.get_active_iter()
1122                 if itr is None:
1123                         return
1124                 factoryId = int(self._booksList.get_value(itr, 0))
1125                 bookId = self._booksList.get_value(itr, 1)
1126                 self.open_addressbook(factoryId, bookId)
1127
1128         def _on_contactsview_row_activated(self, treeview, path, view_column):
1129                 model, itr = self._contactsviewselection.get_selected()
1130                 if not itr:
1131                         return
1132
1133                 contactId = self._contactsmodel.get_value(itr, 3)
1134                 contactName = self._contactsmodel.get_value(itr, 1)
1135                 try:
1136                         contactDetails = self._addressBook.get_contact_details(contactId)
1137                 except RuntimeError, e:
1138                         contactDetails = []
1139                         self._errorDisplay.push_exception(e)
1140                 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1141
1142                 if len(contactPhoneNumbers) == 0:
1143                         return
1144
1145                 action, phoneNumber, message = self._phoneTypeSelector.run(
1146                         contactPhoneNumbers,
1147                         message = contactName,
1148                         parent = self._window,
1149                 )
1150                 if action == PhoneTypeSelector.ACTION_CANCEL:
1151                         return
1152                 assert phoneNumber
1153
1154                 self.number_selected(action, phoneNumber, message)
1155                 self._contactsviewselection.unselect_all()