""" PreferencesDialog.py

Module setting user preferences.
"""
__copyright__ = "Copyright (c) 2002-2005 Free Software Foundation, Inc."
__license__ = """ GNU General Public 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 pygtk
pygtk.require('2.0')
import gobject
import gtk
import pango
from gtk import glade
import locale
import error
import MVP
from weakref import WeakKeyDictionary
import Event
import FeedCategoryList
import FeedList
import Config
import ValueMonitor

class CategoryListView(MVP.WidgetView):
    """View object for the category tree view"""
    COLUMN_TITLE = 0
    COLUMN_OBJ = 1
    COLUMN_BOLD = 2
    COLUMN_EDITABLE = 3

    def _initialize(self):
        treemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_PYOBJECT,
                                  gobject.TYPE_INT, gobject.TYPE_BOOLEAN)
        treemodel.set_sort_func(self.COLUMN_TITLE, self._sort_feeds)
        treemodel.set_sort_column_id(self.COLUMN_TITLE, gtk.SORT_ASCENDING)
        self._widget.set_model(treemodel)
        self._widget.set_rules_hint(False)
        self._create_columns()
        self._widget.get_selection().connect(
            'changed', self._selection_changed, self.COLUMN_OBJ)

    def _sort_feeds(self, model, a, b):
        """
        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
        feed_a = model.get_value(a, self.COLUMN_OBJ)
        feed_b = model.get_value(b, self.COLUMN_OBJ)

        if feed_a in self._model.pseudo_categories:
            retval = -1
        elif feed_b in self._model.pseudo_categories:
            retval = 1
        elif feed_a is not None and feed_b is not None:
            retval = locale.strcoll(feed_a.title, feed_b.title)
        elif feed_a is not None: retval = -1
        elif feed_b is not None: retval = 1
        return retval

    def _model_set(self):
        self._model.signal_connect(Event.FeedCategoryAddedSignal,
                                   self._category_added)
        self._model.signal_connect(Event.FeedCategoryRemovedSignal,
                                   self._category_removed)
        self._model.signal_connect(Event.FeedCategoryChangedSignal,
                                   self._category_changed)
        self._model.signal_connect(Event.FeedCategoryListLoadedSignal,
                                   self._categories_loaded)
        self._populate_treemodel()

    def _create_columns(self):
        renderer = gtk.CellRendererText()
        renderer.connect('edited', self._cell_edited)
        column = gtk.TreeViewColumn(_('_Category'), renderer,
                                    text=self.COLUMN_TITLE,
                                    weight=self.COLUMN_BOLD,
                                    editable=self.COLUMN_EDITABLE)
        self._widget.append_column(column)

    def _populate_treemodel(self):
        treemodel = self._widget.get_model()
        treemodel.clear()
        for category in self._model.pseudo_categories:
            iter = treemodel.append()
            treemodel.set(iter, self.COLUMN_TITLE, category.title,
                          self.COLUMN_OBJ, category,
                          self.COLUMN_BOLD, pango.WEIGHT_BOLD,
                          self.COLUMN_EDITABLE, False)
        for category in self._model.user_categories:
            iter = treemodel.append()
            treemodel.set(iter, self.COLUMN_TITLE, category.title,
                          self.COLUMN_OBJ, category,
                          self.COLUMN_BOLD, pango.WEIGHT_NORMAL,
                          self.COLUMN_EDITABLE, True)

    def _iterate_model_conditionally(self, func):
        treemodel = self._widget.get_model()
        iter = treemodel.get_iter_first()
        while iter is not None:
            if func(treemodel, iter): break
            iter = treemodel.iter_next(iter)

    def _add_category(self, category):
        self._populate_treemodel()
        def edit_row(treemodel, iter):
            if treemodel.get_value(iter, self.COLUMN_OBJ) is category:
                column = self._widget.get_column(self.COLUMN_TITLE)
                path = treemodel.get_path(iter)
                self._widget.set_cursor(path, column, True)
                return True
        self._iterate_model_conditionally(edit_row)

    def _remove_category(self, category):
        def remove_row(treemodel, iter):
            if treemodel.get_value(iter, self.COLUMN_OBJ) is category:
                column = self._widget.get_column(self.COLUMN_TITLE)
                iterpath = treemodel.get_path(iter)
                isIter = treemodel.remove(iter)
                if not isIter:
                    current = iterpath[:-1] + (iterpath[-1]-1,)
                else:
                    current = treemodel.get_path(iter)
                self._widget.set_cursor(current, column)
                return True
            return False
        self._widget.grab_focus()
        self._iterate_model_conditionally(remove_row)

    def _update_category_name(self, category):
        def update_title(treemodel, iter):
            if treemodel.get_value(iter, self.COLUMN_OBJ) is category:
                treemodel.set(iter, self.COLUMN_TITLE, category.title)
                return True
        self._iterate_model_conditionally(update_title)

    def _category_added(self, signal):
        self._add_category(signal.category)

    def _category_removed(self, signal):
        self._remove_category(signal.category)

    def _category_changed(self, signal):
        self._update_category_name(signal.sender)

    def _categories_loaded(self, signal):
        self._populate_treemodel()

    def _selection_changed(self, selection, *args):
        if self._presenter is None:
            error.log("presenter not set!")
            return
        model, iter = selection.get_selected()
        if iter is None:
            return
        category = model.get_value(iter, self.COLUMN_OBJ)
        self._presenter.category_changed(category)

    def _cell_edited(self, cell, path_string, text):
        treemodel = self._widget.get_model()
        iter = treemodel.get_iter_from_string(path_string)
        if not iter: return
        category = treemodel.get_value(iter, self.COLUMN_OBJ)
        self._presenter.category_title_edited(category, text)

