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):
251 self._clipboard = gtk.clipboard_get()
252 self._widgetTree = widgetTree
253 self._window = self._widgetTree.get_widget("smsWindow")
254 self._window.connect("delete-event", self._on_delete)
255 self._window.connect("key-press-event", self._on_key_press)
256 self._window.connect("window-state-event", self._on_window_state_change)
257 self._isFullScreen = False
258 self._parent = parent
260 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
261 self._smsButton.connect("clicked", self._on_send)
262 self._dialButton = self._widgetTree.get_widget("dialButton")
263 self._dialButton.connect("clicked", self._on_dial)
265 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
267 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
268 self._messagesView = self._widgetTree.get_widget("smsMessages")
270 textrenderer = gtk.CellRendererText()
271 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
272 textrenderer.set_property("wrap-width", 450)
273 messageColumn = gtk.TreeViewColumn("")
274 messageColumn.pack_start(textrenderer, expand=True)
275 messageColumn.add_attribute(textrenderer, "markup", 0)
276 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
277 self._messagesView.append_column(messageColumn)
278 self._messagesView.set_headers_visible(False)
279 self._messagesView.set_model(self._messagemodel)
280 self._messagesView.set_fixed_height_mode(False)
282 self._conversationView = self._messagesView.get_parent()
283 self._conversationViewPort = self._conversationView.get_parent()
284 self._scrollWindow = self._conversationViewPort.get_parent()
286 self._targetList = self._widgetTree.get_widget("smsTargetList")
287 self._phoneButton = self._widgetTree.get_widget("phoneTypeSelection")
288 self._phoneButton.connect("clicked", self._on_phone)
289 self._smsEntry = self._widgetTree.get_widget("smsEntry")
290 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
291 self._smsEntrySize = None
295 def add_contact(self, contactDetails, messages = (), defaultIndex = -1):
296 contactNumbers = list(self._to_contact_numbers(contactDetails))
297 assert contactNumbers
298 contactIndex = defaultIndex if defaultIndex != -1 else 0
299 contact = contactNumbers, contactIndex, messages
300 self._contacts.append(contact)
302 selector = gtk.Button(contactNumbers[0][1])
303 removeContact = gtk.Button(stock="gtk-delete")
305 row.pack_start(selector, True, True)
306 row.pack_start(removeContact, False, False)
308 self._targetList.pack_start(row)
309 selector.connect("clicked", self._on_choose_phone_n, row)
310 removeContact.connect("clicked", self._on_remove_phone_n, row)
311 self._update_button_state()
312 self._update_context()
314 parentSize = self._parent.get_size()
315 self._window.resize(parentSize[0], max(parentSize[1]-10, 100))
317 self._window.present()
319 self._smsEntry.grab_focus()
320 dx = self._conversationView.get_allocation().height - self._conversationViewPort.get_allocation().height
322 adjustment = self._scrollWindow.get_vadjustment()
323 adjustment.value = dx
326 del self._contacts[:]
328 for contactNumberSelector in list(self._targetList.get_children()):
329 self._targetList.remove(contactNumberSelector)
330 self._smsEntry.get_buffer().set_text("")
331 self._update_letter_count()
332 self._update_context()
334 def fullscreen(self):
335 self._window.fullscreen()
337 def unfullscreen(self):
338 self._window.unfullscreen()
340 def _remove_contact(self, contactIndex):
341 del self._contacts[contactIndex]
343 contactNumberSelector = list(self._targetList.get_children())[contactIndex]
344 self._targetList.remove(contactNumberSelector)
345 self._update_button_state()
346 self._update_context()
348 def _update_letter_count(self):
349 if self._smsEntrySize is None:
350 self._smsEntrySize = self._smsEntry.size_request()
352 self._smsEntry.set_size_request(*self._smsEntrySize)
353 entryLength = self._smsEntry.get_buffer().get_char_count()
355 numTexts, numCharInText = divmod(entryLength, self.MAX_CHAR)
357 self._letterCountLabel.set_text("%s.%s" % (numTexts, numCharInText))
359 self._letterCountLabel.set_text("%s" % (numCharInText, ))
361 self._update_button_state()
363 def _update_context(self):
364 self._messagemodel.clear()
365 if len(self._contacts) == 0:
366 self._messagesView.hide()
367 self._targetList.hide()
368 self._phoneButton.hide()
369 self._phoneButton.set_label("Error: You shouldn't see this")
370 elif len(self._contacts) == 1:
371 contactNumbers, index, messages = self._contacts[0]
373 self._messagesView.show()
374 for message in messages:
376 self._messagemodel.append(row)
377 messagesSelection = self._messagesView.get_selection()
378 messagesSelection.select_path((len(messages)-1, ))
380 self._messagesView.hide()
381 self._targetList.hide()
382 self._phoneButton.show()
383 self._phoneButton.set_label(contactNumbers[index][1])
384 if 1 < len(contactNumbers):
385 self._phoneButton.set_sensitive(True)
387 self._phoneButton.set_sensitive(False)
389 self._messagesView.hide()
390 self._targetList.show()
391 self._phoneButton.hide()
392 self._phoneButton.set_label("Error: You shouldn't see this")
394 def _update_button_state(self):
395 if len(self._contacts) == 0:
396 self._dialButton.set_sensitive(False)
397 self._smsButton.set_sensitive(False)
398 elif len(self._contacts) == 1:
399 entryLength = self._smsEntry.get_buffer().get_char_count()
401 self._dialButton.set_sensitive(True)
402 self._smsButton.set_sensitive(False)
404 self._dialButton.set_sensitive(False)
405 self._smsButton.set_sensitive(True)
407 self._dialButton.set_sensitive(False)
408 self._smsButton.set_sensitive(True)
410 def _to_contact_numbers(self, contactDetails):
411 for phoneType, phoneNumber in contactDetails:
412 display = " - ".join((make_pretty(phoneNumber), phoneType))
413 yield (phoneNumber, display)
419 def _request_number(self, contactIndex):
420 contactNumbers, index, messages = self._contacts[contactIndex]
421 assert 0 <= index, "%r" % index
423 index = hildonize.touch_selector(
426 (description for (number, description) in contactNumbers),
429 self._contacts[contactIndex] = contactNumbers, index, messages
431 def send_sms(self, numbers, message):
432 raise NotImplementedError()
434 def dial(self, number):
435 raise NotImplementedError()
437 def _on_phone(self, *args):
439 assert len(self._contacts) == 1
440 self._request_number(0)
442 contactNumbers, numberIndex, messages = self._contacts[0]
443 self._phoneButton.set_label(contactNumbers[numberIndex][1])
444 row = list(self._targetList.get_children())[0]
445 phoneButton = list(row.get_children())[0]
446 phoneButton.set_label(contactNumbers[numberIndex][1])
448 _moduleLogger.exception("%s" % str(e))
450 def _on_choose_phone_n(self, button, row):
452 assert 1 < len(self._contacts)
453 targetList = list(self._targetList.get_children())
454 index = targetList.index(row)
455 self._request_number(index)
457 contactNumbers, numberIndex, messages = self._contacts[0]
458 phoneButton = list(row.get_children())[0]
459 phoneButton.set_label(contactNumbers[numberIndex][1])
461 _moduleLogger.exception("%s" % str(e))
463 def _on_remove_phone_n(self, button, row):
465 assert 1 < len(self._contacts)
466 targetList = list(self._targetList.get_children())
467 index = targetList.index(row)
469 del self._contacts[index]
470 self._targetList.remove(row)
471 self._update_context()
472 self._update_button_state()
474 _moduleLogger.exception("%s" % str(e))
476 def _on_entry_changed(self, *args):
477 self._update_letter_count()
479 def _on_send(self, *args):
480 assert 0 < len(self._contacts), "%r" % self._contacts
482 make_ugly(contact[0][contact[1]][0])
483 for contact in self._contacts
486 entryBuffer = self._smsEntry.get_buffer()
487 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
488 enteredMessage = enteredMessage.strip()
489 assert enteredMessage
490 self.send_sms(phoneNumbers, enteredMessage)
493 def _on_dial(self, *args):
494 assert len(self._contacts) == 1, "%r" % self._contacts
495 contact = self._contacts[0]
496 contactNumber = contact[0][contact[1]][0]
497 phoneNumber = make_ugly(contactNumber)
498 self.dial(phoneNumber)
501 def _on_delete(self, *args):
502 self._window.emit_stop_by_name("delete-event")
506 def _on_window_state_change(self, widget, event, *args):
508 @note Hildon specific
511 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
512 self._isFullScreen = True
514 self._isFullScreen = False
516 self._errorDisplay.push_exception()
518 def _on_key_press(self, widget, event):
519 RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
522 event.keyval == gtk.keysyms.F6 or
523 event.keyval in RETURN_TYPES and event.get_state() & gtk.gdk.CONTROL_MASK
525 if self._isFullScreen:
526 self._window.unfullscreen()
528 self._window.fullscreen()
529 elif event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
532 for messagePart in self._messagemodel
534 self._clipboard.set_text(str(message))
536 event.keyval == gtk.keysyms.w and
537 event.get_state() & gtk.gdk.CONTROL_MASK
541 event.keyval == gtk.keysyms.q and
542 event.get_state() & gtk.gdk.CONTROL_MASK
544 self._parent.destroy()
546 _moduleLogger.exception(str(e))
549 class Dialpad(object):
551 def __init__(self, widgetTree, errorDisplay):
552 self._clipboard = gtk.clipboard_get()
553 self._errorDisplay = errorDisplay
555 self._numberdisplay = widgetTree.get_widget("numberdisplay")
556 self._okButton = widgetTree.get_widget("dialpadOk")
557 self._backButton = widgetTree.get_widget("back")
558 self._plusButton = widgetTree.get_widget("plus")
559 self._phonenumber = ""
560 self._prettynumber = ""
563 "on_digit_clicked": self._on_digit_clicked,
565 widgetTree.signal_autoconnect(callbackMapping)
566 self._okButton.connect("clicked", self._on_ok_clicked)
567 self._plusButton.connect("clicked", self._on_plus)
569 self._originalLabel = self._backButton.get_label()
570 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
571 self._backTapHandler.on_tap = self._on_backspace
572 self._backTapHandler.on_hold = self._on_clearall
573 self._backTapHandler.on_holding = self._set_clear_button
574 self._backTapHandler.on_cancel = self._reset_back_button
576 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
577 self._keyPressEventId = 0
580 self._okButton.grab_focus()
581 self._backTapHandler.enable()
582 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
585 self._window.disconnect(self._keyPressEventId)
586 self._keyPressEventId = 0
587 self._reset_back_button()
588 self._backTapHandler.disable()
590 def add_contact(self, *args, **kwds):
592 @note Actual dial function is patched in later
594 raise NotImplementedError("Horrible unknown error has occurred")
596 def get_number(self):
597 return self._phonenumber
599 def set_number(self, number):
601 Set the number to dial
604 self._phonenumber = make_ugly(number)
605 self._prettynumber = make_pretty(self._phonenumber)
606 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
608 self._errorDisplay.push_exception()
617 def load_settings(self, config, section):
620 def save_settings(self, config, section):
622 @note Thread Agnostic
626 def _on_key_press(self, widget, event):
628 if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
629 contents = self._clipboard.wait_for_text()
630 if contents is not None:
631 self.set_number(contents)
633 self._errorDisplay.push_exception()
635 def _on_ok_clicked(self, widget):
637 phoneNumber = self.get_number()
639 [("Dialer", phoneNumber)], ()
643 self._errorDisplay.push_exception()
645 def _on_digit_clicked(self, widget):
647 self.set_number(self._phonenumber + widget.get_name()[-1])
649 self._errorDisplay.push_exception()
651 def _on_plus(self, *args):
653 self.set_number(self._phonenumber + "+")
655 self._errorDisplay.push_exception()
657 def _on_backspace(self, taps):
659 self.set_number(self._phonenumber[:-taps])
660 self._reset_back_button()
662 self._errorDisplay.push_exception()
664 def _on_clearall(self, taps):
667 self._reset_back_button()
669 self._errorDisplay.push_exception()
672 def _set_clear_button(self):
674 self._backButton.set_label("gtk-clear")
676 self._errorDisplay.push_exception()
678 def _reset_back_button(self):
680 self._backButton.set_label(self._originalLabel)
682 self._errorDisplay.push_exception()
685 class AccountInfo(object):
687 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
688 self._errorDisplay = errorDisplay
689 self._backend = backend
690 self._isPopulated = False
691 self._alarmHandler = alarmHandler
692 self._notifyOnMissed = False
693 self._notifyOnVoicemail = False
694 self._notifyOnSms = False
696 self._callbackList = []
697 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
698 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
699 self._onCallbackSelectChangedId = 0
701 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
702 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
703 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
704 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
705 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
706 self._onNotifyToggled = 0
707 self._onMinutesChanged = 0
708 self._onMissedToggled = 0
709 self._onVoicemailToggled = 0
710 self._onSmsToggled = 0
711 self._applyAlarmTimeoutId = None
713 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
714 self._callbackNumber = ""
717 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
719 self._accountViewNumberDisplay.set_use_markup(True)
720 self.set_account_number("")
722 del self._callbackList[:]
723 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
724 self._set_callback_label("")
726 if self._alarmHandler is not None:
727 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
728 self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
729 self._missedCheckbox.set_active(self._notifyOnMissed)
730 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
731 self._smsCheckbox.set_active(self._notifyOnSms)
733 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
734 self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
735 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
736 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
737 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
739 self._notifyCheckbox.set_sensitive(False)
740 self._minutesEntryButton.set_sensitive(False)
741 self._missedCheckbox.set_sensitive(False)
742 self._voicemailCheckbox.set_sensitive(False)
743 self._smsCheckbox.set_sensitive(False)
745 self.update(force=True)
748 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
749 self._onCallbackSelectChangedId = 0
750 self._set_callback_label("")
752 if self._alarmHandler is not None:
753 self._notifyCheckbox.disconnect(self._onNotifyToggled)
754 self._minutesEntryButton.disconnect(self._onMinutesChanged)
755 self._missedCheckbox.disconnect(self._onNotifyToggled)
756 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
757 self._smsCheckbox.disconnect(self._onNotifyToggled)
758 self._onNotifyToggled = 0
759 self._onMinutesChanged = 0
760 self._onMissedToggled = 0
761 self._onVoicemailToggled = 0
762 self._onSmsToggled = 0
764 self._notifyCheckbox.set_sensitive(True)
765 self._minutesEntryButton.set_sensitive(True)
766 self._missedCheckbox.set_sensitive(True)
767 self._voicemailCheckbox.set_sensitive(True)
768 self._smsCheckbox.set_sensitive(True)
771 del self._callbackList[:]
773 def set_account_number(self, number):
775 Displays current account number
777 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
779 def update(self, force = False):
780 if not force and self._isPopulated:
782 self._populate_callback_combo()
783 self.set_account_number(self._backend.get_account_number())
787 self._set_callback_label("")
788 self.set_account_number("")
789 self._isPopulated = False
791 def save_everything(self):
792 raise NotImplementedError
796 return "Account Info"
798 def load_settings(self, config, section):
799 self._callbackNumber = make_ugly(config.get(section, "callback"))
800 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
801 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
802 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
804 def save_settings(self, config, section):
806 @note Thread Agnostic
808 config.set(section, "callback", self._callbackNumber)
809 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
810 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
811 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
813 def _populate_callback_combo(self):
814 self._isPopulated = True
815 del self._callbackList[:]
817 callbackNumbers = self._backend.get_callback_numbers()
819 self._errorDisplay.push_exception()
820 self._isPopulated = False
823 if len(callbackNumbers) == 0:
824 callbackNumbers = {"": "No callback numbers available"}
826 for number, description in callbackNumbers.iteritems():
827 self._callbackList.append((make_pretty(number), description))
829 self._set_callback_number(self._callbackNumber)
831 def _set_callback_number(self, number):
833 if not self._backend.is_valid_syntax(number) and 0 < len(number):
834 self._errorDisplay.push_message("%s is not a valid callback number" % number)
835 elif number == self._backend.get_callback_number() and 0 < len(number):
836 _moduleLogger.warning(
837 "Callback number already is %s" % (
838 self._backend.get_callback_number(),
841 self._set_callback_label(number)
843 if number.startswith("1747"): number = "+" + number
844 self._backend.set_callback_number(number)
845 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
846 make_pretty(number), make_pretty(self._backend.get_callback_number())
848 self._callbackNumber = make_ugly(number)
849 self._set_callback_label(number)
851 "Callback number set to %s" % (
852 self._backend.get_callback_number(),
856 self._errorDisplay.push_exception()
858 def _set_callback_label(self, uglyNumber):
859 prettyNumber = make_pretty(uglyNumber)
860 if len(prettyNumber) == 0:
861 prettyNumber = "No Callback Number"
862 self._callbackSelectButton.set_label(prettyNumber)
864 def _update_alarm_settings(self, recurrence):
866 isEnabled = self._notifyCheckbox.get_active()
867 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
868 self._alarmHandler.apply_settings(isEnabled, recurrence)
870 self.save_everything()
871 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
872 self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
874 def _on_callbackentry_clicked(self, *args):
876 actualSelection = make_pretty(self._callbackNumber)
879 (number, "%s (%s)" % (number, description))
880 for (number, description) in self._callbackList
882 defaultSelection = userOptions.get(actualSelection, actualSelection)
884 userSelection = hildonize.touch_selector_entry(
887 list(userOptions.itervalues()),
890 reversedUserOptions = dict(
891 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
893 selectedNumber = reversedUserOptions.get(userSelection, userSelection)
895 number = make_ugly(selectedNumber)
896 self._set_callback_number(number)
897 except RuntimeError, e:
898 _moduleLogger.exception("%s" % str(e))
900 self._errorDisplay.push_exception()
902 def _on_notify_toggled(self, *args):
904 if self._applyAlarmTimeoutId is not None:
905 gobject.source_remove(self._applyAlarmTimeoutId)
906 self._applyAlarmTimeoutId = None
907 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
909 self._errorDisplay.push_exception()
911 def _on_minutes_clicked(self, *args):
912 recurrenceChoices = [
928 actualSelection = self._alarmHandler.recurrence
930 closestSelectionIndex = 0
931 for i, possible in enumerate(recurrenceChoices):
932 if possible[0] <= actualSelection:
933 closestSelectionIndex = i
934 recurrenceIndex = hildonize.touch_selector(
937 (("%s" % m[1]) for m in recurrenceChoices),
938 closestSelectionIndex,
940 recurrence = recurrenceChoices[recurrenceIndex][0]
942 self._update_alarm_settings(recurrence)
943 except RuntimeError, e:
944 _moduleLogger.exception("%s" % str(e))
946 self._errorDisplay.push_exception()
948 def _on_apply_timeout(self, *args):
950 self._applyAlarmTimeoutId = None
952 self._update_alarm_settings(self._alarmHandler.recurrence)
954 self._errorDisplay.push_exception()
957 def _on_missed_toggled(self, *args):
959 self._notifyOnMissed = self._missedCheckbox.get_active()
960 self.save_everything()
962 self._errorDisplay.push_exception()
964 def _on_voicemail_toggled(self, *args):
966 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
967 self.save_everything()
969 self._errorDisplay.push_exception()
971 def _on_sms_toggled(self, *args):
973 self._notifyOnSms = self._smsCheckbox.get_active()
974 self.save_everything()
976 self._errorDisplay.push_exception()
979 class CallHistoryView(object):
987 HISTORY_ITEM_TYPES = ["All", "Received", "Missed", "Placed"]
989 def __init__(self, widgetTree, backend, errorDisplay):
990 self._errorDisplay = errorDisplay
991 self._backend = backend
993 self._isPopulated = False
994 self._historymodel = gtk.ListStore(
995 gobject.TYPE_STRING, # number
996 gobject.TYPE_STRING, # date
997 gobject.TYPE_STRING, # action
998 gobject.TYPE_STRING, # from
999 gobject.TYPE_STRING, # from id
1001 self._historymodelfiltered = self._historymodel.filter_new()
1002 self._historymodelfiltered.set_visible_func(self._is_history_visible)
1003 self._historyview = widgetTree.get_widget("historyview")
1004 self._historyviewselection = None
1005 self._onRecentviewRowActivatedId = 0
1007 textrenderer = gtk.CellRendererText()
1008 textrenderer.set_property("yalign", 0)
1009 self._dateColumn = gtk.TreeViewColumn("Date")
1010 self._dateColumn.pack_start(textrenderer, expand=True)
1011 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
1013 textrenderer = gtk.CellRendererText()
1014 textrenderer.set_property("yalign", 0)
1015 self._actionColumn = gtk.TreeViewColumn("Action")
1016 self._actionColumn.pack_start(textrenderer, expand=True)
1017 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
1019 textrenderer = gtk.CellRendererText()
1020 textrenderer.set_property("yalign", 0)
1021 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
1022 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
1023 self._numberColumn = gtk.TreeViewColumn("Number")
1024 self._numberColumn.pack_start(textrenderer, expand=True)
1025 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
1027 textrenderer = gtk.CellRendererText()
1028 textrenderer.set_property("yalign", 0)
1029 hildonize.set_cell_thumb_selectable(textrenderer)
1030 self._nameColumn = gtk.TreeViewColumn("From")
1031 self._nameColumn.pack_start(textrenderer, expand=True)
1032 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
1033 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1035 self._window = gtk_toolbox.find_parent_window(self._historyview)
1037 self._historyFilterSelector = widgetTree.get_widget("historyFilterSelector")
1038 self._historyFilterSelector.connect("clicked", self._on_history_filter_clicked)
1039 self._selectedFilter = "All"
1041 self._updateSink = gtk_toolbox.threaded_stage(
1043 self._idly_populate_historyview,
1044 gtk_toolbox.null_sink(),
1049 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1050 self._historyFilterSelector.set_label(self._selectedFilter)
1052 self._historyview.set_model(self._historymodelfiltered)
1053 self._historyview.set_fixed_height_mode(False)
1055 self._historyview.append_column(self._dateColumn)
1056 self._historyview.append_column(self._actionColumn)
1057 self._historyview.append_column(self._numberColumn)
1058 self._historyview.append_column(self._nameColumn)
1059 self._historyviewselection = self._historyview.get_selection()
1060 self._historyviewselection.set_mode(gtk.SELECTION_SINGLE)
1062 self._onRecentviewRowActivatedId = self._historyview.connect("row-activated", self._on_historyview_row_activated)
1065 self._historyview.disconnect(self._onRecentviewRowActivatedId)
1069 self._historyview.remove_column(self._dateColumn)
1070 self._historyview.remove_column(self._actionColumn)
1071 self._historyview.remove_column(self._nameColumn)
1072 self._historyview.remove_column(self._numberColumn)
1073 self._historyview.set_model(None)
1075 def add_contact(self, *args, **kwds):
1077 @note Actual dial function is patched in later
1079 raise NotImplementedError("Horrible unknown error has occurred")
1081 def update(self, force = False):
1082 if not force and self._isPopulated:
1084 self._updateSink.send(())
1088 self._isPopulated = False
1089 self._historymodel.clear()
1093 return "Recent Calls"
1095 def load_settings(self, config, sectionName):
1097 self._selectedFilter = config.get(sectionName, "filter")
1098 if self._selectedFilter not in self.HISTORY_ITEM_TYPES:
1099 self._messageType = self.HISTORY_ITEM_TYPES[0]
1100 except ConfigParser.NoOptionError:
1103 def save_settings(self, config, sectionName):
1105 @note Thread Agnostic
1107 config.set(sectionName, "filter", self._selectedFilter)
1109 def _is_history_visible(self, model, iter):
1111 action = model.get_value(iter, self.ACTION_IDX)
1113 return False # this seems weird but oh well
1115 if self._selectedFilter in [action, "All"]:
1119 except Exception, e:
1120 self._errorDisplay.push_exception()
1122 def _idly_populate_historyview(self):
1123 with gtk_toolbox.gtk_lock():
1124 banner = hildonize.show_busy_banner_start(self._window, "Loading Call History")
1126 self._historymodel.clear()
1127 self._isPopulated = True
1130 historyItems = self._backend.get_recent()
1131 except Exception, e:
1132 self._errorDisplay.push_exception_with_lock()
1133 self._isPopulated = False
1137 gv_backend.decorate_recent(data)
1138 for data in gv_backend.sort_messages(historyItems)
1141 for contactId, personName, phoneNumber, date, action in historyItems:
1143 personName = "Unknown"
1144 date = abbrev_relative_date(date)
1145 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1146 prettyNumber = make_pretty(prettyNumber)
1147 item = (prettyNumber, date, action.capitalize(), personName, contactId)
1148 with gtk_toolbox.gtk_lock():
1149 self._historymodel.append(item)
1150 except Exception, e:
1151 self._errorDisplay.push_exception_with_lock()
1153 with gtk_toolbox.gtk_lock():
1154 hildonize.show_busy_banner_end(banner)
1158 def _on_history_filter_clicked(self, *args, **kwds):
1160 selectedComboIndex = self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
1163 newSelectedComboIndex = hildonize.touch_selector(
1166 self.HISTORY_ITEM_TYPES,
1169 except RuntimeError:
1172 option = self.HISTORY_ITEM_TYPES[newSelectedComboIndex]
1173 self._selectedFilter = option
1174 self._historyFilterSelector.set_label(self._selectedFilter)
1175 self._historymodelfiltered.refilter()
1176 except Exception, e:
1177 self._errorDisplay.push_exception()
1179 def _history_summary(self, expectedNumber):
1180 for number, action, date, whoFrom, whoFromId in self._historymodel:
1181 if expectedNumber is not None and expectedNumber == number:
1182 yield "%s <i>(%s)</i> - %s %s" % (number, whoFrom, date, action)
1184 def _on_historyview_row_activated(self, treeview, path, view_column):
1186 childPath = self._historymodelfiltered.convert_path_to_child_path(path)
1187 itr = self._historymodel.get_iter(childPath)
1191 prettyNumber = self._historymodel.get_value(itr, self.NUMBER_IDX)
1192 number = make_ugly(prettyNumber)
1193 description = list(self._history_summary(prettyNumber))
1194 contactId = self._historymodel.get_value(itr, self.FROM_ID_IDX)
1195 contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1198 contactPhoneNumbers,
1199 messages = description,
1200 defaultIndex = defaultIndex,
1202 self._historyviewselection.unselect_all()
1203 except Exception, e:
1204 self._errorDisplay.push_exception()
1207 class MessagesView(object):
1215 MESSAGE_DATA_IDX = 6
1217 NO_MESSAGES = "None"
1218 VOICEMAIL_MESSAGES = "Voicemail"
1219 TEXT_MESSAGES = "Texts"
1220 ALL_TYPES = "All Messages"
1221 MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
1223 UNREAD_STATUS = "Unread"
1224 UNARCHIVED_STATUS = "Inbox"
1226 MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
1228 def __init__(self, widgetTree, backend, errorDisplay):
1229 self._errorDisplay = errorDisplay
1230 self._backend = backend
1232 self._isPopulated = False
1233 self._messagemodel = gtk.ListStore(
1234 gobject.TYPE_STRING, # number
1235 gobject.TYPE_STRING, # date
1236 gobject.TYPE_STRING, # header
1237 gobject.TYPE_STRING, # message
1239 gobject.TYPE_STRING, # from id
1240 object, # message data
1242 self._messagemodelfiltered = self._messagemodel.filter_new()
1243 self._messagemodelfiltered.set_visible_func(self._is_message_visible)
1244 self._messageview = widgetTree.get_widget("messages_view")
1245 self._messageviewselection = None
1246 self._onMessageviewRowActivatedId = 0
1248 self._messageRenderer = gtk.CellRendererText()
1249 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1250 self._messageRenderer.set_property("wrap-width", 500)
1251 self._messageColumn = gtk.TreeViewColumn("Messages")
1252 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1253 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1254 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1256 self._window = gtk_toolbox.find_parent_window(self._messageview)
1258 self._messageTypeButton = widgetTree.get_widget("messageTypeButton")
1259 self._onMessageTypeClickedId = 0
1260 self._messageType = self.ALL_TYPES
1261 self._messageStatusButton = widgetTree.get_widget("messageStatusButton")
1262 self._onMessageStatusClickedId = 0
1263 self._messageStatus = self.ALL_STATUS
1265 self._updateSink = gtk_toolbox.threaded_stage(
1267 self._idly_populate_messageview,
1268 gtk_toolbox.null_sink(),
1273 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1274 self._messageview.set_model(self._messagemodelfiltered)
1275 self._messageview.set_headers_visible(False)
1276 self._messageview.set_fixed_height_mode(False)
1278 self._messageview.append_column(self._messageColumn)
1279 self._messageviewselection = self._messageview.get_selection()
1280 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1282 self._messageTypeButton.set_label(self._messageType)
1283 self._messageStatusButton.set_label(self._messageStatus)
1285 self._onMessageviewRowActivatedId = self._messageview.connect(
1286 "row-activated", self._on_messageview_row_activated
1288 self._onMessageTypeClickedId = self._messageTypeButton.connect(
1289 "clicked", self._on_message_type_clicked
1291 self._onMessageStatusClickedId = self._messageStatusButton.connect(
1292 "clicked", self._on_message_status_clicked
1296 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1297 self._messageTypeButton.disconnect(self._onMessageTypeClickedId)
1298 self._messageStatusButton.disconnect(self._onMessageStatusClickedId)
1302 self._messageview.remove_column(self._messageColumn)
1303 self._messageview.set_model(None)
1305 def add_contact(self, *args, **kwds):
1307 @note Actual dial function is patched in later
1309 raise NotImplementedError("Horrible unknown error has occurred")
1311 def update(self, force = False):
1312 if not force and self._isPopulated:
1314 self._updateSink.send(())
1318 self._isPopulated = False
1319 self._messagemodel.clear()
1325 def load_settings(self, config, sectionName):
1327 self._messageType = config.get(sectionName, "type")
1328 if self._messageType not in self.MESSAGE_TYPES:
1329 self._messageType = self.ALL_TYPES
1330 self._messageStatus = config.get(sectionName, "status")
1331 if self._messageStatus not in self.MESSAGE_STATUSES:
1332 self._messageStatus = self.ALL_STATUS
1333 except ConfigParser.NoOptionError:
1336 def save_settings(self, config, sectionName):
1338 @note Thread Agnostic
1340 config.set(sectionName, "status", self._messageStatus)
1341 config.set(sectionName, "type", self._messageType)
1343 def _is_message_visible(self, model, iter):
1345 message = model.get_value(iter, self.MESSAGE_DATA_IDX)
1347 return False # this seems weird but oh well
1348 return self._filter_messages(message, self._messageType, self._messageStatus)
1349 except Exception, e:
1350 self._errorDisplay.push_exception()
1353 def _filter_messages(cls, message, type, status):
1354 if type == cls.ALL_TYPES:
1357 messageType = message["type"]
1358 isType = messageType == type
1360 if status == cls.ALL_STATUS:
1363 isUnarchived = not message["isArchived"]
1364 isUnread = not message["isRead"]
1365 if status == cls.UNREAD_STATUS:
1366 isStatus = isUnarchived and isUnread
1367 elif status == cls.UNARCHIVED_STATUS:
1368 isStatus = isUnarchived
1370 assert "Status %s is bad for %r" % (status, message)
1372 return isType and isStatus
1374 _MIN_MESSAGES_SHOWN = 4
1376 def _idly_populate_messageview(self):
1377 with gtk_toolbox.gtk_lock():
1378 banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1380 self._messagemodel.clear()
1381 self._isPopulated = True
1383 if self._messageType == self.NO_MESSAGES:
1387 messageItems = self._backend.get_messages()
1388 except Exception, e:
1389 self._errorDisplay.push_exception_with_lock()
1390 self._isPopulated = False
1394 (gv_backend.decorate_message(message), message)
1395 for message in gv_backend.sort_messages(messageItems)
1398 for (contactId, header, number, relativeDate, messages), messageData in messageItems:
1399 prettyNumber = number[2:] if number.startswith("+1") else number
1400 prettyNumber = make_pretty(prettyNumber)
1402 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1403 expandedMessages = [firstMessage]
1404 expandedMessages.extend(messages)
1405 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1406 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1407 secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1408 collapsedMessages = [firstMessage, secondMessage]
1409 collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1411 collapsedMessages = expandedMessages
1412 #collapsedMessages = _collapse_message(collapsedMessages, 60, self._MIN_MESSAGES_SHOWN)
1414 number = make_ugly(number)
1416 row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId, messageData
1417 with gtk_toolbox.gtk_lock():
1418 self._messagemodel.append(row)
1419 except Exception, e:
1420 self._errorDisplay.push_exception_with_lock()
1422 with gtk_toolbox.gtk_lock():
1423 hildonize.show_busy_banner_end(banner)
1424 self._messagemodelfiltered.refilter()
1428 def _on_messageview_row_activated(self, treeview, path, view_column):
1430 childPath = self._messagemodelfiltered.convert_path_to_child_path(path)
1431 itr = self._messagemodel.get_iter(childPath)
1435 number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX))
1436 description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1438 contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX)
1439 contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1442 contactPhoneNumbers,
1443 messages = description,
1444 defaultIndex = defaultIndex,
1446 self._messageviewselection.unselect_all()
1447 except Exception, e:
1448 self._errorDisplay.push_exception()
1450 def _on_message_type_clicked(self, *args, **kwds):
1452 selectedIndex = self.MESSAGE_TYPES.index(self._messageType)
1455 newSelectedIndex = hildonize.touch_selector(
1461 except RuntimeError:
1464 if selectedIndex != newSelectedIndex:
1465 self._messageType = self.MESSAGE_TYPES[newSelectedIndex]
1466 self._messageTypeButton.set_label(self._messageType)
1467 self._messagemodelfiltered.refilter()
1468 except Exception, e:
1469 self._errorDisplay.push_exception()
1471 def _on_message_status_clicked(self, *args, **kwds):
1473 selectedIndex = self.MESSAGE_STATUSES.index(self._messageStatus)
1476 newSelectedIndex = hildonize.touch_selector(
1479 self.MESSAGE_STATUSES,
1482 except RuntimeError:
1485 if selectedIndex != newSelectedIndex:
1486 self._messageStatus = self.MESSAGE_STATUSES[newSelectedIndex]
1487 self._messageStatusButton.set_label(self._messageStatus)
1488 self._messagemodelfiltered.refilter()
1489 except Exception, e:
1490 self._errorDisplay.push_exception()
1493 class ContactsView(object):
1495 CONTACT_TYPE_IDX = 0
1496 CONTACT_NAME_IDX = 1
1499 def __init__(self, widgetTree, backend, errorDisplay):
1500 self._errorDisplay = errorDisplay
1501 self._backend = backend
1503 self._addressBook = None
1504 self._selectedComboIndex = 0
1505 self._addressBookFactories = [null_backend.NullAddressBook()]
1507 self._booksList = []
1508 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1510 self._isPopulated = False
1511 self._contactsmodel = gtk.ListStore(
1512 gobject.TYPE_STRING, # Contact Type
1513 gobject.TYPE_STRING, # Contact Name
1514 gobject.TYPE_STRING, # Contact ID
1516 self._contactsviewselection = None
1517 self._contactsview = widgetTree.get_widget("contactsview")
1519 self._contactColumn = gtk.TreeViewColumn("Contact")
1520 displayContactSource = False
1521 if displayContactSource:
1522 textrenderer = gtk.CellRendererText()
1523 self._contactColumn.pack_start(textrenderer, expand=False)
1524 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_TYPE_IDX)
1525 textrenderer = gtk.CellRendererText()
1526 hildonize.set_cell_thumb_selectable(textrenderer)
1527 self._contactColumn.pack_start(textrenderer, expand=True)
1528 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_NAME_IDX)
1529 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1530 self._contactColumn.set_sort_column_id(1)
1531 self._contactColumn.set_visible(True)
1533 self._onContactsviewRowActivatedId = 0
1534 self._onAddressbookButtonChangedId = 0
1535 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1537 self._updateSink = gtk_toolbox.threaded_stage(
1539 self._idly_populate_contactsview,
1540 gtk_toolbox.null_sink(),
1545 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1547 self._contactsview.set_model(self._contactsmodel)
1548 self._contactsview.set_fixed_height_mode(False)
1549 self._contactsview.append_column(self._contactColumn)
1550 self._contactsviewselection = self._contactsview.get_selection()
1551 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1553 del self._booksList[:]
1554 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1555 if factoryName and bookName:
1556 entryName = "%s: %s" % (factoryName, bookName)
1558 entryName = factoryName
1560 entryName = bookName
1562 entryName = "Bad name (%d)" % factoryId
1563 row = (str(factoryId), bookId, entryName)
1564 self._booksList.append(row)
1566 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1567 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1569 if len(self._booksList) <= self._selectedComboIndex:
1570 self._selectedComboIndex = 0
1571 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1573 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1574 selectedBookId = self._booksList[self._selectedComboIndex][1]
1575 self.open_addressbook(selectedFactoryId, selectedBookId)
1578 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1579 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1583 self._bookSelectionButton.set_label("")
1584 self._contactsview.set_model(None)
1585 self._contactsview.remove_column(self._contactColumn)
1587 def add_contact(self, *args, **kwds):
1589 @note Actual dial function is patched in later
1591 raise NotImplementedError("Horrible unknown error has occurred")
1593 def get_addressbooks(self):
1595 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1597 for i, factory in enumerate(self._addressBookFactories):
1598 for bookFactory, bookId, bookName in factory.get_addressbooks():
1599 yield (str(i), bookId), (factory.factory_name(), bookName)
1601 def open_addressbook(self, bookFactoryId, bookId):
1602 bookFactoryIndex = int(bookFactoryId)
1603 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1604 self._addressBook = addressBook
1606 def update(self, force = False):
1607 if not force and self._isPopulated:
1609 self._updateSink.send(())
1613 self._isPopulated = False
1614 self._contactsmodel.clear()
1615 for factory in self._addressBookFactories:
1616 factory.clear_caches()
1617 self._addressBook.clear_caches()
1619 def append(self, book):
1620 self._addressBookFactories.append(book)
1622 def extend(self, books):
1623 self._addressBookFactories.extend(books)
1629 def load_settings(self, config, sectionName):
1631 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1632 except ConfigParser.NoOptionError:
1633 self._selectedComboIndex = 0
1635 def save_settings(self, config, sectionName):
1636 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1638 def _idly_populate_contactsview(self):
1639 with gtk_toolbox.gtk_lock():
1640 banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1643 while addressBook is not self._addressBook:
1644 addressBook = self._addressBook
1645 with gtk_toolbox.gtk_lock():
1646 self._contactsview.set_model(None)
1650 contacts = addressBook.get_contacts()
1651 except Exception, e:
1653 self._isPopulated = False
1654 self._errorDisplay.push_exception_with_lock()
1655 for contactId, contactName in contacts:
1656 contactType = addressBook.contact_source_short_name(contactId)
1657 row = contactType, contactName, contactId
1658 self._contactsmodel.append(row)
1660 with gtk_toolbox.gtk_lock():
1661 self._contactsview.set_model(self._contactsmodel)
1663 self._isPopulated = True
1664 except Exception, e:
1665 self._errorDisplay.push_exception_with_lock()
1667 with gtk_toolbox.gtk_lock():
1668 hildonize.show_busy_banner_end(banner)
1671 def _on_addressbook_button_changed(self, *args, **kwds):
1674 newSelectedComboIndex = hildonize.touch_selector(
1677 (("%s" % m[2]) for m in self._booksList),
1678 self._selectedComboIndex,
1680 except RuntimeError:
1683 selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1684 selectedBookId = self._booksList[newSelectedComboIndex][1]
1686 oldAddressbook = self._addressBook
1687 self.open_addressbook(selectedFactoryId, selectedBookId)
1688 forceUpdate = True if oldAddressbook is not self._addressBook else False
1689 self.update(force=forceUpdate)
1691 self._selectedComboIndex = newSelectedComboIndex
1692 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1693 except Exception, e:
1694 self._errorDisplay.push_exception()
1696 def _on_contactsview_row_activated(self, treeview, path, view_column):
1698 itr = self._contactsmodel.get_iter(path)
1702 contactId = self._contactsmodel.get_value(itr, self.CONTACT_ID_IDX)
1703 contactName = self._contactsmodel.get_value(itr, self.CONTACT_NAME_IDX)
1705 contactDetails = self._addressBook.get_contact_details(contactId)
1706 except Exception, e:
1708 self._errorDisplay.push_exception()
1709 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1711 if len(contactPhoneNumbers) == 0:
1715 contactPhoneNumbers,
1716 messages = (contactName, ),
1718 self._contactsviewselection.unselect_all()
1719 except Exception, e:
1720 self._errorDisplay.push_exception()