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)
69 if uglynumber.startswith("+"):
71 elif uglynumber.startswith("1"):
72 uglynumber = "+"+uglynumber
73 elif len(uglynumber) == 10:
74 assert uglynumber[0] not in ("+", "1")
75 uglynumber = "+1"+uglynumber
82 def _make_pretty_with_areacode(phonenumber):
83 prettynumber = "(%s)" % (phonenumber[0:3], )
84 if 3 < len(phonenumber):
85 prettynumber += " %s" % (phonenumber[3:6], )
86 if 6 < len(phonenumber):
87 prettynumber += "-%s" % (phonenumber[6:], )
91 def _make_pretty_local(phonenumber):
92 prettynumber = "%s" % (phonenumber[0:3], )
93 if 3 < len(phonenumber):
94 prettynumber += "-%s" % (phonenumber[3:], )
98 def _make_pretty_international(phonenumber):
99 prettynumber = phonenumber
100 if phonenumber.startswith("0"):
101 prettynumber = "+%s " % (phonenumber[0:3], )
102 if 3 < len(phonenumber):
103 prettynumber += _make_pretty_with_areacode(phonenumber[3:])
104 elif phonenumber.startswith("1"):
106 prettynumber += _make_pretty_with_areacode(phonenumber[1:])
110 def make_pretty(phonenumber):
112 Function to take a phone number and return the pretty version
114 if phonenumber begins with 0:
116 if phonenumber begins with 1: ( for gizmo callback numbers )
118 if phonenumber is 13 digits:
120 if phonenumber is 10 digits:
122 >>> make_pretty("12")
124 >>> make_pretty("1234567")
126 >>> make_pretty("2345678901")
128 >>> make_pretty("12345678901")
130 >>> make_pretty("01234567890")
132 >>> make_pretty("+01234567890")
134 >>> make_pretty("+12")
136 >>> make_pretty("+123")
138 >>> make_pretty("+1234")
141 if phonenumber is None or phonenumber is "":
144 phonenumber = normalize_number(phonenumber)
146 if phonenumber[0] == "+":
147 prettynumber = _make_pretty_international(phonenumber[1:])
148 if not prettynumber.startswith("+"):
149 prettynumber = "+"+prettynumber
150 elif 8 < len(phonenumber) and phonenumber[0] in ("0", "1"):
151 prettynumber = _make_pretty_international(phonenumber)
152 elif 7 < len(phonenumber):
153 prettynumber = _make_pretty_with_areacode(phonenumber)
154 elif 3 < len(phonenumber):
155 prettynumber = _make_pretty_local(phonenumber)
157 prettynumber = phonenumber
158 return prettynumber.strip()
161 def abbrev_relative_date(date):
163 >>> abbrev_relative_date("42 hours ago")
165 >>> abbrev_relative_date("2 days ago")
167 >>> abbrev_relative_date("4 weeks ago")
170 parts = date.split(" ")
171 return "%s %s" % (parts[0], parts[1][0])
174 def _collapse_message(messageLines, maxCharsPerLine, maxLines):
177 numLines = len(messageLines)
178 for line in messageLines[0:min(maxLines, numLines)]:
179 linesPerLine = max(1, int(len(line) / maxCharsPerLine))
180 allowedLines = maxLines - lines
181 acceptedLines = min(allowedLines, linesPerLine)
182 acceptedChars = acceptedLines * maxCharsPerLine
184 if acceptedChars < (len(line) + 3):
187 acceptedChars = len(line) # eh, might as well complete the line
189 abbrevMessage = "%s%s" % (line[0:acceptedChars], suffix)
192 lines += acceptedLines
193 if maxLines <= lines:
197 def collapse_message(message, maxCharsPerLine, maxLines):
199 >>> collapse_message("Hello", 60, 2)
201 >>> collapse_message("Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789", 60, 2)
202 'Hello world how are you doing today? 01234567890123456789012...'
203 >>> collapse_message('''Hello world how are you doing today?
204 ... 01234567890123456789
205 ... 01234567890123456789
206 ... 01234567890123456789
207 ... 01234567890123456789''', 60, 2)
208 'Hello world how are you doing today?\n01234567890123456789'
209 >>> collapse_message('''
210 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
211 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
212 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
213 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
214 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
215 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789''', 60, 2)
216 '\nHello world how are you doing today? 01234567890123456789012...'
218 messageLines = message.split("\n")
219 return "\n".join(_collapse_message(messageLines, maxCharsPerLine, maxLines))
222 def _get_contact_numbers(backend, contactId, number):
224 contactPhoneNumbers = list(backend.get_contact_details(contactId))
225 uglyContactNumbers = (
226 make_ugly(contactNumber)
227 for (numberDescription, contactNumber) in contactPhoneNumbers
231 number == contactNumber or
232 number[1:] == contactNumber and number.startswith("1") or
233 number[2:] == contactNumber and number.startswith("+1") or
234 number == contactNumber[1:] and contactNumber.startswith("1") or
235 number == contactNumber[2:] and contactNumber.startswith("+1")
237 for contactNumber in uglyContactNumbers
240 defaultIndex = defaultMatches.index(True)
242 contactPhoneNumbers.append(("Other", number))
243 defaultIndex = len(contactPhoneNumbers)-1
245 "Could not find contact %r's number %s among %r" % (
246 contactId, number, contactPhoneNumbers
250 contactPhoneNumbers = [("Phone", number)]
253 return contactPhoneNumbers, defaultIndex
256 class SmsEntryWindow(object):
260 def __init__(self, widgetTree, parent, app):
261 self._clipboard = gtk.clipboard_get()
262 self._widgetTree = widgetTree
263 self._parent = parent
265 self._isFullScreen = False
267 self._window = self._widgetTree.get_widget("smsWindow")
268 self._window = hildonize.hildonize_window(self._app, self._window)
269 self._window.set_title("SMS")
270 self._window.connect("delete-event", self._on_delete)
271 self._window.connect("key-press-event", self._on_key_press)
272 self._window.connect("window-state-event", self._on_window_state_change)
273 self._widgetTree.get_widget("smsMessagesViewPort").get_parent().show()
275 errorBox = self._widgetTree.get_widget("smsErrorEventBox")
276 errorDescription = self._widgetTree.get_widget("smsErrorDescription")
277 errorClose = self._widgetTree.get_widget("smsErrorClose")
278 self._errorDisplay = gtk_toolbox.ErrorDisplay(errorBox, errorDescription, errorClose)
280 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
281 self._smsButton.connect("clicked", self._on_send)
282 self._dialButton = self._widgetTree.get_widget("dialButton")
283 self._dialButton.connect("clicked", self._on_dial)
285 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
287 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
288 self._messagesView = self._widgetTree.get_widget("smsMessages")
290 textrenderer = gtk.CellRendererText()
291 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
292 textrenderer.set_property("wrap-width", 450)
293 messageColumn = gtk.TreeViewColumn("")
294 messageColumn.pack_start(textrenderer, expand=True)
295 messageColumn.add_attribute(textrenderer, "markup", 0)
296 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
297 self._messagesView.append_column(messageColumn)
298 self._messagesView.set_headers_visible(False)
299 self._messagesView.set_model(self._messagemodel)
300 self._messagesView.set_fixed_height_mode(False)
302 self._conversationView = self._messagesView.get_parent()
303 self._conversationViewPort = self._conversationView.get_parent()
304 self._scrollWindow = self._conversationViewPort.get_parent()
306 self._targetList = self._widgetTree.get_widget("smsTargetList")
307 self._phoneButton = self._widgetTree.get_widget("phoneTypeSelection")
308 self._phoneButton.connect("clicked", self._on_phone)
309 self._smsEntry = self._widgetTree.get_widget("smsEntry")
310 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
311 self._smsEntrySize = None
315 def add_contact(self, name, contactDetails, messages = (), defaultIndex = -1):
316 contactNumbers = list(self._to_contact_numbers(contactDetails))
317 assert contactNumbers, "Contact must have at least one number"
318 contactIndex = defaultIndex if defaultIndex != -1 else 0
319 contact = contactNumbers, contactIndex, messages
320 self._contacts.append(contact)
322 nameLabel = gtk.Label(name)
323 selector = gtk.Button(contactNumbers[0][1])
324 if len(contactNumbers) == 1:
325 selector.set_sensitive(False)
326 removeContact = gtk.Button(stock="gtk-delete")
328 row.pack_start(nameLabel, True, True)
329 row.pack_start(selector, True, True)
330 row.pack_start(removeContact, False, False)
332 self._targetList.pack_start(row)
333 selector.connect("clicked", self._on_choose_phone_n, row)
334 removeContact.connect("clicked", self._on_remove_phone_n, row)
335 self._update_button_state()
336 self._update_context()
338 parentSize = self._parent.get_size()
339 self._window.resize(parentSize[0], max(parentSize[1]-10, 100))
341 self._window.present()
343 self._smsEntry.grab_focus()
344 self._scroll_to_bottom()
347 del self._contacts[:]
349 for row in list(self._targetList.get_children()):
350 self._targetList.remove(row)
351 self._smsEntry.get_buffer().set_text("")
352 self._update_letter_count()
353 self._update_context()
355 def fullscreen(self):
356 self._window.fullscreen()
358 def unfullscreen(self):
359 self._window.unfullscreen()
361 def _remove_contact(self, contactIndex):
362 del self._contacts[contactIndex]
364 row = list(self._targetList.get_children())[contactIndex]
365 self._targetList.remove(row)
366 self._update_button_state()
367 self._update_context()
368 self._scroll_to_bottom()
370 def _scroll_to_bottom(self):
371 dx = self._conversationView.get_allocation().height - self._conversationViewPort.get_allocation().height
373 adjustment = self._scrollWindow.get_vadjustment()
374 adjustment.value = dx
376 def _update_letter_count(self):
377 if self._smsEntrySize is None:
378 self._smsEntrySize = self._smsEntry.size_request()
380 self._smsEntry.set_size_request(*self._smsEntrySize)
381 entryLength = self._smsEntry.get_buffer().get_char_count()
383 numTexts, numCharInText = divmod(entryLength, self.MAX_CHAR)
385 self._letterCountLabel.set_text("%s.%s" % (numTexts, numCharInText))
387 self._letterCountLabel.set_text("%s" % (numCharInText, ))
389 self._update_button_state()
391 def _update_context(self):
392 self._messagemodel.clear()
393 if len(self._contacts) == 0:
394 self._messagesView.hide()
395 self._targetList.hide()
396 self._phoneButton.hide()
397 self._phoneButton.set_label("Error: You shouldn't see this")
398 elif len(self._contacts) == 1:
399 contactNumbers, index, messages = self._contacts[0]
401 self._messagesView.show()
402 for message in messages:
404 self._messagemodel.append(row)
405 messagesSelection = self._messagesView.get_selection()
406 messagesSelection.select_path((len(messages)-1, ))
408 self._messagesView.hide()
409 self._targetList.hide()
410 self._phoneButton.show()
411 self._phoneButton.set_label(contactNumbers[index][1])
412 if 1 < len(contactNumbers):
413 self._phoneButton.set_sensitive(True)
415 self._phoneButton.set_sensitive(False)
417 self._messagesView.hide()
418 self._targetList.show()
419 self._phoneButton.hide()
420 self._phoneButton.set_label("Error: You shouldn't see this")
422 def _update_button_state(self):
423 if len(self._contacts) == 0:
424 self._dialButton.set_sensitive(False)
425 self._smsButton.set_sensitive(False)
426 elif len(self._contacts) == 1:
427 entryLength = self._smsEntry.get_buffer().get_char_count()
429 self._dialButton.set_sensitive(True)
430 self._smsButton.set_sensitive(False)
432 self._dialButton.set_sensitive(False)
433 self._smsButton.set_sensitive(True)
435 self._dialButton.set_sensitive(False)
436 self._smsButton.set_sensitive(True)
438 def _to_contact_numbers(self, contactDetails):
439 for phoneType, phoneNumber in contactDetails:
440 display = " - ".join((make_pretty(phoneNumber), phoneType))
441 yield (phoneNumber, display)
443 def _pseudo_destroy(self):
447 def _request_number(self, contactIndex):
448 contactNumbers, index, messages = self._contacts[contactIndex]
449 assert 0 <= index, "%r" % index
451 index = hildonize.touch_selector(
454 (description for (number, description) in contactNumbers),
457 self._contacts[contactIndex] = contactNumbers, index, messages
459 def send_sms(self, numbers, message):
460 raise NotImplementedError()
462 def dial(self, number):
463 raise NotImplementedError()
465 def _on_phone(self, *args):
467 assert len(self._contacts) == 1, "One and only one contact is required"
468 self._request_number(0)
470 contactNumbers, numberIndex, messages = self._contacts[0]
471 self._phoneButton.set_label(contactNumbers[numberIndex][1])
472 row = list(self._targetList.get_children())[0]
473 phoneButton = list(row.get_children())[1]
474 phoneButton.set_label(contactNumbers[numberIndex][1])
476 self._errorDisplay.push_exception()
478 def _on_choose_phone_n(self, button, row):
480 assert 1 < len(self._contacts), "More than one contact required"
481 targetList = list(self._targetList.get_children())
482 index = targetList.index(row)
483 self._request_number(index)
485 contactNumbers, numberIndex, messages = self._contacts[0]
486 phoneButton = list(row.get_children())[1]
487 phoneButton.set_label(contactNumbers[numberIndex][1])
489 self._errorDisplay.push_exception()
491 def _on_remove_phone_n(self, button, row):
493 assert 1 < len(self._contacts), "More than one contact required"
494 targetList = list(self._targetList.get_children())
495 index = targetList.index(row)
497 del self._contacts[index]
498 self._targetList.remove(row)
499 self._update_context()
500 self._update_button_state()
502 self._errorDisplay.push_exception()
504 def _on_entry_changed(self, *args):
506 self._update_letter_count()
508 self._errorDisplay.push_exception()
510 def _on_send(self, *args):
512 assert 0 < len(self._contacts), "At least one contact required (%r)" % self._contacts
514 make_ugly(contact[0][contact[1]][0])
515 for contact in self._contacts
518 entryBuffer = self._smsEntry.get_buffer()
519 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
520 enteredMessage = enteredMessage.strip()
521 assert enteredMessage, "No message provided"
522 self.send_sms(phoneNumbers, enteredMessage)
523 self._pseudo_destroy()
525 self._errorDisplay.push_exception()
527 def _on_dial(self, *args):
529 assert len(self._contacts) == 1, "One and only one contact allowed (%r)" % self._contacts
530 contact = self._contacts[0]
531 contactNumber = contact[0][contact[1]][0]
532 phoneNumber = make_ugly(contactNumber)
533 self.dial(phoneNumber)
534 self._pseudo_destroy()
536 self._errorDisplay.push_exception()
538 def _on_delete(self, *args):
540 self._window.emit_stop_by_name("delete-event")
541 if hildonize.IS_FREMANTLE_SUPPORTED:
544 self._pseudo_destroy()
546 self._errorDisplay.push_exception()
549 def _on_window_state_change(self, widget, event, *args):
551 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
552 self._isFullScreen = True
554 self._isFullScreen = False
556 self._errorDisplay.push_exception()
558 def _on_key_press(self, widget, event):
559 RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
562 event.keyval == gtk.keysyms.F6 or
563 event.keyval in RETURN_TYPES and event.get_state() & gtk.gdk.CONTROL_MASK
565 if self._isFullScreen:
566 self._window.unfullscreen()
568 self._window.fullscreen()
569 elif event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
572 for messagePart in self._messagemodel
574 self._clipboard.set_text(str(message))
576 event.keyval == gtk.keysyms.h and
577 event.get_state() & gtk.gdk.CONTROL_MASK
581 event.keyval == gtk.keysyms.w and
582 event.get_state() & gtk.gdk.CONTROL_MASK
584 self._pseudo_destroy()
586 event.keyval == gtk.keysyms.q and
587 event.get_state() & gtk.gdk.CONTROL_MASK
589 self._parent.destroy()
591 self._errorDisplay.push_exception()
594 class Dialpad(object):
596 def __init__(self, widgetTree, errorDisplay):
597 self._clipboard = gtk.clipboard_get()
598 self._errorDisplay = errorDisplay
600 self._numberdisplay = widgetTree.get_widget("numberdisplay")
601 self._callButton = widgetTree.get_widget("dialpadCall")
602 self._sendSMSButton = widgetTree.get_widget("dialpadSMS")
603 self._backButton = widgetTree.get_widget("back")
604 self._plusButton = widgetTree.get_widget("plus")
605 self._phonenumber = ""
606 self._prettynumber = ""
609 "on_digit_clicked": self._on_digit_clicked,
611 widgetTree.signal_autoconnect(callbackMapping)
612 self._sendSMSButton.connect("clicked", self._on_sms_clicked)
613 self._callButton.connect("clicked", self._on_call_clicked)
614 self._plusButton.connect("clicked", self._on_plus)
616 self._originalLabel = self._backButton.get_label()
617 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
618 self._backTapHandler.on_tap = self._on_backspace
619 self._backTapHandler.on_hold = self._on_clearall
620 self._backTapHandler.on_holding = self._set_clear_button
621 self._backTapHandler.on_cancel = self._reset_back_button
623 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
624 self._keyPressEventId = 0
627 self._sendSMSButton.grab_focus()
628 self._backTapHandler.enable()
629 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
632 self._window.disconnect(self._keyPressEventId)
633 self._keyPressEventId = 0
634 self._reset_back_button()
635 self._backTapHandler.disable()
637 def add_contact(self, *args, **kwds):
639 @note Actual function is patched in later
641 raise NotImplementedError("Horrible unknown error has occurred")
643 def dial(self, number):
645 @note Actual function is patched in later
647 raise NotImplementedError("Horrible unknown error has occurred")
649 def get_number(self):
650 return self._phonenumber
652 def set_number(self, number):
654 Set the number to dial
657 self._phonenumber = make_ugly(number)
658 self._prettynumber = make_pretty(self._phonenumber)
659 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
660 if self._phonenumber:
661 self._plusButton.set_sensitive(False)
663 self._plusButton.set_sensitive(True)
665 self._errorDisplay.push_exception()
674 def load_settings(self, config, section):
677 def save_settings(self, config, section):
679 @note Thread Agnostic
683 def _on_key_press(self, widget, event):
685 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
686 contents = self._clipboard.wait_for_text()
687 if contents is not None:
688 self.set_number(contents)
690 self._errorDisplay.push_exception()
692 def _on_call_clicked(self, widget):
694 phoneNumber = self.get_number()
695 self.dial(phoneNumber)
698 self._errorDisplay.push_exception()
700 def _on_sms_clicked(self, widget):
702 phoneNumber = self.get_number()
705 [("Dialer", phoneNumber)], ()
709 self._errorDisplay.push_exception()
711 def _on_digit_clicked(self, widget):
713 self.set_number(self._phonenumber + widget.get_name()[-1])
715 self._errorDisplay.push_exception()
717 def _on_plus(self, *args):
719 self.set_number(self._phonenumber + "+")
721 self._errorDisplay.push_exception()
723 def _on_backspace(self, taps):
725 self.set_number(self._phonenumber[:-taps])
726 self._reset_back_button()
728 self._errorDisplay.push_exception()
730 def _on_clearall(self, taps):
733 self._reset_back_button()
735 self._errorDisplay.push_exception()
738 def _set_clear_button(self):
740 self._backButton.set_label("gtk-clear")
742 self._errorDisplay.push_exception()
744 def _reset_back_button(self):
746 self._backButton.set_label(self._originalLabel)
748 self._errorDisplay.push_exception()
751 class AccountInfo(object):
753 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
754 self._errorDisplay = errorDisplay
755 self._backend = backend
756 self._isPopulated = False
757 self._alarmHandler = alarmHandler
758 self._notifyOnMissed = False
759 self._notifyOnVoicemail = False
760 self._notifyOnSms = False
762 self._callbackList = []
763 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
764 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
765 self._onCallbackSelectChangedId = 0
767 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
768 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
769 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
770 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
771 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
772 self._onNotifyToggled = 0
773 self._onMinutesChanged = 0
774 self._onMissedToggled = 0
775 self._onVoicemailToggled = 0
776 self._onSmsToggled = 0
777 self._applyAlarmTimeoutId = None
779 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
780 self._callbackNumber = ""
783 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
785 self._accountViewNumberDisplay.set_use_markup(True)
786 self.set_account_number("")
788 del self._callbackList[:]
789 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
790 self._set_callback_label("")
792 if self._alarmHandler is not None:
793 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
794 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
795 self._missedCheckbox.set_active(self._notifyOnMissed)
796 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
797 self._smsCheckbox.set_active(self._notifyOnSms)
799 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
800 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
801 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
802 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
803 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
805 self._notifyCheckbox.set_sensitive(False)
806 self._minutesEntryButton.set_sensitive(False)
807 self._missedCheckbox.set_sensitive(False)
808 self._voicemailCheckbox.set_sensitive(False)
809 self._smsCheckbox.set_sensitive(False)
811 self.update(force=True)
814 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
815 self._onCallbackSelectChangedId = 0
816 self._set_callback_label("")
818 if self._alarmHandler is not None:
819 self._notifyCheckbox.disconnect(self._onNotifyToggled)
820 self._minutesEntryButton.disconnect(self._onMinutesChanged)
821 self._missedCheckbox.disconnect(self._onNotifyToggled)
822 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
823 self._smsCheckbox.disconnect(self._onNotifyToggled)
824 self._onNotifyToggled = 0
825 self._onMinutesChanged = 0
826 self._onMissedToggled = 0
827 self._onVoicemailToggled = 0
828 self._onSmsToggled = 0
830 self._notifyCheckbox.set_sensitive(True)
831 self._minutesEntryButton.set_sensitive(True)
832 self._missedCheckbox.set_sensitive(True)
833 self._voicemailCheckbox.set_sensitive(True)
834 self._smsCheckbox.set_sensitive(True)
837 del self._callbackList[:]
839 def set_account_number(self, number):
841 Displays current account number
843 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
845 def update(self, force = False):
846 if not force and self._isPopulated:
848 self._populate_callback_combo()
849 self.set_account_number(self._backend.get_account_number())
853 self._set_callback_label("")
854 self.set_account_number("")
855 self._isPopulated = False
857 def save_everything(self):
858 raise NotImplementedError
862 return "Account Info"
864 def load_settings(self, config, section):
865 self._callbackNumber = make_ugly(config.get(section, "callback"))
866 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
867 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
868 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
870 def save_settings(self, config, section):
872 @note Thread Agnostic
874 config.set(section, "callback", self._callbackNumber)
875 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
876 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
877 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
879 def _populate_callback_combo(self):
880 self._isPopulated = True
881 del self._callbackList[:]
883 callbackNumbers = self._backend.get_callback_numbers()
885 self._errorDisplay.push_exception()
886 self._isPopulated = False
889 if len(callbackNumbers) == 0:
890 callbackNumbers = {"": "No callback numbers available"}
892 for number, description in callbackNumbers.iteritems():
893 self._callbackList.append((make_pretty(number), description))
895 self._set_callback_number(self._callbackNumber)
897 def _set_callback_number(self, number):
899 if not self._backend.is_valid_syntax(number) and 0 < len(number):
900 self._errorDisplay.push_message("%s is not a valid callback number" % number)
901 elif number == self._backend.get_callback_number() and 0 < len(number):
902 _moduleLogger.warning(
903 "Callback number already is %s" % (
904 self._backend.get_callback_number(),
907 self._set_callback_label(number)
909 if number.startswith("1747"): number = "+" + number
910 self._backend.set_callback_number(number)
911 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
912 make_pretty(number), make_pretty(self._backend.get_callback_number())
914 self._callbackNumber = make_ugly(number)
915 self._set_callback_label(number)
917 "Callback number set to %s" % (
918 self._backend.get_callback_number(),
922 self._errorDisplay.push_exception()
924 def _set_callback_label(self, uglyNumber):
925 prettyNumber = make_pretty(uglyNumber)
926 if len(prettyNumber) == 0:
927 prettyNumber = "No Callback Number"
928 self._callbackSelectButton.set_label(prettyNumber)
930 def _update_alarm_settings(self, recurrence):
932 isEnabled = self._notifyCheckbox.get_active()
933 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
934 self._alarmHandler.apply_settings(isEnabled, recurrence)
936 self.save_everything()
937 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
938 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
940 def _on_callbackentry_clicked(self, *args):
942 actualSelection = make_pretty(self._callbackNumber)
945 (number, "%s (%s)" % (number, description))
946 for (number, description) in self._callbackList
948 defaultSelection = userOptions.get(actualSelection, actualSelection)
950 userSelection = hildonize.touch_selector_entry(
953 list(userOptions.itervalues()),
956 reversedUserOptions = dict(
957 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
959 selectedNumber = reversedUserOptions.get(userSelection, userSelection)
961 number = make_ugly(selectedNumber)
962 self._set_callback_number(number)
963 except RuntimeError, e:
964 _moduleLogger.exception("%s" % str(e))
966 self._errorDisplay.push_exception()
968 def _on_notify_toggled(self, *args):
970 if self._applyAlarmTimeoutId is not None:
971 gobject.source_remove(self._applyAlarmTimeoutId)
972 self._applyAlarmTimeoutId = None
973 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
975 self._errorDisplay.push_exception()
977 def _on_minutes_clicked(self, *args):
978 recurrenceChoices = [
994 actualSelection = self._alarmHandler.recurrence
996 closestSelectionIndex = 0
997 for i, possible in enumerate(recurrenceChoices):
998 if possible[0] <= actualSelection:
999 closestSelectionIndex = i
1000 recurrenceIndex = hildonize.touch_selector(
1003 (("%s" % m[1]) for m in recurrenceChoices),
1004 closestSelectionIndex,
1006 recurrence = recurrenceChoices[recurrenceIndex][0]
1008 self._update_alarm_settings(recurrence)
1009 except RuntimeError, e:
1010 _moduleLogger.exception("%s" % str(e))
1011 except Exception, e:
1012 self._errorDisplay.push_exception()
1014 def _on_apply_timeout(self, *args):
1016 self._applyAlarmTimeoutId = None
1018 self._update_alarm_settings(self._alarmHandler.recurrence)
1019 except Exception, e:
1020 self._errorDisplay.push_exception()
1023 def _on_missed_toggled(self, *args):
1025 self._notifyOnMissed = self._missedCheckbox.get_active()
1026 self.save_everything()
1027 except Exception, e:
1028 self._errorDisplay.push_exception()
1030 def _on_voicemail_toggled(self, *args):
1032 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
1033 self.save_everything()
1034 except Exception, e:
1035 self._errorDisplay.push_exception()
1037 def _on_sms_toggled(self, *args):
1039 self._notifyOnSms = self._smsCheckbox.get_active()
1040 self.save_everything()
1041 except Exception, e:
1042 self._errorDisplay.push_exception()
1045 class CallHistoryView(object):
1053 HISTORY_ITEM_TYPES = ["All", "Received", "Missed", "Placed"]
1055 def __init__(self, widgetTree, backend, errorDisplay):
1056 self._errorDisplay = errorDisplay
1057 self._backend = backend
1059 self._isPopulated = False
1060 self._historymodel = gtk.ListStore(
1061 gobject.TYPE_STRING, # number
1062 gobject.TYPE_STRING, # date
1063 gobject.TYPE_STRING, # action
1064 gobject.TYPE_STRING, # from
1065 gobject.TYPE_STRING, # from id
1067 self._historymodelfiltered = self._historymodel.filter_new()
1068 self._historymodelfiltered.set_visible_func(self._is_history_visible)
1069 self._historyview = widgetTree.get_widget("historyview")
1070 self._historyviewselection = None
1071 self._onRecentviewRowActivatedId = 0
1073 textrenderer = gtk.CellRendererText()
1074 textrenderer.set_property("yalign", 0)
1075 self._dateColumn = gtk.TreeViewColumn("Date")
1076 self._dateColumn.pack_start(textrenderer, expand=True)
1077 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
1079 textrenderer = gtk.CellRendererText()
1080 textrenderer.set_property("yalign", 0)
1081 self._actionColumn = gtk.TreeViewColumn("Action")
1082 self._actionColumn.pack_start(textrenderer, expand=True)
1083 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
1085 textrenderer = gtk.CellRendererText()
1086 textrenderer.set_property("yalign", 0)
1087 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
1088 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
1089 self._numberColumn = gtk.TreeViewColumn("Number")
1090 self._numberColumn.pack_start(textrenderer, expand=True)
1091 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
1093 textrenderer = gtk.CellRendererText()
1094 textrenderer.set_property("yalign", 0)
1095 hildonize.set_cell_thumb_selectable(textrenderer)
1096 self._nameColumn = gtk.TreeViewColumn("From")
1097 self._nameColumn.pack_start(textrenderer, expand=True)
1098 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
1099 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1101 self._window = gtk_toolbox.find_parent_window(self._historyview)
1103 self._historyFilterSelector = widgetTree.get_widget("historyFilterSelector")
1104 self._historyFilterSelector.connect("clicked", self._on_history_filter_clicked)
1105 self._selectedFilter = "All"
1107 self._updateSink = gtk_toolbox.threaded_stage(
1109 self._idly_populate_historyview,
1110 gtk_toolbox.null_sink(),
1115 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1116 self._historyFilterSelector.set_label(self._selectedFilter)
1118 self._historyview.set_model(self._historymodelfiltered)
1119 self._historyview.set_fixed_height_mode(False)
1121 self._historyview.append_column(self._dateColumn)
1122 self._historyview.append_column(self._actionColumn)
1123 self._historyview.append_column(self._numberColumn)
1124 self._historyview.append_column(self._nameColumn)
1125 self._historyviewselection = self._historyview.get_selection()
1126 self._historyviewselection.set_mode(gtk.SELECTION_SINGLE)
1128 self._onRecentviewRowActivatedId = self._historyview.connect("row-activated", self._on_historyview_row_activated)
1131 self._historyview.disconnect(self._onRecentviewRowActivatedId)
1135 self._historyview.remove_column(self._dateColumn)
1136 self._historyview.remove_column(self._actionColumn)
1137 self._historyview.remove_column(self._nameColumn)
1138 self._historyview.remove_column(self._numberColumn)
1139 self._historyview.set_model(None)
1141 def add_contact(self, *args, **kwds):
1143 @note Actual dial function is patched in later
1145 raise NotImplementedError("Horrible unknown error has occurred")
1147 def update(self, force = False):
1148 if not force and self._isPopulated:
1150 self._updateSink.send(())
1154 self._isPopulated = False
1155 self._historymodel.clear()
1159 return "Recent Calls"
1161 def load_settings(self, config, sectionName):
1163 self._selectedFilter = config.get(sectionName, "filter")
1164 if self._selectedFilter not in self.HISTORY_ITEM_TYPES:
1165 self._messageType = self.HISTORY_ITEM_TYPES[0]
1166 except ConfigParser.NoOptionError:
1169 def save_settings(self, config, sectionName):
1171 @note Thread Agnostic
1173 config.set(sectionName, "filter", self._selectedFilter)
1175 def _is_history_visible(self, model, iter):
1177 action = model.get_value(iter, self.ACTION_IDX)
1179 return False # this seems weird but oh well
1181 if self._selectedFilter in [action, "All"]:
1185 except Exception, e:
1186 self._errorDisplay.push_exception()
1188 def _idly_populate_historyview(self):
1189 with gtk_toolbox.gtk_lock():
1190 banner = hildonize.show_busy_banner_start(self._window, "Loading Call History")
1192 self._historymodel.clear()
1193 self._isPopulated = True
1196 historyItems = self._backend.get_recent()
1197 except Exception, e:
1198 self._errorDisplay.push_exception_with_lock()
1199 self._isPopulated = False
1203 gv_backend.decorate_recent(data)
1204 for data in gv_backend.sort_messages(historyItems)
1207 for contactId, personName, phoneNumber, date, action in historyItems:
1209 personName = "Unknown"
1210 date = abbrev_relative_date(date)
1211 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1212 prettyNumber = make_pretty(prettyNumber)
1213 item = (prettyNumber, date, action.capitalize(), personName, contactId)
1214 with gtk_toolbox.gtk_lock():
1215 self._historymodel.append(item)
1216 except Exception, e:
1217 self._errorDisplay.push_exception_with_lock()
1219 with gtk_toolbox.gtk_lock():
1220 hildonize.show_busy_banner_end(banner)
1224 def _on_history_filter_clicked(self, *args, **kwds):
1226 selectedComboIndex = self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
1229 newSelectedComboIndex = hildonize.touch_selector(
1232 self.HISTORY_ITEM_TYPES,
1235 except RuntimeError:
1238 option = self.HISTORY_ITEM_TYPES[newSelectedComboIndex]
1239 self._selectedFilter = option
1240 self._historyFilterSelector.set_label(self._selectedFilter)
1241 self._historymodelfiltered.refilter()
1242 except Exception, e:
1243 self._errorDisplay.push_exception()
1245 def _history_summary(self, expectedNumber):
1246 for number, action, date, whoFrom, whoFromId in self._historymodel:
1247 if expectedNumber is not None and expectedNumber == number:
1248 yield "%s <i>(%s)</i> - %s %s" % (number, whoFrom, date, action)
1250 def _on_historyview_row_activated(self, treeview, path, view_column):
1252 childPath = self._historymodelfiltered.convert_path_to_child_path(path)
1253 itr = self._historymodel.get_iter(childPath)
1257 prettyNumber = self._historymodel.get_value(itr, self.NUMBER_IDX)
1258 number = make_ugly(prettyNumber)
1259 description = list(self._history_summary(prettyNumber))
1260 contactName = self._historymodel.get_value(itr, self.FROM_IDX)
1261 contactId = self._historymodel.get_value(itr, self.FROM_ID_IDX)
1262 contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1266 contactPhoneNumbers,
1267 messages = description,
1268 defaultIndex = defaultIndex,
1270 self._historyviewselection.unselect_all()
1271 except Exception, e:
1272 self._errorDisplay.push_exception()
1275 class MessagesView(object):
1283 MESSAGE_DATA_IDX = 6
1285 NO_MESSAGES = "None"
1286 VOICEMAIL_MESSAGES = "Voicemail"
1287 TEXT_MESSAGES = "SMS"
1288 ALL_TYPES = "All Messages"
1289 MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
1291 UNREAD_STATUS = "Unread"
1292 UNARCHIVED_STATUS = "Inbox"
1294 MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
1296 def __init__(self, widgetTree, backend, errorDisplay):
1297 self._errorDisplay = errorDisplay
1298 self._backend = backend
1300 self._isPopulated = False
1301 self._messagemodel = gtk.ListStore(
1302 gobject.TYPE_STRING, # number
1303 gobject.TYPE_STRING, # date
1304 gobject.TYPE_STRING, # header
1305 gobject.TYPE_STRING, # message
1307 gobject.TYPE_STRING, # from id
1308 object, # message data
1310 self._messagemodelfiltered = self._messagemodel.filter_new()
1311 self._messagemodelfiltered.set_visible_func(self._is_message_visible)
1312 self._messageview = widgetTree.get_widget("messages_view")
1313 self._messageviewselection = None
1314 self._onMessageviewRowActivatedId = 0
1316 self._messageRenderer = gtk.CellRendererText()
1317 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1318 self._messageRenderer.set_property("wrap-width", 500)
1319 self._messageColumn = gtk.TreeViewColumn("Messages")
1320 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1321 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1322 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1324 self._window = gtk_toolbox.find_parent_window(self._messageview)
1326 self._messageTypeButton = widgetTree.get_widget("messageTypeButton")
1327 self._onMessageTypeClickedId = 0
1328 self._messageType = self.ALL_TYPES
1329 self._messageStatusButton = widgetTree.get_widget("messageStatusButton")
1330 self._onMessageStatusClickedId = 0
1331 self._messageStatus = self.ALL_STATUS
1333 self._updateSink = gtk_toolbox.threaded_stage(
1335 self._idly_populate_messageview,
1336 gtk_toolbox.null_sink(),
1341 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1342 self._messageview.set_model(self._messagemodelfiltered)
1343 self._messageview.set_headers_visible(False)
1344 self._messageview.set_fixed_height_mode(False)
1346 self._messageview.append_column(self._messageColumn)
1347 self._messageviewselection = self._messageview.get_selection()
1348 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1350 self._messageTypeButton.set_label(self._messageType)
1351 self._messageStatusButton.set_label(self._messageStatus)
1353 self._onMessageviewRowActivatedId = self._messageview.connect(
1354 "row-activated", self._on_messageview_row_activated
1356 self._onMessageTypeClickedId = self._messageTypeButton.connect(
1357 "clicked", self._on_message_type_clicked
1359 self._onMessageStatusClickedId = self._messageStatusButton.connect(
1360 "clicked", self._on_message_status_clicked
1364 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1365 self._messageTypeButton.disconnect(self._onMessageTypeClickedId)
1366 self._messageStatusButton.disconnect(self._onMessageStatusClickedId)
1370 self._messageview.remove_column(self._messageColumn)
1371 self._messageview.set_model(None)
1373 def add_contact(self, *args, **kwds):
1375 @note Actual dial function is patched in later
1377 raise NotImplementedError("Horrible unknown error has occurred")
1379 def update(self, force = False):
1380 if not force and self._isPopulated:
1382 self._updateSink.send(())
1386 self._isPopulated = False
1387 self._messagemodel.clear()
1393 def load_settings(self, config, sectionName):
1395 self._messageType = config.get(sectionName, "type")
1396 if self._messageType not in self.MESSAGE_TYPES:
1397 self._messageType = self.ALL_TYPES
1398 self._messageStatus = config.get(sectionName, "status")
1399 if self._messageStatus not in self.MESSAGE_STATUSES:
1400 self._messageStatus = self.ALL_STATUS
1401 except ConfigParser.NoOptionError:
1404 def save_settings(self, config, sectionName):
1406 @note Thread Agnostic
1408 config.set(sectionName, "status", self._messageStatus)
1409 config.set(sectionName, "type", self._messageType)
1411 def _is_message_visible(self, model, iter):
1413 message = model.get_value(iter, self.MESSAGE_DATA_IDX)
1415 return False # this seems weird but oh well
1416 return self._filter_messages(message, self._messageType, self._messageStatus)
1417 except Exception, e:
1418 self._errorDisplay.push_exception()
1421 def _filter_messages(cls, message, type, status):
1422 if type == cls.ALL_TYPES:
1425 messageType = message["type"]
1426 isType = messageType == type
1428 if status == cls.ALL_STATUS:
1431 isUnarchived = not message["isArchived"]
1432 isUnread = not message["isRead"]
1433 if status == cls.UNREAD_STATUS:
1434 isStatus = isUnarchived and isUnread
1435 elif status == cls.UNARCHIVED_STATUS:
1436 isStatus = isUnarchived
1438 assert "Status %s is bad for %r" % (status, message)
1440 return isType and isStatus
1442 _MIN_MESSAGES_SHOWN = 4
1444 def _idly_populate_messageview(self):
1445 with gtk_toolbox.gtk_lock():
1446 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1448 self._messagemodel.clear()
1449 self._isPopulated = True
1451 if self._messageType == self.NO_MESSAGES:
1455 messageItems = self._backend.get_messages()
1456 except Exception, e:
1457 self._errorDisplay.push_exception_with_lock()
1458 self._isPopulated = False
1462 (gv_backend.decorate_message(message), message)
1463 for message in gv_backend.sort_messages(messageItems)
1466 for (contactId, header, number, relativeDate, messages), messageData in messageItems:
1467 prettyNumber = number[2:] if number.startswith("+1") else number
1468 prettyNumber = make_pretty(prettyNumber)
1470 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1471 expandedMessages = [firstMessage]
1472 expandedMessages.extend(messages)
1473 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1474 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1475 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1476 collapsedMessages = [firstMessage, secondMessage]
1477 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1479 collapsedMessages = expandedMessages
1480 #collapsedMessages = _collapse_message(collapsedMessages, 60, self._MIN_MESSAGES_SHOWN)
1482 number = make_ugly(number)
1484 row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId, messageData
1485 with gtk_toolbox.gtk_lock():
1486 self._messagemodel.append(row)
1487 except Exception, e:
1488 self._errorDisplay.push_exception_with_lock()
1490 with gtk_toolbox.gtk_lock():
1491 hildonize.show_busy_banner_end(banner)
1492 self._messagemodelfiltered.refilter()
1496 def _on_messageview_row_activated(self, treeview, path, view_column):
1498 childPath = self._messagemodelfiltered.convert_path_to_child_path(path)
1499 itr = self._messagemodel.get_iter(childPath)
1503 number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX))
1504 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1506 contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX)
1507 header = self._messagemodel.get_value(itr, self.HEADER_IDX)
1508 contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1512 contactPhoneNumbers,
1513 messages = description,
1514 defaultIndex = defaultIndex,
1516 self._messageviewselection.unselect_all()
1517 except Exception, e:
1518 self._errorDisplay.push_exception()
1520 def _on_message_type_clicked(self, *args, **kwds):
1522 selectedIndex = self.MESSAGE_TYPES.index(self._messageType)
1525 newSelectedIndex = hildonize.touch_selector(
1531 except RuntimeError:
1534 if selectedIndex != newSelectedIndex:
1535 self._messageType = self.MESSAGE_TYPES[newSelectedIndex]
1536 self._messageTypeButton.set_label(self._messageType)
1537 self._messagemodelfiltered.refilter()
1538 except Exception, e:
1539 self._errorDisplay.push_exception()
1541 def _on_message_status_clicked(self, *args, **kwds):
1543 selectedIndex = self.MESSAGE_STATUSES.index(self._messageStatus)
1546 newSelectedIndex = hildonize.touch_selector(
1549 self.MESSAGE_STATUSES,
1552 except RuntimeError:
1555 if selectedIndex != newSelectedIndex:
1556 self._messageStatus = self.MESSAGE_STATUSES[newSelectedIndex]
1557 self._messageStatusButton.set_label(self._messageStatus)
1558 self._messagemodelfiltered.refilter()
1559 except Exception, e:
1560 self._errorDisplay.push_exception()
1563 class ContactsView(object):
1565 CONTACT_TYPE_IDX = 0
1566 CONTACT_NAME_IDX = 1
1569 def __init__(self, widgetTree, backend, errorDisplay):
1570 self._errorDisplay = errorDisplay
1571 self._backend = backend
1573 self._addressBook = None
1574 self._selectedComboIndex = 0
1575 self._addressBookFactories = [null_backend.NullAddressBook()]
1577 self._booksList = []
1578 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1580 self._isPopulated = False
1581 self._contactsmodel = gtk.ListStore(
1582 gobject.TYPE_STRING, # Contact Type
1583 gobject.TYPE_STRING, # Contact Name
1584 gobject.TYPE_STRING, # Contact ID
1586 self._contactsviewselection = None
1587 self._contactsview = widgetTree.get_widget("contactsview")
1589 self._contactColumn = gtk.TreeViewColumn("Contact")
1590 displayContactSource = False
1591 if displayContactSource:
1592 textrenderer = gtk.CellRendererText()
1593 self._contactColumn.pack_start(textrenderer, expand=False)
1594 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_TYPE_IDX)
1595 textrenderer = gtk.CellRendererText()
1596 hildonize.set_cell_thumb_selectable(textrenderer)
1597 self._contactColumn.pack_start(textrenderer, expand=True)
1598 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_NAME_IDX)
1599 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1600 self._contactColumn.set_sort_column_id(1)
1601 self._contactColumn.set_visible(True)
1603 self._onContactsviewRowActivatedId = 0
1604 self._onAddressbookButtonChangedId = 0
1605 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1607 self._updateSink = gtk_toolbox.threaded_stage(
1609 self._idly_populate_contactsview,
1610 gtk_toolbox.null_sink(),
1615 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1617 self._contactsview.set_model(self._contactsmodel)
1618 self._contactsview.set_fixed_height_mode(False)
1619 self._contactsview.append_column(self._contactColumn)
1620 self._contactsviewselection = self._contactsview.get_selection()
1621 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1623 del self._booksList[:]
1624 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1625 if factoryName and bookName:
1626 entryName = "%s: %s" % (factoryName, bookName)
1628 entryName = factoryName
1630 entryName = bookName
1632 entryName = "Bad name (%d)" % factoryId
1633 row = (str(factoryId), bookId, entryName)
1634 self._booksList.append(row)
1636 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1637 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1639 if len(self._booksList) <= self._selectedComboIndex:
1640 self._selectedComboIndex = 0
1641 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1643 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1644 selectedBookId = self._booksList[self._selectedComboIndex][1]
1645 self.open_addressbook(selectedFactoryId, selectedBookId)
1648 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1649 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1653 self._bookSelectionButton.set_label("")
1654 self._contactsview.set_model(None)
1655 self._contactsview.remove_column(self._contactColumn)
1657 def add_contact(self, *args, **kwds):
1659 @note Actual dial function is patched in later
1661 raise NotImplementedError("Horrible unknown error has occurred")
1663 def get_addressbooks(self):
1665 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1667 for i, factory in enumerate(self._addressBookFactories):
1668 for bookFactory, bookId, bookName in factory.get_addressbooks():
1669 yield (str(i), bookId), (factory.factory_name(), bookName)
1671 def open_addressbook(self, bookFactoryId, bookId):
1672 bookFactoryIndex = int(bookFactoryId)
1673 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1674 self._addressBook = addressBook
1676 def update(self, force = False):
1677 if not force and self._isPopulated:
1679 self._updateSink.send(())
1683 self._isPopulated = False
1684 self._contactsmodel.clear()
1685 for factory in self._addressBookFactories:
1686 factory.clear_caches()
1687 self._addressBook.clear_caches()
1689 def append(self, book):
1690 self._addressBookFactories.append(book)
1692 def extend(self, books):
1693 self._addressBookFactories.extend(books)
1699 def load_settings(self, config, sectionName):
1701 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1702 except ConfigParser.NoOptionError:
1703 self._selectedComboIndex = 0
1705 def save_settings(self, config, sectionName):
1706 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1708 def _idly_populate_contactsview(self):
1709 with gtk_toolbox.gtk_lock():
1710 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1713 while addressBook is not self._addressBook:
1714 addressBook = self._addressBook
1715 with gtk_toolbox.gtk_lock():
1716 self._contactsview.set_model(None)
1720 contacts = addressBook.get_contacts()
1721 except Exception, e:
1723 self._isPopulated = False
1724 self._errorDisplay.push_exception_with_lock()
1725 for contactId, contactName in contacts:
1726 contactType = addressBook.contact_source_short_name(contactId)
1727 row = contactType, contactName, contactId
1728 self._contactsmodel.append(row)
1730 with gtk_toolbox.gtk_lock():
1731 self._contactsview.set_model(self._contactsmodel)
1733 self._isPopulated = True
1734 except Exception, e:
1735 self._errorDisplay.push_exception_with_lock()
1737 with gtk_toolbox.gtk_lock():
1738 hildonize.show_busy_banner_end(banner)
1741 def _on_addressbook_button_changed(self, *args, **kwds):
1744 newSelectedComboIndex = hildonize.touch_selector(
1747 (("%s" % m[2]) for m in self._booksList),
1748 self._selectedComboIndex,
1750 except RuntimeError:
1753 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1754 selectedBookId = self._booksList[newSelectedComboIndex][1]
1756 oldAddressbook = self._addressBook
1757 self.open_addressbook(selectedFactoryId, selectedBookId)
1758 forceUpdate = True if oldAddressbook is not self._addressBook else False
1759 self.update(force=forceUpdate)
1761 self._selectedComboIndex = newSelectedComboIndex
1762 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1763 except Exception, e:
1764 self._errorDisplay.push_exception()
1766 def _on_contactsview_row_activated(self, treeview, path, view_column):
1768 itr = self._contactsmodel.get_iter(path)
1772 contactId = self._contactsmodel.get_value(itr, self.CONTACT_ID_IDX)
1773 contactName = self._contactsmodel.get_value(itr, self.CONTACT_NAME_IDX)
1775 contactDetails = self._addressBook.get_contact_details(contactId)
1776 except Exception, e:
1778 self._errorDisplay.push_exception()
1779 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1781 if len(contactPhoneNumbers) == 0:
1786 contactPhoneNumbers,
1787 messages = (contactName, ),
1789 self._contactsviewselection.unselect_all()
1790 except Exception, e:
1791 self._errorDisplay.push_exception()