Playing with explicit fixed height mode in other places
[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 SmsEntryDialog(object):
269         """
270         @todo Add multi-SMS messages like GoogleVoice
271         """
272
273         ACTION_CANCEL = "cancel"
274         ACTION_SELECT = "select"
275         ACTION_DIAL = "dial"
276         ACTION_SEND_SMS = "sms"
277
278         MAX_CHAR = 160
279
280         def __init__(self, widgetTree):
281                 self._clipboard = gtk.clipboard_get()
282                 self._widgetTree = widgetTree
283                 self._dialog = self._widgetTree.get_widget("smsDialog")
284
285                 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
286                 self._smsButton.connect("clicked", self._on_send)
287                 self._dialButton = self._widgetTree.get_widget("dialButton")
288                 self._dialButton.connect("clicked", self._on_dial)
289                 self._selectButton = self._widgetTree.get_widget("selectButton")
290                 self._selectButton.connect("clicked", self._on_select)
291                 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
292                 self._cancelButton.connect("clicked", self._on_cancel)
293
294                 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
295
296                 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
297                 self._messagesView = self._widgetTree.get_widget("smsMessages")
298                 self._scrollWindow = self._messagesView.get_parent()
299
300                 self._phoneButton = self._widgetTree.get_widget("phoneTypeSelection")
301                 self._smsEntry = self._widgetTree.get_widget("smsEntry")
302
303                 self._action = self.ACTION_CANCEL
304
305                 self._numberIndex = -1
306                 self._contactDetails = []
307
308         def run(self, contactDetails, messages = (), parent = None):
309                 entryConnectId = self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
310                 phoneConnectId = self._phoneButton.connect("clicked", self._on_phone)
311                 keyConnectId = self._keyPressEventId = self._dialog.connect("key-press-event", self._on_key_press)
312                 try:
313                         # Setup the phone selection button
314                         del self._contactDetails[:]
315                         for phoneType, phoneNumber in contactDetails:
316                                 display = " - ".join((make_pretty(phoneNumber), phoneType))
317                                 row = (phoneNumber, display)
318                                 self._contactDetails.append(row)
319                         if 0 < len(self._contactDetails):
320                                 self._numberIndex = 0
321                                 self._phoneButton.set_label(self._contactDetails[0][1])
322                                 self._phoneButton.set_sensitive(True)
323                         else:
324                                 self._numberIndex = -1
325                                 self._phoneButton.set_label("Error: No Number Available")
326                                 self._phoneButton.set_sensitive(False)
327
328                         # Add the column to the messages tree view
329                         self._messagemodel.clear()
330                         self._messagesView.set_model(self._messagemodel)
331                         self._messagesView.set_fixed_height_mode(False)
332
333                         textrenderer = gtk.CellRendererText()
334                         textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
335                         textrenderer.set_property("wrap-width", 450)
336                         messageColumn = gtk.TreeViewColumn("")
337                         messageColumn.pack_start(textrenderer, expand=True)
338                         messageColumn.add_attribute(textrenderer, "markup", 0)
339                         messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
340                         self._messagesView.append_column(messageColumn)
341                         self._messagesView.set_headers_visible(False)
342
343                         if messages:
344                                 for message in messages:
345                                         row = (message, )
346                                         self._messagemodel.append(row)
347                                 self._messagesView.show()
348                                 self._scrollWindow.show()
349                                 messagesSelection = self._messagesView.get_selection()
350                                 messagesSelection.select_path((len(messages)-1, ))
351                         else:
352                                 self._messagesView.hide()
353                                 self._scrollWindow.hide()
354
355                         self._smsEntry.get_buffer().set_text("")
356                         self._update_letter_count()
357
358                         if parent is not None:
359                                 self._dialog.set_transient_for(parent)
360
361                         # Run
362                         try:
363                                 self._dialog.show()
364                                 if messages:
365                                         self._messagesView.scroll_to_cell((len(messages)-1, ))
366                                 self._smsEntry.grab_focus()
367
368                                 userResponse = self._dialog.run()
369                         finally:
370                                 self._dialog.hide()
371
372                         # Process the users response
373                         if userResponse == gtk.RESPONSE_OK and 0 <= self._numberIndex:
374                                 phoneNumber = self._contactDetails[self._numberIndex][0]
375                                 phoneNumber = make_ugly(phoneNumber)
376                         else:
377                                 phoneNumber = ""
378                         if not phoneNumber:
379                                 self._action = self.ACTION_CANCEL
380                         if self._action == self.ACTION_SEND_SMS:
381                                 entryBuffer = self._smsEntry.get_buffer()
382                                 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
383                                 enteredMessage = enteredMessage[0:self.MAX_CHAR].strip()
384                                 if not enteredMessage:
385                                         phoneNumber = ""
386                                         self._action = self.ACTION_CANCEL
387                         else:
388                                 enteredMessage = ""
389
390                         self._messagesView.remove_column(messageColumn)
391                         self._messagesView.set_model(None)
392
393                         return self._action, phoneNumber, enteredMessage
394                 finally:
395                         self._smsEntry.get_buffer().disconnect(entryConnectId)
396                         self._phoneButton.disconnect(phoneConnectId)
397                         self._keyPressEventId = self._dialog.disconnect(keyConnectId)
398
399         def _update_letter_count(self, *args):
400                 entryLength = self._smsEntry.get_buffer().get_char_count()
401                 charsLeft = self.MAX_CHAR - entryLength
402                 self._letterCountLabel.set_text(str(charsLeft))
403                 if charsLeft < 0 or charsLeft == self.MAX_CHAR:
404                         self._smsButton.set_sensitive(False)
405                 else:
406                         self._smsButton.set_sensitive(True)
407
408         def _on_phone(self, *args):
409                 try:
410                         assert 0 <= self._numberIndex, "%r" % self._numberIndex
411
412                         self._numberIndex = hildonize.touch_selector(
413                                 self._dialog,
414                                 "Phone Numbers",
415                                 (description for (number, description) in self._contactDetails),
416                                 self._numberIndex,
417                         )
418                         self._phoneButton.set_label(self._contactDetails[self._numberIndex][1])
419                 except Exception, e:
420                         _moduleLogger.exception("%s" % str(e))
421
422         def _on_entry_changed(self, *args):
423                 self._update_letter_count()
424
425         def _on_send(self, *args):
426                 self._dialog.response(gtk.RESPONSE_OK)
427
428         def _on_dial(self, *args):
429                 self._dialog.response(gtk.RESPONSE_OK)
430                 self._action = self.ACTION_DIAL
431
432         def _on_select(self, *args):
433                 self._dialog.response(gtk.RESPONSE_OK)
434                 self._action = self.ACTION_SELECT
435
436         def _on_cancel(self, *args):
437                 self._dialog.response(gtk.RESPONSE_CANCEL)
438                 self._action = self.ACTION_CANCEL
439
440         def _on_key_press(self, widget, event):
441                 try:
442                         if event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
443                                 message = "\n".join(
444                                         messagePart[0]
445                                         for messagePart in self._messagemodel
446                                 )
447                                 # For some reason this kills clipboard stuff
448                                 #self._clipboard.set_text(message)
449                 except Exception, e:
450                         _moduleLogger.exception(str(e))
451
452
453 class Dialpad(object):
454
455         def __init__(self, widgetTree, errorDisplay):
456                 self._clipboard = gtk.clipboard_get()
457                 self._errorDisplay = errorDisplay
458                 self._smsDialog = SmsEntryDialog(widgetTree)
459
460                 self._numberdisplay = widgetTree.get_widget("numberdisplay")
461                 self._smsButton = widgetTree.get_widget("sms")
462                 self._dialButton = widgetTree.get_widget("dial")
463                 self._backButton = widgetTree.get_widget("back")
464                 self._phonenumber = ""
465                 self._prettynumber = ""
466
467                 callbackMapping = {
468                         "on_digit_clicked": self._on_digit_clicked,
469                 }
470                 widgetTree.signal_autoconnect(callbackMapping)
471                 self._dialButton.connect("clicked", self._on_dial_clicked)
472                 self._smsButton.connect("clicked", self._on_sms_clicked)
473
474                 self._originalLabel = self._backButton.get_label()
475                 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
476                 self._backTapHandler.on_tap = self._on_backspace
477                 self._backTapHandler.on_hold = self._on_clearall
478                 self._backTapHandler.on_holding = self._set_clear_button
479                 self._backTapHandler.on_cancel = self._reset_back_button
480
481                 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
482                 self._keyPressEventId = 0
483
484         def enable(self):
485                 self._dialButton.grab_focus()
486                 self._backTapHandler.enable()
487                 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
488
489         def disable(self):
490                 self._window.disconnect(self._keyPressEventId)
491                 self._keyPressEventId = 0
492                 self._reset_back_button()
493                 self._backTapHandler.disable()
494
495         def number_selected(self, action, number, message):
496                 """
497                 @note Actual dial function is patched in later
498                 """
499                 raise NotImplementedError("Horrible unknown error has occurred")
500
501         def get_number(self):
502                 return self._phonenumber
503
504         def set_number(self, number):
505                 """
506                 Set the number to dial
507                 """
508                 try:
509                         self._phonenumber = make_ugly(number)
510                         self._prettynumber = make_pretty(self._phonenumber)
511                         self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
512                 except TypeError, e:
513                         self._errorDisplay.push_exception()
514
515         def clear(self):
516                 self.set_number("")
517
518         @staticmethod
519         def name():
520                 return "Dialpad"
521
522         def load_settings(self, config, section):
523                 pass
524
525         def save_settings(self, config, section):
526                 """
527                 @note Thread Agnostic
528                 """
529                 pass
530
531         def _on_key_press(self, widget, event):
532                 try:
533                         if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
534                                 contents = self._clipboard.wait_for_text()
535                                 if contents is not None:
536                                         self.set_number(contents)
537                 except Exception, e:
538                         self._errorDisplay.push_exception()
539
540         def _on_sms_clicked(self, widget):
541                 try:
542                         phoneNumber = self.get_number()
543                         action, phoneNumber, message = self._smsDialog.run([("Dialer", phoneNumber)], (), self._window)
544
545                         if action == SmsEntryDialog.ACTION_CANCEL:
546                                 return
547                         self.number_selected(action, phoneNumber, message)
548                 except Exception, e:
549                         self._errorDisplay.push_exception()
550
551         def _on_dial_clicked(self, widget):
552                 try:
553                         action = SmsEntryDialog.ACTION_DIAL
554                         phoneNumber = self.get_number()
555                         message = ""
556                         self.number_selected(action, phoneNumber, message)
557                 except Exception, e:
558                         self._errorDisplay.push_exception()
559
560         def _on_digit_clicked(self, widget):
561                 try:
562                         self.set_number(self._phonenumber + widget.get_name()[-1])
563                 except Exception, e:
564                         self._errorDisplay.push_exception()
565
566         def _on_backspace(self, taps):
567                 try:
568                         self.set_number(self._phonenumber[:-taps])
569                         self._reset_back_button()
570                 except Exception, e:
571                         self._errorDisplay.push_exception()
572
573         def _on_clearall(self, taps):
574                 try:
575                         self.clear()
576                         self._reset_back_button()
577                 except Exception, e:
578                         self._errorDisplay.push_exception()
579                 return False
580
581         def _set_clear_button(self):
582                 try:
583                         self._backButton.set_label("gtk-clear")
584                 except Exception, e:
585                         self._errorDisplay.push_exception()
586
587         def _reset_back_button(self):
588                 try:
589                         self._backButton.set_label(self._originalLabel)
590                 except Exception, e:
591                         self._errorDisplay.push_exception()
592
593
594 class AccountInfo(object):
595
596         def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
597                 self._errorDisplay = errorDisplay
598                 self._backend = backend
599                 self._isPopulated = False
600                 self._alarmHandler = alarmHandler
601                 self._notifyOnMissed = False
602                 self._notifyOnVoicemail = False
603                 self._notifyOnSms = False
604
605                 self._callbackList = []
606                 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
607                 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
608                 self._onCallbackSelectChangedId = 0
609
610                 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
611                 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
612                 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
613                 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
614                 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
615                 self._onNotifyToggled = 0
616                 self._onMinutesChanged = 0
617                 self._onMissedToggled = 0
618                 self._onVoicemailToggled = 0
619                 self._onSmsToggled = 0
620                 self._applyAlarmTimeoutId = None
621
622                 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
623                 self._defaultCallback = ""
624
625         def enable(self):
626                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
627
628                 self._accountViewNumberDisplay.set_use_markup(True)
629                 self.set_account_number("")
630
631                 del self._callbackList[:]
632                 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
633
634                 if self._alarmHandler is not None:
635                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
636                         self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
637                         self._missedCheckbox.set_active(self._notifyOnMissed)
638                         self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
639                         self._smsCheckbox.set_active(self._notifyOnSms)
640
641                         self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
642                         self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
643                         self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
644                         self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
645                         self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
646                 else:
647                         self._notifyCheckbox.set_sensitive(False)
648                         self._minutesEntryButton.set_sensitive(False)
649                         self._missedCheckbox.set_sensitive(False)
650                         self._voicemailCheckbox.set_sensitive(False)
651                         self._smsCheckbox.set_sensitive(False)
652
653                 self.update(force=True)
654
655         def disable(self):
656                 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
657                 self._onCallbackSelectChangedId = 0
658
659                 if self._alarmHandler is not None:
660                         self._notifyCheckbox.disconnect(self._onNotifyToggled)
661                         self._minutesEntryButton.disconnect(self._onMinutesChanged)
662                         self._missedCheckbox.disconnect(self._onNotifyToggled)
663                         self._voicemailCheckbox.disconnect(self._onNotifyToggled)
664                         self._smsCheckbox.disconnect(self._onNotifyToggled)
665                         self._onNotifyToggled = 0
666                         self._onMinutesChanged = 0
667                         self._onMissedToggled = 0
668                         self._onVoicemailToggled = 0
669                         self._onSmsToggled = 0
670                 else:
671                         self._notifyCheckbox.set_sensitive(True)
672                         self._minutesEntryButton.set_sensitive(True)
673                         self._missedCheckbox.set_sensitive(True)
674                         self._voicemailCheckbox.set_sensitive(True)
675                         self._smsCheckbox.set_sensitive(True)
676
677                 self.clear()
678                 del self._callbackList[:]
679
680         def get_selected_callback_number(self):
681                 currentLabel = self._callbackSelectButton.get_label()
682                 if currentLabel is not None:
683                         return make_ugly(currentLabel)
684                 else:
685                         return ""
686
687         def set_account_number(self, number):
688                 """
689                 Displays current account number
690                 """
691                 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
692
693         def update(self, force = False):
694                 if not force and self._isPopulated:
695                         return False
696                 self._populate_callback_combo()
697                 self.set_account_number(self._backend.get_account_number())
698                 return True
699
700         def clear(self):
701                 self._callbackSelectButton.set_label("No Callback Number")
702                 self.set_account_number("")
703                 self._isPopulated = False
704
705         def save_everything(self):
706                 raise NotImplementedError
707
708         @staticmethod
709         def name():
710                 return "Account Info"
711
712         def load_settings(self, config, section):
713                 self._defaultCallback = config.get(section, "callback")
714                 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
715                 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
716                 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
717
718         def save_settings(self, config, section):
719                 """
720                 @note Thread Agnostic
721                 """
722                 callback = self.get_selected_callback_number()
723                 config.set(section, "callback", callback)
724                 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
725                 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
726                 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
727
728         def _populate_callback_combo(self):
729                 self._isPopulated = True
730                 del self._callbackList[:]
731                 try:
732                         callbackNumbers = self._backend.get_callback_numbers()
733                 except Exception, e:
734                         self._errorDisplay.push_exception()
735                         self._isPopulated = False
736                         return
737
738                 if len(callbackNumbers) == 0:
739                         callbackNumbers = {"": "No callback numbers available"}
740
741                 for number, description in callbackNumbers.iteritems():
742                         self._callbackList.append((make_pretty(number), description))
743
744                 self._set_callback_number(self._defaultCallback)
745
746         def _set_callback_number(self, number):
747                 try:
748                         if not self._backend.is_valid_syntax(number) and 0 < len(number):
749                                 self._errorDisplay.push_message("%s is not a valid callback number" % number)
750                         elif number == self._backend.get_callback_number() and 0 < len(number):
751                                 _moduleLogger.warning(
752                                         "Callback number already is %s" % (
753                                                 self._backend.get_callback_number(),
754                                         ),
755                                 )
756                         else:
757                                 self._backend.set_callback_number(number)
758                                 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
759                                         make_pretty(number), make_pretty(self._backend.get_callback_number())
760                                 )
761                                 prettyNumber = make_pretty(number)
762                                 if len(prettyNumber) == 0:
763                                         prettyNumber = "No Callback Number"
764                                 self._callbackSelectButton.set_label(prettyNumber)
765                                 _moduleLogger.info(
766                                         "Callback number set to %s" % (
767                                                 self._backend.get_callback_number(),
768                                         ),
769                                 )
770                 except Exception, e:
771                         self._errorDisplay.push_exception()
772
773         def _update_alarm_settings(self, recurrence):
774                 try:
775                         isEnabled = self._notifyCheckbox.get_active()
776                         if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
777                                 self._alarmHandler.apply_settings(isEnabled, recurrence)
778                 finally:
779                         self.save_everything()
780                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
781                         self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
782
783         def _on_callbackentry_clicked(self, *args):
784                 try:
785                         actualSelection = make_pretty(self.get_selected_callback_number())
786
787                         userOptions = dict(
788                                 (number, "%s (%s)" % (number, description))
789                                 for (number, description) in self._callbackList
790                         )
791                         defaultSelection = userOptions.get(actualSelection, actualSelection)
792
793                         userSelection = hildonize.touch_selector_entry(
794                                 self._window,
795                                 "Callback Number",
796                                 list(userOptions.itervalues()),
797                                 defaultSelection,
798                         )
799                         reversedUserOptions = dict(
800                                 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
801                         )
802                         selectedNumber = reversedUserOptions.get(userSelection, userSelection)
803
804                         number = make_ugly(selectedNumber)
805                         self._set_callback_number(number)
806                 except RuntimeError, e:
807                         _moduleLogger.exception("%s" % str(e))
808                 except Exception, e:
809                         self._errorDisplay.push_exception()
810
811         def _on_notify_toggled(self, *args):
812                 try:
813                         if self._applyAlarmTimeoutId is not None:
814                                 gobject.source_remove(self._applyAlarmTimeoutId)
815                                 self._applyAlarmTimeoutId = None
816                         self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
817                 except Exception, e:
818                         self._errorDisplay.push_exception()
819
820         def _on_minutes_clicked(self, *args):
821                 recurrenceChoices = [
822                         (1, "1 minute"),
823                         (2, "2 minutes"),
824                         (3, "3 minutes"),
825                         (5, "5 minutes"),
826                         (8, "8 minutes"),
827                         (10, "10 minutes"),
828                         (15, "15 minutes"),
829                         (30, "30 minutes"),
830                         (45, "45 minutes"),
831                         (60, "1 hour"),
832                         (3*60, "3 hours"),
833                         (6*60, "6 hours"),
834                         (12*60, "12 hours"),
835                 ]
836                 try:
837                         actualSelection = self._alarmHandler.recurrence
838
839                         closestSelectionIndex = 0
840                         for i, possible in enumerate(recurrenceChoices):
841                                 if possible[0] <= actualSelection:
842                                         closestSelectionIndex = i
843                         recurrenceIndex = hildonize.touch_selector(
844                                 self._window,
845                                 "Minutes",
846                                 (("%s" % m[1]) for m in recurrenceChoices),
847                                 closestSelectionIndex,
848                         )
849                         recurrence = recurrenceChoices[recurrenceIndex][0]
850
851                         self._update_alarm_settings(recurrence)
852                 except RuntimeError, e:
853                         _moduleLogger.exception("%s" % str(e))
854                 except Exception, e:
855                         self._errorDisplay.push_exception()
856
857         def _on_apply_timeout(self, *args):
858                 try:
859                         self._applyAlarmTimeoutId = None
860
861                         self._update_alarm_settings(self._alarmHandler.recurrence)
862                 except Exception, e:
863                         self._errorDisplay.push_exception()
864                 return False
865
866         def _on_missed_toggled(self, *args):
867                 try:
868                         self._notifyOnMissed = self._missedCheckbox.get_active()
869                         self.save_everything()
870                 except Exception, e:
871                         self._errorDisplay.push_exception()
872
873         def _on_voicemail_toggled(self, *args):
874                 try:
875                         self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
876                         self.save_everything()
877                 except Exception, e:
878                         self._errorDisplay.push_exception()
879
880         def _on_sms_toggled(self, *args):
881                 try:
882                         self._notifyOnSms = self._smsCheckbox.get_active()
883                         self.save_everything()
884                 except Exception, e:
885                         self._errorDisplay.push_exception()
886
887
888 class RecentCallsView(object):
889
890         NUMBER_IDX = 0
891         DATE_IDX = 1
892         ACTION_IDX = 2
893         FROM_IDX = 3
894
895         def __init__(self, widgetTree, backend, errorDisplay):
896                 self._errorDisplay = errorDisplay
897                 self._backend = backend
898
899                 self._isPopulated = False
900                 self._recentmodel = gtk.ListStore(
901                         gobject.TYPE_STRING, # number
902                         gobject.TYPE_STRING, # date
903                         gobject.TYPE_STRING, # action
904                         gobject.TYPE_STRING, # from
905                 )
906                 self._recentview = widgetTree.get_widget("recentview")
907                 self._recentviewselection = None
908                 self._onRecentviewRowActivatedId = 0
909
910                 textrenderer = gtk.CellRendererText()
911                 textrenderer.set_property("yalign", 0)
912                 self._dateColumn = gtk.TreeViewColumn("Date")
913                 self._dateColumn.pack_start(textrenderer, expand=True)
914                 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
915
916                 textrenderer = gtk.CellRendererText()
917                 textrenderer.set_property("yalign", 0)
918                 self._actionColumn = gtk.TreeViewColumn("Action")
919                 self._actionColumn.pack_start(textrenderer, expand=True)
920                 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
921
922                 textrenderer = gtk.CellRendererText()
923                 textrenderer.set_property("yalign", 0)
924                 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
925                 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
926                 self._numberColumn = gtk.TreeViewColumn("Number")
927                 self._numberColumn.pack_start(textrenderer, expand=True)
928                 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
929
930                 textrenderer = gtk.CellRendererText()
931                 textrenderer.set_property("yalign", 0)
932                 hildonize.set_cell_thumb_selectable(textrenderer)
933                 self._nameColumn = gtk.TreeViewColumn("From")
934                 self._nameColumn.pack_start(textrenderer, expand=True)
935                 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
936                 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
937
938                 self._window = gtk_toolbox.find_parent_window(self._recentview)
939                 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
940
941                 self._updateSink = gtk_toolbox.threaded_stage(
942                         gtk_toolbox.comap(
943                                 self._idly_populate_recentview,
944                                 gtk_toolbox.null_sink(),
945                         )
946                 )
947
948         def enable(self):
949                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
950                 self._recentview.set_model(self._recentmodel)
951                 self._recentview.set_fixed_height_mode(False)
952
953                 self._recentview.append_column(self._dateColumn)
954                 self._recentview.append_column(self._actionColumn)
955                 self._recentview.append_column(self._numberColumn)
956                 self._recentview.append_column(self._nameColumn)
957                 self._recentviewselection = self._recentview.get_selection()
958                 self._recentviewselection.set_mode(gtk.SELECTION_NONE)
959
960                 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
961
962         def disable(self):
963                 self._recentview.disconnect(self._onRecentviewRowActivatedId)
964
965                 self.clear()
966
967                 self._recentview.remove_column(self._dateColumn)
968                 self._recentview.remove_column(self._actionColumn)
969                 self._recentview.remove_column(self._nameColumn)
970                 self._recentview.remove_column(self._numberColumn)
971                 self._recentview.set_model(None)
972
973         def number_selected(self, action, number, message):
974                 """
975                 @note Actual dial function is patched in later
976                 """
977                 raise NotImplementedError("Horrible unknown error has occurred")
978
979         def update(self, force = False):
980                 if not force and self._isPopulated:
981                         return False
982                 self._updateSink.send(())
983                 return True
984
985         def clear(self):
986                 self._isPopulated = False
987                 self._recentmodel.clear()
988
989         @staticmethod
990         def name():
991                 return "Recent Calls"
992
993         def load_settings(self, config, section):
994                 pass
995
996         def save_settings(self, config, section):
997                 """
998                 @note Thread Agnostic
999                 """
1000                 pass
1001
1002         def _idly_populate_recentview(self):
1003                 with gtk_toolbox.gtk_lock():
1004                         banner = hildonize.show_busy_banner_start(self._window, "Loading Recent History")
1005                 try:
1006                         self._recentmodel.clear()
1007                         self._isPopulated = True
1008
1009                         try:
1010                                 recentItems = self._backend.get_recent()
1011                         except Exception, e:
1012                                 self._errorDisplay.push_exception_with_lock()
1013                                 self._isPopulated = False
1014                                 recentItems = []
1015
1016                         recentItems = (
1017                                 gv_backend.decorate_recent(data)
1018                                 for data in gv_backend.sort_messages(recentItems)
1019                         )
1020
1021                         for personName, phoneNumber, date, action in recentItems:
1022                                 if not personName:
1023                                         personName = "Unknown"
1024                                 date = abbrev_relative_date(date)
1025                                 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1026                                 prettyNumber = make_pretty(prettyNumber)
1027                                 item = (prettyNumber, date, action.capitalize(), personName)
1028                                 with gtk_toolbox.gtk_lock():
1029                                         self._recentmodel.append(item)
1030                 except Exception, e:
1031                         self._errorDisplay.push_exception_with_lock()
1032                 finally:
1033                         with gtk_toolbox.gtk_lock():
1034                                 hildonize.show_busy_banner_end(banner)
1035
1036                 return False
1037
1038         def _on_recentview_row_activated(self, treeview, path, view_column):
1039                 try:
1040                         itr = self._recentmodel.get_iter(path)
1041                         if not itr:
1042                                 return
1043
1044                         number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1045                         number = make_ugly(number)
1046                         contactPhoneNumbers = [("Phone", number)]
1047                         description = self._recentmodel.get_value(itr, self.FROM_IDX)
1048
1049                         action, phoneNumber, message = self._phoneTypeSelector.run(
1050                                 contactPhoneNumbers,
1051                                 messages = (description, ),
1052                                 parent = self._window,
1053                         )
1054                         if action == SmsEntryDialog.ACTION_CANCEL:
1055                                 return
1056                         assert phoneNumber, "A lack of phone number exists"
1057
1058                         self.number_selected(action, phoneNumber, message)
1059                         self._recentviewselection.unselect_all()
1060                 except Exception, e:
1061                         self._errorDisplay.push_exception()
1062
1063
1064 class MessagesView(object):
1065
1066         NUMBER_IDX = 0
1067         DATE_IDX = 1
1068         HEADER_IDX = 2
1069         MESSAGE_IDX = 3
1070         MESSAGES_IDX = 4
1071
1072         def __init__(self, widgetTree, backend, errorDisplay):
1073                 self._errorDisplay = errorDisplay
1074                 self._backend = backend
1075
1076                 self._isPopulated = False
1077                 self._messagemodel = gtk.ListStore(
1078                         gobject.TYPE_STRING, # number
1079                         gobject.TYPE_STRING, # date
1080                         gobject.TYPE_STRING, # header
1081                         gobject.TYPE_STRING, # message
1082                         object, # messages
1083                 )
1084                 self._messageview = widgetTree.get_widget("messages_view")
1085                 self._messageviewselection = None
1086                 self._onMessageviewRowActivatedId = 0
1087
1088                 self._messageRenderer = gtk.CellRendererText()
1089                 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1090                 self._messageRenderer.set_property("wrap-width", 500)
1091                 self._messageColumn = gtk.TreeViewColumn("Messages")
1092                 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1093                 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1094                 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1095
1096                 self._window = gtk_toolbox.find_parent_window(self._messageview)
1097                 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
1098
1099                 self._updateSink = gtk_toolbox.threaded_stage(
1100                         gtk_toolbox.comap(
1101                                 self._idly_populate_messageview,
1102                                 gtk_toolbox.null_sink(),
1103                         )
1104                 )
1105
1106         def enable(self):
1107                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1108                 self._messageview.set_model(self._messagemodel)
1109                 self._messageview.set_headers_visible(False)
1110                 self._messageview.set_fixed_height_mode(False)
1111
1112                 self._messageview.append_column(self._messageColumn)
1113                 self._messageviewselection = self._messageview.get_selection()
1114                 self._messageviewselection.set_mode(gtk.SELECTION_NONE)
1115
1116                 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1117
1118         def disable(self):
1119                 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1120
1121                 self.clear()
1122
1123                 self._messageview.remove_column(self._messageColumn)
1124                 self._messageview.set_model(None)
1125
1126         def number_selected(self, action, number, message):
1127                 """
1128                 @note Actual dial function is patched in later
1129                 """
1130                 raise NotImplementedError("Horrible unknown error has occurred")
1131
1132         def update(self, force = False):
1133                 if not force and self._isPopulated:
1134                         return False
1135                 self._updateSink.send(())
1136                 return True
1137
1138         def clear(self):
1139                 self._isPopulated = False
1140                 self._messagemodel.clear()
1141
1142         @staticmethod
1143         def name():
1144                 return "Messages"
1145
1146         def load_settings(self, config, section):
1147                 pass
1148
1149         def save_settings(self, config, section):
1150                 """
1151                 @note Thread Agnostic
1152                 """
1153                 pass
1154
1155         _MIN_MESSAGES_SHOWN = 4
1156
1157         def _idly_populate_messageview(self):
1158                 with gtk_toolbox.gtk_lock():
1159                         banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1160                 try:
1161                         self._messagemodel.clear()
1162                         self._isPopulated = True
1163
1164                         try:
1165                                 messageItems = self._backend.get_messages()
1166                         except Exception, e:
1167                                 self._errorDisplay.push_exception_with_lock()
1168                                 self._isPopulated = False
1169                                 messageItems = []
1170
1171                         messageItems = (
1172                                 gv_backend.decorate_message(message)
1173                                 for message in gv_backend.sort_messages(messageItems)
1174                         )
1175
1176                         for header, number, relativeDate, messages in messageItems:
1177                                 prettyNumber = number[2:] if number.startswith("+1") else number
1178                                 prettyNumber = make_pretty(prettyNumber)
1179
1180                                 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1181                                 expandedMessages = [firstMessage]
1182                                 expandedMessages.extend(messages)
1183                                 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1184                                         firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1185                                         secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1186                                         collapsedMessages = [firstMessage, secondMessage]
1187                                         collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1188                                 else:
1189                                         collapsedMessages = expandedMessages
1190
1191                                 number = make_ugly(number)
1192
1193                                 row = (number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages)
1194                                 with gtk_toolbox.gtk_lock():
1195                                         self._messagemodel.append(row)
1196                 except Exception, e:
1197                         self._errorDisplay.push_exception_with_lock()
1198                 finally:
1199                         with gtk_toolbox.gtk_lock():
1200                                 hildonize.show_busy_banner_end(banner)
1201
1202                 return False
1203
1204         def _on_messageview_row_activated(self, treeview, path, view_column):
1205                 try:
1206                         itr = self._messagemodel.get_iter(path)
1207                         if not itr:
1208                                 return
1209
1210                         contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1211                         description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1212
1213                         action, phoneNumber, message = self._phoneTypeSelector.run(
1214                                 contactPhoneNumbers,
1215                                 messages = description,
1216                                 parent = self._window,
1217                         )
1218                         if action == SmsEntryDialog.ACTION_CANCEL:
1219                                 return
1220                         assert phoneNumber, "A lock of phone number exists"
1221
1222                         self.number_selected(action, phoneNumber, message)
1223                         self._messageviewselection.unselect_all()
1224                 except Exception, e:
1225                         self._errorDisplay.push_exception()
1226
1227
1228 class ContactsView(object):
1229
1230         def __init__(self, widgetTree, backend, errorDisplay):
1231                 self._errorDisplay = errorDisplay
1232                 self._backend = backend
1233
1234                 self._addressBook = None
1235                 self._selectedComboIndex = 0
1236                 self._addressBookFactories = [null_backend.NullAddressBook()]
1237
1238                 self._booksList = []
1239                 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1240
1241                 self._isPopulated = False
1242                 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1243                 self._contactsviewselection = None
1244                 self._contactsview = widgetTree.get_widget("contactsview")
1245
1246                 self._contactColumn = gtk.TreeViewColumn("Contact")
1247                 displayContactSource = False
1248                 if displayContactSource:
1249                         textrenderer = gtk.CellRendererText()
1250                         self._contactColumn.pack_start(textrenderer, expand=False)
1251                         self._contactColumn.add_attribute(textrenderer, 'text', 0)
1252                 textrenderer = gtk.CellRendererText()
1253                 hildonize.set_cell_thumb_selectable(textrenderer)
1254                 self._contactColumn.pack_start(textrenderer, expand=True)
1255                 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1256                 textrenderer = gtk.CellRendererText()
1257                 self._contactColumn.pack_start(textrenderer, expand=True)
1258                 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1259                 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1260                 self._contactColumn.set_sort_column_id(1)
1261                 self._contactColumn.set_visible(True)
1262
1263                 self._onContactsviewRowActivatedId = 0
1264                 self._onAddressbookButtonChangedId = 0
1265                 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1266                 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
1267
1268                 self._updateSink = gtk_toolbox.threaded_stage(
1269                         gtk_toolbox.comap(
1270                                 self._idly_populate_contactsview,
1271                                 gtk_toolbox.null_sink(),
1272                         )
1273                 )
1274
1275         def enable(self):
1276                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1277
1278                 self._contactsview.set_model(self._contactsmodel)
1279                 self._contactsview.set_fixed_height_mode(True)
1280                 self._contactsview.append_column(self._contactColumn)
1281                 self._contactsviewselection = self._contactsview.get_selection()
1282                 self._contactsviewselection.set_mode(gtk.SELECTION_NONE)
1283
1284                 del self._booksList[:]
1285                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1286                         if factoryName and bookName:
1287                                 entryName = "%s: %s" % (factoryName, bookName)
1288                         elif factoryName:
1289                                 entryName = factoryName
1290                         elif bookName:
1291                                 entryName = bookName
1292                         else:
1293                                 entryName = "Bad name (%d)" % factoryId
1294                         row = (str(factoryId), bookId, entryName)
1295                         self._booksList.append(row)
1296
1297                 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1298                 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1299
1300                 if len(self._booksList) <= self._selectedComboIndex:
1301                         self._selectedComboIndex = 0
1302                 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1303
1304                 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1305                 selectedBookId = self._booksList[self._selectedComboIndex][1]
1306                 self.open_addressbook(selectedFactoryId, selectedBookId)
1307
1308         def disable(self):
1309                 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1310                 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1311
1312                 self.clear()
1313
1314                 self._bookSelectionButton.set_label("")
1315                 self._contactsview.set_model(None)
1316                 self._contactsview.remove_column(self._contactColumn)
1317
1318         def number_selected(self, action, number, message):
1319                 """
1320                 @note Actual dial function is patched in later
1321                 """
1322                 raise NotImplementedError("Horrible unknown error has occurred")
1323
1324         def get_addressbooks(self):
1325                 """
1326                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1327                 """
1328                 for i, factory in enumerate(self._addressBookFactories):
1329                         for bookFactory, bookId, bookName in factory.get_addressbooks():
1330                                 yield (str(i), bookId), (factory.factory_name(), bookName)
1331
1332         def open_addressbook(self, bookFactoryId, bookId):
1333                 bookFactoryIndex = int(bookFactoryId)
1334                 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1335
1336                 forceUpdate = True if addressBook is not self._addressBook else False
1337
1338                 self._addressBook = addressBook
1339                 self.update(force=forceUpdate)
1340
1341         def update(self, force = False):
1342                 if not force and self._isPopulated:
1343                         return False
1344                 self._updateSink.send(())
1345                 return True
1346
1347         def clear(self):
1348                 self._isPopulated = False
1349                 self._contactsmodel.clear()
1350                 for factory in self._addressBookFactories:
1351                         factory.clear_caches()
1352                 self._addressBook.clear_caches()
1353
1354         def append(self, book):
1355                 self._addressBookFactories.append(book)
1356
1357         def extend(self, books):
1358                 self._addressBookFactories.extend(books)
1359
1360         @staticmethod
1361         def name():
1362                 return "Contacts"
1363
1364         def load_settings(self, config, sectionName):
1365                 try:
1366                         self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1367                 except ConfigParser.NoOptionError:
1368                         self._selectedComboIndex = 0
1369
1370         def save_settings(self, config, sectionName):
1371                 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1372
1373         def _idly_populate_contactsview(self):
1374                 with gtk_toolbox.gtk_lock():
1375                         banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1376                 try:
1377                         addressBook = None
1378                         while addressBook is not self._addressBook:
1379                                 addressBook = self._addressBook
1380                                 with gtk_toolbox.gtk_lock():
1381                                         self._contactsview.set_model(None)
1382                                         self.clear()
1383
1384                                 try:
1385                                         contacts = addressBook.get_contacts()
1386                                 except Exception, e:
1387                                         contacts = []
1388                                         self._isPopulated = False
1389                                         self._errorDisplay.push_exception_with_lock()
1390                                 for contactId, contactName in contacts:
1391                                         contactType = (addressBook.contact_source_short_name(contactId), )
1392                                         self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1393
1394                                 with gtk_toolbox.gtk_lock():
1395                                         self._contactsview.set_model(self._contactsmodel)
1396
1397                         self._isPopulated = True
1398                 except Exception, e:
1399                         self._errorDisplay.push_exception_with_lock()
1400                 finally:
1401                         with gtk_toolbox.gtk_lock():
1402                                 hildonize.show_busy_banner_end(banner)
1403                 return False
1404
1405         def _on_addressbook_button_changed(self, *args, **kwds):
1406                 try:
1407                         try:
1408                                 newSelectedComboIndex = hildonize.touch_selector(
1409                                         self._window,
1410                                         "Addressbook",
1411                                         (("%s" % m[2]) for m in self._booksList),
1412                                         self._selectedComboIndex,
1413                                 )
1414                         except RuntimeError:
1415                                 return
1416
1417                         selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1418                         selectedBookId = self._booksList[newSelectedComboIndex][1]
1419                         self.open_addressbook(selectedFactoryId, selectedBookId)
1420                         self._selectedComboIndex = newSelectedComboIndex
1421                         self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1422                 except Exception, e:
1423                         self._errorDisplay.push_exception()
1424
1425         def _on_contactsview_row_activated(self, treeview, path, view_column):
1426                 try:
1427                         itr = self._contactsmodel.get_iter(path)
1428                         if not itr:
1429                                 return
1430
1431                         contactId = self._contactsmodel.get_value(itr, 3)
1432                         contactName = self._contactsmodel.get_value(itr, 1)
1433                         try:
1434                                 contactDetails = self._addressBook.get_contact_details(contactId)
1435                         except Exception, e:
1436                                 contactDetails = []
1437                                 self._errorDisplay.push_exception()
1438                         contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1439
1440                         if len(contactPhoneNumbers) == 0:
1441                                 return
1442
1443                         action, phoneNumber, message = self._phoneTypeSelector.run(
1444                                 contactPhoneNumbers,
1445                                 messages = (contactName, ),
1446                                 parent = self._window,
1447                         )
1448                         if action == SmsEntryDialog.ACTION_CANCEL:
1449                                 return
1450                         assert phoneNumber, "A lack of phone number exists"
1451
1452                         self.number_selected(action, phoneNumber, message)
1453                         self._contactsviewselection.unselect_all()
1454                 except Exception, e:
1455                         self._errorDisplay.push_exception()