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