# -*- 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/>.
"""Handles to control elements in the :mod:`.guis.qt` backend.
Currently handles are always naturally related to another Qt item.
This item should be the handle parent.
Allowing handles without any parent (``parent=None``) is *not* implemented yet.
.. note::
:class:`PointHandle` derive from a Qt class,
and 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 `self.scene` -- mind the ``()``.
This simplifies the calls, when :class:`PointHandle` are created with given
``parent=``. The Qt scene() is inherited from parent,
hence no need to add the ``scene=`` keyword.
This is not the case for :class:`LineHandle` that are python objects
"""
import weakref
from PyQt5.QtCore import QPointF, Qt
from PyQt5.QtGui import QPen, QVector2D
from PyQt5.QtWidgets import (
QGraphicsEllipseItem,
QGraphicsItem,
QGraphicsLineItem,
)
from .signal import Signal
# -------------------------------------------------------------------------
# Handles
# -------------------------------------------------------------------------
[docs]class PointHandle(QGraphicsEllipseItem):
"""Handle to control a "point".
Args:
relative (bool):
- False (default): positions are absolute and in **scene coordinates**
- True: positions are relative to the parent, and in **view coordinates**
Note:
Beware that a call to :meth:`setPos()` will emit `signal_moved`.
Hence, call :meth:`setPos()` first,
and only then connect to `signal_moved`.
"""
def __init__(self, tag=None, zvalue=1000, relative=False, **kwargs):
QGraphicsEllipseItem.__init__(self, **kwargs)
#: Signal to be emitted when the PointHandle is moved.
#:
#: **slot args:** (`dx`, `dy`),
#: where `dx` and `dy` are displacements along *x* and *y*,
#: in scene coordinates.
self.signal_moved = Signal()
#: Signal emitted when an `ItemSelectedChange` occurs.
#:
#: **slot args:** (:obj:`boolean`)
self.signal_selected_change = Signal()
self.relative = relative
self.setFlag(QGraphicsItem.ItemIsMovable, True)
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
# needed to handle move event
self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
if not relative:
self.setFlag(QGraphicsItem.ItemSendsScenePositionChanges, True)
# keep same size when zooming
# then we need to map position from scene to device in itemChange
self.setFlag(QGraphicsItem.ItemIgnoresTransformations, True)
# zvalue should be higher than rays,
# otherwise the ray takes focus before its handle
self.setZValue(zvalue)
# radius (in pixels)
r = 4
# circle is centered on (0, 0) in item coordinates
self.setRect(-r, -r, 2 * r, 2 * r)
self.position_before_move = None
#: ignore :term:`move restrictions` ?
#: Initially True so that the first move, to the initial position, is free
#: The user is responsible for setting it back to False,
#: to honor the scene setting
self.ignore_move_restrictions = True
[docs] def reset_move(self):
"""Store the initial position, for :term:`move restrictions`."""
self.position_before_move = self.pos()
[docs] def setPos(self, *args):
"""Overload QGraphicsEllipseItem."""
parent_item = self.parentItem()
if (
(parent_item is None) or not
(QGraphicsItem.ItemIgnoresTransformations and parent_item.flags())
):
# no parent, or parent has not the ItemIgnoresTransformations flag
# so args are in scene coordinates
# but since the flag ItemIgnoresTransformations is set,
# setPos should receive device (i.e. view) coordinates
view_pos = self.mapFromScene(*args)
QGraphicsEllipseItem.setPos(self, view_pos)
else:
# parent has the ItemIgnoresTransformations flag
# hence, by convention adopted here,
# args are already in view coordinates
# (relative to the parent of course)
QGraphicsEllipseItem.setPos(self, *args)
[docs] def itemChange(self, change, value):
"""Overload QGraphicsEllipseItem."""
# 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.ItemPositionChange):
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 (
(not self.ignore_move_restrictions)
and 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
# move
if self.relative:
self.signal_moved.emit(current_dx, current_dy)
self.scene().move_id += 1
value = old_pos + QPointF(current_dx, current_dy)
elif change == QGraphicsItem.ItemScenePositionHasChanged:
new_pos = value
#print "abs: ", new_pos
self.signal_moved.emit(new_pos.x(), new_pos.y())
# don't do that here, ItemSceneChange is not raised for children...
#elif change == QGraphicsItem.ItemSceneChange:
# print "scene change"
# old_scene = self.scene()
# new_scene = value
# if old_scene:
# old_scene.signal_reset_move.disconnect(self.reset_move)
# new_scene.signal_reset_move.connect(self.reset_move)
elif change == QGraphicsItem.ItemSelectedChange:
self.signal_selected_change.emit(value)
# forward event
return QGraphicsItem.itemChange(self, change, value)
# note: - making it a QGraphicsItemGroup would always move the whole thing
# - It used to be a QGraphicsItem with ItemHasNoContents flag
# but this required to reimplement shape and boundingrect,
# and a weakref machinery on the caller side
[docs]class LineHandle(object):
"""Handle to control a "line".
That is, control a point and a vector from that point.
Args:
line (~geoptics.elements.line.Line)
parent (QGraphicsItem):
the parent for all items composing this handle
"""
def __init__(self, line, parent=None, zvalue=1000, **kwargs):
#: signal emitted when either end of the LineHandle has been moved.
#:
#: **slot args:** (:py:class:`~geoptics.elements.line.Line`)
self.signal_moved = Signal()
# a copy of the line this handle is representing
self.line = line.copy()
if parent is None:
raise NotImplementedError(
"parent can not be None.\n"
"Please use the Qt item (e.g. _GPolycurve)\n"
"that is related to this handle,\n"
"or file an issue with a sufficiently clear motivation.")
# handle for the starting point of the line
self._h_p0_wr = weakref.ref(PointHandle(parent=parent))
self.h_p0.setPos(self.line.p.x, self.line.p.y)
self.h_p0.ignore_move_restrictions = False
# connect to signal_moved only here,
# otherwise setPos() would emit signal_moved
# which would call
# update_line() which needs the yet undefined self.line_item
self.h_p0.signal_moved.connect(self.p0_moved)
# self.h_p0.scene() returns the Qt object (_GScene)
view = self.h_p0.scene().active_view
if view:
u_view_x, u_view_y = view.map_vector_from_scene(self.line.u.x,
self.line.u.y)
else:
# no active view (probably inside a test) => no transformation
u_view_x = self.line.u.x
u_view_y = self.line.u.y
# use QVector2D to have normalize
self.u_view = QVector2D(u_view_x, u_view_y)
# length of 50 pixels to start with
self.u_view.normalize()
self.u_view *= 50
#m = self.scene.active_view.matrix()
#print "m11", m.m11(), "m21", m.m21()
#print "m12", m.m12(), "m22", m.m22()
#print "dx", m.dx(), "dy", m.dy()
# create the handle for the end of the vector, h_u
# parent is the starting point handle h_p0,
# so that when h_p0 is moved the end point follows
# hence no relative move and signal_moved is not emitted by h_u
# use weakref since the object has a Qt parent,
# to avoid circular references
self._h_u_wr = weakref.ref(PointHandle(relative=True, parent=self.h_p0))
# relative to the parent
self.h_u.setPos(self.u_view.toPointF())
# connect to signal_moved only here,
# otherwise setPos() would emit signal_moved
# which would call update_line()
# which needs the yet undefined self.line_item
self.h_u.signal_moved.connect(self.u_moved)
# use weakref since the object has a Qt parent,
# to avoid circular references
self._line_item_wr = weakref.ref(QGraphicsLineItem(parent=self.h_p0))
self.line_item.setPen(QPen(Qt.black, 0, Qt.DotLine))
self.update_line()
self.setZValue(zvalue)
@property
def h_p0(self):
"""Handle for the starting point of the line."""
return self._h_p0_wr()
@property
def h_u(self):
"""Handle for the end of the u vector."""
return self._h_u_wr()
@property
def line_item(self):
"""Line item, joining the point and the end of the u vector."""
return self._line_item_wr()
[docs] def update_line(self):
"""Update the line item."""
self.line_item.setLine(0, 0, self.u_view.x(), self.u_view.y())
[docs] def p0_moved(self, x, y):
"""React to change of the point position.
Args:
x (float):
y (float): new position of the point.
"""
self.line.p.x = x
self.line.p.y = y
self.update_line()
self.signal_moved.emit(self.line)
[docs] def u_moved(self, dx, dy):
"""React to change of the vector end position.
Args:
dx (float):
dy (float): displacements of the vector end, in view coordinates.
"""
du_view = QPointF(dx, dy)
self.u_view += QVector2D(du_view)
self.update_line()
view = self.h_p0.scene().active_view
if view:
u_scene_x, u_scene_y = view.map_vector_to_scene(self.u_view.x(),
self.u_view.y())
else:
# no active view (probably inside a test) => no transformation
u_scene_x = self.u_view.x()
u_scene_y = self.u_view.y()
self.line.u.x = u_scene_x
self.line.u.y = u_scene_y
self.line.u.normalize()
self.signal_moved.emit(self.line)
[docs] def reset_move(self):
"""Store the initial position, for :term:`move restrictions`."""
self.h_p0.reset_move()
self.h_u.reset_move()
[docs] def setVisible(self, visible: bool):
"""Set visibility."""
# h_u and line_item are children of h_p0, no need to set them
self.h_p0.setVisible(visible)
[docs] def setZValue(self, zvalue):
"""Set the :term:`z_value`.
The zvalue can be that of the item to be controlled.
The Qt items composing the LineHandle will be set to :term:`z_values`
1 or 2 above, to ensure their visibility.
"""
self.line_item.setZValue(zvalue + 1)
self.h_p0.setZValue(zvalue + 2)
self.h_u.setZValue(zvalue + 2)