Line in icon tool

This commit is contained in:
2026-07-27 14:40:48 +02:00
parent c68e693359
commit e5968e165e
5 changed files with 187 additions and 22 deletions

View File

@@ -24,6 +24,8 @@ class Shape:
return Rectangle.from_data(data) return Rectangle.from_data(data)
if cls is Shape and data.get("type") == "text": if cls is Shape and data.get("type") == "text":
return Text.from_data(data) return Text.from_data(data)
if cls is Shape and data.get("type") == "line":
return Line.from_data(data)
pos = data.get("pos", [0, 0]) pos = data.get("pos", [0, 0])
return cls(layer=int(data.get("layer", 0)), type=data.get("type"), pos=(int(pos[0]), int(pos[1]))) return cls(layer=int(data.get("layer", 0)), type=data.get("type"), pos=(int(pos[0]), int(pos[1])))
@@ -39,6 +41,24 @@ class LineType(Enum):
DASH_DOT = "dash_dot" DASH_DOT = "dash_dot"
@dataclass
class Line(Shape):
type: str = field(init=False, default="line")
end: tuple[int, int] = (32, 0)
line_type: LineType = LineType.SOLID
line_thickness: float = 1.0
line_color: str = "#000000ff"
@classmethod
def from_data(cls, data: Mapping[str, Any]) -> Line:
pos = data.get("pos", [0, 0])
end = data.get("end", [32, 0])
return cls(layer=int(data.get("layer", 0)), pos=(int(pos[0]), int(pos[1])), end=(int(end[0]), int(end[1])), line_type=LineType(data.get("line_type", "solid")), line_thickness=float(data.get("line_thickness", 1.0)), line_color=str(data.get("line_color", "#000000ff")))
def to_data(self) -> dict[str, Any]:
return {**super().to_data(), "end": list(self.end), "line_type": self.line_type.value, "line_thickness": self.line_thickness, "line_color": self.line_color}
@dataclass @dataclass
class Rectangle(Shape): class Rectangle(Shape):
type: str = field(init=False, default="rectangle") type: str = field(init=False, default="rectangle")

View File

@@ -81,6 +81,7 @@
</attribute> </attribute>
<addaction name="actionAdd_Rectangle"/> <addaction name="actionAdd_Rectangle"/>
<addaction name="actionAdd_Text"/> <addaction name="actionAdd_Text"/>
<addaction name="actionAdd_Line"/>
</widget> </widget>
<action name="actionUndo"> <action name="actionUndo">
<property name="icon"> <property name="icon">
@@ -220,6 +221,21 @@
<enum>QAction::MenuRole::NoRole</enum> <enum>QAction::MenuRole::NoRole</enum>
</property> </property>
</action> </action>
<action name="actionAdd_Line">
<property name="icon">
<iconset resource="../../resources/resources.qrc">
<normaloff>:/icons/icons/draw-path.png</normaloff>:/icons/icons/draw-path.png</iconset>
</property>
<property name="text">
<string>Add Line</string>
</property>
<property name="toolTip">
<string>Add a line</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
</widget> </widget>
<resources> <resources>
<include location="../../resources/resources.qrc"/> <include location="../../resources/resources.qrc"/>

View File

