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
35 class CategoryListView(MVP
.WidgetView
):
36 """View object for the category tree view"""
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
):
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
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
:
67 elif feed_b
in self
._model
.pseudo_categories
:
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
76 self
._model
.signal_connect(Event
.FeedCategoryAddedSignal
,
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()
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)
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)
135 current
= iterpath
[:-1] + (iterpath
[-1]-1,)
137 current
= treemodel
.get_path(iter)
138 self
._widget
.set_cursor(current
, column
)
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
)
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!")
167 model
, iter = selection
.get_selected()
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
)
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
):
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
):
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
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"""
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
,
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()
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
326 def _model_updated(self
, signal
):
327 self
._populate
_treemodel
()
329 def category_selected(self
, category
):
330 self
._category
= category
331 self
._populate
_treemodel
()
334 def _cell_toggled(self
, cell
, path_string
):
335 if self
._updating
_display
:
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:
349 if feed
in self
._category
.feeds
:
350 self
._category
.remove_feed(feed
)
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
)
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()
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
)
423 (not subscription_set
) or
424 (newcat
.subscription
.frequency
==
425 newcat
.subscription
.REFRESH_DEFAULT
))
427 ((subscription_set
and newcat
.subscription
.location
) or "",
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
:
436 w
.set_sensitive(not is_pseudo
)
438 self
._source
_frequency
_spin
.set_value(
439 Config
.get_instance().poll_frequency
/ 60)
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()
462 return ensure_and_save
464 if not self
._category
_source
_monitors
.has_key(newcat
):
466 [(key
, ValueMonitor
.create_for_gtkentry(
467 v
, 5000, create_sub_ensurer(f
)))
468 for key
, v
, f
in (("location", self
._source
_field
,
470 ("username", self
._source
_username
_field
,
472 ("password", self
._source
_password
_field
,
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)
492 self
._delete
_button
.set_sensitive(True)
493 self
._feeds
_presenter
.category_selected(category
)
494 self
._switching
_categories
= False
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
:
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
519 self
._source
_frequency
_spin
.set_sensitive(True)
520 self
._selcategory
.subscription
.frequency
= default_frequency
/ 60
523 class PreferencesDialog
:
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")
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
)
540 self
._browser
_override
.set_active(True)
541 self
._browser
_entry
.set_text (browser_cmd
)
542 self
._browser
_entry
.set_sensitive (True)
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)
552 for key
in dir(self
.__class
__):
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
):
566 def on_preferences_dialog_delete_event(self
, *args
):
570 def on_preferences_close_button_clicked(self
, button
):
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
):
587 def on_prefs_browser_setting_toggled(self
, button
):
588 active
= button
.get_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():
604 def connect(self
, category
):
605 if self
.has_key(category
):
606 for m
in self
[category
].values():
609 def flush_monitor(self
, category
, monitor
):
610 if self
.has_key(category
) and self
[category
].has_key(monitor
):
611 self
[category
][monitor
].flush()