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