removed feed title, refresh, and subscriptions label on top of the main view
[straw.git] / src / lib / FeedListView.py
blob79be1183b716494e880a1031ea4bafa7b7742510
1 """ FeedListView.py
3 Module for displaying the feeds in the Feeds TreeView.
4 """
5 __copyright__ = "Copyright (c) 2002-2005 Free Software Foundation, Inc."
6 __license__ = """
7 Straw is free software; you can redistribute it and/or modify it under the
8 terms of the GNU General Public License as published by the Free Software
9 Foundation; either version 2 of the License, or (at your option) any later
10 version.
12 Straw is distributed in the hope that it will be useful, but WITHOUT ANY
13 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License along with
17 this program; if not, write to the Free Software Foundation, Inc., 59 Temple
18 Place - Suite 330, Boston, MA 02111-1307, USA. """
20 import copy
21 import locale
22 from logging import debug, error, warn, info, critical, exception
23 import os
24 import pygtk
25 pygtk.require('2.0')
26 import gobject
27 import gtk
28 import pango
29 import FeedCategoryList
30 import FeedPropertiesDialog
31 import FeedList
32 import Feed
33 import Event
34 import dialogs
35 import Config
36 import PollManager
37 import MVP
39 import utils
41 class Column:
42 pixbuf, name, foreground, object = range(4)
45 class FeedListPopup:
46 ''' Abstracts a popup widget '''
48 def __init__(self, listener):
49 self.manager = gtk.UIManager()
50 actions = [
51 ("refresh", gtk.STOCK_REFRESH, _("_Refresh"), None, _("Update this feed"),
52 listener.on_menu_poll_selected_activate),
53 ("mark_as_read", gtk.STOCK_CLEAR, _("_Mark As Read"), None, _("Mark all items in this feed as read"),
54 listener.on_menu_mark_all_as_read_activate),
55 ("stop_refresh", None, _("_Stop Refresh"), None, _("Stop updating this feed"),
56 listener.on_menu_stop_poll_selected_activate),
57 ("remove", None, _("Remo_ve Feed"), None, _("Remove this feed from my subscription"),
58 listener.on_remove_selected_feed),
59 ("category-sort",None,_("_Arrange Feeds"), None, _("Sort the current category")),
60 ("ascending", gtk.STOCK_SORT_ASCENDING, _("Alpha_betical Order"), None, _("Sort in alphabetical order"),
61 listener.on_sort_ascending),
62 ("descending", gtk.STOCK_SORT_DESCENDING, _("Re_verse Order"), None, _("Sort in reverse order"),
63 listener.on_sort_descending),
64 ("properties", gtk.STOCK_INFO, _("_Information"), None, _("Feed-specific properties"),
65 listener.on_display_properties_feed)
67 ag = gtk.ActionGroup('FeedListPopupActions')
68 ag.add_actions(actions)
69 self.manager.insert_action_group(ag,0)
70 popupui = os.path.join(utils.find_image_dir(), 'ui.xml')
71 self.manager.add_ui_from_file(popupui)
73 @property
74 def popup(self):
75 return self.manager.get_widget('/feed_list_popup')
78 class FeedListModel:
79 ''' The model for the feed list view '''
81 def __init__(self):
82 # name, pixbuf, unread, foreground
83 self.store = gtk.TreeStore(gtk.gdk.Pixbuf, str, str, gobject.TYPE_PYOBJECT)
84 # unread, weight, status_flag feed object, allow_children
85 # str, int, 'gboolean', gobject.TYPE_PYOBJECT, 'gboolean'
86 filename = os.path.join(utils.find_image_dir(), 'feed.png')
87 self.pixbuf = gtk.gdk.pixbuf_new_from_file(filename)
89 self._init_model()
91 def _init_model(self):
92 fclist = FeedCategoryList.get_instance()
93 parent = None
95 # build the user categories and its feeds
96 for category in fclist.user_categories:
97 parent = self.store.append(parent)
98 self.store.set(parent, Column.pixbuf, self.pixbuf,
99 Column.name, category.title,
100 Column.object, None)
101 for f in category.feeds:
102 rowiter = self.store.append(parent)
103 self.store.set(rowiter, Column.pixbuf, self.get_pixbuf(f),
104 Column.name, f.title,
105 Column.foreground, self.get_foreground(f.n_items_unread),
106 Column.object, f) # maybe use create_adapter(f) here?
108 # build the feeds that do not belong to any other category
109 for feed in fclist._un_category.feeds:
110 self.store.append(None, [self.get_pixbuf(feed), feed.title,
111 self.get_foreground(feed.n_items_unread),
112 feed])
114 print "finished initializing feed list vie model"
116 def get_foreground(self, unread):
117 ''' gets the foreground color according to the number of unread items'''
118 return ('black', 'blue')[(unread > 0) and 1 or 0]
120 def get_pixbuf(self, feed):
121 ''' gets the pixbuf to display according to the status of the feed '''
122 widget = gtk.Label()
123 # ignore why above is a gtk.Label. We just need
124 # gtk.Widget.render_icon to get a pixbuf out of a stock image.
125 # Fyi, gtk.Widget is abstract that is why we need a subclass to do
126 # this for us.
127 if feed.process_status is not Feed.Feed.STATUS_IDLE:
128 return widget.render_icon(gtk.STOCK_EXECUTE. gtk.ICON_SIZE_MENU)
129 elif feed.error:
130 return widget.render_icon(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_MENU)
131 else:
132 return self.pixbuf
134 @property
135 def model(self):
136 return self.store
138 def get_feeds(self, pathlist):
139 ''' Returns a list of feed objects that live in the pathlist '''
140 selected_feeds = []
141 for path in pathlist:
142 treeiter = self.store.get_iter(path)
143 object = self.store.get_value(treeiter, Column.object)
144 if object:
145 selected_feeds.append(object)
146 return selected_feeds
149 class FeedsView(MVP.WidgetView):
150 def _initialize(self):
151 self._widget.set_search_column(Column.name)
153 # pixbuf column
154 column = gtk.TreeViewColumn()
155 status_renderer = gtk.CellRendererPixbuf()
156 column.pack_start(status_renderer, False)
157 column.set_attributes(status_renderer,
158 pixbuf=Column.pixbuf)
160 # feed title renderer
161 title_renderer = gtk.CellRendererText()
162 column.pack_start(title_renderer, False)
163 column.set_attributes(title_renderer,
164 foreground=Column.foreground,
165 text=Column.name) #, weight=Column.BOLD)
167 self._widget.append_column(column)
169 selection = self._widget.get_selection()
170 selection.set_mode(gtk.SELECTION_MULTIPLE)
172 selection.connect("changed", self._selection_changed)
174 self._widget.connect("button_press_event", self._on_button_press_event)
175 self._widget.connect("popup-menu", self._on_popup_menu)
177 self._popup = FeedListPopup(self).popup
179 def _model_set(self):
180 self._widget.set_model(self._model.model)
182 def add_selection_changed_listener(self, listener):
183 selection = self._widget.get_selection()
184 selection.connect('changed', listener.feedlist_selection_changed, self._model)
186 def _on_popup_menu(self, treeview, *args):
187 self._popup.popup(None, None, None, 0, 0)
190 def foreach_selected(self, func):
191 selection = self._widget.get_selection()
192 (model, pathlist) = selection.get_selected_rows()
193 iters = [model.get_iter(path) for path in pathlist]
194 for treeiter in iters:
195 object = model.get_value(treeiter, Column.object)
196 func(object, model, treeiter)
197 return
200 def _on_button_press_event(self, treeview, event):
201 retval = 0
202 if event.button == 3:
203 x = int(event.x)
204 y = int(event.y)
205 time = gtk.get_current_event_time()
206 path = treeview.get_path_at_pos(x, y)
207 if path is None:
208 return 1
209 path, col, cellx, celly = path
210 treeview.grab_focus()
211 self._popup.popup(None, None, None, event.button, time)
212 retval = 1
213 return retval
215 ### XXX don't do feed related tasks when path is a category
216 ### this applies to every operation I suppose
217 def on_menu_poll_selected_activate(self, *args):
218 config = Config.get_instance()
219 poll = True
220 if config.offline:
221 config.offline = not config.offline
222 # XXX pass 'list' of feeds
223 #PollManager.get_instance().poll([self._curr_feed])
224 return
227 def on_menu_stop_poll_selected_activate(self, *args):
228 self.foreach_selected(lambda o,*args: o.router.stop_polling())
230 def on_menu_mark_all_as_read_activate(self, *args):
231 self.foreach_selected(lambda o,*args: o.mark_all_read())
233 def on_remove_selected_feed(self, *args):
234 def remove(*args):
235 (object, model, treeiter) = args
236 model.remove(treeiter)
237 #feedlist = FeedList.get_instance()
238 #idx = feedlist.index(object)
239 #del feedlist[idx]
240 self.foreach_selected(remove)
241 return
244 def on_sort_ascending(self, *args):
245 ## XXX this will not work with tree list
246 self._presenter.sort_category()
248 def on_sort_descending(self, *args):
249 ### XXX this will not work with tree list
250 self._presenter.sort_category(reverse=True)
252 def on_display_properties_feed(self, *args):
253 self._presenter.show_feed_properties()
254 return
256 def _selection_changed(self, selection):
258 Called when the current feed selection changed
260 (model, pathlist) = selection.get_selected_rows()
261 print "feed list view ", pathlist
262 #model, rowiter = selection.get_selected()
263 #if not rowiter:
264 # return
265 #object = model.get_value(rowiter, column)
266 #if object:
267 #self._presenter.selection_changed(None) #object
268 return
270 #def set_cursor(self, treeiter, col_id=None, edit=False):
271 # path = self._model.get_path(treeiter)
272 # if col_id:
273 # column = self._widget.get_column(col_id)
274 # else:
275 # column = None
276 # self._widget.set_cursor(path, column, edit)
277 # self._widget.scroll_to_cell(path, column)
278 # self._widget.grab_focus()
279 # return
282 #def get_location(self):
283 # model, iter = self._widget.get_selection().get_selected()
284 # if iter is None:
285 # return (None, None)
286 # path = model.get_path(iter)
287 # return self.get_parent_with_path(FeedList.get_instance(), path)
289 class FeedsPresenter(MVP.BasicPresenter):
290 def _initialize(self):
291 self.model = FeedListModel()
292 # self._init_signals()
293 # self._curr_feed = None
294 # self._curr_category = None
295 return
298 def _init_signals(self):
299 flist = FeedList.get_instance()
300 flist.signal_connect(Event.ItemReadSignal,
301 self._feed_item_read)
302 flist.signal_connect(Event.AllItemsReadSignal,
303 self._feed_all_items_read)
304 flist.signal_connect(Event.FeedsChangedSignal,
305 self._feeds_changed)
306 flist.signal_connect(Event.FeedDetailChangedSignal,
307 self._feed_detail_changed)
308 fclist = FeedCategoryList.get_instance()
309 fclist.signal_connect(Event.FeedCategorySortedSignal,
310 self._feeds_sorted_cb)
311 fclist.signal_connect(Event.FeedCategoryChangedSignal,
312 self._fcategory_changed_cb)
314 # def _sort_func(self, model, a, b):
316 Sorts the feeds lexically.
318 From the gtk.TreeSortable.set_sort_func doc:
320 The comparison callback should return -1 if the iter1 row should come before
321 the iter2 row, 0 if the rows are equal, or 1 if the iter1 row should come
322 after the iter2 row.
324 """ retval = 0
325 fa = model.get_value(a, Column.OBJECT)
326 fb = model.get_value(b, Column.OBJECT)
328 if fa and fb:
329 retval = locale.strcoll(fa.title, fb.title)
330 elif fa is not None: retval = -1
331 elif fb is not None: retval = 1
332 return retval
334 def get_selected_feed(self):
335 return self._curr_feed
338 def sort_category(self, reverse=False):
339 self._curr_category.sort()
340 if reverse:
341 self._curr_category.reverse()
342 return
344 def show_feed_properties(self):
345 FeedPropertiesDialog.show_feed_properties(None, self._curr_feed)
346 return
348 def select_first_feed(self):
349 treeiter = self._model.get_iter_first()
350 if not treeiter or not self._model.iter_is_valid(treeiter):
351 return False
352 self._view.set_cursor(treeiter)
353 return True
355 # def select_next_feed(self):
357 # Scrolls to the next feed in the feed list
359 """ selection = self._view.get_selection()
360 model, treeiter = selection.get_selected()
361 if not treeiter:
362 treeiter = model.get_iter_first()
363 next_feed_iter = model.iter_next(treeiter)
364 if not next_feed_iter or not self._model.iter_is_valid(next_feed_iter):
365 return False
366 self._view.set_cursor(next_feed_iter)
367 return True
369 def select_previous_feed(self):
371 Scrolls to the previous feeds in the feed list.
373 selection = self._view.get_selection()
374 model, treeiter = selection.get_selected()
375 if not treeiter:
376 treeiter = model.get_iter_first()
377 path = model.get_path(treeiter)
378 # check if there's a feed in the path
379 if not path:
380 return False
381 prev_path = path[-1]-1
382 if prev_path < 0:
383 # go to the last feed in the list
384 prev_path = len(self._model) - 1
385 self._view.set_cursor(self._model.get_iter(prev_path))
386 return True
388 def select_next_unread_feed(self):
389 has_unread = False
390 mark_treerow = 1
391 treerow = self._model[0]
392 selection = self._view.get_selection()
393 srow = selection.get_selected()
394 if srow:
395 model, treeiter = srow
396 nextiter = model.iter_next(treeiter)
397 if nextiter:
398 treerow = self._model[model.get_path(nextiter)]
399 while(treerow):
400 feedrow = treerow[Column.OBJECT]
401 if feedrow.feed.n_items_unread:
402 self._view.set_cursor(treerow.iter)
403 has_unread = True
404 break
405 treerow = treerow.next
406 if not treerow and mark_treerow:
407 # should only do this once.
408 mark_treerow = treerow
409 treerow = self._model[0]
410 return has_unread
412 def display_feed(self, feed):
413 # set_cursor will emit a 'changed' event in the treeview
414 # and then feed_selection_changed above will be called.
415 path = self._curr_category.index_feed(feed)
416 treeiter = self._model.get_iter(path)
417 self._view.set_cursor(treeiter, Column.NAME)
419 def display_category_feeds(self, category):
420 self._curr_category = category
421 feeds = self._curr_category.feeds
422 self._model.foreach(self._disconnect)
423 self._model.clear()
424 curr_feed_iter = self._display_feeds(feeds)
425 if curr_feed_iter:
426 self._view.set_cursor(curr_feed_iter)
427 else:
428 it = self._model.get_iter_first()
429 if it:
430 self._view.set_cursor(it)
431 else:
432 self.emit_signal(Event.FeedsEmptySignal(self))
433 return
435 def _display_feeds(self, feeds, parent=None):
436 def _connect_adapter(adapter, feedindex):
437 adapter.signal_connect(Event.ItemsAddedSignal,
438 self._adapter_updated_handler, feedindex)
439 adapter.signal_connect(Event.FeedPolledSignal,
440 self._adapter_updated_handler, feedindex)
441 adapter.signal_connect(Event.FeedStatusChangedSignal,
442 self._adapter_updated_handler, feedindex)
443 adapter.signal_connect(Event.ItemReadSignal,
444 self._adapter_updated_handler, feedindex)
445 adapter.signal_connect(Event.AllItemsReadSignal,
446 self._adapter_updated_handler, feedindex)
447 curr_feed_iter = None
448 for f in feeds:
449 adapter = create_adapter(f)
450 rowiter = self._model.append(parent)
451 self._model.set(rowiter,
452 Column.NAME, adapter.title,
453 Column.OBJECT, adapter)
455 idx = self._curr_category.index_feed(adapter.feed)
456 _connect_adapter(adapter, idx) # we need this to get the rest of the data
457 new_string, new = adapter.unread
458 weight = (pango.WEIGHT_NORMAL, pango.WEIGHT_BOLD)[new > 0]
459 status, pixbuf = adapter.status_icon
460 self._model.set(rowiter,
461 Column.UNREAD, new_string,
462 Column.BOLD, weight,
463 Column.STATUS_FLAG, status,
464 Column.STATUS_PIXBUF, pixbuf,
465 Column.ALLOW_CHILDREN, False)
466 if adapter.feed is self._curr_feed:
467 curr_feed_iter = rowiter
468 return curr_feed_iter
470 def display_empty_category(self):
471 self._model.foreach(self._disconnect)
472 self._model.clear()
473 return
475 def _disconnect(self, model, path, iter, user_data=None):
476 ob = model[path][Column.OBJECT]
477 self._disconnect_adapter(ob)
478 if not len(model):
479 return True
480 return False
482 def _disconnect_adapter(self, adapter):
483 adapter.disconnect()
484 adapter.signal_disconnect(Event.ItemsAddedSignal,
485 self._adapter_updated_handler)
486 adapter.signal_disconnect(Event.FeedPolledSignal,
487 self._adapter_updated_handler)
488 adapter.signal_disconnect(Event.FeedStatusChangedSignal,
489 self._adapter_updated_handler)
490 del adapter
492 def _adapter_updated_handler(self, signal, feed_index):
493 self._update_adapter_view(signal.sender, feed_index)
495 def _update_adapter_view(self, adapter, feed_index):
496 new = adapter.unread
497 row = self._model[feed_index]
498 row[Column.NAME] = adapter.title
499 row[Column.UNREAD] = new[0]
500 row[Column.BOLD] = (pango.WEIGHT_NORMAL, pango.WEIGHT_BOLD)[new[1] > 0]
501 row[Column.OBJECT] = adapter
502 status, pixbuf = adapter.status_icon
503 row[Column.STATUS_FLAG] = status
504 if pixbuf:
505 row[Column.STATUS_PIXBUF] = pixbuf
506 self._view.queue_draw()
508 def _feeds_changed(self, signal):
509 self._feed_view_update(signal.feed)
511 def _feed_detail_changed(self, signal):
512 self._feed_view_update(signal.sender)
514 def _feed_item_read(self, signal):
515 path = (0,)
516 selection = self._view.get_selection()
517 selected_row = selection.get_selected()
518 if selected_row:
519 model, treeiter = selected_row
520 path = model.get_path(treeiter)
521 treerow = self._model[path]
522 adapter = treerow[Column.OBJECT]
523 self._update_adapter_view(adapter, path)
525 def _feed_all_items_read(self, signal):
526 pass
528 def _feed_view_update(self, feed):
529 for index, f in enumerate(self._model):
530 adapter = self._model[index][Column.OBJECT]
531 if adapter.feed is feed:
532 self._update_adapter_view(adapter, index)
533 break
534 return
536 def _feeds_sorted_cb(self, signal):
537 self.model.set_sort_func(Column.NAME, self._sort_func)
538 if not signal.descending:
539 self.model.set_sort_column_id(Column.NAME, gtk.SORT_ASCENDING)
540 else:
541 self.model.set_sort_column_id(Column.NAME, gtk.SORT_DESCENDING)
542 self.model.sort_column_changed()
543 return
545 def _fcategory_changed_cb(self,signal):
546 if signal.sender is self._curr_category:
547 self._curr_category = signal.sender
548 self.display_category_feeds(self._curr_category)
549 return
551 def expand_row(self, obj):
552 obj.open = True
554 def collapse_row(self, obj):
555 obj.open = False
557 def move_feed(self, sidx, tidx):
558 self._curr_category.move_feed(sidx, tidx)
559 return
561 class DisplayAdapter(object, Event.SignalEmitter):
563 View adapter for feeds and categories
565 def __init__(self, ob):
566 self._ob = ob
567 Event.SignalEmitter.__init__(self)
568 self.initialize_slots(Event.ItemReadSignal,
569 Event.ItemsAddedSignal,
570 Event.AllItemsReadSignal,
571 Event.FeedPolledSignal,
572 Event.FeedStatusChangedSignal)
574 def equals(self, ob):
575 return self._ob is ob
577 class FeedDisplayAdapter(DisplayAdapter):
578 """Adapter for displaying Feed objects in the tree"""
579 def __init__(self, ob):
580 DisplayAdapter.__init__(self, ob)
581 ob.signal_connect(Event.ItemReadSignal, self.resend_signal)
582 ob.signal_connect(Event.ItemsAddedSignal, self.resend_signal)
583 ob.signal_connect(Event.AllItemsReadSignal, self.resend_signal)
584 ob.signal_connect(Event.FeedPolledSignal, self.resend_signal)
585 ob.signal_connect(Event.FeedStatusChangedSignal, self.resend_signal)
587 def disconnect(self):
588 self._ob.signal_disconnect(
589 Event.ItemReadSignal, self.resend_signal)
590 self._ob.signal_disconnect(
591 Event.ItemsAddedSignal, self.resend_signal)
592 self._ob.signal_disconnect(
593 Event.AllItemsReadSignal, self.resend_signal)
594 self._ob.signal_disconnect(
595 Event.FeedPolledSignal, self.resend_signal)
596 self._ob.signal_disconnect(
597 Event.FeedStatusChangedSignal, self.resend_signal)
599 def resend_signal(self, signal):
600 new = copy.copy(signal)
601 new.sender = self
602 self.emit_signal(new)
604 @property
605 def title(self):
606 return self._ob.title
608 @property
609 def unread(self):
610 nu = self._ob.n_items_unread
611 if nu != 0:
612 return ("%s" % nu, nu)
613 else:
614 return ("", nu)
616 @property
617 def status_icon(self):
618 if self._ob.process_status is not Feed.Feed.STATUS_IDLE:
619 return (1, gtk.STOCK_EXECUTE)
620 elif self._ob.error:
621 return (1, gtk.STOCK_DIALOG_ERROR)
622 return (0, None)
624 @property
625 def feed(self):
626 return self._ob
628 contents = property(lambda x: None)
629 open = property(lambda x: None)
631 def create_adapter(ob):
632 if isinstance(ob, Feed.Feed):
633 return FeedDisplayAdapter(ob)