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