Fixed exception that occured in some cases when removing nodes.
[straw.git] / straw / FeedListView.py
blob6f8f815366a1861bd58d515348cf7295abb09269
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 try:
121 return self.store.get_iter_from_string(path)
122 except:
123 return None
125 @property
126 def parent_path(self):
127 path = self.path_list
129 path.pop()
131 if len(path) == 0:
132 return None
133 else:
134 return ":".join(path)
136 @property
137 def parent_iter(self):
138 path = self.parent_path
139 #print path
140 if path == None:
141 return None
142 else:
143 return self.store.get_iter_from_string(path)
145 class FeedListModel:
146 ''' The model for the feed list view '''
148 def __init__(self):
149 self.refresh_tree()
150 self._init_signals()
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()])
166 #print [TreeViewNode(child_node, self.store) for child_node in self.appmodel[1].children][1].node.parent_id
167 #for node in self.appmodel.values():
168 # if not self.tv_nodes.has_key(node.parent_id) and node.parent:
169 # self.tv_nodes[node.parent_id] = [TreeViewNode(child_node, self.store) for child_node in node.parent.children]
171 def _prepare_store(self):
172 self.store = gtk.TreeStore(gtk.gdk.Pixbuf, str, str, str, gobject.TYPE_PYOBJECT, bool)
174 def _populate_tree(self, parent_id, parent_iter, done):
175 for node in self.tv_nodes[parent_id].node.children:
176 tv_node = self.tv_nodes[node.id]
178 if node.type == "F":
179 self._create_row(tv_node)
180 tv_node.store = self.store
181 elif node.type == "C":
182 current_parent = self._create_row(tv_node)
183 tv_node.store = self.store
185 if self.tv_nodes.has_key(node.id):
186 self._populate_tree(node.id, current_parent, done)
188 def _create_row(self, node, editable = False):
189 return self.store.append(node.parent_iter, [node.pixbuf,
190 helpers.pango_escape(node.title),
191 'black',
192 node.unread_count,
193 node, editable])
195 def add_node(self, tv_node):
196 self.tv_nodes[tv_node.node.id] = tv_node
198 def node_added_cb(self, src, node):
199 tv_node = TreeViewNode(node, self.store)
200 self.add_node(tv_node)
201 self._create_row(tv_node)
203 @property
204 def model(self):
205 return self.store
207 def search(self, rows, func, data):
208 if not rows: return None
209 for row in rows:
210 if func(row, data):
211 return row
212 result = self.search(row.iterchildren(), func, data)
213 if result: return result
214 return None
216 class FeedsView(MVP.WidgetView):
217 def _initialize(self):
218 self._widget.set_search_column(Column.name)
220 # pixbuf column
221 column = gtk.TreeViewColumn()
222 unread_renderer = gtk.CellRendererText()
223 column.pack_start(unread_renderer, False)
224 column.set_attributes(unread_renderer,
225 text=Column.unread)
227 status_renderer = gtk.CellRendererPixbuf()
228 column.pack_start(status_renderer, False)
229 column.set_attributes(status_renderer,
230 pixbuf=Column.pixbuf)
232 # feed title renderer
233 title_renderer = gtk.CellRendererText()
235 title_renderer.connect("edited", self.on_node_edit_title_edited)
236 title_renderer.connect("editing-canceled", self.on_node_edit_title_canceled)
238 #title_renderer.set_property('editable', True)
239 column.pack_start(title_renderer, False)
240 column.set_attributes(title_renderer,
241 foreground=Column.foreground,
242 markup=Column.name,
243 editable=Column.editable) #, weight=Column.BOLD)
245 self._widget.append_column(column)
247 selection = self._widget.get_selection()
248 selection.set_mode(gtk.SELECTION_MULTIPLE)
250 self._widget.connect("button_press_event", self._on_button_press_event)
251 self._widget.connect("popup-menu", self._on_popup_menu)
253 uifactory = helpers.UIFactory('FeedListActions')
254 action = uifactory.get_action('/feedlist_popup/refresh')
255 action.connect('activate', self.on_menu_poll_selected_activate)
256 action = uifactory.get_action('/feedlist_popup/add_child')
257 action.connect('activate', self.on_menu_add_child_activate)
258 action = uifactory.get_action('/feedlist_popup/mark_as_read')
259 action.connect('activate', self.on_menu_mark_all_as_read_activate)
260 action = uifactory.get_action('/feedlist_popup/stop_refresh')
261 action.connect('activate', self.on_menu_stop_poll_selected_activate)
262 action = uifactory.get_action('/feedlist_popup/remove')
263 action.connect('activate', self.on_remove_selected_feed)
264 action = uifactory.get_action('/feedlist_popup/properties')
265 action.connect('activate', self.on_display_properties_feed)
266 self.popup = uifactory.get_popup('/feedlist_popup')
268 treeview = self._widget
270 treeview.enable_model_drag_source(gtk.gdk.BUTTON1_MASK,
271 [("example", 0, 0)], gtk.gdk.ACTION_MOVE)
272 treeview.enable_model_drag_dest([("example", 0, 0)],
273 gtk.gdk.ACTION_MOVE)
274 treeview.connect("drag_data_received", self.on_dragdata_received_cb)
276 def on_dragdata_received_cb(self, treeview, drag_context, x, y, selection, info, eventtime):
277 model, pathlist = treeview.get_selection().get_selected_rows()
278 iter_to_copy = model.get_iter(pathlist[0])
280 temp = treeview.get_dest_row_at_pos(x, y)
282 if temp != None:
283 path, pos = temp
284 else:
285 path, pos = (len(model) - 1,), gtk.TREE_VIEW_DROP_AFTER
287 target_iter = model.get_iter(path)
288 path_of_target_iter = model.get_path(target_iter)
290 if self.check_row_path(model, iter_to_copy, target_iter):
291 path = model.get_path(iter_to_copy)
293 from_path = path
294 to_path = list(path_of_target_iter)
296 if pos == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE:
297 print "TREE_VIEW_DROP_INTO_OR_BEFORE"
298 to_path.append(0)
299 elif pos == gtk.TREE_VIEW_DROP_INTO_OR_AFTER:
300 print "TREE_VIEW_DROP_INTO_OR_AFTER"
301 to_path.append(0)
302 elif pos == gtk.TREE_VIEW_DROP_BEFORE:
303 print "dropping before"
304 elif pos == gtk.TREE_VIEW_DROP_AFTER:
305 print "dropping after %s" % (str(path_of_target_iter))
306 to_path = list(path_of_target_iter)
307 order = to_path.pop()
309 if ":".join(map(str, to_path)):
310 iter = model.get_iter(":".join(map(str, to_path)))
311 else:
312 iter = None
314 #if order + 1 >= model.iter_n_children(iter):
315 # to_path.append(path_of_target_iter[len(path_of_target_iter) - 1])
316 #else:
317 to_path.append(path_of_target_iter[len(path_of_target_iter) - 1] + 1)
319 print "%s -> %s" % (str(from_path), str(to_path))
321 node = model[from_path][Column.object].node
322 self.iter_copy(model, iter_to_copy, target_iter, pos)
324 drag_context.finish(True, True, eventtime)
325 FeedManager.move_node(node, to_path)
326 else:
327 drag_context.finish(False, False, eventtime)
329 def check_row_path(self, model, iter_to_copy, target_iter):
330 path_of_iter_to_copy = model.get_path(iter_to_copy)
331 path_of_target_iter = model.get_path(target_iter)
332 if path_of_target_iter[0:len(path_of_iter_to_copy)] == path_of_iter_to_copy:
333 return False
334 else:
335 return True
337 def iter_copy(self, model, iter_to_copy, target_iter, pos):
338 path = model.get_path(iter_to_copy)
340 if (pos == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE) or (pos == gtk.TREE_VIEW_DROP_INTO_OR_AFTER):
341 new_iter = model.prepend(target_iter, model[path])
342 elif pos == gtk.TREE_VIEW_DROP_BEFORE:
343 new_iter = model.insert_before(None, target_iter, model[path])
344 elif pos == gtk.TREE_VIEW_DROP_AFTER:
345 new_iter = model.insert_after(None, target_iter, model[path])
347 n = model.iter_n_children(iter_to_copy)
348 for i in range(n):
349 next_iter_to_copy = model.iter_nth_child(iter_to_copy, n - i - 1)
350 self.iter_copy(model, next_iter_to_copy, new_iter, gtk.TREE_VIEW_DROP_INTO_OR_BEFORE)
352 def _model_set(self):
353 self._widget.set_model(self._model.model)
355 def add_selection_changed_listener(self, listener):
356 selection = self._widget.get_selection()
357 selection.connect('changed', listener.feedlist_selection_changed, Column.object)
359 def _on_popup_menu(self, treeview, *args):
360 self.popup.popup(None, None, None, 0, 0)
362 def _on_button_press_event(self, treeview, event):
363 retval = 0
365 if event.button == 3:
366 x = int(event.x)
367 y = int(event.y)
368 time = gtk.get_current_event_time()
369 path = treeview.get_path_at_pos(x, y)
371 if path is None:
372 return 1
374 path = path[0]
375 self.node_at_popup = self.model.store[path][Column.object]
376 treeview.grab_focus()
378 if self.selected_count() < 2:
379 selection = treeview.get_selection()
380 selection.unselect_all()
381 selection.select_path(path)
383 self.popup.popup(None, None, None, event.button, time)
384 retval = 1
386 return retval
388 def selected_count(self):
389 selection = self._widget.get_selection()
390 pathlist = selection.get_selected_rows()[1]
391 return len(pathlist)
393 def selected(self):
394 selection = self._widget.get_selection()
395 (model, pathlist) = selection.get_selected_rows()
396 nodes = [model[path][Column.object] for path in pathlist]
398 for tv_node in nodes:
399 yield tv_node
401 def foreach_selected(self, func):
402 selection = self._widget.get_selection()
403 (model, pathlist) = selection.get_selected_rows()
404 iters = [model.get_iter(path) for path in pathlist]
405 try:
406 for treeiter in iters:
407 object = model.get_value(treeiter, Column.object)
408 func(object, model, treeiter)
409 except TypeError, te:
410 logging.exception(te)
411 return
413 def on_menu_add_child_activate(self, *args):
414 self.begin_add_category(self.node_at_popup.node)
416 def begin_add_category(self, node):
417 category = Category()
418 category.parent = node
419 category.norder = len(node.children)
420 self.new_child = TreeViewNode(category, self.model.store)
421 iter = self.model._create_row(self.new_child, editable = True)
422 path = self.model.store.get_path(iter)
423 column = self._widget.get_column(0)
425 parent_path = self.new_child.parent_path
427 if parent_path:
428 self._widget.expand_row(parent_path, False)
430 self._widget.set_cursor_on_cell(path, focus_column = column, start_editing = True)
432 def on_menu_poll_selected_activate(self, *args):
433 config = Config.get_instance()
435 if config.offline: #XXX
436 config.offline = not config.offline
438 selection = self._widget.get_selection()
439 (model, pathlist) = selection.get_selected_rows()
440 iters = [model.get_iter(path) for path in pathlist]
441 nodes = [model.get_value(treeiter,Column.object) for treeiter in iters]
443 FeedManager.update_all_feeds({}, [node.node for node in nodes])
445 def on_menu_stop_poll_selected_activate(self, *args):
446 self.foreach_selected(lambda o,*args: o.feed.router.stop_polling())
448 def on_menu_mark_all_as_read_activate(self, *args):
449 self.foreach_selected(lambda o,*args: o.feed.mark_items_as_read())
451 def on_remove_selected_feed(self, *args):
452 nodes = [tv_node for tv_node in self.selected()]
454 FeedManager.delete_nodes([tv_node.node.id for tv_node in nodes])
456 for node in nodes:
457 iter = node.iter
459 if iter:
460 self.model.store.remove(iter)
462 def on_node_edit_title_canceled(self, cellrenderer):
463 if self.new_child:
464 debug("on_node_edit_title_canceled (new_child), removing %s" % str(self.new_child.path))
465 self.model.store.remove(self.new_child.iter)
466 self.new_child = None
468 def on_node_edit_title_edited(self, cellrenderertext, path, new_text):
469 if len(new_text) > 0:
470 self.new_child.node.name = new_text
471 FeedManager.save_category(self.new_child.node)
473 self.model.store.remove(self.new_child.iter)
474 self.new_child = None
476 def on_display_properties_feed(self, *args):
477 selection = self._widget.get_selection()
478 (model, pathlist) = selection.get_selected_rows()
479 iters = [model.get_iter(path) for path in pathlist]
480 path = pathlist.pop()
481 node = self.model.model[path][Column.object]
482 self._presenter.show_feed_information(node)
484 def add_category(self):
485 self.begin_add_category(self._model.tv_nodes[1].node)
487 def select_first_feed(self):
488 selection = self._widget.get_selection()
489 (model, pathlist) = selection.get_selected_rows()
490 treeiter = model.get_iter_first()
491 if not treeiter or not model.iter_is_valid(treeiter):
492 return False
493 self.set_cursor(treeiter)
494 return True
496 def select_next_feed(self, with_unread=False):
497 ''' Scrolls to the next feed in the feed list
499 If there is no selection, selects the first feed. If multiple feeds
500 are selected, selects the feed after the last selected feed.
502 If unread is True, selects the next unread with unread items.
504 If the selection next-to-be is a category, go to the iter its first
505 child. If current selection is a child, then go to (parent + 1),
506 provided that (parent + 1) is not a category.
508 has_unread = False
509 def next(model, current):
510 treeiter = model.iter_next(current)
511 if not treeiter: return False
512 if model.iter_depth(current): next(model, model.iter_parent(current))
513 path = model.get_path(treeiter)
514 if with_unread and model[path][Column.unread] < 1:
515 next(model, current)
516 self.set_cursor(treeiter)
517 return True
518 selection = self._widget.get_selection()
519 (model, pathlist) = selection.get_selected_rows()
520 iters = [model.get_iter(path) for path in pathlist]
521 try:
522 current = iters.pop()
523 if model.iter_has_child(current):
524 iterchild = model.iter_children(current)
525 # make the row visible
526 path = model.get_path(iterchild)
527 for i in range(len(path)):
528 self._widget.expand_row(path[:i+1], False)
529 # select his first born child
530 if with_unread and model[path][Column.unread] > 0:
531 self.set_cursor(iterchild)
532 has_unread = True
533 else:
534 has_unread = next(model, current)
535 has_unread = next(model,current)
536 except IndexError:
537 self.set_cursor(model.get_iter_first())
538 has_unread = True
539 return has_unread
541 def select_previous_feed(self):
542 ''' Scrolls to the previous feed in the feed list.
544 If there is no selection, selects the first feed. If there's multiple
545 selection, selects the feed before the first selected feed.
547 If the previous selection is a category, select the last node in that
548 category. If the current selection is a child, then go to (parent -
549 1). If parent is the first feed, wrap and select the last feed or
550 category in the list.
552 def previous(model, current):
553 path = model.get_path(current)
554 treerow = model[path[-1]-1]
555 self.set_cursor(treerow.iter)
556 selection = self._widget.get_selection()
557 (model, pathlist) = selection.get_selected_rows()
558 iters = [model.get_iter(path) for path in pathlist]
559 try:
560 current_first = iters.pop(0)
561 if model.iter_has_child(current_first):
562 children = model.iter_n_children(current_first)
563 treeiter = model.iter_nth_child(children - 1)
564 self.set_cursor(treeiter)
565 return
566 previous(model, current_first)
567 except IndexError:
568 self.set_cursor(model.get_iter_first())
569 return
571 def set_cursor(self, treeiter, col_id=None, edit=False):
572 if not treeiter:
573 return
574 column = None
575 path = self._model.model.get_path(treeiter)
576 if col_id:
577 column = self._widget.get_column(col_id)
578 self._widget.set_cursor(path, column, edit)
579 self._widget.scroll_to_cell(path, column)
580 self._widget.grab_focus()
581 return
583 class FeedsPresenter(MVP.BasicPresenter):
584 def _initialize(self):
585 self.model = FeedListModel()
586 self._init_signals()
588 def _init_signals(self):
589 pass
591 def add_category(self):
592 self.view.add_category()
594 def select_first_feed(self):
595 return self.view.select_first_feed()
597 def select_next_feed(self, with_unread=False):
598 return self.view.select_next_feed(with_unread)
600 def select_previous_feed(self):
601 return self.view.select_previous_feed()
603 def _sort_func(self, model, a, b):
605 Sorts the feeds lexically.
607 From the gtk.TreeSortable.set_sort_func doc:
609 The comparison callback should return -1 if the iter1 row should come before
610 the iter2 row, 0 if the rows are equal, or 1 if the iter1 row should come
611 after the iter2 row.
613 retval = 0
614 fa = model.get_value(a, Column.OBJECT)
615 fb = model.get_value(b, Column.OBJECT)
617 if fa and fb:
618 retval = locale.strcoll(fa.title, fb.title)
619 elif fa is not None: retval = -1
620 elif fb is not None: retval = 1
621 return retval
623 def show_feed_information(self, feed):
624 straw.feed_properties_show(feed)