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