Cleaned up item view.
[straw.git] / straw / ItemList.py
blob48e38964f3b5ade4c0433bf61644e1ebe9593e46
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 Constants import *
25 from straw import helpers
26 from xml.sax import saxutils
27 import Config
28 import ItemManager
29 import MVP
30 import error
31 import gobject
32 import gtk
33 import os.path, operator
34 import pango
35 import pygtk
36 import time
37 pygtk.require('2.0')
39 class Column:
40 """
41 Constants for the item list treeview columns
42 """
43 TITLE, PUB_DATE, STICKY, ITEM, WEIGHT, STICKY_FLAG, FOREGROUND = range(7)
45 class ItemListModel:
46 ''' A model of summary items for tree views '''
48 def __init__(self):
49 self.store = gtk.ListStore(str, str, gobject.TYPE_OBJECT, gobject.TYPE_PYOBJECT, int, bool, str)
51 def populate(self, items):
52 self.store.clear()
54 if not items:
55 return
56 i = 0
58 for item in items:
59 weight = (pango.WEIGHT_BOLD, pango.WEIGHT_NORMAL)[item.is_read]
60 #weight = pango.WEIGHT_NORMAL
61 #colour = ("#0000FF", "#000000")[item.is_read]
62 colour = "#000000"
63 treeiter = self.store.append()
64 #print self.store[i]
66 self.store.set(treeiter,
67 Column.TITLE, item.title,
68 Column.PUB_DATE, item.pub_date,
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)
75 i += 1
77 def item_changed_cb(self, item, property):
78 row = [row for row in self.store if row[Column.ITEM] is item]
80 if len(row) == 0:
81 return
83 row[0][Column.WEIGHT] = (pango.WEIGHT_BOLD, pango.WEIGHT_NORMAL)[item.is_read]
85 @property
86 def model(self):
87 return self.store
89 class ItemListView(MVP.WidgetView):
91 def _initialize(self):
92 self._widget.set_rules_hint(False)
93 self._widget.connect("button_press_event",self._on_button_press_event)
94 self._widget.connect("popup-menu", self._on_popup_menu)
95 self._widget.connect("row-activated", self._on_row_activated)
96 uifactory = helpers.UIFactory('ItemListActions')
97 action = uifactory.get_action('/itemlist_popup/mark_as_unread')
98 action.connect('activate', self.on_menu_mark_as_unread_activate)
99 self.popup = uifactory.get_popup('/itemlist_popup')
101 renderer = gtk.CellRendererToggle()
102 column = gtk.TreeViewColumn(_('Keep'), renderer,
103 active=Column.STICKY_FLAG)
104 column.set_resizable(True)
105 column.set_reorderable(True)
106 self._widget.append_column(column)
107 renderer.connect('toggled', self._sticky_toggled)
109 renderer = gtk.CellRendererText()
110 column = gtk.TreeViewColumn(_('_Date'), renderer,
111 text=Column.PUB_DATE,
112 weight=Column.WEIGHT)
113 column.set_resizable(True)
114 column.set_reorderable(True)
115 self._widget.append_column(column)
117 renderer = gtk.CellRendererText()
118 column = gtk.TreeViewColumn(_('_Title'), renderer,
119 text=Column.TITLE,
120 foreground=Column.FOREGROUND,
121 weight=Column.WEIGHT)
122 column.set_resizable(True)
123 column.set_reorderable(True)
124 self._widget.append_column(column)
126 selection = self._widget.get_selection()
127 selection.set_mode(gtk.SELECTION_SINGLE)
128 # selection.connect('changed', lambda x: self.item_mark_as_read(True))
130 def add_selection_changed_listener(self, listener):
131 selection = self._widget.get_selection()
132 selection.connect('changed',
133 listener.itemlist_selection_changed,
134 Column.ITEM)
136 def _model_set(self):
137 self._widget.set_model(self._model.model)
139 def _sticky_toggled(self, cell, path):
140 model = self._widget.get_model()
141 treeiter = model.get_iter((int(path),))
142 item = model.get_value(treeiter, Column.ITEM)
143 item.sticky = not item.sticky
144 model.set(treeiter, Column.STICKY_FLAG, item.sticky)
146 def _on_popup_menu(self, treeview, *args):
147 self.popup.popup(None,None,None,0,0)
149 def _on_button_press_event(self, treeview, event):
150 val = 0
151 if event.button == 3:
152 x = int(event.x)
153 y = int(event.y)
154 time = gtk.get_current_event_time()
155 path = treeview.get_path_at_pos(x, y)
156 if path is None:
157 return 1
158 path, col, cellx, celly = path
159 treeview.grab_focus()
160 treeview.set_cursor( path, col, 0)
161 self.popup.popup(None, None, None, event.button, time)
162 val = 1
163 return val
165 def on_menu_mark_as_unread_activate(self, *args):
166 self.item_mark_as_read(False)
168 def item_mark_as_read(self, isRead):
169 selection = self._widget.get_selection()
170 (model, treeiter) = selection.get_selected()
171 if not treeiter: return
172 item = model.get_value(treeiter, Column.ITEM)
174 item.props.is_read = isRead
176 if isRead:
177 weight = pango.WEIGHT_NORMAL
178 else:
179 weight = pango.WEIGHT_BOLD
180 #colour = ("#0000FF", "#000000")[item.is_read]
181 colour = "#000000"
182 model.set(treeiter, Column.FOREGROUND, colour,
183 Column.WEIGHT, weight,
184 Column.ITEM, item)
186 def mark_all_as_read(self):
187 for row in self._widget.get_model():
188 row[Column.WEIGHT] = pango.WEIGHT_NORMAL
190 def _on_row_activated(self, treeview, path, column):
191 ''' double-clicking an item opens that item in the web browser '''
192 model = self._widget.get_model()
193 try:
194 treeiter = model.get_iter(path)
195 except ValueError:
196 return
197 link = model.get_value(treeiter, Column.ITEM).link
198 if link:
199 helpers.url_show(link)
200 return
202 def select_first_item(self):
203 selection = self._widget.get_selection()
204 (model, treeiter) = selection.get_selected()
206 if not treeiter:
207 return False
209 path = model.get_path(model.get_iter_first())
210 selection.select_path(path)
211 return True
213 def select_previous_item(self):
215 Selects the item before the current selection. If there
216 is no current selection, selects the last item. If there is no
217 previous row, returns False.
219 selection = self._widget.get_selection()
220 (model, treeiter) = selection.get_selected()
221 if not treeiter: return False
222 path = model.get_path(treeiter)
223 prev = path[-1]-1
224 if prev < 0:
225 return False
226 selection.select_path(prev)
227 return True
229 def select_next_item(self):
231 Selects the item after the current selection. If there is no current
232 selection, selects the first item. If there is no next row, returns False.
234 selection = self._widget.get_selection()
235 (model, treeiter) = selection.get_selected()
236 if not treeiter:
237 treeiter = model.get_iter_first()
238 next_iter = model.iter_next(treeiter)
239 if not next_iter or not model.iter_is_valid(treeiter):
240 return False
241 next_path = model.get_path(next_iter)
242 selection.select_path(next_path)
243 return True
245 def select_last_item(self):
247 Selects the last item in this list.
249 selection = self._widget.get_selection()
250 selection.select_path(len(self.model.model) - 1)
251 return True
253 def select_next_unread_item(self):
255 Selects the first unread item after the current selection. If there is
256 no current selection, or if there are no unread items, returns False..
258 By returning False, the caller can either go to the next feed, or go
259 to the next feed with an unread item.
261 selection = self._widget.get_selection()
262 (model, treeiter) = selection.get_selected()
263 # check if we have a selection. if none,
264 # start searching from the first item
265 if not treeiter:
266 treeiter = model.get_iter_first()
267 if not treeiter: return False
268 nextiter = model.iter_next(treeiter)
269 if not nextiter: return False # no more rows to iterate
270 treerow = model[treeiter]
271 has_unread = False
272 while(treerow):
273 item = treerow[Column.ITEM]
274 if not item.is_read:
275 has_unread = True
276 selection.select_path(treerow.path)
277 break
278 treerow = treerow.next
279 return has_unread
281 class ItemListPresenter(MVP.BasicPresenter):
283 ## XXX listen for RefreshFeedDisplaySignal, FeedOrder changes,
285 def _initialize(self):
286 self.model = ItemListModel()
287 self.presented_nodes = []
289 def populate(self, items):
290 self.model.populate(items)
291 self.view.set_model(self.model)
293 def feedlist_selection_changed(self, feedlist_selection, column):
294 (model, pathlist) = feedlist_selection.get_selected_rows()
295 iters = [model.get_iter(path) for path in pathlist]
297 if not iters:
298 return
300 nodes = [model.get_value(treeiter, column) for treeiter in iters]
302 if nodes == self.presented_nodes:
303 # Selection hasn't actually changed so let's ignore the signal
304 return
306 self.presented_nodes = nodes
308 items = []
309 node = nodes[0]
311 if node.node.type == "F":
312 items = ItemManager.get_feed_items(node.node.id)
313 self.view.set_model(None)
314 self.model.populate(items)
315 self.view.set_model(self.model)
316 elif node.node.type == "C":
317 self.view.set_model(None)
318 items = ItemManager.get_category_items(node.node.id, self.populate)
320 if not self.view.select_next_unread_item():
321 self.view.select_first_item()
323 def on_mark_all_as_read(self, action):
324 self.view.mark_all_as_read()
326 def flatten(self, sequence):
327 ''' python cookbook recipe 4.6 '''
328 for item in sequence:
329 if isinstance(item, (list, tuple)):
330 for subitem in self.flatten(item):
331 yield subitem
332 else:
333 yield item
335 def select_previous_item(self):
336 return self.view.select_previous_item()
338 def select_next_item(self):
339 return self.view.select_next_item()
341 def select_next_unread_item(self):
342 return self.view.select_next_unread_item()
344 def select_last_item(self):
345 return self.view.select_last_item()