Removed generated files.
[straw/fork.git] / straw / PreferencesDialog.py
bloba72a82501f24846d3707656000471717fd7bd188
1 """ PreferencesDialog.py
3 Module setting user preferences.
4 """
5 __copyright__ = "Copyright (c) 2002-2005 Free Software Foundation, Inc."
6 __license__ = """ GNU General Public License
8 Straw is free software; you can redistribute it and/or modify it under the
9 terms of the GNU General Public License as published by the Free Software
10 Foundation; either version 2 of the License, or (at your option) any later
11 version.
13 Straw is distributed in the hope that it will be useful, but WITHOUT ANY
14 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15 A PARTICULAR PURPOSE. See the GNU General Public License for more details.
17 You should have received a copy of the GNU General Public License along with
18 this program; if not, write to the Free Software Foundation, Inc., 59 Temple
19 Place - Suite 330, Boston, MA 02111-1307, USA. """
21 import pygtk
22 pygtk.require('2.0')
23 import gobject
24 import gtk
25 import pango
26 from gtk import glade
27 import locale
28 import error
29 import MVP
30 from weakref import WeakKeyDictionary
31 import Event
32 import Config
33 import ValueMonitor
35 class CategoryListView(MVP.WidgetView):
36 """View object for the category tree view"""
37 COLUMN_TITLE = 0
38 COLUMN_OBJ = 1
39 COLUMN_BOLD = 2
40 COLUMN_EDITABLE = 3
42 def _initialize(self):
43 treemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_PYOBJECT,
44 gobject.TYPE_INT, gobject.TYPE_BOOLEAN)
45 treemodel.set_sort_func(self.COLUMN_TITLE, self._sort_feeds)
46 treemodel.set_sort_column_id(self.COLUMN_TITLE, gtk.SORT_ASCENDING)
47 self._widget.set_model(treemodel)
48 self._widget.set_rules_hint(False)
49 self._create_columns()
50 self._widget.get_selection().connect(
51 'changed', self._selection_changed, self.COLUMN_OBJ)
53 def _sort_feeds(self, model, a, b):
54 """
55 From the gtk.TreeSortable.set_sort_func doc:
57 The comparison callback should return -1 if the iter1 row should come before
58 the iter2 row, 0 if the rows are equal, or 1 if the iter1 row should come
59 after the iter2 row.
60 """
61 retval = 0
62 feed_a = model.get_value(a, self.COLUMN_OBJ)
63 feed_b = model.get_value(b, self.COLUMN_OBJ)
65 if feed_a in self._model.pseudo_categories:
66 retval = -1
67 elif feed_b in self._model.pseudo_categories:
68 retval = 1
69 elif feed_a is not None and feed_b is not None:
70 retval = locale.strcoll(feed_a.title, feed_b.title)
71 elif feed_a is not None: retval = -1
72 elif feed_b is not None: retval = 1
73 return retval
75 def _model_set(self):
76 self._model.signal_connect(Event.FeedCategoryAddedSignal,
77 self._category_added)
78 self._model.signal_connect(Event.FeedCategoryRemovedSignal,
79 self._category_removed)
80 self._model.signal_connect(Event.FeedCategoryChangedSignal,
81 self._category_changed)
82 self._model.signal_connect(Event.FeedCategoryListLoadedSignal,
83 self._categories_loaded)
84 self._populate_treemodel()
86 def _create_columns(self):
87 renderer = gtk.CellRendererText()
88 renderer.connect('edited', self._cell_edited)
89 column = gtk.TreeViewColumn(_('_Category'), renderer,
90 text=self.COLUMN_TITLE,
91 weight=self.COLUMN_BOLD,
92 editable=self.COLUMN_EDITABLE)
93 self._widget.append_column(column)
95 def _populate_treemodel(self):
96 treemodel = self._widget.get_model()
97 treemodel.clear()
98 for category in self._model.pseudo_categories:
99 iter = treemodel.append()
100 treemodel.set(iter, self.COLUMN_TITLE, category.title,
101 self.COLUMN_OBJ, category,
102 self.COLUMN_BOLD, pango.WEIGHT_BOLD,
103 self.COLUMN_EDITABLE, False)
104 for category in self._model.user_categories:
105 iter = treemodel.append()
106 treemodel.set(iter, self.COLUMN_TITLE, category.title,
107 self.COLUMN_OBJ, category,
108 self.COLUMN_BOLD, pango.WEIGHT_NORMAL,
109 self.COLUMN_EDITABLE, True)
111 def _iterate_model_conditionally(self, func):
112 treemodel = self._widget.get_model()
113 iter = treemodel.get_iter_first()
114 while iter is not None:
115 if func(treemodel, iter): break
116 iter = treemodel.iter_next(iter)
118 def _add_category(self, category):
119 self._populate_treemodel()
120 def edit_row(treemodel, iter):
121 if treemodel.get_value(iter, self.COLUMN_OBJ) is category:
122 column = self._widget.get_column(self.COLUMN_TITLE)
123 path = treemodel.get_path(iter)
124 self._widget.set_cursor(path, column, True)
125 return True
126 self._iterate_model_conditionally(edit_row)
128 def _remove_category(self, category):
129 def remove_row(treemodel, iter):
130 if treemodel.get_value(iter, self.COLUMN_OBJ) is category:
131 column = self._widget.get_column(self.COLUMN_TITLE)
132 iterpath = treemodel.get_path(iter)
133 isIter = treemodel.remove(iter)
134 if not isIter:
135 current = iterpath[:-1] + (iterpath[-1]-1,)
136 else:
137 current = treemodel.get_path(iter)
138 self._widget.set_cursor(current, column)
139 return True
140 return False
141 self._widget.grab_focus()
142 self._iterate_model_conditionally(remove_row)
144 def _update_category_name(self, category):
145 def update_title(treemodel, iter):
146 if treemodel.get_value(iter, self.COLUMN_OBJ) is category:
147 treemodel.set(iter, self.COLUMN_TITLE, category.title)
148 return True
149 self._iterate_model_conditionally(update_title)
151 def _category_added(self, signal):
152 self._add_category(signal.category)
154 def _category_removed(self, signal):
155 self._remove_category(signal.category)
157 def _category_changed(self, signal):
158 self._update_category_name(signal.sender)
160 def _categories_loaded(self, signal):
161 self._populate_treemodel()
163 def _selection_changed(self, selection, *args):
164 if self._presenter is None:
165 error.log("presenter not set!")
166 return
167 model, iter = selection.get_selected()
168 if iter is None:
169 return
170 category = model.get_value(iter, self.COLUMN_OBJ)
171 self._presenter.category_changed(category)
173 def _cell_edited(self, cell, path_string, text):
174 treemodel = self._widget.get_model()
175 iter = treemodel.get_iter_from_string(path_string)
176 if not iter: return
177 category = treemodel.get_value(iter, self.COLUMN_OBJ)
178 self._presenter.category_title_edited(category, text)
180 class CategorySelectionChangedSignal(Event.BaseSignal):
181 def __init__(self, sender, category):
182 Event.BaseSignal.__init__(self, sender)
183 self.category = category
185 class CategoryListPresenter(MVP.BasicPresenter):
186 """Presenter object for the category tree view"""
187 def _initialize(self):
188 self.initialize_slots(CategorySelectionChangedSignal)
189 self._selected_category = None
191 def category_changed(self, category):
192 self._selected_category = category
193 self.emit_signal(CategorySelectionChangedSignal(self, category))
195 def category_title_edited(self, category, title):
196 category.title = title
198 def add_category(self):
199 cat = feeds.FeedCategory(_("Category name"))
200 self._model.add_category(cat)
202 def remove_category(self):
203 if self._selected_category is not None:
204 self._model.remove_category(self._selected_category)
206 class CategoryAddView(MVP.WidgetView):
207 """View object for the add category button"""
208 def _on_category_add_button_clicked(self, *args):
209 if self._presenter is not None:
210 self._presenter.add_category()
212 class AddCategorySignal(Event.BaseSignal):
213 pass
215 class CategoryAddPresenter(MVP.BasicPresenter):
216 """Presenter object for the add category button"""
217 def _initialize(self):
218 self.initialize_slots(AddCategorySignal)
220 def add_category(self):
221 self.emit_signal(AddCategorySignal(self))
223 class CategoryRemoveView(MVP.WidgetView):
224 """View object for the remove category button"""
225 def _on_category_delete_button_clicked(self, *args):
226 if self._presenter is not None:
227 self._presenter.remove_category()
229 class RemoveCategorySignal(Event.BaseSignal):
230 pass
232 class CategoryRemovePresenter(MVP.BasicPresenter):
233 """Presenter object for the remove category button"""
234 def _initialize(self):
235 self.initialize_slots(RemoveCategorySignal)
237 def remove_category(self):
238 self.emit_signal(RemoveCategorySignal(self))
240 class CategoriesPresenter(object, Event.SignalEmitter):
241 """Presenter object that combines the category list presenter and
242 category add button presenter"""
243 def __init__(self, lister, adder, remover):
244 Event.SignalEmitter.__init__(self)
245 self.initialize_slots(CategorySelectionChangedSignal)
247 lister.signal_connect(CategorySelectionChangedSignal,
248 lambda s: self.emit_signal(s))
249 adder.signal_connect(AddCategorySignal, self._add_category)
250 remover.signal_connect(RemoveCategorySignal, self._remove_category)
251 self._lister = lister
252 self._adder = adder
253 self._remover = remover
255 def _add_category(self, signal):
256 self._lister.add_category()
258 def _remove_category(self, signal):
259 self._lister.remove_category()
261 class FeedListView(MVP.WidgetView):
262 """View object for the feed list"""
263 COLUMN_TITLE = 0
264 COLUMN_OBJ = 1
265 COLUMN_CHECKED = 2
266 COLUMN_TOGGLABLE = 3
268 def _initialize(self):
269 treemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_PYOBJECT,
270 gobject.TYPE_BOOLEAN, gobject.TYPE_BOOLEAN)
271 treemodel.set_sort_func(self.COLUMN_TITLE, self._sort_feeds)
272 treemodel.set_sort_column_id(self.COLUMN_TITLE, gtk.SORT_ASCENDING)
273 self._widget.set_model(treemodel)
274 self._widget.set_rules_hint(False)
275 self._create_columns()
277 self._category = None
278 self._updating_display = False
280 def _model_set(self):
281 self._model.signal_connect(Event.FeedsChangedSignal,
282 self._model_updated)
283 fclist = feeds.category_list
284 self._populate_treemodel()
286 def _create_columns(self):
287 renderer = gtk.CellRendererToggle()
288 renderer.connect('toggled', self._cell_toggled)
289 column = gtk.TreeViewColumn(_('_Member'), renderer,
290 active=self.COLUMN_CHECKED,
291 activatable=self.COLUMN_TOGGLABLE)
292 column.set_sort_column_id(self.COLUMN_CHECKED)
293 self._widget.append_column(column)
294 renderer = gtk.CellRendererText()
295 column = gtk.TreeViewColumn(_('_Feed'), renderer,
296 text=self.COLUMN_TITLE)
297 column.set_sort_column_id(self.COLUMN_TITLE)
298 self._widget.append_column(column)
300 def _populate_treemodel(self):
301 treemodel = self._widget.get_model()
302 treemodel.clear()
303 feedlist = feeds.get_instance()
304 if self._category is None: return
305 category_feeds = self._category.feeds
306 enabletoggle = not isinstance(
307 self._category, feeds.PseudoCategory)
308 for feed in feedlist:
309 iter = treemodel.append()
310 treemodel.set(iter, self.COLUMN_TITLE, feed.title,
311 self.COLUMN_OBJ, feed,
312 self.COLUMN_CHECKED, feed in category_feeds,
313 self.COLUMN_TOGGLABLE, enabletoggle)
315 def _sort_feeds(self, model, a, b):
316 feed_a = model.get_value(a, self.COLUMN_OBJ)
317 feed_b = model.get_value(b, self.COLUMN_OBJ)
319 if feed_a and feed_b :
320 return locale.strcoll(feed_a.title.lower(), feed_b.title.lower())
321 elif feed_a is not None: return -1
322 elif feed_b is not None: return 1
323 else:
324 return 0
326 def _model_updated(self, signal):
327 self._populate_treemodel()
329 def category_selected(self, category):
330 self._category = category
331 self._populate_treemodel()
332 return
334 def _cell_toggled(self, cell, path_string):
335 if self._updating_display:
336 return
337 treemodel = self._widget.get_model()
338 treeiter = treemodel.get_iter_from_string(path_string)
339 if not treeiter: return
340 feed = treemodel.get_value(treeiter, self.COLUMN_OBJ)
341 self._presenter.feed_toggled(feed)
342 self._populate_treemodel()
344 class FeedListPresenter(MVP.BasicPresenter):
345 """Presenter object for the feed list"""
346 def feed_toggled(self, feed):
347 if self._category is None:
348 return
349 if feed in self._category.feeds:
350 self._category.remove_feed(feed)
351 else:
352 self._category.append_feed(feed, False)
354 def category_selected(self, category):
355 self._category = category
356 if self._view is not None:
357 self._view.category_selected(category)
359 class CategoriesTab:
360 def __init__(self, xml):
361 self._selcategory = None # currently selected category
363 self._delete_button = xml.get_widget('category_delete_button')
364 self._delete_button.set_sensitive(False)
366 self._selcategory = None
368 clpresenter = CategoryListPresenter(
369 model = feeds.category_list,
370 view = CategoryListView(xml.get_widget('category_treeview')))
371 capresenter = CategoryAddPresenter(
372 view = CategoryAddView(xml.get_widget('category_add_button')))
373 crpresenter = CategoryRemovePresenter(
374 view = CategoryRemoveView(
375 xml.get_widget('category_delete_button')))
377 self._categories_presenter = CategoriesPresenter(
378 clpresenter, capresenter, crpresenter)
379 self._categories_presenter.signal_connect(
380 CategorySelectionChangedSignal, self._category_changed)
382 feedmodel = feeds.get_instance()
383 self._feeds_presenter = FeedListPresenter()
384 self._feeds_presenter.model = feedmodel
385 self._feeds_presenter.view = FeedListView(
386 xml.get_widget('feeds_treeview'))
388 self._source_field = xml.get_widget(
389 'category_external_source')
390 self._source_username_field = xml.get_widget(
391 'category_external_source_username')
392 self._source_password_field = xml.get_widget(
393 'category_external_source_password')
394 self._source_frequency_spin = xml.get_widget(
395 'category_external_source_frequency_spin')
396 self._source_frequency_default_check = xml.get_widget(
397 'category_refresh_default_check')
399 sizegroup = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
400 for l in [xml.get_widget(w) for w in (
401 'category_external_source_label',
402 'category_external_source_username_label',
403 'category_external_source_password_label',
404 'category_external_source_frequency_label')]:
405 sizegroup.add_widget(l)
407 self._category_source_monitors = CategorySourceMonitors()
409 nameFuncMap = {}
410 for key in dir(self.__class__):
411 if key[:4] == '_on_':
412 nameFuncMap[key[1:]] = getattr(self, key)
413 xml.signal_autoconnect(nameFuncMap)
415 self._switching_categories = False
417 def _setup_category_source_monitors(self, oldcat, newcat):
418 self._category_source_monitors.disconnect(oldcat)
420 subscription_set = newcat.subscription is not None
421 is_pseudo = isinstance(newcat, feeds.PseudoCategory)
422 default_refresh = (
423 (not subscription_set) or
424 (newcat.subscription.frequency ==
425 newcat.subscription.REFRESH_DEFAULT))
426 attrfields = (
427 ((subscription_set and newcat.subscription.location) or "",
428 self._source_field),
429 ((subscription_set and newcat.subscription.username) or "",
430 self._source_username_field),
431 ((subscription_set and newcat.subscription.password) or "",
432 self._source_password_field))
434 for f, w in attrfields:
435 w.set_text(f)
436 w.set_sensitive(not is_pseudo)
437 if default_refresh:
438 self._source_frequency_spin.set_value(
439 Config.get_instance().poll_frequency / 60)
440 else:
441 self._source_frequency_spin.set_value(
442 (subscription_set and newcat.subscription.frequency / 60) or 0)
444 self._source_frequency_spin.set_sensitive(
445 (not is_pseudo) and (not default_refresh))
446 self._source_frequency_default_check.set_active(
447 (not is_pseudo) and default_refresh)
449 self._source_frequency_default_check.set_sensitive(not is_pseudo)
451 def save_location(value, widget): newcat.subscription.location = value
452 def save_username(value, widget): newcat.subscription.username = value
453 def save_password(value, widget): newcat.subscription.password = value
454 def save_frequency(value, widget):
455 newcat.subscription.frequency = value * 60
457 def create_sub_ensurer(saver):
458 def ensure_and_save(value, widget):
459 if newcat.subscription is None:
460 newcat.subscription = feeds.OPMLCategorySubscription()
461 saver(value, widget)
462 return ensure_and_save
464 if not self._category_source_monitors.has_key(newcat):
465 monitors = dict(
466 [(key, ValueMonitor.create_for_gtkentry(
467 v, 5000, create_sub_ensurer(f)))
468 for key, v, f in (("location", self._source_field,
469 save_location),
470 ("username", self._source_username_field,
471 save_username),
472 ("password", self._source_password_field,
473 save_password))])
474 monitors.update(
475 {"frequency": ValueMonitor.create_for_gtkspin(
476 self._source_frequency_spin, 5000,
477 create_sub_ensurer(save_frequency))})
478 self._category_source_monitors[newcat] = monitors
480 self._category_source_monitors.connect(newcat)
482 def _category_changed(self, signal):
483 self._switching_categories = True
484 category = signal.category
485 self._setup_category_source_monitors(
486 self._selcategory, category)
487 self._selcategory = category
489 if isinstance(category, feeds.PseudoCategory):
490 self._delete_button.set_sensitive(False)
491 else:
492 self._delete_button.set_sensitive(True)
493 self._feeds_presenter.category_selected(category)
494 self._switching_categories = False
495 return
497 def _on_category_sort_ascending_button_clicked(self, widget):
498 self._selcategory.sort()
500 def _on_category_sort_descending_button_clicked(self, widget):
501 self._selcategory.sort()
502 self._selcategory.reverse()
504 def _on_category_refresh_default_check_toggled(self, widget):
505 if self._switching_categories:
506 return
507 default_frequency = Config.get_instance().poll_frequency / 60
508 if self._selcategory.subscription is None:
509 self._selcategory.subscription = feeds.OPMLCategorySubscription()
510 if widget.get_active():
511 self._source_frequency_spin.set_value(default_frequency)
512 self._source_frequency_spin.set_sensitive(False)
513 # flush the monitor so we don't get pending updates to the
514 # frequency overriding this change
515 self._category_source_monitors.flush_monitor(
516 self._selcategory, "frequency")
517 self._selcategory.subscription.frequency = self._selcategory.subscription.REFRESH_DEFAULT
518 else:
519 self._source_frequency_spin.set_sensitive(True)
520 self._selcategory.subscription.frequency = default_frequency / 60
523 class PreferencesDialog:
524 SEC_PER_MINUTE = 60
526 def __init__(self, xml, parent):
527 config = Config.get_instance()
528 self._window = xml.get_widget('preferences_dialog')
529 self._window.set_transient_for(parent)
530 self._browser_override = xml.get_widget("prefs_browser_setting")
531 self._browser_entry = xml.get_widget("prefs_browser_setting_entry")
533 # General
534 config = Config.get_instance()
535 poll_frequency = int(config.poll_frequency/self.SEC_PER_MINUTE)
536 items_stored = int(config.number_of_items_stored)
538 browser_cmd = str (config.browser_cmd)
539 if browser_cmd:
540 self._browser_override.set_active(True)
541 self._browser_entry.set_text (browser_cmd)
542 self._browser_entry.set_sensitive (True)
543 else:
544 self._browser_entry.set_text (_("Using desktop setting"))
546 xml.get_widget('poll_frequency_spin').set_value(poll_frequency)
547 xml.get_widget('number_of_items_spin').set_value(items_stored)
548 xml.get_widget(['item_order_oldest',
549 'item_order_newest'][config.item_order]).set_active(1)
551 nameFuncMap = {}
552 for key in dir(self.__class__):
553 if key[:3] == 'on_':
554 nameFuncMap[key] = getattr(self, key)
555 xml.signal_autoconnect(nameFuncMap)
557 self._categories_tab = CategoriesTab(glade.get_widget_tree(
558 xml.get_widget('preferences_categories_tab')))
560 def show(self, *args):
561 self._window.present()
563 def hide(self, *args):
564 self._window.hide()
566 def on_preferences_dialog_delete_event(self, *args):
567 self.hide()
568 return True
570 def on_preferences_close_button_clicked(self, button):
571 self.hide()
572 return
574 def on_poll_frequency_spin_value_changed(self, spin):
575 Config.get_instance().poll_frequency = int(spin.get_value() * self.SEC_PER_MINUTE)
577 def on_number_of_items_spin_value_changed(self, spin):
578 Config.get_instance().number_of_items_stored = int(spin.get_value())
580 def on_item_order_newest_toggled(self, radio):
581 config = Config.get_instance()
582 config.item_order = not config.item_order
584 def on_item_order_oldest_toggled(self, radio):
585 pass
587 def on_prefs_browser_setting_toggled(self, button):
588 active = button.get_active()
589 if not active:
590 config = Config.get_instance()
591 config.browser_cmd = ""
592 self._browser_entry.set_sensitive(active)
594 def on_prefs_browser_setting_entry_focus_out_event(self, entry, *args):
595 config = Config.get_instance()
596 config.browser_cmd = entry.get_text()
598 class CategorySourceMonitors(WeakKeyDictionary):
599 def disconnect(self, category):
600 if self.has_key(category):
601 for m in self[category].values():
602 m.disconnect()
604 def connect(self, category):
605 if self.has_key(category):
606 for m in self[category].values():
607 m.connect()
609 def flush_monitor(self, category, monitor):
610 if self.has_key(category) and self[category].has_key(monitor):
611 self[category][monitor].flush()