Fixed item navigation and cleaned up feed and item changes
[straw.git] / src / lib / ItemList.py
blob05e1e3a326b5581c0a8ade82cc36b35861d970c3
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 import os.path, operator
25 from xml.sax import saxutils
26 import pygtk
27 pygtk.require('2.0')
28 import gobject
29 import gtk
30 import pango
31 import MVP
32 import error
33 import utils
34 import Config
36 class Column:
37 """
38 Constants for the item list treeview columns
39 """
40 title, sticky, item, weight, sticky_flag, foreground = range(6)
42 # XXX FIX THIS
43 class Popup:
44 ''' A popup '''
46 def __init__(self, listener):
47 self.manager = gtk.UIManager()
48 actions = [
49 ("mark_as_unread", gtk.STOCK_CONVERT, "Mark as _Unread", None,
50 "Mark this item as unread", listener.on_menu_mark_as_unread_activate)
52 actiongroup = gtk.ActionGroup("ItemListActions")
53 actiongroup.add_actions(actions)
54 self.manager.insert_action_group(actiongroup, 1)
55 #popupui = os.path.join(utils.find_image_dir(), 'ui.xml')
56 #self.manager.add_ui_from_file(popupui)
58 @property
59 def popup(self):
60 return self.manager.get_widget('/itemlist_popup')
63 class ItemListModel:
64 ''' A model of summary items for tree views '''
66 def __init__(self):
67 self.store = gtk.ListStore(str, gobject.TYPE_OBJECT, gobject.TYPE_PYOBJECT, int, bool, str)
68 config = Config.get_instance()
69 self.item_order = config.item_order
71 def populate(self, summaryItems):
72 print "POPULATE ", len(summaryItems)
73 ## XXX should we sort here or in feeds.Feed?
74 items = sorted(summaryItems, key=operator.attrgetter('pub_date'),
75 reverse=self.item_order)
76 self.store.clear()
77 for item in items:
78 weight = (pango.WEIGHT_BOLD, pango.WEIGHT_NORMAL)[item.seen]
79 colour = ("#0000FF", "#000000")[item.seen]
80 treeiter = self.store.append()
81 self.store.set(treeiter,
82 Column.title, item.title,
83 Column.item, item,
84 Column.weight, weight,
85 Column.sticky_flag, item.sticky,
86 Column.foreground, colour)
87 item.connect('changed', self.item_changed_cb)
89 def item_changed_cb(self, item):
90 for row in self.store:
91 rowitem = row[Column.item]
92 if rowitem is item:
93 row[Column.weight] = item.seen and pango.WEIGHT_NORMAL or pango.WEIGHT_BOLD
94 row[Column.foreground] = item.seen and 'black' or 'blue'
95 row[Column.sticky_flag] = item.sticky
96 row[Column.item] = item
98 @property
99 def model(self):
100 return self.store
103 class ItemListView(MVP.WidgetView):
105 def _initialize(self):
106 self._widget.set_rules_hint(False)
107 self._widget.connect("button_press_event",self._on_button_press_event)
108 self._widget.connect("popup-menu", self._on_popup_menu)
109 self._widget.connect("row-activated", self._on_row_activated)
110 #self._popup = Popup(self)
112 renderer = gtk.CellRendererToggle()
113 column = gtk.TreeViewColumn(_('Keep'), renderer,
114 active=Column.sticky_flag)
115 column.set_resizable(True)
116 column.set_reorderable(True)
117 self._widget.append_column(column)
118 renderer.connect('toggled', self._sticky_toggled)
120 renderer = gtk.CellRendererText()
121 column = gtk.TreeViewColumn(_('_Title'), renderer,
122 text=Column.title,
123 foreground=Column.foreground,
124 weight=Column.weight)
125 column.set_resizable(True)
126 column.set_reorderable(True)
127 self._widget.append_column(column)
129 selection = self._widget.get_selection()
130 selection.set_mode(gtk.SELECTION_SINGLE)
131 selection.connect('changed', lambda x: self.item_mark_as_read(True))
133 def add_selection_changed_listener(self, listener):
134 selection = self._widget.get_selection()
135 selection.connect('changed',
136 listener.itemlist_selection_changed,
137 Column.item)
139 def _model_set(self):
140 self._widget.set_model(self._model.model)
142 def _sticky_toggled(self, cell, path):
143 model = self._widget.get_model()
144 treeiter = model.get_iter((int(path),))
145 item = model.get_value(treeiter, Column.item)
146 item.sticky = not item.sticky
147 model.set(treeiter, Column.sticky_flag, item.sticky)
149 def _on_popup_menu(self, treeview, *args):
150 self._popup.popup(None,None,None,0,0)
152 def _on_button_press_event(self, treeview, event):
153 val = 0
154 if event.button == 3:
155 x = int(event.x)
156 y = int(event.y)
157 time = gtk.get_current_event_time()
158 path = treeview.get_path_at_pos(x, y)
159 if path is None:
160 return 1
161 path, col, cellx, celly = path
162 treeview.grab_focus()
163 treeview.set_cursor( path, col, 0)
164 self._popup.popup(None, None, None, event.button, time)
165 val = 1
166 return val
168 def on_menu_mark_as_unread_activate(self, *args):
169 self.item_mark_as_read(False)
171 def item_mark_as_read(self, isRead):
172 selection = self._widget.get_selection()
173 (model, treeiter) = selection.get_selected()
174 if not treeiter: return
175 item = model.get_value(treeiter, Column.item)
176 item.seen = isRead
177 weight = (pango.WEIGHT_BOLD, pango.WEIGHT_NORMAL)[item.seen]
178 colour = ("#0000FF", "#000000")[item.seen]
179 model.set(treeiter, Column.foreground, colour,
180 Column.weight, weight,
181 Column.item, item)
182 return
184 def _on_row_activated(self, treeview, path, column):
185 ''' double-clicking an item opens that item in the web browser '''
186 model = self._widget.get_model()
187 try:
188 treeiter = model.get_iter(path)
189 except ValueError:
190 return
191 link = model.get_value(treeiter, Column.ITEM).link
192 if link:
193 utils.url_show(link)
194 return
196 def select_first_item(self):
197 selection = self._widget.get_selection()
198 (model, treeiter) = selection.get_selected()
199 path = model.get_path(model.get_iter_first())
200 selection.select_path(path)
201 return True
203 def select_previous_item(self):
205 Selects the item before the current selection. If there
206 is no current selection, selects the last item. If there is no
207 previous row, returns False.
209 selection = self._widget.get_selection()
210 (model, treeiter) = selection.get_selected()
211 if not treeiter: return False
212 path = model.get_path(treeiter)
213 prev = path[-1]-1
214 print prev, path
215 if prev < 0:
216 return False
217 selection.select_path(prev)
218 return True
220 def select_next_item(self):
222 Selects the item after the current selection. If there is no current
223 selection, selects the first item. If there is no next row, returns False.
225 selection = self._widget.get_selection()
226 (model, treeiter) = selection.get_selected()
227 if not treeiter:
228 treeiter = model.get_iter_first()
229 next_iter = model.iter_next(treeiter)
230 if not next_iter or not model.iter_is_valid(treeiter):
231 return False
232 next_path = model.get_path(next_iter)
233 selection.select_path(next_path)
234 return True
236 def select_last_item(self):
238 Selects the last item in this list.
240 selection = self._widget.get_selection()
241 selection.select_path(len(self.model.model) - 1)
242 return True
244 def select_next_unread_item(self):
246 Selects the first unread item after the current selection. If there is
247 no current selection, or if there are no unread items, returns False..
249 By returning False, the caller can either go to the next feed, or go
250 to the next feed with an unread item.
252 selection = self._widget.get_selection()
253 (model, treeiter) = selection.get_selected()
254 # check if we have a selection. if none,
255 # start searching from the first item
256 if not treeiter:
257 treeiter = model.get_iter_first()
258 if not treeiter: return False
259 nextiter = model.iter_next(treeiter)
260 if not nextiter: return False # no more rows to iterate
261 treerow = model[nextiter]
262 has_unread = False
263 while(treerow):
264 item = treerow[Column.item]
265 if not item.seen:
266 has_unread = True
267 selection.select_path(treerow.path)
268 break
269 treerow = treerow.next
270 return has_unread
272 class ItemListPresenter(MVP.BasicPresenter):
274 ## XXX listen for RefreshFeedDisplaySignal, AllItemsReadSignal,
275 ## ItemReadSignal, FeedOrder changes,
277 def _initialize(self):
278 self.model = ItemListModel()
280 def feedlist_selection_changed(self, feedlist_selection, column):
281 (model, pathlist) = feedlist_selection.get_selected_rows()
282 iters = [model.get_iter(path) for path in pathlist]
283 if not iters:
284 return
285 nodes = [model.get_value(treeiter, column) for treeiter in iters]
286 items = []
287 try:
288 items += [node.feed.items for node in nodes]
289 except TypeError:
290 gen = (node.category.feeds for node in nodes)
291 items += [feed.items for feed in gen.next()]
292 items = list(self.flatten(items))
293 self.model.populate(items)
294 if not len(self.model.model): return
295 if not self.view.select_next_unread_item():
296 self.view.select_first_item()
298 def flatten(self, sequence):
299 ''' python cookbook recipe 4.6 '''
300 for item in sequence:
301 if isinstance(item, (list, tuple)):
302 for subitem in self.flatten(item):
303 yield subitem
304 else:
305 yield item
307 def select_previous_item(self):
308 return self.view.select_previous_item()
310 def select_next_item(self):
311 return self.view.select_next_item()
313 def select_next_unread_item(self):
314 return self.view.select_next_unread_item()
316 def select_last_item(self):
317 return self.view.select_last_item()
320 def display_feed_items(self, feed, select_first=1):
321 redisplay = self._selected_feed is feed
322 #if self._selected_feed and not redisplay:
323 # self._selected_feed.signal_disconnect(Event.RefreshFeedDisplaySignal,
324 # self._feed_order_changed)
325 # self._selected_feed.signal_disconnect(Event.AllItemsReadSignal,
326 # self._all_items_read)
327 # self._selected_feed.signal_disconnect(Event.ItemReadSignal,
328 # self._item_read)
329 self._selected_feed = feed
330 if not redisplay:
331 #self._selected_feed.connect(Event.RefreshFeedDisplaySignal,
332 # self._feed_order_changed)
333 self._selected_feed.connect('items_read', self._all_items_read)
334 #self._selected_feed.signal_connect(Event.ItemReadSignal,
335 # self._item_read)
336 count = self._render_feed(self._selected_feed)
337 if not redisplay and count:
338 if not self.select_next_unread_item():
339 selection = self._view.get_widget_selection()
340 selection.select_path((0,))
341 return
343 def _feed_order_changed(self, event):
344 if event.sender is self._selected_feed:
345 item = None
346 selection = self._view.get_widget_selection()
347 model, treeiter = selection.get_selected()
348 if treeiter is not None:
349 path = model.get_path(treeiter)
350 item = model[path[0]][Column.ITEM]
351 self._render_feed(event.sender)
352 if item:
353 path = (item.feed.get_item_index(item),)
354 else:
355 path = (0,) # first item
356 selection.select_path(path)
358 def _all_items_read(self, feed, unread_items):
359 print unread_items
360 for index, item in enumerate(unread_items):
361 self._mark_item(item, index)
363 def _item_read(self, signal):
364 self._mark_item(signal.item)
366 def _render_feed(self, feed):
367 self._model.clear()
368 curr_iter = None
369 for item in feed.items:
370 weight = (pango.WEIGHT_BOLD, pango.WEIGHT_NORMAL)[item.seen]
371 colour = ("#0000FF", "#000000")[item.seen]
372 treeiter = self._model.append()
373 self._model.set(treeiter,
374 Column.title, item.title,
375 Column.ITEM, item,
376 Column.WEIGHT, weight,
377 Column.sticky_flag, item.sticky,
378 Column.FOREGROUND, colour)
379 if item is self._selected_item:
380 curr_iter = treeiter
382 if curr_iter:
383 path = self._model.get_path(curr_iter)
384 self._view.scroll_to_cell(path)
385 return len(feed.items)
387 def _mark_item(self, item, path=None):
388 if path is None:
389 path = (item.feed.get_item_index(item),)
390 self._set_column_weight(item, path)
391 return