Work on feed discovery assistant.
[straw/fork.git] / straw / ItemList.py
blob31526eb0e6912f64637d95a39d65ea8d269ff802
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 Config
35 from straw import helpers
37 class Column:
38 """
39 Constants for the item list treeview columns
40 """
41 TITLE, STICKY, ITEM, WEIGHT, STICKY_FLAG, FOREGROUND = range(6)
43 class ItemListModel:
44 ''' A model of summary items for tree views '''
46 def __init__(self):
47 self.store = gtk.ListStore(str, gobject.TYPE_OBJECT, gobject.TYPE_PYOBJECT, int, bool, str)
48 config = Config.get_instance()
49 self.item_order = config.item_order
51 def populate(self, items):
52 #print "POPULATE ", len(summaryItems)
53 ## XXX should we sort here or in feeds.Feed?
54 #items = sorted(summaryItems, key=operator.attrgetter('title'),
55 # reverse=self.item_order)
56 self.store.clear()
57 for item in items:
58 weight = (pango.WEIGHT_BOLD, pango.WEIGHT_NORMAL)[item.is_read]
59 #weight = pango.WEIGHT_NORMAL
60 #colour = ("#0000FF", "#000000")[item.is_read]
61 colour = "#000000"
62 treeiter = self.store.append()
63 self.store.set(treeiter,
64 Column.TITLE, item.title,
65 Column.ITEM, item,
66 Column.WEIGHT, weight,
67 Column.STICKY_FLAG, False,
68 Column.FOREGROUND, colour)
69 #item.connect('changed', self.item_changed_cb)
71 def item_changed_cb(self, item):
72 for row in self.store:
73 rowitem = row[Column.ITEM]
74 if rowitem is item:
75 #row[Column.weight] = item.is_read and pango.WEIGHT_NORMAL or pango.WEIGHT_BOLD
76 #row[Column.foreground] = item.is_read and 'black' or 'blue'
77 #row[Column.sticky_flag] = item.sticky
78 row[Column.ITEM] = item
80 @property
81 def model(self):
82 return self.store
85 class ItemListView(MVP.WidgetView):
87 def _initialize(self):
88 self._widget.set_rules_hint(False)
89 self._widget.connect("button_press_event",self._on_button_press_event)
90 self._widget.connect("popup-menu", self._on_popup_menu)
91 self._widget.connect("row-activated", self._on_row_activated)
92 uifactory = helpers.UIFactory('ItemListActions')
93 action = uifactory.get_action('/itemlist_popup/mark_as_unread')
94 action.connect('activate', self.on_menu_mark_as_unread_activate)
95 self.popup = uifactory.get_popup('/itemlist_popup')
97 renderer = gtk.CellRendererToggle()
98 column = gtk.TreeViewColumn(_('Keep'), renderer,
99 active=Column.STICKY_FLAG)
100 column.set_resizable(True)
101 column.set_reorderable(True)
102 self._widget.append_column(column)
103 renderer.connect('toggled', self._sticky_toggled)
105 renderer = gtk.CellRendererText()
106 column = gtk.TreeViewColumn(_('_Title'), renderer,
107 text=Column.TITLE,
108 foreground=Column.FOREGROUND,
109 weight=Column.WEIGHT)
110 column.set_resizable(True)
111 column.set_reorderable(True)
112 self._widget.append_column(column)
114 selection = self._widget.get_selection()
115 selection.set_mode(gtk.SELECTION_SINGLE)
116 selection.connect('changed', lambda x: self.item_mark_as_read(True))
118 def add_selection_changed_listener(self, listener):
119 selection = self._widget.get_selection()
120 selection.connect('changed',
121 listener.itemlist_selection_changed,
122 Column.ITEM)
124 def _model_set(self):
125 self._widget.set_model(self._model.model)
127 def _sticky_toggled(self, cell, path):
128 model = self._widget.get_model()
129 treeiter = model.get_iter((int(path),))
130 item = model.get_value(treeiter, Column.ITEM)
131 item.sticky = not item.sticky
132 model.set(treeiter, Column.STICKY_FLAG, item.sticky)
134 def _on_popup_menu(self, treeview, *args):
135 self.popup.popup(None,None,None,0,0)
137 def _on_button_press_event(self, treeview, event):
138 val = 0
139 if event.button == 3:
140 x = int(event.x)
141 y = int(event.y)
142 time = gtk.get_current_event_time()
143 path = treeview.get_path_at_pos(x, y)
144 if path is None:
145 return 1
146 path, col, cellx, celly = path
147 treeview.grab_focus()
148 treeview.set_cursor( path, col, 0)
149 self.popup.popup(None, None, None, event.button, time)
150 val = 1
151 return val
153 def on_menu_mark_as_unread_activate(self, *args):
154 self.item_mark_as_read(False)
156 def item_mark_as_read(self, isRead):
157 selection = self._widget.get_selection()
158 (model, treeiter) = selection.get_selected()
159 if not treeiter: return
160 item = model.get_value(treeiter, Column.ITEM)
162 item.props.is_read = isRead
164 if isRead:
165 weight = pango.WEIGHT_NORMAL
166 else:
167 weight = pango.WEIGHT_BOLD
168 #colour = ("#0000FF", "#000000")[item.is_read]
169 colour = "#000000"
170 model.set(treeiter, Column.FOREGROUND, colour,
171 Column.WEIGHT, weight,
172 Column.ITEM, item)
173 return
175 def _on_row_activated(self, treeview, path, column):
176 ''' double-clicking an item opens that item in the web browser '''
177 model = self._widget.get_model()
178 try:
179 treeiter = model.get_iter(path)
180 except ValueError:
181 return
182 link = model.get_value(treeiter, Column.ITEM).link
183 if link:
184 helpers.url_show(link)
185 return
187 def select_first_item(self):
188 selection = self._widget.get_selection()
189 (model, treeiter) = selection.get_selected()
190 path = model.get_path(model.get_iter_first())
191 selection.select_path(path)
192 return True
194 def select_previous_item(self):
196 Selects the item before the current selection. If there
197 is no current selection, selects the last item. If there is no
198 previous row, returns False.
200 selection = self._widget.get_selection()
201 (model, treeiter) = selection.get_selected()
202 if not treeiter: return False
203 path = model.get_path(treeiter)
204 prev = path[-1]-1
205 if prev < 0:
206 return False
207 selection.select_path(prev)
208 return True
210 def select_next_item(self):
212 Selects the item after the current selection. If there is no current
213 selection, selects the first item. If there is no next row, returns False.
215 selection = self._widget.get_selection()
216 (model, treeiter) = selection.get_selected()
217 if not treeiter:
218 treeiter = model.get_iter_first()
219 next_iter = model.iter_next(treeiter)
220 if not next_iter or not model.iter_is_valid(treeiter):
221 return False
222 next_path = model.get_path(next_iter)
223 selection.select_path(next_path)
224 return True
226 def select_last_item(self):
228 Selects the last item in this list.
230 selection = self._widget.get_selection()
231 selection.select_path(len(self.model.model) - 1)
232 return True
234 def select_next_unread_item(self):
236 Selects the first unread item after the current selection. If there is
237 no current selection, or if there are no unread items, returns False..
239 By returning False, the caller can either go to the next feed, or go
240 to the next feed with an unread item.
242 selection = self._widget.get_selection()
243 (model, treeiter) = selection.get_selected()
244 # check if we have a selection. if none,
245 # start searching from the first item
246 if not treeiter:
247 treeiter = model.get_iter_first()
248 if not treeiter: return False
249 nextiter = model.iter_next(treeiter)
250 if not nextiter: return False # no more rows to iterate
251 treerow = model[nextiter]
252 has_unread = False
253 while(treerow):
254 item = treerow[Column.ITEM]
255 if not item.is_read:
256 has_unread = True
257 selection.select_path(treerow.path)
258 break
259 treerow = treerow.next
260 return has_unread
262 class ItemListPresenter(MVP.BasicPresenter):
264 ## XXX listen for RefreshFeedDisplaySignal, FeedOrder changes,
266 def _initialize(self):
267 self.model = ItemListModel()
269 def feedlist_selection_changed(self, feedlist_selection, column):
270 (model, pathlist) = feedlist_selection.get_selected_rows()
271 iters = [model.get_iter(path) for path in pathlist]
272 if not iters:
273 return
274 nodes = [model.get_value(treeiter, column) for treeiter in iters]
275 items = []
276 node = nodes[0]
277 import FeedManager
279 if node.type == "F":
280 items = FeedManager.get_feed_items(nodes[0].obj.id)
281 elif node.type == "C":
282 items = FeedManager.get_category_items(nodes[0].obj.id)
283 #try:
284 # items += [node.feed.items for node in nodes]
285 #except TypeError, te:
286 # gen = (node.category.feeds for node in nodes)
287 # items += [feed.items for feed in gen.next()]
288 #items = list(self.flatten(items))
289 self.model.populate(items)
290 if not len(self.model.model): return
291 if not self.view.select_next_unread_item():
292 self.view.select_first_item()
294 def flatten(self, sequence):
295 ''' python cookbook recipe 4.6 '''
296 for item in sequence:
297 if isinstance(item, (list, tuple)):
298 for subitem in self.flatten(item):
299 yield subitem
300 else:
301 yield item
303 def select_previous_item(self):
304 return self.view.select_previous_item()
306 def select_next_item(self):
307 return self.view.select_next_item()
309 def select_next_unread_item(self):
310 return self.view.select_next_unread_item()
312 def select_last_item(self):
313 return self.view.select_last_item()