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 feedproperties
import feed_properties_show
22 from model
import Feed
, Item
, Category
29 import os
, copy
, locale
, logging
34 pixbuf
, name
, foreground
, unread
, object, editable
= range(6)
36 _tmp_widget
= gtk
.Label()
38 class TreeViewNode(object):
39 def __init__(self
, node
, store
):
46 self
.node
.connect("notify", self
.obj_changed
)
48 def obj_changed(self
, obj
, property):
49 #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)))
50 #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)))
51 if property.name
== "unread-count":
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)))
55 debug("setting %d unread_count = %d, self.path = %s" % (obj
.id, self
.unread_count
, str(self
.path
)))
58 self
.store
.set(self
.iter, 3, self
.node
.unread_count
)
59 elif property.name
== "status":
60 if (self
.node
.status
& straw
.FS_UPDATING
) > 0:
61 title
= self
.store
.get_value(self
.iter, 1)
64 self
.store
.set(self
.iter, 1, "<i>" + title
+ "</i>")
66 title
= self
.node
.title
67 self
.store
.set(self
.iter, 1, title
)
71 ''' The title of the node be it a category or a feed '''
72 if self
.node
.type == "C":
74 elif self
.node
.type == "F":
75 return self
.node
.title
78 def unread_count(self
):
79 ''' The title of the node be it a category or a feed '''
80 return self
.node
.unread_count
84 ''' gets the pixbuf to display according to the status of the feed '''
87 if isinstance(self
.node
, Feed
):
88 return _tmp_widget
.render_icon(gtk
.STOCK_FILE
, gtk
.ICON_SIZE_MENU
)
90 return _tmp_widget
.render_icon(gtk
.STOCK_DIRECTORY
, gtk
.ICON_SIZE_MENU
)
96 node
= copy
.copy(self
.node
)
99 path
.append(node
.norder
)
100 node
= copy
.copy(node
.parent
)
102 path
.pop() # We don't need path to "root" category here since it's not in the tree view.
109 path
= self
.path_list
114 return ":".join(map(str, path
))
124 return self
.store
.get_iter_from_string(path
)
129 def parent_path(self
):
130 path
= self
.path_list
137 return ":".join(map(str, path
))
140 def parent_iter(self
):
141 path
= self
.parent_path
146 return self
.store
.get_iter_from_string(path
)
149 ''' The model for the feed list view '''
155 def refresh_tree(self
):
156 self
.appmodel
= FeedManager
.get_model()
158 self
._prepare
_store
()
159 self
._prepare
_model
()
161 self
._populate
_tree
(1, None, [])
163 def _init_signals(self
):
164 FeedManager
._get
_instance
().connect("feed-added", self
.node_added_cb
)
165 FeedManager
._get
_instance
().connect("category-added", self
.node_added_cb
)
167 def _prepare_model(self
):
168 self
.tv_nodes
= dict([(node
.id, TreeViewNode(node
, self
.store
)) for node
in self
.appmodel
.values()])
170 def _prepare_store(self
):
171 self
.store
= gtk
.TreeStore(gtk
.gdk
.Pixbuf
, str, str, str, gobject
.TYPE_PYOBJECT
, bool)
173 def _populate_tree(self
, parent_id
, parent_iter
, done
):
174 for node
in self
.tv_nodes
[parent_id
].node
.children
:
175 tv_node
= self
.tv_nodes
[node
.id]
178 self
._create
_row
(tv_node
)
179 tv_node
.store
= self
.store
180 elif node
.type == "C":
181 current_parent
= self
._create
_row
(tv_node
)
182 tv_node
.store
= self
.store
184 if self
.tv_nodes
.has_key(node
.id):
185 self
._populate
_tree
(node
.id, current_parent
, done
)
187 def _create_row(self
, node
, editable
= False):
188 return self
.store
.append(node
.parent_iter
, [node
.pixbuf
,
189 helpers
.pango_escape(node
.title
),
194 def add_node(self
, tv_node
):
195 self
.tv_nodes
[tv_node
.node
.id] = tv_node
197 def node_added_cb(self
, src
, node
):
198 tv_node
= TreeViewNode(node
, self
.store
)
199 self
.add_node(tv_node
)
200 self
._create
_row
(tv_node
)
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
, text
= Column
.unread
)
225 status_renderer
= gtk
.CellRendererPixbuf()
226 column
.pack_start(status_renderer
, False)
227 column
.set_attributes(status_renderer
, pixbuf
= Column
.pixbuf
)
229 # feed title renderer
230 title_renderer
= gtk
.CellRendererText()
232 title_renderer
.connect("edited", self
.on_node_edit_title_edited
)
233 title_renderer
.connect("editing-canceled", self
.on_node_edit_title_canceled
)
235 #title_renderer.set_property('editable', True)
236 column
.pack_start(title_renderer
, False)
237 column
.set_attributes(title_renderer
,
238 foreground
=Column
.foreground
,
240 editable
=Column
.editable
) #, weight=Column.BOLD)
242 self
._widget
.append_column(column
)
244 selection
= self
._widget
.get_selection()
245 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
247 self
._widget
.connect("button_press_event", self
._on
_button
_press
_event
)
248 self
._widget
.connect("popup-menu", self
._on
_popup
_menu
)
250 uifactory
= helpers
.UIFactory('FeedListActions')
251 action
= uifactory
.get_action('/feedlist_popup/refresh')
252 action
.connect('activate', self
.on_menu_poll_selected_activate
)
253 action
= uifactory
.get_action('/feedlist_popup/add_child')
254 action
.connect('activate', self
.on_menu_add_child_activate
)
255 action
= uifactory
.get_action('/feedlist_popup/mark_as_read')
256 action
.connect('activate', self
.on_menu_mark_all_as_read_activate
)
257 action
= uifactory
.get_action('/feedlist_popup/stop_refresh')
258 action
.connect('activate', self
.on_menu_stop_poll_selected_activate
)
259 action
= uifactory
.get_action('/feedlist_popup/remove')
260 action
.connect('activate', self
.on_remove_selected_feed
)
261 action
= uifactory
.get_action('/feedlist_popup/properties')
262 action
.connect('activate', self
.on_display_properties_feed
)
263 self
.popup
= uifactory
.get_popup('/feedlist_popup')
265 treeview
= self
._widget
267 treeview
.enable_model_drag_source(gtk
.gdk
.BUTTON1_MASK
, [("drop_yes", 0, 0)], gtk
.gdk
.ACTION_MOVE
)
268 treeview
.enable_model_drag_dest([("drop_yes", 0, 0)], gtk
.gdk
.ACTION_MOVE
)
269 treeview
.connect("drag_data_received", self
._on
_dragdata
_received
)
270 treeview
.connect("drag_motion", self
._on
_drag
_motion
)
272 def _on_drag_motion(self
, treeview
, drag_context
, x
, y
, eventtime
):
273 temp
= treeview
.get_dest_row_at_pos(x
, y
)
278 model
= treeview
.get_model()
279 drop_path
, drop_position
= temp
281 # FIXME: Here we use only first selected node of possibly multiple
282 # selected. See fixme comment in self._on_dragdata_received.
283 source_node
= [tv_node
for tv_node
in self
.selected()][0]
285 drop_node
= model
[drop_path
][Column
.object]
286 source_path
= source_node
.path_list
288 sane_drop_path
= self
._check
_drop
_path
(model
, source_path
, drop_path
)
290 is_drop_into
= drop_position
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
or \
291 drop_position
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
293 can_drop_into
= drop_node
.node
.is_parent()
297 if sane_drop_path
and ((is_drop_into
and can_drop_into
) or not is_drop_into
):
298 treeview
.enable_model_drag_dest([("drop_yes", 0, 0)], gtk
.gdk
.ACTION_MOVE
)
300 treeview
.enable_model_drag_dest([("drop_no", 0, 0)], gtk
.gdk
.ACTION_MOVE
)
302 def _on_dragdata_received(self
, treeview
, drag_context
, x
, y
, selection
, info
, eventtime
):
303 model
, pathlist
= treeview
.get_selection().get_selected_rows()
305 if len(pathlist
) > 1:
306 # FIXME: Maybe we want to support drag and drop for multiple rows?
307 # For now it's not worth it while there are other things to do.
308 drag_context
.finish(False, False, eventtime
)
311 source_path
= pathlist
[0]
312 source_iter
= model
.get_iter(source_path
)
314 temp
= treeview
.get_dest_row_at_pos(x
, y
)
317 drop_path
, drop_pos
= temp
319 drop_path
, drop_pos
= (len(model
) - 1,), gtk
.TREE_VIEW_DROP_AFTER
321 effective_drop_path
= self
._calculate
_effective
_drop
_path
(source_path
, drop_path
, drop_pos
)
323 if source_path
== effective_drop_path
:
324 drag_context
.finish(False, False, eventtime
)
327 drop_iter
= model
.get_iter(drop_path
)
329 if not self
._check
_drop
_path
(model
, source_path
, drop_path
):
330 drag_context
.finish(False, False, eventtime
)
333 node
= model
[source_path
][Column
.object].node
334 self
._iter
_copy
(model
, source_iter
, drop_iter
, drop_pos
)
336 drag_context
.finish(True, True, eventtime
)
337 FeedManager
.move_node(node
, effective_drop_path
)
339 def _check_drop_path(self
, model
, source_path
, drop_path
):
341 Verifies if drop path is not within the subree of source path so that
342 we can disallow dropping parent into its own subtree etc. using this check.
345 return list(drop_path
[0:len(source_path
)]) != list(source_path
)
347 def _iter_copy(self
, model
, iter_to_copy
, target_iter
, pos
):
349 Recursively copies GTK TreeView iters from source to target using
350 GTK relative data about drag and drop operation.
353 path
= model
.get_path(iter_to_copy
)
355 if (pos
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
) or (pos
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
):
356 new_iter
= model
.prepend(target_iter
, model
[path
])
357 elif pos
== gtk
.TREE_VIEW_DROP_BEFORE
:
358 new_iter
= model
.insert_before(None, target_iter
, model
[path
])
359 elif pos
== gtk
.TREE_VIEW_DROP_AFTER
:
360 new_iter
= model
.insert_after(None, target_iter
, model
[path
])
362 n
= model
.iter_n_children(iter_to_copy
)
365 next_iter_to_copy
= model
.iter_nth_child(iter_to_copy
, n
- i
- 1)
366 self
._iter
_copy
(model
, next_iter_to_copy
, new_iter
, gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
)
368 def _calculate_effective_drop_path(self
, source_path
, drop_path
, drop_pos
):
370 Calculate effective absolute drop path given drop_pos and source/destination
371 of drag and drop operation. GTK uses relative terms for describing drop
372 destination (after/before/into etc.) while we prefer absolute drop path
373 and we can take care of reordering ourselves.
376 result
= list(drop_path
)
377 same_level
= len(source_path
) == len(drop_path
) and source_path
[:-1] == drop_path
[:-1]
379 if drop_pos
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
:
381 elif drop_pos
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
:
383 elif drop_pos
== gtk
.TREE_VIEW_DROP_BEFORE
:
384 if not same_level
or (same_level
and source_path
[-1] < drop_path
[-1]):
387 elif drop_pos
== gtk
.TREE_VIEW_DROP_AFTER
:
388 if not same_level
or (same_level
and source_path
[-1] > drop_path
[-1]):
393 def _model_set(self
):
394 self
._widget
.set_model(self
._model
.model
)
396 def add_selection_changed_listener(self
, listener
):
397 selection
= self
._widget
.get_selection()
398 selection
.connect('changed', listener
.feedlist_selection_changed
, Column
.object)
400 def _on_popup_menu(self
, treeview
, *args
):
401 self
.popup
.popup(None, None, None, 0, 0)
403 def _on_button_press_event(self
, treeview
, event
):
406 if event
.button
== 3:
409 time
= gtk
.get_current_event_time()
410 path
= treeview
.get_path_at_pos(x
, y
)
416 self
.node_at_popup
= self
.model
.store
[path
][Column
.object]
417 treeview
.grab_focus()
419 if self
.selected_count() < 2:
420 selection
= treeview
.get_selection()
421 selection
.unselect_all()
422 selection
.select_path(path
)
424 self
.popup
.popup(None, None, None, event
.button
, time
)
429 def selected_count(self
):
430 selection
= self
._widget
.get_selection()
431 pathlist
= selection
.get_selected_rows()[1]
435 selection
= self
._widget
.get_selection()
436 (model
, pathlist
) = selection
.get_selected_rows()
437 nodes
= [model
[path
][Column
.object] for path
in pathlist
]
439 for tv_node
in nodes
:
442 def foreach_selected(self
, func
):
443 selection
= self
._widget
.get_selection()
444 (model
, pathlist
) = selection
.get_selected_rows()
445 iters
= [model
.get_iter(path
) for path
in pathlist
]
447 for treeiter
in iters
:
448 object = model
.get_value(treeiter
, Column
.object)
449 func(object, model
, treeiter
)
450 except TypeError, te
:
451 logging
.exception(te
)
453 def on_menu_add_child_activate(self
, *args
):
454 self
.begin_add_category(self
.node_at_popup
.node
)
456 def begin_add_category(self
, node
):
457 category
= Category()
458 category
.parent
= node
459 category
.norder
= len(node
.children
)
460 self
.new_child
= TreeViewNode(category
, self
.model
.store
)
461 iter = self
.model
._create
_row
(self
.new_child
, editable
= True)
462 path
= self
.model
.store
.get_path(iter)
463 column
= self
._widget
.get_column(0)
465 parent_path
= self
.new_child
.parent_path
468 self
._widget
.expand_row(parent_path
, False)
470 self
._widget
.set_cursor_on_cell(path
, focus_column
= column
, start_editing
= True)
472 def on_menu_poll_selected_activate(self
, *args
):
473 config
= Config
.get_instance()
475 if config
.offline
: #XXX
476 config
.offline
= not config
.offline
478 FeedManager
.update_nodes([node
.node
for node
in self
.selected()])
480 def on_menu_stop_poll_selected_activate(self
, *args
):
481 self
.foreach_selected(lambda o
,*args
: o
.feed
.router
.stop_polling())
483 def on_menu_mark_all_as_read_activate(self
, *args
):
484 self
.foreach_selected(lambda o
,*args
: o
.feed
.mark_items_as_read())
486 def on_remove_selected_feed(self
, *args
):
487 nodes
= [tv_node
for tv_node
in self
.selected()]
489 FeedManager
.delete_nodes([tv_node
.node
.id for tv_node
in nodes
])
495 self
.model
.store
.remove(iter)
497 def on_node_edit_title_canceled(self
, cellrenderer
):
499 debug("on_node_edit_title_canceled (new_child), removing %s" % str(self
.new_child
.path
))
500 self
.model
.store
.remove(self
.new_child
.iter)
501 self
.new_child
= None
503 def on_node_edit_title_edited(self
, cellrenderertext
, path
, new_text
):
504 if len(new_text
) > 0:
505 self
.new_child
.node
.name
= new_text
506 FeedManager
.save_category(self
.new_child
.node
)
508 self
.model
.store
.remove(self
.new_child
.iter)
509 self
.new_child
= None
511 def on_display_properties_feed(self
, *args
):
512 selection
= self
._widget
.get_selection()
513 (model
, pathlist
) = selection
.get_selected_rows()
514 iters
= [model
.get_iter(path
) for path
in pathlist
]
515 path
= pathlist
.pop()
516 node
= self
.model
.model
[path
][Column
.object]
517 self
._presenter
.show_feed_information(node
)
519 def add_category(self
):
520 self
.begin_add_category(self
._model
.tv_nodes
[1].node
)
522 def select_first_feed(self
):
523 selection
= self
._widget
.get_selection()
524 (model
, pathlist
) = selection
.get_selected_rows()
525 treeiter
= model
.get_iter_first()
526 if not treeiter
or not model
.iter_is_valid(treeiter
):
528 self
.set_cursor(treeiter
)
531 def select_next_feed(self
, with_unread
=False):
532 ''' Scrolls to the next feed in the feed list
534 If there is no selection, selects the first feed. If multiple feeds
535 are selected, selects the feed after the last selected feed.
537 If unread is True, selects the next unread with unread items.
539 If the selection next-to-be is a category, go to the iter its first
540 child. If current selection is a child, then go to (parent + 1),
541 provided that (parent + 1) is not a category.
544 def next(model
, current
):
545 treeiter
= model
.iter_next(current
)
546 if not treeiter
: return False
547 if model
.iter_depth(current
): next(model
, model
.iter_parent(current
))
548 path
= model
.get_path(treeiter
)
549 if with_unread
and model
[path
][Column
.unread
] < 1:
551 self
.set_cursor(treeiter
)
553 selection
= self
._widget
.get_selection()
554 (model
, pathlist
) = selection
.get_selected_rows()
555 iters
= [model
.get_iter(path
) for path
in pathlist
]
557 current
= iters
.pop()
558 if model
.iter_has_child(current
):
559 iterchild
= model
.iter_children(current
)
560 # make the row visible
561 path
= model
.get_path(iterchild
)
562 for i
in range(len(path
)):
563 self
._widget
.expand_row(path
[:i
+1], False)
564 # select his first born child
565 if with_unread
and model
[path
][Column
.unread
] > 0:
566 self
.set_cursor(iterchild
)
569 has_unread
= next(model
, current
)
570 has_unread
= next(model
,current
)
572 self
.set_cursor(model
.get_iter_first())
576 def select_previous_feed(self
):
577 ''' Scrolls to the previous feed in the feed list.
579 If there is no selection, selects the first feed. If there's multiple
580 selection, selects the feed before the first selected feed.
582 If the previous selection is a category, select the last node in that
583 category. If the current selection is a child, then go to (parent -
584 1). If parent is the first feed, wrap and select the last feed or
585 category in the list.
587 def previous(model
, current
):
588 path
= model
.get_path(current
)
589 treerow
= model
[path
[-1]-1]
590 self
.set_cursor(treerow
.iter)
591 selection
= self
._widget
.get_selection()
592 (model
, pathlist
) = selection
.get_selected_rows()
593 iters
= [model
.get_iter(path
) for path
in pathlist
]
595 current_first
= iters
.pop(0)
596 if model
.iter_has_child(current_first
):
597 children
= model
.iter_n_children(current_first
)
598 treeiter
= model
.iter_nth_child(children
- 1)
599 self
.set_cursor(treeiter
)
601 previous(model
, current_first
)
603 self
.set_cursor(model
.get_iter_first())
606 def set_cursor(self
, treeiter
, col_id
=None, edit
=False):
611 path
= self
._model
.model
.get_path(treeiter
)
614 column
= self
._widget
.get_column(col_id
)
616 self
._widget
.set_cursor(path
, column
, edit
)
617 self
._widget
.scroll_to_cell(path
, column
)
618 self
._widget
.grab_focus()
620 class FeedsPresenter(MVP
.BasicPresenter
):
621 def _initialize(self
):
622 self
.model
= FeedListModel()
625 def _init_signals(self
):
628 def add_category(self
):
629 self
.view
.add_category()
631 def select_first_feed(self
):
632 return self
.view
.select_first_feed()
634 def select_next_feed(self
, with_unread
=False):
635 return self
.view
.select_next_feed(with_unread
)
637 def select_previous_feed(self
):
638 return self
.view
.select_previous_feed()
640 def _sort_func(self
, model
, a
, b
):
642 Sorts the feeds lexically.
644 From the gtk.TreeSortable.set_sort_func doc:
646 The comparison callback should return -1 if the iter1 row should come before
647 the iter2 row, 0 if the rows are equal, or 1 if the iter1 row should come
651 fa
= model
.get_value(a
, Column
.OBJECT
)
652 fb
= model
.get_value(b
, Column
.OBJECT
)
655 retval
= locale
.strcoll(fa
.title
, fb
.title
)
656 elif fa
is not None: retval
= -1
657 elif fb
is not None: retval
= 1
660 def show_feed_information(self
, feed
):
661 feed_properties_show(feed
.node
)