Added "Add child" context menu option, more fixes on tree view manipulation...
[straw.git] / straw / FeedListView.py
blob4768409944828ac76b94c3961696a801b65274e8
1 """ FeedListView.py
3 Module for displaying the feeds in the Feeds TreeView.
4 """
5 __copyright__ = "Copyright (c) 2002-2005 Free Software Foundation, Inc."
6 __license__ = """
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
10 version.
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
22 import Config
23 import FeedManager
24 import MVP
25 import gobject
26 import gtk
27 import helpers
28 import os, copy, locale, logging
29 import pango
30 import straw
32 class Column:
33 pixbuf, name, foreground, unread, object, editable = range(6)
35 _tmp_widget = gtk.Label()
37 class TreeViewNode(object):
38 def __init__(self, node, store):
39 self.node = node
40 self.store = store
42 self.setup_node()
44 def setup_node(self):
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>")
59 else:
60 title = self.node.title
61 self.store.set(self.iter, 1, title)
63 @property
64 def title(self):
65 ''' The title of the node be it a category or a feed '''
66 if self.node.type == "C":
67 return self.node.name
68 elif self.node.type == "F":
69 return self.node.title
71 @property
72 def unread_count(self):
73 ''' The title of the node be it a category or a feed '''
74 return self.node.unread_count
76 @property
77 def pixbuf(self):
78 ''' gets the pixbuf to display according to the status of the feed '''
79 global _tmp_widget
81 if isinstance(self.node, Feed):
82 return _tmp_widget.render_icon(gtk.STOCK_FILE, gtk.ICON_SIZE_MENU)
83 else:
84 return _tmp_widget.render_icon(gtk.STOCK_DIRECTORY, gtk.ICON_SIZE_MENU)
86 @property
87 def path_list(self):
88 path = []
90 node = copy.copy(self.node)
92 while 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.
97 path.reverse()
99 return path
101 @property
102 def path(self):
103 path = self.path_list
105 if len(path) == 0:
106 return None
107 else:
108 return ":".join(path)
110 @property
111 def iter(self):
112 path = self.path
114 if path == None:
115 return None
116 else:
117 ble = self.store.get_iter_from_string(path)
118 return self.store.get_iter_from_string(path)
120 @property
121 def parent_path(self):
122 path = self.path_list
124 path.pop()
126 if len(path) == 0:
127 return None
128 else:
129 return ":".join(path)
131 @property
132 def parent_iter(self):
133 path = self.parent_path
134 #print path
135 if path == None:
136 return None
137 else:
138 return self.store.get_iter_from_string(path)
140 class FeedListModel:
141 ''' The model for the feed list view '''
143 def __init__(self):
144 self.refresh_tree()
145 self._init_signals()
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):
160 self.tv_nodes = {}
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):
171 return
173 for tv_node in self.tv_nodes[parent_id]:
174 node = tv_node.node
176 if node.type == "F":
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,
189 node.title,
190 'black',
191 node.unread_count,
192 node, editable])
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"""
211 @property
212 def model(self):
213 return self.store
215 def search(self, rows, func, data):
216 if not rows: return None
217 for row in rows:
218 if func(row, data):
219 return row
220 result = self.search(row.iterchildren(), func, data)
221 if result: return result
222 return None
224 class FeedsView(MVP.WidgetView):
225 def _initialize(self):
226 self._widget.set_search_column(Column.name)
228 # pixbuf column
229 column = gtk.TreeViewColumn()
230 unread_renderer = gtk.CellRendererText()
231 column.pack_start(unread_renderer, False)
232 column.set_attributes(unread_renderer,
233 text=Column.unread)
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,
250 markup=Column.name,
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)],
281 gtk.gdk.ACTION_MOVE)
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)
288 if temp != None:
289 path, pos = temp
290 else:
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)
299 from_path = path
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"
304 to_path.append(0)
305 elif pos == gtk.TREE_VIEW_DROP_INTO_OR_AFTER:
306 print "TREE_VIEW_DROP_INTO_OR_AFTER"
307 to_path.append(0)
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)))
317 else:
318 iter = None
320 #if order + 1 >= model.iter_n_children(iter):
321 # to_path.append(path_of_target_iter[len(path_of_target_iter) - 1])
322 #else:
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)
332 else:
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:
339 return False
340 else:
341 return True
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)
354 for i in range(n):
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):
369 retval = 0
370 if event.button == 3:
371 x = int(event.x)
372 y = int(event.y)
373 time = gtk.get_current_event_time()
374 path = treeview.get_path_at_pos(x, y)
375 if path is None:
376 return 1
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)
385 retval = 1
386 return retval
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]
396 try:
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)
402 return
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):
434 def remove(*args):
435 (object, model, treeiter) = args
436 model.remove(treeiter)
437 feedlist = feeds.get_feedlist_instance()
438 idx = feedlist.index(object.feed)
439 del feedlist[idx]
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)
458 return
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):
465 return False
466 self.set_cursor(treeiter)
467 return True
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.
481 has_unread = False
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:
488 next(model, current)
489 self.set_cursor(treeiter)
490 return True
491 selection = self._widget.get_selection()
492 (model, pathlist) = selection.get_selected_rows()
493 iters = [model.get_iter(path) for path in pathlist]
494 try:
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)
505 has_unread = True
506 else:
507 has_unread = next(model, current)
508 has_unread = next(model,current)
509 except IndexError:
510 self.set_cursor(model.get_iter_first())
511 has_unread = True
512 return has_unread
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]
532 try:
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)
538 return
539 previous(model, current_first)
540 except IndexError:
541 self.set_cursor(model.get_iter_first())
542 return
544 def set_cursor(self, treeiter, col_id=None, edit=False):
545 if not treeiter:
546 return
547 column = None
548 path = self._model.model.get_path(treeiter)
549 if col_id:
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()
554 return
556 class FeedsPresenter(MVP.BasicPresenter):
557 def _initialize(self):
558 self.model = FeedListModel()
559 self._init_signals()
561 def _init_signals(self):
563 pass
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
595 after the iter2 row.
597 retval = 0
598 fa = model.get_value(a, Column.OBJECT)
599 fb = model.get_value(b, Column.OBJECT)
601 if fa and fb:
602 retval = locale.strcoll(fa.title, fb.title)
603 elif fa is not None: retval = -1
604 elif fb is not None: retval = 1
605 return retval
607 def show_feed_information(self, feed):
608 straw.feed_properties_show(feed)