ErorDisplay overkill now in effect
[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                 hildonize.set_cell_thumb_selectable(textrenderer)
890                 self._numberColumn = gtk.TreeViewColumn("Number")
891                 self._numberColumn.pack_start(textrenderer, expand=True)
892                 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
893
894                 self._window = gtk_toolbox.find_parent_window(self._recentview)
895                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
896
897                 self._updateSink = gtk_toolbox.threaded_stage(
898                         gtk_toolbox.comap(
899                                 self._idly_populate_recentview,
900                                 gtk_toolbox.null_sink(),
901                         )
902                 )
903
904         def enable(self):
905                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
906                 self._recentview.set_model(self._recentmodel)
907
908                 self._recentview.append_column(self._dateColumn)
909                 self._recentview.append_column(self._actionColumn)
910                 self._recentview.append_column(self._numberColumn)
911                 self._recentview.append_column(self._nameColumn)
912                 self._recentviewselection = self._recentview.get_selection()
913                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
914
915                 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
916
917         def disable(self):
918                 self._recentview.disconnect(self._onRecentviewRowActivatedId)
919
920                 self.clear()
921
922                 self._recentview.remove_column(self._dateColumn)
923                 self._recentview.remove_column(self._actionColumn)
924                 self._recentview.remove_column(self._nameColumn)
925                 self._recentview.remove_column(self._numberColumn)
926                 self._recentview.set_model(None)
927
928         def number_selected(self, action, number, message):
929                 """
930                 @note Actual dial function is patched in later
931                 """
932                 raise NotImplementedError("Horrible unknown error has occurred")
933
934         def update(self, force = False):
935                 if not force and self._isPopulated:
936                         return False
937                 self._updateSink.send(())
938                 return True
939
940         def clear(self):
941                 self._isPopulated = False
942                 self._recentmodel.clear()
943
944         @staticmethod
945         def name():
946                 return "Recent Calls"
947
948         def load_settings(self, config, section):
949                 pass
950
951         def save_settings(self, config, section):
952                 """
953                 @note Thread Agnostic
954                 """
955                 pass
956
957         def _idly_populate_recentview(self):
958                 try:
959                         self._recentmodel.clear()
960                         self._isPopulated = True
961
962                         try:
963                                 recentItems = self._backend.get_recent()
964                         except Exception, e:
965                                 self._errorDisplay.push_exception_with_lock()
966                                 self._isPopulated = False
967                                 recentItems = []
968
969                         for personName, phoneNumber, date, action in recentItems:
970                                 if not personName:
971                                         personName = "Unknown"
972                                 date = abbrev_relative_date(date)
973                                 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
974                                 prettyNumber = make_pretty(prettyNumber)
975                                 item = (prettyNumber, date, action.capitalize(), personName)
976                                 with gtk_toolbox.gtk_lock():
977                                         self._recentmodel.append(item)
978                 except Exception, e:
979                         self._errorDisplay.push_exception_with_lock()
980
981                 return False
982
983         def _on_recentview_row_activated(self, treeview, path, view_column):
984                 try:
985                         model, itr = self._recentviewselection.get_selected()
986                         if not itr:
987                                 return
988
989                         number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
990                         number = make_ugly(number)
991                         contactPhoneNumbers = [("Phone", number)]
992                         description = self._recentmodel.get_value(itr, self.FROM_IDX)
993
994                         action, phoneNumber, message = self._phoneTypeSelector.run(
995                                 contactPhoneNumbers,
996                                 message = description,
997                                 parent = self._window,
998                         )
999                         if action == PhoneTypeSelector.ACTION_CANCEL:
1000                                 return
1001                         assert phoneNumber, "A lack of phone number exists"
1002
1003                         self.number_selected(action, phoneNumber, message)
1004                         self._recentviewselection.unselect_all()
1005                 except Exception, e:
1006                         self._errorDisplay.push_exception()
1007
1008
1009 class MessagesView(object):
1010
1011         NUMBER_IDX = 0
1012         DATE_IDX = 1
1013         HEADER_IDX = 2
1014         MESSAGE_IDX = 3
1015
1016         def __init__(self, widgetTree, backend, errorDisplay):
1017                 self._errorDisplay = errorDisplay
1018                 self._backend = backend
1019
1020                 self._isPopulated = False
1021                 self._messagemodel = gtk.ListStore(
1022                         gobject.TYPE_STRING, # number
1023                         gobject.TYPE_STRING, # date
1024                         gobject.TYPE_STRING, # header
1025                         gobject.TYPE_STRING, # message
1026                 )
1027                 self._messageview = widgetTree.get_widget("messages_view")
1028                 self._messageviewselection = None
1029                 self._onMessageviewRowActivatedId = 0
1030
1031                 self._messageRenderer = gtk.CellRendererText()
1032                 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1033                 self._messageRenderer.set_property("wrap-width", 500)
1034                 self._messageColumn = gtk.TreeViewColumn("Messages")
1035                 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1036                 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1037                 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1038
1039                 self._window = gtk_toolbox.find_parent_window(self._messageview)
1040                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1041
1042                 self._updateSink = gtk_toolbox.threaded_stage(
1043                         gtk_toolbox.comap(
1044                                 self._idly_populate_messageview,
1045                                 gtk_toolbox.null_sink(),
1046                         )
1047                 )
1048
1049         def enable(self):
1050                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1051                 self._messageview.set_model(self._messagemodel)
1052
1053                 self._messageview.append_column(self._messageColumn)
1054                 self._messageviewselection = self._messageview.get_selection()
1055                 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1056
1057                 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1058
1059         def disable(self):
1060                 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1061
1062                 self.clear()
1063
1064                 self._messageview.remove_column(self._messageColumn)
1065                 self._messageview.set_model(None)
1066
1067         def number_selected(self, action, number, message):
1068                 """
1069                 @note Actual dial function is patched in later
1070                 """
1071                 raise NotImplementedError("Horrible unknown error has occurred")
1072
1073         def update(self, force = False):
1074                 if not force and self._isPopulated:
1075                         return False
1076                 self._updateSink.send(())
1077                 return True
1078
1079         def clear(self):
1080                 self._isPopulated = False
1081                 self._messagemodel.clear()
1082
1083         @staticmethod
1084         def name():
1085                 return "Messages"
1086
1087         def load_settings(self, config, section):
1088                 pass
1089
1090         def save_settings(self, config, section):
1091                 """
1092                 @note Thread Agnostic
1093                 """
1094                 pass
1095
1096         def _idly_populate_messageview(self):
1097                 try:
1098                         self._messagemodel.clear()
1099                         self._isPopulated = True
1100
1101                         try:
1102                                 messageItems = self._backend.get_messages()
1103                         except Exception, e:
1104                                 self._errorDisplay.push_exception_with_lock()
1105                                 self._isPopulated = False
1106                                 messageItems = []
1107
1108                         for header, number, relativeDate, message in messageItems:
1109                                 prettyNumber = number[2:] if number.startswith("+1") else number
1110                                 prettyNumber = make_pretty(prettyNumber)
1111                                 message = "<b>%s - %s</b> <i>(%s)</i>\n\n%s" % (header, prettyNumber, relativeDate, message)
1112                                 number = make_ugly(number)
1113                                 row = (number, relativeDate, header, message)
1114                                 with gtk_toolbox.gtk_lock():
1115                                         self._messagemodel.append(row)
1116                 except Exception, e:
1117                         self._errorDisplay.push_exception_with_lock()
1118
1119                 return False
1120
1121         def _on_messageview_row_activated(self, treeview, path, view_column):
1122                 try:
1123                         model, itr = self._messageviewselection.get_selected()
1124                         if not itr:
1125                                 return
1126
1127                         contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1128                         description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
1129
1130                         action, phoneNumber, message = self._phoneTypeSelector.run(
1131                                 contactPhoneNumbers,
1132                                 message = description,
1133                                 parent = self._window,
1134                         )
1135                         if action == PhoneTypeSelector.ACTION_CANCEL:
1136                                 return
1137                         assert phoneNumber, "A lock of phone number exists"
1138
1139                         self.number_selected(action, phoneNumber, message)
1140                         self._messageviewselection.unselect_all()
1141                 except Exception, e:
1142                         self._errorDisplay.push_exception()
1143
1144
1145 class ContactsView(object):
1146
1147         def __init__(self, widgetTree, backend, errorDisplay):
1148                 self._errorDisplay = errorDisplay
1149                 self._backend = backend
1150
1151                 self._addressBook = None
1152                 self._selectedComboIndex = 0
1153                 self._addressBookFactories = [null_backend.NullAddressBook()]
1154
1155                 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1156                 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
1157
1158                 self._isPopulated = False
1159                 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1160                 self._contactsviewselection = None
1161                 self._contactsview = widgetTree.get_widget("contactsview")
1162
1163                 self._contactColumn = gtk.TreeViewColumn("Contact")
1164                 displayContactSource = False
1165                 if displayContactSource:
1166                         textrenderer = gtk.CellRendererText()
1167                         self._contactColumn.pack_start(textrenderer, expand=False)
1168                         self._contactColumn.add_attribute(textrenderer, 'text', 0)
1169                 textrenderer = gtk.CellRendererText()
1170                 hildonize.set_cell_thumb_selectable(textrenderer)
1171                 self._contactColumn.pack_start(textrenderer, expand=True)
1172                 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1173                 textrenderer = gtk.CellRendererText()
1174                 self._contactColumn.pack_start(textrenderer, expand=True)
1175                 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1176                 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1177                 self._contactColumn.set_sort_column_id(1)
1178                 self._contactColumn.set_visible(True)
1179
1180                 self._onContactsviewRowActivatedId = 0
1181                 self._onAddressbookComboChangedId = 0
1182                 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1183                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1184
1185                 self._updateSink = gtk_toolbox.threaded_stage(
1186                         gtk_toolbox.comap(
1187                                 self._idly_populate_contactsview,
1188                                 gtk_toolbox.null_sink(),
1189                         )
1190                 )
1191
1192         def enable(self):
1193                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1194
1195                 self._contactsview.set_model(self._contactsmodel)
1196                 self._contactsview.append_column(self._contactColumn)
1197                 self._contactsviewselection = self._contactsview.get_selection()
1198                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1199
1200                 self._booksList.clear()
1201                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1202                         if factoryName and bookName:
1203                                 entryName = "%s: %s" % (factoryName, bookName)
1204                         elif factoryName:
1205                                 entryName = factoryName
1206                         elif bookName:
1207                                 entryName = bookName
1208                         else:
1209                                 entryName = "Bad name (%d)" % factoryId
1210                         row = (str(factoryId), bookId, entryName)
1211                         self._booksList.append(row)
1212
1213                 self._booksSelectionBox.set_model(self._booksList)
1214                 cell = gtk.CellRendererText()
1215                 self._booksSelectionBox.pack_start(cell, True)
1216                 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1217
1218                 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1219                 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1220
1221                 if len(self._booksList) <= self._selectedComboIndex:
1222                         self._selectedComboIndex = 0
1223                 self._booksSelectionBox.set_active(self._selectedComboIndex)
1224
1225         def disable(self):
1226                 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1227                 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1228
1229                 self.clear()
1230
1231                 self._booksSelectionBox.clear()
1232                 self._booksSelectionBox.set_model(None)
1233                 self._contactsview.set_model(None)
1234                 self._contactsview.remove_column(self._contactColumn)
1235
1236         def number_selected(self, action, number, message):
1237                 """
1238                 @note Actual dial function is patched in later
1239                 """
1240                 raise NotImplementedError("Horrible unknown error has occurred")
1241
1242         def get_addressbooks(self):
1243                 """
1244                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1245                 """
1246                 for i, factory in enumerate(self._addressBookFactories):
1247                         for bookFactory, bookId, bookName in factory.get_addressbooks():
1248                                 yield (str(i), bookId), (factory.factory_name(), bookName)
1249
1250         def open_addressbook(self, bookFactoryId, bookId):
1251                 bookFactoryIndex = int(bookFactoryId)
1252                 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1253
1254                 forceUpdate = True if addressBook is not self._addressBook else False
1255
1256                 self._addressBook = addressBook
1257                 self.update(force=forceUpdate)
1258
1259         def update(self, force = False):
1260                 if not force and self._isPopulated:
1261                         return False
1262                 self._updateSink.send(())
1263                 return True
1264
1265         def clear(self):
1266                 self._isPopulated = False
1267                 self._contactsmodel.clear()
1268                 for factory in self._addressBookFactories:
1269                         factory.clear_caches()
1270                 self._addressBook.clear_caches()
1271
1272         def append(self, book):
1273                 self._addressBookFactories.append(book)
1274
1275         def extend(self, books):
1276                 self._addressBookFactories.extend(books)
1277
1278         @staticmethod
1279         def name():
1280                 return "Contacts"
1281
1282         def load_settings(self, config, sectionName):
1283                 try:
1284                         self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1285                 except ConfigParser.NoOptionError:
1286                         self._selectedComboIndex = 0
1287
1288         def save_settings(self, config, sectionName):
1289                 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1290
1291         def _idly_populate_contactsview(self):
1292                 try:
1293                         addressBook = None
1294                         while addressBook is not self._addressBook:
1295                                 addressBook = self._addressBook
1296                                 with gtk_toolbox.gtk_lock():
1297                                         self._contactsview.set_model(None)
1298                                         self.clear()
1299
1300                                 try:
1301                                         contacts = addressBook.get_contacts()
1302                                 except Exception, e:
1303                                         contacts = []
1304                                         self._isPopulated = False
1305                                         self._errorDisplay.push_exception_with_lock()
1306                                 for contactId, contactName in contacts:
1307                                         contactType = (addressBook.contact_source_short_name(contactId), )
1308                                         self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1309
1310                                 with gtk_toolbox.gtk_lock():
1311                                         self._contactsview.set_model(self._contactsmodel)
1312
1313                         self._isPopulated = True
1314                 except Exception, e:
1315                         self._errorDisplay.push_exception_with_lock()
1316                 return False
1317
1318         def _on_addressbook_combo_changed(self, *args, **kwds):
1319                 try:
1320                         itr = self._booksSelectionBox.get_active_iter()
1321                         if itr is None:
1322                                 return
1323                         self._selectedComboIndex = self._booksSelectionBox.get_active()
1324                         selectedFactoryId = self._booksList.get_value(itr, 0)
1325                         selectedBookId = self._booksList.get_value(itr, 1)
1326                         self.open_addressbook(selectedFactoryId, selectedBookId)
1327                 except Exception, e:
1328                         self._errorDisplay.push_exception()
1329
1330         def _on_contactsview_row_activated(self, treeview, path, view_column):
1331                 try:
1332                         model, itr = self._contactsviewselection.get_selected()
1333                         if not itr:
1334                                 return
1335
1336                         contactId = self._contactsmodel.get_value(itr, 3)
1337                         contactName = self._contactsmodel.get_value(itr, 1)
1338                         try:
1339                                 contactDetails = self._addressBook.get_contact_details(contactId)
1340                         except Exception, e:
1341                                 contactDetails = []
1342                                 self._errorDisplay.push_exception()
1343                         contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1344
1345                         if len(contactPhoneNumbers) == 0:
1346                                 return
1347
1348                         action, phoneNumber, message = self._phoneTypeSelector.run(
1349                                 contactPhoneNumbers,
1350                                 message = contactName,
1351                                 parent = self._window,
1352                         )
1353                         if action == PhoneTypeSelector.ACTION_CANCEL:
1354                                 return
1355                         assert phoneNumber, "A lack of phone number exists"
1356
1357                         self.number_selected(action, phoneNumber, message)
1358                         self._contactsviewselection.unselect_all()
1359                 except Exception, e:
1360                         self._errorDisplay.push_exception()