Layout is good, click boxes is wrong

This commit is contained in:
Christopher T. Johnson
2024-05-07 11:26:15 -04:00
parent 51b1121176
commit 7d2532d775
6 changed files with 568 additions and 574 deletions

30
deftest.py Normal file → Executable file
View File

@@ -3,20 +3,28 @@ import faulthandler
import os import os
import signal import signal
import sys 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.QtGui import QFontDatabase
from PyQt6.QtSql import QSqlDatabase, QSqlQuery 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.sounds import SoundOff
from lib.utils import query_error from lib.utils import query_error
from lib.words import Definition 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 = QSettings("Troglodite", "esl_reader")
settings.setValue("geometry", self.saveGeometry()) settings.setValue("geometry", self.saveGeometry())
super(DefinitionArea, self).closeEvent(event) super(DefinitionArea, self).closeEvent(event)
@@ -66,12 +74,15 @@ def main() -> int:
): ):
query_error(query) query_error(query)
word = Word("cowbell") word = Word("lower")
snd = SoundOff() snd = SoundOff()
DefinitionArea.closeEvent = monkeyClose print("Pre widget")
widget = DefinitionArea(word) # xnoqa: F841 widget = DefinitionArea(word) # xnoqa: F841
print("post widget")
settings = QSettings("Troglodite", "esl_reader") 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()) d = cast(Definition, widget.widget())
assert d is not None assert d is not None
d.pronounce.connect(snd.playSound) d.pronounce.connect(snd.playSound)
@@ -81,4 +92,7 @@ def main() -> int:
if __name__ == "__main__": if __name__ == "__main__":
faulthandler.register(signal.Signals.SIGUSR1) faulthandler.register(signal.Signals.SIGUSR1)
faulthandler.register(signal.Signals.SIGTERM)
faulthandler.register(signal.Signals.SIGHUP)
faulthandler.enable()
sys.exit(main()) sys.exit(main())

View File

@@ -5,4 +5,4 @@ from .definition import Definition, Fragment, Line
from .person import PersonDialog from .person import PersonDialog
from .read import ReadDialog from .read import ReadDialog
from .session import SessionDialog from .session import SessionDialog
from .words import DefinitionArea, Word from .words import Word

View File

