######################################################################################################################
# Copyright (C) 2017-2021 Spine project consortium
# This file is part of Spine Toolbox.
# Spine Toolbox is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser 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 Lesser General
# Public License for more details. You should have received a copy of the GNU Lesser General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
######################################################################################################################
"""
Custom QGraphicsScene used in the Design View.
:author: P. Savolainen (VTT)
:date: 13.2.2019
"""
import math
from PySide2.QtCore import Qt, Signal, Slot, QItemSelectionModel, QPointF, QEvent
from PySide2.QtWidgets import QGraphicsItem, QGraphicsScene
from PySide2.QtGui import QColor, QPen, QBrush
from ..project_item_icon import ProjectItemIcon
from ..link import Link, LinkDrawer
from .project_item_drag import ProjectItemDragMixin
[docs]class CustomGraphicsScene(QGraphicsScene):
"""
A custom QGraphicsScene. It provides signals to notify about items,
and a method to center all items in the scene.
At the moment it's used by DesignGraphicsScene and the GraphViewMixin
"""
[docs] item_move_finished = Signal(QGraphicsItem)
"""Emitted when an item has finished moving."""
[docs] item_removed = Signal(QGraphicsItem)
"""Emitted when an item has been removed."""
[docs] def center_items(self):
"""Centers toplevel items in the scene."""
rect = self.itemsBoundingRect()
delta = -rect.center()
for item in self.items():
if item.topLevelItem() != item:
continue
item.moveBy(delta.x(), delta.y())
self.setSceneRect(rect.translated(delta))
[docs]class DesignGraphicsScene(CustomGraphicsScene):
"""A scene for the Design view.
Mainly, it handles drag and drop events of ProjectItemDragMixin sources.
"""
def __init__(self, parent, toolbox):
"""
Args:
parent (QObject): scene's parent object
toolbox (ToolboxUI): reference to the main window
"""
super().__init__(parent)
self._toolbox = toolbox
self.item_shadow = None
self._last_selected_items = set()
# Set background attributes
settings = toolbox.qsettings()
self.bg_choice = settings.value("appSettings/bgChoice", defaultValue="solid")
bg_color = settings.value("appSettings/bgColor", defaultValue="false")
self.bg_color = QColor("#f5f5f5") if bg_color == "false" else bg_color
self.bg_origin = None
self.link_drawer = LinkDrawer(toolbox)
self.link_drawer.hide()
self.connect_signals()
[docs] def mouseMoveEvent(self, event):
"""Moves link drawer."""
if self.link_drawer.isVisible():
self.link_drawer.tip = event.scenePos()
self.link_drawer.update_geometry()
event.setButtons(Qt.NoButton) # this is so super().mouseMoveEvent sends hover events to connector buttons
super().mouseMoveEvent(event)
[docs] def mousePressEvent(self, event):
"""Puts link drawer to sleep and log message if it looks like the user doesn't know what they're doing."""
was_drawing = self.link_drawer.isVisible()
super().mousePressEvent(event)
if was_drawing and self.link_drawer.isVisible():
self.link_drawer.sleep()
if event.button() == Qt.LeftButton:
self.emit_connection_failed()
[docs] def mouseReleaseEvent(self, event):
"""Makes link if drawer is released over a valid connector button."""
super().mouseReleaseEvent(event)
if not self.link_drawer.isVisible() or self.link_drawer.src_connector.isUnderMouse():
return
if self.link_drawer.dst_connector is None:
self.link_drawer.sleep()
self.emit_connection_failed()
return
self.link_drawer.add_link()
[docs] def emit_connection_failed(self):
self._toolbox.msg_warning.emit(
"Unable to make connection. Try landing the connection onto a valid connector button."
)
[docs] def keyPressEvent(self, event):
"""Puts link drawer to sleep if user presses ESC."""
super().keyPressEvent(event)
if self.link_drawer.isVisible() and event.key() == Qt.Key_Escape:
self.link_drawer.sleep()
[docs] def connect_signals(self):
"""Connect scene signals."""
self.selectionChanged.connect(self.handle_selection_changed)
[docs] def project_item_icons(self):
return [item for item in self.items() if isinstance(item, ProjectItemIcon)]
@Slot()
[docs] def handle_selection_changed(self):
"""Synchronizes selection with the project tree."""
selected_items = set(self.selectedItems())
if self._last_selected_items == selected_items:
return
self._last_selected_items = selected_items
project_item_icons = []
links = []
for item in self.selectedItems():
if isinstance(item, ProjectItemIcon):
project_item_icons.append(item)
elif isinstance(item, Link):
links.append(item)
# Set active project item, active link, and executed item in toolbox
active_project_item = (
self._toolbox.project_item_model.get_item(project_item_icons[0].name()).project_item
if len(project_item_icons) == 1
else None
)
active_link = links[0] if len(links) == 1 else None
self._toolbox.refresh_active_elements(active_project_item, active_link)
# Sync selection with project tree view
selected_item_names = {icon.name() for icon in project_item_icons}
self._toolbox.sync_item_selection_with_scene = False
for ind in self._toolbox.project_item_model.leaf_indexes():
item_name = self._toolbox.project_item_model.item(ind).name
cmd = QItemSelectionModel.Select if item_name in selected_item_names else QItemSelectionModel.Deselect
self._toolbox.ui.treeView_project.selectionModel().select(ind, cmd)
self._toolbox.sync_item_selection_with_scene = True
# Make last item selected the current index in project tree view
if project_item_icons:
last_ind = self._toolbox.project_item_model.find_item(project_item_icons[-1].name())
self._toolbox.ui.treeView_project.selectionModel().setCurrentIndex(last_ind, QItemSelectionModel.NoUpdate)
[docs] def set_bg_color(self, color):
"""Change background color when this is changed in Settings.
Args:
color (QColor): Background color
"""
self.bg_color = color
[docs] def set_bg_choice(self, bg_choice):
"""Set background choice when this is changed in Settings.
Args:
bg (str): "grid", "tree", or "solid"
"""
self.bg_choice = bg_choice
[docs] def dragLeaveEvent(self, event):
"""Accept event."""
event.accept()
[docs] def dragEnterEvent(self, event):
"""Accept event. Then call the super class method
only if drag source is not a ProjectItemDragMixin."""
source = event.source()
event.setAccepted(isinstance(source, ProjectItemDragMixin))
[docs] def dragMoveEvent(self, event):
"""Accept event. Then call the super class method
only if drag source is not a ProjectItemDragMixin."""
source = event.source()
event.setAccepted(isinstance(source, ProjectItemDragMixin))
[docs] def dropEvent(self, event):
"""Only accept drops when the source is an instance of ProjectItemDragMixin.
Capture text from event's mimedata and show the appropriate 'Add Item form.'
"""
source = event.source()
if not isinstance(source, ProjectItemDragMixin):
return
if not self._toolbox.project():
self._toolbox.msg.emit("Please open or create a project first")
event.ignore()
return
event.acceptProposedAction()
item_type, spec = event.mimeData().text().split(",")
pos = event.scenePos()
x = pos.x()
y = pos.y()
factory = self._toolbox.item_factories[item_type]
self.item_shadow = factory.make_icon(self._toolbox)
self.item_shadow.finalize("", x, y)
self.addItem(self.item_shadow)
self._toolbox.show_add_project_item_form(item_type, x, y, spec=spec)
[docs] def event(self, event):
"""Accepts GraphicsSceneHelp events without doing anything, to not interfere with our usage of
QToolTip.showText in graphics_items.ExclamationIcon.
"""
if event.type() == QEvent.GraphicsSceneHelp:
event.accept()
return True
return super().event(event)
[docs] def drawBackground(self, painter, rect):
"""Reimplemented method to make a custom background.
Args:
painter (QPainter): Painter that is used to paint background
rect (QRectF): The exposed (viewport) rectangle in scene coordinates
"""
if self.bg_origin is None:
self.bg_origin = rect.center()
{"solid": self._draw_solid_bg, "grid": self._draw_grid_bg, "tree": self._draw_tree_bg}.get(
self.bg_choice, self._draw_solid_bg
)(painter, rect)
[docs] def _draw_solid_bg(self, painter, rect):
"""Draws solid bg."""
painter.fillRect(rect, QBrush(self.bg_color))
[docs] def _draw_grid_bg(self, painter, rect):
"""Draws grid bg."""
step = round(ProjectItemIcon.ITEM_EXTENT / 3) # Grid step
painter.setPen(QPen(self.bg_color))
delta = rect.topLeft() - self.bg_origin
x_start = round(delta.x() / step)
y_start = round(delta.y() / step)
x_stop = x_start + round(rect.width() / step) + 1
y_stop = y_start + round(rect.height() / step) + 1
for i in range(x_start, x_stop):
x = step * i
painter.drawLine(x, rect.top(), x, rect.bottom())
for j in range(y_start, y_stop):
y = step * j
painter.drawLine(rect.left(), y, rect.right(), y)
painter.setPen(QPen(self.bg_color.darker(110)))
painter.drawLine(self.bg_origin.x(), rect.top(), self.bg_origin.x(), rect.bottom())
painter.drawLine(rect.left(), self.bg_origin.y(), rect.right(), self.bg_origin.y())
[docs] def _draw_tree_bg(self, painter, rect):
"""Draws 'tree of life' bg."""
painter.setPen(QPen(self.bg_color))
radius = ProjectItemIcon.ITEM_EXTENT
dx = math.sin(math.pi / 3) * radius
dy = math.cos(math.pi / 3) * radius
delta = rect.topLeft() - self.bg_origin
x_start = round(delta.x() / dx)
y_start = round(delta.y() / radius)
x_stop = x_start + round(rect.width() / dx) + 1
y_stop = y_start + round(rect.height() / radius) + 1
for i in range(x_start, x_stop):
ref = QPointF(i * dx, (i & 1) * dy)
for j in range(y_start, y_stop):
painter.drawEllipse(ref + QPointF(0, j * radius), radius, radius)
painter.setPen(QPen(self.bg_color.darker(110)))
painter.drawEllipse(self.bg_origin, 2 * radius, 2 * radius)