""" FeedListView.py Module for displaying the feeds in the Feeds TreeView. """ __copyright__ = "Copyright (c) 2002-2005 Free Software Foundation, Inc." __license__ = """ Straw is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Straw is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. """ import copy import locale import pygtk pygtk.require('2.0') import gobject import gtk import pango import FeedCategoryList from FeedPropertiesDialog import FeedPropertiesDialog import FeedList import Feed import Event import dialogs import Config import PollManager import MVP class Column: NAME = 0 UNREAD = 1 BOLD = 2 STATUS_FLAG = 3 STATUS_PIXBUF = 4 OBJECT = 5 ALLOW_CHILDREN = 6 class FeedsView(MVP.WidgetView): popup_ui = """ """ def _initialize(self): self._popup = None self._widget.set_rules_hint(False) self._widget.set_search_column(Column.NAME) self._create_columns() selection = self._widget.get_selection() selection.connect("changed", self._feed_selection_changed, Column.OBJECT) self._widget.connect("button_press_event", self._on_button_press_event) self._widget.connect("popup-menu", self._on_popup_menu) self._create_popup() def _create_columns(self): # hide the expander. This should remove the extra space at the left # of the the treeview. expander = gtk.TreeViewColumn() expander.set_visible(False) self._widget.append_column(expander) self._widget.set_expander_column(expander) column = gtk.TreeViewColumn() status_renderer = gtk.CellRendererPixbuf() column.pack_start(status_renderer, False) column.set_attributes(status_renderer, stock_id=Column.STATUS_PIXBUF, visible=Column.STATUS_FLAG) unread_renderer = gtk.CellRendererText() column.pack_start(unread_renderer, False) column.set_attributes(unread_renderer, text=Column.UNREAD, weight=Column.BOLD) column.set_title("_Subscriptions") title_renderer = gtk.CellRendererText() column.pack_end(title_renderer, False) column.set_attributes(title_renderer, text=Column.NAME, weight=Column.BOLD) self._widget.append_column(column) return def _create_popup(self): actions = [ ("refresh", gtk.STOCK_REFRESH, _("_Refresh"), None, _("Update this feed"), self._on_menu_poll_selected_activate), ("mark_as_read", gtk.STOCK_CLEAR, _("_Mark As Read"), None, _("Mark all items in this feed as read"), self._on_menu_mark_all_as_read_activate), ("stop_refresh", None, _("_Stop Refresh"), None, _("Stop updating this feed"), self._on_menu_stop_poll_selected_activate), ("remove", None, _("Remo_ve Feed"), None, _("Remove this feed from my subscription"), self._remove_selected_feed), ("category-sort",None,_("_Arrange Feeds"), None, _("Sort the current category")), ("ascending", gtk.STOCK_SORT_ASCENDING, _("Alpha_betical Order"), None, _("Sort in alphabetical order"), self._sort_ascending), ("descending", gtk.STOCK_SORT_DESCENDING, _("Re_verse Order"), None, _("Sort in reverse order"), self._sort_descending), ("properties", gtk.STOCK_INFO, _("_Information"), None, _("Feed-specific properties"), self._display_properties_feed) ] ag = gtk.ActionGroup('FeedListPopupActions') ag.add_actions(actions) uimanager = gtk.UIManager() uimanager.insert_action_group(ag,0) uimanager.add_ui_from_string(FeedsView.popup_ui) self._popup = uimanager.get_widget('/feed_list_popup') return def _model_set(self): self._widget.set_model(self._model) def get_selection(self): return self._widget.get_selection() def _on_popup_menu(self, treeview, *args): self._popup.popup(None, None, None, 0, 0) def _on_button_press_event(self, treeview, event): retval = 0 if event.button == 3: x = int(event.x) y = int(event.y) time = gtk.get_current_event_time() path = treeview.get_path_at_pos(x, y) if path is None: return 1 path, col, cellx, celly = path treeview.grab_focus() #treeview.set_cursor( path, col, 0) self._popup.popup(None, None, None, event.button, time) retval = 1 return retval def _on_menu_poll_selected_activate(self, *args): self._presenter.poll_current_feed() def _on_menu_stop_poll_selected_activate(self, *args): self._presenter.stop_polling_current_feed() def _on_menu_mark_all_as_read_activate(self, *args): self._presenter.mark_current_feed_as_read() def _remove_selected_feed(self, *args): title = self._presenter.get_selected_feed().title response = dialogs.confirm_delete(_("Delete %s?") % title, _("Deleting %s will remove it from your subscription list.") % title) if (response == gtk.RESPONSE_OK): selection = self._widget.get_selection() self._presenter.remove_selected_feed() return def _sort_ascending(self, *args): self._presenter.sort_category() def _sort_descending(self, *args): self._presenter.sort_category(reverse=True) def _display_properties_feed(self, *args): self._presenter.show_feed_properties() return def _feed_selection_changed(self, selection, column): """ Called when the current feed selection changed """ model, rowiter = selection.get_selected() if not rowiter: return adapter = model.get_value(rowiter, column) if adapter: self._presenter.feed_selection_changed(adapter.feed) return def _on_feed_selection_treeview_row_expanded(self, widget,iter,path,*data): obj = self._model[path][Column.OBJECT] self._presenter.expand_row(obj) def _on_feed_selection_treeview_row_collapsed(self, widget,iter,path,*data): obj = self._model[path][Column.OBJECT] self._presenter.collapse_row(obj) def set_cursor(self, treeiter, col_id=None, edit=False): path = self._model.get_path(treeiter) if col_id: column = self._widget.get_column(col_id) else: column = None self._widget.set_cursor(path, column, edit) self._widget.scroll_to_cell(path, column) self._widget.grab_focus() return def queue_draw(self): self._widget.queue_draw() return def get_location(self): model, iter = self._widget.get_selection().get_selected() if iter is None: return (None, None) path = model.get_path(iter) return self.get_parent_with_path(FeedList.get_instance(), path) class FeedsPresenter(MVP.BasicPresenter): def _initialize(self): self.initialize_slots(Event.FeedSelectionChangedSignal, Event.FeedsEmptySignal) model = gtk.TreeStore( gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_INT, gobject.TYPE_BOOLEAN, gobject.TYPE_STRING, gobject.TYPE_PYOBJECT, gobject.TYPE_BOOLEAN) self.model = model self._init_signals() self._curr_feed = None self._curr_category = None return def _init_signals(self): flist = FeedList.get_instance() flist.signal_connect(Event.ItemReadSignal, self._feed_item_read) flist.signal_connect(Event.AllItemsReadSignal, self._feed_all_items_read) flist.signal_connect(Event.FeedsChangedSignal, self._feeds_changed) flist.signal_connect(Event.FeedDetailChangedSignal, self._feed_detail_changed) fclist = FeedCategoryList.get_instance() fclist.signal_connect(Event.FeedCategorySortedSignal, self._feeds_sorted_cb) fclist.signal_connect(Event.FeedCategoryChangedSignal, self._fcategory_changed_cb) def _sort_func(self, model, a, b): """ Sorts the feeds lexically. From the gtk.TreeSortable.set_sort_func doc: The comparison callback should return -1 if the iter1 row should come before the iter2 row, 0 if the rows are equal, or 1 if the iter1 row should come after the iter2 row. """ retval = 0 fa = model.get_value(a, Column.OBJECT) fb = model.get_value(b, Column.OBJECT) if fa and fb: retval = locale.strcoll(fa.title, fb.title) elif fa is not None: retval = -1 elif fb is not None: retval = 1 return retval def poll_current_feed(self): config = Config.get_instance() poll = False if config.offline: response = dialogs.report_offline_status() if response == gtk.RESPONSE_OK: config.offline = not config.offline poll = True else: poll = True if poll: PollManager.get_instance().poll([self._curr_feed]) return def get_selected_feed(self): return self._curr_feed def stop_polling_current_feed(self): self._curr_feed.router.stop_polling() def mark_current_feed_as_read(self): self._curr_feed.mark_all_read() def remove_selected_feed(self): selection = self._view.get_selection() model, rowiter = selection.get_selected() if rowiter: adapter = model.get_value(rowiter, Column.OBJECT) it = model.remove(rowiter) feedlist = FeedList.get_instance() idx = feedlist.index(adapter.feed) del feedlist[idx] return def sort_category(self, reverse=False): self._curr_category.sort() if reverse: self._curr_category.reverse() return def show_feed_properties(self): fpd = FeedPropertiesDialog.show_feed_properties(None, self._curr_feed) return def select_first_feed(self): treeiter = self._model.get_iter_first() if not treeiter or not self._model.iter_is_valid(treeiter): return False self._view.set_cursor(treeiter) return True def select_next_feed(self): """ Scrolls to the next feed in the feed list """ selection = self._view.get_selection() model, treeiter = selection.get_selected() if not treeiter: treeiter = model.get_iter_first() next_feed_iter = model.iter_next(treeiter) if not next_feed_iter or not self._model.iter_is_valid(next_feed_iter): return False self._view.set_cursor(next_feed_iter) return True def select_previous_feed(self): """ Scrolls to the previous feeds in the feed list. """ selection = self._view.get_selection() model, treeiter = selection.get_selected() if not treeiter: treeiter = model.get_iter_first() path = model.get_path(treeiter) # check if there's a feed in the path if not path: return False prev_path = path[-1]-1 if prev_path < 0: # go to the last feed in the list prev_path = len(self._model) - 1 self._view.set_cursor(self._model.get_iter(prev_path)) return True def select_next_unread_feed(self): has_unread = False mark_treerow = 1 treerow = self._model[0] selection = self._view.get_selection() srow = selection.get_selected() if srow: model, treeiter = srow nextiter = model.iter_next(treeiter) if nextiter: treerow = self._model[model.get_path(nextiter)] while(treerow): feedrow = treerow[Column.OBJECT] if feedrow.feed.n_items_unread: self._view.set_cursor(treerow.iter) has_unread = True break treerow = treerow.next if not treerow and mark_treerow: # should only do this once. mark_treerow = treerow treerow = self._model[0] return has_unread def feed_selection_changed(self, feed): """ Called when the current feed selection was changed. This is called everytime a 'changed' signal in the treeview occurs """ oldfeed = self._curr_feed if feed: self._curr_feed = feed else: self._curr_feed = None if oldfeed is not self._curr_feed: self.emit_signal(Event.FeedSelectionChangedSignal(self,oldfeed,self._curr_feed)) return def display_feed(self, feed): # set_cursor will emit a 'changed' event in the treeview # and then feed_selection_changed above will be called. path = self._curr_category.index_feed(feed) treeiter = self._model.get_iter(path) self._view.set_cursor(treeiter, Column.NAME) def display_category_feeds(self, category): self._curr_category = category feeds = self._curr_category.feeds self._model.foreach(self._disconnect) self._model.clear() curr_feed_iter = self._display_feeds(feeds) if curr_feed_iter: self._view.set_cursor(curr_feed_iter) else: it = self._model.get_iter_first() if it: self._view.set_cursor(it) else: self.emit_signal(Event.FeedsEmptySignal(self)) return def _display_feeds(self, feeds, parent=None): def _connect_adapter(adapter, feedindex): adapter.signal_connect(Event.ItemsAddedSignal, self._adapter_updated_handler, feedindex) adapter.signal_connect(Event.FeedPolledSignal, self._adapter_updated_handler, feedindex) adapter.signal_connect(Event.FeedStatusChangedSignal, self._adapter_updated_handler, feedindex) adapter.signal_connect(Event.ItemReadSignal, self._adapter_updated_handler, feedindex) adapter.signal_connect(Event.AllItemsReadSignal, self._adapter_updated_handler, feedindex) curr_feed_iter = None for f in feeds: adapter = create_adapter(f) rowiter = self._model.append(parent) self._model.set(rowiter, Column.NAME, adapter.title, Column.OBJECT, adapter) idx = self._curr_category.index_feed(adapter.feed) _connect_adapter(adapter, idx) # we need this to get the rest of the data new_string, new = adapter.unread weight = (pango.WEIGHT_NORMAL, pango.WEIGHT_BOLD)[new > 0] status, pixbuf = adapter.status_icon self._model.set(rowiter, Column.UNREAD, new_string, Column.BOLD, weight, Column.STATUS_FLAG, status, Column.STATUS_PIXBUF, pixbuf, Column.ALLOW_CHILDREN, False) if adapter.feed is self._curr_feed: curr_feed_iter = rowiter return curr_feed_iter def display_empty_category(self): self._model.foreach(self._disconnect) self._model.clear() return def _disconnect(self, model, path, iter, user_data=None): ob = model[path][Column.OBJECT] self._disconnect_adapter(ob) if not len(model): return True return False def _disconnect_adapter(self, adapter): adapter.disconnect() adapter.signal_disconnect(Event.ItemsAddedSignal, self._adapter_updated_handler) adapter.signal_disconnect(Event.FeedPolledSignal, self._adapter_updated_handler) adapter.signal_disconnect(Event.FeedStatusChangedSignal, self._adapter_updated_handler) del adapter def _adapter_updated_handler(self, signal, feed_index): self._update_adapter_view(signal.sender, feed_index) def _update_adapter_view(self, adapter, feed_index): new = adapter.unread row = self._model[feed_index] row[Column.NAME] = adapter.title row[Column.UNREAD] = new[0] row[Column.BOLD] = (pango.WEIGHT_NORMAL, pango.WEIGHT_BOLD)[new[1] > 0] row[Column.OBJECT] = adapter status, pixbuf = adapter.status_icon row[Column.STATUS_FLAG] = status if pixbuf: row[Column.STATUS_PIXBUF] = pixbuf self._view.queue_draw() def _feeds_changed(self, signal): self._feed_view_update(signal.feed) def _feed_detail_changed(self, signal): self._feed_view_update(signal.sender) def _feed_item_read(self, signal): path = (0,) selection = self._view.get_selection() selected_row = selection.get_selected() if selected_row: model, treeiter = selected_row path = model.get_path(treeiter) treerow = self._model[path] adapter = treerow[Column.OBJECT] self._update_adapter_view(adapter, path) def _feed_all_items_read(self, signal): pass def _feed_view_update(self, feed): for index, f in enumerate(self._model): adapter = self._model[index][Column.OBJECT] if adapter.feed is feed: self._update_adapter_view(adapter, index) break return def _feeds_sorted_cb(self, signal): self.model.set_sort_func(Column.NAME, self._sort_func) if not signal.descending: self.model.set_sort_column_id(Column.NAME, gtk.SORT_ASCENDING) else: self.model.set_sort_column_id(Column.NAME, gtk.SORT_DESCENDING) self.model.sort_column_changed() return def _fcategory_changed_cb(self,signal): if signal.sender is self._curr_category: self._curr_category = signal.sender self.display_category_feeds(self._curr_category) return def expand_row(self, obj): obj.open = True def collapse_row(self, obj): obj.open = False def move_feed(self, sidx, tidx): self._curr_category.move_feed(sidx, tidx) return class DisplayAdapter(object, Event.SignalEmitter): """ View adapter for feeds and categories """ def __init__(self, ob): self._ob = ob Event.SignalEmitter.__init__(self) self.initialize_slots(Event.ItemReadSignal, Event.ItemsAddedSignal, Event.AllItemsReadSignal, Event.FeedPolledSignal, Event.FeedStatusChangedSignal) def equals(self, ob): return self._ob is ob class FeedDisplayAdapter(DisplayAdapter): """Adapter for displaying Feed objects in the tree""" def __init__(self, ob): DisplayAdapter.__init__(self, ob) ob.signal_connect(Event.ItemReadSignal, self.resend_signal) ob.signal_connect(Event.ItemsAddedSignal, self.resend_signal) ob.signal_connect(Event.AllItemsReadSignal, self.resend_signal) ob.signal_connect(Event.FeedPolledSignal, self.resend_signal) ob.signal_connect(Event.FeedStatusChangedSignal, self.resend_signal) def disconnect(self): self._ob.signal_disconnect( Event.ItemReadSignal, self.resend_signal) self._ob.signal_disconnect( Event.ItemsAddedSignal, self.resend_signal) self._ob.signal_disconnect( Event.AllItemsReadSignal, self.resend_signal) self._ob.signal_disconnect( Event.FeedPolledSignal, self.resend_signal) self._ob.signal_disconnect( Event.FeedStatusChangedSignal, self.resend_signal) def resend_signal(self, signal): new = copy.copy(signal) new.sender = self self.emit_signal(new) @property def title(self): return self._ob.title @property def unread(self): nu = self._ob.n_items_unread if nu != 0: return ("%s" % nu, nu) else: return ("", nu) @property def status_icon(self): if self._ob.process_status is not Feed.Feed.STATUS_IDLE: return (1, gtk.STOCK_EXECUTE) elif self._ob.error: return (1, gtk.STOCK_DIALOG_ERROR) return (0, None) @property def feed(self): return self._ob contents = property(lambda x: None) open = property(lambda x: None) def create_adapter(ob): if isinstance(ob, Feed.Feed): return FeedDisplayAdapter(ob)