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