Intermediate work commit.
[straw/fork.git] / straw / FeedListView.py
blobf8e14b6d9923ae9766376ca94c8587a9e22ef422
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 from straw import TreeViewManager, helpers
22 import Config
23 import FeedManager
24 import MVP
25 import PollManager
26 import gobject
27 import gtk
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 TreeNodeAdapter (gobject.GObject):
37 ''' A Node Adapter which encapsulates either a Category or a Feed '''
39 __gsignals__ = {
40 'feed-changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
41 (gobject.TYPE_PYOBJECT,)),
42 'category-changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
43 (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,)),
44 'category-feed-added' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
45 (gobject.TYPE_PYOBJECT,)),
46 'category-feed-removed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
47 (gobject.TYPE_PYOBJECT,))
50 def __init__(self, object):
51 gobject.GObject.__init__(self)
52 self.obj = object
53 filename = os.path.join(straw.STRAW_DATA_DIR, 'feed.png')
54 self.default_pixbuf = gtk.gdk.pixbuf_new_from_file(filename)
55 if isinstance(self.obj, Feed):
56 pass#self.obj.connect('changed', self.feed_changed_cb)
57 elif isinstance(self.obj, Category):
58 print repr(self.obj)
59 #self.obj.connect('feed-added', self.feed_added_cb)
60 #self.obj.connect('feed-removed', self.feed_removed_cb)
61 #self.obj.connect('changed', self.category_changed_cb)
63 def feed_changed_cb(self, feed):
64 self.emit('feed-changed', feed)
66 def category_changed_cb(self, category, *args):
67 logging.debug("TREENODEADAPTER CATEGORY_CHANGED_CB -> ", category, feed)
68 self.emit('category-changed', category, args)
70 def feed_added_cb(self, category, feed):
71 logging.debug("TREE NODE ADAPTER FEED ADDED CB!!!", feed)
72 self.emit('category-feed-added', feed)
74 def feed_removed_cb(self, category, feed):
75 self.emit('category-feed-removed', feed)
77 def has_children(self):
78 ''' Checks if the node has children. Essentially this means the object
79 is a category.'''
80 has_child = False
81 try:
82 has_child = self.obj.feeds and True or False
83 except AttributeError:
84 has_child = False
85 return has_child
87 @property
88 def title(self):
89 ''' The title of the node be it a category or a feed '''
90 return self.obj.title
92 @property
93 def num_unread_items(self):
94 ''' The number of unread items of the feed or if it's a category,
95 the aggregate number of unread items of the feeds belonging to the
96 category.'''
97 return 0
98 return self.obj.unread_count
99 def _r(a,b):return a + b
100 try:
101 unread_items = self.obj.number_of_unread
102 except AttributeError:
103 unread_items = reduce(_r, [feed.number_of_unread for feed in self.obj.feeds])
104 return unread_items or ''
106 @property
107 def pixbuf(self):
108 ''' gets the pixbuf to display according to the status of the feed '''
109 widget = gtk.Label()
110 return widget.render_icon(gtk.STOCK_HOME, gtk.ICON_SIZE_MENU)
112 # ignore why above is a gtk.Label. We just need
113 # gtk.Widget.render_icon to get a pixbuf out of a stock image.
114 # Fyi, gtk.Widget is abstract that is why we need a subclass to do
115 # this for us.
116 try:
117 if self.obj.process_status is not feeds.Feed.STATUS_IDLE:
118 return widget.render_icon(gtk.STOCK_EXECUTE, gtk.ICON_SIZE_MENU)
119 elif self.obj.error:
120 return widget.render_icon(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_MENU)
121 except AttributeError, ex:
122 logging.exception(ex)
123 return widget.render_icon(gtk.STOCK_DIRECTORY, gtk.ICON_SIZE_MENU)
124 return self.default_pixbuf
126 @property
127 def feed(self):
128 ''' An alias to a Feed object '''
129 if not isinstance(self.obj, Feed):
130 raise TypeError, _("object is not of a Feed")
131 return self.obj
133 @property
134 def category(self):
135 ''' An alias to a Category object '''
136 if not isinstance(self.obj, Category):
137 raise TypeError, _("object is not a Category")
138 return self.obj
140 class FeedListModel:
141 ''' The model for the feed list view '''
143 def __init__(self):
144 # name, pixbuf, unread, foreground
145 self.store = gtk.TreeStore(gtk.gdk.Pixbuf, str, str, str, gobject.TYPE_PYOBJECT)
146 # unread, weight, status_flag feed object, allow_children
147 # str, int, 'gboolean', gobject.TYPE_PYOBJECT, 'gboolean'
149 import time
150 s = time.time()
151 print "BEFORE!!!"
153 self.refresh_tree()
154 self._init_signals()
156 print "AFTER!!!"
157 print time.time() - s
159 #self._populate_tree(None, None, [])
161 def refresh_tree(self):
162 self.store = gtk.TreeStore(gtk.gdk.Pixbuf, str, str, str, gobject.TYPE_PYOBJECT)
163 self.categories, self.feeds = FeedManager.get_model()
164 self.nodes = TreeViewManager.get_nodes()
166 self._populate_tree(1, None, [])
167 #self.store.clear()
169 def _init_signals(self):
170 TreeViewManager._get_instance().connect("node-added", self.node_added_cb)
172 def _get_feed(self, node, category):
173 for feed in self.feeds[category]:
174 if feed.id == node.obj_id:
175 node.obj = feed
176 return feed
178 def _populate_tree(self, parent_id, parent_iter, done):
179 if not self.nodes.has_key(parent_id):
180 return
182 for node in self.nodes[parent_id]:
183 if node.type == "F":
184 node.obj = self._get_feed(node, parent_id)
185 node.treeiter = self._create_row(node, parent_iter)
186 node.store = self.store
187 self.categories[node.obj.category_id].props.unread_count += node.obj.unread_count
188 elif node.type == "C":
189 node.obj = self.categories[node.obj_id]
190 current_parent = self._create_row(node, parent_iter)
191 node.treeiter = current_parent
192 node.store = self.store
194 if self.nodes.has_key(node.obj_id):
195 self._populate_tree(node.obj_id, current_parent, done)
197 def populate(self, category, feeds):
198 parent = None
199 if category:
200 parent = self.store.append(parent)
201 node_adapter = TreeNodeAdapter(category)
202 node_adapter.connect('category-changed', self.category_changed_cb)
203 node_adapter.connect('category-feed-added', self.feed_added_cb)
204 node_adapter.connect('category-feed-removed', self.feed_removed_cb)
205 self.store.set(parent, Column.pixbuf, node_adapter.pixbuf,
206 Column.name, node_adapter.title,
207 Column.unread, node_adapter.num_unread_items,
208 Column.object, node_adapter)
209 for f in feeds:
210 rowiter = self.store.append(parent)
211 node_adapter = TreeNodeAdapter(f)
212 node_adapter.connect('feed-changed', self.feed_changed_cb)
213 self.store.set(rowiter, Column.pixbuf, node_adapter.pixbuf,
214 Column.name, node_adapter.title,
215 Column.foreground, node_adapter.num_unread_items and 'blue' or 'black',
216 Column.unread, node_adapter.num_unread_items,
217 Column.object, node_adapter)
219 def __getattribute__(self, name):
220 attr = None
221 try:
222 attr = getattr(self, name)
223 except AttributeError, ae:
224 attr = getattr(self.store, name)
225 else:
226 return attr
228 def _create_row(self, node, parent = None):
229 return self.store.append(parent, [node.pixbuf,
230 node.title,
231 'blue',
232 node.unread_count,
233 node])
235 def category_changed_cb(self, node_adapter, *args):
236 # XXX What changes do we need here? new feed? udated subscription?
237 print "FEED LIST MODEL -> ", node_adapter, args
239 def _lookup_parent(self, n):
240 for children in self.nodes.values():
241 for node in children:
242 #print str(n.parent_id) + " ?=? " + str(node.obj_id)
243 if node.type == "C" and n.parent_id == node.obj_id:
244 return node
246 def node_added_cb(self, src, node):
247 #feed_node_adapter.connect('feed-changed', self.feed_changed_cb)
248 #self.refresh_tree()
249 #self._create_row(node)
250 #print node.obj
251 #self.nodes[node.parent_id].append(node)
253 #print node.parent_id
254 if node.type == "F":
255 # node.obj = self._get_feed(node, node.parent_id)
256 node.store = self.store
257 elif node.type == "C":
258 node.obj = self.categories[node.obj_id]
259 node.store = self.store
261 parent_node = self._lookup_parent(node)
262 parent_iter = None
264 if parent_node != None and hasattr(parent_node, "treeiter"):
265 parent_iter = parent_node.treeiter
266 node.treeiter = self._create_row(node, parent_iter)
267 #node.store = self.store
269 def feed_removed_cb(self, node_adapter, feed):
270 logging.debug("FEED REMOVED CB ", node_adapter, feed)
272 def feed_changed_cb(self, node_adapter, feed):
273 row = self.search(self.store,
274 lambda r, data: r[data[0]] == data[1],
275 (Column.object, node_adapter))
276 if not row: return
277 path = self.store.get_path(row.iter)
278 self.store[path] = [node_adapter.pixbuf, node_adapter.title,
279 node_adapter.num_unread_items and 'blue' or 'black',
280 node_adapter.num_unread_items,
281 node_adapter]
283 @property
284 def model(self):
285 return self.store
287 def search(self, rows, func, data):
288 if not rows: return None
289 for row in rows:
290 if func(row, data):
291 return row
292 result = self.search(row.iterchildren(), func, data)
293 if result: return result
294 return None
296 class FeedsView(MVP.WidgetView):
297 def _initialize(self):
298 self._widget.set_search_column(Column.name)
300 # pixbuf column
301 column = gtk.TreeViewColumn()
302 unread_renderer = gtk.CellRendererText()
303 column.pack_start(unread_renderer, False)
304 column.set_attributes(unread_renderer,
305 text=Column.unread)
307 status_renderer = gtk.CellRendererPixbuf()
308 column.pack_start(status_renderer, False)
309 column.set_attributes(status_renderer,
310 pixbuf=Column.pixbuf)
312 # feed title renderer
313 title_renderer = gtk.CellRendererText()
314 column.pack_start(title_renderer, False)
315 column.set_attributes(title_renderer,
316 foreground=Column.foreground,
317 text=Column.name) #, weight=Column.BOLD)
319 self._widget.append_column(column)
321 selection = self._widget.get_selection()
322 selection.set_mode(gtk.SELECTION_MULTIPLE)
324 self._widget.connect("button_press_event", self._on_button_press_event)
325 self._widget.connect("popup-menu", self._on_popup_menu)
327 uifactory = helpers.UIFactory('FeedListActions')
328 action = uifactory.get_action('/feedlist_popup/refresh')
329 action.connect('activate', self.on_menu_poll_selected_activate)
330 action = uifactory.get_action('/feedlist_popup/mark_as_read')
331 action.connect('activate', self.on_menu_mark_all_as_read_activate)
332 action = uifactory.get_action('/feedlist_popup/stop_refresh')
333 action.connect('activate', self.on_menu_stop_poll_selected_activate)
334 action = uifactory.get_action('/feedlist_popup/remove')
335 action.connect('activate', self.on_remove_selected_feed)
336 action = uifactory.get_action('/feedlist_popup/properties')
337 action.connect('activate', self.on_display_properties_feed)
338 self.popup = uifactory.get_popup('/feedlist_popup')
341 def _model_set(self):
342 self._widget.set_model(self._model.model)
344 def add_selection_changed_listener(self, listener):
345 selection = self._widget.get_selection()
346 selection.connect('changed', listener.feedlist_selection_changed, Column.object)
348 def _on_popup_menu(self, treeview, *args):
349 self.popup.popup(None, None, None, 0, 0)
351 def _on_button_press_event(self, treeview, event):
352 retval = 0
353 if event.button == 3:
354 x = int(event.x)
355 y = int(event.y)
356 time = gtk.get_current_event_time()
357 path = treeview.get_path_at_pos(x, y)
358 if path is None:
359 return 1
360 path, col, cellx, celly = path
361 selection = treeview.get_selection()
362 selection.unselect_all()
363 selection.select_path(path)
364 treeview.grab_focus()
365 self.popup.popup(None, None, None, event.button, time)
366 retval = 1
367 return retval
369 def foreach_selected(self, func):
370 selection = self._widget.get_selection()
371 (model, pathlist) = selection.get_selected_rows()
372 iters = [model.get_iter(path) for path in pathlist]
373 try:
374 for treeiter in iters:
375 object = model.get_value(treeiter, Column.object)
376 func(object, model, treeiter)
377 except TypeError, te:
378 ## XXX maybe object is a category
379 logging.exception(te)
380 return
382 def on_menu_poll_selected_activate(self, *args):
383 config = Config.get_instance()
384 poll = True
385 if config.offline: #XXX
386 config.offline = not config.offline
387 selection = self._widget.get_selection()
388 (model, pathlist) = selection.get_selected_rows()
389 iters = [model.get_iter(path) for path in pathlist]
390 nodes = [model.get_value(treeiter,Column.object) for treeiter in iters]
391 fds = []
392 for n in nodes:
393 try:
394 fds.append(n.feed)
395 except TypeError:
396 fds += n.category.feeds
397 if fds:
398 PollManager.get_instance().poll(fds)
399 return
401 def on_menu_stop_poll_selected_activate(self, *args):
402 self.foreach_selected(lambda o,*args: o.feed.router.stop_polling())
404 def on_menu_mark_all_as_read_activate(self, *args):
405 self.foreach_selected(lambda o,*args: o.feed.mark_items_as_read())
407 def on_remove_selected_feed(self, *args):
408 def remove(*args):
409 (object, model, treeiter) = args
410 model.remove(treeiter)
411 feedlist = feeds.get_feedlist_instance()
412 idx = feedlist.index(object.feed)
413 del feedlist[idx]
414 self.foreach_selected(remove)
415 return
417 def on_display_properties_feed(self, *args):
418 selection = self._widget.get_selection()
419 (model, pathlist) = selection.get_selected_rows()
420 iters = [model.get_iter(path) for path in pathlist]
421 path = pathlist.pop()
422 node = self.model.model[path][Column.object]
423 self._presenter.show_feed_information(node)
424 return
426 def select_first_feed(self):
427 selection = self._widget.get_selection()
428 (model, pathlist) = selection.get_selected_rows()
429 treeiter = model.get_iter_first()
430 if not treeiter or not model.iter_is_valid(treeiter):
431 return False
432 self.set_cursor(treeiter)
433 return True
435 def select_next_feed(self, with_unread=False):
436 ''' Scrolls to the next feed in the feed list
438 If there is no selection, selects the first feed. If multiple feeds
439 are selected, selects the feed after the last selected feed.
441 If unread is True, selects the next unread with unread items.
443 If the selection next-to-be is a category, go to the iter its first
444 child. If current selection is a child, then go to (parent + 1),
445 provided that (parent + 1) is not a category.
447 has_unread = False
448 def next(model, current):
449 treeiter = model.iter_next(current)
450 if not treeiter: return False
451 if model.iter_depth(current): next(model, model.iter_parent(current))
452 path = model.get_path(treeiter)
453 if with_unread and model[path][Column.unread] < 1:
454 next(model, current)
455 self.set_cursor(treeiter)
456 return True
457 selection = self._widget.get_selection()
458 (model, pathlist) = selection.get_selected_rows()
459 iters = [model.get_iter(path) for path in pathlist]
460 try:
461 current = iters.pop()
462 if model.iter_has_child(current):
463 iterchild = model.iter_children(current)
464 # make the row visible
465 path = model.get_path(iterchild)
466 for i in range(len(path)):
467 self._widget.expand_row(path[:i+1], False)
468 # select his first born child
469 if with_unread and model[path][Column.unread] > 0:
470 self.set_cursor(iterchild)
471 has_unread = True
472 else:
473 has_unread = next(model, current)
474 has_unread = next(model,current)
475 except IndexError:
476 self.set_cursor(model.get_iter_first())
477 has_unread = True
478 return has_unread
480 def select_previous_feed(self):
481 ''' Scrolls to the previous feed in the feed list.
483 If there is no selection, selects the first feed. If there's multiple
484 selection, selects the feed before the first selected feed.
486 If the previous selection is a category, select the last node in that
487 category. If the current selection is a child, then go to (parent -
488 1). If parent is the first feed, wrap and select the last feed or
489 category in the list.
491 def previous(model, current):
492 path = model.get_path(current)
493 treerow = model[path[-1]-1]
494 self.set_cursor(treerow.iter)
495 selection = self._widget.get_selection()
496 (model, pathlist) = selection.get_selected_rows()
497 iters = [model.get_iter(path) for path in pathlist]
498 try:
499 current_first = iters.pop(0)
500 if model.iter_has_child(current_first):
501 children = model.iter_n_children(current_first)
502 treeiter = model.iter_nth_child(children - 1)
503 self.set_cursor(treeiter)
504 return
505 previous(model, current_first)
506 except IndexError:
507 self.set_cursor(model.get_iter_first())
508 return
510 def set_cursor(self, treeiter, col_id=None, edit=False):
511 if not treeiter:
512 return
513 column = None
514 path = self._model.model.get_path(treeiter)
515 if col_id:
516 column = self._widget.get_column(col_id)
517 self._widget.set_cursor(path, column, edit)
518 self._widget.scroll_to_cell(path, column)
519 self._widget.grab_focus()
520 return
522 class FeedsPresenter(MVP.BasicPresenter):
523 def _initialize(self):
524 self.model = FeedListModel()
525 self._init_signals()
527 def _init_signals(self):
529 pass
530 #flist.signal_connect(Event.ItemReadSignal,
531 # self._feed_item_read)
532 #flist.signal_connect(Event.AllItemsReadSignal,
533 # self._feed_all_items_read)
534 #flist.signal_connect(Event.FeedsChangedSignal,
535 # self._feeds_changed)
536 #flist.signal_connect(Event.FeedDetailChangedSignal,
537 # self._feed_detail_changed)
538 #fclist = FeedCategoryList.get_instance()
539 #fclist.signal_connect(Event.FeedCategorySortedSignal,
540 # self._feeds_sorted_cb)
541 #fclist.signal_connect(Event.FeedCategoryChangedSignal,
542 # self._fcategory_changed_cb)
544 def select_first_feed(self):
545 return self.view.select_first_feed()
547 def select_next_feed(self, with_unread=False):
548 return self.view.select_next_feed(with_unread)
550 def select_previous_feed(self):
551 return self.view.select_previous_feed()
553 def _sort_func(self, model, a, b):
555 Sorts the feeds lexically.
557 From the gtk.TreeSortable.set_sort_func doc:
559 The comparison callback should return -1 if the iter1 row should come before
560 the iter2 row, 0 if the rows are equal, or 1 if the iter1 row should come
561 after the iter2 row.
563 retval = 0
564 fa = model.get_value(a, Column.OBJECT)
565 fb = model.get_value(b, Column.OBJECT)
567 if fa and fb:
568 retval = locale.strcoll(fa.title, fb.title)
569 elif fa is not None: retval = -1
570 elif fb is not None: retval = 1
571 return retval
573 def show_feed_information(self, feed):
574 straw.feed_properties_show(feed)