# -*- coding: utf-8 -*-
# Copyright (C) 2014 ederag <edera@gmx.fr>
#
# This program 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 3 of the License, or
# (at your option) any later version.
#
# This program 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 GeOptics; see the file LICENSE.txt. If not, see
# <http://www.gnu.org/licenses/>.
"""Regions for the :mod:`.guis.qt` backend."""
import logging
logger = logging.getLogger(__name__) # noqa: E402
from math import pi
from PyQt5.QtCore import QPointF, Qt
from PyQt5.QtGui import (
QColor,
QPainterPath,
QPen,
)
from PyQt5.QtWidgets import QGraphicsItem, QGraphicsPathItem
from geoptics import elements
from .counterpart import GOverload, g_counterpart
# ---------------------------------------------------------------------------
# Region
# ---------------------------------------------------------------------------
[docs]@g_counterpart
class _GPolycurve(QGraphicsPathItem):
"""Graphical class corresponding to :class:`.Polycurve`.
.. note::
_G* objects are living in the Qt realm only.
They are not aware of the underlying elements.
Hence the Qt :meth:`self.scene()` should be used,
instead of :meth:`self.scene` -- mind the ``()``.
.. note::
when about to change the `pos()` of this item,
e.g. before a :meth:`setPos()` of a :meth:`translate()`,
first issue a :meth:`reset_move()`
"""
# note: @g_counterpart will add a keyword argument, "element"
def __init__(self, **kwargs):
QGraphicsPathItem.__init__(self, **kwargs)
# pen
pen = QPen(Qt.black, 1.5, Qt.SolidLine)
pen.setCosmetic(True) # thickness independent os scale
self.setPen(pen)
self.setBrush(QColor("lightYellow"))
#self.setBrush(QColor("Magenta").darker(120))
# move handling
self.setFlag(QGraphicsItem.ItemIsMovable, True)
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
# needed to handle move event
self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
self.setAcceptHoverEvents(True)
[docs] def g_add_arc(self, M_next, tangent):
elements.regions.Polycurve.add_arc(self.e, M_next, tangent)
arc = self.e.curves[-1]
C = arc.C
w = h = 2 * arc.r
span = arc.theta2 - arc.theta1
# span is defined modulo 2pi.
if span < 0 and arc.ccw:
# in the scene coordinates, ccw mean positive span
span = 2 * pi + span
if span > 0 and not arc.ccw:
span = span - 2 * pi
# the graphicsview has an indirect coordinates system => angles are opposite
# this should be changed when working in scene coordinates
qt_theta1 = -arc.theta1 * 180.0 / pi
qt_span = -span * 180.0 / pi
path = self.path()
# Qt paths are relative to the first point of the region
M0 = self.e.M[0]
path.arcTo(C.x - M0.x - arc.r, C.y - M0.y - arc.r,
w, h, qt_theta1, qt_span)
self.setPath(path)
[docs] def g_add_line(self, M_next):
elements.regions.Polycurve.add_line(self.e, M_next)
# we can not do self.setPath( self.path().lineTo(M_next.x, M_next.y) )
path = self.path()
# Qt paths are relative to the first point of the region
M0 = self.e.M[0]
path.lineTo(M_next.x - M0.x, M_next.y - M0.y)
self.setPath(path)
[docs] def g_close(self):
elements.regions.Polycurve.close(self.e)
path = self.path()
path.closeSubpath()
self.setPath(path)
[docs] def g_start(self, M_start):
elements.regions.Polycurve.start(self.e, M_start)
path = QPainterPath()
path.setFillRule(Qt.WindingFill)
# set starting point
# Qt paths are relative to the first point of the region
path.moveTo(0, 0)
self.setPath(path)
# about to move the _G item
self.reset_move()
self.setPos(M_start.x, M_start.y)
[docs] def g_translate(self, v=None, dx=0, dy=0):
if v:
dx = v.x
dy = v.y
# about to move the _G item
self.reset_move()
# moveBy is inherited from QGraphicsPathItem
self.moveBy(dx, dy)
[docs] def itemChange(self, change, value):
"""Overload QGraphicsPathItem."""
# noqa see file:///usr/share/doc/packages/python-qt4-devel/doc/html/qgraphicsitem.html#itemChange
# they add && scene() to the condition. To check
# the example shows also how to keep the item in the scene area
if change == QGraphicsItem.ItemSelectedChange:
# reset, since not in a move
self.position_before_move = None
elif change == QGraphicsItem.ItemPositionChange:
#print "pos changed to: ", value
new_pos = value
old_pos = self.pos()
if self.position_before_move is None:
# beginning move => keep the original position
self.position_before_move = old_pos
# total displacement since the beginning of the move
total_dx = new_pos.x() - self.position_before_move.x()
total_dy = new_pos.y() - self.position_before_move.y()
if self.scene().move_restrictions_on:
if abs(total_dx) >= abs(total_dy):
# move along x only
new_pos.setY(self.position_before_move.y())
else:
# move along y only
new_pos.setX(self.position_before_move.x())
# displacement for this elementary move
current_dx = new_pos.x() - old_pos.x()
current_dy = new_pos.y() - old_pos.y()
#print "dx = ", current_dx, "dy = ", current_dy, self
# move
# the Qt item will be translated by Qt, based on "value"
# the listener will move the element
elements.regions.Polycurve.translate(self.e, dx=current_dx,
dy=current_dy)
self.scene().move_id += 1
value = old_pos + QPointF(current_dx, current_dy)
elif change == QGraphicsItem.ItemSceneChange:
old_scene = self.scene()
new_scene = value
if old_scene:
old_scene.signal_set_all_selected.disconnect(self.setSelected)
old_scene.signal_reset_move.disconnect(self.reset_move)
if new_scene:
new_scene.signal_set_all_selected.connect(self.setSelected)
new_scene.signal_reset_move.connect(self.reset_move)
# forward event
return QGraphicsPathItem.itemChange(self, change, value)
[docs] def hoverEnterEvent(self, event):
"""Overload QGraphicsPathItem."""
self.setOpacity(0.8)
[docs] def hoverLeaveEvent(self, event):
"""Overload QGraphicsPathItem."""
self.setOpacity(1.0)
#def hoverMoveEvent(self, event):
# noqa see http://pyqt.sourceforge.net/Docs/PyQt5/api/qgraphicsscenehoverevent.html
#pos = event.pos()
#lastPos = event.lastPos()
#delta_pos = pos - lastPos
#print delta_pos
#self.move(delta_pos.x, delta_pos.y)
[docs] def reset_move(self):
"""Reset the move machinery.
Should be called at the beginning of a new move.
Store the initial position. This is important to be able to constraint
moves along a particular direction (move restrictions on).
"""
self.position_before_move = self.pos()
# workaround Qt bug fixed in feb2016:
# inserting a python method because
# disconnect does not work on non-slot Qt methods
# https://www.riverbankcomputing.com/pipermail/pyqt/2016-February/037000.html
[docs] def setSelected(self, selected):
"""Overload QGraphicsItem."""
QGraphicsPathItem.setSelected(self, selected)
[docs]@GOverload("start", "add_line", "add_arc", "close", "translate")
class Polycurve(elements.regions.Polycurve):
"""Define a region inside curves (for instance line segments).
Args:
scene (~qt.scene.Scene): to belong to.
"""
def __init__(self, n=None, scene=None, tag=None, zvalue=0, **kwargs):
# do not pass the scene here
self.g = _GPolycurve(element=self, **kwargs)
# The element __init__ method will call self.scene.add,
# which will need self.g
elements.regions.Polycurve.__init__(self, n=n, scene=scene, tag=tag)
# higher zvalue means above
# regions should be below rays
# the default zvalue is OK
self.g.setZValue(zvalue)