#!/usr/bin/env python3
"""
Module MIXINS -- Mixin Classes for GUI
Sub-Package GUI of Package PLIB3 -- Python GUI Framework
Copyright (C) 2008-2013 by Peter A. Donis

Released under the GNU General Public License, Version 2
See the LICENSE and README files for more information

Defines mixin classes to customize behavior of GUI objects.
The following classes are included:

- PTableMixin: combines PEditor with table-type widget.

- PTreeMixin: combines PEditor with tree-type widget.

- PTextMixin: combines PEditor or PFileEditor with text-editing
  widget.

- PPanelMixin: combines panel widget with automated generation
  of sub-widgets based on a list of widget specs (each spec
  is a 4-tuple as described in the class docstring). The specs
  can be generated by hand, but it is usually easier to use
  the pre-built convenience functions in the ``plib.gui.specs``
  module.

- PTabMixin, PGroupMixin: give tab and groupbox widgets the
  ability to auto-construct sub-widgets based on specs.

- PStatusMixin: allows status bar widget to auto-construct its
  child widgets based on specs.
"""

import types

from plib.gui.defs import *
from plib.gui import main


# Mixin classes to customize editor behavior for various types of widgets;
# note that these should appear *before* the editor class in the list of
# base classes.

class PTableMixin(object):
    """ Mixin class for editor with table widget. """
    
    _data_loaded = False
    
    def cellselected(self, row, col):
        """Call event handler for cell selection.
        """
        self._on_cellselected(row, col)
    
    def _on_cellselected(self, row, col):
        """Hook to take other actions when cell is selected.
        """
        pass
    
    def tablechanged(self, row, col):
        """Update data store with changed value from table cell.
        """
        if not self.control[row]._updating:
            self.data[row][col] = self.control[row][col]
            self.modified = True
            self._on_tablechanged(row, col)
    
    def _on_tablechanged(self, row, col):
        """Hook to take other actions when table is changed.
        """
        pass
        
        # TODO: Add this to PTableEditor?
    
    def _helperadd(self, index, value):
        """Update data store with new row's value
         
        Note: only update if data is already loaded.
        """
        
        if self._data_loaded:
            self.data.insert(index, value)
        super(PTableMixin, self)._helperadd(index, value)
    
    def _helperdel(self, index, item):
        """Remove deleted item from data store.
        """
        super(PTableMixin, self)._helperdel(index, item)
        if self._data_loaded:
            del self.data[index]
    
    def _connect_control(self):
        """Connect to table changed signal.
        """
        self.control.setup_notify(SIGNAL_CELLSELECTED, self.cellselected)
        self.control.setup_notify(SIGNAL_TABLECHANGED, self.tablechanged)
    
    def _doload(self):
        """Populate table widget from data.
        
        Assumes that data is a sequence of sequences; each top-level
        sequence item is a table row, each item within the row is data
        for a cell.
        """
        
        self.control.extend(self.data)
        # Since the widget wasn't initialized with data, we have to size
        # it here
        self.control.set_min_size(
            self.control.minwidth(), self.control.minheight())
        # Update the loaded field so further row adds/deletes will update
        # the data store
        self._data_loaded = True


class PTreeMixin(object):
    """Mixin class for editor with tree-type widget.
    """
    
    def _on_treeselected(self):
        """Override to take action on new item selection in tree.
        """
        pass
    
    def _connect_control(self):
        """Connect to list view changed signal.
        """
        self.control.setup_notify(SIGNAL_LISTSELECTED, self._on_treeselected)
    
    def _doload(self):
        """Populate tree widget from data.
        
        Assume that data is a sequence of 2-tuples, with the first item being
        a sequence of item strings (to allow for multiple columns), and the
        second item being a sub-sequence of 2-tuples for child tree nodes.
        """
        self.control.extend(self.data)


class PTextMixin(object):
    """Mixin class for editor with text control widget.
    """
    
    modifysignals = {SIGNAL_TEXTCHANGED: 'control'}
    
    def _gettype(self):
        return "Text"
    
    def _connect_mainwidget(self):
        super(PTextMixin, self)._connect_mainwidget()
        # connect editing actions
        self.mainwidget.connectaction(ACTION_EDITUNDO, self.editundo)
        self.mainwidget.connectaction(ACTION_EDITREDO, self.editredo)
        self.mainwidget.connectaction(ACTION_EDITCUT, self.editcut)
        self.mainwidget.connectaction(ACTION_EDITCOPY, self.editcopy)
        self.mainwidget.connectaction(ACTION_EDITPASTE, self.editpaste)
        self.mainwidget.connectaction(ACTION_EDITDELETE, self.editdelete)
        self.mainwidget.connectaction(ACTION_EDITSELECTALL, self.selectall)
    
    def _connect_control(self):
        """Connect to edit control state changed signal.
        """
        self.control.setup_notify(SIGNAL_TEXTSTATECHANGED,
                                  self._on_statechanged)
        self.statelist = [
            ([ACTION_EDITUNDO], self.control.can_undo),
            ([ACTION_EDITREDO], self.control.can_redo),
            ([ACTION_EDITPASTE], self.control.can_paste),
            (
                [ACTION_EDITCUT, ACTION_EDITCOPY, ACTION_EDITDELETE],
                self.control.can_clip
            )
        ]
        self._on_statechanged()
    
    def _on_statechanged(self):
        for keylist, flagfunc in self.statelist:
            for key in keylist:
                self.mainwidget._update_action(key, flagfunc())
    
    def editundo(self):
        self.control.undo_last()
    
    def editredo(self):
        self.control.redo_last()
    
    def editcut(self):
        self.control.cut_to_clipboard()
    
    def editcopy(self):
        self.control.copy_to_clipboard()
    
    def editpaste(self):
        self.control.paste_from_clipboard()
    
    def editdelete(self):
        self.control.delete_selected()
    
    def selectall(self):
        self.control.select_all()


