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)))
52 debug("setting %d unread_count = %d, self.path = %s" % (obj
.id, self
.unread_count
, str(self
.path
)))
54 self
.store
.set(self
.iter, 3, self
.node
.unread_count
)
55 elif property.name
== "status":
56 if (self
.node
.status
& straw
.FS_UPDATING
) > 0:
57 title
= self
.store
.get_value(self
.iter, 1)
58 self
.store
.set(self
.iter, 1, "<i>" + title
+ "</i>")
60 title
= self
.node
.title
61 self
.store
.set(self
.iter, 1, title
)
65 ''' The title of the node be it a category or a feed '''
66 if self
.node
.type == "C":
68 elif self
.node
.type == "F":
69 return self
.node
.title
72 def unread_count(self
):
73 ''' The title of the node be it a category or a feed '''
74 return self
.node
.unread_count
78 ''' gets the pixbuf to display according to the status of the feed '''
81 if isinstance(self
.node
, Feed
):
82 return _tmp_widget
.render_icon(gtk
.STOCK_FILE
, gtk
.ICON_SIZE_MENU
)
84 return _tmp_widget
.render_icon(gtk
.STOCK_DIRECTORY
, gtk
.ICON_SIZE_MENU
)
90 node
= copy
.copy(self
.node
)
93 path
.append(str(node
.norder
))
94 node
= copy
.copy(node
.parent
)
96 path
.pop() # We don't need path to "root" category here since it's not in the tree view.
103 path
= self
.path_list
108 return ":".join(path
)
117 ble
= self
.store
.get_iter_from_string(path
)
118 return self
.store
.get_iter_from_string(path
)
121 def parent_path(self
):
122 path
= self
.path_list
129 return ":".join(path
)
132 def parent_iter(self
):
133 path
= self
.parent_path
138 return self
.store
.get_iter_from_string(path
)
141 ''' The model for the feed list view '''
147 def refresh_tree(self
):
148 self
.appmodel
= FeedManager
.get_model()
150 self
._prepare
_store
()
151 self
._prepare
_model
()
153 self
._populate
_tree
(1, None, [])
155 def _init_signals(self
):
156 FeedManager
._get
_instance
().connect("feed-added", self
.node_added_cb
)
157 FeedManager
._get
_instance
().connect("category-added", self
.node_added_cb
)
159 def _prepare_model(self
):
161 #print [TreeViewNode(child_node, self.store) for child_node in self.appmodel[1].children][1].node.parent_id
162 for node
in self
.appmodel
.values():
163 if not self
.tv_nodes
.has_key(node
.parent_id
) and node
.parent
:
164 self
.tv_nodes
[node
.parent_id
] = [TreeViewNode(child_node
, self
.store
) for child_node
in node
.parent
.children
]
166 def _prepare_store(self
):
167 self
.store
= gtk
.TreeStore(gtk
.gdk
.Pixbuf
, str, str, str, gobject
.TYPE_PYOBJECT
, bool)
169 def _populate_tree(self
, parent_id
, parent_iter
, done
):
170 if not self
.tv_nodes
.has_key(parent_id
):
173 for tv_node
in self
.tv_nodes
[parent_id
]:
177 tv_node
.treeiter
= 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
.treeiter
= current_parent
182 tv_node
.store
= self
.store
184 if self
.tv_nodes
.has_key(node
.id):
185 self
._populate
_tree
(node
.id, current_parent
, done
)
187 def _create_row(self
, node
, editable
= False):
188 return self
.store
.append(node
.parent_iter
, [node
.pixbuf
,
194 def add_node(self
, tv_node
):
195 if not self
.tv_nodes
.has_key(tv_node
.node
.parent_id
):
196 self
.tv_nodes
[tv_node
.node
.parent_id
] = []
198 self
.tv_nodes
[tv_node
.node
.parent_id
].append(tv_node
)
200 def node_added_cb(self
, src
, node
):
201 tv_node
= TreeViewNode(node
, self
.store
)
202 self
.add_node(tv_node
)
204 self
._create
_row
(tv_node
)
206 """if parent_node != None and hasattr(parent_node, "treeiter"):
207 parent_iter = parent_node.treeiter
208 tv_node.treeiter = self._create_row(tv_node, parent_iter)
209 tv_node.store = self.store"""
215 def search(self
, rows
, func
, data
):
216 if not rows
: return None
220 result
= self
.search(row
.iterchildren(), func
, data
)
221 if result
: return result
224 class FeedsView(MVP
.WidgetView
):
225 def _initialize(self
):
226 self
._widget
.set_search_column(Column
.name
)
229 column
= gtk
.TreeViewColumn()
230 unread_renderer
= gtk
.CellRendererText()
231 column
.pack_start(unread_renderer
, False)
232 column
.set_attributes(unread_renderer
,
235 status_renderer
= gtk
.CellRendererPixbuf()
236 column
.pack_start(status_renderer
, False)
237 column
.set_attributes(status_renderer
,
238 pixbuf
=Column
.pixbuf
)
240 # feed title renderer
241 title_renderer
= gtk
.CellRendererText()
243 title_renderer
.connect("edited", self
.on_node_edit_title_edited
)
244 title_renderer
.connect("editing-canceled", self
.on_node_edit_title_canceled
)
246 #title_renderer.set_property('editable', True)
247 column
.pack_start(title_renderer
, False)
248 column
.set_attributes(title_renderer
,
249 foreground
=Column
.foreground
,
251 editable
=Column
.editable
) #, weight=Column.BOLD)
253 self
._widget
.append_column(column
)
255 selection
= self
._widget
.get_selection()
256 selection
.set_mode(gtk
.SELECTION_SINGLE
)
258 self
._widget
.connect("button_press_event", self
._on
_button
_press
_event
)
259 self
._widget
.connect("popup-menu", self
._on
_popup
_menu
)
261 uifactory
= helpers
.UIFactory('FeedListActions')
262 action
= uifactory
.get_action('/feedlist_popup/refresh')
263 action
.connect('activate', self
.on_menu_poll_selected_activate
)
264 action
= uifactory
.get_action('/feedlist_popup/add_child')
265 action
.connect('activate', self
.on_menu_add_child_activate
)
266 action
= uifactory
.get_action('/feedlist_popup/mark_as_read')
267 action
.connect('activate', self
.on_menu_mark_all_as_read_activate
)
268 action
= uifactory
.get_action('/feedlist_popup/stop_refresh')
269 action
.connect('activate', self
.on_menu_stop_poll_selected_activate
)
270 action
= uifactory
.get_action('/feedlist_popup/remove')
271 action
.connect('activate', self
.on_remove_selected_feed
)
272 action
= uifactory
.get_action('/feedlist_popup/properties')
273 action
.connect('activate', self
.on_display_properties_feed
)
274 self
.popup
= uifactory
.get_popup('/feedlist_popup')
276 treeview
= self
._widget
278 treeview
.enable_model_drag_source(gtk
.gdk
.BUTTON1_MASK
,
279 [("example", 0, 0)], gtk
.gdk
.ACTION_MOVE
)
280 treeview
.enable_model_drag_dest([("example", 0, 0)],
282 treeview
.connect("drag_data_received", self
.on_dragdata_received_cb
)
284 def on_dragdata_received_cb(self
, treeview
, drag_context
, x
, y
, selection
, info
, eventtime
):
285 model
, iter_to_copy
= treeview
.get_selection().get_selected()
286 temp
= treeview
.get_dest_row_at_pos(x
, y
)
291 path
, pos
= (len(model
) - 1,), gtk
.TREE_VIEW_DROP_AFTER
293 target_iter
= model
.get_iter(path
)
294 path_of_target_iter
= model
.get_path(target_iter
)
296 if self
.check_row_path(model
, iter_to_copy
, target_iter
):
297 path
= model
.get_path(iter_to_copy
)
300 to_path
= list(path_of_target_iter
)
302 if pos
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
:
303 print "TREE_VIEW_DROP_INTO_OR_BEFORE"
305 elif pos
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
:
306 print "TREE_VIEW_DROP_INTO_OR_AFTER"
308 elif pos
== gtk
.TREE_VIEW_DROP_BEFORE
:
309 print "dropping before"
310 elif pos
== gtk
.TREE_VIEW_DROP_AFTER
:
311 print "dropping after %s" % (str(path_of_target_iter
))
312 to_path
= list(path_of_target_iter
)
313 order
= to_path
.pop()
315 if ":".join(map(str, to_path
)):
316 iter = model
.get_iter(":".join(map(str, to_path
)))
320 #if order + 1 >= model.iter_n_children(iter):
321 # to_path.append(path_of_target_iter[len(path_of_target_iter) - 1])
323 to_path
.append(path_of_target_iter
[len(path_of_target_iter
) - 1] + 1)
325 print "%s -> %s" % (str(from_path
), str(to_path
))
327 node
= model
[from_path
][Column
.object].node
328 self
.iter_copy(model
, iter_to_copy
, target_iter
, pos
)
330 drag_context
.finish(True, True, eventtime
)
331 FeedManager
.move_node(node
, to_path
)
333 drag_context
.finish(False, False, eventtime
)
335 def check_row_path(self
, model
, iter_to_copy
, target_iter
):
336 path_of_iter_to_copy
= model
.get_path(iter_to_copy
)
337 path_of_target_iter
= model
.get_path(target_iter
)
338 if path_of_target_iter
[0:len(path_of_iter_to_copy
)] == path_of_iter_to_copy
:
343 def iter_copy(self
, model
, iter_to_copy
, target_iter
, pos
):
344 path
= model
.get_path(iter_to_copy
)
346 if (pos
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
) or (pos
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
):
347 new_iter
= model
.prepend(target_iter
, model
[path
])
348 elif pos
== gtk
.TREE_VIEW_DROP_BEFORE
:
349 new_iter
= model
.insert_before(None, target_iter
, model
[path
])
350 elif pos
== gtk
.TREE_VIEW_DROP_AFTER
:
351 new_iter
= model
.insert_after(None, target_iter
, model
[path
])
353 n
= model
.iter_n_children(iter_to_copy
)
355 next_iter_to_copy
= model
.iter_nth_child(iter_to_copy
, n
- i
- 1)
356 self
.iter_copy(model
, next_iter_to_copy
, new_iter
, gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
)
358 def _model_set(self
):
359 self
._widget
.set_model(self
._model
.model
)
361 def add_selection_changed_listener(self
, listener
):
362 selection
= self
._widget
.get_selection()
363 selection
.connect('changed', listener
.feedlist_selection_changed
, Column
.object)
365 def _on_popup_menu(self
, treeview
, *args
):
366 self
.popup
.popup(None, None, None, 0, 0)
368 def _on_button_press_event(self
, treeview
, event
):
370 if event
.button
== 3:
373 time
= gtk
.get_current_event_time()
374 path
= treeview
.get_path_at_pos(x
, y
)
378 self
.node_at_popup
= self
.model
.store
[path
[0]][Column
.object]
379 path
, col
, cellx
, celly
= path
380 selection
= treeview
.get_selection()
381 selection
.unselect_all()
382 selection
.select_path(path
)
383 treeview
.grab_focus()
384 self
.popup
.popup(None, None, None, event
.button
, time
)
388 def get_selected_tv_node(self
):
389 selection
= self
._widget
.get_selection()
390 (model
, pathlist
) = selection
.get_selected_rows()
392 def foreach_selected(self
, func
):
393 selection
= self
._widget
.get_selection()
394 (model
, pathlist
) = selection
.get_selected_rows()
395 iters
= [model
.get_iter(path
) for path
in pathlist
]
397 for treeiter
in iters
:
398 object = model
.get_value(treeiter
, Column
.object)
399 func(object, model
, treeiter
)
400 except TypeError, te
:
401 logging
.exception(te
)
404 def on_menu_add_child_activate(self
, *args
):
405 category
= Category()
406 category
.parent
= self
.node_at_popup
.node
#FeedManager.get_model()[1]
407 self
.new_child
= TreeViewNode(category
, self
.model
.store
)
408 iter = self
.model
._create
_row
(self
.new_child
, editable
= True)
409 path
= self
.model
.store
.get_path(iter)
410 column
= self
._widget
.get_column(0)
411 self
._widget
.grab_focus()
412 self
._widget
.set_cursor_on_cell(path
, focus_column
= column
, start_editing
= True)
414 def on_menu_poll_selected_activate(self
, *args
):
415 config
= Config
.get_instance()
417 if config
.offline
: #XXX
418 config
.offline
= not config
.offline
420 selection
= self
._widget
.get_selection()
421 (model
, pathlist
) = selection
.get_selected_rows()
422 iters
= [model
.get_iter(path
) for path
in pathlist
]
423 nodes
= [model
.get_value(treeiter
,Column
.object) for treeiter
in iters
]
425 FeedManager
.update_all_feeds({}, [node
.node
for node
in nodes
])
427 def on_menu_stop_poll_selected_activate(self
, *args
):
428 self
.foreach_selected(lambda o
,*args
: o
.feed
.router
.stop_polling())
430 def on_menu_mark_all_as_read_activate(self
, *args
):
431 self
.foreach_selected(lambda o
,*args
: o
.feed
.mark_items_as_read())
433 def on_remove_selected_feed(self
, *args
):
435 (object, model
, treeiter
) = args
436 model
.remove(treeiter
)
437 feedlist
= feeds
.get_feedlist_instance()
438 idx
= feedlist
.index(object.feed
)
440 self
.foreach_selected(remove
)
442 def on_node_edit_title_canceled(self
, cellrenderer
):
443 print "on_node_edit_title_canceled"
445 def on_node_edit_title_edited(self
, cellrenderertext
, path
, new_text
):
446 self
.new_child
.node
.name
= new_text
447 FeedManager
.save_category(self
.new_child
.node
)
448 self
.model
.store
.remove(self
.new_child
.iter)
449 self
.new_child
= None
451 def on_display_properties_feed(self
, *args
):
452 selection
= self
._widget
.get_selection()
453 (model
, pathlist
) = selection
.get_selected_rows()
454 iters
= [model
.get_iter(path
) for path
in pathlist
]
455 path
= pathlist
.pop()
456 node
= self
.model
.model
[path
][Column
.object]
457 self
._presenter
.show_feed_information(node
)
460 def select_first_feed(self
):
461 selection
= self
._widget
.get_selection()
462 (model
, pathlist
) = selection
.get_selected_rows()
463 treeiter
= model
.get_iter_first()
464 if not treeiter
or not model
.iter_is_valid(treeiter
):
466 self
.set_cursor(treeiter
)
469 def select_next_feed(self
, with_unread
=False):
470 ''' Scrolls to the next feed in the feed list
472 If there is no selection, selects the first feed. If multiple feeds
473 are selected, selects the feed after the last selected feed.
475 If unread is True, selects the next unread with unread items.
477 If the selection next-to-be is a category, go to the iter its first
478 child. If current selection is a child, then go to (parent + 1),
479 provided that (parent + 1) is not a category.
482 def next(model
, current
):
483 treeiter
= model
.iter_next(current
)
484 if not treeiter
: return False
485 if model
.iter_depth(current
): next(model
, model
.iter_parent(current
))
486 path
= model
.get_path(treeiter
)
487 if with_unread
and model
[path
][Column
.unread
] < 1:
489 self
.set_cursor(treeiter
)
491 selection
= self
._widget
.get_selection()
492 (model
, pathlist
) = selection
.get_selected_rows()
493 iters
= [model
.get_iter(path
) for path
in pathlist
]
495 current
= iters
.pop()
496 if model
.iter_has_child(current
):
497 iterchild
= model
.iter_children(current
)
498 # make the row visible
499 path
= model
.get_path(iterchild
)
500 for i
in range(len(path
)):
501 self
._widget
.expand_row(path
[:i
+1], False)
502 # select his first born child
503 if with_unread
and model
[path
][Column
.unread
] > 0:
504 self
.set_cursor(iterchild
)
507 has_unread
= next(model
, current
)
508 has_unread
= next(model
,current
)
510 self
.set_cursor(model
.get_iter_first())
514 def select_previous_feed(self
):
515 ''' Scrolls to the previous feed in the feed list.
517 If there is no selection, selects the first feed. If there's multiple
518 selection, selects the feed before the first selected feed.
520 If the previous selection is a category, select the last node in that
521 category. If the current selection is a child, then go to (parent -
522 1). If parent is the first feed, wrap and select the last feed or
523 category in the list.
525 def previous(model
, current
):
526 path
= model
.get_path(current
)
527 treerow
= model
[path
[-1]-1]
528 self
.set_cursor(treerow
.iter)
529 selection
= self
._widget
.get_selection()
530 (model
, pathlist
) = selection
.get_selected_rows()
531 iters
= [model
.get_iter(path
) for path
in pathlist
]
533 current_first
= iters
.pop(0)
534 if model
.iter_has_child(current_first
):
535 children
= model
.iter_n_children(current_first
)
536 treeiter
= model
.iter_nth_child(children
- 1)
537 self
.set_cursor(treeiter
)
539 previous(model
, current_first
)
541 self
.set_cursor(model
.get_iter_first())
544 def set_cursor(self
, treeiter
, col_id
=None, edit
=False):
548 path
= self
._model
.model
.get_path(treeiter
)
550 column
= self
._widget
.get_column(col_id
)
551 self
._widget
.set_cursor(path
, column
, edit
)
552 self
._widget
.scroll_to_cell(path
, column
)
553 self
._widget
.grab_focus()
556 class FeedsPresenter(MVP
.BasicPresenter
):
557 def _initialize(self
):
558 self
.model
= FeedListModel()
561 def _init_signals(self
):
564 #flist.signal_connect(Event.ItemReadSignal,
565 # self._feed_item_read)
566 #flist.signal_connect(Event.AllItemsReadSignal,
567 # self._feed_all_items_read)
568 #flist.signal_connect(Event.FeedsChangedSignal,
569 # self._feeds_changed)
570 #flist.signal_connect(Event.FeedDetailChangedSignal,
571 # self._feed_detail_changed)
572 #fclist = FeedCategoryList.get_instance()
573 #fclist.signal_connect(Event.FeedCategorySortedSignal,
574 # self._feeds_sorted_cb)
575 #fclist.signal_connect(Event.FeedCategoryChangedSignal,
576 # self._fcategory_changed_cb)
578 def select_first_feed(self
):
579 return self
.view
.select_first_feed()
581 def select_next_feed(self
, with_unread
=False):
582 return self
.view
.select_next_feed(with_unread
)
584 def select_previous_feed(self
):
585 return self
.view
.select_previous_feed()
587 def _sort_func(self
, model
, a
, b
):
589 Sorts the feeds lexically.
591 From the gtk.TreeSortable.set_sort_func doc:
593 The comparison callback should return -1 if the iter1 row should come before
594 the iter2 row, 0 if the rows are equal, or 1 if the iter1 row should come
598 fa
= model
.get_value(a
, Column
.OBJECT
)
599 fb
= model
.get_value(b
, Column
.OBJECT
)
602 retval
= locale
.strcoll(fa
.title
, fb
.title
)
603 elif fa
is not None: retval
= -1
604 elif fb
is not None: retval
= 1
607 def show_feed_information(self
, feed
):
608 straw
.feed_properties_show(feed
)