""" 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