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