Rectangles in incons
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
@@ -15,8 +16,22 @@ from bedit_gui.documents import Document
|
||||
from bedit_gui.services.application_settings import ApplicationSettings
|
||||
from bedit_gui.views.main_window import MainWindow
|
||||
|
||||
def parse_arguments():
|
||||
parser = argparse.ArgumentParser(exit_on_error=False)
|
||||
|
||||
parser.add_argument(
|
||||
"-f", "--file", type=str, help="Path to a file to open", default=None, required=False
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
args = parse_arguments()
|
||||
except argparse.ArgumentError as e:
|
||||
print(e.message)
|
||||
return 1
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
app.setOrganizationName("BEdit")
|
||||
@@ -36,7 +51,11 @@ def main() -> int:
|
||||
window_state_controller = WindowStateController(app, window)
|
||||
window_state_controller.restore()
|
||||
|
||||
document.new()
|
||||
if args.file:
|
||||
document.open(args.file)
|
||||
else:
|
||||
document.new()
|
||||
|
||||
window.showMaximized()
|
||||
|
||||
return app.exec()
|
||||
|
||||
@@ -45,13 +45,13 @@ class Rectangle(Shape):
|
||||
line_type: LineType = LineType.SOLID
|
||||
line_thickness: float = 1.0
|
||||
corner_radius: float = 0.0
|
||||
line_color: str = "#000000"
|
||||
fill_color: str = "#ffffff"
|
||||
line_color: str = "#000000ff"
|
||||
fill_color: str = "#ffffff00"
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data: Mapping[str, Any]) -> Rectangle:
|
||||
pos = data.get("pos", [0, 0])
|
||||
return cls(layer=int(data.get("layer", 0)), pos=(int(pos[0]), int(pos[1])), width=float(data.get("width", 100.0)), height=float(data.get("height", 100.0)), line_type=LineType(data.get("line_type", "solid")), line_thickness=float(data.get("line_thickness", 1.0)), corner_radius=float(data.get("corner_radius", 0.0)), line_color=str(data.get("line_color", "#000000")), fill_color=str(data.get("fill_color", "#ffffff")))
|
||||
return cls(layer=int(data.get("layer", 0)), pos=(int(pos[0]), int(pos[1])), width=float(data.get("width", 100.0)), height=float(data.get("height", 100.0)), line_type=LineType(data.get("line_type", "solid")), line_thickness=float(data.get("line_thickness", 1.0)), corner_radius=float(data.get("corner_radius", 0.0)), line_color=str(data.get("line_color", "#000000ff")), fill_color=str(data.get("fill_color", "#ffffff00")))
|
||||
|
||||
def to_data(self) -> dict[str, Any]:
|
||||
return {**super().to_data(), "width": self.width, "height": self.height, "line_type": self.line_type.value, "line_thickness": self.line_thickness, "corner_radius": self.corner_radius, "line_color": self.line_color, "fill_color": self.fill_color}
|
||||
|
||||
42
src/bedit_gui/views/color_button.py
Normal file
42
src/bedit_gui/views/color_button.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtGui import QColor
|
||||
from PySide6.QtWidgets import QColorDialog, QPushButton, QWidget
|
||||
|
||||
|
||||
class ColorButton(QPushButton):
|
||||
def __init__(self, color: str, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setObjectName("colorButton")
|
||||
self._color = self._from_rgba(color)
|
||||
self.clicked.connect(self._select_color)
|
||||
self._update_display()
|
||||
|
||||
def color(self) -> str:
|
||||
return f"#{self._color.red():02x}{self._color.green():02x}{self._color.blue():02x}{self._color.alpha():02x}"
|
||||
|
||||
def set_color(self, color: str) -> None:
|
||||
self._color = self._from_rgba(color)
|
||||
self._update_display()
|
||||
|
||||
def _select_color(self) -> None:
|
||||
parent = self.window()
|
||||
dialog = QColorDialog(self._color, parent if parent is not self else None)
|
||||
dialog.setOption(QColorDialog.ColorDialogOption.ShowAlphaChannel)
|
||||
if dialog.exec() == QColorDialog.DialogCode.Accepted:
|
||||
self._color = dialog.selectedColor()
|
||||
self._update_display()
|
||||
|
||||
def _update_display(self) -> None:
|
||||
self.setText(self.color())
|
||||
foreground = "#000000" if self._color.lightness() > 127 or self._color.alpha() < 128 else "#ffffff"
|
||||
self.setStyleSheet(f"QPushButton#colorButton {{ background-color: rgba({self._color.red()}, {self._color.green()}, {self._color.blue()}, {self._color.alpha()}); color: {foreground}; }}")
|
||||
|
||||
@staticmethod
|
||||
def _from_rgba(value: str) -> QColor:
|
||||
color = value.removeprefix("#")
|
||||
if len(color) == 6:
|
||||
color += "ff"
|
||||
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)
|
||||
@@ -3,12 +3,13 @@ from __future__ import annotations
|
||||
from copy import deepcopy
|
||||
|
||||
from PySide6.QtCore import QEvent, QObject, Qt, Signal
|
||||
from PySide6.QtGui import QKeySequence, QPainter, QShortcut, QUndoCommand, QUndoStack, QWheelEvent
|
||||
from PySide6.QtWidgets import QGraphicsView, QMainWindow, QWidget
|
||||
from PySide6.QtGui import QKeySequence, QPainter, QShortcut, QUndoCommand, QUndoStack, QWheelEvent, QShowEvent
|
||||
from PySide6.QtWidgets import QDialog, QGraphicsView, QMainWindow, QWidget
|
||||
|
||||
from bedit_gui.models import Icon, Shape, ShapeID
|
||||
from bedit_gui.ui.generated.ui_icon_editor_window import Ui_iconEditor
|
||||
from bedit_gui.views.icon_graphics_scene import IconGraphicsScene, RectangleCreationTool
|
||||
from bedit_gui.views.shape_options_dialog import ShapeOptionsDialog
|
||||
|
||||
|
||||
class ChangeIconDraftCommand(QUndoCommand):
|
||||
@@ -39,6 +40,35 @@ class AddShapeCommand(QUndoCommand):
|
||||
self.editor._remove_shape(self.shape_id)
|
||||
|
||||
|
||||
class ChangeShapeCommand(QUndoCommand):
|
||||
def __init__(self, editor: IconEditorWindow, shape_id: ShapeID, old_shape: Shape, new_shape: Shape) -> None:
|
||||
super().__init__(f"Change {new_shape.type}")
|
||||
self.editor = editor
|
||||
self.shape_id = shape_id
|
||||
self.old_shape = deepcopy(old_shape)
|
||||
self.new_shape = deepcopy(new_shape)
|
||||
|
||||
def redo(self) -> None:
|
||||
self.editor._change_shape(self.shape_id, self.new_shape)
|
||||
|
||||
def undo(self) -> None:
|
||||
self.editor._change_shape(self.shape_id, self.old_shape)
|
||||
|
||||
|
||||
class DeleteShapesCommand(QUndoCommand):
|
||||
def __init__(self, editor: IconEditorWindow, shapes: dict[ShapeID, Shape]) -> None:
|
||||
text = "Delete shape" if len(shapes) == 1 else "Delete shapes"
|
||||
super().__init__(text)
|
||||
self.editor = editor
|
||||
self.shapes = deepcopy(shapes)
|
||||
|
||||
def redo(self) -> None:
|
||||
self.editor._remove_shapes(self.shapes)
|
||||
|
||||
def undo(self) -> None:
|
||||
self.editor._restore_shapes(self.shapes)
|
||||
|
||||
|
||||
class IconEditorWindow(QMainWindow):
|
||||
"""Independent icon editing session with its own undo stack."""
|
||||
|
||||
@@ -60,6 +90,8 @@ class IconEditorWindow(QMainWindow):
|
||||
self.scene = IconGraphicsScene(self)
|
||||
self.scene.set_icon(self._icon)
|
||||
self.scene.shape_created.connect(self._shape_created)
|
||||
self.scene.shape_changed.connect(self._shape_changed)
|
||||
self.scene.shape_options_requested.connect(self._show_shape_options)
|
||||
self.scene.tool_active_changed.connect(self._tool_active_changed)
|
||||
self.ui.graphicsView.setScene(self.scene)
|
||||
self.ui.graphicsView.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
@@ -69,6 +101,8 @@ class IconEditorWindow(QMainWindow):
|
||||
self.ui.actionAdd_Rectangle.triggered.connect(self._start_rectangle_tool)
|
||||
self.cancel_tool_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Escape), self)
|
||||
self.cancel_tool_shortcut.activated.connect(self.scene.cancel_creation_tool)
|
||||
self.delete_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Delete), self)
|
||||
self.delete_shortcut.activated.connect(self._delete_selected_shapes)
|
||||
self.zoom_in_shortcut = QShortcut(QKeySequence("Ctrl++"), self)
|
||||
self.zoom_in_alt_shortcut = QShortcut(QKeySequence("Ctrl+="), self)
|
||||
self.zoom_out_shortcut = QShortcut(QKeySequence("Ctrl+-"), self)
|
||||
@@ -90,6 +124,10 @@ class IconEditorWindow(QMainWindow):
|
||||
self.ui.actionUndo.setEnabled(False)
|
||||
self.ui.actionRedo.setEnabled(False)
|
||||
|
||||
def showEvent(self, event: QShowEvent) -> None:
|
||||
super().showEvent(event)
|
||||
self.fit_scene()
|
||||
|
||||
def icon(self) -> Icon:
|
||||
return deepcopy(self._icon)
|
||||
|
||||
@@ -107,7 +145,10 @@ class IconEditorWindow(QMainWindow):
|
||||
self._zoom(1 / self.zoom_step)
|
||||
|
||||
def reset_zoom(self) -> None:
|
||||
self.ui.graphicsView.resetTransform()
|
||||
self.fit_scene()
|
||||
|
||||
def fit_scene(self) -> None:
|
||||
self.ui.graphicsView.fitInView(self.scene.sceneRect(), Qt.AspectRatioMode.KeepAspectRatio)
|
||||
|
||||
def eventFilter(self, watched: QObject, event: QEvent) -> bool:
|
||||
if watched is self.ui.graphicsView.viewport() and isinstance(event, QWheelEvent):
|
||||
@@ -140,9 +181,44 @@ class IconEditorWindow(QMainWindow):
|
||||
self.scene.set_icon(self._icon)
|
||||
self.icon_changed.emit(self.icon())
|
||||
|
||||
def _remove_shapes(self, shapes: dict[ShapeID, Shape]) -> None:
|
||||
for shape_id in shapes:
|
||||
self._icon.shapes.pop(shape_id, None)
|
||||
self.scene.set_icon(self._icon)
|
||||
self.icon_changed.emit(self.icon())
|
||||
|
||||
def _restore_shapes(self, shapes: dict[ShapeID, Shape]) -> None:
|
||||
self._icon.shapes.update(deepcopy(shapes))
|
||||
self.scene.set_icon(self._icon)
|
||||
self.icon_changed.emit(self.icon())
|
||||
|
||||
def _change_shape(self, shape_id: ShapeID, shape: Shape) -> None:
|
||||
self._icon.shapes[shape_id] = deepcopy(shape)
|
||||
self.scene.set_icon(self._icon)
|
||||
self.icon_changed.emit(self.icon())
|
||||
|
||||
def _shape_created(self, shape: Shape) -> None:
|
||||
self.undo_stack.push(AddShapeCommand(self, ShapeID(), shape))
|
||||
|
||||
def _shape_changed(self, shape_id: ShapeID, old_shape: Shape, new_shape: Shape) -> None:
|
||||
self.undo_stack.push(ChangeShapeCommand(self, shape_id, old_shape, new_shape))
|
||||
|
||||
def _delete_selected_shapes(self) -> None:
|
||||
shape_ids = self.scene.selected_shape_ids()
|
||||
shapes = {shape_id: self._icon.shapes[shape_id] for shape_id in shape_ids}
|
||||
if shapes:
|
||||
self.undo_stack.push(DeleteShapesCommand(self, shapes))
|
||||
|
||||
def _show_shape_options(self, shape_id: ShapeID) -> None:
|
||||
old_shape = self._icon.shapes.get(shape_id)
|
||||
if old_shape is None:
|
||||
return
|
||||
dialog = ShapeOptionsDialog(old_shape, self.scene.sceneRect(), self)
|
||||
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||
new_shape = dialog.shape()
|
||||
if new_shape != old_shape:
|
||||
self.undo_stack.push(ChangeShapeCommand(self, shape_id, old_shape, new_shape))
|
||||
|
||||
def _start_rectangle_tool(self) -> None:
|
||||
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
|
||||
self.scene.set_creation_tool(RectangleCreationTool(self.scene, layer))
|
||||
|
||||
@@ -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)
|
||||
|
||||
81
src/bedit_gui/views/shape_options_dialog.py
Normal file
81
src/bedit_gui/views/shape_options_dialog.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
from PySide6.QtCore import QRectF
|
||||
from PySide6.QtWidgets import QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox, QFormLayout, QLabel, QSpinBox, QVBoxLayout, QWidget
|
||||
|
||||
from bedit_gui.models import LineType, Rectangle, Shape
|
||||
from bedit_gui.views.color_button import ColorButton
|
||||
|
||||
|
||||
class ShapeOptionsDialog(QDialog):
|
||||
def __init__(self, shape: Shape, scene_rect: QRectF, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._shape = deepcopy(shape)
|
||||
self._scene_rect = scene_rect
|
||||
self.setWindowTitle("Shape Options")
|
||||
|
||||
self.form = QFormLayout()
|
||||
self.type_label = QLabel(shape.type or "")
|
||||
self.layer = QSpinBox()
|
||||
self.layer.setRange(-1000000, 1000000)
|
||||
self.layer.setValue(shape.layer)
|
||||
self.x = QSpinBox()
|
||||
self.x.setRange(round(scene_rect.left()), round(scene_rect.right() - 1))
|
||||
self.x.setValue(shape.pos[0])
|
||||
self.y = QSpinBox()
|
||||
self.y.setRange(round(scene_rect.top()), round(scene_rect.bottom() - 1))
|
||||
self.y.setValue(shape.pos[1])
|
||||
self.form.addRow("Type", self.type_label)
|
||||
self.form.addRow("Layer", self.layer)
|
||||
self.form.addRow("X", self.x)
|
||||
self.form.addRow("Y", self.y)
|
||||
|
||||
if isinstance(shape, Rectangle):
|
||||
self._add_rectangle_fields(shape)
|
||||
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
||||
buttons.accepted.connect(self.accept)
|
||||
buttons.rejected.connect(self.reject)
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addLayout(self.form)
|
||||
layout.addWidget(buttons)
|
||||
|
||||
def shape(self) -> Shape:
|
||||
if not isinstance(self._shape, Rectangle):
|
||||
shape = deepcopy(self._shape)
|
||||
shape.layer = self.layer.value()
|
||||
shape.pos = (self.x.value(), self.y.value())
|
||||
return shape
|
||||
|
||||
width = min(self.width.value(), self._scene_rect.right() - self.x.value())
|
||||
height = min(self.height.value(), self._scene_rect.bottom() - self.y.value())
|
||||
return Rectangle(layer=self.layer.value(), pos=(self.x.value(), self.y.value()), width=width, height=height, line_type=self.line_type.currentData(), line_thickness=self.line_thickness.value(), corner_radius=self.corner_radius.value(), line_color=self.line_color.color(), fill_color=self.fill_color.color())
|
||||
|
||||
def _add_rectangle_fields(self, shape: Rectangle) -> None:
|
||||
self.width = QSpinBox()
|
||||
self.width.setRange(1, round(self._scene_rect.width()))
|
||||
self.width.setValue(round(shape.width))
|
||||
self.height = QSpinBox()
|
||||
self.height.setRange(1, round(self._scene_rect.height()))
|
||||
self.height.setValue(round(shape.height))
|
||||
self.line_type = QComboBox()
|
||||
for line_type in LineType:
|
||||
self.line_type.addItem(line_type.value.replace("_", " ").title(), line_type)
|
||||
self.line_type.setCurrentIndex(self.line_type.findData(shape.line_type))
|
||||
self.line_thickness = QDoubleSpinBox()
|
||||
self.line_thickness.setRange(0, 1000)
|
||||
self.line_thickness.setValue(shape.line_thickness)
|
||||
self.corner_radius = QDoubleSpinBox()
|
||||
self.corner_radius.setRange(0, max(self._scene_rect.width(), self._scene_rect.height()))
|
||||
self.corner_radius.setValue(shape.corner_radius)
|
||||
self.line_color = ColorButton(shape.line_color)
|
||||
self.fill_color = ColorButton(shape.fill_color)
|
||||
self.form.addRow("Width", self.width)
|
||||
self.form.addRow("Height", self.height)
|
||||
self.form.addRow("Line type", self.line_type)
|
||||
self.form.addRow("Line thickness", self.line_thickness)
|
||||
self.form.addRow("Corner radius", self.corner_radius)
|
||||
self.form.addRow("Line color", self.line_color)
|
||||
self.form.addRow("Fill color", self.fill_color)
|
||||
Reference in New Issue
Block a user