3 Module for displaying the feeds in the Feeds TreeView.
5 __copyright__
= "Copyright (c) 2002-2005 Free Software Foundation, Inc."
7 Straw is free software; you can redistribute it and/or modify it under the
8 terms of the GNU General Public License as published by the Free Software
9 Foundation; either version 2 of the License, or (at your option) any later
12 Straw is distributed in the hope that it will be useful, but WITHOUT ANY
13 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License along with
17 this program; if not, write to the Free Software Foundation, Inc., 59 Temple
18 Place - Suite 330, Boston, MA 02111-1307, USA. """
22 from logging
import debug
, error
, warn
, info
, critical
, exception
29 import FeedCategoryList
30 import FeedPropertiesDialog
41 pixbuf
, name
, foreground
, unread
, object = range(5)
44 class TreeNodeAdapter (gobject
.GObject
):
45 ''' A Node Adapter which encapsulates either a Category or a Feed '''
48 'feed-changed' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
49 (gobject
.TYPE_PYOBJECT
,)),
50 'category-changed' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
51 (gobject
.TYPE_PYOBJECT
, gobject
.TYPE_PYOBJECT
,)),
52 'category-feed-added' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
53 (gobject
.TYPE_PYOBJECT
,)),
54 'category-feed-removed': (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
55 (gobject
.TYPE_PYOBJECT
,))
58 def __init__(self
, object):
59 gobject
.GObject
.__init
__(self
)
61 filename
= os
.path
.join(utils
.find_image_dir(), 'feed.png')
62 self
.default_pixbuf
= gtk
.gdk
.pixbuf_new_from_file(filename
)
63 if isinstance(self
.obj
, feeds
.Feed
):
64 self
.obj
.connect('changed', self
.feed_changed_cb
)
65 elif isinstance(self
.obj
, FeedCategoryList
.FeedCategory
):
66 self
.obj
.connect('feed-added', self
.feed_added_cb
)
67 self
.obj
.connect('feed-removed', self
.feed_removed_cb
)
68 self
.obj
.connect('changed', self
.category_changed_cb
)
70 def feed_changed_cb(self
, feed
):
71 self
.emit('feed-changed', feed
)
73 def category_changed_cb(self
, category
, *args
):
74 print "TREENODEADAPTER CATEGORY_CHANGED_CB -> ", category
, feed
75 self
.emit('category-changed', category
, args
)
77 def feed_added_cb(self
, category
, feed
):
78 self
.emit('category-feed-added', feed
)
80 def feed_removed_cb(self
, category
, feed
):
81 self
.emit('category-feed-removed', feed
)
83 def has_children(self
):
84 ''' Checks if the node has children. Essentially this means the object
88 has_child
= self
.obj
.feeds
and True or False
89 except AttributeError:
95 ''' The title of the node be it a category or a feed '''
99 def num_unread_items(self
):
100 ''' The number of unread items of the feed or if it's a category,
101 the aggregate number of unread items of the feeds belonging to the
103 def _r(a
,b
):return a
+ b
105 unread_items
= self
.obj
.number_of_unread
106 except AttributeError:
107 unread_items
= reduce(_r
, [feed
.number_of_unread
for feed
in self
.obj
.feeds
])
108 return unread_items
or ''
112 ''' gets the pixbuf to display according to the status of the feed '''
114 # ignore why above is a gtk.Label. We just need
115 # gtk.Widget.render_icon to get a pixbuf out of a stock image.
116 # Fyi, gtk.Widget is abstract that is why we need a subclass to do
120 if self
.obj
.process_status
is not feeds
.Feed
.STATUS_IDLE
:
121 return widget
.render_icon(gtk
.STOCK_EXECUTE
, gtk
.ICON_SIZE_MENU
)
123 return widget
.render_icon(gtk
.STOCK_DIALOG_ERROR
, gtk
.ICON_SIZE_MENU
)
124 except AttributeError, ex
:
126 return widget
.render_icon(gtk
.STOCK_DIRECTORY
, gtk
.ICON_SIZE_MENU
)
127 return self
.default_pixbuf
131 ''' An alias to a Feed object '''
132 if not isinstance(self
.obj
, feeds
.Feed
):
133 raise TypeError, _("object is not of a Feed")
138 ''' An alias to a Category object '''
139 if not isinstance(self
.obj
, FeedCategoryList
.FeedCategory
):
140 raise TypeError, _("object is not a Category")
145 ''' Abstracts a popup widget '''
147 def __init__(self
, listener
):
148 self
.manager
= gtk
.UIManager()
150 ("refresh", gtk
.STOCK_REFRESH
, _("_Refresh"), None, _("Update this feed"),
151 listener
.on_menu_poll_selected_activate
),
152 ("mark_as_read", gtk
.STOCK_CLEAR
, _("_Mark As Read"), None, _("Mark all items in this feed as read"),
153 listener
.on_menu_mark_all_as_read_activate
),
154 ("stop_refresh", None, _("_Stop Refresh"), None, _("Stop updating this feed"),
155 listener
.on_menu_stop_poll_selected_activate
),
156 ("remove", None, _("Remo_ve Feed"), None, _("Remove this feed from my subscription"),
157 listener
.on_remove_selected_feed
),
158 ("category-sort",None,_("_Arrange Feeds"), None, _("Sort the current category")),
159 ("ascending", gtk
.STOCK_SORT_ASCENDING
, _("Alpha_betical Order"), None, _("Sort in alphabetical order"),
160 listener
.on_sort_ascending
),
161 ("descending", gtk
.STOCK_SORT_DESCENDING
, _("Re_verse Order"), None, _("Sort in reverse order"),
162 listener
.on_sort_descending
),
163 ("properties", gtk
.STOCK_INFO
, _("_Information"), None, _("Feed-specific properties"),
164 listener
.on_display_properties_feed
)
166 ag
= gtk
.ActionGroup('FeedListPopupActions')
167 ag
.add_actions(actions
)
168 self
.manager
.insert_action_group(ag
,0)
169 popupui
= os
.path
.join(utils
.find_image_dir(), 'ui.xml')
170 self
.manager
.add_ui_from_file(popupui
)
174 return self
.manager
.get_widget('/feed_list_popup')
178 ''' The model for the feed list view '''
181 # name, pixbuf, unread, foreground
182 self
.store
= gtk
.TreeStore(gtk
.gdk
.Pixbuf
, str, str, str, gobject
.TYPE_PYOBJECT
)
183 # unread, weight, status_flag feed object, allow_children
184 # str, int, 'gboolean', gobject.TYPE_PYOBJECT, 'gboolean'
185 self
.fclist
= FeedCategoryList
.get_instance()
187 # build the user categories and its feeds
188 populate
= self
.populate
189 for category
in self
.fclist
.user_categories
:
190 populate(category
, category
.feeds
)
191 populate(None, self
.fclist
.un_category
.feeds
)
193 def populate(self
, category
, feeds
):
196 parent
= self
.store
.append(parent
)
197 node_adapter
= TreeNodeAdapter(category
)
198 node_adapter
.connect('category-changed', self
.category_changed_cb
)
199 node_adapter
.connect('category-feed-added', self
.feed_added_cb
)
200 node_adapter
.connect('category-feed-removed', self
.feed_removed_cb
)
201 self
.store
.set(parent
, Column
.pixbuf
, node_adapter
.pixbuf
,
202 Column
.name
, node_adapter
.title
,
203 Column
.unread
, node_adapter
.num_unread_items
,
204 Column
.object, node_adapter
)
206 rowiter
= self
.store
.append(parent
)
207 node_adapter
= TreeNodeAdapter(f
)
208 node_adapter
.connect('feed-changed', self
.feed_changed_cb
)
209 self
.store
.set(rowiter
, Column
.pixbuf
, node_adapter
.pixbuf
,
210 Column
.name
, node_adapter
.title
,
211 Column
.foreground
, node_adapter
.num_unread_items
and 'blue' or 'black',
212 Column
.unread
, node_adapter
.num_unread_items
,
213 Column
.object, node_adapter
)
215 def __getattribute__(self
, name
):
218 attr
= getattr(self
, name
)
219 except AttributeError, ae
:
220 attr
= getattr(self
.store
, name
)
224 def category_changed_cb(self
, node_adapter
, *args
):
225 # XXX What changes do we need here? new feed? udated subscription?
226 print "FEED LIST MODEL -> ", node_adapter
, args
228 def feed_added_cb(self
, node_adapter
, feed
):
229 if node_adapter
.category
is self
.fclist
.all_category
: #XXX should really fix this
231 treemodelrow
= self
.search(self
.store
,
232 lambda r
, d
: r
[d
[0]] == d
[1],
233 (Column
.object, node_adapter
))
234 feed_node_adapter
= TreeNodeAdapter(feed
)
235 feed_node_adapter
.connect('feed-changed', self
.feed_changed_cb
)
236 self
.store
.append(treemodelrow
.iter, [feed_node_adapter
.pixbuf
,
237 feed_node_adapter
.title
,
238 feed_node_adapter
.num_unread_items
and 'blue' or 'black',
239 feed_node_adapter
.num_unread_items
,
242 def feed_removed_cb(self
, node_adapter
, feed
):
243 print "FEED REMOVED CB ", node_adapter
, feed
245 def feed_changed_cb(self
, node_adapter
, feed
):
246 print "FEED CHANGED ", node_adapter
, feed
247 row
= self
.search(self
.store
,
248 lambda r
, data
: r
[data
[0]] == data
[1],
249 (Column
.object, node_adapter
))
251 path
= self
.store
.get_path(row
.iter)
252 self
.store
[path
] = [node_adapter
.pixbuf
, node_adapter
.title
,
253 node_adapter
.num_unread_items
and 'blue' or 'black',
254 node_adapter
.num_unread_items
,
261 def search(self
, rows
, func
, data
):
262 if not rows
: return None
266 result
= self
.search(row
.iterchildren(), func
, data
)
267 if result
: return result
270 class FeedsView(MVP
.WidgetView
):
271 def _initialize(self
):
272 self
._widget
.set_search_column(Column
.name
)
275 column
= gtk
.TreeViewColumn()
276 status_renderer
= gtk
.CellRendererPixbuf()
277 column
.pack_start(status_renderer
, False)
278 column
.set_attributes(status_renderer
,
279 pixbuf
=Column
.pixbuf
)
281 unread_renderer
= gtk
.CellRendererText()
282 column
.pack_start(unread_renderer
, False)
283 column
.set_attributes(unread_renderer
,
286 # feed title renderer
287 title_renderer
= gtk
.CellRendererText()
288 column
.pack_start(title_renderer
, False)
289 column
.set_attributes(title_renderer
,
290 foreground
=Column
.foreground
,
291 text
=Column
.name
) #, weight=Column.BOLD)
293 self
._widget
.append_column(column
)
295 selection
= self
._widget
.get_selection()
296 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
298 self
._widget
.connect("button_press_event", self
._on
_button
_press
_event
)
299 self
._widget
.connect("popup-menu", self
._on
_popup
_menu
)
301 self
._popup
= FeedListPopup(self
).popup
303 def _model_set(self
):
304 self
._widget
.set_model(self
._model
.model
)
306 def add_selection_changed_listener(self
, listener
):
307 selection
= self
._widget
.get_selection()
308 selection
.connect('changed', listener
.feedlist_selection_changed
, Column
.object)
310 def _on_popup_menu(self
, treeview
, *args
):
311 self
._popup
.popup(None, None, None, 0, 0)
313 def foreach_selected(self
, func
):
314 selection
= self
._widget
.get_selection()
315 (model
, pathlist
) = selection
.get_selected_rows()
316 iters
= [model
.get_iter(path
) for path
in pathlist
]
318 for treeiter
in iters
:
319 object = model
.get_value(treeiter
, Column
.object)
320 func(object, model
, treeiter
)
321 except TypeError, te
:
322 ## XXX maybe object is a category
326 def _on_button_press_event(self
, treeview
, event
):
328 if event
.button
== 3:
331 time
= gtk
.get_current_event_time()
332 path
= treeview
.get_path_at_pos(x
, y
)
335 path
, col
, cellx
, celly
= path
336 treeview
.grab_focus()
337 self
._popup
.popup(None, None, None, event
.button
, time
)
341 ### XXX don't do feed related tasks when path is a category this applies to every operation I suppose
342 def on_sort_ascending(self
, *args
):
343 ## XXX this will not work with tree list
344 self
._presenter
.sort_category()
346 def on_sort_descending(self
, *args
):
347 ### XXX this will not work with tree list
348 self
._presenter
.sort_category(reverse
=True)
350 def on_menu_poll_selected_activate(self
, *args
):
351 config
= Config
.get_instance()
353 if config
.offline
: #XXX
354 config
.offline
= not config
.offline
355 selection
= self
._widget
.get_selection()
356 (model
, pathlist
) = selection
.get_selected_rows()
357 iters
= [model
.get_iter(path
) for path
in pathlist
]
358 feeds
= [o
.feed
for o
in [model
.get_value(treeiter
,Column
.object) for treeiter
in iters
]]
359 PollManager
.get_instance().poll(feeds
)
362 def on_menu_stop_poll_selected_activate(self
, *args
):
363 self
.foreach_selected(lambda o
,*args
: o
.feed
.router
.stop_polling())
365 def on_menu_mark_all_as_read_activate(self
, *args
):
366 self
.foreach_selected(lambda o
,*args
: o
.feed
.mark_items_as_read())
368 def on_remove_selected_feed(self
, *args
):
370 (object, model
, treeiter
) = args
371 model
.remove(treeiter
)
372 feedlist
= feeds
.get_instance()
373 idx
= feedlist
.index(object.feed
)
375 self
.foreach_selected(remove
)
378 def on_display_properties_feed(self
, *args
):
379 selection
= self
._widget
.get_selection()
380 (model
, pathlist
) = selection
.get_selected_rows()
381 iters
= [model
.get_iter(path
) for path
in pathlist
]
382 path
= pathlist
.pop()
383 node_adapter
= self
.model
.model
[path
][Column
.object]
384 self
._presenter
.show_feed_information(node_adapter
.feed
)
387 def select_first_feed(self
):
388 selection
= self
._widget
.get_selection()
389 (model
, pathlist
) = selection
.get_selected_rows()
390 treeiter
= model
.get_iter_first()
391 if not treeiter
or not model
.iter_is_valid(treeiter
):
393 self
.set_cursor(treeiter
)
396 def select_next_feed(self
, with_unread
=False):
397 ''' Scrolls to the next feed in the feed list
399 If there is no selection, selects the first feed. If multiple feeds
400 are selected, selects the feed after the last selected feed.
402 If unread is True, selects the next unread with unread items.
404 If the selection next-to-be is a category, go to the iter its first
405 child. If current selection is a child, then go to (parent + 1),
406 provided that (parent + 1) is not a category.
409 def next(model
, current
):
410 treeiter
= model
.iter_next(current
)
411 if not treeiter
: return False
412 if model
.iter_depth(current
): next(model
, model
.iter_parent(current
))
413 path
= model
.get_path(treeiter
)
414 if with_unread
and model
[path
][Column
.unread
] < 1:
416 self
.set_cursor(treeiter
)
418 selection
= self
._widget
.get_selection()
419 (model
, pathlist
) = selection
.get_selected_rows()
420 iters
= [model
.get_iter(path
) for path
in pathlist
]
422 current
= iters
.pop()
423 if model
.iter_has_child(current
):
424 iterchild
= model
.iter_children(current
)
425 # make the row visible
426 path
= model
.get_path(iterchild
)
427 for i
in range(len(path
)):
428 self
._widget
.expand_row(path
[:i
+1], False)
429 # select his first born child
430 if with_unread
and model
[path
][Column
.unread
] > 0:
431 self
.set_cursor(iterchild
)
434 has_unread
= next(model
, current
)
435 has_unread
= next(model
,current
)
437 self
.set_cursor(model
.get_iter_first())
439 print "HAS UNREAD ", has_unread
442 def select_previous_feed(self
):
443 ''' Scrolls to the previous feed in the feed list.
445 If there is no selection, selects the first feed. If there's multiple
446 selection, selects the feed before the first selected feed.
448 If the previous selection is a category, select the last node in that
449 category. If the current selection is a child, then go to (parent -
450 1). If parent is the first feed, wrap and select the last feed or
451 category in the list.
453 def previous(model
, current
):
454 path
= model
.get_path(current
)
455 treerow
= model
[path
[-1]-1]
456 self
.set_cursor(treerow
.iter)
457 selection
= self
._widget
.get_selection()
458 (model
, pathlist
) = selection
.get_selected_rows()
459 iters
= [model
.get_iter(path
) for path
in pathlist
]
461 current_first
= iters
.pop(0)
462 if model
.iter_has_child(current_first
):
463 children
= model
.iter_n_children(current_first
)
464 treeiter
= model
.iter_nth_child(children
- 1)
465 self
.set_cursor(treeiter
)
467 previous(model
, current_first
)
469 self
.set_cursor(model
.get_iter_first())
472 def set_cursor(self
, treeiter
, col_id
=None, edit
=False):
476 path
= self
._model
.model
.get_path(treeiter
)
478 column
= self
._widget
.get_column(col_id
)
479 self
._widget
.set_cursor(path
, column
, edit
)
480 self
._widget
.scroll_to_cell(path
, column
)
481 self
._widget
.grab_focus()
484 class FeedsPresenter(MVP
.BasicPresenter
):
485 def _initialize(self
):
486 self
.model
= FeedListModel()
487 # self._init_signals()
490 def _init_signals(self
):
491 flist
= feeds
.get_instance()
492 #flist.signal_connect(Event.ItemReadSignal,
493 # self._feed_item_read)
494 #flist.signal_connect(Event.AllItemsReadSignal,
495 # self._feed_all_items_read)
496 #flist.signal_connect(Event.FeedsChangedSignal,
497 # self._feeds_changed)
498 #flist.signal_connect(Event.FeedDetailChangedSignal,
499 # self._feed_detail_changed)
500 #fclist = FeedCategoryList.get_instance()
501 #fclist.signal_connect(Event.FeedCategorySortedSignal,
502 # self._feeds_sorted_cb)
503 #fclist.signal_connect(Event.FeedCategoryChangedSignal,
504 # self._fcategory_changed_cb)
506 def select_first_feed(self
):
507 return self
.view
.select_first_feed()
509 def select_next_feed(self
, with_unread
=False):
510 return self
.view
.select_next_feed(with_unread
)
512 def select_previous_feed(self
):
513 return self
.view
.select_previous_feed()
515 def _sort_func(self
, model
, a
, b
):
517 Sorts the feeds lexically.
519 From the gtk.TreeSortable.set_sort_func doc:
521 The comparison callback should return -1 if the iter1 row should come before
522 the iter2 row, 0 if the rows are equal, or 1 if the iter1 row should come
526 fa
= model
.get_value(a
, Column
.OBJECT
)
527 fb
= model
.get_value(b
, Column
.OBJECT
)
530 retval
= locale
.strcoll(fa
.title
, fb
.title
)
531 elif fa
is not None: retval
= -1
532 elif fb
is not None: retval
= 1
535 #def sort_category(self, reverse=False):
536 # self._curr_category.sort()
538 # self._curr_category.reverse()
541 def show_feed_information(self
, feed
):
542 FeedPropertiesDialog
.show_feed_properties(None, feed
)