Compare commits
10 Commits
814206148a
...
plugin-rew
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73a96e79a2 | ||
|
|
0acba3ed9b | ||
|
|
7c65b466f1 | ||
|
|
f97305e36e | ||
|
|
7d2532d775 | ||
|
|
51b1121176 | ||
|
|
f1ad24d70a | ||
|
|
1bce000978 | ||
|
|
303dbe6fe0 | ||
|
|
51a924b510 |
8
clean.sh
8
clean.sh
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
source venv/bin/activate
|
||||
set -e
|
||||
isort --profile black *.py lib/*.py
|
||||
black -l 80 *.py lib/*.py
|
||||
flynt *.py lib/*.py
|
||||
mypy esl_reader.py
|
||||
isort --profile black *.py lib/*.py plugins/*.py
|
||||
black -l 80 *.py lib/*.py plugins/*.py
|
||||
flynt *.py lib/*.py plugins/*.py
|
||||
mypy esl_reader.py plugins/*.py
|
||||
|
||||
46
deftest.py
Normal file → Executable file
46
deftest.py
Normal file → Executable file
@@ -3,24 +3,40 @@ import faulthandler
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
from typing import cast
|
||||
from typing import Any, cast
|
||||
|
||||
from PyQt6.QtCore import QResource, QSettings
|
||||
from PyQt6.QtCore import QResource, QSettings, Qt, pyqtSlot
|
||||
from PyQt6.QtGui import QFontDatabase
|
||||
from PyQt6.QtSql import QSqlDatabase, QSqlQuery
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
from PyQt6.QtWidgets import QApplication, QScrollArea
|
||||
|
||||
from lib import DefinitionArea, Word
|
||||
from lib import Word
|
||||
from lib.sounds import SoundOff
|
||||
from lib.utils import query_error
|
||||
from lib.words import Definition
|
||||
|
||||
class DefinitionArea(QScrollArea):
|
||||
def __init__(self, w: Word, *args: Any, **kwargs: Any) -> None:
|
||||
super(DefinitionArea, self).__init__(*args, *kwargs)
|
||||
self.definition = Definition(w)
|
||||
self.setWidget(self.definition)
|
||||
self.setWidgetResizable(True)
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
|
||||
self.definition.newWord.connect(self.newWord)
|
||||
return
|
||||
|
||||
def monkeyClose(self, event):
|
||||
settings = QSettings("Troglodite", "esl_reader")
|
||||
settings.setValue("geometry", self.saveGeometry())
|
||||
super(DefinitionArea, self).closeEvent(event)
|
||||
return
|
||||
@pyqtSlot(str)
|
||||
def newWord(self, word:str) -> None:
|
||||
print(f"newWord: {word}")
|
||||
w = Word(word)
|
||||
self.definition.setWord(w)
|
||||
return
|
||||
|
||||
def closeEvent(self, event):
|
||||
settings = QSettings("Troglodite", "esl_reader")
|
||||
settings.setValue("geometry", self.saveGeometry())
|
||||
super(DefinitionArea, self).closeEvent(event)
|
||||
return
|
||||
|
||||
|
||||
def main() -> int:
|
||||
@@ -66,12 +82,15 @@ def main() -> int:
|
||||
):
|
||||
query_error(query)
|
||||
|
||||
word = Word("cowbell")
|
||||
word = Word("lower")
|
||||
snd = SoundOff()
|
||||
DefinitionArea.closeEvent = monkeyClose
|
||||
print("Pre widget")
|
||||
widget = DefinitionArea(word) # xnoqa: F841
|
||||
print("post widget")
|
||||
settings = QSettings("Troglodite", "esl_reader")
|
||||
widget.restoreGeometry(settings.value("geometry"))
|
||||
geometry = settings.value("geometry")
|
||||
if geometry is not None:
|
||||
widget.restoreGeometry(geometry)
|
||||
d = cast(Definition, widget.widget())
|
||||
assert d is not None
|
||||
d.pronounce.connect(snd.playSound)
|
||||
@@ -81,4 +100,7 @@ def main() -> int:
|
||||
|
||||
if __name__ == "__main__":
|
||||
faulthandler.register(signal.Signals.SIGUSR1)
|
||||
faulthandler.register(signal.Signals.SIGTERM)
|
||||
faulthandler.register(signal.Signals.SIGHUP)
|
||||
faulthandler.enable()
|
||||
sys.exit(main())
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# pyright: ignore
|
||||
from .utils import query_error # isort: skip
|
||||
from .books import Book
|
||||
from .definition import Definition, Fragment, Line
|
||||
from .person import PersonDialog
|
||||
from .read import ReadDialog
|
||||
from .session import SessionDialog
|
||||
from .words import DefinitionArea, Word
|
||||
from .definition import Fragment, Line, Definition
|
||||
from .words import Word
|
||||
|
||||
@@ -1,39 +1,64 @@
|
||||
import re
|
||||
from typing import Any, Optional, Self, cast, overload
|
||||
import re
|
||||
from PyQt6.QtCore import QMargins, QPoint, QRect, QSize, QUrl, Qt, pyqtSignal
|
||||
from PyQt6.QtGui import QColor, QFont, QFontMetrics, QMouseEvent, QPaintEvent, QPainter, QResizeEvent, QTextOption, QTransform, QBrush
|
||||
import unicodedata
|
||||
from typing import Any, Callable, Optional, Self, TypedDict, cast
|
||||
|
||||
from PyQt6.QtCore import QMargins, QPoint, QPointF, QRect, QRectF, QSize, Qt, QUrl, pyqtSignal
|
||||
from PyQt6.QtGui import (
|
||||
QBrush,
|
||||
QColor,
|
||||
QFont,
|
||||
QFontDatabase,
|
||||
QFontMetrics,
|
||||
QMouseEvent,
|
||||
QPainter,
|
||||
QPaintEvent,
|
||||
QResizeEvent,
|
||||
QTextCharFormat,
|
||||
QTextLayout,
|
||||
QTextOption,
|
||||
)
|
||||
from lib.sounds import SoundOff
|
||||
from PyQt6.QtWidgets import QWidget
|
||||
|
||||
class MyPointF(QPointF):
|
||||
def __repr__(self):
|
||||
return f"({self.x()}, {self.y()})"
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
class Fragment:
|
||||
"""A fragment of text to be displayed"""
|
||||
|
||||
_indentAmount = 35
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
which: str|Self,
|
||||
font: QFont|None = None,
|
||||
audio: str = "",
|
||||
color: Optional[QColor] = None,
|
||||
asis: bool = False,
|
||||
self,
|
||||
which: str | Self | None = None,
|
||||
font: QFont | None = None,
|
||||
audio: str = "",
|
||||
color: Optional[QColor] = None,
|
||||
asis: bool = False,
|
||||
) -> None:
|
||||
if isinstance(which, Fragment):
|
||||
for k,v in which.__dict__.items():
|
||||
for k, v in which.__dict__.items():
|
||||
self.__dict__[k] = v
|
||||
return
|
||||
self._text:str = which
|
||||
self._layout = QTextLayout()
|
||||
if font is None:
|
||||
raise TypeError("Missing required parameter 'font'")
|
||||
self._font = font
|
||||
self._audio: QUrl = QUrl(audio)
|
||||
self._align = QTextOption(
|
||||
self._layout.setFont(
|
||||
QFontDatabase.font("OpenDyslexic", None, 20)
|
||||
)
|
||||
else:
|
||||
self._layout.setFont(font)
|
||||
align = QTextOption(
|
||||
Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignBaseline
|
||||
)
|
||||
self._layout.setTextOption(align)
|
||||
self._audio: QUrl = QUrl(audio)
|
||||
self._padding = QMargins()
|
||||
self._border = QMargins()
|
||||
self._margin = QMargins()
|
||||
self._wref = ""
|
||||
self._position = QPoint()
|
||||
self._rect = QRect()
|
||||
self._borderRect = QRect()
|
||||
self._clickRect = QRect()
|
||||
@@ -44,147 +69,114 @@ class Fragment:
|
||||
self._background = QColor()
|
||||
self._asis = asis
|
||||
self._indent = 0
|
||||
self._target = "word"
|
||||
if which is not None:
|
||||
self.setText(which)
|
||||
return
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
def size(self) -> QSize:
|
||||
return self.paintEvent()
|
||||
|
||||
def size(self, width: int) -> QSize:
|
||||
return self.paintEvent(width)
|
||||
def height(self) -> int:
|
||||
return self.size().height()
|
||||
|
||||
def height(self, width: int) -> int:
|
||||
return self.size(width).height()
|
||||
|
||||
def width(self, width: int) -> int:
|
||||
return self.size(width).width()
|
||||
def width(self) -> int:
|
||||
return self.size().width()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"({self._position.x()}, {self._position.y()}): {self._text}"
|
||||
rect = self._layout.boundingRect()
|
||||
text = self._layout.text()
|
||||
return f"{text}: (({rect.x()}, {rect.y()}), {rect.width()}, {rect.height()})"
|
||||
|
||||
@overload
|
||||
def paintEvent(self, widthSrc:int) -> QSize:
|
||||
...
|
||||
@overload
|
||||
def paintEvent(self, widthSrc: QPainter) -> int:
|
||||
...
|
||||
def doLayout(self, width: int) -> QPointF:
|
||||
leading = QFontMetrics(self._layout.font()).leading()
|
||||
eol = self._layout.position()
|
||||
base = 0
|
||||
indent = 0
|
||||
self._layout.setCacheEnabled(True)
|
||||
self._layout.beginLayout()
|
||||
while True:
|
||||
line = self._layout.createLine()
|
||||
if not line.isValid():
|
||||
break
|
||||
line.setLineWidth(width - self._layout.position().x())
|
||||
line.setPosition(QPointF(indent, base+leading))
|
||||
rect = line.naturalTextRect()
|
||||
eol = rect.bottomRight()
|
||||
assert isinstance(eol, QPointF)
|
||||
base += line.height()
|
||||
indent = self.pixelIndent() - self._layout.position().x()
|
||||
self._layout.endLayout()
|
||||
result = eol
|
||||
return result
|
||||
|
||||
def paintEvent(self, widthSrc) -> int|QSize:
|
||||
if isinstance(widthSrc, QPainter):
|
||||
viewportWidth = widthSrc.viewport().width()
|
||||
painter = widthSrc
|
||||
else:
|
||||
viewportWidth = widthSrc
|
||||
painter = None
|
||||
fm = QFontMetrics(self._font)
|
||||
top = (
|
||||
self._position.y()
|
||||
+ fm.descent()
|
||||
- fm.height()
|
||||
)
|
||||
left = self._position.x()
|
||||
width = viewportWidth - left
|
||||
height = 2000
|
||||
rect = QRect(left, top, width, height)
|
||||
indent = self._indent * self._indentAmount
|
||||
flags = (
|
||||
Qt.AlignmentFlag.AlignLeft
|
||||
| Qt.AlignmentFlag.AlignBaseline
|
||||
)
|
||||
boundingNoWrap = fm.boundingRect(
|
||||
rect, flags|Qt.TextFlag.TextSingleLine, self._text
|
||||
)
|
||||
bounding = fm.boundingRect(
|
||||
rect, flags|Qt.TextFlag.TextWordWrap, self._text
|
||||
)
|
||||
text = self._text
|
||||
remainingText = ''
|
||||
if boundingNoWrap.height() < bounding.height():
|
||||
#
|
||||
# This is not optimal, but it is only a few iterations
|
||||
#
|
||||
lastSpace = 0
|
||||
char = 0
|
||||
pos = rect.x()
|
||||
while pos < rect.right():
|
||||
if text[char] == ' ':
|
||||
lastSpace = char
|
||||
pos += fm.horizontalAdvance(
|
||||
text[char]
|
||||
)
|
||||
char += 1
|
||||
if lastSpace > 0:
|
||||
remainingText = text[lastSpace+1:]
|
||||
text = text[:lastSpace]
|
||||
|
||||
size = boundingNoWrap.size()
|
||||
|
||||
boundingNoWrap = fm.boundingRect(
|
||||
rect, flags|Qt.TextFlag.TextSingleLine, text
|
||||
)
|
||||
rect.setSize(boundingNoWrap.size())
|
||||
|
||||
|
||||
if remainingText != '':
|
||||
top += size.height()
|
||||
remainingRect = QRect(
|
||||
indent, top,
|
||||
viewportWidth - indent, height
|
||||
)
|
||||
boundingRemaingRect = fm.boundingRect(
|
||||
remainingRect, flags | Qt.TextFlag.TextWordWrap, remainingText
|
||||
)
|
||||
|
||||
size = size.grownBy(
|
||||
QMargins(
|
||||
0,0,0, boundingRemaingRect.height()
|
||||
)
|
||||
)
|
||||
remainingRect.setSize(boundingRemaingRect.size())
|
||||
size = size.grownBy(self._margin)
|
||||
size = size.grownBy(self._border)
|
||||
size = size.grownBy(self._padding)
|
||||
def paintEvent(self, painter: Optional[QPainter] | None = None) -> QSize:
|
||||
rect = self._layout.boundingRect()
|
||||
size = rect.size()
|
||||
assert size is not None
|
||||
if painter is None:
|
||||
return size
|
||||
return QSize(int(size.width()), int(size.height()))
|
||||
painter.save()
|
||||
painter.setFont(self._font)
|
||||
painter.setPen(QColor("#f00"))
|
||||
if self._audio.isValid():
|
||||
radius = self._borderRect.height() / 2
|
||||
painter.drawRoundedRect(self._borderRect, radius, radius)
|
||||
if self._wref:
|
||||
start = bounding.bottomLeft()
|
||||
end = bounding.bottomRight()
|
||||
painter.drawLine(start, end)
|
||||
|
||||
painter.setPen(self._color)
|
||||
if self._background.isValid():
|
||||
brush = painter.brush()
|
||||
brush.setColor(self._background)
|
||||
brush.setStyle(Qt.BrushStyle.SolidPattern)
|
||||
painter.setBrush(brush)
|
||||
painter.fillRect(rect,brush)
|
||||
painter.drawText(rect, flags, text)
|
||||
if remainingText:
|
||||
if self._background.isValid():
|
||||
painter.fillRect(remainingRect, brush)
|
||||
painter.drawText(remainingRect, flags|Qt.TextFlag.TextWordWrap, remainingText)
|
||||
self._layout.draw(painter, QPointF(0,0))
|
||||
#
|
||||
# TODO: draw the rounded rect around audio buttons
|
||||
#
|
||||
painter.brush().setColor(Qt.GlobalColor.green)
|
||||
for fmt in self._layout.formats():
|
||||
if fmt.format.isAnchor():
|
||||
runs = self._layout.glyphRuns(fmt.start, fmt.length)
|
||||
bb = runs[0].boundingRect()
|
||||
bb.moveTo(bb.topLeft() + self._layout.position())
|
||||
url = QUrl(fmt.format.anchorHref())
|
||||
if url.scheme() == 'audio':
|
||||
painter.setPen(QColor('red'))
|
||||
radius = (bb.topLeft() - bb.bottomLeft()).manhattanLength()/4
|
||||
painter.drawRoundedRect(bb, radius,radius)
|
||||
else:
|
||||
painter.setPen(QColor("blue"))
|
||||
#painter.drawRect(bb)
|
||||
|
||||
painter.restore()
|
||||
return size.height()
|
||||
return QSize(int(size.width()), int(size.height()))
|
||||
|
||||
#
|
||||
# Setters
|
||||
#
|
||||
def setText(self, text: str) -> None:
|
||||
self._text = text
|
||||
def addText(self, text: str, fmt: Optional[QTextCharFormat] = None) -> None:
|
||||
oldText = self._layout.text()
|
||||
self._layout.setText(oldText + text)
|
||||
if Line.parseText:
|
||||
self._layout = Line.parseText(self)
|
||||
|
||||
if fmt is not None:
|
||||
fr = QTextLayout.FormatRange()
|
||||
fr.format = fmt
|
||||
fr.length = len(self._layout.text()) - len(oldText)
|
||||
fr.start = len(oldText)
|
||||
fmts = self._layout.formats()
|
||||
fmts.append(fr)
|
||||
self._layout.setFormats(fmts)
|
||||
return
|
||||
|
||||
def setTarget(self, target: str) -> None:
|
||||
self._target = target
|
||||
def setText(self, text: str) -> None:
|
||||
text = unicodedata.normalize("NFKD",text)
|
||||
self._layout.setText(text)
|
||||
if Line.parseText:
|
||||
self._layout = Line.parseText(self)
|
||||
if self.audio().isValid():
|
||||
fr = QTextLayout.FormatRange()
|
||||
fr.start=0
|
||||
fr.length = len(self._layout.text())
|
||||
fmt = QTextCharFormat()
|
||||
fmt.setAnchor(True)
|
||||
fmt.setAnchorHref(self._audio.toString())
|
||||
fr.format = fmt
|
||||
formats = self._layout.formats()
|
||||
formats.append(fr)
|
||||
self._layout.setFormats(formats)
|
||||
return
|
||||
|
||||
def setFont(self, font: QFont) -> None:
|
||||
self._font = font
|
||||
self._layout.setFont(font)
|
||||
return
|
||||
|
||||
def setAudio(self, audio: str | QUrl) -> None:
|
||||
@@ -195,7 +187,7 @@ class Fragment:
|
||||
return
|
||||
|
||||
def setAlign(self, align: QTextOption) -> None:
|
||||
self._align = align
|
||||
self._layout.setTextOption(align)
|
||||
return
|
||||
|
||||
def setRect(self, rect: QRect) -> None:
|
||||
@@ -292,7 +284,7 @@ class Fragment:
|
||||
return
|
||||
|
||||
def setPosition(self, pnt: QPoint) -> None:
|
||||
self._position = pnt
|
||||
self._layout.setPosition(QPointF(pnt.x(), pnt.y()))
|
||||
return
|
||||
|
||||
def setBorderRect(self, rect: QRect) -> None:
|
||||
@@ -310,6 +302,7 @@ class Fragment:
|
||||
def setBackground(self, color: QColor) -> None:
|
||||
self._background = color
|
||||
return
|
||||
|
||||
def setIndent(self, indent: int) -> None:
|
||||
self._indent = indent
|
||||
return
|
||||
@@ -317,20 +310,23 @@ class Fragment:
|
||||
#
|
||||
# Getters
|
||||
#
|
||||
def background(self) -> QColor:
|
||||
return self._background
|
||||
|
||||
def wRef(self) -> str:
|
||||
return self._wref
|
||||
|
||||
def text(self) -> str:
|
||||
return self._text
|
||||
return self._layout.text()
|
||||
|
||||
def font(self) -> QFont:
|
||||
return self._font
|
||||
return self._layout.font()
|
||||
|
||||
def audio(self) -> QUrl:
|
||||
return self._audio
|
||||
|
||||
def align(self) -> QTextOption:
|
||||
return self._align
|
||||
return self._layout.textOption()
|
||||
|
||||
def rect(self) -> QRect:
|
||||
return self._rect
|
||||
@@ -344,8 +340,8 @@ class Fragment:
|
||||
def margin(self) -> QMargins:
|
||||
return self._margin
|
||||
|
||||
def position(self) -> QPoint:
|
||||
return self._position
|
||||
def position(self) -> QPointF:
|
||||
return self._layout.position()
|
||||
|
||||
def borderRect(self) -> QRect:
|
||||
return self._borderRect
|
||||
@@ -365,8 +361,12 @@ class Fragment:
|
||||
def pixelIndent(self) -> int:
|
||||
return self._indent * self._indentAmount
|
||||
|
||||
def layout(self) -> QTextLayout:
|
||||
return self._layout
|
||||
|
||||
class Line:
|
||||
parseText = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._maxHeight = -1
|
||||
self._baseLine = -1
|
||||
@@ -379,104 +379,58 @@ class Line:
|
||||
"|".join([x.text() for x in self._fragments])
|
||||
+ f"|{self._maxHeight}"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def setParseText(cls, call) -> None:
|
||||
def setParseText(cls, call: Callable) -> None:
|
||||
cls.parseText = call
|
||||
return
|
||||
|
||||
|
||||
def paintEvent(self, painter: QPainter) -> int:
|
||||
#
|
||||
# we do not have an event field because we are not a true widget
|
||||
#
|
||||
lineSpacing = 0
|
||||
pos = QSize(0,0)
|
||||
for frag in self._fragments:
|
||||
ls = frag.paintEvent(painter)
|
||||
if ls > lineSpacing:
|
||||
lineSpacing = ls
|
||||
return lineSpacing
|
||||
pos = frag.paintEvent(painter)
|
||||
return pos.height()
|
||||
|
||||
|
||||
def addFragment(self, frags: Fragment|list[Fragment],) -> None:
|
||||
SPEAKER = "\U0001F508"
|
||||
def addFragment(
|
||||
self,
|
||||
frags: Fragment | list[Fragment],
|
||||
) -> None:
|
||||
#SPEAKER = "\U0001F508"
|
||||
|
||||
if not isinstance(frags, list):
|
||||
frags = [frags, ]
|
||||
for frag in frags:
|
||||
if frag.audio().isValid():
|
||||
frag.setText(frag.text() + " " + SPEAKER)
|
||||
|
||||
text = frag.text()
|
||||
text = re.sub(r"\*", "\u2022", text)
|
||||
text = re.sub(r"\{ldquo\}", "\u201c", text)
|
||||
text = re.sub(r"\{rdquo\}", "\u201d", text)
|
||||
frag.setText(text)
|
||||
if frag.audio().isValid():
|
||||
frag.setPadding(3, 0, 0, 5)
|
||||
frag.setBorder(1)
|
||||
frag.setMargin(0, 0, 0, 0)
|
||||
if Line.parseText:
|
||||
items = Line.parseText(frag)
|
||||
self._fragments += items
|
||||
else:
|
||||
self._fragments.append(frag)
|
||||
frags = [
|
||||
frags,
|
||||
]
|
||||
self._fragments += frags
|
||||
return
|
||||
|
||||
def finalizeLine(self, width: int, base: int) -> None:
|
||||
"""Create all of the positions for all the fragments."""
|
||||
#
|
||||
# Find the maximum hight and max baseline
|
||||
# Each fragment needs to be positioned to the left of the
|
||||
# last fragment or at the indent level.
|
||||
# It needs to be aligned with the baseline of all the
|
||||
# other fragments in the line.
|
||||
#
|
||||
maxHeight = -1
|
||||
baseLine = -1
|
||||
leading = -1
|
||||
|
||||
left = 0 # Left size of rect
|
||||
maxHeight = 0
|
||||
|
||||
for frag in self._fragments:
|
||||
fm = QFontMetrics(frag.font())
|
||||
height = frag.height(width)
|
||||
bl = fm.height() - fm.descent()
|
||||
if fm.leading() > leading:
|
||||
leading = fm.leading()
|
||||
if height > maxHeight:
|
||||
maxHeight = height
|
||||
if bl > baseLine:
|
||||
baseLine = bl
|
||||
self._baseLine = baseLine
|
||||
self._maxHeight = maxHeight
|
||||
self._leading = leading
|
||||
x = 0
|
||||
for frag in self._fragments:
|
||||
left = frag.pixelIndent()
|
||||
if x < left:
|
||||
x = 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())
|
||||
offset = (
|
||||
frag.margin().left()
|
||||
+ frag.border().left()
|
||||
+ frag.padding().left()
|
||||
)
|
||||
frag.setPosition(QPoint(x + offset, self._baseLine))
|
||||
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
|
||||
# text to the bottom of the rect
|
||||
# The top of the bounding rect is at self._baseLine
|
||||
# + fm.descent - rect.height
|
||||
# The border is drawn at top-padding-border-margin+marin
|
||||
#
|
||||
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()
|
||||
if left < frag.pixelIndent():
|
||||
left = frag.pixelIndent()
|
||||
frag.setPosition(QPoint(left, base))
|
||||
eol =frag.doLayout(width)
|
||||
left = int(eol.x()+0.5)
|
||||
if frag.layout().lineCount() > 1:
|
||||
base = int(eol.y()+0.5)
|
||||
if eol.y() > maxHeight:
|
||||
maxHeight = eol.y()
|
||||
self._maxHeight = int(maxHeight+0.5)
|
||||
self._leading = 0
|
||||
return
|
||||
|
||||
def getLine(self) -> list[Fragment]:
|
||||
@@ -485,13 +439,22 @@ class Line:
|
||||
def getLineSpacing(self) -> int:
|
||||
return self._leading + self._maxHeight
|
||||
|
||||
class Definition(QWidget):
|
||||
pronounce = pyqtSignal(str)
|
||||
class Clickable(TypedDict):
|
||||
bb: QRectF
|
||||
frag: Fragment
|
||||
fmt: QTextCharFormat
|
||||
|
||||
class Definition(QWidget):
|
||||
pronounce = pyqtSignal(QUrl)
|
||||
alert = pyqtSignal()
|
||||
newWord = pyqtSignal(str)
|
||||
def __init__(
|
||||
self, word: Optional[Any] = None, *args: Any, **kwargs: Any
|
||||
) -> None:
|
||||
super(Definition, self).__init__(*args, **kwargs)
|
||||
self._sound = SoundOff()
|
||||
self.pronounce.connect(self._sound.playSound)
|
||||
self.alert.connect(self._sound.alert)
|
||||
self._word = word
|
||||
if word is not None:
|
||||
self.setWord(word)
|
||||
@@ -499,21 +462,15 @@ class Definition(QWidget):
|
||||
|
||||
def setWord(self, word: Any) -> None:
|
||||
self._word = word
|
||||
lines:list[Line] = word.get_def()
|
||||
lines: list[Line] = word.get_def()
|
||||
assert lines is not None
|
||||
self._lines = lines
|
||||
self._buttons: list[Fragment] = []
|
||||
base = 0
|
||||
|
||||
for line in self._lines:
|
||||
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
|
||||
|
||||
@@ -526,40 +483,58 @@ class Definition(QWidget):
|
||||
super(Definition, self).resizeEvent(event)
|
||||
return
|
||||
|
||||
_downFrag: Optional[Fragment | None] = None
|
||||
_downClickable: Optional[Clickable] = 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.clickRect()
|
||||
if rect.contains(event.pos()):
|
||||
self._downFrag = frag
|
||||
return
|
||||
position = MyPointF(event.position())
|
||||
print(f"mousePressEvent: {position}")
|
||||
for line in self._lines:
|
||||
for frag in line.getLine():
|
||||
layout = frag.layout()
|
||||
for fmtRng in layout.formats():
|
||||
if fmtRng.format.isAnchor():
|
||||
runs = layout.glyphRuns(fmtRng.start, fmtRng.length)
|
||||
assert len(runs) == 1
|
||||
bb = runs[0].boundingRect()
|
||||
bb.moveTo(bb.topLeft() + layout.position())
|
||||
if bb.contains(event.position()):
|
||||
self._downClickable = {
|
||||
'bb': bb,
|
||||
'fmt': fmtRng.format,
|
||||
'frag': frag,
|
||||
}
|
||||
return super().mousePressEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event: Optional[QMouseEvent]) -> None:
|
||||
if not event:
|
||||
return super().mouseReleaseEvent(event)
|
||||
if self._downFrag is not None and self._downFrag.clickRect().contains(
|
||||
event.pos()
|
||||
if (self._downClickable is not None and
|
||||
self._downClickable["bb"].contains(event.position())
|
||||
):
|
||||
audio = self._downFrag.audio().url()
|
||||
print(audio)
|
||||
self.pronounce.emit(audio)
|
||||
print("emit done")
|
||||
self._downFrag = None
|
||||
print(f"mousePressPseudoEvent: {event.position()}")
|
||||
clk = self._downClickable
|
||||
url = QUrl(clk['fmt'].anchorHref())
|
||||
if url.scheme() == 'audio':
|
||||
url.setScheme('https')
|
||||
self.pronounce.emit(url)
|
||||
elif url.scheme() == 'word':
|
||||
self.newWord.emit(url.path())
|
||||
elif url.scheme() == 'sense':
|
||||
self.newWord.emit(url.path())
|
||||
else:
|
||||
print(f"{clk['fmt'].anchorHref()}")
|
||||
self.alert.emit()
|
||||
self._downClickable = None
|
||||
return
|
||||
self._downFrag = None
|
||||
self._downClickable = None
|
||||
return super().mouseReleaseEvent(event)
|
||||
|
||||
def paintEvent(self, _: Optional[QPaintEvent]) -> None: # noqa
|
||||
painter = QPainter(self)
|
||||
painter.save()
|
||||
painter.setBrush(QBrush())
|
||||
painter.setPen(QColor("white"))
|
||||
|
||||
#
|
||||
# Each line needs a base calculated. To do that, we need to find the
|
||||
# bounding rectangle of the text. Once we have the bounding rectangle,
|
||||
@@ -569,11 +544,6 @@ class Definition(QWidget):
|
||||
# All text on this line needs to be on the same baseline
|
||||
#
|
||||
assert self._lines is not None
|
||||
base = 0
|
||||
for line in self._lines:
|
||||
transform = QTransform()
|
||||
transform.translate(0, base)
|
||||
painter.setTransform(transform)
|
||||
base += line.paintEvent(painter)
|
||||
painter.restore()
|
||||
line.paintEvent(painter)
|
||||
return
|
||||
|
||||
23
lib/read.py
23
lib/read.py
@@ -1,12 +1,9 @@
|
||||
import json
|
||||
from typing import Any, Dict, List, Optional, cast
|
||||
from typing import Dict, List, Optional, cast
|
||||
|
||||
import requests
|
||||
from PyQt6.QtCore import QPoint, QResource, Qt, QTimer, pyqtSignal, pyqtSlot
|
||||
from PyQt6.QtGui import (
|
||||
QBrush,
|
||||
QColor,
|
||||
QCursor,
|
||||
QKeyEvent,
|
||||
QPainter,
|
||||
QPainterPath,
|
||||
@@ -15,7 +12,7 @@ from PyQt6.QtGui import (
|
||||
QTextCursor,
|
||||
)
|
||||
from PyQt6.QtSql import QSqlQuery
|
||||
from PyQt6.QtWidgets import QDialog, QTextEdit, QWidget
|
||||
from PyQt6.QtWidgets import QDialog, QWidget
|
||||
|
||||
from lib import query_error
|
||||
from lib.preferences import Preferences
|
||||
@@ -89,6 +86,7 @@ class ReadDialog(QDialog, Ui_ReadDialog):
|
||||
self.playSound.connect(self.sound.playSound)
|
||||
self.playAlert.connect(self.sound.alert)
|
||||
self.definition.pronounce.connect(self.sound.playSound)
|
||||
self.definition.newWord.connect(self.newWord)
|
||||
return
|
||||
|
||||
#
|
||||
@@ -98,6 +96,15 @@ class ReadDialog(QDialog, Ui_ReadDialog):
|
||||
#
|
||||
# slots
|
||||
#
|
||||
@pyqtSlot(str)
|
||||
def newWord(self, word: str) -> None:
|
||||
w = Word(word)
|
||||
if not w.isValid():
|
||||
self.playAlert.emit()
|
||||
return
|
||||
self.definition.setWord(w)
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def timerAction(self) -> None:
|
||||
if self.session.isActive(): # We are stopping
|
||||
@@ -127,6 +134,9 @@ class ReadDialog(QDialog, Ui_ReadDialog):
|
||||
cursor.select(QTextCursor.SelectionType.WordUnderCursor)
|
||||
text = cursor.selectedText().strip()
|
||||
word = Word(text)
|
||||
if not word.isValid():
|
||||
self.playAlert.emit()
|
||||
return
|
||||
word.playPRS()
|
||||
return
|
||||
|
||||
@@ -221,6 +231,9 @@ class ReadDialog(QDialog, Ui_ReadDialog):
|
||||
cursor.select(cursor.SelectionType.WordUnderCursor)
|
||||
text = cursor.selectedText().strip()
|
||||
word = Word(text)
|
||||
if not word.isValid():
|
||||
self.playAlert.emit()
|
||||
return
|
||||
self.definition.setWord(word)
|
||||
self.showDefinition()
|
||||
return
|
||||
|
||||
116
lib/sounds.py
116
lib/sounds.py
@@ -22,6 +22,7 @@ from PyQt6.QtNetwork import (
|
||||
QNetworkReply,
|
||||
QNetworkRequest,
|
||||
)
|
||||
from trycast import trycast
|
||||
|
||||
# from PyQt6.QtWidgets import QWidget
|
||||
|
||||
@@ -33,39 +34,53 @@ class SoundOff(QObject):
|
||||
if cls._instance:
|
||||
return cls._instance
|
||||
cls._instance = super(SoundOff, cls).__new__(cls)
|
||||
#
|
||||
# Setup devices
|
||||
#
|
||||
cls.virtualDevice = None
|
||||
|
||||
for output in QMediaDevices.audioOutputs():
|
||||
if output.id().data().decode("utf-8") == "virt-input":
|
||||
cls.virtualDevice = output
|
||||
if output.isDefault():
|
||||
cls.localDevice = output
|
||||
cls.alertEffect = QSoundEffect()
|
||||
cls.alertEffect.setSource(QUrl("qrc:/beep.wav"))
|
||||
cls.alertEffect.setAudioDevice(cls.localDevice)
|
||||
cls.alertEffect.setVolume(0.25)
|
||||
cls.alertEffect.setLoopCount(1)
|
||||
|
||||
cls.localPlayer = QMediaPlayer()
|
||||
cls.localPlayer.setObjectName("localPlayer")
|
||||
cls.localOutput = QAudioOutput()
|
||||
cls.localOutput.setDevice(cls.localDevice)
|
||||
cls.localPlayer.setAudioOutput(cls.localOutput)
|
||||
if cls.virtualDevice:
|
||||
cls.virtualPlayer = QMediaPlayer()
|
||||
cls.virtualPlayer.setObjectName("virtualPlayer")
|
||||
cls.virtualOutput = QAudioOutput()
|
||||
cls.virtualOutput.setVolume(1.0)
|
||||
cls.virtualOutput.setDevice(cls.virtualDevice)
|
||||
cls.virtualPlayer.setAudioOutput(cls.virtualOutput)
|
||||
|
||||
cacheDir = QDir(
|
||||
QStandardPaths.writableLocation(
|
||||
QStandardPaths.StandardLocation.GenericCacheLocation
|
||||
)
|
||||
)
|
||||
cacheDir.mkdir("Troglodite")
|
||||
cacheDir = QDir(cacheDir.path() + QDir.separator() + "Troglodite")
|
||||
netCache = QNetworkDiskCache()
|
||||
netCache.setCacheDirectory(cacheDir.path())
|
||||
cls.nam = QNetworkAccessManager()
|
||||
cls.nam.setCache(netCache)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
#
|
||||
# Setup devices
|
||||
#
|
||||
self.virtualDevice = None
|
||||
if self.localPlayer.receivers(self.localPlayer.errorOccurred) > 0:
|
||||
print("SoundOff, __init__() after __init__()")
|
||||
|
||||
for output in QMediaDevices.audioOutputs():
|
||||
if output.id().data().decode("utf-8") == "virt-input":
|
||||
self.virtualDevice = output
|
||||
if output.isDefault():
|
||||
self.localDevice = output
|
||||
|
||||
self.alertEffect = QSoundEffect()
|
||||
self.alertEffect.setSource(QUrl("qrc:/beep.wav"))
|
||||
self.alertEffect.setAudioDevice(self.localDevice)
|
||||
self.alertEffect.setVolume(0.25)
|
||||
self.alertEffect.setLoopCount(1)
|
||||
|
||||
self.localPlayer = QMediaPlayer()
|
||||
self.localPlayer.setObjectName("localPlayer")
|
||||
self.localOutput = QAudioOutput()
|
||||
self.localOutput.setDevice(self.localDevice)
|
||||
self.localPlayer.setAudioOutput(self.localOutput)
|
||||
if self.virtualDevice:
|
||||
self.virtualPlayer = QMediaPlayer()
|
||||
self.virtualPlayer.setObjectName("virtualPlayer")
|
||||
self.virtualOutput = QAudioOutput()
|
||||
self.virtualOutput.setVolume(1.0)
|
||||
self.virtualOutput.setDevice(self.virtualDevice)
|
||||
self.virtualPlayer.setAudioOutput(self.virtualOutput)
|
||||
#
|
||||
# Connections
|
||||
#
|
||||
@@ -76,18 +91,6 @@ class SoundOff(QObject):
|
||||
self.virtualPlayer.errorOccurred.connect(self.mediaError)
|
||||
self.virtualPlayer.mediaStatusChanged.connect(self.mediaStatus)
|
||||
self.virtualPlayer.playbackStateChanged.connect(self.playbackState)
|
||||
cacheDir = QDir(
|
||||
QStandardPaths.writableLocation(
|
||||
QStandardPaths.StandardLocation.GenericCacheLocation
|
||||
)
|
||||
)
|
||||
cacheDir.mkdir("Troglodite")
|
||||
cacheDir = QDir(cacheDir.path() + QDir.separator() + "Troglodite")
|
||||
netCache = QNetworkDiskCache()
|
||||
netCache.setCacheDirectory(cacheDir.path())
|
||||
self.nam = QNetworkAccessManager()
|
||||
self.nam.setCache(netCache)
|
||||
self.nam.finished.connect(self.finished)
|
||||
return
|
||||
|
||||
@pyqtSlot(QMediaPlayer.Error, str)
|
||||
@@ -98,11 +101,13 @@ class SoundOff(QObject):
|
||||
|
||||
@pyqtSlot(QMediaPlayer.MediaStatus)
|
||||
def mediaStatus(self, status: QMediaPlayer.MediaStatus) -> None:
|
||||
# print(f"mediaStatus: {status}")
|
||||
if status == QMediaPlayer.MediaStatus.LoadedMedia:
|
||||
player: Optional[QMediaPlayer] = cast(QMediaPlayer, self.sender())
|
||||
player = trycast(QMediaPlayer, self.sender())
|
||||
if player is None:
|
||||
player = trycast(QMediaPlayer, self.lastSender)
|
||||
assert player is not None
|
||||
player.play()
|
||||
self.lastSender = self.sender()
|
||||
return
|
||||
|
||||
@pyqtSlot(QMediaPlayer.PlaybackState)
|
||||
@@ -119,6 +124,7 @@ class SoundOff(QObject):
|
||||
return
|
||||
|
||||
@pyqtSlot(str)
|
||||
@pyqtSlot(QUrl)
|
||||
def playSound(self, url: str | QUrl) -> None:
|
||||
if isinstance(url, str):
|
||||
url = QUrl(url)
|
||||
@@ -128,8 +134,10 @@ class SoundOff(QObject):
|
||||
self.virtualPlayer.setAudioOutput(self.virtualOutput)
|
||||
if url != self._lastUrl:
|
||||
request = QNetworkRequest(url)
|
||||
self.nam.get(request)
|
||||
reply = self.nam.get(request)
|
||||
assert reply is not None
|
||||
self._lastUrl = url
|
||||
reply.finished.connect(self.finished)
|
||||
return
|
||||
for player in [self.localPlayer, self.virtualPlayer]:
|
||||
if not player:
|
||||
@@ -151,20 +159,22 @@ class SoundOff(QObject):
|
||||
_buffer: dict[QMediaPlayer, QBuffer] = {}
|
||||
_lastUrl = QUrl()
|
||||
|
||||
@pyqtSlot(QNetworkReply)
|
||||
def finished(self, reply: QNetworkReply) -> None:
|
||||
storage = reply.readAll()
|
||||
|
||||
crypto = QCryptographicHash(QCryptographicHash.Algorithm.Sha256)
|
||||
@pyqtSlot()
|
||||
def finished(self) -> None:
|
||||
reply = trycast(QNetworkReply, self.sender())
|
||||
assert reply is not None
|
||||
code = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute)
|
||||
print(f"HttpStatusCodeAttribute: {code}, error: {reply.error()}")
|
||||
self._reply = reply.readAll()
|
||||
url = reply.request().url()
|
||||
reply.close()
|
||||
if self._reply.isEmpty() or self._reply.isNull():
|
||||
return
|
||||
for player in [self.localPlayer, self.virtualPlayer]:
|
||||
if not player:
|
||||
continue
|
||||
self._storage[player] = QByteArray(storage)
|
||||
crypto.addData(self._storage[player])
|
||||
print(player, crypto.result().toHex())
|
||||
crypto.reset()
|
||||
self._storage[player] = QByteArray(self._reply)
|
||||
self._buffer[player] = QBuffer(self._storage[player])
|
||||
url = reply.request().url()
|
||||
player.setSourceDevice(self._buffer[player], url)
|
||||
player.setPosition(0)
|
||||
if player.mediaStatus() == QMediaPlayer.MediaStatus.LoadedMedia:
|
||||
|
||||
61
lib/utils.py
61
lib/utils.py
@@ -2,7 +2,7 @@
|
||||
from typing import NoReturn, Self
|
||||
|
||||
from PyQt6.QtCore import QCoreApplication, QDir, QStandardPaths, Qt
|
||||
from PyQt6.QtGui import QColor, QFont, QFontDatabase
|
||||
from PyQt6.QtGui import QColor, QFont, QFontDatabase, QTextCharFormat
|
||||
from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkDiskCache
|
||||
from PyQt6.QtSql import QSqlQuery
|
||||
|
||||
@@ -22,6 +22,7 @@ def query_error(query: QSqlQuery) -> NoReturn:
|
||||
)
|
||||
raise Exception(translate("MainWindow", "SQL Error"))
|
||||
|
||||
|
||||
class Resources:
|
||||
_instance = None
|
||||
nam = QNetworkAccessManager()
|
||||
@@ -38,11 +39,60 @@ class Resources:
|
||||
linkColor: QColor
|
||||
subduedColor: QColor
|
||||
|
||||
subduedBackground: QColor
|
||||
|
||||
headerFormat = QTextCharFormat()
|
||||
labelFormat = QTextCharFormat()
|
||||
subduedFormat = QTextCharFormat()
|
||||
subduedItalicFormat = QTextCharFormat()
|
||||
sOnSFormat = QTextCharFormat()
|
||||
subduedLabelFormat = QTextCharFormat()
|
||||
phonticFormat = QTextCharFormat()
|
||||
boldFormat = QTextCharFormat()
|
||||
boldOnSFormat = QTextCharFormat()
|
||||
italicFormat = QTextCharFormat()
|
||||
textFormat = QTextCharFormat()
|
||||
smallCapsFormat = QTextCharFormat()
|
||||
|
||||
def __new__(cls: type[Self]) -> Self:
|
||||
if cls._instance:
|
||||
return cls._instance
|
||||
cls._instance = super(Resources, cls).__new__(cls)
|
||||
#
|
||||
# colors
|
||||
#
|
||||
cls.baseColor = QColor(Qt.GlobalColor.white)
|
||||
cls.linkColor = QColor("#4a7d95")
|
||||
cls.subduedColor = QColor(Qt.GlobalColor.gray)
|
||||
cls.subduedBackground = QColor("#444")
|
||||
#
|
||||
# Formats
|
||||
#
|
||||
LARGE = 36
|
||||
MEDIUM = 22
|
||||
SMALL = 18
|
||||
cls.headerFormat.setFontPointSize(LARGE)
|
||||
cls.labelFormat.setFontPointSize(MEDIUM)
|
||||
cls.sOnSFormat.setForeground(cls.subduedColor)
|
||||
#cls.sOnSFormat.setBackground(cls.subduedBackground)
|
||||
cls.sOnSFormat.setFontPointSize(SMALL)
|
||||
cls.subduedFormat.setForeground(cls.subduedColor)
|
||||
cls.subduedFormat.setFontPointSize(SMALL)
|
||||
cls.subduedLabelFormat.setForeground(cls.subduedColor)
|
||||
cls.subduedLabelFormat.setFontPointSize(SMALL)
|
||||
cls.phonticFormat.setFont(QFontDatabase.font("Gentium", None,20))
|
||||
cls.phonticFormat.setFontPointSize(SMALL)
|
||||
cls.boldFormat.setFontWeight(QFont.Weight.Bold)
|
||||
cls.boldFormat.setFontPointSize(SMALL)
|
||||
cls.boldOnSFormat.setFontWeight(QFont.Weight.Bold)
|
||||
cls.boldOnSFormat.setFontPointSize(SMALL)
|
||||
cls.boldOnSFormat.setBackground(cls.subduedBackground)
|
||||
cls.italicFormat.setFontItalic(True)
|
||||
cls.italicFormat.setFontPointSize(SMALL)
|
||||
cls.textFormat.setFontPointSize(SMALL)
|
||||
cls.smallCapsFormat.setFontPointSize(SMALL)
|
||||
cls.smallCapsFormat.setFontCapitalization(QFont.Capitalization.SmallCaps)
|
||||
#
|
||||
# Fonts
|
||||
#
|
||||
cls.headerFont = QFontDatabase.font("OpenDyslexic", None, 10)
|
||||
@@ -59,21 +109,12 @@ class Resources:
|
||||
cls.headerFont.setWeight(QFont.Weight.Bold)
|
||||
cls.boldFont.setBold(True)
|
||||
cls.italicFont.setItalic(True)
|
||||
print(f"Resources().italicFont: {cls.italicFont.toString()}")
|
||||
print(f"Resources().boldFont: {cls.boldFont.toString()}")
|
||||
cls.capsFont.setCapitalization(QFont.Capitalization.AllUppercase)
|
||||
cls.smallCapsFont.setCapitalization(QFont.Capitalization.SmallCaps)
|
||||
|
||||
cls.phonicFont = QFontDatabase.font("Gentium", None, 10)
|
||||
cls.phonicFont.setPixelSize(20)
|
||||
|
||||
#
|
||||
# colors
|
||||
#
|
||||
cls.baseColor = QColor(Qt.GlobalColor.white)
|
||||
cls.linkColor = QColor("#4a7d95")
|
||||
cls.subduedColor = QColor(Qt.GlobalColor.gray)
|
||||
|
||||
#
|
||||
# Setup the Network Manager
|
||||
#
|
||||
|
||||
73
lib/words.py
73
lib/words.py
@@ -1,40 +1,47 @@
|
||||
import importlib
|
||||
import pkgutil
|
||||
import json
|
||||
from typing import Any, TypedDict, cast
|
||||
import pkgutil
|
||||
from types import ModuleType
|
||||
from typing import Any, Iterable, TypedDict, cast
|
||||
|
||||
from PyQt6.QtCore import (
|
||||
Qt,
|
||||
pyqtSlot,
|
||||
)
|
||||
from PyQt6.QtCore import Qt, pyqtSlot
|
||||
from PyQt6.QtSql import QSqlQuery
|
||||
from PyQt6.QtWidgets import QScrollArea
|
||||
|
||||
from lib.utils import query_error
|
||||
from lib.sounds import SoundOff
|
||||
from lib.definition import Definition, Line
|
||||
from trycast import trycast
|
||||
|
||||
import plugins
|
||||
def find_plugins(ns_pkg):
|
||||
return pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + '.')
|
||||
from lib.definition import Definition, Line
|
||||
from lib.sounds import SoundOff
|
||||
from lib.utils import query_error
|
||||
|
||||
|
||||
def find_plugins(ns_pkg: ModuleType) -> Iterable[pkgutil.ModuleInfo]:
|
||||
return pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + ".")
|
||||
|
||||
|
||||
discovered_plugins = {
|
||||
# finder, name, ispkg
|
||||
importlib.import_module(name).registration['source']: importlib.import_module(name) for _, name, _ in find_plugins(plugins)
|
||||
importlib.import_module(name).registration[
|
||||
"source"
|
||||
]: importlib.import_module(name)
|
||||
for _, name, _ in find_plugins(plugins)
|
||||
}
|
||||
|
||||
API = "https://api.dictionaryapi.dev/api/v2/entries/en/{word}"
|
||||
|
||||
|
||||
class WordType(TypedDict):
|
||||
word: str
|
||||
source: str
|
||||
definition: str
|
||||
|
||||
|
||||
|
||||
class Word:
|
||||
"""All processing of a dictionary word."""
|
||||
|
||||
_words: dict[str, WordType] = {}
|
||||
|
||||
_valid = False
|
||||
|
||||
def __init__(self, word: str) -> None:
|
||||
#
|
||||
# Have we already retrieved this word?
|
||||
@@ -56,14 +63,18 @@ class Word:
|
||||
"definition": json.loads(query.value("definition")),
|
||||
}
|
||||
self.current = Word._words[word]
|
||||
self._valid = True
|
||||
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"
|
||||
|
||||
|
||||
self._words[word] = discovered_plugins[source].fetch(word)
|
||||
if self._words[word] is None:
|
||||
self._valid = False
|
||||
return
|
||||
self.current = Word._words[word]
|
||||
query.prepare(
|
||||
"INSERT INTO words "
|
||||
@@ -75,39 +86,41 @@ class Word:
|
||||
query.bindValue(":definition", json.dumps(self.current["definition"]))
|
||||
if not query.exec():
|
||||
query_error(query)
|
||||
self._valid = True
|
||||
return
|
||||
|
||||
def isValid(self) -> bool:
|
||||
return self._valid
|
||||
|
||||
@pyqtSlot()
|
||||
def playSound(self) -> None:
|
||||
url = discovered_plugins[self.current['source']].getFirstSound(self.current['definition'])
|
||||
url = discovered_plugins[self.current["source"]].getFirstSound(
|
||||
self.current["definition"]
|
||||
)
|
||||
if url.isValid():
|
||||
snd = SoundOff()
|
||||
snd.playSound(url)
|
||||
return
|
||||
|
||||
def playPRS(self) -> None:
|
||||
return
|
||||
|
||||
def getWord(self) -> str:
|
||||
return cast(str, self.current["word"])
|
||||
return self.current["word"]
|
||||
|
||||
def get_html(self) -> str | None:
|
||||
src = self.current['source']
|
||||
src = self.current["source"]
|
||||
try:
|
||||
return discovered_plugins[src].getHtml(self.current)
|
||||
return cast(str, discovered_plugins[src].getHtml(self.current))
|
||||
except KeyError:
|
||||
raise Exception(f"Unknown source: {src}")
|
||||
|
||||
def get_def(self) -> list[Line]:
|
||||
src = self.current['source']
|
||||
src = self.current["source"]
|
||||
try:
|
||||
lines = discovered_plugins[src].getDef(self.current["definition"])
|
||||
lines = trycast(list[Line], lines)
|
||||
assert lines is not None
|
||||
return lines
|
||||
except KeyError:
|
||||
raise Exception(f"Unknown source: {self.current['source']}")
|
||||
|
||||
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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
94
requirements.venv
Normal file
94
requirements.venv
Normal file
@@ -0,0 +1,94 @@
|
||||
altgraph==0.17.4
|
||||
asgiref==3.7.2
|
||||
astor==0.8.1
|
||||
astroid==3.0.3
|
||||
asttokens==2.4.1
|
||||
autopep8==2.0.4
|
||||
beautifulsoup4==4.12.3
|
||||
black==23.12.1
|
||||
bs4==0.0.2
|
||||
certifi==2023.11.17
|
||||
charlockholmes==0.0.3
|
||||
charset-normalizer==3.3.2
|
||||
click==8.1.7
|
||||
coloredlogs==15.0.1
|
||||
css-inline==0.13.0
|
||||
debugpy==1.8.1
|
||||
decorator==5.1.1
|
||||
dill==0.3.8
|
||||
docstring-to-markdown==0.13
|
||||
executing==2.0.1
|
||||
flake8==7.0.0
|
||||
flynt==1.0.1
|
||||
future==0.18.3
|
||||
futures==3.0.5
|
||||
goslate==1.5.4
|
||||
hiredis==2.3.2
|
||||
humanfriendly==10.0
|
||||
idna==3.6
|
||||
importlib-metadata==7.0.1
|
||||
ipython==8.20.0
|
||||
isort==5.13.2
|
||||
jedi==0.19.1
|
||||
matplotlib-inline==0.1.6
|
||||
mccabe==0.7.0
|
||||
mime==0.1.0
|
||||
mypy==1.8.0
|
||||
mypy-extensions==1.0.0
|
||||
packaging==23.2
|
||||
param==2.1.0
|
||||
parso==0.8.3
|
||||
pathspec==0.12.1
|
||||
pexpect==4.9.0
|
||||
pickleshare==0.7.5
|
||||
pillow==10.2.0
|
||||
platformdirs==4.1.0
|
||||
pluggy==1.4.0
|
||||
prompt-toolkit==3.0.43
|
||||
ptyprocess==0.7.0
|
||||
pure-eval==0.2.2
|
||||
pycodestyle==2.11.1
|
||||
PyDictionary==2.0.1
|
||||
pydocstyle==6.3.0
|
||||
pyflakes==3.2.0
|
||||
pygame==2.5.2
|
||||
Pygments==2.17.2
|
||||
pygments-github-lexers==0.0.5
|
||||
pyinstaller==6.5.0
|
||||
pyinstaller-hooks-contrib==2024.3
|
||||
pylint==3.0.3
|
||||
PyQt6==6.6.1
|
||||
PyQt6-Qt6==6.6.1
|
||||
PyQt6-sip==13.6.0
|
||||
PySide6==6.6.1
|
||||
PySide6-Addons==6.6.1
|
||||
PySide6-Essentials==6.6.1
|
||||
python-lsp-jsonrpc==1.1.2
|
||||
python-lsp-server==1.10.0
|
||||
pytoolconfig==1.3.1
|
||||
PyYAML==6.0.1
|
||||
qt6-applications==6.5.0.2.3
|
||||
requests==2.31.0
|
||||
rope==1.12.0
|
||||
scanner==0.1.0
|
||||
setuptools==69.0.3
|
||||
shiboken6==6.6.1
|
||||
six==1.16.0
|
||||
snowballstemmer==2.2.0
|
||||
soupsieve==2.5
|
||||
sqlparse==0.4.4
|
||||
stack-data==0.6.3
|
||||
tabulate==0.9.0
|
||||
tomli==2.0.1
|
||||
tomlkit==0.12.3
|
||||
traitlets==5.14.1
|
||||
trycast==1.1.0
|
||||
types-requests==2.31.0.20240106
|
||||
typing_extensions==4.9.0
|
||||
ujson==5.9.0
|
||||
unicorn==2.0.1.post1
|
||||
urllib3==2.1.0
|
||||
wcwidth==0.2.13
|
||||
whatthepatch==1.0.5
|
||||
yapf==0.40.2
|
||||
zipp==3.17.0
|
||||
Reference in New Issue
Block a user