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.
110 path
= self
.path_list
115 return ":".join(map(str, path
))
125 return self
.store
.get_iter_from_string(path
)
131 def parent_path(self
):
132 path
= self
.path_list
138 return ":".join(map(str, path
))
141 def parent_iter(self
):
142 path
= self
.parent_path
147 return self
.store
.get_iter_from_string(path
)
150 ''' The model for the feed list view '''
156 def refresh_tree(self
):
157 self
.appmodel
= FeedManager
.get_model()
159 self
._prepare
_store
()
160 self
._prepare
_model
()
162 self
._populate
_tree
(1, None, [])
164 def _init_signals(self
):
165 FeedManager
._get
_instance
().connect("feed-added", self
._on
_node
_added
)
166 FeedManager
._get
_instance
().connect("feed-status-changed", self
._on
_feed
_status
_changed
)
167 FeedManager
._get
_instance
().connect("category-added", self
._on
_node
_added
)
169 def _prepare_model(self
):
170 self
.tv_nodes
= dict([(node
.id, TreeViewNode(node
, self
.store
)) for node
in self
.appmodel
.values()])
172 def _prepare_store(self
):
173 self
.store
= gtk
.TreeStore(gtk
.gdk
.Pixbuf
, str, str, str, gobject
.TYPE_PYOBJECT
, bool)
175 def _populate_tree(self
, parent_id
, parent_iter
, done
):
176 for node
in self
.tv_nodes
[parent_id
].node
.children
:
177 tv_node
= self
.tv_nodes
[node
.id]
180 self
._create
_row
(tv_node
)
181 tv_node
.store
= self
.store
182 elif node
.type == "C":
183 current_parent
= self
._create
_row
(tv_node
)
184 tv_node
.store
= self
.store
186 if self
.tv_nodes
.has_key(node
.id):
187 self
._populate
_tree
(node
.id, current_parent
, done
)
189 def _create_row(self
, node
, editable
= False):
190 return self
.store
.append(node
.parent_iter
, [node
.pixbuf
,
196 def _on_node_added(self
, src
, node
):
197 tv_node
= TreeViewNode(node
, self
.store
)
198 self
.add_node(tv_node
)
199 self
._create
_row
(tv_node
)
201 def _on_feed_status_changed(self
, src
, feed
):
202 self
.tv_nodes
[feed
.id].refresh()
204 def add_node(self
, tv_node
):
205 self
.tv_nodes
[tv_node
.node
.id] = tv_node
211 def search(self
, rows
, func
, data
):
212 if not rows
: return None
216 result
= self
.search(row
.iterchildren(), func
, data
)
217 if result
: return result
220 class FeedsView(MVP
.WidgetView
):
221 def _initialize(self
):
222 self
._widget
.set_search_column(Column
.name
)
225 column
= gtk
.TreeViewColumn()
226 unread_renderer
= gtk
.CellRendererText()
227 column
.pack_start(unread_renderer
, False)
228 column
.set_attributes(unread_renderer
, text
= Column
.unread
)
230 status_renderer
= gtk
.CellRendererPixbuf()
231 column
.pack_start(status_renderer
, False)
232 column
.set_attributes(status_renderer
, pixbuf
= Column
.pixbuf
)
234 # feed title renderer
235 title_renderer
= gtk
.CellRendererText()
237 title_renderer
.connect("edited", self
.on_node_edit_title_edited
)
238 title_renderer
.connect("editing-canceled", self
.on_node_edit_title_canceled
)
240 #title_renderer.set_property('editable', True)
241 column
.pack_start(title_renderer
, False)
242 column
.set_attributes(title_renderer
,
243 foreground
=Column
.foreground
,
245 editable
=Column
.editable
) #, weight=Column.BOLD)
247 self
._widget
.append_column(column
)
249 selection
= self
._widget
.get_selection()
250 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
252 self
._widget
.connect("button_press_event", self
._on
_button
_press
_event
)
253 self
._widget
.connect("popup-menu", self
._on
_popup
_menu
)
255 uifactory
= helpers
.UIFactory('FeedListActions')
256 action
= uifactory
.get_action('/feedlist_popup/refresh')
257 action
.connect('activate', self
.on_menu_poll_selected_activate
)
258 action
= uifactory
.get_action('/feedlist_popup/add_child')
259 action
.connect('activate', self
.on_menu_add_child_activate
)
260 self
.mark_all_as_read_action
= uifactory
.get_action('/feedlist_popup/mark_as_read')
261 self
.mark_all_as_read_action
.connect('activate', self
.on_menu_mark_all_as_read_activate
)
262 action
= uifactory
.get_action('/feedlist_popup/stop_refresh')
263 action
.connect('activate', self
.on_menu_stop_poll_selected_activate
)
264 action
= uifactory
.get_action('/feedlist_popup/remove')
265 action
.connect('activate', self
.on_remove_selected_feed
)
266 action
= uifactory
.get_action('/feedlist_popup/properties')
267 action
.connect('activate', self
.on_display_properties_feed
)
268 self
.popup
= uifactory
.get_popup('/feedlist_popup')
270 treeview
= self
._widget
272 treeview
.enable_model_drag_source(gtk
.gdk
.BUTTON1_MASK
, [("drop_yes", 0, 0)], gtk
.gdk
.ACTION_MOVE
)
273 treeview
.enable_model_drag_dest([("drop_yes", 0, 0)], gtk
.gdk
.ACTION_MOVE
)
274 treeview
.connect("drag_data_received", self
._on
_dragdata
_received
)
275 treeview
.connect("drag_motion", self
._on
_drag
_motion
)
277 def _on_drag_motion(self
, treeview
, drag_context
, x
, y
, eventtime
):
278 temp
= treeview
.get_dest_row_at_pos(x
, y
)
283 model
= treeview
.get_model()
284 drop_path
, drop_position
= temp
286 # FIXME: Here we use only first selected node of possibly multiple
287 # selected. See fixme comment in self._on_dragdata_received.
288 source_node
= [tv_node
for tv_node
in self
.selected()][0]
290 drop_node
= model
[drop_path
][Column
.object]
291 source_path
= source_node
.path_list
293 sane_drop_path
= self
._check
_drop
_path
(model
, source_path
, drop_path
)
295 is_drop_into
= drop_position
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
or \
296 drop_position
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
298 can_drop_into
= drop_node
.node
.is_parent()
300 if sane_drop_path
and ((is_drop_into
and can_drop_into
) or not is_drop_into
):
301 treeview
.enable_model_drag_dest([("drop_yes", 0, 0)], gtk
.gdk
.ACTION_MOVE
)
303 treeview
.enable_model_drag_dest([("drop_no", 0, 0)], gtk
.gdk
.ACTION_MOVE
)
305 def _on_dragdata_received(self
, treeview
, drag_context
, x
, y
, selection
, info
, eventtime
):
306 model
, pathlist
= treeview
.get_selection().get_selected_rows()
308 if len(pathlist
) > 1:
309 # FIXME: Maybe we want to support drag and drop for multiple rows?
310 # For now it's not worth it while there are other things to do.
311 drag_context
.finish(False, False, eventtime
)
314 source_path
= pathlist
[0]
315 source_iter
= model
.get_iter(source_path
)
317 temp
= treeview
.get_dest_row_at_pos(x
, y
)
320 drop_path
, drop_pos
= temp
322 drop_path
, drop_pos
= (len(model
) - 1,), gtk
.TREE_VIEW_DROP_AFTER
324 effective_drop_path
= self
._calculate
_effective
_drop
_path
(source_path
, drop_path
, drop_pos
)
326 if source_path
== effective_drop_path
:
327 drag_context
.finish(False, False, eventtime
)
330 drop_iter
= model
.get_iter(drop_path
)
332 if not self
._check
_drop
_path
(model
, source_path
, drop_path
):
333 drag_context
.finish(False, False, eventtime
)
336 node
= model
[source_path
][Column
.object].node
337 self
._iter
_copy
(model
, source_iter
, drop_iter
, drop_pos
)
339 drag_context
.finish(True, True, eventtime
)
340 FeedManager
.move_node(node
, effective_drop_path
)
342 def _check_drop_path(self
, model
, source_path
, drop_path
):
344 Verifies if a drop path is not within the subtree of source path so that
345 we can disallow dropping parent into its own subtree etc. using this check.
348 return list(drop_path
[0:len(source_path
)]) != list(source_path
)
350 def _iter_copy(self
, model
, iter_to_copy
, target_iter
, pos
):
352 Recursively copies GTK TreeView iters from source to target using
353 GTK relative data about drag and drop operation.
356 path
= model
.get_path(iter_to_copy
)
358 if (pos
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
) or (pos
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
):
359 new_iter
= model
.prepend(target_iter
, model
[path
])
360 elif pos
== gtk
.TREE_VIEW_DROP_BEFORE
:
361 new_iter
= model
.insert_before(None, target_iter
, model
[path
])
362 elif pos
== gtk
.TREE_VIEW_DROP_AFTER
:
363 new_iter
= model
.insert_after(None, target_iter
, model
[path
])
365 n
= model
.iter_n_children(iter_to_copy
)
368 next_iter_to_copy
= model
.iter_nth_child(iter_to_copy
, n
- i
- 1)
369 self
._iter
_copy
(model
, next_iter_to_copy
, new_iter
, gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
)
371 def _calculate_effective_drop_path(self
, source_path
, drop_path
, drop_pos
):
373 Calculate effective absolute drop path given drop_pos and source/destination
374 of drag and drop operation. GTK uses relative terms for describing drop
375 destination (after/before/into etc.) while we prefer absolute drop path
376 and we can take care of reordering ourselves.
379 result
= list(drop_path
)
380 same_level
= len(source_path
) == len(drop_path
) and source_path
[:-1] == drop_path
[:-1]
382 if drop_pos
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
:
384 elif drop_pos
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
:
386 elif drop_pos
== gtk
.TREE_VIEW_DROP_BEFORE
:
387 if not same_level
or (same_level
and source_path
[-1] < drop_path
[-1]):
390 elif drop_pos
== gtk
.TREE_VIEW_DROP_AFTER
:
391 if not same_level
or (same_level
and source_path
[-1] > drop_path
[-1]):
396 def _model_set(self
):
397 self
._widget
.set_model(self
._model
.model
)
399 def add_selection_changed_listener(self
, listener
):
400 selection
= self
._widget
.get_selection()
401 selection
.connect('changed', listener
.feedlist_selection_changed
, Column
.object)
403 def add_mark_all_as_read_listener(self
, listener
):
404 self
.mark_all_as_read_action
.connect('activate', listener
.on_mark_all_as_read
)
406 def _on_popup_menu(self
, treeview
, *args
):
407 self
.popup
.popup(None, None, None, 0, 0)
409 def _on_button_press_event(self
, treeview
, event
):
412 if event
.button
== 3:
415 time
= gtk
.get_current_event_time()
416 path
= treeview
.get_path_at_pos(x
, y
)
422 self
.node_at_popup
= self
.model
.store
[path
][Column
.object]
423 treeview
.grab_focus()
425 if self
.selected_count() < 2:
426 selection
= treeview
.get_selection()
427 selection
.unselect_all()
428 selection
.select_path(path
)
430 self
.popup
.popup(None, None, None, event
.button
, time
)
435 def get_selected_node(self
):
436 nodes
= [node
for node
in self
.selected()]
443 def get_expanded_nodes(self
):
446 def add(treeview
, path
, expanded
):
447 node
= treeview
.get_model()[path
][Column
.object].node
448 expanded
.append(node
)
450 self
._widget
.map_expanded_rows(add
, expanded
)
454 def expand_nodes(self
, nodes
):
455 for node_id
in nodes
:
456 if node_id
in self
.model
.tv_nodes
:
457 path
= self
.model
.tv_nodes
[node_id
].path
458 self
._widget
.expand_row(path
, False)
460 def select_node(self
, id):
461 if not id in self
.model
.tv_nodes
:
464 path
= self
.model
.tv_nodes
[id].path
469 selection
= self
._widget
.get_selection()
470 selection
.unselect_all()
471 self
._widget
.expand_to_path(path
)
472 selection
.select_path(path
)
473 self
._widget
.grab_focus()
475 def selected_count(self
):
476 selection
= self
._widget
.get_selection()
477 pathlist
= selection
.get_selected_rows()[1]
481 selection
= self
._widget
.get_selection()
482 (model
, pathlist
) = selection
.get_selected_rows()
483 nodes
= [model
[path
][Column
.object] for path
in pathlist
]
485 for tv_node
in nodes
:
488 def foreach_selected(self
, func
):
489 selection
= self
._widget
.get_selection()
490 (model
, pathlist
) = selection
.get_selected_rows()
491 iters
= [model
.get_iter(path
) for path
in pathlist
]
493 for treeiter
in iters
:
494 object = model
.get_value(treeiter
, Column
.object)
495 func(object, model
, treeiter
)
496 except TypeError, te
:
497 logging
.exception(te
)
499 def on_menu_add_child_activate(self
, *args
):
500 node
= self
.node_at_popup
.node
502 if not self
.node_at_popup
.node
.is_parent():
505 self
.begin_add_category(node
)
507 def begin_add_category(self
, node
):
508 category
= Category()
509 category
.parent
= node
510 category
.norder
= len(node
.children
)
511 self
.new_child
= TreeViewNode(category
, self
.model
.store
)
512 iter = self
.model
._create
_row
(self
.new_child
, editable
= True)
513 path
= self
.model
.store
.get_path(iter)
514 column
= self
._widget
.get_column(0)
516 parent_path
= self
.new_child
.parent_path
519 self
._widget
.expand_row(parent_path
, False)
521 self
._widget
.set_cursor_on_cell(path
, focus_column
= column
, start_editing
= True)
523 def on_menu_poll_selected_activate(self
, *args
):
524 """config = Config.get_instance()
526 if config.offline: #XXX
527 config.offline = not config.offline"""
529 FeedManager
.update_nodes([node
.node
for node
in self
.selected()])
531 def on_menu_stop_poll_selected_activate(self
, *args
):
532 self
.foreach_selected(lambda o
,*args
: o
.feed
.router
.stop_polling())
534 def on_menu_mark_all_as_read_activate(self
, *args
):
535 self
.foreach_selected(lambda o
, *args
: o
.node
.mark_items_as_read())
537 def on_remove_selected_feed(self
, *args
):
538 nodes
= [tv_node
for tv_node
in self
.selected()]
540 FeedManager
.delete_nodes([tv_node
.node
.id for tv_node
in nodes
])
546 self
.model
.store
.remove(iter)
548 def on_node_edit_title_canceled(self
, cellrenderer
):
550 debug("on_node_edit_title_canceled (new_child), removing %s" % str(self
.new_child
.path
))
551 self
.model
.store
.remove(self
.new_child
.iter)
552 self
.new_child
= None
554 def on_node_edit_title_edited(self
, cellrenderertext
, path
, new_text
):
555 if len(new_text
) > 0:
556 self
.new_child
.node
.name
= new_text
557 FeedManager
.save_category(self
.new_child
.node
)
559 self
.model
.store
.remove(self
.new_child
.iter)
560 self
.new_child
= None
562 def on_display_properties_feed(self
, *args
):
563 selected_tv_node
= [tv_node
for tv_node
in self
.selected()][0]
564 self
._presenter
.show_feed_information(selected_tv_node
.node
)
566 def add_category(self
):
567 self
.begin_add_category(self
._model
.tv_nodes
[1].node
)
569 def select_first_feed(self
):
570 selection
= self
._widget
.get_selection()
571 (model
, pathlist
) = selection
.get_selected_rows()
572 treeiter
= model
.get_iter_first()
573 if not treeiter
or not model
.iter_is_valid(treeiter
):
575 self
.set_cursor(treeiter
)
578 def select_next_feed(self
, with_unread
=False):
579 ''' Scrolls to the next feed in the feed list
581 If there is no selection, selects the first feed. If multiple feeds
582 are selected, selects the feed after the last selected feed.
584 If unread is True, selects the next unread with unread items.
586 If the selection next-to-be is a category, go to the iter its first
587 child. If current selection is a child, then go to (parent + 1),
588 provided that (parent + 1) is not a category.
591 def next(model
, current
):
592 treeiter
= model
.iter_next(current
)
593 if not treeiter
: return False
594 if model
.iter_depth(current
): next(model
, model
.iter_parent(current
))
595 path
= model
.get_path(treeiter
)
596 if with_unread
and model
[path
][Column
.unread
] < 1:
598 self
.set_cursor(treeiter
)
600 selection
= self
._widget
.get_selection()
601 (model
, pathlist
) = selection
.get_selected_rows()
602 iters
= [model
.get_iter(path
) for path
in pathlist
]
604 current
= iters
.pop()
605 if model
.iter_has_child(current
):
606 iterchild
= model
.iter_children(current
)
607 # make the row visible
608 path
= model
.get_path(iterchild
)
609 for i
in range(len(path
)):
610 self
._widget
.expand_row(path
[:i
+1], False)
611 # select his first born child
612 if with_unread
and model
[path
][Column
.unread
] > 0:
613 self
.set_cursor(iterchild
)
616 has_unread
= next(model
, current
)
617 has_unread
= next(model
,current
)
619 self
.set_cursor(model
.get_iter_first())
623 def select_previous_feed(self
):
624 ''' Scrolls to the previous feed in the feed list.
626 If there is no selection, selects the first feed. If there's multiple
627 selection, selects the feed before the first selected feed.
629 If the previous selection is a category, select the last node in that
630 category. If the current selection is a child, then go to (parent -
631 1). If parent is the first feed, wrap and select the last feed or
632 category in the list.
634 def previous(model
, current
):
635 path
= model
.get_path(current
)
636 treerow
= model
[path
[-1]-1]
637 self
.set_cursor(treerow
.iter)
638 selection
= self
._widget
.get_selection()
639 (model
, pathlist
) = selection
.get_selected_rows()
640 iters
= [model
.get_iter(path
) for path
in pathlist
]
642 current_first
= iters
.pop(0)
643 if model
.iter_has_child(current_first
):
644 children
= model
.iter_n_children(current_first
)
645 treeiter
= model
.iter_nth_child(children
- 1)
646 self
.set_cursor(treeiter
)
648 previous(model
, current_first
)
650 self
.set_cursor(model
.get_iter_first())
653 def set_cursor(self
, treeiter
, col_id
=None, edit
=False):
658 path
= self
._model
.model
.get_path(treeiter
)
661 column
= self
._widget
.get_column(col_id
)
663 self
._widget
.set_cursor(path
, column
, edit
)
664 self
._widget
.scroll_to_cell(path
, column
)
665 self
._widget
.grab_focus()
667 class FeedsPresenter(MVP
.BasicPresenter
):
668 def _initialize(self
):
669 self
.model
= FeedListModel()
671 def store_state(self
):
672 node
= self
.view
.get_selected_node()
678 Config
.set(OPTION_LAST_SELECTED_NODE
, id)
679 Config
.set(OPTION_LAST_EXPANDED_NODES
, ",".join([str(node
.id) for node
in self
.view
.get_expanded_nodes()]))
681 def restore_state(self
):
682 id = Config
.get(OPTION_LAST_SELECTED_NODE
)
685 self
.view
.select_node(id)
687 expanded
= Config
.get(OPTION_LAST_EXPANDED_NODES
)
691 expanded
= map(int, expanded
.split(","))
695 self
.view
.expand_nodes(expanded
)
697 def add_category(self
):
698 self
.view
.add_category()
700 def select_first_feed(self
):
701 return self
.view
.select_first_feed()
703 def select_next_feed(self
, with_unread
=False):
704 return self
.view
.select_next_feed(with_unread
)
706 def select_previous_feed(self
):
707 return self
.view
.select_previous_feed()
709 def show_feed_information(self
, node
):
711 properties
= categoryproperties
712 elif node
.type == "F":
713 properties
= feedproperties
715 properties
.show(node
)