Removed more class variables to turn them into instance variables Removed type from fragment Removed container from fragment Moved to a more object oriented draw methodology. The widget calls on the Line to paint itself. The Line calls on the Fragment to paint itself.
903 lines
32 KiB
Python
903 lines
32 KiB
Python
import copy
|
|
import json
|
|
import re
|
|
from typing import Any, Optional, cast
|
|
|
|
import requests
|
|
from PyQt6.QtCore import QPoint, QRect, QUrl, Qt, pyqtSignal
|
|
from PyQt6.QtGui import (
|
|
QBrush,
|
|
QColor,
|
|
QFont,
|
|
QFontDatabase,
|
|
QFontMetrics,
|
|
QMouseEvent,
|
|
QPainter,
|
|
QPaintEvent,
|
|
QResizeEvent,
|
|
QTextOption,
|
|
QTransform,
|
|
)
|
|
from PyQt6.QtSql import QSqlQuery
|
|
from PyQt6.QtWidgets import QScrollArea, QWidget
|
|
|
|
from lib import query_error
|
|
|
|
class Fragment:
|
|
"""A fragment of text to be displayed"""
|
|
|
|
def __init__(self,
|
|
text:str,
|
|
font:QFont,
|
|
audio:str = '',
|
|
color: Optional[QColor] = None,
|
|
asis: bool = False,
|
|
) -> None:
|
|
self._text = text
|
|
self._font = font
|
|
self._audio:QUrl = QUrl(audio)
|
|
self._align = QTextOption(
|
|
Qt.AlignmentFlag.AlignLeft
|
|
| Qt.AlignmentFlag.AlignBaseline
|
|
)
|
|
self._padding = [0, 0, 0, 0]
|
|
self._border = [0, 0, 0, 0]
|
|
self._margin = [0, 0, 0, 0]
|
|
self._wref = ''
|
|
self._position = QPoint()
|
|
self._rect = QRect()
|
|
self._borderRect = QRect()
|
|
if color:
|
|
self._color = color
|
|
else:
|
|
self._color = QColor()
|
|
self._asis = asis
|
|
self._left = 0
|
|
return
|
|
|
|
def __str__(self) -> str:
|
|
return self._text
|
|
|
|
def repaintEvent(self, painter:QPainter) -> int:
|
|
painter.save()
|
|
painter.setFont(self._font)
|
|
painter.setPen(self._color)
|
|
rect = QRect()
|
|
rect.setLeft(self._position.x())
|
|
rect.setTop(self._position.y() - painter.fontMetrics().ascent())
|
|
rect.setWidth(painter.viewport().width() - self._position.x())
|
|
rect.setHeight(2000)
|
|
flags = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignBaseline | Qt.TextFlag.TextWordWrap
|
|
bounding = painter.boundingRect(rect,flags, self._text)
|
|
height = bounding.height()+self._padding[2]+self._border[2]+self._margin[2]
|
|
|
|
painter.setPen(QColor("#f00"))
|
|
if self._audio.isValid():
|
|
radius = self._borderRect.height() /2
|
|
painter.drawRoundedRect(self._borderRect, radius, radius)
|
|
if self._wref:
|
|
start = bounding.bottomLeft()
|
|
end = bounding.bottomRight()
|
|
painter.drawLine(start,end)
|
|
painter.setPen(self._color)
|
|
painter.drawText(
|
|
rect,
|
|
flags,
|
|
self._text
|
|
)
|
|
painter.restore()
|
|
return height
|
|
#
|
|
# Setters
|
|
#
|
|
def setText(self, text:str) -> None:
|
|
self._text = text
|
|
return
|
|
def setFont(self, font:QFont) -> None:
|
|
self._font = font
|
|
return
|
|
def setAudio(self, audio:str|QUrl) -> None:
|
|
if type(audio) is str:
|
|
self._audio = QUrl(audio)
|
|
else:
|
|
self._audio = cast(QUrl,audio)
|
|
return
|
|
def setAlign(self, align:QTextOption) -> None:
|
|
self._align = align
|
|
return
|
|
def setRect(self,rect:QRect) -> None:
|
|
self._rect = rect
|
|
return
|
|
def setPadding(self, top:int = -1, right:int = -1, bottom:int = -1, left:int = -1, *args:int) -> None:
|
|
if top > -1 or right > -1 or bottom > -1 or left > -1:
|
|
if top >= 0:
|
|
self._padding[0] = top
|
|
if right >= 0:
|
|
self._padding[1] = right
|
|
if bottom >= 0:
|
|
self._padding[2] = bottom
|
|
if left >= 0:
|
|
self._padding[3] = left
|
|
return
|
|
if len(args) == 4:
|
|
self._padding = [args[0], args[1], args[2], args[3]]
|
|
elif len(args) == 3:
|
|
self._padding = [args[0], args[1], args[2], args[1]]
|
|
elif len(args) == 2:
|
|
self._padding = [args[0], args[1], args[0], args[1]]
|
|
elif len(args) == 1:
|
|
self._padding = [args[0], args[0], args[0], args[0]]
|
|
else:
|
|
raise Exception("argument error")
|
|
return
|
|
def setBorder(self, top:int = -1, right:int = -1, bottom:int = -1, left:int = -1, *args:int) -> None:
|
|
if top > -1 or right > -1 or bottom > -1 or left > -1:
|
|
if top >= 0:
|
|
self._border[0] = top
|
|
if right >= 0:
|
|
self._border[1] = right
|
|
if bottom >= 0:
|
|
self._border[2] = bottom
|
|
if left >= 0:
|
|
self._border[3] = left
|
|
return
|
|
if len(args) == 4:
|
|
self._border = [args[0], args[1], args[2], args[3]]
|
|
elif len(args) == 3:
|
|
self._border = [args[0], args[1], args[2], args[1]]
|
|
elif len(args) == 2:
|
|
self._border = [args[0], args[1], args[0], args[1]]
|
|
elif len(args) == 1:
|
|
self._border = [args[0], args[0], args[0], args[0]]
|
|
else:
|
|
raise Exception("argument error")
|
|
return
|
|
def setMargin(self, top:int = -1, right:int = -1, bottom:int = -1, left:int = -1, *args:int) -> None:
|
|
if top > -1 or right > -1 or bottom > -1 or left > -1:
|
|
if top >= 0:
|
|
self._margin[0] = top
|
|
if right >= 0:
|
|
self._margin[1] = right
|
|
if bottom >= 0:
|
|
self._margin[2] = bottom
|
|
if left >= 0:
|
|
self._margin[3] = left
|
|
return
|
|
if len(args) == 4:
|
|
self._margin = [args[0], args[1], args[2], args[3]]
|
|
elif len(args) == 3:
|
|
self._margin = [args[0], args[1], args[2], args[1]]
|
|
elif len(args) == 2:
|
|
self._margin = [args[0], args[1], args[0], args[1]]
|
|
elif len(args) == 1:
|
|
self._margin = [args[0], args[0], args[0], args[0]]
|
|
else:
|
|
raise Exception("argument error")
|
|
return
|
|
def setWRef(self, ref:str) -> None:
|
|
self._wref = ref
|
|
return
|
|
def setPosition(self, pnt:QPoint) -> None:
|
|
self._position = pnt
|
|
return
|
|
def setBorderRect(self, rect:QRect) -> None:
|
|
self._borderRect = rect
|
|
return
|
|
def setColor(self,color:QColor) -> None:
|
|
self._color = color
|
|
return
|
|
def setLeft(self, left:int) -> None:
|
|
self._left = left
|
|
return
|
|
#
|
|
# Getters
|
|
#
|
|
def wRef(self) -> str:
|
|
return self._wref
|
|
def text(self) -> str:
|
|
return self._text
|
|
def font(self) -> QFont:
|
|
return self._font
|
|
def audio(self) -> QUrl:
|
|
return self._audio
|
|
def align(self) -> QTextOption:
|
|
return self._align
|
|
def rect(self) -> QRect:
|
|
return self._rect
|
|
def padding(self) -> list[int]:
|
|
return self._padding
|
|
def border(self) -> list[int]:
|
|
return self._border
|
|
def margin(self) -> list[int]:
|
|
return self._margin
|
|
def position(self) -> QPoint:
|
|
return self._position
|
|
def borderRect(self) -> QRect:
|
|
return self._borderRect
|
|
def color(self) -> QColor:
|
|
return self._color
|
|
def asis(self) -> bool:
|
|
return self._asis
|
|
def left(self) -> int:
|
|
return self._left
|
|
|
|
|
|
API = "https://api.dictionaryapi.dev/api/v2/entries/en/{word}"
|
|
MWAPI = "https://www.dictionaryapi.com/api/v3/references/collegiate/json/{word}?key=51d9df34-ee13-489e-8656-478c215e846c"
|
|
|
|
|
|
class Word:
|
|
"""All processing of a dictionary word."""
|
|
|
|
|
|
_words: dict[str, Any] = {}
|
|
|
|
class Line:
|
|
|
|
def __init__(self) -> None:
|
|
self._maxHeight = -1
|
|
self._baseLine = -1
|
|
self._leading = -1
|
|
self._fragments:list[Fragment] = []
|
|
return
|
|
|
|
def __repr__(self) -> str:
|
|
return '|'.join([x.text() for x in self._fragments])+f'|{self._maxHeight}'
|
|
|
|
def repaintEvent(self, painter: QPainter) -> int:
|
|
#
|
|
# we do not have an event field because we are not a true widget
|
|
#
|
|
lineSpacing = 0
|
|
for frag in self._fragments:
|
|
ls = frag.repaintEvent(painter)
|
|
if ls > lineSpacing:
|
|
lineSpacing = ls
|
|
return lineSpacing
|
|
|
|
def parseText(self, frag: Fragment) -> list[Fragment]:
|
|
org = frag.text()
|
|
if frag.asis():
|
|
return [frag]
|
|
#
|
|
# Needed Fonts
|
|
#
|
|
bold = QFont(frag.font())
|
|
bold.setWeight(QFont.Weight.Bold)
|
|
italic = QFont(frag.font())
|
|
italic.setItalic(True)
|
|
smallCaps = QFont(frag.font())
|
|
smallCaps.setCapitalization(QFont.Capitalization.SmallCaps)
|
|
script = QFont(frag.font())
|
|
script.setPixelSize(int(script.pixelSize()/4))
|
|
|
|
results: list[Fragment] = []
|
|
while True:
|
|
text = frag.text()
|
|
start = text.find('{')
|
|
if start < 0:
|
|
results.append(frag)
|
|
return results
|
|
if start > 0:
|
|
newFrag = copy.copy(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:])
|
|
newFrag = copy.copy(frag)
|
|
oldFont = QFont(frag.font())
|
|
if token == 'bc':
|
|
results.append(Fragment(': ', bold, color=QColor('#fff')))
|
|
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(bold)
|
|
elif token in ['it', 'qword', 'wi']:
|
|
frag.setFont(italic)
|
|
elif token == 'sc':
|
|
frag.setFont(smallCaps)
|
|
elif token in ['inf', 'sup']:
|
|
frag.setFont(script)
|
|
elif token == 'phrase':
|
|
font = QFont(bold)
|
|
font.setItalic(True)
|
|
frag.setFont(font)
|
|
elif token == 'parahw':
|
|
font = QFont(smallCaps)
|
|
font.setWeight(QFont.Weight.Bold)
|
|
frag.setFont(font)
|
|
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 Exception(f"Unknown block marker: {token}")
|
|
results += self.parseText(frag)
|
|
frag = results.pop()
|
|
frag.setFont(oldFont)
|
|
text = frag.text()
|
|
if not text.startswith('{/'+token+'}'):
|
|
raise Exception(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]
|
|
if token in [ 'a_line', 'd_link', 'dxt', 'et_link', 'i_link',
|
|
'mat', 'sx']:
|
|
wref = ''
|
|
htext = fields[1]
|
|
oldFont = QFont(frag.font())
|
|
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:
|
|
wref = fields[2]
|
|
if token == 'i_link':
|
|
frag.setFont(italic)
|
|
elif token == 'i_link':
|
|
if fields[2] == '':
|
|
wref = fields[1]
|
|
else:
|
|
wref = fields[2]
|
|
else:
|
|
raise Exception(f"Unknown code: {token} in {org}")
|
|
newFrag = copy.copy(frag)
|
|
newFrag.setText(htext)
|
|
newFrag.setWRef(wref)
|
|
results.append(newFrag)
|
|
frag.setFont(oldFont)
|
|
text = frag.text()
|
|
continue
|
|
raise Exception(f"Unable to locate a known token {token} in {org}")
|
|
|
|
|
|
def addFragment(self, frag: Fragment,) -> None:
|
|
SPEAKER = "\U0001F508"
|
|
|
|
if frag.audio():
|
|
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)
|
|
frag.setBorder(1)
|
|
frag.setMargin(2)
|
|
items = self.parseText(frag)
|
|
self._fragments += items
|
|
return
|
|
|
|
def finalizeLine(self) -> None:
|
|
"""Create all of the positions for all the fragments."""
|
|
#
|
|
# Find the maximum hight and max baseline
|
|
#
|
|
maxHeight = -1
|
|
baseLine = -1
|
|
leading = -1
|
|
for frag in self._fragments:
|
|
fm = QFontMetrics(frag.font())
|
|
rect = fm.boundingRect(frag.text(), frag.align())
|
|
height = rect.height()
|
|
bl = height - fm.descent()
|
|
if fm.leading() > leading:
|
|
leading = fm.leading()
|
|
#
|
|
# Add the padding, border and margin to adjust the baseline and height
|
|
#
|
|
b = frag.padding()
|
|
height += b[0] + b[2]
|
|
bl += b[2]
|
|
b = frag.border()
|
|
height += b[0] + b[2]
|
|
bl += b[2]
|
|
b = frag.margin()
|
|
height += b[0] + b[2]
|
|
bl += b[2]
|
|
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:
|
|
if x < frag.left():
|
|
x = frag.left()
|
|
fm = QFontMetrics(frag.font())
|
|
width = fm.horizontalAdvance(frag.text())
|
|
padding = frag.padding()
|
|
offset = padding[3] # Left margin
|
|
width += padding[1] + padding[3]
|
|
border = frag.border()
|
|
offset += border[3]
|
|
width += border[1] + border[3]
|
|
margin = frag.margin()
|
|
offset += margin[3]
|
|
width += margin[1] + margin[3]
|
|
frag.setPosition(QPoint(x+offset, self._baseLine))
|
|
if frag.border()[0] != 0:
|
|
#
|
|
# 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() - rect.height() -1
|
|
y = top - padding[0] - border[0]
|
|
frag.setBorderRect(QRect(x+margin[3], y, width, height))
|
|
x += width
|
|
return
|
|
|
|
def getLine(self) -> list[Fragment]:
|
|
return self._fragments
|
|
|
|
def getLeading(self) -> int:
|
|
return self._leading + self._maxHeight
|
|
|
|
_lines: list[Line] = []
|
|
|
|
def __init__(self, word: str) -> None:
|
|
self.resources = {}
|
|
#
|
|
# Have we already retrieved this word?
|
|
#
|
|
try:
|
|
self.current = json.loads(Word._words[word])
|
|
return
|
|
except KeyError:
|
|
pass
|
|
query = QSqlQuery()
|
|
query.prepare("SELECT * FROM words " "WHERE word = :word")
|
|
query.bindValue(":word", word)
|
|
if not query.exec():
|
|
query_error(query)
|
|
if query.next():
|
|
Word._words[word] = {
|
|
'word': word,
|
|
'source': query.value('source'),
|
|
'definition': json.loads(query.value("definition")),
|
|
}
|
|
self.current = Word._words[word]
|
|
return
|
|
source = 'mw'
|
|
response = requests.get(MWAPI.format(word=word))
|
|
if response.status_code != 200:
|
|
self.current = {}
|
|
return
|
|
data = json.loads(response.content.decode("utf-8"))
|
|
print(data)
|
|
self._words[word] = {
|
|
'word': word,
|
|
'source': source,
|
|
'definition': data,
|
|
}
|
|
self.current = Word._words[word]
|
|
query.prepare(
|
|
"INSERT INTO words "
|
|
"(word, source, definition) "
|
|
"VALUES (:word, :source, :definition)"
|
|
)
|
|
query.bindValue(":word", self.current['word'])
|
|
query.bindValue(":source", self.current['source'])
|
|
query.bindValue(":definition", json.dumps(self.current['definition']))
|
|
if not query.exec():
|
|
query_error(query)
|
|
return
|
|
|
|
def getWord(self) -> str:
|
|
return self.current['word']
|
|
|
|
def get_html(self) -> str | None:
|
|
if self.current['source'] == 'mw':
|
|
return self.mw_html()
|
|
elif self.current['source'] == 'apidictionary':
|
|
return self.apidictionary_html()
|
|
else:
|
|
raise Exception(f"Unknown source: {self.current['source']}")
|
|
|
|
def get_def(self) -> list[Line] | None:
|
|
if len(self._lines) > 0:
|
|
return self._lines
|
|
if len(self.resources.keys()) < 1:
|
|
#
|
|
# Colors we used
|
|
#
|
|
headerFont = QFontDatabase.font("OpenDyslexic", None, 10)
|
|
headerFont.setPixelSize(48)
|
|
labelFont = QFont(headerFont)
|
|
labelFont.setPixelSize(30)
|
|
boldFont = QFont(headerFont)
|
|
boldFont.setPixelSize(20)
|
|
textFont = QFont(boldFont)
|
|
italicFont = QFont(boldFont)
|
|
|
|
headerFont.setWeight(QFont.Weight.Bold)
|
|
boldFont.setBold(True)
|
|
italicFont.setItalic(True)
|
|
|
|
phonicFont = QFontDatabase.font("Gentium", None, 10)
|
|
phonicFont.setPixelSize(20)
|
|
|
|
self.resources = {
|
|
'colors': {
|
|
'base':QColor(Qt.GlobalColor.white),
|
|
'blue': QColor("#4a7d95"),
|
|
},
|
|
'fonts': {
|
|
'header': headerFont,
|
|
'label': labelFont,
|
|
'phonic': phonicFont,
|
|
'bold': boldFont,
|
|
'italic': italicFont,
|
|
'text': textFont,
|
|
}
|
|
}
|
|
if self.current['source'] == 'mw':
|
|
return self.mw_def()
|
|
elif self.current['source'] == 'apidictionary':
|
|
return None
|
|
else:
|
|
raise Exception(f"Unknown source: {self.current['source']}")
|
|
|
|
def mw_def(self) -> list[Line]:
|
|
lines: list[Word.Line] = []
|
|
for entry in self.current['definition']:
|
|
line = Word.Line()
|
|
meta = json.dumps(entry['meta'])
|
|
line.addFragment(
|
|
Fragment(
|
|
meta,
|
|
self.resources['fonts']['text'],asis=True
|
|
)
|
|
)
|
|
lines.append(line)
|
|
lines += self.mw_def_entry(entry)
|
|
self._lines = lines
|
|
return lines
|
|
|
|
def mw_seq(self, seq: list[Any]) -> list[Line]:
|
|
lines: list[Word.Line] = []
|
|
outer = ' '
|
|
inner = ' '
|
|
for value in seq:
|
|
sense = value[1]
|
|
#
|
|
# The optional 'sn' field tells us what sort of labeling to do
|
|
#
|
|
sn = sense.get('sn', '')
|
|
sns = sn.split(' ')
|
|
if len(sns) == 2:
|
|
outer = sns[0]
|
|
inner = sns[1]
|
|
elif len(sns) == 1:
|
|
if inner == ' ':
|
|
outer = sns[0]
|
|
else:
|
|
inner = sns[0]
|
|
|
|
for dt in sense['dt']:
|
|
if dt[0] == 'text':
|
|
line = Word.Line()
|
|
frag = Fragment(
|
|
f"{outer} {inner} ",
|
|
self.resources['fonts']['bold'],
|
|
color = self.resources['colors']['base']
|
|
)
|
|
outer = ' '
|
|
frag.setLeft(10)
|
|
line.addFragment(frag)
|
|
frag = Fragment(
|
|
dt[1],
|
|
self.resources['fonts']['text'],
|
|
color = self.resources['colors']['base']
|
|
)
|
|
frag.setLeft(30)
|
|
line.addFragment(frag)
|
|
lines.append(line)
|
|
elif dt[0] == 'vis':
|
|
for vis in dt[1]:
|
|
line = Word.Line()
|
|
frag =Fragment(f" ",
|
|
self.resources['fonts']['bold'],
|
|
)
|
|
frag.setLeft(45)
|
|
line.addFragment(frag)
|
|
line.addFragment(
|
|
Fragment(vis['t'],
|
|
self.resources['fonts']['text'],
|
|
color = QColor('#aaa')
|
|
)
|
|
)
|
|
lines.append(line)
|
|
return lines
|
|
def mw_def_entry(self, entry) -> list[Line]:
|
|
#
|
|
# Easy reference to colors
|
|
#
|
|
base = self.resources['colors']['base']
|
|
blue = self.resources['colors']['blue']
|
|
|
|
lines: list[Word.Line] = []
|
|
line = Word.Line()
|
|
hw = re.sub(r'\*', '', entry['hwi']['hw'])
|
|
frag = Fragment(hw, self.resources['fonts']['header'], color=base)
|
|
line.addFragment(frag)
|
|
frag = Fragment(' '+entry["fl"], self.resources['fonts']['label'], color=blue)
|
|
line.addFragment(frag)
|
|
lines.append(line)
|
|
|
|
if "vrs" in entry.keys():
|
|
line = self.Line()
|
|
space = ''
|
|
for vrs in entry["vrs"]:
|
|
frag = Fragment(space + vrs["va"], self.resources['fonts']['label'], color=base)
|
|
space = ' '
|
|
line.addFragment(frag)
|
|
lines.append(line)
|
|
if "prs" in entry["hwi"].keys():
|
|
line = self.Line()
|
|
frag = Fragment(entry['hwi']['hw'] + ' ', self.resources['fonts']['phonic'], color=base)
|
|
line.addFragment(frag)
|
|
for prs in entry["hwi"]["prs"]:
|
|
audio = self.sound_url(prs)
|
|
if audio is None:
|
|
audio = ""
|
|
frag = Fragment(prs['mw'], self.resources['fonts']['phonic'], color=blue)
|
|
frag.setAudio(audio)
|
|
frag.setPadding(0,10,3,12)
|
|
frag.setBorder(1)
|
|
frag.setMargin(0,3,0,3)
|
|
line.addFragment(frag)
|
|
lines.append(line)
|
|
if "ins" in entry.keys():
|
|
line = self.Line()
|
|
space = ''
|
|
for ins in entry['ins']:
|
|
try:
|
|
frag = Fragment(ins['il'], self.resources['fonts']['text'], color=base)
|
|
line.addFragment(frag)
|
|
space = ' '
|
|
except KeyError:
|
|
pass
|
|
frag = Fragment(space + ins['if'], self.resources['fonts']['bold'], color=base)
|
|
line.addFragment(frag)
|
|
space = '; '
|
|
lines.append(line)
|
|
if 'lbs' in entry.keys():
|
|
line = self.Line()
|
|
frag = Fragment('; '.join(entry['lbs']), self.resources['fonts']['bold'], color=base)
|
|
line.addFragment(frag)
|
|
lines.append(line)
|
|
for value in entry['def']: # has multiple 'sseg' or 'vd' init
|
|
for k,v in value.items():
|
|
if k == 'sseq': # has multiple 'senses'
|
|
for seq in v:
|
|
r = self.mw_seq(seq)
|
|
lines += r
|
|
elif k == 'vd':
|
|
line = self.Line()
|
|
line.addFragment(Fragment(
|
|
v,
|
|
self.resources['fonts']['italic'],
|
|
color=blue
|
|
))
|
|
lines.append(line)
|
|
return lines
|
|
|
|
def sound_url(self, prs: dict[str, Any], fmt: str = "ogg") -> str | None:
|
|
"""Create a URL from a PRS structure."""
|
|
base = f"https://media.merriam-webster.com/audio/prons/en/us/{fmt}"
|
|
if "sound" not in prs.keys():
|
|
return None
|
|
audio = prs["sound"]["audio"]
|
|
m = re.match(r"(bix|gg|[a-zA-Z])", audio)
|
|
if m:
|
|
url = base + f"/{m.group(1)}/"
|
|
else:
|
|
url = base + "/number/"
|
|
url += audio + f".fmt"
|
|
return url
|
|
|
|
def mw_html(self) -> str:
|
|
#
|
|
# Create the header, base word and its label
|
|
#
|
|
word = self.current["hwi"]["hw"]
|
|
label = self.current["fl"]
|
|
html = f'<h1 class="def-word">{word} <span class="def-label">{label}</span></h1>\n'
|
|
|
|
#
|
|
# If there are variants, then add them in an unordered list.
|
|
# CSS will make it pretty
|
|
#
|
|
if "vrs" in self.current.keys():
|
|
html += "<ul class=\"def-vrs'>\n"
|
|
html += "<li>"
|
|
html += "</li>\n<li>".join(
|
|
[vrs["va"] for vrs in self.current["vrs"]]
|
|
)
|
|
html += "</li>\n</ul>\n"
|
|
|
|
#
|
|
# If there is a pronunciation section, create it
|
|
#
|
|
if "prs" in self.current["hwi"].keys():
|
|
tmp = []
|
|
for prs in self.current["hwi"]["prs"]:
|
|
url = self.sound_url(prs)
|
|
how = prs["mw"]
|
|
if url:
|
|
tmp.append(f'<a href="{url}">\\{how}\\</a>')
|
|
else:
|
|
tmp.append(f"\\{how}\\")
|
|
html += '<span class="def-phonetic">'
|
|
html += '</span><span="def-phonetic">'.join(tmp)
|
|
html += "</span>\n"
|
|
|
|
#
|
|
# If there are inflections, create a header for that.
|
|
#
|
|
if "ins" in self.current.keys():
|
|
html += '<h2 class="def-word">'
|
|
html += ", ".join([ins["if"] for ins in self.current["ins"]])
|
|
html += "</h2>\n"
|
|
|
|
#
|
|
# Start creating the definition section
|
|
#
|
|
html += "<ul class='def-outer'>\n"
|
|
for meaning in self.current["def"]:
|
|
html += f"<li>{meaning['vd']}\n"
|
|
html += '<ul class="def-inner">\n'
|
|
label = ""
|
|
for sseq in meaning["sseq"]:
|
|
for sense in sseq:
|
|
label = sense[1]["sn"]
|
|
sls = ""
|
|
if "sls" in sense[1].keys():
|
|
sls = ", ".join(sense[1]["sls"])
|
|
sls = f'<span class="def-sls">{sls}</span> '
|
|
for dt in sense[1]["dt"]:
|
|
if dt[0] == "text":
|
|
html += f'<li class="def-text"><span class="def-sn">{label}</span>{sls}{dt[1]}</li>\n'
|
|
elif dt[0] == "vis":
|
|
for vis in dt[1]:
|
|
html += (
|
|
f"<li class=\"def-vis\">{vis['t']}</li>\n"
|
|
)
|
|
else:
|
|
print(f"Do something with {dt[0]}")
|
|
html += "</ul>\n"
|
|
html += "</ul>\n"
|
|
return html
|
|
|
|
def apidictionary_html(self) -> str:
|
|
html = ""
|
|
return html
|
|
|
|
|
|
class Definition(QWidget):
|
|
pronounce = pyqtSignal(str)
|
|
|
|
def __init__(self, w: Word, *args: Any, **kwargs: Any) -> None:
|
|
super(Definition, self).__init__(*args, **kwargs)
|
|
self._word:str = w.getWord()
|
|
lines = w.get_def()
|
|
assert lines is not None
|
|
self._lines = lines
|
|
self._buttons:list[Fragment] = []
|
|
base = 0
|
|
for line in self._lines:
|
|
line.finalizeLine()
|
|
base += line.getLeading()
|
|
self.setFixedHeight(base)
|
|
return
|
|
|
|
def resizeEvent(self, event: QResizeEvent) -> None:
|
|
base = 0
|
|
for line in self._lines:
|
|
line.finalizeLine()
|
|
base += line.getLeading()
|
|
self.setFixedHeight(base)
|
|
super(Definition,self).resizeEvent(event)
|
|
return
|
|
|
|
_downRect: QRect | None = None
|
|
|
|
def mousePressEvent(self, event: Optional[QMouseEvent]) -> None:
|
|
if not event:
|
|
return super().mousePressEvent(event)
|
|
for frag in self._buttons:
|
|
rect = frag.borderRect()
|
|
if rect.contains(event.pos()):
|
|
self._downRect = rect
|
|
return
|
|
return super().mousePressEvent(event)
|
|
|
|
def mouseReleaseEvent(self, event: Optional[QMouseEvent]) -> None:
|
|
if not event:
|
|
return super().mouseReleaseEvent(event)
|
|
if self._downRect is not None and self._downRect.contains(event.pos()):
|
|
self.pronounce.emit(
|
|
"https://media.merriam-webster.com/audio/prons/en/us/ogg/a/await001.ogg"
|
|
)
|
|
self._downRect = None
|
|
return
|
|
self._downRect = None
|
|
return super().mouseReleaseEvent(event)
|
|
|
|
def paintEvent(self, _: Optional[QPaintEvent]) -> None: # noqa
|
|
painter = QPainter(self)
|
|
painter.save()
|
|
painter.setBrush(QBrush())
|
|
painter.setPen(QColor("white"))
|
|
|
|
#
|
|
# 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,
|
|
# we can use the descendant to calculate the baseline within that
|
|
# bounding box.
|
|
#
|
|
# 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.repaintEvent(painter)
|
|
painter.restore()
|
|
return
|
|
|
|
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
|