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