""" 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()
syntax highlighted by Code2HTML, v. 0.9.1