beeref/beeref/items.py
DarkDefender e02ccc675f Fix crash when dragging an image and toggling on crop mode
`crop_mode_event_start` would be set to `None` and thus crash the program. Add a check for this.
2024-05-30 17:00:55 +02:00

806 lines
29 KiB
Python

# This file is part of BeeRef.
#
# BeeRef is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BeeRef 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
"""Classes for items that are added to the scene by the user (images,
text).
"""
from collections import defaultdict
from functools import cached_property
import logging
import os.path
from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6.QtCore import Qt
from beeref import commands
from beeref.config import BeeSettings
from beeref.constants import COLORS
from beeref.selection import SelectableMixin
logger = logging.getLogger(__name__)
item_registry = {}
def register_item(cls):
item_registry[cls.TYPE] = cls
return cls
def sort_by_filename(items):
"""Order items by filename.
Items with a filename (ordered by filename) first, then items
without a filename but with a save_id follow (ordered by
save_id), then remaining items in the order that they have
been inserted into the scene.
"""
items_by_filename = []
items_by_save_id = []
items_remaining = []
for item in items:
if getattr(item, 'filename', None):
items_by_filename.append(item)
elif getattr(item, 'save_id', None):
items_by_save_id.append(item)
else:
items_remaining.append(item)
items_by_filename.sort(key=lambda x: x.filename)
items_by_save_id.sort(key=lambda x: x.save_id)
return items_by_filename + items_by_save_id + items_remaining
class BeeItemMixin(SelectableMixin):
"""Base for all items added by the user."""
def set_pos_center(self, pos):
"""Sets the position using the item's center as the origin point."""
self.setPos(pos - self.center_scene_coords)
def has_selection_outline(self):
return self.isSelected()
def has_selection_handles(self):
return (self.isSelected()
and self.scene()
and self.scene().has_single_selection())
def selection_action_items(self):
"""The items affected by selection actions like scaling and rotating.
"""
return [self]
def on_selected_change(self, value):
if (value and self.scene()
and not self.scene().has_selection()
and not self.scene().active_mode is None):
self.bring_to_front()
def update_from_data(self, **kwargs):
self.save_id = kwargs.get('save_id', self.save_id)
self.setPos(kwargs.get('x', self.pos().x()),
kwargs.get('y', self.pos().y()))
self.setZValue(kwargs.get('z', self.zValue()))
self.setScale(kwargs.get('scale', self.scale()))
self.setRotation(kwargs.get('rotation', self.rotation()))
if kwargs.get('flip', 1) != self.flip():
self.do_flip()
@register_item
class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem):
"""Class for images added by the user."""
TYPE = 'pixmap'
CROP_HANDLE_SIZE = 15
def __init__(self, image, filename=None, **kwargs):
super().__init__(QtGui.QPixmap.fromImage(image))
self.save_id = None
self.filename = filename
self.reset_crop()
logger.debug(f'Initialized {self}')
self.is_image = True
self.crop_mode = False
self.init_selectable()
self.settings = BeeSettings()
self.grayscale = False
@classmethod
def create_from_data(self, **kwargs):
item = kwargs.pop('item')
data = kwargs.pop('data', {})
item.filename = item.filename or data.get('filename')
if 'crop' in data:
item.crop = QtCore.QRectF(*data['crop'])
item.setOpacity(data.get('opacity', 1))
item.grayscale = data.get('grayscale', False)
return item
def __str__(self):
size = self.pixmap().size()
return (f'Image "{self.filename}" {size.width()} x {size.height()}')
@property
def crop(self):
return self._crop
@crop.setter
def crop(self, value):
logger.debug(f'Setting crop for {self} to {value}')
self.prepareGeometryChange()
self._crop = value
self.update()
@property
def grayscale(self):
return self._grayscale
@grayscale.setter
def grayscale(self, value):
logger.debug('Setting grayscale for {self} to {value}')
self._grayscale = value
if value is True:
# Using the grayscale image format to convert to grayscale
# loses an image's tranparency. So the straightworward
# following method gives us an ugly black replacement:
# img = img.convertToFormat(QtGui.QImage.Format.Format_Grayscale8)
# Instead, we will fill the background with the current
# canvas colour, so the issue is only visible if the image
# overlaps other images. The way we do it here only works
# as long as the canvas colour is itself grayscale,
# though.
img = QtGui.QImage(
self.pixmap().size(), QtGui.QImage.Format.Format_Grayscale8)
img.fill(QtGui.QColor(*COLORS['Scene:Canvas']))
painter = QtGui.QPainter(img)
painter.drawPixmap(0, 0, self.pixmap())
painter.end()
self._grayscale_pixmap = QtGui.QPixmap.fromImage(img)
# Alternative methods that have their own issues:
#
# 1. Use setAlphaChannel of the resulting grayscale
# image. How do we get the original alpha channel? Using
# the whole original image also takes color values into
# account, not just their alpha values.
#
# 2. QtWidgets.QGraphicsColorizeEffect() with black colour
# on the GraphicsItem. This applys to everything the paint
# method does, so the selection outline/handles will also
# be gray. setGraphicsEffect is only available on some
# widgets, so we can't apply it selectively.
#
# 3. Going through every pixel and doing it manually — bad
# performance.
else:
self._grayscale_pixmap = None
self.update()
def sample_color_at(self, pos):
ipos = self.mapFromScene(pos)
if self.grayscale:
pm = self._grayscale_pixmap
else:
pm = self.pixmap()
img = pm.toImage()
color = img.pixelColor(int(ipos.x()), int(ipos.y()))
if color.alpha():
return color
def bounding_rect_unselected(self):
if self.crop_mode:
return QtWidgets.QGraphicsPixmapItem.boundingRect(self)
else:
return self.crop
def get_extra_save_data(self):
return {'filename': self.filename,
'opacity': self.opacity(),
'grayscale': self.grayscale,
'crop': [self.crop.topLeft().x(),
self.crop.topLeft().y(),
self.crop.width(),
self.crop.height()]}
def get_filename_for_export(self, imgformat, save_id_default=None):
save_id = self.save_id or save_id_default
assert save_id is not None
if self.filename:
basename = os.path.splitext(os.path.basename(self.filename))[0]
return f'{save_id:04}-{basename}.{imgformat}'
else:
return f'{save_id:04}.{imgformat}'
def get_imgformat(self, img):
"""Determines the format for storing this image."""
formt = self.settings.valueOrDefault('Items/image_storage_format')
if formt == 'best':
# Images with alpha channel and small images are stored as png
if (img.hasAlphaChannel()
or (img.height() < 500 and img.width() < 500)):
formt = 'png'
else:
formt = 'jpg'
logger.debug(f'Found format {formt} for {self}')
return formt
def pixmap_to_bytes(self, apply_grayscale=False, apply_crop=False):
"""Convert the pixmap data to PNG bytestring."""
barray = QtCore.QByteArray()
buffer = QtCore.QBuffer(barray)
buffer.open(QtCore.QIODevice.OpenModeFlag.WriteOnly)
if apply_grayscale and self.grayscale:
pm = self._grayscale_pixmap
else:
pm = self.pixmap()
if apply_crop:
pm = pm.copy(self.crop.toRect())
img = pm.toImage()
imgformat = self.get_imgformat(img)
img.save(buffer, imgformat.upper(), quality=90)
return (barray.data(), imgformat)
def setPixmap(self, pixmap):
super().setPixmap(pixmap)
self.reset_crop()
def pixmap_from_bytes(self, data):
"""Set image pimap from a bytestring."""
pixmap = QtGui.QPixmap()
pixmap.loadFromData(data)
self.setPixmap(pixmap)
def create_copy(self):
item = BeePixmapItem(QtGui.QImage(), self.filename)
item.setPixmap(self.pixmap())
item.setPos(self.pos())
item.setZValue(self.zValue())
item.setScale(self.scale())
item.setRotation(self.rotation())
item.setOpacity(self.opacity())
item.grayscale = self.grayscale
if self.flip() == -1:
item.do_flip()
item.crop = self.crop
return item
@cached_property
def color_gamut(self):
logger.debug(f'Calculating color gamut for {self}')
gamut = defaultdict(int)
img = self.pixmap().toImage()
# Don't evaluate every pixel for larger images:
step = max(1, int(max(img.width(), img.height()) / 1000))
logger.debug(f'Considering every {step}. row/column')
# Not actually faster than solution below :(
# ptr = img.bits()
# size = img.sizeInBytes()
# pixelsize = int(img.sizeInBytes() / img.width() / img.height())
# ptr.setsize(size)
# for pixel in batched(ptr, n=pixelsize):
# r, g, b, alpha = tuple(map(ord, pixel))
# if 5 < alpha and 5 < r < 250 and 5 < g < 250 and 5 < b < 250:
# # Only consider pixels that aren't close to
# # transparent, white or black
# rgb = QtGui.QColor(r, g, b)
# gamut[rgb.hue(), rgb.saturation()] += 1
for i in range(0, img.width(), step):
for j in range(0, img.height(), step):
rgb = img.pixelColor(i, j)
rgbtuple = (rgb.red(), rgb.blue(), rgb.green())
if (5 < rgb.alpha()
and min(rgbtuple) < 250 and max(rgbtuple) > 5):
# Only consider pixels that aren't close to
# transparent, white or black
gamut[rgb.hue(), rgb.saturation()] += 1
logger.debug(f'Got {len(gamut)} color gamut values')
return gamut
def copy_to_clipboard(self, clipboard):
clipboard.setPixmap(self.pixmap())
def reset_crop(self):
self.crop = QtCore.QRectF(
0, 0, self.pixmap().size().width(), self.pixmap().size().height())
@property
def crop_handle_size(self):
return self.fixed_length_for_viewport(self.CROP_HANDLE_SIZE)
def crop_handle_topleft(self):
topleft = self.crop_temp.topLeft()
return QtCore.QRectF(
topleft.x(),
topleft.y(),
self.crop_handle_size,
self.crop_handle_size)
def crop_handle_bottomleft(self):
bottomleft = self.crop_temp.bottomLeft()
return QtCore.QRectF(
bottomleft.x(),
bottomleft.y() - self.crop_handle_size,
self.crop_handle_size,
self.crop_handle_size)
def crop_handle_bottomright(self):
bottomright = self.crop_temp.bottomRight()
return QtCore.QRectF(
bottomright.x() - self.crop_handle_size,
bottomright.y() - self.crop_handle_size,
self.crop_handle_size,
self.crop_handle_size)
def crop_handle_topright(self):
topright = self.crop_temp.topRight()
return QtCore.QRectF(
topright.x() - self.crop_handle_size,
topright.y(),
self.crop_handle_size,
self.crop_handle_size)
def crop_handles(self):
return (self.crop_handle_topleft,
self.crop_handle_bottomleft,
self.crop_handle_bottomright,
self.crop_handle_topright)
def crop_edge_top(self):
topleft = self.crop_temp.topLeft()
return QtCore.QRectF(
topleft.x() + self.crop_handle_size,
topleft.y(),
self.crop_temp.width() - 2 * self.crop_handle_size,
self.crop_handle_size)
def crop_edge_left(self):
topleft = self.crop_temp.topLeft()
return QtCore.QRectF(
topleft.x(),
topleft.y() + self.crop_handle_size,
self.crop_handle_size,
self.crop_temp.height() - 2 * self.crop_handle_size)
def crop_edge_bottom(self):
bottomleft = self.crop_temp.bottomLeft()
return QtCore.QRectF(
bottomleft.x() + self.crop_handle_size,
bottomleft.y() - self.crop_handle_size,
self.crop_temp.width() - 2 * self.crop_handle_size,
self.crop_handle_size)
def crop_edge_right(self):
topright = self.crop_temp.topRight()
return QtCore.QRectF(
topright.x() - self.crop_handle_size,
topright.y() + self.crop_handle_size,
self.crop_handle_size,
self.crop_temp.height() - 2 * self.crop_handle_size)
def crop_edges(self):
return (self.crop_edge_top,
self.crop_edge_left,
self.crop_edge_bottom,
self.crop_edge_right)
def get_crop_handle_cursor(self, handle):
"""Gets the crop cursor for the given handle."""
is_topleft_or_bottomright = handle in (
self.crop_handle_topleft, self.crop_handle_bottomright)
return self.get_diag_cursor(is_topleft_or_bottomright)
def get_crop_edge_cursor(self, edge):
"""Gets the crop edge cursor for the given edge."""
top_or_bottom = edge in (
self.crop_edge_top, self.crop_edge_bottom)
sideways = (45 < self.rotation() < 135
or 225 < self.rotation() < 315)
if top_or_bottom is sideways:
return Qt.CursorShape.SizeHorCursor
else:
return Qt.CursorShape.SizeVerCursor
def draw_crop_rect(self, painter, rect):
"""Paint a dotted rectangle for the cropping UI."""
pen = QtGui.QPen(QtGui.QColor(255, 255, 255))
pen.setWidth(2)
pen.setCosmetic(True)
painter.setPen(pen)
painter.drawRect(rect)
pen.setColor(QtGui.QColor(0, 0, 0))
pen.setStyle(Qt.PenStyle.DotLine)
painter.setPen(pen)
painter.drawRect(rect)
def paint(self, painter, option, widget):
if abs(painter.combinedTransform().m11()) < 2:
# We want image smoothing, but only for images where we
# are not zoomed in a lot. This is to ensure that for
# example icons and pixel sprites can be viewed correctly.
painter.setRenderHint(painter.RenderHint.SmoothPixmapTransform)
if self.crop_mode:
self.paint_debug(painter, option, widget)
# Darken image outside of cropped area
painter.drawPixmap(0, 0, self.pixmap())
path = QtWidgets.QGraphicsPixmapItem.shape(self)
path.addRect(self.crop_temp)
color = QtGui.QColor(0, 0, 0)
color.setAlpha(100)
painter.setBrush(QtGui.QBrush(color))
painter.setPen(Qt.PenStyle.NoPen)
painter.drawPath(path)
painter.setBrush(QtGui.QBrush())
for handle in self.crop_handles():
self.draw_crop_rect(painter, handle())
self.draw_crop_rect(painter, self.crop_temp)
else:
pm = self._grayscale_pixmap if self.grayscale else self.pixmap()
painter.drawPixmap(self.crop, pm, self.crop)
self.paint_selectable(painter, option, widget)
def enter_crop_mode(self):
logger.debug(f'Entering crop mode on {self}')
self.prepareGeometryChange()
self.crop_mode = True
self.crop_temp = QtCore.QRectF(self.crop)
self.crop_mode_move = None
self.crop_mode_event_start = None
self.grabKeyboard()
self.update()
self.scene().crop_item = self
def exit_crop_mode(self, confirm):
logger.debug(f'Exiting crop mode with {confirm} on {self}')
if confirm and self.crop != self.crop_temp:
self.scene().undo_stack.push(
commands.CropItem(self, self.crop_temp))
self.prepareGeometryChange()
self.crop_mode = False
self.crop_temp = None
self.crop_mode_move = None
self.crop_mode_event_start = None
self.ungrabKeyboard()
self.update()
self.scene().crop_item = None
def keyPressEvent(self, event):
if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
self.exit_crop_mode(confirm=True)
elif event.key() == Qt.Key.Key_Escape:
self.exit_crop_mode(confirm=False)
else:
super().keyPressEvent(event)
def hoverMoveEvent(self, event):
if not self.crop_mode:
return super().hoverMoveEvent(event)
for handle in self.crop_handles():
if handle().contains(event.pos()):
self.set_cursor(self.get_crop_handle_cursor(handle))
return
for edge in self.crop_edges():
if edge().contains(event.pos()):
self.set_cursor(self.get_crop_edge_cursor(edge))
return
self.unset_cursor()
def mousePressEvent(self, event):
if not self.crop_mode:
return super().mousePressEvent(event)
event.accept()
for handle in self.crop_handles():
# Click into a handle?
if handle().contains(event.pos()):
self.crop_mode_event_start = event.pos()
self.crop_mode_move = handle
return
for edge in self.crop_edges():
# Click into an edge handle?
if edge().contains(event.pos()):
self.crop_mode_event_start = event.pos()
self.crop_mode_move = edge
return
# Click not in handle, end cropping mode:
self.exit_crop_mode(
confirm=self.crop_temp.contains(event.pos()))
def ensure_point_within_crop_bounds(self, point, handle):
"""Returns the point, or the nearest point within the pixmap."""
if handle == self.crop_handle_topleft:
topleft = QtCore.QPointF(0, 0)
bottomright = self.crop_temp.bottomRight()
if handle == self.crop_handle_bottomleft:
topleft = QtCore.QPointF(0, self.crop_temp.top())
bottomright = QtCore.QPointF(
self.crop_temp.right(), self.pixmap().size().height())
if handle == self.crop_handle_bottomright:
topleft = self.crop_temp.topLeft()
bottomright = QtCore.QPointF(
self.pixmap().size().width(), self.pixmap().size().height())
if handle == self.crop_handle_topright:
topleft = QtCore.QPointF(self.crop_temp.left(), 0)
bottomright = QtCore.QPointF(
self.pixmap().size().width(), self.crop_temp.bottom())
if handle == self.crop_edge_top:
topleft = QtCore.QPointF(0, 0)
bottomright = QtCore.QPointF(
self.pixmap().size().width(), self.crop_temp.bottom())
if handle == self.crop_edge_bottom:
topleft = QtCore.QPointF(0, self.crop_temp.top())
bottomright = QtCore.QPointF(
self.pixmap().size().width(), self.pixmap().size().height())
if handle == self.crop_edge_left:
topleft = QtCore.QPointF(0, 0)
bottomright = QtCore.QPointF(
self.crop_temp.right(), self.pixmap().size().height())
if handle == self.crop_edge_right:
topleft = QtCore.QPointF(self.crop_temp.left(), 0)
bottomright = QtCore.QPointF(
self.pixmap().size().width(), self.pixmap().size().height())
point.setX(min(bottomright.x(), max(topleft.x(), point.x())))
point.setY(min(bottomright.y(), max(topleft.y(), point.y())))
return point
def mouseMoveEvent(self, event):
if self.crop_mode and self.crop_mode_event_start:
diff = event.pos() - self.crop_mode_event_start
if self.crop_mode_move == self.crop_handle_topleft:
new = self.ensure_point_within_crop_bounds(
self.crop_temp.topLeft() + diff, self.crop_mode_move)
self.crop_temp.setTopLeft(new)
if self.crop_mode_move == self.crop_handle_bottomleft:
new = self.ensure_point_within_crop_bounds(
self.crop_temp.bottomLeft() + diff, self.crop_mode_move)
self.crop_temp.setBottomLeft(new)
if self.crop_mode_move == self.crop_handle_bottomright:
new = self.ensure_point_within_crop_bounds(
self.crop_temp.bottomRight() + diff, self.crop_mode_move)
self.crop_temp.setBottomRight(new)
if self.crop_mode_move == self.crop_handle_topright:
new = self.ensure_point_within_crop_bounds(
self.crop_temp.topRight() + diff, self.crop_mode_move)
self.crop_temp.setTopRight(new)
if self.crop_mode_move == self.crop_edge_top:
new = self.ensure_point_within_crop_bounds(
self.crop_temp.topLeft() + diff, self.crop_mode_move)
self.crop_temp.setTop(new.y())
if self.crop_mode_move == self.crop_edge_left:
new = self.ensure_point_within_crop_bounds(
self.crop_temp.topLeft() + diff, self.crop_mode_move)
self.crop_temp.setLeft(new.x())
if self.crop_mode_move == self.crop_edge_bottom:
new = self.ensure_point_within_crop_bounds(
self.crop_temp.bottomLeft() + diff, self.crop_mode_move)
self.crop_temp.setBottom(new.y())
if self.crop_mode_move == self.crop_edge_right:
new = self.ensure_point_within_crop_bounds(
self.crop_temp.topRight() + diff, self.crop_mode_move)
self.crop_temp.setRight(new.x())
self.update()
self.crop_mode_event_start = event.pos()
event.accept()
else:
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if self.crop_mode:
self.crop_mode_move = None
self.crop_mode_event_start = None
event.accept()
else:
super().mouseReleaseEvent(event)
@register_item
class BeeTextItem(BeeItemMixin, QtWidgets.QGraphicsTextItem):
"""Class for text added by the user."""
TYPE = 'text'
def __init__(self, text=None, **kwargs):
super().__init__(text or "Text")
self.save_id = None
logger.debug(f'Initialized {self}')
self.is_image = False
self.init_selectable()
self.is_editable = True
self.edit_mode = False
self.setDefaultTextColor(QtGui.QColor(*COLORS['Scene:Text']))
@classmethod
def create_from_data(cls, **kwargs):
data = kwargs.get('data', {})
item = cls(**data)
return item
def __str__(self):
txt = self.toPlainText()[:40]
return (f'Text "{txt}"')
def get_extra_save_data(self):
return {'text': self.toPlainText()}
def contains(self, point):
return self.boundingRect().contains(point)
def paint(self, painter, option, widget):
painter.setPen(Qt.PenStyle.NoPen)
color = QtGui.QColor(0, 0, 0)
color.setAlpha(40)
brush = QtGui.QBrush(color)
painter.setBrush(brush)
painter.drawRect(QtWidgets.QGraphicsTextItem.boundingRect(self))
option.state = QtWidgets.QStyle.StateFlag.State_Enabled
super().paint(painter, option, widget)
self.paint_selectable(painter, option, widget)
def create_copy(self):
item = BeeTextItem(self.toPlainText())
item.setPos(self.pos())
item.setZValue(self.zValue())
item.setScale(self.scale())
item.setRotation(self.rotation())
if self.flip() == -1:
item.do_flip()
return item
def enter_edit_mode(self):
logger.debug(f'Entering edit mode on {self}')
self.edit_mode = True
self.old_text = self.toPlainText()
self.setTextInteractionFlags(
Qt.TextInteractionFlag.TextEditorInteraction)
self.scene().edit_item = self
def exit_edit_mode(self, commit=True):
logger.debug(f'Exiting edit mode on {self}')
self.edit_mode = False
# reset selection:
self.setTextCursor(QtGui.QTextCursor(self.document()))
self.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
self.scene().edit_item = None
if commit:
self.scene().undo_stack.push(
commands.ChangeText(self, self.toPlainText(), self.old_text))
if not self.toPlainText().strip():
logger.debug('Removing empty text item')
self.scene().undo_stack.push(
commands.DeleteItems(self.scene(), [self]))
else:
self.setPlainText(self.old_text)
def has_selection_handles(self):
return super().has_selection_handles() and not self.edit_mode
def keyPressEvent(self, event):
if (event.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return)
and event.modifiers() == Qt.KeyboardModifier.NoModifier):
self.exit_edit_mode()
event.accept()
return
if (event.key() == Qt.Key.Key_Escape
and event.modifiers() == Qt.KeyboardModifier.NoModifier):
self.exit_edit_mode(commit=False)
event.accept()
return
super().keyPressEvent(event)
def copy_to_clipboard(self, clipboard):
clipboard.setText(self.toPlainText())
@register_item
class BeeErrorItem(BeeItemMixin, QtWidgets.QGraphicsTextItem):
"""Class for displaying error messages when an item can't be loaded
from a bee file.
This item will be displayed instead of the original item. It won't
save to bee files. The original item will be preserved in the bee
file, unless this item gets deleted by the user, or a new bee file
is saved.
"""
TYPE = 'error'
def __init__(self, text=None, **kwargs):
super().__init__(text or "Text")
self.original_save_id = None
logger.debug(f'Initialized {self}')
self.is_image = False
self.init_selectable()
self.is_editable = False
self.setDefaultTextColor(QtGui.QColor(*COLORS['Scene:Text']))
@classmethod
def create_from_data(cls, **kwargs):
data = kwargs.get('data', {})
item = cls(**data)
return item
def __str__(self):
txt = self.toPlainText()[:40]
return (f'Error "{txt}"')
def contains(self, point):
return self.boundingRect().contains(point)
def paint(self, painter, option, widget):
painter.setPen(Qt.PenStyle.NoPen)
color = QtGui.QColor(200, 0, 0)
brush = QtGui.QBrush(color)
painter.setBrush(brush)
painter.drawRect(QtWidgets.QGraphicsTextItem.boundingRect(self))
option.state = QtWidgets.QStyle.StateFlag.State_Enabled
super().paint(painter, option, widget)
self.paint_selectable(painter, option, widget)
def update_from_data(self, **kwargs):
self.original_save_id = kwargs.get('save_id', self.original_save_id)
self.setPos(kwargs.get('x', self.pos().x()),
kwargs.get('y', self.pos().y()))
self.setZValue(kwargs.get('z', self.zValue()))
self.setScale(kwargs.get('scale', self.scale()))
self.setRotation(kwargs.get('rotation', self.rotation()))
def create_copy(self):
item = BeeErrorItem(self.toPlainText())
item.setPos(self.pos())
item.setZValue(self.zValue())
item.setScale(self.scale())
item.setRotation(self.rotation())
return item
def flip(self, *args, **kwargs):
"""Returns the flip value (1 or -1)"""
# Never display error messages flipped
return 1
def do_flip(self, *args, **kwargs):
"""Flips the item."""
# Never flip error messages
pass
def copy_to_clipboard(self, clipboard):
clipboard.setText(self.toPlainText())