@@ -11,7 +11,7 @@ from bedit_gui.models import Icon, Shape, ShapeID
from bedit_gui.services import icon_files from bedit_gui.services import icon_files
from bedit_gui.services.application_logging import get_logger from bedit_gui.services.application_logging import get_logger
from bedit_gui.ui.generated.ui_icon_editor_window import Ui_iconEditor from bedit_gui.ui.generated.ui_icon_editor_window import Ui_iconEditor
from bedit_gui.views.icon_graphics_scene import IconGraphicsScene, RectangleCreationTool, TextCreationTool from bedit_gui.views.icon_graphics_scene import IconGraphicsScene, LineCreationTool, RectangleCreationTool, TextCreationTool
from bedit_gui.views.shape_options_dialog import ShapeOptionsDialog from bedit_gui.views.shape_options_dialog import ShapeOptionsDialog
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -125,6 +125,8 @@ class IconEditorWindow(QMainWindow):
self.ui.actionAdd_Rectangle.triggered.connect(self._start_rectangle_tool) self.ui.actionAdd_Rectangle.triggered.connect(self._start_rectangle_tool)
self.ui.actionAdd_Text.setCheckable(True) self.ui.actionAdd_Text.setCheckable(True)
self.ui.actionAdd_Text.triggered.connect(self._start_text_tool) self.ui.actionAdd_Text.triggered.connect(self._start_text_tool)
self.ui.actionAdd_Line.setCheckable(True)
self.ui.actionAdd_Line.triggered.connect(self._start_line_tool)
self.cancel_tool_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Escape), self) self.cancel_tool_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Escape), self)
self.cancel_tool_shortcut.activated.connect(self.scene.cancel_creation_tool) self.cancel_tool_shortcut.activated.connect(self.scene.cancel_creation_tool)
self.delete_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Delete), self) self.delete_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Delete), self)
@@ -305,17 +307,29 @@ class IconEditorWindow(QMainWindow):
def _start_rectangle_tool(self) -> None: def _start_rectangle_tool(self) -> None:
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1 layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
self.ui.actionAdd_Text.setChecked(False) self.ui.actionAdd_Text.setChecked(False)
self.ui.actionAdd_Line.setChecked(False)
self.scene.set_creation_tool(RectangleCreationTool(self.scene, layer)) self.scene.set_creation_tool(RectangleCreationTool(self.scene, layer))
self.ui.actionAdd_Rectangle.setChecked(True)
def _start_text_tool(self) -> None: def _start_text_tool(self) -> None:
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1 layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
self.ui.actionAdd_Rectangle.setChecked(False) self.ui.actionAdd_Rectangle.setChecked(False)
self.ui.actionAdd_Line.setChecked(False)
self.scene.set_creation_tool(TextCreationTool(self.scene, layer)) self.scene.set_creation_tool(TextCreationTool(self.scene, layer))
self.ui.actionAdd_Text.setChecked(True)
def _start_line_tool(self) -> None:
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
self.ui.actionAdd_Rectangle.setChecked(False)
self.ui.actionAdd_Text.setChecked(False)
self.scene.set_creation_tool(LineCreationTool(self.scene, layer))
self.ui.actionAdd_Line.setChecked(True)
def _tool_active_changed(self, active: bool) -> None: def _tool_active_changed(self, active: bool) -> None:
if not active: if not active:
self.ui.actionAdd_Rectangle.setChecked(False) self.ui.actionAdd_Rectangle.setChecked(False)
self.ui.actionAdd_Text.setChecked(False) self.ui.actionAdd_Text.setChecked(False)
self.ui.actionAdd_Line.setChecked(False)
cursor = Qt.CursorShape.CrossCursor if active else Qt.CursorShape.ArrowCursor cursor = Qt.CursorShape.CrossCursor if active else Qt.CursorShape.ArrowCursor
self.ui.graphicsView.viewport().setCursor(cursor) self.ui.graphicsView.viewport().setCursor(cursor)

View File

