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