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 Constants
import *
21 from error
import debug
22 from model
import Feed
, Item
, Category
26 import categoryproperties
31 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
):
43 self
.node
.connect("notify", self
._on
_unread
_count
_changed
)
45 def _on_unread_count_changed(self
, node
, delta
):
54 self
.store
.set(iter, Column
.pixbuf
, self
.pixbuf
)
55 self
.store
.set(iter, Column
.unread
, self
.node
.unread_count
)
56 self
.store
.set(iter, Column
.name
, self
.title
)
60 ''' The title of the node be it a category or a feed '''
62 if self
.node
.type == "C":
63 title
= self
.node
.name
64 elif self
.node
.type == "F":
65 title
= self
.node
.title
67 title
= helpers
.pango_escape(title
)
69 if hasattr(self
.node
, "status") and (self
.node
.status
& FS_UPDATING
):
71 title
= "<i>" + title
+ "</i>"
76 def unread_count(self
):
77 ''' The title of the node be it a category or a feed '''
78 return self
.node
.unread_count
82 ''' gets the pixbuf to display according to the status of the feed '''
85 if isinstance(self
.node
, Feed
):
86 if self
.node
.status
== FS_ERROR
:
87 return _tmp_widget
.render_icon(gtk
.STOCK_CANCEL
, gtk
.ICON_SIZE_MENU
)
89 return _tmp_widget
.render_icon(gtk
.STOCK_FILE
, gtk
.ICON_SIZE_MENU
)
91 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
136 return ":".join(map(str, path
))
139 def parent_iter(self
):
140 path
= self
.parent_path
145 return self
.store
.get_iter_from_string(path
)
148 ''' The model for the feed list view '''
154 def refresh_tree(self
):
155 self
.appmodel
= FeedManager
.get_model()
157 self
._prepare
_store
()
158 self
._prepare
_model
()
160 self
._populate
_tree
(1, None, [])
162 def _init_signals(self
):
163 FeedManager
._get
_instance
().connect("feed-added", self
._on
_node
_added
)
164 FeedManager
._get
_instance
().connect("feed-status-changed", self
._on
_feed
_status
_changed
)
165 FeedManager
._get
_instance
().connect("category-added", self
._on
_node
_added
)
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
,
194 def _on_node_added(self
, src
, node
):
195 tv_node
= TreeViewNode(node
, self
.store
)
196 self
.add_node(tv_node
)
197 self
._create
_row
(tv_node
)
199 def _on_feed_status_changed(self
, src
, feed
):
200 self
.tv_nodes
[feed
.id].refresh()
202 def add_node(self
, tv_node
):
203 self
.tv_nodes
[tv_node
.node
.id] = tv_node
209 def search(self
, rows
, func
, data
):
210 if not rows
: return None
214 result
= self
.search(row
.iterchildren(), func
, data
)
215 if result
: return result
218 class FeedsView(MVP
.WidgetView
):
219 def _initialize(self
):
220 self
._widget
.set_search_column(Column
.name
)
223 column
= gtk
.TreeViewColumn()
224 unread_renderer
= gtk
.CellRendererText()
225 column
.pack_start(unread_renderer
, False)
226 column
.set_attributes(unread_renderer
, text
= Column
.unread
)
228 status_renderer
= gtk
.CellRendererPixbuf()
229 column
.pack_start(status_renderer
, False)
230 column
.set_attributes(status_renderer
, pixbuf
= Column
.pixbuf
)
232 # feed title renderer
233 title_renderer
= gtk
.CellRendererText()
235 title_renderer
.connect("edited", self
.on_node_edit_title_edited
)
236 title_renderer
.connect("editing-canceled", self
.on_node_edit_title_canceled
)
238 #title_renderer.set_property('editable', True)
239 column
.pack_start(title_renderer
, False)
240 column
.set_attributes(title_renderer
,
241 foreground
=Column
.foreground
,
243 editable
=Column
.editable
) #, weight=Column.BOLD)
245 self
._widget
.append_column(column
)
247 selection
= self
._widget
.get_selection()
248 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
250 self
._widget
.connect("button_press_event", self
._on
_button
_press
_event
)
251 self
._widget
.connect("popup-menu", self
._on
_popup
_menu
)
253 uifactory
= helpers
.UIFactory('FeedListActions')
254 action
= uifactory
.get_action('/feedlist_popup/refresh')
255 action
.connect('activate', self
.on_menu_poll_selected_activate
)
256 action
= uifactory
.get_action('/feedlist_popup/add_child')
257 action
.connect('activate', self
.on_menu_add_child_activate
)
258 action
= uifactory
.get_action('/feedlist_popup/mark_as_read')
259 action
.connect('activate', self
.on_menu_mark_all_as_read_activate
)
260 action
= uifactory
.get_action('/feedlist_popup/stop_refresh')
261 action
.connect('activate', self
.on_menu_stop_poll_selected_activate
)
262 action
= uifactory
.get_action('/feedlist_popup/remove')
263 action
.connect('activate', self
.on_remove_selected_feed
)
264 action
= uifactory
.get_action('/feedlist_popup/properties')
265 action
.connect('activate', self
.on_display_properties_feed
)
266 self
.popup
= uifactory
.get_popup('/feedlist_popup')
268 treeview
= self
._widget
270 treeview
.enable_model_drag_source(gtk
.gdk
.BUTTON1_MASK
, [("drop_yes", 0, 0)], gtk
.gdk
.ACTION_MOVE
)
271 treeview
.enable_model_drag_dest([("drop_yes", 0, 0)], gtk
.gdk
.ACTION_MOVE
)
272 treeview
.connect("drag_data_received", self
._on
_dragdata
_received
)
273 treeview
.connect("drag_motion", self
._on
_drag
_motion
)
275 def _on_drag_motion(self
, treeview
, drag_context
, x
, y
, eventtime
):
276 temp
= treeview
.get_dest_row_at_pos(x
, y
)
281 model
= treeview
.get_model()
282 drop_path
, drop_position
= temp
284 # FIXME: Here we use only first selected node of possibly multiple
285 # selected. See fixme comment in self._on_dragdata_received.
286 source_node
= [tv_node
for tv_node
in self
.selected()][0]
288 drop_node
= model
[drop_path
][Column
.object]
289 source_path
= source_node
.path_list
291 sane_drop_path
= self
._check
_drop
_path
(model
, source_path
, drop_path
)
293 is_drop_into
= drop_position
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
or \
294 drop_position
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
296 can_drop_into
= drop_node
.node
.is_parent()
298 if sane_drop_path
and ((is_drop_into
and can_drop_into
) or not is_drop_into
):
299 treeview
.enable_model_drag_dest([("drop_yes", 0, 0)], gtk
.gdk
.ACTION_MOVE
)
301 treeview
.enable_model_drag_dest([("drop_no", 0, 0)], gtk
.gdk
.ACTION_MOVE
)
303 def _on_dragdata_received(self
, treeview
, drag_context
, x
, y
, selection
, info
, eventtime
):
304 model
, pathlist
= treeview
.get_selection().get_selected_rows()
306 if len(pathlist
) > 1:
307 # FIXME: Maybe we want to support drag and drop for multiple rows?
308 # For now it's not worth it while there are other things to do.
309 drag_context
.finish(False, False, eventtime
)
312 source_path
= pathlist
[0]
313 source_iter
= model
.get_iter(source_path
)
315 temp
= treeview
.get_dest_row_at_pos(x
, y
)
318 drop_path
, drop_pos
= temp
320 drop_path
, drop_pos
= (len(model
) - 1,), gtk
.TREE_VIEW_DROP_AFTER
322 effective_drop_path
= self
._calculate
_effective
_drop
_path
(source_path
, drop_path
, drop_pos
)
324 if source_path
== effective_drop_path
:
325 drag_context
.finish(False, False, eventtime
)
328 drop_iter
= model
.get_iter(drop_path
)
330 if not self
._check
_drop
_path
(model
, source_path
, drop_path
):
331 drag_context
.finish(False, False, eventtime
)
334 node
= model
[source_path
][Column
.object].node
335 self
._iter
_copy
(model
, source_iter
, drop_iter
, drop_pos
)
337 drag_context
.finish(True, True, eventtime
)
338 FeedManager
.move_node(node
, effective_drop_path
)
340 def _check_drop_path(self
, model
, source_path
, drop_path
):
342 Verifies if a drop path is not within the subtree of source path so that
343 we can disallow dropping parent into its own subtree etc. using this check.
346 return list(drop_path
[0:len(source_path
)]) != list(source_path
)
348 def _iter_copy(self
, model
, iter_to_copy
, target_iter
, pos
):
350 Recursively copies GTK TreeView iters from source to target using
351 GTK relative data about drag and drop operation.
354 path
= model
.get_path(iter_to_copy
)
356 if (pos
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
) or (pos
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
):
357 new_iter
= model
.prepend(target_iter
, model
[path
])
358 elif pos
== gtk
.TREE_VIEW_DROP_BEFORE
:
359 new_iter
= model
.insert_before(None, target_iter
, model
[path
])
360 elif pos
== gtk
.TREE_VIEW_DROP_AFTER
:
361 new_iter
= model
.insert_after(None, target_iter
, model
[path
])
363 n
= model
.iter_n_children(iter_to_copy
)
366 next_iter_to_copy
= model
.iter_nth_child(iter_to_copy
, n
- i
- 1)
367 self
._iter
_copy
(model
, next_iter_to_copy
, new_iter
, gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
)
369 def _calculate_effective_drop_path(self
, source_path
, drop_path
, drop_pos
):
371 Calculate effective absolute drop path given drop_pos and source/destination
372 of drag and drop operation. GTK uses relative terms for describing drop
373 destination (after/before/into etc.) while we prefer absolute drop path
374 and we can take care of reordering ourselves.
377 result
= list(drop_path
)
378 same_level
= len(source_path
) == len(drop_path
) and source_path
[:-1] == drop_path
[:-1]
380 if drop_pos
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
:
382 elif drop_pos
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
:
384 elif drop_pos
== gtk
.TREE_VIEW_DROP_BEFORE
:
385 if not same_level
or (same_level
and source_path
[-1] < drop_path
[-1]):
388 elif drop_pos
== gtk
.TREE_VIEW_DROP_AFTER
:
389 if not same_level
or (same_level
and source_path
[-1] > drop_path
[-1]):
394 def _model_set(self
):
395 self
._widget
.set_model(self
._model
.model
)
397 def add_selection_changed_listener(self
, listener
):
398 selection
= self
._widget
.get_selection()
399 selection
.connect('changed', listener
.feedlist_selection_changed
, Column
.object)
401 def _on_popup_menu(self
, treeview
, *args
):
402 self
.popup
.popup(None, None, None, 0, 0)
404 def _on_button_press_event(self
, treeview
, event
):
407 if event
.button
== 3:
410 time
= gtk
.get_current_event_time()
411 path
= treeview
.get_path_at_pos(x
, y
)
417 self
.node_at_popup
= self
.model
.store
[path
][Column
.object]
418 treeview
.grab_focus()
420 if self
.selected_count() < 2:
421 selection
= treeview
.get_selection()
422 selection
.unselect_all()
423 selection
.select_path(path
)
425 self
.popup
.popup(None, None, None, event
.button
, time
)
430 def get_selected_node(self
):
431 nodes
= [node
for node
in self
.selected()]
438 def get_expanded_nodes(self
):
441 def add(treeview
, path
, expanded
):
442 node
= treeview
.get_model()[path
][Column
.object].node
443 expanded
.append(node
)
445 self
._widget
.map_expanded_rows(add
, expanded
)
449 def expand_nodes(self
, nodes
):
450 for node_id
in nodes
:
451 if node_id
in self
.model
.tv_nodes
:
452 path
= self
.model
.tv_nodes
[node_id
].path
453 self
._widget
.expand_row(path
, False)
455 def select_node(self
, id):
456 if not id in self
.model
.tv_nodes
:
459 path
= self
.model
.tv_nodes
[id].path
464 selection
= self
._widget
.get_selection()
465 selection
.unselect_all()
466 self
._widget
.expand_to_path(path
)
467 selection
.select_path(path
)
468 self
._widget
.grab_focus()
470 def selected_count(self
):
471 selection
= self
._widget
.get_selection()
472 pathlist
= selection
.get_selected_rows()[1]
476 selection
= self
._widget
.get_selection()
477 (model
, pathlist
) = selection
.get_selected_rows()
478 nodes
= [model
[path
][Column
.object] for path
in pathlist
]
480 for tv_node
in nodes
:
483 def foreach_selected(self
, func
):
484 selection
= self
._widget
.get_selection()
485 (model
, pathlist
) = selection
.get_selected_rows()
486 iters
= [model
.get_iter(path
) for path
in pathlist
]
488 for treeiter
in iters
:
489 object = model
.get_value(treeiter
, Column
.object)
490 func(object, model
, treeiter
)
491 except TypeError, te
:
492 logging
.exception(te
)
494 def on_menu_add_child_activate(self
, *args
):
495 node
= self
.node_at_popup
.node
497 if not self
.node_at_popup
.node
.is_parent():
500 self
.begin_add_category(node
)
502 def begin_add_category(self
, node
):
503 category
= Category()
504 category
.parent
= node
505 category
.norder
= len(node
.children
)
506 self
.new_child
= TreeViewNode(category
, self
.model
.store
)
507 iter = self
.model
._create
_row
(self
.new_child
, editable
= True)
508 path
= self
.model
.store
.get_path(iter)
509 column
= self
._widget
.get_column(0)
511 parent_path
= self
.new_child
.parent_path
514 self
._widget
.expand_row(parent_path
, False)
516 self
._widget
.set_cursor_on_cell(path
, focus_column
= column
, start_editing
= True)
518 def on_menu_poll_selected_activate(self
, *args
):
519 """config = Config.get_instance()
521 if config.offline: #XXX
522 config.offline = not config.offline"""
524 FeedManager
.update_nodes([node
.node
for node
in self
.selected()])
526 def on_menu_stop_poll_selected_activate(self
, *args
):
527 self
.foreach_selected(lambda o
,*args
: o
.feed
.router
.stop_polling())
529 def on_menu_mark_all_as_read_activate(self
, *args
):
530 self
.foreach_selected(lambda o
,*args
: o
.feed
.mark_items_as_read())
532 def on_remove_selected_feed(self
, *args
):
533 nodes
= [tv_node
for tv_node
in self
.selected()]
535 FeedManager
.delete_nodes([tv_node
.node
.id for tv_node
in nodes
])
541 self
.model
.store
.remove(iter)
543 def on_node_edit_title_canceled(self
, cellrenderer
):
545 debug("on_node_edit_title_canceled (new_child), removing %s" % str(self
.new_child
.path
))
546 self
.model
.store
.remove(self
.new_child
.iter)
547 self
.new_child
= None
549 def on_node_edit_title_edited(self
, cellrenderertext
, path
, new_text
):
550 if len(new_text
) > 0:
551 self
.new_child
.node
.name
= new_text
552 FeedManager
.save_category(self
.new_child
.node
)
554 self
.model
.store
.remove(self
.new_child
.iter)
555 self
.new_child
= None
557 def on_display_properties_feed(self
, *args
):
558 selected_tv_node
= [tv_node
for tv_node
in self
.selected()][0]
559 self
._presenter
.show_feed_information(selected_tv_node
.node
)
561 def add_category(self
):
562 self
.begin_add_category(self
._model
.tv_nodes
[1].node
)
564 def select_first_feed(self
):
565 selection
= self
._widget
.get_selection()
566 (model
, pathlist
) = selection
.get_selected_rows()
567 treeiter
= model
.get_iter_first()
568 if not treeiter
or not model
.iter_is_valid(treeiter
):
570 self
.set_cursor(treeiter
)
573 def select_next_feed(self
, with_unread
=False):
574 ''' Scrolls to the next feed in the feed list
576 If there is no selection, selects the first feed. If multiple feeds
577 are selected, selects the feed after the last selected feed.
579 If unread is True, selects the next unread with unread items.
581 If the selection next-to-be is a category, go to the iter its first
582 child. If current selection is a child, then go to (parent + 1),
583 provided that (parent + 1) is not a category.
586 def next(model
, current
):
587 treeiter
= model
.iter_next(current
)
588 if not treeiter
: return False
589 if model
.iter_depth(current
): next(model
, model
.iter_parent(current
))
590 path
= model
.get_path(treeiter
)
591 if with_unread
and model
[path
][Column
.unread
] < 1:
593 self
.set_cursor(treeiter
)
595 selection
= self
._widget
.get_selection()
596 (model
, pathlist
) = selection
.get_selected_rows()
597 iters
= [model
.get_iter(path
) for path
in pathlist
]
599 current
= iters
.pop()
600 if model
.iter_has_child(current
):
601 iterchild
= model
.iter_children(current
)
602 # make the row visible
603 path
= model
.get_path(iterchild
)
604 for i
in range(len(path
)):
605 self
._widget
.expand_row(path
[:i
+1], False)
606 # select his first born child
607 if with_unread
and model
[path
][Column
.unread
] > 0:
608 self
.set_cursor(iterchild
)
611 has_unread
= next(model
, current
)
612 has_unread
= next(model
,current
)
614 self
.set_cursor(model
.get_iter_first())
618 def select_previous_feed(self
):
619 ''' Scrolls to the previous feed in the feed list.
621 If there is no selection, selects the first feed. If there's multiple
622 selection, selects the feed before the first selected feed.
624 If the previous selection is a category, select the last node in that
625 category. If the current selection is a child, then go to (parent -
626 1). If parent is the first feed, wrap and select the last feed or
627 category in the list.
629 def previous(model
, current
):
630 path
= model
.get_path(current
)
631 treerow
= model
[path
[-1]-1]
632 self
.set_cursor(treerow
.iter)
633 selection
= self
._widget
.get_selection()
634 (model
, pathlist
) = selection
.get_selected_rows()
635 iters
= [model
.get_iter(path
) for path
in pathlist
]
637 current_first
= iters
.pop(0)
638 if model
.iter_has_child(current_first
):
639 children
= model
.iter_n_children(current_first
)
640 treeiter
= model
.iter_nth_child(children
- 1)
641 self
.set_cursor(treeiter
)
643 previous(model
, current_first
)
645 self
.set_cursor(model
.get_iter_first())
648 def set_cursor(self
, treeiter
, col_id
=None, edit
=False):
653 path
= self
._model
.model
.get_path(treeiter
)
656 column
= self
._widget
.get_column(col_id
)
658 self
._widget
.set_cursor(path
, column
, edit
)
659 self
._widget
.scroll_to_cell(path
, column
)
660 self
._widget
.grab_focus()
662 class FeedsPresenter(MVP
.BasicPresenter
):
663 def _initialize(self
):
664 self
.model
= FeedListModel()
666 def store_state(self
):
667 node
= self
.view
.get_selected_node()
673 Config
.set(OPTION_LAST_SELECTED_NODE
, id)
674 Config
.set(OPTION_LAST_EXPANDED_NODES
, ",".join([str(node
.id) for node
in self
.view
.get_expanded_nodes()]))
676 def restore_state(self
):
677 id = Config
.get(OPTION_LAST_SELECTED_NODE
)
680 self
.view
.select_node(id)
682 expanded
= Config
.get(OPTION_LAST_EXPANDED_NODES
)
686 expanded
= map(int, expanded
.split(","))
690 self
.view
.expand_nodes(expanded
)
692 def add_category(self
):
693 self
.view
.add_category()
695 def select_first_feed(self
):
696 return self
.view
.select_first_feed()
698 def select_next_feed(self
, with_unread
=False):
699 return self
.view
.select_next_feed(with_unread
)
701 def select_previous_feed(self
):
702 return self
.view
.select_previous_feed()
704 def show_feed_information(self
, node
):
706 properties
= categoryproperties
707 elif node
.type == "F":
708 properties
= feedproperties
710 properties
.show(node
)