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,15 +1,17 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os import os
import sys import sys
from typing import cast
from PyQt6.QtCore import QResource from PyQt6.QtCore import QResource
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
from lib import Definition, Word from lib import DefinitionArea, 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
def main() -> int: def main() -> int:
@@ -32,14 +34,17 @@ def main() -> int:
query = QSqlQuery() query = QSqlQuery()
if not query.exec( if not query.exec(
"CREATE TABLE IF NOT EXISTS words " "CREATE TABLE IF NOT EXISTS words "
"(word_id INTEGER PRIMARY KEY AUTOINCREMENT, word TEXT, definition TEXT)" "(word_id INTEGER PRIMARY KEY AUTOINCREMENT, "
"word TEXT, source TEXT, definition TEXT)"
): ):
query_error(query) query_error(query)
word = Word("boat") word = Word("boat")
snd = SoundOff() snd = SoundOff()
widget = Definition(word) # noqa: F841 widget = DefinitionArea(word) # xnoqa: F841
widget.pronounce.connect(snd.playSound) d = cast(Definition,widget.widget())
assert d is not None
d.pronounce.connect(snd.playSound)
widget.show() widget.show()
return app.exec() return app.exec()

View File

@@ -1,5 +1,7 @@
# pyright: ignore
from .utils import query_error # isort: skip from .utils import query_error # isort: skip
from .books import Book from .books import Book
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 Definition, Word, DefinitionArea

View File

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