1 """ PreferencesDialog.py
3 Module setting user preferences.
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
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. """
30 from weakref
import WeakKeyDictionary
32 import FeedCategoryList
37 class CategoryListView(MVP
.WidgetView
):
38 """View object for the category tree view"""
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
):
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
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
:
69 elif feed_b
in self
._model
.pseudo_categories
:
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
78 self
._model
.signal_connect(Event
.FeedCategoryAddedSignal
,
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()
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)
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)
137 current
= iterpath
[:-1] + (iterpath
[-1]-1,)
139 current
= treemodel
.get_path(iter)
140 self
._widget
.set_cursor(current
, column
)
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
)
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!")
169 model
, iter = selection
.get_selected()
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
)
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
):
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
):
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
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"""
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
,
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()
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
328 def _model_updated(self
, signal
):
329 self
._populate
_treemodel
()
331 def category_selected(self
, category
):
332 self
._category
= category
333 self
._populate
_treemodel
()
336 def _cell_toggled(self
, cell
, path_string
):
337 if self
._updating
_display
:
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:
351 if feed
in self
._category
.feeds
:
352 self
._category
.remove_feed(feed
)
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
)
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()
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
)
425 (not subscription_set
) or
426 (newcat
.subscription
.frequency
==
427 newcat
.subscription
.REFRESH_DEFAULT
))
429 ((subscription_set
and newcat
.subscription
.location
) or "",
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
:
438 w
.set_sensitive(not is_pseudo
)
440 self
._source
_frequency
_spin
.set_value(
441 Config
.get_instance().poll_frequency
/ 60)
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()
464 return ensure_and_save
466 if not self
._category
_source
_monitors
.has_key(newcat
):
468 [(key
, ValueMonitor
.create_for_gtkentry(
469 v
, 5000, create_sub_ensurer(f
)))
470 for key
, v
, f
in (("location", self
._source
_field
,
472 ("username", self
._source
_username
_field
,
474 ("password", self
._source
_password
_field
,
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)
494 self
._delete
_button
.set_sensitive(True)
495 self
._feeds
_presenter
.category_selected(category
)
496 self
._switching
_categories
= False
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
:
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
521 self
._source
_frequency
_spin
.set_sensitive(True)
522 self
._selcategory
.subscription
.frequency
= default_frequency
/ 60
525 class PreferencesDialog
:
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")
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
)
542 self
._browser
_override
.set_active(True)
543 self
._browser
_entry
.set_text (browser_cmd
)
544 self
._browser
_entry
.set_sensitive (True)
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)
554 for key
in dir(self
.__class
__):
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
):
568 def on_preferences_dialog_delete_event(self
, *args
):
572 def on_preferences_close_button_clicked(self
, button
):
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
):
589 def on_prefs_browser_setting_toggled(self
, button
):
590 active
= button
.get_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():
606 def connect(self
, category
):
607 if self
.has_key(category
):
608 for m
in self
[category
].values():
611 def flush_monitor(self
, category
, monitor
):
612 if self
.has_key(category
) and self
[category
].has_key(monitor
):
613 self
[category
][monitor
].flush()