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 10 <= len(uglynumber):
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("1"):
102 prettynumber += _make_pretty_with_areacode(phonenumber[1:])
106 def make_pretty(phonenumber):
108 Function to take a phone number and return the pretty version
110 if phonenumber begins with 0:
112 if phonenumber begins with 1: ( for gizmo callback numbers )
114 if phonenumber is 13 digits:
116 if phonenumber is 10 digits:
118 >>> make_pretty("12")
120 >>> make_pretty("1234567")
122 >>> make_pretty("2345678901")
124 >>> make_pretty("12345678901")
126 >>> make_pretty("01234567890")
128 >>> make_pretty("+01234567890")
130 >>> make_pretty("+12")
132 >>> make_pretty("+123")
134 >>> make_pretty("+1234")
137 if phonenumber is None or phonenumber == "":
140 phonenumber = normalize_number(phonenumber)
142 if phonenumber == "":
144 elif phonenumber[0] == "+":
145 prettynumber = _make_pretty_international(phonenumber[1:])
146 if not prettynumber.startswith("+"):
147 prettynumber = "+"+prettynumber
148 elif 8 < len(phonenumber) and phonenumber[0] in ("1", ):
149 prettynumber = _make_pretty_international(phonenumber)
150 elif 7 < len(phonenumber):
151 prettynumber = _make_pretty_with_areacode(phonenumber)
152 elif 3 < len(phonenumber):
153 prettynumber = _make_pretty_local(phonenumber)
155 prettynumber = phonenumber
156 return prettynumber.strip()
159 def abbrev_relative_date(date):
161 >>> abbrev_relative_date("42 hours ago")
163 >>> abbrev_relative_date("2 days ago")
165 >>> abbrev_relative_date("4 weeks ago")
168 parts = date.split(" ")
169 return "%s %s" % (parts[0], parts[1][0])
172 def _collapse_message(messageLines, maxCharsPerLine, maxLines):
175 numLines = len(messageLines)
176 for line in messageLines[0:min(maxLines, numLines)]:
177 linesPerLine = max(1, int(len(line) / maxCharsPerLine))
178 allowedLines = maxLines - lines
179 acceptedLines = min(allowedLines, linesPerLine)
180 acceptedChars = acceptedLines * maxCharsPerLine
182 if acceptedChars < (len(line) + 3):
185 acceptedChars = len(line) # eh, might as well complete the line
187 abbrevMessage = "%s%s" % (line[0:acceptedChars], suffix)
190 lines += acceptedLines
191 if maxLines <= lines:
195 def collapse_message(message, maxCharsPerLine, maxLines):
197 >>> collapse_message("Hello", 60, 2)
199 >>> collapse_message("Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789", 60, 2)
200 'Hello world how are you doing today? 01234567890123456789012...'
201 >>> collapse_message('''Hello world how are you doing today?
202 ... 01234567890123456789
203 ... 01234567890123456789
204 ... 01234567890123456789
205 ... 01234567890123456789''', 60, 2)
206 'Hello world how are you doing today?\n01234567890123456789'
207 >>> collapse_message('''
208 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
209 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
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''', 60, 2)
214 '\nHello world how are you doing today? 01234567890123456789012...'
216 messageLines = message.split("\n")
217 return "\n".join(_collapse_message(messageLines, maxCharsPerLine, maxLines))
220 def _get_contact_numbers(backend, contactId, number):
221 if contactId and contactId != '0':
222 contactPhoneNumbers = list(backend.get_contact_details(contactId))
223 uglyContactNumbers = (
224 make_ugly(contactNumber)
225 for (numberDescription, contactNumber) in contactPhoneNumbers
229 number == contactNumber or
230 number[1:] == contactNumber and number.startswith("1") or
231 number[2:] == contactNumber and number.startswith("+1") or
232 number == contactNumber[1:] and contactNumber.startswith("1") or
233 number == contactNumber[2:] and contactNumber.startswith("+1")
235 for contactNumber in uglyContactNumbers
238 defaultIndex = defaultMatches.index(True)
240 contactPhoneNumbers.append(("Other", number))
241 defaultIndex = len(contactPhoneNumbers)-1
243 "Could not find contact %r's number %s among %r" % (
244 contactId, number, contactPhoneNumbers
248 contactPhoneNumbers = [("Phone", number)]
251 return contactPhoneNumbers, defaultIndex
254 class SmsEntryWindow(object):
258 def __init__(self, widgetTree, parent, app):
259 self._clipboard = gtk.clipboard_get()
260 self._widgetTree = widgetTree
261 self._parent = parent
263 self._isFullScreen = False
265 self._window = self._widgetTree.get_widget("smsWindow")
266 self._window = hildonize.hildonize_window(self._app, self._window)
267 self._window.set_title("SMS")
268 self._window.connect("delete-event", self._on_delete)
269 self._window.connect("key-press-event", self._on_key_press)
270 self._window.connect("window-state-event", self._on_window_state_change)
271 self._widgetTree.get_widget("smsMessagesViewPort").get_parent().show()
273 errorBox = self._widgetTree.get_widget("smsErrorEventBox")
274 errorDescription = self._widgetTree.get_widget("smsErrorDescription")
275 errorClose = self._widgetTree.get_widget("smsErrorClose")
276 self._errorDisplay = gtk_toolbox.ErrorDisplay(errorBox, errorDescription, errorClose)
278 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
279 self._smsButton.connect("clicked", self._on_send)
280 self._dialButton = self._widgetTree.get_widget("dialButton")
281 self._dialButton.connect("clicked", self._on_dial)
283 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
285 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
286 self._messagesView = self._widgetTree.get_widget("smsMessages")
288 textrenderer = gtk.CellRendererText()
289 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
290 textrenderer.set_property("wrap-width", 450)
291 messageColumn = gtk.TreeViewColumn("")
292 messageColumn.pack_start(textrenderer, expand=True)
293 messageColumn.add_attribute(textrenderer, "markup", 0)
294 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
295 self._messagesView.append_column(messageColumn)
296 self._messagesView.set_headers_visible(False)
297 self._messagesView.set_model(self._messagemodel)
298 self._messagesView.set_fixed_height_mode(False)
300 self._conversationView = self._messagesView.get_parent()
301 self._conversationViewPort = self._conversationView.get_parent()
302 self._scrollWindow = self._conversationViewPort.get_parent()
304 self._targetList = self._widgetTree.get_widget("smsTargetList")
305 self._phoneButton = self._widgetTree.get_widget("phoneTypeSelection")
306 self._phoneButton.connect("clicked", self._on_phone)
307 self._smsEntry = self._widgetTree.get_widget("smsEntry")
308 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
309 self._smsEntrySize = None
313 def add_contact(self, name, contactDetails, messages = (), defaultIndex = -1):
314 contactNumbers = list(self._to_contact_numbers(contactDetails))
315 assert contactNumbers, "Contact must have at least one number"
316 contactIndex = defaultIndex if defaultIndex != -1 else 0
317 contact = contactNumbers, contactIndex, messages
318 self._contacts.append(contact)
320 nameLabel = gtk.Label(name)
321 selector = gtk.Button(contactNumbers[0][1])
322 if len(contactNumbers) == 1:
323 selector.set_sensitive(False)
324 removeContact = gtk.Button(stock="gtk-delete")
326 row.pack_start(nameLabel, True, True)
327 row.pack_start(selector, True, True)
328 row.pack_start(removeContact, False, False)
330 self._targetList.pack_start(row)
331 selector.connect("clicked", self._on_choose_phone_n, row)
332 removeContact.connect("clicked", self._on_remove_phone_n, row)
333 self._update_button_state()
334 self._update_context()
336 parentSize = self._parent.get_size()
337 self._window.resize(parentSize[0], max(parentSize[1]-10, 100))
339 self._window.present()
341 self._smsEntry.grab_focus()
342 self._scroll_to_bottom()
345 del self._contacts[:]
347 for row in list(self._targetList.get_children()):
348 self._targetList.remove(row)
349 self._smsEntry.get_buffer().set_text("")
350 self._update_letter_count()
351 self._update_context()
353 def fullscreen(self):
354 self._window.fullscreen()
356 def unfullscreen(self):
357 self._window.unfullscreen()
359 def _remove_contact(self, contactIndex):
360 del self._contacts[contactIndex]
362 row = list(self._targetList.get_children())[contactIndex]
363 self._targetList.remove(row)
364 self._update_button_state()
365 self._update_context()
366 self._scroll_to_bottom()
368 def _scroll_to_bottom(self):
369 dx = self._conversationView.get_allocation().height - self._conversationViewPort.get_allocation().height
371 adjustment = self._scrollWindow.get_vadjustment()
372 adjustment.value = dx
374 def _update_letter_count(self):
375 if self._smsEntrySize is None:
376 self._smsEntrySize = self._smsEntry.size_request()
378 self._smsEntry.set_size_request(*self._smsEntrySize)
379 entryLength = self._smsEntry.get_buffer().get_char_count()
381 numTexts, numCharInText = divmod(entryLength, self.MAX_CHAR)
383 self._letterCountLabel.set_text("%s.%s" % (numTexts, numCharInText))
385 self._letterCountLabel.set_text("%s" % (numCharInText, ))
387 self._update_button_state()
389 def _update_context(self):
390 self._messagemodel.clear()
391 if len(self._contacts) == 0:
392 self._messagesView.hide()
393 self._targetList.hide()
394 self._phoneButton.hide()
395 self._phoneButton.set_label("Error: You shouldn't see this")
396 elif len(self._contacts) == 1:
397 contactNumbers, index, messages = self._contacts[0]
399 self._messagesView.show()
400 for message in messages:
402 self._messagemodel.append(row)
403 messagesSelection = self._messagesView.get_selection()
404 messagesSelection.select_path((len(messages)-1, ))
406 self._messagesView.hide()
407 self._targetList.hide()
408 self._phoneButton.show()
409 self._phoneButton.set_label(contactNumbers[index][1])
410 if 1 < len(contactNumbers):
411 self._phoneButton.set_sensitive(True)
413 self._phoneButton.set_sensitive(False)
415 self._messagesView.hide()
416 self._targetList.show()
417 self._phoneButton.hide()
418 self._phoneButton.set_label("Error: You shouldn't see this")
420 def _update_button_state(self):
421 if len(self._contacts) == 0:
422 self._dialButton.set_sensitive(False)
423 self._smsButton.set_sensitive(False)
424 elif len(self._contacts) == 1:
425 entryLength = self._smsEntry.get_buffer().get_char_count()
427 self._dialButton.set_sensitive(True)
428 self._smsButton.set_sensitive(False)
430 self._dialButton.set_sensitive(False)
431 self._smsButton.set_sensitive(True)
433 self._dialButton.set_sensitive(False)
434 self._smsButton.set_sensitive(True)
436 def _to_contact_numbers(self, contactDetails):
437 for phoneType, phoneNumber in contactDetails:
438 display = " - ".join((make_pretty(phoneNumber), phoneType))
439 yield (phoneNumber, display)
441 def _pseudo_destroy(self):
445 def _request_number(self, contactIndex):
446 contactNumbers, index, messages = self._contacts[contactIndex]
447 assert 0 <= index, "%r" % index
449 index = hildonize.touch_selector(
452 (description for (number, description) in contactNumbers),
455 self._contacts[contactIndex] = contactNumbers, index, messages
457 def send_sms(self, numbers, message):
458 raise NotImplementedError()
460 def dial(self, number):
461 raise NotImplementedError()
463 def _on_phone(self, *args):
465 assert len(self._contacts) == 1, "One and only one contact is required"
466 self._request_number(0)
468 contactNumbers, numberIndex, messages = self._contacts[0]
469 self._phoneButton.set_label(contactNumbers[numberIndex][1])
470 row = list(self._targetList.get_children())[0]
471 phoneButton = list(row.get_children())[1]
472 phoneButton.set_label(contactNumbers[numberIndex][1])
474 self._errorDisplay.push_exception()
476 def _on_choose_phone_n(self, button, row):
478 assert 1 < len(self._contacts), "More than one contact required"
479 targetList = list(self._targetList.get_children())
480 index = targetList.index(row)
481 self._request_number(index)
483 contactNumbers, numberIndex, messages = self._contacts[0]
484 phoneButton = list(row.get_children())[1]
485 phoneButton.set_label(contactNumbers[numberIndex][1])
487 self._errorDisplay.push_exception()
489 def _on_remove_phone_n(self, button, row):
491 assert 1 < len(self._contacts), "More than one contact required"
492 targetList = list(self._targetList.get_children())
493 index = targetList.index(row)
495 del self._contacts[index]
496 self._targetList.remove(row)
497 self._update_context()
498 self._update_button_state()
500 self._errorDisplay.push_exception()
502 def _on_entry_changed(self, *args):
504 self._update_letter_count()
506 self._errorDisplay.push_exception()
508 def _on_send(self, *args):
510 assert 0 < len(self._contacts), "At least one contact required (%r)" % self._contacts
512 make_ugly(contact[0][contact[1]][0])
513 for contact in self._contacts
516 entryBuffer = self._smsEntry.get_buffer()
517 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
518 enteredMessage = enteredMessage.strip()
519 assert enteredMessage, "No message provided"
520 self.send_sms(phoneNumbers, enteredMessage)
521 self._pseudo_destroy()
523 self._errorDisplay.push_exception()
525 def _on_dial(self, *args):
527 assert len(self._contacts) == 1, "One and only one contact allowed (%r)" % self._contacts
528 contact = self._contacts[0]
529 contactNumber = contact[0][contact[1]][0]
530 phoneNumber = make_ugly(contactNumber)
531 self.dial(phoneNumber)
532 self._pseudo_destroy()
534 self._errorDisplay.push_exception()
536 def _on_delete(self, *args):
538 self._window.emit_stop_by_name("delete-event")
539 if hildonize.IS_FREMANTLE_SUPPORTED:
542 self._pseudo_destroy()
544 self._errorDisplay.push_exception()
547 def _on_window_state_change(self, widget, event, *args):
549 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
550 self._isFullScreen = True
552 self._isFullScreen = False
554 self._errorDisplay.push_exception()
556 def _on_key_press(self, widget, event):
557 RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
560 event.keyval == gtk.keysyms.F6 or
561 event.keyval in RETURN_TYPES and event.get_state() & gtk.gdk.CONTROL_MASK
563 if self._isFullScreen:
564 self._window.unfullscreen()
566 self._window.fullscreen()
567 elif event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
570 for messagePart in self._messagemodel
572 self._clipboard.set_text(str(message))
574 event.keyval == gtk.keysyms.h and
575 event.get_state() & gtk.gdk.CONTROL_MASK
579 event.keyval == gtk.keysyms.w and
580 event.get_state() & gtk.gdk.CONTROL_MASK
582 self._pseudo_destroy()
584 event.keyval == gtk.keysyms.q and
585 event.get_state() & gtk.gdk.CONTROL_MASK
587 self._parent.destroy()
589 self._errorDisplay.push_exception()
592 class Dialpad(object):
594 def __init__(self, widgetTree, errorDisplay):
595 self._clipboard = gtk.clipboard_get()
596 self._errorDisplay = errorDisplay
598 self._numberdisplay = widgetTree.get_widget("numberdisplay")
599 self._callButton = widgetTree.get_widget("dialpadCall")
600 self._sendSMSButton = widgetTree.get_widget("dialpadSMS")
601 self._backButton = widgetTree.get_widget("back")
602 self._plusButton = widgetTree.get_widget("plus")
603 self._phonenumber = ""
604 self._prettynumber = ""
607 "on_digit_clicked": self._on_digit_clicked,
609 widgetTree.signal_autoconnect(callbackMapping)
610 self._sendSMSButton.connect("clicked", self._on_sms_clicked)
611 self._callButton.connect("clicked", self._on_call_clicked)
612 self._plusButton.connect("clicked", self._on_plus)
614 self._originalLabel = self._backButton.get_label()
615 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
616 self._backTapHandler.on_tap = self._on_backspace
617 self._backTapHandler.on_hold = self._on_clearall
618 self._backTapHandler.on_holding = self._set_clear_button
619 self._backTapHandler.on_cancel = self._reset_back_button
621 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
622 self._keyPressEventId = 0
625 self._sendSMSButton.grab_focus()
626 self._backTapHandler.enable()
627 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
630 self._window.disconnect(self._keyPressEventId)
631 self._keyPressEventId = 0
632 self._reset_back_button()
633 self._backTapHandler.disable()
635 def add_contact(self, *args, **kwds):
637 @note Actual function is patched in later
639 raise NotImplementedError("Horrible unknown error has occurred")
641 def dial(self, number):
643 @note Actual function is patched in later
645 raise NotImplementedError("Horrible unknown error has occurred")
647 def get_number(self):
648 return self._phonenumber
650 def set_number(self, number):
652 Set the number to dial
655 self._phonenumber = make_ugly(number)
656 self._prettynumber = make_pretty(self._phonenumber)
657 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
658 if self._phonenumber:
659 self._plusButton.set_sensitive(False)
661 self._plusButton.set_sensitive(True)
663 self._errorDisplay.push_exception()
672 def load_settings(self, config, section):
675 def save_settings(self, config, section):
677 @note Thread Agnostic
681 def set_orientation(self, orientation):
682 if orientation == gtk.ORIENTATION_VERTICAL:
684 elif orientation == gtk.ORIENTATION_HORIZONTAL:
687 raise NotImplementedError(orientation)
689 def _on_key_press(self, widget, event):
691 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
692 contents = self._clipboard.wait_for_text()
693 if contents is not None:
694 self.set_number(contents)
696 self._errorDisplay.push_exception()
698 def _on_call_clicked(self, widget):
700 phoneNumber = self.get_number()
701 self.dial(phoneNumber)
704 self._errorDisplay.push_exception()
706 def _on_sms_clicked(self, widget):
708 phoneNumber = self.get_number()
711 [("Dialer", phoneNumber)], ()
715 self._errorDisplay.push_exception()
717 def _on_digit_clicked(self, widget):
719 self.set_number(self._phonenumber + widget.get_name()[-1])
721 self._errorDisplay.push_exception()
723 def _on_plus(self, *args):
725 self.set_number(self._phonenumber + "+")
727 self._errorDisplay.push_exception()
729 def _on_backspace(self, taps):
731 self.set_number(self._phonenumber[:-taps])
732 self._reset_back_button()
734 self._errorDisplay.push_exception()
736 def _on_clearall(self, taps):
739 self._reset_back_button()
741 self._errorDisplay.push_exception()
744 def _set_clear_button(self):
746 self._backButton.set_label("gtk-clear")
748 self._errorDisplay.push_exception()
750 def _reset_back_button(self):
752 self._backButton.set_label(self._originalLabel)
754 self._errorDisplay.push_exception()
757 class AccountInfo(object):
759 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
760 self._errorDisplay = errorDisplay
761 self._backend = backend
762 self._isPopulated = False
763 self._alarmHandler = alarmHandler
764 self._notifyOnMissed = False
765 self._notifyOnVoicemail = False
766 self._notifyOnSms = False
768 self._callbackList = []
769 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
770 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
771 self._onCallbackSelectChangedId = 0
773 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
774 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
775 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
776 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
777 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
778 self._onNotifyToggled = 0
779 self._onMinutesChanged = 0
780 self._onMissedToggled = 0
781 self._onVoicemailToggled = 0
782 self._onSmsToggled = 0
783 self._applyAlarmTimeoutId = None
785 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
786 self._callbackNumber = ""
789 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
791 self._accountViewNumberDisplay.set_use_markup(True)
792 self.set_account_number("")
794 del self._callbackList[:]
795 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
796 self._set_callback_label("")
798 if self._alarmHandler is not None:
799 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
800 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
801 self._missedCheckbox.set_active(self._notifyOnMissed)
802 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
803 self._smsCheckbox.set_active(self._notifyOnSms)
805 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
806 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
807 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
808 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
809 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
811 self._notifyCheckbox.set_sensitive(False)
812 self._minutesEntryButton.set_sensitive(False)
813 self._missedCheckbox.set_sensitive(False)
814 self._voicemailCheckbox.set_sensitive(False)
815 self._smsCheckbox.set_sensitive(False)
817 self.update(force=True)
820 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
821 self._onCallbackSelectChangedId = 0
822 self._set_callback_label("")
824 if self._alarmHandler is not None:
825 self._notifyCheckbox.disconnect(self._onNotifyToggled)
826 self._minutesEntryButton.disconnect(self._onMinutesChanged)
827 self._missedCheckbox.disconnect(self._onNotifyToggled)
828 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
829 self._smsCheckbox.disconnect(self._onNotifyToggled)
830 self._onNotifyToggled = 0
831 self._onMinutesChanged = 0
832 self._onMissedToggled = 0
833 self._onVoicemailToggled = 0
834 self._onSmsToggled = 0
836 self._notifyCheckbox.set_sensitive(True)
837 self._minutesEntryButton.set_sensitive(True)
838 self._missedCheckbox.set_sensitive(True)
839 self._voicemailCheckbox.set_sensitive(True)
840 self._smsCheckbox.set_sensitive(True)
843 del self._callbackList[:]
845 def set_account_number(self, number):
847 Displays current account number
849 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
851 def update(self, force = False):
852 if not force and self._isPopulated:
854 self._populate_callback_combo()
855 self.set_account_number(self._backend.get_account_number())
859 self._set_callback_label("")
860 self.set_account_number("")
861 self._isPopulated = False
863 def save_everything(self):
864 raise NotImplementedError
868 return "Account Info"
870 def load_settings(self, config, section):
871 self._callbackNumber = make_ugly(config.get(section, "callback"))
872 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
873 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
874 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
876 def save_settings(self, config, section):
878 @note Thread Agnostic
880 config.set(section, "callback", self._callbackNumber)
881 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
882 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
883 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
885 def set_orientation(self, orientation):
886 if orientation == gtk.ORIENTATION_VERTICAL:
888 elif orientation == gtk.ORIENTATION_HORIZONTAL:
891 raise NotImplementedError(orientation)
893 def _populate_callback_combo(self):
894 self._isPopulated = True
895 del self._callbackList[:]
897 callbackNumbers = self._backend.get_callback_numbers()
899 self._errorDisplay.push_exception()
900 self._isPopulated = False
903 if len(callbackNumbers) == 0:
904 callbackNumbers = {"": "No callback numbers available"}
906 for number, description in callbackNumbers.iteritems():
907 numberDisplay = make_pretty(number)
908 if not numberDisplay:
910 self._callbackList.append((numberDisplay, description))
912 self._set_callback_number(self._callbackNumber)
914 def _set_callback_number(self, number):
916 if not self._backend.is_valid_syntax(number) and 0 < len(number):
917 self._errorDisplay.push_message("%s is not a valid callback number" % number)
918 elif number == self._backend.get_callback_number() and 0 < len(number):
919 _moduleLogger.warning(
920 "Callback number already is %s" % (
921 self._backend.get_callback_number(),
924 self._set_callback_label(number)
926 if number.startswith("1747"): number = "+" + number
927 self._backend.set_callback_number(number)
928 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
929 make_pretty(number), make_pretty(self._backend.get_callback_number())
931 self._callbackNumber = make_ugly(number)
932 self._set_callback_label(number)
934 "Callback number set to %s" % (
935 self._backend.get_callback_number(),
939 self._errorDisplay.push_exception()
941 def _set_callback_label(self, uglyNumber):
942 prettyNumber = make_pretty(uglyNumber)
943 if len(prettyNumber) == 0:
944 prettyNumber = "No Callback Number"
945 self._callbackSelectButton.set_label(prettyNumber)
947 def _update_alarm_settings(self, recurrence):
949 isEnabled = self._notifyCheckbox.get_active()
950 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
951 self._alarmHandler.apply_settings(isEnabled, recurrence)
953 self.save_everything()
954 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
955 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
957 def _on_callbackentry_clicked(self, *args):
959 actualSelection = make_pretty(self._callbackNumber)
962 (number, "%s (%s)" % (number, description))
963 for (number, description) in self._callbackList
965 defaultSelection = userOptions.get(actualSelection, actualSelection)
967 userSelection = hildonize.touch_selector_entry(
970 list(userOptions.itervalues()),
973 reversedUserOptions = dict(
974 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
976 selectedNumber = reversedUserOptions.get(userSelection, userSelection)
978 number = make_ugly(selectedNumber)
979 self._set_callback_number(number)
980 except RuntimeError, e:
981 _moduleLogger.exception("%s" % str(e))
983 self._errorDisplay.push_exception()
985 def _on_notify_toggled(self, *args):
987 if self._applyAlarmTimeoutId is not None:
988 gobject.source_remove(self._applyAlarmTimeoutId)
989 self._applyAlarmTimeoutId = None
990 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
992 self._errorDisplay.push_exception()
994 def _on_minutes_clicked(self, *args):
995 recurrenceChoices = [
1008 (12*60, "12 hours"),
1011 actualSelection = self._alarmHandler.recurrence
1013 closestSelectionIndex = 0
1014 for i, possible in enumerate(recurrenceChoices):
1015 if possible[0] <= actualSelection:
1016 closestSelectionIndex = i
1017 recurrenceIndex = hildonize.touch_selector(
1020 (("%s" % m[1]) for m in recurrenceChoices),
1021 closestSelectionIndex,
1023 recurrence = recurrenceChoices[recurrenceIndex][0]
1025 self._update_alarm_settings(recurrence)
1026 except RuntimeError, e:
1027 _moduleLogger.exception("%s" % str(e))
1028 except Exception, e:
1029 self._errorDisplay.push_exception()
1031 def _on_apply_timeout(self, *args):
1033 self._applyAlarmTimeoutId = None
1035 self._update_alarm_settings(self._alarmHandler.recurrence)
1036 except Exception, e:
1037 self._errorDisplay.push_exception()
1040 def _on_missed_toggled(self, *args):
1042 self._notifyOnMissed = self._missedCheckbox.get_active()
1043 self.save_everything()
1044 except Exception, e:
1045 self._errorDisplay.push_exception()
1047 def _on_voicemail_toggled(self, *args):
1049 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
1050 self.save_everything()
1051 except Exception, e:
1052 self._errorDisplay.push_exception()
1054 def _on_sms_toggled(self, *args):
1056 self._notifyOnSms = self._smsCheckbox.get_active()
1057 self.save_everything()
1058 except Exception, e:
1059 self._errorDisplay.push_exception()
1062 class CallHistoryView(object):
1070 HISTORY_ITEM_TYPES = ["All", "Received", "Missed", "Placed"]
1072 def __init__(self, widgetTree, backend, errorDisplay):
1073 self._errorDisplay = errorDisplay
1074 self._backend = backend
1076 self._isPopulated = False
1077 self._historymodel = gtk.ListStore(
1078 gobject.TYPE_STRING, # number
1079 gobject.TYPE_STRING, # date
1080 gobject.TYPE_STRING, # action
1081 gobject.TYPE_STRING, # from
1082 gobject.TYPE_STRING, # from id
1084 self._historymodelfiltered = self._historymodel.filter_new()
1085 self._historymodelfiltered.set_visible_func(self._is_history_visible)
1086 self._historyview = widgetTree.get_widget("historyview")
1087 self._historyviewselection = None
1088 self._onRecentviewRowActivatedId = 0
1090 textrenderer = gtk.CellRendererText()
1091 textrenderer.set_property("yalign", 0)
1092 self._dateColumn = gtk.TreeViewColumn("Date")
1093 self._dateColumn.pack_start(textrenderer, expand=True)
1094 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
1096 textrenderer = gtk.CellRendererText()
1097 textrenderer.set_property("yalign", 0)
1098 self._actionColumn = gtk.TreeViewColumn("Action")
1099 self._actionColumn.pack_start(textrenderer, expand=True)
1100 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
1102 textrenderer = gtk.CellRendererText()
1103 textrenderer.set_property("yalign", 0)
1104 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
1105 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
1106 self._numberColumn = gtk.TreeViewColumn("Number")
1107 self._numberColumn.pack_start(textrenderer, expand=True)
1108 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
1110 textrenderer = gtk.CellRendererText()
1111 textrenderer.set_property("yalign", 0)
1112 hildonize.set_cell_thumb_selectable(textrenderer)
1113 self._nameColumn = gtk.TreeViewColumn("From")
1114 self._nameColumn.pack_start(textrenderer, expand=True)
1115 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
1116 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1118 self._window = gtk_toolbox.find_parent_window(self._historyview)
1120 self._historyFilterSelector = widgetTree.get_widget("historyFilterSelector")
1121 self._historyFilterSelector.connect("clicked", self._on_history_filter_clicked)
1122 self._selectedFilter = "All"
1124 self._updateSink = gtk_toolbox.threaded_stage(
1126 self._idly_populate_historyview,
1127 gtk_toolbox.null_sink(),
1132 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1133 self._historyFilterSelector.set_label(self._selectedFilter)
1135 self._historyview.set_model(self._historymodelfiltered)
1136 self._historyview.set_fixed_height_mode(False)
1138 self._historyview.append_column(self._dateColumn)
1139 self._historyview.append_column(self._actionColumn)
1140 self._historyview.append_column(self._numberColumn)
1141 self._historyview.append_column(self._nameColumn)
1142 self._historyviewselection = self._historyview.get_selection()
1143 self._historyviewselection.set_mode(gtk.SELECTION_SINGLE)
1145 self._onRecentviewRowActivatedId = self._historyview.connect("row-activated", self._on_historyview_row_activated)
1148 self._historyview.disconnect(self._onRecentviewRowActivatedId)
1152 self._historyview.remove_column(self._dateColumn)
1153 self._historyview.remove_column(self._actionColumn)
1154 self._historyview.remove_column(self._nameColumn)
1155 self._historyview.remove_column(self._numberColumn)
1156 self._historyview.set_model(None)
1158 def add_contact(self, *args, **kwds):
1160 @note Actual dial function is patched in later
1162 raise NotImplementedError("Horrible unknown error has occurred")
1164 def update(self, force = False):
1165 if not force and self._isPopulated:
1167 self._updateSink.send(())
1171 self._isPopulated = False
1172 self._historymodel.clear()
1176 return "Recent Calls"
1178 def load_settings(self, config, sectionName):
1180 self._selectedFilter = config.get(sectionName, "filter")
1181 if self._selectedFilter not in self.HISTORY_ITEM_TYPES:
1182 self._messageType = self.HISTORY_ITEM_TYPES[0]
1183 except ConfigParser.NoOptionError:
1186 def save_settings(self, config, sectionName):
1188 @note Thread Agnostic
1190 config.set(sectionName, "filter", self._selectedFilter)
1192 def set_orientation(self, orientation):
1193 if orientation == gtk.ORIENTATION_VERTICAL:
1195 elif orientation == gtk.ORIENTATION_HORIZONTAL:
1198 raise NotImplementedError(orientation)
1200 def _is_history_visible(self, model, iter):
1202 action = model.get_value(iter, self.ACTION_IDX)
1204 return False # this seems weird but oh well
1206 if self._selectedFilter in [action, "All"]:
1210 except Exception, e:
1211 self._errorDisplay.push_exception()
1213 def _idly_populate_historyview(self):
1214 with gtk_toolbox.gtk_lock():
1215 banner = hildonize.show_busy_banner_start(self._window, "Loading Call History")
1217 self._historymodel.clear()
1218 self._isPopulated = True
1221 historyItems = self._backend.get_recent()
1222 except Exception, e:
1223 self._errorDisplay.push_exception_with_lock()
1224 self._isPopulated = False
1228 gv_backend.decorate_recent(data)
1229 for data in gv_backend.sort_messages(historyItems)
1232 for contactId, personName, phoneNumber, date, action in historyItems:
1234 personName = "Unknown"
1235 date = abbrev_relative_date(date)
1236 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1237 prettyNumber = make_pretty(prettyNumber)
1238 item = (prettyNumber, date, action.capitalize(), personName, contactId)
1239 with gtk_toolbox.gtk_lock():
1240 self._historymodel.append(item)
1241 except Exception, e:
1242 self._errorDisplay.push_exception_with_lock()
1244 with gtk_toolbox.gtk_lock():
1245 hildonize.show_busy_banner_end(banner)
1249 def _on_history_filter_clicked(self, *args, **kwds):
1251 selectedComboIndex = self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
1254 newSelectedComboIndex = hildonize.touch_selector(
1257 self.HISTORY_ITEM_TYPES,
1260 except RuntimeError:
1263 option = self.HISTORY_ITEM_TYPES[newSelectedComboIndex]
1264 self._selectedFilter = option
1265 self._historyFilterSelector.set_label(self._selectedFilter)
1266 self._historymodelfiltered.refilter()
1267 except Exception, e:
1268 self._errorDisplay.push_exception()
1270 def _history_summary(self, expectedNumber):
1271 for number, action, date, whoFrom, whoFromId in self._historymodel:
1272 if expectedNumber is not None and expectedNumber == number:
1273 yield "%s <i>(%s)</i> - %s %s" % (number, whoFrom, date, action)
1275 def _on_historyview_row_activated(self, treeview, path, view_column):
1277 childPath = self._historymodelfiltered.convert_path_to_child_path(path)
1278 itr = self._historymodel.get_iter(childPath)
1282 prettyNumber = self._historymodel.get_value(itr, self.NUMBER_IDX)
1283 number = make_ugly(prettyNumber)
1284 description = list(self._history_summary(prettyNumber))
1285 contactName = self._historymodel.get_value(itr, self.FROM_IDX)
1286 contactId = self._historymodel.get_value(itr, self.FROM_ID_IDX)
1287 contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1291 contactPhoneNumbers,
1292 messages = description,
1293 defaultIndex = defaultIndex,
1295 self._historyviewselection.unselect_all()
1296 except Exception, e:
1297 self._errorDisplay.push_exception()
1300 class MessagesView(object):
1308 MESSAGE_DATA_IDX = 6
1310 NO_MESSAGES = "None"
1311 VOICEMAIL_MESSAGES = "Voicemail"
1312 TEXT_MESSAGES = "SMS"
1313 ALL_TYPES = "All Messages"
1314 MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
1316 UNREAD_STATUS = "Unread"
1317 UNARCHIVED_STATUS = "Inbox"
1319 MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
1321 def __init__(self, widgetTree, backend, errorDisplay):
1322 self._errorDisplay = errorDisplay
1323 self._backend = backend
1325 self._isPopulated = False
1326 self._messagemodel = gtk.ListStore(
1327 gobject.TYPE_STRING, # number
1328 gobject.TYPE_STRING, # date
1329 gobject.TYPE_STRING, # header
1330 gobject.TYPE_STRING, # message
1332 gobject.TYPE_STRING, # from id
1333 object, # message data
1335 self._messagemodelfiltered = self._messagemodel.filter_new()
1336 self._messagemodelfiltered.set_visible_func(self._is_message_visible)
1337 self._messageview = widgetTree.get_widget("messages_view")
1338 self._messageviewselection = None
1339 self._onMessageviewRowActivatedId = 0
1341 self._messageRenderer = gtk.CellRendererText()
1342 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1343 self._messageRenderer.set_property("wrap-width", 500)
1344 self._messageColumn = gtk.TreeViewColumn("Messages")
1345 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1346 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1347 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1349 self._window = gtk_toolbox.find_parent_window(self._messageview)
1351 self._messageTypeButton = widgetTree.get_widget("messageTypeButton")
1352 self._onMessageTypeClickedId = 0
1353 self._messageType = self.ALL_TYPES
1354 self._messageStatusButton = widgetTree.get_widget("messageStatusButton")
1355 self._onMessageStatusClickedId = 0
1356 self._messageStatus = self.ALL_STATUS
1358 self._updateSink = gtk_toolbox.threaded_stage(
1360 self._idly_populate_messageview,
1361 gtk_toolbox.null_sink(),
1366 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1367 self._messageview.set_model(self._messagemodelfiltered)
1368 self._messageview.set_headers_visible(False)
1369 self._messageview.set_fixed_height_mode(False)
1371 self._messageview.append_column(self._messageColumn)
1372 self._messageviewselection = self._messageview.get_selection()
1373 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1375 self._messageTypeButton.set_label(self._messageType)
1376 self._messageStatusButton.set_label(self._messageStatus)
1378 self._onMessageviewRowActivatedId = self._messageview.connect(
1379 "row-activated", self._on_messageview_row_activated
1381 self._onMessageTypeClickedId = self._messageTypeButton.connect(
1382 "clicked", self._on_message_type_clicked
1384 self._onMessageStatusClickedId = self._messageStatusButton.connect(
1385 "clicked", self._on_message_status_clicked
1389 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1390 self._messageTypeButton.disconnect(self._onMessageTypeClickedId)
1391 self._messageStatusButton.disconnect(self._onMessageStatusClickedId)
1395 self._messageview.remove_column(self._messageColumn)
1396 self._messageview.set_model(None)
1398 def add_contact(self, *args, **kwds):
1400 @note Actual dial function is patched in later
1402 raise NotImplementedError("Horrible unknown error has occurred")
1404 def update(self, force = False):
1405 if not force and self._isPopulated:
1407 self._updateSink.send(())
1411 self._isPopulated = False
1412 self._messagemodel.clear()
1418 def load_settings(self, config, sectionName):
1420 self._messageType = config.get(sectionName, "type")
1421 if self._messageType not in self.MESSAGE_TYPES:
1422 self._messageType = self.ALL_TYPES
1423 self._messageStatus = config.get(sectionName, "status")
1424 if self._messageStatus not in self.MESSAGE_STATUSES:
1425 self._messageStatus = self.ALL_STATUS
1426 except ConfigParser.NoOptionError:
1429 def save_settings(self, config, sectionName):
1431 @note Thread Agnostic
1433 config.set(sectionName, "status", self._messageStatus)
1434 config.set(sectionName, "type", self._messageType)
1436 def set_orientation(self, orientation):
1437 if orientation == gtk.ORIENTATION_VERTICAL:
1439 elif orientation == gtk.ORIENTATION_HORIZONTAL:
1442 raise NotImplementedError(orientation)
1444 def _is_message_visible(self, model, iter):
1446 message = model.get_value(iter, self.MESSAGE_DATA_IDX)
1448 return False # this seems weird but oh well
1449 return self._filter_messages(message, self._messageType, self._messageStatus)
1450 except Exception, e:
1451 self._errorDisplay.push_exception()
1454 def _filter_messages(cls, message, type, status):
1455 if type == cls.ALL_TYPES:
1458 messageType = message["type"]
1459 isType = messageType == type
1461 if status == cls.ALL_STATUS:
1464 isUnarchived = not message["isArchived"]
1465 isUnread = not message["isRead"]
1466 if status == cls.UNREAD_STATUS:
1467 isStatus = isUnarchived and isUnread
1468 elif status == cls.UNARCHIVED_STATUS:
1469 isStatus = isUnarchived
1471 assert "Status %s is bad for %r" % (status, message)
1473 return isType and isStatus
1475 _MIN_MESSAGES_SHOWN = 4
1477 def _idly_populate_messageview(self):
1478 with gtk_toolbox.gtk_lock():
1479 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1481 self._messagemodel.clear()
1482 self._isPopulated = True
1484 if self._messageType == self.NO_MESSAGES:
1488 messageItems = self._backend.get_messages()
1489 except Exception, e:
1490 self._errorDisplay.push_exception_with_lock()
1491 self._isPopulated = False
1495 (gv_backend.decorate_message(message), message)
1496 for message in gv_backend.sort_messages(messageItems)
1499 for (contactId, header, number, relativeDate, messages), messageData in messageItems:
1500 prettyNumber = number[2:] if number.startswith("+1") else number
1501 prettyNumber = make_pretty(prettyNumber)
1503 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1504 expandedMessages = [firstMessage]
1505 expandedMessages.extend(messages)
1506 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1507 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1508 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1509 collapsedMessages = [firstMessage, secondMessage]
1510 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1512 collapsedMessages = expandedMessages
1513 #collapsedMessages = _collapse_message(collapsedMessages, 60, self._MIN_MESSAGES_SHOWN)
1515 number = make_ugly(number)
1517 row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId, messageData
1518 with gtk_toolbox.gtk_lock():
1519 self._messagemodel.append(row)
1520 except Exception, e:
1521 self._errorDisplay.push_exception_with_lock()
1523 with gtk_toolbox.gtk_lock():
1524 hildonize.show_busy_banner_end(banner)
1525 self._messagemodelfiltered.refilter()
1529 def _on_messageview_row_activated(self, treeview, path, view_column):
1531 childPath = self._messagemodelfiltered.convert_path_to_child_path(path)
1532 itr = self._messagemodel.get_iter(childPath)
1536 number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX))
1537 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1539 contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX)
1540 header = self._messagemodel.get_value(itr, self.HEADER_IDX)
1541 contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1545 contactPhoneNumbers,
1546 messages = description,
1547 defaultIndex = defaultIndex,
1549 self._messageviewselection.unselect_all()
1550 except Exception, e:
1551 self._errorDisplay.push_exception()
1553 def _on_message_type_clicked(self, *args, **kwds):
1555 selectedIndex = self.MESSAGE_TYPES.index(self._messageType)
1558 newSelectedIndex = hildonize.touch_selector(
1564 except RuntimeError:
1567 if selectedIndex != newSelectedIndex:
1568 self._messageType = self.MESSAGE_TYPES[newSelectedIndex]
1569 self._messageTypeButton.set_label(self._messageType)
1570 self._messagemodelfiltered.refilter()
1571 except Exception, e:
1572 self._errorDisplay.push_exception()
1574 def _on_message_status_clicked(self, *args, **kwds):
1576 selectedIndex = self.MESSAGE_STATUSES.index(self._messageStatus)
1579 newSelectedIndex = hildonize.touch_selector(
1582 self.MESSAGE_STATUSES,
1585 except RuntimeError:
1588 if selectedIndex != newSelectedIndex:
1589 self._messageStatus = self.MESSAGE_STATUSES[newSelectedIndex]
1590 self._messageStatusButton.set_label(self._messageStatus)
1591 self._messagemodelfiltered.refilter()
1592 except Exception, e:
1593 self._errorDisplay.push_exception()
1596 class ContactsView(object):
1598 CONTACT_TYPE_IDX = 0
1599 CONTACT_NAME_IDX = 1
1602 def __init__(self, widgetTree, backend, errorDisplay):
1603 self._errorDisplay = errorDisplay
1604 self._backend = backend
1606 self._addressBook = None
1607 self._selectedComboIndex = 0
1608 self._addressBookFactories = [null_backend.NullAddressBook()]
1610 self._booksList = []
1611 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1613 self._isPopulated = False
1614 self._contactsmodel = gtk.ListStore(
1615 gobject.TYPE_STRING, # Contact Type
1616 gobject.TYPE_STRING, # Contact Name
1617 gobject.TYPE_STRING, # Contact ID
1619 self._contactsviewselection = None
1620 self._contactsview = widgetTree.get_widget("contactsview")
1622 self._contactColumn = gtk.TreeViewColumn("Contact")
1623 displayContactSource = False
1624 if displayContactSource:
1625 textrenderer = gtk.CellRendererText()
1626 self._contactColumn.pack_start(textrenderer, expand=False)
1627 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_TYPE_IDX)
1628 textrenderer = gtk.CellRendererText()
1629 hildonize.set_cell_thumb_selectable(textrenderer)
1630 self._contactColumn.pack_start(textrenderer, expand=True)
1631 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_NAME_IDX)
1632 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1633 self._contactColumn.set_sort_column_id(1)
1634 self._contactColumn.set_visible(True)
1636 self._onContactsviewRowActivatedId = 0
1637 self._onAddressbookButtonChangedId = 0
1638 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1640 self._updateSink = gtk_toolbox.threaded_stage(
1642 self._idly_populate_contactsview,
1643 gtk_toolbox.null_sink(),
1648 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1650 self._contactsview.set_model(self._contactsmodel)
1651 self._contactsview.set_fixed_height_mode(False)
1652 self._contactsview.append_column(self._contactColumn)
1653 self._contactsviewselection = self._contactsview.get_selection()
1654 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1656 del self._booksList[:]
1657 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1658 if factoryName and bookName:
1659 entryName = "%s: %s" % (factoryName, bookName)
1661 entryName = factoryName
1663 entryName = bookName
1665 entryName = "Bad name (%d)" % factoryId
1666 row = (str(factoryId), bookId, entryName)
1667 self._booksList.append(row)
1669 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1670 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1672 if len(self._booksList) <= self._selectedComboIndex:
1673 self._selectedComboIndex = 0
1674 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1676 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1677 selectedBookId = self._booksList[self._selectedComboIndex][1]
1678 self.open_addressbook(selectedFactoryId, selectedBookId)
1681 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1682 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1686 self._bookSelectionButton.set_label("")
1687 self._contactsview.set_model(None)
1688 self._contactsview.remove_column(self._contactColumn)
1690 def add_contact(self, *args, **kwds):
1692 @note Actual dial function is patched in later
1694 raise NotImplementedError("Horrible unknown error has occurred")
1696 def get_addressbooks(self):
1698 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1700 for i, factory in enumerate(self._addressBookFactories):
1701 for bookFactory, bookId, bookName in factory.get_addressbooks():
1702 yield (str(i), bookId), (factory.factory_name(), bookName)
1704 def open_addressbook(self, bookFactoryId, bookId):
1705 bookFactoryIndex = int(bookFactoryId)
1706 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1707 self._addressBook = addressBook
1709 def update(self, force = False):
1710 if not force and self._isPopulated:
1712 self._updateSink.send(())
1716 self._isPopulated = False
1717 self._contactsmodel.clear()
1718 for factory in self._addressBookFactories:
1719 factory.clear_caches()
1720 self._addressBook.clear_caches()
1722 def append(self, book):
1723 self._addressBookFactories.append(book)
1725 def extend(self, books):
1726 self._addressBookFactories.extend(books)
1732 def load_settings(self, config, sectionName):
1734 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1735 except ConfigParser.NoOptionError:
1736 self._selectedComboIndex = 0
1738 def save_settings(self, config, sectionName):
1739 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1741 def set_orientation(self, orientation):
1742 if orientation == gtk.ORIENTATION_VERTICAL:
1744 elif orientation == gtk.ORIENTATION_HORIZONTAL:
1747 raise NotImplementedError(orientation)
1749 def _idly_populate_contactsview(self):
1750 with gtk_toolbox.gtk_lock():
1751 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1754 while addressBook is not self._addressBook:
1755 addressBook = self._addressBook
1756 with gtk_toolbox.gtk_lock():
1757 self._contactsview.set_model(None)
1761 contacts = addressBook.get_contacts()
1762 except Exception, e:
1764 self._isPopulated = False
1765 self._errorDisplay.push_exception_with_lock()
1766 for contactId, contactName in contacts:
1767 contactType = addressBook.contact_source_short_name(contactId)
1768 row = contactType, contactName, contactId
1769 self._contactsmodel.append(row)
1771 with gtk_toolbox.gtk_lock():
1772 self._contactsview.set_model(self._contactsmodel)
1774 self._isPopulated = True
1775 except Exception, e:
1776 self._errorDisplay.push_exception_with_lock()
1778 with gtk_toolbox.gtk_lock():
1779 hildonize.show_busy_banner_end(banner)
1782 def _on_addressbook_button_changed(self, *args, **kwds):
1785 newSelectedComboIndex = hildonize.touch_selector(
1788 (("%s" % m[2]) for m in self._booksList),
1789 self._selectedComboIndex,
1791 except RuntimeError:
1794 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1795 selectedBookId = self._booksList[newSelectedComboIndex][1]
1797 oldAddressbook = self._addressBook
1798 self.open_addressbook(selectedFactoryId, selectedBookId)
1799 forceUpdate = True if oldAddressbook is not self._addressBook else False
1800 self.update(force=forceUpdate)
1802 self._selectedComboIndex = newSelectedComboIndex
1803 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1804 except Exception, e:
1805 self._errorDisplay.push_exception()
1807 def _on_contactsview_row_activated(self, treeview, path, view_column):
1809 itr = self._contactsmodel.get_iter(path)
1813 contactId = self._contactsmodel.get_value(itr, self.CONTACT_ID_IDX)
1814 contactName = self._contactsmodel.get_value(itr, self.CONTACT_NAME_IDX)
1816 contactDetails = self._addressBook.get_contact_details(contactId)
1817 except Exception, e:
1819 self._errorDisplay.push_exception()
1820 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1822 if len(contactPhoneNumbers) == 0:
1827 contactPhoneNumbers,
1828 messages = (contactName, ),
1830 self._contactsviewselection.unselect_all()
1831 except Exception, e:
1832 self._errorDisplay.push_exception()