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