Some cleanup
[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                                 # For some reason this kills clipboard stuff
363                                 #self._clipboard.set_text(message)
364                 except Exception, e:
365                         _moduleLogger.exception(str(e))
366
367
368 class Dialpad(object):
369
370         def __init__(self, widgetTree, errorDisplay):
371                 self._clipboard = gtk.clipboard_get()
372                 self._errorDisplay = errorDisplay
373                 self._smsDialog = SmsEntryDialog(widgetTree)
374
375                 self._numberdisplay = widgetTree.get_widget("numberdisplay")
376                 self._smsButton = widgetTree.get_widget("sms")
377                 self._dialButton = widgetTree.get_widget("dial")
378                 self._backButton = widgetTree.get_widget("back")
379                 self._phonenumber = ""
380                 self._prettynumber = ""
381
382                 callbackMapping = {
383                         "on_digit_clicked": self._on_digit_clicked,
384                 }
385                 widgetTree.signal_autoconnect(callbackMapping)
386                 self._dialButton.connect("clicked", self._on_dial_clicked)
387                 self._smsButton.connect("clicked", self._on_sms_clicked)
388
389                 self._originalLabel = self._backButton.get_label()
390                 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
391                 self._backTapHandler.on_tap = self._on_backspace
392                 self._backTapHandler.on_hold = self._on_clearall
393                 self._backTapHandler.on_holding = self._set_clear_button
394                 self._backTapHandler.on_cancel = self._reset_back_button
395
396                 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
397                 self._keyPressEventId = 0
398
399         def enable(self):
400                 self._dialButton.grab_focus()
401                 self._backTapHandler.enable()
402                 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
403
404         def disable(self):
405                 self._window.disconnect(self._keyPressEventId)
406                 self._keyPressEventId = 0
407                 self._reset_back_button()
408                 self._backTapHandler.disable()
409
410         def number_selected(self, action, number, message):
411                 """
412                 @note Actual dial function is patched in later
413                 """
414                 raise NotImplementedError("Horrible unknown error has occurred")
415
416         def get_number(self):
417                 return self._phonenumber
418
419         def set_number(self, number):
420                 """
421                 Set the number to dial
422                 """
423                 try:
424                         self._phonenumber = make_ugly(number)
425                         self._prettynumber = make_pretty(self._phonenumber)
426                         self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
427                 except TypeError, e:
428                         self._errorDisplay.push_exception()
429
430         def clear(self):
431                 self.set_number("")
432
433         @staticmethod
434         def name():
435                 return "Dialpad"
436
437         def load_settings(self, config, section):
438                 pass
439
440         def save_settings(self, config, section):
441                 """
442                 @note Thread Agnostic
443                 """
444                 pass
445
446         def _on_key_press(self, widget, event):
447                 try:
448                         if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
449                                 contents = self._clipboard.wait_for_text()
450                                 if contents is not None:
451                                         self.set_number(contents)
452                 except Exception, e:
453                         self._errorDisplay.push_exception()
454
455         def _on_sms_clicked(self, widget):
456                 try:
457                         phoneNumber = self.get_number()
458                         action, phoneNumber, message = self._smsDialog.run([("Dialer", phoneNumber)], (), self._window)
459
460                         if action == SmsEntryDialog.ACTION_CANCEL:
461                                 return
462                         self.number_selected(action, phoneNumber, message)
463                 except Exception, e:
464                         self._errorDisplay.push_exception()
465
466         def _on_dial_clicked(self, widget):
467                 try:
468                         action = SmsEntryDialog.ACTION_DIAL
469                         phoneNumber = self.get_number()
470                         message = ""
471                         self.number_selected(action, phoneNumber, message)
472                 except Exception, e:
473                         self._errorDisplay.push_exception()
474
475         def _on_digit_clicked(self, widget):
476                 try:
477                         self.set_number(self._phonenumber + widget.get_name()[-1])
478                 except Exception, e:
479                         self._errorDisplay.push_exception()
480
481         def _on_backspace(self, taps):
482                 try:
483                         self.set_number(self._phonenumber[:-taps])
484                         self._reset_back_button()
485                 except Exception, e:
486                         self._errorDisplay.push_exception()
487
488         def _on_clearall(self, taps):
489                 try:
490                         self.clear()
491                         self._reset_back_button()
492                 except Exception, e:
493                         self._errorDisplay.push_exception()
494                 return False
495
496         def _set_clear_button(self):
497                 try:
498                         self._backButton.set_label("gtk-clear")
499                 except Exception, e:
500                         self._errorDisplay.push_exception()
501
502         def _reset_back_button(self):
503                 try:
504                         self._backButton.set_label(self._originalLabel)
505                 except Exception, e:
506                         self._errorDisplay.push_exception()
507
508
509 class AccountInfo(object):
510
511         def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
512                 self._errorDisplay = errorDisplay
513                 self._backend = backend
514                 self._isPopulated = False
515                 self._alarmHandler = alarmHandler
516                 self._notifyOnMissed = False
517                 self._notifyOnVoicemail = False
518                 self._notifyOnSms = False
519
520                 self._callbackList = []
521                 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
522                 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
523                 self._onCallbackSelectChangedId = 0
524
525                 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
526                 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
527                 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
528                 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
529                 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
530                 self._onNotifyToggled = 0
531                 self._onMinutesChanged = 0
532                 self._onMissedToggled = 0
533                 self._onVoicemailToggled = 0
534                 self._onSmsToggled = 0
535                 self._applyAlarmTimeoutId = None
536
537                 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
538                 self._callbackNumber = ""
539
540         def enable(self):
541                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
542
543                 self._accountViewNumberDisplay.set_use_markup(True)
544                 self.set_account_number("")
545
546                 del self._callbackList[:]
547                 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
548                 self._set_callback_label("")
549
550                 if self._alarmHandler is not None:
551                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
552                         self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
553                         self._missedCheckbox.set_active(self._notifyOnMissed)
554                         self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
555                         self._smsCheckbox.set_active(self._notifyOnSms)
556
557                         self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
558                         self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
559                         self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
560                         self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
561                         self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
562                 else:
563                         self._notifyCheckbox.set_sensitive(False)
564                         self._minutesEntryButton.set_sensitive(False)
565                         self._missedCheckbox.set_sensitive(False)
566                         self._voicemailCheckbox.set_sensitive(False)
567                         self._smsCheckbox.set_sensitive(False)
568
569                 self.update(force=True)
570
571         def disable(self):
572                 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
573                 self._onCallbackSelectChangedId = 0
574                 self._set_callback_label("")
575
576                 if self._alarmHandler is not None:
577                         self._notifyCheckbox.disconnect(self._onNotifyToggled)
578                         self._minutesEntryButton.disconnect(self._onMinutesChanged)
579                         self._missedCheckbox.disconnect(self._onNotifyToggled)
580                         self._voicemailCheckbox.disconnect(self._onNotifyToggled)
581                         self._smsCheckbox.disconnect(self._onNotifyToggled)
582                         self._onNotifyToggled = 0
583                         self._onMinutesChanged = 0
584                         self._onMissedToggled = 0
585                         self._onVoicemailToggled = 0
586                         self._onSmsToggled = 0
587                 else:
588                         self._notifyCheckbox.set_sensitive(True)
589                         self._minutesEntryButton.set_sensitive(True)
590                         self._missedCheckbox.set_sensitive(True)
591                         self._voicemailCheckbox.set_sensitive(True)
592                         self._smsCheckbox.set_sensitive(True)
593
594                 self.clear()
595                 del self._callbackList[:]
596
597         def set_account_number(self, number):
598                 """
599                 Displays current account number
600                 """
601                 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
602
603         def update(self, force = False):
604                 if not force and self._isPopulated:
605                         return False
606                 self._populate_callback_combo()
607                 self.set_account_number(self._backend.get_account_number())
608                 return True
609
610         def clear(self):
611                 self._set_callback_label("")
612                 self.set_account_number("")
613                 self._isPopulated = False
614
615         def save_everything(self):
616                 raise NotImplementedError
617
618         @staticmethod
619         def name():
620                 return "Account Info"
621
622         def load_settings(self, config, section):
623                 self._callbackNumber = make_ugly(config.get(section, "callback"))
624                 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
625                 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
626                 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
627
628         def save_settings(self, config, section):
629                 """
630                 @note Thread Agnostic
631                 """
632                 config.set(section, "callback", self._callbackNumber)
633                 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
634                 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
635                 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
636
637         def _populate_callback_combo(self):
638                 self._isPopulated = True
639                 del self._callbackList[:]
640                 try:
641                         callbackNumbers = self._backend.get_callback_numbers()
642                 except Exception, e:
643                         self._errorDisplay.push_exception()
644                         self._isPopulated = False
645                         return
646
647                 if len(callbackNumbers) == 0:
648                         callbackNumbers = {"": "No callback numbers available"}
649
650                 for number, description in callbackNumbers.iteritems():
651                         self._callbackList.append((make_pretty(number), description))
652
653                 self._set_callback_number(self._callbackNumber)
654
655         def _set_callback_number(self, number):
656                 try:
657                         if not self._backend.is_valid_syntax(number) and 0 < len(number):
658                                 self._errorDisplay.push_message("%s is not a valid callback number" % number)
659                         elif number == self._backend.get_callback_number() and 0 < len(number):
660                                 _moduleLogger.warning(
661                                         "Callback number already is %s" % (
662                                                 self._backend.get_callback_number(),
663                                         ),
664                                 )
665                                 self._set_callback_label(number)
666                         else:
667                                 self._backend.set_callback_number(number)
668                                 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
669                                         make_pretty(number), make_pretty(self._backend.get_callback_number())
670                                 )
671                                 self._callbackNumber = make_ugly(number)
672                                 self._set_callback_label(number)
673                                 _moduleLogger.info(
674                                         "Callback number set to %s" % (
675                                                 self._backend.get_callback_number(),
676                                         ),
677                                 )
678                 except Exception, e:
679                         self._errorDisplay.push_exception()
680
681         def _set_callback_label(self, uglyNumber):
682                 prettyNumber = make_pretty(uglyNumber)
683                 if len(prettyNumber) == 0:
684                         prettyNumber = "No Callback Number"
685                 self._callbackSelectButton.set_label(prettyNumber)
686
687         def _update_alarm_settings(self, recurrence):
688                 try:
689                         isEnabled = self._notifyCheckbox.get_active()
690                         if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
691                                 self._alarmHandler.apply_settings(isEnabled, recurrence)
692                 finally:
693                         self.save_everything()
694                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
695                         self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
696
697         def _on_callbackentry_clicked(self, *args):
698                 try:
699                         actualSelection = make_pretty(self._callbackNumber)
700
701                         userOptions = dict(
702                                 (number, "%s (%s)" % (number, description))
703                                 for (number, description) in self._callbackList
704                         )
705                         defaultSelection = userOptions.get(actualSelection, actualSelection)
706
707                         userSelection = hildonize.touch_selector_entry(
708                                 self._window,
709                                 "Callback Number",
710                                 list(userOptions.itervalues()),
711                                 defaultSelection,
712                         )
713                         reversedUserOptions = dict(
714                                 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
715                         )
716                         selectedNumber = reversedUserOptions.get(userSelection, userSelection)
717
718                         number = make_ugly(selectedNumber)
719                         self._set_callback_number(number)
720                 except RuntimeError, e:
721                         _moduleLogger.exception("%s" % str(e))
722                 except Exception, e:
723                         self._errorDisplay.push_exception()
724
725         def _on_notify_toggled(self, *args):
726                 try:
727                         if self._applyAlarmTimeoutId is not None:
728                                 gobject.source_remove(self._applyAlarmTimeoutId)
729                                 self._applyAlarmTimeoutId = None
730                         self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
731                 except Exception, e:
732                         self._errorDisplay.push_exception()
733
734         def _on_minutes_clicked(self, *args):
735                 recurrenceChoices = [
736                         (1, "1 minute"),
737                         (2, "2 minutes"),
738                         (3, "3 minutes"),
739                         (5, "5 minutes"),
740                         (8, "8 minutes"),
741                         (10, "10 minutes"),
742                         (15, "15 minutes"),
743                         (30, "30 minutes"),
744                         (45, "45 minutes"),
745                         (60, "1 hour"),
746                         (3*60, "3 hours"),
747                         (6*60, "6 hours"),
748                         (12*60, "12 hours"),
749                 ]
750                 try:
751                         actualSelection = self._alarmHandler.recurrence
752
753                         closestSelectionIndex = 0
754                         for i, possible in enumerate(recurrenceChoices):
755                                 if possible[0] <= actualSelection:
756                                         closestSelectionIndex = i
757                         recurrenceIndex = hildonize.touch_selector(
758                                 self._window,
759                                 "Minutes",
760                                 (("%s" % m[1]) for m in recurrenceChoices),
761                                 closestSelectionIndex,
762                         )
763                         recurrence = recurrenceChoices[recurrenceIndex][0]
764
765                         self._update_alarm_settings(recurrence)
766                 except RuntimeError, e:
767                         _moduleLogger.exception("%s" % str(e))
768                 except Exception, e:
769                         self._errorDisplay.push_exception()
770
771         def _on_apply_timeout(self, *args):
772                 try:
773                         self._applyAlarmTimeoutId = None
774
775                         self._update_alarm_settings(self._alarmHandler.recurrence)
776                 except Exception, e:
777                         self._errorDisplay.push_exception()
778                 return False
779
780         def _on_missed_toggled(self, *args):
781                 try:
782                         self._notifyOnMissed = self._missedCheckbox.get_active()
783                         self.save_everything()
784                 except Exception, e:
785                         self._errorDisplay.push_exception()
786
787         def _on_voicemail_toggled(self, *args):
788                 try:
789                         self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
790                         self.save_everything()
791                 except Exception, e:
792                         self._errorDisplay.push_exception()
793
794         def _on_sms_toggled(self, *args):
795                 try:
796                         self._notifyOnSms = self._smsCheckbox.get_active()
797                         self.save_everything()
798                 except Exception, e:
799                         self._errorDisplay.push_exception()
800
801
802 class RecentCallsView(object):
803
804         NUMBER_IDX = 0
805         DATE_IDX = 1
806         ACTION_IDX = 2
807         FROM_IDX = 3
808         FROM_ID_IDX = 4
809
810         def __init__(self, widgetTree, backend, errorDisplay):
811                 self._errorDisplay = errorDisplay
812                 self._backend = backend
813
814                 self._isPopulated = False
815                 self._recentmodel = gtk.ListStore(
816                         gobject.TYPE_STRING, # number
817                         gobject.TYPE_STRING, # date
818                         gobject.TYPE_STRING, # action
819                         gobject.TYPE_STRING, # from
820                         gobject.TYPE_STRING, # from id
821                 )
822                 self._recentview = widgetTree.get_widget("recentview")
823                 self._recentviewselection = None
824                 self._onRecentviewRowActivatedId = 0
825
826                 textrenderer = gtk.CellRendererText()
827                 textrenderer.set_property("yalign", 0)
828                 self._dateColumn = gtk.TreeViewColumn("Date")
829                 self._dateColumn.pack_start(textrenderer, expand=True)
830                 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
831
832                 textrenderer = gtk.CellRendererText()
833                 textrenderer.set_property("yalign", 0)
834                 self._actionColumn = gtk.TreeViewColumn("Action")
835                 self._actionColumn.pack_start(textrenderer, expand=True)
836                 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
837
838                 textrenderer = gtk.CellRendererText()
839                 textrenderer.set_property("yalign", 0)
840                 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
841                 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
842                 self._numberColumn = gtk.TreeViewColumn("Number")
843                 self._numberColumn.pack_start(textrenderer, expand=True)
844                 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
845
846                 textrenderer = gtk.CellRendererText()
847                 textrenderer.set_property("yalign", 0)
848                 hildonize.set_cell_thumb_selectable(textrenderer)
849                 self._nameColumn = gtk.TreeViewColumn("From")
850                 self._nameColumn.pack_start(textrenderer, expand=True)
851                 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
852                 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
853
854                 self._window = gtk_toolbox.find_parent_window(self._recentview)
855                 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
856
857                 self._updateSink = gtk_toolbox.threaded_stage(
858                         gtk_toolbox.comap(
859                                 self._idly_populate_recentview,
860                                 gtk_toolbox.null_sink(),
861                         )
862                 )
863
864         def enable(self):
865                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
866                 self._recentview.set_model(self._recentmodel)
867                 self._recentview.set_fixed_height_mode(False)
868
869                 self._recentview.append_column(self._dateColumn)
870                 self._recentview.append_column(self._actionColumn)
871                 self._recentview.append_column(self._numberColumn)
872                 self._recentview.append_column(self._nameColumn)
873                 self._recentviewselection = self._recentview.get_selection()
874                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
875
876                 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
877
878         def disable(self):
879                 self._recentview.disconnect(self._onRecentviewRowActivatedId)
880
881                 self.clear()
882
883                 self._recentview.remove_column(self._dateColumn)
884                 self._recentview.remove_column(self._actionColumn)
885                 self._recentview.remove_column(self._nameColumn)
886                 self._recentview.remove_column(self._numberColumn)
887                 self._recentview.set_model(None)
888
889         def number_selected(self, action, number, message):
890                 """
891                 @note Actual dial function is patched in later
892                 """
893                 raise NotImplementedError("Horrible unknown error has occurred")
894
895         def update(self, force = False):
896                 if not force and self._isPopulated:
897                         return False
898                 self._updateSink.send(())
899                 return True
900
901         def clear(self):
902                 self._isPopulated = False
903                 self._recentmodel.clear()
904
905         @staticmethod
906         def name():
907                 return "Recent Calls"
908
909         def load_settings(self, config, section):
910                 pass
911
912         def save_settings(self, config, section):
913                 """
914                 @note Thread Agnostic
915                 """
916                 pass
917
918         def _idly_populate_recentview(self):
919                 with gtk_toolbox.gtk_lock():
920                         banner = hildonize.show_busy_banner_start(self._window, "Loading Recent History")
921                 try:
922                         self._recentmodel.clear()
923                         self._isPopulated = True
924
925                         try:
926                                 recentItems = self._backend.get_recent()
927                         except Exception, e:
928                                 self._errorDisplay.push_exception_with_lock()
929                                 self._isPopulated = False
930                                 recentItems = []
931
932                         recentItems = (
933                                 gv_backend.decorate_recent(data)
934                                 for data in gv_backend.sort_messages(recentItems)
935                         )
936
937                         for contactId, personName, phoneNumber, date, action in recentItems:
938                                 if not personName:
939                                         personName = "Unknown"
940                                 date = abbrev_relative_date(date)
941                                 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
942                                 prettyNumber = make_pretty(prettyNumber)
943                                 item = (prettyNumber, date, action.capitalize(), personName, contactId)
944                                 with gtk_toolbox.gtk_lock():
945                                         self._recentmodel.append(item)
946                 except Exception, e:
947                         self._errorDisplay.push_exception_with_lock()
948                 finally:
949                         with gtk_toolbox.gtk_lock():
950                                 hildonize.show_busy_banner_end(banner)
951
952                 return False
953
954         def _on_recentview_row_activated(self, treeview, path, view_column):
955                 try:
956                         itr = self._recentmodel.get_iter(path)
957                         if not itr:
958                                 return
959
960                         number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
961                         number = make_ugly(number)
962                         description = self._recentmodel.get_value(itr, self.FROM_IDX)
963                         contactId = self._recentmodel.get_value(itr, self.FROM_ID_IDX)
964                         if contactId:
965                                 contactPhoneNumbers = list(self._backend.get_contact_details(contactId))
966                                 defaultMatches = [
967                                         (number == make_ugly(contactNumber) or number[1:] == make_ugly(contactNumber))
968                                         for (numberDescription, contactNumber) in contactPhoneNumbers
969                                 ]
970                                 try:
971                                         defaultIndex = defaultMatches.index(True)
972                                 except ValueError:
973                                         contactPhoneNumbers.append(("Other", number))
974                                         defaultIndex = len(contactPhoneNumbers)-1
975                                         _moduleLogger.warn(
976                                                 "Could not find contact %r's number %s among %r" % (
977                                                         contactId, number, contactPhoneNumbers
978                                                 )
979                                         )
980                         else:
981                                 contactPhoneNumbers = [("Phone", number)]
982                                 defaultIndex = -1
983
984                         action, phoneNumber, message = self._phoneTypeSelector.run(
985                                 contactPhoneNumbers,
986                                 messages = (description, ),
987                                 parent = self._window,
988                                 defaultIndex = defaultIndex,
989                         )
990                         if action == SmsEntryDialog.ACTION_CANCEL:
991                                 return
992                         assert phoneNumber, "A lack of phone number exists"
993
994                         self.number_selected(action, phoneNumber, message)
995                         self._recentviewselection.unselect_all()
996                 except Exception, e:
997                         self._errorDisplay.push_exception()
998
999
1000 class MessagesView(object):
1001
1002         NUMBER_IDX = 0
1003         DATE_IDX = 1
1004         HEADER_IDX = 2
1005         MESSAGE_IDX = 3
1006         MESSAGES_IDX = 4
1007         FROM_ID_IDX = 5
1008         MESSAGE_DATA_IDX = 6
1009
1010         NO_MESSAGES = "None"
1011         VOICEMAIL_MESSAGES = "Voicemail"
1012         TEXT_MESSAGES = "Texts"
1013         ALL_TYPES = "All Messages"
1014         MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
1015
1016         UNREAD_STATUS = "Unread"
1017         UNARCHIVED_STATUS = "Inbox"
1018         ALL_STATUS = "Any"
1019         MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
1020
1021         def __init__(self, widgetTree, backend, errorDisplay):
1022                 self._errorDisplay = errorDisplay
1023                 self._backend = backend
1024
1025                 self._isPopulated = False
1026                 self._messagemodel = gtk.ListStore(
1027                         gobject.TYPE_STRING, # number
1028                         gobject.TYPE_STRING, # date
1029                         gobject.TYPE_STRING, # header
1030                         gobject.TYPE_STRING, # message
1031                         object, # messages
1032                         gobject.TYPE_STRING, # from id
1033                         object, # message data
1034                 )
1035                 self._messagemodelfiltered = self._messagemodel.filter_new()
1036                 self._messagemodelfiltered.set_visible_func(self._is_message_visible)
1037                 self._messageview = widgetTree.get_widget("messages_view")
1038                 self._messageviewselection = None
1039                 self._onMessageviewRowActivatedId = 0
1040
1041                 self._messageRenderer = gtk.CellRendererText()
1042                 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1043                 self._messageRenderer.set_property("wrap-width", 500)
1044                 self._messageColumn = gtk.TreeViewColumn("Messages")
1045                 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1046                 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1047                 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1048
1049                 self._window = gtk_toolbox.find_parent_window(self._messageview)
1050                 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
1051
1052                 self._messageTypeButton = widgetTree.get_widget("messageTypeButton")
1053                 self._onMessageTypeClickedId = 0
1054                 self._messageType = self.ALL_TYPES
1055                 self._messageStatusButton = widgetTree.get_widget("messageStatusButton")
1056                 self._onMessageStatusClickedId = 0
1057                 self._messageStatus = self.ALL_STATUS
1058
1059                 self._updateSink = gtk_toolbox.threaded_stage(
1060                         gtk_toolbox.comap(
1061                                 self._idly_populate_messageview,
1062                                 gtk_toolbox.null_sink(),
1063                         )
1064                 )
1065
1066         def enable(self):
1067                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1068                 self._messageview.set_model(self._messagemodelfiltered)
1069                 self._messageview.set_headers_visible(False)
1070                 self._messageview.set_fixed_height_mode(False)
1071
1072                 self._messageview.append_column(self._messageColumn)
1073                 self._messageviewselection = self._messageview.get_selection()
1074                 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1075
1076                 self._messageTypeButton.set_label(self._messageType)
1077                 self._messageStatusButton.set_label(self._messageStatus)
1078
1079                 self._onMessageviewRowActivatedId = self._messageview.connect(
1080                         "row-activated", self._on_messageview_row_activated
1081                 )
1082                 self._onMessageTypeClickedId = self._messageTypeButton.connect(
1083                         "clicked", self._on_message_type_clicked
1084                 )
1085                 self._onMessageStatusClickedId = self._messageStatusButton.connect(
1086                         "clicked", self._on_message_status_clicked
1087                 )
1088
1089         def disable(self):
1090                 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1091                 self._messageTypeButton.disconnect(self._onMessageTypeClickedId)
1092                 self._messageStatusButton.disconnect(self._onMessageStatusClickedId)
1093
1094                 self.clear()
1095
1096                 self._messageview.remove_column(self._messageColumn)
1097                 self._messageview.set_model(None)
1098
1099         def number_selected(self, action, number, message):
1100                 """
1101                 @note Actual dial function is patched in later
1102                 """
1103                 raise NotImplementedError("Horrible unknown error has occurred")
1104
1105         def update(self, force = False):
1106                 if not force and self._isPopulated:
1107                         return False
1108                 self._updateSink.send(())
1109                 return True
1110
1111         def clear(self):
1112                 self._isPopulated = False
1113                 self._messagemodel.clear()
1114
1115         @staticmethod
1116         def name():
1117                 return "Messages"
1118
1119         def load_settings(self, config, sectionName):
1120                 try:
1121                         self._messageType = config.get(sectionName, "type")
1122                         if self._messageType not in self.MESSAGE_TYPES:
1123                                 self._messageType = self.ALL_TYPES
1124                         self._messageStatus = config.get(sectionName, "status")
1125                         if self._messageStatus not in self.MESSAGE_STATUSES:
1126                                 self._messageStatus = self.ALL_STATUS
1127                 except ConfigParser.NoOptionError:
1128                         pass
1129
1130         def save_settings(self, config, sectionName):
1131                 """
1132                 @note Thread Agnostic
1133                 """
1134                 config.set(sectionName, "status", self._messageStatus)
1135                 config.set(sectionName, "type", self._messageType)
1136
1137         def _is_message_visible(self, model, iter):
1138                 try:
1139                         message = model.get_value(iter, self.MESSAGE_DATA_IDX)
1140                         if message is None:
1141                                 return False # this seems weird but oh well
1142                         return self._filter_messages(message, self._messageType, self._messageStatus)
1143                 except Exception, e:
1144                         self._errorDisplay.push_exception()
1145
1146         @classmethod
1147         def _filter_messages(cls, message, type, status):
1148                 if type == cls.ALL_TYPES:
1149                         isType = True
1150                 else:
1151                         messageType = message["type"]
1152                         isType = messageType == type
1153
1154                 if status == cls.ALL_STATUS:
1155                         isStatus = True
1156                 else:
1157                         isUnarchived = not message["isArchived"]
1158                         isUnread = not message["isRead"]
1159                         if status == cls.UNREAD_STATUS:
1160                                 isStatus = isUnarchived and isUnread
1161                         elif status == cls.UNARCHIVED_STATUS:
1162                                 isStatus = isUnarchived
1163                         else:
1164                                 assert "Status %s is bad for %r" % (status, message)
1165
1166                 return isType and isStatus
1167
1168         _MIN_MESSAGES_SHOWN = 4
1169
1170         def _idly_populate_messageview(self):
1171                 with gtk_toolbox.gtk_lock():
1172                         banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1173                 try:
1174                         self._messagemodel.clear()
1175                         self._isPopulated = True
1176
1177                         if self._messageType == self.NO_MESSAGES:
1178                                 messageItems = []
1179                         else:
1180                                 try:
1181                                         messageItems = self._backend.get_messages()
1182                                 except Exception, e:
1183                                         self._errorDisplay.push_exception_with_lock()
1184                                         self._isPopulated = False
1185                                         messageItems = []
1186
1187                         messageItems = (
1188                                 (gv_backend.decorate_message(message), message)
1189                                 for message in gv_backend.sort_messages(messageItems)
1190                         )
1191
1192                         for (contactId, header, number, relativeDate, messages), messageData in messageItems:
1193                                 prettyNumber = number[2:] if number.startswith("+1") else number
1194                                 prettyNumber = make_pretty(prettyNumber)
1195
1196                                 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1197                                 expandedMessages = [firstMessage]
1198                                 expandedMessages.extend(messages)
1199                                 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1200                                         firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1201                                         secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1202                                         collapsedMessages = [firstMessage, secondMessage]
1203                                         collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1204                                 else:
1205                                         collapsedMessages = expandedMessages
1206                                 #collapsedMessages = _collapse_message(collapsedMessages, 60, self._MIN_MESSAGES_SHOWN)
1207
1208                                 number = make_ugly(number)
1209
1210                                 row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId, messageData
1211                                 with gtk_toolbox.gtk_lock():
1212                                         self._messagemodel.append(row)
1213                 except Exception, e:
1214                         self._errorDisplay.push_exception_with_lock()
1215                 finally:
1216                         with gtk_toolbox.gtk_lock():
1217                                 hildonize.show_busy_banner_end(banner)
1218                                 self._messagemodelfiltered.refilter()
1219
1220                 return False
1221
1222         def _on_messageview_row_activated(self, treeview, path, view_column):
1223                 try:
1224                         itr = self._messagemodel.get_iter(path)
1225                         if not itr:
1226                                 return
1227
1228                         number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX))
1229                         description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1230
1231                         contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX)
1232                         if contactId:
1233                                 contactPhoneNumbers = list(self._backend.get_contact_details(contactId))
1234                                 defaultMatches = [
1235                                         (number == make_ugly(contactNumber) or number[1:] == make_ugly(contactNumber))
1236                                         for (numberDescription, contactNumber) in contactPhoneNumbers
1237                                 ]
1238                                 try:
1239                                         defaultIndex = defaultMatches.index(True)
1240                                 except ValueError:
1241                                         contactPhoneNumbers.append(("Other", number))
1242                                         defaultIndex = len(contactPhoneNumbers)-1
1243                                         _moduleLogger.warn(
1244                                                 "Could not find contact %r's number %s among %r" % (
1245                                                         contactId, number, contactPhoneNumbers
1246                                                 )
1247                                         )
1248                         else:
1249                                 contactPhoneNumbers = [("Phone", number)]
1250                                 defaultIndex = -1
1251
1252                         action, phoneNumber, message = self._phoneTypeSelector.run(
1253                                 contactPhoneNumbers,
1254                                 messages = description,
1255                                 parent = self._window,
1256                                 defaultIndex = defaultIndex,
1257                         )
1258                         if action == SmsEntryDialog.ACTION_CANCEL:
1259                                 return
1260                         assert phoneNumber, "A lock of phone number exists"
1261
1262                         self.number_selected(action, phoneNumber, message)
1263                         self._messageviewselection.unselect_all()
1264                 except Exception, e:
1265                         self._errorDisplay.push_exception()
1266
1267         def _on_message_type_clicked(self, *args, **kwds):
1268                 try:
1269                         selectedIndex = self.MESSAGE_TYPES.index(self._messageType)
1270
1271                         try:
1272                                 newSelectedIndex = hildonize.touch_selector(
1273                                         self._window,
1274                                         "Message Type",
1275                                         self.MESSAGE_TYPES,
1276                                         selectedIndex,
1277                                 )
1278                         except RuntimeError:
1279                                 return
1280
1281                         if selectedIndex != newSelectedIndex:
1282                                 self._messageType = self.MESSAGE_TYPES[newSelectedIndex]
1283                                 self._messageTypeButton.set_label(self._messageType)
1284                                 self._messagemodelfiltered.refilter()
1285                 except Exception, e:
1286                         self._errorDisplay.push_exception()
1287
1288         def _on_message_status_clicked(self, *args, **kwds):
1289                 try:
1290                         selectedIndex = self.MESSAGE_STATUSES.index(self._messageStatus)
1291
1292                         try:
1293                                 newSelectedIndex = hildonize.touch_selector(
1294                                         self._window,
1295                                         "Message Status",
1296                                         self.MESSAGE_STATUSES,
1297                                         selectedIndex,
1298                                 )
1299                         except RuntimeError:
1300                                 return
1301
1302                         if selectedIndex != newSelectedIndex:
1303                                 self._messageStatus = self.MESSAGE_STATUSES[newSelectedIndex]
1304                                 self._messageStatusButton.set_label(self._messageStatus)
1305                                 self._messagemodelfiltered.refilter()
1306                 except Exception, e:
1307                         self._errorDisplay.push_exception()
1308
1309
1310 class ContactsView(object):
1311
1312         CONTACT_TYPE_IDX = 0
1313         CONTACT_NAME_IDX = 1
1314         CONTACT_ID_IDX = 2
1315
1316         def __init__(self, widgetTree, backend, errorDisplay):
1317                 self._errorDisplay = errorDisplay
1318                 self._backend = backend
1319
1320                 self._addressBook = None
1321                 self._selectedComboIndex = 0
1322                 self._addressBookFactories = [null_backend.NullAddressBook()]
1323
1324                 self._booksList = []
1325                 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1326
1327                 self._isPopulated = False
1328                 self._contactsmodel = gtk.ListStore(
1329                         gobject.TYPE_STRING, # Contact Type
1330                         gobject.TYPE_STRING, # Contact Name
1331                         gobject.TYPE_STRING, # Contact ID
1332                 )
1333                 self._contactsviewselection = None
1334                 self._contactsview = widgetTree.get_widget("contactsview")
1335
1336                 self._contactColumn = gtk.TreeViewColumn("Contact")
1337                 displayContactSource = False
1338                 if displayContactSource:
1339                         textrenderer = gtk.CellRendererText()
1340                         self._contactColumn.pack_start(textrenderer, expand=False)
1341                         self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_TYPE_IDX)
1342                 textrenderer = gtk.CellRendererText()
1343                 hildonize.set_cell_thumb_selectable(textrenderer)
1344                 self._contactColumn.pack_start(textrenderer, expand=True)
1345                 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_NAME_IDX)
1346                 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1347                 self._contactColumn.set_sort_column_id(1)
1348                 self._contactColumn.set_visible(True)
1349
1350                 self._onContactsviewRowActivatedId = 0
1351                 self._onAddressbookButtonChangedId = 0
1352                 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1353                 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
1354
1355                 self._updateSink = gtk_toolbox.threaded_stage(
1356                         gtk_toolbox.comap(
1357                                 self._idly_populate_contactsview,
1358                                 gtk_toolbox.null_sink(),
1359                         )
1360                 )
1361
1362         def enable(self):
1363                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1364
1365                 self._contactsview.set_model(self._contactsmodel)
1366                 self._contactsview.set_fixed_height_mode(False)
1367                 self._contactsview.append_column(self._contactColumn)
1368                 self._contactsviewselection = self._contactsview.get_selection()
1369                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1370
1371                 del self._booksList[:]
1372                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1373                         if factoryName and bookName:
1374                                 entryName = "%s: %s" % (factoryName, bookName)
1375                         elif factoryName:
1376                                 entryName = factoryName
1377                         elif bookName:
1378                                 entryName = bookName
1379                         else:
1380                                 entryName = "Bad name (%d)" % factoryId
1381                         row = (str(factoryId), bookId, entryName)
1382                         self._booksList.append(row)
1383
1384                 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1385                 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1386
1387                 if len(self._booksList) <= self._selectedComboIndex:
1388                         self._selectedComboIndex = 0
1389                 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1390
1391                 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1392                 selectedBookId = self._booksList[self._selectedComboIndex][1]
1393                 self.open_addressbook(selectedFactoryId, selectedBookId)
1394
1395         def disable(self):
1396                 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1397                 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1398
1399                 self.clear()
1400
1401                 self._bookSelectionButton.set_label("")
1402                 self._contactsview.set_model(None)
1403                 self._contactsview.remove_column(self._contactColumn)
1404
1405         def number_selected(self, action, number, message):
1406                 """
1407                 @note Actual dial function is patched in later
1408                 """
1409                 raise NotImplementedError("Horrible unknown error has occurred")
1410
1411         def get_addressbooks(self):
1412                 """
1413                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1414                 """
1415                 for i, factory in enumerate(self._addressBookFactories):
1416                         for bookFactory, bookId, bookName in factory.get_addressbooks():
1417                                 yield (str(i), bookId), (factory.factory_name(), bookName)
1418
1419         def open_addressbook(self, bookFactoryId, bookId):
1420                 bookFactoryIndex = int(bookFactoryId)
1421                 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1422                 self._addressBook = addressBook
1423
1424         def update(self, force = False):
1425                 if not force and self._isPopulated:
1426                         return False
1427                 self._updateSink.send(())
1428                 return True
1429
1430         def clear(self):
1431                 self._isPopulated = False
1432                 self._contactsmodel.clear()
1433                 for factory in self._addressBookFactories:
1434                         factory.clear_caches()
1435                 self._addressBook.clear_caches()
1436
1437         def append(self, book):
1438                 self._addressBookFactories.append(book)
1439
1440         def extend(self, books):
1441                 self._addressBookFactories.extend(books)
1442
1443         @staticmethod
1444         def name():
1445                 return "Contacts"
1446
1447         def load_settings(self, config, sectionName):
1448                 try:
1449                         self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1450                 except ConfigParser.NoOptionError:
1451                         self._selectedComboIndex = 0
1452
1453         def save_settings(self, config, sectionName):
1454                 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1455
1456         def _idly_populate_contactsview(self):
1457                 with gtk_toolbox.gtk_lock():
1458                         banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1459                 try:
1460                         addressBook = None
1461                         while addressBook is not self._addressBook:
1462                                 addressBook = self._addressBook
1463                                 with gtk_toolbox.gtk_lock():
1464                                         self._contactsview.set_model(None)
1465                                         self.clear()
1466
1467                                 try:
1468                                         contacts = addressBook.get_contacts()
1469                                 except Exception, e:
1470                                         contacts = []
1471                                         self._isPopulated = False
1472                                         self._errorDisplay.push_exception_with_lock()
1473                                 for contactId, contactName in contacts:
1474                                         contactType = addressBook.contact_source_short_name(contactId)
1475                                         row = contactType, contactName, contactId
1476                                         self._contactsmodel.append(row)
1477
1478                                 with gtk_toolbox.gtk_lock():
1479                                         self._contactsview.set_model(self._contactsmodel)
1480
1481                         self._isPopulated = True
1482                 except Exception, e:
1483                         self._errorDisplay.push_exception_with_lock()
1484                 finally:
1485                         with gtk_toolbox.gtk_lock():
1486                                 hildonize.show_busy_banner_end(banner)
1487                 return False
1488
1489         def _on_addressbook_button_changed(self, *args, **kwds):
1490                 try:
1491                         try:
1492                                 newSelectedComboIndex = hildonize.touch_selector(
1493                                         self._window,
1494                                         "Addressbook",
1495                                         (("%s" % m[2]) for m in self._booksList),
1496                                         self._selectedComboIndex,
1497                                 )
1498                         except RuntimeError:
1499                                 return
1500
1501                         selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1502                         selectedBookId = self._booksList[newSelectedComboIndex][1]
1503
1504                         oldAddressbook = self._addressBook
1505                         self.open_addressbook(selectedFactoryId, selectedBookId)
1506                         forceUpdate = True if oldAddressbook is not self._addressBook else False
1507                         self.update(force=forceUpdate)
1508
1509                         self._selectedComboIndex = newSelectedComboIndex
1510                         self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1511                 except Exception, e:
1512                         self._errorDisplay.push_exception()
1513
1514         def _on_contactsview_row_activated(self, treeview, path, view_column):
1515                 try:
1516                         itr = self._contactsmodel.get_iter(path)
1517                         if not itr:
1518                                 return
1519
1520                         contactId = self._contactsmodel.get_value(itr, self.CONTACT_ID_IDX)
1521                         contactName = self._contactsmodel.get_value(itr, self.CONTACT_NAME_IDX)
1522                         try:
1523                                 contactDetails = self._addressBook.get_contact_details(contactId)
1524                         except Exception, e:
1525                                 contactDetails = []
1526                                 self._errorDisplay.push_exception()
1527                         contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1528
1529                         if len(contactPhoneNumbers) == 0:
1530                                 return
1531
1532                         action, phoneNumber, message = self._phoneTypeSelector.run(
1533                                 contactPhoneNumbers,
1534                                 messages = (contactName, ),
1535                                 parent = self._window,
1536                         )
1537                         if action == SmsEntryDialog.ACTION_CANCEL:
1538                                 return
1539                         assert phoneNumber, "A lack of phone number exists"
1540
1541                         self.number_selected(action, phoneNumber, message)
1542                         self._contactsviewselection.unselect_all()
1543                 except Exception, e:
1544                         self._errorDisplay.push_exception()