4 DialCentral - Front end for Google's GoogleVoice service.
5 Copyright (C) 2008 Mark Bergman bergman AT merctech DOT com
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.
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.
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
21 @todo Collapse voicemails
24 from __future__ import with_statement
37 from backends import gv_backend
38 from backends import null_backend
41 _moduleLogger = logging.getLogger("gv_views")
44 def make_ugly(prettynumber):
46 function to take a phone number and strip out all non-numeric
49 >>> make_ugly("+012-(345)-678-90")
52 return normalize_number(prettynumber)
55 def normalize_number(prettynumber):
57 function to take a phone number and strip out all non-numeric
60 >>> normalize_number("+012-(345)-678-90")
62 >>> normalize_number("1-(345)-678-9000")
64 >>> normalize_number("+1-(345)-678-9000")
67 uglynumber = re.sub('[^0-9+]', '', prettynumber)
72 def _make_pretty_with_areacodde(phonenumber):
73 prettynumber = "(%s)" % (phonenumber[0:3], )
74 if 3 < len(phonenumber):
75 prettynumber += " %s" % (phonenumber[3:6], )
76 if 6 < len(phonenumber):
77 prettynumber += "-%s" % (phonenumber[6:], )
81 def _make_pretty_local(phonenumber):
82 prettynumber = "%s" % (phonenumber[0:3], )
83 if 3 < len(phonenumber):
84 prettynumber += "-%s" % (phonenumber[3:], )
88 def _make_pretty_international(phonenumber):
89 prettynumber = phonenumber
90 if phonenumber.startswith("0"):
91 prettynumber = "+%s " % (phonenumber[0:3], )
92 if 3 < len(phonenumber):
93 prettynumber += _make_pretty_with_areacodde(phonenumber[3:])
94 if phonenumber.startswith("1"):
96 prettynumber += _make_pretty_with_areacodde(phonenumber[1:])
100 def make_pretty(phonenumber):
102 Function to take a phone number and return the pretty version
104 if phonenumber begins with 0:
106 if phonenumber begins with 1: ( for gizmo callback numbers )
108 if phonenumber is 13 digits:
110 if phonenumber is 10 digits:
112 >>> make_pretty("12")
114 >>> make_pretty("1234567")
116 >>> make_pretty("2345678901")
118 >>> make_pretty("12345678901")
120 >>> make_pretty("01234567890")
122 >>> make_pretty("+01234567890")
124 >>> make_pretty("+12")
126 >>> make_pretty("+123")
128 >>> make_pretty("+1234")
131 if phonenumber is None or phonenumber is "":
134 phonenumber = normalize_number(phonenumber)
136 if phonenumber[0] == "+":
137 prettynumber = _make_pretty_international(phonenumber[1:])
138 if not prettynumber.startswith("+"):
139 prettynumber = "+"+prettynumber
140 elif 8 < len(phonenumber) and phonenumber[0] in ("0", "1"):
141 prettynumber = _make_pretty_international(phonenumber)
142 elif 7 < len(phonenumber):
143 prettynumber = _make_pretty_with_areacodde(phonenumber)
144 elif 3 < len(phonenumber):
145 prettynumber = _make_pretty_local(phonenumber)
147 prettynumber = phonenumber
148 return prettynumber.strip()
151 def abbrev_relative_date(date):
153 >>> abbrev_relative_date("42 hours ago")
155 >>> abbrev_relative_date("2 days ago")
157 >>> abbrev_relative_date("4 weeks ago")
160 parts = date.split(" ")
161 return "%s %s" % (parts[0], parts[1][0])
164 def _collapse_message(messageLines, maxCharsPerLine, maxLines):
167 numLines = len(messageLines)
168 for line in messageLines[0:min(maxLines, numLines)]:
169 linesPerLine = max(1, int(len(line) / maxCharsPerLine))
170 allowedLines = maxLines - lines
171 acceptedLines = min(allowedLines, linesPerLine)
172 acceptedChars = acceptedLines * maxCharsPerLine
174 if acceptedChars < (len(line) + 3):
177 acceptedChars = len(line) # eh, might as well complete the line
179 abbrevMessage = "%s%s" % (line[0:acceptedChars], suffix)
182 lines += acceptedLines
183 if maxLines <= lines:
187 def collapse_message(message, maxCharsPerLine, maxLines):
189 >>> collapse_message("Hello", 60, 2)
191 >>> collapse_message("Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789", 60, 2)
192 'Hello world how are you doing today? 01234567890123456789012...'
193 >>> collapse_message('''Hello world how are you doing today?
194 ... 01234567890123456789
195 ... 01234567890123456789
196 ... 01234567890123456789
197 ... 01234567890123456789''', 60, 2)
198 'Hello world how are you doing today?\n01234567890123456789'
199 >>> collapse_message('''
200 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
201 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
202 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
203 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
204 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
205 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789''', 60, 2)
206 '\nHello world how are you doing today? 01234567890123456789012...'
208 messageLines = message.split("\n")
209 return "\n".join(_collapse_message(messageLines, maxCharsPerLine, maxLines))
212 def _get_contact_numbers(backend, contactId, number):
214 contactPhoneNumbers = list(backend.get_contact_details(contactId))
215 uglyContactNumbers = (
216 make_ugly(contactNumber)
217 for (numberDescription, contactNumber) in contactPhoneNumbers
221 number == contactNumber or
222 number[1:] == contactNumber and number.startswith("1") or
223 number[2:] == contactNumber and number.startswith("+1") or
224 number == contactNumber[1:] and contactNumber.startswith("1") or
225 number == contactNumber[2:] and contactNumber.startswith("+1")
227 for contactNumber in uglyContactNumbers
230 defaultIndex = defaultMatches.index(True)
232 contactPhoneNumbers.append(("Other", number))
233 defaultIndex = len(contactPhoneNumbers)-1
235 "Could not find contact %r's number %s among %r" % (
236 contactId, number, contactPhoneNumbers
240 contactPhoneNumbers = [("Phone", number)]
243 return contactPhoneNumbers, defaultIndex
246 class SmsEntryWindow(object):
250 def __init__(self, widgetTree, parent, app):
251 self._clipboard = gtk.clipboard_get()
252 self._widgetTree = widgetTree
253 self._parent = parent
255 self._isFullScreen = False
257 self._window = self._widgetTree.get_widget("smsWindow")
258 self._window = hildonize.hildonize_window(self._app, self._window)
259 self._window.connect("delete-event", self._on_delete)
260 self._window.connect("key-press-event", self._on_key_press)
261 self._window.connect("window-state-event", self._on_window_state_change)
262 self._widgetTree.get_widget("smsMessagesViewPort").show()
264 errorBox = self._widgetTree.get_widget("smsErrorEventBox")
265 errorDescription = self._widgetTree.get_widget("smsErrorDescription")
266 errorClose = self._widgetTree.get_widget("smsErrorClose")
267 self._errorDisplay = gtk_toolbox.ErrorDisplay(errorBox, errorDescription, errorClose)
269 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
270 self._smsButton.connect("clicked", self._on_send)
271 self._dialButton = self._widgetTree.get_widget("dialButton")
272 self._dialButton.connect("clicked", self._on_dial)
274 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
276 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
277 self._messagesView = self._widgetTree.get_widget("smsMessages")
279 textrenderer = gtk.CellRendererText()
280 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
281 textrenderer.set_property("wrap-width", 450)
282 messageColumn = gtk.TreeViewColumn("")
283 messageColumn.pack_start(textrenderer, expand=True)
284 messageColumn.add_attribute(textrenderer, "markup", 0)
285 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
286 self._messagesView.append_column(messageColumn)
287 self._messagesView.set_headers_visible(False)
288 self._messagesView.set_model(self._messagemodel)
289 self._messagesView.set_fixed_height_mode(False)
291 self._conversationView = self._messagesView.get_parent()
292 self._conversationViewPort = self._conversationView.get_parent()
293 self._scrollWindow = self._conversationViewPort.get_parent()
295 self._targetList = self._widgetTree.get_widget("smsTargetList")
296 self._phoneButton = self._widgetTree.get_widget("phoneTypeSelection")
297 self._phoneButton.connect("clicked", self._on_phone)
298 self._smsEntry = self._widgetTree.get_widget("smsEntry")
299 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
300 self._smsEntrySize = None
304 def add_contact(self, name, contactDetails, messages = (), defaultIndex = -1):
305 contactNumbers = list(self._to_contact_numbers(contactDetails))
306 assert contactNumbers
307 contactIndex = defaultIndex if defaultIndex != -1 else 0
308 contact = contactNumbers, contactIndex, messages
309 self._contacts.append(contact)
311 nameLabel = gtk.Label(name)
312 selector = gtk.Button(contactNumbers[0][1])
313 if len(contactNumbers) == 1:
314 selector.set_sensitive(False)
315 removeContact = gtk.Button(stock="gtk-delete")
317 row.pack_start(nameLabel, True, True)
318 row.pack_start(selector, True, True)
319 row.pack_start(removeContact, False, False)
321 self._targetList.pack_start(row)
322 selector.connect("clicked", self._on_choose_phone_n, row)
323 removeContact.connect("clicked", self._on_remove_phone_n, row)
324 self._update_button_state()
325 self._update_context()
327 parentSize = self._parent.get_size()
328 self._window.resize(parentSize[0], max(parentSize[1]-10, 100))
330 self._window.present()
332 self._smsEntry.grab_focus()
333 dx = self._conversationView.get_allocation().height - self._conversationViewPort.get_allocation().height
335 adjustment = self._scrollWindow.get_vadjustment()
336 adjustment.value = dx
339 del self._contacts[:]
341 for row in list(self._targetList.get_children()):
342 self._targetList.remove(row)
343 self._smsEntry.get_buffer().set_text("")
344 self._update_letter_count()
345 self._update_context()
347 def fullscreen(self):
348 self._window.fullscreen()
350 def unfullscreen(self):
351 self._window.unfullscreen()
353 def _remove_contact(self, contactIndex):
354 del self._contacts[contactIndex]
356 row = list(self._targetList.get_children())[contactIndex]
357 self._targetList.remove(row)
358 self._update_button_state()
359 self._update_context()
361 def _update_letter_count(self):
362 if self._smsEntrySize is None:
363 self._smsEntrySize = self._smsEntry.size_request()
365 self._smsEntry.set_size_request(*self._smsEntrySize)
366 entryLength = self._smsEntry.get_buffer().get_char_count()
368 numTexts, numCharInText = divmod(entryLength, self.MAX_CHAR)
370 self._letterCountLabel.set_text("%s.%s" % (numTexts, numCharInText))
372 self._letterCountLabel.set_text("%s" % (numCharInText, ))
374 self._update_button_state()
376 def _update_context(self):
377 self._messagemodel.clear()
378 if len(self._contacts) == 0:
379 self._messagesView.hide()
380 self._targetList.hide()
381 self._phoneButton.hide()
382 self._phoneButton.set_label("Error: You shouldn't see this")
383 elif len(self._contacts) == 1:
384 contactNumbers, index, messages = self._contacts[0]
386 self._messagesView.show()
387 for message in messages:
389 self._messagemodel.append(row)
390 messagesSelection = self._messagesView.get_selection()
391 messagesSelection.select_path((len(messages)-1, ))
393 self._messagesView.hide()
394 self._targetList.hide()
395 self._phoneButton.show()
396 self._phoneButton.set_label(contactNumbers[index][1])
397 if 1 < len(contactNumbers):
398 self._phoneButton.set_sensitive(True)
400 self._phoneButton.set_sensitive(False)
402 self._messagesView.hide()
403 self._targetList.show()
404 self._phoneButton.hide()
405 self._phoneButton.set_label("Error: You shouldn't see this")
407 def _update_button_state(self):
408 if len(self._contacts) == 0:
409 self._dialButton.set_sensitive(False)
410 self._smsButton.set_sensitive(False)
411 elif len(self._contacts) == 1:
412 entryLength = self._smsEntry.get_buffer().get_char_count()
414 self._dialButton.set_sensitive(True)
415 self._smsButton.set_sensitive(False)
417 self._dialButton.set_sensitive(False)
418 self._smsButton.set_sensitive(True)
420 self._dialButton.set_sensitive(False)
421 self._smsButton.set_sensitive(True)
423 def _to_contact_numbers(self, contactDetails):
424 for phoneType, phoneNumber in contactDetails:
425 display = " - ".join((make_pretty(phoneNumber), phoneType))
426 yield (phoneNumber, display)
428 def _pseudo_destroy(self):
432 def _request_number(self, contactIndex):
433 contactNumbers, index, messages = self._contacts[contactIndex]
434 assert 0 <= index, "%r" % index
436 index = hildonize.touch_selector(
439 (description for (number, description) in contactNumbers),
442 self._contacts[contactIndex] = contactNumbers, index, messages
444 def send_sms(self, numbers, message):
445 raise NotImplementedError()
447 def dial(self, number):
448 raise NotImplementedError()
450 def _on_phone(self, *args):
452 assert len(self._contacts) == 1
453 self._request_number(0)
455 contactNumbers, numberIndex, messages = self._contacts[0]
456 self._phoneButton.set_label(contactNumbers[numberIndex][1])
457 row = list(self._targetList.get_children())[0]
458 phoneButton = list(row.get_children())[1]
459 phoneButton.set_label(contactNumbers[numberIndex][1])
461 self._errorDisplay.push_exception()
463 def _on_choose_phone_n(self, button, row):
465 assert 1 < len(self._contacts)
466 targetList = list(self._targetList.get_children())
467 index = targetList.index(row)
468 self._request_number(index)
470 contactNumbers, numberIndex, messages = self._contacts[0]
471 phoneButton = list(row.get_children())[1]
472 phoneButton.set_label(contactNumbers[numberIndex][1])
474 self._errorDisplay.push_exception()
476 def _on_remove_phone_n(self, button, row):
478 assert 1 < len(self._contacts)
479 targetList = list(self._targetList.get_children())
480 index = targetList.index(row)
482 del self._contacts[index]
483 self._targetList.remove(row)
484 self._update_context()
485 self._update_button_state()
487 self._errorDisplay.push_exception()
489 def _on_entry_changed(self, *args):
491 self._update_letter_count()
493 self._errorDisplay.push_exception()
495 def _on_send(self, *args):
497 assert 0 < len(self._contacts), "%r" % self._contacts
499 make_ugly(contact[0][contact[1]][0])
500 for contact in self._contacts
503 entryBuffer = self._smsEntry.get_buffer()
504 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
505 enteredMessage = enteredMessage.strip()
506 assert enteredMessage
507 self.send_sms(phoneNumbers, enteredMessage)
508 self._pseudo_destroy()
510 self._errorDisplay.push_exception()
512 def _on_dial(self, *args):
514 assert len(self._contacts) == 1, "%r" % self._contacts
515 contact = self._contacts[0]
516 contactNumber = contact[0][contact[1]][0]
517 phoneNumber = make_ugly(contactNumber)
518 self.dial(phoneNumber)
519 self._pseudo_destroy()
521 self._errorDisplay.push_exception()
523 def _on_delete(self, *args):
525 self._window.emit_stop_by_name("delete-event")
526 if hildonize.IS_FREMANTLE_SUPPORTED:
529 self._pseudo_destroy()
531 self._errorDisplay.push_exception()
534 def _on_window_state_change(self, widget, event, *args):
536 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
537 self._isFullScreen = True
539 self._isFullScreen = False
541 self._errorDisplay.push_exception()
543 def _on_key_press(self, widget, event):
544 RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
547 event.keyval == gtk.keysyms.F6 or
548 event.keyval in RETURN_TYPES and event.get_state() & gtk.gdk.CONTROL_MASK
550 if self._isFullScreen:
551 self._window.unfullscreen()
553 self._window.fullscreen()
554 elif event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
557 for messagePart in self._messagemodel
559 self._clipboard.set_text(str(message))
561 event.keyval == gtk.keysyms.h and
562 event.get_state() & gtk.gdk.CONTROL_MASK
566 event.keyval == gtk.keysyms.w and
567 event.get_state() & gtk.gdk.CONTROL_MASK
569 self._pseudo_destroy()
571 event.keyval == gtk.keysyms.q and
572 event.get_state() & gtk.gdk.CONTROL_MASK
574 self._parent.destroy()
576 self._errorDisplay.push_exception()
579 class Dialpad(object):
581 def __init__(self, widgetTree, errorDisplay):
582 self._clipboard = gtk.clipboard_get()
583 self._errorDisplay = errorDisplay
585 self._numberdisplay = widgetTree.get_widget("numberdisplay")
586 self._okButton = widgetTree.get_widget("dialpadOk")
587 self._backButton = widgetTree.get_widget("back")
588 self._plusButton = widgetTree.get_widget("plus")
589 self._phonenumber = ""
590 self._prettynumber = ""
593 "on_digit_clicked": self._on_digit_clicked,
595 widgetTree.signal_autoconnect(callbackMapping)
596 self._okButton.connect("clicked", self._on_ok_clicked)
597 self._plusButton.connect("clicked", self._on_plus)
599 self._originalLabel = self._backButton.get_label()
600 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
601 self._backTapHandler.on_tap = self._on_backspace
602 self._backTapHandler.on_hold = self._on_clearall
603 self._backTapHandler.on_holding = self._set_clear_button
604 self._backTapHandler.on_cancel = self._reset_back_button
606 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
607 self._keyPressEventId = 0
610 self._okButton.grab_focus()
611 self._backTapHandler.enable()
612 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
615 self._window.disconnect(self._keyPressEventId)
616 self._keyPressEventId = 0
617 self._reset_back_button()
618 self._backTapHandler.disable()
620 def add_contact(self, *args, **kwds):
622 @note Actual dial function is patched in later
624 raise NotImplementedError("Horrible unknown error has occurred")
626 def get_number(self):
627 return self._phonenumber
629 def set_number(self, number):
631 Set the number to dial
634 self._phonenumber = make_ugly(number)
635 self._prettynumber = make_pretty(self._phonenumber)
636 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
638 self._errorDisplay.push_exception()
647 def load_settings(self, config, section):
650 def save_settings(self, config, section):
652 @note Thread Agnostic
656 def _on_key_press(self, widget, event):
658 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
659 contents = self._clipboard.wait_for_text()
660 if contents is not None:
661 self.set_number(contents)
663 self._errorDisplay.push_exception()
665 def _on_ok_clicked(self, widget):
667 phoneNumber = self.get_number()
670 [("Dialer", phoneNumber)], ()
674 self._errorDisplay.push_exception()
676 def _on_digit_clicked(self, widget):
678 self.set_number(self._phonenumber + widget.get_name()[-1])
680 self._errorDisplay.push_exception()
682 def _on_plus(self, *args):
684 self.set_number(self._phonenumber + "+")
686 self._errorDisplay.push_exception()
688 def _on_backspace(self, taps):
690 self.set_number(self._phonenumber[:-taps])
691 self._reset_back_button()
693 self._errorDisplay.push_exception()
695 def _on_clearall(self, taps):
698 self._reset_back_button()
700 self._errorDisplay.push_exception()
703 def _set_clear_button(self):
705 self._backButton.set_label("gtk-clear")
707 self._errorDisplay.push_exception()
709 def _reset_back_button(self):
711 self._backButton.set_label(self._originalLabel)
713 self._errorDisplay.push_exception()
716 class AccountInfo(object):
718 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
719 self._errorDisplay = errorDisplay
720 self._backend = backend
721 self._isPopulated = False
722 self._alarmHandler = alarmHandler
723 self._notifyOnMissed = False
724 self._notifyOnVoicemail = False
725 self._notifyOnSms = False
727 self._callbackList = []
728 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
729 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
730 self._onCallbackSelectChangedId = 0
732 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
733 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
734 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
735 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
736 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
737 self._onNotifyToggled = 0
738 self._onMinutesChanged = 0
739 self._onMissedToggled = 0
740 self._onVoicemailToggled = 0
741 self._onSmsToggled = 0
742 self._applyAlarmTimeoutId = None
744 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
745 self._callbackNumber = ""
748 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
750 self._accountViewNumberDisplay.set_use_markup(True)
751 self.set_account_number("")
753 del self._callbackList[:]
754 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
755 self._set_callback_label("")
757 if self._alarmHandler is not None:
758 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
759 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
760 self._missedCheckbox.set_active(self._notifyOnMissed)
761 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
762 self._smsCheckbox.set_active(self._notifyOnSms)
764 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
765 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
766 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
767 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
768 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
770 self._notifyCheckbox.set_sensitive(False)
771 self._minutesEntryButton.set_sensitive(False)
772 self._missedCheckbox.set_sensitive(False)
773 self._voicemailCheckbox.set_sensitive(False)
774 self._smsCheckbox.set_sensitive(False)
776 self.update(force=True)
779 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
780 self._onCallbackSelectChangedId = 0
781 self._set_callback_label("")
783 if self._alarmHandler is not None:
784 self._notifyCheckbox.disconnect(self._onNotifyToggled)
785 self._minutesEntryButton.disconnect(self._onMinutesChanged)
786 self._missedCheckbox.disconnect(self._onNotifyToggled)
787 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
788 self._smsCheckbox.disconnect(self._onNotifyToggled)
789 self._onNotifyToggled = 0
790 self._onMinutesChanged = 0
791 self._onMissedToggled = 0
792 self._onVoicemailToggled = 0
793 self._onSmsToggled = 0
795 self._notifyCheckbox.set_sensitive(True)
796 self._minutesEntryButton.set_sensitive(True)
797 self._missedCheckbox.set_sensitive(True)
798 self._voicemailCheckbox.set_sensitive(True)
799 self._smsCheckbox.set_sensitive(True)
802 del self._callbackList[:]
804 def set_account_number(self, number):
806 Displays current account number
808 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
810 def update(self, force = False):
811 if not force and self._isPopulated:
813 self._populate_callback_combo()
814 self.set_account_number(self._backend.get_account_number())
818 self._set_callback_label("")
819 self.set_account_number("")
820 self._isPopulated = False
822 def save_everything(self):
823 raise NotImplementedError
827 return "Account Info"
829 def load_settings(self, config, section):
830 self._callbackNumber = make_ugly(config.get(section, "callback"))
831 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
832 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
833 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
835 def save_settings(self, config, section):
837 @note Thread Agnostic
839 config.set(section, "callback", self._callbackNumber)
840 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
841 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
842 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
844 def _populate_callback_combo(self):
845 self._isPopulated = True
846 del self._callbackList[:]
848 callbackNumbers = self._backend.get_callback_numbers()
850 self._errorDisplay.push_exception()
851 self._isPopulated = False
854 if len(callbackNumbers) == 0:
855 callbackNumbers = {"": "No callback numbers available"}
857 for number, description in callbackNumbers.iteritems():
858 self._callbackList.append((make_pretty(number), description))
860 self._set_callback_number(self._callbackNumber)
862 def _set_callback_number(self, number):
864 if not self._backend.is_valid_syntax(number) and 0 < len(number):
865 self._errorDisplay.push_message("%s is not a valid callback number" % number)
866 elif number == self._backend.get_callback_number() and 0 < len(number):
867 _moduleLogger.warning(
868 "Callback number already is %s" % (
869 self._backend.get_callback_number(),
872 self._set_callback_label(number)
874 if number.startswith("1747"): number = "+" + number
875 self._backend.set_callback_number(number)
876 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
877 make_pretty(number), make_pretty(self._backend.get_callback_number())
879 self._callbackNumber = make_ugly(number)
880 self._set_callback_label(number)
882 "Callback number set to %s" % (
883 self._backend.get_callback_number(),
887 self._errorDisplay.push_exception()
889 def _set_callback_label(self, uglyNumber):
890 prettyNumber = make_pretty(uglyNumber)
891 if len(prettyNumber) == 0:
892 prettyNumber = "No Callback Number"
893 self._callbackSelectButton.set_label(prettyNumber)
895 def _update_alarm_settings(self, recurrence):
897 isEnabled = self._notifyCheckbox.get_active()
898 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
899 self._alarmHandler.apply_settings(isEnabled, recurrence)
901 self.save_everything()
902 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
903 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
905 def _on_callbackentry_clicked(self, *args):
907 actualSelection = make_pretty(self._callbackNumber)
910 (number, "%s (%s)" % (number, description))
911 for (number, description) in self._callbackList
913 defaultSelection = userOptions.get(actualSelection, actualSelection)
915 userSelection = hildonize.touch_selector_entry(
918 list(userOptions.itervalues()),
921 reversedUserOptions = dict(
922 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
924 selectedNumber = reversedUserOptions.get(userSelection, userSelection)
926 number = make_ugly(selectedNumber)
927 self._set_callback_number(number)
928 except RuntimeError, e:
929 _moduleLogger.exception("%s" % str(e))
931 self._errorDisplay.push_exception()
933 def _on_notify_toggled(self, *args):
935 if self._applyAlarmTimeoutId is not None:
936 gobject.source_remove(self._applyAlarmTimeoutId)
937 self._applyAlarmTimeoutId = None
938 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
940 self._errorDisplay.push_exception()
942 def _on_minutes_clicked(self, *args):
943 recurrenceChoices = [
959 actualSelection = self._alarmHandler.recurrence
961 closestSelectionIndex = 0
962 for i, possible in enumerate(recurrenceChoices):
963 if possible[0] <= actualSelection:
964 closestSelectionIndex = i
965 recurrenceIndex = hildonize.touch_selector(
968 (("%s" % m[1]) for m in recurrenceChoices),
969 closestSelectionIndex,
971 recurrence = recurrenceChoices[recurrenceIndex][0]
973 self._update_alarm_settings(recurrence)
974 except RuntimeError, e:
975 _moduleLogger.exception("%s" % str(e))
977 self._errorDisplay.push_exception()
979 def _on_apply_timeout(self, *args):
981 self._applyAlarmTimeoutId = None
983 self._update_alarm_settings(self._alarmHandler.recurrence)
985 self._errorDisplay.push_exception()
988 def _on_missed_toggled(self, *args):
990 self._notifyOnMissed = self._missedCheckbox.get_active()
991 self.save_everything()
993 self._errorDisplay.push_exception()
995 def _on_voicemail_toggled(self, *args):
997 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
998 self.save_everything()
1000 self._errorDisplay.push_exception()
1002 def _on_sms_toggled(self, *args):
1004 self._notifyOnSms = self._smsCheckbox.get_active()
1005 self.save_everything()
1006 except Exception, e:
1007 self._errorDisplay.push_exception()
1010 class CallHistoryView(object):
1018 HISTORY_ITEM_TYPES = ["All", "Received", "Missed", "Placed"]
1020 def __init__(self, widgetTree, backend, errorDisplay):
1021 self._errorDisplay = errorDisplay
1022 self._backend = backend
1024 self._isPopulated = False
1025 self._historymodel = gtk.ListStore(
1026 gobject.TYPE_STRING, # number
1027 gobject.TYPE_STRING, # date
1028 gobject.TYPE_STRING, # action
1029 gobject.TYPE_STRING, # from
1030 gobject.TYPE_STRING, # from id
1032 self._historymodelfiltered = self._historymodel.filter_new()
1033 self._historymodelfiltered.set_visible_func(self._is_history_visible)
1034 self._historyview = widgetTree.get_widget("historyview")
1035 self._historyviewselection = None
1036 self._onRecentviewRowActivatedId = 0
1038 textrenderer = gtk.CellRendererText()
1039 textrenderer.set_property("yalign", 0)
1040 self._dateColumn = gtk.TreeViewColumn("Date")
1041 self._dateColumn.pack_start(textrenderer, expand=True)
1042 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
1044 textrenderer = gtk.CellRendererText()
1045 textrenderer.set_property("yalign", 0)
1046 self._actionColumn = gtk.TreeViewColumn("Action")
1047 self._actionColumn.pack_start(textrenderer, expand=True)
1048 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
1050 textrenderer = gtk.CellRendererText()
1051 textrenderer.set_property("yalign", 0)
1052 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
1053 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
1054 self._numberColumn = gtk.TreeViewColumn("Number")
1055 self._numberColumn.pack_start(textrenderer, expand=True)
1056 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
1058 textrenderer = gtk.CellRendererText()
1059 textrenderer.set_property("yalign", 0)
1060 hildonize.set_cell_thumb_selectable(textrenderer)
1061 self._nameColumn = gtk.TreeViewColumn("From")
1062 self._nameColumn.pack_start(textrenderer, expand=True)
1063 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
1064 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1066 self._window = gtk_toolbox.find_parent_window(self._historyview)
1068 self._historyFilterSelector = widgetTree.get_widget("historyFilterSelector")
1069 self._historyFilterSelector.connect("clicked", self._on_history_filter_clicked)
1070 self._selectedFilter = "All"
1072 self._updateSink = gtk_toolbox.threaded_stage(
1074 self._idly_populate_historyview,
1075 gtk_toolbox.null_sink(),
1080 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1081 self._historyFilterSelector.set_label(self._selectedFilter)
1083 self._historyview.set_model(self._historymodelfiltered)
1084 self._historyview.set_fixed_height_mode(False)
1086 self._historyview.append_column(self._dateColumn)
1087 self._historyview.append_column(self._actionColumn)
1088 self._historyview.append_column(self._numberColumn)
1089 self._historyview.append_column(self._nameColumn)
1090 self._historyviewselection = self._historyview.get_selection()
1091 self._historyviewselection.set_mode(gtk.SELECTION_SINGLE)
1093 self._onRecentviewRowActivatedId = self._historyview.connect("row-activated", self._on_historyview_row_activated)
1096 self._historyview.disconnect(self._onRecentviewRowActivatedId)
1100 self._historyview.remove_column(self._dateColumn)
1101 self._historyview.remove_column(self._actionColumn)
1102 self._historyview.remove_column(self._nameColumn)
1103 self._historyview.remove_column(self._numberColumn)
1104 self._historyview.set_model(None)
1106 def add_contact(self, *args, **kwds):
1108 @note Actual dial function is patched in later
1110 raise NotImplementedError("Horrible unknown error has occurred")
1112 def update(self, force = False):
1113 if not force and self._isPopulated:
1115 self._updateSink.send(())
1119 self._isPopulated = False
1120 self._historymodel.clear()
1124 return "Recent Calls"
1126 def load_settings(self, config, sectionName):
1128 self._selectedFilter = config.get(sectionName, "filter")
1129 if self._selectedFilter not in self.HISTORY_ITEM_TYPES:
1130 self._messageType = self.HISTORY_ITEM_TYPES[0]
1131 except ConfigParser.NoOptionError:
1134 def save_settings(self, config, sectionName):
1136 @note Thread Agnostic
1138 config.set(sectionName, "filter", self._selectedFilter)
1140 def _is_history_visible(self, model, iter):
1142 action = model.get_value(iter, self.ACTION_IDX)
1144 return False # this seems weird but oh well
1146 if self._selectedFilter in [action, "All"]:
1150 except Exception, e:
1151 self._errorDisplay.push_exception()
1153 def _idly_populate_historyview(self):
1154 with gtk_toolbox.gtk_lock():
1155 banner = hildonize.show_busy_banner_start(self._window, "Loading Call History")
1157 self._historymodel.clear()
1158 self._isPopulated = True
1161 historyItems = self._backend.get_recent()
1162 except Exception, e:
1163 self._errorDisplay.push_exception_with_lock()
1164 self._isPopulated = False
1168 gv_backend.decorate_recent(data)
1169 for data in gv_backend.sort_messages(historyItems)
1172 for contactId, personName, phoneNumber, date, action in historyItems:
1174 personName = "Unknown"
1175 date = abbrev_relative_date(date)
1176 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1177 prettyNumber = make_pretty(prettyNumber)
1178 item = (prettyNumber, date, action.capitalize(), personName, contactId)
1179 with gtk_toolbox.gtk_lock():
1180 self._historymodel.append(item)
1181 except Exception, e:
1182 self._errorDisplay.push_exception_with_lock()
1184 with gtk_toolbox.gtk_lock():
1185 hildonize.show_busy_banner_end(banner)
1189 def _on_history_filter_clicked(self, *args, **kwds):
1191 selectedComboIndex = self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
1194 newSelectedComboIndex = hildonize.touch_selector(
1197 self.HISTORY_ITEM_TYPES,
1200 except RuntimeError:
1203 option = self.HISTORY_ITEM_TYPES[newSelectedComboIndex]
1204 self._selectedFilter = option
1205 self._historyFilterSelector.set_label(self._selectedFilter)
1206 self._historymodelfiltered.refilter()
1207 except Exception, e:
1208 self._errorDisplay.push_exception()
1210 def _history_summary(self, expectedNumber):
1211 for number, action, date, whoFrom, whoFromId in self._historymodel:
1212 if expectedNumber is not None and expectedNumber == number:
1213 yield "%s <i>(%s)</i> - %s %s" % (number, whoFrom, date, action)
1215 def _on_historyview_row_activated(self, treeview, path, view_column):
1217 childPath = self._historymodelfiltered.convert_path_to_child_path(path)
1218 itr = self._historymodel.get_iter(childPath)
1222 prettyNumber = self._historymodel.get_value(itr, self.NUMBER_IDX)
1223 number = make_ugly(prettyNumber)
1224 description = list(self._history_summary(prettyNumber))
1225 contactName = self._historymodel.get_value(itr, self.FROM_IDX)
1226 contactId = self._historymodel.get_value(itr, self.FROM_ID_IDX)
1227 contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1231 contactPhoneNumbers,
1232 messages = description,
1233 defaultIndex = defaultIndex,
1235 self._historyviewselection.unselect_all()
1236 except Exception, e:
1237 self._errorDisplay.push_exception()
1240 class MessagesView(object):
1248 MESSAGE_DATA_IDX = 6
1250 NO_MESSAGES = "None"
1251 VOICEMAIL_MESSAGES = "Voicemail"
1252 TEXT_MESSAGES = "SMS"
1253 ALL_TYPES = "All Messages"
1254 MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
1256 UNREAD_STATUS = "Unread"
1257 UNARCHIVED_STATUS = "Inbox"
1259 MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
1261 def __init__(self, widgetTree, backend, errorDisplay):
1262 self._errorDisplay = errorDisplay
1263 self._backend = backend
1265 self._isPopulated = False
1266 self._messagemodel = gtk.ListStore(
1267 gobject.TYPE_STRING, # number
1268 gobject.TYPE_STRING, # date
1269 gobject.TYPE_STRING, # header
1270 gobject.TYPE_STRING, # message
1272 gobject.TYPE_STRING, # from id
1273 object, # message data
1275 self._messagemodelfiltered = self._messagemodel.filter_new()
1276 self._messagemodelfiltered.set_visible_func(self._is_message_visible)
1277 self._messageview = widgetTree.get_widget("messages_view")
1278 self._messageviewselection = None
1279 self._onMessageviewRowActivatedId = 0
1281 self._messageRenderer = gtk.CellRendererText()
1282 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1283 self._messageRenderer.set_property("wrap-width", 500)
1284 self._messageColumn = gtk.TreeViewColumn("Messages")
1285 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1286 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1287 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1289 self._window = gtk_toolbox.find_parent_window(self._messageview)
1291 self._messageTypeButton = widgetTree.get_widget("messageTypeButton")
1292 self._onMessageTypeClickedId = 0
1293 self._messageType = self.ALL_TYPES
1294 self._messageStatusButton = widgetTree.get_widget("messageStatusButton")
1295 self._onMessageStatusClickedId = 0
1296 self._messageStatus = self.ALL_STATUS
1298 self._updateSink = gtk_toolbox.threaded_stage(
1300 self._idly_populate_messageview,
1301 gtk_toolbox.null_sink(),
1306 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1307 self._messageview.set_model(self._messagemodelfiltered)
1308 self._messageview.set_headers_visible(False)
1309 self._messageview.set_fixed_height_mode(False)
1311 self._messageview.append_column(self._messageColumn)
1312 self._messageviewselection = self._messageview.get_selection()
1313 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1315 self._messageTypeButton.set_label(self._messageType)
1316 self._messageStatusButton.set_label(self._messageStatus)
1318 self._onMessageviewRowActivatedId = self._messageview.connect(
1319 "row-activated", self._on_messageview_row_activated
1321 self._onMessageTypeClickedId = self._messageTypeButton.connect(
1322 "clicked", self._on_message_type_clicked
1324 self._onMessageStatusClickedId = self._messageStatusButton.connect(
1325 "clicked", self._on_message_status_clicked
1329 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1330 self._messageTypeButton.disconnect(self._onMessageTypeClickedId)
1331 self._messageStatusButton.disconnect(self._onMessageStatusClickedId)
1335 self._messageview.remove_column(self._messageColumn)
1336 self._messageview.set_model(None)
1338 def add_contact(self, *args, **kwds):
1340 @note Actual dial function is patched in later
1342 raise NotImplementedError("Horrible unknown error has occurred")
1344 def update(self, force = False):
1345 if not force and self._isPopulated:
1347 self._updateSink.send(())
1351 self._isPopulated = False
1352 self._messagemodel.clear()
1358 def load_settings(self, config, sectionName):
1360 self._messageType = config.get(sectionName, "type")
1361 if self._messageType not in self.MESSAGE_TYPES:
1362 self._messageType = self.ALL_TYPES
1363 self._messageStatus = config.get(sectionName, "status")
1364 if self._messageStatus not in self.MESSAGE_STATUSES:
1365 self._messageStatus = self.ALL_STATUS
1366 except ConfigParser.NoOptionError:
1369 def save_settings(self, config, sectionName):
1371 @note Thread Agnostic
1373 config.set(sectionName, "status", self._messageStatus)
1374 config.set(sectionName, "type", self._messageType)
1376 def _is_message_visible(self, model, iter):
1378 message = model.get_value(iter, self.MESSAGE_DATA_IDX)
1380 return False # this seems weird but oh well
1381 return self._filter_messages(message, self._messageType, self._messageStatus)
1382 except Exception, e:
1383 self._errorDisplay.push_exception()
1386 def _filter_messages(cls, message, type, status):
1387 if type == cls.ALL_TYPES:
1390 messageType = message["type"]
1391 isType = messageType == type
1393 if status == cls.ALL_STATUS:
1396 isUnarchived = not message["isArchived"]
1397 isUnread = not message["isRead"]
1398 if status == cls.UNREAD_STATUS:
1399 isStatus = isUnarchived and isUnread
1400 elif status == cls.UNARCHIVED_STATUS:
1401 isStatus = isUnarchived
1403 assert "Status %s is bad for %r" % (status, message)
1405 return isType and isStatus
1407 _MIN_MESSAGES_SHOWN = 4
1409 def _idly_populate_messageview(self):
1410 with gtk_toolbox.gtk_lock():
1411 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1413 self._messagemodel.clear()
1414 self._isPopulated = True
1416 if self._messageType == self.NO_MESSAGES:
1420 messageItems = self._backend.get_messages()
1421 except Exception, e:
1422 self._errorDisplay.push_exception_with_lock()
1423 self._isPopulated = False
1427 (gv_backend.decorate_message(message), message)
1428 for message in gv_backend.sort_messages(messageItems)
1431 for (contactId, header, number, relativeDate, messages), messageData in messageItems:
1432 prettyNumber = number[2:] if number.startswith("+1") else number
1433 prettyNumber = make_pretty(prettyNumber)
1435 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1436 expandedMessages = [firstMessage]
1437 expandedMessages.extend(messages)
1438 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1439 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1440 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1441 collapsedMessages = [firstMessage, secondMessage]
1442 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1444 collapsedMessages = expandedMessages
1445 #collapsedMessages = _collapse_message(collapsedMessages, 60, self._MIN_MESSAGES_SHOWN)
1447 number = make_ugly(number)
1449 row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId, messageData
1450 with gtk_toolbox.gtk_lock():
1451 self._messagemodel.append(row)
1452 except Exception, e:
1453 self._errorDisplay.push_exception_with_lock()
1455 with gtk_toolbox.gtk_lock():
1456 hildonize.show_busy_banner_end(banner)
1457 self._messagemodelfiltered.refilter()
1461 def _on_messageview_row_activated(self, treeview, path, view_column):
1463 childPath = self._messagemodelfiltered.convert_path_to_child_path(path)
1464 itr = self._messagemodel.get_iter(childPath)
1468 number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX))
1469 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1471 contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX)
1472 header = self._messagemodel.get_value(itr, self.HEADER_IDX)
1473 contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1477 contactPhoneNumbers,
1478 messages = description,
1479 defaultIndex = defaultIndex,
1481 self._messageviewselection.unselect_all()
1482 except Exception, e:
1483 self._errorDisplay.push_exception()
1485 def _on_message_type_clicked(self, *args, **kwds):
1487 selectedIndex = self.MESSAGE_TYPES.index(self._messageType)
1490 newSelectedIndex = hildonize.touch_selector(
1496 except RuntimeError:
1499 if selectedIndex != newSelectedIndex:
1500 self._messageType = self.MESSAGE_TYPES[newSelectedIndex]
1501 self._messageTypeButton.set_label(self._messageType)
1502 self._messagemodelfiltered.refilter()
1503 except Exception, e:
1504 self._errorDisplay.push_exception()
1506 def _on_message_status_clicked(self, *args, **kwds):
1508 selectedIndex = self.MESSAGE_STATUSES.index(self._messageStatus)
1511 newSelectedIndex = hildonize.touch_selector(
1514 self.MESSAGE_STATUSES,
1517 except RuntimeError:
1520 if selectedIndex != newSelectedIndex:
1521 self._messageStatus = self.MESSAGE_STATUSES[newSelectedIndex]
1522 self._messageStatusButton.set_label(self._messageStatus)
1523 self._messagemodelfiltered.refilter()
1524 except Exception, e:
1525 self._errorDisplay.push_exception()
1528 class ContactsView(object):
1530 CONTACT_TYPE_IDX = 0
1531 CONTACT_NAME_IDX = 1
1534 def __init__(self, widgetTree, backend, errorDisplay):
1535 self._errorDisplay = errorDisplay
1536 self._backend = backend
1538 self._addressBook = None
1539 self._selectedComboIndex = 0
1540 self._addressBookFactories = [null_backend.NullAddressBook()]
1542 self._booksList = []
1543 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1545 self._isPopulated = False
1546 self._contactsmodel = gtk.ListStore(
1547 gobject.TYPE_STRING, # Contact Type
1548 gobject.TYPE_STRING, # Contact Name
1549 gobject.TYPE_STRING, # Contact ID
1551 self._contactsviewselection = None
1552 self._contactsview = widgetTree.get_widget("contactsview")
1554 self._contactColumn = gtk.TreeViewColumn("Contact")
1555 displayContactSource = False
1556 if displayContactSource:
1557 textrenderer = gtk.CellRendererText()
1558 self._contactColumn.pack_start(textrenderer, expand=False)
1559 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_TYPE_IDX)
1560 textrenderer = gtk.CellRendererText()
1561 hildonize.set_cell_thumb_selectable(textrenderer)
1562 self._contactColumn.pack_start(textrenderer, expand=True)
1563 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_NAME_IDX)
1564 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1565 self._contactColumn.set_sort_column_id(1)
1566 self._contactColumn.set_visible(True)
1568 self._onContactsviewRowActivatedId = 0
1569 self._onAddressbookButtonChangedId = 0
1570 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1572 self._updateSink = gtk_toolbox.threaded_stage(
1574 self._idly_populate_contactsview,
1575 gtk_toolbox.null_sink(),
1580 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1582 self._contactsview.set_model(self._contactsmodel)
1583 self._contactsview.set_fixed_height_mode(False)
1584 self._contactsview.append_column(self._contactColumn)
1585 self._contactsviewselection = self._contactsview.get_selection()
1586 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1588 del self._booksList[:]
1589 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1590 if factoryName and bookName:
1591 entryName = "%s: %s" % (factoryName, bookName)
1593 entryName = factoryName
1595 entryName = bookName
1597 entryName = "Bad name (%d)" % factoryId
1598 row = (str(factoryId), bookId, entryName)
1599 self._booksList.append(row)
1601 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1602 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1604 if len(self._booksList) <= self._selectedComboIndex:
1605 self._selectedComboIndex = 0
1606 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1608 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1609 selectedBookId = self._booksList[self._selectedComboIndex][1]
1610 self.open_addressbook(selectedFactoryId, selectedBookId)
1613 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1614 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1618 self._bookSelectionButton.set_label("")
1619 self._contactsview.set_model(None)
1620 self._contactsview.remove_column(self._contactColumn)
1622 def add_contact(self, *args, **kwds):
1624 @note Actual dial function is patched in later
1626 raise NotImplementedError("Horrible unknown error has occurred")
1628 def get_addressbooks(self):
1630 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1632 for i, factory in enumerate(self._addressBookFactories):
1633 for bookFactory, bookId, bookName in factory.get_addressbooks():
1634 yield (str(i), bookId), (factory.factory_name(), bookName)
1636 def open_addressbook(self, bookFactoryId, bookId):
1637 bookFactoryIndex = int(bookFactoryId)
1638 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1639 self._addressBook = addressBook
1641 def update(self, force = False):
1642 if not force and self._isPopulated:
1644 self._updateSink.send(())
1648 self._isPopulated = False
1649 self._contactsmodel.clear()
1650 for factory in self._addressBookFactories:
1651 factory.clear_caches()
1652 self._addressBook.clear_caches()
1654 def append(self, book):
1655 self._addressBookFactories.append(book)
1657 def extend(self, books):
1658 self._addressBookFactories.extend(books)
1664 def load_settings(self, config, sectionName):
1666 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1667 except ConfigParser.NoOptionError:
1668 self._selectedComboIndex = 0
1670 def save_settings(self, config, sectionName):
1671 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1673 def _idly_populate_contactsview(self):
1674 with gtk_toolbox.gtk_lock():
1675 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1678 while addressBook is not self._addressBook:
1679 addressBook = self._addressBook
1680 with gtk_toolbox.gtk_lock():
1681 self._contactsview.set_model(None)
1685 contacts = addressBook.get_contacts()
1686 except Exception, e:
1688 self._isPopulated = False
1689 self._errorDisplay.push_exception_with_lock()
1690 for contactId, contactName in contacts:
1691 contactType = addressBook.contact_source_short_name(contactId)
1692 row = contactType, contactName, contactId
1693 self._contactsmodel.append(row)
1695 with gtk_toolbox.gtk_lock():
1696 self._contactsview.set_model(self._contactsmodel)
1698 self._isPopulated = True
1699 except Exception, e:
1700 self._errorDisplay.push_exception_with_lock()
1702 with gtk_toolbox.gtk_lock():
1703 hildonize.show_busy_banner_end(banner)
1706 def _on_addressbook_button_changed(self, *args, **kwds):
1709 newSelectedComboIndex = hildonize.touch_selector(
1712 (("%s" % m[2]) for m in self._booksList),
1713 self._selectedComboIndex,
1715 except RuntimeError:
1718 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1719 selectedBookId = self._booksList[newSelectedComboIndex][1]
1721 oldAddressbook = self._addressBook
1722 self.open_addressbook(selectedFactoryId, selectedBookId)
1723 forceUpdate = True if oldAddressbook is not self._addressBook else False
1724 self.update(force=forceUpdate)
1726 self._selectedComboIndex = newSelectedComboIndex
1727 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1728 except Exception, e:
1729 self._errorDisplay.push_exception()
1731 def _on_contactsview_row_activated(self, treeview, path, view_column):
1733 itr = self._contactsmodel.get_iter(path)
1737 contactId = self._contactsmodel.get_value(itr, self.CONTACT_ID_IDX)
1738 contactName = self._contactsmodel.get_value(itr, self.CONTACT_NAME_IDX)
1740 contactDetails = self._addressBook.get_contact_details(contactId)
1741 except Exception, e:
1743 self._errorDisplay.push_exception()
1744 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1746 if len(contactPhoneNumbers) == 0:
1751 contactPhoneNumbers,
1752 messages = (contactName, ),
1754 self._contactsviewselection.unselect_all()
1755 except Exception, e:
1756 self._errorDisplay.push_exception()