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 error
import debug
21 from model
import Feed
, Item
, Category
25 import categoryproperties
30 import os
, copy
, locale
, logging
35 pixbuf
, name
, foreground
, unread
, object, editable
= range(6)
37 _tmp_widget
= gtk
.Label()
39 class TreeViewNode(object):
40 def __init__(self
, node
, store
):
47 self
.node
.connect("notify", self
.obj_changed
)
49 def obj_changed(self
, obj
, property):
51 #print "obj_changed: %s" % threading.currentThread()
52 #debug("obj %d changed: property.name = %s, self.path = %s, self.store[path].id = %s" % (obj.id, property.name, str(self.path), str(self.store[self.path][Column.object].node.id)))
53 #debug("obj %d changed: property.name = %s, self.path = %s, self.store[path].id = %s" % (obj.id, property.name, str(self.path), str(self.store[self.path][Column.object].node.id)))
55 #self.store.set(self.iter, Column.pixbuf, self.pixbuf)
57 if property.name
== "unread-count":
58 #debug("obj %d changed: property.name = %s, self.path = %s, self.store[path].id = %s" % (obj.id, property.name, str(self.path), str(self.store[self.path][Column.object].node.id)))
61 debug("setting %d unread_count = %d, self.path = %s" % (obj
.id, self
.unread_count
, str(self
.path
)))
64 self
.store
.set(self
.iter, 3, self
.node
.unread_count
)
65 elif property.name
== "status":
66 if (self
.node
.status
& straw
.FS_UPDATING
) > 0:
67 title
= self
.store
.get_value(self
.iter, 1)
70 self
.store
.set(self
.iter, 1, "<i>" + title
+ "</i>")
72 title
= self
.node
.title
73 self
.store
.set(self
.iter, 1, title
)
77 ''' The title of the node be it a category or a feed '''
78 if self
.node
.type == "C":
80 elif self
.node
.type == "F":
81 return self
.node
.title
84 def unread_count(self
):
85 ''' The title of the node be it a category or a feed '''
86 return self
.node
.unread_count
90 ''' gets the pixbuf to display according to the status of the feed '''
93 if isinstance(self
.node
, Feed
):
94 if self
.node
.status
== straw
.FS_ERROR
:
95 return _tmp_widget
.render_icon(gtk
.STOCK_CANCEL
, gtk
.ICON_SIZE_MENU
)
97 return _tmp_widget
.render_icon(gtk
.STOCK_FILE
, gtk
.ICON_SIZE_MENU
)
99 return _tmp_widget
.render_icon(gtk
.STOCK_DIRECTORY
, gtk
.ICON_SIZE_MENU
)
105 node
= copy
.copy(self
.node
)
108 path
.append(node
.norder
)
109 node
= copy
.copy(node
.parent
)
111 path
.pop() # We don't need path to "root" category here since it's not in the tree view.
118 path
= self
.path_list
123 return ":".join(map(str, path
))
133 return self
.store
.get_iter_from_string(path
)
138 def parent_path(self
):
139 path
= self
.path_list
146 return ":".join(map(str, path
))
149 def parent_iter(self
):
150 path
= self
.parent_path
155 return self
.store
.get_iter_from_string(path
)
158 ''' The model for the feed list view '''
164 def refresh_tree(self
):
165 self
.appmodel
= FeedManager
.get_model()
167 self
._prepare
_store
()
168 self
._prepare
_model
()
170 self
._populate
_tree
(1, None, [])
172 def _init_signals(self
):
173 FeedManager
._get
_instance
().connect("feed-added", self
.node_added_cb
)
174 FeedManager
._get
_instance
().connect("category-added", self
.node_added_cb
)
176 def _prepare_model(self
):
177 self
.tv_nodes
= dict([(node
.id, TreeViewNode(node
, self
.store
)) for node
in self
.appmodel
.values()])
179 def _prepare_store(self
):
180 self
.store
= gtk
.TreeStore(gtk
.gdk
.Pixbuf
, str, str, str, gobject
.TYPE_PYOBJECT
, bool)
182 def _populate_tree(self
, parent_id
, parent_iter
, done
):
183 for node
in self
.tv_nodes
[parent_id
].node
.children
:
184 tv_node
= self
.tv_nodes
[node
.id]
187 self
._create
_row
(tv_node
)
188 tv_node
.store
= self
.store
189 elif node
.type == "C":
190 current_parent
= self
._create
_row
(tv_node
)
191 tv_node
.store
= self
.store
193 if self
.tv_nodes
.has_key(node
.id):
194 self
._populate
_tree
(node
.id, current_parent
, done
)
196 def _create_row(self
, node
, editable
= False):
197 return self
.store
.append(node
.parent_iter
, [node
.pixbuf
,
198 helpers
.pango_escape(node
.title
),
203 def add_node(self
, tv_node
):
204 self
.tv_nodes
[tv_node
.node
.id] = tv_node
206 def node_added_cb(self
, src
, node
):
207 tv_node
= TreeViewNode(node
, self
.store
)
208 self
.add_node(tv_node
)
209 self
._create
_row
(tv_node
)
215 def search(self
, rows
, func
, data
):
216 if not rows
: return None
220 result
= self
.search(row
.iterchildren(), func
, data
)
221 if result
: return result
224 class FeedsView(MVP
.WidgetView
):
225 def _initialize(self
):
226 self
._widget
.set_search_column(Column
.name
)
229 column
= gtk
.TreeViewColumn()
230 unread_renderer
= gtk
.CellRendererText()
231 column
.pack_start(unread_renderer
, False)
232 column
.set_attributes(unread_renderer
, text
= Column
.unread
)
234 status_renderer
= gtk
.CellRendererPixbuf()
235 column
.pack_start(status_renderer
, False)
236 column
.set_attributes(status_renderer
, pixbuf
= Column
.pixbuf
)
238 # feed title renderer
239 title_renderer
= gtk
.CellRendererText()
241 title_renderer
.connect("edited", self
.on_node_edit_title_edited
)
242 title_renderer
.connect("editing-canceled", self
.on_node_edit_title_canceled
)
244 #title_renderer.set_property('editable', True)
245 column
.pack_start(title_renderer
, False)
246 column
.set_attributes(title_renderer
,
247 foreground
=Column
.foreground
,
249 editable
=Column
.editable
) #, weight=Column.BOLD)
251 self
._widget
.append_column(column
)
253 selection
= self
._widget
.get_selection()
254 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
256 self
._widget
.connect("button_press_event", self
._on
_button
_press
_event
)
257 self
._widget
.connect("popup-menu", self
._on
_popup
_menu
)
259 uifactory
= helpers
.UIFactory('FeedListActions')
260 action
= uifactory
.get_action('/feedlist_popup/refresh')
261 action
.connect('activate', self
.on_menu_poll_selected_activate
)
262 action
= uifactory
.get_action('/feedlist_popup/add_child')
263 action
.connect('activate', self
.on_menu_add_child_activate
)
264 action
= uifactory
.get_action('/feedlist_popup/mark_as_read')
265 action
.connect('activate', self
.on_menu_mark_all_as_read_activate
)
266 action
= uifactory
.get_action('/feedlist_popup/stop_refresh')
267 action
.connect('activate', self
.on_menu_stop_poll_selected_activate
)
268 action
= uifactory
.get_action('/feedlist_popup/remove')
269 action
.connect('activate', self
.on_remove_selected_feed
)
270 action
= uifactory
.get_action('/feedlist_popup/properties')
271 action
.connect('activate', self
.on_display_properties_feed
)
272 self
.popup
= uifactory
.get_popup('/feedlist_popup')
274 treeview
= self
._widget
276 treeview
.enable_model_drag_source(gtk
.gdk
.BUTTON1_MASK
, [("drop_yes", 0, 0)], gtk
.gdk
.ACTION_MOVE
)
277 treeview
.enable_model_drag_dest([("drop_yes", 0, 0)], gtk
.gdk
.ACTION_MOVE
)
278 treeview
.connect("drag_data_received", self
._on
_dragdata
_received
)
279 treeview
.connect("drag_motion", self
._on
_drag
_motion
)
281 def _on_drag_motion(self
, treeview
, drag_context
, x
, y
, eventtime
):
282 temp
= treeview
.get_dest_row_at_pos(x
, y
)
287 model
= treeview
.get_model()
288 drop_path
, drop_position
= temp
290 # FIXME: Here we use only first selected node of possibly multiple
291 # selected. See fixme comment in self._on_dragdata_received.
292 source_node
= [tv_node
for tv_node
in self
.selected()][0]
294 drop_node
= model
[drop_path
][Column
.object]
295 source_path
= source_node
.path_list
297 sane_drop_path
= self
._check
_drop
_path
(model
, source_path
, drop_path
)
299 is_drop_into
= drop_position
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
or \
300 drop_position
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
302 can_drop_into
= drop_node
.node
.is_parent()
306 if sane_drop_path
and ((is_drop_into
and can_drop_into
) or not is_drop_into
):
307 treeview
.enable_model_drag_dest([("drop_yes", 0, 0)], gtk
.gdk
.ACTION_MOVE
)
309 treeview
.enable_model_drag_dest([("drop_no", 0, 0)], gtk
.gdk
.ACTION_MOVE
)
311 def _on_dragdata_received(self
, treeview
, drag_context
, x
, y
, selection
, info
, eventtime
):
312 model
, pathlist
= treeview
.get_selection().get_selected_rows()
314 if len(pathlist
) > 1:
315 # FIXME: Maybe we want to support drag and drop for multiple rows?
316 # For now it's not worth it while there are other things to do.
317 drag_context
.finish(False, False, eventtime
)
320 source_path
= pathlist
[0]
321 source_iter
= model
.get_iter(source_path
)
323 temp
= treeview
.get_dest_row_at_pos(x
, y
)
326 drop_path
, drop_pos
= temp
328 drop_path
, drop_pos
= (len(model
) - 1,), gtk
.TREE_VIEW_DROP_AFTER
330 effective_drop_path
= self
._calculate
_effective
_drop
_path
(source_path
, drop_path
, drop_pos
)
332 if source_path
== effective_drop_path
:
333 drag_context
.finish(False, False, eventtime
)
336 drop_iter
= model
.get_iter(drop_path
)
338 if not self
._check
_drop
_path
(model
, source_path
, drop_path
):
339 drag_context
.finish(False, False, eventtime
)
342 node
= model
[source_path
][Column
.object].node
343 self
._iter
_copy
(model
, source_iter
, drop_iter
, drop_pos
)
345 drag_context
.finish(True, True, eventtime
)
346 FeedManager
.move_node(node
, effective_drop_path
)
348 def _check_drop_path(self
, model
, source_path
, drop_path
):
350 Verifies if a drop path is not within the subtree of source path so that
351 we can disallow dropping parent into its own subtree etc. using this check.
354 return list(drop_path
[0:len(source_path
)]) != list(source_path
)
356 def _iter_copy(self
, model
, iter_to_copy
, target_iter
, pos
):
358 Recursively copies GTK TreeView iters from source to target using
359 GTK relative data about drag and drop operation.
362 path
= model
.get_path(iter_to_copy
)
364 if (pos
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
) or (pos
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
):
365 new_iter
= model
.prepend(target_iter
, model
[path
])
366 elif pos
== gtk
.TREE_VIEW_DROP_BEFORE
:
367 new_iter
= model
.insert_before(None, target_iter
, model
[path
])
368 elif pos
== gtk
.TREE_VIEW_DROP_AFTER
:
369 new_iter
= model
.insert_after(None, target_iter
, model
[path
])
371 n
= model
.iter_n_children(iter_to_copy
)
374 next_iter_to_copy
= model
.iter_nth_child(iter_to_copy
, n
- i
- 1)
375 self
._iter
_copy
(model
, next_iter_to_copy
, new_iter
, gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
)
377 def _calculate_effective_drop_path(self
, source_path
, drop_path
, drop_pos
):
379 Calculate effective absolute drop path given drop_pos and source/destination
380 of drag and drop operation. GTK uses relative terms for describing drop
381 destination (after/before/into etc.) while we prefer absolute drop path
382 and we can take care of reordering ourselves.
385 result
= list(drop_path
)
386 same_level
= len(source_path
) == len(drop_path
) and source_path
[:-1] == drop_path
[:-1]
388 if drop_pos
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
:
390 elif drop_pos
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
:
392 elif drop_pos
== gtk
.TREE_VIEW_DROP_BEFORE
:
393 if not same_level
or (same_level
and source_path
[-1] < drop_path
[-1]):
396 elif drop_pos
== gtk
.TREE_VIEW_DROP_AFTER
:
397 if not same_level
or (same_level
and source_path
[-1] > drop_path
[-1]):
402 def _model_set(self
):
403 self
._widget
.set_model(self
._model
.model
)
405 def add_selection_changed_listener(self
, listener
):
406 selection
= self
._widget
.get_selection()
407 selection
.connect('changed', listener
.feedlist_selection_changed
, Column
.object)
409 def _on_popup_menu(self
, treeview
, *args
):
410 self
.popup
.popup(None, None, None, 0, 0)
412 def _on_button_press_event(self
, treeview
, event
):
415 if event
.button
== 3:
418 time
= gtk
.get_current_event_time()
419 path
= treeview
.get_path_at_pos(x
, y
)
425 self
.node_at_popup
= self
.model
.store
[path
][Column
.object]
426 treeview
.grab_focus()
428 if self
.selected_count() < 2:
429 selection
= treeview
.get_selection()
430 selection
.unselect_all()
431 selection
.select_path(path
)
433 self
.popup
.popup(None, None, None, event
.button
, time
)
438 def selected_count(self
):
439 selection
= self
._widget
.get_selection()
440 pathlist
= selection
.get_selected_rows()[1]
444 selection
= self
._widget
.get_selection()
445 (model
, pathlist
) = selection
.get_selected_rows()
446 nodes
= [model
[path
][Column
.object] for path
in pathlist
]
448 for tv_node
in nodes
:
451 def foreach_selected(self
, func
):
452 selection
= self
._widget
.get_selection()
453 (model
, pathlist
) = selection
.get_selected_rows()
454 iters
= [model
.get_iter(path
) for path
in pathlist
]
456 for treeiter
in iters
:
457 object = model
.get_value(treeiter
, Column
.object)
458 func(object, model
, treeiter
)
459 except TypeError, te
:
460 logging
.exception(te
)
462 def on_menu_add_child_activate(self
, *args
):
463 self
.begin_add_category(self
.node_at_popup
.node
)
465 def begin_add_category(self
, node
):
466 category
= Category()
467 category
.parent
= node
468 category
.norder
= len(node
.children
)
469 self
.new_child
= TreeViewNode(category
, self
.model
.store
)
470 iter = self
.model
._create
_row
(self
.new_child
, editable
= True)
471 path
= self
.model
.store
.get_path(iter)
472 column
= self
._widget
.get_column(0)
474 parent_path
= self
.new_child
.parent_path
477 self
._widget
.expand_row(parent_path
, False)
479 self
._widget
.set_cursor_on_cell(path
, focus_column
= column
, start_editing
= True)
481 def on_menu_poll_selected_activate(self
, *args
):
482 """config = Config.get_instance()
484 if config.offline: #XXX
485 config.offline = not config.offline"""
487 FeedManager
.update_nodes([node
.node
for node
in self
.selected()])
489 def on_menu_stop_poll_selected_activate(self
, *args
):
490 self
.foreach_selected(lambda o
,*args
: o
.feed
.router
.stop_polling())
492 def on_menu_mark_all_as_read_activate(self
, *args
):
493 self
.foreach_selected(lambda o
,*args
: o
.feed
.mark_items_as_read())
495 def on_remove_selected_feed(self
, *args
):
496 nodes
= [tv_node
for tv_node
in self
.selected()]
498 FeedManager
.delete_nodes([tv_node
.node
.id for tv_node
in nodes
])
504 self
.model
.store
.remove(iter)
506 def on_node_edit_title_canceled(self
, cellrenderer
):
508 debug("on_node_edit_title_canceled (new_child), removing %s" % str(self
.new_child
.path
))
509 self
.model
.store
.remove(self
.new_child
.iter)
510 self
.new_child
= None
512 def on_node_edit_title_edited(self
, cellrenderertext
, path
, new_text
):
513 if len(new_text
) > 0:
514 self
.new_child
.node
.name
= new_text
515 FeedManager
.save_category(self
.new_child
.node
)
517 self
.model
.store
.remove(self
.new_child
.iter)
518 self
.new_child
= None
520 def on_display_properties_feed(self
, *args
):
521 selected_tv_node
= [tv_node
for tv_node
in self
.selected()][0]
522 self
._presenter
.show_feed_information(selected_tv_node
.node
)
524 def add_category(self
):
525 self
.begin_add_category(self
._model
.tv_nodes
[1].node
)
527 def select_first_feed(self
):
528 selection
= self
._widget
.get_selection()
529 (model
, pathlist
) = selection
.get_selected_rows()
530 treeiter
= model
.get_iter_first()
531 if not treeiter
or not model
.iter_is_valid(treeiter
):
533 self
.set_cursor(treeiter
)
536 def select_next_feed(self
, with_unread
=False):
537 ''' Scrolls to the next feed in the feed list
539 If there is no selection, selects the first feed. If multiple feeds
540 are selected, selects the feed after the last selected feed.
542 If unread is True, selects the next unread with unread items.
544 If the selection next-to-be is a category, go to the iter its first
545 child. If current selection is a child, then go to (parent + 1),
546 provided that (parent + 1) is not a category.
549 def next(model
, current
):
550 treeiter
= model
.iter_next(current
)
551 if not treeiter
: return False
552 if model
.iter_depth(current
): next(model
, model
.iter_parent(current
))
553 path
= model
.get_path(treeiter
)
554 if with_unread
and model
[path
][Column
.unread
] < 1:
556 self
.set_cursor(treeiter
)
558 selection
= self
._widget
.get_selection()
559 (model
, pathlist
) = selection
.get_selected_rows()
560 iters
= [model
.get_iter(path
) for path
in pathlist
]
562 current
= iters
.pop()
563 if model
.iter_has_child(current
):
564 iterchild
= model
.iter_children(current
)
565 # make the row visible
566 path
= model
.get_path(iterchild
)
567 for i
in range(len(path
)):
568 self
._widget
.expand_row(path
[:i
+1], False)
569 # select his first born child
570 if with_unread
and model
[path
][Column
.unread
] > 0:
571 self
.set_cursor(iterchild
)
574 has_unread
= next(model
, current
)
575 has_unread
= next(model
,current
)
577 self
.set_cursor(model
.get_iter_first())
581 def select_previous_feed(self
):
582 ''' Scrolls to the previous feed in the feed list.
584 If there is no selection, selects the first feed. If there's multiple
585 selection, selects the feed before the first selected feed.
587 If the previous selection is a category, select the last node in that
588 category. If the current selection is a child, then go to (parent -
589 1). If parent is the first feed, wrap and select the last feed or
590 category in the list.
592 def previous(model
, current
):
593 path
= model
.get_path(current
)
594 treerow
= model
[path
[-1]-1]
595 self
.set_cursor(treerow
.iter)
596 selection
= self
._widget
.get_selection()
597 (model
, pathlist
) = selection
.get_selected_rows()
598 iters
= [model
.get_iter(path
) for path
in pathlist
]
600 current_first
= iters
.pop(0)
601 if model
.iter_has_child(current_first
):
602 children
= model
.iter_n_children(current_first
)
603 treeiter
= model
.iter_nth_child(children
- 1)
604 self
.set_cursor(treeiter
)
606 previous(model
, current_first
)
608 self
.set_cursor(model
.get_iter_first())
611 def set_cursor(self
, treeiter
, col_id
=None, edit
=False):
616 path
= self
._model
.model
.get_path(treeiter
)
619 column
= self
._widget
.get_column(col_id
)
621 self
._widget
.set_cursor(path
, column
, edit
)
622 self
._widget
.scroll_to_cell(path
, column
)
623 self
._widget
.grab_focus()
625 class FeedsPresenter(MVP
.BasicPresenter
):
626 def _initialize(self
):
627 self
.model
= FeedListModel()
630 def _init_signals(self
):
633 def add_category(self
):
634 self
.view
.add_category()
636 def select_first_feed(self
):
637 return self
.view
.select_first_feed()
639 def select_next_feed(self
, with_unread
=False):
640 return self
.view
.select_next_feed(with_unread
)
642 def select_previous_feed(self
):
643 return self
.view
.select_previous_feed()
645 def _sort_func(self
, model
, a
, b
):
647 Sorts the feeds lexically.
649 From the gtk.TreeSortable.set_sort_func doc:
651 The comparison callback should return -1 if the iter1 row should come before
652 the iter2 row, 0 if the rows are equal, or 1 if the iter1 row should come
656 fa
= model
.get_value(a
, Column
.OBJECT
)
657 fb
= model
.get_value(b
, Column
.OBJECT
)
660 retval
= locale
.strcoll(fa
.title
, fb
.title
)
661 elif fa
is not None: retval
= -1
662 elif fb
is not None: retval
= 1
665 def show_feed_information(self
, node
):
667 properties
= categoryproperties
668 elif node
.type == "F":
669 properties
= feedproperties
671 properties
.show(node
)