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
28 import os
, copy
, locale
, logging
33 pixbuf
, name
, foreground
, unread
, object, editable
= range(6)
35 _tmp_widget
= gtk
.Label()
37 class TreeViewNode(object):
38 def __init__(self
, node
, store
):
45 self
.node
.connect("notify", self
.obj_changed
)
47 def obj_changed(self
, obj
, property):
48 #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)))
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 if property.name
== "unread-count":
51 #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)))
54 debug("setting %d unread_count = %d, self.path = %s" % (obj
.id, self
.unread_count
, str(self
.path
)))
57 self
.store
.set(self
.iter, 3, self
.node
.unread_count
)
58 elif property.name
== "status":
59 if (self
.node
.status
& straw
.FS_UPDATING
) > 0:
60 title
= self
.store
.get_value(self
.iter, 1)
61 self
.store
.set(self
.iter, 1, "<i>" + title
+ "</i>")
63 title
= self
.node
.title
64 self
.store
.set(self
.iter, 1, title
)
68 ''' The title of the node be it a category or a feed '''
69 if self
.node
.type == "C":
71 elif self
.node
.type == "F":
72 return self
.node
.title
75 def unread_count(self
):
76 ''' The title of the node be it a category or a feed '''
77 return self
.node
.unread_count
81 ''' gets the pixbuf to display according to the status of the feed '''
84 if isinstance(self
.node
, Feed
):
85 return _tmp_widget
.render_icon(gtk
.STOCK_FILE
, gtk
.ICON_SIZE_MENU
)
87 return _tmp_widget
.render_icon(gtk
.STOCK_DIRECTORY
, gtk
.ICON_SIZE_MENU
)
93 node
= copy
.copy(self
.node
)
96 path
.append(str(node
.norder
))
97 node
= copy
.copy(node
.parent
)
99 path
.pop() # We don't need path to "root" category here since it's not in the tree view.
106 path
= self
.path_list
111 return ":".join(path
)
121 return self
.store
.get_iter_from_string(path
)
126 def parent_path(self
):
127 path
= self
.path_list
134 return ":".join(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
.node_added_cb
)
162 FeedManager
._get
_instance
().connect("category-added", self
.node_added_cb
)
164 def _prepare_model(self
):
165 self
.tv_nodes
= dict([(node
.id, TreeViewNode(node
, self
.store
)) for node
in self
.appmodel
.values()])
166 #print [TreeViewNode(child_node, self.store) for child_node in self.appmodel[1].children][1].node.parent_id
167 #for node in self.appmodel.values():
168 # if not self.tv_nodes.has_key(node.parent_id) and node.parent:
169 # self.tv_nodes[node.parent_id] = [TreeViewNode(child_node, self.store) for child_node in node.parent.children]
171 def _prepare_store(self
):
172 self
.store
= gtk
.TreeStore(gtk
.gdk
.Pixbuf
, str, str, str, gobject
.TYPE_PYOBJECT
, bool)
174 def _populate_tree(self
, parent_id
, parent_iter
, done
):
175 for node
in self
.tv_nodes
[parent_id
].node
.children
:
176 tv_node
= self
.tv_nodes
[node
.id]
179 self
._create
_row
(tv_node
)
180 tv_node
.store
= self
.store
181 elif node
.type == "C":
182 current_parent
= self
._create
_row
(tv_node
)
183 tv_node
.store
= self
.store
185 if self
.tv_nodes
.has_key(node
.id):
186 self
._populate
_tree
(node
.id, current_parent
, done
)
188 def _create_row(self
, node
, editable
= False):
189 return self
.store
.append(node
.parent_iter
, [node
.pixbuf
,
190 helpers
.pango_escape(node
.title
),
195 def add_node(self
, tv_node
):
196 self
.tv_nodes
[tv_node
.node
.id] = tv_node
198 def node_added_cb(self
, src
, node
):
199 tv_node
= TreeViewNode(node
, self
.store
)
200 self
.add_node(tv_node
)
201 self
._create
_row
(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
,
227 status_renderer
= gtk
.CellRendererPixbuf()
228 column
.pack_start(status_renderer
, False)
229 column
.set_attributes(status_renderer
,
230 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
,
271 [("example", 0, 0)], gtk
.gdk
.ACTION_MOVE
)
272 treeview
.enable_model_drag_dest([("example", 0, 0)],
274 treeview
.connect("drag_data_received", self
.on_dragdata_received_cb
)
276 def on_dragdata_received_cb(self
, treeview
, drag_context
, x
, y
, selection
, info
, eventtime
):
277 model
, pathlist
= treeview
.get_selection().get_selected_rows()
278 iter_to_copy
= model
.get_iter(pathlist
[0])
280 temp
= treeview
.get_dest_row_at_pos(x
, y
)
285 path
, pos
= (len(model
) - 1,), gtk
.TREE_VIEW_DROP_AFTER
287 target_iter
= model
.get_iter(path
)
288 path_of_target_iter
= model
.get_path(target_iter
)
290 if self
.check_row_path(model
, iter_to_copy
, target_iter
):
291 path
= model
.get_path(iter_to_copy
)
294 to_path
= list(path_of_target_iter
)
296 if pos
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
:
297 print "TREE_VIEW_DROP_INTO_OR_BEFORE"
299 elif pos
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
:
300 print "TREE_VIEW_DROP_INTO_OR_AFTER"
302 elif pos
== gtk
.TREE_VIEW_DROP_BEFORE
:
303 print "dropping before"
304 elif pos
== gtk
.TREE_VIEW_DROP_AFTER
:
305 print "dropping after %s" % (str(path_of_target_iter
))
306 to_path
= list(path_of_target_iter
)
307 order
= to_path
.pop()
309 if ":".join(map(str, to_path
)):
310 iter = model
.get_iter(":".join(map(str, to_path
)))
314 #if order + 1 >= model.iter_n_children(iter):
315 # to_path.append(path_of_target_iter[len(path_of_target_iter) - 1])
317 to_path
.append(path_of_target_iter
[len(path_of_target_iter
) - 1] + 1)
319 print "%s -> %s" % (str(from_path
), str(to_path
))
321 node
= model
[from_path
][Column
.object].node
322 self
.iter_copy(model
, iter_to_copy
, target_iter
, pos
)
324 drag_context
.finish(True, True, eventtime
)
325 FeedManager
.move_node(node
, to_path
)
327 drag_context
.finish(False, False, eventtime
)
329 def check_row_path(self
, model
, iter_to_copy
, target_iter
):
330 path_of_iter_to_copy
= model
.get_path(iter_to_copy
)
331 path_of_target_iter
= model
.get_path(target_iter
)
332 if path_of_target_iter
[0:len(path_of_iter_to_copy
)] == path_of_iter_to_copy
:
337 def iter_copy(self
, model
, iter_to_copy
, target_iter
, pos
):
338 path
= model
.get_path(iter_to_copy
)
340 if (pos
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
) or (pos
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
):
341 new_iter
= model
.prepend(target_iter
, model
[path
])
342 elif pos
== gtk
.TREE_VIEW_DROP_BEFORE
:
343 new_iter
= model
.insert_before(None, target_iter
, model
[path
])
344 elif pos
== gtk
.TREE_VIEW_DROP_AFTER
:
345 new_iter
= model
.insert_after(None, target_iter
, model
[path
])
347 n
= model
.iter_n_children(iter_to_copy
)
349 next_iter_to_copy
= model
.iter_nth_child(iter_to_copy
, n
- i
- 1)
350 self
.iter_copy(model
, next_iter_to_copy
, new_iter
, gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
)
352 def _model_set(self
):
353 self
._widget
.set_model(self
._model
.model
)
355 def add_selection_changed_listener(self
, listener
):
356 selection
= self
._widget
.get_selection()
357 selection
.connect('changed', listener
.feedlist_selection_changed
, Column
.object)
359 def _on_popup_menu(self
, treeview
, *args
):
360 self
.popup
.popup(None, None, None, 0, 0)
362 def _on_button_press_event(self
, treeview
, event
):
365 if event
.button
== 3:
368 time
= gtk
.get_current_event_time()
369 path
= treeview
.get_path_at_pos(x
, y
)
375 self
.node_at_popup
= self
.model
.store
[path
][Column
.object]
376 treeview
.grab_focus()
378 if self
.selected_count() < 2:
379 selection
= treeview
.get_selection()
380 selection
.unselect_all()
381 selection
.select_path(path
)
383 self
.popup
.popup(None, None, None, event
.button
, time
)
388 def selected_count(self
):
389 selection
= self
._widget
.get_selection()
390 pathlist
= selection
.get_selected_rows()[1]
394 selection
= self
._widget
.get_selection()
395 (model
, pathlist
) = selection
.get_selected_rows()
396 nodes
= [model
[path
][Column
.object] for path
in pathlist
]
398 for tv_node
in nodes
:
401 def foreach_selected(self
, func
):
402 selection
= self
._widget
.get_selection()
403 (model
, pathlist
) = selection
.get_selected_rows()
404 iters
= [model
.get_iter(path
) for path
in pathlist
]
406 for treeiter
in iters
:
407 object = model
.get_value(treeiter
, Column
.object)
408 func(object, model
, treeiter
)
409 except TypeError, te
:
410 logging
.exception(te
)
413 def on_menu_add_child_activate(self
, *args
):
414 self
.begin_add_category(self
.node_at_popup
.node
)
416 def begin_add_category(self
, node
):
417 category
= Category()
418 category
.parent
= node
419 category
.norder
= len(node
.children
)
420 self
.new_child
= TreeViewNode(category
, self
.model
.store
)
421 iter = self
.model
._create
_row
(self
.new_child
, editable
= True)
422 path
= self
.model
.store
.get_path(iter)
423 column
= self
._widget
.get_column(0)
425 parent_path
= self
.new_child
.parent_path
428 self
._widget
.expand_row(parent_path
, False)
430 self
._widget
.set_cursor_on_cell(path
, focus_column
= column
, start_editing
= True)
432 def on_menu_poll_selected_activate(self
, *args
):
433 config
= Config
.get_instance()
435 if config
.offline
: #XXX
436 config
.offline
= not config
.offline
438 selection
= self
._widget
.get_selection()
439 (model
, pathlist
) = selection
.get_selected_rows()
440 iters
= [model
.get_iter(path
) for path
in pathlist
]
441 nodes
= [model
.get_value(treeiter
,Column
.object) for treeiter
in iters
]
443 FeedManager
.update_all_feeds({}, [node
.node
for node
in nodes
])
445 def on_menu_stop_poll_selected_activate(self
, *args
):
446 self
.foreach_selected(lambda o
,*args
: o
.feed
.router
.stop_polling())
448 def on_menu_mark_all_as_read_activate(self
, *args
):
449 self
.foreach_selected(lambda o
,*args
: o
.feed
.mark_items_as_read())
451 def on_remove_selected_feed(self
, *args
):
452 nodes
= [tv_node
for tv_node
in self
.selected()]
454 FeedManager
.delete_nodes([tv_node
.node
.id for tv_node
in nodes
])
460 self
.model
.store
.remove(iter)
462 def on_node_edit_title_canceled(self
, cellrenderer
):
464 debug("on_node_edit_title_canceled (new_child), removing %s" % str(self
.new_child
.path
))
465 self
.model
.store
.remove(self
.new_child
.iter)
466 self
.new_child
= None
468 def on_node_edit_title_edited(self
, cellrenderertext
, path
, new_text
):
469 if len(new_text
) > 0:
470 self
.new_child
.node
.name
= new_text
471 FeedManager
.save_category(self
.new_child
.node
)
473 self
.model
.store
.remove(self
.new_child
.iter)
474 self
.new_child
= None
476 def on_display_properties_feed(self
, *args
):
477 selection
= self
._widget
.get_selection()
478 (model
, pathlist
) = selection
.get_selected_rows()
479 iters
= [model
.get_iter(path
) for path
in pathlist
]
480 path
= pathlist
.pop()
481 node
= self
.model
.model
[path
][Column
.object]
482 self
._presenter
.show_feed_information(node
)
484 def add_category(self
):
485 self
.begin_add_category(self
._model
.tv_nodes
[1].node
)
487 def select_first_feed(self
):
488 selection
= self
._widget
.get_selection()
489 (model
, pathlist
) = selection
.get_selected_rows()
490 treeiter
= model
.get_iter_first()
491 if not treeiter
or not model
.iter_is_valid(treeiter
):
493 self
.set_cursor(treeiter
)
496 def select_next_feed(self
, with_unread
=False):
497 ''' Scrolls to the next feed in the feed list
499 If there is no selection, selects the first feed. If multiple feeds
500 are selected, selects the feed after the last selected feed.
502 If unread is True, selects the next unread with unread items.
504 If the selection next-to-be is a category, go to the iter its first
505 child. If current selection is a child, then go to (parent + 1),
506 provided that (parent + 1) is not a category.
509 def next(model
, current
):
510 treeiter
= model
.iter_next(current
)
511 if not treeiter
: return False
512 if model
.iter_depth(current
): next(model
, model
.iter_parent(current
))
513 path
= model
.get_path(treeiter
)
514 if with_unread
and model
[path
][Column
.unread
] < 1:
516 self
.set_cursor(treeiter
)
518 selection
= self
._widget
.get_selection()
519 (model
, pathlist
) = selection
.get_selected_rows()
520 iters
= [model
.get_iter(path
) for path
in pathlist
]
522 current
= iters
.pop()
523 if model
.iter_has_child(current
):
524 iterchild
= model
.iter_children(current
)
525 # make the row visible
526 path
= model
.get_path(iterchild
)
527 for i
in range(len(path
)):
528 self
._widget
.expand_row(path
[:i
+1], False)
529 # select his first born child
530 if with_unread
and model
[path
][Column
.unread
] > 0:
531 self
.set_cursor(iterchild
)
534 has_unread
= next(model
, current
)
535 has_unread
= next(model
,current
)
537 self
.set_cursor(model
.get_iter_first())
541 def select_previous_feed(self
):
542 ''' Scrolls to the previous feed in the feed list.
544 If there is no selection, selects the first feed. If there's multiple
545 selection, selects the feed before the first selected feed.
547 If the previous selection is a category, select the last node in that
548 category. If the current selection is a child, then go to (parent -
549 1). If parent is the first feed, wrap and select the last feed or
550 category in the list.
552 def previous(model
, current
):
553 path
= model
.get_path(current
)
554 treerow
= model
[path
[-1]-1]
555 self
.set_cursor(treerow
.iter)
556 selection
= self
._widget
.get_selection()
557 (model
, pathlist
) = selection
.get_selected_rows()
558 iters
= [model
.get_iter(path
) for path
in pathlist
]
560 current_first
= iters
.pop(0)
561 if model
.iter_has_child(current_first
):
562 children
= model
.iter_n_children(current_first
)
563 treeiter
= model
.iter_nth_child(children
- 1)
564 self
.set_cursor(treeiter
)
566 previous(model
, current_first
)
568 self
.set_cursor(model
.get_iter_first())
571 def set_cursor(self
, treeiter
, col_id
=None, edit
=False):
575 path
= self
._model
.model
.get_path(treeiter
)
577 column
= self
._widget
.get_column(col_id
)
578 self
._widget
.set_cursor(path
, column
, edit
)
579 self
._widget
.scroll_to_cell(path
, column
)
580 self
._widget
.grab_focus()
583 class FeedsPresenter(MVP
.BasicPresenter
):
584 def _initialize(self
):
585 self
.model
= FeedListModel()
588 def _init_signals(self
):
591 def add_category(self
):
592 self
.view
.add_category()
594 def select_first_feed(self
):
595 return self
.view
.select_first_feed()
597 def select_next_feed(self
, with_unread
=False):
598 return self
.view
.select_next_feed(with_unread
)
600 def select_previous_feed(self
):
601 return self
.view
.select_previous_feed()
603 def _sort_func(self
, model
, a
, b
):
605 Sorts the feeds lexically.
607 From the gtk.TreeSortable.set_sort_func doc:
609 The comparison callback should return -1 if the iter1 row should come before
610 the iter2 row, 0 if the rows are equal, or 1 if the iter1 row should come
614 fa
= model
.get_value(a
, Column
.OBJECT
)
615 fb
= model
.get_value(b
, Column
.OBJECT
)
618 retval
= locale
.strcoll(fa
.title
, fb
.title
)
619 elif fa
is not None: retval
= -1
620 elif fb
is not None: retval
= 1
623 def show_feed_information(self
, feed
):
624 straw
.feed_properties_show(feed
)