Take functionality related to items out of FeedManager to ItemManager.
[straw.git] / straw / ItemList.py
blob2476eb856979f7ae0736ec627be76471e8b533e1
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 straw import helpers
25 from xml.sax import saxutils
26 import Config
27 import ItemManager
28 import MVP
29 import error
30 import gobject
31 import gtk
32 import os.path, operator
33 import pango
34 import pygtk
35 pygtk.require('2.0')
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()
58 if not items:
59 return
61 for item in items:
62 weight = (pango.WEIGHT_BOLD, pango.WEIGHT_NORMAL)[item.is_read]
63 #weight = pango.WEIGHT_NORMAL
64 #colour = ("#0000FF", "#000000")[item.is_read]
65 colour = "#000000"
66 treeiter = self.store.append()
67 self.store.set(treeiter,
68 Column.TITLE, item.title,
69 Column.ITEM, item,
70 Column.WEIGHT, weight,
71 Column.STICKY_FLAG, False,
72 Column.FOREGROUND, colour)
74 item.connect("notify", self.item_changed_cb)
76 def item_changed_cb(self, item, property):
77 row = [row for row in self.store if row[Column.ITEM] is item]
79 if len(row) == 0:
80 return
82 row[0][Column.WEIGHT] = (pango.WEIGHT_BOLD, pango.WEIGHT_NORMAL)[item.is_read]
84 @property
85 def model(self):
86 return self.store
88 class ItemListView(MVP.WidgetView):
90 def _initialize(self):
91 self._widget.set_rules_hint(False)
92 self._widget.connect("button_press_event",self._on_button_press_event)
93 self._widget.connect("popup-menu", self._on_popup_menu)
94 self._widget.connect("row-activated", self._on_row_activated)
95 uifactory = helpers.UIFactory('ItemListActions')
96 action = uifactory.get_action('/itemlist_popup/mark_as_unread')
97 action.connect('activate', self.on_menu_mark_as_unread_activate)
98 self.popup = uifactory.get_popup('/itemlist_popup')
100 renderer = gtk.CellRendererToggle()
101 column = gtk.TreeViewColumn(_('Keep'), renderer,
102 active=Column.STICKY_FLAG)
103 column.set_resizable(True)
104 column.set_reorderable(True)
105 self._widget.append_column(column)
106 renderer.connect('toggled', self._sticky_toggled)
108 renderer = gtk.CellRendererText()
109 column = gtk.TreeViewColumn(_('_Title'), renderer,
110 text=Column.TITLE,
111 foreground=Column.FOREGROUND,
112 weight=Column.WEIGHT)
113 column.set_resizable(True)
114 column.set_reorderable(True)
115 self._widget.append_column(column)
117 selection = self._widget.get_selection()
118 selection.set_mode(gtk.SELECTION_SINGLE)
119 # selection.connect('changed', lambda x: self.item_mark_as_read(True))
121 def add_selection_changed_listener(self, listener):
122 selection = self._widget.get_selection()
123 selection.connect('changed',
124 listener.itemlist_selection_changed,
125 Column.ITEM)
127 def _model_set(self):
128 self._widget.set_model(self._model.model)
130 def _sticky_toggled(self, cell, path):
131 model = self._widget.get_model()
132 treeiter = model.get_iter((int(path),))
133 item = model.get_value(treeiter, Column.ITEM)
134 item.sticky = not item.sticky
135 model.set(treeiter, Column.STICKY_FLAG, item.sticky)
137 def _on_popup_menu(self, treeview, *args):
138 self.popup.popup(None,None,None,0,0)
140 def _on_button_press_event(self, treeview, event):
141 val = 0
142 if event.button == 3:
143 x = int(event.x)
144 y = int(event.y)
145 time = gtk.get_current_event_time()
146 path = treeview.get_path_at_pos(x, y)
147 if path is None:
148 return 1
149 path, col, cellx, celly = path
150 treeview.grab_focus()
151 treeview.set_cursor( path, col, 0)
152 self.popup.popup(None, None, None, event.button, time)
153 val = 1
154 return val
156 def on_menu_mark_as_unread_activate(self, *args):
157 self.item_mark_as_read(False)
159 def item_mark_as_read(self, isRead):
160 selection = self._widget.get_selection()
161 (model, treeiter) = selection.get_selected()
162 if not treeiter: return
163 item = model.get_value(treeiter, Column.ITEM)
165 item.props.is_read = isRead
167 if isRead:
168 weight = pango.WEIGHT_NORMAL
169 else:
170 weight = pango.WEIGHT_BOLD
171 #colour = ("#0000FF", "#000000")[item.is_read]
172 colour = "#000000"
173 model.set(treeiter, Column.FOREGROUND, colour,
174 Column.WEIGHT, weight,
175 Column.ITEM, item)
176 return
178 def _on_row_activated(self, treeview, path, column):
179 ''' double-clicking an item opens that item in the web browser '''
180 model = self._widget.get_model()
181 try:
182 treeiter = model.get_iter(path)
183 except ValueError:
184 return
185 link = model.get_value(treeiter, Column.ITEM).link
186 if link:
187 helpers.url_show(link)
188 return
190 def select_first_item(self):
191 selection = self._widget.get_selection()
192 (model, treeiter) = selection.get_selected()
193 path = model.get_path(model.get_iter_first())
194 selection.select_path(path)
195 return True
197 def select_previous_item(self):
199 Selects the item before the current selection. If there
200 is no current selection, selects the last item. If there is no
201 previous row, returns False.
203 selection = self._widget.get_selection()
204 (model, treeiter) = selection.get_selected()
205 if not treeiter: return False
206 path = model.get_path(treeiter)
207 prev = path[-1]-1
208 if prev < 0:
209 return False
210 selection.select_path(prev)
211 return True
213 def select_next_item(self):
215 Selects the item after the current selection. If there is no current
216 selection, selects the first item. If there is no next row, returns False.
218 selection = self._widget.get_selection()
219 (model, treeiter) = selection.get_selected()
220 if not treeiter:
221 treeiter = model.get_iter_first()
222 next_iter = model.iter_next(treeiter)
223 if not next_iter or not model.iter_is_valid(treeiter):
224 return False
225 next_path = model.get_path(next_iter)
226 selection.select_path(next_path)
227 return True
229 def select_last_item(self):
231 Selects the last item in this list.
233 selection = self._widget.get_selection()
234 selection.select_path(len(self.model.model) - 1)
235 return True
237 def select_next_unread_item(self):
239 Selects the first unread item after the current selection. If there is
240 no current selection, or if there are no unread items, returns False..
242 By returning False, the caller can either go to the next feed, or go
243 to the next feed with an unread item.
245 selection = self._widget.get_selection()
246 (model, treeiter) = selection.get_selected()
247 # check if we have a selection. if none,
248 # start searching from the first item
249 if not treeiter:
250 treeiter = model.get_iter_first()
251 if not treeiter: return False
252 nextiter = model.iter_next(treeiter)
253 if not nextiter: return False # no more rows to iterate
254 treerow = model[nextiter]
255 has_unread = False
256 while(treerow):
257 item = treerow[Column.ITEM]
258 if not item.is_read:
259 has_unread = True
260 selection.select_path(treerow.path)
261 break
262 treerow = treerow.next
263 return has_unread
265 class ItemListPresenter(MVP.BasicPresenter):
267 ## XXX listen for RefreshFeedDisplaySignal, FeedOrder changes,
269 def _initialize(self):
270 self.model = ItemListModel()
272 def feedlist_selection_changed(self, feedlist_selection, column):
273 (model, pathlist) = feedlist_selection.get_selected_rows()
274 iters = [model.get_iter(path) for path in pathlist]
276 if not iters:
277 return
279 nodes = [model.get_value(treeiter, column) for treeiter in iters]
280 items = []
281 node = nodes[0]
283 if node.node.type == "F":
284 items = ItemManager.get_feed_items(node.node.id)
285 elif node.node.type == "C":
286 items = ItemManager.get_category_items(node.node.id)
288 self.model.populate(items)
289 if not len(self.model.model): return
290 if not self.view.select_next_unread_item():
291 self.view.select_first_item()
293 def flatten(self, sequence):
294 ''' python cookbook recipe 4.6 '''
295 for item in sequence:
296 if isinstance(item, (list, tuple)):
297 for subitem in self.flatten(item):
298 yield subitem
299 else:
300 yield item
302 def select_previous_item(self):
303 return self.view.select_previous_item()
305 def select_next_item(self):
306 return self.view.select_next_item()
308 def select_next_unread_item(self):
309 return self.view.select_next_unread_item()
311 def select_last_item(self):
312 return self.view.select_last_item()