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