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