""" 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 = """
<ui>
<popup name=\"feed_list_popup\">
<menuitem action=\"refresh\"/>
<menuitem action=\"mark_as_read\"/>
<menuitem action=\"stop_refresh\"/>
<menuitem action=\"remove\"/>
<separator/>
<menu name=\"Sort\" action=\"category-sort\">
<menuitem action=\"ascending\"/>
<menuitem action=\"descending\"/>
</menu>
<menuitem action=\"properties\"/>
</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)
syntax highlighted by Code2HTML, v. 0.9.1