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