@@ -1,20 +1,23 @@
import re import unicodedata
from typing import Any, Callable, Optional, Self, cast, overload 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 ( from PyQt6.QtGui import (
QBrush, QBrush,
QColor, QColor,
QFont, QFont,
QFontDatabase,
QFontMetrics, QFontMetrics,
QMouseEvent, QMouseEvent,
QPainter, QPainter,
QPaintEvent, QPaintEvent,
QResizeEvent, QResizeEvent,
QTextCharFormat,
QTextLayout,
QTextOption, QTextOption,
QTransform,
) )
from PyQt6.QtWidgets import QWidget from PyQt6.QtWidgets import QWidget
from trycast import trycast
class Fragment: class Fragment:
@@ -24,7 +27,7 @@ class Fragment:
def __init__( def __init__(
self, self,
which: str | Self, which: str | Self | None = None,
font: QFont | None = None, font: QFont | None = None,
audio: str = "", audio: str = "",
color: Optional[QColor] = None, color: Optional[QColor] = None,
@@ -34,19 +37,22 @@ class Fragment:
for k, v in which.__dict__.items(): for k, v in which.__dict__.items():
self.__dict__[k] = v self.__dict__[k] = v
return return
self._text: str = which self._layout = QTextLayout()
if font is None: if font is None:
raise TypeError("Missing required parameter 'font'") self._layout.setFont(
self._font = font QFontDatabase.font("OpenDyslexic", None, 20)
self._audio: QUrl = QUrl(audio) )
self._align = QTextOption( else:
self._layout.setFont(font)
align = QTextOption(
Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignBaseline Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignBaseline
) )
self._layout.setTextOption(align)
self._audio: QUrl = QUrl(audio)
self._padding = QMargins() self._padding = QMargins()
self._border = QMargins() self._border = QMargins()
self._margin = QMargins() self._margin = QMargins()
self._wref = "" self._wref = ""
self._position = QPoint()
self._rect = QRect() self._rect = QRect()
self._borderRect = QRect() self._borderRect = QRect()
self._clickRect = QRect() self._clickRect = QRect()
@@ -57,133 +63,109 @@ class Fragment:
self._background = QColor() self._background = QColor()
self._asis = asis self._asis = asis
self._indent = 0 self._indent = 0
self._target = "word" if which is not None:
self.setText(which)
return return
def __str__(self) -> str: def size(self) -> QSize:
return self.__repr__() return self.paintEvent()
def size(self, width: int) -> QSize: def height(self) -> int:
return self.paintEvent(width) return self.size().height()
def height(self, width: int) -> int: def width(self) -> int:
return self.size(width).height() return self.size().width()
def width(self, width: int) -> int:
return self.size(width).width()
def __repr__(self) -> str: 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 doLayout(self, width: int) -> QPointF:
def paintEvent(self, widthSrc: int) -> QSize: 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, painter: Optional[QPainter] | None = None) -> QSize:
def paintEvent(self, widthSrc: QPainter) -> int: rect = self._layout.boundingRect()
... size = rect.size()
assert size is not None
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)
if painter is None: if painter is None:
return size return QSize(int(size.width()), int(size.height()))
painter.save() painter.save()
painter.setFont(self._font) self._layout.draw(painter, QPointF(0,0))
painter.setPen(QColor("#f00")) #
if self._audio.isValid(): # TODO: draw the rounded rect around audio buttons
radius = self._borderRect.height() / 2 #
painter.drawRoundedRect(self._borderRect, radius, radius) painter.brush().setColor(Qt.GlobalColor.green)
if self._wref: for fmt in self._layout.formats():
start = bounding.bottomLeft() if fmt.format.isAnchor():
end = bounding.bottomRight() #text = self._layout.text()[fmt.start:fmt.start+fmt.length]
painter.drawLine(start, end) 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() painter.restore()
return size.height() return QSize(int(size.width()), int(size.height()))
# #
# Setters # Setters
# #
def setText(self, text: str) -> None: def addText(self, text: str, fmt: Optional[QTextCharFormat] = None) -> None:
self._text = text 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 return
def setTarget(self, target: str) -> None: def setText(self, text: str) -> None:
self._target = target 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 return
def setFont(self, font: QFont) -> None: def setFont(self, font: QFont) -> None:
self._font = font self._layout.setFont(font)
return return
def setAudio(self, audio: str | QUrl) -> None: def setAudio(self, audio: str | QUrl) -> None:
@@ -194,7 +176,7 @@ class Fragment:
return return
def setAlign(self, align: QTextOption) -> None: def setAlign(self, align: QTextOption) -> None:
self._align = align self._layout.setTextOption(align)
return return
def setRect(self, rect: QRect) -> None: def setRect(self, rect: QRect) -> None:
@@ -291,7 +273,7 @@ class Fragment:
return return
def setPosition(self, pnt: QPoint) -> None: def setPosition(self, pnt: QPoint) -> None:
self._position = pnt self._layout.setPosition(QPointF(pnt.x(), pnt.y()))
return return
def setBorderRect(self, rect: QRect) -> None: def setBorderRect(self, rect: QRect) -> None:
@@ -317,20 +299,23 @@ class Fragment:
# #
# Getters # Getters
# #
def background(self) -> QColor:
return self._background
def wRef(self) -> str: def wRef(self) -> str:
return self._wref return self._wref
def text(self) -> str: def text(self) -> str:
return self._text return self._layout.text()
def font(self) -> QFont: def font(self) -> QFont:
return self._font return self._layout.font()
def audio(self) -> QUrl: def audio(self) -> QUrl:
return self._audio return self._audio
def align(self) -> QTextOption: def align(self) -> QTextOption:
return self._align return self._layout.textOption()
def rect(self) -> QRect: def rect(self) -> QRect:
return self._rect return self._rect
@@ -344,8 +329,8 @@ class Fragment:
def margin(self) -> QMargins: def margin(self) -> QMargins:
return self._margin return self._margin
def position(self) -> QPoint: def position(self) -> QPointF:
return self._position return self._layout.position()
def borderRect(self) -> QRect: def borderRect(self) -> QRect:
return self._borderRect return self._borderRect
@@ -365,6 +350,8 @@ class Fragment:
def pixelIndent(self) -> int: def pixelIndent(self) -> int:
return self._indent * self._indentAmount return self._indent * self._indentAmount
def layout(self) -> QTextLayout:
return self._layout
class Line: class Line:
parseText = None parseText = None
@@ -391,99 +378,48 @@ class Line:
# #
# we do not have an event field because we are not a true widget # 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: for frag in self._fragments:
ls = frag.paintEvent(painter) pos = frag.paintEvent(painter)
if ls > lineSpacing: return pos.height()
lineSpacing = ls
return lineSpacing
def addFragment( def addFragment(
self, self,
frags: Fragment | list[Fragment], frags: Fragment | list[Fragment],
) -> None: ) -> None:
SPEAKER = "\U0001F508" #SPEAKER = "\U0001F508"
if not isinstance(frags, list): if not isinstance(frags, list):
frags = [ frags = [
frags, frags,
] ]
for frag in frags: self._fragments += 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)
return return
def finalizeLine(self, width: int, base: int) -> None: def finalizeLine(self, width: int, base: int) -> None:
"""Create all of the positions for all the fragments.""" """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 left = 0 # Left size of rect
leading = -1 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: for frag in self._fragments:
if left < frag.pixelIndent():
left = frag.pixelIndent() left = frag.pixelIndent()
if x < left: frag.setPosition(QPoint(left, base))
x = left eol =frag.doLayout(width)
# left = int(eol.x()+0.5)
# We need to calculate the location to draw the if frag.layout().lineCount() > 1:
# text. We also need to calculate the bounding Rectangle base = int(eol.y()+0.5)
# for this fragment if eol.y() > maxHeight:
# maxHeight = eol.y()
size = frag.size(width) self._maxHeight = int(maxHeight+0.5)
fm = QFontMetrics(frag.font()) self._leading = 0
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()
return return
def getLine(self) -> list[Fragment]: def getLine(self) -> list[Fragment]:
@@ -492,6 +428,10 @@ class Line:
def getLineSpacing(self) -> int: def getLineSpacing(self) -> int:
return self._leading + self._maxHeight return self._leading + self._maxHeight
class Clickable(TypedDict):
bb: QRectF
frag: Fragment
fmt: QTextCharFormat
class Definition(QWidget): class Definition(QWidget):
pronounce = pyqtSignal(str) pronounce = pyqtSignal(str)
@@ -510,64 +450,77 @@ class Definition(QWidget):
lines: list[Line] = word.get_def() lines: list[Line] = word.get_def()
assert lines is not None assert lines is not None
self._lines = lines self._lines = lines
self._buttons: list[Fragment] = [] self._buttons: list[Clickable] = []
base = 0 base = 0
for line in self._lines: for line in self._lines:
line.finalizeLine(self.width(), base) 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() 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) self.setFixedHeight(base)
return return
def resizeEvent(self, event: Optional[QResizeEvent] = None) -> None: def resizeEvent(self, event: Optional[QResizeEvent] = None) -> None:
base = 0 base = 0
for line in self._lines: for idx, line in enumerate(self._lines):
line.finalizeLine(self.width(), base) line.finalizeLine(self.width(), base)
base += line.getLineSpacing() base += line.getLineSpacing()
self.setFixedHeight(base) self.setFixedHeight(base)
super(Definition, self).resizeEvent(event) super(Definition, self).resizeEvent(event)
return return
_downFrag: Optional[Fragment | None] = None _downClickable: Optional[Clickable] = None
def mousePressEvent(self, event: Optional[QMouseEvent]) -> None: def mousePressEvent(self, event: Optional[QMouseEvent]) -> None:
if not event: if not event:
return super().mousePressEvent(event) return super().mousePressEvent(event)
print(f"mousePressEvent: {event.pos()}") print(f"mousePressEvent: {event.position()}")
for frag in self._buttons: for clk in self._buttons:
rect = frag.clickRect() if clk["bb"].contains(event.position()):
if rect.contains(event.pos()): print("inside")
self._downFrag = frag self._downClickable = clk
return return
return super().mousePressEvent(event) return super().mousePressEvent(event)
def mouseReleaseEvent(self, event: Optional[QMouseEvent]) -> None: def mouseReleaseEvent(self, event: Optional[QMouseEvent]) -> None:
if not event: if not event:
return super().mouseReleaseEvent(event) return super().mouseReleaseEvent(event)
if self._downFrag is not None and self._downFrag.clickRect().contains( if (self._downClickable is not None and
event.pos() self._downClickable["bb"].contains(event.position())
): ):
audio = self._downFrag.audio().url() print(f"mousePressPseudoEvent: {event.position()}")
print(audio) clk = self._downClickable
self.pronounce.emit(audio) bb = clk['bb']
print("emit done") print(f"({bb.left()}-{bb.right()}, {bb.top()}-{bb.bottom()})", clk["fmt"].anchorHref(),)
self._downFrag = None #self.pronounce.emit(audio)
self._downClickable = None
return return
self._downFrag = None self._downClickable = None
return super().mouseReleaseEvent(event) return super().mouseReleaseEvent(event)
def paintEvent(self, _: Optional[QPaintEvent]) -> None: # noqa def paintEvent(self, _: Optional[QPaintEvent]) -> None: # noqa
painter = QPainter(self) painter = QPainter(self)
painter.save()
painter.setBrush(QBrush()) painter.setBrush(QBrush())
painter.setPen(QColor("white")) painter.setPen(QColor("white"))
red = QColor("red")
# #
# Each line needs a base calculated. To do that, we need to find the # 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, # 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 # All text on this line needs to be on the same baseline
# #
assert self._lines is not None assert self._lines is not None
base = 0 for idx, line in enumerate(self._lines):
for line in self._lines: text = ''
transform = QTransform() for frag in line.getLine():
transform.translate(0, base) text += frag.text() + '_'
painter.setTransform(transform) line.paintEvent(painter)
base += line.paintEvent(painter) green = QColor("green")
painter.restore() 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 return

View File

@@ -2,7 +2,7 @@
from typing import NoReturn, Self from typing import NoReturn, Self
from PyQt6.QtCore import QCoreApplication, QDir, QStandardPaths, Qt 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.QtNetwork import QNetworkAccessManager, QNetworkDiskCache
from PyQt6.QtSql import QSqlQuery from PyQt6.QtSql import QSqlQuery
@@ -41,11 +41,58 @@ class Resources:
subduedBackground: QColor 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: def __new__(cls: type[Self]) -> Self:
if cls._instance: if cls._instance:
return cls._instance return cls._instance
cls._instance = super(Resources, cls).__new__(cls) 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 # Fonts
# #
cls.headerFont = QFontDatabase.font("OpenDyslexic", None, 10) cls.headerFont = QFontDatabase.font("OpenDyslexic", None, 10)
@@ -68,13 +115,6 @@ class Resources:
cls.phonicFont = QFontDatabase.font("Gentium", None, 10) cls.phonicFont = QFontDatabase.font("Gentium", None, 10)
cls.phonicFont.setPixelSize(20) 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 # Setup the Network Manager
# #

View File

@@ -115,13 +115,3 @@ class Word:
return lines return lines
except KeyError: except KeyError:
raise Exception(f"Unknown source: {self.current['source']}") 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

View File

@@ -2,8 +2,8 @@ import json
import re import re
from typing import Any, Literal, NotRequired, TypedDict, cast from typing import Any, Literal, NotRequired, TypedDict, cast
from PyQt6.QtCore import QEventLoop, Qt, QUrl from PyQt6.QtCore import QEventLoop, QUrl
from PyQt6.QtGui import QColor, QFont from PyQt6.QtGui import QFont, QFontDatabase, QTextCharFormat, QTextLayout
from PyQt6.QtNetwork import QNetworkRequest from PyQt6.QtNetwork import QNetworkRequest
from trycast import trycast from trycast import trycast
@@ -151,6 +151,18 @@ class DefinitionSection(TypedDict):
sls: NotRequired[list[str]] sls: NotRequired[list[str]]
sseq: Any # list[list[Pair]] 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 = TypedDict(
"Definition", "Definition",
@@ -302,13 +314,9 @@ def getFirstSound(definition: Any) -> QUrl:
return 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 assert prs is not None
r = Resources() r = Resources()
frags: list[Fragment] = []
font = r.labelFont
linkColor = r.linkColor
subduedColor = r.subduedColor
for pr in prs: for pr in prs:
if "pun" in pr: if "pun" in pr:
@@ -316,22 +324,30 @@ def do_prs(prs: list[Pronunciation] | None) -> list[Fragment]:
else: else:
pun = " " pun = " "
if "l" in pr: if "l" in pr:
frags.append( frag.addText(pr["l"] + pun, r.subduedItalicFormat)
Fragment(pr["l"] + pun, r.italicFont, color=subduedColor) fmt = r.phonticFormat
)
frag = Fragment(pr["mw"], font, color=subduedColor)
if "sound" in pr: if "sound" in pr:
frag.setAudio(soundUrl(pr["sound"])) fmt = QTextCharFormat(r.phonticFormat)
frag.setColor(linkColor) fmt.setAnchor(True)
frags.append(frag) fmt.setAnchorHref(soundUrl(pr["sound"]).toString())
frags.append(Fragment(" ", r.phonicFont)) 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: if "l2" in pr:
frags.append(Fragment(pun + pr["l2"], font, color=subduedColor)) frag.addText(pun + pr["l2"], r.subduedLabelFormat)
return frags 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]: def do_aq(aq: AttributionOfQuote | None) -> list[Line]:
assert aq is not None assert aq is not None
raise NotImplementedError("aq")
return [] return []
@@ -341,7 +357,8 @@ def do_vis(vis: list[VerbalIllustration] | None, indent=0) -> list[Line]:
lines: list[Line] = [] lines: list[Line] = []
for vi in vis: for vi in vis:
line = Line() line = Line()
frag = Fragment(vi["t"], r.textFont, color=r.subduedColor) frag = Fragment()
frag.addText(vi['t'], r.subduedFormat)
if indent > 0: if indent > 0:
frag.setIndent(indent) frag.setIndent(indent)
line.addFragment(frag) line.addFragment(frag)
@@ -376,90 +393,95 @@ def do_uns(
return (frags, lines) return (frags, lines)
def do_dt( def do_dt(frag, dt: list[list[Pair]] | None, indent: int) -> list[Line]:
dt: list[list[Pair]] | None, indent: int
) -> tuple[list[Fragment], list[Line]]:
assert dt is not None assert dt is not None
frags: list[Fragment] = []
lines: list[Line] = [] lines: list[Line] = []
r = Resources() r = Resources()
first = True first = True
for entry in dt: for entry in dt:
for pair in entry: for pair in entry:
if pair["objType"] == "text": if pair["objType"] == "text":
frag = Fragment(pair["obj"], r.textFont, color=r.baseColor)
frag.setIndent(indent)
if first: if first:
frags.append(frag) frag.setIndent(indent)
frag.addText(pair["obj"], r.textFormat)
else: else:
line = Line() line = Line()
f = Fragment()
f.setIndent(indent)
f.addText(pair["obj"], r.textFormat)
line.addFragment(frag) line.addFragment(frag)
lines.append(line) lines.append(line)
elif pair["objType"] == "vis": elif pair["objType"] == "vis":
first = False
lines += do_vis( lines += do_vis(
trycast(list[VerbalIllustration], pair["obj"]), indent trycast(list[VerbalIllustration], pair["obj"]), indent
) )
elif pair["objType"] == "uns": elif pair["objType"] == "uns":
first = False
(newFrags, newLines) = do_uns( (newFrags, newLines) = do_uns(
trycast(list[list[list[Pair]]], pair["obj"]), indent trycast(list[list[list[Pair]]], pair["obj"]), indent
) )
frags += newFrags #frags += newFrags
lines += newLines #lines += newLines
raise NotImplementedError("uns")
else: else:
print(json.dumps(pair, indent=2)) print(json.dumps(pair, indent=2))
raise NotImplementedError( raise NotImplementedError(
f"Unknown or unimplimented element {pair['objType']}" f"Unknown or unimplimented element {pair['objType']}"
) )
first = False first = False
return (frags, lines) return lines
def do_sense( def do_sense(
sense: Sense | None, indent: int = 3 sense: Sense | None, indent: int = 3
) -> tuple[list[Fragment], list[Line]]: ) -> tuple[Fragment, list[Line]]:
if sense is None: assert sense is not None
return ([], [])
lines: list[Line] = [] lines: list[Line] = []
frags: list[Fragment] = []
r = Resources() r = Resources()
first = True
frag = Fragment()
for k, v in sense.items(): for k, v in sense.items():
if k == "sn": if k == "sn":
continue continue
elif k == "dt": elif k == "dt":
(newFrags, newLines) = do_dt( newLines = do_dt(frag, trycast(list[list[Pair]], sense["dt"]), indent)
trycast(list[list[Pair]], sense["dt"]), indent if first:
) firstFrag = frag
frags += newFrags frag = Fragment()
else:
line = Line()
line.addFragment(frag)
lines.append(line)
lines += newLines lines += newLines
elif k == "sdsense": elif k == "sdsense":
# XXX - This needs to expand to handle et, ins, lbs, prs, sgram, sls, vrs # XXX - This needs to expand to handle et, ins, lbs, prs, sgram, sls, vrs
sdsense = trycast(DividedSense, v) sdsense = trycast(DividedSense, v)
assert sdsense is not None assert sdsense is not None
frag = Fragment( frag = Fragment()
sdsense["sd"] + " ", r.italicFont, color=r.baseColor
)
frag.setIndent(indent) 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 = Line()
line.addFragment(frag) line.addFragment(frag)
(newFrags, newLines) = do_dt(
trycast(list[list[Pair]], sdsense["dt"]), indent=indent
)
line.addFragment(newFrags)
lines.append(line) lines.append(line)
lines += newLines lines += newLines
elif k == "sls": elif k == "sls":
labels = trycast(list[str], v) labels = trycast(list[str], v)
assert labels is not None assert labels is not None
frag = Fragment( frag.addText(", ".join(labels) + " ",r.boldOnSFormat)
", ".join(labels) + " ", r.boldFont, color=r.subduedColor elif "lbs" == k:
) pass
frag.setIndent(indent)
frag.setBackground(r.subduedBackground)
frags.append(frag)
else: else:
print(k, v) print(k, v)
raise NotImplementedError(f"Unknown or unimplimented element {k}") raise NotImplementedError(f"Unknown or unimplimented element {k}")
return (frags, lines) return (firstFrag, lines)
def do_pseq( def do_pseq(
@@ -475,28 +497,23 @@ def do_pseq(
for pair in entry: for pair in entry:
if pair["objType"] == "bs": if pair["objType"] == "bs":
sense = pair["obj"]["sense"] sense = pair["obj"]["sense"]
(newFrags, newLines) = do_sense( (frag, newLines) = do_sense(
trycast(Sense, sense), indent=indent trycast(Sense, sense), indent=indent
) )
frags += newFrags frags.append(frag)
lines += newLines lines += newLines
newLine = True newLine = True
elif pair["objType"] == "sense": elif pair["objType"] == "sense":
frag = Fragment(f"({count})", r.textFont, color=r.baseColor) sn = Fragment()
frag.setIndent(indent) sn.addText(f"({count})", r.textFormat)
sn.setIndent(indent)
(frag, newLines) = do_sense(trycast(Sense, pair["obj"]), indent=indent + 1)
if newLine: if newLine:
line = Line() line = Line()
line.addFragment(sn)
line.addFragment(frag) line.addFragment(frag)
else: else:
frags.append(frag) frags = [sn, frag, ]
(newFrags, newLines) = do_sense(
trycast(Sense, pair["obj"]), indent=indent + 1
)
if newLine:
line.addFragment(newFrags)
lines.append(line)
else:
frags += newFrags
newLine = True newLine = True
lines += newLines lines += newLines
count += 1 count += 1
@@ -510,17 +527,17 @@ def do_pseq(
def do_sseq(sseq: list[list[list[Pair]]]) -> list[Line]: def do_sseq(sseq: list[list[list[Pair]]]) -> list[Line]:
lines: list[Line] = [] lines: list[Line] = []
r = Resources() r = Resources()
for outer, item_o in enumerate(sseq):
line = Line() 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.setIndent(1)
frag.addText(str(outer +1), r.boldFormat)
line.addFragment(frag) line.addFragment(frag)
for inner, item_i in enumerate(item_o): for inner, item_i in enumerate(item_o):
indent = 2 indent = 2
if len(item_o) > 1: if len(item_o) > 1:
frag = Fragment( frag = Fragment()
chr(ord("a") + inner), r.boldFont, color=r.baseColor frag.addText(chr(ord("a") + inner), r.boldFormat)
)
frag.setIndent(2) frag.setIndent(2)
line.addFragment(frag) line.addFragment(frag)
indent = 3 indent = 3
@@ -528,8 +545,8 @@ def do_sseq(sseq: list[list[list[Pair]]]) -> list[Line]:
objType = pair["objType"] objType = pair["objType"]
if objType == "sense": if objType == "sense":
sense = trycast(Sense, pair["obj"]) sense = trycast(Sense, pair["obj"])
(frags, newlines) = do_sense(sense, indent=indent) (frag, newlines) = do_sense(sense, indent=indent)
line.addFragment(frags) line.addFragment(frag)
lines.append(line) lines.append(line)
line = Line() line = Line()
lines += newlines lines += newlines
@@ -542,6 +559,7 @@ def do_sseq(sseq: list[list[list[Pair]]]) -> list[Line]:
line = Line() line = Line()
lines += newlines lines += newlines
elif objType == "bs": elif objType == "bs":
raise NotImplementedError("bs")
sense = pair["obj"]["sense"] sense = pair["obj"]["sense"]
(newFrags, newLines) = do_sense( (newFrags, newLines) = do_sense(
trycast(Sense, sense), indent=indent trycast(Sense, sense), indent=indent
@@ -557,18 +575,15 @@ def do_sseq(sseq: list[list[list[Pair]]]) -> list[Line]:
return lines 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 assert inflections is not None
r = Resources() r = Resources()
frags: list[Fragment] = []
sep = "" sep = ""
for inflection in inflections: for inflection in inflections:
if sep == "; ": if sep == "; ":
frag = Fragment("; ", font=r.boldFont, color=r.baseColor) frag.addText(sep, r.boldFormat)
frags.append(frag)
elif sep != "": elif sep != "":
frag = Fragment(sep, font=r.italicFont, color=r.baseColor) frag.addText(sep, r.italicFormat)
frags.append(frag)
if "ifc" in inflection: if "ifc" in inflection:
text = inflection["ifc"] text = inflection["ifc"]
@@ -577,19 +592,18 @@ def do_ins(inflections: list[Inflection] | None) -> list[Fragment]:
else: else:
raise ValueError(f"Missing 'if' or 'ifc' in {inflection}") raise ValueError(f"Missing 'if' or 'ifc' in {inflection}")
frag = Fragment(text, r.boldFont, color=r.baseColor) text = re.sub(r'\*', '\u00b7', text)
frags.append(frag) frag.addText(text, r.boldFormat)
sep = "; " sep = "; "
if "il" in inflection: if "il" in inflection:
sep = " " + inflection["il"] + " " sep = " " + inflection["il"] + " "
if "prs" in inflection: if "prs" in inflection:
newFrags = do_prs(trycast(list[Pronunciation], inflection["prs"])) do_prs(frag, trycast(list[Pronunciation], inflection["prs"]))
frags += newFrags
if "spl" in inflection: if "spl" in inflection:
raise NotImplementedError( raise NotImplementedError(
f"We haven't implimented 'spl' for inflection: {inflection}" f"We haven't implimented 'spl' for inflection: {inflection}"
) )
return frags return
def do_ets(ets: list[list[Pair]] | None) -> list[Line]: 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: for pair in et:
if pair["objType"] == "text": if pair["objType"] == "text":
line = Line() line = Line()
line.addFragment( frag = Fragment('', r.textFont)
Fragment(pair["obj"], r.textFont, color=r.baseColor) frag.addText(pair['obj'], r.textFormat)
) line.addFragment(frag)
lines.append(line) lines.append(line)
elif pair["objType"] == "et_snote": elif pair["objType"] == "et_snote":
line = Line() line = Line()
line.addFragment( frag = Fragment('', r.textFont)
Fragment( frag.addText(f"Note: {pair['obj']}",r.textFormat)
"Note: " + pair["obj"], r.textFont, color=r.baseColor line.addFragment(frag)
)
)
lines.append(line) lines.append(line)
else: else:
raise NotImplementedError( raise NotImplementedError(
@@ -625,7 +637,9 @@ def do_def(entry: DefinitionSection) -> list[Line]:
lines: list[Line] = [] lines: list[Line] = []
if "vd" in entry: if "vd" in entry:
line = Line() 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) lines.append(line)
# #
# sseg is required # sseg is required
@@ -634,6 +648,46 @@ def do_def(entry: DefinitionSection) -> list[Line]:
lines += do_sseq(sseq) lines += do_sseq(sseq)
return lines 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]: def getDef(defines: Any) -> list[Line]:
Line.setParseText(parseText) Line.setParseText(parseText)
@@ -667,7 +721,7 @@ def getDef(defines: Any) -> list[Line]:
used[k] = 0 used[k] = 0
ets: list[Line] = [] ets: list[Line] = []
phrases: list[Line] = []
for count, work in enumerate(workList): for count, work in enumerate(workList):
testId = work["meta"]["id"].lower().split(":")[0] 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 # Create the First line from the hwi, [ahws] and fl
# #
line = Line() line = Line()
frag = Fragment()
hwi = trycast(HeadWordInformation, work["hwi"]) hwi = trycast(HeadWordInformation, work["hwi"])
assert hwi is not None assert hwi is not None
hw = re.sub(r"\*", "", hwi["hw"]) hw = re.sub(r"\*", "", hwi["hw"])
line.addFragment(Fragment(hw, r.headerFont, color=r.baseColor)) frag.addText(hw,r.headerFormat)
if "ahws" in work: if "ahws" in work:
ahws = trycast(list[AlternanteHeadword], work["ahws"]) ahws = trycast(list[AlternanteHeadword], work["ahws"])
assert ahws is not None assert ahws is not None
for ahw in ahws: for ahw in ahws:
hw = re.sub(r"\*", "", ahw["hw"]) hw = re.sub(r"\*", "", ahw["hw"])
line.addFragment( frag.addText(", " + hw)
Fragment(", " + hw, r.headerFont, color=r.baseColor)
)
if entries > 1: if entries > 1:
frag = Fragment( frag.addText(f" {count + 1} of {entries} ", r.sOnSFormat)
f" {count + 1} of {entries} ", r.textFont, color=r.subduedColor
)
frag.setBackground(r.subduedBackground)
line.addFragment(frag)
if "fl" in work: if "fl" in work:
text = work["fl"] text = work["fl"]
used[text] += 1 used[text] += 1
if uses[text] > 1: if uses[text] > 1:
text += f" ({used[text]})" text += f" ({used[text]})"
line.addFragment(Fragment(text, r.labelFont, color=r.baseColor)) frag.addText(text, r.labelFormat)
line.addFragment(frag)
lines.append(line) 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. # While 'prs' is optional, the headword is not. This gets us what we want.
# #
line = Line() line = Line()
frag = Fragment()
if hwi["hw"].find("*") >= 0: if hwi["hw"].find("*") >= 0:
hw = re.sub(r"\*", "\u00b7", hwi["hw"]) hw = re.sub(r"\*", "\u00b7", hwi["hw"])
line.addFragment( frag.addText(hw + " ", r.subduedFormat)
Fragment(hw + " ", r.textFont, color=r.subduedColor)
)
if "prs" in hwi: if "prs" in hwi:
newFrags = do_prs(trycast(list[Pronunciation], hwi["prs"])) do_prs(frag, trycast(list[Pronunciation], hwi["prs"]))
line.addFragment(newFrags) line.addFragment(frag)
lines.append(line) lines.append(line)
line = Line() line = Line()
frag = Fragment()
if 'vrs' in work:
lines.append(do_vrs(trycast(list[Variant], work['vrs'])))
if "ins" in work: if "ins" in work:
inflections = trycast(list[Inflection], work["ins"]) inflections = trycast(list[Inflection], work["ins"])
newFrags = do_ins(inflections) do_ins(frag,inflections)
line = Line() line.addFragment(frag)
line.addFragment(newFrags)
lines.append(line) lines.append(line)
line = Line()
frag = Fragment()
defines = trycast(list[DefinitionSection], work["def"]) defines = trycast(list[DefinitionSection], work["def"])
assert defines is not None assert defines is not None
for define in defines: for define in defines:
try: try:
lines += do_def(define) lines += do_def(define)
except NotImplementedError as e: except NotImplementedError:
print(e) 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: if "et" in work:
line = Line() line = Line()
line.addFragment( frag = Fragment('', r.textFont)
Fragment( frag.addText(f"{work['fl']} ({used[work['fl']]})",r.labelFormat)
f"{work['fl']} ({used[work['fl']]})", line.addFragment(frag)
r.labelFont,
color=r.baseColor,
)
)
ets.append(line)
ets += do_ets(trycast(list[list[Pair]], work["et"])) ets += do_ets(trycast(list[list[Pair]], work["et"]))
for k in work.keys(): for k in work.keys():
if k not in [ if k not in [
@@ -756,9 +813,12 @@ def getDef(defines: Any) -> list[Line]:
"et", "et",
"date", "date",
"shortdef", "shortdef",
"vrs",
"dros",
]: ]:
# raise NotImplementedError(f"Unknown key {k} in work") raise NotImplementedError(f"Unknown key {k} in work")
print(f"Unknown key {k} in work") if len(phrases) > 0:
lines += phrases
if len(ets) > 0: if len(ets) > 0:
line = Line() line = Line()
line.addFragment(Fragment("Etymology", r.labelFont, color=r.baseColor)) line.addFragment(Fragment("Etymology", r.labelFont, color=r.baseColor))
@@ -766,185 +826,116 @@ def getDef(defines: Any) -> list[Line]:
lines += ets lines += ets
return lines return lines
def replaceCode(code:str) -> tuple[str, QTextCharFormat]:
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()
r = Resources() r = Resources()
fmt = QTextCharFormat()
results: list[Fragment] = [] if code == 'bc':
while True: fmt.setFontWeight(QFont.Weight.Bold)
text = frag.text() return (': ', fmt)
start = text.find("{") elif code == 'ldquo':
if start < 0: return ('\u201c', fmt)
results.append(frag) elif code == 'rdquo':
return results return ('\u201d', fmt)
if start > 0: fmt.setAnchor(True)
newFrag = Fragment(frag) fmt.setForeground(r.linkColor)
newFrag.setText(text[:start]) fmt.setFontUnderline(True)
results.append(newFrag) fmt.setUnderlineColor(r.linkColor)
frag.setText(text[start:]) fmt.setFontUnderline(True)
continue fields = code.split('|')
#
# 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("|")
token = fields[0] token = fields[0]
if token in [ if token == 'a_link':
"a_link", text = fields[1]
"d_link", fmt.setAnchorHref(fields[1])
"dxt", elif token in ['d_link', 'et_link', 'mat', 'sx', 'i_link']:
"et_link", text = fields[1]
"i_link", pre = 'word://'
"mat", if fields[2] == '':
"sx", fmt.setAnchorHref(pre+fields[1])
]:
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]
else: else:
wref = fields[2] fmt.setAnchorHref(pre+fields[2])
if token == "i_link": if token == 'i_link':
frag.setFont(italicFont) fmt.setFontItalic(True)
elif token == "sx": elif token == 'sx':
frag.setFont(capsFont) fmt.setFontCapitalization(QFont.Capitalization.SmallCaps)
elif token == "dxt": elif token == 'dxt':
if fields[3] == "illustration": if fields[3] == 'illustration':
wref = fields[2] fmt.setAnchorHref('article://'+fields[2])
target = "article" elif fields[3] == 'table':
elif fields[3] == "table": fmt.setAnchorHref('table://'+fields[2])
wref = fields[2]
target = "table"
elif fields[3] != "": elif fields[3] != "":
wref = fields[3] fmt.setAnchorHref('sense://'+fields[3])
target = "sense"
else: else:
wref = fields[1] fmt.setAnchorHref('word://'+fields[1])
target = "word" elif token == 'et_link':
elif token == "a_link": if fields[2] != '':
target = "word" fmt.setAnchorHref('etymology://'+fields[2])
wref = fields[1]
else: else:
raise NotImplementedError(f"Unknown code: {token} in {org}") fmt.setAnchorHref('etymology://' + fields[1])
newFrag = Fragment(frag) else:
newFrag.setText(htext) raise NotImplementedError(f"Token {code} not implimented")
newFrag.setWRef(wref) fmt.setForeground(r.linkColor)
newFrag.setTarget(target) print(f"Format.capitalization(): {fmt.fontCapitalization()}")
newFrag.setColor(r.linkColor) return (text,fmt)
results.append(newFrag)
frag.setFont(oldFont) def markup(offset: int, text:str) -> tuple[str, list[QTextLayout.FormatRange]]:
text = frag.text() close = text.find('}')
continue code = text[1:close]
raise NotImplementedError( text = text[close+1:-(close+2)]
f"Unable to locate a known token {token} in {org}" 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