Layout is good, click boxes is wrong
This commit is contained in:
30
deftest.py
Normal file → Executable file
30
deftest.py
Normal file → Executable file
@@ -3,20 +3,28 @@ import faulthandler
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
from typing import cast
|
||||
from typing import Any, cast
|
||||
|
||||
from PyQt6.QtCore import QResource, QSettings
|
||||
from PyQt6.QtCore import QResource, QSettings, Qt
|
||||
from PyQt6.QtGui import QFontDatabase
|
||||
from PyQt6.QtSql import QSqlDatabase, QSqlQuery
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
from PyQt6.QtWidgets import QApplication, QScrollArea
|
||||
|
||||
from lib import DefinitionArea, Word
|
||||
from lib import Word
|
||||
from lib.sounds import SoundOff
|
||||
from lib.utils import query_error
|
||||
from lib.words import Definition
|
||||
|
||||
class DefinitionArea(QScrollArea):
|
||||
def __init__(self, w: Word, *args: Any, **kwargs: Any) -> None:
|
||||
super(DefinitionArea, self).__init__(*args, *kwargs)
|
||||
d = Definition(w)
|
||||
self.setWidget(d)
|
||||
self.setWidgetResizable(True)
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
|
||||
return
|
||||
|
||||
def monkeyClose(self, event):
|
||||
def closeEvent(self, event):
|
||||
settings = QSettings("Troglodite", "esl_reader")
|
||||
settings.setValue("geometry", self.saveGeometry())
|
||||
super(DefinitionArea, self).closeEvent(event)
|
||||
@@ -66,12 +74,15 @@ def main() -> int:
|
||||
):
|
||||
query_error(query)
|
||||
|
||||
word = Word("cowbell")
|
||||
word = Word("lower")
|
||||
snd = SoundOff()
|
||||
DefinitionArea.closeEvent = monkeyClose
|
||||
print("Pre widget")
|
||||
widget = DefinitionArea(word) # xnoqa: F841
|
||||
print("post widget")
|
||||
settings = QSettings("Troglodite", "esl_reader")
|
||||
widget.restoreGeometry(settings.value("geometry"))
|
||||
geometry = settings.value("geometry")
|
||||
if geometry is not None:
|
||||
widget.restoreGeometry(geometry)
|
||||
d = cast(Definition, widget.widget())
|
||||
assert d is not None
|
||||
d.pronounce.connect(snd.playSound)
|
||||
@@ -81,4 +92,7 @@ def main() -> int:
|
||||
|
||||
if __name__ == "__main__":
|
||||
faulthandler.register(signal.Signals.SIGUSR1)
|
||||
faulthandler.register(signal.Signals.SIGTERM)
|
||||
faulthandler.register(signal.Signals.SIGHUP)
|
||||
faulthandler.enable()
|
||||
sys.exit(main())
|
||||
|
||||
@@ -5,4 +5,4 @@ from .definition import Definition, Fragment, Line
|
||||
from .person import PersonDialog
|
||||
from .read import ReadDialog
|
||||
from .session import SessionDialog
|
||||
from .words import DefinitionArea, Word
|
||||
from .words import Word
|
||||
|
||||
@@ -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)
|
||||
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.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
|
||||
)
|
||||
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
|
||||
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
|
||||
|
||||
left = 0 # Left size of rect
|
||||
maxHeight = 0
|
||||
|
||||
for frag in self._fragments:
|
||||
if left < frag.pixelIndent():
|
||||
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()
|
||||
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
|
||||
|
||||
56
lib/utils.py
56
lib/utils.py
@@ -2,7 +2,7 @@
|
||||
from typing import NoReturn, Self
|
||||
|
||||
from PyQt6.QtCore import QCoreApplication, QDir, QStandardPaths, Qt
|
||||
from PyQt6.QtGui import QColor, QFont, QFontDatabase
|
||||
from PyQt6.QtGui import QColor, QFont, QFontDatabase, QTextCharFormat
|
||||
from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkDiskCache
|
||||
from PyQt6.QtSql import QSqlQuery
|
||||
|
||||
@@ -41,11 +41,58 @@ class Resources:
|
||||
|
||||
subduedBackground: QColor
|
||||
|
||||
headerFormat = QTextCharFormat()
|
||||
labelFormat = QTextCharFormat()
|
||||
subduedFormat = QTextCharFormat()
|
||||
subduedItalicFormat = QTextCharFormat()
|
||||
sOnSFormat = QTextCharFormat()
|
||||
subduedLabelFormat = QTextCharFormat()
|
||||
phonticFormat = QTextCharFormat()
|
||||
boldFormat = QTextCharFormat()
|
||||
boldOnSFormat = QTextCharFormat()
|
||||
italicFormat = QTextCharFormat()
|
||||
textFormat = QTextCharFormat()
|
||||
smallCapsFormat = QTextCharFormat()
|
||||
|
||||
def __new__(cls: type[Self]) -> Self:
|
||||
if cls._instance:
|
||||
return cls._instance
|
||||
cls._instance = super(Resources, cls).__new__(cls)
|
||||
#
|
||||
# colors
|
||||
#
|
||||
cls.baseColor = QColor(Qt.GlobalColor.white)
|
||||
cls.linkColor = QColor("#4a7d95")
|
||||
cls.subduedColor = QColor(Qt.GlobalColor.gray)
|
||||
cls.subduedBackground = QColor("#444")
|
||||
#
|
||||
# Formats
|
||||
#
|
||||
LARGE = 36
|
||||
MEDIUM = 22
|
||||
SMALL = 18
|
||||
cls.headerFormat.setFontPointSize(LARGE)
|
||||
cls.labelFormat.setFontPointSize(MEDIUM)
|
||||
cls.sOnSFormat.setForeground(cls.subduedColor)
|
||||
#cls.sOnSFormat.setBackground(cls.subduedBackground)
|
||||
cls.sOnSFormat.setFontPointSize(SMALL)
|
||||
cls.subduedFormat.setForeground(cls.subduedColor)
|
||||
cls.subduedFormat.setFontPointSize(SMALL)
|
||||
cls.subduedLabelFormat.setForeground(cls.subduedColor)
|
||||
cls.subduedLabelFormat.setFontPointSize(SMALL)
|
||||
cls.phonticFormat.setFont(QFontDatabase.font("Gentium", None,20))
|
||||
cls.phonticFormat.setFontPointSize(SMALL)
|
||||
cls.boldFormat.setFontWeight(QFont.Weight.Bold)
|
||||
cls.boldFormat.setFontPointSize(SMALL)
|
||||
cls.boldOnSFormat.setFontWeight(QFont.Weight.Bold)
|
||||
cls.boldOnSFormat.setFontPointSize(SMALL)
|
||||
cls.boldOnSFormat.setBackground(cls.subduedBackground)
|
||||
cls.italicFormat.setFontItalic(True)
|
||||
cls.italicFormat.setFontPointSize(SMALL)
|
||||
cls.textFormat.setFontPointSize(SMALL)
|
||||
cls.smallCapsFormat.setFontPointSize(SMALL)
|
||||
cls.smallCapsFormat.setFontCapitalization(QFont.Capitalization.SmallCaps)
|
||||
#
|
||||
# Fonts
|
||||
#
|
||||
cls.headerFont = QFontDatabase.font("OpenDyslexic", None, 10)
|
||||
@@ -68,13 +115,6 @@ class Resources:
|
||||
cls.phonicFont = QFontDatabase.font("Gentium", None, 10)
|
||||
cls.phonicFont.setPixelSize(20)
|
||||
|
||||
#
|
||||
# colors
|
||||
#
|
||||
cls.baseColor = QColor(Qt.GlobalColor.white)
|
||||
cls.linkColor = QColor("#4a7d95")
|
||||
cls.subduedColor = QColor(Qt.GlobalColor.gray)
|
||||
cls.subduedBackground = QColor("#444")
|
||||
#
|
||||
# Setup the Network Manager
|
||||
#
|
||||
|
||||
10
lib/words.py
10
lib/words.py
@@ -115,13 +115,3 @@ class Word:
|
||||
return lines
|
||||
except KeyError:
|
||||
raise Exception(f"Unknown source: {self.current['source']}")
|
||||
|
||||
|
||||
class DefinitionArea(QScrollArea):
|
||||
def __init__(self, w: Word, *args: Any, **kwargs: Any) -> None:
|
||||
super(DefinitionArea, self).__init__(*args, *kwargs)
|
||||
d = Definition(w)
|
||||
self.setWidget(d)
|
||||
self.setWidgetResizable(True)
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
|
||||
return
|
||||
|
||||
@@ -2,8 +2,8 @@ import json
|
||||
import re
|
||||
from typing import Any, Literal, NotRequired, TypedDict, cast
|
||||
|
||||
from PyQt6.QtCore import QEventLoop, Qt, QUrl
|
||||
from PyQt6.QtGui import QColor, QFont
|
||||
from PyQt6.QtCore import QEventLoop, QUrl
|
||||
from PyQt6.QtGui import QFont, QFontDatabase, QTextCharFormat, QTextLayout
|
||||
from PyQt6.QtNetwork import QNetworkRequest
|
||||
from trycast import trycast
|
||||
|
||||
@@ -151,6 +151,18 @@ class DefinitionSection(TypedDict):
|
||||
sls: NotRequired[list[str]]
|
||||
sseq: Any # list[list[Pair]]
|
||||
|
||||
DefinedRunOn = TypedDict(
|
||||
"DefinedRunOn",
|
||||
{
|
||||
"drp": str,
|
||||
"def": list[DefinitionSection],
|
||||
"et": NotRequired[list[Pair]],
|
||||
"lbs": NotRequired[list[str]],
|
||||
"prs": NotRequired[list[Pronunciation]],
|
||||
"sls": NotRequired[list[str]],
|
||||
"vrs": NotRequired[list[Variant]]
|
||||
}
|
||||
)
|
||||
|
||||
Definition = TypedDict(
|
||||
"Definition",
|
||||
@@ -302,13 +314,9 @@ def getFirstSound(definition: Any) -> QUrl:
|
||||
return QUrl()
|
||||
|
||||
|
||||
def do_prs(prs: list[Pronunciation] | None) -> list[Fragment]:
|
||||
def do_prs(frag: Fragment, prs: list[Pronunciation] | None) -> None:
|
||||
assert prs is not None
|
||||
r = Resources()
|
||||
frags: list[Fragment] = []
|
||||
font = r.labelFont
|
||||
linkColor = r.linkColor
|
||||
subduedColor = r.subduedColor
|
||||
|
||||
for pr in prs:
|
||||
if "pun" in pr:
|
||||
@@ -316,22 +324,30 @@ def do_prs(prs: list[Pronunciation] | None) -> list[Fragment]:
|
||||
else:
|
||||
pun = " "
|
||||
if "l" in pr:
|
||||
frags.append(
|
||||
Fragment(pr["l"] + pun, r.italicFont, color=subduedColor)
|
||||
)
|
||||
frag = Fragment(pr["mw"], font, color=subduedColor)
|
||||
frag.addText(pr["l"] + pun, r.subduedItalicFormat)
|
||||
fmt = r.phonticFormat
|
||||
if "sound" in pr:
|
||||
frag.setAudio(soundUrl(pr["sound"]))
|
||||
frag.setColor(linkColor)
|
||||
frags.append(frag)
|
||||
frags.append(Fragment(" ", r.phonicFont))
|
||||
fmt = QTextCharFormat(r.phonticFormat)
|
||||
fmt.setAnchor(True)
|
||||
fmt.setAnchorHref(soundUrl(pr["sound"]).toString())
|
||||
fmt.setForeground(r.linkColor)
|
||||
#text = pr["mw"] +' \N{SPEAKER} '
|
||||
text = pr["mw"] +' '
|
||||
else:
|
||||
text = pr['mw'] + ' '
|
||||
print(f"text: {text}, length: {len(text)}")
|
||||
frag.addText(text, fmt)
|
||||
if "l2" in pr:
|
||||
frags.append(Fragment(pun + pr["l2"], font, color=subduedColor))
|
||||
return frags
|
||||
frag.addText(pun + pr["l2"], r.subduedLabelFormat)
|
||||
text = frag.layout().text()
|
||||
for fmt in frag.layout().formats():
|
||||
print(f"start: {fmt.start}, length: {fmt.length}, text: \"{text[fmt.start:fmt.start+fmt.length]}\"")
|
||||
return
|
||||
|
||||
|
||||
def do_aq(aq: AttributionOfQuote | None) -> list[Line]:
|
||||
assert aq is not None
|
||||
raise NotImplementedError("aq")
|
||||
return []
|
||||
|
||||
|
||||
@@ -341,7 +357,8 @@ def do_vis(vis: list[VerbalIllustration] | None, indent=0) -> list[Line]:
|
||||
lines: list[Line] = []
|
||||
for vi in vis:
|
||||
line = Line()
|
||||
frag = Fragment(vi["t"], r.textFont, color=r.subduedColor)
|
||||
frag = Fragment()
|
||||
frag.addText(vi['t'], r.subduedFormat)
|
||||
if indent > 0:
|
||||
frag.setIndent(indent)
|
||||
line.addFragment(frag)
|
||||
@@ -376,90 +393,95 @@ def do_uns(
|
||||
return (frags, lines)
|
||||
|
||||
|
||||
def do_dt(
|
||||
dt: list[list[Pair]] | None, indent: int
|
||||
) -> tuple[list[Fragment], list[Line]]:
|
||||
def do_dt(frag, dt: list[list[Pair]] | None, indent: int) -> list[Line]:
|
||||
assert dt is not None
|
||||
frags: list[Fragment] = []
|
||||
lines: list[Line] = []
|
||||
r = Resources()
|
||||
first = True
|
||||
for entry in dt:
|
||||
for pair in entry:
|
||||
if pair["objType"] == "text":
|
||||
frag = Fragment(pair["obj"], r.textFont, color=r.baseColor)
|
||||
frag.setIndent(indent)
|
||||
if first:
|
||||
frags.append(frag)
|
||||
frag.setIndent(indent)
|
||||
frag.addText(pair["obj"], r.textFormat)
|
||||
else:
|
||||
line = Line()
|
||||
f = Fragment()
|
||||
f.setIndent(indent)
|
||||
f.addText(pair["obj"], r.textFormat)
|
||||
line.addFragment(frag)
|
||||
lines.append(line)
|
||||
elif pair["objType"] == "vis":
|
||||
first = False
|
||||
lines += do_vis(
|
||||
trycast(list[VerbalIllustration], pair["obj"]), indent
|
||||
)
|
||||
elif pair["objType"] == "uns":
|
||||
first = False
|
||||
(newFrags, newLines) = do_uns(
|
||||
trycast(list[list[list[Pair]]], pair["obj"]), indent
|
||||
)
|
||||
frags += newFrags
|
||||
lines += newLines
|
||||
#frags += newFrags
|
||||
#lines += newLines
|
||||
raise NotImplementedError("uns")
|
||||
else:
|
||||
print(json.dumps(pair, indent=2))
|
||||
raise NotImplementedError(
|
||||
f"Unknown or unimplimented element {pair['objType']}"
|
||||
)
|
||||
first = False
|
||||
return (frags, lines)
|
||||
return lines
|
||||
|
||||
|
||||
def do_sense(
|
||||
sense: Sense | None, indent: int = 3
|
||||
) -> tuple[list[Fragment], list[Line]]:
|
||||
if sense is None:
|
||||
return ([], [])
|
||||
) -> tuple[Fragment, list[Line]]:
|
||||
assert sense is not None
|
||||
lines: list[Line] = []
|
||||
frags: list[Fragment] = []
|
||||
r = Resources()
|
||||
first = True
|
||||
frag = Fragment()
|
||||
for k, v in sense.items():
|
||||
if k == "sn":
|
||||
continue
|
||||
elif k == "dt":
|
||||
(newFrags, newLines) = do_dt(
|
||||
trycast(list[list[Pair]], sense["dt"]), indent
|
||||
)
|
||||
frags += newFrags
|
||||
newLines = do_dt(frag, trycast(list[list[Pair]], sense["dt"]), indent)
|
||||
if first:
|
||||
firstFrag = frag
|
||||
frag = Fragment()
|
||||
else:
|
||||
line = Line()
|
||||
line.addFragment(frag)
|
||||
lines.append(line)
|
||||
lines += newLines
|
||||
elif k == "sdsense":
|
||||
# XXX - This needs to expand to handle et, ins, lbs, prs, sgram, sls, vrs
|
||||
sdsense = trycast(DividedSense, v)
|
||||
assert sdsense is not None
|
||||
frag = Fragment(
|
||||
sdsense["sd"] + " ", r.italicFont, color=r.baseColor
|
||||
)
|
||||
frag = Fragment()
|
||||
frag.setIndent(indent)
|
||||
frag.addText(sdsense["sd"] + ' ', r.italicFormat)
|
||||
line = Line()
|
||||
line.addFragment(frag)
|
||||
newLines = do_dt(frag, trycast(list[list[Pair]], sdsense["dt"]), indent=indent)
|
||||
if first:
|
||||
firstFrag = frag
|
||||
frag = Fragment()
|
||||
else:
|
||||
line = Line()
|
||||
line.addFragment(frag)
|
||||
(newFrags, newLines) = do_dt(
|
||||
trycast(list[list[Pair]], sdsense["dt"]), indent=indent
|
||||
)
|
||||
line.addFragment(newFrags)
|
||||
lines.append(line)
|
||||
lines += newLines
|
||||
elif k == "sls":
|
||||
labels = trycast(list[str], v)
|
||||
assert labels is not None
|
||||
frag = Fragment(
|
||||
", ".join(labels) + " ", r.boldFont, color=r.subduedColor
|
||||
)
|
||||
frag.setIndent(indent)
|
||||
frag.setBackground(r.subduedBackground)
|
||||
frags.append(frag)
|
||||
frag.addText(", ".join(labels) + " ",r.boldOnSFormat)
|
||||
elif "lbs" == k:
|
||||
pass
|
||||
else:
|
||||
print(k, v)
|
||||
raise NotImplementedError(f"Unknown or unimplimented element {k}")
|
||||
return (frags, lines)
|
||||
return (firstFrag, lines)
|
||||
|
||||
|
||||
def do_pseq(
|
||||
@@ -475,28 +497,23 @@ def do_pseq(
|
||||
for pair in entry:
|
||||
if pair["objType"] == "bs":
|
||||
sense = pair["obj"]["sense"]
|
||||
(newFrags, newLines) = do_sense(
|
||||
(frag, newLines) = do_sense(
|
||||
trycast(Sense, sense), indent=indent
|
||||
)
|
||||
frags += newFrags
|
||||
frags.append(frag)
|
||||
lines += newLines
|
||||
newLine = True
|
||||
elif pair["objType"] == "sense":
|
||||
frag = Fragment(f"({count})", r.textFont, color=r.baseColor)
|
||||
frag.setIndent(indent)
|
||||
sn = Fragment()
|
||||
sn.addText(f"({count})", r.textFormat)
|
||||
sn.setIndent(indent)
|
||||
(frag, newLines) = do_sense(trycast(Sense, pair["obj"]), indent=indent + 1)
|
||||
if newLine:
|
||||
line = Line()
|
||||
line.addFragment(sn)
|
||||
line.addFragment(frag)
|
||||
else:
|
||||
frags.append(frag)
|
||||
(newFrags, newLines) = do_sense(
|
||||
trycast(Sense, pair["obj"]), indent=indent + 1
|
||||
)
|
||||
if newLine:
|
||||
line.addFragment(newFrags)
|
||||
lines.append(line)
|
||||
else:
|
||||
frags += newFrags
|
||||
frags = [sn, frag, ]
|
||||
newLine = True
|
||||
lines += newLines
|
||||
count += 1
|
||||
@@ -510,17 +527,17 @@ def do_pseq(
|
||||
def do_sseq(sseq: list[list[list[Pair]]]) -> list[Line]:
|
||||
lines: list[Line] = []
|
||||
r = Resources()
|
||||
for outer, item_o in enumerate(sseq):
|
||||
line = Line()
|
||||
frag = Fragment(str(outer + 1), r.boldFont, color=r.baseColor)
|
||||
for outer, item_o in enumerate(sseq):
|
||||
frag = Fragment()
|
||||
frag.setIndent(1)
|
||||
frag.addText(str(outer +1), r.boldFormat)
|
||||
line.addFragment(frag)
|
||||
for inner, item_i in enumerate(item_o):
|
||||
indent = 2
|
||||
if len(item_o) > 1:
|
||||
frag = Fragment(
|
||||
chr(ord("a") + inner), r.boldFont, color=r.baseColor
|
||||
)
|
||||
frag = Fragment()
|
||||
frag.addText(chr(ord("a") + inner), r.boldFormat)
|
||||
frag.setIndent(2)
|
||||
line.addFragment(frag)
|
||||
indent = 3
|
||||
@@ -528,8 +545,8 @@ def do_sseq(sseq: list[list[list[Pair]]]) -> list[Line]:
|
||||
objType = pair["objType"]
|
||||
if objType == "sense":
|
||||
sense = trycast(Sense, pair["obj"])
|
||||
(frags, newlines) = do_sense(sense, indent=indent)
|
||||
line.addFragment(frags)
|
||||
(frag, newlines) = do_sense(sense, indent=indent)
|
||||
line.addFragment(frag)
|
||||
lines.append(line)
|
||||
line = Line()
|
||||
lines += newlines
|
||||
@@ -542,6 +559,7 @@ def do_sseq(sseq: list[list[list[Pair]]]) -> list[Line]:
|
||||
line = Line()
|
||||
lines += newlines
|
||||
elif objType == "bs":
|
||||
raise NotImplementedError("bs")
|
||||
sense = pair["obj"]["sense"]
|
||||
(newFrags, newLines) = do_sense(
|
||||
trycast(Sense, sense), indent=indent
|
||||
@@ -557,18 +575,15 @@ def do_sseq(sseq: list[list[list[Pair]]]) -> list[Line]:
|
||||
return lines
|
||||
|
||||
|
||||
def do_ins(inflections: list[Inflection] | None) -> list[Fragment]:
|
||||
def do_ins(frag: Fragment, inflections: list[Inflection] | None) -> None:
|
||||
assert inflections is not None
|
||||
r = Resources()
|
||||
frags: list[Fragment] = []
|
||||
sep = ""
|
||||
for inflection in inflections:
|
||||
if sep == "; ":
|
||||
frag = Fragment("; ", font=r.boldFont, color=r.baseColor)
|
||||
frags.append(frag)
|
||||
frag.addText(sep, r.boldFormat)
|
||||
elif sep != "":
|
||||
frag = Fragment(sep, font=r.italicFont, color=r.baseColor)
|
||||
frags.append(frag)
|
||||
frag.addText(sep, r.italicFormat)
|
||||
|
||||
if "ifc" in inflection:
|
||||
text = inflection["ifc"]
|
||||
@@ -577,19 +592,18 @@ def do_ins(inflections: list[Inflection] | None) -> list[Fragment]:
|
||||
else:
|
||||
raise ValueError(f"Missing 'if' or 'ifc' in {inflection}")
|
||||
|
||||
frag = Fragment(text, r.boldFont, color=r.baseColor)
|
||||
frags.append(frag)
|
||||
text = re.sub(r'\*', '\u00b7', text)
|
||||
frag.addText(text, r.boldFormat)
|
||||
sep = "; "
|
||||
if "il" in inflection:
|
||||
sep = " " + inflection["il"] + " "
|
||||
if "prs" in inflection:
|
||||
newFrags = do_prs(trycast(list[Pronunciation], inflection["prs"]))
|
||||
frags += newFrags
|
||||
do_prs(frag, trycast(list[Pronunciation], inflection["prs"]))
|
||||
if "spl" in inflection:
|
||||
raise NotImplementedError(
|
||||
f"We haven't implimented 'spl' for inflection: {inflection}"
|
||||
)
|
||||
return frags
|
||||
return
|
||||
|
||||
|
||||
def do_ets(ets: list[list[Pair]] | None) -> list[Line]:
|
||||
@@ -600,17 +614,15 @@ def do_ets(ets: list[list[Pair]] | None) -> list[Line]:
|
||||
for pair in et:
|
||||
if pair["objType"] == "text":
|
||||
line = Line()
|
||||
line.addFragment(
|
||||
Fragment(pair["obj"], r.textFont, color=r.baseColor)
|
||||
)
|
||||
frag = Fragment('', r.textFont)
|
||||
frag.addText(pair['obj'], r.textFormat)
|
||||
line.addFragment(frag)
|
||||
lines.append(line)
|
||||
elif pair["objType"] == "et_snote":
|
||||
line = Line()
|
||||
line.addFragment(
|
||||
Fragment(
|
||||
"Note: " + pair["obj"], r.textFont, color=r.baseColor
|
||||
)
|
||||
)
|
||||
frag = Fragment('', r.textFont)
|
||||
frag.addText(f"Note: {pair['obj']}",r.textFormat)
|
||||
line.addFragment(frag)
|
||||
lines.append(line)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
@@ -625,7 +637,9 @@ def do_def(entry: DefinitionSection) -> list[Line]:
|
||||
lines: list[Line] = []
|
||||
if "vd" in entry:
|
||||
line = Line()
|
||||
line.addFragment(Fragment(entry["vd"], r.italicFont, color=r.linkColor))
|
||||
frag = Fragment()
|
||||
frag.addText(entry["vd"], r.italicFormat)
|
||||
line.addFragment(frag)
|
||||
lines.append(line)
|
||||
#
|
||||
# sseg is required
|
||||
@@ -634,6 +648,46 @@ def do_def(entry: DefinitionSection) -> list[Line]:
|
||||
lines += do_sseq(sseq)
|
||||
return lines
|
||||
|
||||
def do_vrs(vrs: list[Variant]|None) -> Line:
|
||||
assert vrs is not None
|
||||
r = Resources()
|
||||
line = Line()
|
||||
frag = Fragment()
|
||||
frag.addText('variants: ', r.sOnSFormat)
|
||||
for var in vrs:
|
||||
if 'vl' in var:
|
||||
frag.addText(var['vl']+' ', r.italicFormat)
|
||||
if 'spl' in var:
|
||||
frag.addText(var['spl']+' ', r.sOnSFormat)
|
||||
frag.addText(var['va'], r.boldFormat)
|
||||
if 'prs' in var:
|
||||
frag.addText(' ')
|
||||
do_prs(frag, trycast(list[Pronunciation], var['prs']))
|
||||
frag.addText(' ')
|
||||
line.addFragment(frag)
|
||||
return line
|
||||
|
||||
def do_dros(dros: list[DefinedRunOn]|None) -> list[Line]:
|
||||
assert dros is not None
|
||||
r = Resources()
|
||||
lines: list[Line] = []
|
||||
for dro in dros:
|
||||
line = Line()
|
||||
frag = Fragment()
|
||||
frag.addText(dro["drp"], r.boldFormat)
|
||||
line.addFragment(frag)
|
||||
lines.append(line)
|
||||
for entry in dro['def']:
|
||||
lines += do_def(entry)
|
||||
for k,v in dro.items():
|
||||
if 'drp' == k or 'def' == k:
|
||||
continue
|
||||
elif 'et' == k:
|
||||
lines += do_ets(trycast(list[list[Pair]], v))
|
||||
else:
|
||||
raise NotImplementedError(f"Key of {k}")
|
||||
return lines
|
||||
|
||||
|
||||
def getDef(defines: Any) -> list[Line]:
|
||||
Line.setParseText(parseText)
|
||||
@@ -667,7 +721,7 @@ def getDef(defines: Any) -> list[Line]:
|
||||
used[k] = 0
|
||||
|
||||
ets: list[Line] = []
|
||||
|
||||
phrases: list[Line] = []
|
||||
for count, work in enumerate(workList):
|
||||
testId = work["meta"]["id"].lower().split(":")[0]
|
||||
#
|
||||
@@ -679,30 +733,26 @@ def getDef(defines: Any) -> list[Line]:
|
||||
# Create the First line from the hwi, [ahws] and fl
|
||||
#
|
||||
line = Line()
|
||||
frag = Fragment()
|
||||
hwi = trycast(HeadWordInformation, work["hwi"])
|
||||
assert hwi is not None
|
||||
hw = re.sub(r"\*", "", hwi["hw"])
|
||||
line.addFragment(Fragment(hw, r.headerFont, color=r.baseColor))
|
||||
frag.addText(hw,r.headerFormat)
|
||||
if "ahws" in work:
|
||||
ahws = trycast(list[AlternanteHeadword], work["ahws"])
|
||||
assert ahws is not None
|
||||
for ahw in ahws:
|
||||
hw = re.sub(r"\*", "", ahw["hw"])
|
||||
line.addFragment(
|
||||
Fragment(", " + hw, r.headerFont, color=r.baseColor)
|
||||
)
|
||||
frag.addText(", " + hw)
|
||||
if entries > 1:
|
||||
frag = Fragment(
|
||||
f" {count + 1} of {entries} ", r.textFont, color=r.subduedColor
|
||||
)
|
||||
frag.setBackground(r.subduedBackground)
|
||||
line.addFragment(frag)
|
||||
frag.addText(f" {count + 1} of {entries} ", r.sOnSFormat)
|
||||
if "fl" in work:
|
||||
text = work["fl"]
|
||||
used[text] += 1
|
||||
if uses[text] > 1:
|
||||
text += f" ({used[text]})"
|
||||
line.addFragment(Fragment(text, r.labelFont, color=r.baseColor))
|
||||
frag.addText(text, r.labelFormat)
|
||||
line.addFragment(frag)
|
||||
lines.append(line)
|
||||
|
||||
#
|
||||
@@ -710,39 +760,46 @@ def getDef(defines: Any) -> list[Line]:
|
||||
# While 'prs' is optional, the headword is not. This gets us what we want.
|
||||
#
|
||||
line = Line()
|
||||
frag = Fragment()
|
||||
if hwi["hw"].find("*") >= 0:
|
||||
hw = re.sub(r"\*", "\u00b7", hwi["hw"])
|
||||
line.addFragment(
|
||||
Fragment(hw + " ", r.textFont, color=r.subduedColor)
|
||||
)
|
||||
frag.addText(hw + " ", r.subduedFormat)
|
||||
if "prs" in hwi:
|
||||
newFrags = do_prs(trycast(list[Pronunciation], hwi["prs"]))
|
||||
line.addFragment(newFrags)
|
||||
do_prs(frag, trycast(list[Pronunciation], hwi["prs"]))
|
||||
line.addFragment(frag)
|
||||
lines.append(line)
|
||||
line = Line()
|
||||
frag = Fragment()
|
||||
if 'vrs' in work:
|
||||
lines.append(do_vrs(trycast(list[Variant], work['vrs'])))
|
||||
if "ins" in work:
|
||||
inflections = trycast(list[Inflection], work["ins"])
|
||||
newFrags = do_ins(inflections)
|
||||
line = Line()
|
||||
line.addFragment(newFrags)
|
||||
do_ins(frag,inflections)
|
||||
line.addFragment(frag)
|
||||
lines.append(line)
|
||||
line = Line()
|
||||
frag = Fragment()
|
||||
defines = trycast(list[DefinitionSection], work["def"])
|
||||
assert defines is not None
|
||||
for define in defines:
|
||||
try:
|
||||
lines += do_def(define)
|
||||
except NotImplementedError as e:
|
||||
print(e)
|
||||
except NotImplementedError:
|
||||
raise
|
||||
if "dros" in work:
|
||||
dros = trycast(list[DefinedRunOn], work["dros"])
|
||||
if len(phrases) < 1:
|
||||
frag = Fragment()
|
||||
frag.addText("Phrases", r.labelFormat)
|
||||
line = Line()
|
||||
line.addFragment(frag)
|
||||
phrases.append(line)
|
||||
phrases += do_dros(dros)
|
||||
if "et" in work:
|
||||
line = Line()
|
||||
line.addFragment(
|
||||
Fragment(
|
||||
f"{work['fl']} ({used[work['fl']]})",
|
||||
r.labelFont,
|
||||
color=r.baseColor,
|
||||
)
|
||||
)
|
||||
ets.append(line)
|
||||
frag = Fragment('', r.textFont)
|
||||
frag.addText(f"{work['fl']} ({used[work['fl']]})",r.labelFormat)
|
||||
line.addFragment(frag)
|
||||
ets += do_ets(trycast(list[list[Pair]], work["et"]))
|
||||
for k in work.keys():
|
||||
if k not in [
|
||||
@@ -756,9 +813,12 @@ def getDef(defines: Any) -> list[Line]:
|
||||
"et",
|
||||
"date",
|
||||
"shortdef",
|
||||
"vrs",
|
||||
"dros",
|
||||
]:
|
||||
# raise NotImplementedError(f"Unknown key {k} in work")
|
||||
print(f"Unknown key {k} in work")
|
||||
raise NotImplementedError(f"Unknown key {k} in work")
|
||||
if len(phrases) > 0:
|
||||
lines += phrases
|
||||
if len(ets) > 0:
|
||||
line = Line()
|
||||
line.addFragment(Fragment("Etymology", r.labelFont, color=r.baseColor))
|
||||
@@ -766,185 +826,116 @@ def getDef(defines: Any) -> list[Line]:
|
||||
lines += ets
|
||||
return lines
|
||||
|
||||
|
||||
def parseText(frag: Fragment) -> list[Fragment]:
|
||||
org = frag.text()
|
||||
if frag.asis():
|
||||
return [frag]
|
||||
|
||||
#
|
||||
# Get the fonts we might need.
|
||||
# We can't use Resources() because we don't know the original font.
|
||||
textFont = QFont(frag.font())
|
||||
textFont.setWeight(QFont.Weight.Normal)
|
||||
textFont.setItalic(False)
|
||||
textFont.setCapitalization(QFont.Capitalization.MixedCase)
|
||||
boldFont = QFont(textFont)
|
||||
boldFont.setBold(True)
|
||||
italicFont = QFont(textFont)
|
||||
italicFont.setItalic(True)
|
||||
smallCapsFont = QFont(textFont)
|
||||
smallCapsFont.setCapitalization(QFont.Capitalization.SmallCaps)
|
||||
scriptFont = QFont(textFont)
|
||||
scriptFont.setPixelSize(int(scriptFont.pixelSize() / 4))
|
||||
boldItalicFont = QFont(boldFont)
|
||||
boldItalicFont.setItalic(True)
|
||||
boldSmallCapsFont = QFont(smallCapsFont)
|
||||
boldSmallCapsFont.setBold(True)
|
||||
capsFont = QFont(textFont)
|
||||
capsFont.setCapitalization(QFont.Capitalization.AllUppercase)
|
||||
#
|
||||
# Default color:
|
||||
#
|
||||
baseColor = frag.color()
|
||||
def replaceCode(code:str) -> tuple[str, QTextCharFormat]:
|
||||
r = Resources()
|
||||
|
||||
results: list[Fragment] = []
|
||||
while True:
|
||||
text = frag.text()
|
||||
start = text.find("{")
|
||||
if start < 0:
|
||||
results.append(frag)
|
||||
return results
|
||||
if start > 0:
|
||||
newFrag = Fragment(frag)
|
||||
newFrag.setText(text[:start])
|
||||
results.append(newFrag)
|
||||
frag.setText(text[start:])
|
||||
continue
|
||||
#
|
||||
# Start == 0
|
||||
#
|
||||
|
||||
#
|
||||
# If the token is an end-token, return now.
|
||||
#
|
||||
if text.startswith("{/"):
|
||||
results.append(frag)
|
||||
return results
|
||||
|
||||
#
|
||||
# extract this token
|
||||
#
|
||||
end = text.find("}")
|
||||
token = text[1:end]
|
||||
frag.setText(text[end + 1 :])
|
||||
oldFont = QFont(frag.font())
|
||||
if token == "bc":
|
||||
newFrag = Fragment(": ", boldFont, color=baseColor)
|
||||
newFrag.setIndent(frag.indent())
|
||||
results.append(newFrag)
|
||||
continue
|
||||
if token in [
|
||||
"b",
|
||||
"inf",
|
||||
"it",
|
||||
"sc",
|
||||
"sup",
|
||||
"phrase",
|
||||
"parahw",
|
||||
"gloss",
|
||||
"qword",
|
||||
"wi",
|
||||
"dx",
|
||||
"dx_def",
|
||||
"dx_ety",
|
||||
"ma",
|
||||
]:
|
||||
if token == "b":
|
||||
frag.setFont(boldFont)
|
||||
elif token in ["it", "qword", "wi"]:
|
||||
frag.setFont(italicFont)
|
||||
elif token == "sc":
|
||||
frag.setFont(smallCapsFont)
|
||||
elif token in ["inf", "sup"]:
|
||||
frag.setFont(scriptFont)
|
||||
elif token == "phrase":
|
||||
frag.setFont(boldItalicFont)
|
||||
elif token == "parahw":
|
||||
frag.setFont(boldSmallCapsFont)
|
||||
elif token == "gloss":
|
||||
frag.setText("[" + frag.text())
|
||||
elif token in ["dx", "dx_ety"]:
|
||||
frag.setText("\u2014" + frag.text())
|
||||
elif token == "ma":
|
||||
frag.setText("\u2014 more at " + frag.text())
|
||||
elif token == "dx_def":
|
||||
frag.setText("(" + frag.text())
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown block marker: {token}")
|
||||
results += parseText(frag)
|
||||
frag = results.pop()
|
||||
frag.setFont(oldFont)
|
||||
text = frag.text()
|
||||
if not text.startswith("{/" + token + "}"):
|
||||
raise NotImplementedError(
|
||||
f"No matching close for {token} in {org}"
|
||||
)
|
||||
if token == "gloss":
|
||||
results[-1].setText(results[-1].text() + "]")
|
||||
elif token == "dx_def":
|
||||
results[-1].setText(results[-1].text() + ")")
|
||||
end = text.find("}")
|
||||
text = text[end + 1 :]
|
||||
frag.setText(text)
|
||||
continue
|
||||
#
|
||||
# These are codes that include all information within the token
|
||||
#
|
||||
fields = token.split("|")
|
||||
fmt = QTextCharFormat()
|
||||
if code == 'bc':
|
||||
fmt.setFontWeight(QFont.Weight.Bold)
|
||||
return (': ', fmt)
|
||||
elif code == 'ldquo':
|
||||
return ('\u201c', fmt)
|
||||
elif code == 'rdquo':
|
||||
return ('\u201d', fmt)
|
||||
fmt.setAnchor(True)
|
||||
fmt.setForeground(r.linkColor)
|
||||
fmt.setFontUnderline(True)
|
||||
fmt.setUnderlineColor(r.linkColor)
|
||||
fmt.setFontUnderline(True)
|
||||
fields = code.split('|')
|
||||
token = fields[0]
|
||||
if token in [
|
||||
"a_link",
|
||||
"d_link",
|
||||
"dxt",
|
||||
"et_link",
|
||||
"i_link",
|
||||
"mat",
|
||||
"sx",
|
||||
]:
|
||||
wref = ""
|
||||
htext = fields[1]
|
||||
oldFont = QFont(frag.font())
|
||||
target = "word"
|
||||
if token == "a_link":
|
||||
wref = fields[1]
|
||||
elif token in ["d_link", "et_link", "mat", "sx", "i_link"]:
|
||||
if fields[2] == "":
|
||||
wref = fields[1]
|
||||
if token == 'a_link':
|
||||
text = fields[1]
|
||||
fmt.setAnchorHref(fields[1])
|
||||
elif token in ['d_link', 'et_link', 'mat', 'sx', 'i_link']:
|
||||
text = fields[1]
|
||||
pre = 'word://'
|
||||
if fields[2] == '':
|
||||
fmt.setAnchorHref(pre+fields[1])
|
||||
else:
|
||||
wref = fields[2]
|
||||
if token == "i_link":
|
||||
frag.setFont(italicFont)
|
||||
elif token == "sx":
|
||||
frag.setFont(capsFont)
|
||||
elif token == "dxt":
|
||||
if fields[3] == "illustration":
|
||||
wref = fields[2]
|
||||
target = "article"
|
||||
elif fields[3] == "table":
|
||||
wref = fields[2]
|
||||
target = "table"
|
||||
fmt.setAnchorHref(pre+fields[2])
|
||||
if token == 'i_link':
|
||||
fmt.setFontItalic(True)
|
||||
elif token == 'sx':
|
||||
fmt.setFontCapitalization(QFont.Capitalization.SmallCaps)
|
||||
elif token == 'dxt':
|
||||
if fields[3] == 'illustration':
|
||||
fmt.setAnchorHref('article://'+fields[2])
|
||||
elif fields[3] == 'table':
|
||||
fmt.setAnchorHref('table://'+fields[2])
|
||||
elif fields[3] != "":
|
||||
wref = fields[3]
|
||||
target = "sense"
|
||||
fmt.setAnchorHref('sense://'+fields[3])
|
||||
else:
|
||||
wref = fields[1]
|
||||
target = "word"
|
||||
elif token == "a_link":
|
||||
target = "word"
|
||||
wref = fields[1]
|
||||
fmt.setAnchorHref('word://'+fields[1])
|
||||
elif token == 'et_link':
|
||||
if fields[2] != '':
|
||||
fmt.setAnchorHref('etymology://'+fields[2])
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown code: {token} in {org}")
|
||||
newFrag = Fragment(frag)
|
||||
newFrag.setText(htext)
|
||||
newFrag.setWRef(wref)
|
||||
newFrag.setTarget(target)
|
||||
newFrag.setColor(r.linkColor)
|
||||
results.append(newFrag)
|
||||
frag.setFont(oldFont)
|
||||
text = frag.text()
|
||||
continue
|
||||
raise NotImplementedError(
|
||||
f"Unable to locate a known token {token} in {org}"
|
||||
)
|
||||
fmt.setAnchorHref('etymology://' + fields[1])
|
||||
else:
|
||||
raise NotImplementedError(f"Token {code} not implimented")
|
||||
fmt.setForeground(r.linkColor)
|
||||
print(f"Format.capitalization(): {fmt.fontCapitalization()}")
|
||||
return (text,fmt)
|
||||
|
||||
def markup(offset: int, text:str) -> tuple[str, list[QTextLayout.FormatRange]]:
|
||||
close = text.find('}')
|
||||
code = text[1:close]
|
||||
text = text[close+1:-(close+2)]
|
||||
fmt = QTextCharFormat()
|
||||
if code == 'b':
|
||||
fmt.setFontWeight(QFont.Weight.Bold)
|
||||
elif code == 'inf':
|
||||
fmt.setVerticalAlignment(QTextCharFormat.VerticalAlignment.AlignSubScript)
|
||||
elif code == 'it':
|
||||
fmt.setFontItalic(True)
|
||||
elif code == 'sc':
|
||||
fmt.setFontCapitalization(QFont.Capitalization.SmallCaps)
|
||||
fr = QTextLayout.FormatRange()
|
||||
fr.start = offset
|
||||
fr.length = len(text)
|
||||
fr.format = fmt
|
||||
return (text, [fr,])
|
||||
|
||||
def parseText(frag: Fragment) -> QTextLayout:
|
||||
layout = frag.layout()
|
||||
text = layout.text()
|
||||
formats = layout.formats()
|
||||
REPLACE_TEXT = [
|
||||
'bc','a_link', 'd_link', 'dxt', 'et_link', 'i_link', 'mat',
|
||||
'sx'
|
||||
]
|
||||
pos = 0
|
||||
start = text[pos:].find('{')
|
||||
|
||||
while start >= 0:
|
||||
start += pos
|
||||
end = text[start+1:].find('}')
|
||||
end += start
|
||||
code = text[start+1:end+1]
|
||||
pos = end+2
|
||||
for maybe in REPLACE_TEXT:
|
||||
if code.startswith(maybe):
|
||||
(repl, tfmt) = replaceCode(code)
|
||||
text = text[:start] + repl + text[end+2:]
|
||||
fmt = QTextLayout.FormatRange()
|
||||
fmt.format = tfmt
|
||||
fmt.start=start
|
||||
fmt.length = len(repl)
|
||||
formats.append(fmt)
|
||||
pos = start + len(repl)
|
||||
code = ''
|
||||
break
|
||||
if code != '':
|
||||
needle = f'{{/{code}}}'
|
||||
codeEnd = text[start:].find(needle)
|
||||
codeEnd += start+len(needle)
|
||||
straw = text[start:codeEnd]
|
||||
(repl, frs) = markup(start, straw)
|
||||
fmt = QTextLayout.FormatRange()
|
||||
formats += frs
|
||||
text = text[:start] + repl + text[codeEnd:]
|
||||
pos = start + len(repl)
|
||||
start = text[pos:].find('{')
|
||||
layout.setFormats(formats)
|
||||
layout.setText(text)
|
||||
return layout
|
||||
|
||||
Reference in New Issue
Block a user