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 callback phonenumber
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(e)
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, errorDisplay):
556 self._errorDisplay = errorDisplay
557 self._backend = backend
558 self._isPopulated = False
560 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
561 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
562 self._callbackCombo = widgetTree.get_widget("callbackcombo")
563 self._onCallbackentryChangedId = 0
565 self._defaultCallback = ""
568 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
569 self._accountViewNumberDisplay.set_use_markup(True)
570 self.set_account_number("")
571 self._callbackList.clear()
572 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
573 self.update(force=True)
576 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
580 self._callbackList.clear()
582 def get_selected_callback_number(self):
583 return make_ugly(self._callbackCombo.get_child().get_text())
585 def set_account_number(self, number):
587 Displays current account number
589 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
591 def update(self, force = False):
592 if not force and self._isPopulated:
594 self._populate_callback_combo()
595 self.set_account_number(self._backend.get_account_number())
598 self._callbackCombo.get_child().set_text("")
599 self.set_account_number("")
600 self._isPopulated = False
604 return "Account Info"
606 def load_settings(self, config, section):
607 self._defaultCallback = config.get(section, "callback")
609 def save_settings(self, config, section):
611 @note Thread Agnostic
613 callback = self.get_selected_callback_number()
614 config.set(section, "callback", callback)
616 def _populate_callback_combo(self):
617 self._isPopulated = True
618 self._callbackList.clear()
620 callbackNumbers = self._backend.get_callback_numbers()
621 except StandardError, e:
622 self._errorDisplay.push_exception(e)
623 self._isPopulated = False
626 for number, description in callbackNumbers.iteritems():
627 self._callbackList.append((make_pretty(number),))
629 self._callbackCombo.set_model(self._callbackList)
630 self._callbackCombo.set_text_column(0)
631 #callbackNumber = self._backend.get_callback_number()
632 callbackNumber = self._defaultCallback
633 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
635 def _set_callback_number(self, number):
637 if not self._backend.is_valid_syntax(number):
638 self._errorDisplay.push_message("%s is not a valid callback number" % number)
639 elif number == self._backend.get_callback_number():
641 "Callback number already is %s" % (
642 self._backend.get_callback_number(),
648 self._backend.set_callback_number(number)
649 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
650 make_pretty(number), make_pretty(self._backend.get_callback_number())
653 "Callback number set to %s" % (
654 self._backend.get_callback_number(),
658 except StandardError, e:
659 self._errorDisplay.push_exception(e)
661 def _on_callbackentry_changed(self, *args):
662 text = self.get_selected_callback_number()
663 number = make_ugly(text)
664 self._set_callback_number(number)
667 class RecentCallsView(object):
674 def __init__(self, widgetTree, backend, errorDisplay):
675 self._errorDisplay = errorDisplay
676 self._backend = backend
678 self._isPopulated = False
679 self._recentmodel = gtk.ListStore(
680 gobject.TYPE_STRING, # number
681 gobject.TYPE_STRING, # date
682 gobject.TYPE_STRING, # action
683 gobject.TYPE_STRING, # from
685 self._recentview = widgetTree.get_widget("recentview")
686 self._recentviewselection = None
687 self._onRecentviewRowActivatedId = 0
689 textrenderer = gtk.CellRendererText()
690 textrenderer.set_property("yalign", 0)
691 self._dateColumn = gtk.TreeViewColumn("Date")
692 self._dateColumn.pack_start(textrenderer, expand=True)
693 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
695 textrenderer = gtk.CellRendererText()
696 textrenderer.set_property("yalign", 0)
697 self._actionColumn = gtk.TreeViewColumn("Action")
698 self._actionColumn.pack_start(textrenderer, expand=True)
699 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
701 textrenderer = gtk.CellRendererText()
702 textrenderer.set_property("yalign", 0)
703 self._fromColumn = gtk.TreeViewColumn("From")
704 self._fromColumn.pack_start(textrenderer, expand=True)
705 self._fromColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
706 self._fromColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
708 self._window = gtk_toolbox.find_parent_window(self._recentview)
709 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
711 self._updateSink = gtk_toolbox.threaded_stage(
713 self._idly_populate_recentview,
714 gtk_toolbox.null_sink(),
719 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
720 self._recentview.set_model(self._recentmodel)
722 self._recentview.append_column(self._dateColumn)
723 self._recentview.append_column(self._actionColumn)
724 self._recentview.append_column(self._fromColumn)
725 self._recentviewselection = self._recentview.get_selection()
726 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
728 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
731 self._recentview.disconnect(self._onRecentviewRowActivatedId)
735 self._recentview.remove_column(self._dateColumn)
736 self._recentview.remove_column(self._actionColumn)
737 self._recentview.remove_column(self._fromColumn)
738 self._recentview.set_model(None)
740 def number_selected(self, action, number, message):
742 @note Actual dial function is patched in later
744 raise NotImplementedError("Horrible unknown error has occurred")
746 def update(self, force = False):
747 if not force and self._isPopulated:
749 self._updateSink.send(())
752 self._isPopulated = False
753 self._recentmodel.clear()
757 return "Recent Calls"
759 def load_settings(self, config, section):
762 def save_settings(self, config, section):
764 @note Thread Agnostic
768 def _idly_populate_recentview(self):
769 self._recentmodel.clear()
770 self._isPopulated = True
773 recentItems = self._backend.get_recent()
774 except StandardError, e:
775 self._errorDisplay.push_exception_with_lock(e)
776 self._isPopulated = False
779 for personName, phoneNumber, date, action in recentItems:
781 personName = "Unknown"
782 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
783 prettyNumber = make_pretty(prettyNumber)
784 description = "%s - %s" % (personName, prettyNumber)
785 item = (phoneNumber, date, action.capitalize(), description)
786 with gtk_toolbox.gtk_lock():
787 self._recentmodel.append(item)
791 def _on_recentview_row_activated(self, treeview, path, view_column):
792 model, itr = self._recentviewselection.get_selected()
796 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
797 number = make_ugly(number)
798 contactPhoneNumbers = [("Phone", number)]
799 description = self._recentmodel.get_value(itr, self.FROM_IDX)
801 action, phoneNumber, message = self._phoneTypeSelector.run(
803 message = description,
804 parent = self._window,
806 if action == PhoneTypeSelector.ACTION_CANCEL:
808 assert phoneNumber, "A lack of phone number exists"
810 self.number_selected(action, phoneNumber, message)
811 self._recentviewselection.unselect_all()
814 class MessagesView(object):
821 def __init__(self, widgetTree, backend, errorDisplay):
822 self._errorDisplay = errorDisplay
823 self._backend = backend
825 self._isPopulated = False
826 self._messagemodel = gtk.ListStore(
827 gobject.TYPE_STRING, # number
828 gobject.TYPE_STRING, # date
829 gobject.TYPE_STRING, # header
830 gobject.TYPE_STRING, # message
832 self._messageview = widgetTree.get_widget("messages_view")
833 self._messageviewselection = None
834 self._onMessageviewRowActivatedId = 0
836 textrenderer = gtk.CellRendererText()
837 textrenderer.set_property("yalign", 0)
838 self._dateColumn = gtk.TreeViewColumn("Date")
839 self._dateColumn.pack_start(textrenderer, expand=True)
840 self._dateColumn.add_attribute(textrenderer, "markup", self.DATE_IDX)
842 textrenderer = gtk.CellRendererText()
843 textrenderer.set_property("yalign", 0)
844 self._headerColumn = gtk.TreeViewColumn("From")
845 self._headerColumn.pack_start(textrenderer, expand=True)
846 self._headerColumn.add_attribute(textrenderer, "markup", self.HEADER_IDX)
848 textrenderer = gtk.CellRendererText()
849 textrenderer.set_property("yalign", 0)
850 self._messageColumn = gtk.TreeViewColumn("Messages")
851 self._messageColumn.pack_start(textrenderer, expand=True)
852 self._messageColumn.add_attribute(textrenderer, "markup", self.MESSAGE_IDX)
853 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
855 self._window = gtk_toolbox.find_parent_window(self._messageview)
856 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
858 self._updateSink = gtk_toolbox.threaded_stage(
860 self._idly_populate_messageview,
861 gtk_toolbox.null_sink(),
866 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
867 self._messageview.set_model(self._messagemodel)
869 self._messageview.append_column(self._dateColumn)
870 self._messageview.append_column(self._headerColumn)
871 self._messageview.append_column(self._messageColumn)
872 self._messageviewselection = self._messageview.get_selection()
873 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
875 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
878 self._messageview.disconnect(self._onMessageviewRowActivatedId)
882 self._messageview.remove_column(self._dateColumn)
883 self._messageview.remove_column(self._headerColumn)
884 self._messageview.remove_column(self._messageColumn)
885 self._messageview.set_model(None)
887 def number_selected(self, action, number, message):
889 @note Actual dial function is patched in later
891 raise NotImplementedError("Horrible unknown error has occurred")
893 def update(self, force = False):
894 if not force and self._isPopulated:
896 self._updateSink.send(())
899 self._isPopulated = False
900 self._messagemodel.clear()
906 def load_settings(self, config, section):
909 def save_settings(self, config, section):
911 @note Thread Agnostic
915 def _idly_populate_messageview(self):
916 self._messagemodel.clear()
917 self._isPopulated = True
920 messageItems = self._backend.get_messages()
921 except StandardError, e:
922 self._errorDisplay.push_exception_with_lock(e)
923 self._isPopulated = False
926 for header, number, relativeDate, message in messageItems:
927 number = make_ugly(number)
928 row = (number, relativeDate, header, message)
929 with gtk_toolbox.gtk_lock():
930 self._messagemodel.append(row)
934 def _on_messageview_row_activated(self, treeview, path, view_column):
935 model, itr = self._messageviewselection.get_selected()
939 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
940 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
942 action, phoneNumber, message = self._phoneTypeSelector.run(
944 message = description,
945 parent = self._window,
947 if action == PhoneTypeSelector.ACTION_CANCEL:
949 assert phoneNumber, "A lock of phone number exists"
951 self.number_selected(action, phoneNumber, message)
952 self._messageviewselection.unselect_all()
955 class ContactsView(object):
957 def __init__(self, widgetTree, backend, errorDisplay):
958 self._errorDisplay = errorDisplay
959 self._backend = backend
961 self._addressBook = None
962 self._addressBookFactories = [null_backend.NullAddressBook()]
964 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
965 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
967 self._isPopulated = False
968 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
969 self._contactsviewselection = None
970 self._contactsview = widgetTree.get_widget("contactsview")
972 self._contactColumn = gtk.TreeViewColumn("Contact")
973 displayContactSource = False
974 if displayContactSource:
975 textrenderer = gtk.CellRendererText()
976 self._contactColumn.pack_start(textrenderer, expand=False)
977 self._contactColumn.add_attribute(textrenderer, 'text', 0)
978 textrenderer = gtk.CellRendererText()
979 self._contactColumn.pack_start(textrenderer, expand=True)
980 self._contactColumn.add_attribute(textrenderer, 'text', 1)
981 textrenderer = gtk.CellRendererText()
982 self._contactColumn.pack_start(textrenderer, expand=True)
983 self._contactColumn.add_attribute(textrenderer, 'text', 4)
984 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
985 self._contactColumn.set_sort_column_id(1)
986 self._contactColumn.set_visible(True)
988 self._onContactsviewRowActivatedId = 0
989 self._onAddressbookComboChangedId = 0
990 self._window = gtk_toolbox.find_parent_window(self._contactsview)
991 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
993 self._updateSink = gtk_toolbox.threaded_stage(
995 self._idly_populate_contactsview,
996 gtk_toolbox.null_sink(),
1001 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1003 self._contactsview.set_model(self._contactsmodel)
1004 self._contactsview.append_column(self._contactColumn)
1005 self._contactsviewselection = self._contactsview.get_selection()
1006 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1008 self._booksList.clear()
1009 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1010 if factoryName and bookName:
1011 entryName = "%s: %s" % (factoryName, bookName)
1013 entryName = factoryName
1015 entryName = bookName
1017 entryName = "Bad name (%d)" % factoryId
1018 row = (str(factoryId), bookId, entryName)
1019 self._booksList.append(row)
1021 self._booksSelectionBox.set_model(self._booksList)
1022 cell = gtk.CellRendererText()
1023 self._booksSelectionBox.pack_start(cell, True)
1024 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1025 self._booksSelectionBox.set_active(0)
1027 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1028 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1031 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1032 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1036 self._booksSelectionBox.clear()
1037 self._booksSelectionBox.set_model(None)
1038 self._contactsview.set_model(None)
1039 self._contactsview.remove_column(self._contactColumn)
1041 def number_selected(self, action, number, message):
1043 @note Actual dial function is patched in later
1045 raise NotImplementedError("Horrible unknown error has occurred")
1047 def get_addressbooks(self):
1049 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1051 for i, factory in enumerate(self._addressBookFactories):
1052 for bookFactory, bookId, bookName in factory.get_addressbooks():
1053 yield (i, bookId), (factory.factory_name(), bookName)
1055 def open_addressbook(self, bookFactoryId, bookId):
1056 self._addressBook = self._addressBookFactories[bookFactoryId].open_addressbook(bookId)
1057 self.update(force=True)
1059 def update(self, force = False):
1060 if not force and self._isPopulated:
1062 self._updateSink.send(())
1065 self._isPopulated = False
1066 self._contactsmodel.clear()
1067 for factory in self._addressBookFactories:
1068 factory.clear_caches()
1069 self._addressBook.clear_caches()
1071 def append(self, book):
1072 self._addressBookFactories.append(book)
1074 def extend(self, books):
1075 self._addressBookFactories.extend(books)
1081 def load_settings(self, config, section):
1084 def save_settings(self, config, section):
1086 @note Thread Agnostic
1090 def _idly_populate_contactsview(self):
1092 self._isPopulated = True
1094 # completely disable updating the treeview while we populate the data
1095 self._contactsview.freeze_child_notify()
1097 self._contactsview.set_model(None)
1099 addressBook = self._addressBook
1101 contacts = addressBook.get_contacts()
1102 except StandardError, e:
1104 self._isPopulated = False
1105 self._errorDisplay.push_exception_with_lock(e)
1106 for contactId, contactName in contacts:
1107 contactType = (addressBook.contact_source_short_name(contactId), )
1108 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1110 # restart the treeview data rendering
1111 self._contactsview.set_model(self._contactsmodel)
1113 self._contactsview.thaw_child_notify()
1116 def _on_addressbook_combo_changed(self, *args, **kwds):
1117 itr = self._booksSelectionBox.get_active_iter()
1120 factoryId = int(self._booksList.get_value(itr, 0))
1121 bookId = self._booksList.get_value(itr, 1)
1122 self.open_addressbook(factoryId, bookId)
1124 def _on_contactsview_row_activated(self, treeview, path, view_column):
1125 model, itr = self._contactsviewselection.get_selected()
1129 contactId = self._contactsmodel.get_value(itr, 3)
1130 contactName = self._contactsmodel.get_value(itr, 1)
1132 contactDetails = self._addressBook.get_contact_details(contactId)
1133 except StandardError, e:
1135 self._errorDisplay.push_exception(e)
1136 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1138 if len(contactPhoneNumbers) == 0:
1141 action, phoneNumber, message = self._phoneTypeSelector.run(
1142 contactPhoneNumbers,
1143 message = contactName,
1144 parent = self._window,
1146 if action == PhoneTypeSelector.ACTION_CANCEL:
1148 assert phoneNumber, "A lack of phone number exists"
1150 self.number_selected(action, phoneNumber, message)
1151 self._contactsviewselection.unselect_all()