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