Source code for geoptics.guis.qt.rays

# -*- 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/>.


"""Rays for the :mod:`.guis.qt` backend."""

import logging
logger = logging.getLogger(__name__)   # noqa: E402
import weakref
from math import isinf

from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPainterPath, QPainterPathStroker, QPen
from PyQt5.QtWidgets import (
	QGraphicsItem,
	QGraphicsPathItem,
	QStyle,
	QStyleOptionGraphicsItem,
)

from geoptics import elements

from .counterpart import GOverload, g_counterpart


# -------------------------------------------------------------------------
#                             Ray
# -------------------------------------------------------------------------

[docs]@g_counterpart class _GRay(QGraphicsPathItem): """The graphical class corresponding to :class:`.Ray`.""" # note: @g_counterpart will add a keyword argument, "element" def __init__(self, **kwargs): QGraphicsPathItem.__init__(self, **kwargs) self.setAcceptHoverEvents(True) self.setFlag(QGraphicsItem.ItemIsSelectable, True) # Rays stay just below the source, # without any need to set Z-value explicitly # This avoid selection problems (ray is always behind handles) self.setFlag(QGraphicsItem.ItemStacksBehindParent, True) # pen for normal state self.pen_normal = QPen(Qt.blue, 1.5, Qt.SolidLine) self.pen_normal.setCosmetic(True) # thickness does not scale self.setPen(self.pen_normal) # pen for hover state self.pen_hover = QPen(Qt.gray, 1.5, Qt.SolidLine) self.pen_hover.setCosmetic(True) # thickness does not scale # will be used in shape() self.stroker = QPainterPathStroker() self._selected = False
[docs] def g_draw(self): self.prepareGeometryChange() # prevent BSPtree corruption (Qt crash) self.e.source.g.prepareGeometryChange() path = QPainterPath() # begin at the beginning p0 = self.e.parts[0].line.p path.moveTo(p0.x, p0.y) # add lines for part in self.e.parts: if isinf(part.s): # something large, but not inf, for Qt # make sure the ray extends more than the whole scene scene_rect = self.scene().sceneRect() s = 2 * max(scene_rect.width(), scene_rect.height()) else: s = part.s path.lineTo(part.line.p.x + part.line.u.x * s, part.line.p.y + part.line.u.y * s) # update to the new path self.setPath(path)
[docs] def g_add_part(self, u, s, n=None): elements.rays.Ray.add_part(self.e, u, s, n) self.g_draw()
[docs] def g_change_s(self, part_number, new_s): elements.rays.Ray.change_s(self.e, part_number, new_s) self.g_draw()
[docs] def hoverEnterEvent(self, event): """Overload QGraphicsPathItem method.""" self.setPen(self.pen_hover) QGraphicsPathItem.hoverEnterEvent(self, event)
[docs] def hoverLeaveEvent(self, event): """Overload QGraphicsPathItem method.""" self.setPen(self.pen_normal) QGraphicsPathItem.hoverEnterEvent(self, event)
[docs] def itemChange(self, change, value): """Overload QGraphicsPathItem method.""" if (change == QGraphicsItem.ItemSelectedChange): # usually setSelected should not be called here, but # setSelected has been overriden and does not call the base method self.setSelected(value) # return False to avoid the deselection of the pointhandle # (multiple selection seems impossible without holding ctrl) return False # forward event return QGraphicsPathItem.itemChange(self, change, value)
[docs] def paint(self, painter, option, widget=None): """Overload QGraphicsPathItem method.""" new_option = QStyleOptionGraphicsItem(option) # suppress the "selected" state # this avoids the dashed rectangle surrounding the ray when selected new_option.state = QStyle.State_None QGraphicsPathItem.paint(self, painter, new_option, widget)
[docs] def shape(self): """Overload QGraphicsPathItem method.""" # by default, the shape is the path, # but closed, even if the path is a line # then sometimes the ray seems hovered, even when mouse is not on it # to avoid that, we need to reimplement shape, # with a QPainterPathStroker which # creates a shape that closely fits the line return self.stroker.createStroke(self.path())
[docs] def setSelected(self, selected): """Overload QGraphicsPathItem method.""" # override base method, without calling it # otherwise the selection of pointHandle # deselected the ray and vice-versa # (multiple selection seems impossible without holding ctrl) # FIXME: should not use the element(.g) here. Find another way. self.e.source.g.setSelected(selected) self._selected = selected
[docs] def isSelected(self): """Overload QGraphicsPathItem method.""" return self._selected
[docs]@GOverload("add_part", "change_s", "draw") class Ray(elements.rays.Ray): """Ray of light. This is the Ray that should be instanciated, in the :mod:`.guis.qt` backend. .. note:: Regular users should not use Ray directly, but instead use one of the sources in :mod:`.qt.sources`. """ def __init__(self, line0=None, s0=100, source=None, n=None, tag=None, zvalue=100, **kwargs): g = _GRay(element=self, **kwargs) # rays must be children of the source, in order to # - be removed when source is removed from scene # - inherit the zvalue of the source # - be added to the scene when the source is added g.setParentItem(source.g) # the _G object has a Qt parent, so will be deleted by the C++ part # keep only a weak reference, otherwise there may be deletion races self._g_wr = weakref.ref(g) elements.rays.Ray.__init__(self, line0, s0, source, n, tag) self.source = source self.g.g_draw() self.set_tag(tag)
[docs] def __del__(self): """Cleanup upon deletion.""" # if the deletion comes from the qt side, self.g is None if self.g: self.source.g.prepareGeometryChange() self.g.scene().removeItem(self.g)
@property def g(self): """Return the corresponding graphical item.""" return self._g_wr()