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