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_areacode(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_areacode(phonenumber[3:])
94 elif phonenumber.startswith("1"):
96 prettynumber += _make_pretty_with_areacode(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_areacode(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.set_title("SMS")
260 self._window.connect("delete-event", self._on_delete)
261 self._window.connect("key-press-event", self._on_key_press)
262 self._window.connect("window-state-event", self._on_window_state_change)
263 self._widgetTree.get_widget("smsMessagesViewPort").get_parent().show()
265 errorBox = self._widgetTree.get_widget("smsErrorEventBox")
266 errorDescription = self._widgetTree.get_widget("smsErrorDescription")
267 errorClose = self._widgetTree.get_widget("smsErrorClose")
268 self._errorDisplay = gtk_toolbox.ErrorDisplay(errorBox, errorDescription, errorClose)
270 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
271 self._smsButton.connect("clicked", self._on_send)
272 self._dialButton = self._widgetTree.get_widget("dialButton")
273 self._dialButton.connect("clicked", self._on_dial)
275 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
277 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
278 self._messagesView = self._widgetTree.get_widget("smsMessages")
280 textrenderer = gtk.CellRendererText()
281 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
282 textrenderer.set_property("wrap-width", 450)
283 messageColumn = gtk.TreeViewColumn("")
284 messageColumn.pack_start(textrenderer, expand=True)
285 messageColumn.add_attribute(textrenderer, "markup", 0)
286 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
287 self._messagesView.append_column(messageColumn)
288 self._messagesView.set_headers_visible(False)
289 self._messagesView.set_model(self._messagemodel)
290 self._messagesView.set_fixed_height_mode(False)
292 self._conversationView = self._messagesView.get_parent()
293 self._conversationViewPort = self._conversationView.get_parent()
294 self._scrollWindow = self._conversationViewPort.get_parent()
296 self._targetList = self._widgetTree.get_widget("smsTargetList")
297 self._phoneButton = self._widgetTree.get_widget("phoneTypeSelection")
298 self._phoneButton.connect("clicked", self._on_phone)
299 self._smsEntry = self._widgetTree.get_widget("smsEntry")
300 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
301 self._smsEntrySize = None
305 def add_contact(self, name, contactDetails, messages = (), defaultIndex = -1):
306 contactNumbers = list(self._to_contact_numbers(contactDetails))
307 assert contactNumbers, "Contact must have at least one number"
308 contactIndex = defaultIndex if defaultIndex != -1 else 0
309 contact = contactNumbers, contactIndex, messages
310 self._contacts.append(contact)
312 nameLabel = gtk.Label(name)
313 selector = gtk.Button(contactNumbers[0][1])
314 if len(contactNumbers) == 1:
315 selector.set_sensitive(False)
316 removeContact = gtk.Button(stock="gtk-delete")
318 row.pack_start(nameLabel, True, True)
319 row.pack_start(selector, True, True)
320 row.pack_start(removeContact, False, False)
322 self._targetList.pack_start(row)
323 selector.connect("clicked", self._on_choose_phone_n, row)
324 removeContact.connect("clicked", self._on_remove_phone_n, row)
325 self._update_button_state()
326 self._update_context()
328 parentSize = self._parent.get_size()
329 self._window.resize(parentSize[0], max(parentSize[1]-10, 100))
331 self._window.present()
333 self._smsEntry.grab_focus()
334 self._scroll_to_bottom()
337 del self._contacts[:]
339 for row in list(self._targetList.get_children()):
340 self._targetList.remove(row)
341 self._smsEntry.get_buffer().set_text("")
342 self._update_letter_count()
343 self._update_context()
345 def fullscreen(self):
346 self._window.fullscreen()
348 def unfullscreen(self):
349 self._window.unfullscreen()
351 def _remove_contact(self, contactIndex):
352 del self._contacts[contactIndex]
354 row = list(self._targetList.get_children())[contactIndex]
355 self._targetList.remove(row)
356 self._update_button_state()
357 self._update_context()
358 self._scroll_to_bottom()
360 def _scroll_to_bottom(self):
361 dx = self._conversationView.get_allocation().height - self._conversationViewPort.get_allocation().height
363 adjustment = self._scrollWindow.get_vadjustment()
364 adjustment.value = dx
366 def _update_letter_count(self):
367 if self._smsEntrySize is None:
368 self._smsEntrySize = self._smsEntry.size_request()
370 self._smsEntry.set_size_request(*self._smsEntrySize)
371 entryLength = self._smsEntry.get_buffer().get_char_count()
373 numTexts, numCharInText = divmod(entryLength, self.MAX_CHAR)
375 self._letterCountLabel.set_text("%s.%s" % (numTexts, numCharInText))
377 self._letterCountLabel.set_text("%s" % (numCharInText, ))
379 self._update_button_state()
381 def _update_context(self):
382 self._messagemodel.clear()
383 if len(self._contacts) == 0:
384 self._messagesView.hide()
385 self._targetList.hide()
386 self._phoneButton.hide()
387 self._phoneButton.set_label("Error: You shouldn't see this")
388 elif len(self._contacts) == 1:
389 contactNumbers, index, messages = self._contacts[0]
391 self._messagesView.show()
392 for message in messages:
394 self._messagemodel.append(row)
395 messagesSelection = self._messagesView.get_selection()
396 messagesSelection.select_path((len(messages)-1, ))
398 self._messagesView.hide()
399 self._targetList.hide()
400 self._phoneButton.show()
401 self._phoneButton.set_label(contactNumbers[index][1])
402 if 1 < len(contactNumbers):
403 self._phoneButton.set_sensitive(True)
405 self._phoneButton.set_sensitive(False)
407 self._messagesView.hide()
408 self._targetList.show()
409 self._phoneButton.hide()
410 self._phoneButton.set_label("Error: You shouldn't see this")
412 def _update_button_state(self):
413 if len(self._contacts) == 0:
414 self._dialButton.set_sensitive(False)
415 self._smsButton.set_sensitive(False)
416 elif len(self._contacts) == 1:
417 entryLength = self._smsEntry.get_buffer().get_char_count()
419 self._dialButton.set_sensitive(True)
420 self._smsButton.set_sensitive(False)
422 self._dialButton.set_sensitive(False)
423 self._smsButton.set_sensitive(True)
425 self._dialButton.set_sensitive(False)
426 self._smsButton.set_sensitive(True)
428 def _to_contact_numbers(self, contactDetails):
429 for phoneType, phoneNumber in contactDetails:
430 display = " - ".join((make_pretty(phoneNumber), phoneType))
431 yield (phoneNumber, display)
433 def _pseudo_destroy(self):
437 def _request_number(self, contactIndex):
438 contactNumbers, index, messages = self._contacts[contactIndex]
439 assert 0 <= index, "%r" % index
441 index = hildonize.touch_selector(
444 (description for (number, description) in contactNumbers),
447 self._contacts[contactIndex] = contactNumbers, index, messages
449 def send_sms(self, numbers, message):
450 raise NotImplementedError()
452 def dial(self, number):
453 raise NotImplementedError()
455 def _on_phone(self, *args):
457 assert len(self._contacts) == 1, "One and only one contact is required"
458 self._request_number(0)
460 contactNumbers, numberIndex, messages = self._contacts[0]
461 self._phoneButton.set_label(contactNumbers[numberIndex][1])
462 row = list(self._targetList.get_children())[0]
463 phoneButton = list(row.get_children())[1]
464 phoneButton.set_label(contactNumbers[numberIndex][1])
466 self._errorDisplay.push_exception()
468 def _on_choose_phone_n(self, button, row):
470 assert 1 < len(self._contacts), "More than one contact required"
471 targetList = list(self._targetList.get_children())
472 index = targetList.index(row)
473 self._request_number(index)
475 contactNumbers, numberIndex, messages = self._contacts[0]
476 phoneButton = list(row.get_children())[1]
477 phoneButton.set_label(contactNumbers[numberIndex][1])
479 self._errorDisplay.push_exception()
481 def _on_remove_phone_n(self, button, row):
483 assert 1 < len(self._contacts), "More than one contact required"
484 targetList = list(self._targetList.get_children())
485 index = targetList.index(row)
487 del self._contacts[index]
488 self._targetList.remove(row)
489 self._update_context()
490 self._update_button_state()
492 self._errorDisplay.push_exception()
494 def _on_entry_changed(self, *args):
496 self._update_letter_count()
498 self._errorDisplay.push_exception()
500 def _on_send(self, *args):
502 assert 0 < len(self._contacts), "At least one contact required (%r)" % self._contacts
504 make_ugly(contact[0][contact[1]][0])
505 for contact in self._contacts
508 entryBuffer = self._smsEntry.get_buffer()
509 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
510 enteredMessage = enteredMessage.strip()
511 assert enteredMessage, "No message provided"
512 self.send_sms(phoneNumbers, enteredMessage)
513 self._pseudo_destroy()
515 self._errorDisplay.push_exception()
517 def _on_dial(self, *args):
519 assert len(self._contacts) == 1, "One and only one contact allowed (%r)" % self._contacts
520 contact = self._contacts[0]
521 contactNumber = contact[0][contact[1]][0]
522 phoneNumber = make_ugly(contactNumber)
523 self.dial(phoneNumber)
524 self._pseudo_destroy()
526 self._errorDisplay.push_exception()
528 def _on_delete(self, *args):
530 self._window.emit_stop_by_name("delete-event")
531 if hildonize.IS_FREMANTLE_SUPPORTED:
534 self._pseudo_destroy()
536 self._errorDisplay.push_exception()
539 def _on_window_state_change(self, widget, event, *args):
541 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
542 self._isFullScreen = True
544 self._isFullScreen = False
546 self._errorDisplay.push_exception()
548 def _on_key_press(self, widget, event):
549 RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
552 event.keyval == gtk.keysyms.F6 or
553 event.keyval in RETURN_TYPES and event.get_state() & gtk.gdk.CONTROL_MASK
555 if self._isFullScreen:
556 self._window.unfullscreen()
558 self._window.fullscreen()
559 elif event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
562 for messagePart in self._messagemodel
564 self._clipboard.set_text(str(message))
566 event.keyval == gtk.keysyms.h and
567 event.get_state() & gtk.gdk.CONTROL_MASK
571 event.keyval == gtk.keysyms.w and
572 event.get_state() & gtk.gdk.CONTROL_MASK
574 self._pseudo_destroy()
576 event.keyval == gtk.keysyms.q and
577 event.get_state() & gtk.gdk.CONTROL_MASK
579 self._parent.destroy()
581 self._errorDisplay.push_exception()
584 class Dialpad(object):
586 def __init__(self, widgetTree, errorDisplay):
587 self._clipboard = gtk.clipboard_get()
588 self._errorDisplay = errorDisplay
590 self._numberdisplay = widgetTree.get_widget("numberdisplay")
591 self._callButton = widgetTree.get_widget("dialpadCall")
592 self._sendSMSButton = widgetTree.get_widget("dialpadSMS")
593 self._backButton = widgetTree.get_widget("back")
594 self._plusButton = widgetTree.get_widget("plus")
595 self._phonenumber = ""
596 self._prettynumber = ""
599 "on_digit_clicked": self._on_digit_clicked,
601 widgetTree.signal_autoconnect(callbackMapping)
602 self._sendSMSButton.connect("clicked", self._on_sms_clicked)
603 self._callButton.connect("clicked", self._on_call_clicked)
604 self._plusButton.connect("clicked", self._on_plus)
606 self._originalLabel = self._backButton.get_label()
607 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
608 self._backTapHandler.on_tap = self._on_backspace
609 self._backTapHandler.on_hold = self._on_clearall
610 self._backTapHandler.on_holding = self._set_clear_button
611 self._backTapHandler.on_cancel = self._reset_back_button
613 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
614 self._keyPressEventId = 0
617 self._sendSMSButton.grab_focus()
618 self._backTapHandler.enable()
619 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
622 self._window.disconnect(self._keyPressEventId)
623 self._keyPressEventId = 0
624 self._reset_back_button()
625 self._backTapHandler.disable()
627 def add_contact(self, *args, **kwds):
629 @note Actual function is patched in later
631 raise NotImplementedError("Horrible unknown error has occurred")
633 def dial(self, number):
635 @note Actual function is patched in later
637 raise NotImplementedError("Horrible unknown error has occurred")
639 def get_number(self):
640 return self._phonenumber
642 def set_number(self, number):
644 Set the number to dial
647 self._phonenumber = make_ugly(number)
648 self._prettynumber = make_pretty(self._phonenumber)
649 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
650 if self._phonenumber:
651 self._plusButton.set_sensitive(False)
653 self._plusButton.set_sensitive(True)
655 self._errorDisplay.push_exception()
664 def load_settings(self, config, section):
667 def save_settings(self, config, section):
669 @note Thread Agnostic
673 def _on_key_press(self, widget, event):
675 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
676 contents = self._clipboard.wait_for_text()
677 if contents is not None:
678 self.set_number(contents)
680 self._errorDisplay.push_exception()
682 def _on_call_clicked(self, widget):
684 phoneNumber = self.get_number()
685 self.dial(phoneNumber)
688 self._errorDisplay.push_exception()
690 def _on_sms_clicked(self, widget):
692 phoneNumber = self.get_number()
695 [("Dialer", phoneNumber)], ()
699 self._errorDisplay.push_exception()
701 def _on_digit_clicked(self, widget):
703 self.set_number(self._phonenumber + widget.get_name()[-1])
705 self._errorDisplay.push_exception()
707 def _on_plus(self, *args):
709 self.set_number(self._phonenumber + "+")
711 self._errorDisplay.push_exception()
713 def _on_backspace(self, taps):
715 self.set_number(self._phonenumber[:-taps])
716 self._reset_back_button()
718 self._errorDisplay.push_exception()
720 def _on_clearall(self, taps):
723 self._reset_back_button()
725 self._errorDisplay.push_exception()
728 def _set_clear_button(self):
730 self._backButton.set_label("gtk-clear")
732 self._errorDisplay.push_exception()
734 def _reset_back_button(self):
736 self._backButton.set_label(self._originalLabel)
738 self._errorDisplay.push_exception()
741 class AccountInfo(object):
743 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
744 self._errorDisplay = errorDisplay
745 self._backend = backend
746 self._isPopulated = False
747 self._alarmHandler = alarmHandler
748 self._notifyOnMissed = False
749 self._notifyOnVoicemail = False
750 self._notifyOnSms = False
752 self._callbackList = []
753 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
754 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
755 self._onCallbackSelectChangedId = 0
757 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
758 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
759 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
760 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
761 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
762 self._onNotifyToggled = 0
763 self._onMinutesChanged = 0
764 self._onMissedToggled = 0
765 self._onVoicemailToggled = 0
766 self._onSmsToggled = 0
767 self._applyAlarmTimeoutId = None
769 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
770 self._callbackNumber = ""
773 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
775 self._accountViewNumberDisplay.set_use_markup(True)
776 self.set_account_number("")
778 del self._callbackList[:]
779 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
780 self._set_callback_label("")
782 if self._alarmHandler is not None:
783 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
784 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
785 self._missedCheckbox.set_active(self._notifyOnMissed)
786 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
787 self._smsCheckbox.set_active(self._notifyOnSms)
789 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
790 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
791 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
792 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
793 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
795 self._notifyCheckbox.set_sensitive(False)
796 self._minutesEntryButton.set_sensitive(False)
797 self._missedCheckbox.set_sensitive(False)
798 self._voicemailCheckbox.set_sensitive(False)
799 self._smsCheckbox.set_sensitive(False)
801 self.update(force=True)
804 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
805 self._onCallbackSelectChangedId = 0
806 self._set_callback_label("")
808 if self._alarmHandler is not None:
809 self._notifyCheckbox.disconnect(self._onNotifyToggled)
810 self._minutesEntryButton.disconnect(self._onMinutesChanged)
811 self._missedCheckbox.disconnect(self._onNotifyToggled)
812 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
813 self._smsCheckbox.disconnect(self._onNotifyToggled)
814 self._onNotifyToggled = 0
815 self._onMinutesChanged = 0
816 self._onMissedToggled = 0
817 self._onVoicemailToggled = 0
818 self._onSmsToggled = 0
820 self._notifyCheckbox.set_sensitive(True)
821 self._minutesEntryButton.set_sensitive(True)
822 self._missedCheckbox.set_sensitive(True)
823 self._voicemailCheckbox.set_sensitive(True)
824 self._smsCheckbox.set_sensitive(True)
827 del self._callbackList[:]
829 def set_account_number(self, number):
831 Displays current account number
833 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
835 def update(self, force = False):
836 if not force and self._isPopulated:
838 self._populate_callback_combo()
839 self.set_account_number(self._backend.get_account_number())
843 self._set_callback_label("")
844 self.set_account_number("")
845 self._isPopulated = False
847 def save_everything(self):
848 raise NotImplementedError
852 return "Account Info"
854 def load_settings(self, config, section):
855 self._callbackNumber = make_ugly(config.get(section, "callback"))
856 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
857 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
858 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
860 def save_settings(self, config, section):
862 @note Thread Agnostic
864 config.set(section, "callback", self._callbackNumber)
865 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
866 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
867 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
869 def _populate_callback_combo(self):
870 self._isPopulated = True
871 del self._callbackList[:]
873 callbackNumbers = self._backend.get_callback_numbers()
875 self._errorDisplay.push_exception()
876 self._isPopulated = False
879 if len(callbackNumbers) == 0:
880 callbackNumbers = {"": "No callback numbers available"}
882 for number, description in callbackNumbers.iteritems():
883 self._callbackList.append((make_pretty(number), description))
885 self._set_callback_number(self._callbackNumber)
887 def _set_callback_number(self, number):
889 if not self._backend.is_valid_syntax(number) and 0 < len(number):
890 self._errorDisplay.push_message("%s is not a valid callback number" % number)
891 elif number == self._backend.get_callback_number() and 0 < len(number):
892 _moduleLogger.warning(
893 "Callback number already is %s" % (
894 self._backend.get_callback_number(),
897 self._set_callback_label(number)
899 if number.startswith("1747"): number = "+" + number
900 self._backend.set_callback_number(number)
901 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
902 make_pretty(number), make_pretty(self._backend.get_callback_number())
904 self._callbackNumber = make_ugly(number)
905 self._set_callback_label(number)
907 "Callback number set to %s" % (
908 self._backend.get_callback_number(),
912 self._errorDisplay.push_exception()
914 def _set_callback_label(self, uglyNumber):
915 prettyNumber = make_pretty(uglyNumber)
916 if len(prettyNumber) == 0:
917 prettyNumber = "No Callback Number"
918 self._callbackSelectButton.set_label(prettyNumber)
920 def _update_alarm_settings(self, recurrence):
922 isEnabled = self._notifyCheckbox.get_active()
923 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
924 self._alarmHandler.apply_settings(isEnabled, recurrence)
926 self.save_everything()
927 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
928 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
930 def _on_callbackentry_clicked(self, *args):
932 actualSelection = make_pretty(self._callbackNumber)
935 (number, "%s (%s)" % (number, description))
936 for (number, description) in self._callbackList
938 defaultSelection = userOptions.get(actualSelection, actualSelection)
940 userSelection = hildonize.touch_selector_entry(
943 list(userOptions.itervalues()),
946 reversedUserOptions = dict(
947 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
949 selectedNumber = reversedUserOptions.get(userSelection, userSelection)
951 number = make_ugly(selectedNumber)
952 self._set_callback_number(number)
953 except RuntimeError, e:
954 _moduleLogger.exception("%s" % str(e))
956 self._errorDisplay.push_exception()
958 def _on_notify_toggled(self, *args):
960 if self._applyAlarmTimeoutId is not None:
961 gobject.source_remove(self._applyAlarmTimeoutId)
962 self._applyAlarmTimeoutId = None
963 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
965 self._errorDisplay.push_exception()
967 def _on_minutes_clicked(self, *args):
968 recurrenceChoices = [
984 actualSelection = self._alarmHandler.recurrence
986 closestSelectionIndex = 0
987 for i, possible in enumerate(recurrenceChoices):
988 if possible[0] <= actualSelection:
989 closestSelectionIndex = i
990 recurrenceIndex = hildonize.touch_selector(
993 (("%s" % m[1]) for m in recurrenceChoices),
994 closestSelectionIndex,
996 recurrence = recurrenceChoices[recurrenceIndex][0]
998 self._update_alarm_settings(recurrence)
999 except RuntimeError, e:
1000 _moduleLogger.exception("%s" % str(e))
1001 except Exception, e:
1002 self._errorDisplay.push_exception()
1004 def _on_apply_timeout(self, *args):
1006 self._applyAlarmTimeoutId = None
1008 self._update_alarm_settings(self._alarmHandler.recurrence)
1009 except Exception, e:
1010 self._errorDisplay.push_exception()
1013 def _on_missed_toggled(self, *args):
1015 self._notifyOnMissed = self._missedCheckbox.get_active()
1016 self.save_everything()
1017 except Exception, e:
1018 self._errorDisplay.push_exception()
1020 def _on_voicemail_toggled(self, *args):
1022 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
1023 self.save_everything()
1024 except Exception, e:
1025 self._errorDisplay.push_exception()
1027 def _on_sms_toggled(self, *args):
1029 self._notifyOnSms = self._smsCheckbox.get_active()
1030 self.save_everything()
1031 except Exception, e:
1032 self._errorDisplay.push_exception()
1035 class CallHistoryView(object):
1043 HISTORY_ITEM_TYPES = ["All", "Received", "Missed", "Placed"]
1045 def __init__(self, widgetTree, backend, errorDisplay):
1046 self._errorDisplay = errorDisplay
1047 self._backend = backend
1049 self._isPopulated = False
1050 self._historymodel = gtk.ListStore(
1051 gobject.TYPE_STRING, # number
1052 gobject.TYPE_STRING, # date
1053 gobject.TYPE_STRING, # action
1054 gobject.TYPE_STRING, # from
1055 gobject.TYPE_STRING, # from id
1057 self._historymodelfiltered = self._historymodel.filter_new()
1058 self._historymodelfiltered.set_visible_func(self._is_history_visible)
1059 self._historyview = widgetTree.get_widget("historyview")
1060 self._historyviewselection = None
1061 self._onRecentviewRowActivatedId = 0
1063 textrenderer = gtk.CellRendererText()
1064 textrenderer.set_property("yalign", 0)
1065 self._dateColumn = gtk.TreeViewColumn("Date")
1066 self._dateColumn.pack_start(textrenderer, expand=True)
1067 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
1069 textrenderer = gtk.CellRendererText()
1070 textrenderer.set_property("yalign", 0)
1071 self._actionColumn = gtk.TreeViewColumn("Action")
1072 self._actionColumn.pack_start(textrenderer, expand=True)
1073 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
1075 textrenderer = gtk.CellRendererText()
1076 textrenderer.set_property("yalign", 0)
1077 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
1078 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
1079 self._numberColumn = gtk.TreeViewColumn("Number")
1080 self._numberColumn.pack_start(textrenderer, expand=True)
1081 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
1083 textrenderer = gtk.CellRendererText()
1084 textrenderer.set_property("yalign", 0)
1085 hildonize.set_cell_thumb_selectable(textrenderer)
1086 self._nameColumn = gtk.TreeViewColumn("From")
1087 self._nameColumn.pack_start(textrenderer, expand=True)
1088 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
1089 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1091 self._window = gtk_toolbox.find_parent_window(self._historyview)
1093 self._historyFilterSelector = widgetTree.get_widget("historyFilterSelector")
1094 self._historyFilterSelector.connect("clicked", self._on_history_filter_clicked)
1095 self._selectedFilter = "All"
1097 self._updateSink = gtk_toolbox.threaded_stage(
1099 self._idly_populate_historyview,
1100 gtk_toolbox.null_sink(),
1105 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1106 self._historyFilterSelector.set_label(self._selectedFilter)
1108 self._historyview.set_model(self._historymodelfiltered)
1109 self._historyview.set_fixed_height_mode(False)
1111 self._historyview.append_column(self._dateColumn)
1112 self._historyview.append_column(self._actionColumn)
1113 self._historyview.append_column(self._numberColumn)
1114 self._historyview.append_column(self._nameColumn)
1115 self._historyviewselection = self._historyview.get_selection()
1116 self._historyviewselection.set_mode(gtk.SELECTION_SINGLE)
1118 self._onRecentviewRowActivatedId = self._historyview.connect("row-activated", self._on_historyview_row_activated)
1121 self._historyview.disconnect(self._onRecentviewRowActivatedId)
1125 self._historyview.remove_column(self._dateColumn)
1126 self._historyview.remove_column(self._actionColumn)
1127 self._historyview.remove_column(self._nameColumn)
1128 self._historyview.remove_column(self._numberColumn)
1129 self._historyview.set_model(None)
1131 def add_contact(self, *args, **kwds):
1133 @note Actual dial function is patched in later
1135 raise NotImplementedError("Horrible unknown error has occurred")
1137 def update(self, force = False):
1138 if not force and self._isPopulated:
1140 self._updateSink.send(())
1144 self._isPopulated = False
1145 self._historymodel.clear()
1149 return "Recent Calls"
1151 def load_settings(self, config, sectionName):
1153 self._selectedFilter = config.get(sectionName, "filter")
1154 if self._selectedFilter not in self.HISTORY_ITEM_TYPES:
1155 self._messageType = self.HISTORY_ITEM_TYPES[0]
1156 except ConfigParser.NoOptionError:
1159 def save_settings(self, config, sectionName):
1161 @note Thread Agnostic
1163 config.set(sectionName, "filter", self._selectedFilter)
1165 def _is_history_visible(self, model, iter):
1167 action = model.get_value(iter, self.ACTION_IDX)
1169 return False # this seems weird but oh well
1171 if self._selectedFilter in [action, "All"]:
1175 except Exception, e:
1176 self._errorDisplay.push_exception()
1178 def _idly_populate_historyview(self):
1179 with gtk_toolbox.gtk_lock():
1180 banner = hildonize.show_busy_banner_start(self._window, "Loading Call History")
1182 self._historymodel.clear()
1183 self._isPopulated = True
1186 historyItems = self._backend.get_recent()
1187 except Exception, e:
1188 self._errorDisplay.push_exception_with_lock()
1189 self._isPopulated = False
1193 gv_backend.decorate_recent(data)
1194 for data in gv_backend.sort_messages(historyItems)
1197 for contactId, personName, phoneNumber, date, action in historyItems:
1199 personName = "Unknown"
1200 date = abbrev_relative_date(date)
1201 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1202 prettyNumber = make_pretty(prettyNumber)
1203 item = (prettyNumber, date, action.capitalize(), personName, contactId)
1204 with gtk_toolbox.gtk_lock():
1205 self._historymodel.append(item)
1206 except Exception, e:
1207 self._errorDisplay.push_exception_with_lock()
1209 with gtk_toolbox.gtk_lock():
1210 hildonize.show_busy_banner_end(banner)
1214 def _on_history_filter_clicked(self, *args, **kwds):
1216 selectedComboIndex = self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
1219 newSelectedComboIndex = hildonize.touch_selector(
1222 self.HISTORY_ITEM_TYPES,
1225 except RuntimeError:
1228 option = self.HISTORY_ITEM_TYPES[newSelectedComboIndex]
1229 self._selectedFilter = option
1230 self._historyFilterSelector.set_label(self._selectedFilter)
1231 self._historymodelfiltered.refilter()
1232 except Exception, e:
1233 self._errorDisplay.push_exception()
1235 def _history_summary(self, expectedNumber):
1236 for number, action, date, whoFrom, whoFromId in self._historymodel:
1237 if expectedNumber is not None and expectedNumber == number:
1238 yield "%s <i>(%s)</i> - %s %s" % (number, whoFrom, date, action)
1240 def _on_historyview_row_activated(self, treeview, path, view_column):
1242 childPath = self._historymodelfiltered.convert_path_to_child_path(path)
1243 itr = self._historymodel.get_iter(childPath)
1247 prettyNumber = self._historymodel.get_value(itr, self.NUMBER_IDX)
1248 number = make_ugly(prettyNumber)
1249 description = list(self._history_summary(prettyNumber))
1250 contactName = self._historymodel.get_value(itr, self.FROM_IDX)
1251 contactId = self._historymodel.get_value(itr, self.FROM_ID_IDX)
1252 contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1256 contactPhoneNumbers,
1257 messages = description,
1258 defaultIndex = defaultIndex,
1260 self._historyviewselection.unselect_all()
1261 except Exception, e:
1262 self._errorDisplay.push_exception()
1265 class MessagesView(object):
1273 MESSAGE_DATA_IDX = 6
1275 NO_MESSAGES = "None"
1276 VOICEMAIL_MESSAGES = "Voicemail"
1277 TEXT_MESSAGES = "SMS"
1278 ALL_TYPES = "All Messages"
1279 MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
1281 UNREAD_STATUS = "Unread"
1282 UNARCHIVED_STATUS = "Inbox"
1284 MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
1286 def __init__(self, widgetTree, backend, errorDisplay):
1287 self._errorDisplay = errorDisplay
1288 self._backend = backend
1290 self._isPopulated = False
1291 self._messagemodel = gtk.ListStore(
1292 gobject.TYPE_STRING, # number
1293 gobject.TYPE_STRING, # date
1294 gobject.TYPE_STRING, # header
1295 gobject.TYPE_STRING, # message
1297 gobject.TYPE_STRING, # from id
1298 object, # message data
1300 self._messagemodelfiltered = self._messagemodel.filter_new()
1301 self._messagemodelfiltered.set_visible_func(self._is_message_visible)
1302 self._messageview = widgetTree.get_widget("messages_view")
1303 self._messageviewselection = None
1304 self._onMessageviewRowActivatedId = 0
1306 self._messageRenderer = gtk.CellRendererText()
1307 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1308 self._messageRenderer.set_property("wrap-width", 500)
1309 self._messageColumn = gtk.TreeViewColumn("Messages")
1310 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1311 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1312 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1314 self._window = gtk_toolbox.find_parent_window(self._messageview)
1316 self._messageTypeButton = widgetTree.get_widget("messageTypeButton")
1317 self._onMessageTypeClickedId = 0
1318 self._messageType = self.ALL_TYPES
1319 self._messageStatusButton = widgetTree.get_widget("messageStatusButton")
1320 self._onMessageStatusClickedId = 0
1321 self._messageStatus = self.ALL_STATUS
1323 self._updateSink = gtk_toolbox.threaded_stage(
1325 self._idly_populate_messageview,
1326 gtk_toolbox.null_sink(),
1331 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1332 self._messageview.set_model(self._messagemodelfiltered)
1333 self._messageview.set_headers_visible(False)
1334 self._messageview.set_fixed_height_mode(False)
1336 self._messageview.append_column(self._messageColumn)
1337 self._messageviewselection = self._messageview.get_selection()
1338 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1340 self._messageTypeButton.set_label(self._messageType)
1341 self._messageStatusButton.set_label(self._messageStatus)
1343 self._onMessageviewRowActivatedId = self._messageview.connect(
1344 "row-activated", self._on_messageview_row_activated
1346 self._onMessageTypeClickedId = self._messageTypeButton.connect(
1347 "clicked", self._on_message_type_clicked
1349 self._onMessageStatusClickedId = self._messageStatusButton.connect(
1350 "clicked", self._on_message_status_clicked
1354 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1355 self._messageTypeButton.disconnect(self._onMessageTypeClickedId)
1356 self._messageStatusButton.disconnect(self._onMessageStatusClickedId)
1360 self._messageview.remove_column(self._messageColumn)
1361 self._messageview.set_model(None)
1363 def add_contact(self, *args, **kwds):
1365 @note Actual dial function is patched in later
1367 raise NotImplementedError("Horrible unknown error has occurred")
1369 def update(self, force = False):
1370 if not force and self._isPopulated:
1372 self._updateSink.send(())
1376 self._isPopulated = False
1377 self._messagemodel.clear()
1383 def load_settings(self, config, sectionName):
1385 self._messageType = config.get(sectionName, "type")
1386 if self._messageType not in self.MESSAGE_TYPES:
1387 self._messageType = self.ALL_TYPES
1388 self._messageStatus = config.get(sectionName, "status")
1389 if self._messageStatus not in self.MESSAGE_STATUSES:
1390 self._messageStatus = self.ALL_STATUS
1391 except ConfigParser.NoOptionError:
1394 def save_settings(self, config, sectionName):
1396 @note Thread Agnostic
1398 config.set(sectionName, "status", self._messageStatus)
1399 config.set(sectionName, "type", self._messageType)
1401 def _is_message_visible(self, model, iter):
1403 message = model.get_value(iter, self.MESSAGE_DATA_IDX)
1405 return False # this seems weird but oh well
1406 return self._filter_messages(message, self._messageType, self._messageStatus)
1407 except Exception, e:
1408 self._errorDisplay.push_exception()
1411 def _filter_messages(cls, message, type, status):
1412 if type == cls.ALL_TYPES:
1415 messageType = message["type"]
1416 isType = messageType == type
1418 if status == cls.ALL_STATUS:
1421 isUnarchived = not message["isArchived"]
1422 isUnread = not message["isRead"]
1423 if status == cls.UNREAD_STATUS:
1424 isStatus = isUnarchived and isUnread
1425 elif status == cls.UNARCHIVED_STATUS:
1426 isStatus = isUnarchived
1428 assert "Status %s is bad for %r" % (status, message)
1430 return isType and isStatus
1432 _MIN_MESSAGES_SHOWN = 4
1434 def _idly_populate_messageview(self):
1435 with gtk_toolbox.gtk_lock():
1436 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1438 self._messagemodel.clear()
1439 self._isPopulated = True
1441 if self._messageType == self.NO_MESSAGES:
1445 messageItems = self._backend.get_messages()
1446 except Exception, e:
1447 self._errorDisplay.push_exception_with_lock()
1448 self._isPopulated = False
1452 (gv_backend.decorate_message(message), message)
1453 for message in gv_backend.sort_messages(messageItems)
1456 for (contactId, header, number, relativeDate, messages), messageData in messageItems:
1457 prettyNumber = number[2:] if number.startswith("+1") else number
1458 prettyNumber = make_pretty(prettyNumber)
1460 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1461 expandedMessages = [firstMessage]
1462 expandedMessages.extend(messages)
1463 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1464 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1465 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1466 collapsedMessages = [firstMessage, secondMessage]
1467 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1469 collapsedMessages = expandedMessages
1470 #collapsedMessages = _collapse_message(collapsedMessages, 60, self._MIN_MESSAGES_SHOWN)
1472 number = make_ugly(number)
1474 row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId, messageData
1475 with gtk_toolbox.gtk_lock():
1476 self._messagemodel.append(row)
1477 except Exception, e:
1478 self._errorDisplay.push_exception_with_lock()
1480 with gtk_toolbox.gtk_lock():
1481 hildonize.show_busy_banner_end(banner)
1482 self._messagemodelfiltered.refilter()
1486 def _on_messageview_row_activated(self, treeview, path, view_column):
1488 childPath = self._messagemodelfiltered.convert_path_to_child_path(path)
1489 itr = self._messagemodel.get_iter(childPath)
1493 number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX))
1494 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1496 contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX)
1497 header = self._messagemodel.get_value(itr, self.HEADER_IDX)
1498 contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1502 contactPhoneNumbers,
1503 messages = description,
1504 defaultIndex = defaultIndex,
1506 self._messageviewselection.unselect_all()
1507 except Exception, e:
1508 self._errorDisplay.push_exception()
1510 def _on_message_type_clicked(self, *args, **kwds):
1512 selectedIndex = self.MESSAGE_TYPES.index(self._messageType)
1515 newSelectedIndex = hildonize.touch_selector(
1521 except RuntimeError:
1524 if selectedIndex != newSelectedIndex:
1525 self._messageType = self.MESSAGE_TYPES[newSelectedIndex]
1526 self._messageTypeButton.set_label(self._messageType)
1527 self._messagemodelfiltered.refilter()
1528 except Exception, e:
1529 self._errorDisplay.push_exception()
1531 def _on_message_status_clicked(self, *args, **kwds):
1533 selectedIndex = self.MESSAGE_STATUSES.index(self._messageStatus)
1536 newSelectedIndex = hildonize.touch_selector(
1539 self.MESSAGE_STATUSES,
1542 except RuntimeError:
1545 if selectedIndex != newSelectedIndex:
1546 self._messageStatus = self.MESSAGE_STATUSES[newSelectedIndex]
1547 self._messageStatusButton.set_label(self._messageStatus)
1548 self._messagemodelfiltered.refilter()
1549 except Exception, e:
1550 self._errorDisplay.push_exception()
1553 class ContactsView(object):
1555 CONTACT_TYPE_IDX = 0
1556 CONTACT_NAME_IDX = 1
1559 def __init__(self, widgetTree, backend, errorDisplay):
1560 self._errorDisplay = errorDisplay
1561 self._backend = backend
1563 self._addressBook = None
1564 self._selectedComboIndex = 0
1565 self._addressBookFactories = [null_backend.NullAddressBook()]
1567 self._booksList = []
1568 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1570 self._isPopulated = False
1571 self._contactsmodel = gtk.ListStore(
1572 gobject.TYPE_STRING, # Contact Type
1573 gobject.TYPE_STRING, # Contact Name
1574 gobject.TYPE_STRING, # Contact ID
1576 self._contactsviewselection = None
1577 self._contactsview = widgetTree.get_widget("contactsview")
1579 self._contactColumn = gtk.TreeViewColumn("Contact")
1580 displayContactSource = False
1581 if displayContactSource:
1582 textrenderer = gtk.CellRendererText()
1583 self._contactColumn.pack_start(textrenderer, expand=False)
1584 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_TYPE_IDX)
1585 textrenderer = gtk.CellRendererText()
1586 hildonize.set_cell_thumb_selectable(textrenderer)
1587 self._contactColumn.pack_start(textrenderer, expand=True)
1588 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_NAME_IDX)
1589 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1590 self._contactColumn.set_sort_column_id(1)
1591 self._contactColumn.set_visible(True)
1593 self._onContactsviewRowActivatedId = 0
1594 self._onAddressbookButtonChangedId = 0
1595 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1597 self._updateSink = gtk_toolbox.threaded_stage(
1599 self._idly_populate_contactsview,
1600 gtk_toolbox.null_sink(),
1605 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1607 self._contactsview.set_model(self._contactsmodel)
1608 self._contactsview.set_fixed_height_mode(False)
1609 self._contactsview.append_column(self._contactColumn)
1610 self._contactsviewselection = self._contactsview.get_selection()
1611 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1613 del self._booksList[:]
1614 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1615 if factoryName and bookName:
1616 entryName = "%s: %s" % (factoryName, bookName)
1618 entryName = factoryName
1620 entryName = bookName
1622 entryName = "Bad name (%d)" % factoryId
1623 row = (str(factoryId), bookId, entryName)
1624 self._booksList.append(row)
1626 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1627 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1629 if len(self._booksList) <= self._selectedComboIndex:
1630 self._selectedComboIndex = 0
1631 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1633 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1634 selectedBookId = self._booksList[self._selectedComboIndex][1]
1635 self.open_addressbook(selectedFactoryId, selectedBookId)
1638 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1639 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1643 self._bookSelectionButton.set_label("")
1644 self._contactsview.set_model(None)
1645 self._contactsview.remove_column(self._contactColumn)
1647 def add_contact(self, *args, **kwds):
1649 @note Actual dial function is patched in later
1651 raise NotImplementedError("Horrible unknown error has occurred")
1653 def get_addressbooks(self):
1655 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1657 for i, factory in enumerate(self._addressBookFactories):
1658 for bookFactory, bookId, bookName in factory.get_addressbooks():
1659 yield (str(i), bookId), (factory.factory_name(), bookName)
1661 def open_addressbook(self, bookFactoryId, bookId):
1662 bookFactoryIndex = int(bookFactoryId)
1663 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1664 self._addressBook = addressBook
1666 def update(self, force = False):
1667 if not force and self._isPopulated:
1669 self._updateSink.send(())
1673 self._isPopulated = False
1674 self._contactsmodel.clear()
1675 for factory in self._addressBookFactories:
1676 factory.clear_caches()
1677 self._addressBook.clear_caches()
1679 def append(self, book):
1680 self._addressBookFactories.append(book)
1682 def extend(self, books):
1683 self._addressBookFactories.extend(books)
1689 def load_settings(self, config, sectionName):
1691 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1692 except ConfigParser.NoOptionError:
1693 self._selectedComboIndex = 0
1695 def save_settings(self, config, sectionName):
1696 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1698 def _idly_populate_contactsview(self):
1699 with gtk_toolbox.gtk_lock():
1700 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1703 while addressBook is not self._addressBook:
1704 addressBook = self._addressBook
1705 with gtk_toolbox.gtk_lock():
1706 self._contactsview.set_model(None)
1710 contacts = addressBook.get_contacts()
1711 except Exception, e:
1713 self._isPopulated = False
1714 self._errorDisplay.push_exception_with_lock()
1715 for contactId, contactName in contacts:
1716 contactType = addressBook.contact_source_short_name(contactId)
1717 row = contactType, contactName, contactId
1718 self._contactsmodel.append(row)
1720 with gtk_toolbox.gtk_lock():
1721 self._contactsview.set_model(self._contactsmodel)
1723 self._isPopulated = True
1724 except Exception, e:
1725 self._errorDisplay.push_exception_with_lock()
1727 with gtk_toolbox.gtk_lock():
1728 hildonize.show_busy_banner_end(banner)
1731 def _on_addressbook_button_changed(self, *args, **kwds):
1734 newSelectedComboIndex = hildonize.touch_selector(
1737 (("%s" % m[2]) for m in self._booksList),
1738 self._selectedComboIndex,
1740 except RuntimeError:
1743 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1744 selectedBookId = self._booksList[newSelectedComboIndex][1]
1746 oldAddressbook = self._addressBook
1747 self.open_addressbook(selectedFactoryId, selectedBookId)
1748 forceUpdate = True if oldAddressbook is not self._addressBook else False
1749 self.update(force=forceUpdate)
1751 self._selectedComboIndex = newSelectedComboIndex
1752 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1753 except Exception, e:
1754 self._errorDisplay.push_exception()
1756 def _on_contactsview_row_activated(self, treeview, path, view_column):
1758 itr = self._contactsmodel.get_iter(path)
1762 contactId = self._contactsmodel.get_value(itr, self.CONTACT_ID_IDX)
1763 contactName = self._contactsmodel.get_value(itr, self.CONTACT_NAME_IDX)
1765 contactDetails = self._addressBook.get_contact_details(contactId)
1766 except Exception, e:
1768 self._errorDisplay.push_exception()
1769 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1771 if len(contactPhoneNumbers) == 0:
1776 contactPhoneNumbers,
1777 messages = (contactName, ),
1779 self._contactsviewselection.unselect_all()
1780 except Exception, e:
1781 self._errorDisplay.push_exception()