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
22 from __future__ import with_statement
35 def make_ugly(prettynumber):
37 function to take a phone number and strip out all non-numeric
40 >>> make_ugly("+012-(345)-678-90")
44 uglynumber = re.sub('\D', '', prettynumber)
48 def make_pretty(phonenumber):
50 Function to take a phone number and return the pretty version
52 if phonenumber begins with 0:
54 if phonenumber begins with 1: ( for gizmo callback numbers )
56 if phonenumber is 13 digits:
58 if phonenumber is 10 digits:
62 >>> make_pretty("1234567")
64 >>> make_pretty("2345678901")
66 >>> make_pretty("12345678901")
68 >>> make_pretty("01234567890")
71 if phonenumber is None or phonenumber is "":
74 phonenumber = make_ugly(phonenumber)
76 if len(phonenumber) < 3:
79 if phonenumber[0] == "0":
81 prettynumber += "+%s" % phonenumber[0:3]
82 if 3 < len(phonenumber):
83 prettynumber += "-(%s)" % phonenumber[3:6]
84 if 6 < len(phonenumber):
85 prettynumber += "-%s" % phonenumber[6:9]
86 if 9 < len(phonenumber):
87 prettynumber += "-%s" % phonenumber[9:]
89 elif len(phonenumber) <= 7:
90 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
91 elif len(phonenumber) > 8 and phonenumber[0] == "1":
92 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
93 elif len(phonenumber) > 7:
94 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
98 class MergedAddressBook(object):
100 Merger of all addressbooks
103 def __init__(self, addressbookFactories, sorter = None):
104 self.__addressbookFactories = addressbookFactories
105 self.__addressbooks = None
106 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
108 def clear_caches(self):
109 self.__addressbooks = None
110 for factory in self.__addressbookFactories:
111 factory.clear_caches()
113 def get_addressbooks(self):
115 @returns Iterable of (Address Book Factory, Book Id, Book Name)
119 def open_addressbook(self, bookId):
122 def contact_source_short_name(self, contactId):
123 if self.__addressbooks is None:
125 bookIndex, originalId = contactId.split("-", 1)
126 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
130 return "All Contacts"
132 def get_contacts(self):
134 @returns Iterable of (contact id, contact name)
136 if self.__addressbooks is None:
137 self.__addressbooks = list(
138 factory.open_addressbook(id)
139 for factory in self.__addressbookFactories
140 for (f, id, name) in factory.get_addressbooks()
143 ("-".join([str(bookIndex), contactId]), contactName)
144 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
145 for (contactId, contactName) in addressbook.get_contacts()
147 sortedContacts = self.__sort_contacts(contacts)
148 return sortedContacts
150 def get_contact_details(self, contactId):
152 @returns Iterable of (Phone Type, Phone Number)
154 if self.__addressbooks is None:
156 bookIndex, originalId = contactId.split("-", 1)
157 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
160 def null_sorter(contacts):
162 Good for speed/low memory
167 def basic_firtname_sorter(contacts):
169 Expects names in "First Last" format
172 (contactName.rsplit(" ", 1)[0], (contactId, contactName))
173 for (contactId, contactName) in contacts
175 contactsWithKey.sort()
176 return (contactData for (lastName, contactData) in contactsWithKey)
179 def basic_lastname_sorter(contacts):
181 Expects names in "First Last" format
184 (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
185 for (contactId, contactName) in contacts
187 contactsWithKey.sort()
188 return (contactData for (lastName, contactData) in contactsWithKey)
191 def reversed_firtname_sorter(contacts):
193 Expects names in "Last, First" format
196 (contactName.split(", ", 1)[-1], (contactId, contactName))
197 for (contactId, contactName) in contacts
199 contactsWithKey.sort()
200 return (contactData for (lastName, contactData) in contactsWithKey)
203 def reversed_lastname_sorter(contacts):
205 Expects names in "Last, First" format
208 (contactName.split(", ", 1)[0], (contactId, contactName))
209 for (contactId, contactName) in contacts
211 contactsWithKey.sort()
212 return (contactData for (lastName, contactData) in contactsWithKey)
215 def guess_firstname(name):
217 return name.split(", ", 1)[-1]
219 return name.rsplit(" ", 1)[0]
222 def guess_lastname(name):
224 return name.split(", ", 1)[0]
226 return name.rsplit(" ", 1)[-1]
229 def advanced_firstname_sorter(cls, contacts):
231 (cls.guess_firstname(contactName), (contactId, contactName))
232 for (contactId, contactName) in contacts
234 contactsWithKey.sort()
235 return (contactData for (lastName, contactData) in contactsWithKey)
238 def advanced_lastname_sorter(cls, contacts):
240 (cls.guess_lastname(contactName), (contactId, contactName))
241 for (contactId, contactName) in contacts
243 contactsWithKey.sort()
244 return (contactData for (lastName, contactData) in contactsWithKey)
247 class PhoneTypeSelector(object):
249 ACTION_CANCEL = "cancel"
250 ACTION_SELECT = "select"
252 ACTION_SEND_SMS = "sms"
254 def __init__(self, widgetTree, gcBackend):
255 self._gcBackend = gcBackend
256 self._widgetTree = widgetTree
258 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
259 self._smsDialog = SmsEntryDialog(self._widgetTree)
261 self._smsButton = self._widgetTree.get_widget("sms_button")
262 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
264 self._dialButton = self._widgetTree.get_widget("dial_button")
265 self._dialButton.connect("clicked", self._on_phonetype_dial)
267 self._selectButton = self._widgetTree.get_widget("select_button")
268 self._selectButton.connect("clicked", self._on_phonetype_select)
270 self._cancelButton = self._widgetTree.get_widget("cancel_button")
271 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
273 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
274 self._typeviewselection = None
276 self._message = self._widgetTree.get_widget("phoneSelectionMessage")
277 self._typeview = self._widgetTree.get_widget("phonetypes")
278 self._typeview.connect("row-activated", self._on_phonetype_select)
280 self._action = self.ACTION_CANCEL
282 def run(self, contactDetails, message = "", parent = None):
283 self._action = self.ACTION_CANCEL
284 self._typemodel.clear()
285 self._typeview.set_model(self._typemodel)
287 # Add the column to the treeview
288 textrenderer = gtk.CellRendererText()
289 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
290 self._typeview.append_column(numberColumn)
292 textrenderer = gtk.CellRendererText()
293 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
294 self._typeview.append_column(typeColumn)
296 self._typeviewselection = self._typeview.get_selection()
297 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
299 for phoneType, phoneNumber in contactDetails:
300 display = " - ".join((phoneNumber, phoneType))
302 row = (phoneNumber, display)
303 self._typemodel.append(row)
305 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
307 self._message.set_markup(message)
310 self._message.set_markup("")
313 if parent is not None:
314 self._dialog.set_transient_for(parent)
317 userResponse = self._dialog.run()
321 if userResponse == gtk.RESPONSE_OK:
322 phoneNumber = self._get_number()
323 phoneNumber = make_ugly(phoneNumber)
327 self._action = self.ACTION_CANCEL
329 if self._action == self.ACTION_SEND_SMS:
330 smsMessage = self._smsDialog.run(phoneNumber, message, parent)
333 self._action = self.ACTION_CANCEL
337 self._typeviewselection.unselect_all()
338 self._typeview.remove_column(numberColumn)
339 self._typeview.remove_column(typeColumn)
340 self._typeview.set_model(None)
342 return self._action, phoneNumber, smsMessage
344 def _get_number(self):
345 model, itr = self._typeviewselection.get_selected()
349 phoneNumber = self._typemodel.get_value(itr, 0)
352 def _on_phonetype_dial(self, *args):
353 self._dialog.response(gtk.RESPONSE_OK)
354 self._action = self.ACTION_DIAL
356 def _on_phonetype_send_sms(self, *args):
357 self._dialog.response(gtk.RESPONSE_OK)
358 self._action = self.ACTION_SEND_SMS
360 def _on_phonetype_select(self, *args):
361 self._dialog.response(gtk.RESPONSE_OK)
362 self._action = self.ACTION_SELECT
364 def _on_phonetype_cancel(self, *args):
365 self._dialog.response(gtk.RESPONSE_CANCEL)
366 self._action = self.ACTION_CANCEL
369 class SmsEntryDialog(object):
372 @todo Add multi-SMS messages like GoogleVoice
377 def __init__(self, widgetTree):
378 self._widgetTree = widgetTree
379 self._dialog = self._widgetTree.get_widget("smsDialog")
381 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
382 self._smsButton.connect("clicked", self._on_send)
384 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
385 self._cancelButton.connect("clicked", self._on_cancel)
387 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
388 self._message = self._widgetTree.get_widget("smsMessage")
389 self._smsEntry = self._widgetTree.get_widget("smsEntry")
390 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
392 def run(self, number, message = "", parent = None):
394 self._message.set_markup(message)
397 self._message.set_markup("")
399 self._smsEntry.get_buffer().set_text("")
400 self._update_letter_count()
402 if parent is not None:
403 self._dialog.set_transient_for(parent)
406 userResponse = self._dialog.run()
410 if userResponse == gtk.RESPONSE_OK:
411 entryBuffer = self._smsEntry.get_buffer()
412 enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
413 enteredMessage = enteredMessage[0:self.MAX_CHAR]
417 return enteredMessage.strip()
419 def _update_letter_count(self, *args):
420 entryLength = self._smsEntry.get_buffer().get_char_count()
421 charsLeft = self.MAX_CHAR - entryLength
422 self._letterCountLabel.set_text(str(charsLeft))
424 self._smsButton.set_sensitive(False)
426 self._smsButton.set_sensitive(True)
428 def _on_entry_changed(self, *args):
429 self._update_letter_count()
431 def _on_send(self, *args):
432 self._dialog.response(gtk.RESPONSE_OK)
434 def _on_cancel(self, *args):
435 self._dialog.response(gtk.RESPONSE_CANCEL)
438 class Dialpad(object):
440 def __init__(self, widgetTree, errorDisplay):
441 self._errorDisplay = errorDisplay
442 self._smsDialog = SmsEntryDialog(widgetTree)
444 self._numberdisplay = widgetTree.get_widget("numberdisplay")
445 self._dialButton = widgetTree.get_widget("dial")
446 self._backButton = widgetTree.get_widget("back")
447 self._phonenumber = ""
448 self._prettynumber = ""
451 "on_dial_clicked": self._on_dial_clicked,
452 "on_sms_clicked": self._on_sms_clicked,
453 "on_digit_clicked": self._on_digit_clicked,
454 "on_clear_number": self._on_clear_number,
456 widgetTree.signal_autoconnect(callbackMapping)
458 self._originalLabel = self._backButton.get_label()
459 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
460 self._backTapHandler.on_tap = self._on_backspace
461 self._backTapHandler.on_hold = self._on_clearall
462 self._backTapHandler.on_holding = self._set_clear_button
463 self._backTapHandler.on_cancel = self._reset_back_button
465 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
468 self.set_number(self._phonenumber)
469 self._dialButton.grab_focus()
470 self._backTapHandler.enable()
473 self._reset_back_button()
474 self._backTapHandler.disable()
477 def number_selected(self, action, number, message):
479 @note Actual dial function is patched in later
481 raise NotImplementedError("Horrible unknown error has occurred")
483 def get_number(self):
484 return self._phonenumber
486 def set_number(self, number):
488 Set the number to dial
491 self._phonenumber = make_ugly(number)
492 self._prettynumber = make_pretty(self._phonenumber)
493 self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
495 self._errorDisplay.push_exception()
504 def load_settings(self, config, section):
507 def save_settings(self, config, section):
509 @note Thread Agnostic
513 def _on_sms_clicked(self, widget):
514 action = PhoneTypeSelector.ACTION_SEND_SMS
515 phoneNumber = self.get_number()
517 message = self._smsDialog.run(phoneNumber, "", self._window)
520 action = PhoneTypeSelector.ACTION_CANCEL
522 if action == PhoneTypeSelector.ACTION_CANCEL:
524 self.number_selected(action, phoneNumber, message)
526 def _on_dial_clicked(self, widget):
527 action = PhoneTypeSelector.ACTION_DIAL
528 phoneNumber = self.get_number()
530 self.number_selected(action, phoneNumber, message)
532 def _on_clear_number(self, *args):
535 def _on_digit_clicked(self, widget):
536 self.set_number(self._phonenumber + widget.get_name()[-1])
538 def _on_backspace(self, taps):
539 self.set_number(self._phonenumber[:-taps])
540 self._reset_back_button()
542 def _on_clearall(self, taps):
544 self._reset_back_button()
547 def _set_clear_button(self):
548 self._backButton.set_label("gtk-clear")
550 def _reset_back_button(self):
551 self._backButton.set_label(self._originalLabel)
554 class AccountInfo(object):
556 def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
557 self._errorDisplay = errorDisplay
558 self._backend = backend
559 self._isPopulated = False
560 self._alarmHandler = alarmHandler
561 self._notifyOnMissed = False
562 self._notifyOnVoicemail = False
563 self._notifyOnSms = False
565 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
566 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
567 self._callbackCombo = widgetTree.get_widget("callbackcombo")
568 self._onCallbackentryChangedId = 0
570 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
571 self._minutesEntry = widgetTree.get_widget("minutesEntry")
572 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
573 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
574 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
575 self._onNotifyToggled = 0
576 self._onMinutesChanged = 0
577 self._onMissedToggled = 0
578 self._onVoicemailToggled = 0
579 self._onSmsToggled = 0
580 self._applyAlarmTimeoutId = None
582 self._defaultCallback = ""
585 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
587 self._accountViewNumberDisplay.set_use_markup(True)
588 self.set_account_number("")
590 self._callbackList.clear()
591 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
593 if self._alarmHandler is not None:
594 self._minutesEntry.set_range(1, 60)
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 textrenderer.set_property("scale", 1.5)
814 self._fromColumn = gtk.TreeViewColumn("From")
815 self._fromColumn.pack_start(textrenderer, expand=True)
816 self._fromColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
817 self._fromColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
819 self._window = gtk_toolbox.find_parent_window(self._recentview)
820 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
822 self._updateSink = gtk_toolbox.threaded_stage(
824 self._idly_populate_recentview,
825 gtk_toolbox.null_sink(),
830 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
831 self._recentview.set_model(self._recentmodel)
833 self._recentview.append_column(self._dateColumn)
834 self._recentview.append_column(self._actionColumn)
835 self._recentview.append_column(self._fromColumn)
836 self._recentviewselection = self._recentview.get_selection()
837 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
839 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
842 self._recentview.disconnect(self._onRecentviewRowActivatedId)
846 self._recentview.remove_column(self._dateColumn)
847 self._recentview.remove_column(self._actionColumn)
848 self._recentview.remove_column(self._fromColumn)
849 self._recentview.set_model(None)
851 def number_selected(self, action, number, message):
853 @note Actual dial function is patched in later
855 raise NotImplementedError("Horrible unknown error has occurred")
857 def update(self, force = False):
858 if not force and self._isPopulated:
860 self._updateSink.send(())
864 self._isPopulated = False
865 self._recentmodel.clear()
869 return "Recent Calls"
871 def load_settings(self, config, section):
874 def save_settings(self, config, section):
876 @note Thread Agnostic
880 def _idly_populate_recentview(self):
881 self._recentmodel.clear()
882 self._isPopulated = True
885 recentItems = self._backend.get_recent()
886 except StandardError, e:
887 self._errorDisplay.push_exception_with_lock()
888 self._isPopulated = False
891 for personName, phoneNumber, date, action in recentItems:
893 personName = "Unknown"
894 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
895 prettyNumber = make_pretty(prettyNumber)
896 description = "%s - %s" % (personName, prettyNumber)
897 item = (phoneNumber, date, action.capitalize(), description)
898 with gtk_toolbox.gtk_lock():
899 self._recentmodel.append(item)
903 def _on_recentview_row_activated(self, treeview, path, view_column):
904 model, itr = self._recentviewselection.get_selected()
908 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
909 number = make_ugly(number)
910 contactPhoneNumbers = [("Phone", number)]
911 description = self._recentmodel.get_value(itr, self.FROM_IDX)
913 action, phoneNumber, message = self._phoneTypeSelector.run(
915 message = description,
916 parent = self._window,
918 if action == PhoneTypeSelector.ACTION_CANCEL:
920 assert phoneNumber, "A lack of phone number exists"
922 self.number_selected(action, phoneNumber, message)
923 self._recentviewselection.unselect_all()
926 class MessagesView(object):
933 def __init__(self, widgetTree, backend, errorDisplay):
934 self._errorDisplay = errorDisplay
935 self._backend = backend
937 self._isPopulated = False
938 self._messagemodel = gtk.ListStore(
939 gobject.TYPE_STRING, # number
940 gobject.TYPE_STRING, # date
941 gobject.TYPE_STRING, # header
942 gobject.TYPE_STRING, # message
944 self._messageview = widgetTree.get_widget("messages_view")
945 self._messageviewselection = None
946 self._onMessageviewRowActivatedId = 0
948 dateRenderer = gtk.CellRendererText()
949 dateRenderer.set_property("yalign", 0)
950 self._dateColumn = gtk.TreeViewColumn("Date")
951 self._dateColumn.pack_start(dateRenderer, expand=True)
952 self._dateColumn.add_attribute(dateRenderer, "markup", self.DATE_IDX)
954 self._nameRenderer = gtk.CellRendererText()
955 self._nameRenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
956 self._nameRenderer.set_property("yalign", 0)
957 self._headerColumn = gtk.TreeViewColumn("From")
958 self._headerColumn.pack_start(self._nameRenderer, expand=True)
959 self._headerColumn.add_attribute(self._nameRenderer, "markup", self.HEADER_IDX)
961 self._messageRenderer = gtk.CellRendererText()
962 self._messageRenderer.set_property("yalign", 0)
963 self._messageRenderer.set_property("scale", 1.5)
964 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
965 self._messageRenderer.set_property("wrap-width", 500)
966 self._messageColumn = gtk.TreeViewColumn("Messages")
967 self._messageColumn.pack_start(self._messageRenderer, expand=True)
968 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
969 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
971 self._window = gtk_toolbox.find_parent_window(self._messageview)
972 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
974 self._updateSink = gtk_toolbox.threaded_stage(
976 self._idly_populate_messageview,
977 gtk_toolbox.null_sink(),
982 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
983 self._messageview.set_model(self._messagemodel)
985 self._messageview.append_column(self._dateColumn)
986 self._messageview.append_column(self._headerColumn)
987 self._messageview.append_column(self._messageColumn)
988 self._messageviewselection = self._messageview.get_selection()
989 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
991 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
994 self._messageview.disconnect(self._onMessageviewRowActivatedId)
998 self._messageview.remove_column(self._dateColumn)
999 self._messageview.remove_column(self._headerColumn)
1000 self._messageview.remove_column(self._messageColumn)
1001 self._messageview.set_model(None)
1003 def number_selected(self, action, number, message):
1005 @note Actual dial function is patched in later
1007 raise NotImplementedError("Horrible unknown error has occurred")
1009 def update(self, force = False):
1010 if not force and self._isPopulated:
1012 self._updateSink.send(())
1016 self._isPopulated = False
1017 self._messagemodel.clear()
1023 def load_settings(self, config, section):
1026 def save_settings(self, config, section):
1028 @note Thread Agnostic
1032 def _idly_populate_messageview(self):
1033 self._messagemodel.clear()
1034 self._isPopulated = True
1037 messageItems = self._backend.get_messages()
1038 except StandardError, e:
1039 self._errorDisplay.push_exception_with_lock()
1040 self._isPopulated = False
1044 combinedHeaderLength = 0
1046 for header, number, relativeDate, message in messageItems:
1047 number = make_ugly(number)
1048 row = (number, relativeDate, header, message)
1049 with gtk_toolbox.gtk_lock():
1050 self._messagemodel.append(row)
1052 maxHeaderLength = max(maxHeaderLength, len(header))
1053 combinedHeaderLength += len(header)
1057 cellWidth = min(int((4 * combinedHeaderLength) / (3 * headerCount)), maxHeaderLength)
1060 with gtk_toolbox.gtk_lock():
1061 self._nameRenderer.set_property("width-chars", cellWidth)
1062 cellPosAndDim = self._messageRenderer.get_size(self._messageview, None)
1063 self._messageRenderer.set_property("wrap-width", cellPosAndDim[2])
1067 def _on_messageview_row_activated(self, treeview, path, view_column):
1068 model, itr = self._messageviewselection.get_selected()
1072 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1073 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
1075 action, phoneNumber, message = self._phoneTypeSelector.run(
1076 contactPhoneNumbers,
1077 message = description,
1078 parent = self._window,
1080 if action == PhoneTypeSelector.ACTION_CANCEL:
1082 assert phoneNumber, "A lock of phone number exists"
1084 self.number_selected(action, phoneNumber, message)
1085 self._messageviewselection.unselect_all()
1088 class ContactsView(object):
1090 def __init__(self, widgetTree, backend, errorDisplay):
1091 self._errorDisplay = errorDisplay
1092 self._backend = backend
1094 self._addressBook = None
1095 self._selectedComboIndex = 0
1096 self._addressBookFactories = [null_backend.NullAddressBook()]
1098 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1099 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
1101 self._isPopulated = False
1102 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1103 self._contactsviewselection = None
1104 self._contactsview = widgetTree.get_widget("contactsview")
1106 self._contactColumn = gtk.TreeViewColumn("Contact")
1107 displayContactSource = False
1108 if displayContactSource:
1109 textrenderer = gtk.CellRendererText()
1110 self._contactColumn.pack_start(textrenderer, expand=False)
1111 self._contactColumn.add_attribute(textrenderer, 'text', 0)
1112 textrenderer = gtk.CellRendererText()
1113 textrenderer.set_property("scale", 1.5)
1114 self._contactColumn.pack_start(textrenderer, expand=True)
1115 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1116 textrenderer = gtk.CellRendererText()
1117 self._contactColumn.pack_start(textrenderer, expand=True)
1118 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1119 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1120 self._contactColumn.set_sort_column_id(1)
1121 self._contactColumn.set_visible(True)
1123 self._onContactsviewRowActivatedId = 0
1124 self._onAddressbookComboChangedId = 0
1125 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1126 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1128 self._updateSink = gtk_toolbox.threaded_stage(
1130 self._idly_populate_contactsview,
1131 gtk_toolbox.null_sink(),
1136 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1138 self._contactsview.set_model(self._contactsmodel)
1139 self._contactsview.append_column(self._contactColumn)
1140 self._contactsviewselection = self._contactsview.get_selection()
1141 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1143 self._booksList.clear()
1144 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1145 if factoryName and bookName:
1146 entryName = "%s: %s" % (factoryName, bookName)
1148 entryName = factoryName
1150 entryName = bookName
1152 entryName = "Bad name (%d)" % factoryId
1153 row = (str(factoryId), bookId, entryName)
1154 self._booksList.append(row)
1156 self._booksSelectionBox.set_model(self._booksList)
1157 cell = gtk.CellRendererText()
1158 self._booksSelectionBox.pack_start(cell, True)
1159 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1161 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1162 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1164 if len(self._booksList) <= self._selectedComboIndex:
1165 self._selectedComboIndex = 0
1166 self._booksSelectionBox.set_active(self._selectedComboIndex)
1169 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1170 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1174 self._booksSelectionBox.clear()
1175 self._booksSelectionBox.set_model(None)
1176 self._contactsview.set_model(None)
1177 self._contactsview.remove_column(self._contactColumn)
1179 def number_selected(self, action, number, message):
1181 @note Actual dial function is patched in later
1183 raise NotImplementedError("Horrible unknown error has occurred")
1185 def get_addressbooks(self):
1187 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1189 for i, factory in enumerate(self._addressBookFactories):
1190 for bookFactory, bookId, bookName in factory.get_addressbooks():
1191 yield (str(i), bookId), (factory.factory_name(), bookName)
1193 def open_addressbook(self, bookFactoryId, bookId):
1194 bookFactoryIndex = int(bookFactoryId)
1195 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1197 forceUpdate = True if addressBook is not self._addressBook else False
1199 self._addressBook = addressBook
1200 self.update(force=forceUpdate)
1202 def update(self, force = False):
1203 if not force and self._isPopulated:
1205 self._updateSink.send(())
1209 self._isPopulated = False
1210 self._contactsmodel.clear()
1211 for factory in self._addressBookFactories:
1212 factory.clear_caches()
1213 self._addressBook.clear_caches()
1215 def append(self, book):
1216 self._addressBookFactories.append(book)
1218 def extend(self, books):
1219 self._addressBookFactories.extend(books)
1225 def load_settings(self, config, sectionName):
1227 self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1228 except ConfigParser.NoOptionError:
1229 self._selectedComboIndex = 0
1231 def save_settings(self, config, sectionName):
1232 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1234 def _idly_populate_contactsview(self):
1236 while addressBook is not self._addressBook:
1237 addressBook = self._addressBook
1238 with gtk_toolbox.gtk_lock():
1239 self._contactsview.set_model(None)
1243 contacts = addressBook.get_contacts()
1244 except StandardError, e:
1246 self._isPopulated = False
1247 self._errorDisplay.push_exception_with_lock()
1248 for contactId, contactName in contacts:
1249 contactType = (addressBook.contact_source_short_name(contactId), )
1250 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1252 with gtk_toolbox.gtk_lock():
1253 self._contactsview.set_model(self._contactsmodel)
1255 self._isPopulated = True
1258 def _on_addressbook_combo_changed(self, *args, **kwds):
1259 itr = self._booksSelectionBox.get_active_iter()
1262 self._selectedComboIndex = self._booksSelectionBox.get_active()
1263 selectedFactoryId = self._booksList.get_value(itr, 0)
1264 selectedBookId = self._booksList.get_value(itr, 1)
1265 self.open_addressbook(selectedFactoryId, selectedBookId)
1267 def _on_contactsview_row_activated(self, treeview, path, view_column):
1268 model, itr = self._contactsviewselection.get_selected()
1272 contactId = self._contactsmodel.get_value(itr, 3)
1273 contactName = self._contactsmodel.get_value(itr, 1)
1275 contactDetails = self._addressBook.get_contact_details(contactId)
1276 except StandardError, e:
1278 self._errorDisplay.push_exception()
1279 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1281 if len(contactPhoneNumbers) == 0:
1284 action, phoneNumber, message = self._phoneTypeSelector.run(
1285 contactPhoneNumbers,
1286 message = contactName,
1287 parent = self._window,
1289 if action == PhoneTypeSelector.ACTION_CANCEL:
1291 assert phoneNumber, "A lack of phone number exists"
1293 self.number_selected(action, phoneNumber, message)
1294 self._contactsviewselection.unselect_all()