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