class CategorySelectionChangedSignal(Event.BaseSignal):
    def __init__(self, sender, category):
        Event.BaseSignal.__init__(self, sender)
        self.category = category

class CategoryListPresenter(MVP.BasicPresenter):
    """Presenter object for the category tree view"""
    def _initialize(self):
        self.initialize_slots(CategorySelectionChangedSignal)
        self._selected_category = None

    def category_changed(self, category):
        self._selected_category = category
        self.emit_signal(CategorySelectionChangedSignal(self, category))

    def category_title_edited(self, category, title):
        category.title = title

    def add_category(self):
        cat = FeedCategoryList.FeedCategory(_("Category name"))
        self._model.add_category(cat)

    def remove_category(self):
        if self._selected_category is not None:
            self._model.remove_category(self._selected_category)

class CategoryAddView(MVP.WidgetView):
    """View object for the add category button"""
    def _on_category_add_button_clicked(self, *args):
        if self._presenter is not None:
            self._presenter.add_category()

class AddCategorySignal(Event.BaseSignal):
    pass

class CategoryAddPresenter(MVP.BasicPresenter):
    """Presenter object for the add category button"""
    def _initialize(self):
        self.initialize_slots(AddCategorySignal)

    def add_category(self):
        self.emit_signal(AddCategorySignal(self))

class CategoryRemoveView(MVP.WidgetView):
    """View object for the remove category button"""
    def _on_category_delete_button_clicked(self, *args):
        if self._presenter is not None:
            self._presenter.remove_category()

class RemoveCategorySignal(Event.BaseSignal):
    pass

class CategoryRemovePresenter(MVP.BasicPresenter):
    """Presenter object for the remove category button"""
    def _initialize(self):
        self.initialize_slots(RemoveCategorySignal)

    def remove_category(self):
        self.emit_signal(RemoveCategorySignal(self))

class CategoriesPresenter(object, Event.SignalEmitter):
    """Presenter object that combines the category list presenter and
    category add button presenter"""
    def __init__(self, lister, adder, remover):
        Event.SignalEmitter.__init__(self)
        self.initialize_slots(CategorySelectionChangedSignal)

        lister.signal_connect(CategorySelectionChangedSignal,
                              lambda s: self.emit_signal(s))
        adder.signal_connect(AddCategorySignal, self._add_category)
        remover.signal_connect(RemoveCategorySignal, self._remove_category)
        self._lister = lister
        self._adder = adder
        self._remover = remover

    def _add_category(self, signal):
        self._lister.add_category()

    def _remove_category(self, signal):
        self._lister.remove_category()
    
