Rectangles in incons

This commit is contained in:
2026-07-27 14:03:06 +02:00
parent 09a9e3f12f
commit 035cbdf53f
8 changed files with 403 additions and 47 deletions

View File

@@ -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)