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()])
167 def _prepare_store(self
):
168 self
.store
= gtk
.TreeStore(gtk
.gdk
.Pixbuf
, str, str, str, gobject
.TYPE_PYOBJECT
, bool)
170 def _populate_tree(self
, parent_id
, parent_iter
, done
):
171 for node
in self
.tv_nodes
[parent_id
].node
.children
:
172 tv_node
= self
.tv_nodes
[node
.id]
175 self
._create
_row
(tv_node
)
176 tv_node
.store
= self
.store
177 elif node
.type == "C":
178 current_parent
= self
._create
_row
(tv_node
)
179 tv_node
.store
= self
.store
181 if self
.tv_nodes
.has_key(node
.id):
182 self
._populate
_tree
(node
.id, current_parent
, done
)
184 def _create_row(self
, node
, editable
= False):
185 return self
.store
.append(node
.parent_iter
, [node
.pixbuf
,
186 helpers
.pango_escape(node
.title
),
191 def add_node(self
, tv_node
):
192 self
.tv_nodes
[tv_node
.node
.id] = tv_node
194 def node_added_cb(self
, src
, node
):
195 tv_node
= TreeViewNode(node
, self
.store
)
196 self
.add_node(tv_node
)
197 self
._create
_row
(tv_node
)
203 def search(self
, rows
, func
, data
):
204 if not rows
: return None
208 result
= self
.search(row
.iterchildren(), func
, data
)
209 if result
: return result
212 class FeedsView(MVP
.WidgetView
):
213 def _initialize(self
):
214 self
._widget
.set_search_column(Column
.name
)
217 column
= gtk
.TreeViewColumn()
218 unread_renderer
= gtk
.CellRendererText()
219 column
.pack_start(unread_renderer
, False)
220 column
.set_attributes(unread_renderer
, text
= Column
.unread
)
222 status_renderer
= gtk
.CellRendererPixbuf()
223 column
.pack_start(status_renderer
, False)
224 column
.set_attributes(status_renderer
, pixbuf
= Column
.pixbuf
)
226 # feed title renderer
227 title_renderer
= gtk
.CellRendererText()
229 title_renderer
.connect("edited", self
.on_node_edit_title_edited
)
230 title_renderer
.connect("editing-canceled", self
.on_node_edit_title_canceled
)
232 #title_renderer.set_property('editable', True)
233 column
.pack_start(title_renderer
, False)
234 column
.set_attributes(title_renderer
,
235 foreground
=Column
.foreground
,
237 editable
=Column
.editable
) #, weight=Column.BOLD)
239 self
._widget
.append_column(column
)
241 selection
= self
._widget
.get_selection()
242 selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
244 self
._widget
.connect("button_press_event", self
._on
_button
_press
_event
)
245 self
._widget
.connect("popup-menu", self
._on
_popup
_menu
)
247 uifactory
= helpers
.UIFactory('FeedListActions')
248 action
= uifactory
.get_action('/feedlist_popup/refresh')
249 action
.connect('activate', self
.on_menu_poll_selected_activate
)
250 action
= uifactory
.get_action('/feedlist_popup/add_child')
251 action
.connect('activate', self
.on_menu_add_child_activate
)
252 action
= uifactory
.get_action('/feedlist_popup/mark_as_read')
253 action
.connect('activate', self
.on_menu_mark_all_as_read_activate
)
254 action
= uifactory
.get_action('/feedlist_popup/stop_refresh')
255 action
.connect('activate', self
.on_menu_stop_poll_selected_activate
)
256 action
= uifactory
.get_action('/feedlist_popup/remove')
257 action
.connect('activate', self
.on_remove_selected_feed
)
258 action
= uifactory
.get_action('/feedlist_popup/properties')
259 action
.connect('activate', self
.on_display_properties_feed
)
260 self
.popup
= uifactory
.get_popup('/feedlist_popup')
262 treeview
= self
._widget
264 treeview
.enable_model_drag_source(gtk
.gdk
.BUTTON1_MASK
, [("example", 0, 0)], gtk
.gdk
.ACTION_MOVE
)
265 treeview
.enable_model_drag_dest([("example", 0, 0)], gtk
.gdk
.ACTION_MOVE
)
266 treeview
.connect("drag_data_received", self
.on_dragdata_received_cb
)
268 def on_dragdata_received_cb(self
, treeview
, drag_context
, x
, y
, selection
, info
, eventtime
):
269 model
, pathlist
= treeview
.get_selection().get_selected_rows()
270 iter_to_copy
= model
.get_iter(pathlist
[0])
272 temp
= treeview
.get_dest_row_at_pos(x
, y
)
277 path
, pos
= (len(model
) - 1,), gtk
.TREE_VIEW_DROP_AFTER
279 target_iter
= model
.get_iter(path
)
280 path_of_target_iter
= model
.get_path(target_iter
)
282 if self
.check_row_path(model
, iter_to_copy
, target_iter
):
283 path
= model
.get_path(iter_to_copy
)
286 to_path
= list(path_of_target_iter
)
288 if pos
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
:
289 print "TREE_VIEW_DROP_INTO_OR_BEFORE"
291 elif pos
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
:
292 print "TREE_VIEW_DROP_INTO_OR_AFTER"
294 elif pos
== gtk
.TREE_VIEW_DROP_BEFORE
:
295 print "dropping before"
296 elif pos
== gtk
.TREE_VIEW_DROP_AFTER
:
297 print "dropping after %s" % (str(path_of_target_iter
))
298 to_path
= list(path_of_target_iter
)
299 order
= to_path
.pop()
301 if ":".join(map(str, to_path
)):
302 iter = model
.get_iter(":".join(map(str, to_path
)))
306 #if order + 1 >= model.iter_n_children(iter):
307 # to_path.append(path_of_target_iter[len(path_of_target_iter) - 1])
309 to_path
.append(path_of_target_iter
[len(path_of_target_iter
) - 1] + 1)
311 print "%s -> %s" % (str(from_path
), str(to_path
))
313 node
= model
[from_path
][Column
.object].node
314 self
.iter_copy(model
, iter_to_copy
, target_iter
, pos
)
316 drag_context
.finish(True, True, eventtime
)
317 FeedManager
.move_node(node
, to_path
)
319 drag_context
.finish(False, False, eventtime
)
321 def check_row_path(self
, model
, iter_to_copy
, target_iter
):
322 path_of_iter_to_copy
= model
.get_path(iter_to_copy
)
323 path_of_target_iter
= model
.get_path(target_iter
)
324 return not (path_of_target_iter
[0:len(path_of_iter_to_copy
)] == path_of_iter_to_copy
)
326 def iter_copy(self
, model
, iter_to_copy
, target_iter
, pos
):
327 path
= model
.get_path(iter_to_copy
)
329 if (pos
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
) or (pos
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
):
330 new_iter
= model
.prepend(target_iter
, model
[path
])
331 elif pos
== gtk
.TREE_VIEW_DROP_BEFORE
:
332 new_iter
= model
.insert_before(None, target_iter
, model
[path
])
333 elif pos
== gtk
.TREE_VIEW_DROP_AFTER
:
334 new_iter
= model
.insert_after(None, target_iter
, model
[path
])
336 n
= model
.iter_n_children(iter_to_copy
)
339 next_iter_to_copy
= model
.iter_nth_child(iter_to_copy
, n
- i
- 1)
340 self
.iter_copy(model
, next_iter_to_copy
, new_iter
, gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
)
342 def _model_set(self
):
343 self
._widget
.set_model(self
._model
.model
)
345 def add_selection_changed_listener(self
, listener
):
346 selection
= self
._widget
.get_selection()
347 selection
.connect('changed', listener
.feedlist_selection_changed
, Column
.object)
349 def _on_popup_menu(self
, treeview
, *args
):
350 self
.popup
.popup(None, None, None, 0, 0)
352 def _on_button_press_event(self
, treeview
, event
):
355 if event
.button
== 3:
358 time
= gtk
.get_current_event_time()
359 path
= treeview
.get_path_at_pos(x
, y
)
365 self
.node_at_popup
= self
.model
.store
[path
][Column
.object]
366 treeview
.grab_focus()
368 if self
.selected_count() < 2:
369 selection
= treeview
.get_selection()
370 selection
.unselect_all()
371 selection
.select_path(path
)
373 self
.popup
.popup(None, None, None, event
.button
, time
)
378 def selected_count(self
):
379 selection
= self
._widget
.get_selection()
380 pathlist
= selection
.get_selected_rows()[1]
384 selection
= self
._widget
.get_selection()
385 (model
, pathlist
) = selection
.get_selected_rows()
386 nodes
= [model
[path
][Column
.object] for path
in pathlist
]
388 for tv_node
in nodes
:
391 def foreach_selected(self
, func
):
392 selection
= self
._widget
.get_selection()
393 (model
, pathlist
) = selection
.get_selected_rows()
394 iters
= [model
.get_iter(path
) for path
in pathlist
]
396 for treeiter
in iters
:
397 object = model
.get_value(treeiter
, Column
.object)
398 func(object, model
, treeiter
)
399 except TypeError, te
:
400 logging
.exception(te
)
402 def on_menu_add_child_activate(self
, *args
):
403 self
.begin_add_category(self
.node_at_popup
.node
)
405 def begin_add_category(self
, node
):
406 category
= Category()
407 category
.parent
= node
408 category
.norder
= len(node
.children
)
409 self
.new_child
= TreeViewNode(category
, self
.model
.store
)
410 iter = self
.model
._create
_row
(self
.new_child
, editable
= True)
411 path
= self
.model
.store
.get_path(iter)
412 column
= self
._widget
.get_column(0)
414 parent_path
= self
.new_child
.parent_path
417 self
._widget
.expand_row(parent_path
, False)
419 self
._widget
.set_cursor_on_cell(path
, focus_column
= column
, start_editing
= True)
421 def on_menu_poll_selected_activate(self
, *args
):
422 config
= Config
.get_instance()
424 if config
.offline
: #XXX
425 config
.offline
= not config
.offline
427 FeedManager
.update_nodes([node
.node
for node
in self
.selected()])
429 def on_menu_stop_poll_selected_activate(self
, *args
):
430 self
.foreach_selected(lambda o
,*args
: o
.feed
.router
.stop_polling())
432 def on_menu_mark_all_as_read_activate(self
, *args
):
433 self
.foreach_selected(lambda o
,*args
: o
.feed
.mark_items_as_read())
435 def on_remove_selected_feed(self
, *args
):
436 nodes
= [tv_node
for tv_node
in self
.selected()]
438 FeedManager
.delete_nodes([tv_node
.node
.id for tv_node
in nodes
])
444 self
.model
.store
.remove(iter)
446 def on_node_edit_title_canceled(self
, cellrenderer
):
448 debug("on_node_edit_title_canceled (new_child), removing %s" % str(self
.new_child
.path
))
449 self
.model
.store
.remove(self
.new_child
.iter)
450 self
.new_child
= None
452 def on_node_edit_title_edited(self
, cellrenderertext
, path
, new_text
):
453 if len(new_text
) > 0:
454 self
.new_child
.node
.name
= new_text
455 FeedManager
.save_category(self
.new_child
.node
)
457 self
.model
.store
.remove(self
.new_child
.iter)
458 self
.new_child
= None
460 def on_display_properties_feed(self
, *args
):
461 selection
= self
._widget
.get_selection()
462 (model
, pathlist
) = selection
.get_selected_rows()
463 iters
= [model
.get_iter(path
) for path
in pathlist
]
464 path
= pathlist
.pop()
465 node
= self
.model
.model
[path
][Column
.object]
466 self
._presenter
.show_feed_information(node
)
468 def add_category(self
):
469 self
.begin_add_category(self
._model
.tv_nodes
[1].node
)
471 def select_first_feed(self
):
472 selection
= self
._widget
.get_selection()
473 (model
, pathlist
) = selection
.get_selected_rows()
474 treeiter
= model
.get_iter_first()
475 if not treeiter
or not model
.iter_is_valid(treeiter
):
477 self
.set_cursor(treeiter
)
480 def select_next_feed(self
, with_unread
=False):
481 ''' Scrolls to the next feed in the feed list
483 If there is no selection, selects the first feed. If multiple feeds
484 are selected, selects the feed after the last selected feed.
486 If unread is True, selects the next unread with unread items.
488 If the selection next-to-be is a category, go to the iter its first
489 child. If current selection is a child, then go to (parent + 1),
490 provided that (parent + 1) is not a category.
493 def next(model
, current
):
494 treeiter
= model
.iter_next(current
)
495 if not treeiter
: return False
496 if model
.iter_depth(current
): next(model
, model
.iter_parent(current
))
497 path
= model
.get_path(treeiter
)
498 if with_unread
and model
[path
][Column
.unread
] < 1:
500 self
.set_cursor(treeiter
)
502 selection
= self
._widget
.get_selection()
503 (model
, pathlist
) = selection
.get_selected_rows()
504 iters
= [model
.get_iter(path
) for path
in pathlist
]
506 current
= iters
.pop()
507 if model
.iter_has_child(current
):
508 iterchild
= model
.iter_children(current
)
509 # make the row visible
510 path
= model
.get_path(iterchild
)
511 for i
in range(len(path
)):
512 self
._widget
.expand_row(path
[:i
+1], False)
513 # select his first born child
514 if with_unread
and model
[path
][Column
.unread
] > 0:
515 self
.set_cursor(iterchild
)
518 has_unread
= next(model
, current
)
519 has_unread
= next(model
,current
)
521 self
.set_cursor(model
.get_iter_first())
525 def select_previous_feed(self
):
526 ''' Scrolls to the previous feed in the feed list.
528 If there is no selection, selects the first feed. If there's multiple
529 selection, selects the feed before the first selected feed.
531 If the previous selection is a category, select the last node in that
532 category. If the current selection is a child, then go to (parent -
533 1). If parent is the first feed, wrap and select the last feed or
534 category in the list.
536 def previous(model
, current
):
537 path
= model
.get_path(current
)
538 treerow
= model
[path
[-1]-1]
539 self
.set_cursor(treerow
.iter)
540 selection
= self
._widget
.get_selection()
541 (model
, pathlist
) = selection
.get_selected_rows()
542 iters
= [model
.get_iter(path
) for path
in pathlist
]
544 current_first
= iters
.pop(0)
545 if model
.iter_has_child(current_first
):
546 children
= model
.iter_n_children(current_first
)
547 treeiter
= model
.iter_nth_child(children
- 1)
548 self
.set_cursor(treeiter
)
550 previous(model
, current_first
)
552 self
.set_cursor(model
.get_iter_first())
555 def set_cursor(self
, treeiter
, col_id
=None, edit
=False):
560 path
= self
._model
.model
.get_path(treeiter
)
563 column
= self
._widget
.get_column(col_id
)
565 self
._widget
.set_cursor(path
, column
, edit
)
566 self
._widget
.scroll_to_cell(path
, column
)
567 self
._widget
.grab_focus()
569 class FeedsPresenter(MVP
.BasicPresenter
):
570 def _initialize(self
):
571 self
.model
= FeedListModel()
574 def _init_signals(self
):
577 def add_category(self
):
578 self
.view
.add_category()
580 def select_first_feed(self
):
581 return self
.view
.select_first_feed()
583 def select_next_feed(self
, with_unread
=False):
584 return self
.view
.select_next_feed(with_unread
)
586 def select_previous_feed(self
):
587 return self
.view
.select_previous_feed()
589 def _sort_func(self
, model
, a
, b
):
591 Sorts the feeds lexically.
593 From the gtk.TreeSortable.set_sort_func doc:
595 The comparison callback should return -1 if the iter1 row should come before
596 the iter2 row, 0 if the rows are equal, or 1 if the iter1 row should come
600 fa
= model
.get_value(a
, Column
.OBJECT
)
601 fb
= model
.get_value(b
, Column
.OBJECT
)
604 retval
= locale
.strcoll(fa
.title
, fb
.title
)
605 elif fa
is not None: retval
= -1
606 elif fb
is not None: retval
= 1
609 def show_feed_information(self
, feed
):
610 straw
.feed_properties_show(feed
)