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 if hasattr(self
.node
, "status") and (self
.node
.status
& FS_UPDATING
):
69 title
= "<i>" + title
+ "</i>"
74 def unread_count(self
):
75 ''' The title of the node be it a category or a feed '''
76 return self
.node
.unread_count
80 ''' gets the pixbuf to display according to the status of the feed '''
83 if isinstance(self
.node
, Feed
):
84 if self
.node
.status
== FS_ERROR
:
85 return _tmp_widget
.render_icon(gtk
.STOCK_CANCEL
, gtk
.ICON_SIZE_MENU
)
87 return _tmp_widget
.render_icon(gtk
.STOCK_FILE
, gtk
.ICON_SIZE_MENU
)
89 return _tmp_widget
.render_icon(gtk
.STOCK_DIRECTORY
, gtk
.ICON_SIZE_MENU
)
94 node
= copy
.copy(self
.node
)
97 path
.append(node
.norder
)
98 node
= copy
.copy(node
.parent
)
100 path
.pop() # We don't need path to "root" category here since it's not in the tree view.
107 path
= self
.path_list
112 return ":".join(map(str, path
))
122 return self
.store
.get_iter_from_string(path
)
127 def parent_path(self
):
128 path
= self
.path_list
134 return ":".join(map(str, path
))
137 def parent_iter(self
):
138 path
= self
.parent_path
143 return self
.store
.get_iter_from_string(path
)
146 ''' The model for the feed list view '''
152 def refresh_tree(self
):
153 self
.appmodel
= FeedManager
.get_model()
155 self
._prepare
_store
()
156 self
._prepare
_model
()
158 self
._populate
_tree
(1, None, [])
160 def _init_signals(self
):
161 FeedManager
._get
_instance
().connect("feed-added", self
._on
_node
_added
)
162 FeedManager
._get
_instance
().connect("feed-status-changed", self
._on
_feed
_status
_changed
)
163 FeedManager
._get
_instance
().connect("category-added", self
._on
_node
_added
)
165 def _prepare_model(self
):
166 self
.tv_nodes
= dict([(node
.id, TreeViewNode(node
, self
.store
)) for node
in self
.appmodel
.values()])
168 def _prepare_store(self
):
169 self
.store
= gtk
.TreeStore(gtk
.gdk
.Pixbuf
, str, str, str, gobject
.TYPE_PYOBJECT
, bool)
171 def _populate_tree(self
, parent_id
, parent_iter
, done
):
172 for node
in self
.tv_nodes
[parent_id
].node
.children
:
173 tv_node
= self
.tv_nodes
[node
.id]
176 self
._create
_row
(tv_node
)
177 tv_node
.store
= self
.store
178 elif node
.type == "C":
179 current_parent
= self
._create
_row
(tv_node
)
180 tv_node
.store
= self
.store
182 if self
.tv_nodes
.has_key(node
.id):
183 self
._populate
_tree
(node
.id, current_parent
, done
)
185 def _create_row(self
, node
, editable
= False):
186 return self
.store
.append(node
.parent_iter
, [node
.pixbuf
,
187 helpers
.pango_escape(node
.title
),
192 def _on_node_added(self
, src
, node
):
193 tv_node
= TreeViewNode(node
, self
.store
)
194 self
.add_node(tv_node
)
195 self
._create
_row
(tv_node
)
197 def _on_feed_status_changed(self
, src
, feed
):
198 self
.tv_nodes
[feed
.id].refresh()
200 def add_node(self
, tv_node
):
201 self
.tv_nodes
[tv_node
.node
.id] = tv_node
207 def search(self
, rows
, func
, data
):
208 if not rows
: return None
212 result
= self
.search(row
.iterchildren(), func
, data
)
213 if result
: return result
216 class FeedsView(MVP
.WidgetView
):
217 def _initialize(self
):
218 self
._widget
.set_search_column(Column
.name
)
221 column
= gtk
.TreeViewColumn()
222 unread_renderer
= gtk
.CellRendererText()
223 column
.pack_start(unread_renderer
, False)
224 column
.set_attributes(unread_renderer
, text
= Column
.unread
)
226 status_renderer
= gtk
.CellRendererPixbuf()
227 column
.pack_start(status_renderer
, False)
228 column
.set_attributes(status_renderer
, pixbuf
= Column
.pixbuf
)
230 # feed title renderer
231 title_renderer
= gtk
.CellRendererText()
233 title_renderer
.connect("edited", self
.on_node_edit_title_edited
)
234 title_renderer
.connect("editing-canceled", self
.on_node_edit_title_canceled
)
236 #title_renderer.set_property('editable', True)
237 column
.pack_start(title_renderer
, False)
238 column
.set_attributes(title_renderer
,
239 foreground
=Column
.foreground
,
241 editable
=Column
.editable
) #, weight=Column.BOLD)
243 self
._widget
.append_column(column
)
245 selection
= self
._widget
.get_selection()
246 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
248 self
._widget
.connect("button_press_event", self
._on
_button
_press
_event
)
249 self
._widget
.connect("popup-menu", self
._on
_popup
_menu
)
251 uifactory
= helpers
.UIFactory('FeedListActions')
252 action
= uifactory
.get_action('/feedlist_popup/refresh')
253 action
.connect('activate', self
.on_menu_poll_selected_activate
)
254 action
= uifactory
.get_action('/feedlist_popup/add_child')
255 action
.connect('activate', self
.on_menu_add_child_activate
)
256 action
= uifactory
.get_action('/feedlist_popup/mark_as_read')
257 action
.connect('activate', self
.on_menu_mark_all_as_read_activate
)
258 action
= uifactory
.get_action('/feedlist_popup/stop_refresh')
259 action
.connect('activate', self
.on_menu_stop_poll_selected_activate
)
260 action
= uifactory
.get_action('/feedlist_popup/remove')
261 action
.connect('activate', self
.on_remove_selected_feed
)
262 action
= uifactory
.get_action('/feedlist_popup/properties')
263 action
.connect('activate', self
.on_display_properties_feed
)
264 self
.popup
= uifactory
.get_popup('/feedlist_popup')
266 treeview
= self
._widget
268 treeview
.enable_model_drag_source(gtk
.gdk
.BUTTON1_MASK
, [("drop_yes", 0, 0)], gtk
.gdk
.ACTION_MOVE
)
269 treeview
.enable_model_drag_dest([("drop_yes", 0, 0)], gtk
.gdk
.ACTION_MOVE
)
270 treeview
.connect("drag_data_received", self
._on
_dragdata
_received
)
271 treeview
.connect("drag_motion", self
._on
_drag
_motion
)
273 def _on_drag_motion(self
, treeview
, drag_context
, x
, y
, eventtime
):
274 temp
= treeview
.get_dest_row_at_pos(x
, y
)
279 model
= treeview
.get_model()
280 drop_path
, drop_position
= temp
282 # FIXME: Here we use only first selected node of possibly multiple
283 # selected. See fixme comment in self._on_dragdata_received.
284 source_node
= [tv_node
for tv_node
in self
.selected()][0]
286 drop_node
= model
[drop_path
][Column
.object]
287 source_path
= source_node
.path_list
289 sane_drop_path
= self
._check
_drop
_path
(model
, source_path
, drop_path
)
291 is_drop_into
= drop_position
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
or \
292 drop_position
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
294 can_drop_into
= drop_node
.node
.is_parent()
296 if sane_drop_path
and ((is_drop_into
and can_drop_into
) or not is_drop_into
):
297 treeview
.enable_model_drag_dest([("drop_yes", 0, 0)], gtk
.gdk
.ACTION_MOVE
)
299 treeview
.enable_model_drag_dest([("drop_no", 0, 0)], gtk
.gdk
.ACTION_MOVE
)
301 def _on_dragdata_received(self
, treeview
, drag_context
, x
, y
, selection
, info
, eventtime
):
302 model
, pathlist
= treeview
.get_selection().get_selected_rows()
304 if len(pathlist
) > 1:
305 # FIXME: Maybe we want to support drag and drop for multiple rows?
306 # For now it's not worth it while there are other things to do.
307 drag_context
.finish(False, False, eventtime
)
310 source_path
= pathlist
[0]
311 source_iter
= model
.get_iter(source_path
)
313 temp
= treeview
.get_dest_row_at_pos(x
, y
)
316 drop_path
, drop_pos
= temp
318 drop_path
, drop_pos
= (len(model
) - 1,), gtk
.TREE_VIEW_DROP_AFTER
320 effective_drop_path
= self
._calculate
_effective
_drop
_path
(source_path
, drop_path
, drop_pos
)
322 if source_path
== effective_drop_path
:
323 drag_context
.finish(False, False, eventtime
)
326 drop_iter
= model
.get_iter(drop_path
)
328 if not self
._check
_drop
_path
(model
, source_path
, drop_path
):
329 drag_context
.finish(False, False, eventtime
)
332 node
= model
[source_path
][Column
.object].node
333 self
._iter
_copy
(model
, source_iter
, drop_iter
, drop_pos
)
335 drag_context
.finish(True, True, eventtime
)
336 FeedManager
.move_node(node
, effective_drop_path
)
338 def _check_drop_path(self
, model
, source_path
, drop_path
):
340 Verifies if a drop path is not within the subtree of source path so that
341 we can disallow dropping parent into its own subtree etc. using this check.
344 return list(drop_path
[0:len(source_path
)]) != list(source_path
)
346 def _iter_copy(self
, model
, iter_to_copy
, target_iter
, pos
):
348 Recursively copies GTK TreeView iters from source to target using
349 GTK relative data about drag and drop operation.
352 path
= model
.get_path(iter_to_copy
)
354 if (pos
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
) or (pos
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
):
355 new_iter
= model
.prepend(target_iter
, model
[path
])
356 elif pos
== gtk
.TREE_VIEW_DROP_BEFORE
:
357 new_iter
= model
.insert_before(None, target_iter
, model
[path
])
358 elif pos
== gtk
.TREE_VIEW_DROP_AFTER
:
359 new_iter
= model
.insert_after(None, target_iter
, model
[path
])
361 n
= model
.iter_n_children(iter_to_copy
)
364 next_iter_to_copy
= model
.iter_nth_child(iter_to_copy
, n
- i
- 1)
365 self
._iter
_copy
(model
, next_iter_to_copy
, new_iter
, gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
)
367 def _calculate_effective_drop_path(self
, source_path
, drop_path
, drop_pos
):
369 Calculate effective absolute drop path given drop_pos and source/destination
370 of drag and drop operation. GTK uses relative terms for describing drop
371 destination (after/before/into etc.) while we prefer absolute drop path
372 and we can take care of reordering ourselves.
375 result
= list(drop_path
)
376 same_level
= len(source_path
) == len(drop_path
) and source_path
[:-1] == drop_path
[:-1]
378 if drop_pos
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
:
380 elif drop_pos
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
:
382 elif drop_pos
== gtk
.TREE_VIEW_DROP_BEFORE
:
383 if not same_level
or (same_level
and source_path
[-1] < drop_path
[-1]):
386 elif drop_pos
== gtk
.TREE_VIEW_DROP_AFTER
:
387 if not same_level
or (same_level
and source_path
[-1] > drop_path
[-1]):
392 def _model_set(self
):
393 self
._widget
.set_model(self
._model
.model
)
395 def add_selection_changed_listener(self
, listener
):
396 selection
= self
._widget
.get_selection()
397 selection
.connect('changed', listener
.feedlist_selection_changed
, Column
.object)
399 def _on_popup_menu(self
, treeview
, *args
):
400 self
.popup
.popup(None, None, None, 0, 0)
402 def _on_button_press_event(self
, treeview
, event
):
405 if event
.button
== 3:
408 time
= gtk
.get_current_event_time()
409 path
= treeview
.get_path_at_pos(x
, y
)
415 self
.node_at_popup
= self
.model
.store
[path
][Column
.object]
416 treeview
.grab_focus()
418 if self
.selected_count() < 2:
419 selection
= treeview
.get_selection()
420 selection
.unselect_all()
421 selection
.select_path(path
)
423 self
.popup
.popup(None, None, None, event
.button
, time
)
428 def selected_count(self
):
429 selection
= self
._widget
.get_selection()
430 pathlist
= selection
.get_selected_rows()[1]
434 selection
= self
._widget
.get_selection()
435 (model
, pathlist
) = selection
.get_selected_rows()
436 nodes
= [model
[path
][Column
.object] for path
in pathlist
]
438 for tv_node
in nodes
:
441 def foreach_selected(self
, func
):
442 selection
= self
._widget
.get_selection()
443 (model
, pathlist
) = selection
.get_selected_rows()
444 iters
= [model
.get_iter(path
) for path
in pathlist
]
446 for treeiter
in iters
:
447 object = model
.get_value(treeiter
, Column
.object)
448 func(object, model
, treeiter
)
449 except TypeError, te
:
450 logging
.exception(te
)
452 def on_menu_add_child_activate(self
, *args
):
453 self
.begin_add_category(self
.node_at_popup
.node
)
455 def begin_add_category(self
, node
):
456 category
= Category()
457 category
.parent
= node
458 category
.norder
= len(node
.children
)
459 self
.new_child
= TreeViewNode(category
, self
.model
.store
)
460 iter = self
.model
._create
_row
(self
.new_child
, editable
= True)
461 path
= self
.model
.store
.get_path(iter)
462 column
= self
._widget
.get_column(0)
464 parent_path
= self
.new_child
.parent_path
467 self
._widget
.expand_row(parent_path
, False)
469 self
._widget
.set_cursor_on_cell(path
, focus_column
= column
, start_editing
= True)
471 def on_menu_poll_selected_activate(self
, *args
):
472 """config = Config.get_instance()
474 if config.offline: #XXX
475 config.offline = not config.offline"""
477 FeedManager
.update_nodes([node
.node
for node
in self
.selected()])
479 def on_menu_stop_poll_selected_activate(self
, *args
):
480 self
.foreach_selected(lambda o
,*args
: o
.feed
.router
.stop_polling())
482 def on_menu_mark_all_as_read_activate(self
, *args
):
483 self
.foreach_selected(lambda o
,*args
: o
.feed
.mark_items_as_read())
485 def on_remove_selected_feed(self
, *args
):
486 nodes
= [tv_node
for tv_node
in self
.selected()]
488 FeedManager
.delete_nodes([tv_node
.node
.id for tv_node
in nodes
])
494 self
.model
.store
.remove(iter)
496 def on_node_edit_title_canceled(self
, cellrenderer
):
498 debug("on_node_edit_title_canceled (new_child), removing %s" % str(self
.new_child
.path
))
499 self
.model
.store
.remove(self
.new_child
.iter)
500 self
.new_child
= None
502 def on_node_edit_title_edited(self
, cellrenderertext
, path
, new_text
):
503 if len(new_text
) > 0:
504 self
.new_child
.node
.name
= new_text
505 FeedManager
.save_category(self
.new_child
.node
)
507 self
.model
.store
.remove(self
.new_child
.iter)
508 self
.new_child
= None
510 def on_display_properties_feed(self
, *args
):
511 selected_tv_node
= [tv_node
for tv_node
in self
.selected()][0]
512 self
._presenter
.show_feed_information(selected_tv_node
.node
)
514 def add_category(self
):
515 self
.begin_add_category(self
._model
.tv_nodes
[1].node
)
517 def select_first_feed(self
):
518 selection
= self
._widget
.get_selection()
519 (model
, pathlist
) = selection
.get_selected_rows()
520 treeiter
= model
.get_iter_first()
521 if not treeiter
or not model
.iter_is_valid(treeiter
):
523 self
.set_cursor(treeiter
)
526 def select_next_feed(self
, with_unread
=False):
527 ''' Scrolls to the next feed in the feed list
529 If there is no selection, selects the first feed. If multiple feeds
530 are selected, selects the feed after the last selected feed.
532 If unread is True, selects the next unread with unread items.
534 If the selection next-to-be is a category, go to the iter its first
535 child. If current selection is a child, then go to (parent + 1),
536 provided that (parent + 1) is not a category.
539 def next(model
, current
):
540 treeiter
= model
.iter_next(current
)
541 if not treeiter
: return False
542 if model
.iter_depth(current
): next(model
, model
.iter_parent(current
))
543 path
= model
.get_path(treeiter
)
544 if with_unread
and model
[path
][Column
.unread
] < 1:
546 self
.set_cursor(treeiter
)
548 selection
= self
._widget
.get_selection()
549 (model
, pathlist
) = selection
.get_selected_rows()
550 iters
= [model
.get_iter(path
) for path
in pathlist
]
552 current
= iters
.pop()
553 if model
.iter_has_child(current
):
554 iterchild
= model
.iter_children(current
)
555 # make the row visible
556 path
= model
.get_path(iterchild
)
557 for i
in range(len(path
)):
558 self
._widget
.expand_row(path
[:i
+1], False)
559 # select his first born child
560 if with_unread
and model
[path
][Column
.unread
] > 0:
561 self
.set_cursor(iterchild
)
564 has_unread
= next(model
, current
)
565 has_unread
= next(model
,current
)
567 self
.set_cursor(model
.get_iter_first())
571 def select_previous_feed(self
):
572 ''' Scrolls to the previous feed in the feed list.
574 If there is no selection, selects the first feed. If there's multiple
575 selection, selects the feed before the first selected feed.
577 If the previous selection is a category, select the last node in that
578 category. If the current selection is a child, then go to (parent -
579 1). If parent is the first feed, wrap and select the last feed or
580 category in the list.
582 def previous(model
, current
):
583 path
= model
.get_path(current
)
584 treerow
= model
[path
[-1]-1]
585 self
.set_cursor(treerow
.iter)
586 selection
= self
._widget
.get_selection()
587 (model
, pathlist
) = selection
.get_selected_rows()
588 iters
= [model
.get_iter(path
) for path
in pathlist
]
590 current_first
= iters
.pop(0)
591 if model
.iter_has_child(current_first
):
592 children
= model
.iter_n_children(current_first
)
593 treeiter
= model
.iter_nth_child(children
- 1)
594 self
.set_cursor(treeiter
)
596 previous(model
, current_first
)
598 self
.set_cursor(model
.get_iter_first())
601 def set_cursor(self
, treeiter
, col_id
=None, edit
=False):
606 path
= self
._model
.model
.get_path(treeiter
)
609 column
= self
._widget
.get_column(col_id
)
611 self
._widget
.set_cursor(path
, column
, edit
)
612 self
._widget
.scroll_to_cell(path
, column
)
613 self
._widget
.grab_focus()
615 class FeedsPresenter(MVP
.BasicPresenter
):
616 def _initialize(self
):
617 self
.model
= FeedListModel()
620 def _init_signals(self
):
623 def add_category(self
):
624 self
.view
.add_category()
626 def select_first_feed(self
):
627 return self
.view
.select_first_feed()
629 def select_next_feed(self
, with_unread
=False):
630 return self
.view
.select_next_feed(with_unread
)
632 def select_previous_feed(self
):
633 return self
.view
.select_previous_feed()
635 def _sort_func(self
, model
, a
, b
):
637 Sorts the feeds lexically.
639 From the gtk.TreeSortable.set_sort_func doc:
641 The comparison callback should return -1 if the iter1 row should come before
642 the iter2 row, 0 if the rows are equal, or 1 if the iter1 row should come
646 fa
= model
.get_value(a
, Column
.OBJECT
)
647 fb
= model
.get_value(b
, Column
.OBJECT
)
650 retval
= locale
.strcoll(fa
.title
, fb
.title
)
651 elif fa
is not None: retval
= -1
652 elif fb
is not None: retval
= 1
655 def show_feed_information(self
, node
):
657 properties
= categoryproperties
658 elif node
.type == "F":
659 properties
= feedproperties
661 properties
.show(node
)