class FeedListView(MVP.WidgetView):
    """View object for the feed list"""
    COLUMN_TITLE = 0
    COLUMN_OBJ = 1
    COLUMN_CHECKED = 2
    COLUMN_TOGGLABLE = 3

    def _initialize(self):
        treemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_PYOBJECT,
                                  gobject.TYPE_BOOLEAN, gobject.TYPE_BOOLEAN)
        treemodel.set_sort_func(self.COLUMN_TITLE, self._sort_feeds)
        treemodel.set_sort_column_id(self.COLUMN_TITLE, gtk.SORT_ASCENDING)
        self._widget.set_model(treemodel)
        self._widget.set_rules_hint(False)
        self._create_columns()

        self._category = None
        self._updating_display = False

    def _model_set(self):
        self._model.signal_connect(Event.FeedsChangedSignal,
                                   self._model_updated)
        fclist = FeedCategoryList.get_instance()
        self._populate_treemodel()

    def _create_columns(self):
        renderer = gtk.CellRendererToggle()
        renderer.connect('toggled', self._cell_toggled)
        column = gtk.TreeViewColumn(_('_Member'), renderer,
                                    active=self.COLUMN_CHECKED,
                                    activatable=self.COLUMN_TOGGLABLE)
        column.set_sort_column_id(self.COLUMN_CHECKED)
        self._widget.append_column(column)
        renderer = gtk.CellRendererText()
        column = gtk.TreeViewColumn(_('_Feed'), renderer,
                                    text=self.COLUMN_TITLE)
        column.set_sort_column_id(self.COLUMN_TITLE)
        self._widget.append_column(column)

    def _populate_treemodel(self):
        treemodel = self._widget.get_model()
        treemodel.clear()
        feedlist = FeedList.get_instance()
        if self._category is None: return
        category_feeds = self._category.feeds
        enabletoggle = not isinstance(
            self._category, FeedCategoryList.PseudoCategory)
        for feed in feedlist:
            iter = treemodel.append()
            treemodel.set(iter, self.COLUMN_TITLE, feed.title,
                          self.COLUMN_OBJ, feed,
                          self.COLUMN_CHECKED, feed in category_feeds,
                          self.COLUMN_TOGGLABLE, enabletoggle)

    def _sort_feeds(self, model, a, b):
        feed_a = model.get_value(a, self.COLUMN_OBJ)
        feed_b = model.get_value(b, self.COLUMN_OBJ)

        if feed_a and feed_b :
            return locale.strcoll(feed_a.title.lower(), feed_b.title.lower())
        elif feed_a is not None: return -1
        elif feed_b is not None: return 1
        else:
            return 0

    def _model_updated(self, signal):
        self._populate_treemodel()

    def category_selected(self, category):
        self._category = category
        self._populate_treemodel()
        return

    def _cell_toggled(self, cell, path_string):
        if self._updating_display:
            return
        treemodel = self._widget.get_model()
        treeiter = treemodel.get_iter_from_string(path_string)
        if not treeiter: return
        feed = treemodel.get_value(treeiter, self.COLUMN_OBJ)
        self._presenter.feed_toggled(feed)
        self._populate_treemodel()

class FeedListPresenter(MVP.BasicPresenter):
    """Presenter object for the feed list"""
    def feed_toggled(self, feed):
        if self._category is None:
            return
        if feed in self._category.feeds:
            self._category.remove_feed(feed)
        else:
            self._category.append_feed(feed, False)

    def category_selected(self, category):
        self._category = category
        if self._view is not None:
            self._view.category_selected(category)

