feed list view and feed/category event handling fixes
[straw.git] / src / lib / ItemList.py
blobad2c34b48b9e026680fcb661eac090a7f3df1728
1 """ ItemList.py
3 Handles listing of feed items in a view (i.e. GTK+ TreeView)
4 """
5 __copyright__ = "Copyright (c) 2002-2005 Free Software Foundation, Inc."
6 __license__ = """GNU General Public License
8 This program is free software; you can redistribute it and/or
9 modify it under the terms of the GNU General Public License as
10 published by the Free Software Foundation; either version 2 of the
11 License, or (at your option) any later version.
13 This program is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 General Public License for more details.
18 You should have received a copy of the GNU General Public
19 License along with this program; if not, write to the
20 Free Software Foundation, Inc., 59 Temple Place - Suite 330,
21 Boston, MA 02111-1307, USA.
22 """
24 from xml.sax import saxutils
25 import pygtk
26 pygtk.require('2.0')
27 import gobject
28 import gtk
29 import pango
30 import Event
31 import MVP
32 import error
33 import utils
35 class Column:
36 """
37 Constants for the item list treeview columns
38 """
39 TITLE = 0
40 STICKY = 1
41 ITEM = 2
42 WEIGHT = 3
43 STICKY_FLAG = 4
44 FOREGROUND = 5
46 class ItemsView(MVP.WidgetView):
47 ui = """
48 <ui>
49 <popup name=\"itemlist_popup\">
50 <menuitem action=\"mark_as_unread\"/>
51 </popup>
52 </ui>
53 """
55 def _initialize(self):
56 self._widget.connect("button_press_event",self._on_button_press_event)
57 self._widget.connect("popup-menu", self._on_popup_menu)
58 self._widget.connect("row-activated", self._on_row_activated)
59 self._popup = None
62 def scroll_to_cell(self, path):
63 selection = self._widget.get_selection()
64 selection.select_path(path)
65 self._widget.scroll_to_cell(path)
66 self._widget.set_cursor(path)
67 return
69 def get_widget_selection(self):
70 return self._widget.get_selection()
72 def _create_popup(self):
73 actions = [
74 ("mark_as_unread", gtk.STOCK_CONVERT, "Mark as _Unread", None,
75 "Mark this item as unread", self._on_menu_mark_as_unread_activate)
77 self._uimanager = gtk.UIManager()
78 actiongroup = gtk.ActionGroup("ItemListActions")
79 actiongroup.add_actions(actions)
80 self._uimanager.insert_action_group(actiongroup, 0)
81 self._uimanager.add_ui_from_string(self.ui)
82 self._popup = self._uimanager.get_widget('/itemlist_popup')
84 def _on_popup_menu(self, treeview, *args):
85 self._popup.popup(None,None,None,0,0)
87 def _on_button_press_event(self, treeview, event):
88 val = 0
89 if event.button == 3:
90 x = int(event.x)
91 y = int(event.y)
92 time = gtk.get_current_event_time()
93 path = treeview.get_path_at_pos(x, y)
94 if path is None:
95 return 1
96 path, col, cellx, celly = path
97 treeview.grab_focus()
98 treeview.set_cursor( path, col, 0)
99 self._popup.popup(None, None, None, event.button, time)
100 val = 1
101 return val
103 def _on_menu_mark_as_unread_activate(self, *args):
104 self._presenter.mark_selected_item_as_unread()
106 def _item_selection_changed(self, selection):
107 selected = selection.get_selected()
108 if selected:
109 self._presenter.selection_changed(selected)
110 return
112 def _on_row_activated(self, treeview, path, column):
113 model = self._widget.get_model()
114 try: treeiter = model.get_iter(path)
115 except ValueError:
116 return
117 link = model.get_value(treeiter, Column.ITEM).link
118 if link:
119 utils.url_show(link)
120 return
122 class ItemListView(ItemsView):
124 Widget: gtk.Treeview
125 Model: ListStore
126 Presenter: ItemListPresenter
128 def _initialize(self):
129 ItemsView._initialize(self)
130 self._widget.set_rules_hint(False)
131 self._create_popup()
132 selection = self._widget.get_selection()
133 selection.connect("changed", self._item_selection_changed)
135 def _model_set(self):
136 self._widget.set_model(self._model)
137 self._create_columns()
139 def _sticky_toggled(self, cell, path):
140 self._presenter.sticky_toggled(cell, path,
141 Column.ITEM, Column.STICKY_FLAG)
142 return
144 def _create_columns(self):
145 renderer = gtk.CellRendererToggle()
146 column = gtk.TreeViewColumn(_('Keep'), renderer,
147 active=Column.STICKY_FLAG)
148 column.set_resizable(True)
149 column.set_reorderable(True)
150 self._widget.append_column(column)
151 renderer.connect('toggled', self._sticky_toggled)
153 renderer = gtk.CellRendererText()
154 column = gtk.TreeViewColumn(_('_Title'), renderer,
155 text=Column.TITLE,
156 foreground=Column.FOREGROUND,
157 weight=Column.WEIGHT)
158 column.set_resizable(True)
159 column.set_reorderable(True)
160 self._widget.append_column(column)
161 return
163 class ItemsPresenter(MVP.BasicPresenter):
164 def _connect(self):
165 self.initialize_slots(Event.ItemSelectionChangedSignal)
166 self._selected_item = None
167 return
169 def selection_changed(self, selected):
170 model, treeiter = selected
171 if not treeiter: return
172 assert model is self._model
173 path = model.get_path(treeiter)
174 self._item_selected(path)
175 self._view.scroll_to_cell(path)
177 def _item_selected(self, path):
178 item = self._model[path][Column.ITEM]
179 if self._selected_item is not item:
180 self._selected_item = item
181 if not self._selected_item.seen:
182 self._selected_item.seen = 1
183 self.emit_signal(Event.ItemSelectionChangedSignal(self,self._selected_item))
184 return
186 def mark_selected_item_as_unread(self):
187 self._selected_item.seen = 0
188 self._mark_item(self._selected_item)
190 def display_empty_feed(self):
191 self._model.clear()
192 return
194 def _set_column_weight(self, item, path):
195 tree_iter = self._model.get_iter(path)
196 weight = (pango.WEIGHT_BOLD, pango.WEIGHT_NORMAL)[item.seen]
197 colour = ("#0000FF", "#000000")[item.seen]
198 self._model.set_value(tree_iter, Column.FOREGROUND, colour)
199 self._model.set_value(tree_iter, Column.WEIGHT, weight)
200 return
202 def _mark_item(self, item, index=None):
204 Marks an item as 'READ'
206 Subclass SHOULD implement this
208 raise NotImplemented
210 class ItemListPresenter(ItemsPresenter):
211 def _initialize(self):
212 model = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_OBJECT,
213 gobject.TYPE_PYOBJECT, gobject.TYPE_INT,
214 gobject.TYPE_BOOLEAN, gobject.TYPE_STRING)
215 self.model = model
216 self._connect()
217 self._selected_feed = None
218 selection = self._view.get_widget_selection()
219 selection.set_mode(gtk.SELECTION_SINGLE)
221 def feedlist_selection_changed(self, feedlist_selection, feedlist_model):
222 (model, pathlist) = feedlist_selection.get_selected_rows()
223 ## XXX fix this, get and display the union of feeds
224 ## currently, it only displays the first feed at path
225 nodes = feedlist_model.get_selected_nodes(pathlist)
226 if nodes:
227 obj = nodes.pop()
228 try:
229 print "displaying items of feed ", obj.feed
230 self.display_feed_items(obj.feed)
231 except TypeError:
232 ## XXX display child items of Category
233 pass
234 #print "feed list selection chaanged in item list ", nodes
236 def select_previous_item(self):
238 Selects the item before the current selection. If there
239 is no current selection, selects the last item. If there is no
240 previous row, returns False.
242 selection = self._view.get_widget_selection()
243 model, treeiter = selection.get_selected()
244 # select the first item if there is no selection
245 if not treeiter:
246 treeiter = model.get_iter_first()
247 path = self._model.get_path(treeiter)
248 # check if there's an item
249 if not path:
250 return False
251 prev_path = path[-1]-1
252 # we don't cycle through the items so return False if there are no
253 # more items to go to
254 if prev_path < 0:
255 return False
256 selection.select_path(path[-1]-1,)
257 return True
259 def select_next_item(self):
261 Selects the item after the current selection. If there is no current
262 selection, selects the first item. If there is no next row, returns False.
264 selection = self._view.get_widget_selection()
265 model, treeiter = selection.get_selected()
266 if not treeiter:
267 treeiter = model.get_iter_first()
268 next_iter = model.iter_next(treeiter)
269 if not next_iter or not self._model.iter_is_valid(treeiter):
270 return False
271 next_path = model.get_path(next_iter)
272 selection.select_path(next_path)
273 return True
275 def select_last_item(self):
277 Selects the last item in this list.
279 selection = self._view.get_widget_selection()
280 selection.select_path(len(self._model) - 1)
281 return True
283 def select_next_unread_item(self):
285 Selects the first unread item after the current selection. If there is
286 no current selection, or if there are no unread items, returns False..
288 By returning False, the caller can either go to the next feed, or go
289 to the next feed with an unread item.
291 has_unread = False
292 selection = self._view.get_widget_selection()
293 selected_row = selection.get_selected()
294 # check if we have a selection. if none,
295 # start searching from the first item
296 if not selected_row[1]:
297 treerow = self._model[0]
298 else:
299 model, treeiter = selected_row
300 if not treeiter:
301 return has_unread
302 nextiter = model.iter_next(treeiter)
303 if not nextiter: # no more rows to iterate
304 return has_unread
305 treerow = self._model[model.get_path(nextiter)]
306 while(treerow):
307 item = treerow[Column.ITEM]
308 if not item.seen:
309 has_unread = True
310 selection.select_path(treerow.path)
311 break
312 treerow = treerow.next
313 return has_unread
316 def sticky_toggled(self, cell, path, item_column, stickyflag_column):
317 treeiter = self._model.get_iter((int(path),))
318 item = self._model.get_value(treeiter, item_column)
319 item.sticky = not item.sticky
320 self._model.set(treeiter, stickyflag_column, item.sticky)
322 def display_feed_items(self, feed, select_first=1):
323 redisplay = self._selected_feed is feed
324 #if self._selected_feed and not redisplay:
325 # self._selected_feed.signal_disconnect(Event.RefreshFeedDisplaySignal,
326 # self._feed_order_changed)
327 # self._selected_feed.signal_disconnect(Event.AllItemsReadSignal,
328 # self._all_items_read)
329 # self._selected_feed.signal_disconnect(Event.ItemReadSignal,
330 # self._item_read)
331 self._selected_feed = feed
332 if not redisplay:
333 #self._selected_feed.connect(Event.RefreshFeedDisplaySignal,
334 # self._feed_order_changed)
335 self._selected_feed.connect('items_read', self._all_items_read)
336 #self._selected_feed.signal_connect(Event.ItemReadSignal,
337 # self._item_read)
338 count = self._render_feed(self._selected_feed)
339 if not redisplay and count:
340 if not self.select_next_unread_item():
341 selection = self._view.get_widget_selection()
342 selection.select_path((0,))
343 return
345 def _feed_order_changed(self, event):
346 if event.sender is self._selected_feed:
347 item = None
348 selection = self._view.get_widget_selection()
349 model, treeiter = selection.get_selected()
350 if treeiter is not None:
351 path = model.get_path(treeiter)
352 item = model[path[0]][Column.ITEM]
353 self._render_feed(event.sender)
354 if item:
355 path = (item.feed.get_item_index(item),)
356 else:
357 path = (0,) # first item
358 selection.select_path(path)
360 def _all_items_read(self, signal):
361 for index, item in signal.changed:
362 self._mark_item(item, index)
364 def _item_read(self, signal):
365 self._mark_item(signal.item)
367 def _render_feed(self, feed):
368 self._model.clear()
369 curr_iter = None
370 for item in feed.items:
371 weight = (pango.WEIGHT_BOLD, pango.WEIGHT_NORMAL)[item.seen]
372 colour = ("#0000FF", "#000000")[item.seen]
373 treeiter = self._model.append()
374 self._model.set(treeiter,
375 Column.TITLE, item.title,
376 Column.ITEM, item,
377 Column.WEIGHT, weight,
378 Column.STICKY_FLAG, item.sticky,
379 Column.FOREGROUND, colour)
380 if item is self._selected_item:
381 curr_iter = treeiter
383 if curr_iter:
384 path = self._model.get_path(curr_iter)
385 self._view.scroll_to_cell(path)
386 return len(feed.items)
388 def _mark_item(self, item, path=None):
389 if path is None:
390 path = (item.feed.get_item_index(item),)
391 self._set_column_weight(item, path)
392 return