Accidently left some debug code in
[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
665                 self.update(force=True)
666
667         def disable(self):
668                 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
669                 self._onCallbackentryChangedId = 0
670
671                 if self._alarmHandler is not None:
672                         self._notifyCheckbox.disconnect(self._onNotifyToggled)
673                         self._minutesEntryButton.disconnect(self._onMinutesChanged)
674                         self._missedCheckbox.disconnect(self._onNotifyToggled)
675                         self._voicemailCheckbox.disconnect(self._onNotifyToggled)
676                         self._smsCheckbox.disconnect(self._onNotifyToggled)
677                         self._onNotifyToggled = 0
678                         self._onMinutesChanged = 0
679                         self._onMissedToggled = 0
680                         self._onVoicemailToggled = 0
681                         self._onSmsToggled = 0
682                 else:
683                         self._notifyCheckbox.set_sensitive(True)
684                         self._minutesEntryButton.set_sensitive(True)
685                         self._missedCheckbox.set_sensitive(True)
686                         self._voicemailCheckbox.set_sensitive(True)
687                         self._smsCheckbox.set_sensitive(True)
688
689                 self.clear()
690                 self._callbackList.clear()
691
692         def get_selected_callback_number(self):
693                 return make_ugly(self._callbackCombo.get_child().get_text())
694
695         def set_account_number(self, number):
696                 """
697                 Displays current account number
698                 """
699                 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
700
701         def update(self, force = False):
702                 if not force and self._isPopulated:
703                         return False
704                 self._populate_callback_combo()
705                 self.set_account_number(self._backend.get_account_number())
706                 return True
707
708         def clear(self):
709                 self._callbackCombo.get_child().set_text("")
710                 self.set_account_number("")
711                 self._isPopulated = False
712
713         def save_everything(self):
714                 raise NotImplementedError
715
716         @staticmethod
717         def name():
718                 return "Account Info"
719
720         def load_settings(self, config, section):
721                 self._defaultCallback = config.get(section, "callback")
722                 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
723                 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
724                 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
725
726         def save_settings(self, config, section):
727                 """
728                 @note Thread Agnostic
729                 """
730                 callback = self.get_selected_callback_number()
731                 config.set(section, "callback", callback)
732                 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
733                 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
734                 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
735
736         def _populate_callback_combo(self):
737                 self._isPopulated = True
738                 self._callbackList.clear()
739                 try:
740                         callbackNumbers = self._backend.get_callback_numbers()
741                 except Exception, e:
742                         self._errorDisplay.push_exception()
743                         self._isPopulated = False
744                         return
745
746                 for number, description in callbackNumbers.iteritems():
747                         self._callbackList.append((make_pretty(number),))
748
749                 self._callbackCombo.set_model(self._callbackList)
750                 self._callbackCombo.set_text_column(0)
751                 #callbackNumber = self._backend.get_callback_number()
752                 callbackNumber = self._defaultCallback
753                 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
754
755         def _set_callback_number(self, number):
756                 try:
757                         if not self._backend.is_valid_syntax(number) and 0 < len(number):
758                                 self._errorDisplay.push_message("%s is not a valid callback number" % number)
759                         elif number == self._backend.get_callback_number():
760                                 logging.warning(
761                                         "Callback number already is %s" % (
762                                                 self._backend.get_callback_number(),
763                                         ),
764                                 )
765                         else:
766                                 self._backend.set_callback_number(number)
767                                 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
768                                         make_pretty(number), make_pretty(self._backend.get_callback_number())
769                                 )
770                                 logging.info(
771                                         "Callback number set to %s" % (
772                                                 self._backend.get_callback_number(),
773                                         ),
774                                 )
775                 except Exception, e:
776                         self._errorDisplay.push_exception()
777
778         def _update_alarm_settings(self, recurrence):
779                 try:
780                         isEnabled = self._notifyCheckbox.get_active()
781                         if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
782                                 self._alarmHandler.apply_settings(isEnabled, recurrence)
783                 finally:
784                         self.save_everything()
785                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
786                         self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
787
788         def _on_callbackentry_changed(self, *args):
789                 try:
790                         text = self.get_selected_callback_number()
791                         number = make_ugly(text)
792                         self._set_callback_number(number)
793                 except Exception, e:
794                         self._errorDisplay.push_exception()
795
796         def _on_notify_toggled(self, *args):
797                 try:
798                         if self._applyAlarmTimeoutId is not None:
799                                 gobject.source_remove(self._applyAlarmTimeoutId)
800                                 self._applyAlarmTimeoutId = None
801                         self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
802                 except Exception, e:
803                         self._errorDisplay.push_exception()
804
805         def _on_minutes_clicked(self, *args):
806                 recurrenceChoices = [
807                         (1, "1 minute"),
808                         (3, "3 minutes"),
809                         (5, "5 minutes"),
810                         (10, "10 minutes"),
811                         (15, "15 minutes"),
812                         (30, "30 minutes"),
813                         (45, "45 minutes"),
814                         (60, "1 hour"),
815                         (12*60, "12 hours"),
816                 ]
817                 try:
818                         actualSelection = self._alarmHandler.recurrence
819
820                         closestSelectionIndex = 0
821                         for i, possible in enumerate(recurrenceChoices):
822                                 if possible[0] <= actualSelection:
823                                         closestSelectionIndex = i
824                         recurrenceIndex = hildonize.touch_selector(
825                                 self._window,
826                                 "Minutes",
827                                 (("%s" % m[1]) for m in recurrenceChoices),
828                                 closestSelectionIndex,
829                         )
830                         recurrence = recurrenceChoices[recurrenceIndex][0]
831
832                         self._update_alarm_settings(recurrence)
833                 except Exception, e:
834                         self._errorDisplay.push_exception()
835
836         def _on_apply_timeout(self, *args):
837                 try:
838                         self._applyAlarmTimeoutId = None
839
840                         self._update_alarm_settings(self._alarmHandler.recurrence)
841                 except Exception, e:
842                         self._errorDisplay.push_exception()
843                 return False
844
845         def _on_missed_toggled(self, *args):
846                 try:
847                         self._notifyOnMissed = self._missedCheckbox.get_active()
848                         self.save_everything()
849                 except Exception, e:
850                         self._errorDisplay.push_exception()
851
852         def _on_voicemail_toggled(self, *args):
853                 try:
854                         self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
855                         self.save_everything()
856                 except Exception, e:
857                         self._errorDisplay.push_exception()
858
859         def _on_sms_toggled(self, *args):
860                 try:
861                         self._notifyOnSms = self._smsCheckbox.get_active()
862                         self.save_everything()
863                 except Exception, e:
864                         self._errorDisplay.push_exception()
865
866
867 class RecentCallsView(object):
868
869         NUMBER_IDX = 0
870         DATE_IDX = 1
871         ACTION_IDX = 2
872         FROM_IDX = 3
873
874         def __init__(self, widgetTree, backend, errorDisplay):
875                 self._errorDisplay = errorDisplay
876                 self._backend = backend
877
878                 self._isPopulated = False
879                 self._recentmodel = gtk.ListStore(
880                         gobject.TYPE_STRING, # number
881                         gobject.TYPE_STRING, # date
882                         gobject.TYPE_STRING, # action
883                         gobject.TYPE_STRING, # from
884                 )
885                 self._recentview = widgetTree.get_widget("recentview")
886                 self._recentviewselection = None
887                 self._onRecentviewRowActivatedId = 0
888
889                 textrenderer = gtk.CellRendererText()
890                 textrenderer.set_property("yalign", 0)
891                 self._dateColumn = gtk.TreeViewColumn("Date")
892                 self._dateColumn.pack_start(textrenderer, expand=True)
893                 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
894
895                 textrenderer = gtk.CellRendererText()
896                 textrenderer.set_property("yalign", 0)
897                 self._actionColumn = gtk.TreeViewColumn("Action")
898                 self._actionColumn.pack_start(textrenderer, expand=True)
899                 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
900
901                 textrenderer = gtk.CellRendererText()
902                 textrenderer.set_property("yalign", 0)
903                 hildonize.set_cell_thumb_selectable(textrenderer)
904                 self._nameColumn = gtk.TreeViewColumn("From")
905                 self._nameColumn.pack_start(textrenderer, expand=True)
906                 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
907                 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
908
909                 textrenderer = gtk.CellRendererText()
910                 textrenderer.set_property("yalign", 0)
911                 self._numberColumn = gtk.TreeViewColumn("Number")
912                 self._numberColumn.pack_start(textrenderer, expand=True)
913                 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
914
915                 self._window = gtk_toolbox.find_parent_window(self._recentview)
916                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
917
918                 self._updateSink = gtk_toolbox.threaded_stage(
919                         gtk_toolbox.comap(
920                                 self._idly_populate_recentview,
921                                 gtk_toolbox.null_sink(),
922                         )
923                 )
924
925         def enable(self):
926                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
927                 self._recentview.set_model(self._recentmodel)
928
929                 self._recentview.append_column(self._dateColumn)
930                 self._recentview.append_column(self._actionColumn)
931                 self._recentview.append_column(self._numberColumn)
932                 self._recentview.append_column(self._nameColumn)
933                 self._recentviewselection = self._recentview.get_selection()
934                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
935
936                 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
937
938         def disable(self):
939                 self._recentview.disconnect(self._onRecentviewRowActivatedId)
940
941                 self.clear()
942
943                 self._recentview.remove_column(self._dateColumn)
944                 self._recentview.remove_column(self._actionColumn)
945                 self._recentview.remove_column(self._nameColumn)
946                 self._recentview.remove_column(self._numberColumn)
947                 self._recentview.set_model(None)
948
949         def number_selected(self, action, number, message):
950                 """
951                 @note Actual dial function is patched in later
952                 """
953                 raise NotImplementedError("Horrible unknown error has occurred")
954
955         def update(self, force = False):
956                 if not force and self._isPopulated:
957                         return False
958                 self._updateSink.send(())
959                 return True
960
961         def clear(self):
962                 self._isPopulated = False
963                 self._recentmodel.clear()
964
965         @staticmethod
966         def name():
967                 return "Recent Calls"
968
969         def load_settings(self, config, section):
970                 pass
971
972         def save_settings(self, config, section):
973                 """
974                 @note Thread Agnostic
975                 """
976                 pass
977
978         def _idly_populate_recentview(self):
979                 try:
980                         self._recentmodel.clear()
981                         self._isPopulated = True
982
983                         try:
984                                 recentItems = self._backend.get_recent()
985                         except Exception, e:
986                                 self._errorDisplay.push_exception_with_lock()
987                                 self._isPopulated = False
988                                 recentItems = []
989
990                         for personName, phoneNumber, date, action in recentItems:
991                                 if not personName:
992                                         personName = "Unknown"
993                                 date = abbrev_relative_date(date)
994                                 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
995                                 prettyNumber = make_pretty(prettyNumber)
996                                 item = (prettyNumber, date, action.capitalize(), personName)
997                                 with gtk_toolbox.gtk_lock():
998                                         self._recentmodel.append(item)
999                 except Exception, e:
1000                         self._errorDisplay.push_exception_with_lock()
1001
1002                 return False
1003
1004         def _on_recentview_row_activated(self, treeview, path, view_column):
1005                 try:
1006                         model, itr = self._recentviewselection.get_selected()
1007                         if not itr:
1008                                 return
1009
1010                         number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1011                         number = make_ugly(number)
1012                         contactPhoneNumbers = [("Phone", number)]
1013                         description = self._recentmodel.get_value(itr, self.FROM_IDX)
1014
1015                         action, phoneNumber, message = self._phoneTypeSelector.run(
1016                                 contactPhoneNumbers,
1017                                 message = description,
1018                                 parent = self._window,
1019                         )
1020                         if action == PhoneTypeSelector.ACTION_CANCEL:
1021                                 return
1022                         assert phoneNumber, "A lack of phone number exists"
1023
1024                         self.number_selected(action, phoneNumber, message)
1025                         self._recentviewselection.unselect_all()
1026                 except Exception, e:
1027                         self._errorDisplay.push_exception()
1028
1029
1030 class MessagesView(object):
1031
1032         NUMBER_IDX = 0
1033         DATE_IDX = 1
1034         HEADER_IDX = 2
1035         MESSAGE_IDX = 3
1036
1037         def __init__(self, widgetTree, backend, errorDisplay):
1038                 self._errorDisplay = errorDisplay
1039                 self._backend = backend
1040
1041                 self._isPopulated = False
1042                 self._messagemodel = gtk.ListStore(
1043                         gobject.TYPE_STRING, # number
1044                         gobject.TYPE_STRING, # date
1045                         gobject.TYPE_STRING, # header
1046                         gobject.TYPE_STRING, # message
1047                 )
1048                 self._messageview = widgetTree.get_widget("messages_view")
1049                 self._messageviewselection = None
1050                 self._onMessageviewRowActivatedId = 0
1051
1052                 self._messageRenderer = gtk.CellRendererText()
1053                 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1054                 self._messageRenderer.set_property("wrap-width", 500)
1055                 self._messageColumn = gtk.TreeViewColumn("Messages")
1056                 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1057                 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1058                 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1059
1060                 self._window = gtk_toolbox.find_parent_window(self._messageview)
1061                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1062
1063                 self._updateSink = gtk_toolbox.threaded_stage(
1064                         gtk_toolbox.comap(
1065                                 self._idly_populate_messageview,
1066                                 gtk_toolbox.null_sink(),
1067                         )
1068                 )
1069
1070         def enable(self):
1071                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1072                 self._messageview.set_model(self._messagemodel)
1073
1074                 self._messageview.append_column(self._messageColumn)
1075                 self._messageviewselection = self._messageview.get_selection()
1076                 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1077
1078                 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1079
1080         def disable(self):
1081                 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1082
1083                 self.clear()
1084
1085                 self._messageview.remove_column(self._messageColumn)
1086                 self._messageview.set_model(None)
1087
1088         def number_selected(self, action, number, message):
1089                 """
1090                 @note Actual dial function is patched in later
1091                 """
1092                 raise NotImplementedError("Horrible unknown error has occurred")
1093
1094         def update(self, force = False):
1095                 if not force and self._isPopulated:
1096                         return False
1097                 self._updateSink.send(())
1098                 return True
1099
1100         def clear(self):
1101                 self._isPopulated = False
1102                 self._messagemodel.clear()
1103
1104         @staticmethod
1105         def name():
1106                 return "Messages"
1107
1108         def load_settings(self, config, section):
1109                 pass
1110
1111         def save_settings(self, config, section):
1112                 """
1113                 @note Thread Agnostic
1114                 """
1115                 pass
1116
1117         def _idly_populate_messageview(self):
1118                 try:
1119                         self._messagemodel.clear()
1120                         self._isPopulated = True
1121
1122                         try:
1123                                 messageItems = self._backend.get_messages()
1124                         except Exception, e:
1125                                 self._errorDisplay.push_exception_with_lock()
1126                                 self._isPopulated = False
1127                                 messageItems = []
1128
1129                         for header, number, relativeDate, message in messageItems:
1130                                 prettyNumber = number[2:] if number.startswith("+1") else number
1131                                 prettyNumber = make_pretty(prettyNumber)
1132                                 message = "<b>%s - %s</b> <i>(%s)</i>\n\n%s" % (header, prettyNumber, relativeDate, message)
1133                                 number = make_ugly(number)
1134                                 row = (number, relativeDate, header, message)
1135                                 with gtk_toolbox.gtk_lock():
1136                                         self._messagemodel.append(row)
1137                 except Exception, e:
1138                         self._errorDisplay.push_exception_with_lock()
1139
1140                 return False
1141
1142         def _on_messageview_row_activated(self, treeview, path, view_column):
1143                 try:
1144                         model, itr = self._messageviewselection.get_selected()
1145                         if not itr:
1146                                 return
1147
1148                         contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1149                         description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
1150
1151                         action, phoneNumber, message = self._phoneTypeSelector.run(
1152                                 contactPhoneNumbers,
1153                                 message = description,
1154                                 parent = self._window,
1155                         )
1156                         if action == PhoneTypeSelector.ACTION_CANCEL:
1157                                 return
1158                         assert phoneNumber, "A lock of phone number exists"
1159
1160                         self.number_selected(action, phoneNumber, message)
1161                         self._messageviewselection.unselect_all()
1162                 except Exception, e:
1163                         self._errorDisplay.push_exception()
1164
1165
1166 class ContactsView(object):
1167
1168         def __init__(self, widgetTree, backend, errorDisplay):
1169                 self._errorDisplay = errorDisplay
1170                 self._backend = backend
1171
1172                 self._addressBook = None
1173                 self._selectedComboIndex = 0
1174                 self._addressBookFactories = [null_backend.NullAddressBook()]
1175
1176                 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1177                 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
1178
1179                 self._isPopulated = False
1180                 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1181                 self._contactsviewselection = None
1182                 self._contactsview = widgetTree.get_widget("contactsview")
1183
1184                 self._contactColumn = gtk.TreeViewColumn("Contact")
1185                 displayContactSource = False
1186                 if displayContactSource:
1187                         textrenderer = gtk.CellRendererText()
1188                         self._contactColumn.pack_start(textrenderer, expand=False)
1189                         self._contactColumn.add_attribute(textrenderer, 'text', 0)
1190                 textrenderer = gtk.CellRendererText()
1191                 hildonize.set_cell_thumb_selectable(textrenderer)
1192                 self._contactColumn.pack_start(textrenderer, expand=True)
1193                 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1194                 textrenderer = gtk.CellRendererText()
1195                 self._contactColumn.pack_start(textrenderer, expand=True)
1196                 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1197                 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1198                 self._contactColumn.set_sort_column_id(1)
1199                 self._contactColumn.set_visible(True)
1200
1201                 self._onContactsviewRowActivatedId = 0
1202                 self._onAddressbookComboChangedId = 0
1203                 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1204                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1205
1206                 self._updateSink = gtk_toolbox.threaded_stage(
1207                         gtk_toolbox.comap(
1208                                 self._idly_populate_contactsview,
1209                                 gtk_toolbox.null_sink(),
1210                         )
1211                 )
1212
1213         def enable(self):
1214                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1215
1216                 self._contactsview.set_model(self._contactsmodel)
1217                 self._contactsview.append_column(self._contactColumn)
1218                 self._contactsviewselection = self._contactsview.get_selection()
1219                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1220
1221                 self._booksList.clear()
1222                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1223                         if factoryName and bookName:
1224                                 entryName = "%s: %s" % (factoryName, bookName)
1225                         elif factoryName:
1226                                 entryName = factoryName
1227                         elif bookName:
1228                                 entryName = bookName
1229                         else:
1230                                 entryName = "Bad name (%d)" % factoryId
1231                         row = (str(factoryId), bookId, entryName)
1232                         self._booksList.append(row)
1233
1234                 self._booksSelectionBox.set_model(self._booksList)
1235                 cell = gtk.CellRendererText()
1236                 self._booksSelectionBox.pack_start(cell, True)
1237                 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1238
1239                 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1240                 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1241
1242                 if len(self._booksList) <= self._selectedComboIndex:
1243                         self._selectedComboIndex = 0
1244                 self._booksSelectionBox.set_active(self._selectedComboIndex)
1245
1246         def disable(self):
1247                 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1248                 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1249
1250                 self.clear()
1251
1252                 self._booksSelectionBox.clear()
1253                 self._booksSelectionBox.set_model(None)
1254                 self._contactsview.set_model(None)
1255                 self._contactsview.remove_column(self._contactColumn)
1256
1257         def number_selected(self, action, number, message):
1258                 """
1259                 @note Actual dial function is patched in later
1260                 """
1261                 raise NotImplementedError("Horrible unknown error has occurred")
1262
1263         def get_addressbooks(self):
1264                 """
1265                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1266                 """
1267                 for i, factory in enumerate(self._addressBookFactories):
1268                         for bookFactory, bookId, bookName in factory.get_addressbooks():
1269                                 yield (str(i), bookId), (factory.factory_name(), bookName)
1270
1271         def open_addressbook(self, bookFactoryId, bookId):
1272                 bookFactoryIndex = int(bookFactoryId)
1273                 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1274
1275                 forceUpdate = True if addressBook is not self._addressBook else False
1276
1277                 self._addressBook = addressBook
1278                 self.update(force=forceUpdate)
1279
1280         def update(self, force = False):
1281                 if not force and self._isPopulated:
1282                         return False
1283                 self._updateSink.send(())
1284                 return True
1285
1286         def clear(self):
1287                 self._isPopulated = False
1288                 self._contactsmodel.clear()
1289                 for factory in self._addressBookFactories:
1290                         factory.clear_caches()
1291                 self._addressBook.clear_caches()
1292
1293         def append(self, book):
1294                 self._addressBookFactories.append(book)
1295
1296         def extend(self, books):
1297                 self._addressBookFactories.extend(books)
1298
1299         @staticmethod
1300         def name():
1301                 return "Contacts"
1302
1303         def load_settings(self, config, sectionName):
1304                 try:
1305                         self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1306                 except ConfigParser.NoOptionError:
1307                         self._selectedComboIndex = 0
1308
1309         def save_settings(self, config, sectionName):
1310                 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1311
1312         def _idly_populate_contactsview(self):
1313                 try:
1314                         addressBook = None
1315                         while addressBook is not self._addressBook:
1316                                 addressBook = self._addressBook
1317                                 with gtk_toolbox.gtk_lock():
1318                                         self._contactsview.set_model(None)
1319                                         self.clear()
1320
1321                                 try:
1322                                         contacts = addressBook.get_contacts()
1323                                 except Exception, e:
1324                                         contacts = []
1325                                         self._isPopulated = False
1326                                         self._errorDisplay.push_exception_with_lock()
1327                                 for contactId, contactName in contacts:
1328                                         contactType = (addressBook.contact_source_short_name(contactId), )
1329                                         self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1330
1331                                 with gtk_toolbox.gtk_lock():
1332                                         self._contactsview.set_model(self._contactsmodel)
1333
1334                         self._isPopulated = True
1335                 except Exception, e:
1336                         self._errorDisplay.push_exception_with_lock()
1337                 return False
1338
1339         def _on_addressbook_combo_changed(self, *args, **kwds):
1340                 try:
1341                         itr = self._booksSelectionBox.get_active_iter()
1342                         if itr is None:
1343                                 return
1344                         self._selectedComboIndex = self._booksSelectionBox.get_active()
1345                         selectedFactoryId = self._booksList.get_value(itr, 0)
1346                         selectedBookId = self._booksList.get_value(itr, 1)
1347                         self.open_addressbook(selectedFactoryId, selectedBookId)
1348                 except Exception, e:
1349                         self._errorDisplay.push_exception()
1350
1351         def _on_contactsview_row_activated(self, treeview, path, view_column):
1352                 try:
1353                         model, itr = self._contactsviewselection.get_selected()
1354                         if not itr:
1355                                 return
1356
1357                         contactId = self._contactsmodel.get_value(itr, 3)
1358                         contactName = self._contactsmodel.get_value(itr, 1)
1359                         try:
1360                                 contactDetails = self._addressBook.get_contact_details(contactId)
1361                         except Exception, e:
1362                                 contactDetails = []
1363                                 self._errorDisplay.push_exception()
1364                         contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1365
1366                         if len(contactPhoneNumbers) == 0:
1367                                 return
1368
1369                         action, phoneNumber, message = self._phoneTypeSelector.run(
1370                                 contactPhoneNumbers,
1371                                 message = contactName,
1372                                 parent = self._window,
1373                         )
1374                         if action == PhoneTypeSelector.ACTION_CANCEL:
1375                                 return
1376                         assert phoneNumber, "A lack of phone number exists"
1377
1378                         self.number_selected(action, phoneNumber, message)
1379                         self._contactsviewselection.unselect_all()
1380                 except Exception, e:
1381                         self._errorDisplay.push_exception()