class CategoriesTab:
    def __init__(self, xml):
        self._selcategory = None # currently selected category

        self._delete_button = xml.get_widget('category_delete_button')
        self._delete_button.set_sensitive(False)

        self._selcategory = None

        clpresenter = CategoryListPresenter(
            model = FeedCategoryList.get_instance(),
            view = CategoryListView(xml.get_widget('category_treeview')))
        capresenter = CategoryAddPresenter(
            view = CategoryAddView(xml.get_widget('category_add_button')))
        crpresenter = CategoryRemovePresenter(
            view = CategoryRemoveView(
            xml.get_widget('category_delete_button')))

        self._categories_presenter = CategoriesPresenter(
            clpresenter, capresenter, crpresenter)
        self._categories_presenter.signal_connect(
            CategorySelectionChangedSignal, self._category_changed)

        feedmodel = FeedList.get_instance()
        self._feeds_presenter = FeedListPresenter()
        self._feeds_presenter.model = feedmodel
        self._feeds_presenter.view = FeedListView(
            xml.get_widget('feeds_treeview'))
        
        self._source_field = xml.get_widget(
            'category_external_source')
        self._source_username_field = xml.get_widget(
            'category_external_source_username')
        self._source_password_field = xml.get_widget(
            'category_external_source_password')
        self._source_frequency_spin = xml.get_widget(
            'category_external_source_frequency_spin')
        self._source_frequency_default_check = xml.get_widget(
            'category_refresh_default_check')
        
        sizegroup = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
        for l in [xml.get_widget(w) for w in (
            'category_external_source_label',
            'category_external_source_username_label',
            'category_external_source_password_label',
            'category_external_source_frequency_label')]:
            sizegroup.add_widget(l)
        
        self._category_source_monitors = CategorySourceMonitors()

        nameFuncMap = {}
        for key in dir(self.__class__):
            if key[:4] == '_on_':
                nameFuncMap[key[1:]] = getattr(self, key)
        xml.signal_autoconnect(nameFuncMap)

        self._switching_categories = False

    def _setup_category_source_monitors(self, oldcat, newcat):
        self._category_source_monitors.disconnect(oldcat)
        
        subscription_set = newcat.subscription is not None
        is_pseudo = isinstance(newcat, FeedCategoryList.PseudoCategory)
        default_refresh = (
            (not subscription_set) or
            (newcat.subscription.frequency ==
             newcat.subscription.REFRESH_DEFAULT))
        attrfields = (
            ((subscription_set and newcat.subscription.location) or "",
             self._source_field),
            ((subscription_set and newcat.subscription.username) or "",
             self._source_username_field),
            ((subscription_set and newcat.subscription.password) or "",
             self._source_password_field))

        for f, w in attrfields:
            w.set_text(f)
            w.set_sensitive(not is_pseudo)
        if default_refresh:
            self._source_frequency_spin.set_value(
                Config.get_instance().poll_frequency / 60)
        else:
            self._source_frequency_spin.set_value(
                (subscription_set and newcat.subscription.frequency / 60) or 0)
        
        self._source_frequency_spin.set_sensitive(
            (not is_pseudo) and (not default_refresh))
        self._source_frequency_default_check.set_active(
            (not is_pseudo) and default_refresh)

        self._source_frequency_default_check.set_sensitive(not is_pseudo)

        def save_location(value, widget): newcat.subscription.location = value
        def save_username(value, widget): newcat.subscription.username = value
        def save_password(value, widget): newcat.subscription.password = value
        def save_frequency(value, widget):
            newcat.subscription.frequency = value * 60

        def create_sub_ensurer(saver):
            def ensure_and_save(value, widget):
                if newcat.subscription is None:
                    newcat.subscription = FeedCategoryList.OPMLCategorySubscription()
                saver(value, widget)
            return ensure_and_save
        
        if not self._category_source_monitors.has_key(newcat):
            monitors = dict(
                [(key, ValueMonitor.create_for_gtkentry(
                v, 5000, create_sub_ensurer(f)))
                for key, v, f in (("location", self._source_field,
                                   save_location),
                                  ("username", self._source_username_field,
                                   save_username),
                                  ("password", self._source_password_field,
                                   save_password))])
            monitors.update(
                {"frequency": ValueMonitor.create_for_gtkspin(
                self._source_frequency_spin, 5000,
                create_sub_ensurer(save_frequency))})
            self._category_source_monitors[newcat] = monitors

        self._category_source_monitors.connect(newcat)

    def _category_changed(self, signal):
        self._switching_categories = True
        category = signal.category
        self._setup_category_source_monitors(
            self._selcategory, category)
        self._selcategory = category

        if isinstance(category, FeedCategoryList.PseudoCategory):
            self._delete_button.set_sensitive(False)
        else:
            self._delete_button.set_sensitive(True)
        self._feeds_presenter.category_selected(category)
        self._switching_categories = False
        return

    def _on_category_sort_ascending_button_clicked(self, widget):
        self._selcategory.sort()

    def _on_category_sort_descending_button_clicked(self, widget):
        self._selcategory.sort()
        self._selcategory.reverse()

    def _on_category_refresh_default_check_toggled(self, widget):
        if self._switching_categories:
            return
        default_frequency = Config.get_instance().poll_frequency / 60
        if self._selcategory.subscription is None:
            self._selcategory.subscription = FeedCategoryList.OPMLCategorySubscription()
        if widget.get_active():
            self._source_frequency_spin.set_value(default_frequency)
            self._source_frequency_spin.set_sensitive(False)
            # flush the monitor so we don't get pending updates to the
            # frequency overriding this change
            self._category_source_monitors.flush_monitor(
                self._selcategory, "frequency")
            self._selcategory.subscription.frequency = self._selcategory.subscription.REFRESH_DEFAULT
        else:
            self._source_frequency_spin.set_sensitive(True)
            self._selcategory.subscription.frequency = default_frequency / 60


