Rectangles in incons
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from copy import deepcopy
|
||||
from typing import Protocol
|
||||
|
||||
from PySide6.QtCore import QObject, QPointF, QRectF, Qt, Signal
|
||||
from PySide6.QtGui import QBrush, QColor, QPainterPath, QPen
|
||||
from PySide6.QtWidgets import QGraphicsPathItem, QGraphicsRectItem, QGraphicsScene, QGraphicsSceneMouseEvent
|
||||
from PySide6.QtGui import QBrush, QColor, QPainter, QPainterPath, QPen
|
||||
from PySide6.QtWidgets import QGraphicsItem, QGraphicsPathItem, QGraphicsRectItem, QGraphicsScene, QGraphicsSceneContextMenuEvent, QGraphicsSceneMouseEvent, QMenu, QStyleOptionGraphicsItem, QWidget
|
||||
|
||||
from bedit_gui.models import Icon, LineType, Rectangle, Shape
|
||||
from bedit_gui.models import Icon, LineType, Rectangle, Shape, ShapeID
|
||||
|
||||
|
||||
class ShapeCreationTool(Protocol):
|
||||
@@ -24,16 +26,19 @@ class RectangleCreationTool:
|
||||
self.preview: QGraphicsRectItem | None = None
|
||||
|
||||
def begin(self, position: QPointF) -> None:
|
||||
position = self._bounded(position)
|
||||
self.start = position
|
||||
self.preview = self.scene.addRect(QRectF(position, position), QPen(Qt.PenStyle.DashLine))
|
||||
|
||||
def update(self, position: QPointF) -> None:
|
||||
if self.preview is not None and self.start is not None:
|
||||
position = self._bounded(position)
|
||||
self.preview.setRect(QRectF(self.start, position).normalized())
|
||||
|
||||
def finish(self, position: QPointF) -> Rectangle | None:
|
||||
if self.start is None:
|
||||
return None
|
||||
position = self._bounded(position)
|
||||
rect = QRectF(self.start, position).normalized()
|
||||
self.cancel()
|
||||
if rect.width() < 1 or rect.height() < 1:
|
||||
@@ -46,21 +51,143 @@ class RectangleCreationTool:
|
||||
self.preview = None
|
||||
self.start = None
|
||||
|
||||
def _bounded(self, position: QPointF) -> QPointF:
|
||||
rect = self.scene.sceneRect()
|
||||
x = max(rect.left(), min(rect.right(), round(position.x())))
|
||||
y = max(rect.top(), min(rect.bottom(), round(position.y())))
|
||||
return QPointF(x, y)
|
||||
|
||||
|
||||
class ShapeGraphicsItem(QGraphicsPathItem):
|
||||
handle_size = 6.0
|
||||
|
||||
def __init__(self, shape_id: ShapeID, shape: Shape, scene: IconGraphicsScene) -> None:
|
||||
super().__init__()
|
||||
self.shape_id = shape_id
|
||||
self.shape_model = deepcopy(shape)
|
||||
self.icon_scene = scene
|
||||
self._original_shape: Shape | None = None
|
||||
self._resizing = False
|
||||
self.setFlags(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable | QGraphicsItem.GraphicsItemFlag.ItemIsMovable | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
|
||||
|
||||
def resize_handle_rect(self) -> QRectF:
|
||||
size = self.handle_size
|
||||
corner = self.path().boundingRect().bottomRight()
|
||||
return QRectF(corner.x() - size / 2, corner.y() - size / 2, size, size)
|
||||
|
||||
def boundingRect(self) -> QRectF:
|
||||
margin = self.handle_size / 2
|
||||
return super().boundingRect().adjusted(-margin, -margin, margin, margin)
|
||||
|
||||
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
|
||||
self._original_shape = self.current_shape()
|
||||
self._resizing = self.isSelected() and self.resize_handle_rect().contains(event.pos())
|
||||
if self._resizing:
|
||||
event.accept()
|
||||
return
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None:
|
||||
if self._resizing:
|
||||
self.resize_to(event.scenePos())
|
||||
event.accept()
|
||||
return
|
||||
super().mouseMoveEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:
|
||||
if self._resizing:
|
||||
self.resize_to(event.scenePos())
|
||||
self._resizing = False
|
||||
event.accept()
|
||||
else:
|
||||
super().mouseReleaseEvent(event)
|
||||
current = self.current_shape()
|
||||
if self._original_shape is not None and current != self._original_shape:
|
||||
self.icon_scene.shape_changed.emit(self.shape_id, self._original_shape, current)
|
||||
self._original_shape = None
|
||||
|
||||
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None:
|
||||
if not self.isSelected():
|
||||
self.icon_scene.clearSelection()
|
||||
self.setSelected(True)
|
||||
menu = QMenu()
|
||||
options = menu.addAction("Shape Options")
|
||||
if menu.exec(event.screenPos()) is options:
|
||||
self.icon_scene.shape_options_requested.emit(self.shape_id)
|
||||
event.accept()
|
||||
|
||||
def itemChange(self, change: QGraphicsItem.GraphicsItemChange, value: object) -> object:
|
||||
if change is QGraphicsItem.GraphicsItemChange.ItemPositionChange and self.scene() is not None:
|
||||
position = value
|
||||
if isinstance(position, QPointF):
|
||||
bounds = self.icon_scene.sceneRect()
|
||||
size = self.path().boundingRect()
|
||||
x = max(bounds.left(), min(bounds.right() - size.width(), round(position.x())))
|
||||
y = max(bounds.top(), min(bounds.bottom() - size.height(), round(position.y())))
|
||||
return QPointF(x, y)
|
||||
return super().itemChange(change, value)
|
||||
|
||||
def paint(self, painter: QPainter, option: QStyleOptionGraphicsItem, widget: QWidget | None = None) -> None:
|
||||
super().paint(painter, option, widget)
|
||||
if self.isSelected():
|
||||
painter.setPen(QPen(QColor("#ffffff")))
|
||||
painter.setBrush(QBrush(QColor("#2675bf")))
|
||||
painter.drawRect(self.resize_handle_rect())
|
||||
|
||||
def current_shape(self) -> Shape:
|
||||
raise NotImplementedError
|
||||
|
||||
def resize_to(self, position: QPointF) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class RectangleGraphicsItem(ShapeGraphicsItem):
|
||||
def __init__(self, shape_id: ShapeID, shape: Rectangle, scene: IconGraphicsScene) -> None:
|
||||
super().__init__(shape_id, shape, scene)
|
||||
self.rectangle = deepcopy(shape)
|
||||
self.setPos(shape.pos[0], shape.pos[1])
|
||||
self._set_size(shape.width, shape.height)
|
||||
self.setPen(scene._pen(shape))
|
||||
self.setBrush(QBrush(scene._color(shape.fill_color)))
|
||||
self.setZValue(shape.layer)
|
||||
|
||||
def current_shape(self) -> Rectangle:
|
||||
shape = deepcopy(self.rectangle)
|
||||
shape.pos = (round(self.pos().x()), round(self.pos().y()))
|
||||
rect = self.path().boundingRect()
|
||||
shape.width = rect.width()
|
||||
shape.height = rect.height()
|
||||
return shape
|
||||
|
||||
def resize_to(self, position: QPointF) -> None:
|
||||
bounds = self.icon_scene.sceneRect()
|
||||
width = max(1.0, min(bounds.right(), round(position.x())) - self.pos().x())
|
||||
height = max(1.0, min(bounds.bottom(), round(position.y())) - self.pos().y())
|
||||
self._set_size(width, height)
|
||||
|
||||
def _set_size(self, width: float, height: float) -> None:
|
||||
self.prepareGeometryChange()
|
||||
path = QPainterPath()
|
||||
path.addRoundedRect(QRectF(0, 0, width, height), self.rectangle.corner_radius, self.rectangle.corner_radius)
|
||||
self.setPath(path)
|
||||
|
||||
|
||||
class IconGraphicsScene(QGraphicsScene):
|
||||
shape_created = Signal(object)
|
||||
shape_changed = Signal(object, object, object)
|
||||
shape_options_requested = Signal(object)
|
||||
tool_active_changed = Signal(bool)
|
||||
|
||||
def __init__(self, parent: QObject | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._tool: ShapeCreationTool | None = None
|
||||
self.setSceneRect(-100, -100, 200, 200)
|
||||
self.setSceneRect(-64, -64, 128, 128)
|
||||
|
||||
def set_icon(self, icon: Icon) -> None:
|
||||
self.cancel_creation_tool()
|
||||
self.clear()
|
||||
for shape in sorted(icon.shapes.values(), key=lambda item: item.layer):
|
||||
self._add_shape_item(shape)
|
||||
for shape_id, shape in sorted(icon.shapes.items(), key=lambda item: item[1].layer):
|
||||
self._add_shape_item(shape_id, shape)
|
||||
|
||||
def set_creation_tool(self, tool: ShapeCreationTool) -> None:
|
||||
self.cancel_creation_tool()
|
||||
@@ -77,6 +204,29 @@ class IconGraphicsScene(QGraphicsScene):
|
||||
def has_creation_tool(self) -> bool:
|
||||
return self._tool is not None
|
||||
|
||||
def selected_shape_ids(self) -> list[ShapeID]:
|
||||
return [item.shape_id for item in self.selectedItems() if isinstance(item, ShapeGraphicsItem)]
|
||||
|
||||
def drawBackground(self, painter: QPainter, rect: QRectF) -> None:
|
||||
super().drawBackground(painter, rect)
|
||||
rect = rect.intersected(self.sceneRect())
|
||||
if rect.isEmpty():
|
||||
return
|
||||
pen = QPen(QColor("#d0d0d0"))
|
||||
pen.setCosmetic(True)
|
||||
painter.setPen(pen)
|
||||
|
||||
first_x = math.floor(rect.left() / 8) * 8
|
||||
last_x = math.ceil(rect.right() / 8) * 8
|
||||
first_y = math.floor(rect.top() / 8) * 8
|
||||
last_y = math.ceil(rect.bottom() / 8) * 8
|
||||
|
||||
for x in range(first_x, last_x + 1, 8):
|
||||
painter.drawLine(QPointF(x, rect.top()), QPointF(x, rect.bottom()))
|
||||
|
||||
for y in range(first_y, last_y + 1, 8):
|
||||
painter.drawLine(QPointF(rect.left(), y), QPointF(rect.right(), y))
|
||||
|
||||
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
|
||||
if self._tool is not None and event.button() == Qt.MouseButton.LeftButton:
|
||||
self._tool.begin(event.scenePos())
|
||||
@@ -103,21 +253,21 @@ class IconGraphicsScene(QGraphicsScene):
|
||||
return
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
def _add_shape_item(self, shape: Shape) -> None:
|
||||
def _add_shape_item(self, shape_id: ShapeID, shape: Shape) -> None:
|
||||
if not isinstance(shape, Rectangle):
|
||||
return
|
||||
rect = QRectF(shape.pos[0], shape.pos[1], shape.width, shape.height)
|
||||
path = QPainterPath()
|
||||
path.addRoundedRect(rect, shape.corner_radius, shape.corner_radius)
|
||||
item = QGraphicsPathItem(path)
|
||||
item.setPen(self._pen(shape))
|
||||
item.setBrush(QBrush(QColor(shape.fill_color)))
|
||||
item.setZValue(shape.layer)
|
||||
self.addItem(item)
|
||||
self.addItem(RectangleGraphicsItem(shape_id, shape, self))
|
||||
|
||||
@staticmethod
|
||||
def _pen(shape: Rectangle) -> QPen:
|
||||
styles = {LineType.SOLID: Qt.PenStyle.SolidLine, LineType.DASHED: Qt.PenStyle.DashLine, LineType.DOTTED: Qt.PenStyle.DotLine, LineType.DASH_DOT: Qt.PenStyle.DashDotLine}
|
||||
if shape.line_type is LineType.NONE:
|
||||
return QPen(Qt.PenStyle.NoPen)
|
||||
return QPen(QColor(shape.line_color), shape.line_thickness, styles[shape.line_type])
|
||||
return QPen(IconGraphicsScene._color(shape.line_color), shape.line_thickness, styles[shape.line_type])
|
||||
|
||||
@staticmethod
|
||||
def _color(value: str) -> QColor:
|
||||
color = value.removeprefix("#")
|
||||
if len(color) == 8:
|
||||
return QColor(int(color[0:2], 16), int(color[2:4], 16), int(color[4:6], 16), int(color[6:8], 16))
|
||||
return QColor(value)
|
||||
|
||||
Reference in New Issue
Block a user