""" PreferencesDialog.py Module setting user preferences. """ __copyright__ = "Copyright (c) 2002-2005 Free Software Foundation, Inc." __license__ = """ GNU General Public License Straw is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Straw is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. """ import pygtk pygtk.require('2.0') import gobject import gtk import pango from gtk import glade import locale import error import MVP from weakref import WeakKeyDictionary import Event import FeedCategoryList import FeedList import Config import ValueMonitor class CategoryListView(MVP.WidgetView): """View object for the category tree view""" COLUMN_TITLE = 0 COLUMN_OBJ = 1 COLUMN_BOLD = 2 COLUMN_EDITABLE = 3 def _initialize(self): treemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_PYOBJECT, gobject.TYPE_INT, gobject.TYPE_BOOLEAN) treemodel.set_sort_func(self.COLUMN_TITLE, self._sort_feeds) treemodel.set_sort_column_id(self.COLUMN_TITLE, gtk.SORT_ASCENDING) self._widget.set_model(treemodel) self._widget.set_rules_hint(False) self._create_columns() self._widget.get_selection().connect( 'changed', self._selection_changed, self.COLUMN_OBJ) def _sort_feeds(self, model, a, b): """ From the gtk.TreeSortable.set_sort_func doc: The comparison callback should return -1 if the iter1 row should come before the iter2 row, 0 if the rows are equal, or 1 if the iter1 row should come after the iter2 row. """ retval = 0 feed_a = model.get_value(a, self.COLUMN_OBJ) feed_b = model.get_value(b, self.COLUMN_OBJ) if feed_a in self._model.pseudo_categories: retval = -1 elif feed_b in self._model.pseudo_categories: retval = 1 elif feed_a is not None and feed_b is not None: retval = locale.strcoll(feed_a.title, feed_b.title) elif feed_a is not None: retval = -1 elif feed_b is not None: retval = 1 return retval def _model_set(self): self._model.signal_connect(Event.FeedCategoryAddedSignal, self._category_added) self._model.signal_connect(Event.FeedCategoryRemovedSignal, self._category_removed) self._model.signal_connect(Event.FeedCategoryChangedSignal, self._category_changed) self._model.signal_connect(Event.FeedCategoryListLoadedSignal, self._categories_loaded) self._populate_treemodel() def _create_columns(self): renderer = gtk.CellRendererText() renderer.connect('edited', self._cell_edited) column = gtk.TreeViewColumn(_('_Category'), renderer, text=self.COLUMN_TITLE, weight=self.COLUMN_BOLD, editable=self.COLUMN_EDITABLE) self._widget.append_column(column) def _populate_treemodel(self): treemodel = self._widget.get_model() treemodel.clear() for category in self._model.pseudo_categories: iter = treemodel.append() treemodel.set(iter, self.COLUMN_TITLE, category.title, self.COLUMN_OBJ, category, self.COLUMN_BOLD, pango.WEIGHT_BOLD, self.COLUMN_EDITABLE, False) for category in self._model.user_categories: iter = treemodel.append() treemodel.set(iter, self.COLUMN_TITLE, category.title, self.COLUMN_OBJ, category, self.COLUMN_BOLD, pango.WEIGHT_NORMAL, self.COLUMN_EDITABLE, True) def _iterate_model_conditionally(self, func): treemodel = self._widget.get_model() iter = treemodel.get_iter_first() while iter is not None: if func(treemodel, iter): break iter = treemodel.iter_next(iter) def _add_category(self, category): self._populate_treemodel() def edit_row(treemodel, iter): if treemodel.get_value(iter, self.COLUMN_OBJ) is category: column = self._widget.get_column(self.COLUMN_TITLE) path = treemodel.get_path(iter) self._widget.set_cursor(path, column, True) return True self._iterate_model_conditionally(edit_row) def _remove_category(self, category): def remove_row(treemodel, iter): if treemodel.get_value(iter, self.COLUMN_OBJ) is category: column = self._widget.get_column(self.COLUMN_TITLE) iterpath = treemodel.get_path(iter) isIter = treemodel.remove(iter) if not isIter: current = iterpath[:-1] + (iterpath[-1]-1,) else: current = treemodel.get_path(iter) self._widget.set_cursor(current, column) return True return False self._widget.grab_focus() self._iterate_model_conditionally(remove_row) def _update_category_name(self, category): def update_title(treemodel, iter): if treemodel.get_value(iter, self.COLUMN_OBJ) is category: treemodel.set(iter, self.COLUMN_TITLE, category.title) return True self._iterate_model_conditionally(update_title) def _category_added(self, signal): self._add_category(signal.category) def _category_removed(self, signal): self._remove_category(signal.category) def _category_changed(self, signal): self._update_category_name(signal.sender) def _categories_loaded(self, signal): self._populate_treemodel() def _selection_changed(self, selection, *args): if self._presenter is None: error.log("presenter not set!") return model, iter = selection.get_selected() if iter is None: return category = model.get_value(iter, self.COLUMN_OBJ) self._presenter.category_changed(category) def _cell_edited(self, cell, path_string, text): treemodel = self._widget.get_model() iter = treemodel.get_iter_from_string(path_string) if not iter: return category = treemodel.get_value(iter, self.COLUMN_OBJ) self._presenter.category_title_edited(category, text) class CategorySelectionChangedSignal(Event.BaseSignal): def __init__(self, sender, category): Event.BaseSignal.__init__(self, sender) self.category = category class CategoryListPresenter(MVP.BasicPresenter): """Presenter object for the category tree view""" def _initialize(self): self.initialize_slots(CategorySelectionChangedSignal) self._selected_category = None def category_changed(self, category): self._selected_category = category self.emit_signal(CategorySelectionChangedSignal(self, category)) def category_title_edited(self, category, title): category.title = title def add_category(self): cat = FeedCategoryList.FeedCategory(_("Category name")) self._model.add_category(cat) def remove_category(self): if self._selected_category is not None: self._model.remove_category(self._selected_category) class CategoryAddView(MVP.WidgetView): """View object for the add category button""" def _on_category_add_button_clicked(self, *args): if self._presenter is not None: self._presenter.add_category() class AddCategorySignal(Event.BaseSignal): pass class CategoryAddPresenter(MVP.BasicPresenter): """Presenter object for the add category button""" def _initialize(self): self.initialize_slots(AddCategorySignal) def add_category(self): self.emit_signal(AddCategorySignal(self)) class CategoryRemoveView(MVP.WidgetView): """View object for the remove category button""" def _on_category_delete_button_clicked(self, *args): if self._presenter is not None: self._presenter.remove_category() class RemoveCategorySignal(Event.BaseSignal): pass class CategoryRemovePresenter(MVP.BasicPresenter): """Presenter object for the remove category button""" def _initialize(self): self.initialize_slots(RemoveCategorySignal) def remove_category(self): self.emit_signal(RemoveCategorySignal(self)) class CategoriesPresenter(object, Event.SignalEmitter): """Presenter object that combines the category list presenter and category add button presenter""" def __init__(self, lister, adder, remover): Event.SignalEmitter.__init__(self) self.initialize_slots(CategorySelectionChangedSignal) lister.signal_connect(CategorySelectionChangedSignal, lambda s: self.emit_signal(s)) adder.signal_connect(AddCategorySignal, self._add_category) remover.signal_connect(RemoveCategorySignal, self._remove_category) self._lister = lister self._adder = adder self._remover = remover def _add_category(self, signal): self._lister.add_category() def _remove_category(self, signal): self._lister.remove_category() class FeedListView(MVP.WidgetView): """View object for the feed list""" COLUMN_TITLE = 0 COLUMN_OBJ = 1 COLUMN_CHECKED = 2 COLUMN_TOGGLABLE = 3 def _initialize(self): treemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_PYOBJECT, gobject.TYPE_BOOLEAN, gobject.TYPE_BOOLEAN) treemodel.set_sort_func(self.COLUMN_TITLE, self._sort_feeds) treemodel.set_sort_column_id(self.COLUMN_TITLE, gtk.SORT_ASCENDING) self._widget.set_model(treemodel) self._widget.set_rules_hint(False) self._create_columns() self._category = None self._updating_display = False def _model_set(self): self._model.signal_connect(Event.FeedsChangedSignal, self._model_updated) fclist = FeedCategoryList.get_instance() self._populate_treemodel() def _create_columns(self): renderer = gtk.CellRendererToggle() renderer.connect('toggled', self._cell_toggled) column = gtk.TreeViewColumn(_('_Member'), renderer, active=self.COLUMN_CHECKED, activatable=self.COLUMN_TOGGLABLE) column.set_sort_column_id(self.COLUMN_CHECKED) self._widget.append_column(column) renderer = gtk.CellRendererText() column = gtk.TreeViewColumn(_('_Feed'), renderer, text=self.COLUMN_TITLE) column.set_sort_column_id(self.COLUMN_TITLE) self._widget.append_column(column) def _populate_treemodel(self): treemodel = self._widget.get_model() treemodel.clear() feedlist = FeedList.get_instance() if self._category is None: return category_feeds = self._category.feeds enabletoggle = not isinstance( self._category, FeedCategoryList.PseudoCategory) for feed in feedlist: iter = treemodel.append() treemodel.set(iter, self.COLUMN_TITLE, feed.title, self.COLUMN_OBJ, feed, self.COLUMN_CHECKED, feed in category_feeds, self.COLUMN_TOGGLABLE, enabletoggle) def _sort_feeds(self, model, a, b): feed_a = model.get_value(a, self.COLUMN_OBJ) feed_b = model.get_value(b, self.COLUMN_OBJ) if feed_a and feed_b : return locale.strcoll(feed_a.title.lower(), feed_b.title.lower()) elif feed_a is not None: return -1 elif feed_b is not None: return 1 else: return 0 def _model_updated(self, signal): self._populate_treemodel() def category_selected(self, category): self._category = category self._populate_treemodel() return def _cell_toggled(self, cell, path_string): if self._updating_display: return treemodel = self._widget.get_model() treeiter = treemodel.get_iter_from_string(path_string) if not treeiter: return feed = treemodel.get_value(treeiter, self.COLUMN_OBJ) self._presenter.feed_toggled(feed) self._populate_treemodel() class FeedListPresenter(MVP.BasicPresenter): """Presenter object for the feed list""" def feed_toggled(self, feed): if self._category is None: return if feed in self._category.feeds: self._category.remove_feed(feed) else: self._category.append_feed(feed, False) def category_selected(self, category): self._category = category if self._view is not None: self._view.category_selected(category) class CategoriesTab: def __init__(self, xml): self._selcategory = None # currently selected category self._delete_button = xml.get_widget('category_delete_button') self._delete_button.set_sensitive(False) self._selcategory = None clpresenter = CategoryListPresenter( model = FeedCategoryList.get_instance(), view = CategoryListView(xml.get_widget('category_treeview'))) capresenter = CategoryAddPresenter( view = CategoryAddView(xml.get_widget('category_add_button'))) crpresenter = CategoryRemovePresenter( view = CategoryRemoveView( xml.get_widget('category_delete_button'))) self._categories_presenter = CategoriesPresenter( clpresenter, capresenter, crpresenter) self._categories_presenter.signal_connect( CategorySelectionChangedSignal, self._category_changed) feedmodel = FeedList.get_instance() self._feeds_presenter = FeedListPresenter() self._feeds_presenter.model = feedmodel self._feeds_presenter.view = FeedListView( xml.get_widget('feeds_treeview')) self._source_field = xml.get_widget( 'category_external_source') self._source_username_field = xml.get_widget( 'category_external_source_username') self._source_password_field = xml.get_widget( 'category_external_source_password') self._source_frequency_spin = xml.get_widget( 'category_external_source_frequency_spin') self._source_frequency_default_check = xml.get_widget( 'category_refresh_default_check') sizegroup = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL) for l in [xml.get_widget(w) for w in ( 'category_external_source_label', 'category_external_source_username_label', 'category_external_source_password_label', 'category_external_source_frequency_label')]: sizegroup.add_widget(l) self._category_source_monitors = CategorySourceMonitors() nameFuncMap = {} for key in dir(self.__class__): if key[:4] == '_on_': nameFuncMap[key[1:]] = getattr(self, key) xml.signal_autoconnect(nameFuncMap) self._switching_categories = False def _setup_category_source_monitors(self, oldcat, newcat): self._category_source_monitors.disconnect(oldcat) subscription_set = newcat.subscription is not None is_pseudo = isinstance(newcat, FeedCategoryList.PseudoCategory) default_refresh = ( (not subscription_set) or (newcat.subscription.frequency == newcat.subscription.REFRESH_DEFAULT)) attrfields = ( ((subscription_set and newcat.subscription.location) or "", self._source_field), ((subscription_set and newcat.subscription.username) or "", self._source_username_field), ((subscription_set and newcat.subscription.password) or "", self._source_password_field)) for f, w in attrfields: w.set_text(f) w.set_sensitive(not is_pseudo) if default_refresh: self._source_frequency_spin.set_value( Config.get_instance().poll_frequency / 60) else: self._source_frequency_spin.set_value( (subscription_set and newcat.subscription.frequency / 60) or 0) self._source_frequency_spin.set_sensitive( (not is_pseudo) and (not default_refresh)) self._source_frequency_default_check.set_active( (not is_pseudo) and default_refresh) self._source_frequency_default_check.set_sensitive(not is_pseudo) def save_location(value, widget): newcat.subscription.location = value def save_username(value, widget): newcat.subscription.username = value def save_password(value, widget): newcat.subscription.password = value def save_frequency(value, widget): newcat.subscription.frequency = value * 60 def create_sub_ensurer(saver): def ensure_and_save(value, widget): if newcat.subscription is None: newcat.subscription = FeedCategoryList.OPMLCategorySubscription() saver(value, widget) return ensure_and_save if not self._category_source_monitors.has_key(newcat): monitors = dict( [(key, ValueMonitor.create_for_gtkentry( v, 5000, create_sub_ensurer(f))) for key, v, f in (("location", self._source_field, save_location), ("username", self._source_username_field, save_username), ("password", self._source_password_field, save_password))]) monitors.update( {"frequency": ValueMonitor.create_for_gtkspin( self._source_frequency_spin, 5000, create_sub_ensurer(save_frequency))}) self._category_source_monitors[newcat] = monitors self._category_source_monitors.connect(newcat) def _category_changed(self, signal): self._switching_categories = True category = signal.category self._setup_category_source_monitors( self._selcategory, category) self._selcategory = category if isinstance(category, FeedCategoryList.PseudoCategory): self._delete_button.set_sensitive(False) else: self._delete_button.set_sensitive(True) self._feeds_presenter.category_selected(category) self._switching_categories = False return def _on_category_sort_ascending_button_clicked(self, widget): self._selcategory.sort() def _on_category_sort_descending_button_clicked(self, widget): self._selcategory.sort() self._selcategory.reverse() def _on_category_refresh_default_check_toggled(self, widget): if self._switching_categories: return default_frequency = Config.get_instance().poll_frequency / 60 if self._selcategory.subscription is None: self._selcategory.subscription = FeedCategoryList.OPMLCategorySubscription() if widget.get_active(): self._source_frequency_spin.set_value(default_frequency) self._source_frequency_spin.set_sensitive(False) # flush the monitor so we don't get pending updates to the # frequency overriding this change self._category_source_monitors.flush_monitor( self._selcategory, "frequency") self._selcategory.subscription.frequency = self._selcategory.subscription.REFRESH_DEFAULT else: self._source_frequency_spin.set_sensitive(True) self._selcategory.subscription.frequency = default_frequency / 60 class PreferencesDialog: SEC_PER_MINUTE = 60 def __init__(self, xml, parent): config = Config.get_instance() self._window = xml.get_widget('preferences_dialog') self._window.set_transient_for(parent) self._browser_override = xml.get_widget("prefs_browser_setting") self._browser_entry = xml.get_widget("prefs_browser_setting_entry") # General config = Config.get_instance() poll_frequency = int(config.poll_frequency/self.SEC_PER_MINUTE) items_stored = int(config.number_of_items_stored) browser_cmd = str (config.browser_cmd) if browser_cmd: self._browser_override.set_active(True) self._browser_entry.set_text (browser_cmd) self._browser_entry.set_sensitive (True) else: self._browser_entry.set_text (_("Using desktop setting")) xml.get_widget('poll_frequency_spin').set_value(poll_frequency) xml.get_widget('number_of_items_spin').set_value(items_stored) xml.get_widget(['item_order_oldest', 'item_order_newest'][config.item_order]).set_active(1) nameFuncMap = {} for key in dir(self.__class__): if key[:3] == 'on_': nameFuncMap[key] = getattr(self, key) xml.signal_autoconnect(nameFuncMap) self._categories_tab = CategoriesTab(glade.get_widget_tree( xml.get_widget('preferences_categories_tab'))) def show(self, *args): self._window.present() def hide(self, *args): self._window.hide() def on_preferences_dialog_delete_event(self, *args): self.hide() return True def on_preferences_close_button_clicked(self, button): self.hide() return def on_poll_frequency_spin_value_changed(self, spin): Config.get_instance().poll_frequency = int(spin.get_value() * self.SEC_PER_MINUTE) def on_number_of_items_spin_value_changed(self, spin): Config.get_instance().number_of_items_stored = int(spin.get_value()) def on_item_order_newest_toggled(self, radio): config = Config.get_instance() config.item_order = not config.item_order def on_item_order_oldest_toggled(self, radio): pass def on_prefs_browser_setting_toggled(self, button): active = button.get_active() if not active: config = Config.get_instance() config.browser_cmd = "" self._browser_entry.set_sensitive(active) def on_prefs_browser_setting_entry_focus_out_event(self, entry, *args): config = Config.get_instance() config.browser_cmd = entry.get_text() class CategorySourceMonitors(WeakKeyDictionary): def disconnect(self, category): if self.has_key(category): for m in self[category].values(): m.disconnect() def connect(self, category): if self.has_key(category): for m in self[category].values(): m.connect() def flush_monitor(self, category, monitor): if self.has_key(category) and self[category].has_key(monitor): self[category][monitor].flush()