Code cleanup.
[straw.git] / straw / FeedListView.py
blob3dcf539917b19beb4ae07b7ab89e55b153d4f05a
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()])
167 def _prepare_store(self):
168 self.store = gtk.TreeStore(gtk.gdk.Pixbuf, str, str, str, gobject.TYPE_PYOBJECT, bool)
170 def _populate_tree(self, parent_id, parent_iter, done):
171 for node in self.tv_nodes[parent_id].node.children:
172 tv_node = self.tv_nodes[node.id]
174 if node.type == "F":
175 self._create_row(tv_node)
176 tv_node.store = self.store
177 elif node.type == "C":
178 current_parent = self._create_row(tv_node)
179 tv_node.store = self.store
181 if self.tv_nodes.has_key(node.id):
182 self._populate_tree(node.id, current_parent, done)
184 def _create_row(self, node, editable = False):
185 return self.store.append(node.parent_iter, [node.pixbuf,
186 helpers.pango_escape(node.title),
187 'black',
188 node.unread_count,
189 node, editable])
191 def add_node(self, tv_node):
192 self.tv_nodes[tv_node.node.id] = tv_node
194 def node_added_cb(self, src, node):
195 tv_node = TreeViewNode(node, self.store)
196 self.add_node(tv_node)
197 self._create_row(tv_node)
199 @property
200 def model(self):
201 return self.store
203 def search(self, rows, func, data):
204 if not rows: return None
205 for row in rows:
206 if func(row, data):
207 return row
208 result = self.search(row.iterchildren(), func, data)
209 if result: return result
210 return None
212 class FeedsView(MVP.WidgetView):
213 def _initialize(self):
214 self._widget.set_search_column(Column.name)
216 # pixbuf column
217 column = gtk.TreeViewColumn()
218 unread_renderer = gtk.CellRendererText()
219 column.pack_start(unread_renderer, False)
220 column.set_attributes(unread_renderer, text = Column.unread)
222 status_renderer = gtk.CellRendererPixbuf()
223 column.pack_start(status_renderer, False)
224 column.set_attributes(status_renderer, pixbuf = Column.pixbuf)
226 # feed title renderer
227 title_renderer = gtk.CellRendererText()
229 title_renderer.connect("edited", self.on_node_edit_title_edited)
230 title_renderer.connect("editing-canceled", self.on_node_edit_title_canceled)
232 #title_renderer.set_property('editable', True)
233 column.pack_start(title_renderer, False)
234 column.set_attributes(title_renderer,
235 foreground=Column.foreground,
236 markup=Column.name,
237 editable=Column.editable) #, weight=Column.BOLD)
239 self._widget.append_column(column)
241 selection = self._widget.get_selection()
242 selection.set_mode(gtk.SELECTION_MULTIPLE)
244 self._widget.connect("button_press_event", self._on_button_press_event)
245 self._widget.connect("popup-menu", self._on_popup_menu)
247 uifactory = helpers.UIFactory('FeedListActions')
248 action = uifactory.get_action('/feedlist_popup/refresh')
249 action.connect('activate', self.on_menu_poll_selected_activate)
250 action = uifactory.get_action('/feedlist_popup/add_child')
251 action.connect('activate', self.on_menu_add_child_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, [("example", 0, 0)], gtk.gdk.ACTION_MOVE)
265 treeview.enable_model_drag_dest([("example", 0, 0)], gtk.gdk.ACTION_MOVE)
266 treeview.connect("drag_data_received", self.on_dragdata_received_cb)
268 def on_dragdata_received_cb(self, treeview, drag_context, x, y, selection, info, eventtime):
269 model, pathlist = treeview.get_selection().get_selected_rows()
270 iter_to_copy = model.get_iter(pathlist[0])
272 temp = treeview.get_dest_row_at_pos(x, y)
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)
285 from_path = path
286 to_path = list(path_of_target_iter)
288 if pos == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE:
289 print "TREE_VIEW_DROP_INTO_OR_BEFORE"
290 to_path.append(0)
291 elif pos == gtk.TREE_VIEW_DROP_INTO_OR_AFTER:
292 print "TREE_VIEW_DROP_INTO_OR_AFTER"
293 to_path.append(0)
294 elif pos == gtk.TREE_VIEW_DROP_BEFORE:
295 print "dropping before"
296 elif pos == gtk.TREE_VIEW_DROP_AFTER:
297 print "dropping after %s" % (str(path_of_target_iter))
298 to_path = list(path_of_target_iter)
299 order = to_path.pop()
301 if ":".join(map(str, to_path)):
302 iter = model.get_iter(":".join(map(str, to_path)))
303 else:
304 iter = None
306 #if order + 1 >= model.iter_n_children(iter):
307 # to_path.append(path_of_target_iter[len(path_of_target_iter) - 1])
308 #else:
309 to_path.append(path_of_target_iter[len(path_of_target_iter) - 1] + 1)
311 print "%s -> %s" % (str(from_path), str(to_path))
313 node = model[from_path][Column.object].node
314 self.iter_copy(model, iter_to_copy, target_iter, pos)
316 drag_context.finish(True, True, eventtime)
317 FeedManager.move_node(node, to_path)
318 else:
319 drag_context.finish(False, False, eventtime)
321 def check_row_path(self, model, iter_to_copy, target_iter):
322 path_of_iter_to_copy = model.get_path(iter_to_copy)
323 path_of_target_iter = model.get_path(target_iter)
324 return not (path_of_target_iter[0:len(path_of_iter_to_copy)] == path_of_iter_to_copy)
326 def iter_copy(self, model, iter_to_copy, target_iter, pos):
327 path = model.get_path(iter_to_copy)
329 if (pos == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE) or (pos == gtk.TREE_VIEW_DROP_INTO_OR_AFTER):
330 new_iter = model.prepend(target_iter, model[path])
331 elif pos == gtk.TREE_VIEW_DROP_BEFORE:
332 new_iter = model.insert_before(None, target_iter, model[path])
333 elif pos == gtk.TREE_VIEW_DROP_AFTER:
334 new_iter = model.insert_after(None, target_iter, model[path])
336 n = model.iter_n_children(iter_to_copy)
338 for i in range(n):
339 next_iter_to_copy = model.iter_nth_child(iter_to_copy, n - i - 1)
340 self.iter_copy(model, next_iter_to_copy, new_iter, gtk.TREE_VIEW_DROP_INTO_OR_BEFORE)
342 def _model_set(self):
343 self._widget.set_model(self._model.model)
345 def add_selection_changed_listener(self, listener):
346 selection = self._widget.get_selection()
347 selection.connect('changed', listener.feedlist_selection_changed, Column.object)
349 def _on_popup_menu(self, treeview, *args):
350 self.popup.popup(None, None, None, 0, 0)
352 def _on_button_press_event(self, treeview, event):
353 retval = 0
355 if event.button == 3:
356 x = int(event.x)
357 y = int(event.y)
358 time = gtk.get_current_event_time()
359 path = treeview.get_path_at_pos(x, y)
361 if path is None:
362 return 1
364 path = path[0]
365 self.node_at_popup = self.model.store[path][Column.object]
366 treeview.grab_focus()
368 if self.selected_count() < 2:
369 selection = treeview.get_selection()
370 selection.unselect_all()
371 selection.select_path(path)
373 self.popup.popup(None, None, None, event.button, time)
374 retval = 1
376 return retval
378 def selected_count(self):
379 selection = self._widget.get_selection()
380 pathlist = selection.get_selected_rows()[1]
381 return len(pathlist)
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)
402 def on_menu_add_child_activate(self, *args):
403 self.begin_add_category(self.node_at_popup.node)
405 def begin_add_category(self, node):
406 category = Category()
407 category.parent = node
408 category.norder = len(node.children)
409 self.new_child = TreeViewNode(category, self.model.store)
410 iter = self.model._create_row(self.new_child, editable = True)
411 path = self.model.store.get_path(iter)
412 column = self._widget.get_column(0)
414 parent_path = self.new_child.parent_path
416 if parent_path:
417 self._widget.expand_row(parent_path, False)
419 self._widget.set_cursor_on_cell(path, focus_column = column, start_editing = True)
421 def on_menu_poll_selected_activate(self, *args):
422 config = Config.get_instance()
424 if config.offline: #XXX
425 config.offline = not config.offline
427 FeedManager.update_nodes([node.node for node in self.selected()])
429 def on_menu_stop_poll_selected_activate(self, *args):
430 self.foreach_selected(lambda o,*args: o.feed.router.stop_polling())
432 def on_menu_mark_all_as_read_activate(self, *args):
433 self.foreach_selected(lambda o,*args: o.feed.mark_items_as_read())
435 def on_remove_selected_feed(self, *args):
436 nodes = [tv_node for tv_node in self.selected()]
438 FeedManager.delete_nodes([tv_node.node.id for tv_node in nodes])
440 for node in nodes:
441 iter = node.iter
443 if iter:
444 self.model.store.remove(iter)
446 def on_node_edit_title_canceled(self, cellrenderer):
447 if self.new_child:
448 debug("on_node_edit_title_canceled (new_child), removing %s" % str(self.new_child.path))
449 self.model.store.remove(self.new_child.iter)
450 self.new_child = None
452 def on_node_edit_title_edited(self, cellrenderertext, path, new_text):
453 if len(new_text) > 0:
454 self.new_child.node.name = new_text
455 FeedManager.save_category(self.new_child.node)
457 self.model.store.remove(self.new_child.iter)
458 self.new_child = None
460 def on_display_properties_feed(self, *args):
461 selection = self._widget.get_selection()
462 (model, pathlist) = selection.get_selected_rows()
463 iters = [model.get_iter(path) for path in pathlist]
464 path = pathlist.pop()
465 node = self.model.model[path][Column.object]
466 self._presenter.show_feed_information(node)
468 def add_category(self):
469 self.begin_add_category(self._model.tv_nodes[1].node)
471 def select_first_feed(self):
472 selection = self._widget.get_selection()
473 (model, pathlist) = selection.get_selected_rows()
474 treeiter = model.get_iter_first()
475 if not treeiter or not model.iter_is_valid(treeiter):
476 return False
477 self.set_cursor(treeiter)
478 return True
480 def select_next_feed(self, with_unread=False):
481 ''' Scrolls to the next feed in the feed list
483 If there is no selection, selects the first feed. If multiple feeds
484 are selected, selects the feed after the last selected feed.
486 If unread is True, selects the next unread with unread items.
488 If the selection next-to-be is a category, go to the iter its first
489 child. If current selection is a child, then go to (parent + 1),
490 provided that (parent + 1) is not a category.
492 has_unread = False
493 def next(model, current):
494 treeiter = model.iter_next(current)
495 if not treeiter: return False
496 if model.iter_depth(current): next(model, model.iter_parent(current))
497 path = model.get_path(treeiter)
498 if with_unread and model[path][Column.unread] < 1:
499 next(model, current)
500 self.set_cursor(treeiter)
501 return True
502 selection = self._widget.get_selection()
503 (model, pathlist) = selection.get_selected_rows()
504 iters = [model.get_iter(path) for path in pathlist]
505 try:
506 current = iters.pop()
507 if model.iter_has_child(current):
508 iterchild = model.iter_children(current)
509 # make the row visible
510 path = model.get_path(iterchild)
511 for i in range(len(path)):
512 self._widget.expand_row(path[:i+1], False)
513 # select his first born child
514 if with_unread and model[path][Column.unread] > 0:
515 self.set_cursor(iterchild)
516 has_unread = True
517 else:
518 has_unread = next(model, current)
519 has_unread = next(model,current)
520 except IndexError:
521 self.set_cursor(model.get_iter_first())
522 has_unread = True
523 return has_unread
525 def select_previous_feed(self):
526 ''' Scrolls to the previous feed in the feed list.
528 If there is no selection, selects the first feed. If there's multiple
529 selection, selects the feed before the first selected feed.
531 If the previous selection is a category, select the last node in that
532 category. If the current selection is a child, then go to (parent -
533 1). If parent is the first feed, wrap and select the last feed or
534 category in the list.
536 def previous(model, current):
537 path = model.get_path(current)
538 treerow = model[path[-1]-1]
539 self.set_cursor(treerow.iter)
540 selection = self._widget.get_selection()
541 (model, pathlist) = selection.get_selected_rows()
542 iters = [model.get_iter(path) for path in pathlist]
543 try:
544 current_first = iters.pop(0)
545 if model.iter_has_child(current_first):
546 children = model.iter_n_children(current_first)
547 treeiter = model.iter_nth_child(children - 1)
548 self.set_cursor(treeiter)
549 return
550 previous(model, current_first)
551 except IndexError:
552 self.set_cursor(model.get_iter_first())
553 return
555 def set_cursor(self, treeiter, col_id=None, edit=False):
556 if not treeiter:
557 return
559 column = None
560 path = self._model.model.get_path(treeiter)
562 if col_id:
563 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()
569 class FeedsPresenter(MVP.BasicPresenter):
570 def _initialize(self):
571 self.model = FeedListModel()
572 self._init_signals()
574 def _init_signals(self):
575 pass
577 def add_category(self):
578 self.view.add_category()
580 def select_first_feed(self):
581 return self.view.select_first_feed()
583 def select_next_feed(self, with_unread=False):
584 return self.view.select_next_feed(with_unread)
586 def select_previous_feed(self):
587 return self.view.select_previous_feed()
589 def _sort_func(self, model, a, b):
591 Sorts the feeds lexically.
593 From the gtk.TreeSortable.set_sort_func doc:
595 The comparison callback should return -1 if the iter1 row should come before
596 the iter2 row, 0 if the rows are equal, or 1 if the iter1 row should come
597 after the iter2 row.
599 retval = 0
600 fa = model.get_value(a, Column.OBJECT)
601 fb = model.get_value(b, Column.OBJECT)
603 if fa and fb:
604 retval = locale.strcoll(fa.title, fb.title)
605 elif fa is not None: retval = -1
606 elif fb is not None: retval = 1
607 return retval
609 def show_feed_information(self, feed):
610 straw.feed_properties_show(feed)