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 contactPhoneNumbers = []
222 if contactId and contactId != '0':
224 contactPhoneNumbers = list(backend.get_contact_details(contactId))
226 contactPhoneNumbers = []
227 if contactPhoneNumbers:
228 uglyContactNumbers = (
229 make_ugly(contactNumber)
230 for (numberDescription, contactNumber) in contactPhoneNumbers
234 number == contactNumber or
235 number[1:] == contactNumber and number.startswith("1") or
236 number[2:] == contactNumber and number.startswith("+1") or
237 number == contactNumber[1:] and contactNumber.startswith("1") or
238 number == contactNumber[2:] and contactNumber.startswith("+1")
240 for contactNumber in uglyContactNumbers
243 defaultIndex = defaultMatches.index(True)
245 contactPhoneNumbers.append(("Other", number))
246 defaultIndex = len(contactPhoneNumbers)-1
248 "Could not find contact %r's number %s among %r" % (
249 contactId, number, contactPhoneNumbers
253 if not contactPhoneNumbers:
254 contactPhoneNumbers = [("Phone", number)]
257 return contactPhoneNumbers, defaultIndex
260 class SmsEntryWindow(object):
264 def __init__(self, widgetTree, parent, app):
265 self._clipboard = gtk.clipboard_get()
266 self._widgetTree = widgetTree
267 self._parent = parent
269 self._isFullScreen = False
271 self._window = self._widgetTree.get_widget("smsWindow")
272 self._window = hildonize.hildonize_window(self._app, self._window)
273 self._window.set_title("SMS")
274 self._window.connect("delete-event", self._on_delete)
275 self._window.connect("key-press-event", self._on_key_press)
276 self._window.connect("window-state-event", self._on_window_state_change)
277 self._widgetTree.get_widget("smsMessagesViewPort").get_parent().show()
279 errorBox = self._widgetTree.get_widget("smsErrorEventBox")
280 errorDescription = self._widgetTree.get_widget("smsErrorDescription")
281 errorClose = self._widgetTree.get_widget("smsErrorClose")
282 self._errorDisplay = gtk_toolbox.ErrorDisplay(errorBox, errorDescription, errorClose)
284 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
285 self._smsButton.connect("clicked", self._on_send)
286 self._dialButton = self._widgetTree.get_widget("dialButton")
287 self._dialButton.connect("clicked", self._on_dial)
289 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
291 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
292 self._messagesView = self._widgetTree.get_widget("smsMessages")
294 textrenderer = gtk.CellRendererText()
295 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
296 textrenderer.set_property("wrap-width", 450)
297 messageColumn = gtk.TreeViewColumn("")
298 messageColumn.pack_start(textrenderer, expand=True)
299 messageColumn.add_attribute(textrenderer, "markup", 0)
300 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
301 self._messagesView.append_column(messageColumn)
302 self._messagesView.set_headers_visible(False)
303 self._messagesView.set_model(self._messagemodel)
304 self._messagesView.set_fixed_height_mode(False)
306 self._conversationView = self._messagesView.get_parent()
307 self._conversationViewPort = self._conversationView.get_parent()
308 self._scrollWindow = self._conversationViewPort.get_parent()
310 self._targetList = self._widgetTree.get_widget("smsTargetList")
311 self._phoneButton = self._widgetTree.get_widget("phoneTypeSelection")
312 self._phoneButton.connect("clicked", self._on_phone)
313 self._smsEntry = self._widgetTree.get_widget("smsEntry")
314 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
315 self._smsEntrySize = None
319 def add_contact(self, name, contactDetails, messages = (), defaultIndex = -1):
320 contactNumbers = list(self._to_contact_numbers(contactDetails))
321 assert contactNumbers, "Contact must have at least one number"
322 contactIndex = defaultIndex if defaultIndex != -1 else 0
323 contact = contactNumbers, contactIndex, messages
324 self._contacts.append(contact)
326 nameLabel = gtk.Label(name)
327 selector = gtk.Button(contactNumbers[0][1])
328 if len(contactNumbers) == 1:
329 selector.set_sensitive(False)
330 removeContact = gtk.Button(stock="gtk-delete")
332 row.pack_start(nameLabel, True, True)
333 row.pack_start(selector, True, True)
334 row.pack_start(removeContact, False, False)
336 self._targetList.pack_start(row)
337 selector.connect("clicked", self._on_choose_phone_n, row)
338 removeContact.connect("clicked", self._on_remove_phone_n, row)
339 self._update_button_state()
340 self._update_context()
342 parentSize = self._parent.get_size()
343 self._window.resize(parentSize[0], max(parentSize[1]-10, 100))
345 self._window.present()
347 self._smsEntry.grab_focus()
348 self._scroll_to_bottom()
351 del self._contacts[:]
353 for row in list(self._targetList.get_children()):
354 self._targetList.remove(row)
355 self._smsEntry.get_buffer().set_text("")
356 self._update_letter_count()
357 self._update_context()
359 def fullscreen(self):
360 self._window.fullscreen()
362 def unfullscreen(self):
363 self._window.unfullscreen()
365 def _remove_contact(self, contactIndex):
366 del self._contacts[contactIndex]
368 row = list(self._targetList.get_children())[contactIndex]
369 self._targetList.remove(row)
370 self._update_button_state()
371 self._update_context()
372 self._scroll_to_bottom()
374 def _scroll_to_bottom(self):
375 dx = self._conversationView.get_allocation().height - self._conversationViewPort.get_allocation().height
377 adjustment = self._scrollWindow.get_vadjustment()
378 adjustment.value = dx
380 def _update_letter_count(self):
381 if self._smsEntrySize is None:
382 self._smsEntrySize = self._smsEntry.size_request()
384 self._smsEntry.set_size_request(*self._smsEntrySize)
385 entryLength = self._smsEntry.get_buffer().get_char_count()
387 numTexts, numCharInText = divmod(entryLength, self.MAX_CHAR)
389 self._letterCountLabel.set_text("%s.%s" % (numTexts, numCharInText))
391 self._letterCountLabel.set_text("%s" % (numCharInText, ))
393 self._update_button_state()
395 def _update_context(self):
396 self._messagemodel.clear()
397 if len(self._contacts) == 0:
398 self._messagesView.hide()
399 self._targetList.hide()
400 self._phoneButton.hide()
401 self._phoneButton.set_label("Error: You shouldn't see this")
402 elif len(self._contacts) == 1:
403 contactNumbers, index, messages = self._contacts[0]
405 self._messagesView.show()
406 for message in messages:
408 self._messagemodel.append(row)
409 messagesSelection = self._messagesView.get_selection()
410 messagesSelection.select_path((len(messages)-1, ))
412 self._messagesView.hide()
413 self._targetList.hide()
414 self._phoneButton.show()
415 self._phoneButton.set_label(contactNumbers[index][1])
416 if 1 < len(contactNumbers):
417 self._phoneButton.set_sensitive(True)
419 self._phoneButton.set_sensitive(False)
421 self._messagesView.hide()
422 self._targetList.show()
423 self._phoneButton.hide()
424 self._phoneButton.set_label("Error: You shouldn't see this")
426 def _update_button_state(self):
427 if len(self._contacts) == 0:
428 self._dialButton.set_sensitive(False)
429 self._smsButton.set_sensitive(False)
430 elif len(self._contacts) == 1:
431 entryLength = self._smsEntry.get_buffer().get_char_count()
433 self._dialButton.set_sensitive(True)
434 self._smsButton.set_sensitive(False)
436 self._dialButton.set_sensitive(False)
437 self._smsButton.set_sensitive(True)
439 self._dialButton.set_sensitive(False)
440 self._smsButton.set_sensitive(True)
442 def _to_contact_numbers(self, contactDetails):
443 for phoneType, phoneNumber in contactDetails:
444 display = " - ".join((make_pretty(phoneNumber), phoneType))
445 yield (phoneNumber, display)
447 def _pseudo_destroy(self):
451 def _request_number(self, contactIndex):
452 contactNumbers, index, messages = self._contacts[contactIndex]
453 assert 0 <= index, "%r" % index
455 index = hildonize.touch_selector(
458 (description for (number, description) in contactNumbers),
461 self._contacts[contactIndex] = contactNumbers, index, messages
463 def send_sms(self, numbers, message):
464 raise NotImplementedError()
466 def dial(self, number):
467 raise NotImplementedError()
469 def _on_phone(self, *args):
471 assert len(self._contacts) == 1, "One and only one contact is required"
472 self._request_number(0)
474 contactNumbers, numberIndex, messages = self._contacts[0]
475 self._phoneButton.set_label(contactNumbers[numberIndex][1])
476 row = list(self._targetList.get_children())[0]
477 phoneButton = list(row.get_children())[1]
478 phoneButton.set_label(contactNumbers[numberIndex][1])
480 self._errorDisplay.push_exception()
482 def _on_choose_phone_n(self, button, row):
484 assert 1 < len(self._contacts), "More than one contact required"
485 targetList = list(self._targetList.get_children())
486 index = targetList.index(row)
487 self._request_number(index)
489 contactNumbers, numberIndex, messages = self._contacts[0]
490 phoneButton = list(row.get_children())[1]
491 phoneButton.set_label(contactNumbers[numberIndex][1])
493 self._errorDisplay.push_exception()
495 def _on_remove_phone_n(self, button, row):
497 assert 1 < len(self._contacts), "More than one contact required"
498 targetList = list(self._targetList.get_children())
499 index = targetList.index(row)
501 del self._contacts[index]
502 self._targetList.remove(row)
503 self._update_context()
504 self._update_button_state()
506 self._errorDisplay.push_exception()
508 def _on_entry_changed(self, *args):
510 self._update_letter_count()
512 self._errorDisplay.push_exception()
514 def _on_send(self, *args):
516 assert 0 < len(self._contacts), "At least one contact required (%r)" % self._contacts
518 make_ugly(contact[0][contact[1]][0])
519 for contact in self._contacts
522 entryBuffer = self._smsEntry.get_buffer()
523 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
524 enteredMessage = enteredMessage.strip()
525 assert enteredMessage, "No message provided"
526 self.send_sms(phoneNumbers, enteredMessage)
527 self._pseudo_destroy()
529 self._errorDisplay.push_exception()
531 def _on_dial(self, *args):
533 assert len(self._contacts) == 1, "One and only one contact allowed (%r)" % self._contacts
534 contact = self._contacts[0]
535 contactNumber = contact[0][contact[1]][0]
536 phoneNumber = make_ugly(contactNumber)
537 self.dial(phoneNumber)
538 self._pseudo_destroy()
540 self._errorDisplay.push_exception()
542 def _on_delete(self, *args):
544 self._window.emit_stop_by_name("delete-event")
545 if hildonize.IS_FREMANTLE_SUPPORTED:
548 self._pseudo_destroy()
550 self._errorDisplay.push_exception()
553 def _on_window_state_change(self, widget, event, *args):
555 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
556 self._isFullScreen = True
558 self._isFullScreen = False
560 self._errorDisplay.push_exception()
562 def _on_key_press(self, widget, event):
563 RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
566 event.keyval == gtk.keysyms.F6 or
567 event.keyval in RETURN_TYPES and event.get_state() & gtk.gdk.CONTROL_MASK
569 if self._isFullScreen:
570 self._window.unfullscreen()
572 self._window.fullscreen()
573 elif event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
576 for messagePart in self._messagemodel
578 self._clipboard.set_text(str(message))
580 event.keyval == gtk.keysyms.h and
581 event.get_state() & gtk.gdk.CONTROL_MASK
585 event.keyval == gtk.keysyms.w and
586 event.get_state() & gtk.gdk.CONTROL_MASK
588 self._pseudo_destroy()
590 event.keyval == gtk.keysyms.q and
591 event.get_state() & gtk.gdk.CONTROL_MASK
593 self._parent.destroy()
595 self._errorDisplay.push_exception()
598 class Dialpad(object):
600 def __init__(self, widgetTree, errorDisplay):
601 self._clipboard = gtk.clipboard_get()
602 self._errorDisplay = errorDisplay
604 self._numberdisplay = widgetTree.get_widget("numberdisplay")
605 self._callButton = widgetTree.get_widget("dialpadCall")
606 self._sendSMSButton = widgetTree.get_widget("dialpadSMS")
607 self._backButton = widgetTree.get_widget("back")
608 self._plusButton = widgetTree.get_widget("plus")
609 self._phonenumber = ""
610 self._prettynumber = ""
613 "on_digit_clicked": self._on_digit_clicked,
615 widgetTree.signal_autoconnect(callbackMapping)
616 self._sendSMSButton.connect("clicked", self._on_sms_clicked)
617 self._callButton.connect("clicked", self._on_call_clicked)
618 self._plusButton.connect("clicked", self._on_plus)
620 self._originalLabel = self._backButton.get_label()
621 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
622 self._backTapHandler.on_tap = self._on_backspace
623 self._backTapHandler.on_hold = self._on_clearall
624 self._backTapHandler.on_holding = self._set_clear_button
625 self._backTapHandler.on_cancel = self._reset_back_button
627 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
628 self._keyPressEventId = 0
631 self._sendSMSButton.grab_focus()
632 self._backTapHandler.enable()
633 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
636 self._window.disconnect(self._keyPressEventId)
637 self._keyPressEventId = 0
638 self._reset_back_button()
639 self._backTapHandler.disable()
641 def add_contact(self, *args, **kwds):
643 @note Actual function is patched in later
645 raise NotImplementedError("Horrible unknown error has occurred")
647 def dial(self, number):
649 @note Actual function is patched in later
651 raise NotImplementedError("Horrible unknown error has occurred")
653 def get_number(self):
654 return self._phonenumber
656 def set_number(self, number):
658 Set the number to dial
661 self._phonenumber = make_ugly(number)
662 self._prettynumber = make_pretty(self._phonenumber)
663 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
664 if self._phonenumber:
665 self._plusButton.set_sensitive(False)
667 self._plusButton.set_sensitive(True)
669 self._errorDisplay.push_exception()
678 def load_settings(self, config, section):
681 def save_settings(self, config, section):
683 @note Thread Agnostic
687 def set_orientation(self, orientation):
688 if orientation == gtk.ORIENTATION_VERTICAL:
690 elif orientation == gtk.ORIENTATION_HORIZONTAL:
693 raise NotImplementedError(orientation)
695 def _on_key_press(self, widget, event):
697 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
698 contents = self._clipboard.wait_for_text()
699 if contents is not None:
700 self.set_number(contents)
702 self._errorDisplay.push_exception()
704 def _on_call_clicked(self, widget):
706 phoneNumber = self.get_number()
707 self.dial(phoneNumber)
710 self._errorDisplay.push_exception()
712 def _on_sms_clicked(self, widget):
714 phoneNumber = self.get_number()
717 [("Dialer", phoneNumber)], ()
721 self._errorDisplay.push_exception()
723 def _on_digit_clicked(self, widget):
725 self.set_number(self._phonenumber + widget.get_name()[-1])
727 self._errorDisplay.push_exception()
729 def _on_plus(self, *args):
731 self.set_number(self._phonenumber + "+")
733 self._errorDisplay.push_exception()
735 def _on_backspace(self, taps):
737 self.set_number(self._phonenumber[:-taps])
738 self._reset_back_button()
740 self._errorDisplay.push_exception()
742 def _on_clearall(self, taps):
745 self._reset_back_button()
747 self._errorDisplay.push_exception()
750 def _set_clear_button(self):
752 self._backButton.set_label("gtk-clear")
754 self._errorDisplay.push_exception()
756 def _reset_back_button(self):
758 self._backButton.set_label(self._originalLabel)
760 self._errorDisplay.push_exception()
763 class AccountInfo(object):
765 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
766 self._errorDisplay = errorDisplay
767 self._backend = backend
768 self._isPopulated = False
769 self._alarmHandler = alarmHandler
770 self._notifyOnMissed = False
771 self._notifyOnVoicemail = False
772 self._notifyOnSms = False
774 self._callbackList = []
775 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
776 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
777 self._onCallbackSelectChangedId = 0
779 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
780 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
781 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
782 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
783 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
784 self._onNotifyToggled = 0
785 self._onMinutesChanged = 0
786 self._onMissedToggled = 0
787 self._onVoicemailToggled = 0
788 self._onSmsToggled = 0
789 self._applyAlarmTimeoutId = None
791 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
792 self._callbackNumber = ""
795 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
797 self._accountViewNumberDisplay.set_use_markup(True)
798 self.set_account_number("")
800 del self._callbackList[:]
801 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
802 self._set_callback_label("")
804 if self._alarmHandler is not None:
805 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
806 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
807 self._missedCheckbox.set_active(self._notifyOnMissed)
808 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
809 self._smsCheckbox.set_active(self._notifyOnSms)
811 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
812 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
813 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
814 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
815 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
817 self._notifyCheckbox.set_sensitive(False)
818 self._minutesEntryButton.set_sensitive(False)
819 self._missedCheckbox.set_sensitive(False)
820 self._voicemailCheckbox.set_sensitive(False)
821 self._smsCheckbox.set_sensitive(False)
823 self.update(force=True)
826 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
827 self._onCallbackSelectChangedId = 0
828 self._set_callback_label("")
830 if self._alarmHandler is not None:
831 self._notifyCheckbox.disconnect(self._onNotifyToggled)
832 self._minutesEntryButton.disconnect(self._onMinutesChanged)
833 self._missedCheckbox.disconnect(self._onNotifyToggled)
834 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
835 self._smsCheckbox.disconnect(self._onNotifyToggled)
836 self._onNotifyToggled = 0
837 self._onMinutesChanged = 0
838 self._onMissedToggled = 0
839 self._onVoicemailToggled = 0
840 self._onSmsToggled = 0
842 self._notifyCheckbox.set_sensitive(True)
843 self._minutesEntryButton.set_sensitive(True)
844 self._missedCheckbox.set_sensitive(True)
845 self._voicemailCheckbox.set_sensitive(True)
846 self._smsCheckbox.set_sensitive(True)
849 del self._callbackList[:]
851 def set_account_number(self, number):
853 Displays current account number
855 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
857 def update(self, force = False):
858 if not force and self._isPopulated:
860 self._populate_callback_combo()
861 self.set_account_number(self._backend.get_account_number())
865 self._set_callback_label("")
866 self.set_account_number("")
867 self._isPopulated = False
869 def save_everything(self):
870 raise NotImplementedError
874 return "Account Info"
876 def load_settings(self, config, section):
877 self._callbackNumber = make_ugly(config.get(section, "callback"))
878 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
879 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
880 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
882 def save_settings(self, config, section):
884 @note Thread Agnostic
886 config.set(section, "callback", self._callbackNumber)
887 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
888 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
889 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
891 def set_orientation(self, orientation):
892 if orientation == gtk.ORIENTATION_VERTICAL:
894 elif orientation == gtk.ORIENTATION_HORIZONTAL:
897 raise NotImplementedError(orientation)
899 def _populate_callback_combo(self):
900 self._isPopulated = True
901 del self._callbackList[:]
903 callbackNumbers = self._backend.get_callback_numbers()
905 self._errorDisplay.push_exception()
906 self._isPopulated = False
909 if len(callbackNumbers) == 0:
910 callbackNumbers = {"": "No callback numbers available"}
912 for number, description in callbackNumbers.iteritems():
913 numberDisplay = make_pretty(number)
914 if not numberDisplay:
916 self._callbackList.append((numberDisplay, description))
918 self._set_callback_number(self._callbackNumber)
920 def _set_callback_number(self, number):
922 if not self._backend.is_valid_syntax(number) and 0 < len(number):
923 self._errorDisplay.push_message("%s is not a valid callback number" % number)
924 elif number == self._backend.get_callback_number() and 0 < len(number):
925 _moduleLogger.warning(
926 "Callback number already is %s" % (
927 self._backend.get_callback_number(),
930 self._set_callback_label(number)
932 if number.startswith("1747"): number = "+" + number
933 self._backend.set_callback_number(number)
934 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
935 make_pretty(number), make_pretty(self._backend.get_callback_number())
937 self._callbackNumber = make_ugly(number)
938 self._set_callback_label(number)
940 "Callback number set to %s" % (
941 self._backend.get_callback_number(),
945 self._errorDisplay.push_exception()
947 def _set_callback_label(self, uglyNumber):
948 prettyNumber = make_pretty(uglyNumber)
949 if len(prettyNumber) == 0:
950 prettyNumber = "No Callback Number"
951 self._callbackSelectButton.set_label(prettyNumber)
953 def _update_alarm_settings(self, recurrence):
955 isEnabled = self._notifyCheckbox.get_active()
956 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
957 self._alarmHandler.apply_settings(isEnabled, recurrence)
959 self.save_everything()
960 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
961 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
963 def _on_callbackentry_clicked(self, *args):
965 actualSelection = make_pretty(self._callbackNumber)
968 (number, "%s (%s)" % (number, description))
969 for (number, description) in self._callbackList
971 defaultSelection = userOptions.get(actualSelection, actualSelection)
973 userSelection = hildonize.touch_selector_entry(
976 list(userOptions.itervalues()),
979 reversedUserOptions = dict(
980 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
982 selectedNumber = reversedUserOptions.get(userSelection, userSelection)
984 number = make_ugly(selectedNumber)
985 self._set_callback_number(number)
986 except RuntimeError, e:
987 _moduleLogger.exception("%s" % str(e))
989 self._errorDisplay.push_exception()
991 def _on_notify_toggled(self, *args):
993 if self._applyAlarmTimeoutId is not None:
994 gobject.source_remove(self._applyAlarmTimeoutId)
995 self._applyAlarmTimeoutId = None
996 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
998 self._errorDisplay.push_exception()
1000 def _on_minutes_clicked(self, *args):
1001 recurrenceChoices = [
1014 (12*60, "12 hours"),
1017 actualSelection = self._alarmHandler.recurrence
1019 closestSelectionIndex = 0
1020 for i, possible in enumerate(recurrenceChoices):
1021 if possible[0] <= actualSelection:
1022 closestSelectionIndex = i
1023 recurrenceIndex = hildonize.touch_selector(
1026 (("%s" % m[1]) for m in recurrenceChoices),
1027 closestSelectionIndex,
1029 recurrence = recurrenceChoices[recurrenceIndex][0]
1031 self._update_alarm_settings(recurrence)
1032 except RuntimeError, e:
1033 _moduleLogger.exception("%s" % str(e))
1034 except Exception, e:
1035 self._errorDisplay.push_exception()
1037 def _on_apply_timeout(self, *args):
1039 self._applyAlarmTimeoutId = None
1041 self._update_alarm_settings(self._alarmHandler.recurrence)
1042 except Exception, e:
1043 self._errorDisplay.push_exception()
1046 def _on_missed_toggled(self, *args):
1048 self._notifyOnMissed = self._missedCheckbox.get_active()
1049 self.save_everything()
1050 except Exception, e:
1051 self._errorDisplay.push_exception()
1053 def _on_voicemail_toggled(self, *args):
1055 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
1056 self.save_everything()
1057 except Exception, e:
1058 self._errorDisplay.push_exception()
1060 def _on_sms_toggled(self, *args):
1062 self._notifyOnSms = self._smsCheckbox.get_active()
1063 self.save_everything()
1064 except Exception, e:
1065 self._errorDisplay.push_exception()
1068 class CallHistoryView(object):
1076 HISTORY_ITEM_TYPES = ["All", "Received", "Missed", "Placed"]
1078 def __init__(self, widgetTree, backend, errorDisplay):
1079 self._errorDisplay = errorDisplay
1080 self._backend = backend
1082 self._isPopulated = False
1083 self._historymodel = gtk.ListStore(
1084 gobject.TYPE_STRING, # number
1085 gobject.TYPE_STRING, # date
1086 gobject.TYPE_STRING, # action
1087 gobject.TYPE_STRING, # from
1088 gobject.TYPE_STRING, # from id
1090 self._historymodelfiltered = self._historymodel.filter_new()
1091 self._historymodelfiltered.set_visible_func(self._is_history_visible)
1092 self._historyview = widgetTree.get_widget("historyview")
1093 self._historyviewselection = None
1094 self._onRecentviewRowActivatedId = 0
1096 textrenderer = gtk.CellRendererText()
1097 textrenderer.set_property("yalign", 0)
1098 self._dateColumn = gtk.TreeViewColumn("Date")
1099 self._dateColumn.pack_start(textrenderer, expand=True)
1100 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
1102 textrenderer = gtk.CellRendererText()
1103 textrenderer.set_property("yalign", 0)
1104 self._actionColumn = gtk.TreeViewColumn("Action")
1105 self._actionColumn.pack_start(textrenderer, expand=True)
1106 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
1108 textrenderer = gtk.CellRendererText()
1109 textrenderer.set_property("yalign", 0)
1110 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
1111 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
1112 self._numberColumn = gtk.TreeViewColumn("Number")
1113 self._numberColumn.pack_start(textrenderer, expand=True)
1114 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
1116 textrenderer = gtk.CellRendererText()
1117 textrenderer.set_property("yalign", 0)
1118 hildonize.set_cell_thumb_selectable(textrenderer)
1119 self._nameColumn = gtk.TreeViewColumn("From")
1120 self._nameColumn.pack_start(textrenderer, expand=True)
1121 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
1122 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1124 self._window = gtk_toolbox.find_parent_window(self._historyview)
1126 self._historyFilterSelector = widgetTree.get_widget("historyFilterSelector")
1127 self._historyFilterSelector.connect("clicked", self._on_history_filter_clicked)
1128 self._selectedFilter = "All"
1130 self._updateSink = gtk_toolbox.threaded_stage(
1132 self._idly_populate_historyview,
1133 gtk_toolbox.null_sink(),
1138 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1139 self._historyFilterSelector.set_label(self._selectedFilter)
1141 self._historyview.set_model(self._historymodelfiltered)
1142 self._historyview.set_fixed_height_mode(False)
1144 self._historyview.append_column(self._dateColumn)
1145 self._historyview.append_column(self._actionColumn)
1146 self._historyview.append_column(self._numberColumn)
1147 self._historyview.append_column(self._nameColumn)
1148 self._historyviewselection = self._historyview.get_selection()
1149 self._historyviewselection.set_mode(gtk.SELECTION_SINGLE)
1151 self._onRecentviewRowActivatedId = self._historyview.connect("row-activated", self._on_historyview_row_activated)
1154 self._historyview.disconnect(self._onRecentviewRowActivatedId)
1158 self._historyview.remove_column(self._dateColumn)
1159 self._historyview.remove_column(self._actionColumn)
1160 self._historyview.remove_column(self._nameColumn)
1161 self._historyview.remove_column(self._numberColumn)
1162 self._historyview.set_model(None)
1164 def add_contact(self, *args, **kwds):
1166 @note Actual dial function is patched in later
1168 raise NotImplementedError("Horrible unknown error has occurred")
1170 def update(self, force = False):
1171 if not force and self._isPopulated:
1173 self._updateSink.send(())
1177 self._isPopulated = False
1178 self._historymodel.clear()
1182 return "Recent Calls"
1184 def load_settings(self, config, sectionName):
1186 self._selectedFilter = config.get(sectionName, "filter")
1187 if self._selectedFilter not in self.HISTORY_ITEM_TYPES:
1188 self._messageType = self.HISTORY_ITEM_TYPES[0]
1189 except ConfigParser.NoOptionError:
1192 def save_settings(self, config, sectionName):
1194 @note Thread Agnostic
1196 config.set(sectionName, "filter", self._selectedFilter)
1198 def set_orientation(self, orientation):
1199 if orientation == gtk.ORIENTATION_VERTICAL:
1201 elif orientation == gtk.ORIENTATION_HORIZONTAL:
1204 raise NotImplementedError(orientation)
1206 def _is_history_visible(self, model, iter):
1208 action = model.get_value(iter, self.ACTION_IDX)
1210 return False # this seems weird but oh well
1212 if self._selectedFilter in [action, "All"]:
1216 except Exception, e:
1217 self._errorDisplay.push_exception()
1219 def _idly_populate_historyview(self):
1220 with gtk_toolbox.gtk_lock():
1221 banner = hildonize.show_busy_banner_start(self._window, "Loading Call History")
1223 self._historymodel.clear()
1224 self._isPopulated = True
1227 historyItems = self._backend.get_recent()
1228 except Exception, e:
1229 self._errorDisplay.push_exception_with_lock()
1230 self._isPopulated = False
1234 gv_backend.decorate_recent(data)
1235 for data in gv_backend.sort_messages(historyItems)
1238 for contactId, personName, phoneNumber, date, action in historyItems:
1240 personName = "Unknown"
1241 date = abbrev_relative_date(date)
1242 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1243 prettyNumber = make_pretty(prettyNumber)
1244 item = (prettyNumber, date, action.capitalize(), personName, contactId)
1245 with gtk_toolbox.gtk_lock():
1246 self._historymodel.append(item)
1247 except Exception, e:
1248 self._errorDisplay.push_exception_with_lock()
1250 with gtk_toolbox.gtk_lock():
1251 hildonize.show_busy_banner_end(banner)
1255 def _on_history_filter_clicked(self, *args, **kwds):
1257 selectedComboIndex = self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
1260 newSelectedComboIndex = hildonize.touch_selector(
1263 self.HISTORY_ITEM_TYPES,
1266 except RuntimeError:
1269 option = self.HISTORY_ITEM_TYPES[newSelectedComboIndex]
1270 self._selectedFilter = option
1271 self._historyFilterSelector.set_label(self._selectedFilter)
1272 self._historymodelfiltered.refilter()
1273 except Exception, e:
1274 self._errorDisplay.push_exception()
1276 def _history_summary(self, expectedNumber):
1277 for number, action, date, whoFrom, whoFromId in self._historymodel:
1278 if expectedNumber is not None and expectedNumber == number:
1279 yield "%s <i>(%s)</i> - %s %s" % (number, whoFrom, date, action)
1281 def _on_historyview_row_activated(self, treeview, path, view_column):
1283 childPath = self._historymodelfiltered.convert_path_to_child_path(path)
1284 itr = self._historymodel.get_iter(childPath)
1288 prettyNumber = self._historymodel.get_value(itr, self.NUMBER_IDX)
1289 number = make_ugly(prettyNumber)
1290 description = list(self._history_summary(prettyNumber))
1291 contactName = self._historymodel.get_value(itr, self.FROM_IDX)
1292 contactId = self._historymodel.get_value(itr, self.FROM_ID_IDX)
1293 contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1297 contactPhoneNumbers,
1298 messages = description,
1299 defaultIndex = defaultIndex,
1301 self._historyviewselection.unselect_all()
1302 except Exception, e:
1303 self._errorDisplay.push_exception()
1306 class MessagesView(object):
1314 MESSAGE_DATA_IDX = 6
1316 NO_MESSAGES = "None"
1317 VOICEMAIL_MESSAGES = "Voicemail"
1318 TEXT_MESSAGES = "SMS"
1319 ALL_TYPES = "All Messages"
1320 MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
1322 UNREAD_STATUS = "Unread"
1323 UNARCHIVED_STATUS = "Inbox"
1325 MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
1327 def __init__(self, widgetTree, backend, errorDisplay):
1328 self._errorDisplay = errorDisplay
1329 self._backend = backend
1331 self._isPopulated = False
1332 self._messagemodel = gtk.ListStore(
1333 gobject.TYPE_STRING, # number
1334 gobject.TYPE_STRING, # date
1335 gobject.TYPE_STRING, # header
1336 gobject.TYPE_STRING, # message
1338 gobject.TYPE_STRING, # from id
1339 object, # message data
1341 self._messagemodelfiltered = self._messagemodel.filter_new()
1342 self._messagemodelfiltered.set_visible_func(self._is_message_visible)
1343 self._messageview = widgetTree.get_widget("messages_view")
1344 self._messageviewselection = None
1345 self._onMessageviewRowActivatedId = 0
1347 self._messageRenderer = gtk.CellRendererText()
1348 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1349 self._messageRenderer.set_property("wrap-width", 500)
1350 self._messageColumn = gtk.TreeViewColumn("Messages")
1351 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1352 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1353 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1355 self._window = gtk_toolbox.find_parent_window(self._messageview)
1357 self._messageTypeButton = widgetTree.get_widget("messageTypeButton")
1358 self._onMessageTypeClickedId = 0
1359 self._messageType = self.ALL_TYPES
1360 self._messageStatusButton = widgetTree.get_widget("messageStatusButton")
1361 self._onMessageStatusClickedId = 0
1362 self._messageStatus = self.ALL_STATUS
1364 self._updateSink = gtk_toolbox.threaded_stage(
1366 self._idly_populate_messageview,
1367 gtk_toolbox.null_sink(),
1372 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1373 self._messageview.set_model(self._messagemodelfiltered)
1374 self._messageview.set_headers_visible(False)
1375 self._messageview.set_fixed_height_mode(False)
1377 self._messageview.append_column(self._messageColumn)
1378 self._messageviewselection = self._messageview.get_selection()
1379 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1381 self._messageTypeButton.set_label(self._messageType)
1382 self._messageStatusButton.set_label(self._messageStatus)
1384 self._onMessageviewRowActivatedId = self._messageview.connect(
1385 "row-activated", self._on_messageview_row_activated
1387 self._onMessageTypeClickedId = self._messageTypeButton.connect(
1388 "clicked", self._on_message_type_clicked
1390 self._onMessageStatusClickedId = self._messageStatusButton.connect(
1391 "clicked", self._on_message_status_clicked
1395 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1396 self._messageTypeButton.disconnect(self._onMessageTypeClickedId)
1397 self._messageStatusButton.disconnect(self._onMessageStatusClickedId)
1401 self._messageview.remove_column(self._messageColumn)
1402 self._messageview.set_model(None)
1404 def add_contact(self, *args, **kwds):
1406 @note Actual dial function is patched in later
1408 raise NotImplementedError("Horrible unknown error has occurred")
1410 def update(self, force = False):
1411 if not force and self._isPopulated:
1413 self._updateSink.send(())
1417 self._isPopulated = False
1418 self._messagemodel.clear()
1424 def load_settings(self, config, sectionName):
1426 self._messageType = config.get(sectionName, "type")
1427 if self._messageType not in self.MESSAGE_TYPES:
1428 self._messageType = self.ALL_TYPES
1429 self._messageStatus = config.get(sectionName, "status")
1430 if self._messageStatus not in self.MESSAGE_STATUSES:
1431 self._messageStatus = self.ALL_STATUS
1432 except ConfigParser.NoOptionError:
1435 def save_settings(self, config, sectionName):
1437 @note Thread Agnostic
1439 config.set(sectionName, "status", self._messageStatus)
1440 config.set(sectionName, "type", self._messageType)
1442 def set_orientation(self, orientation):
1443 if orientation == gtk.ORIENTATION_VERTICAL:
1445 elif orientation == gtk.ORIENTATION_HORIZONTAL:
1448 raise NotImplementedError(orientation)
1450 def _is_message_visible(self, model, iter):
1452 message = model.get_value(iter, self.MESSAGE_DATA_IDX)
1454 return False # this seems weird but oh well
1455 return self._filter_messages(message, self._messageType, self._messageStatus)
1456 except Exception, e:
1457 self._errorDisplay.push_exception()
1460 def _filter_messages(cls, message, type, status):
1461 if type == cls.ALL_TYPES:
1464 messageType = message["type"]
1465 isType = messageType == type
1467 if status == cls.ALL_STATUS:
1470 isUnarchived = not message["isArchived"]
1471 isUnread = not message["isRead"]
1472 if status == cls.UNREAD_STATUS:
1473 isStatus = isUnarchived and isUnread
1474 elif status == cls.UNARCHIVED_STATUS:
1475 isStatus = isUnarchived
1477 assert "Status %s is bad for %r" % (status, message)
1479 return isType and isStatus
1481 _MIN_MESSAGES_SHOWN = 4
1483 def _idly_populate_messageview(self):
1484 with gtk_toolbox.gtk_lock():
1485 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1487 self._messagemodel.clear()
1488 self._isPopulated = True
1490 if self._messageType == self.NO_MESSAGES:
1494 messageItems = self._backend.get_messages()
1495 except Exception, e:
1496 self._errorDisplay.push_exception_with_lock()
1497 self._isPopulated = False
1501 (gv_backend.decorate_message(message), message)
1502 for message in gv_backend.sort_messages(messageItems)
1505 for (contactId, header, number, relativeDate, messages), messageData in messageItems:
1506 prettyNumber = number[2:] if number.startswith("+1") else number
1507 prettyNumber = make_pretty(prettyNumber)
1509 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1510 expandedMessages = [firstMessage]
1511 expandedMessages.extend(messages)
1512 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1513 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1514 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1515 collapsedMessages = [firstMessage, secondMessage]
1516 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1518 collapsedMessages = expandedMessages
1519 #collapsedMessages = _collapse_message(collapsedMessages, 60, self._MIN_MESSAGES_SHOWN)
1521 number = make_ugly(number)
1523 row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId, messageData
1524 with gtk_toolbox.gtk_lock():
1525 self._messagemodel.append(row)
1526 except Exception, e:
1527 self._errorDisplay.push_exception_with_lock()
1529 with gtk_toolbox.gtk_lock():
1530 hildonize.show_busy_banner_end(banner)
1531 self._messagemodelfiltered.refilter()
1535 def _on_messageview_row_activated(self, treeview, path, view_column):
1537 childPath = self._messagemodelfiltered.convert_path_to_child_path(path)
1538 itr = self._messagemodel.get_iter(childPath)
1542 number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX))
1543 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1545 contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX)
1546 header = self._messagemodel.get_value(itr, self.HEADER_IDX)
1547 contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1551 contactPhoneNumbers,
1552 messages = description,
1553 defaultIndex = defaultIndex,
1555 self._messageviewselection.unselect_all()
1556 except Exception, e:
1557 self._errorDisplay.push_exception()
1559 def _on_message_type_clicked(self, *args, **kwds):
1561 selectedIndex = self.MESSAGE_TYPES.index(self._messageType)
1564 newSelectedIndex = hildonize.touch_selector(
1570 except RuntimeError:
1573 if selectedIndex != newSelectedIndex:
1574 self._messageType = self.MESSAGE_TYPES[newSelectedIndex]
1575 self._messageTypeButton.set_label(self._messageType)
1576 self._messagemodelfiltered.refilter()
1577 except Exception, e:
1578 self._errorDisplay.push_exception()
1580 def _on_message_status_clicked(self, *args, **kwds):
1582 selectedIndex = self.MESSAGE_STATUSES.index(self._messageStatus)
1585 newSelectedIndex = hildonize.touch_selector(
1588 self.MESSAGE_STATUSES,
1591 except RuntimeError:
1594 if selectedIndex != newSelectedIndex:
1595 self._messageStatus = self.MESSAGE_STATUSES[newSelectedIndex]
1596 self._messageStatusButton.set_label(self._messageStatus)
1597 self._messagemodelfiltered.refilter()
1598 except Exception, e:
1599 self._errorDisplay.push_exception()
1602 class ContactsView(object):
1604 CONTACT_TYPE_IDX = 0
1605 CONTACT_NAME_IDX = 1
1608 def __init__(self, widgetTree, backend, errorDisplay):
1609 self._errorDisplay = errorDisplay
1610 self._backend = backend
1612 self._addressBook = None
1613 self._selectedComboIndex = 0
1614 self._addressBookFactories = [null_backend.NullAddressBook()]
1616 self._booksList = []
1617 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1619 self._isPopulated = False
1620 self._contactsmodel = gtk.ListStore(
1621 gobject.TYPE_STRING, # Contact Type
1622 gobject.TYPE_STRING, # Contact Name
1623 gobject.TYPE_STRING, # Contact ID
1625 self._contactsviewselection = None
1626 self._contactsview = widgetTree.get_widget("contactsview")
1628 self._contactColumn = gtk.TreeViewColumn("Contact")
1629 displayContactSource = False
1630 if displayContactSource:
1631 textrenderer = gtk.CellRendererText()
1632 self._contactColumn.pack_start(textrenderer, expand=False)
1633 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_TYPE_IDX)
1634 textrenderer = gtk.CellRendererText()
1635 hildonize.set_cell_thumb_selectable(textrenderer)
1636 self._contactColumn.pack_start(textrenderer, expand=True)
1637 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_NAME_IDX)
1638 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1639 self._contactColumn.set_sort_column_id(1)
1640 self._contactColumn.set_visible(True)
1642 self._onContactsviewRowActivatedId = 0
1643 self._onAddressbookButtonChangedId = 0
1644 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1646 self._updateSink = gtk_toolbox.threaded_stage(
1648 self._idly_populate_contactsview,
1649 gtk_toolbox.null_sink(),
1654 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1656 self._contactsview.set_model(self._contactsmodel)
1657 self._contactsview.set_fixed_height_mode(False)
1658 self._contactsview.append_column(self._contactColumn)
1659 self._contactsviewselection = self._contactsview.get_selection()
1660 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1662 del self._booksList[:]
1663 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1664 if factoryName and bookName:
1665 entryName = "%s: %s" % (factoryName, bookName)
1667 entryName = factoryName
1669 entryName = bookName
1671 entryName = "Bad name (%d)" % factoryId
1672 row = (str(factoryId), bookId, entryName)
1673 self._booksList.append(row)
1675 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1676 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1678 if len(self._booksList) <= self._selectedComboIndex:
1679 self._selectedComboIndex = 0
1680 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1682 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1683 selectedBookId = self._booksList[self._selectedComboIndex][1]
1684 self.open_addressbook(selectedFactoryId, selectedBookId)
1687 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1688 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1692 self._bookSelectionButton.set_label("")
1693 self._contactsview.set_model(None)
1694 self._contactsview.remove_column(self._contactColumn)
1696 def add_contact(self, *args, **kwds):
1698 @note Actual dial function is patched in later
1700 raise NotImplementedError("Horrible unknown error has occurred")
1702 def get_addressbooks(self):
1704 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1706 for i, factory in enumerate(self._addressBookFactories):
1707 for bookFactory, bookId, bookName in factory.get_addressbooks():
1708 yield (str(i), bookId), (factory.factory_name(), bookName)
1710 def open_addressbook(self, bookFactoryId, bookId):
1711 bookFactoryIndex = int(bookFactoryId)
1712 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1713 self._addressBook = addressBook
1715 def update(self, force = False):
1716 if not force and self._isPopulated:
1718 self._updateSink.send(())
1722 self._isPopulated = False
1723 self._contactsmodel.clear()
1724 for factory in self._addressBookFactories:
1725 factory.clear_caches()
1726 self._addressBook.clear_caches()
1728 def append(self, book):
1729 self._addressBookFactories.append(book)
1731 def extend(self, books):
1732 self._addressBookFactories.extend(books)
1738 def load_settings(self, config, sectionName):
1740 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1741 except ConfigParser.NoOptionError:
1742 self._selectedComboIndex = 0
1744 def save_settings(self, config, sectionName):
1745 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1747 def set_orientation(self, orientation):
1748 if orientation == gtk.ORIENTATION_VERTICAL:
1750 elif orientation == gtk.ORIENTATION_HORIZONTAL:
1753 raise NotImplementedError(orientation)
1755 def _idly_populate_contactsview(self):
1756 with gtk_toolbox.gtk_lock():
1757 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1760 while addressBook is not self._addressBook:
1761 addressBook = self._addressBook
1762 with gtk_toolbox.gtk_lock():
1763 self._contactsview.set_model(None)
1767 contacts = addressBook.get_contacts()
1768 except Exception, e:
1770 self._isPopulated = False
1771 self._errorDisplay.push_exception_with_lock()
1772 for contactId, contactName in contacts:
1773 contactType = addressBook.contact_source_short_name(contactId)
1774 row = contactType, contactName, contactId
1775 self._contactsmodel.append(row)
1777 with gtk_toolbox.gtk_lock():
1778 self._contactsview.set_model(self._contactsmodel)
1780 self._isPopulated = True
1781 except Exception, e:
1782 self._errorDisplay.push_exception_with_lock()
1784 with gtk_toolbox.gtk_lock():
1785 hildonize.show_busy_banner_end(banner)
1788 def _on_addressbook_button_changed(self, *args, **kwds):
1791 newSelectedComboIndex = hildonize.touch_selector(
1794 (("%s" % m[2]) for m in self._booksList),
1795 self._selectedComboIndex,
1797 except RuntimeError:
1800 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1801 selectedBookId = self._booksList[newSelectedComboIndex][1]
1803 oldAddressbook = self._addressBook
1804 self.open_addressbook(selectedFactoryId, selectedBookId)
1805 forceUpdate = True if oldAddressbook is not self._addressBook else False
1806 self.update(force=forceUpdate)
1808 self._selectedComboIndex = newSelectedComboIndex
1809 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1810 except Exception, e:
1811 self._errorDisplay.push_exception()
1813 def _on_contactsview_row_activated(self, treeview, path, view_column):
1815 itr = self._contactsmodel.get_iter(path)
1819 contactId = self._contactsmodel.get_value(itr, self.CONTACT_ID_IDX)
1820 contactName = self._contactsmodel.get_value(itr, self.CONTACT_NAME_IDX)
1822 contactDetails = self._addressBook.get_contact_details(contactId)
1823 except Exception, e:
1825 self._errorDisplay.push_exception()
1826 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1828 if len(contactPhoneNumbers) == 0:
1833 contactPhoneNumbers,
1834 messages = (contactName, ),
1836 self._contactsviewselection.unselect_all()
1837 except Exception, e:
1838 self._errorDisplay.push_exception()