Layout is good, click boxes is wrong
This commit is contained in:
@@ -1,20 +1,23 @@
|
||||
import re
|
||||
from typing import Any, Callable, Optional, Self, cast, overload
|
||||
import unicodedata
|
||||
from typing import Any, Callable, Optional, Self, TypedDict, cast
|
||||
|
||||
from PyQt6.QtCore import QMargins, QPoint, QRect, QSize, Qt, QUrl, pyqtSignal
|
||||
from PyQt6.QtCore import QMargins, QPoint, QPointF, QRect, QRectF, QSize, Qt, QUrl, pyqtSignal
|
||||
from PyQt6.QtGui import (
|
||||
QBrush,
|
||||
QColor,
|
||||
QFont,
|
||||
QFontDatabase,
|
||||
QFontMetrics,
|
||||
QMouseEvent,
|
||||
QPainter,
|
||||
QPaintEvent,
|
||||
QResizeEvent,
|
||||
QTextCharFormat,
|
||||
QTextLayout,
|
||||
QTextOption,
|
||||
QTransform,
|
||||
)
|
||||
from PyQt6.QtWidgets import QWidget
|
||||
from trycast import trycast
|
||||
|
||||
|
||||
class Fragment:
|
||||
@@ -24,7 +27,7 @@ class Fragment:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
which: str | Self,
|
||||
which: str | Self | None = None,
|
||||
font: QFont | None = None,
|
||||
audio: str = "",
|
||||
color: Optional[QColor] = None,
|
||||
@@ -34,19 +37,22 @@ class Fragment:
|
||||
for k, v in which.__dict__.items():
|
||||
self.__dict__[k] = v
|
||||
return
|
||||
self._text: str = which
|
||||
self._layout = QTextLayout()
|
||||
if font is None:
|
||||
raise TypeError("Missing required parameter 'font'")
|
||||
self._font = font
|
||||
self._audio: QUrl = QUrl(audio)
|
||||
self._align = QTextOption(
|
||||
self._layout.setFont(
|
||||
QFontDatabase.font("OpenDyslexic", None, 20)
|
||||
)
|
||||
else:
|
||||
self._layout.setFont(font)
|
||||
align = QTextOption(
|
||||
Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignBaseline
|
||||
)
|
||||
self._layout.setTextOption(align)
|
||||
self._audio: QUrl = QUrl(audio)
|
||||
self._padding = QMargins()
|
||||
self._border = QMargins()
|
||||
self._margin = QMargins()
|
||||
self._wref = ""
|
||||
self._position = QPoint()
|
||||
self._rect = QRect()
|
||||
self._borderRect = QRect()
|
||||
self._clickRect = QRect()
|
||||
@@ -57,133 +63,109 @@ class Fragment:
|
||||
self._background = QColor()
|
||||
self._asis = asis
|
||||
self._indent = 0
|
||||
self._target = "word"
|
||||
if which is not None:
|
||||
self.setText(which)
|
||||
return
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
def size(self) -> QSize:
|
||||
return self.paintEvent()
|
||||
|
||||
def size(self, width: int) -> QSize:
|
||||
return self.paintEvent(width)
|
||||
def height(self) -> int:
|
||||
return self.size().height()
|
||||
|
||||
def height(self, width: int) -> int:
|
||||
return self.size(width).height()
|
||||
|
||||
def width(self, width: int) -> int:
|
||||
return self.size(width).width()
|
||||
def width(self) -> int:
|
||||
return self.size().width()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"({self._position.x()}, {self._position.y()}): {self._text}"
|
||||
rect = self._layout.boundingRect()
|
||||
text = self._layout.text()
|
||||
return f"{text}: (({rect.x()}, {rect.y()}), {rect.width()}, {rect.height()})"
|
||||
|
||||
@overload
|
||||
def paintEvent(self, widthSrc: int) -> QSize:
|
||||
...
|
||||
def doLayout(self, width: int) -> QPointF:
|
||||
leading = QFontMetrics(self._layout.font()).leading()
|
||||
eol = self._layout.position()
|
||||
base = 0
|
||||
indent = 0
|
||||
self._layout.setCacheEnabled(True)
|
||||
self._layout.beginLayout()
|
||||
while True:
|
||||
line = self._layout.createLine()
|
||||
if not line.isValid():
|
||||
break
|
||||
line.setLineWidth(width - self._layout.position().x())
|
||||
line.setPosition(QPointF(indent, base+leading))
|
||||
rect = line.naturalTextRect()
|
||||
eol = rect.bottomRight()
|
||||
assert isinstance(eol, QPointF)
|
||||
base += line.height()
|
||||
indent = self.pixelIndent() - self._layout.position().x()
|
||||
self._layout.endLayout()
|
||||
result = eol
|
||||
return result
|
||||
|
||||
@overload
|
||||
def paintEvent(self, widthSrc: QPainter) -> int:
|
||||
...
|
||||
|
||||
def paintEvent(self, widthSrc: QPainter | int) -> int | QSize:
|
||||
if isinstance(widthSrc, QPainter):
|
||||
viewportWidth = widthSrc.viewport().width()
|
||||
painter = widthSrc
|
||||
else:
|
||||
viewportWidth = widthSrc
|
||||
painter = None
|
||||
fm = QFontMetrics(self._font)
|
||||
top = self._position.y() + fm.descent() - fm.height()
|
||||
left = self._position.x()
|
||||
width = viewportWidth - left
|
||||
height = 2000
|
||||
rect = QRect(left, top, width, height)
|
||||
indent = self._indent * self._indentAmount
|
||||
flags = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignBaseline
|
||||
boundingNoWrap = fm.boundingRect(
|
||||
rect, flags | Qt.TextFlag.TextSingleLine, self._text
|
||||
)
|
||||
bounding = fm.boundingRect(
|
||||
rect, flags | Qt.TextFlag.TextWordWrap, self._text
|
||||
)
|
||||
text = self._text
|
||||
remainingText = ""
|
||||
if boundingNoWrap.height() < bounding.height():
|
||||
#
|
||||
# This is not optimal, but it is only a few iterations
|
||||
#
|
||||
lastSpace = 0
|
||||
char = 0
|
||||
pos = rect.x()
|
||||
while pos < rect.right():
|
||||
if text[char] == " ":
|
||||
lastSpace = char
|
||||
pos += fm.horizontalAdvance(text[char])
|
||||
char += 1
|
||||
if lastSpace > 0:
|
||||
remainingText = text[lastSpace + 1 :]
|
||||
text = text[:lastSpace]
|
||||
|
||||
size = boundingNoWrap.size()
|
||||
|
||||
boundingNoWrap = fm.boundingRect(
|
||||
rect, flags | Qt.TextFlag.TextSingleLine, text
|
||||
)
|
||||
rect.setSize(boundingNoWrap.size())
|
||||
|
||||
if remainingText != "":
|
||||
top += size.height()
|
||||
remainingRect = QRect(indent, top, viewportWidth - indent, height)
|
||||
boundingRemaingRect = fm.boundingRect(
|
||||
remainingRect, flags | Qt.TextFlag.TextWordWrap, remainingText
|
||||
)
|
||||
|
||||
size = size.grownBy(QMargins(0, 0, 0, boundingRemaingRect.height()))
|
||||
remainingRect.setSize(boundingRemaingRect.size())
|
||||
size = size.grownBy(self._margin)
|
||||
size = size.grownBy(self._border)
|
||||
size = size.grownBy(self._padding)
|
||||
def paintEvent(self, painter: Optional[QPainter] | None = None) -> QSize:
|
||||
rect = self._layout.boundingRect()
|
||||
size = rect.size()
|
||||
assert size is not None
|
||||
if painter is None:
|
||||
return size
|
||||
return QSize(int(size.width()), int(size.height()))
|
||||
painter.save()
|
||||
painter.setFont(self._font)
|
||||
painter.setPen(QColor("#f00"))
|
||||
if self._audio.isValid():
|
||||
radius = self._borderRect.height() / 2
|
||||
painter.drawRoundedRect(self._borderRect, radius, radius)
|
||||
if self._wref:
|
||||
start = bounding.bottomLeft()
|
||||
end = bounding.bottomRight()
|
||||
painter.drawLine(start, end)
|
||||
|
||||
painter.setPen(self._color)
|
||||
if self._background.isValid():
|
||||
brush = painter.brush()
|
||||
brush.setColor(self._background)
|
||||
brush.setStyle(Qt.BrushStyle.SolidPattern)
|
||||
painter.setBrush(brush)
|
||||
painter.fillRect(rect, brush)
|
||||
painter.drawText(rect, flags, text)
|
||||
if remainingText:
|
||||
if self._background.isValid():
|
||||
painter.fillRect(remainingRect, brush)
|
||||
painter.drawText(
|
||||
remainingRect, flags | Qt.TextFlag.TextWordWrap, remainingText
|
||||
)
|
||||
self._layout.draw(painter, QPointF(0,0))
|
||||
#
|
||||
# TODO: draw the rounded rect around audio buttons
|
||||
#
|
||||
painter.brush().setColor(Qt.GlobalColor.green)
|
||||
for fmt in self._layout.formats():
|
||||
if fmt.format.isAnchor():
|
||||
#text = self._layout.text()[fmt.start:fmt.start+fmt.length]
|
||||
runs = self._layout.glyphRuns(fmt.start, fmt.length)
|
||||
bb = runs[0].boundingRect()
|
||||
bb.moveTo(bb.topLeft() + self._layout.position())
|
||||
painter.drawRect(bb)
|
||||
#print(f"({bb.left()}-{bb.right()}, {bb.top()}-{bb.bottom()}): {text}")
|
||||
|
||||
painter.restore()
|
||||
return size.height()
|
||||
return QSize(int(size.width()), int(size.height()))
|
||||
|
||||
#
|
||||
# Setters
|
||||
#
|
||||
def setText(self, text: str) -> None:
|
||||
self._text = text
|
||||
def addText(self, text: str, fmt: Optional[QTextCharFormat] = None) -> None:
|
||||
oldText = self._layout.text()
|
||||
self._layout.setText(oldText + text)
|
||||
if Line.parseText:
|
||||
self._layout = Line.parseText(self)
|
||||
|
||||
if fmt is not None:
|
||||
fr = QTextLayout.FormatRange()
|
||||
fr.format = fmt
|
||||
fr.length = len(self._layout.text()) - len(oldText)
|
||||
fr.start = len(oldText)
|
||||
fmts = self._layout.formats()
|
||||
fmts.append(fr)
|
||||
self._layout.setFormats(fmts)
|
||||
return
|
||||
|
||||
def setTarget(self, target: str) -> None:
|
||||
self._target = target
|
||||
def setText(self, text: str) -> None:
|
||||
text = unicodedata.normalize("NFKD",text)
|
||||
self._layout.setText(text)
|
||||
if Line.parseText:
|
||||
self._layout = Line.parseText(self)
|
||||
if self.audio().isValid():
|
||||
fr = QTextLayout.FormatRange()
|
||||
fr.start=0
|
||||
fr.length = len(self._layout.text())
|
||||
fmt = QTextCharFormat()
|
||||
fmt.setAnchor(True)
|
||||
fmt.setAnchorHref(self._audio.toString())
|
||||
fr.format = fmt
|
||||
formats = self._layout.formats()
|
||||
formats.append(fr)
|
||||
self._layout.setFormats(formats)
|
||||
return
|
||||
|
||||
def setFont(self, font: QFont) -> None:
|
||||
self._font = font
|
||||
self._layout.setFont(font)
|
||||
return
|
||||
|
||||
def setAudio(self, audio: str | QUrl) -> None:
|
||||
@@ -194,7 +176,7 @@ class Fragment:
|
||||
return
|
||||
|
||||
def setAlign(self, align: QTextOption) -> None:
|
||||
self._align = align
|
||||
self._layout.setTextOption(align)
|
||||
return
|
||||
|
||||
def setRect(self, rect: QRect) -> None:
|
||||
@@ -291,7 +273,7 @@ class Fragment:
|
||||
return
|
||||
|
||||
def setPosition(self, pnt: QPoint) -> None:
|
||||
self._position = pnt
|
||||
self._layout.setPosition(QPointF(pnt.x(), pnt.y()))
|
||||
return
|
||||
|
||||
def setBorderRect(self, rect: QRect) -> None:
|
||||
@@ -317,20 +299,23 @@ class Fragment:
|
||||
#
|
||||
# Getters
|
||||
#
|
||||
def background(self) -> QColor:
|
||||
return self._background
|
||||
|
||||
def wRef(self) -> str:
|
||||
return self._wref
|
||||
|
||||
def text(self) -> str:
|
||||
return self._text
|
||||
return self._layout.text()
|
||||
|
||||
def font(self) -> QFont:
|
||||
return self._font
|
||||
return self._layout.font()
|
||||
|
||||
def audio(self) -> QUrl:
|
||||
return self._audio
|
||||
|
||||
def align(self) -> QTextOption:
|
||||
return self._align
|
||||
return self._layout.textOption()
|
||||
|
||||
def rect(self) -> QRect:
|
||||
return self._rect
|
||||
@@ -344,8 +329,8 @@ class Fragment:
|
||||
def margin(self) -> QMargins:
|
||||
return self._margin
|
||||
|
||||
def position(self) -> QPoint:
|
||||
return self._position
|
||||
def position(self) -> QPointF:
|
||||
return self._layout.position()
|
||||
|
||||
def borderRect(self) -> QRect:
|
||||
return self._borderRect
|
||||
@@ -365,6 +350,8 @@ class Fragment:
|
||||
def pixelIndent(self) -> int:
|
||||
return self._indent * self._indentAmount
|
||||
|
||||
def layout(self) -> QTextLayout:
|
||||
return self._layout
|
||||
|
||||
class Line:
|
||||
parseText = None
|
||||
@@ -391,99 +378,48 @@ class Line:
|
||||
#
|
||||
# we do not have an event field because we are not a true widget
|
||||
#
|
||||
lineSpacing = 0
|
||||
pos = QSize(0,0)
|
||||
for frag in self._fragments:
|
||||
ls = frag.paintEvent(painter)
|
||||
if ls > lineSpacing:
|
||||
lineSpacing = ls
|
||||
return lineSpacing
|
||||
pos = frag.paintEvent(painter)
|
||||
return pos.height()
|
||||
|
||||
def addFragment(
|
||||
self,
|
||||
frags: Fragment | list[Fragment],
|
||||
) -> None:
|
||||
SPEAKER = "\U0001F508"
|
||||
#SPEAKER = "\U0001F508"
|
||||
|
||||
if not isinstance(frags, list):
|
||||
frags = [
|
||||
frags,
|
||||
]
|
||||
for frag in frags:
|
||||
if frag.audio().isValid():
|
||||
frag.setText(frag.text() + " " + SPEAKER)
|
||||
|
||||
text = frag.text()
|
||||
text = re.sub(r"\*", "\u2022", text)
|
||||
text = re.sub(r"\{ldquo\}", "\u201c", text)
|
||||
text = re.sub(r"\{rdquo\}", "\u201d", text)
|
||||
frag.setText(text)
|
||||
if frag.audio().isValid():
|
||||
frag.setPadding(3, 0, 0, 5)
|
||||
frag.setBorder(1)
|
||||
frag.setMargin(0, 0, 0, 0)
|
||||
if Line.parseText:
|
||||
items = Line.parseText(frag)
|
||||
self._fragments += items
|
||||
else:
|
||||
self._fragments.append(frag)
|
||||
self._fragments += frags
|
||||
return
|
||||
|
||||
def finalizeLine(self, width: int, base: int) -> None:
|
||||
"""Create all of the positions for all the fragments."""
|
||||
#
|
||||
# Find the maximum hight and max baseline
|
||||
# Each fragment needs to be positioned to the left of the
|
||||
# last fragment or at the indent level.
|
||||
# It needs to be aligned with the baseline of all the
|
||||
# other fragments in the line.
|
||||
#
|
||||
maxHeight = -1
|
||||
baseLine = -1
|
||||
leading = -1
|
||||
|
||||
left = 0 # Left size of rect
|
||||
maxHeight = 0
|
||||
|
||||
for frag in self._fragments:
|
||||
fm = QFontMetrics(frag.font())
|
||||
height = frag.height(width)
|
||||
bl = fm.height() - fm.descent()
|
||||
if fm.leading() > leading:
|
||||
leading = fm.leading()
|
||||
if height > maxHeight:
|
||||
maxHeight = height
|
||||
if bl > baseLine:
|
||||
baseLine = bl
|
||||
self._baseLine = baseLine
|
||||
self._maxHeight = maxHeight
|
||||
self._leading = leading
|
||||
x = 0
|
||||
for frag in self._fragments:
|
||||
left = frag.pixelIndent()
|
||||
if x < left:
|
||||
x = left
|
||||
#
|
||||
# We need to calculate the location to draw the
|
||||
# text. We also need to calculate the bounding Rectangle
|
||||
# for this fragment
|
||||
#
|
||||
size = frag.size(width)
|
||||
fm = QFontMetrics(frag.font())
|
||||
offset = (
|
||||
frag.margin().left()
|
||||
+ frag.border().left()
|
||||
+ frag.padding().left()
|
||||
)
|
||||
frag.setPosition(QPoint(x + offset, self._baseLine))
|
||||
if not frag.border().isNull() or not frag.wRef():
|
||||
#
|
||||
# self._baseLine is where the text will be drawn
|
||||
# fm.descent is the distance from the baseline of the
|
||||
# text to the bottom of the rect
|
||||
# The top of the bounding rect is at self._baseLine
|
||||
# + fm.descent - rect.height
|
||||
# The border is drawn at top-padding-border-margin+marin
|
||||
#
|
||||
top = self._baseLine + fm.descent() - fm.height()
|
||||
y = top - frag.padding().top() - frag.border().top()
|
||||
pos = QPoint(x, y)
|
||||
rect = QRect(pos, size.shrunkBy(frag.margin()))
|
||||
frag.setBorderRect(rect)
|
||||
pos.setY(pos.y() + base)
|
||||
frag.setClickRect(QRect(pos, size.shrunkBy(frag.margin())))
|
||||
x += size.width()
|
||||
if left < frag.pixelIndent():
|
||||
left = frag.pixelIndent()
|
||||
frag.setPosition(QPoint(left, base))
|
||||
eol =frag.doLayout(width)
|
||||
left = int(eol.x()+0.5)
|
||||
if frag.layout().lineCount() > 1:
|
||||
base = int(eol.y()+0.5)
|
||||
if eol.y() > maxHeight:
|
||||
maxHeight = eol.y()
|
||||
self._maxHeight = int(maxHeight+0.5)
|
||||
self._leading = 0
|
||||
return
|
||||
|
||||
def getLine(self) -> list[Fragment]:
|
||||
@@ -492,6 +428,10 @@ class Line:
|
||||
def getLineSpacing(self) -> int:
|
||||
return self._leading + self._maxHeight
|
||||
|
||||
class Clickable(TypedDict):
|
||||
bb: QRectF
|
||||
frag: Fragment
|
||||
fmt: QTextCharFormat
|
||||
|
||||
class Definition(QWidget):
|
||||
pronounce = pyqtSignal(str)
|
||||
@@ -510,64 +450,77 @@ class Definition(QWidget):
|
||||
lines: list[Line] = word.get_def()
|
||||
assert lines is not None
|
||||
self._lines = lines
|
||||
self._buttons: list[Fragment] = []
|
||||
self._buttons: list[Clickable] = []
|
||||
base = 0
|
||||
|
||||
for line in self._lines:
|
||||
line.finalizeLine(self.width(), base)
|
||||
for frag in line.getLine():
|
||||
if frag.audio().isValid():
|
||||
self._buttons.append(frag)
|
||||
if frag.wRef():
|
||||
print(f"Adding {frag} as an anchor")
|
||||
self._buttons.append(frag)
|
||||
base += line.getLineSpacing()
|
||||
|
||||
for line in self._lines:
|
||||
for frag in line.getLine():
|
||||
layout = frag.layout()
|
||||
for fmtRng in layout.formats():
|
||||
if fmtRng.format.isAnchor():
|
||||
runs = layout.glyphRuns(fmtRng.start, fmtRng.length)
|
||||
bb = runs[0].boundingRect()
|
||||
pos = layout.position()
|
||||
text = frag.text()[fmtRng.start:fmtRng.start + fmtRng.length]
|
||||
new = bb.topLeft() + pos
|
||||
print(f"({bb.left()}, {bb.top()}), ({pos.x()}, {pos.y()}), ({new.x()}, {new.y()}): {text}")
|
||||
bb.moveTo(bb.topLeft() + pos)
|
||||
self._buttons.append(
|
||||
{'bb': bb,
|
||||
'fmt': fmtRng.format,
|
||||
'frag': frag,
|
||||
}
|
||||
)
|
||||
self.setFixedHeight(base)
|
||||
return
|
||||
|
||||
def resizeEvent(self, event: Optional[QResizeEvent] = None) -> None:
|
||||
base = 0
|
||||
for line in self._lines:
|
||||
for idx, line in enumerate(self._lines):
|
||||
line.finalizeLine(self.width(), base)
|
||||
base += line.getLineSpacing()
|
||||
self.setFixedHeight(base)
|
||||
super(Definition, self).resizeEvent(event)
|
||||
return
|
||||
|
||||
_downFrag: Optional[Fragment | None] = None
|
||||
_downClickable: Optional[Clickable] = None
|
||||
|
||||
def mousePressEvent(self, event: Optional[QMouseEvent]) -> None:
|
||||
if not event:
|
||||
return super().mousePressEvent(event)
|
||||
print(f"mousePressEvent: {event.pos()}")
|
||||
for frag in self._buttons:
|
||||
rect = frag.clickRect()
|
||||
if rect.contains(event.pos()):
|
||||
self._downFrag = frag
|
||||
print(f"mousePressEvent: {event.position()}")
|
||||
for clk in self._buttons:
|
||||
if clk["bb"].contains(event.position()):
|
||||
print("inside")
|
||||
self._downClickable = clk
|
||||
return
|
||||
return super().mousePressEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event: Optional[QMouseEvent]) -> None:
|
||||
if not event:
|
||||
return super().mouseReleaseEvent(event)
|
||||
if self._downFrag is not None and self._downFrag.clickRect().contains(
|
||||
event.pos()
|
||||
if (self._downClickable is not None and
|
||||
self._downClickable["bb"].contains(event.position())
|
||||
):
|
||||
audio = self._downFrag.audio().url()
|
||||
print(audio)
|
||||
self.pronounce.emit(audio)
|
||||
print("emit done")
|
||||
self._downFrag = None
|
||||
print(f"mousePressPseudoEvent: {event.position()}")
|
||||
clk = self._downClickable
|
||||
bb = clk['bb']
|
||||
print(f"({bb.left()}-{bb.right()}, {bb.top()}-{bb.bottom()})", clk["fmt"].anchorHref(),)
|
||||
#self.pronounce.emit(audio)
|
||||
self._downClickable = None
|
||||
return
|
||||
self._downFrag = None
|
||||
self._downClickable = None
|
||||
return super().mouseReleaseEvent(event)
|
||||
|
||||
def paintEvent(self, _: Optional[QPaintEvent]) -> None: # noqa
|
||||
painter = QPainter(self)
|
||||
painter.save()
|
||||
painter.setBrush(QBrush())
|
||||
painter.setPen(QColor("white"))
|
||||
|
||||
red = QColor("red")
|
||||
#
|
||||
# Each line needs a base calculated. To do that, we need to find the
|
||||
# bounding rectangle of the text. Once we have the bounding rectangle,
|
||||
@@ -577,11 +530,17 @@ class Definition(QWidget):
|
||||
# All text on this line needs to be on the same baseline
|
||||
#
|
||||
assert self._lines is not None
|
||||
base = 0
|
||||
for line in self._lines:
|
||||
transform = QTransform()
|
||||
transform.translate(0, base)
|
||||
painter.setTransform(transform)
|
||||
base += line.paintEvent(painter)
|
||||
painter.restore()
|
||||
for idx, line in enumerate(self._lines):
|
||||
text = ''
|
||||
for frag in line.getLine():
|
||||
text += frag.text() + '_'
|
||||
line.paintEvent(painter)
|
||||
green = QColor("green")
|
||||
for clickRect in self._buttons:
|
||||
painter.setPen(green)
|
||||
painter.drawRect(clickRect['bb'])
|
||||
painter.setPen(red)
|
||||
bb = clickRect['frag'].layout().boundingRect()
|
||||
bb.moveTo(clickRect['frag'].layout().position())
|
||||
painter.drawRect(bb)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user