4 DialCentral - Front end for Google's GoogleVoice service.
5 Copyright (C) 2008 Mark Bergman bergman AT merctech DOT com
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 Lesser General Public License for more details.
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21 @todo Collapse voicemails
24 from __future__ import with_statement
37 from backends import gv_backend
38 from backends import null_backend
41 _moduleLogger = logging.getLogger("gv_views")
44 def make_ugly(prettynumber):
46 function to take a phone number and strip out all non-numeric
49 >>> make_ugly("+012-(345)-678-90")
52 return normalize_number(prettynumber)
55 def normalize_number(prettynumber):
57 function to take a phone number and strip out all non-numeric
60 >>> normalize_number("+012-(345)-678-90")
62 >>> normalize_number("1-(345)-678-9000")
64 >>> normalize_number("+1-(345)-678-9000")
67 uglynumber = re.sub('[^0-9+]', '', prettynumber)
72 def _make_pretty_with_areacodde(phonenumber):
73 prettynumber = "(%s)" % (phonenumber[0:3], )
74 if 3 < len(phonenumber):
75 prettynumber += " %s" % (phonenumber[3:6], )
76 if 6 < len(phonenumber):
77 prettynumber += "-%s" % (phonenumber[6:], )
81 def _make_pretty_local(phonenumber):
82 prettynumber = "%s" % (phonenumber[0:3], )
83 if 3 < len(phonenumber):
84 prettynumber += "-%s" % (phonenumber[3:], )
88 def _make_pretty_international(phonenumber):
89 prettynumber = phonenumber
90 if phonenumber.startswith("0"):
91 prettynumber = "+%s " % (phonenumber[0:3], )
92 if 3 < len(phonenumber):
93 prettynumber += _make_pretty_with_areacodde(phonenumber[3:])
94 if phonenumber.startswith("1"):
96 prettynumber += _make_pretty_with_areacodde(phonenumber[1:])
100 def make_pretty(phonenumber):
102 Function to take a phone number and return the pretty version
104 if phonenumber begins with 0:
106 if phonenumber begins with 1: ( for gizmo callback numbers )
108 if phonenumber is 13 digits:
110 if phonenumber is 10 digits:
112 >>> make_pretty("12")
114 >>> make_pretty("1234567")
116 >>> make_pretty("2345678901")
118 >>> make_pretty("12345678901")
120 >>> make_pretty("01234567890")
122 >>> make_pretty("+01234567890")
124 >>> make_pretty("+12")
126 >>> make_pretty("+123")
128 >>> make_pretty("+1234")
131 if phonenumber is None or phonenumber is "":
134 phonenumber = normalize_number(phonenumber)
136 if phonenumber[0] == "+":
137 prettynumber = _make_pretty_international(phonenumber[1:])
138 if not prettynumber.startswith("+"):
139 prettynumber = "+"+prettynumber
140 elif 8 < len(phonenumber) and phonenumber[0] in ("0", "1"):
141 prettynumber = _make_pretty_international(phonenumber)
142 elif 7 < len(phonenumber):
143 prettynumber = _make_pretty_with_areacodde(phonenumber)
144 elif 3 < len(phonenumber):
145 prettynumber = _make_pretty_local(phonenumber)
147 prettynumber = phonenumber
148 return prettynumber.strip()
151 def abbrev_relative_date(date):
153 >>> abbrev_relative_date("42 hours ago")
155 >>> abbrev_relative_date("2 days ago")
157 >>> abbrev_relative_date("4 weeks ago")
160 parts = date.split(" ")
161 return "%s %s" % (parts[0], parts[1][0])
164 def _collapse_message(messageLines, maxCharsPerLine, maxLines):
167 numLines = len(messageLines)
168 for line in messageLines[0:min(maxLines, numLines)]:
169 linesPerLine = max(1, int(len(line) / maxCharsPerLine))
170 allowedLines = maxLines - lines
171 acceptedLines = min(allowedLines, linesPerLine)
172 acceptedChars = acceptedLines * maxCharsPerLine
174 if acceptedChars < (len(line) + 3):
177 acceptedChars = len(line) # eh, might as well complete the line
179 abbrevMessage = "%s%s" % (line[0:acceptedChars], suffix)
182 lines += acceptedLines
183 if maxLines <= lines:
187 def collapse_message(message, maxCharsPerLine, maxLines):
189 >>> collapse_message("Hello", 60, 2)
191 >>> collapse_message("Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789", 60, 2)
192 'Hello world how are you doing today? 01234567890123456789012...'
193 >>> collapse_message('''Hello world how are you doing today?
194 ... 01234567890123456789
195 ... 01234567890123456789
196 ... 01234567890123456789
197 ... 01234567890123456789''', 60, 2)
198 'Hello world how are you doing today?\n01234567890123456789'
199 >>> collapse_message('''
200 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
201 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
202 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
203 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
204 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
205 ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789''', 60, 2)
206 '\nHello world how are you doing today? 01234567890123456789012...'
208 messageLines = message.split("\n")
209 return "\n".join(_collapse_message(messageLines, maxCharsPerLine, maxLines))
212 def _get_contact_numbers(backend, contactId, number):
214 contactPhoneNumbers = list(backend.get_contact_details(contactId))
215 uglyContactNumbers = (
216 make_ugly(contactNumber)
217 for (numberDescription, contactNumber) in contactPhoneNumbers
221 number == contactNumber or
222 number[1:] == contactNumber and number.startswith("1") or
223 number[2:] == contactNumber and number.startswith("+1") or
224 number == contactNumber[1:] and contactNumber.startswith("1") or
225 number == contactNumber[2:] and contactNumber.startswith("+1")
227 for contactNumber in uglyContactNumbers
230 defaultIndex = defaultMatches.index(True)
232 contactPhoneNumbers.append(("Other", number))
233 defaultIndex = len(contactPhoneNumbers)-1
235 "Could not find contact %r's number %s among %r" % (
236 contactId, number, contactPhoneNumbers
240 contactPhoneNumbers = [("Phone", number)]
243 return contactPhoneNumbers, defaultIndex
246 class SmsEntryWindow(object):
250 def __init__(self, widgetTree, parent, app):
251 self._clipboard = gtk.clipboard_get()
252 self._widgetTree = widgetTree
253 self._parent = parent
255 self._isFullScreen = False
257 self._window = self._widgetTree.get_widget("smsWindow")
258 self._window = hildonize.hildonize_window(self._app, self._window)
259 self._window.connect("delete-event", self._on_delete)
260 self._window.connect("key-press-event", self._on_key_press)
261 self._window.connect("window-state-event", self._on_window_state_change)
262 self._widgetTree.get_widget("smsMessagesViewPort").get_parent().show()
264 errorBox = self._widgetTree.get_widget("smsErrorEventBox")
265 errorDescription = self._widgetTree.get_widget("smsErrorDescription")
266 errorClose = self._widgetTree.get_widget("smsErrorClose")
267 self._errorDisplay = gtk_toolbox.ErrorDisplay(errorBox, errorDescription, errorClose)
269 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
270 self._smsButton.connect("clicked", self._on_send)
271 self._dialButton = self._widgetTree.get_widget("dialButton")
272 self._dialButton.connect("clicked", self._on_dial)
274 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
276 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
277 self._messagesView = self._widgetTree.get_widget("smsMessages")
279 textrenderer = gtk.CellRendererText()
280 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
281 textrenderer.set_property("wrap-width", 450)
282 messageColumn = gtk.TreeViewColumn("")
283 messageColumn.pack_start(textrenderer, expand=True)
284 messageColumn.add_attribute(textrenderer, "markup", 0)
285 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
286 self._messagesView.append_column(messageColumn)
287 self._messagesView.set_headers_visible(False)
288 self._messagesView.set_model(self._messagemodel)
289 self._messagesView.set_fixed_height_mode(False)
291 self._conversationView = self._messagesView.get_parent()
292 self._conversationViewPort = self._conversationView.get_parent()
293 self._scrollWindow = self._conversationViewPort.get_parent()
295 self._targetList = self._widgetTree.get_widget("smsTargetList")
296 self._phoneButton = self._widgetTree.get_widget("phoneTypeSelection")
297 self._phoneButton.connect("clicked", self._on_phone)
298 self._smsEntry = self._widgetTree.get_widget("smsEntry")
299 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
300 self._smsEntrySize = None
304 def add_contact(self, name, contactDetails, messages = (), defaultIndex = -1):
305 contactNumbers = list(self._to_contact_numbers(contactDetails))
306 assert contactNumbers, "Contact must have at least one number"
307 contactIndex = defaultIndex if defaultIndex != -1 else 0
308 contact = contactNumbers, contactIndex, messages
309 self._contacts.append(contact)
311 nameLabel = gtk.Label(name)
312 selector = gtk.Button(contactNumbers[0][1])
313 if len(contactNumbers) == 1:
314 selector.set_sensitive(False)
315 removeContact = gtk.Button(stock="gtk-delete")
317 row.pack_start(nameLabel, True, True)
318 row.pack_start(selector, True, True)
319 row.pack_start(removeContact, False, False)
321 self._targetList.pack_start(row)
322 selector.connect("clicked", self._on_choose_phone_n, row)
323 removeContact.connect("clicked", self._on_remove_phone_n, row)
324 self._update_button_state()
325 self._update_context()
327 parentSize = self._parent.get_size()
328 self._window.resize(parentSize[0], max(parentSize[1]-10, 100))
330 self._window.present()
332 self._smsEntry.grab_focus()
333 self._scroll_to_bottom()
336 del self._contacts[:]
338 for row in list(self._targetList.get_children()):
339 self._targetList.remove(row)
340 self._smsEntry.get_buffer().set_text("")
341 self._update_letter_count()
342 self._update_context()
344 def fullscreen(self):
345 self._window.fullscreen()
347 def unfullscreen(self):
348 self._window.unfullscreen()
350 def _remove_contact(self, contactIndex):
351 del self._contacts[contactIndex]
353 row = list(self._targetList.get_children())[contactIndex]
354 self._targetList.remove(row)
355 self._update_button_state()
356 self._update_context()
357 self._scroll_to_bottom()
359 def _scroll_to_bottom(self):
360 dx = self._conversationView.get_allocation().height - self._conversationViewPort.get_allocation().height
362 adjustment = self._scrollWindow.get_vadjustment()
363 adjustment.value = dx
365 def _update_letter_count(self):
366 if self._smsEntrySize is None:
367 self._smsEntrySize = self._smsEntry.size_request()
369 self._smsEntry.set_size_request(*self._smsEntrySize)
370 entryLength = self._smsEntry.get_buffer().get_char_count()
372 numTexts, numCharInText = divmod(entryLength, self.MAX_CHAR)
374 self._letterCountLabel.set_text("%s.%s" % (numTexts, numCharInText))
376 self._letterCountLabel.set_text("%s" % (numCharInText, ))
378 self._update_button_state()
380 def _update_context(self):
381 self._messagemodel.clear()
382 if len(self._contacts) == 0:
383 self._messagesView.hide()
384 self._targetList.hide()
385 self._phoneButton.hide()
386 self._phoneButton.set_label("Error: You shouldn't see this")
387 elif len(self._contacts) == 1:
388 contactNumbers, index, messages = self._contacts[0]
390 self._messagesView.show()
391 for message in messages:
393 self._messagemodel.append(row)
394 messagesSelection = self._messagesView.get_selection()
395 messagesSelection.select_path((len(messages)-1, ))
397 self._messagesView.hide()
398 self._targetList.hide()
399 self._phoneButton.show()
400 self._phoneButton.set_label(contactNumbers[index][1])
401 if 1 < len(contactNumbers):
402 self._phoneButton.set_sensitive(True)
404 self._phoneButton.set_sensitive(False)
406 self._messagesView.hide()
407 self._targetList.show()
408 self._phoneButton.hide()
409 self._phoneButton.set_label("Error: You shouldn't see this")
411 def _update_button_state(self):
412 if len(self._contacts) == 0:
413 self._dialButton.set_sensitive(False)
414 self._smsButton.set_sensitive(False)
415 elif len(self._contacts) == 1:
416 entryLength = self._smsEntry.get_buffer().get_char_count()
418 self._dialButton.set_sensitive(True)
419 self._smsButton.set_sensitive(False)
421 self._dialButton.set_sensitive(False)
422 self._smsButton.set_sensitive(True)
424 self._dialButton.set_sensitive(False)
425 self._smsButton.set_sensitive(True)
427 def _to_contact_numbers(self, contactDetails):
428 for phoneType, phoneNumber in contactDetails:
429 display = " - ".join((make_pretty(phoneNumber), phoneType))
430 yield (phoneNumber, display)
432 def _pseudo_destroy(self):
436 def _request_number(self, contactIndex):
437 contactNumbers, index, messages = self._contacts[contactIndex]
438 assert 0 <= index, "%r" % index
440 index = hildonize.touch_selector(
443 (description for (number, description) in contactNumbers),
446 self._contacts[contactIndex] = contactNumbers, index, messages
448 def send_sms(self, numbers, message):
449 raise NotImplementedError()
451 def dial(self, number):
452 raise NotImplementedError()
454 def _on_phone(self, *args):
456 assert len(self._contacts) == 1, "One and only one contact is required"
457 self._request_number(0)
459 contactNumbers, numberIndex, messages = self._contacts[0]
460 self._phoneButton.set_label(contactNumbers[numberIndex][1])
461 row = list(self._targetList.get_children())[0]
462 phoneButton = list(row.get_children())[1]
463 phoneButton.set_label(contactNumbers[numberIndex][1])
465 self._errorDisplay.push_exception()
467 def _on_choose_phone_n(self, button, row):
469 assert 1 < len(self._contacts), "More than one contact required"
470 targetList = list(self._targetList.get_children())
471 index = targetList.index(row)
472 self._request_number(index)
474 contactNumbers, numberIndex, messages = self._contacts[0]
475 phoneButton = list(row.get_children())[1]
476 phoneButton.set_label(contactNumbers[numberIndex][1])
478 self._errorDisplay.push_exception()
480 def _on_remove_phone_n(self, button, row):
482 assert 1 < len(self._contacts), "More than one contact required"
483 targetList = list(self._targetList.get_children())
484 index = targetList.index(row)
486 del self._contacts[index]
487 self._targetList.remove(row)
488 self._update_context()
489 self._update_button_state()
491 self._errorDisplay.push_exception()
493 def _on_entry_changed(self, *args):
495 self._update_letter_count()
497 self._errorDisplay.push_exception()
499 def _on_send(self, *args):
501 assert 0 < len(self._contacts), "At least one contact required (%r)" % self._contacts
503 make_ugly(contact[0][contact[1]][0])
504 for contact in self._contacts
507 entryBuffer = self._smsEntry.get_buffer()
508 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
509 enteredMessage = enteredMessage.strip()
510 assert enteredMessage, "No message provided"
511 self.send_sms(phoneNumbers, enteredMessage)
512 self._pseudo_destroy()
514 self._errorDisplay.push_exception()
516 def _on_dial(self, *args):
518 assert len(self._contacts) == 1, "One and only one contact allowed (%r)" % self._contacts
519 contact = self._contacts[0]
520 contactNumber = contact[0][contact[1]][0]
521 phoneNumber = make_ugly(contactNumber)
522 self.dial(phoneNumber)
523 self._pseudo_destroy()
525 self._errorDisplay.push_exception()
527 def _on_delete(self, *args):
529 self._window.emit_stop_by_name("delete-event")
530 if hildonize.IS_FREMANTLE_SUPPORTED:
533 self._pseudo_destroy()
535 self._errorDisplay.push_exception()
538 def _on_window_state_change(self, widget, event, *args):
540 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
541 self._isFullScreen = True
543 self._isFullScreen = False
545 self._errorDisplay.push_exception()
547 def _on_key_press(self, widget, event):
548 RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
551 event.keyval == gtk.keysyms.F6 or
552 event.keyval in RETURN_TYPES and event.get_state() & gtk.gdk.CONTROL_MASK
554 if self._isFullScreen:
555 self._window.unfullscreen()
557 self._window.fullscreen()
558 elif event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
561 for messagePart in self._messagemodel
563 self._clipboard.set_text(str(message))
565 event.keyval == gtk.keysyms.h and
566 event.get_state() & gtk.gdk.CONTROL_MASK
570 event.keyval == gtk.keysyms.w and
571 event.get_state() & gtk.gdk.CONTROL_MASK
573 self._pseudo_destroy()
575 event.keyval == gtk.keysyms.q and
576 event.get_state() & gtk.gdk.CONTROL_MASK
578 self._parent.destroy()
580 self._errorDisplay.push_exception()
583 class Dialpad(object):
585 def __init__(self, widgetTree, errorDisplay):
586 self._clipboard = gtk.clipboard_get()
587 self._errorDisplay = errorDisplay
589 self._numberdisplay = widgetTree.get_widget("numberdisplay")
590 self._okButton = widgetTree.get_widget("dialpadOk")
591 self._backButton = widgetTree.get_widget("back")
592 self._plusButton = widgetTree.get_widget("plus")
593 self._phonenumber = ""
594 self._prettynumber = ""
597 "on_digit_clicked": self._on_digit_clicked,
599 widgetTree.signal_autoconnect(callbackMapping)
600 self._okButton.connect("clicked", self._on_ok_clicked)
601 self._plusButton.connect("clicked", self._on_plus)
603 self._originalLabel = self._backButton.get_label()
604 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
605 self._backTapHandler.on_tap = self._on_backspace
606 self._backTapHandler.on_hold = self._on_clearall
607 self._backTapHandler.on_holding = self._set_clear_button
608 self._backTapHandler.on_cancel = self._reset_back_button
610 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
611 self._keyPressEventId = 0
614 self._okButton.grab_focus()
615 self._backTapHandler.enable()
616 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
619 self._window.disconnect(self._keyPressEventId)
620 self._keyPressEventId = 0
621 self._reset_back_button()
622 self._backTapHandler.disable()
624 def add_contact(self, *args, **kwds):
626 @note Actual dial function is patched in later
628 raise NotImplementedError("Horrible unknown error has occurred")
630 def get_number(self):
631 return self._phonenumber
633 def set_number(self, number):
635 Set the number to dial
638 self._phonenumber = make_ugly(number)
639 self._prettynumber = make_pretty(self._phonenumber)
640 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
642 self._errorDisplay.push_exception()
651 def load_settings(self, config, section):
654 def save_settings(self, config, section):
656 @note Thread Agnostic
660 def _on_key_press(self, widget, event):
662 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
663 contents = self._clipboard.wait_for_text()
664 if contents is not None:
665 self.set_number(contents)
667 self._errorDisplay.push_exception()
669 def _on_ok_clicked(self, widget):
671 phoneNumber = self.get_number()
674 [("Dialer", phoneNumber)], ()
678 self._errorDisplay.push_exception()
680 def _on_digit_clicked(self, widget):
682 self.set_number(self._phonenumber + widget.get_name()[-1])
684 self._errorDisplay.push_exception()
686 def _on_plus(self, *args):
688 self.set_number(self._phonenumber + "+")
690 self._errorDisplay.push_exception()
692 def _on_backspace(self, taps):
694 self.set_number(self._phonenumber[:-taps])
695 self._reset_back_button()
697 self._errorDisplay.push_exception()
699 def _on_clearall(self, taps):
702 self._reset_back_button()
704 self._errorDisplay.push_exception()
707 def _set_clear_button(self):
709 self._backButton.set_label("gtk-clear")
711 self._errorDisplay.push_exception()
713 def _reset_back_button(self):
715 self._backButton.set_label(self._originalLabel)
717 self._errorDisplay.push_exception()
720 class AccountInfo(object):
722 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
723 self._errorDisplay = errorDisplay
724 self._backend = backend
725 self._isPopulated = False
726 self._alarmHandler = alarmHandler
727 self._notifyOnMissed = False
728 self._notifyOnVoicemail = False
729 self._notifyOnSms = False
731 self._callbackList = []
732 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
733 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
734 self._onCallbackSelectChangedId = 0
736 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
737 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
738 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
739 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
740 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
741 self._onNotifyToggled = 0
742 self._onMinutesChanged = 0
743 self._onMissedToggled = 0
744 self._onVoicemailToggled = 0
745 self._onSmsToggled = 0
746 self._applyAlarmTimeoutId = None
748 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
749 self._callbackNumber = ""
752 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
754 self._accountViewNumberDisplay.set_use_markup(True)
755 self.set_account_number("")
757 del self._callbackList[:]
758 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
759 self._set_callback_label("")
761 if self._alarmHandler is not None:
762 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
763 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
764 self._missedCheckbox.set_active(self._notifyOnMissed)
765 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
766 self._smsCheckbox.set_active(self._notifyOnSms)
768 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
769 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
770 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
771 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
772 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
774 self._notifyCheckbox.set_sensitive(False)
775 self._minutesEntryButton.set_sensitive(False)
776 self._missedCheckbox.set_sensitive(False)
777 self._voicemailCheckbox.set_sensitive(False)
778 self._smsCheckbox.set_sensitive(False)
780 self.update(force=True)
783 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
784 self._onCallbackSelectChangedId = 0
785 self._set_callback_label("")
787 if self._alarmHandler is not None:
788 self._notifyCheckbox.disconnect(self._onNotifyToggled)
789 self._minutesEntryButton.disconnect(self._onMinutesChanged)
790 self._missedCheckbox.disconnect(self._onNotifyToggled)
791 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
792 self._smsCheckbox.disconnect(self._onNotifyToggled)
793 self._onNotifyToggled = 0
794 self._onMinutesChanged = 0
795 self._onMissedToggled = 0
796 self._onVoicemailToggled = 0
797 self._onSmsToggled = 0
799 self._notifyCheckbox.set_sensitive(True)
800 self._minutesEntryButton.set_sensitive(True)
801 self._missedCheckbox.set_sensitive(True)
802 self._voicemailCheckbox.set_sensitive(True)
803 self._smsCheckbox.set_sensitive(True)
806 del self._callbackList[:]
808 def set_account_number(self, number):
810 Displays current account number
812 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
814 def update(self, force = False):
815 if not force and self._isPopulated:
817 self._populate_callback_combo()
818 self.set_account_number(self._backend.get_account_number())
822 self._set_callback_label("")
823 self.set_account_number("")
824 self._isPopulated = False
826 def save_everything(self):
827 raise NotImplementedError
831 return "Account Info"
833 def load_settings(self, config, section):
834 self._callbackNumber = make_ugly(config.get(section, "callback"))
835 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
836 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
837 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
839 def save_settings(self, config, section):
841 @note Thread Agnostic
843 config.set(section, "callback", self._callbackNumber)
844 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
845 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
846 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
848 def _populate_callback_combo(self):
849 self._isPopulated = True
850 del self._callbackList[:]
852 callbackNumbers = self._backend.get_callback_numbers()
854 self._errorDisplay.push_exception()
855 self._isPopulated = False
858 if len(callbackNumbers) == 0:
859 callbackNumbers = {"": "No callback numbers available"}
861 for number, description in callbackNumbers.iteritems():
862 self._callbackList.append((make_pretty(number), description))
864 self._set_callback_number(self._callbackNumber)
866 def _set_callback_number(self, number):
868 if not self._backend.is_valid_syntax(number) and 0 < len(number):
869 self._errorDisplay.push_message("%s is not a valid callback number" % number)
870 elif number == self._backend.get_callback_number() and 0 < len(number):
871 _moduleLogger.warning(
872 "Callback number already is %s" % (
873 self._backend.get_callback_number(),
876 self._set_callback_label(number)
878 if number.startswith("1747"): number = "+" + number
879 self._backend.set_callback_number(number)
880 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
881 make_pretty(number), make_pretty(self._backend.get_callback_number())
883 self._callbackNumber = make_ugly(number)
884 self._set_callback_label(number)
886 "Callback number set to %s" % (
887 self._backend.get_callback_number(),
891 self._errorDisplay.push_exception()
893 def _set_callback_label(self, uglyNumber):
894 prettyNumber = make_pretty(uglyNumber)
895 if len(prettyNumber) == 0:
896 prettyNumber = "No Callback Number"
897 self._callbackSelectButton.set_label(prettyNumber)
899 def _update_alarm_settings(self, recurrence):
901 isEnabled = self._notifyCheckbox.get_active()
902 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
903 self._alarmHandler.apply_settings(isEnabled, recurrence)
905 self.save_everything()
906 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
907 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
909 def _on_callbackentry_clicked(self, *args):
911 actualSelection = make_pretty(self._callbackNumber)
914 (number, "%s (%s)" % (number, description))
915 for (number, description) in self._callbackList
917 defaultSelection = userOptions.get(actualSelection, actualSelection)
919 userSelection = hildonize.touch_selector_entry(
922 list(userOptions.itervalues()),
925 reversedUserOptions = dict(
926 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
928 selectedNumber = reversedUserOptions.get(userSelection, userSelection)
930 number = make_ugly(selectedNumber)
931 self._set_callback_number(number)
932 except RuntimeError, e:
933 _moduleLogger.exception("%s" % str(e))
935 self._errorDisplay.push_exception()
937 def _on_notify_toggled(self, *args):
939 if self._applyAlarmTimeoutId is not None:
940 gobject.source_remove(self._applyAlarmTimeoutId)
941 self._applyAlarmTimeoutId = None
942 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
944 self._errorDisplay.push_exception()
946 def _on_minutes_clicked(self, *args):
947 recurrenceChoices = [
963 actualSelection = self._alarmHandler.recurrence
965 closestSelectionIndex = 0
966 for i, possible in enumerate(recurrenceChoices):
967 if possible[0] <= actualSelection:
968 closestSelectionIndex = i
969 recurrenceIndex = hildonize.touch_selector(
972 (("%s" % m[1]) for m in recurrenceChoices),
973 closestSelectionIndex,
975 recurrence = recurrenceChoices[recurrenceIndex][0]
977 self._update_alarm_settings(recurrence)
978 except RuntimeError, e:
979 _moduleLogger.exception("%s" % str(e))
981 self._errorDisplay.push_exception()
983 def _on_apply_timeout(self, *args):
985 self._applyAlarmTimeoutId = None
987 self._update_alarm_settings(self._alarmHandler.recurrence)
989 self._errorDisplay.push_exception()
992 def _on_missed_toggled(self, *args):
994 self._notifyOnMissed = self._missedCheckbox.get_active()
995 self.save_everything()
997 self._errorDisplay.push_exception()
999 def _on_voicemail_toggled(self, *args):
1001 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
1002 self.save_everything()
1003 except Exception, e:
1004 self._errorDisplay.push_exception()
1006 def _on_sms_toggled(self, *args):
1008 self._notifyOnSms = self._smsCheckbox.get_active()
1009 self.save_everything()
1010 except Exception, e:
1011 self._errorDisplay.push_exception()
1014 class CallHistoryView(object):
1022 HISTORY_ITEM_TYPES = ["All", "Received", "Missed", "Placed"]
1024 def __init__(self, widgetTree, backend, errorDisplay):
1025 self._errorDisplay = errorDisplay
1026 self._backend = backend
1028 self._isPopulated = False
1029 self._historymodel = gtk.ListStore(
1030 gobject.TYPE_STRING, # number
1031 gobject.TYPE_STRING, # date
1032 gobject.TYPE_STRING, # action
1033 gobject.TYPE_STRING, # from
1034 gobject.TYPE_STRING, # from id
1036 self._historymodelfiltered = self._historymodel.filter_new()
1037 self._historymodelfiltered.set_visible_func(self._is_history_visible)
1038 self._historyview = widgetTree.get_widget("historyview")
1039 self._historyviewselection = None
1040 self._onRecentviewRowActivatedId = 0
1042 textrenderer = gtk.CellRendererText()
1043 textrenderer.set_property("yalign", 0)
1044 self._dateColumn = gtk.TreeViewColumn("Date")
1045 self._dateColumn.pack_start(textrenderer, expand=True)
1046 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
1048 textrenderer = gtk.CellRendererText()
1049 textrenderer.set_property("yalign", 0)
1050 self._actionColumn = gtk.TreeViewColumn("Action")
1051 self._actionColumn.pack_start(textrenderer, expand=True)
1052 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
1054 textrenderer = gtk.CellRendererText()
1055 textrenderer.set_property("yalign", 0)
1056 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
1057 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
1058 self._numberColumn = gtk.TreeViewColumn("Number")
1059 self._numberColumn.pack_start(textrenderer, expand=True)
1060 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
1062 textrenderer = gtk.CellRendererText()
1063 textrenderer.set_property("yalign", 0)
1064 hildonize.set_cell_thumb_selectable(textrenderer)
1065 self._nameColumn = gtk.TreeViewColumn("From")
1066 self._nameColumn.pack_start(textrenderer, expand=True)
1067 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
1068 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1070 self._window = gtk_toolbox.find_parent_window(self._historyview)
1072 self._historyFilterSelector = widgetTree.get_widget("historyFilterSelector")
1073 self._historyFilterSelector.connect("clicked", self._on_history_filter_clicked)
1074 self._selectedFilter = "All"
1076 self._updateSink = gtk_toolbox.threaded_stage(
1078 self._idly_populate_historyview,
1079 gtk_toolbox.null_sink(),
1084 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1085 self._historyFilterSelector.set_label(self._selectedFilter)
1087 self._historyview.set_model(self._historymodelfiltered)
1088 self._historyview.set_fixed_height_mode(False)
1090 self._historyview.append_column(self._dateColumn)
1091 self._historyview.append_column(self._actionColumn)
1092 self._historyview.append_column(self._numberColumn)
1093 self._historyview.append_column(self._nameColumn)
1094 self._historyviewselection = self._historyview.get_selection()
1095 self._historyviewselection.set_mode(gtk.SELECTION_SINGLE)
1097 self._onRecentviewRowActivatedId = self._historyview.connect("row-activated", self._on_historyview_row_activated)
1100 self._historyview.disconnect(self._onRecentviewRowActivatedId)
1104 self._historyview.remove_column(self._dateColumn)
1105 self._historyview.remove_column(self._actionColumn)
1106 self._historyview.remove_column(self._nameColumn)
1107 self._historyview.remove_column(self._numberColumn)
1108 self._historyview.set_model(None)
1110 def add_contact(self, *args, **kwds):
1112 @note Actual dial function is patched in later
1114 raise NotImplementedError("Horrible unknown error has occurred")
1116 def update(self, force = False):
1117 if not force and self._isPopulated:
1119 self._updateSink.send(())
1123 self._isPopulated = False
1124 self._historymodel.clear()
1128 return "Recent Calls"
1130 def load_settings(self, config, sectionName):
1132 self._selectedFilter = config.get(sectionName, "filter")
1133 if self._selectedFilter not in self.HISTORY_ITEM_TYPES:
1134 self._messageType = self.HISTORY_ITEM_TYPES[0]
1135 except ConfigParser.NoOptionError:
1138 def save_settings(self, config, sectionName):
1140 @note Thread Agnostic
1142 config.set(sectionName, "filter", self._selectedFilter)
1144 def _is_history_visible(self, model, iter):
1146 action = model.get_value(iter, self.ACTION_IDX)
1148 return False # this seems weird but oh well
1150 if self._selectedFilter in [action, "All"]:
1154 except Exception, e:
1155 self._errorDisplay.push_exception()
1157 def _idly_populate_historyview(self):
1158 with gtk_toolbox.gtk_lock():
1159 banner = hildonize.show_busy_banner_start(self._window, "Loading Call History")
1161 self._historymodel.clear()
1162 self._isPopulated = True
1165 historyItems = self._backend.get_recent()
1166 except Exception, e:
1167 self._errorDisplay.push_exception_with_lock()
1168 self._isPopulated = False
1172 gv_backend.decorate_recent(data)
1173 for data in gv_backend.sort_messages(historyItems)
1176 for contactId, personName, phoneNumber, date, action in historyItems:
1178 personName = "Unknown"
1179 date = abbrev_relative_date(date)
1180 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1181 prettyNumber = make_pretty(prettyNumber)
1182 item = (prettyNumber, date, action.capitalize(), personName, contactId)
1183 with gtk_toolbox.gtk_lock():
1184 self._historymodel.append(item)
1185 except Exception, e:
1186 self._errorDisplay.push_exception_with_lock()
1188 with gtk_toolbox.gtk_lock():
1189 hildonize.show_busy_banner_end(banner)
1193 def _on_history_filter_clicked(self, *args, **kwds):
1195 selectedComboIndex = self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
1198 newSelectedComboIndex = hildonize.touch_selector(
1201 self.HISTORY_ITEM_TYPES,
1204 except RuntimeError:
1207 option = self.HISTORY_ITEM_TYPES[newSelectedComboIndex]
1208 self._selectedFilter = option
1209 self._historyFilterSelector.set_label(self._selectedFilter)
1210 self._historymodelfiltered.refilter()
1211 except Exception, e:
1212 self._errorDisplay.push_exception()
1214 def _history_summary(self, expectedNumber):
1215 for number, action, date, whoFrom, whoFromId in self._historymodel:
1216 if expectedNumber is not None and expectedNumber == number:
1217 yield "%s <i>(%s)</i> - %s %s" % (number, whoFrom, date, action)
1219 def _on_historyview_row_activated(self, treeview, path, view_column):
1221 childPath = self._historymodelfiltered.convert_path_to_child_path(path)
1222 itr = self._historymodel.get_iter(childPath)
1226 prettyNumber = self._historymodel.get_value(itr, self.NUMBER_IDX)
1227 number = make_ugly(prettyNumber)
1228 description = list(self._history_summary(prettyNumber))
1229 contactName = self._historymodel.get_value(itr, self.FROM_IDX)
1230 contactId = self._historymodel.get_value(itr, self.FROM_ID_IDX)
1231 contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1235 contactPhoneNumbers,
1236 messages = description,
1237 defaultIndex = defaultIndex,
1239 self._historyviewselection.unselect_all()
1240 except Exception, e:
1241 self._errorDisplay.push_exception()
1244 class MessagesView(object):
1252 MESSAGE_DATA_IDX = 6
1254 NO_MESSAGES = "None"
1255 VOICEMAIL_MESSAGES = "Voicemail"
1256 TEXT_MESSAGES = "SMS"
1257 ALL_TYPES = "All Messages"
1258 MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
1260 UNREAD_STATUS = "Unread"
1261 UNARCHIVED_STATUS = "Inbox"
1263 MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
1265 def __init__(self, widgetTree, backend, errorDisplay):
1266 self._errorDisplay = errorDisplay
1267 self._backend = backend
1269 self._isPopulated = False
1270 self._messagemodel = gtk.ListStore(
1271 gobject.TYPE_STRING, # number
1272 gobject.TYPE_STRING, # date
1273 gobject.TYPE_STRING, # header
1274 gobject.TYPE_STRING, # message
1276 gobject.TYPE_STRING, # from id
1277 object, # message data
1279 self._messagemodelfiltered = self._messagemodel.filter_new()
1280 self._messagemodelfiltered.set_visible_func(self._is_message_visible)
1281 self._messageview = widgetTree.get_widget("messages_view")
1282 self._messageviewselection = None
1283 self._onMessageviewRowActivatedId = 0
1285 self._messageRenderer = gtk.CellRendererText()
1286 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1287 self._messageRenderer.set_property("wrap-width", 500)
1288 self._messageColumn = gtk.TreeViewColumn("Messages")
1289 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1290 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1291 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1293 self._window = gtk_toolbox.find_parent_window(self._messageview)
1295 self._messageTypeButton = widgetTree.get_widget("messageTypeButton")
1296 self._onMessageTypeClickedId = 0
1297 self._messageType = self.ALL_TYPES
1298 self._messageStatusButton = widgetTree.get_widget("messageStatusButton")
1299 self._onMessageStatusClickedId = 0
1300 self._messageStatus = self.ALL_STATUS
1302 self._updateSink = gtk_toolbox.threaded_stage(
1304 self._idly_populate_messageview,
1305 gtk_toolbox.null_sink(),
1310 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1311 self._messageview.set_model(self._messagemodelfiltered)
1312 self._messageview.set_headers_visible(False)
1313 self._messageview.set_fixed_height_mode(False)
1315 self._messageview.append_column(self._messageColumn)
1316 self._messageviewselection = self._messageview.get_selection()
1317 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1319 self._messageTypeButton.set_label(self._messageType)
1320 self._messageStatusButton.set_label(self._messageStatus)
1322 self._onMessageviewRowActivatedId = self._messageview.connect(
1323 "row-activated", self._on_messageview_row_activated
1325 self._onMessageTypeClickedId = self._messageTypeButton.connect(
1326 "clicked", self._on_message_type_clicked
1328 self._onMessageStatusClickedId = self._messageStatusButton.connect(
1329 "clicked", self._on_message_status_clicked
1333 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1334 self._messageTypeButton.disconnect(self._onMessageTypeClickedId)
1335 self._messageStatusButton.disconnect(self._onMessageStatusClickedId)
1339 self._messageview.remove_column(self._messageColumn)
1340 self._messageview.set_model(None)
1342 def add_contact(self, *args, **kwds):
1344 @note Actual dial function is patched in later
1346 raise NotImplementedError("Horrible unknown error has occurred")
1348 def update(self, force = False):
1349 if not force and self._isPopulated:
1351 self._updateSink.send(())
1355 self._isPopulated = False
1356 self._messagemodel.clear()
1362 def load_settings(self, config, sectionName):
1364 self._messageType = config.get(sectionName, "type")
1365 if self._messageType not in self.MESSAGE_TYPES:
1366 self._messageType = self.ALL_TYPES
1367 self._messageStatus = config.get(sectionName, "status")
1368 if self._messageStatus not in self.MESSAGE_STATUSES:
1369 self._messageStatus = self.ALL_STATUS
1370 except ConfigParser.NoOptionError:
1373 def save_settings(self, config, sectionName):
1375 @note Thread Agnostic
1377 config.set(sectionName, "status", self._messageStatus)
1378 config.set(sectionName, "type", self._messageType)
1380 def _is_message_visible(self, model, iter):
1382 message = model.get_value(iter, self.MESSAGE_DATA_IDX)
1384 return False # this seems weird but oh well
1385 return self._filter_messages(message, self._messageType, self._messageStatus)
1386 except Exception, e:
1387 self._errorDisplay.push_exception()
1390 def _filter_messages(cls, message, type, status):
1391 if type == cls.ALL_TYPES:
1394 messageType = message["type"]
1395 isType = messageType == type
1397 if status == cls.ALL_STATUS:
1400 isUnarchived = not message["isArchived"]
1401 isUnread = not message["isRead"]
1402 if status == cls.UNREAD_STATUS:
1403 isStatus = isUnarchived and isUnread
1404 elif status == cls.UNARCHIVED_STATUS:
1405 isStatus = isUnarchived
1407 assert "Status %s is bad for %r" % (status, message)
1409 return isType and isStatus
1411 _MIN_MESSAGES_SHOWN = 4
1413 def _idly_populate_messageview(self):
1414 with gtk_toolbox.gtk_lock():
1415 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1417 self._messagemodel.clear()
1418 self._isPopulated = True
1420 if self._messageType == self.NO_MESSAGES:
1424 messageItems = self._backend.get_messages()
1425 except Exception, e:
1426 self._errorDisplay.push_exception_with_lock()
1427 self._isPopulated = False
1431 (gv_backend.decorate_message(message), message)
1432 for message in gv_backend.sort_messages(messageItems)
1435 for (contactId, header, number, relativeDate, messages), messageData in messageItems:
1436 prettyNumber = number[2:] if number.startswith("+1") else number
1437 prettyNumber = make_pretty(prettyNumber)
1439 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1440 expandedMessages = [firstMessage]
1441 expandedMessages.extend(messages)
1442 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1443 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1444 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1445 collapsedMessages = [firstMessage, secondMessage]
1446 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1448 collapsedMessages = expandedMessages
1449 #collapsedMessages = _collapse_message(collapsedMessages, 60, self._MIN_MESSAGES_SHOWN)
1451 number = make_ugly(number)
1453 row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId, messageData
1454 with gtk_toolbox.gtk_lock():
1455 self._messagemodel.append(row)
1456 except Exception, e:
1457 self._errorDisplay.push_exception_with_lock()
1459 with gtk_toolbox.gtk_lock():
1460 hildonize.show_busy_banner_end(banner)
1461 self._messagemodelfiltered.refilter()
1465 def _on_messageview_row_activated(self, treeview, path, view_column):
1467 childPath = self._messagemodelfiltered.convert_path_to_child_path(path)
1468 itr = self._messagemodel.get_iter(childPath)
1472 number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX))
1473 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1475 contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX)
1476 header = self._messagemodel.get_value(itr, self.HEADER_IDX)
1477 contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1481 contactPhoneNumbers,
1482 messages = description,
1483 defaultIndex = defaultIndex,
1485 self._messageviewselection.unselect_all()
1486 except Exception, e:
1487 self._errorDisplay.push_exception()
1489 def _on_message_type_clicked(self, *args, **kwds):
1491 selectedIndex = self.MESSAGE_TYPES.index(self._messageType)
1494 newSelectedIndex = hildonize.touch_selector(
1500 except RuntimeError:
1503 if selectedIndex != newSelectedIndex:
1504 self._messageType = self.MESSAGE_TYPES[newSelectedIndex]
1505 self._messageTypeButton.set_label(self._messageType)
1506 self._messagemodelfiltered.refilter()
1507 except Exception, e:
1508 self._errorDisplay.push_exception()
1510 def _on_message_status_clicked(self, *args, **kwds):
1512 selectedIndex = self.MESSAGE_STATUSES.index(self._messageStatus)
1515 newSelectedIndex = hildonize.touch_selector(
1518 self.MESSAGE_STATUSES,
1521 except RuntimeError:
1524 if selectedIndex != newSelectedIndex:
1525 self._messageStatus = self.MESSAGE_STATUSES[newSelectedIndex]
1526 self._messageStatusButton.set_label(self._messageStatus)
1527 self._messagemodelfiltered.refilter()
1528 except Exception, e:
1529 self._errorDisplay.push_exception()
1532 class ContactsView(object):
1534 CONTACT_TYPE_IDX = 0
1535 CONTACT_NAME_IDX = 1
1538 def __init__(self, widgetTree, backend, errorDisplay):
1539 self._errorDisplay = errorDisplay
1540 self._backend = backend
1542 self._addressBook = None
1543 self._selectedComboIndex = 0
1544 self._addressBookFactories = [null_backend.NullAddressBook()]
1546 self._booksList = []
1547 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1549 self._isPopulated = False
1550 self._contactsmodel = gtk.ListStore(
1551 gobject.TYPE_STRING, # Contact Type
1552 gobject.TYPE_STRING, # Contact Name
1553 gobject.TYPE_STRING, # Contact ID
1555 self._contactsviewselection = None
1556 self._contactsview = widgetTree.get_widget("contactsview")
1558 self._contactColumn = gtk.TreeViewColumn("Contact")
1559 displayContactSource = False
1560 if displayContactSource:
1561 textrenderer = gtk.CellRendererText()
1562 self._contactColumn.pack_start(textrenderer, expand=False)
1563 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_TYPE_IDX)
1564 textrenderer = gtk.CellRendererText()
1565 hildonize.set_cell_thumb_selectable(textrenderer)
1566 self._contactColumn.pack_start(textrenderer, expand=True)
1567 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_NAME_IDX)
1568 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1569 self._contactColumn.set_sort_column_id(1)
1570 self._contactColumn.set_visible(True)
1572 self._onContactsviewRowActivatedId = 0
1573 self._onAddressbookButtonChangedId = 0
1574 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1576 self._updateSink = gtk_toolbox.threaded_stage(
1578 self._idly_populate_contactsview,
1579 gtk_toolbox.null_sink(),
1584 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1586 self._contactsview.set_model(self._contactsmodel)
1587 self._contactsview.set_fixed_height_mode(False)
1588 self._contactsview.append_column(self._contactColumn)
1589 self._contactsviewselection = self._contactsview.get_selection()
1590 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1592 del self._booksList[:]
1593 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1594 if factoryName and bookName:
1595 entryName = "%s: %s" % (factoryName, bookName)
1597 entryName = factoryName
1599 entryName = bookName
1601 entryName = "Bad name (%d)" % factoryId
1602 row = (str(factoryId), bookId, entryName)
1603 self._booksList.append(row)
1605 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1606 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1608 if len(self._booksList) <= self._selectedComboIndex:
1609 self._selectedComboIndex = 0
1610 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1612 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1613 selectedBookId = self._booksList[self._selectedComboIndex][1]
1614 self.open_addressbook(selectedFactoryId, selectedBookId)
1617 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1618 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1622 self._bookSelectionButton.set_label("")
1623 self._contactsview.set_model(None)
1624 self._contactsview.remove_column(self._contactColumn)
1626 def add_contact(self, *args, **kwds):
1628 @note Actual dial function is patched in later
1630 raise NotImplementedError("Horrible unknown error has occurred")
1632 def get_addressbooks(self):
1634 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1636 for i, factory in enumerate(self._addressBookFactories):
1637 for bookFactory, bookId, bookName in factory.get_addressbooks():
1638 yield (str(i), bookId), (factory.factory_name(), bookName)
1640 def open_addressbook(self, bookFactoryId, bookId):
1641 bookFactoryIndex = int(bookFactoryId)
1642 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1643 self._addressBook = addressBook
1645 def update(self, force = False):
1646 if not force and self._isPopulated:
1648 self._updateSink.send(())
1652 self._isPopulated = False
1653 self._contactsmodel.clear()
1654 for factory in self._addressBookFactories:
1655 factory.clear_caches()
1656 self._addressBook.clear_caches()
1658 def append(self, book):
1659 self._addressBookFactories.append(book)
1661 def extend(self, books):
1662 self._addressBookFactories.extend(books)
1668 def load_settings(self, config, sectionName):
1670 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1671 except ConfigParser.NoOptionError:
1672 self._selectedComboIndex = 0
1674 def save_settings(self, config, sectionName):
1675 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1677 def _idly_populate_contactsview(self):
1678 with gtk_toolbox.gtk_lock():
1679 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1682 while addressBook is not self._addressBook:
1683 addressBook = self._addressBook
1684 with gtk_toolbox.gtk_lock():
1685 self._contactsview.set_model(None)
1689 contacts = addressBook.get_contacts()
1690 except Exception, e:
1692 self._isPopulated = False
1693 self._errorDisplay.push_exception_with_lock()
1694 for contactId, contactName in contacts:
1695 contactType = addressBook.contact_source_short_name(contactId)
1696 row = contactType, contactName, contactId
1697 self._contactsmodel.append(row)
1699 with gtk_toolbox.gtk_lock():
1700 self._contactsview.set_model(self._contactsmodel)
1702 self._isPopulated = True
1703 except Exception, e:
1704 self._errorDisplay.push_exception_with_lock()
1706 with gtk_toolbox.gtk_lock():
1707 hildonize.show_busy_banner_end(banner)
1710 def _on_addressbook_button_changed(self, *args, **kwds):
1713 newSelectedComboIndex = hildonize.touch_selector(
1716 (("%s" % m[2]) for m in self._booksList),
1717 self._selectedComboIndex,
1719 except RuntimeError:
1722 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1723 selectedBookId = self._booksList[newSelectedComboIndex][1]
1725 oldAddressbook = self._addressBook
1726 self.open_addressbook(selectedFactoryId, selectedBookId)
1727 forceUpdate = True if oldAddressbook is not self._addressBook else False
1728 self.update(force=forceUpdate)
1730 self._selectedComboIndex = newSelectedComboIndex
1731 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1732 except Exception, e:
1733 self._errorDisplay.push_exception()
1735 def _on_contactsview_row_activated(self, treeview, path, view_column):
1737 itr = self._contactsmodel.get_iter(path)
1741 contactId = self._contactsmodel.get_value(itr, self.CONTACT_ID_IDX)
1742 contactName = self._contactsmodel.get_value(itr, self.CONTACT_NAME_IDX)
1744 contactDetails = self._addressBook.get_contact_details(contactId)
1745 except Exception, e:
1747 self._errorDisplay.push_exception()
1748 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1750 if len(contactPhoneNumbers) == 0:
1755 contactPhoneNumbers,
1756 messages = (contactName, ),
1758 self._contactsviewselection.unselect_all()
1759 except Exception, e:
1760 self._errorDisplay.push_exception()