class PTextFileMixin(PTextMixin):
    """Mixin class for file editor with text control widget.
    """
    
    def _gettype(self):
        return "Text File"
    
    def _donew(self):
        self.control.clear_edit()
    
    def _filedata_to_control(self, filedata):
        self.control.edit_text = filedata
    
    def _doload(self):
        filedata = ""
        f = open(self.filename, 'rU')
        try:
            filedata = f.read()
        finally:
            f.close()
        if filedata:
            self._filedata_to_control(filedata)
    
    def _filedata_from_control(self):
        return self.control.edit_text
    
    def _dosave(self):
        filedata = self._filedata_from_control()
        if filedata:
            f = open(self.filename, 'w')
            try:
                f.write(filedata)
            finally:
                f.close()


# Mixin classes to enable automatic GUI generation from specifications
# stored in Python lists, tuples, and dicts

class _PAutoBase(object):
    """Base class for GUI auto-generation.
    
    Base class for mixins that allow automatic construction of
    sub-widgets from specs.
    """
    
    def _widget_from_klass(self, klass, args, kwds, attrname):
        # Hack to allow patching in attribute name if needed
        if hasattr(klass, '_use_attrname') and klass._use_attrname:
            return klass(self, attrname, *args, **kwds)
        return klass(self, *args, **kwds)
    
    def widget_from_spec(self, spec, fn=None):
        """Construct widget from spec.
        
        Constructs widget from spec, putting self as first constructor
        argument (i.e., assumes that widget constructor takes 'parent' as
        first arg), and replacing ``target`` keyword argument, if present,
        with the actual callable (found by looking for a method on self,
        or in self's parent tree, with the given name).
        """
        
        klass, args, kwds, attrname = spec
        if 'target' in kwds:
            targetspec = kwds['target']
            if isinstance(targetspec, tuple):
                auto, targetname = targetspec
                assert auto == AUTO_TARGET  # specs should guarantee this
            else:
                targetname = targetspec
                auto = False
            if isinstance(targetname, str) and targetname:
                # Look up the method by name, starting with self and
                # walking the parent tree
                p = self
                target = None
                # FIXME: wtf do we need p is not None below? For some crazy
                # reason, a PAutoTabWidget instance registers as boolean False???
                while (p is not None) and not target:
                    try:
                        target = getattr(p, targetname)
                    except AttributeError:
                        p = getattr(p, '_parent', None)
                if not (target or auto):
                    raise ValueError(
                        "Could not find target method {} for widget {}".format(
                        targetname, attrname)
                    )
                kwds['target'] = target
        # This allows us to defer loading the actual module containing
        # the widget class until here (instead of forcing it to happen
        # when the specs module is imported)
        if isinstance(klass, types.FunctionType):
            klass = klass()
        result = self._widget_from_klass(klass, args, kwds, attrname)
        if fn is not None:
            fn(result, attrname)
        elif attrname is not None:
            setattr(self, attrname, result)
        return result


class PPanelMixin(_PAutoBase):
    """Mixin class for panel that auto-generates child widgets.
    
    Mixin class that provides an easier way to create child
    widgets for panels. The childlist class field is used
    to provide a list of 4-tuples (class, args, kwds, attrname)
    which is used to guide the creation of child widgets. The
    attrname field, if not None, tells what named attribute of
    the panel the created widget should be bound to.
    
    Note that this method requires that, if any of the child
    widgets are themselves panels, they will require their own
    classes to fill in their childlist fields. However, an
    alternative is provided: if the baseclass class field is
    filled in, then the 'class' member of the 4-tuples in
    childlist may instead be a sub-list of 4-tuples; the
    sub-list will then be used to create a panel class on
    the fly, subclassed from baseclass, and with the given
    list as its childlist. (As long as baseclass is derived
    from PPanelMixin, this process will work recursively.)
    
    As an added feature, this class overrides __getattr__ to
    allow attributes of child panels to appear as attributes
    of the parent panel. This is mainly to allow easier access
    to controls defined in child panels (so you don't have to
    remember at which level of nested panels you inserted a
    control -- just treat it as part of the main panel).
    """
    
    baseclass = None
    childlist = []
    panelclass = None
    
    def _widget_from_klass(self, klass, args, kwds, attrname):
        if not isinstance(klass, type):
            # klass is a sub-list, create a class on the fly
            klass = type(self.baseclass)(
                "{}_{}".format(self.__class__.__name__, attrname),
                (self.baseclass,),
                {'baseclass': self.baseclass, 'childlist': klass})
        return _PAutoBase._widget_from_klass(
            self, klass, args, kwds, attrname)
    
    def _createpanels(self):
        """Create child widgets listed in class field.
        """
        for spec in self.childlist:
            widget = self.widget_from_spec(spec)
            # Panel-type widgets add themselves, others we need to add here
            if not isinstance(widget, self.panelclass):
                self._addwidget(widget)
    
    def __getattr__(self, name):
        """Allow 'pass-through' access to attributes of child panels.
        """
        try:
            # This is in case we're mixing in with a class that also
            # overrides __getattr__
            result = super(PPanelMixin, self).__getattr__(name)
            return result
        except AttributeError:
            # Now loop through child panels
            for panel in self.__dict__['_panels']:
                try:
                    result = getattr(panel, name)
                    return result
                except AttributeError:
                    pass
        # If we get here, nothing was found
        raise AttributeError("{} object has no attribute '{}'".format(
                             self.__class__.__name__, name))


