Todos
[gc-dialer] / src / gv_views.py
1 #!/usr/bin/python2.5
2
3 """
4 DialCentral - Front end for Google's Grand Central service.
5 Copyright (C) 2008  Mark Bergman bergman AT merctech DOT com
6
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
11
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 Lesser General Public License for more details.
16
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
20
21 @todo Add CTRL-V support to Dialpad
22 @todo Touch selector for addressbook selection
23 @todo Touch selector for callback number
24 @todo Look into top half of dialogs being a treeview rather than a label
25 @todo Alternate UI for dialogs (stackables)
26 """
27
28 from __future__ import with_statement
29
30 import ConfigParser
31 import logging
32
33 import gobject
34 import pango
35 import gtk
36
37 import gtk_toolbox
38 import hildonize
39 import null_backend
40
41
42 def make_ugly(prettynumber):
43         """
44         function to take a phone number and strip out all non-numeric
45         characters
46
47         >>> make_ugly("+012-(345)-678-90")
48         '01234567890'
49         """
50         import re
51         uglynumber = re.sub('\D', '', prettynumber)
52         return uglynumber
53
54
55 def make_pretty(phonenumber):
56         """
57         Function to take a phone number and return the pretty version
58         pretty numbers:
59                 if phonenumber begins with 0:
60                         ...-(...)-...-....
61                 if phonenumber begins with 1: ( for gizmo callback numbers )
62                         1 (...)-...-....
63                 if phonenumber is 13 digits:
64                         (...)-...-....
65                 if phonenumber is 10 digits:
66                         ...-....
67         >>> make_pretty("12")
68         '12'
69         >>> make_pretty("1234567")
70         '123-4567'
71         >>> make_pretty("2345678901")
72         '(234)-567-8901'
73         >>> make_pretty("12345678901")
74         '1 (234)-567-8901'
75         >>> make_pretty("01234567890")
76         '+012-(345)-678-90'
77         """
78         if phonenumber is None or phonenumber is "":
79                 return ""
80
81         phonenumber = make_ugly(phonenumber)
82
83         if len(phonenumber) < 3:
84                 return phonenumber
85
86         if phonenumber[0] == "0":
87                 prettynumber = ""
88                 prettynumber += "+%s" % phonenumber[0:3]
89                 if 3 < len(phonenumber):
90                         prettynumber += "-(%s)" % phonenumber[3:6]
91                         if 6 < len(phonenumber):
92                                 prettynumber += "-%s" % phonenumber[6:9]
93                                 if 9 < len(phonenumber):
94                                         prettynumber += "-%s" % phonenumber[9:]
95                 return prettynumber
96         elif len(phonenumber) <= 7:
97                 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
98         elif len(phonenumber) > 8 and phonenumber[0] == "1":
99                 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
100         elif len(phonenumber) > 7:
101                 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
102         return prettynumber
103
104
105 def abbrev_relative_date(date):
106         """
107         >>> abbrev_relative_date("42 hours ago")
108         '42 h'
109         >>> abbrev_relative_date("2 days ago")
110         '2 d'
111         >>> abbrev_relative_date("4 weeks ago")
112         '4 w'
113         """
114         parts = date.split(" ")
115         return "%s %s" % (parts[0], parts[1][0])
116
117
118 class MergedAddressBook(object):
119         """
120         Merger of all addressbooks
121         """
122
123         def __init__(self, addressbookFactories, sorter = None):
124                 self.__addressbookFactories = addressbookFactories
125                 self.__addressbooks = None
126                 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
127
128         def clear_caches(self):
129                 self.__addressbooks = None
130                 for factory in self.__addressbookFactories:
131                         factory.clear_caches()
132
133         def get_addressbooks(self):
134                 """
135                 @returns Iterable of (Address Book Factory, Book Id, Book Name)
136                 """
137                 yield self, "", ""
138
139         def open_addressbook(self, bookId):
140                 return self
141
142         def contact_source_short_name(self, contactId):
143                 if self.__addressbooks is None:
144                         return ""
145                 bookIndex, originalId = contactId.split("-", 1)
146                 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
147
148         @staticmethod
149         def factory_name():
150                 return "All Contacts"
151
152         def get_contacts(self):
153                 """
154                 @returns Iterable of (contact id, contact name)
155                 """
156                 if self.__addressbooks is None:
157                         self.__addressbooks = list(
158                                 factory.open_addressbook(id)
159                                 for factory in self.__addressbookFactories
160                                 for (f, id, name) in factory.get_addressbooks()
161                         )
162                 contacts = (
163                         ("-".join([str(bookIndex), contactId]), contactName)
164                                 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
165                                         for (contactId, contactName) in addressbook.get_contacts()
166                 )
167                 sortedContacts = self.__sort_contacts(contacts)
168                 return sortedContacts
169
170         def get_contact_details(self, contactId):
171                 """
172                 @returns Iterable of (Phone Type, Phone Number)
173                 """
174                 if self.__addressbooks is None:
175                         return []
176                 bookIndex, originalId = contactId.split("-", 1)
177                 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
178
179         @staticmethod
180         def null_sorter(contacts):
181                 """
182                 Good for speed/low memory
183                 """
184                 return contacts
185
186         @staticmethod
187         def basic_firtname_sorter(contacts):
188                 """
189                 Expects names in "First Last" format
190                 """
191                 contactsWithKey = [
192                         (contactName.rsplit(" ", 1)[0], (contactId, contactName))
193                                 for (contactId, contactName) in contacts
194                 ]
195                 contactsWithKey.sort()
196                 return (contactData for (lastName, contactData) in contactsWithKey)
197
198         @staticmethod
199         def basic_lastname_sorter(contacts):
200                 """
201                 Expects names in "First Last" format
202                 """
203                 contactsWithKey = [
204                         (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
205                                 for (contactId, contactName) in contacts
206                 ]
207                 contactsWithKey.sort()
208                 return (contactData for (lastName, contactData) in contactsWithKey)
209
210         @staticmethod
211         def reversed_firtname_sorter(contacts):
212                 """
213                 Expects names in "Last, First" format
214                 """
215                 contactsWithKey = [
216                         (contactName.split(", ", 1)[-1], (contactId, contactName))
217                                 for (contactId, contactName) in contacts
218                 ]
219                 contactsWithKey.sort()
220                 return (contactData for (lastName, contactData) in contactsWithKey)
221
222         @staticmethod
223         def reversed_lastname_sorter(contacts):
224                 """
225                 Expects names in "Last, First" format
226                 """
227                 contactsWithKey = [
228                         (contactName.split(", ", 1)[0], (contactId, contactName))
229                                 for (contactId, contactName) in contacts
230                 ]
231                 contactsWithKey.sort()
232                 return (contactData for (lastName, contactData) in contactsWithKey)
233
234         @staticmethod
235         def guess_firstname(name):
236                 if ", " in name:
237                         return name.split(", ", 1)[-1]
238                 else:
239                         return name.rsplit(" ", 1)[0]
240
241         @staticmethod
242         def guess_lastname(name):
243                 if ", " in name:
244                         return name.split(", ", 1)[0]
245                 else:
246                         return name.rsplit(" ", 1)[-1]
247
248         @classmethod
249         def advanced_firstname_sorter(cls, contacts):
250                 contactsWithKey = [
251                         (cls.guess_firstname(contactName), (contactId, contactName))
252                                 for (contactId, contactName) in contacts
253                 ]
254                 contactsWithKey.sort()
255                 return (contactData for (lastName, contactData) in contactsWithKey)
256
257         @classmethod
258         def advanced_lastname_sorter(cls, contacts):
259                 contactsWithKey = [
260                         (cls.guess_lastname(contactName), (contactId, contactName))
261                                 for (contactId, contactName) in contacts
262                 ]
263                 contactsWithKey.sort()
264                 return (contactData for (lastName, contactData) in contactsWithKey)
265
266
267 class PhoneTypeSelector(object):
268
269         ACTION_CANCEL = "cancel"
270         ACTION_SELECT = "select"
271         ACTION_DIAL = "dial"
272         ACTION_SEND_SMS = "sms"
273
274         def __init__(self, widgetTree, gcBackend):
275                 self._gcBackend = gcBackend
276                 self._widgetTree = widgetTree
277
278                 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
279                 self._smsDialog = SmsEntryDialog(self._widgetTree)
280
281                 self._smsButton = self._widgetTree.get_widget("sms_button")
282                 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
283
284                 self._dialButton = self._widgetTree.get_widget("dial_button")
285                 self._dialButton.connect("clicked", self._on_phonetype_dial)
286
287                 self._selectButton = self._widgetTree.get_widget("select_button")
288                 self._selectButton.connect("clicked", self._on_phonetype_select)
289
290                 self._cancelButton = self._widgetTree.get_widget("cancel_button")
291                 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
292
293                 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
294                 self._typeviewselection = None
295
296                 self._message = self._widgetTree.get_widget("phoneSelectionMessage")
297                 self._messageViewport = self._widgetTree.get_widget("phoneSelectionMessage_viewport")
298                 self._scrollWindow = self._widgetTree.get_widget("phoneSelectionMessage_scrolledwindow")
299                 self._typeview = self._widgetTree.get_widget("phonetypes")
300                 self._typeview.connect("row-activated", self._on_phonetype_select)
301
302                 self._action = self.ACTION_CANCEL
303
304         def run(self, contactDetails, message = "", parent = None):
305                 self._action = self.ACTION_CANCEL
306                 self._typemodel.clear()
307                 self._typeview.set_model(self._typemodel)
308
309                 # Add the column to the treeview
310                 textrenderer = gtk.CellRendererText()
311                 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
312                 self._typeview.append_column(numberColumn)
313
314                 textrenderer = gtk.CellRendererText()
315                 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
316                 self._typeview.append_column(typeColumn)
317
318                 self._typeviewselection = self._typeview.get_selection()
319                 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
320
321                 for phoneType, phoneNumber in contactDetails:
322                         display = " - ".join((phoneNumber, phoneType))
323                         display = phoneType
324                         row = (phoneNumber, display)
325                         self._typemodel.append(row)
326
327                 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
328                 if message:
329                         self._message.set_markup(message)
330                         self._message.show()
331                 else:
332                         self._message.set_markup("")
333                         self._message.hide()
334
335                 if parent is not None:
336                         self._dialog.set_transient_for(parent)
337
338                 try:
339                         self._dialog.show()
340                         adjustment = self._scrollWindow.get_vadjustment()
341                         dx = self._message.get_allocation().height - self._messageViewport.get_allocation().height
342                         dx = max(dx, 0)
343                         adjustment.value = dx
344
345                         userResponse = self._dialog.run()
346                 finally:
347                         self._dialog.hide()
348
349                 if userResponse == gtk.RESPONSE_OK:
350                         phoneNumber = self._get_number()
351                         phoneNumber = make_ugly(phoneNumber)
352                 else:
353                         phoneNumber = ""
354                 if not phoneNumber:
355                         self._action = self.ACTION_CANCEL
356
357                 if self._action == self.ACTION_SEND_SMS:
358                         smsMessage = self._smsDialog.run(phoneNumber, message, parent)
359                         if not smsMessage:
360                                 phoneNumber = ""
361                                 self._action = self.ACTION_CANCEL
362                 else:
363                         smsMessage = ""
364
365                 self._typeviewselection.unselect_all()
366                 self._typeview.remove_column(numberColumn)
367                 self._typeview.remove_column(typeColumn)
368                 self._typeview.set_model(None)
369
370                 return self._action, phoneNumber, smsMessage
371
372         def _get_number(self):
373                 model, itr = self._typeviewselection.get_selected()
374                 if not itr:
375                         return ""
376
377                 phoneNumber = self._typemodel.get_value(itr, 0)
378                 return phoneNumber
379
380         def _on_phonetype_dial(self, *args):
381                 self._dialog.response(gtk.RESPONSE_OK)
382                 self._action = self.ACTION_DIAL
383
384         def _on_phonetype_send_sms(self, *args):
385                 self._dialog.response(gtk.RESPONSE_OK)
386                 self._action = self.ACTION_SEND_SMS
387
388         def _on_phonetype_select(self, *args):
389                 self._dialog.response(gtk.RESPONSE_OK)
390                 self._action = self.ACTION_SELECT
391
392         def _on_phonetype_cancel(self, *args):
393                 self._dialog.response(gtk.RESPONSE_CANCEL)
394                 self._action = self.ACTION_CANCEL
395
396
397 class SmsEntryDialog(object):
398
399         """
400         @todo Add multi-SMS messages like GoogleVoice
401         """
402
403         MAX_CHAR = 160
404
405         def __init__(self, widgetTree):
406                 self._widgetTree = widgetTree
407                 self._dialog = self._widgetTree.get_widget("smsDialog")
408
409                 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
410                 self._smsButton.connect("clicked", self._on_send)
411
412                 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
413                 self._cancelButton.connect("clicked", self._on_cancel)
414
415                 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
416                 self._message = self._widgetTree.get_widget("smsMessage")
417                 self._messageViewport = self._widgetTree.get_widget("smsMessage_viewport")
418                 self._scrollWindow = self._widgetTree.get_widget("smsMessage_scrolledwindow")
419                 self._smsEntry = self._widgetTree.get_widget("smsEntry")
420                 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
421
422         def run(self, number, message = "", parent = None):
423                 if message:
424                         self._message.set_markup(message)
425                         self._message.show()
426                 else:
427                         self._message.set_markup("")
428                         self._message.hide()
429                 self._smsEntry.get_buffer().set_text("")
430                 self._update_letter_count()
431
432                 if parent is not None:
433                         self._dialog.set_transient_for(parent)
434
435                 try:
436                         self._dialog.show()
437                         adjustment = self._scrollWindow.get_vadjustment()
438                         dx = self._message.get_allocation().height - self._messageViewport.get_allocation().height
439                         dx = max(dx, 0)
440                         adjustment.value = dx
441
442                         userResponse = self._dialog.run()
443                 finally:
444                         self._dialog.hide()
445
446                 if userResponse == gtk.RESPONSE_OK:
447                         entryBuffer = self._smsEntry.get_buffer()
448                         enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
449                         enteredMessage = enteredMessage[0:self.MAX_CHAR]
450                 else:
451                         enteredMessage = ""
452
453                 return enteredMessage.strip()
454
455         def _update_letter_count(self, *args):
456                 entryLength = self._smsEntry.get_buffer().get_char_count()
457                 charsLeft = self.MAX_CHAR - entryLength
458                 self._letterCountLabel.set_text(str(charsLeft))
459                 if charsLeft < 0:
460                         self._smsButton.set_sensitive(False)
461                 else:
462                         self._smsButton.set_sensitive(True)
463
464         def _on_entry_changed(self, *args):
465                 self._update_letter_count()
466
467         def _on_send(self, *args):
468                 self._dialog.response(gtk.RESPONSE_OK)
469
470         def _on_cancel(self, *args):
471                 self._dialog.response(gtk.RESPONSE_CANCEL)
472
473
474 class Dialpad(object):
475
476         def __init__(self, widgetTree, errorDisplay):
477                 self._errorDisplay = errorDisplay
478                 self._smsDialog = SmsEntryDialog(widgetTree)
479
480                 self._numberdisplay = widgetTree.get_widget("numberdisplay")
481                 self._smsButton = widgetTree.get_widget("sms")
482                 self._dialButton = widgetTree.get_widget("dial")
483                 self._backButton = widgetTree.get_widget("back")
484                 self._phonenumber = ""
485                 self._prettynumber = ""
486
487                 callbackMapping = {
488                         "on_digit_clicked": self._on_digit_clicked,
489                 }
490                 widgetTree.signal_autoconnect(callbackMapping)
491                 self._dialButton.connect("clicked", self._on_dial_clicked)
492                 self._smsButton.connect("clicked", self._on_sms_clicked)
493
494                 self._originalLabel = self._backButton.get_label()
495                 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
496                 self._backTapHandler.on_tap = self._on_backspace
497                 self._backTapHandler.on_hold = self._on_clearall
498                 self._backTapHandler.on_holding = self._set_clear_button
499                 self._backTapHandler.on_cancel = self._reset_back_button
500
501                 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
502
503         def enable(self):
504                 self._dialButton.grab_focus()
505                 self._backTapHandler.enable()
506
507         def disable(self):
508                 self._reset_back_button()
509                 self._backTapHandler.disable()
510
511         def number_selected(self, action, number, message):
512                 """
513                 @note Actual dial function is patched in later
514                 """
515                 raise NotImplementedError("Horrible unknown error has occurred")
516
517         def get_number(self):
518                 return self._phonenumber
519
520         def set_number(self, number):
521                 """
522                 Set the number to dial
523                 """
524                 try:
525                         self._phonenumber = make_ugly(number)
526                         self._prettynumber = make_pretty(self._phonenumber)
527                         self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
528                 except TypeError, e:
529                         self._errorDisplay.push_exception()
530
531         def clear(self):
532                 self.set_number("")
533
534         @staticmethod
535         def name():
536                 return "Dialpad"
537
538         def load_settings(self, config, section):
539                 pass
540
541         def save_settings(self, config, section):
542                 """
543                 @note Thread Agnostic
544                 """
545                 pass
546
547         def _on_sms_clicked(self, widget):
548                 try:
549                         action = PhoneTypeSelector.ACTION_SEND_SMS
550                         phoneNumber = self.get_number()
551
552                         message = self._smsDialog.run(phoneNumber, "", self._window)
553                         if not message:
554                                 phoneNumber = ""
555                                 action = PhoneTypeSelector.ACTION_CANCEL
556
557                         if action == PhoneTypeSelector.ACTION_CANCEL:
558                                 return
559                         self.number_selected(action, phoneNumber, message)
560                 except Exception, e:
561                         self._errorDisplay.push_exception()
562
563         def _on_dial_clicked(self, widget):
564                 try:
565                         action = PhoneTypeSelector.ACTION_DIAL
566                         phoneNumber = self.get_number()
567                         message = ""
568                         self.number_selected(action, phoneNumber, message)
569                 except Exception, e:
570                         self._errorDisplay.push_exception()
571
572         def _on_digit_clicked(self, widget):
573                 try:
574                         self.set_number(self._phonenumber + widget.get_name()[-1])
575                 except Exception, e:
576                         self._errorDisplay.push_exception()
577
578         def _on_backspace(self, taps):
579                 try:
580                         self.set_number(self._phonenumber[:-taps])
581                         self._reset_back_button()
582                 except Exception, e:
583                         self._errorDisplay.push_exception()
584
585         def _on_clearall(self, taps):
586                 try:
587                         self.clear()
588                         self._reset_back_button()
589                 except Exception, e:
590                         self._errorDisplay.push_exception()
591                 return False
592
593         def _set_clear_button(self):
594                 try:
595                         self._backButton.set_label("gtk-clear")
596                 except Exception, e:
597                         self._errorDisplay.push_exception()
598
599         def _reset_back_button(self):
600                 try:
601                         self._backButton.set_label(self._originalLabel)
602                 except Exception, e:
603                         self._errorDisplay.push_exception()
604
605
606 class AccountInfo(object):
607
608         def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
609                 self._errorDisplay = errorDisplay
610                 self._backend = backend
611                 self._isPopulated = False
612                 self._alarmHandler = alarmHandler
613                 self._notifyOnMissed = False
614                 self._notifyOnVoicemail = False
615                 self._notifyOnSms = False
616
617                 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
618                 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
619                 self._callbackCombo = widgetTree.get_widget("callbackcombo")
620                 self._onCallbackentryChangedId = 0
621
622                 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
623                 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
624                 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
625                 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
626                 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
627                 self._onNotifyToggled = 0
628                 self._onMinutesChanged = 0
629                 self._onMissedToggled = 0
630                 self._onVoicemailToggled = 0
631                 self._onSmsToggled = 0
632                 self._applyAlarmTimeoutId = None
633
634                 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
635                 self._defaultCallback = ""
636
637         def enable(self):
638                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
639
640                 self._accountViewNumberDisplay.set_use_markup(True)
641                 self.set_account_number("")
642
643                 self._callbackList.clear()
644                 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
645
646                 if self._alarmHandler is not None:
647                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
648                         self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
649                         self._missedCheckbox.set_active(self._notifyOnMissed)
650                         self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
651                         self._smsCheckbox.set_active(self._notifyOnSms)
652
653                         self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
654                         self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
655                         self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
656                         self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
657                         self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
658                 else:
659                         self._notifyCheckbox.set_sensitive(False)
660                         self._minutesEntryButton.set_sensitive(False)
661                         self._missedCheckbox.set_sensitive(False)
662                         self._voicemailCheckbox.set_sensitive(False)
663                         self._smsCheckbox.set_sensitive(False)
664                 self._minutesEntryButton.set_sensitive(True)
665                 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
666
667                 self.update(force=True)
668
669         def disable(self):
670                 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
671                 self._onCallbackentryChangedId = 0
672
673                 if self._alarmHandler is not None:
674                         self._notifyCheckbox.disconnect(self._onNotifyToggled)
675                         self._minutesEntryButton.disconnect(self._onMinutesChanged)
676                         self._missedCheckbox.disconnect(self._onNotifyToggled)
677                         self._voicemailCheckbox.disconnect(self._onNotifyToggled)
678                         self._smsCheckbox.disconnect(self._onNotifyToggled)
679                         self._onNotifyToggled = 0
680                         self._onMinutesChanged = 0
681                         self._onMissedToggled = 0
682                         self._onVoicemailToggled = 0
683                         self._onSmsToggled = 0
684                 else:
685                         self._notifyCheckbox.set_sensitive(True)
686                         self._minutesEntryButton.set_sensitive(True)
687                         self._missedCheckbox.set_sensitive(True)
688                         self._voicemailCheckbox.set_sensitive(True)
689                         self._smsCheckbox.set_sensitive(True)
690
691                 self.clear()
692                 self._callbackList.clear()
693
694         def get_selected_callback_number(self):
695                 return make_ugly(self._callbackCombo.get_child().get_text())
696
697         def set_account_number(self, number):
698                 """
699                 Displays current account number
700                 """
701                 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
702
703         def update(self, force = False):
704                 if not force and self._isPopulated:
705                         return False
706                 self._populate_callback_combo()
707                 self.set_account_number(self._backend.get_account_number())
708                 return True
709
710         def clear(self):
711                 self._callbackCombo.get_child().set_text("")
712                 self.set_account_number("")
713                 self._isPopulated = False
714
715         def save_everything(self):
716                 raise NotImplementedError
717
718         @staticmethod
719         def name():
720                 return "Account Info"
721
722         def load_settings(self, config, section):
723                 self._defaultCallback = config.get(section, "callback")
724                 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
725                 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
726                 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
727
728         def save_settings(self, config, section):
729                 """
730                 @note Thread Agnostic
731                 """
732                 callback = self.get_selected_callback_number()
733                 config.set(section, "callback", callback)
734                 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
735                 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
736                 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
737
738         def _populate_callback_combo(self):
739                 self._isPopulated = True
740                 self._callbackList.clear()
741                 try:
742                         callbackNumbers = self._backend.get_callback_numbers()
743                 except Exception, e:
744                         self._errorDisplay.push_exception()
745                         self._isPopulated = False
746                         return
747
748                 for number, description in callbackNumbers.iteritems():
749                         self._callbackList.append((make_pretty(number),))
750
751                 self._callbackCombo.set_model(self._callbackList)
752                 self._callbackCombo.set_text_column(0)
753                 #callbackNumber = self._backend.get_callback_number()
754                 callbackNumber = self._defaultCallback
755                 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
756
757         def _set_callback_number(self, number):
758                 try:
759                         if not self._backend.is_valid_syntax(number) and 0 < len(number):
760                                 self._errorDisplay.push_message("%s is not a valid callback number" % number)
761                         elif number == self._backend.get_callback_number():
762                                 logging.warning(
763                                         "Callback number already is %s" % (
764                                                 self._backend.get_callback_number(),
765                                         ),
766                                 )
767                         else:
768                                 self._backend.set_callback_number(number)
769                                 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
770                                         make_pretty(number), make_pretty(self._backend.get_callback_number())
771                                 )
772                                 logging.info(
773                                         "Callback number set to %s" % (
774                                                 self._backend.get_callback_number(),
775                                         ),
776                                 )
777                 except Exception, e:
778                         self._errorDisplay.push_exception()
779
780         def _update_alarm_settings(self, recurrence):
781                 try:
782                         isEnabled = self._notifyCheckbox.get_active()
783                         if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
784                                 self._alarmHandler.apply_settings(isEnabled, recurrence)
785                 finally:
786                         self.save_everything()
787                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
788                         self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
789
790         def _on_callbackentry_changed(self, *args):
791                 try:
792                         text = self.get_selected_callback_number()
793                         number = make_ugly(text)
794                         self._set_callback_number(number)
795                 except Exception, e:
796                         self._errorDisplay.push_exception()
797
798         def _on_notify_toggled(self, *args):
799                 try:
800                         if self._applyAlarmTimeoutId is not None:
801                                 gobject.source_remove(self._applyAlarmTimeoutId)
802                                 self._applyAlarmTimeoutId = None
803                         self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
804                 except Exception, e:
805                         self._errorDisplay.push_exception()
806
807         def _on_minutes_clicked(self, *args):
808                 recurrenceChoices = [
809                         (1, "1 minute"),
810                         (3, "3 minutes"),
811                         (5, "5 minutes"),
812                         (10, "10 minutes"),
813                         (15, "15 minutes"),
814                         (30, "30 minutes"),
815                         (45, "45 minutes"),
816                         (60, "1 hour"),
817                         (12*60, "12 hours"),
818                 ]
819                 try:
820                         actualSelection = self._alarmHandler.recurrence
821
822                         closestSelectionIndex = 0
823                         for i, possible in enumerate(recurrenceChoices):
824                                 if possible[0] <= actualSelection:
825                                         closestSelectionIndex = i
826                         recurrenceIndex = hildonize.touch_selector(
827                                 self._window,
828                                 "Minutes",
829                                 (("%s" % m[1]) for m in recurrenceChoices),
830                                 closestSelectionIndex,
831                         )
832                         recurrence = recurrenceChoices[recurrenceIndex][0]
833
834                         self._update_alarm_settings(recurrence)
835                 except Exception, e:
836                         self._errorDisplay.push_exception()
837
838         def _on_apply_timeout(self, *args):
839                 try:
840                         self._applyAlarmTimeoutId = None
841
842                         self._update_alarm_settings(self._alarmHandler.recurrence)
843                 except Exception, e:
844                         self._errorDisplay.push_exception()
845                 return False
846
847         def _on_missed_toggled(self, *args):
848                 try:
849                         self._notifyOnMissed = self._missedCheckbox.get_active()
850                         self.save_everything()
851                 except Exception, e:
852                         self._errorDisplay.push_exception()
853
854         def _on_voicemail_toggled(self, *args):
855                 try:
856                         self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
857                         self.save_everything()
858                 except Exception, e:
859                         self._errorDisplay.push_exception()
860
861         def _on_sms_toggled(self, *args):
862                 try:
863                         self._notifyOnSms = self._smsCheckbox.get_active()
864                         self.save_everything()
865                 except Exception, e:
866                         self._errorDisplay.push_exception()
867
868
869 class RecentCallsView(object):
870
871         NUMBER_IDX = 0
872         DATE_IDX = 1
873         ACTION_IDX = 2
874         FROM_IDX = 3
875
876         def __init__(self, widgetTree, backend, errorDisplay):
877                 self._errorDisplay = errorDisplay
878                 self._backend = backend
879
880                 self._isPopulated = False
881                 self._recentmodel = gtk.ListStore(
882                         gobject.TYPE_STRING, # number
883                         gobject.TYPE_STRING, # date
884                         gobject.TYPE_STRING, # action
885                         gobject.TYPE_STRING, # from
886                 )
887                 self._recentview = widgetTree.get_widget("recentview")
888                 self._recentviewselection = None
889                 self._onRecentviewRowActivatedId = 0
890
891                 textrenderer = gtk.CellRendererText()
892                 textrenderer.set_property("yalign", 0)
893                 self._dateColumn = gtk.TreeViewColumn("Date")
894                 self._dateColumn.pack_start(textrenderer, expand=True)
895                 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
896
897                 textrenderer = gtk.CellRendererText()
898                 textrenderer.set_property("yalign", 0)
899                 self._actionColumn = gtk.TreeViewColumn("Action")
900                 self._actionColumn.pack_start(textrenderer, expand=True)
901                 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
902
903                 textrenderer = gtk.CellRendererText()
904                 textrenderer.set_property("yalign", 0)
905                 hildonize.set_cell_thumb_selectable(textrenderer)
906                 self._nameColumn = gtk.TreeViewColumn("From")
907                 self._nameColumn.pack_start(textrenderer, expand=True)
908                 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
909                 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
910
911                 textrenderer = gtk.CellRendererText()
912                 textrenderer.set_property("yalign", 0)
913                 self._numberColumn = gtk.TreeViewColumn("Number")
914                 self._numberColumn.pack_start(textrenderer, expand=True)
915                 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
916
917                 self._window = gtk_toolbox.find_parent_window(self._recentview)
918                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
919
920                 self._updateSink = gtk_toolbox.threaded_stage(
921                         gtk_toolbox.comap(
922                                 self._idly_populate_recentview,
923                                 gtk_toolbox.null_sink(),
924                         )
925                 )
926
927         def enable(self):
928                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
929                 self._recentview.set_model(self._recentmodel)
930
931                 self._recentview.append_column(self._dateColumn)
932                 self._recentview.append_column(self._actionColumn)
933                 self._recentview.append_column(self._numberColumn)
934                 self._recentview.append_column(self._nameColumn)
935                 self._recentviewselection = self._recentview.get_selection()
936                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
937
938                 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
939
940         def disable(self):
941                 self._recentview.disconnect(self._onRecentviewRowActivatedId)
942
943                 self.clear()
944
945                 self._recentview.remove_column(self._dateColumn)
946                 self._recentview.remove_column(self._actionColumn)
947                 self._recentview.remove_column(self._nameColumn)
948                 self._recentview.remove_column(self._numberColumn)
949                 self._recentview.set_model(None)
950
951         def number_selected(self, action, number, message):
952                 """
953                 @note Actual dial function is patched in later
954                 """
955                 raise NotImplementedError("Horrible unknown error has occurred")
956
957         def update(self, force = False):
958                 if not force and self._isPopulated:
959                         return False
960                 self._updateSink.send(())
961                 return True
962
963         def clear(self):
964                 self._isPopulated = False
965                 self._recentmodel.clear()
966
967         @staticmethod
968         def name():
969                 return "Recent Calls"
970
971         def load_settings(self, config, section):
972                 pass
973
974         def save_settings(self, config, section):
975                 """
976                 @note Thread Agnostic
977                 """
978                 pass
979
980         def _idly_populate_recentview(self):
981                 try:
982                         self._recentmodel.clear()
983                         self._isPopulated = True
984
985                         try:
986                                 recentItems = self._backend.get_recent()
987                         except Exception, e:
988                                 self._errorDisplay.push_exception_with_lock()
989                                 self._isPopulated = False
990                                 recentItems = []
991
992                         for personName, phoneNumber, date, action in recentItems:
993                                 if not personName:
994                                         personName = "Unknown"
995                                 date = abbrev_relative_date(date)
996                                 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
997                                 prettyNumber = make_pretty(prettyNumber)
998                                 item = (prettyNumber, date, action.capitalize(), personName)
999                                 with gtk_toolbox.gtk_lock():
1000                                         self._recentmodel.append(item)
1001                 except Exception, e:
1002                         self._errorDisplay.push_exception_with_lock()
1003
1004                 return False
1005
1006         def _on_recentview_row_activated(self, treeview, path, view_column):
1007                 try:
1008                         model, itr = self._recentviewselection.get_selected()
1009                         if not itr:
1010                                 return
1011
1012                         number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1013                         number = make_ugly(number)
1014                         contactPhoneNumbers = [("Phone", number)]
1015                         description = self._recentmodel.get_value(itr, self.FROM_IDX)
1016
1017                         action, phoneNumber, message = self._phoneTypeSelector.run(
1018                                 contactPhoneNumbers,
1019                                 message = description,
1020                                 parent = self._window,
1021                         )
1022                         if action == PhoneTypeSelector.ACTION_CANCEL:
1023                                 return
1024                         assert phoneNumber, "A lack of phone number exists"
1025
1026                         self.number_selected(action, phoneNumber, message)
1027                         self._recentviewselection.unselect_all()
1028                 except Exception, e:
1029                         self._errorDisplay.push_exception()
1030
1031
1032 class MessagesView(object):
1033
1034         NUMBER_IDX = 0
1035         DATE_IDX = 1
1036         HEADER_IDX = 2
1037         MESSAGE_IDX = 3
1038
1039         def __init__(self, widgetTree, backend, errorDisplay):
1040                 self._errorDisplay = errorDisplay
1041                 self._backend = backend
1042
1043                 self._isPopulated = False
1044                 self._messagemodel = gtk.ListStore(
1045                         gobject.TYPE_STRING, # number
1046                         gobject.TYPE_STRING, # date
1047                         gobject.TYPE_STRING, # header
1048                         gobject.TYPE_STRING, # message
1049                 )
1050                 self._messageview = widgetTree.get_widget("messages_view")
1051                 self._messageviewselection = None
1052                 self._onMessageviewRowActivatedId = 0
1053
1054                 self._messageRenderer = gtk.CellRendererText()
1055                 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1056                 self._messageRenderer.set_property("wrap-width", 500)
1057                 self._messageColumn = gtk.TreeViewColumn("Messages")
1058                 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1059                 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1060                 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1061
1062                 self._window = gtk_toolbox.find_parent_window(self._messageview)
1063                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1064
1065                 self._updateSink = gtk_toolbox.threaded_stage(
1066                         gtk_toolbox.comap(
1067                                 self._idly_populate_messageview,
1068                                 gtk_toolbox.null_sink(),
1069                         )
1070                 )
1071
1072         def enable(self):
1073                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1074                 self._messageview.set_model(self._messagemodel)
1075
1076                 self._messageview.append_column(self._messageColumn)
1077                 self._messageviewselection = self._messageview.get_selection()
1078                 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1079
1080                 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1081
1082         def disable(self):
1083                 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1084
1085                 self.clear()
1086
1087                 self._messageview.remove_column(self._messageColumn)
1088                 self._messageview.set_model(None)
1089
1090         def number_selected(self, action, number, message):
1091                 """
1092                 @note Actual dial function is patched in later
1093                 """
1094                 raise NotImplementedError("Horrible unknown error has occurred")
1095
1096         def update(self, force = False):
1097                 if not force and self._isPopulated:
1098                         return False
1099                 self._updateSink.send(())
1100                 return True
1101
1102         def clear(self):
1103                 self._isPopulated = False
1104                 self._messagemodel.clear()
1105
1106         @staticmethod
1107         def name():
1108                 return "Messages"
1109
1110         def load_settings(self, config, section):
1111                 pass
1112
1113         def save_settings(self, config, section):
1114                 """
1115                 @note Thread Agnostic
1116                 """
1117                 pass
1118
1119         def _idly_populate_messageview(self):
1120                 try:
1121                         self._messagemodel.clear()
1122                         self._isPopulated = True
1123
1124                         try:
1125                                 messageItems = self._backend.get_messages()
1126                         except Exception, e:
1127                                 self._errorDisplay.push_exception_with_lock()
1128                                 self._isPopulated = False
1129                                 messageItems = []
1130
1131                         for header, number, relativeDate, message in messageItems:
1132                                 prettyNumber = number[2:] if number.startswith("+1") else number
1133                                 prettyNumber = make_pretty(prettyNumber)
1134                                 message = "<b>%s - %s</b> <i>(%s)</i>\n\n%s" % (header, prettyNumber, relativeDate, message)
1135                                 number = make_ugly(number)
1136                                 row = (number, relativeDate, header, message)
1137                                 with gtk_toolbox.gtk_lock():
1138                                         self._messagemodel.append(row)
1139                 except Exception, e:
1140                         self._errorDisplay.push_exception_with_lock()
1141
1142                 return False
1143
1144         def _on_messageview_row_activated(self, treeview, path, view_column):
1145                 try:
1146                         model, itr = self._messageviewselection.get_selected()
1147                         if not itr:
1148                                 return
1149
1150                         contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1151                         description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
1152
1153                         action, phoneNumber, message = self._phoneTypeSelector.run(
1154                                 contactPhoneNumbers,
1155                                 message = description,
1156                                 parent = self._window,
1157                         )
1158                         if action == PhoneTypeSelector.ACTION_CANCEL:
1159                                 return
1160                         assert phoneNumber, "A lock of phone number exists"
1161
1162                         self.number_selected(action, phoneNumber, message)
1163                         self._messageviewselection.unselect_all()
1164                 except Exception, e:
1165                         self._errorDisplay.push_exception()
1166
1167
1168 class ContactsView(object):
1169
1170         def __init__(self, widgetTree, backend, errorDisplay):
1171                 self._errorDisplay = errorDisplay
1172                 self._backend = backend
1173
1174                 self._addressBook = None
1175                 self._selectedComboIndex = 0
1176                 self._addressBookFactories = [null_backend.NullAddressBook()]
1177
1178                 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1179                 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
1180
1181                 self._isPopulated = False
1182                 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1183                 self._contactsviewselection = None
1184                 self._contactsview = widgetTree.get_widget("contactsview")
1185
1186                 self._contactColumn = gtk.TreeViewColumn("Contact")
1187                 displayContactSource = False
1188                 if displayContactSource:
1189                         textrenderer = gtk.CellRendererText()
1190                         self._contactColumn.pack_start(textrenderer, expand=False)
1191                         self._contactColumn.add_attribute(textrenderer, 'text', 0)
1192                 textrenderer = gtk.CellRendererText()
1193                 hildonize.set_cell_thumb_selectable(textrenderer)
1194                 self._contactColumn.pack_start(textrenderer, expand=True)
1195                 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1196                 textrenderer = gtk.CellRendererText()
1197                 self._contactColumn.pack_start(textrenderer, expand=True)
1198                 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1199                 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1200                 self._contactColumn.set_sort_column_id(1)
1201                 self._contactColumn.set_visible(True)
1202
1203                 self._onContactsviewRowActivatedId = 0
1204                 self._onAddressbookComboChangedId = 0
1205                 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1206                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1207
1208                 self._updateSink = gtk_toolbox.threaded_stage(
1209                         gtk_toolbox.comap(
1210                                 self._idly_populate_contactsview,
1211                                 gtk_toolbox.null_sink(),
1212                         )
1213                 )
1214
1215         def enable(self):
1216                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1217
1218                 self._contactsview.set_model(self._contactsmodel)
1219                 self._contactsview.append_column(self._contactColumn)
1220                 self._contactsviewselection = self._contactsview.get_selection()
1221                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1222
1223                 self._booksList.clear()
1224                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1225                         if factoryName and bookName:
1226                                 entryName = "%s: %s" % (factoryName, bookName)
1227                         elif factoryName:
1228                                 entryName = factoryName
1229                         elif bookName:
1230                                 entryName = bookName
1231                         else:
1232                                 entryName = "Bad name (%d)" % factoryId
1233                         row = (str(factoryId), bookId, entryName)
1234                         self._booksList.append(row)
1235
1236                 self._booksSelectionBox.set_model(self._booksList)
1237                 cell = gtk.CellRendererText()
1238                 self._booksSelectionBox.pack_start(cell, True)
1239                 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1240
1241                 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1242                 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1243
1244                 if len(self._booksList) <= self._selectedComboIndex:
1245                         self._selectedComboIndex = 0
1246                 self._booksSelectionBox.set_active(self._selectedComboIndex)
1247
1248         def disable(self):
1249                 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1250                 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1251
1252                 self.clear()
1253
1254                 self._booksSelectionBox.clear()
1255                 self._booksSelectionBox.set_model(None)
1256                 self._contactsview.set_model(None)
1257                 self._contactsview.remove_column(self._contactColumn)
1258
1259         def number_selected(self, action, number, message):
1260                 """
1261                 @note Actual dial function is patched in later
1262                 """
1263                 raise NotImplementedError("Horrible unknown error has occurred")
1264
1265         def get_addressbooks(self):
1266                 """
1267                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1268                 """
1269                 for i, factory in enumerate(self._addressBookFactories):
1270                         for bookFactory, bookId, bookName in factory.get_addressbooks():
1271                                 yield (str(i), bookId), (factory.factory_name(), bookName)
1272
1273         def open_addressbook(self, bookFactoryId, bookId):
1274                 bookFactoryIndex = int(bookFactoryId)
1275                 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1276
1277                 forceUpdate = True if addressBook is not self._addressBook else False
1278
1279                 self._addressBook = addressBook
1280                 self.update(force=forceUpdate)
1281
1282         def update(self, force = False):
1283                 if not force and self._isPopulated:
1284                         return False
1285                 self._updateSink.send(())
1286                 return True
1287
1288         def clear(self):
1289                 self._isPopulated = False
1290                 self._contactsmodel.clear()
1291                 for factory in self._addressBookFactories:
1292                         factory.clear_caches()
1293                 self._addressBook.clear_caches()
1294
1295         def append(self, book):
1296                 self._addressBookFactories.append(book)
1297
1298         def extend(self, books):
1299                 self._addressBookFactories.extend(books)
1300
1301         @staticmethod
1302         def name():
1303                 return "Contacts"
1304
1305         def load_settings(self, config, sectionName):
1306                 try:
1307                         self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1308                 except ConfigParser.NoOptionError:
1309                         self._selectedComboIndex = 0
1310
1311         def save_settings(self, config, sectionName):
1312                 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1313
1314         def _idly_populate_contactsview(self):
1315                 try:
1316                         addressBook = None
1317                         while addressBook is not self._addressBook:
1318                                 addressBook = self._addressBook
1319                                 with gtk_toolbox.gtk_lock():
1320                                         self._contactsview.set_model(None)
1321                                         self.clear()
1322
1323                                 try:
1324                                         contacts = addressBook.get_contacts()
1325                                 except Exception, e:
1326                                         contacts = []
1327                                         self._isPopulated = False
1328                                         self._errorDisplay.push_exception_with_lock()
1329                                 for contactId, contactName in contacts:
1330                                         contactType = (addressBook.contact_source_short_name(contactId), )
1331                                         self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1332
1333                                 with gtk_toolbox.gtk_lock():
1334                                         self._contactsview.set_model(self._contactsmodel)
1335
1336                         self._isPopulated = True
1337                 except Exception, e:
1338                         self._errorDisplay.push_exception_with_lock()
1339                 return False
1340
1341         def _on_addressbook_combo_changed(self, *args, **kwds):
1342                 try:
1343                         itr = self._booksSelectionBox.get_active_iter()
1344                         if itr is None:
1345                                 return
1346                         self._selectedComboIndex = self._booksSelectionBox.get_active()
1347                         selectedFactoryId = self._booksList.get_value(itr, 0)
1348                         selectedBookId = self._booksList.get_value(itr, 1)
1349                         self.open_addressbook(selectedFactoryId, selectedBookId)
1350                 except Exception, e:
1351                         self._errorDisplay.push_exception()
1352
1353         def _on_contactsview_row_activated(self, treeview, path, view_column):
1354                 try:
1355                         model, itr = self._contactsviewselection.get_selected()
1356                         if not itr:
1357                                 return
1358
1359                         contactId = self._contactsmodel.get_value(itr, 3)
1360                         contactName = self._contactsmodel.get_value(itr, 1)
1361                         try:
1362                                 contactDetails = self._addressBook.get_contact_details(contactId)
1363                         except Exception, e:
1364                                 contactDetails = []
1365                                 self._errorDisplay.push_exception()
1366                         contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1367
1368                         if len(contactPhoneNumbers) == 0:
1369                                 return
1370
1371                         action, phoneNumber, message = self._phoneTypeSelector.run(
1372                                 contactPhoneNumbers,
1373                                 message = contactName,
1374                                 parent = self._window,
1375                         )
1376                         if action == PhoneTypeSelector.ACTION_CANCEL:
1377                                 return
1378                         assert phoneNumber, "A lack of phone number exists"
1379
1380                         self.number_selected(action, phoneNumber, message)
1381                         self._contactsviewselection.unselect_all()
1382                 except Exception, e:
1383                         self._errorDisplay.push_exception()