c2397a88c1abbdd94c8ce26c4e59ddc58ccc302b
[gc-dialer] / src / gv_views.py
1 #!/usr/bin/env python
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 Collapse voicemails
22 """
23
24 from __future__ import with_statement
25
26 import re
27 import ConfigParser
28 import itertools
29 import logging
30
31 import gobject
32 import pango
33 import gtk
34
35 import gtk_toolbox
36 import hildonize
37 from backends import gv_backend
38 from backends import null_backend
39
40
41 _moduleLogger = logging.getLogger("gv_views")
42
43
44 def make_ugly(prettynumber):
45         """
46         function to take a phone number and strip out all non-numeric
47         characters
48
49         >>> make_ugly("+012-(345)-678-90")
50         '+01234567890'
51         """
52         return normalize_number(prettynumber)
53
54
55 def normalize_number(prettynumber):
56         """
57         function to take a phone number and strip out all non-numeric
58         characters
59
60         >>> normalize_number("+012-(345)-678-90")
61         '+01234567890'
62         >>> normalize_number("1-(345)-678-9000")
63         '13456789000'
64         >>> normalize_number("+1-(345)-678-9000")
65         '+13456789000'
66         """
67         uglynumber = re.sub('[^0-9+]', '', prettynumber)
68
69         return uglynumber
70
71
72 def _make_pretty_with_areacodde(phonenumber):
73         prettynumber = "(%s)" % (phonenumber[0:3], )
74         if 3 < len(phonenumber):
75                 prettynumber += " %s" % (phonenumber[3:6], )
76                 if 6 < len(phonenumber):
77                         prettynumber += "-%s" % (phonenumber[6:], )
78         return prettynumber
79
80
81 def _make_pretty_local(phonenumber):
82         prettynumber = "%s" % (phonenumber[0:3], )
83         if 3 < len(phonenumber):
84                 prettynumber += "-%s" % (phonenumber[3:], )
85         return prettynumber
86
87
88 def _make_pretty_international(phonenumber):
89         prettynumber = phonenumber
90         if phonenumber.startswith("0"):
91                 prettynumber = "+%s " % (phonenumber[0:3], )
92                 if 3 < len(phonenumber):
93                         prettynumber += _make_pretty_with_areacodde(phonenumber[3:])
94         if phonenumber.startswith("1"):
95                 prettynumber = "1 "
96                 prettynumber += _make_pretty_with_areacodde(phonenumber[1:])
97         return prettynumber
98
99
100 def make_pretty(phonenumber):
101         """
102         Function to take a phone number and return the pretty version
103         pretty numbers:
104                 if phonenumber begins with 0:
105                         ...-(...)-...-....
106                 if phonenumber begins with 1: ( for gizmo callback numbers )
107                         1 (...)-...-....
108                 if phonenumber is 13 digits:
109                         (...)-...-....
110                 if phonenumber is 10 digits:
111                         ...-....
112         >>> make_pretty("12")
113         '12'
114         >>> make_pretty("1234567")
115         '123-4567'
116         >>> make_pretty("2345678901")
117         '+1 (234) 567-8901'
118         >>> make_pretty("12345678901")
119         '+1 (234) 567-8901'
120         >>> make_pretty("01234567890")
121         '+012 (345) 678-90'
122         >>> make_pretty("+01234567890")
123         '+012 (345) 678-90'
124         >>> make_pretty("+12")
125         '+1 (2)'
126         >>> make_pretty("+123")
127         '+1 (23)'
128         >>> make_pretty("+1234")
129         '+1 (234)'
130         """
131         if phonenumber is None or phonenumber is "":
132                 return ""
133
134         phonenumber = normalize_number(phonenumber)
135
136         if phonenumber[0] == "+":
137                 prettynumber = _make_pretty_international(phonenumber[1:])
138                 if not prettynumber.startswith("+"):
139                         prettynumber = "+"+prettynumber
140         elif 8 < len(phonenumber) and phonenumber[0] in ("0", "1"):
141                 prettynumber = _make_pretty_international(phonenumber)
142         elif 7 < len(phonenumber):
143                 prettynumber = _make_pretty_with_areacodde(phonenumber)
144         elif 3 < len(phonenumber):
145                 prettynumber = _make_pretty_local(phonenumber)
146         else:
147                 prettynumber = phonenumber
148         return prettynumber.strip()
149
150
151 def abbrev_relative_date(date):
152         """
153         >>> abbrev_relative_date("42 hours ago")
154         '42 h'
155         >>> abbrev_relative_date("2 days ago")
156         '2 d'
157         >>> abbrev_relative_date("4 weeks ago")
158         '4 w'
159         """
160         parts = date.split(" ")
161         return "%s %s" % (parts[0], parts[1][0])
162
163
164 def _collapse_message(messageLines, maxCharsPerLine, maxLines):
165         lines = 0
166
167         numLines = len(messageLines)
168         for line in messageLines[0:min(maxLines, numLines)]:
169                 linesPerLine = max(1, int(len(line) / maxCharsPerLine))
170                 allowedLines = maxLines - lines
171                 acceptedLines = min(allowedLines, linesPerLine)
172                 acceptedChars = acceptedLines * maxCharsPerLine
173
174                 if acceptedChars < (len(line) + 3):
175                         suffix = "..."
176                 else:
177                         acceptedChars = len(line) # eh, might as well complete the line
178                         suffix = ""
179                 abbrevMessage = "%s%s" % (line[0:acceptedChars], suffix)
180                 yield abbrevMessage
181
182                 lines += acceptedLines
183                 if maxLines <= lines:
184                         break
185
186
187 def collapse_message(message, maxCharsPerLine, maxLines):
188         r"""
189         >>> collapse_message("Hello", 60, 2)
190         'Hello'
191         >>> collapse_message("Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789", 60, 2)
192         'Hello world how are you doing today? 01234567890123456789012...'
193         >>> collapse_message('''Hello world how are you doing today?
194         ... 01234567890123456789
195         ... 01234567890123456789
196         ... 01234567890123456789
197         ... 01234567890123456789''', 60, 2)
198         'Hello world how are you doing today?\n01234567890123456789'
199         >>> collapse_message('''
200         ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
201         ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
202         ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
203         ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
204         ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
205         ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789''', 60, 2)
206         '\nHello world how are you doing today? 01234567890123456789012...'
207         """
208         messageLines = message.split("\n")
209         return "\n".join(_collapse_message(messageLines, maxCharsPerLine, maxLines))
210
211
212 def _get_contact_numbers(backend, contactId, number):
213         if contactId:
214                 contactPhoneNumbers = list(backend.get_contact_details(contactId))
215                 uglyContactNumbers = (
216                         make_ugly(contactNumber)
217                         for (numberDescription, contactNumber) in contactPhoneNumbers
218                 )
219                 defaultMatches = [
220                         (
221                                 number == contactNumber or
222                                 number[1:] == contactNumber and number.startswith("1") or
223                                 number[2:] == contactNumber and number.startswith("+1") or
224                                 number == contactNumber[1:] and contactNumber.startswith("1") or
225                                 number == contactNumber[2:] and contactNumber.startswith("+1")
226                         )
227                         for contactNumber in uglyContactNumbers
228                 ]
229                 try:
230                         defaultIndex = defaultMatches.index(True)
231                 except ValueError:
232                         contactPhoneNumbers.append(("Other", number))
233                         defaultIndex = len(contactPhoneNumbers)-1
234                         _moduleLogger.warn(
235                                 "Could not find contact %r's number %s among %r" % (
236                                         contactId, number, contactPhoneNumbers
237                                 )
238                         )
239         else:
240                 contactPhoneNumbers = [("Phone", number)]
241                 defaultIndex = -1
242
243         return contactPhoneNumbers, defaultIndex
244
245
246 class SmsEntryWindow(object):
247
248         MAX_CHAR = 160
249
250         def __init__(self, widgetTree, parent):
251                 self._clipboard = gtk.clipboard_get()
252                 self._widgetTree = widgetTree
253                 self._window = self._widgetTree.get_widget("smsWindow")
254                 self._window.connect("delete-event", self._on_delete)
255                 self._window.connect("key-press-event", self._on_key_press)
256                 self._window.connect("window-state-event", self._on_window_state_change)
257                 self._isFullScreen = False
258                 self._parent = parent
259
260                 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
261                 self._smsButton.connect("clicked", self._on_send)
262                 self._dialButton = self._widgetTree.get_widget("dialButton")
263                 self._dialButton.connect("clicked", self._on_dial)
264
265                 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
266
267                 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
268                 self._messagesView = self._widgetTree.get_widget("smsMessages")
269
270                 textrenderer = gtk.CellRendererText()
271                 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
272                 textrenderer.set_property("wrap-width", 450)
273                 messageColumn = gtk.TreeViewColumn("")
274                 messageColumn.pack_start(textrenderer, expand=True)
275                 messageColumn.add_attribute(textrenderer, "markup", 0)
276                 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
277                 self._messagesView.append_column(messageColumn)
278                 self._messagesView.set_headers_visible(False)
279                 self._messagesView.set_model(self._messagemodel)
280                 self._messagesView.set_fixed_height_mode(False)
281
282                 self._conversationView = self._messagesView.get_parent()
283                 self._conversationViewPort = self._conversationView.get_parent()
284                 self._scrollWindow = self._conversationViewPort.get_parent()
285
286                 self._targetList = self._widgetTree.get_widget("smsTargetList")
287                 self._phoneButton = self._widgetTree.get_widget("phoneTypeSelection")
288                 self._phoneButton.connect("clicked", self._on_phone)
289                 self._smsEntry = self._widgetTree.get_widget("smsEntry")
290                 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
291                 self._smsEntrySize = None
292
293                 self._contacts = []
294
295         def add_contact(self, contactDetails, messages = (), defaultIndex = -1):
296                 contactNumbers = list(self._to_contact_numbers(contactDetails))
297                 assert contactNumbers
298                 contactIndex = defaultIndex if defaultIndex != -1 else 0
299                 contact = contactNumbers, contactIndex, messages
300                 self._contacts.append(contact)
301
302                 selector = gtk.Button(contactNumbers[0][1])
303                 removeContact = gtk.Button(stock="gtk-delete")
304                 row = gtk.HBox()
305                 row.pack_start(selector, True, True)
306                 row.pack_start(removeContact, False, False)
307                 row.show_all()
308                 self._targetList.pack_start(row)
309                 selector.connect("clicked", self._on_choose_phone_n, row)
310                 removeContact.connect("clicked", self._on_remove_phone_n, row)
311                 self._update_button_state()
312                 self._update_context()
313
314                 parentSize = self._parent.get_size()
315                 self._window.resize(parentSize[0], max(parentSize[1]-10, 100))
316                 self._window.show()
317                 self._window.present()
318
319                 self._smsEntry.grab_focus()
320                 dx = self._conversationView.get_allocation().height - self._conversationViewPort.get_allocation().height
321                 dx = max(dx, 0)
322                 adjustment = self._scrollWindow.get_vadjustment()
323                 adjustment.value = dx
324
325         def clear(self):
326                 del self._contacts[:]
327
328                 for contactNumberSelector in list(self._targetList.get_children()):
329                         self._targetList.remove(contactNumberSelector)
330                 self._smsEntry.get_buffer().set_text("")
331                 self._update_letter_count()
332                 self._update_context()
333
334         def fullscreen(self):
335                 self._window.fullscreen()
336
337         def unfullscreen(self):
338                 self._window.unfullscreen()
339
340         def _remove_contact(self, contactIndex):
341                 del self._contacts[contactIndex]
342
343                 contactNumberSelector = list(self._targetList.get_children())[contactIndex]
344                 self._targetList.remove(contactNumberSelector)
345                 self._update_button_state()
346                 self._update_context()
347
348         def _update_letter_count(self):
349                 if self._smsEntrySize is None:
350                         self._smsEntrySize = self._smsEntry.size_request()
351                 else:
352                         self._smsEntry.set_size_request(*self._smsEntrySize)
353                 entryLength = self._smsEntry.get_buffer().get_char_count()
354
355                 numTexts, numCharInText = divmod(entryLength, self.MAX_CHAR)
356                 if numTexts:
357                         self._letterCountLabel.set_text("%s.%s" % (numTexts, numCharInText))
358                 else:
359                         self._letterCountLabel.set_text("%s" % (numCharInText, ))
360
361                 self._update_button_state()
362
363         def _update_context(self):
364                 self._messagemodel.clear()
365                 if len(self._contacts) == 0:
366                         self._messagesView.hide()
367                         self._targetList.hide()
368                         self._phoneButton.hide()
369                         self._phoneButton.set_label("Error: You shouldn't see this")
370                 elif len(self._contacts) == 1:
371                         contactNumbers, index, messages = self._contacts[0]
372                         if messages:
373                                 self._messagesView.show()
374                                 for message in messages:
375                                         row = (message, )
376                                         self._messagemodel.append(row)
377                                 messagesSelection = self._messagesView.get_selection()
378                                 messagesSelection.select_path((len(messages)-1, ))
379                         else:
380                                 self._messagesView.hide()
381                         self._targetList.hide()
382                         self._phoneButton.show()
383                         self._phoneButton.set_label(contactNumbers[index][1])
384                         if 1 < len(contactNumbers):
385                                 self._phoneButton.set_sensitive(True)
386                         else:
387                                 self._phoneButton.set_sensitive(False)
388                 else:
389                         self._messagesView.hide()
390                         self._targetList.show()
391                         self._phoneButton.hide()
392                         self._phoneButton.set_label("Error: You shouldn't see this")
393
394         def _update_button_state(self):
395                 if len(self._contacts) == 0:
396                         self._dialButton.set_sensitive(False)
397                         self._smsButton.set_sensitive(False)
398                 elif len(self._contacts) == 1:
399                         entryLength = self._smsEntry.get_buffer().get_char_count()
400                         if entryLength == 0:
401                                 self._dialButton.set_sensitive(True)
402                                 self._smsButton.set_sensitive(False)
403                         else:
404                                 self._dialButton.set_sensitive(False)
405                                 self._smsButton.set_sensitive(True)
406                 else:
407                         self._dialButton.set_sensitive(False)
408                         self._smsButton.set_sensitive(True)
409
410         def _to_contact_numbers(self, contactDetails):
411                 for phoneType, phoneNumber in contactDetails:
412                         display = " - ".join((make_pretty(phoneNumber), phoneType))
413                         yield (phoneNumber, display)
414
415         def _hide(self):
416                 self.clear()
417                 self._window.hide()
418
419         def _request_number(self, contactIndex):
420                 contactNumbers, index, messages = self._contacts[contactIndex]
421                 assert 0 <= index, "%r" % index
422
423                 index = hildonize.touch_selector(
424                         self._window,
425                         "Phone Numbers",
426                         (description for (number, description) in contactNumbers),
427                         index,
428                 )
429                 self._contacts[contactIndex] = contactNumbers, index, messages
430
431         def send_sms(self, numbers, message):
432                 raise NotImplementedError()
433
434         def dial(self, number):
435                 raise NotImplementedError()
436
437         def _on_phone(self, *args):
438                 try:
439                         assert len(self._contacts) == 1
440                         self._request_number(0)
441
442                         contactNumbers, numberIndex, messages = self._contacts[0]
443                         self._phoneButton.set_label(contactNumbers[numberIndex][1])
444                         row = list(self._targetList.get_children())[0]
445                         phoneButton = list(row.get_children())[0]
446                         phoneButton.set_label(contactNumbers[numberIndex][1])
447                 except Exception, e:
448                         _moduleLogger.exception("%s" % str(e))
449
450         def _on_choose_phone_n(self, button, row):
451                 try:
452                         assert 1 < len(self._contacts)
453                         targetList = list(self._targetList.get_children())
454                         index = targetList.index(row)
455                         self._request_number(index)
456
457                         contactNumbers, numberIndex, messages = self._contacts[0]
458                         phoneButton = list(row.get_children())[0]
459                         phoneButton.set_label(contactNumbers[numberIndex][1])
460                 except Exception, e:
461                         _moduleLogger.exception("%s" % str(e))
462
463         def _on_remove_phone_n(self, button, row):
464                 try:
465                         assert 1 < len(self._contacts)
466                         targetList = list(self._targetList.get_children())
467                         index = targetList.index(row)
468
469                         del self._contacts[index]
470                         self._targetList.remove(row)
471                         self._update_context()
472                         self._update_button_state()
473                 except Exception, e:
474                         _moduleLogger.exception("%s" % str(e))
475
476         def _on_entry_changed(self, *args):
477                 self._update_letter_count()
478
479         def _on_send(self, *args):
480                 assert 0 < len(self._contacts), "%r" % self._contacts
481                 phoneNumbers = [
482                         make_ugly(contact[0][contact[1]][0])
483                         for contact in self._contacts
484                 ]
485
486                 entryBuffer = self._smsEntry.get_buffer()
487                 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
488                 enteredMessage = enteredMessage.strip()
489                 assert enteredMessage
490                 self.send_sms(phoneNumbers, enteredMessage)
491                 self._hide()
492
493         def _on_dial(self, *args):
494                 assert len(self._contacts) == 1, "%r" % self._contacts
495                 contact = self._contacts[0]
496                 contactNumber = contact[0][contact[1]][0]
497                 phoneNumber = make_ugly(contactNumber)
498                 self.dial(phoneNumber)
499                 self._hide()
500
501         def _on_delete(self, *args):
502                 self._window.emit_stop_by_name("delete-event")
503                 self._hide()
504                 return True
505
506         def _on_window_state_change(self, widget, event, *args):
507                 """
508                 @note Hildon specific
509                 """
510                 try:
511                         if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
512                                 self._isFullScreen = True
513                         else:
514                                 self._isFullScreen = False
515                 except Exception, e:
516                         self._errorDisplay.push_exception()
517
518         def _on_key_press(self, widget, event):
519                 RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
520                 try:
521                         if (
522                                 event.keyval == gtk.keysyms.F6 or
523                                 event.keyval in RETURN_TYPES and event.get_state() & gtk.gdk.CONTROL_MASK
524                         ):
525                                 if self._isFullScreen:
526                                         self._window.unfullscreen()
527                                 else:
528                                         self._window.fullscreen()
529                         elif event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
530                                 message = "\n".join(
531                                         messagePart[0]
532                                         for messagePart in self._messagemodel
533                                 )
534                                 self._clipboard.set_text(str(message))
535                         elif (
536                                 event.keyval == gtk.keysyms.w and
537                                 event.get_state() & gtk.gdk.CONTROL_MASK
538                         ):
539                                 self._hide()
540                         elif (
541                                 event.keyval == gtk.keysyms.q and
542                                 event.get_state() & gtk.gdk.CONTROL_MASK
543                         ):
544                                 self._parent.destroy()
545                 except Exception, e:
546                         _moduleLogger.exception(str(e))
547
548
549 class Dialpad(object):
550
551         def __init__(self, widgetTree, errorDisplay):
552                 self._clipboard = gtk.clipboard_get()
553                 self._errorDisplay = errorDisplay
554
555                 self._numberdisplay = widgetTree.get_widget("numberdisplay")
556                 self._okButton = widgetTree.get_widget("dialpadOk")
557                 self._backButton = widgetTree.get_widget("back")
558                 self._plusButton = widgetTree.get_widget("plus")
559                 self._phonenumber = ""
560                 self._prettynumber = ""
561
562                 callbackMapping = {
563                         "on_digit_clicked": self._on_digit_clicked,
564                 }
565                 widgetTree.signal_autoconnect(callbackMapping)
566                 self._okButton.connect("clicked", self._on_ok_clicked)
567                 self._plusButton.connect("clicked", self._on_plus)
568
569                 self._originalLabel = self._backButton.get_label()
570                 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
571                 self._backTapHandler.on_tap = self._on_backspace
572                 self._backTapHandler.on_hold = self._on_clearall
573                 self._backTapHandler.on_holding = self._set_clear_button
574                 self._backTapHandler.on_cancel = self._reset_back_button
575
576                 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
577                 self._keyPressEventId = 0
578
579         def enable(self):
580                 self._okButton.grab_focus()
581                 self._backTapHandler.enable()
582                 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
583
584         def disable(self):
585                 self._window.disconnect(self._keyPressEventId)
586                 self._keyPressEventId = 0
587                 self._reset_back_button()
588                 self._backTapHandler.disable()
589
590         def add_contact(self, *args, **kwds):
591                 """
592                 @note Actual dial function is patched in later
593                 """
594                 raise NotImplementedError("Horrible unknown error has occurred")
595
596         def get_number(self):
597                 return self._phonenumber
598
599         def set_number(self, number):
600                 """
601                 Set the number to dial
602                 """
603                 try:
604                         self._phonenumber = make_ugly(number)
605                         self._prettynumber = make_pretty(self._phonenumber)
606                         self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
607                 except TypeError, e:
608                         self._errorDisplay.push_exception()
609
610         def clear(self):
611                 self.set_number("")
612
613         @staticmethod
614         def name():
615                 return "Dialpad"
616
617         def load_settings(self, config, section):
618                 pass
619
620         def save_settings(self, config, section):
621                 """
622                 @note Thread Agnostic
623                 """
624                 pass
625
626         def _on_key_press(self, widget, event):
627                 try:
628                         if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
629                                 contents = self._clipboard.wait_for_text()
630                                 if contents is not None:
631                                         self.set_number(contents)
632                 except Exception, e:
633                         self._errorDisplay.push_exception()
634
635         def _on_ok_clicked(self, widget):
636                 try:
637                         phoneNumber = self.get_number()
638                         self.add_contact(
639                                 [("Dialer", phoneNumber)], ()
640                         )
641                         self.set_number("")
642                 except Exception, e:
643                         self._errorDisplay.push_exception()
644
645         def _on_digit_clicked(self, widget):
646                 try:
647                         self.set_number(self._phonenumber + widget.get_name()[-1])
648                 except Exception, e:
649                         self._errorDisplay.push_exception()
650
651         def _on_plus(self, *args):
652                 try:
653                         self.set_number(self._phonenumber + "+")
654                 except Exception, e:
655                         self._errorDisplay.push_exception()
656
657         def _on_backspace(self, taps):
658                 try:
659                         self.set_number(self._phonenumber[:-taps])
660                         self._reset_back_button()
661                 except Exception, e:
662                         self._errorDisplay.push_exception()
663
664         def _on_clearall(self, taps):
665                 try:
666                         self.clear()
667                         self._reset_back_button()
668                 except Exception, e:
669                         self._errorDisplay.push_exception()
670                 return False
671
672         def _set_clear_button(self):
673                 try:
674                         self._backButton.set_label("gtk-clear")
675                 except Exception, e:
676                         self._errorDisplay.push_exception()
677
678         def _reset_back_button(self):
679                 try:
680                         self._backButton.set_label(self._originalLabel)
681                 except Exception, e:
682                         self._errorDisplay.push_exception()
683
684
685 class AccountInfo(object):
686
687         def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
688                 self._errorDisplay = errorDisplay
689                 self._backend = backend
690                 self._isPopulated = False
691                 self._alarmHandler = alarmHandler
692                 self._notifyOnMissed = False
693                 self._notifyOnVoicemail = False
694                 self._notifyOnSms = False
695
696                 self._callbackList = []
697                 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
698                 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
699                 self._onCallbackSelectChangedId = 0
700
701                 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
702                 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
703                 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
704                 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
705                 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
706                 self._onNotifyToggled = 0
707                 self._onMinutesChanged = 0
708                 self._onMissedToggled = 0
709                 self._onVoicemailToggled = 0
710                 self._onSmsToggled = 0
711                 self._applyAlarmTimeoutId = None
712
713                 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
714                 self._callbackNumber = ""
715
716         def enable(self):
717                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
718
719                 self._accountViewNumberDisplay.set_use_markup(True)
720                 self.set_account_number("")
721
722                 del self._callbackList[:]
723                 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
724                 self._set_callback_label("")
725
726                 if self._alarmHandler is not None:
727                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
728                         self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
729                         self._missedCheckbox.set_active(self._notifyOnMissed)
730                         self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
731                         self._smsCheckbox.set_active(self._notifyOnSms)
732
733                         self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
734                         self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
735                         self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
736                         self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
737                         self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
738                 else:
739                         self._notifyCheckbox.set_sensitive(False)
740                         self._minutesEntryButton.set_sensitive(False)
741                         self._missedCheckbox.set_sensitive(False)
742                         self._voicemailCheckbox.set_sensitive(False)
743                         self._smsCheckbox.set_sensitive(False)
744
745                 self.update(force=True)
746
747         def disable(self):
748                 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
749                 self._onCallbackSelectChangedId = 0
750                 self._set_callback_label("")
751
752                 if self._alarmHandler is not None:
753                         self._notifyCheckbox.disconnect(self._onNotifyToggled)
754                         self._minutesEntryButton.disconnect(self._onMinutesChanged)
755                         self._missedCheckbox.disconnect(self._onNotifyToggled)
756                         self._voicemailCheckbox.disconnect(self._onNotifyToggled)
757                         self._smsCheckbox.disconnect(self._onNotifyToggled)
758                         self._onNotifyToggled = 0
759                         self._onMinutesChanged = 0
760                         self._onMissedToggled = 0
761                         self._onVoicemailToggled = 0
762                         self._onSmsToggled = 0
763                 else:
764                         self._notifyCheckbox.set_sensitive(True)
765                         self._minutesEntryButton.set_sensitive(True)
766                         self._missedCheckbox.set_sensitive(True)
767                         self._voicemailCheckbox.set_sensitive(True)
768                         self._smsCheckbox.set_sensitive(True)
769
770                 self.clear()
771                 del self._callbackList[:]
772
773         def set_account_number(self, number):
774                 """
775                 Displays current account number
776                 """
777                 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
778
779         def update(self, force = False):
780                 if not force and self._isPopulated:
781                         return False
782                 self._populate_callback_combo()
783                 self.set_account_number(self._backend.get_account_number())
784                 return True
785
786         def clear(self):
787                 self._set_callback_label("")
788                 self.set_account_number("")
789                 self._isPopulated = False
790
791         def save_everything(self):
792                 raise NotImplementedError
793
794         @staticmethod
795         def name():
796                 return "Account Info"
797
798         def load_settings(self, config, section):
799                 self._callbackNumber = make_ugly(config.get(section, "callback"))
800                 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
801                 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
802                 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
803
804         def save_settings(self, config, section):
805                 """
806                 @note Thread Agnostic
807                 """
808                 config.set(section, "callback", self._callbackNumber)
809                 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
810                 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
811                 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
812
813         def _populate_callback_combo(self):
814                 self._isPopulated = True
815                 del self._callbackList[:]
816                 try:
817                         callbackNumbers = self._backend.get_callback_numbers()
818                 except Exception, e:
819                         self._errorDisplay.push_exception()
820                         self._isPopulated = False
821                         return
822
823                 if len(callbackNumbers) == 0:
824                         callbackNumbers = {"": "No callback numbers available"}
825
826                 for number, description in callbackNumbers.iteritems():
827                         self._callbackList.append((make_pretty(number), description))
828
829                 self._set_callback_number(self._callbackNumber)
830
831         def _set_callback_number(self, number):
832                 try:
833                         if not self._backend.is_valid_syntax(number) and 0 < len(number):
834                                 self._errorDisplay.push_message("%s is not a valid callback number" % number)
835                         elif number == self._backend.get_callback_number() and 0 < len(number):
836                                 _moduleLogger.warning(
837                                         "Callback number already is %s" % (
838                                                 self._backend.get_callback_number(),
839                                         ),
840                                 )
841                                 self._set_callback_label(number)
842                         else:
843                                 if number.startswith("1747"): number = "+" + number
844                                 self._backend.set_callback_number(number)
845                                 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
846                                         make_pretty(number), make_pretty(self._backend.get_callback_number())
847                                 )
848                                 self._callbackNumber = make_ugly(number)
849                                 self._set_callback_label(number)
850                                 _moduleLogger.info(
851                                         "Callback number set to %s" % (
852                                                 self._backend.get_callback_number(),
853                                         ),
854                                 )
855                 except Exception, e:
856                         self._errorDisplay.push_exception()
857
858         def _set_callback_label(self, uglyNumber):
859                 prettyNumber = make_pretty(uglyNumber)
860                 if len(prettyNumber) == 0:
861                         prettyNumber = "No Callback Number"
862                 self._callbackSelectButton.set_label(prettyNumber)
863
864         def _update_alarm_settings(self, recurrence):
865                 try:
866                         isEnabled = self._notifyCheckbox.get_active()
867                         if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
868                                 self._alarmHandler.apply_settings(isEnabled, recurrence)
869                 finally:
870                         self.save_everything()
871                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
872                         self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
873
874         def _on_callbackentry_clicked(self, *args):
875                 try:
876                         actualSelection = make_pretty(self._callbackNumber)
877
878                         userOptions = dict(
879                                 (number, "%s (%s)" % (number, description))
880                                 for (number, description) in self._callbackList
881                         )
882                         defaultSelection = userOptions.get(actualSelection, actualSelection)
883
884                         userSelection = hildonize.touch_selector_entry(
885                                 self._window,
886                                 "Callback Number",
887                                 list(userOptions.itervalues()),
888                                 defaultSelection,
889                         )
890                         reversedUserOptions = dict(
891                                 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
892                         )
893                         selectedNumber = reversedUserOptions.get(userSelection, userSelection)
894
895                         number = make_ugly(selectedNumber)
896                         self._set_callback_number(number)
897                 except RuntimeError, e:
898                         _moduleLogger.exception("%s" % str(e))
899                 except Exception, e:
900                         self._errorDisplay.push_exception()
901
902         def _on_notify_toggled(self, *args):
903                 try:
904                         if self._applyAlarmTimeoutId is not None:
905                                 gobject.source_remove(self._applyAlarmTimeoutId)
906                                 self._applyAlarmTimeoutId = None
907                         self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
908                 except Exception, e:
909                         self._errorDisplay.push_exception()
910
911         def _on_minutes_clicked(self, *args):
912                 recurrenceChoices = [
913                         (1, "1 minute"),
914                         (2, "2 minutes"),
915                         (3, "3 minutes"),
916                         (5, "5 minutes"),
917                         (8, "8 minutes"),
918                         (10, "10 minutes"),
919                         (15, "15 minutes"),
920                         (30, "30 minutes"),
921                         (45, "45 minutes"),
922                         (60, "1 hour"),
923                         (3*60, "3 hours"),
924                         (6*60, "6 hours"),
925                         (12*60, "12 hours"),
926                 ]
927                 try:
928                         actualSelection = self._alarmHandler.recurrence
929
930                         closestSelectionIndex = 0
931                         for i, possible in enumerate(recurrenceChoices):
932                                 if possible[0] <= actualSelection:
933                                         closestSelectionIndex = i
934                         recurrenceIndex = hildonize.touch_selector(
935                                 self._window,
936                                 "Minutes",
937                                 (("%s" % m[1]) for m in recurrenceChoices),
938                                 closestSelectionIndex,
939                         )
940                         recurrence = recurrenceChoices[recurrenceIndex][0]
941
942                         self._update_alarm_settings(recurrence)
943                 except RuntimeError, e:
944                         _moduleLogger.exception("%s" % str(e))
945                 except Exception, e:
946                         self._errorDisplay.push_exception()
947
948         def _on_apply_timeout(self, *args):
949                 try:
950                         self._applyAlarmTimeoutId = None
951
952                         self._update_alarm_settings(self._alarmHandler.recurrence)
953                 except Exception, e:
954                         self._errorDisplay.push_exception()
955                 return False
956
957         def _on_missed_toggled(self, *args):
958                 try:
959                         self._notifyOnMissed = self._missedCheckbox.get_active()
960                         self.save_everything()
961                 except Exception, e:
962                         self._errorDisplay.push_exception()
963
964         def _on_voicemail_toggled(self, *args):
965                 try:
966                         self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
967                         self.save_everything()
968                 except Exception, e:
969                         self._errorDisplay.push_exception()
970
971         def _on_sms_toggled(self, *args):
972                 try:
973                         self._notifyOnSms = self._smsCheckbox.get_active()
974                         self.save_everything()
975                 except Exception, e:
976                         self._errorDisplay.push_exception()
977
978
979 class CallHistoryView(object):
980
981         NUMBER_IDX = 0
982         DATE_IDX = 1
983         ACTION_IDX = 2
984         FROM_IDX = 3
985         FROM_ID_IDX = 4
986
987         HISTORY_ITEM_TYPES = ["All", "Received", "Missed", "Placed"]
988
989         def __init__(self, widgetTree, backend, errorDisplay):
990                 self._errorDisplay = errorDisplay
991                 self._backend = backend
992
993                 self._isPopulated = False
994                 self._historymodel = gtk.ListStore(
995                         gobject.TYPE_STRING, # number
996                         gobject.TYPE_STRING, # date
997                         gobject.TYPE_STRING, # action
998                         gobject.TYPE_STRING, # from
999                         gobject.TYPE_STRING, # from id
1000                 )
1001                 self._historymodelfiltered = self._historymodel.filter_new()
1002                 self._historymodelfiltered.set_visible_func(self._is_history_visible)
1003                 self._historyview = widgetTree.get_widget("historyview")
1004                 self._historyviewselection = None
1005                 self._onRecentviewRowActivatedId = 0
1006
1007                 textrenderer = gtk.CellRendererText()
1008                 textrenderer.set_property("yalign", 0)
1009                 self._dateColumn = gtk.TreeViewColumn("Date")
1010                 self._dateColumn.pack_start(textrenderer, expand=True)
1011                 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
1012
1013                 textrenderer = gtk.CellRendererText()
1014                 textrenderer.set_property("yalign", 0)
1015                 self._actionColumn = gtk.TreeViewColumn("Action")
1016                 self._actionColumn.pack_start(textrenderer, expand=True)
1017                 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
1018
1019                 textrenderer = gtk.CellRendererText()
1020                 textrenderer.set_property("yalign", 0)
1021                 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
1022                 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
1023                 self._numberColumn = gtk.TreeViewColumn("Number")
1024                 self._numberColumn.pack_start(textrenderer, expand=True)
1025                 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
1026
1027                 textrenderer = gtk.CellRendererText()
1028                 textrenderer.set_property("yalign", 0)
1029                 hildonize.set_cell_thumb_selectable(textrenderer)
1030                 self._nameColumn = gtk.TreeViewColumn("From")
1031                 self._nameColumn.pack_start(textrenderer, expand=True)
1032                 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
1033                 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1034
1035                 self._window = gtk_toolbox.find_parent_window(self._historyview)
1036
1037                 self._historyFilterSelector = widgetTree.get_widget("historyFilterSelector")
1038                 self._historyFilterSelector.connect("clicked", self._on_history_filter_clicked)
1039                 self._selectedFilter = "All"
1040
1041                 self._updateSink = gtk_toolbox.threaded_stage(
1042                         gtk_toolbox.comap(
1043                                 self._idly_populate_historyview,
1044                                 gtk_toolbox.null_sink(),
1045                         )
1046                 )
1047
1048         def enable(self):
1049                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1050                 self._historyFilterSelector.set_label(self._selectedFilter)
1051
1052                 self._historyview.set_model(self._historymodelfiltered)
1053                 self._historyview.set_fixed_height_mode(False)
1054
1055                 self._historyview.append_column(self._dateColumn)
1056                 self._historyview.append_column(self._actionColumn)
1057                 self._historyview.append_column(self._numberColumn)
1058                 self._historyview.append_column(self._nameColumn)
1059                 self._historyviewselection = self._historyview.get_selection()
1060                 self._historyviewselection.set_mode(gtk.SELECTION_SINGLE)
1061
1062                 self._onRecentviewRowActivatedId = self._historyview.connect("row-activated", self._on_historyview_row_activated)
1063
1064         def disable(self):
1065                 self._historyview.disconnect(self._onRecentviewRowActivatedId)
1066
1067                 self.clear()
1068
1069                 self._historyview.remove_column(self._dateColumn)
1070                 self._historyview.remove_column(self._actionColumn)
1071                 self._historyview.remove_column(self._nameColumn)
1072                 self._historyview.remove_column(self._numberColumn)
1073                 self._historyview.set_model(None)
1074
1075         def add_contact(self, *args, **kwds):
1076                 """
1077                 @note Actual dial function is patched in later
1078                 """
1079                 raise NotImplementedError("Horrible unknown error has occurred")
1080
1081         def update(self, force = False):
1082                 if not force and self._isPopulated:
1083                         return False
1084                 self._updateSink.send(())
1085                 return True
1086
1087         def clear(self):
1088                 self._isPopulated = False
1089                 self._historymodel.clear()
1090
1091         @staticmethod
1092         def name():
1093                 return "Recent Calls"
1094
1095         def load_settings(self, config, sectionName):
1096                 try:
1097                         self._selectedFilter = config.get(sectionName, "filter")
1098                         if self._selectedFilter not in self.HISTORY_ITEM_TYPES:
1099                                 self._messageType = self.HISTORY_ITEM_TYPES[0]
1100                 except ConfigParser.NoOptionError:
1101                         pass
1102
1103         def save_settings(self, config, sectionName):
1104                 """
1105                 @note Thread Agnostic
1106                 """
1107                 config.set(sectionName, "filter", self._selectedFilter)
1108
1109         def _is_history_visible(self, model, iter):
1110                 try:
1111                         action = model.get_value(iter, self.ACTION_IDX)
1112                         if action is None:
1113                                 return False # this seems weird but oh well
1114
1115                         if self._selectedFilter in [action, "All"]:
1116                                 return True
1117                         else:
1118                                 return False
1119                 except Exception, e:
1120                         self._errorDisplay.push_exception()
1121
1122         def _idly_populate_historyview(self):
1123                 with gtk_toolbox.gtk_lock():
1124                         banner = hildonize.show_busy_banner_start(self._window, "Loading Call History")
1125                 try:
1126                         self._historymodel.clear()
1127                         self._isPopulated = True
1128
1129                         try:
1130                                 historyItems = self._backend.get_recent()
1131                         except Exception, e:
1132                                 self._errorDisplay.push_exception_with_lock()
1133                                 self._isPopulated = False
1134                                 historyItems = []
1135
1136                         historyItems = (
1137                                 gv_backend.decorate_recent(data)
1138                                 for data in gv_backend.sort_messages(historyItems)
1139                         )
1140
1141                         for contactId, personName, phoneNumber, date, action in historyItems:
1142                                 if not personName:
1143                                         personName = "Unknown"
1144                                 date = abbrev_relative_date(date)
1145                                 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1146                                 prettyNumber = make_pretty(prettyNumber)
1147                                 item = (prettyNumber, date, action.capitalize(), personName, contactId)
1148                                 with gtk_toolbox.gtk_lock():
1149                                         self._historymodel.append(item)
1150                 except Exception, e:
1151                         self._errorDisplay.push_exception_with_lock()
1152                 finally:
1153                         with gtk_toolbox.gtk_lock():
1154                                 hildonize.show_busy_banner_end(banner)
1155
1156                 return False
1157
1158         def _on_history_filter_clicked(self, *args, **kwds):
1159                 try:
1160                         selectedComboIndex = self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
1161
1162                         try:
1163                                 newSelectedComboIndex = hildonize.touch_selector(
1164                                         self._window,
1165                                         "History",
1166                                         self.HISTORY_ITEM_TYPES,
1167                                         selectedComboIndex,
1168                                 )
1169                         except RuntimeError:
1170                                 return
1171
1172                         option = self.HISTORY_ITEM_TYPES[newSelectedComboIndex]
1173                         self._selectedFilter = option
1174                         self._historyFilterSelector.set_label(self._selectedFilter)
1175                         self._historymodelfiltered.refilter()
1176                 except Exception, e:
1177                         self._errorDisplay.push_exception()
1178
1179         def _history_summary(self, expectedNumber):
1180                 for number, action, date, whoFrom, whoFromId in self._historymodel:
1181                         if expectedNumber is not None and expectedNumber == number:
1182                                 yield "%s <i>(%s)</i> - %s %s" % (number, whoFrom, date, action)
1183
1184         def _on_historyview_row_activated(self, treeview, path, view_column):
1185                 try:
1186                         childPath = self._historymodelfiltered.convert_path_to_child_path(path)
1187                         itr = self._historymodel.get_iter(childPath)
1188                         if not itr:
1189                                 return
1190
1191                         prettyNumber = self._historymodel.get_value(itr, self.NUMBER_IDX)
1192                         number = make_ugly(prettyNumber)
1193                         description = list(self._history_summary(prettyNumber))
1194                         contactId = self._historymodel.get_value(itr, self.FROM_ID_IDX)
1195                         contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1196
1197                         self.add_contact(
1198                                 contactPhoneNumbers,
1199                                 messages = description,
1200                                 defaultIndex = defaultIndex,
1201                         )
1202                         self._historyviewselection.unselect_all()
1203                 except Exception, e:
1204                         self._errorDisplay.push_exception()
1205
1206
1207 class MessagesView(object):
1208
1209         NUMBER_IDX = 0
1210         DATE_IDX = 1
1211         HEADER_IDX = 2
1212         MESSAGE_IDX = 3
1213         MESSAGES_IDX = 4
1214         FROM_ID_IDX = 5
1215         MESSAGE_DATA_IDX = 6
1216
1217         NO_MESSAGES = "None"
1218         VOICEMAIL_MESSAGES = "Voicemail"
1219         TEXT_MESSAGES = "Texts"
1220         ALL_TYPES = "All Messages"
1221         MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
1222
1223         UNREAD_STATUS = "Unread"
1224         UNARCHIVED_STATUS = "Inbox"
1225         ALL_STATUS = "Any"
1226         MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
1227
1228         def __init__(self, widgetTree, backend, errorDisplay):
1229                 self._errorDisplay = errorDisplay
1230                 self._backend = backend
1231
1232                 self._isPopulated = False
1233                 self._messagemodel = gtk.ListStore(
1234                         gobject.TYPE_STRING, # number
1235                         gobject.TYPE_STRING, # date
1236                         gobject.TYPE_STRING, # header
1237                         gobject.TYPE_STRING, # message
1238                         object, # messages
1239                         gobject.TYPE_STRING, # from id
1240                         object, # message data
1241                 )
1242                 self._messagemodelfiltered = self._messagemodel.filter_new()
1243                 self._messagemodelfiltered.set_visible_func(self._is_message_visible)
1244                 self._messageview = widgetTree.get_widget("messages_view")
1245                 self._messageviewselection = None
1246                 self._onMessageviewRowActivatedId = 0
1247
1248                 self._messageRenderer = gtk.CellRendererText()
1249                 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1250                 self._messageRenderer.set_property("wrap-width", 500)
1251                 self._messageColumn = gtk.TreeViewColumn("Messages")
1252                 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1253                 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1254                 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1255
1256                 self._window = gtk_toolbox.find_parent_window(self._messageview)
1257
1258                 self._messageTypeButton = widgetTree.get_widget("messageTypeButton")
1259                 self._onMessageTypeClickedId = 0
1260                 self._messageType = self.ALL_TYPES
1261                 self._messageStatusButton = widgetTree.get_widget("messageStatusButton")
1262                 self._onMessageStatusClickedId = 0
1263                 self._messageStatus = self.ALL_STATUS
1264
1265                 self._updateSink = gtk_toolbox.threaded_stage(
1266                         gtk_toolbox.comap(
1267                                 self._idly_populate_messageview,
1268                                 gtk_toolbox.null_sink(),
1269                         )
1270                 )
1271
1272         def enable(self):
1273                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1274                 self._messageview.set_model(self._messagemodelfiltered)
1275                 self._messageview.set_headers_visible(False)
1276                 self._messageview.set_fixed_height_mode(False)
1277
1278                 self._messageview.append_column(self._messageColumn)
1279                 self._messageviewselection = self._messageview.get_selection()
1280                 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1281
1282                 self._messageTypeButton.set_label(self._messageType)
1283                 self._messageStatusButton.set_label(self._messageStatus)
1284
1285                 self._onMessageviewRowActivatedId = self._messageview.connect(
1286                         "row-activated", self._on_messageview_row_activated
1287                 )
1288                 self._onMessageTypeClickedId = self._messageTypeButton.connect(
1289                         "clicked", self._on_message_type_clicked
1290                 )
1291                 self._onMessageStatusClickedId = self._messageStatusButton.connect(
1292                         "clicked", self._on_message_status_clicked
1293                 )
1294
1295         def disable(self):
1296                 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1297                 self._messageTypeButton.disconnect(self._onMessageTypeClickedId)
1298                 self._messageStatusButton.disconnect(self._onMessageStatusClickedId)
1299
1300                 self.clear()
1301
1302                 self._messageview.remove_column(self._messageColumn)
1303                 self._messageview.set_model(None)
1304
1305         def add_contact(self, *args, **kwds):
1306                 """
1307                 @note Actual dial function is patched in later
1308                 """
1309                 raise NotImplementedError("Horrible unknown error has occurred")
1310
1311         def update(self, force = False):
1312                 if not force and self._isPopulated:
1313                         return False
1314                 self._updateSink.send(())
1315                 return True
1316
1317         def clear(self):
1318                 self._isPopulated = False
1319                 self._messagemodel.clear()
1320
1321         @staticmethod
1322         def name():
1323                 return "Messages"
1324
1325         def load_settings(self, config, sectionName):
1326                 try:
1327                         self._messageType = config.get(sectionName, "type")
1328                         if self._messageType not in self.MESSAGE_TYPES:
1329                                 self._messageType = self.ALL_TYPES
1330                         self._messageStatus = config.get(sectionName, "status")
1331                         if self._messageStatus not in self.MESSAGE_STATUSES:
1332                                 self._messageStatus = self.ALL_STATUS
1333                 except ConfigParser.NoOptionError:
1334                         pass
1335
1336         def save_settings(self, config, sectionName):
1337                 """
1338                 @note Thread Agnostic
1339                 """
1340                 config.set(sectionName, "status", self._messageStatus)
1341                 config.set(sectionName, "type", self._messageType)
1342
1343         def _is_message_visible(self, model, iter):
1344                 try:
1345                         message = model.get_value(iter, self.MESSAGE_DATA_IDX)
1346                         if message is None:
1347                                 return False # this seems weird but oh well
1348                         return self._filter_messages(message, self._messageType, self._messageStatus)
1349                 except Exception, e:
1350                         self._errorDisplay.push_exception()
1351
1352         @classmethod
1353         def _filter_messages(cls, message, type, status):
1354                 if type == cls.ALL_TYPES:
1355                         isType = True
1356                 else:
1357                         messageType = message["type"]
1358                         isType = messageType == type
1359
1360                 if status == cls.ALL_STATUS:
1361                         isStatus = True
1362                 else:
1363                         isUnarchived = not message["isArchived"]
1364                         isUnread = not message["isRead"]
1365                         if status == cls.UNREAD_STATUS:
1366                                 isStatus = isUnarchived and isUnread
1367                         elif status == cls.UNARCHIVED_STATUS:
1368                                 isStatus = isUnarchived
1369                         else:
1370                                 assert "Status %s is bad for %r" % (status, message)
1371
1372                 return isType and isStatus
1373
1374         _MIN_MESSAGES_SHOWN = 4
1375
1376         def _idly_populate_messageview(self):
1377                 with gtk_toolbox.gtk_lock():
1378                         banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1379                 try:
1380                         self._messagemodel.clear()
1381                         self._isPopulated = True
1382
1383                         if self._messageType == self.NO_MESSAGES:
1384                                 messageItems = []
1385                         else:
1386                                 try:
1387                                         messageItems = self._backend.get_messages()
1388                                 except Exception, e:
1389                                         self._errorDisplay.push_exception_with_lock()
1390                                         self._isPopulated = False
1391                                         messageItems = []
1392
1393                         messageItems = (
1394                                 (gv_backend.decorate_message(message), message)
1395                                 for message in gv_backend.sort_messages(messageItems)
1396                         )
1397
1398                         for (contactId, header, number, relativeDate, messages), messageData in messageItems:
1399                                 prettyNumber = number[2:] if number.startswith("+1") else number
1400                                 prettyNumber = make_pretty(prettyNumber)
1401
1402                                 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1403                                 expandedMessages = [firstMessage]
1404                                 expandedMessages.extend(messages)
1405                                 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1406                                         firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1407                                         secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1408                                         collapsedMessages = [firstMessage, secondMessage]
1409                                         collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1410                                 else:
1411                                         collapsedMessages = expandedMessages
1412                                 #collapsedMessages = _collapse_message(collapsedMessages, 60, self._MIN_MESSAGES_SHOWN)
1413
1414                                 number = make_ugly(number)
1415
1416                                 row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId, messageData
1417                                 with gtk_toolbox.gtk_lock():
1418                                         self._messagemodel.append(row)
1419                 except Exception, e:
1420                         self._errorDisplay.push_exception_with_lock()
1421                 finally:
1422                         with gtk_toolbox.gtk_lock():
1423                                 hildonize.show_busy_banner_end(banner)
1424                                 self._messagemodelfiltered.refilter()
1425
1426                 return False
1427
1428         def _on_messageview_row_activated(self, treeview, path, view_column):
1429                 try:
1430                         childPath = self._messagemodelfiltered.convert_path_to_child_path(path)
1431                         itr = self._messagemodel.get_iter(childPath)
1432                         if not itr:
1433                                 return
1434
1435                         number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX))
1436                         description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1437
1438                         contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX)
1439                         contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1440
1441                         self.add_contact(
1442                                 contactPhoneNumbers,
1443                                 messages = description,
1444                                 defaultIndex = defaultIndex,
1445                         )
1446                         self._messageviewselection.unselect_all()
1447                 except Exception, e:
1448                         self._errorDisplay.push_exception()
1449
1450         def _on_message_type_clicked(self, *args, **kwds):
1451                 try:
1452                         selectedIndex = self.MESSAGE_TYPES.index(self._messageType)
1453
1454                         try:
1455                                 newSelectedIndex = hildonize.touch_selector(
1456                                         self._window,
1457                                         "Message Type",
1458                                         self.MESSAGE_TYPES,
1459                                         selectedIndex,
1460                                 )
1461                         except RuntimeError:
1462                                 return
1463
1464                         if selectedIndex != newSelectedIndex:
1465                                 self._messageType = self.MESSAGE_TYPES[newSelectedIndex]
1466                                 self._messageTypeButton.set_label(self._messageType)
1467                                 self._messagemodelfiltered.refilter()
1468                 except Exception, e:
1469                         self._errorDisplay.push_exception()
1470
1471         def _on_message_status_clicked(self, *args, **kwds):
1472                 try:
1473                         selectedIndex = self.MESSAGE_STATUSES.index(self._messageStatus)
1474
1475                         try:
1476                                 newSelectedIndex = hildonize.touch_selector(
1477                                         self._window,
1478                                         "Message Status",
1479                                         self.MESSAGE_STATUSES,
1480                                         selectedIndex,
1481                                 )
1482                         except RuntimeError:
1483                                 return
1484
1485                         if selectedIndex != newSelectedIndex:
1486                                 self._messageStatus = self.MESSAGE_STATUSES[newSelectedIndex]
1487                                 self._messageStatusButton.set_label(self._messageStatus)
1488                                 self._messagemodelfiltered.refilter()
1489                 except Exception, e:
1490                         self._errorDisplay.push_exception()
1491
1492
1493 class ContactsView(object):
1494
1495         CONTACT_TYPE_IDX = 0
1496         CONTACT_NAME_IDX = 1
1497         CONTACT_ID_IDX = 2
1498
1499         def __init__(self, widgetTree, backend, errorDisplay):
1500                 self._errorDisplay = errorDisplay
1501                 self._backend = backend
1502
1503                 self._addressBook = None
1504                 self._selectedComboIndex = 0
1505                 self._addressBookFactories = [null_backend.NullAddressBook()]
1506
1507                 self._booksList = []
1508                 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1509
1510                 self._isPopulated = False
1511                 self._contactsmodel = gtk.ListStore(
1512                         gobject.TYPE_STRING, # Contact Type
1513                         gobject.TYPE_STRING, # Contact Name
1514                         gobject.TYPE_STRING, # Contact ID
1515                 )
1516                 self._contactsviewselection = None
1517                 self._contactsview = widgetTree.get_widget("contactsview")
1518
1519                 self._contactColumn = gtk.TreeViewColumn("Contact")
1520                 displayContactSource = False
1521                 if displayContactSource:
1522                         textrenderer = gtk.CellRendererText()
1523                         self._contactColumn.pack_start(textrenderer, expand=False)
1524                         self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_TYPE_IDX)
1525                 textrenderer = gtk.CellRendererText()
1526                 hildonize.set_cell_thumb_selectable(textrenderer)
1527                 self._contactColumn.pack_start(textrenderer, expand=True)
1528                 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_NAME_IDX)
1529                 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1530                 self._contactColumn.set_sort_column_id(1)
1531                 self._contactColumn.set_visible(True)
1532
1533                 self._onContactsviewRowActivatedId = 0
1534                 self._onAddressbookButtonChangedId = 0
1535                 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1536
1537                 self._updateSink = gtk_toolbox.threaded_stage(
1538                         gtk_toolbox.comap(
1539                                 self._idly_populate_contactsview,
1540                                 gtk_toolbox.null_sink(),
1541                         )
1542                 )
1543
1544         def enable(self):
1545                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1546
1547                 self._contactsview.set_model(self._contactsmodel)
1548                 self._contactsview.set_fixed_height_mode(False)
1549                 self._contactsview.append_column(self._contactColumn)
1550                 self._contactsviewselection = self._contactsview.get_selection()
1551                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1552
1553                 del self._booksList[:]
1554                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1555                         if factoryName and bookName:
1556                                 entryName = "%s: %s" % (factoryName, bookName)
1557                         elif factoryName:
1558                                 entryName = factoryName
1559                         elif bookName:
1560                                 entryName = bookName
1561                         else:
1562                                 entryName = "Bad name (%d)" % factoryId
1563                         row = (str(factoryId), bookId, entryName)
1564                         self._booksList.append(row)
1565
1566                 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1567                 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1568
1569                 if len(self._booksList) <= self._selectedComboIndex:
1570                         self._selectedComboIndex = 0
1571                 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1572
1573                 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1574                 selectedBookId = self._booksList[self._selectedComboIndex][1]
1575                 self.open_addressbook(selectedFactoryId, selectedBookId)
1576
1577         def disable(self):
1578                 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1579                 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1580
1581                 self.clear()
1582
1583                 self._bookSelectionButton.set_label("")
1584                 self._contactsview.set_model(None)
1585                 self._contactsview.remove_column(self._contactColumn)
1586
1587         def add_contact(self, *args, **kwds):
1588                 """
1589                 @note Actual dial function is patched in later
1590                 """
1591                 raise NotImplementedError("Horrible unknown error has occurred")
1592
1593         def get_addressbooks(self):
1594                 """
1595                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1596                 """
1597                 for i, factory in enumerate(self._addressBookFactories):
1598                         for bookFactory, bookId, bookName in factory.get_addressbooks():
1599                                 yield (str(i), bookId), (factory.factory_name(), bookName)
1600
1601         def open_addressbook(self, bookFactoryId, bookId):
1602                 bookFactoryIndex = int(bookFactoryId)
1603                 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1604                 self._addressBook = addressBook
1605
1606         def update(self, force = False):
1607                 if not force and self._isPopulated:
1608                         return False
1609                 self._updateSink.send(())
1610                 return True
1611
1612         def clear(self):
1613                 self._isPopulated = False
1614                 self._contactsmodel.clear()
1615                 for factory in self._addressBookFactories:
1616                         factory.clear_caches()
1617                 self._addressBook.clear_caches()
1618
1619         def append(self, book):
1620                 self._addressBookFactories.append(book)
1621
1622         def extend(self, books):
1623                 self._addressBookFactories.extend(books)
1624
1625         @staticmethod
1626         def name():
1627                 return "Contacts"
1628
1629         def load_settings(self, config, sectionName):
1630                 try:
1631                         self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1632                 except ConfigParser.NoOptionError:
1633                         self._selectedComboIndex = 0
1634
1635         def save_settings(self, config, sectionName):
1636                 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1637
1638         def _idly_populate_contactsview(self):
1639                 with gtk_toolbox.gtk_lock():
1640                         banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1641                 try:
1642                         addressBook = None
1643                         while addressBook is not self._addressBook:
1644                                 addressBook = self._addressBook
1645                                 with gtk_toolbox.gtk_lock():
1646                                         self._contactsview.set_model(None)
1647                                         self.clear()
1648
1649                                 try:
1650                                         contacts = addressBook.get_contacts()
1651                                 except Exception, e:
1652                                         contacts = []
1653                                         self._isPopulated = False
1654                                         self._errorDisplay.push_exception_with_lock()
1655                                 for contactId, contactName in contacts:
1656                                         contactType = addressBook.contact_source_short_name(contactId)
1657                                         row = contactType, contactName, contactId
1658                                         self._contactsmodel.append(row)
1659
1660                                 with gtk_toolbox.gtk_lock():
1661                                         self._contactsview.set_model(self._contactsmodel)
1662
1663                         self._isPopulated = True
1664                 except Exception, e:
1665                         self._errorDisplay.push_exception_with_lock()
1666                 finally:
1667                         with gtk_toolbox.gtk_lock():
1668                                 hildonize.show_busy_banner_end(banner)
1669                 return False
1670
1671         def _on_addressbook_button_changed(self, *args, **kwds):
1672                 try:
1673                         try:
1674                                 newSelectedComboIndex = hildonize.touch_selector(
1675                                         self._window,
1676                                         "Addressbook",
1677                                         (("%s" % m[2]) for m in self._booksList),
1678                                         self._selectedComboIndex,
1679                                 )
1680                         except RuntimeError:
1681                                 return
1682
1683                         selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1684                         selectedBookId = self._booksList[newSelectedComboIndex][1]
1685
1686                         oldAddressbook = self._addressBook
1687                         self.open_addressbook(selectedFactoryId, selectedBookId)
1688                         forceUpdate = True if oldAddressbook is not self._addressBook else False
1689                         self.update(force=forceUpdate)
1690
1691                         self._selectedComboIndex = newSelectedComboIndex
1692                         self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1693                 except Exception, e:
1694                         self._errorDisplay.push_exception()
1695
1696         def _on_contactsview_row_activated(self, treeview, path, view_column):
1697                 try:
1698                         itr = self._contactsmodel.get_iter(path)
1699                         if not itr:
1700                                 return
1701
1702                         contactId = self._contactsmodel.get_value(itr, self.CONTACT_ID_IDX)
1703                         contactName = self._contactsmodel.get_value(itr, self.CONTACT_NAME_IDX)
1704                         try:
1705                                 contactDetails = self._addressBook.get_contact_details(contactId)
1706                         except Exception, e:
1707                                 contactDetails = []
1708                                 self._errorDisplay.push_exception()
1709                         contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1710
1711                         if len(contactPhoneNumbers) == 0:
1712                                 return
1713
1714                         self.add_contact(
1715                                 contactPhoneNumbers,
1716                                 messages = (contactName, ),
1717                         )
1718                         self._contactsviewselection.unselect_all()
1719                 except Exception, e:
1720                         self._errorDisplay.push_exception()