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