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
from typing import cast
from PyQt6.QtCore import QResource
from PyQt6.QtCore import QResource, QSettings
from PyQt6.QtGui import QFontDatabase
from PyQt6.QtSql import QSqlDatabase, QSqlQuery
from PyQt6.QtWidgets import QApplication
@@ -22,15 +22,31 @@ def main() -> int:
raise Exception(db.lastError())
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
#
if not QResource.registerResource(
os.path.join(os.path.dirname(__file__), "ui/resources.rcc"), "/"
):
raise Exception("Unable to register resources.rcc")
QFontDatabase.addApplicationFont(
":/fonts/opendyslexic/OpenDyslexic-Regular.otf"
)
settings.beginGroup('font')
for name in settings.childGroups():
if settings.contains(f'{name}/url'):
QFontDatabase.addApplicationFont(settings.value(f'{name}/url'))
settings.endGroup()
query = QSqlQuery()
if not query.exec(
"CREATE TABLE IF NOT EXISTS words "

View File

@@ -4,7 +4,7 @@ import re
from typing import Any, Optional, cast
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 (
QBrush,
QColor,
@@ -40,13 +40,14 @@ class Fragment:
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._padding = QMargins()
self._border = QMargins()
self._margin = QMargins()
self._wref = ''
self._position = QPoint()
self._rect = QRect()
self._borderRect = QRect()
self._clickRect = QRect()
if color:
self._color = color
else:
@@ -56,20 +57,40 @@ class Fragment:
return
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:
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.setTop(self._position.y() + painter.fontMetrics().descent() - painter.fontMetrics().height())
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]
size = bounding.size()
painter.setPen(QColor("#f00"))
if self._audio.isValid():
@@ -86,7 +107,10 @@ class Fragment:
self._text
)
painter.restore()
return height
size = size.grownBy(self._margin)
size = size.grownBy(self._border)
size = size.grownBy(self._padding)
return size.height()
#
# Setters
#
@@ -108,71 +132,88 @@ class Fragment:
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:
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 >= 0:
self._padding[0] = top
self._padding.setTop(top)
if right >= 0:
self._padding[1] = right
self._padding.setRight(right)
if bottom >= 0:
self._padding[2] = bottom
self._padding.setBottom(bottom)
if left >= 0:
self._padding[3] = left
self._padding.setLeft(left)
return
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:
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:
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:
self._padding = [args[0], args[0], args[0], args[0]]
(top, right, bottom, left) = [args[0], args[0], args[0], args[0]]
else:
raise Exception("argument error")
self._padding = QMargins(left, top, right, bottom)
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 >= 0:
self._border[0] = top
self._border.setTop(top)
if right >= 0:
self._border[1] = right
self._border.setRight(right)
if bottom >= 0:
self._border[2] = bottom
self._border.setBottom(bottom)
if left >= 0:
self._border[3] = left
self._border.setLeft(left)
return
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:
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:
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:
self._border = [args[0], args[0], args[0], args[0]]
(top, right, bottom, left) = [args[0], args[0], args[0], args[0]]
else:
raise Exception("argument error")
self._border = QMargins(left, top, right, bottom)
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 >= 0:
self._margin[0] = top
self._margin.setTop(top)
if right >= 0:
self._margin[1] = right
self._margin.setRight(right)
if bottom >= 0:
self._margin[2] = bottom
self._margin.setBottom(bottom)
if left >= 0:
self._margin[3] = left
self._margin.setLeft(left)
return
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:
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:
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:
self._margin = [args[0], args[0], args[0], args[0]]
(top, right, bottom, left) = [args[0], args[0], args[0], args[0]]
else:
raise Exception("argument error")
self._margin = QMargins(left, top, right, bottom)
return
def setWRef(self, ref:str) -> None:
self._wref = ref
@@ -183,6 +224,9 @@ class Fragment:
def setBorderRect(self, rect:QRect) -> None:
self._borderRect = rect
return
def setClickRect(self, rect:QRect) -> None:
self._clickRect = rect
return
def setColor(self,color:QColor) -> None:
self._color = color
return
@@ -204,16 +248,18 @@ class Fragment:
return self._align
def rect(self) -> QRect:
return self._rect
def padding(self) -> list[int]:
def padding(self) -> QMargins:
return self._padding
def border(self) -> list[int]:
def border(self) -> QMargins:
return self._border
def margin(self) -> list[int]:
def margin(self) -> QMargins:
return self._margin
def position(self) -> QPoint:
return self._position
def borderRect(self) -> QRect:
return self._borderRect
def clickRect(self) -> QRect:
return self._clickRect
def color(self) -> QColor:
return self._color
def asis(self) -> bool:
@@ -387,7 +433,7 @@ class Word:
def addFragment(self, frag: Fragment,) -> None:
SPEAKER = "\U0001F508"
if frag.audio():
if frag.audio().isValid():
frag.setText(frag.text() + ' ' + SPEAKER)
text = frag.text()
@@ -396,14 +442,14 @@ class Word:
text = re.sub(r"\{rdquo\}", "\u201d", text)
frag.setText(text)
if frag.audio().isValid():
frag.setPadding(3)
frag.setPadding(3,0,0,5)
frag.setBorder(1)
frag.setMargin(2)
frag.setMargin(0,0,0,0)
items = self.parseText(frag)
self._fragments += items
return
def finalizeLine(self) -> None:
def finalizeLine(self, width: int, base:int ) -> None:
"""Create all of the positions for all the fragments."""
#
# Find the maximum hight and max baseline
@@ -413,23 +459,10 @@ class Word:
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()
height = frag.height(width)
bl = fm.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:
@@ -441,19 +474,16 @@ class Word:
for frag in self._fragments:
if 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())
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]
offset = frag.margin().left() + frag.border().left() + frag.padding().left()
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
# fm.descent is the distance from the baseline of the
@@ -462,16 +492,20 @@ class Word:
# + 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
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
def getLine(self) -> list[Fragment]:
return self._fragments
def getLeading(self) -> int:
def getLineSpacing(self) -> int:
return self._leading + self._maxHeight
_lines: list[Line] = []
@@ -499,6 +533,10 @@ class Word:
}
self.current = Word._words[word]
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'
response = requests.get(MWAPI.format(word=word))
if response.status_code != 200:
@@ -679,14 +717,11 @@ class Word:
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)
audio = self.mw_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():
@@ -724,7 +759,7 @@ class Word:
lines.append(line)
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."""
base = f"https://media.merriam-webster.com/audio/prons/en/us/{fmt}"
if "sound" not in prs.keys():
@@ -735,7 +770,7 @@ class Word:
url = base + f"/{m.group(1)}/"
else:
url = base + "/number/"
url += audio + f".fmt"
url += audio + f".{fmt}"
return url
def mw_html(self) -> str:
@@ -764,7 +799,7 @@ class Word:
if "prs" in self.current["hwi"].keys():
tmp = []
for prs in self.current["hwi"]["prs"]:
url = self.sound_url(prs)
url = self.mw_sound_url(prs)
how = prs["mw"]
if url:
tmp.append(f'<a href="{url}">\\{how}\\</a>')
@@ -827,43 +862,52 @@ class Definition(QWidget):
self._lines = lines
self._buttons:list[Fragment] = []
base = 0
for line in self._lines:
line.finalizeLine()
base += line.getLeading()
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()
self.setFixedHeight(base)
return
def resizeEvent(self, event: QResizeEvent) -> None:
base = 0
for line in self._lines:
line.finalizeLine()
base += line.getLeading()
line.finalizeLine(self.width(),base)
base += line.getLineSpacing()
self.setFixedHeight(base)
super(Definition,self).resizeEvent(event)
return
_downRect: QRect | None = None
_downFrag: Optional[Fragment|None] = None
def mousePressEvent(self, event: Optional[QMouseEvent]) -> None:
if not event:
return super().mousePressEvent(event)
print(f'mousePressEvent: {event.pos()}')
for frag in self._buttons:
rect = frag.borderRect()
rect = frag.clickRect()
if rect.contains(event.pos()):
self._downRect = rect
self._downFrag = frag
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
if self._downFrag is not None and self._downFrag.clickRect().contains(event.pos()):
audio = self._downFrag.audio().url()
print(audio)
self.pronounce.emit(audio)
print('emit done')
self._downFrag = None
return
self._downRect = None
self._downFrag = None
return super().mouseReleaseEvent(event)
def paintEvent(self, _: Optional[QPaintEvent]) -> None: # noqa