@@ -4,12 +4,12 @@ import math
from copy import deepcopy from copy import deepcopy
from typing import Protocol from typing import Protocol
from PySide6.QtCore import QObject, QPointF, QRectF, Qt, Signal from PySide6.QtCore import QLineF, QObject, QPointF, QRectF, Qt, Signal
from PySide6.QtGui import QBrush, QColor, QFont, QPainter, QPainterPath, QPen from PySide6.QtGui import QBrush, QColor, QFont, QPainter, QPainterPath, QPen
from PySide6.QtWidgets import QGraphicsItem, QGraphicsPathItem, QGraphicsRectItem, QGraphicsScene, QGraphicsSceneContextMenuEvent, QGraphicsSceneMouseEvent, QMenu, QStyleOptionGraphicsItem, QWidget from PySide6.QtWidgets import QGraphicsItem, QGraphicsLineItem, QGraphicsPathItem, QGraphicsRectItem, QGraphicsScene, QGraphicsSceneContextMenuEvent, QGraphicsSceneMouseEvent, QMenu, QStyleOptionGraphicsItem, QWidget
from bedit_core.models import Port, PortID, SignalDirection from bedit_core.models import Port, PortID, SignalDirection
from bedit_gui.models import Icon, LineType, Rectangle, Shape, ShapeID, Text from bedit_gui.models import Icon, Line, LineType, Rectangle, Shape, ShapeID, Text
class ShapeCreationTool(Protocol): class ShapeCreationTool(Protocol):
@@ -67,6 +67,45 @@ class TextCreationTool(RectangleCreationTool):
return Text(layer=self.layer, pos=(round(rect.x()), round(rect.y())), width=rect.width(), height=rect.height(), text="Text") return Text(layer=self.layer, pos=(round(rect.x()), round(rect.y())), width=rect.width(), height=rect.height(), text="Text")
class LineCreationTool:
def __init__(self, scene: QGraphicsScene, layer: int) -> None:
self.scene = scene
self.layer = layer
self.start: QPointF | None = None
self.preview: QGraphicsLineItem | None = None
def begin(self, position: QPointF) -> None:
position = self._bounded(position)
self.start = position
self.preview = self.scene.addLine(QLineF(position, position), QPen(Qt.PenStyle.DashLine))
def update(self, position: QPointF) -> None:
if self.preview is not None and self.start is not None:
self.preview.setLine(QLineF(self.start, self._bounded(position)))
def finish(self, position: QPointF) -> Shape | None:
if self.start is None:
return None
start = self.start
end = self._bounded(position)
self.cancel()
if start == end:
return None
return Line(layer=self.layer, pos=(round(start.x()), round(start.y())), end=(round(end.x()), round(end.y())))
def cancel(self) -> None:
if self.preview is not None:
self.scene.removeItem(self.preview)
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): class ShapeGraphicsItem(QGraphicsPathItem):
handle_size = 6.0 handle_size = 6.0
@@ -76,7 +115,7 @@ class ShapeGraphicsItem(QGraphicsPathItem):
self.shape_model = deepcopy(shape) self.shape_model = deepcopy(shape)
self.icon_scene = scene self.icon_scene = scene
self._original_shape: Shape | None = None self._original_shape: Shape | None = None
self._resizing = False self._resize_handle: str | None = None
self.setFlags(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable | QGraphicsItem.GraphicsItemFlag.ItemIsMovable | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges) self.setFlags(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable | QGraphicsItem.GraphicsItemFlag.ItemIsMovable | QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
def resize_handle_rect(self) -> QRectF: def resize_handle_rect(self) -> QRectF:
@@ -84,29 +123,32 @@ class ShapeGraphicsItem(QGraphicsPathItem):
corner = self.path().boundingRect().bottomRight() corner = self.path().boundingRect().bottomRight()
return QRectF(corner.x() - size / 2, corner.y() - size / 2, size, size) return QRectF(corner.x() - size / 2, corner.y() - size / 2, size, size)
def resize_handles(self) -> dict[str, QRectF]:
return {"size": self.resize_handle_rect()}
def boundingRect(self) -> QRectF: def boundingRect(self) -> QRectF:
margin = self.handle_size / 2 margin = self.handle_size / 2
return super().boundingRect().adjusted(-margin, -margin, margin, margin) return super().boundingRect().adjusted(-margin, -margin, margin, margin)
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
self._original_shape = self.current_shape() self._original_shape = self.current_shape()
self._resizing = self.isSelected() and self.resize_handle_rect().contains(event.pos()) self._resize_handle = next((name for name, rect in self.resize_handles().items() if self.isSelected() and rect.contains(event.pos())), None)
if self._resizing: if self._resize_handle is not None:
event.accept() event.accept()
return return
super().mousePressEvent(event) super().mousePressEvent(event)
def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None: def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None:
if self._resizing: if self._resize_handle is not None:
self.resize_to(event.scenePos()) self.resize_to(event.scenePos(), self._resize_handle)
event.accept() event.accept()
return return
super().mouseMoveEvent(event) super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:
if self._resizing: if self._resize_handle is not None:
self.resize_to(event.scenePos()) self.resize_to(event.scenePos(), self._resize_handle)
self._resizing = False self._resize_handle = None
event.accept() event.accept()
else: else:
super().mouseReleaseEvent(event) super().mouseReleaseEvent(event)
@@ -130,9 +172,9 @@ class ShapeGraphicsItem(QGraphicsPathItem):
position = value position = value
if isinstance(position, QPointF): if isinstance(position, QPointF):
bounds = self.icon_scene.sceneRect() bounds = self.icon_scene.sceneRect()
size = self.path().boundingRect() shape_rect = self.path().boundingRect()
x = max(bounds.left(), min(bounds.right() - size.width(), round(position.x()))) x = max(bounds.left() - shape_rect.left(), min(bounds.right() - shape_rect.right(), round(position.x())))
y = max(bounds.top(), min(bounds.bottom() - size.height(), round(position.y()))) y = max(bounds.top() - shape_rect.top(), min(bounds.bottom() - shape_rect.bottom(), round(position.y())))
return QPointF(x, y) return QPointF(x, y)
return super().itemChange(change, value) return super().itemChange(change, value)
@@ -141,12 +183,13 @@ class ShapeGraphicsItem(QGraphicsPathItem):
if self.isSelected(): if self.isSelected():
painter.setPen(QPen(QColor("#ffffff"))) painter.setPen(QPen(QColor("#ffffff")))
painter.setBrush(QBrush(QColor("#2675bf"))) painter.setBrush(QBrush(QColor("#2675bf")))
painter.drawRect(self.resize_handle_rect()) for rect in self.resize_handles().values():
painter.drawRect(rect)
def current_shape(self) -> Shape: def current_shape(self) -> Shape:
raise NotImplementedError raise NotImplementedError
def resize_to(self, position: QPointF) -> None: def resize_to(self, position: QPointF, handle: str) -> None:
raise NotImplementedError raise NotImplementedError
@@ -168,7 +211,7 @@ class RectangleGraphicsItem(ShapeGraphicsItem):
shape.height = rect.height() shape.height = rect.height()
return shape return shape
def resize_to(self, position: QPointF) -> None: def resize_to(self, position: QPointF, _handle: str) -> None:
bounds = self.icon_scene.sceneRect() bounds = self.icon_scene.sceneRect()
width = max(1.0, min(bounds.right(), round(position.x())) - self.pos().x()) 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()) height = max(1.0, min(bounds.bottom(), round(position.y())) - self.pos().y())
@@ -199,7 +242,7 @@ class TextGraphicsItem(ShapeGraphicsItem):
shape.height = rect.height() shape.height = rect.height()
return shape return shape
def resize_to(self, position: QPointF) -> None: def resize_to(self, position: QPointF, _handle: str) -> None:
bounds = self.icon_scene.sceneRect() bounds = self.icon_scene.sceneRect()
width = max(1.0, min(bounds.right(), round(position.x())) - self.pos().x()) 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()) height = max(1.0, min(bounds.bottom(), round(position.y())) - self.pos().y())
@@ -223,6 +266,45 @@ class TextGraphicsItem(ShapeGraphicsItem):
self.setPath(path) self.setPath(path)
class LineGraphicsItem(ShapeGraphicsItem):
def __init__(self, shape_id: ShapeID, shape: Line, scene: IconGraphicsScene) -> None:
super().__init__(shape_id, shape, scene)
self.line = deepcopy(shape)
self._set_points(QPointF(shape.pos[0], shape.pos[1]), QPointF(shape.end[0], shape.end[1]))
self.setPen(scene._line_pen(shape.line_type, shape.line_thickness, shape.line_color))
self.setZValue(shape.layer)
def resize_handles(self) -> dict[str, QRectF]:
size = self.handle_size
start = self.path().pointAtPercent(0)
end = self.path().pointAtPercent(1)
return {"start": QRectF(start.x() - size / 2, start.y() - size / 2, size, size), "end": QRectF(end.x() - size / 2, end.y() - size / 2, size, size)}
def current_shape(self) -> Line:
shape = deepcopy(self.line)
start = self.mapToScene(self.path().pointAtPercent(0))
end = self.mapToScene(self.path().pointAtPercent(1))
shape.pos = (round(start.x()), round(start.y()))
shape.end = (round(end.x()), round(end.y()))
return shape
def resize_to(self, position: QPointF, handle: str) -> None:
bounds = self.icon_scene.sceneRect()
position = QPointF(max(bounds.left(), min(bounds.right(), round(position.x()))), max(bounds.top(), min(bounds.bottom(), round(position.y()))))
start = self.mapToScene(self.path().pointAtPercent(0))
end = self.mapToScene(self.path().pointAtPercent(1))
start = position if handle == "start" else start
end = position if handle == "end" else end
self.setPos(0, 0)
self._set_points(start, end)
def _set_points(self, start: QPointF, end: QPointF) -> None:
self.prepareGeometryChange()
path = QPainterPath(start)
path.lineTo(end)
self.setPath(path)
class PortGraphicsItem(QGraphicsRectItem): class PortGraphicsItem(QGraphicsRectItem):
size = 16.0 size = 16.0
@@ -358,13 +440,19 @@ class IconGraphicsScene(QGraphicsScene):
self.addItem(RectangleGraphicsItem(shape_id, shape, self)) self.addItem(RectangleGraphicsItem(shape_id, shape, self))
elif isinstance(shape, Text): elif isinstance(shape, Text):
self.addItem(TextGraphicsItem(shape_id, shape, self)) self.addItem(TextGraphicsItem(shape_id, shape, self))
elif isinstance(shape, Line):
self.addItem(LineGraphicsItem(shape_id, shape, self))
@staticmethod @staticmethod
def _pen(shape: Rectangle) -> QPen: def _pen(shape: Rectangle) -> QPen:
return IconGraphicsScene._line_pen(shape.line_type, shape.line_thickness, shape.line_color)
@staticmethod
def _line_pen(line_type: LineType, thickness: float, color: str) -> QPen:
styles = {LineType.SOLID: Qt.PenStyle.SolidLine, LineType.DASHED: Qt.PenStyle.DashLine, LineType.DOTTED: Qt.PenStyle.DotLine, LineType.DASH_DOT: Qt.PenStyle.DashDotLine} 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: if line_type is LineType.NONE:
return QPen(Qt.PenStyle.NoPen) return QPen(Qt.PenStyle.NoPen)
return QPen(IconGraphicsScene._color(shape.line_color), shape.line_thickness, styles[shape.line_type]) return QPen(IconGraphicsScene._color(color), thickness, styles[line_type])
@staticmethod @staticmethod
def _color(value: str) -> QColor: def _color(value: str) -> QColor:

