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
28 import os
, copy
, locale
, logging
33 pixbuf
, name
, foreground
, unread
, object = range(5)
36 class TreeViewNode(object):
37 def __init__(self
, node
, store
):
44 self
.node
.connect("notify", self
.obj_changed
)
46 def obj_changed(self
, obj
, property):
47 if property.name
== "unread-count":
48 #print self.store[(0, 0, 3)][1]
49 self
.store
.set(self
.treeiter
, 3, self
.node
.unread_count
)
50 elif property.name
== "status":
51 if (self
.node
.status
& straw
.FS_UPDATING
) > 0:
52 title
= self
.store
.get_value(self
.treeiter
, 1)
53 self
.store
.set(self
.treeiter
, 1, "<i>" + title
+ "</i>")
55 title
= self
.node
.title
56 self
.store
.set(self
.treeiter
, 1, title
)
60 ''' The title of the node be it a category or a feed '''
61 if self
.node
.type == "C":
63 elif self
.node
.type == "F":
64 return self
.node
.title
67 def unread_count(self
):
68 ''' The title of the node be it a category or a feed '''
69 return self
.node
.unread_count
73 ''' gets the pixbuf to display according to the status of the feed '''
77 if isinstance(self
.node
, Feed
):
78 return widget
.render_icon(gtk
.STOCK_FILE
, gtk
.ICON_SIZE_MENU
)
80 return widget
.render_icon(gtk
.STOCK_DIRECTORY
, gtk
.ICON_SIZE_MENU
)
82 # ignore why above is a gtk.Label. We just need
83 # gtk.Widget.render_icon to get a pixbuf out of a stock image.
84 # Fyi, gtk.Widget is abstract that is why we need a subclass to do
87 if self
.node
.process_status
is not feeds
.Feed
.STATUS_IDLE
:
88 return widget
.render_icon(gtk
.STOCK_EXECUTE
, gtk
.ICON_SIZE_MENU
)
90 return widget
.render_icon(gtk
.STOCK_DIALOG_ERROR
, gtk
.ICON_SIZE_MENU
)
91 except AttributeError, ex
:
93 return widget
.render_icon(gtk
.STOCK_DIRECTORY
, gtk
.ICON_SIZE_MENU
)
94 return self
.default_pixbuf
97 def parent_path(self
):
102 if self
.node
.parent
!= None:
103 node
= copy
.copy(self
.node
.parent
)
106 path
.append(str(node
.norder
))
107 node
= copy
.copy(node
.parent
)
114 return ":".join(path
)
117 def parent_iter(self
):
118 path
= self
.parent_path
123 return self
.store
.get_iter_from_string(path
)
126 ''' The model for the feed list view '''
132 def refresh_tree(self
):
133 self
.categories
, self
.appmodel
= FeedManager
.get_model()
135 self
._prepare
_store
()
136 self
._prepare
_model
()
138 #print self.tv_nodes[1]
140 self
._populate
_tree
(1, None, [])
142 def _init_signals(self
):
143 FeedManager
._get
_instance
().connect("feed-added", self
.node_added_cb
)
144 FeedManager
._get
_instance
().connect("category-added", self
.node_added_cb
)
146 def _prepare_model(self
):
149 for parent_id
in self
.appmodel
.keys():
150 self
.tv_nodes
[parent_id
] = [TreeViewNode(node
, self
.store
) for node
in self
.appmodel
[parent_id
]]
152 def _prepare_store(self
):
153 self
.store
= gtk
.TreeStore(gtk
.gdk
.Pixbuf
, str, str, str, gobject
.TYPE_PYOBJECT
)
155 def _populate_tree(self
, parent_id
, parent_iter
, done
):
156 if not self
.tv_nodes
.has_key(parent_id
):
159 for tv_node
in self
.tv_nodes
[parent_id
]:
163 tv_node
.treeiter
= self
._create
_row
(tv_node
)
164 tv_node
.store
= self
.store
165 elif node
.type == "C":
166 current_parent
= self
._create
_row
(tv_node
)
167 tv_node
.treeiter
= current_parent
168 tv_node
.store
= self
.store
170 if self
.tv_nodes
.has_key(node
.id):
171 self
._populate
_tree
(node
.id, current_parent
, done
)
173 def _create_row(self
, node
):
174 return self
.store
.append(node
.parent_iter
, [node
.pixbuf
,
180 def _lookup_parent(self
, _tv_node
):
181 for tv_node
in self
.tv_nodes
[_tv_node
.node
.parent_id
]:
182 if tv_node
.node
.type == "C" and _tv_node
.node
.parent_id
== tv_node
.node
.id:
185 def add_node(self
, tv_node
):
186 if not self
.tv_nodes
.has_key(tv_node
.node
.parent_id
):
187 self
.tv_nodes
[tv_node
.node
.parent_id
] = []
189 self
.tv_nodes
[tv_node
.node
.parent_id
].append(tv_node
)
191 def node_added_cb(self
, src
, node
):
192 tv_node
= TreeViewNode(node
, self
.store
)
193 self
.add_node(tv_node
)
195 self
._create
_row
(tv_node
)
197 """if parent_node != None and hasattr(parent_node, "treeiter"):
198 parent_iter = parent_node.treeiter
199 tv_node.treeiter = self._create_row(tv_node, parent_iter)
200 tv_node.store = self.store"""
206 def search(self
, rows
, func
, data
):
207 if not rows
: return None
211 result
= self
.search(row
.iterchildren(), func
, data
)
212 if result
: return result
215 class FeedsView(MVP
.WidgetView
):
216 def _initialize(self
):
217 self
._widget
.set_search_column(Column
.name
)
220 column
= gtk
.TreeViewColumn()
221 unread_renderer
= gtk
.CellRendererText()
222 column
.pack_start(unread_renderer
, False)
223 column
.set_attributes(unread_renderer
,
226 status_renderer
= gtk
.CellRendererPixbuf()
227 column
.pack_start(status_renderer
, False)
228 column
.set_attributes(status_renderer
,
229 pixbuf
=Column
.pixbuf
)
231 # feed title renderer
232 title_renderer
= gtk
.CellRendererText()
233 column
.pack_start(title_renderer
, False)
234 column
.set_attributes(title_renderer
,
235 foreground
=Column
.foreground
,
236 markup
=Column
.name
) #, weight=Column.BOLD)
238 self
._widget
.append_column(column
)
240 selection
= self
._widget
.get_selection()
241 selection
.set_mode(gtk
.SELECTION_SINGLE
)
243 self
._widget
.connect("button_press_event", self
._on
_button
_press
_event
)
244 self
._widget
.connect("popup-menu", self
._on
_popup
_menu
)
246 uifactory
= helpers
.UIFactory('FeedListActions')
247 action
= uifactory
.get_action('/feedlist_popup/refresh')
248 action
.connect('activate', self
.on_menu_poll_selected_activate
)
249 action
= uifactory
.get_action('/feedlist_popup/mark_as_read')
250 action
.connect('activate', self
.on_menu_mark_all_as_read_activate
)
251 action
= uifactory
.get_action('/feedlist_popup/stop_refresh')
252 action
.connect('activate', self
.on_menu_stop_poll_selected_activate
)
253 action
= uifactory
.get_action('/feedlist_popup/remove')
254 action
.connect('activate', self
.on_remove_selected_feed
)
255 action
= uifactory
.get_action('/feedlist_popup/properties')
256 action
.connect('activate', self
.on_display_properties_feed
)
257 self
.popup
= uifactory
.get_popup('/feedlist_popup')
259 treeview
= self
._widget
261 treeview
.enable_model_drag_source(gtk
.gdk
.BUTTON1_MASK
,
262 [("example", 0, 0)], gtk
.gdk
.ACTION_COPY
)
263 treeview
.enable_model_drag_dest([("example", 0, 0)],
265 treeview
.connect("drag_data_received", self
.on_dragdata_received_cb
)
267 def on_dragdata_received_cb(self
, treeview
, drag_context
, x
, y
, selection
, info
, eventtime
):
268 model
, iter_to_copy
= treeview
.get_selection().get_selected()
270 temp
= treeview
.get_dest_row_at_pos(x
, y
)
272 #temp = treeview.get_drag_dest_row()
277 path
, pos
= (len(model
)-1,), gtk
.TREE_VIEW_DROP_AFTER
279 target_iter
= model
.get_iter(path
)
280 path_of_target_iter
= model
.get_path(target_iter
)
282 if self
.check_row_path(model
, iter_to_copy
, target_iter
):
283 path
= model
.get_path(iter_to_copy
)
284 #model.insert_before(None, None, model[path])
285 #print model[path][Column.object].obj.title
286 #print "previous = %s" % str(path)
287 #print "target = %s" % str(path_of_target_iter)
288 #print path_of_target_iter[len(path_of_target_iter) - 1]
291 to_path
= list(path_of_target_iter
)
293 if (pos
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
) or (pos
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
):
294 pass#new_iter = model.prepend(target_iter, model[path])
295 elif pos
== gtk
.TREE_VIEW_DROP_BEFORE
:
297 elif pos
== gtk
.TREE_VIEW_DROP_AFTER
:
298 to_path
= list(path_of_target_iter
[0:len(path_of_target_iter
) - 2])
299 to_path
.append(path_of_target_iter
[len(path_of_target_iter
) - 1] + 1)
301 print "%s -> %s" % (str(from_path
), str(to_path
))
303 self
.iter_copy(model
, iter_to_copy
, target_iter
, pos
)
304 drag_context
.finish(True, True, eventtime
)
305 #model.remove(iter_to_copy)
306 #treeview.expand_all()
308 drag_context
.finish(False, False, eventtime
)
310 def check_row_path(self
, model
, iter_to_copy
, target_iter
):
311 path_of_iter_to_copy
= model
.get_path(iter_to_copy
)
312 path_of_target_iter
= model
.get_path(target_iter
)
313 if path_of_target_iter
[0:len(path_of_iter_to_copy
)] == path_of_iter_to_copy
:
318 def iter_copy(self
, model
, iter_to_copy
, target_iter
, pos
):
319 path
= model
.get_path(iter_to_copy
)
321 if (pos
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
) or (pos
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
):
322 new_iter
= model
.prepend(target_iter
, model
[path
])
323 elif pos
== gtk
.TREE_VIEW_DROP_BEFORE
:
324 new_iter
= model
.insert_before(None, target_iter
, model
[path
])
325 elif pos
== gtk
.TREE_VIEW_DROP_AFTER
:
326 new_iter
= model
.insert_after(None, target_iter
, model
[path
])
328 n
= model
.iter_n_children(iter_to_copy
)
330 next_iter_to_copy
= model
.iter_nth_child(iter_to_copy
, n
- i
- 1)
331 self
.iter_copy(model
, next_iter_to_copy
, new_iter
, gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
)
333 def _model_set(self
):
334 self
._widget
.set_model(self
._model
.model
)
336 def add_selection_changed_listener(self
, listener
):
337 selection
= self
._widget
.get_selection()
338 selection
.connect('changed', listener
.feedlist_selection_changed
, Column
.object)
340 def _on_popup_menu(self
, treeview
, *args
):
341 self
.popup
.popup(None, None, None, 0, 0)
343 def _on_button_press_event(self
, treeview
, event
):
345 if event
.button
== 3:
348 time
= gtk
.get_current_event_time()
349 path
= treeview
.get_path_at_pos(x
, y
)
352 path
, col
, cellx
, celly
= path
353 selection
= treeview
.get_selection()
354 selection
.unselect_all()
355 selection
.select_path(path
)
356 treeview
.grab_focus()
357 self
.popup
.popup(None, None, None, event
.button
, time
)
361 def foreach_selected(self
, func
):
362 selection
= self
._widget
.get_selection()
363 (model
, pathlist
) = selection
.get_selected_rows()
364 iters
= [model
.get_iter(path
) for path
in pathlist
]
366 for treeiter
in iters
:
367 object = model
.get_value(treeiter
, Column
.object)
368 func(object, model
, treeiter
)
369 except TypeError, te
:
370 ## XXX maybe object is a category
371 logging
.exception(te
)
374 def on_menu_poll_selected_activate(self
, *args
):
375 config
= Config
.get_instance()
377 if config
.offline
: #XXX
378 config
.offline
= not config
.offline
379 selection
= self
._widget
.get_selection()
380 (model
, pathlist
) = selection
.get_selected_rows()
381 iters
= [model
.get_iter(path
) for path
in pathlist
]
382 nodes
= [model
.get_value(treeiter
,Column
.object) for treeiter
in iters
]
388 fds
+= n
.category
.feeds
390 PollManager
.get_instance().poll(fds
)
393 def on_menu_stop_poll_selected_activate(self
, *args
):
394 self
.foreach_selected(lambda o
,*args
: o
.feed
.router
.stop_polling())
396 def on_menu_mark_all_as_read_activate(self
, *args
):
397 self
.foreach_selected(lambda o
,*args
: o
.feed
.mark_items_as_read())
399 def on_remove_selected_feed(self
, *args
):
401 (object, model
, treeiter
) = args
402 model
.remove(treeiter
)
403 feedlist
= feeds
.get_feedlist_instance()
404 idx
= feedlist
.index(object.feed
)
406 self
.foreach_selected(remove
)
409 def on_display_properties_feed(self
, *args
):
410 selection
= self
._widget
.get_selection()
411 (model
, pathlist
) = selection
.get_selected_rows()
412 iters
= [model
.get_iter(path
) for path
in pathlist
]
413 path
= pathlist
.pop()
414 node
= self
.model
.model
[path
][Column
.object]
415 self
._presenter
.show_feed_information(node
)
418 def select_first_feed(self
):
419 selection
= self
._widget
.get_selection()
420 (model
, pathlist
) = selection
.get_selected_rows()
421 treeiter
= model
.get_iter_first()
422 if not treeiter
or not model
.iter_is_valid(treeiter
):
424 self
.set_cursor(treeiter
)
427 def select_next_feed(self
, with_unread
=False):
428 ''' Scrolls to the next feed in the feed list
430 If there is no selection, selects the first feed. If multiple feeds
431 are selected, selects the feed after the last selected feed.
433 If unread is True, selects the next unread with unread items.
435 If the selection next-to-be is a category, go to the iter its first
436 child. If current selection is a child, then go to (parent + 1),
437 provided that (parent + 1) is not a category.
440 def next(model
, current
):
441 treeiter
= model
.iter_next(current
)
442 if not treeiter
: return False
443 if model
.iter_depth(current
): next(model
, model
.iter_parent(current
))
444 path
= model
.get_path(treeiter
)
445 if with_unread
and model
[path
][Column
.unread
] < 1:
447 self
.set_cursor(treeiter
)
449 selection
= self
._widget
.get_selection()
450 (model
, pathlist
) = selection
.get_selected_rows()
451 iters
= [model
.get_iter(path
) for path
in pathlist
]
453 current
= iters
.pop()
454 if model
.iter_has_child(current
):
455 iterchild
= model
.iter_children(current
)
456 # make the row visible
457 path
= model
.get_path(iterchild
)
458 for i
in range(len(path
)):
459 self
._widget
.expand_row(path
[:i
+1], False)
460 # select his first born child
461 if with_unread
and model
[path
][Column
.unread
] > 0:
462 self
.set_cursor(iterchild
)
465 has_unread
= next(model
, current
)
466 has_unread
= next(model
,current
)
468 self
.set_cursor(model
.get_iter_first())
472 def select_previous_feed(self
):
473 ''' Scrolls to the previous feed in the feed list.
475 If there is no selection, selects the first feed. If there's multiple
476 selection, selects the feed before the first selected feed.
478 If the previous selection is a category, select the last node in that
479 category. If the current selection is a child, then go to (parent -
480 1). If parent is the first feed, wrap and select the last feed or
481 category in the list.
483 def previous(model
, current
):
484 path
= model
.get_path(current
)
485 treerow
= model
[path
[-1]-1]
486 self
.set_cursor(treerow
.iter)
487 selection
= self
._widget
.get_selection()
488 (model
, pathlist
) = selection
.get_selected_rows()
489 iters
= [model
.get_iter(path
) for path
in pathlist
]
491 current_first
= iters
.pop(0)
492 if model
.iter_has_child(current_first
):
493 children
= model
.iter_n_children(current_first
)
494 treeiter
= model
.iter_nth_child(children
- 1)
495 self
.set_cursor(treeiter
)
497 previous(model
, current_first
)
499 self
.set_cursor(model
.get_iter_first())
502 def set_cursor(self
, treeiter
, col_id
=None, edit
=False):
506 path
= self
._model
.model
.get_path(treeiter
)
508 column
= self
._widget
.get_column(col_id
)
509 self
._widget
.set_cursor(path
, column
, edit
)
510 self
._widget
.scroll_to_cell(path
, column
)
511 self
._widget
.grab_focus()
514 class FeedsPresenter(MVP
.BasicPresenter
):
515 def _initialize(self
):
516 self
.model
= FeedListModel()
519 def _init_signals(self
):
522 #flist.signal_connect(Event.ItemReadSignal,
523 # self._feed_item_read)
524 #flist.signal_connect(Event.AllItemsReadSignal,
525 # self._feed_all_items_read)
526 #flist.signal_connect(Event.FeedsChangedSignal,
527 # self._feeds_changed)
528 #flist.signal_connect(Event.FeedDetailChangedSignal,
529 # self._feed_detail_changed)
530 #fclist = FeedCategoryList.get_instance()
531 #fclist.signal_connect(Event.FeedCategorySortedSignal,
532 # self._feeds_sorted_cb)
533 #fclist.signal_connect(Event.FeedCategoryChangedSignal,
534 # self._fcategory_changed_cb)
536 def select_first_feed(self
):
537 return self
.view
.select_first_feed()
539 def select_next_feed(self
, with_unread
=False):
540 return self
.view
.select_next_feed(with_unread
)
542 def select_previous_feed(self
):
543 return self
.view
.select_previous_feed()
545 def _sort_func(self
, model
, a
, b
):
547 Sorts the feeds lexically.
549 From the gtk.TreeSortable.set_sort_func doc:
551 The comparison callback should return -1 if the iter1 row should come before
552 the iter2 row, 0 if the rows are equal, or 1 if the iter1 row should come
556 fa
= model
.get_value(a
, Column
.OBJECT
)
557 fb
= model
.get_value(b
, Column
.OBJECT
)
560 retval
= locale
.strcoll(fa
.title
, fb
.title
)
561 elif fa
is not None: retval
= -1
562 elif fb
is not None: retval
= 1
565 def show_feed_information(self
, feed
):
566 straw
.feed_properties_show(feed
)