Another batch of fixes for reparenting.
[straw.git] / straw / FeedListView.py
blob7210f32e1725b089815cb317c439dfde9ecbaa72
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 model import Feed, Item, Category
21 import Config
22 import FeedManager
23 import MVP
24 import gobject
25 import gtk
26 import helpers
27 import os, copy, locale, logging
28 import pango
29 import straw
31 class Column:
32 pixbuf, name, foreground, unread, object = range(5)
34 _tmp_widget = gtk.Label()
36 class TreeViewNode(object):
37 def __init__(self, node, store):
38 self.node = node
39 self.store = store
41 self.setup_node()
43 def setup_node(self):
44 self.node.connect("notify", self.obj_changed)
46 def obj_changed(self, obj, property):
47 if property.name == "unread-count":
48 self.store.set(self.iter, 3, self.node.unread_count)
49 elif property.name == "status":
50 if (self.node.status & straw.FS_UPDATING) > 0:
51 title = self.store.get_value(self.iter, 1)
52 self.store.set(self.iter, 1, "<i>" + title + "</i>")
53 else:
54 title = self.node.title
55 self.store.set(self.iter, 1, title)
57 @property
58 def title(self):
59 ''' The title of the node be it a category or a feed '''
60 if self.node.type == "C":
61 return self.node.name
62 elif self.node.type == "F":
63 return self.node.title
65 @property
66 def unread_count(self):
67 ''' The title of the node be it a category or a feed '''
68 return self.node.unread_count
70 @property
71 def pixbuf(self):
72 ''' gets the pixbuf to display according to the status of the feed '''
73 global _tmp_widget
75 if isinstance(self.node, Feed):
76 return _tmp_widget.render_icon(gtk.STOCK_FILE, gtk.ICON_SIZE_MENU)
77 else:
78 return _tmp_widget.render_icon(gtk.STOCK_DIRECTORY, gtk.ICON_SIZE_MENU)
80 @property
81 def path_list(self):
82 path = []
84 node = copy.copy(self.node)
86 while node:
87 path.append(str(node.norder))
88 node = copy.copy(node.parent)
90 path.pop() # We don't need path to "root" category here since it's not in the tree view.
91 path.reverse()
93 return path
95 @property
96 def path(self):
97 path = self.path_list
99 if len(path) == 0:
100 return None
101 else:
102 return ":".join(path)
104 @property
105 def iter(self):
106 path = self.path
108 if path == None:
109 return None
110 else:
111 ble = self.store.get_iter_from_string(path)
112 return self.store.get_iter_from_string(path)
114 @property
115 def parent_path(self):
116 path = self.path_list
118 path.pop()
120 if len(path) == 0:
121 return None
122 else:
123 return ":".join(path)
125 @property
126 def parent_iter(self):
127 path = self.parent_path
128 #print path
129 if path == None:
130 return None
131 else:
132 return self.store.get_iter_from_string(path)
134 class FeedListModel:
135 ''' The model for the feed list view '''
137 def __init__(self):
138 self.refresh_tree()
139 self._init_signals()
141 def refresh_tree(self):
142 self.appmodel = FeedManager.get_model()
144 self._prepare_store()
145 self._prepare_model()
147 self._populate_tree(1, None, [])
149 def _init_signals(self):
150 FeedManager._get_instance().connect("feed-added", self.node_added_cb)
151 FeedManager._get_instance().connect("category-added", self.node_added_cb)
153 def _prepare_model(self):
154 self.tv_nodes = {}
155 #print [TreeViewNode(child_node, self.store) for child_node in self.appmodel[1].children][1].node.parent_id
156 for node in self.appmodel.values():
157 if not self.tv_nodes.has_key(node.parent_id) and node.parent:
158 self.tv_nodes[node.parent_id] = [TreeViewNode(child_node, self.store) for child_node in node.parent.children]
160 def _prepare_store(self):
161 self.store = gtk.TreeStore(gtk.gdk.Pixbuf, str, str, str, gobject.TYPE_PYOBJECT)
163 def _populate_tree(self, parent_id, parent_iter, done):
164 if not self.tv_nodes.has_key(parent_id):
165 return
167 for tv_node in self.tv_nodes[parent_id]:
168 node = tv_node.node
170 if node.type == "F":
171 tv_node.treeiter = self._create_row(tv_node)
172 tv_node.store = self.store
173 elif node.type == "C":
174 current_parent = self._create_row(tv_node)
175 tv_node.treeiter = current_parent
176 tv_node.store = self.store
178 if self.tv_nodes.has_key(node.id):
179 self._populate_tree(node.id, current_parent, done)
181 def _create_row(self, node):
182 return self.store.append(node.parent_iter, [node.pixbuf,
183 node.title,
184 'black',
185 node.unread_count,
186 node])
188 def add_node(self, tv_node):
189 if not self.tv_nodes.has_key(tv_node.node.parent_id):
190 self.tv_nodes[tv_node.node.parent_id] = []
192 self.tv_nodes[tv_node.node.parent_id].append(tv_node)
194 def node_added_cb(self, src, node):
195 tv_node = TreeViewNode(node, self.store)
196 self.add_node(tv_node)
198 self._create_row(tv_node)
200 """if parent_node != None and hasattr(parent_node, "treeiter"):
201 parent_iter = parent_node.treeiter
202 tv_node.treeiter = self._create_row(tv_node, parent_iter)
203 tv_node.store = self.store"""
205 @property
206 def model(self):
207 return self.store
209 def search(self, rows, func, data):
210 if not rows: return None
211 for row in rows:
212 if func(row, data):
213 return row
214 result = self.search(row.iterchildren(), func, data)
215 if result: return result
216 return None
218 class FeedsView(MVP.WidgetView):
219 def _initialize(self):
220 self._widget.set_search_column(Column.name)
222 # pixbuf column
223 column = gtk.TreeViewColumn()
224 unread_renderer = gtk.CellRendererText()
225 column.pack_start(unread_renderer, False)
226 column.set_attributes(unread_renderer,
227 text=Column.unread)
229 status_renderer = gtk.CellRendererPixbuf()
230 column.pack_start(status_renderer, False)
231 column.set_attributes(status_renderer,
232 pixbuf=Column.pixbuf)
234 # feed title renderer
235 title_renderer = gtk.CellRendererText()
236 column.pack_start(title_renderer, False)
237 column.set_attributes(title_renderer,
238 foreground=Column.foreground,
239 markup=Column.name) #, weight=Column.BOLD)
241 self._widget.append_column(column)
243 selection = self._widget.get_selection()
244 selection.set_mode(gtk.SELECTION_SINGLE)
246 self._widget.connect("button_press_event", self._on_button_press_event)
247 self._widget.connect("popup-menu", self._on_popup_menu)
249 uifactory = helpers.UIFactory('FeedListActions')
250 action = uifactory.get_action('/feedlist_popup/refresh')
251 action.connect('activate', self.on_menu_poll_selected_activate)
252 action = uifactory.get_action('/feedlist_popup/mark_as_read')
253 action.connect('activate', self.on_menu_mark_all_as_read_activate)
254 action = uifactory.get_action('/feedlist_popup/stop_refresh')
255 action.connect('activate', self.on_menu_stop_poll_selected_activate)
256 action = uifactory.get_action('/feedlist_popup/remove')
257 action.connect('activate', self.on_remove_selected_feed)
258 action = uifactory.get_action('/feedlist_popup/properties')
259 action.connect('activate', self.on_display_properties_feed)
260 self.popup = uifactory.get_popup('/feedlist_popup')
262 treeview = self._widget
264 treeview.enable_model_drag_source(gtk.gdk.BUTTON1_MASK,
265 [("example", 0, 0)], gtk.gdk.ACTION_COPY)
266 treeview.enable_model_drag_dest([("example", 0, 0)],
267 gtk.gdk.ACTION_COPY)
268 treeview.connect("drag_data_received", self.on_dragdata_received_cb)
270 def on_dragdata_received_cb(self, treeview, drag_context, x, y, selection, info, eventtime):
271 model, iter_to_copy = treeview.get_selection().get_selected()
272 #print locals()
273 temp = treeview.get_dest_row_at_pos(x, y)
275 #temp = treeview.get_drag_dest_row()
277 if temp != None:
278 path, pos = temp
279 else:
280 path, pos = (len(model) - 1,), gtk.TREE_VIEW_DROP_AFTER
282 target_iter = model.get_iter(path)
283 path_of_target_iter = model.get_path(target_iter)
285 if self.check_row_path(model, iter_to_copy, target_iter):
286 path = model.get_path(iter_to_copy)
287 #model.insert_before(None, None, model[path])
288 #print model[path][Column.object].obj.title
289 #print "previous = %s" % str(path)
290 #print "target = %s" % str(path_of_target_iter)
291 #print path_of_target_iter[len(path_of_target_iter) - 1]
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(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 FeedManager.move_node(model[from_path][Column.object].node, to_path)
323 self.iter_copy(model, iter_to_copy, target_iter, pos)
324 drag_context.finish(True, True, eventtime)
325 #model.remove(iter_to_copy)
326 #treeview.expand_all()
327 else:
328 drag_context.finish(False, False, eventtime)
330 def check_row_path(self, model, iter_to_copy, target_iter):
331 path_of_iter_to_copy = model.get_path(iter_to_copy)
332 path_of_target_iter = model.get_path(target_iter)
333 if path_of_target_iter[0:len(path_of_iter_to_copy)] == path_of_iter_to_copy:
334 return False
335 else:
336 return True
338 def iter_copy(self, model, iter_to_copy, target_iter, pos):
339 path = model.get_path(iter_to_copy)
341 if (pos == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE) or (pos == gtk.TREE_VIEW_DROP_INTO_OR_AFTER):
342 new_iter = model.prepend(target_iter, model[path])
343 elif pos == gtk.TREE_VIEW_DROP_BEFORE:
344 new_iter = model.insert_before(None, target_iter, model[path])
345 elif pos == gtk.TREE_VIEW_DROP_AFTER:
346 new_iter = model.insert_after(None, target_iter, model[path])
348 n = model.iter_n_children(iter_to_copy)
349 for i in range(n):
350 next_iter_to_copy = model.iter_nth_child(iter_to_copy, n - i - 1)
351 self.iter_copy(model, next_iter_to_copy, new_iter, gtk.TREE_VIEW_DROP_INTO_OR_BEFORE)
353 def _model_set(self):
354 self._widget.set_model(self._model.model)
356 def add_selection_changed_listener(self, listener):
357 selection = self._widget.get_selection()
358 selection.connect('changed', listener.feedlist_selection_changed, Column.object)
360 def _on_popup_menu(self, treeview, *args):
361 self.popup.popup(None, None, None, 0, 0)
363 def _on_button_press_event(self, treeview, event):
364 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)
370 if path is None:
371 return 1
372 path, col, cellx, celly = path
373 selection = treeview.get_selection()
374 selection.unselect_all()
375 selection.select_path(path)
376 treeview.grab_focus()
377 self.popup.popup(None, None, None, event.button, time)
378 retval = 1
379 return retval
381 def foreach_selected(self, func):
382 selection = self._widget.get_selection()
383 (model, pathlist) = selection.get_selected_rows()
384 iters = [model.get_iter(path) for path in pathlist]
385 try:
386 for treeiter in iters:
387 object = model.get_value(treeiter, Column.object)
388 func(object, model, treeiter)
389 except TypeError, te:
390 ## XXX maybe object is a category
391 logging.exception(te)
392 return
394 def on_menu_poll_selected_activate(self, *args):
395 config = Config.get_instance()
397 if config.offline: #XXX
398 config.offline = not config.offline
400 selection = self._widget.get_selection()
401 (model, pathlist) = selection.get_selected_rows()
402 iters = [model.get_iter(path) for path in pathlist]
403 nodes = [model.get_value(treeiter,Column.object) for treeiter in iters]
405 FeedManager.update_all_feeds({}, [node.node for node in nodes])
407 def on_menu_stop_poll_selected_activate(self, *args):
408 self.foreach_selected(lambda o,*args: o.feed.router.stop_polling())
410 def on_menu_mark_all_as_read_activate(self, *args):
411 self.foreach_selected(lambda o,*args: o.feed.mark_items_as_read())
413 def on_remove_selected_feed(self, *args):
414 def remove(*args):
415 (object, model, treeiter) = args
416 model.remove(treeiter)
417 feedlist = feeds.get_feedlist_instance()
418 idx = feedlist.index(object.feed)
419 del feedlist[idx]
420 self.foreach_selected(remove)
421 return
423 def on_display_properties_feed(self, *args):
424 selection = self._widget.get_selection()
425 (model, pathlist) = selection.get_selected_rows()
426 iters = [model.get_iter(path) for path in pathlist]
427 path = pathlist.pop()
428 node = self.model.model[path][Column.object]
429 self._presenter.show_feed_information(node)
430 return
432 def select_first_feed(self):
433 selection = self._widget.get_selection()
434 (model, pathlist) = selection.get_selected_rows()
435 treeiter = model.get_iter_first()
436 if not treeiter or not model.iter_is_valid(treeiter):
437 return False
438 self.set_cursor(treeiter)
439 return True
441 def select_next_feed(self, with_unread=False):
442 ''' Scrolls to the next feed in the feed list
444 If there is no selection, selects the first feed. If multiple feeds
445 are selected, selects the feed after the last selected feed.
447 If unread is True, selects the next unread with unread items.
449 If the selection next-to-be is a category, go to the iter its first
450 child. If current selection is a child, then go to (parent + 1),
451 provided that (parent + 1) is not a category.
453 has_unread = False
454 def next(model, current):
455 treeiter = model.iter_next(current)
456 if not treeiter: return False
457 if model.iter_depth(current): next(model, model.iter_parent(current))
458 path = model.get_path(treeiter)
459 if with_unread and model[path][Column.unread] < 1:
460 next(model, current)
461 self.set_cursor(treeiter)
462 return True
463 selection = self._widget.get_selection()
464 (model, pathlist) = selection.get_selected_rows()
465 iters = [model.get_iter(path) for path in pathlist]
466 try:
467 current = iters.pop()
468 if model.iter_has_child(current):
469 iterchild = model.iter_children(current)
470 # make the row visible
471 path = model.get_path(iterchild)
472 for i in range(len(path)):
473 self._widget.expand_row(path[:i+1], False)
474 # select his first born child
475 if with_unread and model[path][Column.unread] > 0:
476 self.set_cursor(iterchild)
477 has_unread = True
478 else:
479 has_unread = next(model, current)
480 has_unread = next(model,current)
481 except IndexError:
482 self.set_cursor(model.get_iter_first())
483 has_unread = True
484 return has_unread
486 def select_previous_feed(self):
487 ''' Scrolls to the previous feed in the feed list.
489 If there is no selection, selects the first feed. If there's multiple
490 selection, selects the feed before the first selected feed.
492 If the previous selection is a category, select the last node in that
493 category. If the current selection is a child, then go to (parent -
494 1). If parent is the first feed, wrap and select the last feed or
495 category in the list.
497 def previous(model, current):
498 path = model.get_path(current)
499 treerow = model[path[-1]-1]
500 self.set_cursor(treerow.iter)
501 selection = self._widget.get_selection()
502 (model, pathlist) = selection.get_selected_rows()
503 iters = [model.get_iter(path) for path in pathlist]
504 try:
505 current_first = iters.pop(0)
506 if model.iter_has_child(current_first):
507 children = model.iter_n_children(current_first)
508 treeiter = model.iter_nth_child(children - 1)
509 self.set_cursor(treeiter)
510 return
511 previous(model, current_first)
512 except IndexError:
513 self.set_cursor(model.get_iter_first())
514 return
516 def set_cursor(self, treeiter, col_id=None, edit=False):
517 if not treeiter:
518 return
519 column = None
520 path = self._model.model.get_path(treeiter)
521 if col_id:
522 column = self._widget.get_column(col_id)
523 self._widget.set_cursor(path, column, edit)
524 self._widget.scroll_to_cell(path, column)
525 self._widget.grab_focus()
526 return
528 class FeedsPresenter(MVP.BasicPresenter):
529 def _initialize(self):
530 self.model = FeedListModel()
531 self._init_signals()
533 def _init_signals(self):
535 pass
536 #flist.signal_connect(Event.ItemReadSignal,
537 # self._feed_item_read)
538 #flist.signal_connect(Event.AllItemsReadSignal,
539 # self._feed_all_items_read)
540 #flist.signal_connect(Event.FeedsChangedSignal,
541 # self._feeds_changed)
542 #flist.signal_connect(Event.FeedDetailChangedSignal,
543 # self._feed_detail_changed)
544 #fclist = FeedCategoryList.get_instance()
545 #fclist.signal_connect(Event.FeedCategorySortedSignal,
546 # self._feeds_sorted_cb)
547 #fclist.signal_connect(Event.FeedCategoryChangedSignal,
548 # self._fcategory_changed_cb)
550 def select_first_feed(self):
551 return self.view.select_first_feed()
553 def select_next_feed(self, with_unread=False):
554 return self.view.select_next_feed(with_unread)
556 def select_previous_feed(self):
557 return self.view.select_previous_feed()
559 def _sort_func(self, model, a, b):
561 Sorts the feeds lexically.
563 From the gtk.TreeSortable.set_sort_func doc:
565 The comparison callback should return -1 if the iter1 row should come before
566 the iter2 row, 0 if the rows are equal, or 1 if the iter1 row should come
567 after the iter2 row.
569 retval = 0
570 fa = model.get_value(a, Column.OBJECT)
571 fb = model.get_value(b, Column.OBJECT)
573 if fa and fb:
574 retval = locale.strcoll(fa.title, fb.title)
575 elif fa is not None: retval = -1
576 elif fb is not None: retval = 1
577 return retval
579 def show_feed_information(self, feed):
580 straw.feed_properties_show(feed)