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 class SmsEntryWindow(object):
216 def __init__(self, widgetTree, parent):
217 self._clipboard = gtk.clipboard_get()
218 self._widgetTree = widgetTree
219 self._window = self._widgetTree.get_widget("smsWindow")
220 self._window.connect("delete-event", self._on_delete)
221 self._window.connect("key-press-event", self._on_key_press)
222 self._window.connect("window-state-event", self._on_window_state_change)
223 self._isFullScreen = False
224 self._parent = parent
226 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
227 self._smsButton.connect("clicked", self._on_send)
228 self._dialButton = self._widgetTree.get_widget("dialButton")
229 self._dialButton.connect("clicked", self._on_dial)
231 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
233 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
234 self._messagesView = self._widgetTree.get_widget("smsMessages")
236 textrenderer = gtk.CellRendererText()
237 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
238 textrenderer.set_property("wrap-width", 450)
239 messageColumn = gtk.TreeViewColumn("")
240 messageColumn.pack_start(textrenderer, expand=True)
241 messageColumn.add_attribute(textrenderer, "markup", 0)
242 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
243 self._messagesView.append_column(messageColumn)
244 self._messagesView.set_headers_visible(False)
245 self._messagesView.set_model(self._messagemodel)
246 self._messagesView.set_fixed_height_mode(False)
248 self._conversationView = self._messagesView.get_parent()
249 self._conversationViewPort = self._conversationView.get_parent()
250 self._scrollWindow = self._conversationViewPort.get_parent()
252 self._targetList = self._widgetTree.get_widget("smsTargetList")
253 self._phoneButton = self._widgetTree.get_widget("phoneTypeSelection")
254 self._phoneButton.connect("clicked", self._on_phone)
255 self._smsEntry = self._widgetTree.get_widget("smsEntry")
256 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
257 self._smsEntrySize = None
261 def add_contact(self, contactDetails, messages = (), defaultIndex = -1):
262 contactNumbers = list(self._to_contact_numbers(contactDetails))
263 assert contactNumbers
264 contactIndex = defaultIndex if defaultIndex != -1 else 0
265 contact = contactNumbers, contactIndex, messages
266 self._contacts.append(contact)
268 selector = gtk.Button(contactNumbers[0][1])
269 removeContact = gtk.Button(stock="gtk-delete")
271 row.pack_start(selector, True, True)
272 row.pack_start(removeContact, False, False)
274 self._targetList.pack_start(row)
275 selector.connect("clicked", self._on_choose_phone_n, row)
276 removeContact.connect("clicked", self._on_remove_phone_n, row)
277 self._update_button_state()
278 self._update_context()
280 parentSize = self._parent.get_size()
281 self._window.resize(parentSize[0], max(parentSize[1]-10, 100))
283 self._window.present()
285 self._smsEntry.grab_focus()
286 dx = self._conversationView.get_allocation().height - self._conversationViewPort.get_allocation().height
288 adjustment = self._scrollWindow.get_vadjustment()
289 adjustment.value = dx
292 del self._contacts[:]
294 for contactNumberSelector in list(self._targetList.get_children()):
295 self._targetList.remove(contactNumberSelector)
296 self._smsEntry.get_buffer().set_text("")
297 self._update_letter_count()
298 self._update_context()
300 def fullscreen(self):
301 self._window.fullscreen()
303 def unfullscreen(self):
304 self._window.unfullscreen()
306 def _remove_contact(self, contactIndex):
307 del self._contacts[contactIndex]
309 contactNumberSelector = list(self._targetList.get_children())[contactIndex]
310 self._targetList.remove(contactNumberSelector)
311 self._update_button_state()
312 self._update_context()
314 def _update_letter_count(self):
315 if self._smsEntrySize is None:
316 self._smsEntrySize = self._smsEntry.size_request()
318 self._smsEntry.set_size_request(*self._smsEntrySize)
319 entryLength = self._smsEntry.get_buffer().get_char_count()
321 numTexts, numCharInText = divmod(entryLength, self.MAX_CHAR)
323 self._letterCountLabel.set_text("%s.%s" % (numTexts, numCharInText))
325 self._letterCountLabel.set_text("%s" % (numCharInText, ))
327 self._update_button_state()
329 def _update_context(self):
330 self._messagemodel.clear()
331 if len(self._contacts) == 0:
332 self._messagesView.hide()
333 self._targetList.hide()
334 self._phoneButton.hide()
335 self._phoneButton.set_label("Error: You shouldn't see this")
336 elif len(self._contacts) == 1:
337 contactNumbers, index, messages = self._contacts[0]
339 self._messagesView.show()
340 for message in messages:
342 self._messagemodel.append(row)
343 messagesSelection = self._messagesView.get_selection()
344 messagesSelection.select_path((len(messages)-1, ))
346 self._messagesView.hide()
347 self._targetList.hide()
348 self._phoneButton.show()
349 self._phoneButton.set_label(contactNumbers[index][1])
350 if 1 < len(contactNumbers):
351 self._phoneButton.set_sensitive(True)
353 self._phoneButton.set_sensitive(False)
355 self._messagesView.hide()
356 self._targetList.show()
357 self._phoneButton.hide()
358 self._phoneButton.set_label("Error: You shouldn't see this")
360 def _update_button_state(self):
361 if len(self._contacts) == 0:
362 self._dialButton.set_sensitive(False)
363 self._smsButton.set_sensitive(False)
364 elif len(self._contacts) == 1:
365 entryLength = self._smsEntry.get_buffer().get_char_count()
367 self._dialButton.set_sensitive(True)
368 self._smsButton.set_sensitive(False)
370 self._dialButton.set_sensitive(False)
371 self._smsButton.set_sensitive(True)
373 self._dialButton.set_sensitive(False)
374 self._smsButton.set_sensitive(True)
376 def _to_contact_numbers(self, contactDetails):
377 for phoneType, phoneNumber in contactDetails:
378 display = " - ".join((make_pretty(phoneNumber), phoneType))
379 yield (phoneNumber, display)
385 def _request_number(self, contactIndex):
386 contactNumbers, index, messages = self._contacts[contactIndex]
387 assert 0 <= index, "%r" % index
389 index = hildonize.touch_selector(
392 (description for (number, description) in contactNumbers),
395 self._contacts[contactIndex] = contactNumbers, index, messages
397 def send_sms(self, numbers, message):
398 raise NotImplementedError()
400 def dial(self, number):
401 raise NotImplementedError()
403 def _on_phone(self, *args):
405 assert len(self._contacts) == 1
406 self._request_number(0)
408 contactNumbers, numberIndex, messages = self._contacts[0]
409 self._phoneButton.set_label(contactNumbers[numberIndex][1])
410 row = list(self._targetList.get_children())[0]
411 phoneButton = list(row.get_children())[0]
412 phoneButton.set_label(contactNumbers[numberIndex][1])
414 _moduleLogger.exception("%s" % str(e))
416 def _on_choose_phone_n(self, button, row):
418 assert 1 < len(self._contacts)
419 targetList = list(self._targetList.get_children())
420 index = targetList.index(row)
421 self._request_number(index)
423 contactNumbers, numberIndex, messages = self._contacts[0]
424 phoneButton = list(row.get_children())[0]
425 phoneButton.set_label(contactNumbers[numberIndex][1])
427 _moduleLogger.exception("%s" % str(e))
429 def _on_remove_phone_n(self, button, row):
431 assert 1 < len(self._contacts)
432 targetList = list(self._targetList.get_children())
433 index = targetList.index(row)
435 del self._contacts[index]
436 self._targetList.remove(row)
437 self._update_context()
438 self._update_button_state()
440 _moduleLogger.exception("%s" % str(e))
442 def _on_entry_changed(self, *args):
443 self._update_letter_count()
445 def _on_send(self, *args):
446 assert 0 < len(self._contacts), "%r" % self._contacts
448 make_ugly(contact[0][contact[1]][0])
449 for contact in self._contacts
452 entryBuffer = self._smsEntry.get_buffer()
453 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
454 enteredMessage = enteredMessage.strip()
455 assert enteredMessage
456 self.send_sms(phoneNumbers, enteredMessage)
459 def _on_dial(self, *args):
460 assert len(self._contacts) == 1, "%r" % self._contacts
461 contact = self._contacts[0]
462 contactNumber = contact[0][contact[1]][0]
463 phoneNumber = make_ugly(contactNumber)
464 self.dial(phoneNumber)
467 def _on_delete(self, *args):
468 self._window.emit_stop_by_name("delete-event")
472 def _on_window_state_change(self, widget, event, *args):
474 @note Hildon specific
477 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
478 self._isFullScreen = True
480 self._isFullScreen = False
482 self._errorDisplay.push_exception()
484 def _on_key_press(self, widget, event):
485 RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
488 event.keyval == gtk.keysyms.F6 or
489 event.keyval in RETURN_TYPES and event.get_state() & gtk.gdk.CONTROL_MASK
491 if self._isFullScreen:
492 self._window.unfullscreen()
494 self._window.fullscreen()
495 elif event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
498 for messagePart in self._messagemodel
500 self._clipboard.set_text(str(message))
502 event.keyval == gtk.keysyms.w and
503 event.get_state() & gtk.gdk.CONTROL_MASK
507 event.keyval == gtk.keysyms.q and
508 event.get_state() & gtk.gdk.CONTROL_MASK
510 self._parent.destroy()
512 _moduleLogger.exception(str(e))
515 class Dialpad(object):
517 def __init__(self, widgetTree, errorDisplay):
518 self._clipboard = gtk.clipboard_get()
519 self._errorDisplay = errorDisplay
521 self._numberdisplay = widgetTree.get_widget("numberdisplay")
522 self._okButton = widgetTree.get_widget("dialpadOk")
523 self._backButton = widgetTree.get_widget("back")
524 self._plusButton = widgetTree.get_widget("plus")
525 self._phonenumber = ""
526 self._prettynumber = ""
529 "on_digit_clicked": self._on_digit_clicked,
531 widgetTree.signal_autoconnect(callbackMapping)
532 self._okButton.connect("clicked", self._on_ok_clicked)
533 self._plusButton.connect("clicked", self._on_plus)
535 self._originalLabel = self._backButton.get_label()
536 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
537 self._backTapHandler.on_tap = self._on_backspace
538 self._backTapHandler.on_hold = self._on_clearall
539 self._backTapHandler.on_holding = self._set_clear_button
540 self._backTapHandler.on_cancel = self._reset_back_button
542 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
543 self._keyPressEventId = 0
546 self._okButton.grab_focus()
547 self._backTapHandler.enable()
548 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
551 self._window.disconnect(self._keyPressEventId)
552 self._keyPressEventId = 0
553 self._reset_back_button()
554 self._backTapHandler.disable()
556 def add_contact(self, *args, **kwds):
558 @note Actual dial function is patched in later
560 raise NotImplementedError("Horrible unknown error has occurred")
562 def get_number(self):
563 return self._phonenumber
565 def set_number(self, number):
567 Set the number to dial
570 self._phonenumber = make_ugly(number)
571 self._prettynumber = make_pretty(self._phonenumber)
572 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
574 self._errorDisplay.push_exception()
583 def load_settings(self, config, section):
586 def save_settings(self, config, section):
588 @note Thread Agnostic
592 def _on_key_press(self, widget, event):
594 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
595 contents = self._clipboard.wait_for_text()
596 if contents is not None:
597 self.set_number(contents)
599 self._errorDisplay.push_exception()
601 def _on_ok_clicked(self, widget):
603 phoneNumber = self.get_number()
605 [("Dialer", phoneNumber)], ()
609 self._errorDisplay.push_exception()
611 def _on_digit_clicked(self, widget):
613 self.set_number(self._phonenumber + widget.get_name()[-1])
615 self._errorDisplay.push_exception()
617 def _on_plus(self, *args):
619 self.set_number(self._phonenumber + "+")
621 self._errorDisplay.push_exception()
623 def _on_backspace(self, taps):
625 self.set_number(self._phonenumber[:-taps])
626 self._reset_back_button()
628 self._errorDisplay.push_exception()
630 def _on_clearall(self, taps):
633 self._reset_back_button()
635 self._errorDisplay.push_exception()
638 def _set_clear_button(self):
640 self._backButton.set_label("gtk-clear")
642 self._errorDisplay.push_exception()
644 def _reset_back_button(self):
646 self._backButton.set_label(self._originalLabel)
648 self._errorDisplay.push_exception()
651 class AccountInfo(object):
653 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
654 self._errorDisplay = errorDisplay
655 self._backend = backend
656 self._isPopulated = False
657 self._alarmHandler = alarmHandler
658 self._notifyOnMissed = False
659 self._notifyOnVoicemail = False
660 self._notifyOnSms = False
662 self._callbackList = []
663 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
664 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
665 self._onCallbackSelectChangedId = 0
667 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
668 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
669 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
670 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
671 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
672 self._onNotifyToggled = 0
673 self._onMinutesChanged = 0
674 self._onMissedToggled = 0
675 self._onVoicemailToggled = 0
676 self._onSmsToggled = 0
677 self._applyAlarmTimeoutId = None
679 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
680 self._callbackNumber = ""
683 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
685 self._accountViewNumberDisplay.set_use_markup(True)
686 self.set_account_number("")
688 del self._callbackList[:]
689 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
690 self._set_callback_label("")
692 if self._alarmHandler is not None:
693 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
694 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
695 self._missedCheckbox.set_active(self._notifyOnMissed)
696 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
697 self._smsCheckbox.set_active(self._notifyOnSms)
699 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
700 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
701 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
702 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
703 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
705 self._notifyCheckbox.set_sensitive(False)
706 self._minutesEntryButton.set_sensitive(False)
707 self._missedCheckbox.set_sensitive(False)
708 self._voicemailCheckbox.set_sensitive(False)
709 self._smsCheckbox.set_sensitive(False)
711 self.update(force=True)
714 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
715 self._onCallbackSelectChangedId = 0
716 self._set_callback_label("")
718 if self._alarmHandler is not None:
719 self._notifyCheckbox.disconnect(self._onNotifyToggled)
720 self._minutesEntryButton.disconnect(self._onMinutesChanged)
721 self._missedCheckbox.disconnect(self._onNotifyToggled)
722 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
723 self._smsCheckbox.disconnect(self._onNotifyToggled)
724 self._onNotifyToggled = 0
725 self._onMinutesChanged = 0
726 self._onMissedToggled = 0
727 self._onVoicemailToggled = 0
728 self._onSmsToggled = 0
730 self._notifyCheckbox.set_sensitive(True)
731 self._minutesEntryButton.set_sensitive(True)
732 self._missedCheckbox.set_sensitive(True)
733 self._voicemailCheckbox.set_sensitive(True)
734 self._smsCheckbox.set_sensitive(True)
737 del self._callbackList[:]
739 def set_account_number(self, number):
741 Displays current account number
743 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
745 def update(self, force = False):
746 if not force and self._isPopulated:
748 self._populate_callback_combo()
749 self.set_account_number(self._backend.get_account_number())
753 self._set_callback_label("")
754 self.set_account_number("")
755 self._isPopulated = False
757 def save_everything(self):
758 raise NotImplementedError
762 return "Account Info"
764 def load_settings(self, config, section):
765 self._callbackNumber = make_ugly(config.get(section, "callback"))
766 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
767 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
768 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
770 def save_settings(self, config, section):
772 @note Thread Agnostic
774 config.set(section, "callback", self._callbackNumber)
775 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
776 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
777 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
779 def _populate_callback_combo(self):
780 self._isPopulated = True
781 del self._callbackList[:]
783 callbackNumbers = self._backend.get_callback_numbers()
785 self._errorDisplay.push_exception()
786 self._isPopulated = False
789 if len(callbackNumbers) == 0:
790 callbackNumbers = {"": "No callback numbers available"}
792 for number, description in callbackNumbers.iteritems():
793 self._callbackList.append((make_pretty(number), description))
795 self._set_callback_number(self._callbackNumber)
797 def _set_callback_number(self, number):
799 if not self._backend.is_valid_syntax(number) and 0 < len(number):
800 self._errorDisplay.push_message("%s is not a valid callback number" % number)
801 elif number == self._backend.get_callback_number() and 0 < len(number):
802 _moduleLogger.warning(
803 "Callback number already is %s" % (
804 self._backend.get_callback_number(),
807 self._set_callback_label(number)
809 if number.startswith("1747"): number = "+" + number
810 self._backend.set_callback_number(number)
811 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
812 make_pretty(number), make_pretty(self._backend.get_callback_number())
814 self._callbackNumber = make_ugly(number)
815 self._set_callback_label(number)
817 "Callback number set to %s" % (
818 self._backend.get_callback_number(),
822 self._errorDisplay.push_exception()
824 def _set_callback_label(self, uglyNumber):
825 prettyNumber = make_pretty(uglyNumber)
826 if len(prettyNumber) == 0:
827 prettyNumber = "No Callback Number"
828 self._callbackSelectButton.set_label(prettyNumber)
830 def _update_alarm_settings(self, recurrence):
832 isEnabled = self._notifyCheckbox.get_active()
833 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
834 self._alarmHandler.apply_settings(isEnabled, recurrence)
836 self.save_everything()
837 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
838 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
840 def _on_callbackentry_clicked(self, *args):
842 actualSelection = make_pretty(self._callbackNumber)
845 (number, "%s (%s)" % (number, description))
846 for (number, description) in self._callbackList
848 defaultSelection = userOptions.get(actualSelection, actualSelection)
850 userSelection = hildonize.touch_selector_entry(
853 list(userOptions.itervalues()),
856 reversedUserOptions = dict(
857 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
859 selectedNumber = reversedUserOptions.get(userSelection, userSelection)
861 number = make_ugly(selectedNumber)
862 self._set_callback_number(number)
863 except RuntimeError, e:
864 _moduleLogger.exception("%s" % str(e))
866 self._errorDisplay.push_exception()
868 def _on_notify_toggled(self, *args):
870 if self._applyAlarmTimeoutId is not None:
871 gobject.source_remove(self._applyAlarmTimeoutId)
872 self._applyAlarmTimeoutId = None
873 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
875 self._errorDisplay.push_exception()
877 def _on_minutes_clicked(self, *args):
878 recurrenceChoices = [
894 actualSelection = self._alarmHandler.recurrence
896 closestSelectionIndex = 0
897 for i, possible in enumerate(recurrenceChoices):
898 if possible[0] <= actualSelection:
899 closestSelectionIndex = i
900 recurrenceIndex = hildonize.touch_selector(
903 (("%s" % m[1]) for m in recurrenceChoices),
904 closestSelectionIndex,
906 recurrence = recurrenceChoices[recurrenceIndex][0]
908 self._update_alarm_settings(recurrence)
909 except RuntimeError, e:
910 _moduleLogger.exception("%s" % str(e))
912 self._errorDisplay.push_exception()
914 def _on_apply_timeout(self, *args):
916 self._applyAlarmTimeoutId = None
918 self._update_alarm_settings(self._alarmHandler.recurrence)
920 self._errorDisplay.push_exception()
923 def _on_missed_toggled(self, *args):
925 self._notifyOnMissed = self._missedCheckbox.get_active()
926 self.save_everything()
928 self._errorDisplay.push_exception()
930 def _on_voicemail_toggled(self, *args):
932 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
933 self.save_everything()
935 self._errorDisplay.push_exception()
937 def _on_sms_toggled(self, *args):
939 self._notifyOnSms = self._smsCheckbox.get_active()
940 self.save_everything()
942 self._errorDisplay.push_exception()
945 class CallHistoryView(object):
953 HISTORY_ITEM_TYPES = ["All", "Received", "Missed", "Placed"]
955 def __init__(self, widgetTree, backend, errorDisplay):
956 self._errorDisplay = errorDisplay
957 self._backend = backend
959 self._isPopulated = False
960 self._historymodel = gtk.ListStore(
961 gobject.TYPE_STRING, # number
962 gobject.TYPE_STRING, # date
963 gobject.TYPE_STRING, # action
964 gobject.TYPE_STRING, # from
965 gobject.TYPE_STRING, # from id
967 self._historymodelfiltered = self._historymodel.filter_new()
968 self._historymodelfiltered.set_visible_func(self._is_history_visible)
969 self._historyview = widgetTree.get_widget("historyview")
970 self._historyviewselection = None
971 self._onRecentviewRowActivatedId = 0
973 textrenderer = gtk.CellRendererText()
974 textrenderer.set_property("yalign", 0)
975 self._dateColumn = gtk.TreeViewColumn("Date")
976 self._dateColumn.pack_start(textrenderer, expand=True)
977 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
979 textrenderer = gtk.CellRendererText()
980 textrenderer.set_property("yalign", 0)
981 self._actionColumn = gtk.TreeViewColumn("Action")
982 self._actionColumn.pack_start(textrenderer, expand=True)
983 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
985 textrenderer = gtk.CellRendererText()
986 textrenderer.set_property("yalign", 0)
987 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
988 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
989 self._numberColumn = gtk.TreeViewColumn("Number")
990 self._numberColumn.pack_start(textrenderer, expand=True)
991 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
993 textrenderer = gtk.CellRendererText()
994 textrenderer.set_property("yalign", 0)
995 hildonize.set_cell_thumb_selectable(textrenderer)
996 self._nameColumn = gtk.TreeViewColumn("From")
997 self._nameColumn.pack_start(textrenderer, expand=True)
998 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
999 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1001 self._window = gtk_toolbox.find_parent_window(self._historyview)
1003 self._historyFilterSelector = widgetTree.get_widget("historyFilterSelector")
1004 self._historyFilterSelector.connect("clicked", self._on_history_filter_clicked)
1005 self._selectedFilter = "All"
1007 self._updateSink = gtk_toolbox.threaded_stage(
1009 self._idly_populate_historyview,
1010 gtk_toolbox.null_sink(),
1015 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1016 self._historyFilterSelector.set_label(self._selectedFilter)
1018 self._historyview.set_model(self._historymodelfiltered)
1019 self._historyview.set_fixed_height_mode(False)
1021 self._historyview.append_column(self._dateColumn)
1022 self._historyview.append_column(self._actionColumn)
1023 self._historyview.append_column(self._numberColumn)
1024 self._historyview.append_column(self._nameColumn)
1025 self._historyviewselection = self._historyview.get_selection()
1026 self._historyviewselection.set_mode(gtk.SELECTION_SINGLE)
1028 self._onRecentviewRowActivatedId = self._historyview.connect("row-activated", self._on_historyview_row_activated)
1031 self._historyview.disconnect(self._onRecentviewRowActivatedId)
1035 self._historyview.remove_column(self._dateColumn)
1036 self._historyview.remove_column(self._actionColumn)
1037 self._historyview.remove_column(self._nameColumn)
1038 self._historyview.remove_column(self._numberColumn)
1039 self._historyview.set_model(None)
1041 def add_contact(self, *args, **kwds):
1043 @note Actual dial function is patched in later
1045 raise NotImplementedError("Horrible unknown error has occurred")
1047 def update(self, force = False):
1048 if not force and self._isPopulated:
1050 self._updateSink.send(())
1054 self._isPopulated = False
1055 self._historymodel.clear()
1059 return "Recent Calls"
1061 def load_settings(self, config, sectionName):
1063 self._selectedFilter = config.get(sectionName, "filter")
1064 if self._selectedFilter not in self.HISTORY_ITEM_TYPES:
1065 self._messageType = self.HISTORY_ITEM_TYPES[0]
1066 except ConfigParser.NoOptionError:
1069 def save_settings(self, config, sectionName):
1071 @note Thread Agnostic
1073 config.set(sectionName, "filter", self._selectedFilter)
1075 def _is_history_visible(self, model, iter):
1077 action = model.get_value(iter, self.ACTION_IDX)
1079 return False # this seems weird but oh well
1081 if self._selectedFilter in [action, "All"]:
1085 except Exception, e:
1086 self._errorDisplay.push_exception()
1088 def _idly_populate_historyview(self):
1089 with gtk_toolbox.gtk_lock():
1090 banner = hildonize.show_busy_banner_start(self._window, "Loading Call History")
1092 self._historymodel.clear()
1093 self._isPopulated = True
1096 historyItems = self._backend.get_recent()
1097 except Exception, e:
1098 self._errorDisplay.push_exception_with_lock()
1099 self._isPopulated = False
1103 gv_backend.decorate_recent(data)
1104 for data in gv_backend.sort_messages(historyItems)
1107 for contactId, personName, phoneNumber, date, action in historyItems:
1109 personName = "Unknown"
1110 date = abbrev_relative_date(date)
1111 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1112 prettyNumber = make_pretty(prettyNumber)
1113 item = (prettyNumber, date, action.capitalize(), personName, contactId)
1114 with gtk_toolbox.gtk_lock():
1115 self._historymodel.append(item)
1116 except Exception, e:
1117 self._errorDisplay.push_exception_with_lock()
1119 with gtk_toolbox.gtk_lock():
1120 hildonize.show_busy_banner_end(banner)
1124 def _on_history_filter_clicked(self, *args, **kwds):
1126 selectedComboIndex = self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
1129 newSelectedComboIndex = hildonize.touch_selector(
1132 self.HISTORY_ITEM_TYPES,
1135 except RuntimeError:
1138 option = self.HISTORY_ITEM_TYPES[newSelectedComboIndex]
1139 self._selectedFilter = option
1140 self._historyFilterSelector.set_label(self._selectedFilter)
1141 self._historymodelfiltered.refilter()
1142 except Exception, e:
1143 self._errorDisplay.push_exception()
1145 def _on_historyview_row_activated(self, treeview, path, view_column):
1147 childPath = self._historymodelfiltered.convert_path_to_child_path(path)
1148 itr = self._historymodel.get_iter(childPath)
1152 number = self._historymodel.get_value(itr, self.NUMBER_IDX)
1153 number = make_ugly(number)
1154 description = self._historymodel.get_value(itr, self.FROM_IDX)
1155 contactId = self._historymodel.get_value(itr, self.FROM_ID_IDX)
1157 contactPhoneNumbers = list(self._backend.get_contact_details(contactId))
1159 (number == make_ugly(contactNumber) or number[1:] == make_ugly(contactNumber))
1160 for (numberDescription, contactNumber) in contactPhoneNumbers
1163 defaultIndex = defaultMatches.index(True)
1165 contactPhoneNumbers.append(("Other", number))
1166 defaultIndex = len(contactPhoneNumbers)-1
1168 "Could not find contact %r's number %s among %r" % (
1169 contactId, number, contactPhoneNumbers
1173 contactPhoneNumbers = [("Phone", number)]
1177 contactPhoneNumbers,
1178 messages = (description, ),
1179 defaultIndex = defaultIndex,
1181 self._historyviewselection.unselect_all()
1182 except Exception, e:
1183 self._errorDisplay.push_exception()
1186 class MessagesView(object):
1194 MESSAGE_DATA_IDX = 6
1196 NO_MESSAGES = "None"
1197 VOICEMAIL_MESSAGES = "Voicemail"
1198 TEXT_MESSAGES = "Texts"
1199 ALL_TYPES = "All Messages"
1200 MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
1202 UNREAD_STATUS = "Unread"
1203 UNARCHIVED_STATUS = "Inbox"
1205 MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
1207 def __init__(self, widgetTree, backend, errorDisplay):
1208 self._errorDisplay = errorDisplay
1209 self._backend = backend
1211 self._isPopulated = False
1212 self._messagemodel = gtk.ListStore(
1213 gobject.TYPE_STRING, # number
1214 gobject.TYPE_STRING, # date
1215 gobject.TYPE_STRING, # header
1216 gobject.TYPE_STRING, # message
1218 gobject.TYPE_STRING, # from id
1219 object, # message data
1221 self._messagemodelfiltered = self._messagemodel.filter_new()
1222 self._messagemodelfiltered.set_visible_func(self._is_message_visible)
1223 self._messageview = widgetTree.get_widget("messages_view")
1224 self._messageviewselection = None
1225 self._onMessageviewRowActivatedId = 0
1227 self._messageRenderer = gtk.CellRendererText()
1228 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1229 self._messageRenderer.set_property("wrap-width", 500)
1230 self._messageColumn = gtk.TreeViewColumn("Messages")
1231 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1232 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1233 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1235 self._window = gtk_toolbox.find_parent_window(self._messageview)
1237 self._messageTypeButton = widgetTree.get_widget("messageTypeButton")
1238 self._onMessageTypeClickedId = 0
1239 self._messageType = self.ALL_TYPES
1240 self._messageStatusButton = widgetTree.get_widget("messageStatusButton")
1241 self._onMessageStatusClickedId = 0
1242 self._messageStatus = self.ALL_STATUS
1244 self._updateSink = gtk_toolbox.threaded_stage(
1246 self._idly_populate_messageview,
1247 gtk_toolbox.null_sink(),
1252 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1253 self._messageview.set_model(self._messagemodelfiltered)
1254 self._messageview.set_headers_visible(False)
1255 self._messageview.set_fixed_height_mode(False)
1257 self._messageview.append_column(self._messageColumn)
1258 self._messageviewselection = self._messageview.get_selection()
1259 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1261 self._messageTypeButton.set_label(self._messageType)
1262 self._messageStatusButton.set_label(self._messageStatus)
1264 self._onMessageviewRowActivatedId = self._messageview.connect(
1265 "row-activated", self._on_messageview_row_activated
1267 self._onMessageTypeClickedId = self._messageTypeButton.connect(
1268 "clicked", self._on_message_type_clicked
1270 self._onMessageStatusClickedId = self._messageStatusButton.connect(
1271 "clicked", self._on_message_status_clicked
1275 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1276 self._messageTypeButton.disconnect(self._onMessageTypeClickedId)
1277 self._messageStatusButton.disconnect(self._onMessageStatusClickedId)
1281 self._messageview.remove_column(self._messageColumn)
1282 self._messageview.set_model(None)
1284 def add_contact(self, *args, **kwds):
1286 @note Actual dial function is patched in later
1288 raise NotImplementedError("Horrible unknown error has occurred")
1290 def update(self, force = False):
1291 if not force and self._isPopulated:
1293 self._updateSink.send(())
1297 self._isPopulated = False
1298 self._messagemodel.clear()
1304 def load_settings(self, config, sectionName):
1306 self._messageType = config.get(sectionName, "type")
1307 if self._messageType not in self.MESSAGE_TYPES:
1308 self._messageType = self.ALL_TYPES
1309 self._messageStatus = config.get(sectionName, "status")
1310 if self._messageStatus not in self.MESSAGE_STATUSES:
1311 self._messageStatus = self.ALL_STATUS
1312 except ConfigParser.NoOptionError:
1315 def save_settings(self, config, sectionName):
1317 @note Thread Agnostic
1319 config.set(sectionName, "status", self._messageStatus)
1320 config.set(sectionName, "type", self._messageType)
1322 def _is_message_visible(self, model, iter):
1324 message = model.get_value(iter, self.MESSAGE_DATA_IDX)
1326 return False # this seems weird but oh well
1327 return self._filter_messages(message, self._messageType, self._messageStatus)
1328 except Exception, e:
1329 self._errorDisplay.push_exception()
1332 def _filter_messages(cls, message, type, status):
1333 if type == cls.ALL_TYPES:
1336 messageType = message["type"]
1337 isType = messageType == type
1339 if status == cls.ALL_STATUS:
1342 isUnarchived = not message["isArchived"]
1343 isUnread = not message["isRead"]
1344 if status == cls.UNREAD_STATUS:
1345 isStatus = isUnarchived and isUnread
1346 elif status == cls.UNARCHIVED_STATUS:
1347 isStatus = isUnarchived
1349 assert "Status %s is bad for %r" % (status, message)
1351 return isType and isStatus
1353 _MIN_MESSAGES_SHOWN = 4
1355 def _idly_populate_messageview(self):
1356 with gtk_toolbox.gtk_lock():
1357 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1359 self._messagemodel.clear()
1360 self._isPopulated = True
1362 if self._messageType == self.NO_MESSAGES:
1366 messageItems = self._backend.get_messages()
1367 except Exception, e:
1368 self._errorDisplay.push_exception_with_lock()
1369 self._isPopulated = False
1373 (gv_backend.decorate_message(message), message)
1374 for message in gv_backend.sort_messages(messageItems)
1377 for (contactId, header, number, relativeDate, messages), messageData in messageItems:
1378 prettyNumber = number[2:] if number.startswith("+1") else number
1379 prettyNumber = make_pretty(prettyNumber)
1381 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1382 expandedMessages = [firstMessage]
1383 expandedMessages.extend(messages)
1384 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1385 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1386 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1387 collapsedMessages = [firstMessage, secondMessage]
1388 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1390 collapsedMessages = expandedMessages
1391 #collapsedMessages = _collapse_message(collapsedMessages, 60, self._MIN_MESSAGES_SHOWN)
1393 number = make_ugly(number)
1395 row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId, messageData
1396 with gtk_toolbox.gtk_lock():
1397 self._messagemodel.append(row)
1398 except Exception, e:
1399 self._errorDisplay.push_exception_with_lock()
1401 with gtk_toolbox.gtk_lock():
1402 hildonize.show_busy_banner_end(banner)
1403 self._messagemodelfiltered.refilter()
1407 def _on_messageview_row_activated(self, treeview, path, view_column):
1409 childPath = self._messagemodelfiltered.convert_path_to_child_path(path)
1410 itr = self._messagemodel.get_iter(childPath)
1414 number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX))
1415 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1417 contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX)
1419 contactPhoneNumbers = list(self._backend.get_contact_details(contactId))
1421 (number == make_ugly(contactNumber) or number[1:] == make_ugly(contactNumber))
1422 for (numberDescription, contactNumber) in contactPhoneNumbers
1425 defaultIndex = defaultMatches.index(True)
1427 contactPhoneNumbers.append(("Other", number))
1428 defaultIndex = len(contactPhoneNumbers)-1
1430 "Could not find contact %r's number %s among %r" % (
1431 contactId, number, contactPhoneNumbers
1435 contactPhoneNumbers = [("Phone", number)]
1439 contactPhoneNumbers,
1440 messages = description,
1441 defaultIndex = defaultIndex,
1443 self._messageviewselection.unselect_all()
1444 except Exception, e:
1445 self._errorDisplay.push_exception()
1447 def _on_message_type_clicked(self, *args, **kwds):
1449 selectedIndex = self.MESSAGE_TYPES.index(self._messageType)
1452 newSelectedIndex = hildonize.touch_selector(
1458 except RuntimeError:
1461 if selectedIndex != newSelectedIndex:
1462 self._messageType = self.MESSAGE_TYPES[newSelectedIndex]
1463 self._messageTypeButton.set_label(self._messageType)
1464 self._messagemodelfiltered.refilter()
1465 except Exception, e:
1466 self._errorDisplay.push_exception()
1468 def _on_message_status_clicked(self, *args, **kwds):
1470 selectedIndex = self.MESSAGE_STATUSES.index(self._messageStatus)
1473 newSelectedIndex = hildonize.touch_selector(
1476 self.MESSAGE_STATUSES,
1479 except RuntimeError:
1482 if selectedIndex != newSelectedIndex:
1483 self._messageStatus = self.MESSAGE_STATUSES[newSelectedIndex]
1484 self._messageStatusButton.set_label(self._messageStatus)
1485 self._messagemodelfiltered.refilter()
1486 except Exception, e:
1487 self._errorDisplay.push_exception()
1490 class ContactsView(object):
1492 CONTACT_TYPE_IDX = 0
1493 CONTACT_NAME_IDX = 1
1496 def __init__(self, widgetTree, backend, errorDisplay):
1497 self._errorDisplay = errorDisplay
1498 self._backend = backend
1500 self._addressBook = None
1501 self._selectedComboIndex = 0
1502 self._addressBookFactories = [null_backend.NullAddressBook()]
1504 self._booksList = []
1505 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1507 self._isPopulated = False
1508 self._contactsmodel = gtk.ListStore(
1509 gobject.TYPE_STRING, # Contact Type
1510 gobject.TYPE_STRING, # Contact Name
1511 gobject.TYPE_STRING, # Contact ID
1513 self._contactsviewselection = None
1514 self._contactsview = widgetTree.get_widget("contactsview")
1516 self._contactColumn = gtk.TreeViewColumn("Contact")
1517 displayContactSource = False
1518 if displayContactSource:
1519 textrenderer = gtk.CellRendererText()
1520 self._contactColumn.pack_start(textrenderer, expand=False)
1521 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_TYPE_IDX)
1522 textrenderer = gtk.CellRendererText()
1523 hildonize.set_cell_thumb_selectable(textrenderer)
1524 self._contactColumn.pack_start(textrenderer, expand=True)
1525 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_NAME_IDX)
1526 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1527 self._contactColumn.set_sort_column_id(1)
1528 self._contactColumn.set_visible(True)
1530 self._onContactsviewRowActivatedId = 0
1531 self._onAddressbookButtonChangedId = 0
1532 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1534 self._updateSink = gtk_toolbox.threaded_stage(
1536 self._idly_populate_contactsview,
1537 gtk_toolbox.null_sink(),
1542 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1544 self._contactsview.set_model(self._contactsmodel)
1545 self._contactsview.set_fixed_height_mode(False)
1546 self._contactsview.append_column(self._contactColumn)
1547 self._contactsviewselection = self._contactsview.get_selection()
1548 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1550 del self._booksList[:]
1551 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1552 if factoryName and bookName:
1553 entryName = "%s: %s" % (factoryName, bookName)
1555 entryName = factoryName
1557 entryName = bookName
1559 entryName = "Bad name (%d)" % factoryId
1560 row = (str(factoryId), bookId, entryName)
1561 self._booksList.append(row)
1563 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1564 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1566 if len(self._booksList) <= self._selectedComboIndex:
1567 self._selectedComboIndex = 0
1568 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1570 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1571 selectedBookId = self._booksList[self._selectedComboIndex][1]
1572 self.open_addressbook(selectedFactoryId, selectedBookId)
1575 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1576 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1580 self._bookSelectionButton.set_label("")
1581 self._contactsview.set_model(None)
1582 self._contactsview.remove_column(self._contactColumn)
1584 def add_contact(self, *args, **kwds):
1586 @note Actual dial function is patched in later
1588 raise NotImplementedError("Horrible unknown error has occurred")
1590 def get_addressbooks(self):
1592 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1594 for i, factory in enumerate(self._addressBookFactories):
1595 for bookFactory, bookId, bookName in factory.get_addressbooks():
1596 yield (str(i), bookId), (factory.factory_name(), bookName)
1598 def open_addressbook(self, bookFactoryId, bookId):
1599 bookFactoryIndex = int(bookFactoryId)
1600 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1601 self._addressBook = addressBook
1603 def update(self, force = False):
1604 if not force and self._isPopulated:
1606 self._updateSink.send(())
1610 self._isPopulated = False
1611 self._contactsmodel.clear()
1612 for factory in self._addressBookFactories:
1613 factory.clear_caches()
1614 self._addressBook.clear_caches()
1616 def append(self, book):
1617 self._addressBookFactories.append(book)
1619 def extend(self, books):
1620 self._addressBookFactories.extend(books)
1626 def load_settings(self, config, sectionName):
1628 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1629 except ConfigParser.NoOptionError:
1630 self._selectedComboIndex = 0
1632 def save_settings(self, config, sectionName):
1633 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1635 def _idly_populate_contactsview(self):
1636 with gtk_toolbox.gtk_lock():
1637 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1640 while addressBook is not self._addressBook:
1641 addressBook = self._addressBook
1642 with gtk_toolbox.gtk_lock():
1643 self._contactsview.set_model(None)
1647 contacts = addressBook.get_contacts()
1648 except Exception, e:
1650 self._isPopulated = False
1651 self._errorDisplay.push_exception_with_lock()
1652 for contactId, contactName in contacts:
1653 contactType = addressBook.contact_source_short_name(contactId)
1654 row = contactType, contactName, contactId
1655 self._contactsmodel.append(row)
1657 with gtk_toolbox.gtk_lock():
1658 self._contactsview.set_model(self._contactsmodel)
1660 self._isPopulated = True
1661 except Exception, e:
1662 self._errorDisplay.push_exception_with_lock()
1664 with gtk_toolbox.gtk_lock():
1665 hildonize.show_busy_banner_end(banner)
1668 def _on_addressbook_button_changed(self, *args, **kwds):
1671 newSelectedComboIndex = hildonize.touch_selector(
1674 (("%s" % m[2]) for m in self._booksList),
1675 self._selectedComboIndex,
1677 except RuntimeError:
1680 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1681 selectedBookId = self._booksList[newSelectedComboIndex][1]
1683 oldAddressbook = self._addressBook
1684 self.open_addressbook(selectedFactoryId, selectedBookId)
1685 forceUpdate = True if oldAddressbook is not self._addressBook else False
1686 self.update(force=forceUpdate)
1688 self._selectedComboIndex = newSelectedComboIndex
1689 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1690 except Exception, e:
1691 self._errorDisplay.push_exception()
1693 def _on_contactsview_row_activated(self, treeview, path, view_column):
1695 itr = self._contactsmodel.get_iter(path)
1699 contactId = self._contactsmodel.get_value(itr, self.CONTACT_ID_IDX)
1700 contactName = self._contactsmodel.get_value(itr, self.CONTACT_NAME_IDX)
1702 contactDetails = self._addressBook.get_contact_details(contactId)
1703 except Exception, e:
1705 self._errorDisplay.push_exception()
1706 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1708 if len(contactPhoneNumbers) == 0:
1712 contactPhoneNumbers,
1713 messages = (contactName, ),
1715 self._contactsviewselection.unselect_all()
1716 except Exception, e:
1717 self._errorDisplay.push_exception()