Almost working version of words

This commit is contained in:
Christopher T. Johnson
2024-03-27 12:01:47 -04:00
parent e7b88c2c2e
commit 3dcbd5f78d
3 changed files with 268 additions and 106 deletions

View File

@@ -1,10 +1,10 @@
import copy
import json
import re
from typing import Any, Dict, Optional, Self
from typing import Any, Optional, Self
import requests
from PyQt6.QtCore import QPoint, QRect, QUrl, Qt, pyqtSignal
from PyQt6.QtCore import QPoint, QRect, QSize, QUrl, Qt, pyqtSignal
from PyQt6.QtGui import (
QBrush,
QColor,
@@ -14,11 +14,12 @@ from PyQt6.QtGui import (
QMouseEvent,
QPainter,
QPaintEvent,
QResizeEvent,
QTextOption,
QTransform,
)
from PyQt6.QtSql import QSqlQuery
from PyQt6.QtWidgets import QWidget
from PyQt6.QtWidgets import QScrollArea, QScrollBar, QSizePolicy, QWidget
from lib import query_error
@@ -28,8 +29,8 @@ MWAPI = "https://www.dictionaryapi.com/api/v3/references/collegiate/json/{word}?
class Word:
_instance = None
_words: Dict[str, str] = {}
_current: Dict[str, Any]
_words: dict[str, Any] = {}
_current: dict[str, Any] = {}
_currentWord: str
class Fragment:
@@ -50,6 +51,8 @@ class Word:
_position: QPoint # where to drawText
_borderRect: QRect # where to drawRect
_radius: int # Radius for rounded rects
_asis: bool = False
_left: int = 0
TYPES = [ 'text', 'span', 'button' ]
@@ -58,7 +61,8 @@ class Word:
font:QFont,
t:str = 'text',
audio:str = '',
color: QColor = None
color: Optional[QColor] = None,
asis: bool = False,
) -> None:
if t not in self.TYPES:
raise Exception(f"Unknown fragment type{t}")
@@ -80,7 +84,10 @@ class Word:
self._color = color
else:
self._color = QColor()
self._asis = asis
return
def __str__(self) -> str:
return self._text
#
# Setters
#
@@ -182,6 +189,9 @@ class Word:
def setColor(self,color:QColor) -> None:
self._color = color
return
def setLeft(self, left:int) -> None:
self._left = left
return
#
# Getters
#
@@ -211,6 +221,10 @@ class Word:
return self._borderRect
def color(self) -> QColor:
return self._color
def asis(self) -> bool:
return self._asis
def left(self) -> int:
return self._left
class Line:
@@ -226,9 +240,13 @@ class Word:
self._fragments = []
return
def __repr__(self) -> str:
return '|'.join([x.text() for x in self._fragments])+f'|{self._maxHeight}'
def parseText(self, frag: 'Word.Fragment') -> list['Word.Fragment']:
org = frag.text()
print(org)
if frag.asis():
return [frag]
#
# Needed Fonts
#
@@ -272,13 +290,12 @@ class Word:
token = text[1:end]
frag.setText(text[end+1:])
newFrag = copy.copy(frag)
oldFont = QFont(frag.font)
oldFont = QFont(frag.font())
if token == 'bc':
results.append(Word.Fragment(': ', bold))
results.append(Word.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']:
frag.setText(text)
if token == 'b':
frag.setFont(bold)
elif token in ['it', 'qword', 'wi']:
@@ -374,7 +391,7 @@ class Word:
self._fragments += items
return
def finalizeLine(self, width:int) -> None:
def finalizeLine(self, maxWidth:int) -> None:
"""Create all of the positions for all the fragments."""
#
# Find the maximum hight and max baseline
@@ -386,23 +403,23 @@ class Word:
fm = QFontMetrics(frag.font())
rect = fm.boundingRect(frag.text(), frag.align())
height = rect.height()
baseLine = height - fm.descent()
bl = height - fm.descent()
#
# Add the padding, border and margin to adjust the baseline and height
#
b = frag.padding()
height += b[0] + b[2]
baseLine += b[2]
bl += b[2]
b = frag.border()
height += b[0] + b[2]
baseLine += b[2]
bl += b[2]
b = frag.margin()
height += b[0] + b[2]
baseLine += b[2]
bl += b[2]
if height > maxHeight:
maxHeight = height
if baseLine > baseLine:
baseLine = baseLine
if bl > baseLine:
baseLine = bl
self._baseLine = baseLine
self._maxHeight = maxHeight
self._leading = 0 # XXX - How should this be calculated?
@@ -410,10 +427,11 @@ class Word:
for frag in self._fragments:
#
# TODO - Wordwrap
# TODO - indent
#
if x < frag.left():
x = frag.left()
fm = QFontMetrics(frag.font())
rect = fm.boundingRect(frag.text())
width = rect.width()
width = fm.horizontalAdvance(frag.text())
padding = frag.padding()
offset = padding[3] # Left margin
width += padding[1] + padding[3]
@@ -436,6 +454,8 @@ class Word:
top = self._baseLine + fm.descent() - rect.height() -1
y = top - padding[0] - border[0]
frag.setBorderRect(QRect(x+margin[3], y, width, height))
if x + width > maxWidth:
print(f'Wrap text: {frag.text()}, {x}+{width} = {x + width}')
x += width
return
@@ -454,7 +474,14 @@ class Word:
return cls._instance
def __init__(self, word: str) -> None:
self._currentWord = word
#
# reset the current definition
#
try:
if word != self._current['word']:
self._lines = []
except KeyError:
pass
#
# Have we already retrieved this word?
#
@@ -469,130 +496,240 @@ class Word:
if not query.exec():
query_error(query)
if query.next():
self._words[word] = query.value("definition")
self._current = json.loads(self._words[word])
self._words[word] = {
'word': word,
'source': query.value('source'),
'definition': json.loads(query.value("definition")),
}
self._current = self._words[word]
return
source = 'mw'
response = requests.get(MWAPI.format(word=word))
if response.status_code != 200:
self._current = None
self._current = {}
return
data = json.loads(response.content.decode("utf-8"))
#
# XXX - The first entry should be the correct entry. There could be more
# if there is a "hom" entry, then that will be appended to meta.id
# word = "lady", hom=1, meta.id = "lady:1";
#
print(response.content.decode("utf-8"))
self._words[word] = json.dumps(data[0])
self._current = data[0]
print(data)
self._words[word] = {
'word': word,
'source': source,
'definition': data,
}
self._current = self._words[word]
query.prepare(
"INSERT INTO words "
"(word, definition) "
"VALUES (:word, :definition)"
"(word, source, definition) "
"VALUES (:word, :source, :definition)"
)
query.bindValue(":word", word)
query.bindValue(":definition", self._words[word])
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 getCurrent(self) -> str:
assert self._currentWord is not None
return self._currentWord
return self._current['word']
def get_html(self) -> str | None:
if not self._current:
return None
if "meta" in self._current.keys():
if self._current['source'] == 'mw':
return self.mw_html()
else:
elif self._current['source'] == 'apidictionary':
return self.apidictionary_html()
def get_def(self) -> list[Line] | None:
if not self._current:
return None
if "meta" in self._current.keys():
return self.mw_def()
else:
return None
raise Exception(f"Unknown source: {self._current['source']}")
def mw_def(self) -> list[Line]:
_resources:dict[str,Any] = {}
def get_def(self) -> list[Line] | None:
if len(self._lines) > 0:
return self._lines
assert self._current is not None
line = self.Line()
#
# Colors we used
#
base = QColor(Qt.GlobalColor.white)
blue = QColor("#4a7d95")
headerFont = QFontDatabase.font("OpenDyslexic", None, 10)
headerFont.setPixelSize(48)
headerFont.setWeight(QFont.Weight.Bold)
labelFont = QFontDatabase.font("OpenDyslexic", None, 10)
labelFont.setPixelSize(30)
phonicFont = QFontDatabase.font("Gentium", None, 10)
phonicFont.setPixelSize(20)
boldFont = QFontDatabase.font("OpenDyslexic", None, 10)
boldFont.setPixelSize(20)
boldFont.setBold(True)
textFont = QFontDatabase.font("OpenDyslexic", None, 10)
textFont.setPixelSize(20)
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)
hw = re.sub(r'\*', '', self._current['hwi']['hw'])
frag = Word.Fragment(hw, headerFont, color=base)
line.addFragment(frag)
frag = Word.Fragment(' '+self._current["fl"], labelFont, color=blue)
line.addFragment(frag)
self._lines.append(line)
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']}")
if "vrs" in self._current.keys():
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(
Word.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 = Word.Fragment(
f"{outer} {inner} ",
self._resources['fonts']['bold'],
color = self._resources['colors']['base']
)
outer = ' '
frag.setLeft(10)
line.addFragment(frag)
frag = Word.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 =Word.Fragment(f" ",
self._resources['fonts']['bold'],
)
frag.setLeft(45)
line.addFragment(frag)
line.addFragment(
Word.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 = Word.Fragment(hw, self._resources['fonts']['header'], color=base)
line.addFragment(frag)
frag = Word.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 self._current["vrs"]:
frag = Word.Fragment(space + vrs["va"], labelFont, color=base)
for vrs in entry["vrs"]:
frag = Word.Fragment(space + vrs["va"], self._resources['fonts']['label'], color=base)
space = ' '
line.addFragment(frag)
self._lines.append(line)
if "prs" in self._current["hwi"].keys():
lines.append(line)
if "prs" in entry["hwi"].keys():
line = self.Line()
frag = Word.Fragment(self._current['hwi']['hw'] + ' ', phonicFont, color=base)
frag = Word.Fragment(entry['hwi']['hw'] + ' ', self._resources['fonts']['phonic'], color=base)
line.addFragment(frag)
for prs in self._current["hwi"]["prs"]:
for prs in entry["hwi"]["prs"]:
audio = self.sound_url(prs)
if audio is None:
audio = ""
frag = Word.Fragment(prs['mw'], phonicFont, color=blue)
frag = Word.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)
self._lines.append(line)
if "ins" in self._current.keys():
lines.append(line)
if "ins" in entry.keys():
line = self.Line()
space = ''
for ins in self._current['ins']:
for ins in entry['ins']:
try:
frag = Word.Fragment(ins['il'], textFont, color=base)
frag = Word.Fragment(ins['il'], self._resources['fonts']['text'], color=base)
line.addFragment(frag)
space = ' '
except KeyError:
space = ''
frag = Word.Fragment(space + ins['if'], boldFont, color=base)
pass
frag = Word.Fragment(space + ins['if'], self._resources['fonts']['bold'], color=base)
line.addFragment(frag)
space = '; '
self._lines.append(line)
if 'lbs' in self._current.keys():
print('lbs')
lines.append(line)
if 'lbs' in entry.keys():
line = self.Line()
frag = Word.Fragment('; '.join(self._current['lbs']), boldFont, color=base)
frag = Word.Fragment('; '.join(entry['lbs']), self._resources['fonts']['bold'], color=base)
line.addFragment(frag)
self._lines.append(line)
return self._lines
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(Word.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:
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():
@@ -609,9 +746,6 @@ class Word:
def mw_html(self) -> str:
def parse_sn(sn: str, old: str) -> str:
return sn
assert self._current is not None
#
# Create the header, base word and its label
#
@@ -703,12 +837,23 @@ class Definition(QWidget):
self._lines = lines
self._buttons = []
assert self._lines is not None
#self.setFixedWidth(600)
base = 0
for line in self._lines:
line.finalizeLine(80)
line.finalizeLine(self.width())
base += line.getLeading()
self.setFixedHeight(base)
return
def resizeEvent(self, event: QResizeEvent) -> None:
base = 0
for line in self._lines:
line.finalizeLine(event.size().width())
base += line.getLeading()
self.setFixedHeight(base)
super(Definition,self).resizeEvent(event)
return
_downRect: QRect | None = None
def mousePressEvent(self, event: Optional[QMouseEvent]) -> None:
@@ -768,10 +913,20 @@ class Definition(QWidget):
#
elif frag.wRef():
painter.drawLine(frag.borderRect().bottomLeft(), frag.borderRect().bottomRight())
print(base, painter.pen().color().name(), frag.position(), frag.text())
painter.drawText(frag.position(), frag.text())
painter.restore()
painter.resetTransform()
base += line.getLeading()
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