Source code for guiqwt.widgets.base

# -*- coding: utf-8 -*-
#
# Copyright © 2012 CEA
# Pierre Raybaut
# Licensed under the terms of the CECILL License
# (see guiqwt/__init__.py for details)

"""
base
----

The `base` module provides base objects for internal use of the 
`guiqwt.widgets` package.

"""

from qtpy.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QTabWidget

from guidata.qthelpers import create_toolbutton
from guidata.configtools import get_icon

# Local imports
from guiqwt.config import _
from guiqwt.histogram import lut_range_threshold
from guiqwt.image import INTERP_LINEAR, TrImageItem
from guiqwt.plot import ImageWidget, ImageDialog


class BaseTransformMixin(object):
    """Base transform widget mixin class (for manipulating TrImageItem objects)

    This is to be mixed with a class providing the get_plot method,
    like ImageDialog, or BaseTransformWidget (see below)"""

    def __init__(self):
        self.item = None
        self.item_original_state = None
        self.item_original_crop = None
        self.item_original_transform = None
        self.output_array = None

    # ------Public API----------------------------------------------------------
    def add_reset_button(self, layout):
        """Add the standard reset button"""
        edit_options_btn = create_toolbutton(
            self,
            text=_("Reset"),
            icon=get_icon("eraser.png"),
            triggered=self.reset,
            autoraise=False,
        )
        layout.addWidget(edit_options_btn)
        layout.addStretch()

    def add_apply_button(self, layout):
        """Add the standard apply button"""
        apply_btn = create_toolbutton(
            self,
            text=_("Apply"),
            icon=get_icon("apply.png"),
            triggered=self.apply_transformation,
            autoraise=False,
        )
        layout.addWidget(apply_btn)
        layout.addStretch()

    def add_buttons_to_layout(self, layout):
        """Add tool buttons to layout"""
        self.add_reset_button(layout)
        self.add_apply_button(layout)

    def set_item(self, item):
        """Set associated item -- must be a TrImageItem object"""
        assert isinstance(item, TrImageItem)
        self.item = item
        self.item_original_state = (
            item.can_select(),
            item.can_move(),
            item.can_resize(),
            item.can_rotate(),
        )
        self.item_original_crop = item.get_crop()
        self.item_original_transform = item.get_transform()

        self.item.set_selectable(True)
        self.item.set_movable(True)
        self.item.set_resizable(False)
        self.item.set_rotatable(True)

        item.set_lut_range(lut_range_threshold(item, 256, 2.0))
        item.set_interpolation(INTERP_LINEAR)
        plot = self.get_plot()
        plot.add_item(self.item)

        # Setting the item as active item (even if the cropping rectangle item
        # will also be set as active item just below), for the image tools to
        # register this item (contrast, ...):
        plot.set_active_item(self.item)
        self.item.unselect()

    def unset_item(self):
        """Unset the associated item, freeing memory"""
        plot = self.get_plot()
        plot.del_item(self.item)
        self.item = None

    def reset(self):
        """Reset crop/transform image settings"""
        self.item.set_crop(*self.item_original_crop)
        self.item.set_transform(*self.item_original_transform)
        self.reset_transformation()
        self.apply_transformation()

    def reset_transformation(self):
        """Reset transformation"""
        raise NotImplementedError

    def apply_transformation(self):
        """Apply transformation, e.g. crop or rotate"""
        raise NotImplementedError

    def compute_transformation(self):
        """Compute transformation, return compute output array"""
        raise NotImplementedError

    # ------Private API---------------------------------------------------------
    def restore_original_state(self):
        """Restore item original state"""
        select, move, resize, rotate = self.item_original_state
        self.item.set_selectable(select)
        self.item.set_movable(move)
        self.item.set_resizable(resize)
        self.item.set_rotatable(rotate)

    def accept_changes(self):
        """Computed rotated/cropped array and apply changes to item"""
        self.restore_original_state()
        self.apply_transformation()
        self.output_array = self.compute_transformation()
        # Ignoring image position changes
        pos_x0, pos_y0, _angle, sx, sy, hf, vf = self.item_original_transform
        _pos_x0, _pos_y0, angle, _sx, _sy, hf, vf = self.item.get_transform()
        self.item.set_transform(pos_x0, pos_y0, angle, sx, sy, hf, vf)

    def reject_changes(self):
        """Restore item original transform settings"""
        self.restore_original_state()
        self.item.set_crop(*self.item_original_crop)
        self.item.set_transform(*self.item_original_transform)


class BaseTransformDialog(ImageDialog):
    """Rotate & Crop Dialog

    Rotate and crop a :py:class:`guiqwt.image.TrImageItem` plot item"""

    def __init__(self, parent, wintitle=None, options=None, resize_to=None):
        if wintitle is None:
            wintitle = _("Rotate & Crop")
        ImageDialog.__init__(
            self,
            wintitle=wintitle,
            edit=True,
            toolbar=False,
            options=options,
            parent=parent,
        )
        if resize_to is not None:
            width, height = resize_to
            self.resize(width, height)
        self.accepted.connect(self.accept_changes)
        self.rejected.connect(self.reject_changes)

    def install_button_layout(self):
        """Reimplemented ImageDialog method"""
        self.add_buttons_to_layout(self.button_layout)
        super(BaseTransformDialog, self).install_button_layout()


class BaseTransformWidget(QWidget):
    """Base transform widget: see for example rotatecrop.py"""

    def __init__(self, parent, options=None):
        QWidget.__init__(self, parent=parent)

        if options is None:
            options = {}
        self.imagewidget = ImageWidget(self, **options)
        self.imagewidget.register_all_image_tools()

        hlayout = QHBoxLayout()
        self.add_buttons_to_layout(hlayout)

        vlayout = QVBoxLayout()
        vlayout.addWidget(self.imagewidget)
        vlayout.addLayout(hlayout)
        self.setLayout(vlayout)

    def get_plot(self):
        """Required for BaseTransformMixin"""
        return self.imagewidget.get_plot()


class BaseMultipleTransformWidget(QTabWidget):
    """Base Multiple Transform Widget

    Transform several :py:class:`guiqwt.image.TrImageItem` plot items"""

    TRANSFORM_WIDGET_CLASS = None

    def __init__(self, parent, options=None):
        QTabWidget.__init__(self, parent)
        self.options = options
        self.output_arrays = None

    def set_items(self, *items):
        """Set the associated items -- must be a TrImageItem objects"""
        for item in items:
            self.add_item(item)

    def add_item(self, item):
        """Add item to widget"""
        widget = self.TRANSFORM_WIDGET_CLASS(self, options=self.options)
        widget.set_item(item)
        self.addTab(widget, item.title().text())
        return widget

    def clear_items(self):
        """Clear all items, freeing memory"""
        self.items = None
        for index in range(self.count()):
            self.widget(index).unset_item()
        self.clear()

    def reset(self):
        """Reset transform image settings"""
        for index in range(self.count()):
            self.widget(index).reset()

    def accept_changes(self):
        """Accept all changes"""
        self.output_arrays = []
        for index in range(self.count()):
            widget = self.widget(index)
            widget.accept_changes()
            self.output_arrays.append(widget.output_array)

    def reject_changes(self):
        """Reject all changes"""
        for index in range(self.count()):
            self.widget(index).reject_changes()