4 DialCentral - Front end for Google's Grand Central 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 Feature request: The ability to go to relevant thing in web browser
24 from __future__ import with_statement
36 def make_ugly(prettynumber):
38 function to take a phone number and strip out all non-numeric
41 >>> make_ugly("+012-(345)-678-90")
45 uglynumber = re.sub('\D', '', prettynumber)
49 def make_pretty(phonenumber):
51 Function to take a phone number and return the pretty version
53 if phonenumber begins with 0:
55 if phonenumber begins with 1: ( for gizmo callback numbers )
57 if phonenumber is 13 digits:
59 if phonenumber is 10 digits:
63 >>> make_pretty("1234567")
65 >>> make_pretty("2345678901")
67 >>> make_pretty("12345678901")
69 >>> make_pretty("01234567890")
72 if phonenumber is None or phonenumber is "":
75 phonenumber = make_ugly(phonenumber)
77 if len(phonenumber) < 3:
80 if phonenumber[0] == "0":
82 prettynumber += "+%s" % phonenumber[0:3]
83 if 3 < len(phonenumber):
84 prettynumber += "-(%s)" % phonenumber[3:6]
85 if 6 < len(phonenumber):
86 prettynumber += "-%s" % phonenumber[6:9]
87 if 9 < len(phonenumber):
88 prettynumber += "-%s" % phonenumber[9:]
90 elif len(phonenumber) <= 7:
91 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
92 elif len(phonenumber) > 8 and phonenumber[0] == "1":
93 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
94 elif len(phonenumber) > 7:
95 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
99 class MergedAddressBook(object):
101 Merger of all addressbooks
104 def __init__(self, addressbookFactories, sorter = None):
105 self.__addressbookFactories = addressbookFactories
106 self.__addressbooks = None
107 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
109 def clear_caches(self):
110 self.__addressbooks = None
111 for factory in self.__addressbookFactories:
112 factory.clear_caches()
114 def get_addressbooks(self):
116 @returns Iterable of (Address Book Factory, Book Id, Book Name)
120 def open_addressbook(self, bookId):
123 def contact_source_short_name(self, contactId):
124 if self.__addressbooks is None:
126 bookIndex, originalId = contactId.split("-", 1)
127 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
131 return "All Contacts"
133 def get_contacts(self):
135 @returns Iterable of (contact id, contact name)
137 if self.__addressbooks is None:
138 self.__addressbooks = list(
139 factory.open_addressbook(id)
140 for factory in self.__addressbookFactories
141 for (f, id, name) in factory.get_addressbooks()
144 ("-".join([str(bookIndex), contactId]), contactName)
145 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
146 for (contactId, contactName) in addressbook.get_contacts()
148 sortedContacts = self.__sort_contacts(contacts)
149 return sortedContacts
151 def get_contact_details(self, contactId):
153 @returns Iterable of (Phone Type, Phone Number)
155 if self.__addressbooks is None:
157 bookIndex, originalId = contactId.split("-", 1)
158 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
161 def null_sorter(contacts):
163 Good for speed/low memory
168 def basic_firtname_sorter(contacts):
170 Expects names in "First Last" format
173 (contactName.rsplit(" ", 1)[0], (contactId, contactName))
174 for (contactId, contactName) in contacts
176 contactsWithKey.sort()
177 return (contactData for (lastName, contactData) in contactsWithKey)
180 def basic_lastname_sorter(contacts):
182 Expects names in "First Last" format
185 (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
186 for (contactId, contactName) in contacts
188 contactsWithKey.sort()
189 return (contactData for (lastName, contactData) in contactsWithKey)
192 def reversed_firtname_sorter(contacts):
194 Expects names in "Last, First" format
197 (contactName.split(", ", 1)[-1], (contactId, contactName))
198 for (contactId, contactName) in contacts
200 contactsWithKey.sort()
201 return (contactData for (lastName, contactData) in contactsWithKey)
204 def reversed_lastname_sorter(contacts):
206 Expects names in "Last, First" format
209 (contactName.split(", ", 1)[0], (contactId, contactName))
210 for (contactId, contactName) in contacts
212 contactsWithKey.sort()
213 return (contactData for (lastName, contactData) in contactsWithKey)
216 def guess_firstname(name):
218 return name.split(", ", 1)[-1]
220 return name.rsplit(" ", 1)[0]
223 def guess_lastname(name):
225 return name.split(", ", 1)[0]
227 return name.rsplit(" ", 1)[-1]
230 def advanced_firstname_sorter(cls, contacts):
232 (cls.guess_firstname(contactName), (contactId, contactName))
233 for (contactId, contactName) in contacts
235 contactsWithKey.sort()
236 return (contactData for (lastName, contactData) in contactsWithKey)
239 def advanced_lastname_sorter(cls, contacts):
241 (cls.guess_lastname(contactName), (contactId, contactName))
242 for (contactId, contactName) in contacts
244 contactsWithKey.sort()
245 return (contactData for (lastName, contactData) in contactsWithKey)
248 class PhoneTypeSelector(object):
250 ACTION_CANCEL = "cancel"
251 ACTION_SELECT = "select"
253 ACTION_SEND_SMS = "sms"
255 def __init__(self, widgetTree, gcBackend):
256 self._gcBackend = gcBackend
257 self._widgetTree = widgetTree
259 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
260 self._smsDialog = SmsEntryDialog(self._widgetTree)
262 self._smsButton = self._widgetTree.get_widget("sms_button")
263 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
265 self._dialButton = self._widgetTree.get_widget("dial_button")
266 self._dialButton.connect("clicked", self._on_phonetype_dial)
268 self._selectButton = self._widgetTree.get_widget("select_button")
269 self._selectButton.connect("clicked", self._on_phonetype_select)
271 self._cancelButton = self._widgetTree.get_widget("cancel_button")
272 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
274 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
275 self._typeviewselection = None
277 self._message = self._widgetTree.get_widget("phoneSelectionMessage")
278 self._typeview = self._widgetTree.get_widget("phonetypes")
279 self._typeview.connect("row-activated", self._on_phonetype_select)
281 self._action = self.ACTION_CANCEL
283 def run(self, contactDetails, message = "", parent = None):
284 self._action = self.ACTION_CANCEL
285 self._typemodel.clear()
286 self._typeview.set_model(self._typemodel)
288 # Add the column to the treeview
289 textrenderer = gtk.CellRendererText()
290 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
291 self._typeview.append_column(numberColumn)
293 textrenderer = gtk.CellRendererText()
294 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
295 self._typeview.append_column(typeColumn)
297 self._typeviewselection = self._typeview.get_selection()
298 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
300 for phoneType, phoneNumber in contactDetails:
301 display = " - ".join((phoneNumber, phoneType))
303 row = (phoneNumber, display)
304 self._typemodel.append(row)
306 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
308 self._message.set_markup(message)
311 self._message.set_markup("")
314 if parent is not None:
315 self._dialog.set_transient_for(parent)
318 userResponse = self._dialog.run()
322 if userResponse == gtk.RESPONSE_OK:
323 phoneNumber = self._get_number()
324 phoneNumber = make_ugly(phoneNumber)
328 self._action = self.ACTION_CANCEL
330 if self._action == self.ACTION_SEND_SMS:
331 smsMessage = self._smsDialog.run(phoneNumber, message, parent)
334 self._action = self.ACTION_CANCEL
338 self._typeviewselection.unselect_all()
339 self._typeview.remove_column(numberColumn)
340 self._typeview.remove_column(typeColumn)
341 self._typeview.set_model(None)
343 return self._action, phoneNumber, smsMessage
345 def _get_number(self):
346 model, itr = self._typeviewselection.get_selected()
350 phoneNumber = self._typemodel.get_value(itr, 0)
353 def _on_phonetype_dial(self, *args):
354 self._dialog.response(gtk.RESPONSE_OK)
355 self._action = self.ACTION_DIAL
357 def _on_phonetype_send_sms(self, *args):
358 self._dialog.response(gtk.RESPONSE_OK)
359 self._action = self.ACTION_SEND_SMS
361 def _on_phonetype_select(self, *args):
362 self._dialog.response(gtk.RESPONSE_OK)
363 self._action = self.ACTION_SELECT
365 def _on_phonetype_cancel(self, *args):
366 self._dialog.response(gtk.RESPONSE_CANCEL)
367 self._action = self.ACTION_CANCEL
370 class SmsEntryDialog(object):
373 @todo Add multi-SMS messages like GoogleVoice
378 def __init__(self, widgetTree):
379 self._widgetTree = widgetTree
380 self._dialog = self._widgetTree.get_widget("smsDialog")
382 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
383 self._smsButton.connect("clicked", self._on_send)
385 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
386 self._cancelButton.connect("clicked", self._on_cancel)
388 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
389 self._message = self._widgetTree.get_widget("smsMessage")
390 self._smsEntry = self._widgetTree.get_widget("smsEntry")
391 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
393 def run(self, number, message = "", parent = None):
395 self._message.set_markup(message)
398 self._message.set_markup("")
400 self._smsEntry.get_buffer().set_text("")
401 self._update_letter_count()
403 if parent is not None:
404 self._dialog.set_transient_for(parent)
407 userResponse = self._dialog.run()
411 if userResponse == gtk.RESPONSE_OK:
412 entryBuffer = self._smsEntry.get_buffer()
413 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
414 enteredMessage = enteredMessage[0:self.MAX_CHAR]
418 return enteredMessage.strip()
420 def _update_letter_count(self, *args):
421 entryLength = self._smsEntry.get_buffer().get_char_count()
422 charsLeft = self.MAX_CHAR - entryLength
423 self._letterCountLabel.set_text(str(charsLeft))
425 self._smsButton.set_sensitive(False)
427 self._smsButton.set_sensitive(True)
429 def _on_entry_changed(self, *args):
430 self._update_letter_count()
432 def _on_send(self, *args):
433 self._dialog.response(gtk.RESPONSE_OK)
435 def _on_cancel(self, *args):
436 self._dialog.response(gtk.RESPONSE_CANCEL)
439 class Dialpad(object):
441 def __init__(self, widgetTree, errorDisplay):
442 self._errorDisplay = errorDisplay
443 self._smsDialog = SmsEntryDialog(widgetTree)
445 self._numberdisplay = widgetTree.get_widget("numberdisplay")
446 self._dialButton = widgetTree.get_widget("dial")
447 self._backButton = widgetTree.get_widget("back")
448 self._phonenumber = ""
449 self._prettynumber = ""
452 "on_dial_clicked": self._on_dial_clicked,
453 "on_sms_clicked": self._on_sms_clicked,
454 "on_digit_clicked": self._on_digit_clicked,
455 "on_clear_number": self._on_clear_number,
457 widgetTree.signal_autoconnect(callbackMapping)
459 self._originalLabel = self._backButton.get_label()
460 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
461 self._backTapHandler.on_tap = self._on_backspace
462 self._backTapHandler.on_hold = self._on_clearall
463 self._backTapHandler.on_holding = self._set_clear_button
464 self._backTapHandler.on_cancel = self._reset_back_button
466 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
469 self._dialButton.grab_focus()
470 self._backTapHandler.enable()
473 self._reset_back_button()
474 self._backTapHandler.disable()
476 def number_selected(self, action, number, message):
478 @note Actual dial function is patched in later
480 raise NotImplementedError("Horrible unknown error has occurred")
482 def get_number(self):
483 return self._phonenumber
485 def set_number(self, number):
487 Set the number to dial
490 self._phonenumber = make_ugly(number)
491 self._prettynumber = make_pretty(self._phonenumber)
492 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
494 self._errorDisplay.push_exception()
503 def load_settings(self, config, section):
506 def save_settings(self, config, section):
508 @note Thread Agnostic
512 def _on_sms_clicked(self, widget):
513 action = PhoneTypeSelector.ACTION_SEND_SMS
514 phoneNumber = self.get_number()
516 message = self._smsDialog.run(phoneNumber, "", self._window)
519 action = PhoneTypeSelector.ACTION_CANCEL
521 if action == PhoneTypeSelector.ACTION_CANCEL:
523 self.number_selected(action, phoneNumber, message)
525 def _on_dial_clicked(self, widget):
526 action = PhoneTypeSelector.ACTION_DIAL
527 phoneNumber = self.get_number()
529 self.number_selected(action, phoneNumber, message)
531 def _on_clear_number(self, *args):
534 def _on_digit_clicked(self, widget):
535 self.set_number(self._phonenumber + widget.get_name()[-1])
537 def _on_backspace(self, taps):
538 self.set_number(self._phonenumber[:-taps])
539 self._reset_back_button()
541 def _on_clearall(self, taps):
543 self._reset_back_button()
546 def _set_clear_button(self):
547 self._backButton.set_label("gtk-clear")
549 def _reset_back_button(self):
550 self._backButton.set_label(self._originalLabel)
553 class AccountInfo(object):
555 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
556 self._errorDisplay = errorDisplay
557 self._backend = backend
558 self._isPopulated = False
559 self._alarmHandler = alarmHandler
560 self._notifyOnMissed = False
561 self._notifyOnVoicemail = False
562 self._notifyOnSms = False
564 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
565 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
566 self._callbackCombo = widgetTree.get_widget("callbackcombo")
567 self._onCallbackentryChangedId = 0
569 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
570 self._minutesEntry = widgetTree.get_widget("minutesEntry")
571 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
572 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
573 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
574 self._onNotifyToggled = 0
575 self._onMinutesChanged = 0
576 self._onMissedToggled = 0
577 self._onVoicemailToggled = 0
578 self._onSmsToggled = 0
579 self._applyAlarmTimeoutId = None
581 self._defaultCallback = ""
584 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
586 self._accountViewNumberDisplay.set_use_markup(True)
587 self.set_account_number("")
589 self._callbackList.clear()
590 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
592 if self._alarmHandler is not None:
593 self._minutesEntry.set_range(1, 60)
594 self._minutesEntry.set_increments(1, 5)
596 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
597 self._minutesEntry.set_value(self._alarmHandler.recurrence)
598 self._missedCheckbox.set_active(self._notifyOnMissed)
599 self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
600 self._smsCheckbox.set_active(self._notifyOnSms)
602 self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
603 self._onMinutesChanged = self._minutesEntry.connect("value-changed", self._on_minutes_changed)
604 self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
605 self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
606 self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
608 self._notifyCheckbox.set_sensitive(False)
609 self._minutesEntry.set_sensitive(False)
610 self._missedCheckbox.set_sensitive(False)
611 self._voicemailCheckbox.set_sensitive(False)
612 self._smsCheckbox.set_sensitive(False)
614 self.update(force=True)
617 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
618 self._onCallbackentryChangedId = 0
620 if self._alarmHandler is not None:
621 self._notifyCheckbox.disconnect(self._onNotifyToggled)
622 self._minutesEntry.disconnect(self._onMinutesChanged)
623 self._missedCheckbox.disconnect(self._onNotifyToggled)
624 self._voicemailCheckbox.disconnect(self._onNotifyToggled)
625 self._smsCheckbox.disconnect(self._onNotifyToggled)
626 self._onNotifyToggled = 0
627 self._onMinutesChanged = 0
628 self._onMissedToggled = 0
629 self._onVoicemailToggled = 0
630 self._onSmsToggled = 0
632 self._notifyCheckbox.set_sensitive(True)
633 self._minutesEntry.set_sensitive(True)
634 self._missedCheckbox.set_sensitive(True)
635 self._voicemailCheckbox.set_sensitive(True)
636 self._smsCheckbox.set_sensitive(True)
639 self._callbackList.clear()
641 def get_selected_callback_number(self):
642 return make_ugly(self._callbackCombo.get_child().get_text())
644 def set_account_number(self, number):
646 Displays current account number
648 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
650 def update(self, force = False):
651 if not force and self._isPopulated:
653 self._populate_callback_combo()
654 self.set_account_number(self._backend.get_account_number())
658 self._callbackCombo.get_child().set_text("")
659 self.set_account_number("")
660 self._isPopulated = False
662 def save_everything(self):
663 raise NotImplementedError
667 return "Account Info"
669 def load_settings(self, config, section):
670 self._defaultCallback = config.get(section, "callback")
671 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
672 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
673 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
675 def save_settings(self, config, section):
677 @note Thread Agnostic
679 callback = self.get_selected_callback_number()
680 config.set(section, "callback", callback)
681 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
682 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
683 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
685 def _populate_callback_combo(self):
686 self._isPopulated = True
687 self._callbackList.clear()
689 callbackNumbers = self._backend.get_callback_numbers()
690 except StandardError, e:
691 self._errorDisplay.push_exception()
692 self._isPopulated = False
695 for number, description in callbackNumbers.iteritems():
696 self._callbackList.append((make_pretty(number),))
698 self._callbackCombo.set_model(self._callbackList)
699 self._callbackCombo.set_text_column(0)
700 #callbackNumber = self._backend.get_callback_number()
701 callbackNumber = self._defaultCallback
702 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
704 def _set_callback_number(self, number):
706 if not self._backend.is_valid_syntax(number):
707 self._errorDisplay.push_message("%s is not a valid callback number" % number)
708 elif number == self._backend.get_callback_number():
710 "Callback number already is %s" % (
711 self._backend.get_callback_number(),
717 self._backend.set_callback_number(number)
718 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
719 make_pretty(number), make_pretty(self._backend.get_callback_number())
722 "Callback number set to %s" % (
723 self._backend.get_callback_number(),
727 except StandardError, e:
728 self._errorDisplay.push_exception()
730 def _update_alarm_settings(self):
732 isEnabled = self._notifyCheckbox.get_active()
733 recurrence = self._minutesEntry.get_value_as_int()
734 if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
735 self._alarmHandler.apply_settings(isEnabled, recurrence)
737 self.save_everything()
738 self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
739 self._minutesEntry.set_value(self._alarmHandler.recurrence)
741 def _on_callbackentry_changed(self, *args):
742 text = self.get_selected_callback_number()
743 number = make_ugly(text)
744 self._set_callback_number(number)
746 def _on_notify_toggled(self, *args):
747 if self._applyAlarmTimeoutId is not None:
748 gobject.source_remove(self._applyAlarmTimeoutId)
749 self._applyAlarmTimeoutId = None
750 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
752 def _on_minutes_changed(self, *args):
753 if self._applyAlarmTimeoutId is not None:
754 gobject.source_remove(self._applyAlarmTimeoutId)
755 self._applyAlarmTimeoutId = None
756 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
758 def _on_apply_timeout(self, *args):
759 self._applyAlarmTimeoutId = None
761 self._update_alarm_settings()
764 def _on_missed_toggled(self, *args):
765 self._notifyOnMissed = self._missedCheckbox.get_active()
766 self.save_everything()
768 def _on_voicemail_toggled(self, *args):
769 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
770 self.save_everything()
772 def _on_sms_toggled(self, *args):
773 self._notifyOnSms = self._smsCheckbox.get_active()
774 self.save_everything()
777 class RecentCallsView(object):
784 def __init__(self, widgetTree, backend, errorDisplay):
785 self._errorDisplay = errorDisplay
786 self._backend = backend
788 self._isPopulated = False
789 self._recentmodel = gtk.ListStore(
790 gobject.TYPE_STRING, # number
791 gobject.TYPE_STRING, # date
792 gobject.TYPE_STRING, # action
793 gobject.TYPE_STRING, # from
795 self._recentview = widgetTree.get_widget("recentview")
796 self._recentviewselection = None
797 self._onRecentviewRowActivatedId = 0
799 textrenderer = gtk.CellRendererText()
800 textrenderer.set_property("yalign", 0)
801 self._dateColumn = gtk.TreeViewColumn("Date")
802 self._dateColumn.pack_start(textrenderer, expand=True)
803 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
805 textrenderer = gtk.CellRendererText()
806 textrenderer.set_property("yalign", 0)
807 self._actionColumn = gtk.TreeViewColumn("Action")
808 self._actionColumn.pack_start(textrenderer, expand=True)
809 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
811 textrenderer = gtk.CellRendererText()
812 textrenderer.set_property("yalign", 0)
813 self._fromColumn = gtk.TreeViewColumn("From")
814 self._fromColumn.pack_start(textrenderer, expand=True)
815 self._fromColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
816 self._fromColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
818 self._window = gtk_toolbox.find_parent_window(self._recentview)
819 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
821 self._updateSink = gtk_toolbox.threaded_stage(
823 self._idly_populate_recentview,
824 gtk_toolbox.null_sink(),
829 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
830 self._recentview.set_model(self._recentmodel)
832 self._recentview.append_column(self._dateColumn)
833 self._recentview.append_column(self._actionColumn)
834 self._recentview.append_column(self._fromColumn)
835 self._recentviewselection = self._recentview.get_selection()
836 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
838 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
841 self._recentview.disconnect(self._onRecentviewRowActivatedId)
845 self._recentview.remove_column(self._dateColumn)
846 self._recentview.remove_column(self._actionColumn)
847 self._recentview.remove_column(self._fromColumn)
848 self._recentview.set_model(None)
850 def number_selected(self, action, number, message):
852 @note Actual dial function is patched in later
854 raise NotImplementedError("Horrible unknown error has occurred")
856 def update(self, force = False):
857 if not force and self._isPopulated:
859 self._updateSink.send(())
863 self._isPopulated = False
864 self._recentmodel.clear()
868 return "Recent Calls"
870 def load_settings(self, config, section):
873 def save_settings(self, config, section):
875 @note Thread Agnostic
879 def _idly_populate_recentview(self):
880 self._recentmodel.clear()
881 self._isPopulated = True
884 recentItems = self._backend.get_recent()
885 except StandardError, e:
886 self._errorDisplay.push_exception_with_lock()
887 self._isPopulated = False
890 for personName, phoneNumber, date, action in recentItems:
892 personName = "Unknown"
893 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
894 prettyNumber = make_pretty(prettyNumber)
895 description = "%s - %s" % (personName, prettyNumber)
896 item = (phoneNumber, date, action.capitalize(), description)
897 with gtk_toolbox.gtk_lock():
898 self._recentmodel.append(item)
902 def _on_recentview_row_activated(self, treeview, path, view_column):
903 model, itr = self._recentviewselection.get_selected()
907 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
908 number = make_ugly(number)
909 contactPhoneNumbers = [("Phone", number)]
910 description = self._recentmodel.get_value(itr, self.FROM_IDX)
912 action, phoneNumber, message = self._phoneTypeSelector.run(
914 message = description,
915 parent = self._window,
917 if action == PhoneTypeSelector.ACTION_CANCEL:
919 assert phoneNumber, "A lack of phone number exists"
921 self.number_selected(action, phoneNumber, message)
922 self._recentviewselection.unselect_all()
925 class MessagesView(object):
932 def __init__(self, widgetTree, backend, errorDisplay):
933 self._errorDisplay = errorDisplay
934 self._backend = backend
936 self._isPopulated = False
937 self._messagemodel = gtk.ListStore(
938 gobject.TYPE_STRING, # number
939 gobject.TYPE_STRING, # date
940 gobject.TYPE_STRING, # header
941 gobject.TYPE_STRING, # message
943 self._messageview = widgetTree.get_widget("messages_view")
944 self._messageviewselection = None
945 self._onMessageviewRowActivatedId = 0
947 textrenderer = gtk.CellRendererText()
948 textrenderer.set_property("yalign", 0)
949 self._dateColumn = gtk.TreeViewColumn("Date")
950 self._dateColumn.pack_start(textrenderer, expand=True)
951 self._dateColumn.add_attribute(textrenderer, "markup", self.DATE_IDX)
953 textrenderer = gtk.CellRendererText()
954 textrenderer.set_property("yalign", 0)
955 self._headerColumn = gtk.TreeViewColumn("From")
956 self._headerColumn.pack_start(textrenderer, expand=True)
957 self._headerColumn.add_attribute(textrenderer, "markup", self.HEADER_IDX)
959 textrenderer = gtk.CellRendererText()
960 textrenderer.set_property("yalign", 0)
961 self._messageColumn = gtk.TreeViewColumn("Messages")
962 self._messageColumn.pack_start(textrenderer, expand=True)
963 self._messageColumn.add_attribute(textrenderer, "markup", self.MESSAGE_IDX)
964 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
966 self._window = gtk_toolbox.find_parent_window(self._messageview)
967 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
969 self._updateSink = gtk_toolbox.threaded_stage(
971 self._idly_populate_messageview,
972 gtk_toolbox.null_sink(),
977 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
978 self._messageview.set_model(self._messagemodel)
980 self._messageview.append_column(self._dateColumn)
981 self._messageview.append_column(self._headerColumn)
982 self._messageview.append_column(self._messageColumn)
983 self._messageviewselection = self._messageview.get_selection()
984 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
986 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
989 self._messageview.disconnect(self._onMessageviewRowActivatedId)
993 self._messageview.remove_column(self._dateColumn)
994 self._messageview.remove_column(self._headerColumn)
995 self._messageview.remove_column(self._messageColumn)
996 self._messageview.set_model(None)
998 def number_selected(self, action, number, message):
1000 @note Actual dial function is patched in later
1002 raise NotImplementedError("Horrible unknown error has occurred")
1004 def update(self, force = False):
1005 if not force and self._isPopulated:
1007 self._updateSink.send(())
1011 self._isPopulated = False
1012 self._messagemodel.clear()
1018 def load_settings(self, config, section):
1021 def save_settings(self, config, section):
1023 @note Thread Agnostic
1027 def _idly_populate_messageview(self):
1028 self._messagemodel.clear()
1029 self._isPopulated = True
1032 messageItems = self._backend.get_messages()
1033 except StandardError, e:
1034 self._errorDisplay.push_exception_with_lock()
1035 self._isPopulated = False
1038 for header, number, relativeDate, message in messageItems:
1039 number = make_ugly(number)
1040 row = (number, relativeDate, header, message)
1041 with gtk_toolbox.gtk_lock():
1042 self._messagemodel.append(row)
1046 def _on_messageview_row_activated(self, treeview, path, view_column):
1047 model, itr = self._messageviewselection.get_selected()
1051 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1052 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
1054 action, phoneNumber, message = self._phoneTypeSelector.run(
1055 contactPhoneNumbers,
1056 message = description,
1057 parent = self._window,
1059 if action == PhoneTypeSelector.ACTION_CANCEL:
1061 assert phoneNumber, "A lock of phone number exists"
1063 self.number_selected(action, phoneNumber, message)
1064 self._messageviewselection.unselect_all()
1067 class ContactsView(object):
1069 def __init__(self, widgetTree, backend, errorDisplay):
1070 self._errorDisplay = errorDisplay
1071 self._backend = backend
1073 self._addressBook = None
1074 self._selectedComboIndex = 0
1075 self._addressBookFactories = [null_backend.NullAddressBook()]
1077 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1078 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
1080 self._isPopulated = False
1081 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1082 self._contactsviewselection = None
1083 self._contactsview = widgetTree.get_widget("contactsview")
1085 self._contactColumn = gtk.TreeViewColumn("Contact")
1086 displayContactSource = False
1087 if displayContactSource:
1088 textrenderer = gtk.CellRendererText()
1089 self._contactColumn.pack_start(textrenderer, expand=False)
1090 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1091 textrenderer = gtk.CellRendererText()
1092 self._contactColumn.pack_start(textrenderer, expand=True)
1093 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1094 textrenderer = gtk.CellRendererText()
1095 self._contactColumn.pack_start(textrenderer, expand=True)
1096 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1097 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1098 self._contactColumn.set_sort_column_id(1)
1099 self._contactColumn.set_visible(True)
1101 self._onContactsviewRowActivatedId = 0
1102 self._onAddressbookComboChangedId = 0
1103 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1104 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1106 self._updateSink = gtk_toolbox.threaded_stage(
1108 self._idly_populate_contactsview,
1109 gtk_toolbox.null_sink(),
1114 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1116 self._contactsview.set_model(self._contactsmodel)
1117 self._contactsview.append_column(self._contactColumn)
1118 self._contactsviewselection = self._contactsview.get_selection()
1119 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1121 self._booksList.clear()
1122 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1123 if factoryName and bookName:
1124 entryName = "%s: %s" % (factoryName, bookName)
1126 entryName = factoryName
1128 entryName = bookName
1130 entryName = "Bad name (%d)" % factoryId
1131 row = (str(factoryId), bookId, entryName)
1132 self._booksList.append(row)
1134 self._booksSelectionBox.set_model(self._booksList)
1135 cell = gtk.CellRendererText()
1136 self._booksSelectionBox.pack_start(cell, True)
1137 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1139 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1140 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1142 if len(self._booksList) <= self._selectedComboIndex:
1143 self._selectedComboIndex = 0
1144 self._booksSelectionBox.set_active(self._selectedComboIndex)
1147 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1148 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1152 self._booksSelectionBox.clear()
1153 self._booksSelectionBox.set_model(None)
1154 self._contactsview.set_model(None)
1155 self._contactsview.remove_column(self._contactColumn)
1157 def number_selected(self, action, number, message):
1159 @note Actual dial function is patched in later
1161 raise NotImplementedError("Horrible unknown error has occurred")
1163 def get_addressbooks(self):
1165 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1167 for i, factory in enumerate(self._addressBookFactories):
1168 for bookFactory, bookId, bookName in factory.get_addressbooks():
1169 yield (str(i), bookId), (factory.factory_name(), bookName)
1171 def open_addressbook(self, bookFactoryId, bookId):
1172 bookFactoryIndex = int(bookFactoryId)
1173 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1175 forceUpdate = True if addressBook is not self._addressBook else False
1177 self._addressBook = addressBook
1178 self.update(force=forceUpdate)
1180 def update(self, force = False):
1181 if not force and self._isPopulated:
1183 self._updateSink.send(())
1187 self._isPopulated = False
1188 self._contactsmodel.clear()
1189 for factory in self._addressBookFactories:
1190 factory.clear_caches()
1191 self._addressBook.clear_caches()
1193 def append(self, book):
1194 self._addressBookFactories.append(book)
1196 def extend(self, books):
1197 self._addressBookFactories.extend(books)
1203 def load_settings(self, config, sectionName):
1205 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1206 except ConfigParser.NoOptionError:
1207 self._selectedComboIndex = 0
1209 def save_settings(self, config, sectionName):
1210 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1212 def _idly_populate_contactsview(self):
1214 while addressBook is not self._addressBook:
1215 addressBook = self._addressBook
1216 with gtk_toolbox.gtk_lock():
1217 self._contactsview.set_model(None)
1221 contacts = addressBook.get_contacts()
1222 except StandardError, e:
1224 self._isPopulated = False
1225 self._errorDisplay.push_exception_with_lock()
1226 for contactId, contactName in contacts:
1227 contactType = (addressBook.contact_source_short_name(contactId), )
1228 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1230 with gtk_toolbox.gtk_lock():
1231 self._contactsview.set_model(self._contactsmodel)
1233 self._isPopulated = True
1236 def _on_addressbook_combo_changed(self, *args, **kwds):
1237 itr = self._booksSelectionBox.get_active_iter()
1240 self._selectedComboIndex = self._booksSelectionBox.get_active()
1241 selectedFactoryId = self._booksList.get_value(itr, 0)
1242 selectedBookId = self._booksList.get_value(itr, 1)
1243 self.open_addressbook(selectedFactoryId, selectedBookId)
1245 def _on_contactsview_row_activated(self, treeview, path, view_column):
1246 model, itr = self._contactsviewselection.get_selected()
1250 contactId = self._contactsmodel.get_value(itr, 3)
1251 contactName = self._contactsmodel.get_value(itr, 1)
1253 contactDetails = self._addressBook.get_contact_details(contactId)
1254 except StandardError, e:
1256 self._errorDisplay.push_exception()
1257 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1259 if len(contactPhoneNumbers) == 0:
1262 action, phoneNumber, message = self._phoneTypeSelector.run(
1263 contactPhoneNumbers,
1264 message = contactName,
1265 parent = self._window,
1267 if action == PhoneTypeSelector.ACTION_CANCEL:
1269 assert phoneNumber, "A lack of phone number exists"
1271 self.number_selected(action, phoneNumber, message)
1272 self._contactsviewselection.unselect_all()