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
)
120 ble
= self
.store
.get_iter_from_string(path
)
121 return self
.store
.get_iter_from_string(path
)
124 def parent_path(self
):
125 path
= self
.path_list
132 return ":".join(path
)
135 def parent_iter(self
):
136 path
= self
.parent_path
141 return self
.store
.get_iter_from_string(path
)
144 ''' The model for the feed list view '''
150 def refresh_tree(self
):
151 self
.appmodel
= FeedManager
.get_model()
153 self
._prepare
_store
()
154 self
._prepare
_model
()
156 self
._populate
_tree
(1, None, [])
158 def _init_signals(self
):
159 FeedManager
._get
_instance
().connect("feed-added", self
.node_added_cb
)
160 FeedManager
._get
_instance
().connect("category-added", self
.node_added_cb
)
162 def _prepare_model(self
):
163 self
.tv_nodes
= dict([(node
.id, TreeViewNode(node
, self
.store
)) for node
in self
.appmodel
.values()])
164 #print [TreeViewNode(child_node, self.store) for child_node in self.appmodel[1].children][1].node.parent_id
165 #for node in self.appmodel.values():
166 # if not self.tv_nodes.has_key(node.parent_id) and node.parent:
167 # self.tv_nodes[node.parent_id] = [TreeViewNode(child_node, self.store) for child_node in node.parent.children]
169 def _prepare_store(self
):
170 self
.store
= gtk
.TreeStore(gtk
.gdk
.Pixbuf
, str, str, str, gobject
.TYPE_PYOBJECT
, bool)
172 def _populate_tree(self
, parent_id
, parent_iter
, done
):
173 for node
in self
.tv_nodes
[parent_id
].node
.children
:
174 tv_node
= self
.tv_nodes
[node
.id]
177 self
._create
_row
(tv_node
)
178 tv_node
.store
= self
.store
179 elif node
.type == "C":
180 current_parent
= self
._create
_row
(tv_node
)
181 tv_node
.store
= self
.store
183 if self
.tv_nodes
.has_key(node
.id):
184 self
._populate
_tree
(node
.id, current_parent
, done
)
186 def _create_row(self
, node
, editable
= False):
187 return self
.store
.append(node
.parent_iter
, [node
.pixbuf
,
188 helpers
.pango_escape(node
.title
),
193 def add_node(self
, tv_node
):
194 self
.tv_nodes
[tv_node
.node
.id] = tv_node
196 def node_added_cb(self
, src
, node
):
197 tv_node
= TreeViewNode(node
, self
.store
)
198 self
.add_node(tv_node
)
199 self
._create
_row
(tv_node
)
205 def search(self
, rows
, func
, data
):
206 if not rows
: return None
210 result
= self
.search(row
.iterchildren(), func
, data
)
211 if result
: return result
214 class FeedsView(MVP
.WidgetView
):
215 def _initialize(self
):
216 self
._widget
.set_search_column(Column
.name
)
219 column
= gtk
.TreeViewColumn()
220 unread_renderer
= gtk
.CellRendererText()
221 column
.pack_start(unread_renderer
, False)
222 column
.set_attributes(unread_renderer
,
225 status_renderer
= gtk
.CellRendererPixbuf()
226 column
.pack_start(status_renderer
, False)
227 column
.set_attributes(status_renderer
,
228 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
,
269 [("example", 0, 0)], gtk
.gdk
.ACTION_MOVE
)
270 treeview
.enable_model_drag_dest([("example", 0, 0)],
272 treeview
.connect("drag_data_received", self
.on_dragdata_received_cb
)
274 def on_dragdata_received_cb(self
, treeview
, drag_context
, x
, y
, selection
, info
, eventtime
):
275 model
, pathlist
= treeview
.get_selection().get_selected_rows()
276 iter_to_copy
= model
.get_iter(pathlist
[0])
278 temp
= treeview
.get_dest_row_at_pos(x
, y
)
283 path
, pos
= (len(model
) - 1,), gtk
.TREE_VIEW_DROP_AFTER
285 target_iter
= model
.get_iter(path
)
286 path_of_target_iter
= model
.get_path(target_iter
)
288 if self
.check_row_path(model
, iter_to_copy
, target_iter
):
289 path
= model
.get_path(iter_to_copy
)
292 to_path
= list(path_of_target_iter
)
294 if pos
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
:
295 print "TREE_VIEW_DROP_INTO_OR_BEFORE"
297 elif pos
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
:
298 print "TREE_VIEW_DROP_INTO_OR_AFTER"
300 elif pos
== gtk
.TREE_VIEW_DROP_BEFORE
:
301 print "dropping before"
302 elif pos
== gtk
.TREE_VIEW_DROP_AFTER
:
303 print "dropping after %s" % (str(path_of_target_iter
))
304 to_path
= list(path_of_target_iter
)
305 order
= to_path
.pop()
307 if ":".join(map(str, to_path
)):
308 iter = model
.get_iter(":".join(map(str, to_path
)))
312 #if order + 1 >= model.iter_n_children(iter):
313 # to_path.append(path_of_target_iter[len(path_of_target_iter) - 1])
315 to_path
.append(path_of_target_iter
[len(path_of_target_iter
) - 1] + 1)
317 print "%s -> %s" % (str(from_path
), str(to_path
))
319 node
= model
[from_path
][Column
.object].node
320 self
.iter_copy(model
, iter_to_copy
, target_iter
, pos
)
322 drag_context
.finish(True, True, eventtime
)
323 FeedManager
.move_node(node
, to_path
)
325 drag_context
.finish(False, False, eventtime
)
327 def check_row_path(self
, model
, iter_to_copy
, target_iter
):
328 path_of_iter_to_copy
= model
.get_path(iter_to_copy
)
329 path_of_target_iter
= model
.get_path(target_iter
)
330 if path_of_target_iter
[0:len(path_of_iter_to_copy
)] == path_of_iter_to_copy
:
335 def iter_copy(self
, model
, iter_to_copy
, target_iter
, pos
):
336 path
= model
.get_path(iter_to_copy
)
338 if (pos
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
) or (pos
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
):
339 new_iter
= model
.prepend(target_iter
, model
[path
])
340 elif pos
== gtk
.TREE_VIEW_DROP_BEFORE
:
341 new_iter
= model
.insert_before(None, target_iter
, model
[path
])
342 elif pos
== gtk
.TREE_VIEW_DROP_AFTER
:
343 new_iter
= model
.insert_after(None, target_iter
, model
[path
])
345 n
= model
.iter_n_children(iter_to_copy
)
347 next_iter_to_copy
= model
.iter_nth_child(iter_to_copy
, n
- i
- 1)
348 self
.iter_copy(model
, next_iter_to_copy
, new_iter
, gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
)
350 def _model_set(self
):
351 self
._widget
.set_model(self
._model
.model
)
353 def add_selection_changed_listener(self
, listener
):
354 selection
= self
._widget
.get_selection()
355 selection
.connect('changed', listener
.feedlist_selection_changed
, Column
.object)
357 def _on_popup_menu(self
, treeview
, *args
):
358 self
.popup
.popup(None, None, None, 0, 0)
360 def _on_button_press_event(self
, treeview
, event
):
363 if event
.button
== 3:
366 time
= gtk
.get_current_event_time()
367 path
= treeview
.get_path_at_pos(x
, y
)
373 self
.node_at_popup
= self
.model
.store
[path
][Column
.object]
374 #selection = treeview.get_selection()
375 #selection.unselect_all()
376 #selection.select_path(path)
377 treeview
.grab_focus()
378 self
.popup
.popup(None, None, None, event
.button
, time
)
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
)
403 def on_menu_add_child_activate(self
, *args
):
404 self
.begin_add_category(self
.node_at_popup
.node
)
406 def begin_add_category(self
, node
):
407 category
= Category()
408 category
.parent
= node
409 category
.norder
= len(node
.children
)
410 self
.new_child
= TreeViewNode(category
, self
.model
.store
)
411 iter = self
.model
._create
_row
(self
.new_child
, editable
= True)
412 path
= self
.model
.store
.get_path(iter)
413 column
= self
._widget
.get_column(0)
415 parent_path
= self
.new_child
.parent_path
418 self
._widget
.expand_row(parent_path
, False)
420 self
._widget
.set_cursor_on_cell(path
, focus_column
= column
, start_editing
= True)
422 def on_menu_poll_selected_activate(self
, *args
):
423 config
= Config
.get_instance()
425 if config
.offline
: #XXX
426 config
.offline
= not config
.offline
428 selection
= self
._widget
.get_selection()
429 (model
, pathlist
) = selection
.get_selected_rows()
430 iters
= [model
.get_iter(path
) for path
in pathlist
]
431 nodes
= [model
.get_value(treeiter
,Column
.object) for treeiter
in iters
]
433 FeedManager
.update_all_feeds({}, [node
.node
for node
in nodes
])
435 def on_menu_stop_poll_selected_activate(self
, *args
):
436 self
.foreach_selected(lambda o
,*args
: o
.feed
.router
.stop_polling())
438 def on_menu_mark_all_as_read_activate(self
, *args
):
439 self
.foreach_selected(lambda o
,*args
: o
.feed
.mark_items_as_read())
441 def on_remove_selected_feed(self
, *args
):
442 nodes
= [tv_node
for tv_node
in self
.selected()]
444 FeedManager
.delete_nodes([tv_node
.node
.id for tv_node
in nodes
])
447 self
.model
.store
.remove(node
.iter)
449 def on_node_edit_title_canceled(self
, cellrenderer
):
451 debug("on_node_edit_title_canceled (new_child), removing %s" % str(self
.new_child
.path
))
452 self
.model
.store
.remove(self
.new_child
.iter)
453 self
.new_child
= None
455 def on_node_edit_title_edited(self
, cellrenderertext
, path
, new_text
):
456 if len(new_text
) > 0:
457 self
.new_child
.node
.name
= new_text
458 FeedManager
.save_category(self
.new_child
.node
)
460 self
.model
.store
.remove(self
.new_child
.iter)
461 self
.new_child
= None
463 def on_display_properties_feed(self
, *args
):
464 selection
= self
._widget
.get_selection()
465 (model
, pathlist
) = selection
.get_selected_rows()
466 iters
= [model
.get_iter(path
) for path
in pathlist
]
467 path
= pathlist
.pop()
468 node
= self
.model
.model
[path
][Column
.object]
469 self
._presenter
.show_feed_information(node
)
471 def add_category(self
):
472 self
.begin_add_category(self
._model
.tv_nodes
[1].node
)
474 def select_first_feed(self
):
475 selection
= self
._widget
.get_selection()
476 (model
, pathlist
) = selection
.get_selected_rows()
477 treeiter
= model
.get_iter_first()
478 if not treeiter
or not model
.iter_is_valid(treeiter
):
480 self
.set_cursor(treeiter
)
483 def select_next_feed(self
, with_unread
=False):
484 ''' Scrolls to the next feed in the feed list
486 If there is no selection, selects the first feed. If multiple feeds
487 are selected, selects the feed after the last selected feed.
489 If unread is True, selects the next unread with unread items.
491 If the selection next-to-be is a category, go to the iter its first
492 child. If current selection is a child, then go to (parent + 1),
493 provided that (parent + 1) is not a category.
496 def next(model
, current
):
497 treeiter
= model
.iter_next(current
)
498 if not treeiter
: return False
499 if model
.iter_depth(current
): next(model
, model
.iter_parent(current
))
500 path
= model
.get_path(treeiter
)
501 if with_unread
and model
[path
][Column
.unread
] < 1:
503 self
.set_cursor(treeiter
)
505 selection
= self
._widget
.get_selection()
506 (model
, pathlist
) = selection
.get_selected_rows()
507 iters
= [model
.get_iter(path
) for path
in pathlist
]
509 current
= iters
.pop()
510 if model
.iter_has_child(current
):
511 iterchild
= model
.iter_children(current
)
512 # make the row visible
513 path
= model
.get_path(iterchild
)
514 for i
in range(len(path
)):
515 self
._widget
.expand_row(path
[:i
+1], False)
516 # select his first born child
517 if with_unread
and model
[path
][Column
.unread
] > 0:
518 self
.set_cursor(iterchild
)
521 has_unread
= next(model
, current
)
522 has_unread
= next(model
,current
)
524 self
.set_cursor(model
.get_iter_first())
528 def select_previous_feed(self
):
529 ''' Scrolls to the previous feed in the feed list.
531 If there is no selection, selects the first feed. If there's multiple
532 selection, selects the feed before the first selected feed.
534 If the previous selection is a category, select the last node in that
535 category. If the current selection is a child, then go to (parent -
536 1). If parent is the first feed, wrap and select the last feed or
537 category in the list.
539 def previous(model
, current
):
540 path
= model
.get_path(current
)
541 treerow
= model
[path
[-1]-1]
542 self
.set_cursor(treerow
.iter)
543 selection
= self
._widget
.get_selection()
544 (model
, pathlist
) = selection
.get_selected_rows()
545 iters
= [model
.get_iter(path
) for path
in pathlist
]
547 current_first
= iters
.pop(0)
548 if model
.iter_has_child(current_first
):
549 children
= model
.iter_n_children(current_first
)
550 treeiter
= model
.iter_nth_child(children
- 1)
551 self
.set_cursor(treeiter
)
553 previous(model
, current_first
)
555 self
.set_cursor(model
.get_iter_first())
558 def set_cursor(self
, treeiter
, col_id
=None, edit
=False):
562 path
= self
._model
.model
.get_path(treeiter
)
564 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()
570 class FeedsPresenter(MVP
.BasicPresenter
):
571 def _initialize(self
):
572 self
.model
= FeedListModel()
575 def _init_signals(self
):
578 def add_category(self
):
579 self
.view
.add_category()
581 def select_first_feed(self
):
582 return self
.view
.select_first_feed()
584 def select_next_feed(self
, with_unread
=False):
585 return self
.view
.select_next_feed(with_unread
)
587 def select_previous_feed(self
):
588 return self
.view
.select_previous_feed()
590 def _sort_func(self
, model
, a
, b
):
592 Sorts the feeds lexically.
594 From the gtk.TreeSortable.set_sort_func doc:
596 The comparison callback should return -1 if the iter1 row should come before
597 the iter2 row, 0 if the rows are equal, or 1 if the iter1 row should come
601 fa
= model
.get_value(a
, Column
.OBJECT
)
602 fb
= model
.get_value(b
, Column
.OBJECT
)
605 retval
= locale
.strcoll(fa
.title
, fb
.title
)
606 elif fa
is not None: retval
= -1
607 elif fb
is not None: retval
= 1
610 def show_feed_information(self
, feed
):
611 straw
.feed_properties_show(feed
)