class _PAutoWithPanels(_PAutoBase):
    """Base class for auto-constructing widgets with parent panels.
    
    An auto-constructing class that groks having a ``PAutoPanel`` as a parent
    (it allows the parent panel to 'pass through' attribute access to this
    object's children) and having ``PAutoPanel`` specs in its spec list.
    """
    
    basename = 'with_panels'
    panelclass = None
    
    def _widget_from_klass(self, klass, args, kwds, attrname):
        if not isinstance(klass, type):
            # klass is panel contents, wrap panel around it.
            base = self.panelclass
            if isinstance(self._parent, self.panelclass):
                basename = self._parent.__class__.__name__
            else:
                basename = self.basename
            klass = type(base)("{}_{}".format(basename, attrname),
                               (base,), {'childlist': klass})
        return _PAutoBase._widget_from_klass(
            self, klass, args, kwds, attrname)
    
    def _process_control(self, control, attrname):
        if isinstance(self._parent, self.panelclass):
            if attrname is not None:
                # 'Pass-through' this control
                setattr(self._parent, attrname, control)
            if isinstance(control, self.panelclass):
                # So finding controls by attr name works
                self._parent._panels.append(control)
    
    def widget_from_spec(self, spec):
        return _PAutoBase.widget_from_spec(self, spec, self._process_control)


class PTabMixin(_PAutoWithPanels):
    """Mixin class for auto-generating tab widgets.
    
    Mixin class for tab widgets that 'expands' a list of panel specs
    into the actual panels and adds them as tabs.
    """
    
    basename = 'tab'
    
    def _createtabs(self, tabs):
        """Create tabs from specs.
        
        Assumes that tabs is a sequence of 2-tuples containing
        (title, panelspec) for each tab.
        """
        
        # FIXME: The extend method doesn't take an iterator? (if it did,
        # we could just pass a generator expression to extend here instead
        # of the for loop with append, but that doesn't seem to work)
        for title, spec in tabs:
            self.append((title, self.widget_from_spec(spec)))


class PGroupMixin(_PAutoWithPanels):
    """Mixin class for auto-generating group boxes.
    
    Mixin class to auto-construct group box controls based
    on specs list.
    """
    
    basename = 'group'
    
    def _process_control(self, control, attrname):
        _PAutoWithPanels._process_control(self, control, attrname)
        self._add_control(control)
        self._controls[attrname] = control
    
    def _init_controls(self, controls):
        """ Assume controls is a sequence of specs. """
        if self._controls is None:
            self._controls = {}
        for control in controls:
            self.widget_from_spec(control)


def _widget_helper(wclassname, args, kwds, wname):
    def f():
        w = getattr(main, wclassname)(*args, **kwds)
        return (wname, w)
    f.__name__ = '{}_{}'.format(wclassname, wname)
    return f


class PStatusMixin(object):
    """Mixin class for auto-generating status bar.
    
    Mixin class to allow easier specification of status bar widgets.
    The widgetspecs class field should be filled in with a list of
    4-tuples similar to those used by PPanelMixin above; the tuples
    should contain (classname, args, kwds, attrname), where classname
    is the widget class name, which is assumed to be findable as an
    attribute in the plib.gui.main module, args and kwds are positional
    and keyword arguments to be passed to the class constructor, and
    attrname is the attribute name on the status bar to be used to
    hold a reference to the widget.
    """
    
    widgetspecs = None
    
    def _init_widgets(self, widgets):
        # Override to allow reading widget specs from class field.
        if (widgets is None):
            if self.widgetspecs is not None:
                widgetspecs = self.widgetspecs
            elif self._mainwin is not None:
                widgetspecs = self._mainwin.widgetspecs
            else:
                widgetspecs = None
            if widgetspecs:
                widgets = [_widget_helper(*spec) for spec in widgetspecs]
        super(PStatusMixin, self)._init_widgets(self, widgets)
