diff --git a/deftest.py b/deftest.py index ca5aca6..d18c844 100644 --- a/deftest.py +++ b/deftest.py @@ -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 " diff --git a/lib/words.py b/lib/words.py index 67235fa..97b4d85 100644 --- a/lib/words.py +++ b/lib/words.py @@ -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'\\{how}\\') @@ -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