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. """
20 from model
import Feed
, Item
, Category
21 from straw
import TreeViewManager
, helpers
28 import os
, copy
, locale
, logging
33 pixbuf
, name
, foreground
, unread
, object = range(5)
36 class TreeNodeAdapter (gobject
.GObject
):
37 ''' A Node Adapter which encapsulates either a Category or a Feed '''
40 'feed-changed' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
41 (gobject
.TYPE_PYOBJECT
,)),
42 'category-changed' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
43 (gobject
.TYPE_PYOBJECT
, gobject
.TYPE_PYOBJECT
,)),
44 'category-feed-added' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
45 (gobject
.TYPE_PYOBJECT
,)),
46 'category-feed-removed': (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
47 (gobject
.TYPE_PYOBJECT
,))
50 def __init__(self
, object):
51 gobject
.GObject
.__init
__(self
)
53 filename
= os
.path
.join(straw
.STRAW_DATA_DIR
, 'feed.png')
54 self
.default_pixbuf
= gtk
.gdk
.pixbuf_new_from_file(filename
)
55 if isinstance(self
.obj
, Feed
):
56 pass#self.obj.connect('changed', self.feed_changed_cb)
57 elif isinstance(self
.obj
, Category
):
59 #self.obj.connect('feed-added', self.feed_added_cb)
60 #self.obj.connect('feed-removed', self.feed_removed_cb)
61 #self.obj.connect('changed', self.category_changed_cb)
63 def feed_changed_cb(self
, feed
):
64 self
.emit('feed-changed', feed
)
66 def category_changed_cb(self
, category
, *args
):
67 logging
.debug("TREENODEADAPTER CATEGORY_CHANGED_CB -> ", category
, feed
)
68 self
.emit('category-changed', category
, args
)
70 def feed_added_cb(self
, category
, feed
):
71 logging
.debug("TREE NODE ADAPTER FEED ADDED CB!!!", feed
)
72 self
.emit('category-feed-added', feed
)
74 def feed_removed_cb(self
, category
, feed
):
75 self
.emit('category-feed-removed', feed
)
77 def has_children(self
):
78 ''' Checks if the node has children. Essentially this means the object
82 has_child
= self
.obj
.feeds
and True or False
83 except AttributeError:
89 ''' The title of the node be it a category or a feed '''
93 def num_unread_items(self
):
94 ''' The number of unread items of the feed or if it's a category,
95 the aggregate number of unread items of the feeds belonging to the
98 return self
.obj
.unread_count
99 def _r(a
,b
):return a
+ b
101 unread_items
= self
.obj
.number_of_unread
102 except AttributeError:
103 unread_items
= reduce(_r
, [feed
.number_of_unread
for feed
in self
.obj
.feeds
])
104 return unread_items
or ''
108 ''' gets the pixbuf to display according to the status of the feed '''
110 return widget
.render_icon(gtk
.STOCK_HOME
, gtk
.ICON_SIZE_MENU
)
112 # ignore why above is a gtk.Label. We just need
113 # gtk.Widget.render_icon to get a pixbuf out of a stock image.
114 # Fyi, gtk.Widget is abstract that is why we need a subclass to do
117 if self
.obj
.process_status
is not feeds
.Feed
.STATUS_IDLE
:
118 return widget
.render_icon(gtk
.STOCK_EXECUTE
, gtk
.ICON_SIZE_MENU
)
120 return widget
.render_icon(gtk
.STOCK_DIALOG_ERROR
, gtk
.ICON_SIZE_MENU
)
121 except AttributeError, ex
:
122 logging
.exception(ex
)
123 return widget
.render_icon(gtk
.STOCK_DIRECTORY
, gtk
.ICON_SIZE_MENU
)
124 return self
.default_pixbuf
128 ''' An alias to a Feed object '''
129 if not isinstance(self
.obj
, Feed
):
130 raise TypeError, _("object is not of a Feed")
135 ''' An alias to a Category object '''
136 if not isinstance(self
.obj
, Category
):
137 raise TypeError, _("object is not a Category")
141 ''' The model for the feed list view '''
144 # name, pixbuf, unread, foreground
145 self
.store
= gtk
.TreeStore(gtk
.gdk
.Pixbuf
, str, str, str, gobject
.TYPE_PYOBJECT
)
146 # unread, weight, status_flag feed object, allow_children
147 # str, int, 'gboolean', gobject.TYPE_PYOBJECT, 'gboolean'
157 print time
.time() - s
159 #self._populate_tree(None, None, [])
161 def refresh_tree(self
):
162 self
.store
= gtk
.TreeStore(gtk
.gdk
.Pixbuf
, str, str, str, gobject
.TYPE_PYOBJECT
)
163 self
.categories
, self
.feeds
= FeedManager
.get_model()
164 self
.nodes
= TreeViewManager
.get_nodes()
166 self
._populate
_tree
(1, None, [])
169 def _init_signals(self
):
170 TreeViewManager
._get
_instance
().connect("node-added", self
.node_added_cb
)
172 def _get_feed(self
, node
, category
):
173 for feed
in self
.feeds
[category
]:
174 if feed
.id == node
.obj_id
:
178 def _populate_tree(self
, parent_id
, parent_iter
, done
):
179 if not self
.nodes
.has_key(parent_id
):
182 for node
in self
.nodes
[parent_id
]:
184 node
.obj
= self
._get
_feed
(node
, parent_id
)
185 node
.treeiter
= self
._create
_row
(node
, parent_iter
)
186 node
.store
= self
.store
187 self
.categories
[node
.obj
.category_id
].props
.unread_count
+= node
.obj
.unread_count
188 elif node
.type == "C":
189 node
.obj
= self
.categories
[node
.obj_id
]
190 current_parent
= self
._create
_row
(node
, parent_iter
)
191 node
.treeiter
= current_parent
192 node
.store
= self
.store
194 if self
.nodes
.has_key(node
.obj_id
):
195 self
._populate
_tree
(node
.obj_id
, current_parent
, done
)
197 def populate(self
, category
, feeds
):
200 parent
= self
.store
.append(parent
)
201 node_adapter
= TreeNodeAdapter(category
)
202 node_adapter
.connect('category-changed', self
.category_changed_cb
)
203 node_adapter
.connect('category-feed-added', self
.feed_added_cb
)
204 node_adapter
.connect('category-feed-removed', self
.feed_removed_cb
)
205 self
.store
.set(parent
, Column
.pixbuf
, node_adapter
.pixbuf
,
206 Column
.name
, node_adapter
.title
,
207 Column
.unread
, node_adapter
.num_unread_items
,
208 Column
.object, node_adapter
)
210 rowiter
= self
.store
.append(parent
)
211 node_adapter
= TreeNodeAdapter(f
)
212 node_adapter
.connect('feed-changed', self
.feed_changed_cb
)
213 self
.store
.set(rowiter
, Column
.pixbuf
, node_adapter
.pixbuf
,
214 Column
.name
, node_adapter
.title
,
215 Column
.foreground
, node_adapter
.num_unread_items
and 'blue' or 'black',
216 Column
.unread
, node_adapter
.num_unread_items
,
217 Column
.object, node_adapter
)
219 def __getattribute__(self
, name
):
222 attr
= getattr(self
, name
)
223 except AttributeError, ae
:
224 attr
= getattr(self
.store
, name
)
228 def _create_row(self
, node
, parent
= None):
229 return self
.store
.append(parent
, [node
.pixbuf
,
235 def category_changed_cb(self
, node_adapter
, *args
):
236 # XXX What changes do we need here? new feed? udated subscription?
237 print "FEED LIST MODEL -> ", node_adapter
, args
239 def _lookup_parent(self
, n
):
240 for children
in self
.nodes
.values():
241 for node
in children
:
242 #print str(n.parent_id) + " ?=? " + str(node.obj_id)
243 if node
.type == "C" and n
.parent_id
== node
.obj_id
:
246 def node_added_cb(self
, src
, node
):
247 #feed_node_adapter.connect('feed-changed', self.feed_changed_cb)
249 #self._create_row(node)
251 #self.nodes[node.parent_id].append(node)
253 #print node.parent_id
255 # node.obj = self._get_feed(node, node.parent_id)
256 node
.store
= self
.store
257 elif node
.type == "C":
258 node
.obj
= self
.categories
[node
.obj_id
]
259 node
.store
= self
.store
261 parent_node
= self
._lookup
_parent
(node
)
264 if parent_node
!= None and hasattr(parent_node
, "treeiter"):
265 parent_iter
= parent_node
.treeiter
266 node
.treeiter
= self
._create
_row
(node
, parent_iter
)
267 #node.store = self.store
269 def feed_removed_cb(self
, node_adapter
, feed
):
270 logging
.debug("FEED REMOVED CB ", node_adapter
, feed
)
272 def feed_changed_cb(self
, node_adapter
, feed
):
273 row
= self
.search(self
.store
,
274 lambda r
, data
: r
[data
[0]] == data
[1],
275 (Column
.object, node_adapter
))
277 path
= self
.store
.get_path(row
.iter)
278 self
.store
[path
] = [node_adapter
.pixbuf
, node_adapter
.title
,
279 node_adapter
.num_unread_items
and 'blue' or 'black',
280 node_adapter
.num_unread_items
,
287 def search(self
, rows
, func
, data
):
288 if not rows
: return None
292 result
= self
.search(row
.iterchildren(), func
, data
)
293 if result
: return result
296 class FeedsView(MVP
.WidgetView
):
297 def _initialize(self
):
298 self
._widget
.set_search_column(Column
.name
)
301 column
= gtk
.TreeViewColumn()
302 unread_renderer
= gtk
.CellRendererText()
303 column
.pack_start(unread_renderer
, False)
304 column
.set_attributes(unread_renderer
,
307 status_renderer
= gtk
.CellRendererPixbuf()
308 column
.pack_start(status_renderer
, False)
309 column
.set_attributes(status_renderer
,
310 pixbuf
=Column
.pixbuf
)
312 # feed title renderer
313 title_renderer
= gtk
.CellRendererText()
314 column
.pack_start(title_renderer
, False)
315 column
.set_attributes(title_renderer
,
316 foreground
=Column
.foreground
,
317 text
=Column
.name
) #, weight=Column.BOLD)
319 self
._widget
.append_column(column
)
321 selection
= self
._widget
.get_selection()
322 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
324 self
._widget
.connect("button_press_event", self
._on
_button
_press
_event
)
325 self
._widget
.connect("popup-menu", self
._on
_popup
_menu
)
327 uifactory
= helpers
.UIFactory('FeedListActions')
328 action
= uifactory
.get_action('/feedlist_popup/refresh')
329 action
.connect('activate', self
.on_menu_poll_selected_activate
)
330 action
= uifactory
.get_action('/feedlist_popup/mark_as_read')
331 action
.connect('activate', self
.on_menu_mark_all_as_read_activate
)
332 action
= uifactory
.get_action('/feedlist_popup/stop_refresh')
333 action
.connect('activate', self
.on_menu_stop_poll_selected_activate
)
334 action
= uifactory
.get_action('/feedlist_popup/remove')
335 action
.connect('activate', self
.on_remove_selected_feed
)
336 action
= uifactory
.get_action('/feedlist_popup/properties')
337 action
.connect('activate', self
.on_display_properties_feed
)
338 self
.popup
= uifactory
.get_popup('/feedlist_popup')
341 def _model_set(self
):
342 self
._widget
.set_model(self
._model
.model
)
344 def add_selection_changed_listener(self
, listener
):
345 selection
= self
._widget
.get_selection()
346 selection
.connect('changed', listener
.feedlist_selection_changed
, Column
.object)
348 def _on_popup_menu(self
, treeview
, *args
):
349 self
.popup
.popup(None, None, None, 0, 0)
351 def _on_button_press_event(self
, treeview
, event
):
353 if event
.button
== 3:
356 time
= gtk
.get_current_event_time()
357 path
= treeview
.get_path_at_pos(x
, y
)
360 path
, col
, cellx
, celly
= path
361 selection
= treeview
.get_selection()
362 selection
.unselect_all()
363 selection
.select_path(path
)
364 treeview
.grab_focus()
365 self
.popup
.popup(None, None, None, event
.button
, time
)
369 def foreach_selected(self
, func
):
370 selection
= self
._widget
.get_selection()
371 (model
, pathlist
) = selection
.get_selected_rows()
372 iters
= [model
.get_iter(path
) for path
in pathlist
]
374 for treeiter
in iters
:
375 object = model
.get_value(treeiter
, Column
.object)
376 func(object, model
, treeiter
)
377 except TypeError, te
:
378 ## XXX maybe object is a category
379 logging
.exception(te
)
382 def on_menu_poll_selected_activate(self
, *args
):
383 config
= Config
.get_instance()
385 if config
.offline
: #XXX
386 config
.offline
= not config
.offline
387 selection
= self
._widget
.get_selection()
388 (model
, pathlist
) = selection
.get_selected_rows()
389 iters
= [model
.get_iter(path
) for path
in pathlist
]
390 nodes
= [model
.get_value(treeiter
,Column
.object) for treeiter
in iters
]
396 fds
+= n
.category
.feeds
398 PollManager
.get_instance().poll(fds
)
401 def on_menu_stop_poll_selected_activate(self
, *args
):
402 self
.foreach_selected(lambda o
,*args
: o
.feed
.router
.stop_polling())
404 def on_menu_mark_all_as_read_activate(self
, *args
):
405 self
.foreach_selected(lambda o
,*args
: o
.feed
.mark_items_as_read())
407 def on_remove_selected_feed(self
, *args
):
409 (object, model
, treeiter
) = args
410 model
.remove(treeiter
)
411 feedlist
= feeds
.get_feedlist_instance()
412 idx
= feedlist
.index(object.feed
)
414 self
.foreach_selected(remove
)
417 def on_display_properties_feed(self
, *args
):
418 selection
= self
._widget
.get_selection()
419 (model
, pathlist
) = selection
.get_selected_rows()
420 iters
= [model
.get_iter(path
) for path
in pathlist
]
421 path
= pathlist
.pop()
422 node
= self
.model
.model
[path
][Column
.object]
423 self
._presenter
.show_feed_information(node
)
426 def select_first_feed(self
):
427 selection
= self
._widget
.get_selection()
428 (model
, pathlist
) = selection
.get_selected_rows()
429 treeiter
= model
.get_iter_first()
430 if not treeiter
or not model
.iter_is_valid(treeiter
):
432 self
.set_cursor(treeiter
)
435 def select_next_feed(self
, with_unread
=False):
436 ''' Scrolls to the next feed in the feed list
438 If there is no selection, selects the first feed. If multiple feeds
439 are selected, selects the feed after the last selected feed.
441 If unread is True, selects the next unread with unread items.
443 If the selection next-to-be is a category, go to the iter its first
444 child. If current selection is a child, then go to (parent + 1),
445 provided that (parent + 1) is not a category.
448 def next(model
, current
):
449 treeiter
= model
.iter_next(current
)
450 if not treeiter
: return False
451 if model
.iter_depth(current
): next(model
, model
.iter_parent(current
))
452 path
= model
.get_path(treeiter
)
453 if with_unread
and model
[path
][Column
.unread
] < 1:
455 self
.set_cursor(treeiter
)
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
= iters
.pop()
462 if model
.iter_has_child(current
):
463 iterchild
= model
.iter_children(current
)
464 # make the row visible
465 path
= model
.get_path(iterchild
)
466 for i
in range(len(path
)):
467 self
._widget
.expand_row(path
[:i
+1], False)
468 # select his first born child
469 if with_unread
and model
[path
][Column
.unread
] > 0:
470 self
.set_cursor(iterchild
)
473 has_unread
= next(model
, current
)
474 has_unread
= next(model
,current
)
476 self
.set_cursor(model
.get_iter_first())
480 def select_previous_feed(self
):
481 ''' Scrolls to the previous feed in the feed list.
483 If there is no selection, selects the first feed. If there's multiple
484 selection, selects the feed before the first selected feed.
486 If the previous selection is a category, select the last node in that
487 category. If the current selection is a child, then go to (parent -
488 1). If parent is the first feed, wrap and select the last feed or
489 category in the list.
491 def previous(model
, current
):
492 path
= model
.get_path(current
)
493 treerow
= model
[path
[-1]-1]
494 self
.set_cursor(treerow
.iter)
495 selection
= self
._widget
.get_selection()
496 (model
, pathlist
) = selection
.get_selected_rows()
497 iters
= [model
.get_iter(path
) for path
in pathlist
]
499 current_first
= iters
.pop(0)
500 if model
.iter_has_child(current_first
):
501 children
= model
.iter_n_children(current_first
)
502 treeiter
= model
.iter_nth_child(children
- 1)
503 self
.set_cursor(treeiter
)
505 previous(model
, current_first
)
507 self
.set_cursor(model
.get_iter_first())
510 def set_cursor(self
, treeiter
, col_id
=None, edit
=False):
514 path
= self
._model
.model
.get_path(treeiter
)
516 column
= self
._widget
.get_column(col_id
)
517 self
._widget
.set_cursor(path
, column
, edit
)
518 self
._widget
.scroll_to_cell(path
, column
)
519 self
._widget
.grab_focus()
522 class FeedsPresenter(MVP
.BasicPresenter
):
523 def _initialize(self
):
524 self
.model
= FeedListModel()
527 def _init_signals(self
):
530 #flist.signal_connect(Event.ItemReadSignal,
531 # self._feed_item_read)
532 #flist.signal_connect(Event.AllItemsReadSignal,
533 # self._feed_all_items_read)
534 #flist.signal_connect(Event.FeedsChangedSignal,
535 # self._feeds_changed)
536 #flist.signal_connect(Event.FeedDetailChangedSignal,
537 # self._feed_detail_changed)
538 #fclist = FeedCategoryList.get_instance()
539 #fclist.signal_connect(Event.FeedCategorySortedSignal,
540 # self._feeds_sorted_cb)
541 #fclist.signal_connect(Event.FeedCategoryChangedSignal,
542 # self._fcategory_changed_cb)
544 def select_first_feed(self
):
545 return self
.view
.select_first_feed()
547 def select_next_feed(self
, with_unread
=False):
548 return self
.view
.select_next_feed(with_unread
)
550 def select_previous_feed(self
):
551 return self
.view
.select_previous_feed()
553 def _sort_func(self
, model
, a
, b
):
555 Sorts the feeds lexically.
557 From the gtk.TreeSortable.set_sort_func doc:
559 The comparison callback should return -1 if the iter1 row should come before
560 the iter2 row, 0 if the rows are equal, or 1 if the iter1 row should come
564 fa
= model
.get_value(a
, Column
.OBJECT
)
565 fb
= model
.get_value(b
, Column
.OBJECT
)
568 retval
= locale
.strcoll(fa
.title
, fb
.title
)
569 elif fa
is not None: retval
= -1
570 elif fb
is not None: retval
= 1
573 def show_feed_information(self
, feed
):
574 straw
.feed_properties_show(feed
)