View File

@@ -5,7 +5,7 @@ from copy import deepcopy
from PySide6.QtCore import QRectF from PySide6.QtCore import QRectF
from PySide6.QtWidgets import QCheckBox, QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox, QFormLayout, QLabel, QLineEdit, QSpinBox, QVBoxLayout, QWidget from PySide6.QtWidgets import QCheckBox, QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox, QFormLayout, QLabel, QLineEdit, QSpinBox, QVBoxLayout, QWidget
from bedit_gui.models import LineType, Rectangle, Shape, Text from bedit_gui.models import Line, LineType, Rectangle, Shape, Text
from bedit_gui.views.color_button import ColorButton from bedit_gui.views.color_button import ColorButton
@@ -36,6 +36,8 @@ class ShapeOptionsDialog(QDialog):
self._add_rectangle_fields(shape) self._add_rectangle_fields(shape)
elif isinstance(shape, Text): elif isinstance(shape, Text):
self._add_text_fields(shape) self._add_text_fields(shape)
elif isinstance(shape, Line):
self._add_line_fields(shape)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
buttons.accepted.connect(self.accept) buttons.accepted.connect(self.accept)
@@ -53,6 +55,8 @@ class ShapeOptionsDialog(QDialog):
width = min(self.width.value(), self._scene_rect.right() - self.x.value()) width = min(self.width.value(), self._scene_rect.right() - self.x.value())
height = min(self.height.value(), self._scene_rect.bottom() - self.y.value()) height = min(self.height.value(), self._scene_rect.bottom() - self.y.value())
return Text(layer=self.layer.value(), pos=(self.x.value(), self.y.value()), width=width, height=height, color=self.color.color(), bold=self.bold.isChecked(), italic=self.italic.isChecked(), size=self.size.value(), text=self.text.text()) return Text(layer=self.layer.value(), pos=(self.x.value(), self.y.value()), width=width, height=height, color=self.color.color(), bold=self.bold.isChecked(), italic=self.italic.isChecked(), size=self.size.value(), text=self.text.text())
if isinstance(self._shape, Line):
return Line(layer=self.layer.value(), pos=(self.x.value(), self.y.value()), end=(self.end_x.value(), self.end_y.value()), line_type=self.line_type.currentData(), line_thickness=self.line_thickness.value(), line_color=self.line_color.color())
shape = deepcopy(self._shape) shape = deepcopy(self._shape)
shape.layer = self.layer.value() shape.layer = self.layer.value()
shape.pos = (self.x.value(), self.y.value()) shape.pos = (self.x.value(), self.y.value())
@@ -108,3 +112,26 @@ class ShapeOptionsDialog(QDialog):
self.form.addRow("Italic", self.italic) self.form.addRow("Italic", self.italic)
self.form.addRow("Size", self.size) self.form.addRow("Size", self.size)
self.form.addRow("Text", self.text) self.form.addRow("Text", self.text)
def _add_line_fields(self, shape: Line) -> None:
self.x.setMaximum(round(self._scene_rect.right()))
self.y.setMaximum(round(self._scene_rect.bottom()))
self.end_x = QSpinBox()
self.end_x.setRange(round(self._scene_rect.left()), round(self._scene_rect.right()))
self.end_x.setValue(shape.end[0])
self.end_y = QSpinBox()
self.end_y.setRange(round(self._scene_rect.top()), round(self._scene_rect.bottom()))
self.end_y.setValue(shape.end[1])
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.line_color = ColorButton(shape.line_color)
self.form.addRow("End X", self.end_x)
self.form.addRow("End Y", self.end_y)
self.form.addRow("Line type", self.line_type)
self.form.addRow("Line thickness", self.line_thickness)
self.form.addRow("Line color", self.line_color)