tobspr/RenderPipeline

View on GitHub
toolkit/day_time_editor/curve_widget.py

Summary

Maintainability
C
1 day
Test Coverage
"""

RenderPipeline

Copyright (c) 2014-2016 tobspr <tobias.springer1@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

"""

from __future__ import print_function
from rplibs.six.moves import range  # pylint: disable=import-error

import math

from rplibs.pyqt_imports import * # noqa

class CurveWidget(QWidget):

    """ This is a resizeable Widget which shows an editable curve which can
    be modified. """

    def __init__(self, parent):
        """ Constructs the CurveWidget, we start with an initial curve """
        QWidget.__init__(self, parent)
        self.setFocusPolicy(Qt.ClickFocus)
        self._curves = []

        # Store current display time
        self._current_time = 0.5

        # Widget render constants
        self._cv_point_size = 3
        self._legend_border = 52
        self._bar_h = 30

        # Currently dragged control point, format is:
        # (CurveIndex, PointIndex, Drag-Offset (x,y))
        self._drag_point = None
        self._drag_time = 0.0

        # Currently selected control point, format is:
        # (CurveIndex, PointIndex)
        self._selected_point = None

        self._unit_processor = lambda v: str(round(v, 2))
        self._change_handler = lambda: None

    def set_unit_processor(self, proc):
        """ Sets the function which gets called to map values from 0 .. 1 to
        values like 10% to 30% """
        self._unit_processor = proc

    def set_change_handler(self, handler):
        """ Sets a function which gets called when the data in this widget got
        edited """
        self._change_handler = handler

    def paintEvent(self, e):  # noqa
        """ Internal QT paint event, draws the entire widget """
        qp = QPainter()
        qp.begin(self)
        self._draw(qp)
        qp.end()

    def mousePressEvent(self, QMouseEvent):  # noqa
        """ Internal mouse-press handler """
        self._drag_point = None
        self._selected_point = None
        mouse_pos = QMouseEvent.pos()
        mouse_x = mouse_pos.x() - self._legend_border
        mouse_y = mouse_pos.y() - self._bar_h

        for index, curve in enumerate(self._curves):

            # Check for clicks on control points
            for cv_index, (x, y) in enumerate(curve.control_points):
                point_x = self._get_x_value_for(x)
                point_y = self._get_y_value_for(y) - self._bar_h
                if abs(point_x - mouse_x) < self._cv_point_size + 6:
                    if (abs(point_y - mouse_y)) < self._cv_point_size + 6:
                        drag_x_offset = mouse_x - point_x
                        drag_y_offset = mouse_y - point_y
                        mpos_relx = float(mouse_x) / (self.width() - self._legend_border)
                        self._drag_point = (index, cv_index, (drag_x_offset, drag_y_offset))
                        self._drag_time = mpos_relx
                        self._selected_point = (index, cv_index)

            # If still no intersection, check if we clicked a curve
            if mouse_x > 0 and mouse_x < self.width() - self._legend_border:
                if mouse_y > 0 and mouse_y < self.height() - self._legend_border:
                    mpos_relx = float(mouse_x) / (self.width() - self._legend_border)
                    curve_py = curve.get_value(mpos_relx)
                    curve_offy = self._get_y_value_for(curve_py) - self._bar_h

                    if abs(curve_offy - mouse_y) < 8 and self._selected_point is None:
                        # Clicked on curve, spawn new point
                        cv_index = curve.append_cv(mpos_relx, curve_py)

                        self._selected_point = (index, cv_index)
                        self._drag_point = (index, cv_index, (0, 0))
                        self._drag_time = mpos_relx
                        self._change_handler()
        self.update()

    def mouseReleaseEvent(self, event):  # noqa
        """ Internal mouse-release handler """
        self._drag_point = None
        self._drag_time = -1
        self.update()

    def mouseMoveEvent(self, event):  # noqa
        """ Internal mouse-move handler """
        if len(self._curves) < 1:
            return

        if self._drag_point is not None:
            mouse_x = event.pos().x() - self._drag_point[2][0] - self._legend_border
            mouse_y = event.pos().y() - self._drag_point[2][1] - self._bar_h

            # Convert to local coordinate
            local_x = max(0, min(1, mouse_x / float(self.width() - self._legend_border)))
            local_y = mouse_y / float(self.height() - self._legend_border - self._bar_h)
            local_y = 1 - max(0, min(1, local_y))

            self._drag_time = local_x

            # Set new point data
            self._curves[self._drag_point[0]].set_cv_value(self._drag_point[1], local_x, local_y)

            # Redraw curve
            self._curves[self._drag_point[0]].build_curve()
            self.update()
            self._change_handler()

    def keyPressEvent(self, event):  # noqa
        """ Internal keypress handler """
        # Delete anchor point
        if event.key() == Qt.Key_Delete:
            self.delete_current_point()

    def delete_current_point(self):
        """ Deletes the currently selected point """
        if self._selected_point is not None:
            self._curves[self._selected_point[0]].remove_cv(self._selected_point[1])
            self._selected_point = None
            self._drag_point = None
            self.update()
            self._change_handler()

    def set_curves(self, curves):
        """ Sets the list of displayed curves """
        self._selected_point = None
        self._drag_point = None
        self._curves = curves
        self.update()

    def _get_y_value_for(self, local_value):
        """ Converts a value from 0 to 1 to a value from 0 .. canvas height """
        local_value = max(0, min(1.0, 1.0 - local_value))
        local_value *= self.height() - self._legend_border - self._bar_h
        local_value += self._bar_h
        return local_value

    def _get_x_value_for(self, local_value):
        """ Converts a value from 0 to 1 to a value from 0 .. canvas width """
        local_value = max(0, min(1.0, local_value))
        local_value *= self.width() - self._legend_border
        return local_value

    def set_current_time(self, local_time):
        """ Sets the current displayed time, should range from 0 to 1 """
        self._current_time = max(0.0, min(1.0, local_time))
        self.update()

    def _draw(self, painter):
        """ Internal method to draw the widget """

        canvas_width = self.width() - self._legend_border
        canvas_height = self.height() - self._legend_border - self._bar_h

        # Draw field background
        # painter.setPen(QColor(200, 200, 200))
        # painter.setBrush(QColor(230, 230, 230))
        # painter.drawRect(0, 0, self.width() - 1, self.height() - 1)

        # Draw legend

        # Compute amount of horizontal / vertical lines
        num_vert_lines = 12  # 24 / 12 = 2, one entry per 2 hours
        line_spacing_x = (self.width() - self._legend_border) / num_vert_lines
        line_spacing_y = (self.height() - self._legend_border - self._bar_h) / 20.0
        num_horiz_lines = int(math.ceil(canvas_height / float(line_spacing_y)) + 1)

        # Draw vertical lines
        painter.setPen(QColor(200, 200, 200))
        for i in range(num_vert_lines + 1):
            line_pos = i * line_spacing_x + self._legend_border - 1
            painter.drawLine(line_pos, self._bar_h, line_pos, canvas_height + self._bar_h)

        # Draw horizontal lines
        painter.setPen(QColor(200, 200, 200))
        for i in range(num_horiz_lines):
            line_pos = canvas_height - i * line_spacing_y + self._bar_h
            painter.drawLine(self._legend_border, line_pos, self.width(), line_pos)

        # Draw vetical legend labels
        painter.setPen(QColor(120, 120, 120))
        for i in range(num_horiz_lines):
            line_pos = canvas_height - i * line_spacing_y + self._bar_h
            # painter.drawText(6, line_pos + 3, str(round(float(i) / (num_horiz_lines-1), 2)))
            painter.drawText(
                6, line_pos + 3, self._unit_processor(float(i) / (num_horiz_lines - 1)))

        # Draw horizontal legend labels
        for i in range(num_vert_lines + 1):
            line_pos = i * line_spacing_x + self._legend_border
            offpos_x = -14
            if i == 0:
                offpos_x = -2
            elif i == num_vert_lines:
                offpos_x = -27
            time_string = str(int(float(i) / num_vert_lines * 24)).zfill(2) + ":00"
            painter.drawText(line_pos + offpos_x, canvas_height + self._bar_h + 18, time_string)

        # Draw curve
        for index, curve in enumerate(self._curves):
            painter.setPen(QColor(*curve.color))
            last_value = 0
            for i in range(canvas_width):
                rel_offset = i / (canvas_width - 1.0)
                curve_height = self._get_y_value_for(curve.get_value(rel_offset))

                if i == 0:
                    last_value = curve_height

                painter.drawLine(
                    self._legend_border + i - 1, last_value, self._legend_border + i, curve_height)
                last_value = curve_height

            # Draw the CV points of the curve
            painter.setBrush(QColor(240, 240, 240))

            for cv_index, (x, y) in enumerate(curve.control_points):
                offs_x = x * canvas_width + self._legend_border
                offs_y = (1 - y) * canvas_height + self._bar_h

                if (self._selected_point and self._selected_point[0] == index and
                   self._selected_point[1] == cv_index):
                    painter.setPen(QColor(255, 0, 0))
                else:
                    painter.setPen(QColor(100, 100, 100))
                painter.drawRect(
                    offs_x - self._cv_point_size, offs_y - self._cv_point_size,
                    2 * self._cv_point_size, 2 * self._cv_point_size)

        # Draw bar background
        bar_half_height = 4
        bar_top_pos = 10

        painter.setBrush(QColor(255, 0, 0))
        painter.setPen(QColor(110, 110, 110))

        painter.drawRect(
            self._legend_border - 1, bar_top_pos - 1,
            self.width() - self._legend_border, 2 * bar_half_height + 2)

        # Draw bar
        if len(self._curves) == 0:
            return

        if len(self._curves) == 1:
            bar_curve = self._curves[0]
        else:
            bar_curve = self._curves[0:3]

        for i in range(canvas_width - 1):
            xpos = self._legend_border + i
            relv = float(i) / float(canvas_width)

            if len(self._curves) == 1:
                val = max(0, min(255, int(bar_curve.get_value(relv) * 255.0)))
                painter.setPen(QColor(val, val, val))
            else:
                r = max(0, min(255, int(bar_curve[0].get_value(relv) * 255.0)))
                g = max(0, min(255, int(bar_curve[1].get_value(relv) * 255.0)))
                b = max(0, min(255, int(bar_curve[2].get_value(relv) * 255.0)))
                painter.setPen(QColor(r, g, b))
            painter.drawLine(xpos, bar_top_pos, xpos, bar_top_pos + 2 * bar_half_height)

        # Draw selected time
        if self._drag_point:
            painter.setBrush(QColor(200, 200, 200))
            painter.setPen(QColor(90, 90, 90))
            offs_x = max(0, min(
                canvas_width + 10, self._drag_time * canvas_width + self._legend_border - 19))
            offs_y = self.height() - self._legend_border
            minutes = int(self._drag_time * 24 * 60)
            hours = minutes / 60
            minutes = minutes % 60
            painter.drawRect(offs_x, self.height() - self._legend_border + 5, 40, 20)
            painter.drawText(offs_x + 7, offs_y + 20, "{:02}:{:02}".format(hours, minutes))

            painter.setPen(QColor(150, 150, 150))
            painter.drawLine(
                offs_x + 19, bar_top_pos + 15, offs_x + 19, self.height() - self._legend_border + 5)

        # Display current time
        pen = QPen()
        pen.setColor(QColor(255, 100, 100))
        pen.setStyle(Qt.DashLine)
        painter.setPen(pen)

        xoffs = self._legend_border + self._current_time * (canvas_width - 1)
        painter.drawLine(xoffs, self._bar_h, xoffs, self._bar_h + canvas_height)

        # Draw usage hints
        painter.setPen(QColor(100, 100, 100))
        painter.drawText(
            5, self.height() - 2,
            "Click on the curve to add new control points, click and drag "
            "existing points to move them.")