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