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