iteration 1 - Use gobject for events in feed, summaryitem and feedlist
[straw.git] / src / lib / PreferencesDialog.py
blob0981b5f4fb7c3c013b7935f5c568c8e8f85d72ea
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 FeedCategoryList
33 import feeds
34 import Config
35 import ValueMonitor
37 class CategoryListView(MVP.WidgetView):
38 """View object for the category tree view"""
39 COLUMN_TITLE = 0
40 COLUMN_OBJ = 1
41 COLUMN_BOLD = 2
42 COLUMN_EDITABLE = 3
44 def _initialize(self):
45 treemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_PYOBJECT,
46 gobject.TYPE_INT, gobject.TYPE_BOOLEAN)
47 treemodel.set_sort_func(self.COLUMN_TITLE, self._sort_feeds)
48 treemodel.set_sort_column_id(self.COLUMN_TITLE, gtk.SORT_ASCENDING)
49 self._widget.set_model(treemodel)
50 self._widget.set_rules_hint(False)
51 self._create_columns()
52 self._widget.get_selection().connect(
53 'changed', self._selection_changed, self.COLUMN_OBJ)
55 def _sort_feeds(self, model, a, b):
56 """
57 From the gtk.TreeSortable.set_sort_func doc:
59 The comparison callback should return -1 if the iter1 row should come before
60 the iter2 row, 0 if the rows are equal, or 1 if the iter1 row should come
61 after the iter2 row.
62 """
63 retval = 0
64 feed_a = model.get_value(a, self.COLUMN_OBJ)
65 feed_b = model.get_value(b, self.COLUMN_OBJ)
67 if feed_a in self._model.pseudo_categories:
68 retval = -1
69 elif feed_b in self._model.pseudo_categories:
70 retval = 1
71 elif feed_a is not None and feed_b is not None:
72 retval = locale.strcoll(feed_a.title, feed_b.title)
73 elif feed_a is not None: retval = -1
74 elif feed_b is not None: retval = 1
75 return retval
77 def _model_set(self):
78 self._model.signal_connect(Event.FeedCategoryAddedSignal,
79 self._category_added)
80 self._model.signal_connect(Event.FeedCategoryRemovedSignal,
81 self._category_removed)
82 self._model.signal_connect(Event.FeedCategoryChangedSignal,
83 self._category_changed)
84 self._model.signal_connect(Event.FeedCategoryListLoadedSignal,
85 self._categories_loaded)
86 self._populate_treemodel()
88 def _create_columns(self):
89 renderer = gtk.CellRendererText()
90 renderer.connect('edited', self._cell_edited)
91 column = gtk.TreeViewColumn(_('_Category'), renderer,
92 text=self.COLUMN_TITLE,
93 weight=self.COLUMN_BOLD,
94 editable=self.COLUMN_EDITABLE)
95 self._widget.append_column(column)
97 def _populate_treemodel(self):
98 treemodel = self._widget.get_model()
99 treemodel.clear()
100 for category in self._model.pseudo_categories:
101 iter = treemodel.append()
102 treemodel.set(iter, self.COLUMN_TITLE, category.title,
103 self.COLUMN_OBJ, category,
104 self.COLUMN_BOLD, pango.WEIGHT_BOLD,
105 self.COLUMN_EDITABLE, False)
106 for category in self._model.user_categories:
107 iter = treemodel.append()
108 treemodel.set(iter, self.COLUMN_TITLE, category.title,
109 self.COLUMN_OBJ, category,
110 self.COLUMN_BOLD, pango.WEIGHT_NORMAL,
111 self.COLUMN_EDITABLE, True)
113 def _iterate_model_conditionally(self, func):
114 treemodel = self._widget.get_model()
115 iter = treemodel.get_iter_first()
116 while iter is not None:
117 if func(treemodel, iter): break
118 iter = treemodel.iter_next(iter)
120 def _add_category(self, category):
121 self._populate_treemodel()
122 def edit_row(treemodel, iter):
123 if treemodel.get_value(iter, self.COLUMN_OBJ) is category:
124 column = self._widget.get_column(self.COLUMN_TITLE)
125 path = treemodel.get_path(iter)
126 self._widget.set_cursor(path, column, True)
127 return True
128 self._iterate_model_conditionally(edit_row)
130 def _remove_category(self, category):
131 def remove_row(treemodel, iter):
132 if treemodel.get_value(iter, self.COLUMN_OBJ) is category:
133 column = self._widget.get_column(self.COLUMN_TITLE)
134 iterpath = treemodel.get_path(iter)
135 isIter = treemodel.remove(iter)
136 if not isIter:
137 current = iterpath[:-1] + (iterpath[-1]-1,)
138 else:
139 current = treemodel.get_path(iter)
140 self._widget.set_cursor(current, column)
141 return True
142 return False
143 self._widget.grab_focus()
144 self._iterate_model_conditionally(remove_row)
146 def _update_category_name(self, category):
147 def update_title(treemodel, iter):
148 if treemodel.get_value(iter, self.COLUMN_OBJ) is category:
149 treemodel.set(iter, self.COLUMN_TITLE, category.title)
150 return True
151 self._iterate_model_conditionally(update_title)
153 def _category_added(self, signal):
154 self._add_category(signal.category)
156 def _category_removed(self, signal):
157 self._remove_category(signal.category)
159 def _category_changed(self, signal):
160 self._update_category_name(signal.sender)
162 def _categories_loaded(self, signal):
163 self._populate_treemodel()
165 def _selection_changed(self, selection, *args):
166 if self._presenter is None:
167 error.log("presenter not set!")
168 return
169 model, iter = selection.get_selected()
170 if iter is None:
171 return
172 category = model.get_value(iter, self.COLUMN_OBJ)
173 self._presenter.category_changed(category)
175 def _cell_edited(self, cell, path_string, text):
176 treemodel = self._widget.get_model()
177 iter = treemodel.get_iter_from_string(path_string)
178 if not iter: return
179 category = treemodel.get_value(iter, self.COLUMN_OBJ)
180 self._presenter.category_title_edited(category, text)
182 class CategorySelectionChangedSignal(Event.BaseSignal):
183 def __init__(self, sender, category):
184 Event.BaseSignal.__init__(self, sender)
185 self.category = category
187 class CategoryListPresenter(MVP.BasicPresenter):
188 """Presenter object for the category tree view"""
189 def _initialize(self):
190 self.initialize_slots(CategorySelectionChangedSignal)
191 self._selected_category = None
193 def category_changed(self, category):
194 self._selected_category = category
195 self.emit_signal(CategorySelectionChangedSignal(self, category))
197 def category_title_edited(self, category, title):
198 category.title = title
200 def add_category(self):
201 cat = FeedCategoryList.FeedCategory(_("Category name"))
202 self._model.add_category(cat)
204 def remove_category(self):
205 if self._selected_category is not None:
206 self._model.remove_category(self._selected_category)
208 class CategoryAddView(MVP.WidgetView):
209 """View object for the add category button"""
210 def _on_category_add_button_clicked(self, *args):
211 if self._presenter is not None:
212 self._presenter.add_category()
214 class AddCategorySignal(Event.BaseSignal):
215 pass
217 class CategoryAddPresenter(MVP.BasicPresenter):
218 """Presenter object for the add category button"""
219 def _initialize(self):
220 self.initialize_slots(AddCategorySignal)
222 def add_category(self):
223 self.emit_signal(AddCategorySignal(self))
225 class CategoryRemoveView(MVP.WidgetView):
226 """View object for the remove category button"""
227 def _on_category_delete_button_clicked(self, *args):
228 if self._presenter is not None:
229 self._presenter.remove_category()
231 class RemoveCategorySignal(Event.BaseSignal):
232 pass
234 class CategoryRemovePresenter(MVP.BasicPresenter):
235 """Presenter object for the remove category button"""
236 def _initialize(self):
237 self.initialize_slots(RemoveCategorySignal)
239 def remove_category(self):
240 self.emit_signal(RemoveCategorySignal(self))
242 class CategoriesPresenter(object, Event.SignalEmitter):
243 """Presenter object that combines the category list presenter and
244 category add button presenter"""
245 def __init__(self, lister, adder, remover):
246 Event.SignalEmitter.__init__(self)
247 self.initialize_slots(CategorySelectionChangedSignal)
249 lister.signal_connect(CategorySelectionChangedSignal,
250 lambda s: self.emit_signal(s))
251 adder.signal_connect(AddCategorySignal, self._add_category)
252 remover.signal_connect(RemoveCategorySignal, self._remove_category)
253 self._lister = lister
254 self._adder = adder
255 self._remover = remover
257 def _add_category(self, signal):
258 self._lister.add_category()
260 def _remove_category(self, signal):
261 self._lister.remove_category()
263 class FeedListView(MVP.WidgetView):
264 """View object for the feed list"""
265 COLUMN_TITLE = 0
266 COLUMN_OBJ = 1
267 COLUMN_CHECKED = 2
268 COLUMN_TOGGLABLE = 3
270 def _initialize(self):
271 treemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_PYOBJECT,
272 gobject.TYPE_BOOLEAN, gobject.TYPE_BOOLEAN)
273 treemodel.set_sort_func(self.COLUMN_TITLE, self._sort_feeds)
274 treemodel.set_sort_column_id(self.COLUMN_TITLE, gtk.SORT_ASCENDING)
275 self._widget.set_model(treemodel)
276 self._widget.set_rules_hint(False)
277 self._create_columns()
279 self._category = None
280 self._updating_display = False
282 def _model_set(self):
283 self._model.signal_connect(Event.FeedsChangedSignal,
284 self._model_updated)
285 fclist = FeedCategoryList.get_instance()
286 self._populate_treemodel()
288 def _create_columns(self):
289 renderer = gtk.CellRendererToggle()
290 renderer.connect('toggled', self._cell_toggled)
291 column = gtk.TreeViewColumn(_('_Member'), renderer,
292 active=self.COLUMN_CHECKED,
293 activatable=self.COLUMN_TOGGLABLE)
294 column.set_sort_column_id(self.COLUMN_CHECKED)
295 self._widget.append_column(column)
296 renderer = gtk.CellRendererText()
297 column = gtk.TreeViewColumn(_('_Feed'), renderer,
298 text=self.COLUMN_TITLE)
299 column.set_sort_column_id(self.COLUMN_TITLE)
300 self._widget.append_column(column)
302 def _populate_treemodel(self):
303 treemodel = self._widget.get_model()
304 treemodel.clear()
305 feedlist = feeds.get_instance()
306 if self._category is None: return
307 category_feeds = self._category.feeds
308 enabletoggle = not isinstance(
309 self._category, FeedCategoryList.PseudoCategory)
310 for feed in feedlist:
311 iter = treemodel.append()
312 treemodel.set(iter, self.COLUMN_TITLE, feed.title,
313 self.COLUMN_OBJ, feed,
314 self.COLUMN_CHECKED, feed in category_feeds,
315 self.COLUMN_TOGGLABLE, enabletoggle)
317 def _sort_feeds(self, model, a, b):
318 feed_a = model.get_value(a, self.COLUMN_OBJ)
319 feed_b = model.get_value(b, self.COLUMN_OBJ)
321 if feed_a and feed_b :
322 return locale.strcoll(feed_a.title.lower(), feed_b.title.lower())
323 elif feed_a is not None: return -1
324 elif feed_b is not None: return 1
325 else:
326 return 0
328 def _model_updated(self, signal):
329 self._populate_treemodel()
331 def category_selected(self, category):
332 self._category = category
333 self._populate_treemodel()
334 return
336 def _cell_toggled(self, cell, path_string):
337 if self._updating_display:
338 return
339 treemodel = self._widget.get_model()
340 treeiter = treemodel.get_iter_from_string(path_string)
341 if not treeiter: return
342 feed = treemodel.get_value(treeiter, self.COLUMN_OBJ)
343 self._presenter.feed_toggled(feed)
344 self._populate_treemodel()
346 class FeedListPresenter(MVP.BasicPresenter):
347 """Presenter object for the feed list"""
348 def feed_toggled(self, feed):
349 if self._category is None:
350 return
351 if feed in self._category.feeds:
352 self._category.remove_feed(feed)
353 else:
354 self._category.append_feed(feed, False)
356 def category_selected(self, category):
357 self._category = category
358 if self._view is not None:
359 self._view.category_selected(category)
361 class CategoriesTab:
362 def __init__(self, xml):
363 self._selcategory = None # currently selected category
365 self._delete_button = xml.get_widget('category_delete_button')
366 self._delete_button.set_sensitive(False)
368 self._selcategory = None
370 clpresenter = CategoryListPresenter(
371 model = FeedCategoryList.get_instance(),
372 view = CategoryListView(xml.get_widget('category_treeview')))
373 capresenter = CategoryAddPresenter(
374 view = CategoryAddView(xml.get_widget('category_add_button')))
375 crpresenter = CategoryRemovePresenter(
376 view = CategoryRemoveView(
377 xml.get_widget('category_delete_button')))
379 self._categories_presenter = CategoriesPresenter(
380 clpresenter, capresenter, crpresenter)
381 self._categories_presenter.signal_connect(
382 CategorySelectionChangedSignal, self._category_changed)
384 feedmodel = feeds.get_instance()
385 self._feeds_presenter = FeedListPresenter()
386 self._feeds_presenter.model = feedmodel
387 self._feeds_presenter.view = FeedListView(
388 xml.get_widget('feeds_treeview'))
390 self._source_field = xml.get_widget(
391 'category_external_source')
392 self._source_username_field = xml.get_widget(
393 'category_external_source_username')
394 self._source_password_field = xml.get_widget(
395 'category_external_source_password')
396 self._source_frequency_spin = xml.get_widget(
397 'category_external_source_frequency_spin')
398 self._source_frequency_default_check = xml.get_widget(
399 'category_refresh_default_check')
401 sizegroup = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
402 for l in [xml.get_widget(w) for w in (
403 'category_external_source_label',
404 'category_external_source_username_label',
405 'category_external_source_password_label',
406 'category_external_source_frequency_label')]:
407 sizegroup.add_widget(l)
409 self._category_source_monitors = CategorySourceMonitors()
411 nameFuncMap = {}
412 for key in dir(self.__class__):
413 if key[:4] == '_on_':
414 nameFuncMap[key[1:]] = getattr(self, key)
415 xml.signal_autoconnect(nameFuncMap)
417 self._switching_categories = False
419 def _setup_category_source_monitors(self, oldcat, newcat):
420 self._category_source_monitors.disconnect(oldcat)
422 subscription_set = newcat.subscription is not None
423 is_pseudo = isinstance(newcat, FeedCategoryList.PseudoCategory)
424 default_refresh = (
425 (not subscription_set) or
426 (newcat.subscription.frequency ==
427 newcat.subscription.REFRESH_DEFAULT))
428 attrfields = (
429 ((subscription_set and newcat.subscription.location) or "",
430 self._source_field),
431 ((subscription_set and newcat.subscription.username) or "",
432 self._source_username_field),
433 ((subscription_set and newcat.subscription.password) or "",
434 self._source_password_field))
436 for f, w in attrfields:
437 w.set_text(f)
438 w.set_sensitive(not is_pseudo)
439 if default_refresh:
440 self._source_frequency_spin.set_value(
441 Config.get_instance().poll_frequency / 60)
442 else:
443 self._source_frequency_spin.set_value(
444 (subscription_set and newcat.subscription.frequency / 60) or 0)
446 self._source_frequency_spin.set_sensitive(
447 (not is_pseudo) and (not default_refresh))
448 self._source_frequency_default_check.set_active(
449 (not is_pseudo) and default_refresh)
451 self._source_frequency_default_check.set_sensitive(not is_pseudo)
453 def save_location(value, widget): newcat.subscription.location = value
454 def save_username(value, widget): newcat.subscription.username = value
455 def save_password(value, widget): newcat.subscription.password = value
456 def save_frequency(value, widget):
457 newcat.subscription.frequency = value * 60
459 def create_sub_ensurer(saver):
460 def ensure_and_save(value, widget):
461 if newcat.subscription is None:
462 newcat.subscription = FeedCategoryList.OPMLCategorySubscription()
463 saver(value, widget)
464 return ensure_and_save
466 if not self._category_source_monitors.has_key(newcat):
467 monitors = dict(
468 [(key, ValueMonitor.create_for_gtkentry(
469 v, 5000, create_sub_ensurer(f)))
470 for key, v, f in (("location", self._source_field,
471 save_location),
472 ("username", self._source_username_field,
473 save_username),
474 ("password", self._source_password_field,
475 save_password))])
476 monitors.update(
477 {"frequency": ValueMonitor.create_for_gtkspin(
478 self._source_frequency_spin, 5000,
479 create_sub_ensurer(save_frequency))})
480 self._category_source_monitors[newcat] = monitors
482 self._category_source_monitors.connect(newcat)
484 def _category_changed(self, signal):
485 self._switching_categories = True
486 category = signal.category
487 self._setup_category_source_monitors(
488 self._selcategory, category)
489 self._selcategory = category
491 if isinstance(category, FeedCategoryList.PseudoCategory):
492 self._delete_button.set_sensitive(False)
493 else:
494 self._delete_button.set_sensitive(True)
495 self._feeds_presenter.category_selected(category)
496 self._switching_categories = False
497 return
499 def _on_category_sort_ascending_button_clicked(self, widget):
500 self._selcategory.sort()
502 def _on_category_sort_descending_button_clicked(self, widget):
503 self._selcategory.sort()
504 self._selcategory.reverse()
506 def _on_category_refresh_default_check_toggled(self, widget):
507 if self._switching_categories:
508 return
509 default_frequency = Config.get_instance().poll_frequency / 60
510 if self._selcategory.subscription is None:
511 self._selcategory.subscription = FeedCategoryList.OPMLCategorySubscription()
512 if widget.get_active():
513 self._source_frequency_spin.set_value(default_frequency)
514 self._source_frequency_spin.set_sensitive(False)
515 # flush the monitor so we don't get pending updates to the
516 # frequency overriding this change
517 self._category_source_monitors.flush_monitor(
518 self._selcategory, "frequency")
519 self._selcategory.subscription.frequency = self._selcategory.subscription.REFRESH_DEFAULT
520 else:
521 self._source_frequency_spin.set_sensitive(True)
522 self._selcategory.subscription.frequency = default_frequency / 60
525 class PreferencesDialog:
526 SEC_PER_MINUTE = 60
528 def __init__(self, xml, parent):
529 config = Config.get_instance()
530 self._window = xml.get_widget('preferences_dialog')
531 self._window.set_transient_for(parent)
532 self._browser_override = xml.get_widget("prefs_browser_setting")
533 self._browser_entry = xml.get_widget("prefs_browser_setting_entry")
535 # General
536 config = Config.get_instance()
537 poll_frequency = int(config.poll_frequency/self.SEC_PER_MINUTE)
538 items_stored = int(config.number_of_items_stored)
540 browser_cmd = str (config.browser_cmd)
541 if browser_cmd:
542 self._browser_override.set_active(True)
543 self._browser_entry.set_text (browser_cmd)
544 self._browser_entry.set_sensitive (True)
545 else:
546 self._browser_entry.set_text (_("Using desktop setting"))
548 xml.get_widget('poll_frequency_spin').set_value(poll_frequency)
549 xml.get_widget('number_of_items_spin').set_value(items_stored)
550 xml.get_widget(['item_order_oldest',
551 'item_order_newest'][config.item_order]).set_active(1)
553 nameFuncMap = {}
554 for key in dir(self.__class__):
555 if key[:3] == 'on_':
556 nameFuncMap[key] = getattr(self, key)
557 xml.signal_autoconnect(nameFuncMap)
559 self._categories_tab = CategoriesTab(glade.get_widget_tree(
560 xml.get_widget('preferences_categories_tab')))
562 def show(self, *args):
563 self._window.present()
565 def hide(self, *args):
566 self._window.hide()
568 def on_preferences_dialog_delete_event(self, *args):
569 self.hide()
570 return True
572 def on_preferences_close_button_clicked(self, button):
573 self.hide()
574 return
576 def on_poll_frequency_spin_value_changed(self, spin):
577 Config.get_instance().poll_frequency = int(spin.get_value() * self.SEC_PER_MINUTE)
579 def on_number_of_items_spin_value_changed(self, spin):
580 Config.get_instance().number_of_items_stored = int(spin.get_value())
582 def on_item_order_newest_toggled(self, radio):
583 config = Config.get_instance()
584 config.item_order = not config.item_order
586 def on_item_order_oldest_toggled(self, radio):
587 pass
589 def on_prefs_browser_setting_toggled(self, button):
590 active = button.get_active()
591 if not active:
592 config = Config.get_instance()
593 config.browser_cmd = ""
594 self._browser_entry.set_sensitive(active)
596 def on_prefs_browser_setting_entry_focus_out_event(self, entry, *args):
597 config = Config.get_instance()
598 config.browser_cmd = entry.get_text()
600 class CategorySourceMonitors(WeakKeyDictionary):
601 def disconnect(self, category):
602 if self.has_key(category):
603 for m in self[category].values():
604 m.disconnect()
606 def connect(self, category):
607 if self.has_key(category):
608 for m in self[category].values():
609 m.connect()
611 def flush_monitor(self, category, monitor):
612 if self.has_key(category) and self[category].has_key(monitor):
613 self[category][monitor].flush()