Fixes (workarounds) in OPML parsing, more work on GUI...
[straw/fork.git] / straw / FeedListView.py
blob0390869395930651004027768ce67b03bc139a67
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 PollManager
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 = range(5)
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 #print self.store[(0, 0, 3)][1]
49 self.store.set(self.treeiter, 3, self.node.unread_count)
50 elif property.name == "status":
51 if (self.node.status & straw.FS_UPDATING) > 0:
52 title = self.store.get_value(self.treeiter, 1)
53 self.store.set(self.treeiter, 1, "<i>" + title + "</i>")
54 else:
55 title = self.node.title
56 self.store.set(self.treeiter, 1, title)
58 @property
59 def title(self):
60 ''' The title of the node be it a category or a feed '''
61 if self.node.type == "C":
62 return self.node.name
63 elif self.node.type == "F":
64 return self.node.title
66 @property
67 def unread_count(self):
68 ''' The title of the node be it a category or a feed '''
69 return self.node.unread_count
71 @property
72 def pixbuf(self):
73 ''' gets the pixbuf to display according to the status of the feed '''
74 import gtk
75 widget = gtk.Label()
77 if isinstance(self.node, Feed):
78 return widget.render_icon(gtk.STOCK_FILE, gtk.ICON_SIZE_MENU)
79 else:
80 return widget.render_icon(gtk.STOCK_DIRECTORY, gtk.ICON_SIZE_MENU)
82 # ignore why above is a gtk.Label. We just need
83 # gtk.Widget.render_icon to get a pixbuf out of a stock image.
84 # Fyi, gtk.Widget is abstract that is why we need a subclass to do
85 # this for us.
86 try:
87 if self.node.process_status is not feeds.Feed.STATUS_IDLE:
88 return widget.render_icon(gtk.STOCK_EXECUTE, gtk.ICON_SIZE_MENU)
89 elif self.node.error:
90 return widget.render_icon(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_MENU)
91 except AttributeError, ex:
92 logging.exception(ex)
93 return widget.render_icon(gtk.STOCK_DIRECTORY, gtk.ICON_SIZE_MENU)
94 return self.default_pixbuf
96 @property
97 def parent_path(self):
98 import copy
100 path = []
102 if self.node.parent != None:
103 node = copy.copy(self.node.parent)
105 while node != None:
106 path.append(str(node.norder))
107 node = copy.copy(node.parent)
109 path.pop()
110 path.reverse()
111 if len(path) == 0:
112 return None
113 else:
114 return ":".join(path)
116 @property
117 def parent_iter(self):
118 path = self.parent_path
120 if path == None:
121 return None
122 else:
123 return self.store.get_iter_from_string(path)
125 class FeedListModel:
126 ''' The model for the feed list view '''
128 def __init__(self):
129 self.refresh_tree()
130 self._init_signals()
132 def refresh_tree(self):
133 self.categories, self.appmodel = FeedManager.get_model()
135 self._prepare_store()
136 self._prepare_model()
138 #print self.tv_nodes[1]
140 self._populate_tree(1, None, [])
142 def _init_signals(self):
143 FeedManager._get_instance().connect("feed-added", self.node_added_cb)
144 FeedManager._get_instance().connect("category-added", self.node_added_cb)
146 def _prepare_model(self):
147 self.tv_nodes = {}
149 for parent_id in self.appmodel.keys():
150 self.tv_nodes[parent_id] = [TreeViewNode(node, self.store) for node in self.appmodel[parent_id]]
152 def _prepare_store(self):
153 self.store = gtk.TreeStore(gtk.gdk.Pixbuf, str, str, str, gobject.TYPE_PYOBJECT)
155 def _populate_tree(self, parent_id, parent_iter, done):
156 if not self.tv_nodes.has_key(parent_id):
157 return
159 for tv_node in self.tv_nodes[parent_id]:
160 node = tv_node.node
162 if node.type == "F":
163 tv_node.treeiter = self._create_row(tv_node)
164 tv_node.store = self.store
165 elif node.type == "C":
166 current_parent = self._create_row(tv_node)
167 tv_node.treeiter = current_parent
168 tv_node.store = self.store
170 if self.tv_nodes.has_key(node.id):
171 self._populate_tree(node.id, current_parent, done)
173 def _create_row(self, node):
174 return self.store.append(node.parent_iter, [node.pixbuf,
175 node.title,
176 'black',
177 node.unread_count,
178 node])
180 def _lookup_parent(self, _tv_node):
181 for tv_node in self.tv_nodes[_tv_node.node.parent_id]:
182 if tv_node.node.type == "C" and _tv_node.node.parent_id == tv_node.node.id:
183 return tv_node
185 def add_node(self, tv_node):
186 if not self.tv_nodes.has_key(tv_node.node.parent_id):
187 self.tv_nodes[tv_node.node.parent_id] = []
189 self.tv_nodes[tv_node.node.parent_id].append(tv_node)
191 def node_added_cb(self, src, node):
192 tv_node = TreeViewNode(node, self.store)
193 self.add_node(tv_node)
195 self._create_row(tv_node)
197 """if parent_node != None and hasattr(parent_node, "treeiter"):
198 parent_iter = parent_node.treeiter
199 tv_node.treeiter = self._create_row(tv_node, parent_iter)
200 tv_node.store = self.store"""
202 @property
203 def model(self):
204 return self.store
206 def search(self, rows, func, data):
207 if not rows: return None
208 for row in rows:
209 if func(row, data):
210 return row
211 result = self.search(row.iterchildren(), func, data)
212 if result: return result
213 return None
215 class FeedsView(MVP.WidgetView):
216 def _initialize(self):
217 self._widget.set_search_column(Column.name)
219 # pixbuf column
220 column = gtk.TreeViewColumn()
221 unread_renderer = gtk.CellRendererText()
222 column.pack_start(unread_renderer, False)
223 column.set_attributes(unread_renderer,
224 text=Column.unread)
226 status_renderer = gtk.CellRendererPixbuf()
227 column.pack_start(status_renderer, False)
228 column.set_attributes(status_renderer,
229 pixbuf=Column.pixbuf)
231 # feed title renderer
232 title_renderer = gtk.CellRendererText()
233 column.pack_start(title_renderer, False)
234 column.set_attributes(title_renderer,
235 foreground=Column.foreground,
236 markup=Column.name) #, weight=Column.BOLD)
238 self._widget.append_column(column)
240 selection = self._widget.get_selection()
241 selection.set_mode(gtk.SELECTION_SINGLE)
243 self._widget.connect("button_press_event", self._on_button_press_event)
244 self._widget.connect("popup-menu", self._on_popup_menu)
246 uifactory = helpers.UIFactory('FeedListActions')
247 action = uifactory.get_action('/feedlist_popup/refresh')
248 action.connect('activate', self.on_menu_poll_selected_activate)
249 action = uifactory.get_action('/feedlist_popup/mark_as_read')
250 action.connect('activate', self.on_menu_mark_all_as_read_activate)
251 action = uifactory.get_action('/feedlist_popup/stop_refresh')
252 action.connect('activate', self.on_menu_stop_poll_selected_activate)
253 action = uifactory.get_action('/feedlist_popup/remove')
254 action.connect('activate', self.on_remove_selected_feed)
255 action = uifactory.get_action('/feedlist_popup/properties')
256 action.connect('activate', self.on_display_properties_feed)
257 self.popup = uifactory.get_popup('/feedlist_popup')
259 treeview = self._widget
261 treeview.enable_model_drag_source(gtk.gdk.BUTTON1_MASK,
262 [("example", 0, 0)], gtk.gdk.ACTION_COPY)
263 treeview.enable_model_drag_dest([("example", 0, 0)],
264 gtk.gdk.ACTION_COPY)
265 treeview.connect("drag_data_received", self.on_dragdata_received_cb)
267 def on_dragdata_received_cb(self, treeview, drag_context, x, y, selection, info, eventtime):
268 model, iter_to_copy = treeview.get_selection().get_selected()
269 #print locals()
270 temp = treeview.get_dest_row_at_pos(x, y)
272 #temp = treeview.get_drag_dest_row()
274 if temp != None:
275 path, pos = temp
276 else:
277 path, pos = (len(model)-1,), gtk.TREE_VIEW_DROP_AFTER
279 target_iter = model.get_iter(path)
280 path_of_target_iter = model.get_path(target_iter)
282 if self.check_row_path(model, iter_to_copy, target_iter):
283 path = model.get_path(iter_to_copy)
284 #model.insert_before(None, None, model[path])
285 #print model[path][Column.object].obj.title
286 #print "previous = %s" % str(path)
287 #print "target = %s" % str(path_of_target_iter)
288 #print path_of_target_iter[len(path_of_target_iter) - 1]
290 from_path = path
291 to_path = list(path_of_target_iter)
293 if (pos == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE) or (pos == gtk.TREE_VIEW_DROP_INTO_OR_AFTER):
294 pass#new_iter = model.prepend(target_iter, model[path])
295 elif pos == gtk.TREE_VIEW_DROP_BEFORE:
296 pass
297 elif pos == gtk.TREE_VIEW_DROP_AFTER:
298 to_path = list(path_of_target_iter[0:len(path_of_target_iter) - 2])
299 to_path.append(path_of_target_iter[len(path_of_target_iter) - 1] + 1)
301 print "%s -> %s" % (str(from_path), str(to_path))
303 self.iter_copy(model, iter_to_copy, target_iter, pos)
304 drag_context.finish(True, True, eventtime)
305 #model.remove(iter_to_copy)
306 #treeview.expand_all()
307 else:
308 drag_context.finish(False, False, eventtime)
310 def check_row_path(self, model, iter_to_copy, target_iter):
311 path_of_iter_to_copy = model.get_path(iter_to_copy)
312 path_of_target_iter = model.get_path(target_iter)
313 if path_of_target_iter[0:len(path_of_iter_to_copy)] == path_of_iter_to_copy:
314 return False
315 else:
316 return True
318 def iter_copy(self, model, iter_to_copy, target_iter, pos):
319 path = model.get_path(iter_to_copy)
321 if (pos == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE) or (pos == gtk.TREE_VIEW_DROP_INTO_OR_AFTER):
322 new_iter = model.prepend(target_iter, model[path])
323 elif pos == gtk.TREE_VIEW_DROP_BEFORE:
324 new_iter = model.insert_before(None, target_iter, model[path])
325 elif pos == gtk.TREE_VIEW_DROP_AFTER:
326 new_iter = model.insert_after(None, target_iter, model[path])
328 n = model.iter_n_children(iter_to_copy)
329 for i in range(n):
330 next_iter_to_copy = model.iter_nth_child(iter_to_copy, n - i - 1)
331 self.iter_copy(model, next_iter_to_copy, new_iter, gtk.TREE_VIEW_DROP_INTO_OR_BEFORE)
333 def _model_set(self):
334 self._widget.set_model(self._model.model)
336 def add_selection_changed_listener(self, listener):
337 selection = self._widget.get_selection()
338 selection.connect('changed', listener.feedlist_selection_changed, Column.object)
340 def _on_popup_menu(self, treeview, *args):
341 self.popup.popup(None, None, None, 0, 0)
343 def _on_button_press_event(self, treeview, event):
344 retval = 0
345 if event.button == 3:
346 x = int(event.x)
347 y = int(event.y)
348 time = gtk.get_current_event_time()
349 path = treeview.get_path_at_pos(x, y)
350 if path is None:
351 return 1
352 path, col, cellx, celly = path
353 selection = treeview.get_selection()
354 selection.unselect_all()
355 selection.select_path(path)
356 treeview.grab_focus()
357 self.popup.popup(None, None, None, event.button, time)
358 retval = 1
359 return retval
361 def foreach_selected(self, func):
362 selection = self._widget.get_selection()
363 (model, pathlist) = selection.get_selected_rows()
364 iters = [model.get_iter(path) for path in pathlist]
365 try:
366 for treeiter in iters:
367 object = model.get_value(treeiter, Column.object)
368 func(object, model, treeiter)
369 except TypeError, te:
370 ## XXX maybe object is a category
371 logging.exception(te)
372 return
374 def on_menu_poll_selected_activate(self, *args):
375 config = Config.get_instance()
376 poll = True
377 if config.offline: #XXX
378 config.offline = not config.offline
379 selection = self._widget.get_selection()
380 (model, pathlist) = selection.get_selected_rows()
381 iters = [model.get_iter(path) for path in pathlist]
382 nodes = [model.get_value(treeiter,Column.object) for treeiter in iters]
383 fds = []
384 for n in nodes:
385 try:
386 fds.append(n.feed)
387 except TypeError:
388 fds += n.category.feeds
389 if fds:
390 PollManager.get_instance().poll(fds)
391 return
393 def on_menu_stop_poll_selected_activate(self, *args):
394 self.foreach_selected(lambda o,*args: o.feed.router.stop_polling())
396 def on_menu_mark_all_as_read_activate(self, *args):
397 self.foreach_selected(lambda o,*args: o.feed.mark_items_as_read())
399 def on_remove_selected_feed(self, *args):
400 def remove(*args):
401 (object, model, treeiter) = args
402 model.remove(treeiter)
403 feedlist = feeds.get_feedlist_instance()
404 idx = feedlist.index(object.feed)
405 del feedlist[idx]
406 self.foreach_selected(remove)
407 return
409 def on_display_properties_feed(self, *args):
410 selection = self._widget.get_selection()
411 (model, pathlist) = selection.get_selected_rows()
412 iters = [model.get_iter(path) for path in pathlist]
413 path = pathlist.pop()
414 node = self.model.model[path][Column.object]
415 self._presenter.show_feed_information(node)
416 return
418 def select_first_feed(self):
419 selection = self._widget.get_selection()
420 (model, pathlist) = selection.get_selected_rows()
421 treeiter = model.get_iter_first()
422 if not treeiter or not model.iter_is_valid(treeiter):
423 return False
424 self.set_cursor(treeiter)
425 return True
427 def select_next_feed(self, with_unread=False):
428 ''' Scrolls to the next feed in the feed list
430 If there is no selection, selects the first feed. If multiple feeds
431 are selected, selects the feed after the last selected feed.
433 If unread is True, selects the next unread with unread items.
435 If the selection next-to-be is a category, go to the iter its first
436 child. If current selection is a child, then go to (parent + 1),
437 provided that (parent + 1) is not a category.
439 has_unread = False
440 def next(model, current):
441 treeiter = model.iter_next(current)
442 if not treeiter: return False
443 if model.iter_depth(current): next(model, model.iter_parent(current))
444 path = model.get_path(treeiter)
445 if with_unread and model[path][Column.unread] < 1:
446 next(model, current)
447 self.set_cursor(treeiter)
448 return True
449 selection = self._widget.get_selection()
450 (model, pathlist) = selection.get_selected_rows()
451 iters = [model.get_iter(path) for path in pathlist]
452 try:
453 current = iters.pop()
454 if model.iter_has_child(current):
455 iterchild = model.iter_children(current)
456 # make the row visible
457 path = model.get_path(iterchild)
458 for i in range(len(path)):
459 self._widget.expand_row(path[:i+1], False)
460 # select his first born child
461 if with_unread and model[path][Column.unread] > 0:
462 self.set_cursor(iterchild)
463 has_unread = True
464 else:
465 has_unread = next(model, current)
466 has_unread = next(model,current)
467 except IndexError:
468 self.set_cursor(model.get_iter_first())
469 has_unread = True
470 return has_unread
472 def select_previous_feed(self):
473 ''' Scrolls to the previous feed in the feed list.
475 If there is no selection, selects the first feed. If there's multiple
476 selection, selects the feed before the first selected feed.
478 If the previous selection is a category, select the last node in that
479 category. If the current selection is a child, then go to (parent -
480 1). If parent is the first feed, wrap and select the last feed or
481 category in the list.
483 def previous(model, current):
484 path = model.get_path(current)
485 treerow = model[path[-1]-1]
486 self.set_cursor(treerow.iter)
487 selection = self._widget.get_selection()
488 (model, pathlist) = selection.get_selected_rows()
489 iters = [model.get_iter(path) for path in pathlist]
490 try:
491 current_first = iters.pop(0)
492 if model.iter_has_child(current_first):
493 children = model.iter_n_children(current_first)
494 treeiter = model.iter_nth_child(children - 1)
495 self.set_cursor(treeiter)
496 return
497 previous(model, current_first)
498 except IndexError:
499 self.set_cursor(model.get_iter_first())
500 return
502 def set_cursor(self, treeiter, col_id=None, edit=False):
503 if not treeiter:
504 return
505 column = None
506 path = self._model.model.get_path(treeiter)
507 if col_id:
508 column = self._widget.get_column(col_id)
509 self._widget.set_cursor(path, column, edit)
510 self._widget.scroll_to_cell(path, column)
511 self._widget.grab_focus()
512 return
514 class FeedsPresenter(MVP.BasicPresenter):
515 def _initialize(self):
516 self.model = FeedListModel()
517 self._init_signals()
519 def _init_signals(self):
521 pass
522 #flist.signal_connect(Event.ItemReadSignal,
523 # self._feed_item_read)
524 #flist.signal_connect(Event.AllItemsReadSignal,
525 # self._feed_all_items_read)
526 #flist.signal_connect(Event.FeedsChangedSignal,
527 # self._feeds_changed)
528 #flist.signal_connect(Event.FeedDetailChangedSignal,
529 # self._feed_detail_changed)
530 #fclist = FeedCategoryList.get_instance()
531 #fclist.signal_connect(Event.FeedCategorySortedSignal,
532 # self._feeds_sorted_cb)
533 #fclist.signal_connect(Event.FeedCategoryChangedSignal,
534 # self._fcategory_changed_cb)
536 def select_first_feed(self):
537 return self.view.select_first_feed()
539 def select_next_feed(self, with_unread=False):
540 return self.view.select_next_feed(with_unread)
542 def select_previous_feed(self):
543 return self.view.select_previous_feed()
545 def _sort_func(self, model, a, b):
547 Sorts the feeds lexically.
549 From the gtk.TreeSortable.set_sort_func doc:
551 The comparison callback should return -1 if the iter1 row should come before
552 the iter2 row, 0 if the rows are equal, or 1 if the iter1 row should come
553 after the iter2 row.
555 retval = 0
556 fa = model.get_value(a, Column.OBJECT)
557 fb = model.get_value(b, Column.OBJECT)
559 if fa and fb:
560 retval = locale.strcoll(fa.title, fb.title)
561 elif fa is not None: retval = -1
562 elif fb is not None: retval = 1
563 return retval
565 def show_feed_information(self, feed):
566 straw.feed_properties_show(feed)