Fixed treeview manipulation in multiple selection mode.
[straw.git] / straw / FeedListView.py
blobd68d0e94089a8a01bf5f69fd0283edcc47695841
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 iter = self.iter
54 debug("setting %d unread_count = %d, self.path = %s" % (obj.id, self.unread_count, str(self.path)))
56 if iter:
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>")
62 else:
63 title = self.node.title
64 self.store.set(self.iter, 1, title)
66 @property
67 def title(self):
68 ''' The title of the node be it a category or a feed '''
69 if self.node.type == "C":
70 return self.node.name
71 elif self.node.type == "F":
72 return self.node.title
74 @property
75 def unread_count(self):
76 ''' The title of the node be it a category or a feed '''
77 return self.node.unread_count
79 @property
80 def pixbuf(self):
81 ''' gets the pixbuf to display according to the status of the feed '''
82 global _tmp_widget
84 if isinstance(self.node, Feed):
85 return _tmp_widget.render_icon(gtk.STOCK_FILE, gtk.ICON_SIZE_MENU)
86 else:
87 return _tmp_widget.render_icon(gtk.STOCK_DIRECTORY, gtk.ICON_SIZE_MENU)
89 @property
90 def path_list(self):
91 path = []
93 node = copy.copy(self.node)
95 while 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.
100 path.reverse()
102 return path
104 @property
105 def path(self):
106 path = self.path_list
108 if len(path) == 0:
109 return None
110 else:
111 return ":".join(path)
113 @property
114 def iter(self):
115 path = self.path
117 if path == None:
118 return None
119 else:
120 ble = self.store.get_iter_from_string(path)
121 return self.store.get_iter_from_string(path)
123 @property
124 def parent_path(self):
125 path = self.path_list
127 path.pop()
129 if len(path) == 0:
130 return None
131 else:
132 return ":".join(path)
134 @property
135 def parent_iter(self):
136 path = self.parent_path
137 #print path
138 if path == None:
139 return None
140 else:
141 return self.store.get_iter_from_string(path)
143 class FeedListModel:
144 ''' The model for the feed list view '''
146 def __init__(self):
147 self.refresh_tree()
148 self._init_signals()
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]
176 if node.type == "F":
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),
189 'black',
190 node.unread_count,
191 node, editable])
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)
201 @property
202 def model(self):
203 return self.store
205 def search(self, rows, func, data):
206 if not rows: return None
207 for row in rows:
208 if func(row, data):
209 return row
210 result = self.search(row.iterchildren(), func, data)
211 if result: return result
212 return None
214 class FeedsView(MVP.WidgetView):
215 def _initialize(self):
216 self._widget.set_search_column(Column.name)
218 # pixbuf column
219 column = gtk.TreeViewColumn()
220 unread_renderer = gtk.CellRendererText()
221 column.pack_start(unread_renderer, False)
222 column.set_attributes(unread_renderer,
223 text=Column.unread)
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,
240 markup=Column.name,
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)],
271 gtk.gdk.ACTION_MOVE)
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)
280 if temp != None:
281 path, pos = temp
282 else:
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)
291 from_path = path
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"
296 to_path.append(0)
297 elif pos == gtk.TREE_VIEW_DROP_INTO_OR_AFTER:
298 print "TREE_VIEW_DROP_INTO_OR_AFTER"
299 to_path.append(0)
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)))
309 else:
310 iter = None
312 #if order + 1 >= model.iter_n_children(iter):
313 # to_path.append(path_of_target_iter[len(path_of_target_iter) - 1])
314 #else:
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)
324 else:
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:
331 return False
332 else:
333 return True
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)
346 for i in range(n):
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):
361 retval = 0
363 if event.button == 3:
364 x = int(event.x)
365 y = int(event.y)
366 time = gtk.get_current_event_time()
367 path = treeview.get_path_at_pos(x, y)
369 if path is None:
370 return 1
372 path = path[0]
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)
379 retval = 1
381 return retval
383 def selected(self):
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:
389 yield tv_node
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]
395 try:
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)
401 return
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
417 if 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])
446 for node in nodes:
447 self.model.store.remove(node.iter)
449 def on_node_edit_title_canceled(self, cellrenderer):
450 if self.new_child:
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):
479 return False
480 self.set_cursor(treeiter)
481 return True
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.
495 has_unread = False
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:
502 next(model, current)
503 self.set_cursor(treeiter)
504 return True
505 selection = self._widget.get_selection()
506 (model, pathlist) = selection.get_selected_rows()
507 iters = [model.get_iter(path) for path in pathlist]
508 try:
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)
519 has_unread = True
520 else:
521 has_unread = next(model, current)
522 has_unread = next(model,current)
523 except IndexError:
524 self.set_cursor(model.get_iter_first())
525 has_unread = True
526 return has_unread
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]
546 try:
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)
552 return
553 previous(model, current_first)
554 except IndexError:
555 self.set_cursor(model.get_iter_first())
556 return
558 def set_cursor(self, treeiter, col_id=None, edit=False):
559 if not treeiter:
560 return
561 column = None
562 path = self._model.model.get_path(treeiter)
563 if col_id:
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()
568 return
570 class FeedsPresenter(MVP.BasicPresenter):
571 def _initialize(self):
572 self.model = FeedListModel()
573 self._init_signals()
575 def _init_signals(self):
576 pass
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
598 after the iter2 row.
600 retval = 0
601 fa = model.get_value(a, Column.OBJECT)
602 fb = model.get_value(b, Column.OBJECT)
604 if fa and fb:
605 retval = locale.strcoll(fa.title, fb.title)
606 elif fa is not None: retval = -1
607 elif fb is not None: retval = 1
608 return retval
610 def show_feed_information(self, feed):
611 straw.feed_properties_show(feed)