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