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