Source code for geoptics.guis.qt.scene

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

"""Scene to be used with the Qt gui."""

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

from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtWidgets import QGraphicsScene, QUndoStack

from geoptics import elements
from geoptics.shared.tools import find_classes

from . import rays
from . import regions
from . import sources
from .counterpart import g_counterpart
from .handles import PointHandle


[docs]@g_counterpart class _GScene(QGraphicsScene): """The graphical class corresponding to :class:`.Scene`. Args: element (:class:`.Scene`): The corresponding element .. seealso:: More informations on the relationships between Scene and _GScene can be found in the :ref:`guis.qt architecture` section. """ signal_set_all_selected = pyqtSignal(bool) signal_reset_move = pyqtSignal() signal_mouse_position_changed = pyqtSignal(float, float) signal_element_moved = pyqtSignal() signal_remove_selected_items = pyqtSignal() #: Signal emitted when selected items should be removed #: **slot args:** () # note: @g_counterpart will add a keyword argument, "element" def __init__(self, **kwargs): QGraphicsScene.__init__(self, **kwargs) # view linked to this scene, currently under mouse self.active_view = None # incremented for each move self.move_id = 0 self._last_checked_move_id = 0 self.signal_remove_selected_items.connect(self.remove_selected_items) self.move_restrictions_on = False """Whether objects moves should be restricted. .. glossary:: move restrictions Some displacements can be restricted to certain directions. If ``move_restrictions_on`` is True, then the displacements are constrained to be along the `x` or `y` direction. The actual direction (`x` or `y`) should be the closest to the mouse displacement. For instance, if the mouse movement is mainly horizontal, then a :class:`~.qt.handles.PointHandle` would move along `x` only. This requires to store the initial position, at the beginning of the displacement. This is done by :meth:`reset_move` slots. """ # http://www.informit.com/articles/article.aspx?p=1187104&seqNum=3 # recommends to set the parent # "so that PyQt is able to clean it up at the right time # when the dialog box is destroyed" # but then there is a cycle => possible crashes on exit ? self.undo_stack = QUndoStack(parent=self) @property def active_view(self): """Get current active view.""" if self._active_view_wr: return self._active_view_wr() @active_view.setter def active_view(self, view): logger.debug("setting active_view to {}".format(view)) if view: self._active_view_wr = weakref.ref(view) else: self._active_view_wr = None
[docs] def addItem(self, item): """Overload QGraphicsScene method.""" # If the "scene=" keyword were passed on to constructors, # Then the _G object ItemSceneChange would _not_ be called (Qt 4.8.6). if item in self.items(): raise ValueError("{} is already in {}\n" "never use the 'scene=' keyword for _G objects,\n" "only the _GScene.additem() method" .format(item.e, self) ) else: QGraphicsScene.addItem(self, item)
[docs] def mousePressEvent(self, event): """Overload QGraphicsScene method.""" if event.button() == Qt.RightButton: # block the right mouse press event so that selection is not cleared # before the context menu is shown (not yet implemented) event.accept() else: QGraphicsScene.mousePressEvent(self, event) # forward event
[docs] def mouseReleaseEvent(self, event): """Overload QGraphicsScene method.""" self.element_moved = False QGraphicsScene.mouseReleaseEvent(self, event) # forward event
[docs] def mouseMoveEvent(self, event): """Overload QGraphicsScene method.""" pos = event.scenePos() self.signal_mouse_position_changed.emit(pos.x(), pos.y()) QGraphicsScene.mouseMoveEvent(self, event) if self.move_id != self._last_checked_move_id: #logger.debug("move_id = {}".format(self.move_id)) self._last_checked_move_id = self.move_id self.signal_element_moved.emit()
[docs] def keyPressEvent(self, event): """Handle key pressed events. - ``Delete``: remove selected items - ``ESC``: deselect all """ key = event.key() if key == Qt.Key_Delete: # SUPPR self.signal_remove_selected_items.emit() elif (key == Qt.Key_Escape): # ESC self.signal_set_all_selected.emit(False)
# the following does not work. Actually sceneRect is never called #def sceneRect(self): # # use a boundingRect twice as large as the default tight boundingRect # # so that anypoint of the tight boundingRect # # can be used as the view center by moving the scrollbars #rect = self.itemsBoundingRect() #print rect #ax, ay, aaw, aah = rect.getRect() #rect.setRect(ax - aaw / 2.0, ay - aah / 2.0, aaw * 2, aah * 2) #return rect
[docs] def remove(self, item): """Remove item from both Qt and elements scene.""" logger.info("removing {}".format(item.e)) self.removeItem(item) # remove the element after its Qt counterpart, # otherwise garbage collection might destroy the Qt item => crash ? elements.scene.Scene.remove(self.e, item.e)
[docs] def remove_selected_items(self): """Remove selected items from scene.""" for item in self.items(): if ( item.isSelected() # Rays and PointHandles are removed by their parent and not isinstance(item, (PointHandle, rays._GRay)) ): # workaround children remaining visible (Qt 4.8.6) # item.prepareGeometryChange() # does not work either item.setVisible(False) self.remove(item) self.e.propagate()
# Nothing of these worked, sometimes children remained visible, # until another object is drawn over: #self.invalidate(self.sceneRect(), QGraphicsScene.AllLayers) #self.update() #self.changed.emit([self.sceneRect()])
[docs]class Scene(elements.scene.Scene): """The Scene that should be instanciated by user, in the guis.qt backend.""" def __init__(self, **kwargs): elements.scene.Scene.__init__(self) # The scene Qt part has no parent => owned by self (python part) self.g = _GScene(element=self, **kwargs) #: correspondance between names in config data, and classes self.class_map = {'Rays': find_classes(rays), 'Sources': find_classes(sources), 'Regions': find_classes(regions)} self.g.signal_element_moved.connect(self.propagate)
[docs] def add(self, other, tag=None): """Add an element to the scene. Args: other (dict or element): the element to be added, either as a config dictionnary, or directly as an object such as :class:`.guis.qt.sources.Beam` or :class:`.guis.qt.regions.Polycurve` """ elements.scene.Scene.add(self, other) if not isinstance(other, dict): # other is not a config. Thus it must be an element self.g.addItem(other.g)
[docs] def remove(self, element): """Remove the element from scene.""" self.g.remove(element.g)