Putting in a lot of fixes for fremantle
[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 """
23
24 from __future__ import with_statement
25
26 import ConfigParser
27 import logging
28 import itertools
29
30 import gobject
31 import pango
32 import gtk
33
34 import gtk_toolbox
35 import hildonize
36 import gv_backend
37 import null_backend
38
39
40 _moduleLogger = logging.getLogger("gv_views")
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 or charsLeft == self.MAX_CHAR:
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 = []
677                 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
678                 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
679                 self._onCallbackSelectChangedId = 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                 del self._callbackList[:]
703                 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
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._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
728                 self._onCallbackSelectChangedId = 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                 del self._callbackList[:]
750
751         def get_selected_callback_number(self):
752                 currentLabel = self._callbackSelectButton.get_label()
753                 if currentLabel is not None:
754                         return make_ugly(currentLabel)
755                 else:
756                         return ""
757
758         def set_account_number(self, number):
759                 """
760                 Displays current account number
761                 """
762                 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
763
764         def update(self, force = False):
765                 if not force and self._isPopulated:
766                         return False
767                 self._populate_callback_combo()
768                 self.set_account_number(self._backend.get_account_number())
769                 return True
770
771         def clear(self):
772                 self._callbackSelectButton.set_label("No Callback Number")
773                 self.set_account_number("")
774                 self._isPopulated = False
775
776         def save_everything(self):
777                 raise NotImplementedError
778
779         @staticmethod
780         def name():
781                 return "Account Info"
782
783         def load_settings(self, config, section):
784                 self._defaultCallback = config.get(section, "callback")
785                 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
786                 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
787                 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
788
789         def save_settings(self, config, section):
790                 """
791                 @note Thread Agnostic
792                 """
793                 callback = self.get_selected_callback_number()
794                 config.set(section, "callback", callback)
795                 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
796                 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
797                 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
798
799         def _populate_callback_combo(self):
800                 self._isPopulated = True
801                 del self._callbackList[:]
802                 try:
803                         callbackNumbers = self._backend.get_callback_numbers()
804                 except Exception, e:
805                         self._errorDisplay.push_exception()
806                         self._isPopulated = False
807                         return
808
809                 if len(callbackNumbers) == 0:
810                         callbackNumbers = {"": "No callback numbers available"}
811
812                 for number, description in callbackNumbers.iteritems():
813                         self._callbackList.append((make_pretty(number), description))
814
815                 self._set_callback_number(self._defaultCallback)
816
817         def _set_callback_number(self, number):
818                 try:
819                         if not self._backend.is_valid_syntax(number) and 0 < len(number):
820                                 self._errorDisplay.push_message("%s is not a valid callback number" % number)
821                         elif number == self._backend.get_callback_number() and 0 < len(number):
822                                 _moduleLogger.warning(
823                                         "Callback number already is %s" % (
824                                                 self._backend.get_callback_number(),
825                                         ),
826                                 )
827                         else:
828                                 self._backend.set_callback_number(number)
829                                 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
830                                         make_pretty(number), make_pretty(self._backend.get_callback_number())
831                                 )
832                                 prettyNumber = make_pretty(number)
833                                 if len(prettyNumber) == 0:
834                                         prettyNumber = "No Callback Number"
835                                 self._callbackSelectButton.set_label(prettyNumber)
836                                 _moduleLogger.info(
837                                         "Callback number set to %s" % (
838                                                 self._backend.get_callback_number(),
839                                         ),
840                                 )
841                 except Exception, e:
842                         self._errorDisplay.push_exception()
843
844         def _update_alarm_settings(self, recurrence):
845                 try:
846                         isEnabled = self._notifyCheckbox.get_active()
847                         if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
848                                 self._alarmHandler.apply_settings(isEnabled, recurrence)
849                 finally:
850                         self.save_everything()
851                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
852                         self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
853
854         def _on_callbackentry_clicked(self, *args):
855                 try:
856                         actualSelection = make_pretty(self.get_selected_callback_number())
857
858                         userOptions = dict(
859                                 (number, "%s (%s)" % (number, description))
860                                 for (number, description) in self._callbackList
861                         )
862                         defaultSelection = userOptions.get(actualSelection, actualSelection)
863
864                         userSelection = hildonize.touch_selector_entry(
865                                 self._window,
866                                 "Callback Number",
867                                 list(userOptions.itervalues()),
868                                 defaultSelection,
869                         )
870                         reversedUserOptions = dict(
871                                 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
872                         )
873                         selectedNumber = reversedUserOptions.get(userSelection, userSelection)
874
875                         number = make_ugly(selectedNumber)
876                         self._set_callback_number(number)
877                 except RuntimeError, e:
878                         _moduleLogger.exception("%s" % str(e))
879                 except Exception, e:
880                         self._errorDisplay.push_exception()
881
882         def _on_notify_toggled(self, *args):
883                 try:
884                         if self._applyAlarmTimeoutId is not None:
885                                 gobject.source_remove(self._applyAlarmTimeoutId)
886                                 self._applyAlarmTimeoutId = None
887                         self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
888                 except Exception, e:
889                         self._errorDisplay.push_exception()
890
891         def _on_minutes_clicked(self, *args):
892                 recurrenceChoices = [
893                         (1, "1 minute"),
894                         (2, "2 minutes"),
895                         (3, "3 minutes"),
896                         (5, "5 minutes"),
897                         (8, "8 minutes"),
898                         (10, "10 minutes"),
899                         (15, "15 minutes"),
900                         (30, "30 minutes"),
901                         (45, "45 minutes"),
902                         (60, "1 hour"),
903                         (3*60, "3 hours"),
904                         (6*60, "6 hours"),
905                         (12*60, "12 hours"),
906                 ]
907                 try:
908                         actualSelection = self._alarmHandler.recurrence
909
910                         closestSelectionIndex = 0
911                         for i, possible in enumerate(recurrenceChoices):
912                                 if possible[0] <= actualSelection:
913                                         closestSelectionIndex = i
914                         recurrenceIndex = hildonize.touch_selector(
915                                 self._window,
916                                 "Minutes",
917                                 (("%s" % m[1]) for m in recurrenceChoices),
918                                 closestSelectionIndex,
919                         )
920                         recurrence = recurrenceChoices[recurrenceIndex][0]
921
922                         self._update_alarm_settings(recurrence)
923                 except RuntimeError, e:
924                         _moduleLogger.exception("%s" % str(e))
925                 except Exception, e:
926                         self._errorDisplay.push_exception()
927
928         def _on_apply_timeout(self, *args):
929                 try:
930                         self._applyAlarmTimeoutId = None
931
932                         self._update_alarm_settings(self._alarmHandler.recurrence)
933                 except Exception, e:
934                         self._errorDisplay.push_exception()
935                 return False
936
937         def _on_missed_toggled(self, *args):
938                 try:
939                         self._notifyOnMissed = self._missedCheckbox.get_active()
940                         self.save_everything()
941                 except Exception, e:
942                         self._errorDisplay.push_exception()
943
944         def _on_voicemail_toggled(self, *args):
945                 try:
946                         self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
947                         self.save_everything()
948                 except Exception, e:
949                         self._errorDisplay.push_exception()
950
951         def _on_sms_toggled(self, *args):
952                 try:
953                         self._notifyOnSms = self._smsCheckbox.get_active()
954                         self.save_everything()
955                 except Exception, e:
956                         self._errorDisplay.push_exception()
957
958
959 class RecentCallsView(object):
960
961         NUMBER_IDX = 0
962         DATE_IDX = 1
963         ACTION_IDX = 2
964         FROM_IDX = 3
965
966         def __init__(self, widgetTree, backend, errorDisplay):
967                 self._errorDisplay = errorDisplay
968                 self._backend = backend
969
970                 self._isPopulated = False
971                 self._recentmodel = gtk.ListStore(
972                         gobject.TYPE_STRING, # number
973                         gobject.TYPE_STRING, # date
974                         gobject.TYPE_STRING, # action
975                         gobject.TYPE_STRING, # from
976                 )
977                 self._recentview = widgetTree.get_widget("recentview")
978                 self._recentviewselection = None
979                 self._onRecentviewRowActivatedId = 0
980
981                 textrenderer = gtk.CellRendererText()
982                 textrenderer.set_property("yalign", 0)
983                 self._dateColumn = gtk.TreeViewColumn("Date")
984                 self._dateColumn.pack_start(textrenderer, expand=True)
985                 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
986
987                 textrenderer = gtk.CellRendererText()
988                 textrenderer.set_property("yalign", 0)
989                 self._actionColumn = gtk.TreeViewColumn("Action")
990                 self._actionColumn.pack_start(textrenderer, expand=True)
991                 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
992
993                 textrenderer = gtk.CellRendererText()
994                 textrenderer.set_property("yalign", 0)
995                 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
996                 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
997                 self._numberColumn = gtk.TreeViewColumn("Number")
998                 self._numberColumn.pack_start(textrenderer, expand=True)
999                 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
1000
1001                 textrenderer = gtk.CellRendererText()
1002                 textrenderer.set_property("yalign", 0)
1003                 hildonize.set_cell_thumb_selectable(textrenderer)
1004                 self._nameColumn = gtk.TreeViewColumn("From")
1005                 self._nameColumn.pack_start(textrenderer, expand=True)
1006                 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
1007                 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1008
1009                 self._window = gtk_toolbox.find_parent_window(self._recentview)
1010                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1011
1012                 self._updateSink = gtk_toolbox.threaded_stage(
1013                         gtk_toolbox.comap(
1014                                 self._idly_populate_recentview,
1015                                 gtk_toolbox.null_sink(),
1016                         )
1017                 )
1018
1019         def enable(self):
1020                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1021                 self._recentview.set_model(self._recentmodel)
1022
1023                 self._recentview.append_column(self._dateColumn)
1024                 self._recentview.append_column(self._actionColumn)
1025                 self._recentview.append_column(self._numberColumn)
1026                 self._recentview.append_column(self._nameColumn)
1027                 self._recentviewselection = self._recentview.get_selection()
1028                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
1029
1030                 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
1031
1032         def disable(self):
1033                 self._recentview.disconnect(self._onRecentviewRowActivatedId)
1034
1035                 self.clear()
1036
1037                 self._recentview.remove_column(self._dateColumn)
1038                 self._recentview.remove_column(self._actionColumn)
1039                 self._recentview.remove_column(self._nameColumn)
1040                 self._recentview.remove_column(self._numberColumn)
1041                 self._recentview.set_model(None)
1042
1043         def number_selected(self, action, number, message):
1044                 """
1045                 @note Actual dial function is patched in later
1046                 """
1047                 raise NotImplementedError("Horrible unknown error has occurred")
1048
1049         def update(self, force = False):
1050                 if not force and self._isPopulated:
1051                         return False
1052                 self._updateSink.send(())
1053                 return True
1054
1055         def clear(self):
1056                 self._isPopulated = False
1057                 self._recentmodel.clear()
1058
1059         @staticmethod
1060         def name():
1061                 return "Recent Calls"
1062
1063         def load_settings(self, config, section):
1064                 pass
1065
1066         def save_settings(self, config, section):
1067                 """
1068                 @note Thread Agnostic
1069                 """
1070                 pass
1071
1072         def _idly_populate_recentview(self):
1073                 with gtk_toolbox.gtk_lock():
1074                         banner = hildonize.show_busy_banner_start(self._window, "Loading Recent History")
1075                 try:
1076                         self._recentmodel.clear()
1077                         self._isPopulated = True
1078
1079                         try:
1080                                 recentItems = self._backend.get_recent()
1081                         except Exception, e:
1082                                 self._errorDisplay.push_exception_with_lock()
1083                                 self._isPopulated = False
1084                                 recentItems = []
1085
1086                         recentItems = (
1087                                 gv_backend.decorate_recent(data)
1088                                 for data in gv_backend.sort_messages(recentItems)
1089                         )
1090
1091                         for personName, phoneNumber, date, action in recentItems:
1092                                 if not personName:
1093                                         personName = "Unknown"
1094                                 date = abbrev_relative_date(date)
1095                                 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1096                                 prettyNumber = make_pretty(prettyNumber)
1097                                 item = (prettyNumber, date, action.capitalize(), personName)
1098                                 with gtk_toolbox.gtk_lock():
1099                                         self._recentmodel.append(item)
1100                 except Exception, e:
1101                         self._errorDisplay.push_exception_with_lock()
1102                 finally:
1103                         with gtk_toolbox.gtk_lock():
1104                                 hildonize.show_busy_banner_end(banner)
1105
1106                 return False
1107
1108         def _on_recentview_row_activated(self, treeview, path, view_column):
1109                 try:
1110                         itr = self._recentmodel.get_iter(path)
1111                         if not itr:
1112                                 return
1113
1114                         number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1115                         number = make_ugly(number)
1116                         contactPhoneNumbers = [("Phone", number)]
1117                         description = self._recentmodel.get_value(itr, self.FROM_IDX)
1118
1119                         action, phoneNumber, message = self._phoneTypeSelector.run(
1120                                 contactPhoneNumbers,
1121                                 messages = (description, ),
1122                                 parent = self._window,
1123                         )
1124                         if action == PhoneTypeSelector.ACTION_CANCEL:
1125                                 return
1126                         assert phoneNumber, "A lack of phone number exists"
1127
1128                         self.number_selected(action, phoneNumber, message)
1129                         self._recentviewselection.unselect_all()
1130                 except Exception, e:
1131                         self._errorDisplay.push_exception()
1132
1133
1134 class MessagesView(object):
1135
1136         NUMBER_IDX = 0
1137         DATE_IDX = 1
1138         HEADER_IDX = 2
1139         MESSAGE_IDX = 3
1140         MESSAGES_IDX = 4
1141
1142         def __init__(self, widgetTree, backend, errorDisplay):
1143                 self._errorDisplay = errorDisplay
1144                 self._backend = backend
1145
1146                 self._isPopulated = False
1147                 self._messagemodel = gtk.ListStore(
1148                         gobject.TYPE_STRING, # number
1149                         gobject.TYPE_STRING, # date
1150                         gobject.TYPE_STRING, # header
1151                         gobject.TYPE_STRING, # message
1152                         object, # messages
1153                 )
1154                 self._messageview = widgetTree.get_widget("messages_view")
1155                 self._messageviewselection = None
1156                 self._onMessageviewRowActivatedId = 0
1157
1158                 self._messageRenderer = gtk.CellRendererText()
1159                 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1160                 self._messageRenderer.set_property("wrap-width", 500)
1161                 self._messageColumn = gtk.TreeViewColumn("Messages")
1162                 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1163                 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1164                 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1165
1166                 self._window = gtk_toolbox.find_parent_window(self._messageview)
1167                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1168
1169                 self._updateSink = gtk_toolbox.threaded_stage(
1170                         gtk_toolbox.comap(
1171                                 self._idly_populate_messageview,
1172                                 gtk_toolbox.null_sink(),
1173                         )
1174                 )
1175
1176         def enable(self):
1177                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1178                 self._messageview.set_model(self._messagemodel)
1179                 self._messageview.set_headers_visible(False)
1180
1181                 self._messageview.append_column(self._messageColumn)
1182                 self._messageviewselection = self._messageview.get_selection()
1183                 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1184
1185                 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1186
1187         def disable(self):
1188                 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1189
1190                 self.clear()
1191
1192                 self._messageview.remove_column(self._messageColumn)
1193                 self._messageview.set_model(None)
1194
1195         def number_selected(self, action, number, message):
1196                 """
1197                 @note Actual dial function is patched in later
1198                 """
1199                 raise NotImplementedError("Horrible unknown error has occurred")
1200
1201         def update(self, force = False):
1202                 if not force and self._isPopulated:
1203                         return False
1204                 self._updateSink.send(())
1205                 return True
1206
1207         def clear(self):
1208                 self._isPopulated = False
1209                 self._messagemodel.clear()
1210
1211         @staticmethod
1212         def name():
1213                 return "Messages"
1214
1215         def load_settings(self, config, section):
1216                 pass
1217
1218         def save_settings(self, config, section):
1219                 """
1220                 @note Thread Agnostic
1221                 """
1222                 pass
1223
1224         _MIN_MESSAGES_SHOWN = 4
1225
1226         def _idly_populate_messageview(self):
1227                 with gtk_toolbox.gtk_lock():
1228                         banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1229                 try:
1230                         self._messagemodel.clear()
1231                         self._isPopulated = True
1232
1233                         try:
1234                                 messageItems = self._backend.get_messages()
1235                         except Exception, e:
1236                                 self._errorDisplay.push_exception_with_lock()
1237                                 self._isPopulated = False
1238                                 messageItems = []
1239
1240                         messageItems = (
1241                                 gv_backend.decorate_message(message)
1242                                 for message in gv_backend.sort_messages(messageItems)
1243                         )
1244
1245                         for header, number, relativeDate, messages in messageItems:
1246                                 prettyNumber = number[2:] if number.startswith("+1") else number
1247                                 prettyNumber = make_pretty(prettyNumber)
1248
1249                                 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1250                                 expandedMessages = [firstMessage]
1251                                 expandedMessages.extend(messages)
1252                                 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1253                                         firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1254                                         secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1255                                         collapsedMessages = [firstMessage, secondMessage]
1256                                         collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1257                                 else:
1258                                         collapsedMessages = expandedMessages
1259
1260                                 number = make_ugly(number)
1261
1262                                 row = (number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages)
1263                                 with gtk_toolbox.gtk_lock():
1264                                         self._messagemodel.append(row)
1265                 except Exception, e:
1266                         self._errorDisplay.push_exception_with_lock()
1267                 finally:
1268                         with gtk_toolbox.gtk_lock():
1269                                 hildonize.show_busy_banner_end(banner)
1270
1271                 return False
1272
1273         def _on_messageview_row_activated(self, treeview, path, view_column):
1274                 try:
1275                         itr = self._messagemodel.get_iter(path)
1276                         if not itr:
1277                                 return
1278
1279                         contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1280                         description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1281
1282                         action, phoneNumber, message = self._phoneTypeSelector.run(
1283                                 contactPhoneNumbers,
1284                                 messages = description,
1285                                 parent = self._window,
1286                         )
1287                         if action == PhoneTypeSelector.ACTION_CANCEL:
1288                                 return
1289                         assert phoneNumber, "A lock of phone number exists"
1290
1291                         self.number_selected(action, phoneNumber, message)
1292                         self._messageviewselection.unselect_all()
1293                 except Exception, e:
1294                         self._errorDisplay.push_exception()
1295
1296
1297 class ContactsView(object):
1298
1299         def __init__(self, widgetTree, backend, errorDisplay):
1300                 self._errorDisplay = errorDisplay
1301                 self._backend = backend
1302
1303                 self._addressBook = None
1304                 self._selectedComboIndex = 0
1305                 self._addressBookFactories = [null_backend.NullAddressBook()]
1306
1307                 self._booksList = []
1308                 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1309
1310                 self._isPopulated = False
1311                 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1312                 self._contactsviewselection = None
1313                 self._contactsview = widgetTree.get_widget("contactsview")
1314
1315                 self._contactColumn = gtk.TreeViewColumn("Contact")
1316                 displayContactSource = False
1317                 if displayContactSource:
1318                         textrenderer = gtk.CellRendererText()
1319                         self._contactColumn.pack_start(textrenderer, expand=False)
1320                         self._contactColumn.add_attribute(textrenderer, 'text', 0)
1321                 textrenderer = gtk.CellRendererText()
1322                 hildonize.set_cell_thumb_selectable(textrenderer)
1323                 self._contactColumn.pack_start(textrenderer, expand=True)
1324                 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1325                 textrenderer = gtk.CellRendererText()
1326                 self._contactColumn.pack_start(textrenderer, expand=True)
1327                 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1328                 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1329                 self._contactColumn.set_sort_column_id(1)
1330                 self._contactColumn.set_visible(True)
1331
1332                 self._onContactsviewRowActivatedId = 0
1333                 self._onAddressbookButtonChangedId = 0
1334                 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1335                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1336
1337                 self._updateSink = gtk_toolbox.threaded_stage(
1338                         gtk_toolbox.comap(
1339                                 self._idly_populate_contactsview,
1340                                 gtk_toolbox.null_sink(),
1341                         )
1342                 )
1343
1344         def enable(self):
1345                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1346
1347                 self._contactsview.set_model(self._contactsmodel)
1348                 self._contactsview.append_column(self._contactColumn)
1349                 self._contactsviewselection = self._contactsview.get_selection()
1350                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1351
1352                 del self._booksList[:]
1353                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1354                         if factoryName and bookName:
1355                                 entryName = "%s: %s" % (factoryName, bookName)
1356                         elif factoryName:
1357                                 entryName = factoryName
1358                         elif bookName:
1359                                 entryName = bookName
1360                         else:
1361                                 entryName = "Bad name (%d)" % factoryId
1362                         row = (str(factoryId), bookId, entryName)
1363                         self._booksList.append(row)
1364
1365                 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1366                 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1367
1368                 if len(self._booksList) <= self._selectedComboIndex:
1369                         self._selectedComboIndex = 0
1370                 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1371
1372                 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1373                 selectedBookId = self._booksList[self._selectedComboIndex][1]
1374                 self.open_addressbook(selectedFactoryId, selectedBookId)
1375
1376         def disable(self):
1377                 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1378                 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1379
1380                 self.clear()
1381
1382                 self._bookSelectionButton.set_label("")
1383                 self._contactsview.set_model(None)
1384                 self._contactsview.remove_column(self._contactColumn)
1385
1386         def number_selected(self, action, number, message):
1387                 """
1388                 @note Actual dial function is patched in later
1389                 """
1390                 raise NotImplementedError("Horrible unknown error has occurred")
1391
1392         def get_addressbooks(self):
1393                 """
1394                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1395                 """
1396                 for i, factory in enumerate(self._addressBookFactories):
1397                         for bookFactory, bookId, bookName in factory.get_addressbooks():
1398                                 yield (str(i), bookId), (factory.factory_name(), bookName)
1399
1400         def open_addressbook(self, bookFactoryId, bookId):
1401                 bookFactoryIndex = int(bookFactoryId)
1402                 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1403
1404                 forceUpdate = True if addressBook is not self._addressBook else False
1405
1406                 self._addressBook = addressBook
1407                 self.update(force=forceUpdate)
1408
1409         def update(self, force = False):
1410                 if not force and self._isPopulated:
1411                         return False
1412                 self._updateSink.send(())
1413                 return True
1414
1415         def clear(self):
1416                 self._isPopulated = False
1417                 self._contactsmodel.clear()
1418                 for factory in self._addressBookFactories:
1419                         factory.clear_caches()
1420                 self._addressBook.clear_caches()
1421
1422         def append(self, book):
1423                 self._addressBookFactories.append(book)
1424
1425         def extend(self, books):
1426                 self._addressBookFactories.extend(books)
1427
1428         @staticmethod
1429         def name():
1430                 return "Contacts"
1431
1432         def load_settings(self, config, sectionName):
1433                 try:
1434                         self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1435                 except ConfigParser.NoOptionError:
1436                         self._selectedComboIndex = 0
1437
1438         def save_settings(self, config, sectionName):
1439                 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1440
1441         def _idly_populate_contactsview(self):
1442                 with gtk_toolbox.gtk_lock():
1443                         banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1444                 try:
1445                         addressBook = None
1446                         while addressBook is not self._addressBook:
1447                                 addressBook = self._addressBook
1448                                 with gtk_toolbox.gtk_lock():
1449                                         self._contactsview.set_model(None)
1450                                         self.clear()
1451
1452                                 try:
1453                                         contacts = addressBook.get_contacts()
1454                                 except Exception, e:
1455                                         contacts = []
1456                                         self._isPopulated = False
1457                                         self._errorDisplay.push_exception_with_lock()
1458                                 for contactId, contactName in contacts:
1459                                         contactType = (addressBook.contact_source_short_name(contactId), )
1460                                         self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1461
1462                                 with gtk_toolbox.gtk_lock():
1463                                         self._contactsview.set_model(self._contactsmodel)
1464
1465                         self._isPopulated = True
1466                 except Exception, e:
1467                         self._errorDisplay.push_exception_with_lock()
1468                 finally:
1469                         with gtk_toolbox.gtk_lock():
1470                                 hildonize.show_busy_banner_end(banner)
1471                 return False
1472
1473         def _on_addressbook_button_changed(self, *args, **kwds):
1474                 try:
1475                         try:
1476                                 newSelectedComboIndex = hildonize.touch_selector(
1477                                         self._window,
1478                                         "Addressbook",
1479                                         (("%s" % m[2]) for m in self._booksList),
1480                                         self._selectedComboIndex,
1481                                 )
1482                         except RuntimeError:
1483                                 return
1484
1485                         selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1486                         selectedBookId = self._booksList[newSelectedComboIndex][1]
1487                         self.open_addressbook(selectedFactoryId, selectedBookId)
1488                         self._selectedComboIndex = newSelectedComboIndex
1489                         self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1490                 except Exception, e:
1491                         self._errorDisplay.push_exception()
1492
1493         def _on_contactsview_row_activated(self, treeview, path, view_column):
1494                 try:
1495                         itr = self._contactsmodel.get_iter(path)
1496                         if not itr:
1497                                 return
1498
1499                         contactId = self._contactsmodel.get_value(itr, 3)
1500                         contactName = self._contactsmodel.get_value(itr, 1)
1501                         try:
1502                                 contactDetails = self._addressBook.get_contact_details(contactId)
1503                         except Exception, e:
1504                                 contactDetails = []
1505                                 self._errorDisplay.push_exception()
1506                         contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1507
1508                         if len(contactPhoneNumbers) == 0:
1509                                 return
1510
1511                         action, phoneNumber, message = self._phoneTypeSelector.run(
1512                                 contactPhoneNumbers,
1513                                 messages = (contactName, ),
1514                                 parent = self._window,
1515                         )
1516                         if action == PhoneTypeSelector.ACTION_CANCEL:
1517                                 return
1518                         assert phoneNumber, "A lack of phone number exists"
1519
1520                         self.number_selected(action, phoneNumber, message)
1521                         self._contactsviewselection.unselect_all()
1522                 except Exception, e:
1523                         self._errorDisplay.push_exception()