class PreferencesDialog:
    SEC_PER_MINUTE = 60

    def __init__(self, xml, parent):
        config = Config.get_instance()
        self._window = xml.get_widget('preferences_dialog')
        self._window.set_transient_for(parent)
        self._browser_override = xml.get_widget("prefs_browser_setting")
        self._browser_entry = xml.get_widget("prefs_browser_setting_entry")

        # General
        config = Config.get_instance()
        poll_frequency = int(config.poll_frequency/self.SEC_PER_MINUTE)
        items_stored = int(config.number_of_items_stored)

        browser_cmd = str (config.browser_cmd)
        if browser_cmd:
            self._browser_override.set_active(True)
            self._browser_entry.set_text (browser_cmd)
            self._browser_entry.set_sensitive (True)
        else:
            self._browser_entry.set_text (_("Using desktop setting"))

        xml.get_widget('poll_frequency_spin').set_value(poll_frequency)
        xml.get_widget('number_of_items_spin').set_value(items_stored)
        xml.get_widget(['item_order_oldest',
                        'item_order_newest'][config.item_order]).set_active(1)

        nameFuncMap = {}
        for key in dir(self.__class__):
            if key[:3] == 'on_':
                nameFuncMap[key] = getattr(self, key)
        xml.signal_autoconnect(nameFuncMap)

        self._categories_tab = CategoriesTab(glade.get_widget_tree(
            xml.get_widget('preferences_categories_tab')))

    def show(self, *args):
        self._window.present()

    def hide(self, *args):
        self._window.hide()

    def on_preferences_dialog_delete_event(self, *args):
        self.hide()
        return True

    def on_preferences_close_button_clicked(self, button):
        self.hide()
        return

    def on_poll_frequency_spin_value_changed(self, spin):
        Config.get_instance().poll_frequency = int(spin.get_value() * self.SEC_PER_MINUTE)

    def on_number_of_items_spin_value_changed(self, spin):
        Config.get_instance().number_of_items_stored = int(spin.get_value())

    def on_item_order_newest_toggled(self, radio):
        config = Config.get_instance()
        config.item_order = not config.item_order

    def on_item_order_oldest_toggled(self, radio):
        pass

    def on_prefs_browser_setting_toggled(self, button):
        active = button.get_active()
        if not active:
            config = Config.get_instance()
            config.browser_cmd = ""
        self._browser_entry.set_sensitive(active)

    def on_prefs_browser_setting_entry_focus_out_event(self, entry, *args):
        config = Config.get_instance()
        config.browser_cmd = entry.get_text()

class CategorySourceMonitors(WeakKeyDictionary):
    def disconnect(self, category):
        if self.has_key(category):
            for m in self[category].values():
                m.disconnect()

    def connect(self, category):
        if self.has_key(category):
            for m in self[category].values():
                m.connect()

    def flush_monitor(self, category, monitor):
        if self.has_key(category) and self[category].has_key(monitor):
            self[category][monitor].flush()
