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