Almost there

This commit is contained in:
Christopher T. Johnson
2024-03-29 10:03:47 -04:00
parent 5a7993c3ab
commit 5f4333ba46
2 changed files with 157 additions and 97 deletions

View File

@@ -3,7 +3,7 @@ import os
import sys import sys
from typing import cast from typing import cast
from PyQt6.QtCore import QResource from PyQt6.QtCore import QResource, QSettings
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
@@ -22,15 +22,31 @@ def main() -> int:
raise Exception(db.lastError()) raise Exception(db.lastError())
app = QApplication(sys.argv) app = QApplication(sys.argv)
# #
# Set Default settings
#
settings = QSettings('Troglodite', 'esl_reader')
settings.beginGroup('font')
if not settings.contains('display/url'):
settings.setValue('display/url', ':/fonts/opendyslexic/OpenDyslexic-Regular.otf')
if not settings.contains('display/name'):
settings.setValue('display/name', 'OpenDyslexic')
if not settings.contains('phonic/name'):
settings.setValue('phonic/name', 'Gentium')
settings.endGroup()
if not settings.contains('keys/mw-api'):
settings.setValue('keys/mw-api','51d9df34-ee13-489e-8656-478c215e846c')
#
# Setup resources # Setup resources
# #
if not QResource.registerResource( if not QResource.registerResource(
os.path.join(os.path.dirname(__file__), "ui/resources.rcc"), "/" os.path.join(os.path.dirname(__file__), "ui/resources.rcc"), "/"
): ):
raise Exception("Unable to register resources.rcc") raise Exception("Unable to register resources.rcc")
QFontDatabase.addApplicationFont( settings.beginGroup('font')
":/fonts/opendyslexic/OpenDyslexic-Regular.otf" for name in settings.childGroups():
) if settings.contains(f'{name}/url'):
QFontDatabase.addApplicationFont(settings.value(f'{name}/url'))
settings.endGroup()
query = QSqlQuery() query = QSqlQuery()
if not query.exec( if not query.exec(
"CREATE TABLE IF NOT EXISTS words " "CREATE TABLE IF NOT EXISTS words "

View File

@@ -4,7 +4,7 @@ import re
from typing import Any, Optional, cast from typing import Any, Optional, cast
import requests import requests
from PyQt6.QtCore import QPoint, QRect, QUrl, Qt, pyqtSignal from PyQt6.QtCore import QMargins, QPoint, QRect, QSize, QUrl, Qt, pyqtSignal
from PyQt6.QtGui import ( from PyQt6.QtGui import (
QBrush, QBrush,
QColor, QColor,
@@ -40,13 +40,14 @@ class Fragment:
Qt.AlignmentFlag.AlignLeft Qt.AlignmentFlag.AlignLeft
| Qt.AlignmentFlag.AlignBaseline | Qt.AlignmentFlag.AlignBaseline
) )
self._padding = [0, 0, 0, 0] self._padding = QMargins()
self._border = [0, 0, 0, 0] self._border = QMargins()
self._margin = [0, 0, 0, 0] self._margin = QMargins()
self._wref = '' self._wref = ''
self._position = QPoint() self._position = QPoint()
self._rect = QRect() self._rect = QRect()
self._borderRect = QRect() self._borderRect = QRect()
self._clickRect = QRect()
if color: if color:
self._color = color self._color = color
else: else:
@@ -56,20 +57,40 @@ class Fragment:
return return
def __str__(self) -> str: def __str__(self) -> str:
return self._text return self.__repr__()
def size(self, width:int) -> QSize:
rect = QRect(self._position,QSize(width,2000))
flags = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignBaseline | Qt.TextFlag.TextWordWrap
fm = QFontMetrics(self._font)
bounding = fm.boundingRect(rect, flags, self._text)
size = bounding.size()
size = size.grownBy(self._padding)
size = size.grownBy(self._border)
size = size.grownBy(self._margin)
return size
def height(self, width: int) -> int:
return self.size(width).height()
def width(self, width:int) -> int:
return self.size(width).width()
def __repr__(self) -> str:
return f'({self._position.x()}, {self._position.y()}): {self._text}'
def repaintEvent(self, painter:QPainter) -> int: def repaintEvent(self, painter:QPainter) -> int:
painter.save() painter.save()
painter.setFont(self._font) painter.setFont(self._font)
painter.setPen(self._color) painter.setPen(self._color)
rect = QRect() rect = QRect()
rect.setLeft(self._position.x()) rect.setLeft(self._position.x())
rect.setTop(self._position.y() - painter.fontMetrics().ascent()) rect.setTop(self._position.y() + painter.fontMetrics().descent() - painter.fontMetrics().height())
rect.setWidth(painter.viewport().width() - self._position.x()) rect.setWidth(painter.viewport().width() - self._position.x())
rect.setHeight(2000) rect.setHeight(2000)
flags = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignBaseline | Qt.TextFlag.TextWordWrap flags = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignBaseline | Qt.TextFlag.TextWordWrap
bounding = painter.boundingRect(rect,flags, self._text) bounding = painter.boundingRect(rect,flags, self._text)
height = bounding.height()+self._padding[2]+self._border[2]+self._margin[2] size = bounding.size()
painter.setPen(QColor("#f00")) painter.setPen(QColor("#f00"))
if self._audio.isValid(): if self._audio.isValid():
@@ -86,7 +107,10 @@ class Fragment:
self._text self._text
) )
painter.restore() painter.restore()
return height size = size.grownBy(self._margin)
size = size.grownBy(self._border)
size = size.grownBy(self._padding)
return size.height()
# #
# Setters # Setters
# #
@@ -108,71 +132,88 @@ class Fragment:
def setRect(self,rect:QRect) -> None: def setRect(self,rect:QRect) -> None:
self._rect = rect self._rect = rect
return return
def setPadding(self, top:int = -1, right:int = -1, bottom:int = -1, left:int = -1, *args:int) -> None: def setPadding(self, *args:int, **kwargs:int) -> None:
top = kwargs.get('top', -1)
right = kwargs.get('right', -1)
bottom = kwargs.get('bottom', -1)
left = kwargs.get('left', -1)
if top > -1 or right > -1 or bottom > -1 or left > -1: if top > -1 or right > -1 or bottom > -1 or left > -1:
if top >= 0: if top >= 0:
self._padding[0] = top self._padding.setTop(top)
if right >= 0: if right >= 0:
self._padding[1] = right self._padding.setRight(right)
if bottom >= 0: if bottom >= 0:
self._padding[2] = bottom self._padding.setBottom(bottom)
if left >= 0: if left >= 0:
self._padding[3] = left self._padding.setLeft(left)
return return
if len(args) == 4: if len(args) == 4:
self._padding = [args[0], args[1], args[2], args[3]] (top, right, bottom, left) = [args[0], args[1], args[2], args[3]]
elif len(args) == 3: elif len(args) == 3:
self._padding = [args[0], args[1], args[2], args[1]] (top, right, bottom, left) = [args[0], args[1], args[2], args[1]]
elif len(args) == 2: elif len(args) == 2:
self._padding = [args[0], args[1], args[0], args[1]] (top, right, bottom, left) = [args[0], args[1], args[0], args[1]]
elif len(args) == 1: elif len(args) == 1:
self._padding = [args[0], args[0], args[0], args[0]] (top, right, bottom, left) = [args[0], args[0], args[0], args[0]]
else: else:
raise Exception("argument error") raise Exception("argument error")
self._padding = QMargins(left, top, right, bottom)
return return
def setBorder(self, top:int = -1, right:int = -1, bottom:int = -1, left:int = -1, *args:int) -> None:
def setBorder(self, *args:int, **kwargs:int) -> None:
top = kwargs.get('top', -1)
right = kwargs.get('right', -1)
bottom = kwargs.get('bottom', -1)
left = kwargs.get('left', -1)
if top > -1 or right > -1 or bottom > -1 or left > -1: if top > -1 or right > -1 or bottom > -1 or left > -1:
if top >= 0: if top >= 0:
self._border[0] = top self._border.setTop(top)
if right >= 0: if right >= 0:
self._border[1] = right self._border.setRight(right)
if bottom >= 0: if bottom >= 0:
self._border[2] = bottom self._border.setBottom(bottom)
if left >= 0: if left >= 0:
self._border[3] = left self._border.setLeft(left)
return return
if len(args) == 4: if len(args) == 4:
self._border = [args[0], args[1], args[2], args[3]] (top, right, bottom, left) = [args[0], args[1], args[2], args[3]]
elif len(args) == 3: elif len(args) == 3:
self._border = [args[0], args[1], args[2], args[1]] (top, right, bottom, left) = [args[0], args[1], args[2], args[1]]
elif len(args) == 2: elif len(args) == 2:
self._border = [args[0], args[1], args[0], args[1]] (top, right, bottom, left) = [args[0], args[1], args[0], args[1]]
elif len(args) == 1: elif len(args) == 1:
self._border = [args[0], args[0], args[0], args[0]] (top, right, bottom, left) = [args[0], args[0], args[0], args[0]]
else: else:
raise Exception("argument error") raise Exception("argument error")
self._border = QMargins(left, top, right, bottom)
return return
def setMargin(self, top:int = -1, right:int = -1, bottom:int = -1, left:int = -1, *args:int) -> None: def setMargin(self, *args:int, **kwargs:int) -> None:
top = kwargs.get('top', -1)
right = kwargs.get('right', -1)
bottom = kwargs.get('bottom', -1)
left = kwargs.get('left', -1)
if top > -1 or right > -1 or bottom > -1 or left > -1: if top > -1 or right > -1 or bottom > -1 or left > -1:
if top >= 0: if top >= 0:
self._margin[0] = top self._margin.setTop(top)
if right >= 0: if right >= 0:
self._margin[1] = right self._margin.setRight(right)
if bottom >= 0: if bottom >= 0:
self._margin[2] = bottom self._margin.setBottom(bottom)
if left >= 0: if left >= 0:
self._margin[3] = left self._margin.setLeft(left)
return return
if len(args) == 4: if len(args) == 4:
self._margin = [args[0], args[1], args[2], args[3]] (top, right, bottom, left) =[args[0], args[1], args[2], args[3]]
elif len(args) == 3: elif len(args) == 3:
self._margin = [args[0], args[1], args[2], args[1]] (top, right, bottom, left) = [args[0], args[1], args[2], args[1]]
elif len(args) == 2: elif len(args) == 2:
self._margin = [args[0], args[1], args[0], args[1]] (top, right, bottom, left) = [args[0], args[1], args[0], args[1]]
elif len(args) == 1: elif len(args) == 1:
self._margin = [args[0], args[0], args[0], args[0]] (top, right, bottom, left) = [args[0], args[0], args[0], args[0]]
else: else:
raise Exception("argument error") raise Exception("argument error")
self._margin = QMargins(left, top, right, bottom)
return return
def setWRef(self, ref:str) -> None: def setWRef(self, ref:str) -> None:
self._wref = ref self._wref = ref
@@ -183,6 +224,9 @@ class Fragment:
def setBorderRect(self, rect:QRect) -> None: def setBorderRect(self, rect:QRect) -> None:
self._borderRect = rect self._borderRect = rect
return return
def setClickRect(self, rect:QRect) -> None:
self._clickRect = rect
return
def setColor(self,color:QColor) -> None: def setColor(self,color:QColor) -> None:
self._color = color self._color = color
return return
@@ -204,16 +248,18 @@ class Fragment:
return self._align return self._align
def rect(self) -> QRect: def rect(self) -> QRect:
return self._rect return self._rect
def padding(self) -> list[int]: def padding(self) -> QMargins:
return self._padding return self._padding
def border(self) -> list[int]: def border(self) -> QMargins:
return self._border return self._border
def margin(self) -> list[int]: def margin(self) -> QMargins:
return self._margin return self._margin
def position(self) -> QPoint: def position(self) -> QPoint:
return self._position return self._position
def borderRect(self) -> QRect: def borderRect(self) -> QRect:
return self._borderRect return self._borderRect
def clickRect(self) -> QRect:
return self._clickRect
def color(self) -> QColor: def color(self) -> QColor:
return self._color return self._color
def asis(self) -> bool: def asis(self) -> bool:
@@ -387,7 +433,7 @@ class Word:
def addFragment(self, frag: Fragment,) -> None: def addFragment(self, frag: Fragment,) -> None:
SPEAKER = "\U0001F508" SPEAKER = "\U0001F508"
if frag.audio(): if frag.audio().isValid():
frag.setText(frag.text() + ' ' + SPEAKER) frag.setText(frag.text() + ' ' + SPEAKER)
text = frag.text() text = frag.text()
@@ -396,14 +442,14 @@ class Word:
text = re.sub(r"\{rdquo\}", "\u201d", text) text = re.sub(r"\{rdquo\}", "\u201d", text)
frag.setText(text) frag.setText(text)
if frag.audio().isValid(): if frag.audio().isValid():
frag.setPadding(3) frag.setPadding(3,0,0,5)
frag.setBorder(1) frag.setBorder(1)
frag.setMargin(2) frag.setMargin(0,0,0,0)
items = self.parseText(frag) items = self.parseText(frag)
self._fragments += items self._fragments += items
return return
def finalizeLine(self) -> 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 # Find the maximum hight and max baseline
@@ -413,23 +459,10 @@ class Word:
leading = -1 leading = -1
for frag in self._fragments: for frag in self._fragments:
fm = QFontMetrics(frag.font()) fm = QFontMetrics(frag.font())
rect = fm.boundingRect(frag.text(), frag.align()) height = frag.height(width)
height = rect.height() bl = fm.height() - fm.descent()
bl = height - fm.descent()
if fm.leading() > leading: if fm.leading() > leading:
leading = fm.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: if height > maxHeight:
maxHeight = height maxHeight = height
if bl > baseLine: if bl > baseLine:
@@ -441,19 +474,16 @@ class Word:
for frag in self._fragments: for frag in self._fragments:
if x < frag.left(): if x < frag.left():
x = frag.left() x = frag.left()
#
# We need to calculate the location to draw the
# text. We also need to calculate the bounding Rectangle
# for this fragment
#
size = frag.size(width)
fm = QFontMetrics(frag.font()) fm = QFontMetrics(frag.font())
width = fm.horizontalAdvance(frag.text()) offset = frag.margin().left() + frag.border().left() + frag.padding().left()
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)) frag.setPosition(QPoint(x+offset, self._baseLine))
if frag.border()[0] != 0: if not frag.border().isNull() or not frag.wRef():
# #
# self._baseLine is where the text will be drawn # self._baseLine is where the text will be drawn
# fm.descent is the distance from the baseline of the # fm.descent is the distance from the baseline of the
@@ -462,16 +492,20 @@ class Word:
# + fm.descent - rect.height # + fm.descent - rect.height
# The border is drawn at top-padding-border-margin+marin # The border is drawn at top-padding-border-margin+marin
# #
top = self._baseLine + fm.descent() - rect.height() -1 top = self._baseLine + fm.descent() - fm.height()
y = top - padding[0] - border[0] y = top - frag.padding().top() - frag.border().top()
frag.setBorderRect(QRect(x+margin[3], y, width, height)) pos = QPoint(x, y)
x += width 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]:
return self._fragments return self._fragments
def getLeading(self) -> int: def getLineSpacing(self) -> int:
return self._leading + self._maxHeight return self._leading + self._maxHeight
_lines: list[Line] = [] _lines: list[Line] = []
@@ -499,6 +533,10 @@ class Word:
} }
self.current = Word._words[word] self.current = Word._words[word]
return return
#
# The code should look at our settings to see if we have an API
# key for MW to decide on the source to use.
#
source = 'mw' 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:
@@ -679,14 +717,11 @@ class Word:
frag = Fragment(entry['hwi']['hw'] + ' ', self.resources['fonts']['phonic'], color=base) frag = Fragment(entry['hwi']['hw'] + ' ', self.resources['fonts']['phonic'], color=base)
line.addFragment(frag) line.addFragment(frag)
for prs in entry["hwi"]["prs"]: for prs in entry["hwi"]["prs"]:
audio = self.sound_url(prs) audio = self.mw_sound_url(prs)
if audio is None: if audio is None:
audio = "" audio = ""
frag = Fragment(prs['mw'], self.resources['fonts']['phonic'], color=blue) frag = Fragment(prs['mw'], self.resources['fonts']['phonic'], color=blue)
frag.setAudio(audio) frag.setAudio(audio)
frag.setPadding(0,10,3,12)
frag.setBorder(1)
frag.setMargin(0,3,0,3)
line.addFragment(frag) line.addFragment(frag)
lines.append(line) lines.append(line)
if "ins" in entry.keys(): if "ins" in entry.keys():
@@ -724,7 +759,7 @@ class Word:
lines.append(line) lines.append(line)
return lines return lines
def sound_url(self, prs: dict[str, Any], fmt: str = "ogg") -> str | None: def mw_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():
@@ -735,7 +770,7 @@ class Word:
url = base + f"/{m.group(1)}/" url = base + f"/{m.group(1)}/"
else: else:
url = base + "/number/" url = base + "/number/"
url += audio + f".fmt" url += audio + f".{fmt}"
return url return url
def mw_html(self) -> str: def mw_html(self) -> str:
@@ -764,7 +799,7 @@ class Word:
if "prs" in self.current["hwi"].keys(): if "prs" in self.current["hwi"].keys():
tmp = [] tmp = []
for prs in self.current["hwi"]["prs"]: for prs in self.current["hwi"]["prs"]:
url = self.sound_url(prs) url = self.mw_sound_url(prs)
how = prs["mw"] how = prs["mw"]
if url: if url:
tmp.append(f'<a href="{url}">\\{how}\\</a>') tmp.append(f'<a href="{url}">\\{how}\\</a>')
@@ -827,43 +862,52 @@ class Definition(QWidget):
self._lines = lines self._lines = lines
self._buttons:list[Fragment] = [] self._buttons:list[Fragment] = []
base = 0 base = 0
for line in self._lines: for line in self._lines:
line.finalizeLine() line.finalizeLine(self.width(), base)
base += line.getLeading() 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()
self.setFixedHeight(base) self.setFixedHeight(base)
return return
def resizeEvent(self, event: QResizeEvent) -> None: def resizeEvent(self, event: QResizeEvent) -> None:
base = 0 base = 0
for line in self._lines: for line in self._lines:
line.finalizeLine() line.finalizeLine(self.width(),base)
base += line.getLeading() base += line.getLineSpacing()
self.setFixedHeight(base) self.setFixedHeight(base)
super(Definition,self).resizeEvent(event) super(Definition,self).resizeEvent(event)
return return
_downRect: QRect | None = None _downFrag: Optional[Fragment|None] = 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()}')
for frag in self._buttons: for frag in self._buttons:
rect = frag.borderRect() rect = frag.clickRect()
if rect.contains(event.pos()): if rect.contains(event.pos()):
self._downRect = rect self._downFrag = frag
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._downRect is not None and self._downRect.contains(event.pos()): if self._downFrag is not None and self._downFrag.clickRect().contains(event.pos()):
self.pronounce.emit( audio = self._downFrag.audio().url()
"https://media.merriam-webster.com/audio/prons/en/us/ogg/a/await001.ogg" print(audio)
) self.pronounce.emit(audio)
self._downRect = None print('emit done')
self._downFrag = None
return return
self._downRect = None self._downFrag = None
return super().mouseReleaseEvent(event) return super().mouseReleaseEvent(event)
def paintEvent(self, _: Optional[QPaintEvent]) -> None: # noqa def paintEvent(self, _: Optional[QPaintEvent]) -> None: # noqa