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
, object = range(4)
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
105 unread_items
= self
.obj
.number_of_unread
106 except AttributeError:
107 unread_items
= reduce(lambda a
,b
: a
.number_of_unread
+ b
.number_of_unread
,
109 print "number of unread of category is", unread_items
114 ''' gets the pixbuf to display according to the status of the feed '''
116 # ignore why above is a gtk.Label. We just need
117 # gtk.Widget.render_icon to get a pixbuf out of a stock image.
118 # Fyi, gtk.Widget is abstract that is why we need a subclass to do
122 if self
.obj
.process_status
is not feeds
.Feed
.STATUS_IDLE
:
123 return widget
.render_icon(gtk
.STOCK_EXECUTE
, gtk
.ICON_SIZE_MENU
)
125 return widget
.render_icon(gtk
.STOCK_DIALOG_ERROR
, gtk
.ICON_SIZE_MENU
)
126 except AttributeError, ex
:
128 return widget
.render_icon(gtk
.STOCK_DIRECTORY
, gtk
.ICON_SIZE_MENU
)
129 return self
.default_pixbuf
133 ''' An alias to a Feed object '''
134 if not isinstance(self
.obj
, feeds
.Feed
):
135 raise TypeError, _("object is not of a Feed")
140 ''' An alias to a Category object '''
141 if not isinstance(self
.obj
, FeedCategoryList
.FeedCategory
):
142 raise TypeError, _("object is not a Category")
147 ''' Abstracts a popup widget '''
149 def __init__(self
, listener
):
150 self
.manager
= gtk
.UIManager()
152 ("refresh", gtk
.STOCK_REFRESH
, _("_Refresh"), None, _("Update this feed"),
153 listener
.on_menu_poll_selected_activate
),
154 ("mark_as_read", gtk
.STOCK_CLEAR
, _("_Mark As Read"), None, _("Mark all items in this feed as read"),
155 listener
.on_menu_mark_all_as_read_activate
),
156 ("stop_refresh", None, _("_Stop Refresh"), None, _("Stop updating this feed"),
157 listener
.on_menu_stop_poll_selected_activate
),
158 ("remove", None, _("Remo_ve Feed"), None, _("Remove this feed from my subscription"),
159 listener
.on_remove_selected_feed
),
160 ("category-sort",None,_("_Arrange Feeds"), None, _("Sort the current category")),
161 ("ascending", gtk
.STOCK_SORT_ASCENDING
, _("Alpha_betical Order"), None, _("Sort in alphabetical order"),
162 listener
.on_sort_ascending
),
163 ("descending", gtk
.STOCK_SORT_DESCENDING
, _("Re_verse Order"), None, _("Sort in reverse order"),
164 listener
.on_sort_descending
),
165 ("properties", gtk
.STOCK_INFO
, _("_Information"), None, _("Feed-specific properties"),
166 listener
.on_display_properties_feed
)
168 ag
= gtk
.ActionGroup('FeedListPopupActions')
169 ag
.add_actions(actions
)
170 self
.manager
.insert_action_group(ag
,0)
171 popupui
= os
.path
.join(utils
.find_image_dir(), 'ui.xml')
172 self
.manager
.add_ui_from_file(popupui
)
176 return self
.manager
.get_widget('/feed_list_popup')
180 ''' The model for the feed list view '''
183 # name, pixbuf, unread, foreground
184 self
.store
= gtk
.TreeStore(gtk
.gdk
.Pixbuf
, str, str, gobject
.TYPE_PYOBJECT
)
185 # unread, weight, status_flag feed object, allow_children
186 # str, int, 'gboolean', gobject.TYPE_PYOBJECT, 'gboolean'
187 self
.fclist
= FeedCategoryList
.get_instance()
189 # build the user categories and its feeds
190 for category
in self
.fclist
.all_categories
:
191 # .. except for all_category
192 if category
is self
.fclist
.all_category
:
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
.object, node_adapter
)
204 for f
in category
.feeds
:
205 rowiter
= self
.store
.append(parent
)
206 node_adapter
= TreeNodeAdapter(f
)
207 node_adapter
.connect('feed-changed', self
.feed_changed_cb
)
208 self
.store
.set(rowiter
, Column
.pixbuf
, node_adapter
.pixbuf
,
209 Column
.name
, node_adapter
.title
,
210 Column
.foreground
, self
.get_foreground(node_adapter
.num_unread_items
),
211 Column
.object, node_adapter
) # maybe use create_adapter(f) here?
213 def __getattribute__(self
, name
):
216 attr
= getattr(self
, name
)
217 except AttributeError, ae
:
218 attr
= getattr(self
.store
, name
)
222 def category_changed_cb(self
, node_adapter
, *args
):
223 # XXX What changes do we need here? new feed? udated subscription?
224 print "FEED LIST MODEL -> ", node_adapter
, args
226 def feed_added_cb(self
, node_adapter
, feed
):
227 if node_adapter
.category
is self
.fclist
.all_category
: #XXX should really fix this
229 treemodelrow
= self
.search(self
.store
,
230 lambda r
, d
: r
[d
[0]] == d
[1],
231 (Column
.object, node_adapter
))
232 feed_node_adapter
= TreeNodeAdapter(feed
)
233 feed_node_adapter
.connect('feed-changed', self
.feed_changed_cb
)
234 self
.store
.append(treemodelrow
.iter, [feed_node_adapter
.pixbuf
, feed_node_adapter
.title
,
235 self
.get_foreground(feed_node_adapter
.num_unread_items
),
238 def feed_removed_cb(self
, node_adapter
, feed
):
239 print "FEED REMOVED CB ", node_adapter
, feed
241 def feed_changed_cb(self
, node_adapter
, feed
):
242 print "FEED CHANGED ", node_adapter
, feed
243 row
= self
.search(self
.store
,
244 lambda r
, data
: r
[data
[0]] == data
[1],
245 (Column
.object, node_adapter
))
247 path
= self
.store
.get_path(row
.iter)
248 self
.store
[path
] = [node_adapter
.pixbuf
, node_adapter
.title
,
249 self
.get_foreground(node_adapter
.num_unread_items
),
252 def get_foreground(self
, unread
):
253 ''' gets the foreground color according to the number of unread items'''
254 return ('black', 'blue')[(unread
> 0) and 1 or 0]
260 def search(self
, rows
, func
, data
):
261 if not rows
: return None
265 result
= self
.search(row
.iterchildren(), func
, data
)
266 if result
: return result
269 class FeedsView(MVP
.WidgetView
):
270 def _initialize(self
):
271 self
._widget
.set_search_column(Column
.name
)
274 column
= gtk
.TreeViewColumn()
275 status_renderer
= gtk
.CellRendererPixbuf()
276 column
.pack_start(status_renderer
, False)
277 column
.set_attributes(status_renderer
,
278 pixbuf
=Column
.pixbuf
)
280 # feed title renderer
281 title_renderer
= gtk
.CellRendererText()
282 column
.pack_start(title_renderer
, False)
283 column
.set_attributes(title_renderer
,
284 foreground
=Column
.foreground
,
285 text
=Column
.name
) #, weight=Column.BOLD)
287 self
._widget
.append_column(column
)
289 selection
= self
._widget
.get_selection()
290 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
292 self
._widget
.connect("button_press_event", self
._on
_button
_press
_event
)
293 self
._widget
.connect("popup-menu", self
._on
_popup
_menu
)
295 self
._popup
= FeedListPopup(self
).popup
297 def _model_set(self
):
298 self
._widget
.set_model(self
._model
.model
)
300 def add_selection_changed_listener(self
, listener
):
301 selection
= self
._widget
.get_selection()
302 selection
.connect('changed', listener
.feedlist_selection_changed
, Column
.object)
304 def _on_popup_menu(self
, treeview
, *args
):
305 self
._popup
.popup(None, None, None, 0, 0)
307 def foreach_selected(self
, func
):
308 selection
= self
._widget
.get_selection()
309 (model
, pathlist
) = selection
.get_selected_rows()
310 iters
= [model
.get_iter(path
) for path
in pathlist
]
312 for treeiter
in iters
:
313 object = model
.get_value(treeiter
, Column
.object)
314 func(object, model
, treeiter
)
315 except TypeError, te
:
316 ## XXX maybe object is a category
320 def _on_button_press_event(self
, treeview
, event
):
322 if event
.button
== 3:
325 time
= gtk
.get_current_event_time()
326 path
= treeview
.get_path_at_pos(x
, y
)
329 path
, col
, cellx
, celly
= path
330 treeview
.grab_focus()
331 self
._popup
.popup(None, None, None, event
.button
, time
)
335 ### XXX don't do feed related tasks when path is a category this applies to every operation I suppose
336 def on_sort_ascending(self
, *args
):
337 ## XXX this will not work with tree list
338 self
._presenter
.sort_category()
340 def on_sort_descending(self
, *args
):
341 ### XXX this will not work with tree list
342 self
._presenter
.sort_category(reverse
=True)
344 def on_menu_poll_selected_activate(self
, *args
):
345 config
= Config
.get_instance()
347 if config
.offline
: #XXX
348 config
.offline
= not config
.offline
349 selection
= self
._widget
.get_selection()
350 (model
, pathlist
) = selection
.get_selected_rows()
351 iters
= [model
.get_iter(path
) for path
in pathlist
]
352 feeds
= [o
.feed
for o
in [model
.get_value(treeiter
,Column
.object) for treeiter
in iters
]]
353 PollManager
.get_instance().poll(feeds
)
356 def on_menu_stop_poll_selected_activate(self
, *args
):
357 self
.foreach_selected(lambda o
,*args
: o
.feed
.router
.stop_polling())
359 def on_menu_mark_all_as_read_activate(self
, *args
):
360 self
.foreach_selected(lambda o
,*args
: o
.feed
.mark_all_items_as_read())
362 def on_remove_selected_feed(self
, *args
):
364 (object, model
, treeiter
) = args
365 model
.remove(treeiter
)
366 feedlist
= feeds
.get_instance()
367 idx
= feedlist
.index(object.feed
)
369 self
.foreach_selected(remove
)
372 def on_display_properties_feed(self
, *args
):
373 selection
= self
._widget
.get_selection()
374 (model
, pathlist
) = selection
.get_selected_rows()
375 iters
= [model
.get_iter(path
) for path
in pathlist
]
376 path
= pathlist
.pop()
377 node_adapter
= self
.model
.model
[path
][Column
.object]
378 self
._presenter
.show_feed_information(node_adapter
.feed
)
381 def select_first_feed(self
):
382 treeiter
= self
._model
.get_iter_first()
383 if not treeiter
or not self
._model
.iter_is_valid(treeiter
):
385 self
._view
.set_cursor(treeiter
)
388 def select_next_feed(self
, unread
=False):
389 ''' Scrolls to the next feed in the feed list
391 If there is no selection, selects the first feed. If multiple feeds
392 are selected, selects the feed after the last selected feed.
394 If unread is True, selects the next unread with unread items.
396 If the selection next-to-be is a category, go to the iter its first
397 child. If current selection is a child, then go to (parent + 1),
398 provided that (parent + 1) is not a category.
400 def next(model
, current
):
401 treeiter
= model
.iter_next(current
)
402 if not treeiter
and model
.iter_depth(current
):
403 next(model
, model
.iter_parent(current
))
404 self
.set_cursor(treeiter
)
405 selection
= self
._widget
.get_selection()
406 (model
, pathlist
) = selection
.get_selected_rows()
407 iters
= [model
.get_iter(path
) for path
in pathlist
]
409 current
= iters
.pop()
410 if model
.iter_has_child(current
):
411 iterchild
= model
.iter_children(current
)
412 # make the row visible
413 path
= model
.get_path(iterchild
)
414 for i
in range(len(path
)):
415 self
._widget
.expand_row(path
[:i
+1], False)
416 self
.set_cursor(iterchild
)
420 self
.set_cursor(model
.get_iter_first())
422 def select_previous_feed(self
):
423 ''' Scrolls to the previous feed in the feed list.
425 If there is no selection, selects the first feed. If there's multiple
426 selection, selects the feed before the first selected feed.
428 If the previous selection is a category, select the last node in that
429 category. If the current selection is a child, then go to (parent -
430 1). If parent is the first feed, wrap and select the last feed or
431 category in the list.
433 def previous(model
, current
):
434 path
= model
.get_path(current
)
436 #path_prev = path[:path_len
437 print "\tpath is -> %s , prev_path -> %s", (path
, prev_path
)
438 treeiter
= model
.get_iter(prev_path
)
439 self
.set_cursor(treeiter
)
440 selection
= self
._widget
.get_selection()
441 (model
, pathlist
) = selection
.get_selected_rows()
442 iters
= [model
.get_iter(path
) for path
in pathlist
]
444 current
= iters
.pop(0)
445 if model
.iter_has_child(current
):
446 kids
= model
.iter_n_children(current
)
447 iter = model
.iter_nth_child(kids
- 1)
448 self
.set_cursor(iter)
450 previous(model
, current
)
452 self
.set_cursor(model
.get_iter_first())
454 def select_next_unread_feed(self
):
457 treerow
= self
._model
[0]
458 selection
= self
._view
.get_selection()
459 srow
= selection
.get_selected()
461 model
, treeiter
= srow
462 nextiter
= model
.iter_next(treeiter
)
464 treerow
= self
._model
[model
.get_path(nextiter
)]
466 feedrow
= treerow
[Column
.OBJECT
]
467 if feedrow
.feed
.number_of_unread
:
468 self
._view
.set_cursor(treerow
.iter)
471 treerow
= treerow
.next
472 if not treerow
and mark_treerow
:
473 # should only do this once.
474 mark_treerow
= treerow
475 treerow
= self
._model
[0]
479 def set_cursor(self
, treeiter
, col_id
=None, edit
=False):
483 path
= self
._model
.model
.get_path(treeiter
)
485 column
= self
._widget
.get_column(col_id
)
486 self
._widget
.set_cursor(path
, column
, edit
)
487 self
._widget
.scroll_to_cell(path
, column
)
488 self
._widget
.grab_focus()
491 class FeedsPresenter(MVP
.BasicPresenter
):
492 def _initialize(self
):
493 self
.model
= FeedListModel()
494 # self._init_signals()
497 def _init_signals(self
):
498 flist
= feeds
.get_instance()
499 #flist.signal_connect(Event.ItemReadSignal,
500 # self._feed_item_read)
501 #flist.signal_connect(Event.AllItemsReadSignal,
502 # self._feed_all_items_read)
503 #flist.signal_connect(Event.FeedsChangedSignal,
504 # self._feeds_changed)
505 #flist.signal_connect(Event.FeedDetailChangedSignal,
506 # self._feed_detail_changed)
507 #fclist = FeedCategoryList.get_instance()
508 #fclist.signal_connect(Event.FeedCategorySortedSignal,
509 # self._feeds_sorted_cb)
510 #fclist.signal_connect(Event.FeedCategoryChangedSignal,
511 # self._fcategory_changed_cb)
513 def select_next_feed(self
, unread
=False):
514 self
.view
.select_next_feed(unread
)
516 def select_previous_feed(self
):
517 self
.view
.select_previous_feed()
519 def _sort_func(self
, model
, a
, b
):
521 Sorts the feeds lexically.
523 From the gtk.TreeSortable.set_sort_func doc:
525 The comparison callback should return -1 if the iter1 row should come before
526 the iter2 row, 0 if the rows are equal, or 1 if the iter1 row should come
530 fa
= model
.get_value(a
, Column
.OBJECT
)
531 fb
= model
.get_value(b
, Column
.OBJECT
)
534 retval
= locale
.strcoll(fa
.title
, fb
.title
)
535 elif fa
is not None: retval
= -1
536 elif fb
is not None: retval
= 1
539 #def sort_category(self, reverse=False):
540 # self._curr_category.sort()
542 # self._curr_category.reverse()
545 def show_feed_information(self
, feed
):
546 FeedPropertiesDialog
.show_feed_properties(None, feed
)