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 is "":
140 phonenumber = normalize_number(phonenumber)
142 if phonenumber[0] == "+":
143 prettynumber = _make_pretty_international(phonenumber[1:])
144 if not prettynumber.startswith("+"):
145 prettynumber = "+"+prettynumber
146 elif 8 < len(phonenumber) and phonenumber[0] in ("1", ):
147 prettynumber = _make_pretty_international(phonenumber)
148 elif 7 < len(phonenumber):
149 prettynumber = _make_pretty_with_areacode(phonenumber)
150 elif 3 < len(phonenumber):
151 prettynumber = _make_pretty_local(phonenumber)
153 prettynumber = phonenumber
154 return prettynumber.strip()
157 def abbrev_relative_date(date):
159 >>> abbrev_relative_date("42 hours ago")
161 >>> abbrev_relative_date("2 days ago")
163 >>> abbrev_relative_date("4 weeks ago")
166 parts = date.split(" ")
167 return "%s %s" % (parts[0], parts[1][0])
170 def _collapse_message(messageLines, maxCharsPerLine, maxLines):
173 numLines = len(messageLines)
174 for line in messageLines[0:min(maxLines, numLines)]:
175 linesPerLine = max(1, int(len(line) / maxCharsPerLine))
176 allowedLines = maxLines - lines
177 acceptedLines = min(allowedLines, linesPerLine)
178 acceptedChars = acceptedLines * maxCharsPerLine
180 if acceptedChars < (len(line) + 3):
183 acceptedChars = len(line) # eh, might as well complete the line
185 abbrevMessage = "%s%s" % (line[0:acceptedChars], suffix)
188 lines += acceptedLines
189 if maxLines <= lines:
193 def collapse_message(message, maxCharsPerLine, maxLines):
195 >>> collapse_message("Hello", 60, 2)
197 >>> collapse_message("Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789", 60, 2)
198 'Hello world how are you doing today? 01234567890123456789012...'
199 >>> collapse_message('''Hello world how are you doing today?
200 ... 01234567890123456789
201 ... 01234567890123456789
202 ... 01234567890123456789
203 ... 01234567890123456789''', 60, 2)
204 'Hello world how are you doing today?\n01234567890123456789'
205 >>> collapse_message('''
206 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
207 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
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''', 60, 2)
212 '\nHello world how are you doing today? 01234567890123456789012...'
214 messageLines = message.split("\n")
215 return "\n".join(_collapse_message(messageLines, maxCharsPerLine, maxLines))
218 def _get_contact_numbers(backend, contactId, number):
220 contactPhoneNumbers = list(backend.get_contact_details(contactId))
221 uglyContactNumbers = (
222 make_ugly(contactNumber)
223 for (numberDescription, contactNumber) in contactPhoneNumbers
227 number == contactNumber or
228 number[1:] == contactNumber and number.startswith("1") or
229 number[2:] == contactNumber and number.startswith("+1") or
230 number == contactNumber[1:] and contactNumber.startswith("1") or
231 number == contactNumber[2:] and contactNumber.startswith("+1")
233 for contactNumber in uglyContactNumbers
236 defaultIndex = defaultMatches.index(True)
238 contactPhoneNumbers.append(("Other", number))
239 defaultIndex = len(contactPhoneNumbers)-1
241 "Could not find contact %r's number %s among %r" % (
242 contactId, number, contactPhoneNumbers
246 contactPhoneNumbers = [("Phone", number)]
249 return contactPhoneNumbers, defaultIndex
252 class SmsEntryWindow(object):
256 def __init__(self, widgetTree, parent, app):
257 self._clipboard = gtk.clipboard_get()
258 self._widgetTree = widgetTree
259 self._parent = parent
261 self._isFullScreen = False
263 self._window = self._widgetTree.get_widget("smsWindow")
264 self._window = hildonize.hildonize_window(self._app, self._window)
265 self._window.set_title("SMS")
266 self._window.connect("delete-event", self._on_delete)
267 self._window.connect("key-press-event", self._on_key_press)
268 self._window.connect("window-state-event", self._on_window_state_change)
269 self._widgetTree.get_widget("smsMessagesViewPort").get_parent().show()
271 errorBox = self._widgetTree.get_widget("smsErrorEventBox")
272 errorDescription = self._widgetTree.get_widget("smsErrorDescription")
273 errorClose = self._widgetTree.get_widget("smsErrorClose")
274 self._errorDisplay = gtk_toolbox.ErrorDisplay(errorBox, errorDescription, errorClose)
276 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
277 self._smsButton.connect("clicked", self._on_send)
278 self._dialButton = self._widgetTree.get_widget("dialButton")
279 self._dialButton.connect("clicked", self._on_dial)
281 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
283 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
284 self._messagesView = self._widgetTree.get_widget("smsMessages")
286 textrenderer = gtk.CellRendererText()
287 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
288 textrenderer.set_property("wrap-width", 450)
289 messageColumn = gtk.TreeViewColumn("")
290 messageColumn.pack_start(textrenderer, expand=True)
291 messageColumn.add_attribute(textrenderer, "markup", 0)
292 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
293 self._messagesView.append_column(messageColumn)
294 self._messagesView.set_headers_visible(False)
295 self._messagesView.set_model(self._messagemodel)
296 self._messagesView.set_fixed_height_mode(False)
298 self._conversationView = self._messagesView.get_parent()
299 self._conversationViewPort = self._conversationView.get_parent()
300 self._scrollWindow = self._conversationViewPort.get_parent()
302 self._targetList = self._widgetTree.get_widget("smsTargetList")
303 self._phoneButton = self._widgetTree.get_widget("phoneTypeSelection")
304 self._phoneButton.connect("clicked", self._on_phone)
305 self._smsEntry = self._widgetTree.get_widget("smsEntry")
306 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
307 self._smsEntrySize = None
311 def add_contact(self, name, contactDetails, messages = (), defaultIndex = -1):
312 contactNumbers = list(self._to_contact_numbers(contactDetails))
313 assert contactNumbers, "Contact must have at least one number"
314 contactIndex = defaultIndex if defaultIndex != -1 else 0
315 contact = contactNumbers, contactIndex, messages
316 self._contacts.append(contact)
318 nameLabel = gtk.Label(name)
319 selector = gtk.Button(contactNumbers[0][1])
320 if len(contactNumbers) == 1:
321 selector.set_sensitive(False)
322 removeContact = gtk.Button(stock="gtk-delete")
324 row.pack_start(nameLabel, True, True)
325 row.pack_start(selector, True, True)
326 row.pack_start(removeContact, False, False)
328 self._targetList.pack_start(row)
329 selector.connect("clicked", self._on_choose_phone_n, row)
330 removeContact.connect("clicked", self._on_remove_phone_n, row)
331 self._update_button_state()
332 self._update_context()
334 parentSize = self._parent.get_size()
335 self._window.resize(parentSize[0], max(parentSize[1]-10, 100))
337 self._window.present()
339 self._smsEntry.grab_focus()
340 self._scroll_to_bottom()
343 del self._contacts[:]
345 for row in list(self._targetList.get_children()):
346 self._targetList.remove(row)
347 self._smsEntry.get_buffer().set_text("")
348 self._update_letter_count()
349 self._update_context()
351 def fullscreen(self):
352 self._window.fullscreen()
354 def unfullscreen(self):
355 self._window.unfullscreen()
357 def _remove_contact(self, contactIndex):
358 del self._contacts[contactIndex]
360 row = list(self._targetList.get_children())[contactIndex]
361 self._targetList.remove(row)
362 self._update_button_state()
363 self._update_context()
364 self._scroll_to_bottom()
366 def _scroll_to_bottom(self):
367 dx = self._conversationView.get_allocation().height - self._conversationViewPort.get_allocation().height
369 adjustment = self._scrollWindow.get_vadjustment()
370 adjustment.value = dx
372 def _update_letter_count(self):
373 if self._smsEntrySize is None:
374 self._smsEntrySize = self._smsEntry.size_request()
376 self._smsEntry.set_size_request(*self._smsEntrySize)
377 entryLength = self._smsEntry.get_buffer().get_char_count()
379 numTexts, numCharInText = divmod(entryLength, self.MAX_CHAR)
381 self._letterCountLabel.set_text("%s.%s" % (numTexts, numCharInText))
383 self._letterCountLabel.set_text("%s" % (numCharInText, ))
385 self._update_button_state()
387 def _update_context(self):
388 self._messagemodel.clear()
389 if len(self._contacts) == 0:
390 self._messagesView.hide()
391 self._targetList.hide()
392 self._phoneButton.hide()
393 self._phoneButton.set_label("Error: You shouldn't see this")
394 elif len(self._contacts) == 1:
395 contactNumbers, index, messages = self._contacts[0]
397 self._messagesView.show()
398 for message in messages:
400 self._messagemodel.append(row)
401 messagesSelection = self._messagesView.get_selection()
402 messagesSelection.select_path((len(messages)-1, ))
404 self._messagesView.hide()
405 self._targetList.hide()
406 self._phoneButton.show()
407 self._phoneButton.set_label(contactNumbers[index][1])
408 if 1 < len(contactNumbers):
409 self._phoneButton.set_sensitive(True)
411 self._phoneButton.set_sensitive(False)
413 self._messagesView.hide()
414 self._targetList.show()
415 self._phoneButton.hide()
416 self._phoneButton.set_label("Error: You shouldn't see this")
418 def _update_button_state(self):
419 if len(self._contacts) == 0:
420 self._dialButton.set_sensitive(False)
421 self._smsButton.set_sensitive(False)
422 elif len(self._contacts) == 1:
423 entryLength = self._smsEntry.get_buffer().get_char_count()
425 self._dialButton.set_sensitive(True)
426 self._smsButton.set_sensitive(False)
428 self._dialButton.set_sensitive(False)
429 self._smsButton.set_sensitive(True)
431 self._dialButton.set_sensitive(False)
432 self._smsButton.set_sensitive(True)
434 def _to_contact_numbers(self, contactDetails):
435 for phoneType, phoneNumber in contactDetails:
436 display = " - ".join((make_pretty(phoneNumber), phoneType))
437 yield (phoneNumber, display)
439 def _pseudo_destroy(self):
443 def _request_number(self, contactIndex):
444 contactNumbers, index, messages = self._contacts[contactIndex]
445 assert 0 <= index, "%r" % index
447 index = hildonize.touch_selector(
450 (description for (number, description) in contactNumbers),
453 self._contacts[contactIndex] = contactNumbers, index, messages
455 def send_sms(self, numbers, message):
456 raise NotImplementedError()
458 def dial(self, number):
459 raise NotImplementedError()
461 def _on_phone(self, *args):
463 assert len(self._contacts) == 1, "One and only one contact is required"
464 self._request_number(0)
466 contactNumbers, numberIndex, messages = self._contacts[0]
467 self._phoneButton.set_label(contactNumbers[numberIndex][1])
468 row = list(self._targetList.get_children())[0]
469 phoneButton = list(row.get_children())[1]
470 phoneButton.set_label(contactNumbers[numberIndex][1])
472 self._errorDisplay.push_exception()
474 def _on_choose_phone_n(self, button, row):
476 assert 1 < len(self._contacts), "More than one contact required"
477 targetList = list(self._targetList.get_children())
478 index = targetList.index(row)
479 self._request_number(index)
481 contactNumbers, numberIndex, messages = self._contacts[0]
482 phoneButton = list(row.get_children())[1]
483 phoneButton.set_label(contactNumbers[numberIndex][1])
485 self._errorDisplay.push_exception()
487 def _on_remove_phone_n(self, button, row):
489 assert 1 < len(self._contacts), "More than one contact required"
490 targetList = list(self._targetList.get_children())
491 index = targetList.index(row)
493 del self._contacts[index]
494 self._targetList.remove(row)
495 self._update_context()
496 self._update_button_state()
498 self._errorDisplay.push_exception()
500 def _on_entry_changed(self, *args):
502 self._update_letter_count()
504 self._errorDisplay.push_exception()
506 def _on_send(self, *args):
508 assert 0 < len(self._contacts), "At least one contact required (%r)" % self._contacts
510 make_ugly(contact[0][contact[1]][0])
511 for contact in self._contacts
514 entryBuffer = self._smsEntry.get_buffer()
515 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
516 enteredMessage = enteredMessage.strip()
517 assert enteredMessage, "No message provided"
518 self.send_sms(phoneNumbers, enteredMessage)
519 self._pseudo_destroy()
521 self._errorDisplay.push_exception()
523 def _on_dial(self, *args):
525 assert len(self._contacts) == 1, "One and only one contact allowed (%r)" % self._contacts
526 contact = self._contacts[0]
527 contactNumber = contact[0][contact[1]][0]
528 phoneNumber = make_ugly(contactNumber)
529 self.dial(phoneNumber)
530 self._pseudo_destroy()
532 self._errorDisplay.push_exception()
534 def _on_delete(self, *args):
536 self._window.emit_stop_by_name("delete-event")
537 if hildonize.IS_FREMANTLE_SUPPORTED:
540 self._pseudo_destroy()
542 self._errorDisplay.push_exception()
545 def _on_window_state_change(self, widget, event, *args):
547 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
548 self._isFullScreen = True
550 self._isFullScreen = False
552 self._errorDisplay.push_exception()
554 def _on_key_press(self, widget, event):
555 RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
558 event.keyval == gtk.keysyms.F6 or
559 event.keyval in RETURN_TYPES and event.get_state() & gtk.gdk.CONTROL_MASK
561 if self._isFullScreen:
562 self._window.unfullscreen()
564 self._window.fullscreen()
565 elif event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
568 for messagePart in self._messagemodel
570 self._clipboard.set_text(str(message))
572 event.keyval == gtk.keysyms.h and
573 event.get_state() & gtk.gdk.CONTROL_MASK
577 event.keyval == gtk.keysyms.w and
578 event.get_state() & gtk.gdk.CONTROL_MASK
580 self._pseudo_destroy()
582 event.keyval == gtk.keysyms.q and
583 event.get_state() & gtk.gdk.CONTROL_MASK
585 self._parent.destroy()
587 self._errorDisplay.push_exception()
590 class Dialpad(object):
592 def __init__(self, widgetTree, errorDisplay):
593 self._clipboard = gtk.clipboard_get()
594 self._errorDisplay = errorDisplay
596 self._numberdisplay = widgetTree.get_widget("numberdisplay")
597 self._callButton = widgetTree.get_widget("dialpadCall")
598 self._sendSMSButton = widgetTree.get_widget("dialpadSMS")
599 self._backButton = widgetTree.get_widget("back")
600 self._plusButton = widgetTree.get_widget("plus")
601 self._phonenumber = ""
602 self._prettynumber = ""
605 "on_digit_clicked": self._on_digit_clicked,
607 widgetTree.signal_autoconnect(callbackMapping)
608 self._sendSMSButton.connect("clicked", self._on_sms_clicked)
609 self._callButton.connect("clicked", self._on_call_clicked)
610 self._plusButton.connect("clicked", self._on_plus)
612 self._originalLabel = self._backButton.get_label()
613 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
614 self._backTapHandler.on_tap = self._on_backspace
615 self._backTapHandler.on_hold = self._on_clearall
616 self._backTapHandler.on_holding = self._set_clear_button
617 self._backTapHandler.on_cancel = self._reset_back_button
619 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
620 self._keyPressEventId = 0
623 self._sendSMSButton.grab_focus()
624 self._backTapHandler.enable()
625 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
628 self._window.disconnect(self._keyPressEventId)
629 self._keyPressEventId = 0
630 self._reset_back_button()
631 self._backTapHandler.disable()
633 def add_contact(self, *args, **kwds):
635 @note Actual function is patched in later
637 raise NotImplementedError("Horrible unknown error has occurred")
639 def dial(self, number):
641 @note Actual function is patched in later
643 raise NotImplementedError("Horrible unknown error has occurred")
645 def get_number(self):
646 return self._phonenumber
648 def set_number(self, number):
650 Set the number to dial
653 self._phonenumber = make_ugly(number)
654 self._prettynumber = make_pretty(self._phonenumber)
655 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
656 if self._phonenumber:
657 self._plusButton.set_sensitive(False)
659 self._plusButton.set_sensitive(True)
661 self._errorDisplay.push_exception()
670 def load_settings(self, config, section):
673 def save_settings(self, config, section):
675 @note Thread Agnostic
679 def set_orientation(self, orientation):
680 if orientation == gtk.ORIENTATION_VERTICAL:
682 elif orientation == gtk.ORIENTATION_HORIZONTAL:
685 raise NotImplementedError(orientation)
687 def _on_key_press(self, widget, event):
689 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
690 contents = self._clipboard.wait_for_text()
691 if contents is not None:
692 self.set_number(contents)
694 self._errorDisplay.push_exception()
696 def _on_call_clicked(self, widget):
698 phoneNumber = self.get_number()
699 self.dial(phoneNumber)
702 self._errorDisplay.push_exception()
704 def _on_sms_clicked(self, widget):
706 phoneNumber = self.get_number()
709 [("Dialer", phoneNumber)], ()
713 self._errorDisplay.push_exception()
715 def _on_digit_clicked(self, widget):
717 self.set_number(self._phonenumber + widget.get_name()[-1])
719 self._errorDisplay.push_exception()
721 def _on_plus(self, *args):
723 self.set_number(self._phonenumber + "+")
725 self._errorDisplay.push_exception()
727 def _on_backspace(self, taps):
729 self.set_number(self._phonenumber[:-taps])
730 self._reset_back_button()
732 self._errorDisplay.push_exception()
734 def _on_clearall(self, taps):
737 self._reset_back_button()
739 self._errorDisplay.push_exception()
742 def _set_clear_button(self):
744 self._backButton.set_label("gtk-clear")
746 self._errorDisplay.push_exception()
748 def _reset_back_button(self):
750 self._backButton.set_label(self._originalLabel)
752 self._errorDisplay.push_exception()
755 class AccountInfo(object):
757 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
758 self._errorDisplay = errorDisplay
759 self._backend = backend
760 self._isPopulated = False
761 self._alarmHandler = alarmHandler
762 self._notifyOnMissed = False
763 self._notifyOnVoicemail = False
764 self._notifyOnSms = False
766 self._callbackList = []
767 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
768 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
769 self._onCallbackSelectChangedId = 0
771 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
772 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
773 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
774 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
775 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
776 self._onNotifyToggled = 0
777 self._onMinutesChanged = 0
778 self._onMissedToggled = 0
779 self._onVoicemailToggled = 0
780 self._onSmsToggled = 0
781 self._applyAlarmTimeoutId = None
783 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
784 self._callbackNumber = ""
787 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
789 self._accountViewNumberDisplay.set_use_markup(True)
790 self.set_account_number("")
792 del self._callbackList[:]
793 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
794 self._set_callback_label("")
796 if self._alarmHandler is not None:
797 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
798 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
799 self._missedCheckbox.set_active(self._notifyOnMissed)
800 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
801 self._smsCheckbox.set_active(self._notifyOnSms)
803 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
804 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
805 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
806 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
807 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
809 self._notifyCheckbox.set_sensitive(False)
810 self._minutesEntryButton.set_sensitive(False)
811 self._missedCheckbox.set_sensitive(False)
812 self._voicemailCheckbox.set_sensitive(False)
813 self._smsCheckbox.set_sensitive(False)
815 self.update(force=True)
818 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
819 self._onCallbackSelectChangedId = 0
820 self._set_callback_label("")
822 if self._alarmHandler is not None:
823 self._notifyCheckbox.disconnect(self._onNotifyToggled)
824 self._minutesEntryButton.disconnect(self._onMinutesChanged)
825 self._missedCheckbox.disconnect(self._onNotifyToggled)
826 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
827 self._smsCheckbox.disconnect(self._onNotifyToggled)
828 self._onNotifyToggled = 0
829 self._onMinutesChanged = 0
830 self._onMissedToggled = 0
831 self._onVoicemailToggled = 0
832 self._onSmsToggled = 0
834 self._notifyCheckbox.set_sensitive(True)
835 self._minutesEntryButton.set_sensitive(True)
836 self._missedCheckbox.set_sensitive(True)
837 self._voicemailCheckbox.set_sensitive(True)
838 self._smsCheckbox.set_sensitive(True)
841 del self._callbackList[:]
843 def set_account_number(self, number):
845 Displays current account number
847 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
849 def update(self, force = False):
850 if not force and self._isPopulated:
852 self._populate_callback_combo()
853 self.set_account_number(self._backend.get_account_number())
857 self._set_callback_label("")
858 self.set_account_number("")
859 self._isPopulated = False
861 def save_everything(self):
862 raise NotImplementedError
866 return "Account Info"
868 def load_settings(self, config, section):
869 self._callbackNumber = make_ugly(config.get(section, "callback"))
870 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
871 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
872 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
874 def save_settings(self, config, section):
876 @note Thread Agnostic
878 config.set(section, "callback", self._callbackNumber)
879 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
880 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
881 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
883 def set_orientation(self, orientation):
884 if orientation == gtk.ORIENTATION_VERTICAL:
886 elif orientation == gtk.ORIENTATION_HORIZONTAL:
889 raise NotImplementedError(orientation)
891 def _populate_callback_combo(self):
892 self._isPopulated = True
893 del self._callbackList[:]
895 callbackNumbers = self._backend.get_callback_numbers()
897 self._errorDisplay.push_exception()
898 self._isPopulated = False
901 if len(callbackNumbers) == 0:
902 callbackNumbers = {"": "No callback numbers available"}
904 for number, description in callbackNumbers.iteritems():
905 self._callbackList.append((make_pretty(number), description))
907 self._set_callback_number(self._callbackNumber)
909 def _set_callback_number(self, number):
911 if not self._backend.is_valid_syntax(number) and 0 < len(number):
912 self._errorDisplay.push_message("%s is not a valid callback number" % number)
913 elif number == self._backend.get_callback_number() and 0 < len(number):
914 _moduleLogger.warning(
915 "Callback number already is %s" % (
916 self._backend.get_callback_number(),
919 self._set_callback_label(number)
921 if number.startswith("1747"): number = "+" + number
922 self._backend.set_callback_number(number)
923 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
924 make_pretty(number), make_pretty(self._backend.get_callback_number())
926 self._callbackNumber = make_ugly(number)
927 self._set_callback_label(number)
929 "Callback number set to %s" % (
930 self._backend.get_callback_number(),
934 self._errorDisplay.push_exception()
936 def _set_callback_label(self, uglyNumber):
937 prettyNumber = make_pretty(uglyNumber)
938 if len(prettyNumber) == 0:
939 prettyNumber = "No Callback Number"
940 self._callbackSelectButton.set_label(prettyNumber)
942 def _update_alarm_settings(self, recurrence):
944 isEnabled = self._notifyCheckbox.get_active()
945 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
946 self._alarmHandler.apply_settings(isEnabled, recurrence)
948 self.save_everything()
949 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
950 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
952 def _on_callbackentry_clicked(self, *args):
954 actualSelection = make_pretty(self._callbackNumber)
957 (number, "%s (%s)" % (number, description))
958 for (number, description) in self._callbackList
960 defaultSelection = userOptions.get(actualSelection, actualSelection)
962 userSelection = hildonize.touch_selector_entry(
965 list(userOptions.itervalues()),
968 reversedUserOptions = dict(
969 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
971 selectedNumber = reversedUserOptions.get(userSelection, userSelection)
973 number = make_ugly(selectedNumber)
974 self._set_callback_number(number)
975 except RuntimeError, e:
976 _moduleLogger.exception("%s" % str(e))
978 self._errorDisplay.push_exception()
980 def _on_notify_toggled(self, *args):
982 if self._applyAlarmTimeoutId is not None:
983 gobject.source_remove(self._applyAlarmTimeoutId)
984 self._applyAlarmTimeoutId = None
985 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
987 self._errorDisplay.push_exception()
989 def _on_minutes_clicked(self, *args):
990 recurrenceChoices = [
1003 (12*60, "12 hours"),
1006 actualSelection = self._alarmHandler.recurrence
1008 closestSelectionIndex = 0
1009 for i, possible in enumerate(recurrenceChoices):
1010 if possible[0] <= actualSelection:
1011 closestSelectionIndex = i
1012 recurrenceIndex = hildonize.touch_selector(
1015 (("%s" % m[1]) for m in recurrenceChoices),
1016 closestSelectionIndex,
1018 recurrence = recurrenceChoices[recurrenceIndex][0]
1020 self._update_alarm_settings(recurrence)
1021 except RuntimeError, e:
1022 _moduleLogger.exception("%s" % str(e))
1023 except Exception, e:
1024 self._errorDisplay.push_exception()
1026 def _on_apply_timeout(self, *args):
1028 self._applyAlarmTimeoutId = None
1030 self._update_alarm_settings(self._alarmHandler.recurrence)
1031 except Exception, e:
1032 self._errorDisplay.push_exception()
1035 def _on_missed_toggled(self, *args):
1037 self._notifyOnMissed = self._missedCheckbox.get_active()
1038 self.save_everything()
1039 except Exception, e:
1040 self._errorDisplay.push_exception()
1042 def _on_voicemail_toggled(self, *args):
1044 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
1045 self.save_everything()
1046 except Exception, e:
1047 self._errorDisplay.push_exception()
1049 def _on_sms_toggled(self, *args):
1051 self._notifyOnSms = self._smsCheckbox.get_active()
1052 self.save_everything()
1053 except Exception, e:
1054 self._errorDisplay.push_exception()
1057 class CallHistoryView(object):
1065 HISTORY_ITEM_TYPES = ["All", "Received", "Missed", "Placed"]
1067 def __init__(self, widgetTree, backend, errorDisplay):
1068 self._errorDisplay = errorDisplay
1069 self._backend = backend
1071 self._isPopulated = False
1072 self._historymodel = gtk.ListStore(
1073 gobject.TYPE_STRING, # number
1074 gobject.TYPE_STRING, # date
1075 gobject.TYPE_STRING, # action
1076 gobject.TYPE_STRING, # from
1077 gobject.TYPE_STRING, # from id
1079 self._historymodelfiltered = self._historymodel.filter_new()
1080 self._historymodelfiltered.set_visible_func(self._is_history_visible)
1081 self._historyview = widgetTree.get_widget("historyview")
1082 self._historyviewselection = None
1083 self._onRecentviewRowActivatedId = 0
1085 textrenderer = gtk.CellRendererText()
1086 textrenderer.set_property("yalign", 0)
1087 self._dateColumn = gtk.TreeViewColumn("Date")
1088 self._dateColumn.pack_start(textrenderer, expand=True)
1089 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
1091 textrenderer = gtk.CellRendererText()
1092 textrenderer.set_property("yalign", 0)
1093 self._actionColumn = gtk.TreeViewColumn("Action")
1094 self._actionColumn.pack_start(textrenderer, expand=True)
1095 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
1097 textrenderer = gtk.CellRendererText()
1098 textrenderer.set_property("yalign", 0)
1099 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
1100 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
1101 self._numberColumn = gtk.TreeViewColumn("Number")
1102 self._numberColumn.pack_start(textrenderer, expand=True)
1103 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
1105 textrenderer = gtk.CellRendererText()
1106 textrenderer.set_property("yalign", 0)
1107 hildonize.set_cell_thumb_selectable(textrenderer)
1108 self._nameColumn = gtk.TreeViewColumn("From")
1109 self._nameColumn.pack_start(textrenderer, expand=True)
1110 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
1111 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1113 self._window = gtk_toolbox.find_parent_window(self._historyview)
1115 self._historyFilterSelector = widgetTree.get_widget("historyFilterSelector")
1116 self._historyFilterSelector.connect("clicked", self._on_history_filter_clicked)
1117 self._selectedFilter = "All"
1119 self._updateSink = gtk_toolbox.threaded_stage(
1121 self._idly_populate_historyview,
1122 gtk_toolbox.null_sink(),
1127 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1128 self._historyFilterSelector.set_label(self._selectedFilter)
1130 self._historyview.set_model(self._historymodelfiltered)
1131 self._historyview.set_fixed_height_mode(False)
1133 self._historyview.append_column(self._dateColumn)
1134 self._historyview.append_column(self._actionColumn)
1135 self._historyview.append_column(self._numberColumn)
1136 self._historyview.append_column(self._nameColumn)
1137 self._historyviewselection = self._historyview.get_selection()
1138 self._historyviewselection.set_mode(gtk.SELECTION_SINGLE)
1140 self._onRecentviewRowActivatedId = self._historyview.connect("row-activated", self._on_historyview_row_activated)
1143 self._historyview.disconnect(self._onRecentviewRowActivatedId)
1147 self._historyview.remove_column(self._dateColumn)
1148 self._historyview.remove_column(self._actionColumn)
1149 self._historyview.remove_column(self._nameColumn)
1150 self._historyview.remove_column(self._numberColumn)
1151 self._historyview.set_model(None)
1153 def add_contact(self, *args, **kwds):
1155 @note Actual dial function is patched in later
1157 raise NotImplementedError("Horrible unknown error has occurred")
1159 def update(self, force = False):
1160 if not force and self._isPopulated:
1162 self._updateSink.send(())
1166 self._isPopulated = False
1167 self._historymodel.clear()
1171 return "Recent Calls"
1173 def load_settings(self, config, sectionName):
1175 self._selectedFilter = config.get(sectionName, "filter")
1176 if self._selectedFilter not in self.HISTORY_ITEM_TYPES:
1177 self._messageType = self.HISTORY_ITEM_TYPES[0]
1178 except ConfigParser.NoOptionError:
1181 def save_settings(self, config, sectionName):
1183 @note Thread Agnostic
1185 config.set(sectionName, "filter", self._selectedFilter)
1187 def set_orientation(self, orientation):
1188 if orientation == gtk.ORIENTATION_VERTICAL:
1190 elif orientation == gtk.ORIENTATION_HORIZONTAL:
1193 raise NotImplementedError(orientation)
1195 def _is_history_visible(self, model, iter):
1197 action = model.get_value(iter, self.ACTION_IDX)
1199 return False # this seems weird but oh well
1201 if self._selectedFilter in [action, "All"]:
1205 except Exception, e:
1206 self._errorDisplay.push_exception()
1208 def _idly_populate_historyview(self):
1209 with gtk_toolbox.gtk_lock():
1210 banner = hildonize.show_busy_banner_start(self._window, "Loading Call History")
1212 self._historymodel.clear()
1213 self._isPopulated = True
1216 historyItems = self._backend.get_recent()
1217 except Exception, e:
1218 self._errorDisplay.push_exception_with_lock()
1219 self._isPopulated = False
1223 gv_backend.decorate_recent(data)
1224 for data in gv_backend.sort_messages(historyItems)
1227 for contactId, personName, phoneNumber, date, action in historyItems:
1229 personName = "Unknown"
1230 date = abbrev_relative_date(date)
1231 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1232 prettyNumber = make_pretty(prettyNumber)
1233 item = (prettyNumber, date, action.capitalize(), personName, contactId)
1234 with gtk_toolbox.gtk_lock():
1235 self._historymodel.append(item)
1236 except Exception, e:
1237 self._errorDisplay.push_exception_with_lock()
1239 with gtk_toolbox.gtk_lock():
1240 hildonize.show_busy_banner_end(banner)
1244 def _on_history_filter_clicked(self, *args, **kwds):
1246 selectedComboIndex = self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
1249 newSelectedComboIndex = hildonize.touch_selector(
1252 self.HISTORY_ITEM_TYPES,
1255 except RuntimeError:
1258 option = self.HISTORY_ITEM_TYPES[newSelectedComboIndex]
1259 self._selectedFilter = option
1260 self._historyFilterSelector.set_label(self._selectedFilter)
1261 self._historymodelfiltered.refilter()
1262 except Exception, e:
1263 self._errorDisplay.push_exception()
1265 def _history_summary(self, expectedNumber):
1266 for number, action, date, whoFrom, whoFromId in self._historymodel:
1267 if expectedNumber is not None and expectedNumber == number:
1268 yield "%s <i>(%s)</i> - %s %s" % (number, whoFrom, date, action)
1270 def _on_historyview_row_activated(self, treeview, path, view_column):
1272 childPath = self._historymodelfiltered.convert_path_to_child_path(path)
1273 itr = self._historymodel.get_iter(childPath)
1277 prettyNumber = self._historymodel.get_value(itr, self.NUMBER_IDX)
1278 number = make_ugly(prettyNumber)
1279 description = list(self._history_summary(prettyNumber))
1280 contactName = self._historymodel.get_value(itr, self.FROM_IDX)
1281 contactId = self._historymodel.get_value(itr, self.FROM_ID_IDX)
1282 contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1286 contactPhoneNumbers,
1287 messages = description,
1288 defaultIndex = defaultIndex,
1290 self._historyviewselection.unselect_all()
1291 except Exception, e:
1292 self._errorDisplay.push_exception()
1295 class MessagesView(object):
1303 MESSAGE_DATA_IDX = 6
1305 NO_MESSAGES = "None"
1306 VOICEMAIL_MESSAGES = "Voicemail"
1307 TEXT_MESSAGES = "SMS"
1308 ALL_TYPES = "All Messages"
1309 MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
1311 UNREAD_STATUS = "Unread"
1312 UNARCHIVED_STATUS = "Inbox"
1314 MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
1316 def __init__(self, widgetTree, backend, errorDisplay):
1317 self._errorDisplay = errorDisplay
1318 self._backend = backend
1320 self._isPopulated = False
1321 self._messagemodel = gtk.ListStore(
1322 gobject.TYPE_STRING, # number
1323 gobject.TYPE_STRING, # date
1324 gobject.TYPE_STRING, # header
1325 gobject.TYPE_STRING, # message
1327 gobject.TYPE_STRING, # from id
1328 object, # message data
1330 self._messagemodelfiltered = self._messagemodel.filter_new()
1331 self._messagemodelfiltered.set_visible_func(self._is_message_visible)
1332 self._messageview = widgetTree.get_widget("messages_view")
1333 self._messageviewselection = None
1334 self._onMessageviewRowActivatedId = 0
1336 self._messageRenderer = gtk.CellRendererText()
1337 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1338 self._messageRenderer.set_property("wrap-width", 500)
1339 self._messageColumn = gtk.TreeViewColumn("Messages")
1340 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1341 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1342 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1344 self._window = gtk_toolbox.find_parent_window(self._messageview)
1346 self._messageTypeButton = widgetTree.get_widget("messageTypeButton")
1347 self._onMessageTypeClickedId = 0
1348 self._messageType = self.ALL_TYPES
1349 self._messageStatusButton = widgetTree.get_widget("messageStatusButton")
1350 self._onMessageStatusClickedId = 0
1351 self._messageStatus = self.ALL_STATUS
1353 self._updateSink = gtk_toolbox.threaded_stage(
1355 self._idly_populate_messageview,
1356 gtk_toolbox.null_sink(),
1361 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1362 self._messageview.set_model(self._messagemodelfiltered)
1363 self._messageview.set_headers_visible(False)
1364 self._messageview.set_fixed_height_mode(False)
1366 self._messageview.append_column(self._messageColumn)
1367 self._messageviewselection = self._messageview.get_selection()
1368 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1370 self._messageTypeButton.set_label(self._messageType)
1371 self._messageStatusButton.set_label(self._messageStatus)
1373 self._onMessageviewRowActivatedId = self._messageview.connect(
1374 "row-activated", self._on_messageview_row_activated
1376 self._onMessageTypeClickedId = self._messageTypeButton.connect(
1377 "clicked", self._on_message_type_clicked
1379 self._onMessageStatusClickedId = self._messageStatusButton.connect(
1380 "clicked", self._on_message_status_clicked
1384 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1385 self._messageTypeButton.disconnect(self._onMessageTypeClickedId)
1386 self._messageStatusButton.disconnect(self._onMessageStatusClickedId)
1390 self._messageview.remove_column(self._messageColumn)
1391 self._messageview.set_model(None)
1393 def add_contact(self, *args, **kwds):
1395 @note Actual dial function is patched in later
1397 raise NotImplementedError("Horrible unknown error has occurred")
1399 def update(self, force = False):
1400 if not force and self._isPopulated:
1402 self._updateSink.send(())
1406 self._isPopulated = False
1407 self._messagemodel.clear()
1413 def load_settings(self, config, sectionName):
1415 self._messageType = config.get(sectionName, "type")
1416 if self._messageType not in self.MESSAGE_TYPES:
1417 self._messageType = self.ALL_TYPES
1418 self._messageStatus = config.get(sectionName, "status")
1419 if self._messageStatus not in self.MESSAGE_STATUSES:
1420 self._messageStatus = self.ALL_STATUS
1421 except ConfigParser.NoOptionError:
1424 def save_settings(self, config, sectionName):
1426 @note Thread Agnostic
1428 config.set(sectionName, "status", self._messageStatus)
1429 config.set(sectionName, "type", self._messageType)
1431 def set_orientation(self, orientation):
1432 if orientation == gtk.ORIENTATION_VERTICAL:
1434 elif orientation == gtk.ORIENTATION_HORIZONTAL:
1437 raise NotImplementedError(orientation)
1439 def _is_message_visible(self, model, iter):
1441 message = model.get_value(iter, self.MESSAGE_DATA_IDX)
1443 return False # this seems weird but oh well
1444 return self._filter_messages(message, self._messageType, self._messageStatus)
1445 except Exception, e:
1446 self._errorDisplay.push_exception()
1449 def _filter_messages(cls, message, type, status):
1450 if type == cls.ALL_TYPES:
1453 messageType = message["type"]
1454 isType = messageType == type
1456 if status == cls.ALL_STATUS:
1459 isUnarchived = not message["isArchived"]
1460 isUnread = not message["isRead"]
1461 if status == cls.UNREAD_STATUS:
1462 isStatus = isUnarchived and isUnread
1463 elif status == cls.UNARCHIVED_STATUS:
1464 isStatus = isUnarchived
1466 assert "Status %s is bad for %r" % (status, message)
1468 return isType and isStatus
1470 _MIN_MESSAGES_SHOWN = 4
1472 def _idly_populate_messageview(self):
1473 with gtk_toolbox.gtk_lock():
1474 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1476 self._messagemodel.clear()
1477 self._isPopulated = True
1479 if self._messageType == self.NO_MESSAGES:
1483 messageItems = self._backend.get_messages()
1484 except Exception, e:
1485 self._errorDisplay.push_exception_with_lock()
1486 self._isPopulated = False
1490 (gv_backend.decorate_message(message), message)
1491 for message in gv_backend.sort_messages(messageItems)
1494 for (contactId, header, number, relativeDate, messages), messageData in messageItems:
1495 prettyNumber = number[2:] if number.startswith("+1") else number
1496 prettyNumber = make_pretty(prettyNumber)
1498 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1499 expandedMessages = [firstMessage]
1500 expandedMessages.extend(messages)
1501 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1502 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1503 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1504 collapsedMessages = [firstMessage, secondMessage]
1505 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1507 collapsedMessages = expandedMessages
1508 #collapsedMessages = _collapse_message(collapsedMessages, 60, self._MIN_MESSAGES_SHOWN)
1510 number = make_ugly(number)
1512 row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId, messageData
1513 with gtk_toolbox.gtk_lock():
1514 self._messagemodel.append(row)
1515 except Exception, e:
1516 self._errorDisplay.push_exception_with_lock()
1518 with gtk_toolbox.gtk_lock():
1519 hildonize.show_busy_banner_end(banner)
1520 self._messagemodelfiltered.refilter()
1524 def _on_messageview_row_activated(self, treeview, path, view_column):
1526 childPath = self._messagemodelfiltered.convert_path_to_child_path(path)
1527 itr = self._messagemodel.get_iter(childPath)
1531 number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX))
1532 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1534 contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX)
1535 header = self._messagemodel.get_value(itr, self.HEADER_IDX)
1536 contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1540 contactPhoneNumbers,
1541 messages = description,
1542 defaultIndex = defaultIndex,
1544 self._messageviewselection.unselect_all()
1545 except Exception, e:
1546 self._errorDisplay.push_exception()
1548 def _on_message_type_clicked(self, *args, **kwds):
1550 selectedIndex = self.MESSAGE_TYPES.index(self._messageType)
1553 newSelectedIndex = hildonize.touch_selector(
1559 except RuntimeError:
1562 if selectedIndex != newSelectedIndex:
1563 self._messageType = self.MESSAGE_TYPES[newSelectedIndex]
1564 self._messageTypeButton.set_label(self._messageType)
1565 self._messagemodelfiltered.refilter()
1566 except Exception, e:
1567 self._errorDisplay.push_exception()
1569 def _on_message_status_clicked(self, *args, **kwds):
1571 selectedIndex = self.MESSAGE_STATUSES.index(self._messageStatus)
1574 newSelectedIndex = hildonize.touch_selector(
1577 self.MESSAGE_STATUSES,
1580 except RuntimeError:
1583 if selectedIndex != newSelectedIndex:
1584 self._messageStatus = self.MESSAGE_STATUSES[newSelectedIndex]
1585 self._messageStatusButton.set_label(self._messageStatus)
1586 self._messagemodelfiltered.refilter()
1587 except Exception, e:
1588 self._errorDisplay.push_exception()
1591 class ContactsView(object):
1593 CONTACT_TYPE_IDX = 0
1594 CONTACT_NAME_IDX = 1
1597 def __init__(self, widgetTree, backend, errorDisplay):
1598 self._errorDisplay = errorDisplay
1599 self._backend = backend
1601 self._addressBook = None
1602 self._selectedComboIndex = 0
1603 self._addressBookFactories = [null_backend.NullAddressBook()]
1605 self._booksList = []
1606 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1608 self._isPopulated = False
1609 self._contactsmodel = gtk.ListStore(
1610 gobject.TYPE_STRING, # Contact Type
1611 gobject.TYPE_STRING, # Contact Name
1612 gobject.TYPE_STRING, # Contact ID
1614 self._contactsviewselection = None
1615 self._contactsview = widgetTree.get_widget("contactsview")
1617 self._contactColumn = gtk.TreeViewColumn("Contact")
1618 displayContactSource = False
1619 if displayContactSource:
1620 textrenderer = gtk.CellRendererText()
1621 self._contactColumn.pack_start(textrenderer, expand=False)
1622 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_TYPE_IDX)
1623 textrenderer = gtk.CellRendererText()
1624 hildonize.set_cell_thumb_selectable(textrenderer)
1625 self._contactColumn.pack_start(textrenderer, expand=True)
1626 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_NAME_IDX)
1627 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1628 self._contactColumn.set_sort_column_id(1)
1629 self._contactColumn.set_visible(True)
1631 self._onContactsviewRowActivatedId = 0
1632 self._onAddressbookButtonChangedId = 0
1633 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1635 self._updateSink = gtk_toolbox.threaded_stage(
1637 self._idly_populate_contactsview,
1638 gtk_toolbox.null_sink(),
1643 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1645 self._contactsview.set_model(self._contactsmodel)
1646 self._contactsview.set_fixed_height_mode(False)
1647 self._contactsview.append_column(self._contactColumn)
1648 self._contactsviewselection = self._contactsview.get_selection()
1649 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1651 del self._booksList[:]
1652 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1653 if factoryName and bookName:
1654 entryName = "%s: %s" % (factoryName, bookName)
1656 entryName = factoryName
1658 entryName = bookName
1660 entryName = "Bad name (%d)" % factoryId
1661 row = (str(factoryId), bookId, entryName)
1662 self._booksList.append(row)
1664 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1665 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1667 if len(self._booksList) <= self._selectedComboIndex:
1668 self._selectedComboIndex = 0
1669 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1671 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1672 selectedBookId = self._booksList[self._selectedComboIndex][1]
1673 self.open_addressbook(selectedFactoryId, selectedBookId)
1676 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1677 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1681 self._bookSelectionButton.set_label("")
1682 self._contactsview.set_model(None)
1683 self._contactsview.remove_column(self._contactColumn)
1685 def add_contact(self, *args, **kwds):
1687 @note Actual dial function is patched in later
1689 raise NotImplementedError("Horrible unknown error has occurred")
1691 def get_addressbooks(self):
1693 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1695 for i, factory in enumerate(self._addressBookFactories):
1696 for bookFactory, bookId, bookName in factory.get_addressbooks():
1697 yield (str(i), bookId), (factory.factory_name(), bookName)
1699 def open_addressbook(self, bookFactoryId, bookId):
1700 bookFactoryIndex = int(bookFactoryId)
1701 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1702 self._addressBook = addressBook
1704 def update(self, force = False):
1705 if not force and self._isPopulated:
1707 self._updateSink.send(())
1711 self._isPopulated = False
1712 self._contactsmodel.clear()
1713 for factory in self._addressBookFactories:
1714 factory.clear_caches()
1715 self._addressBook.clear_caches()
1717 def append(self, book):
1718 self._addressBookFactories.append(book)
1720 def extend(self, books):
1721 self._addressBookFactories.extend(books)
1727 def load_settings(self, config, sectionName):
1729 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1730 except ConfigParser.NoOptionError:
1731 self._selectedComboIndex = 0
1733 def save_settings(self, config, sectionName):
1734 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1736 def set_orientation(self, orientation):
1737 if orientation == gtk.ORIENTATION_VERTICAL:
1739 elif orientation == gtk.ORIENTATION_HORIZONTAL:
1742 raise NotImplementedError(orientation)
1744 def _idly_populate_contactsview(self):
1745 with gtk_toolbox.gtk_lock():
1746 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1749 while addressBook is not self._addressBook:
1750 addressBook = self._addressBook
1751 with gtk_toolbox.gtk_lock():
1752 self._contactsview.set_model(None)
1756 contacts = addressBook.get_contacts()
1757 except Exception, e:
1759 self._isPopulated = False
1760 self._errorDisplay.push_exception_with_lock()
1761 for contactId, contactName in contacts:
1762 contactType = addressBook.contact_source_short_name(contactId)
1763 row = contactType, contactName, contactId
1764 self._contactsmodel.append(row)
1766 with gtk_toolbox.gtk_lock():
1767 self._contactsview.set_model(self._contactsmodel)
1769 self._isPopulated = True
1770 except Exception, e:
1771 self._errorDisplay.push_exception_with_lock()
1773 with gtk_toolbox.gtk_lock():
1774 hildonize.show_busy_banner_end(banner)
1777 def _on_addressbook_button_changed(self, *args, **kwds):
1780 newSelectedComboIndex = hildonize.touch_selector(
1783 (("%s" % m[2]) for m in self._booksList),
1784 self._selectedComboIndex,
1786 except RuntimeError:
1789 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1790 selectedBookId = self._booksList[newSelectedComboIndex][1]
1792 oldAddressbook = self._addressBook
1793 self.open_addressbook(selectedFactoryId, selectedBookId)
1794 forceUpdate = True if oldAddressbook is not self._addressBook else False
1795 self.update(force=forceUpdate)
1797 self._selectedComboIndex = newSelectedComboIndex
1798 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1799 except Exception, e:
1800 self._errorDisplay.push_exception()
1802 def _on_contactsview_row_activated(self, treeview, path, view_column):
1804 itr = self._contactsmodel.get_iter(path)
1808 contactId = self._contactsmodel.get_value(itr, self.CONTACT_ID_IDX)
1809 contactName = self._contactsmodel.get_value(itr, self.CONTACT_NAME_IDX)
1811 contactDetails = self._addressBook.get_contact_details(contactId)
1812 except Exception, e:
1814 self._errorDisplay.push_exception()
1815 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1817 if len(contactPhoneNumbers) == 0:
1822 contactPhoneNumbers,
1823 messages = (contactName, ),
1825 self._contactsviewselection.unselect_all()
1826 except Exception, e:
1827 self._errorDisplay.push_exception()