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
|
#!/bin/bash
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
set -e
|
set -e
|
||||||
isort --profile black *.py lib/*.py
|
isort --profile black *.py lib/*.py plugins/*.py
|
||||||
black -l 80 *.py lib/*.py
|
black -l 80 *.py lib/*.py plugins/*.py
|
||||||
flynt *.py lib/*.py
|
flynt *.py lib/*.py plugins/*.py
|
||||||
mypy esl_reader.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 os
|
||||||
import signal
|
import signal
|
||||||
import sys
|
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.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, QScrollArea
|
||||||
|
|
||||||
from lib import DefinitionArea, Word
|
from lib import Word
|
||||||
from lib.sounds import SoundOff
|
from lib.sounds import SoundOff
|
||||||
from lib.utils import query_error
|
from lib.utils import query_error
|
||||||
from lib.words import Definition
|
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):
|
@pyqtSlot(str)
|
||||||
settings = QSettings("Troglodite", "esl_reader")
|
def newWord(self, word:str) -> None:
|
||||||
settings.setValue("geometry", self.saveGeometry())
|
print(f"newWord: {word}")
|
||||||
super(DefinitionArea, self).closeEvent(event)
|
w = Word(word)
|
||||||
return
|
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:
|
def main() -> int:
|
||||||
@@ -66,12 +82,15 @@ def main() -> int:
|
|||||||
):
|
):
|
||||||
query_error(query)
|
query_error(query)
|
||||||
|
|
||||||
word = Word("cowbell")
|
word = Word("lower")
|
||||||
snd = SoundOff()
|
snd = SoundOff()
|
||||||
DefinitionArea.closeEvent = monkeyClose
|
print("Pre widget")
|
||||||
widget = DefinitionArea(word) # xnoqa: F841
|
widget = DefinitionArea(word) # xnoqa: F841
|
||||||
|
print("post widget")
|
||||||
settings = QSettings("Troglodite", "esl_reader")
|
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())
|
d = cast(Definition, widget.widget())
|
||||||
assert d is not None
|
assert d is not None
|
||||||
d.pronounce.connect(snd.playSound)
|
d.pronounce.connect(snd.playSound)
|
||||||
@@ -81,4 +100,7 @@ def main() -> int:
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
faulthandler.register(signal.Signals.SIGUSR1)
|
faulthandler.register(signal.Signals.SIGUSR1)
|
||||||
|
faulthandler.register(signal.Signals.SIGTERM)
|
||||||
|
faulthandler.register(signal.Signals.SIGHUP)
|
||||||
|
faulthandler.enable()
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# pyright: ignore
|
# pyright: ignore
|
||||||
from .utils import query_error # isort: skip
|
from .utils import query_error # isort: skip
|
||||||
from .books import Book
|
from .books import Book
|
||||||
|
from .definition import Definition, Fragment, Line
|
||||||
from .person import PersonDialog
|
from .person import PersonDialog
|
||||||
from .read import ReadDialog
|
from .read import ReadDialog
|
||||||
from .session import SessionDialog
|
from .session import SessionDialog
|
||||||
from .words import DefinitionArea, Word
|
from .words import Word
|
||||||
from .definition import Fragment, Line, Definition
|
|
||||||
|
|||||||
@@ -1,39 +1,64 @@
|
|||||||
import re
|
import unicodedata
|
||||||
from typing import Any, Optional, Self, cast, overload
|
from typing import Any, Callable, Optional, Self, TypedDict, cast
|
||||||
import re
|
|
||||||
from PyQt6.QtCore import QMargins, QPoint, QRect, QSize, QUrl, Qt, pyqtSignal
|
from PyQt6.QtCore import QMargins, QPoint, QPointF, QRect, QRectF, QSize, Qt, QUrl, pyqtSignal
|
||||||
from PyQt6.QtGui import QColor, QFont, QFontMetrics, QMouseEvent, QPaintEvent, QPainter, QResizeEvent, QTextOption, QTransform, QBrush
|
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
|
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:
|
class Fragment:
|
||||||
"""A fragment of text to be displayed"""
|
"""A fragment of text to be displayed"""
|
||||||
|
|
||||||
_indentAmount = 35
|
_indentAmount = 35
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
which: str|Self,
|
which: str | Self | None = None,
|
||||||
font: QFont|None = None,
|
font: QFont | None = None,
|
||||||
audio: str = "",
|
audio: str = "",
|
||||||
color: Optional[QColor] = None,
|
color: Optional[QColor] = None,
|
||||||
asis: bool = False,
|
asis: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
if isinstance(which, Fragment):
|
if isinstance(which, Fragment):
|
||||||
for k,v in which.__dict__.items():
|
for k, v in which.__dict__.items():
|
||||||
self.__dict__[k] = v
|
self.__dict__[k] = v
|
||||||
return
|
return
|
||||||
self._text:str = which
|
self._layout = QTextLayout()
|
||||||
if font is None:
|
if font is None:
|
||||||
raise TypeError("Missing required parameter 'font'")
|
self._layout.setFont(
|
||||||
self._font = font
|
QFontDatabase.font("OpenDyslexic", None, 20)
|
||||||
self._audio: QUrl = QUrl(audio)
|
)
|
||||||
self._align = QTextOption(
|
else:
|
||||||
|
self._layout.setFont(font)
|
||||||
|
align = QTextOption(
|
||||||
Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignBaseline
|
Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignBaseline
|
||||||
)
|
)
|
||||||
|
self._layout.setTextOption(align)
|
||||||
|
self._audio: QUrl = QUrl(audio)
|
||||||
self._padding = QMargins()
|
self._padding = QMargins()
|
||||||
self._border = QMargins()
|
self._border = QMargins()
|
||||||
self._margin = QMargins()
|
self._margin = QMargins()
|
||||||
self._wref = ""
|
self._wref = ""
|
||||||
self._position = QPoint()
|
|
||||||
self._rect = QRect()
|
self._rect = QRect()
|
||||||
self._borderRect = QRect()
|
self._borderRect = QRect()
|
||||||
self._clickRect = QRect()
|
self._clickRect = QRect()
|
||||||
@@ -44,147 +69,114 @@ class Fragment:
|
|||||||
self._background = QColor()
|
self._background = QColor()
|
||||||
self._asis = asis
|
self._asis = asis
|
||||||
self._indent = 0
|
self._indent = 0
|
||||||
self._target = "word"
|
if which is not None:
|
||||||
|
self.setText(which)
|
||||||
return
|
return
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def size(self) -> QSize:
|
||||||
return self.__repr__()
|
return self.paintEvent()
|
||||||
|
|
||||||
def size(self, width: int) -> QSize:
|
def height(self) -> int:
|
||||||
return self.paintEvent(width)
|
return self.size().height()
|
||||||
|
|
||||||
def height(self, width: int) -> int:
|
def width(self) -> int:
|
||||||
return self.size(width).height()
|
return self.size().width()
|
||||||
|
|
||||||
def width(self, width: int) -> int:
|
|
||||||
return self.size(width).width()
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
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 doLayout(self, width: int) -> QPointF:
|
||||||
def paintEvent(self, widthSrc:int) -> QSize:
|
leading = QFontMetrics(self._layout.font()).leading()
|
||||||
...
|
eol = self._layout.position()
|
||||||
@overload
|
base = 0
|
||||||
def paintEvent(self, widthSrc: QPainter) -> int:
|
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:
|
def paintEvent(self, painter: Optional[QPainter] | None = None) -> QSize:
|
||||||
if isinstance(widthSrc, QPainter):
|
rect = self._layout.boundingRect()
|
||||||
viewportWidth = widthSrc.viewport().width()
|
size = rect.size()
|
||||||
painter = widthSrc
|
assert size is not None
|
||||||
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)
|
|
||||||
if painter is None:
|
if painter is None:
|
||||||
return size
|
return QSize(int(size.width()), int(size.height()))
|
||||||
painter.save()
|
painter.save()
|
||||||
painter.setFont(self._font)
|
self._layout.draw(painter, QPointF(0,0))
|
||||||
painter.setPen(QColor("#f00"))
|
#
|
||||||
if self._audio.isValid():
|
# TODO: draw the rounded rect around audio buttons
|
||||||
radius = self._borderRect.height() / 2
|
#
|
||||||
painter.drawRoundedRect(self._borderRect, radius, radius)
|
painter.brush().setColor(Qt.GlobalColor.green)
|
||||||
if self._wref:
|
for fmt in self._layout.formats():
|
||||||
start = bounding.bottomLeft()
|
if fmt.format.isAnchor():
|
||||||
end = bounding.bottomRight()
|
runs = self._layout.glyphRuns(fmt.start, fmt.length)
|
||||||
painter.drawLine(start, end)
|
bb = runs[0].boundingRect()
|
||||||
|
bb.moveTo(bb.topLeft() + self._layout.position())
|
||||||
painter.setPen(self._color)
|
url = QUrl(fmt.format.anchorHref())
|
||||||
if self._background.isValid():
|
if url.scheme() == 'audio':
|
||||||
brush = painter.brush()
|
painter.setPen(QColor('red'))
|
||||||
brush.setColor(self._background)
|
radius = (bb.topLeft() - bb.bottomLeft()).manhattanLength()/4
|
||||||
brush.setStyle(Qt.BrushStyle.SolidPattern)
|
painter.drawRoundedRect(bb, radius,radius)
|
||||||
painter.setBrush(brush)
|
else:
|
||||||
painter.fillRect(rect,brush)
|
painter.setPen(QColor("blue"))
|
||||||
painter.drawText(rect, flags, text)
|
#painter.drawRect(bb)
|
||||||
if remainingText:
|
|
||||||
if self._background.isValid():
|
|
||||||
painter.fillRect(remainingRect, brush)
|
|
||||||
painter.drawText(remainingRect, flags|Qt.TextFlag.TextWordWrap, remainingText)
|
|
||||||
painter.restore()
|
painter.restore()
|
||||||
return size.height()
|
return QSize(int(size.width()), int(size.height()))
|
||||||
|
|
||||||
#
|
#
|
||||||
# Setters
|
# Setters
|
||||||
#
|
#
|
||||||
def setText(self, text: str) -> None:
|
def addText(self, text: str, fmt: Optional[QTextCharFormat] = None) -> None:
|
||||||
self._text = text
|
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
|
return
|
||||||
|
|
||||||
def setTarget(self, target: str) -> None:
|
def setText(self, text: str) -> None:
|
||||||
self._target = target
|
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
|
return
|
||||||
|
|
||||||
def setFont(self, font: QFont) -> None:
|
def setFont(self, font: QFont) -> None:
|
||||||
self._font = font
|
self._layout.setFont(font)
|
||||||
return
|
return
|
||||||
|
|
||||||
def setAudio(self, audio: str | QUrl) -> None:
|
def setAudio(self, audio: str | QUrl) -> None:
|
||||||
@@ -195,7 +187,7 @@ class Fragment:
|
|||||||
return
|
return
|
||||||
|
|
||||||
def setAlign(self, align: QTextOption) -> None:
|
def setAlign(self, align: QTextOption) -> None:
|
||||||
self._align = align
|
self._layout.setTextOption(align)
|
||||||
return
|
return
|
||||||
|
|
||||||
def setRect(self, rect: QRect) -> None:
|
def setRect(self, rect: QRect) -> None:
|
||||||
@@ -292,7 +284,7 @@ class Fragment:
|
|||||||
return
|
return
|
||||||
|
|
||||||
def setPosition(self, pnt: QPoint) -> None:
|
def setPosition(self, pnt: QPoint) -> None:
|
||||||
self._position = pnt
|
self._layout.setPosition(QPointF(pnt.x(), pnt.y()))
|
||||||
return
|
return
|
||||||
|
|
||||||
def setBorderRect(self, rect: QRect) -> None:
|
def setBorderRect(self, rect: QRect) -> None:
|
||||||
@@ -310,6 +302,7 @@ class Fragment:
|
|||||||
def setBackground(self, color: QColor) -> None:
|
def setBackground(self, color: QColor) -> None:
|
||||||
self._background = color
|
self._background = color
|
||||||
return
|
return
|
||||||
|
|
||||||
def setIndent(self, indent: int) -> None:
|
def setIndent(self, indent: int) -> None:
|
||||||
self._indent = indent
|
self._indent = indent
|
||||||
return
|
return
|
||||||
@@ -317,20 +310,23 @@ class Fragment:
|
|||||||
#
|
#
|
||||||
# Getters
|
# Getters
|
||||||
#
|
#
|
||||||
|
def background(self) -> QColor:
|
||||||
|
return self._background
|
||||||
|
|
||||||
def wRef(self) -> str:
|
def wRef(self) -> str:
|
||||||
return self._wref
|
return self._wref
|
||||||
|
|
||||||
def text(self) -> str:
|
def text(self) -> str:
|
||||||
return self._text
|
return self._layout.text()
|
||||||
|
|
||||||
def font(self) -> QFont:
|
def font(self) -> QFont:
|
||||||
return self._font
|
return self._layout.font()
|
||||||
|
|
||||||
def audio(self) -> QUrl:
|
def audio(self) -> QUrl:
|
||||||
return self._audio
|
return self._audio
|
||||||
|
|
||||||
def align(self) -> QTextOption:
|
def align(self) -> QTextOption:
|
||||||
return self._align
|
return self._layout.textOption()
|
||||||
|
|
||||||
def rect(self) -> QRect:
|
def rect(self) -> QRect:
|
||||||
return self._rect
|
return self._rect
|
||||||
@@ -344,8 +340,8 @@ class Fragment:
|
|||||||
def margin(self) -> QMargins:
|
def margin(self) -> QMargins:
|
||||||
return self._margin
|
return self._margin
|
||||||
|
|
||||||
def position(self) -> QPoint:
|
def position(self) -> QPointF:
|
||||||
return self._position
|
return self._layout.position()
|
||||||
|
|
||||||
def borderRect(self) -> QRect:
|
def borderRect(self) -> QRect:
|
||||||
return self._borderRect
|
return self._borderRect
|
||||||
@@ -365,8 +361,12 @@ class Fragment:
|
|||||||
def pixelIndent(self) -> int:
|
def pixelIndent(self) -> int:
|
||||||
return self._indent * self._indentAmount
|
return self._indent * self._indentAmount
|
||||||
|
|
||||||
|
def layout(self) -> QTextLayout:
|
||||||
|
return self._layout
|
||||||
|
|
||||||
class Line:
|
class Line:
|
||||||
parseText = None
|
parseText = None
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._maxHeight = -1
|
self._maxHeight = -1
|
||||||
self._baseLine = -1
|
self._baseLine = -1
|
||||||
@@ -379,104 +379,58 @@ class Line:
|
|||||||
"|".join([x.text() for x in self._fragments])
|
"|".join([x.text() for x in self._fragments])
|
||||||
+ f"|{self._maxHeight}"
|
+ f"|{self._maxHeight}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setParseText(cls, call) -> None:
|
def setParseText(cls, call: Callable) -> None:
|
||||||
cls.parseText = call
|
cls.parseText = call
|
||||||
return
|
return
|
||||||
|
|
||||||
def paintEvent(self, painter: QPainter) -> int:
|
def paintEvent(self, painter: QPainter) -> int:
|
||||||
#
|
#
|
||||||
# we do not have an event field because we are not a true widget
|
# 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:
|
for frag in self._fragments:
|
||||||
ls = frag.paintEvent(painter)
|
pos = frag.paintEvent(painter)
|
||||||
if ls > lineSpacing:
|
return pos.height()
|
||||||
lineSpacing = ls
|
|
||||||
return lineSpacing
|
|
||||||
|
|
||||||
|
def addFragment(
|
||||||
def addFragment(self, frags: Fragment|list[Fragment],) -> None:
|
self,
|
||||||
SPEAKER = "\U0001F508"
|
frags: Fragment | list[Fragment],
|
||||||
|
) -> None:
|
||||||
|
#SPEAKER = "\U0001F508"
|
||||||
|
|
||||||
if not isinstance(frags, list):
|
if not isinstance(frags, list):
|
||||||
frags = [frags, ]
|
frags = [
|
||||||
for frag in frags:
|
frags,
|
||||||
if frag.audio().isValid():
|
]
|
||||||
frag.setText(frag.text() + " " + SPEAKER)
|
self._fragments += frags
|
||||||
|
|
||||||
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)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def finalizeLine(self, width: int, base: int) -> 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
|
# 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
|
left = 0 # Left size of rect
|
||||||
leading = -1
|
maxHeight = 0
|
||||||
|
|
||||||
for frag in self._fragments:
|
for frag in self._fragments:
|
||||||
fm = QFontMetrics(frag.font())
|
if left < frag.pixelIndent():
|
||||||
height = frag.height(width)
|
left = frag.pixelIndent()
|
||||||
bl = fm.height() - fm.descent()
|
frag.setPosition(QPoint(left, base))
|
||||||
if fm.leading() > leading:
|
eol =frag.doLayout(width)
|
||||||
leading = fm.leading()
|
left = int(eol.x()+0.5)
|
||||||
if height > maxHeight:
|
if frag.layout().lineCount() > 1:
|
||||||
maxHeight = height
|
base = int(eol.y()+0.5)
|
||||||
if bl > baseLine:
|
if eol.y() > maxHeight:
|
||||||
baseLine = bl
|
maxHeight = eol.y()
|
||||||
self._baseLine = baseLine
|
self._maxHeight = int(maxHeight+0.5)
|
||||||
self._maxHeight = maxHeight
|
self._leading = 0
|
||||||
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()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def getLine(self) -> list[Fragment]:
|
def getLine(self) -> list[Fragment]:
|
||||||
@@ -485,13 +439,22 @@ class Line:
|
|||||||
def getLineSpacing(self) -> int:
|
def getLineSpacing(self) -> int:
|
||||||
return self._leading + self._maxHeight
|
return self._leading + self._maxHeight
|
||||||
|
|
||||||
class Definition(QWidget):
|
class Clickable(TypedDict):
|
||||||
pronounce = pyqtSignal(str)
|
bb: QRectF
|
||||||
|
frag: Fragment
|
||||||
|
fmt: QTextCharFormat
|
||||||
|
|
||||||
|
class Definition(QWidget):
|
||||||
|
pronounce = pyqtSignal(QUrl)
|
||||||
|
alert = pyqtSignal()
|
||||||
|
newWord = pyqtSignal(str)
|
||||||
def __init__(
|
def __init__(
|
||||||
self, word: Optional[Any] = None, *args: Any, **kwargs: Any
|
self, word: Optional[Any] = None, *args: Any, **kwargs: Any
|
||||||
) -> None:
|
) -> None:
|
||||||
super(Definition, self).__init__(*args, **kwargs)
|
super(Definition, self).__init__(*args, **kwargs)
|
||||||
|
self._sound = SoundOff()
|
||||||
|
self.pronounce.connect(self._sound.playSound)
|
||||||
|
self.alert.connect(self._sound.alert)
|
||||||
self._word = word
|
self._word = word
|
||||||
if word is not None:
|
if word is not None:
|
||||||
self.setWord(word)
|
self.setWord(word)
|
||||||
@@ -499,21 +462,15 @@ class Definition(QWidget):
|
|||||||
|
|
||||||
def setWord(self, word: Any) -> None:
|
def setWord(self, word: Any) -> None:
|
||||||
self._word = word
|
self._word = word
|
||||||
lines:list[Line] = word.get_def()
|
lines: list[Line] = word.get_def()
|
||||||
assert lines is not None
|
assert lines is not None
|
||||||
self._lines = lines
|
self._lines = lines
|
||||||
self._buttons: list[Fragment] = []
|
|
||||||
base = 0
|
base = 0
|
||||||
|
|
||||||
for line in self._lines:
|
for line in self._lines:
|
||||||
line.finalizeLine(self.width(), base)
|
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()
|
base += line.getLineSpacing()
|
||||||
|
|
||||||
self.setFixedHeight(base)
|
self.setFixedHeight(base)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -526,40 +483,58 @@ class Definition(QWidget):
|
|||||||
super(Definition, self).resizeEvent(event)
|
super(Definition, self).resizeEvent(event)
|
||||||
return
|
return
|
||||||
|
|
||||||
_downFrag: Optional[Fragment | None] = None
|
_downClickable: Optional[Clickable] = 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()}")
|
position = MyPointF(event.position())
|
||||||
for frag in self._buttons:
|
print(f"mousePressEvent: {position}")
|
||||||
rect = frag.clickRect()
|
for line in self._lines:
|
||||||
if rect.contains(event.pos()):
|
for frag in line.getLine():
|
||||||
self._downFrag = frag
|
layout = frag.layout()
|
||||||
return
|
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)
|
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._downFrag is not None and self._downFrag.clickRect().contains(
|
if (self._downClickable is not None and
|
||||||
event.pos()
|
self._downClickable["bb"].contains(event.position())
|
||||||
):
|
):
|
||||||
audio = self._downFrag.audio().url()
|
print(f"mousePressPseudoEvent: {event.position()}")
|
||||||
print(audio)
|
clk = self._downClickable
|
||||||
self.pronounce.emit(audio)
|
url = QUrl(clk['fmt'].anchorHref())
|
||||||
print("emit done")
|
if url.scheme() == 'audio':
|
||||||
self._downFrag = None
|
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
|
return
|
||||||
self._downFrag = None
|
self._downClickable = None
|
||||||
return super().mouseReleaseEvent(event)
|
return super().mouseReleaseEvent(event)
|
||||||
|
|
||||||
def paintEvent(self, _: Optional[QPaintEvent]) -> None: # noqa
|
def paintEvent(self, _: Optional[QPaintEvent]) -> None: # noqa
|
||||||
painter = QPainter(self)
|
painter = QPainter(self)
|
||||||
painter.save()
|
|
||||||
painter.setBrush(QBrush())
|
painter.setBrush(QBrush())
|
||||||
painter.setPen(QColor("white"))
|
painter.setPen(QColor("white"))
|
||||||
|
|
||||||
#
|
#
|
||||||
# Each line needs a base calculated. To do that, we need to find the
|
# 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,
|
# 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
|
# All text on this line needs to be on the same baseline
|
||||||
#
|
#
|
||||||
assert self._lines is not None
|
assert self._lines is not None
|
||||||
base = 0
|
|
||||||
for line in self._lines:
|
for line in self._lines:
|
||||||
transform = QTransform()
|
line.paintEvent(painter)
|
||||||
transform.translate(0, base)
|
|
||||||
painter.setTransform(transform)
|
|
||||||
base += line.paintEvent(painter)
|
|
||||||
painter.restore()
|
|
||||||
return
|
return
|
||||||
|
|||||||
23
lib/read.py
23
lib/read.py
@@ -1,12 +1,9 @@
|
|||||||
import json
|
from typing import Dict, List, Optional, cast
|
||||||
from typing import Any, Dict, List, Optional, cast
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from PyQt6.QtCore import QPoint, QResource, Qt, QTimer, pyqtSignal, pyqtSlot
|
from PyQt6.QtCore import QPoint, QResource, Qt, QTimer, pyqtSignal, pyqtSlot
|
||||||
from PyQt6.QtGui import (
|
from PyQt6.QtGui import (
|
||||||
QBrush,
|
QBrush,
|
||||||
QColor,
|
QColor,
|
||||||
QCursor,
|
|
||||||
QKeyEvent,
|
QKeyEvent,
|
||||||
QPainter,
|
QPainter,
|
||||||
QPainterPath,
|
QPainterPath,
|
||||||
@@ -15,7 +12,7 @@ from PyQt6.QtGui import (
|
|||||||
QTextCursor,
|
QTextCursor,
|
||||||
)
|
)
|
||||||
from PyQt6.QtSql import QSqlQuery
|
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 import query_error
|
||||||
from lib.preferences import Preferences
|
from lib.preferences import Preferences
|
||||||
@@ -89,6 +86,7 @@ class ReadDialog(QDialog, Ui_ReadDialog):
|
|||||||
self.playSound.connect(self.sound.playSound)
|
self.playSound.connect(self.sound.playSound)
|
||||||
self.playAlert.connect(self.sound.alert)
|
self.playAlert.connect(self.sound.alert)
|
||||||
self.definition.pronounce.connect(self.sound.playSound)
|
self.definition.pronounce.connect(self.sound.playSound)
|
||||||
|
self.definition.newWord.connect(self.newWord)
|
||||||
return
|
return
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -98,6 +96,15 @@ class ReadDialog(QDialog, Ui_ReadDialog):
|
|||||||
#
|
#
|
||||||
# slots
|
# 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()
|
@pyqtSlot()
|
||||||
def timerAction(self) -> None:
|
def timerAction(self) -> None:
|
||||||
if self.session.isActive(): # We are stopping
|
if self.session.isActive(): # We are stopping
|
||||||
@@ -127,6 +134,9 @@ class ReadDialog(QDialog, Ui_ReadDialog):
|
|||||||
cursor.select(QTextCursor.SelectionType.WordUnderCursor)
|
cursor.select(QTextCursor.SelectionType.WordUnderCursor)
|
||||||
text = cursor.selectedText().strip()
|
text = cursor.selectedText().strip()
|
||||||
word = Word(text)
|
word = Word(text)
|
||||||
|
if not word.isValid():
|
||||||
|
self.playAlert.emit()
|
||||||
|
return
|
||||||
word.playPRS()
|
word.playPRS()
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -221,6 +231,9 @@ class ReadDialog(QDialog, Ui_ReadDialog):
|
|||||||
cursor.select(cursor.SelectionType.WordUnderCursor)
|
cursor.select(cursor.SelectionType.WordUnderCursor)
|
||||||
text = cursor.selectedText().strip()
|
text = cursor.selectedText().strip()
|
||||||
word = Word(text)
|
word = Word(text)
|
||||||
|
if not word.isValid():
|
||||||
|
self.playAlert.emit()
|
||||||
|
return
|
||||||
self.definition.setWord(word)
|
self.definition.setWord(word)
|
||||||
self.showDefinition()
|
self.showDefinition()
|
||||||
return
|
return
|
||||||
|
|||||||
116
lib/sounds.py
116
lib/sounds.py
@@ -22,6 +22,7 @@ from PyQt6.QtNetwork import (
|
|||||||
QNetworkReply,
|
QNetworkReply,
|
||||||
QNetworkRequest,
|
QNetworkRequest,
|
||||||
)
|
)
|
||||||
|
from trycast import trycast
|
||||||
|
|
||||||
# from PyQt6.QtWidgets import QWidget
|
# from PyQt6.QtWidgets import QWidget
|
||||||
|
|
||||||
@@ -33,39 +34,53 @@ class SoundOff(QObject):
|
|||||||
if cls._instance:
|
if cls._instance:
|
||||||
return cls._instance
|
return cls._instance
|
||||||
cls._instance = super(SoundOff, cls).__new__(cls)
|
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
|
return cls._instance
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
#
|
if self.localPlayer.receivers(self.localPlayer.errorOccurred) > 0:
|
||||||
# Setup devices
|
print("SoundOff, __init__() after __init__()")
|
||||||
#
|
|
||||||
self.virtualDevice = None
|
|
||||||
|
|
||||||
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
|
# Connections
|
||||||
#
|
#
|
||||||
@@ -76,18 +91,6 @@ class SoundOff(QObject):
|
|||||||
self.virtualPlayer.errorOccurred.connect(self.mediaError)
|
self.virtualPlayer.errorOccurred.connect(self.mediaError)
|
||||||
self.virtualPlayer.mediaStatusChanged.connect(self.mediaStatus)
|
self.virtualPlayer.mediaStatusChanged.connect(self.mediaStatus)
|
||||||
self.virtualPlayer.playbackStateChanged.connect(self.playbackState)
|
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
|
return
|
||||||
|
|
||||||
@pyqtSlot(QMediaPlayer.Error, str)
|
@pyqtSlot(QMediaPlayer.Error, str)
|
||||||
@@ -98,11 +101,13 @@ class SoundOff(QObject):
|
|||||||
|
|
||||||
@pyqtSlot(QMediaPlayer.MediaStatus)
|
@pyqtSlot(QMediaPlayer.MediaStatus)
|
||||||
def mediaStatus(self, status: QMediaPlayer.MediaStatus) -> None:
|
def mediaStatus(self, status: QMediaPlayer.MediaStatus) -> None:
|
||||||
# print(f"mediaStatus: {status}")
|
|
||||||
if status == QMediaPlayer.MediaStatus.LoadedMedia:
|
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
|
assert player is not None
|
||||||
player.play()
|
player.play()
|
||||||
|
self.lastSender = self.sender()
|
||||||
return
|
return
|
||||||
|
|
||||||
@pyqtSlot(QMediaPlayer.PlaybackState)
|
@pyqtSlot(QMediaPlayer.PlaybackState)
|
||||||
@@ -119,6 +124,7 @@ class SoundOff(QObject):
|
|||||||
return
|
return
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
|
@pyqtSlot(QUrl)
|
||||||
def playSound(self, url: str | QUrl) -> None:
|
def playSound(self, url: str | QUrl) -> None:
|
||||||
if isinstance(url, str):
|
if isinstance(url, str):
|
||||||
url = QUrl(url)
|
url = QUrl(url)
|
||||||
@@ -128,8 +134,10 @@ class SoundOff(QObject):
|
|||||||
self.virtualPlayer.setAudioOutput(self.virtualOutput)
|
self.virtualPlayer.setAudioOutput(self.virtualOutput)
|
||||||
if url != self._lastUrl:
|
if url != self._lastUrl:
|
||||||
request = QNetworkRequest(url)
|
request = QNetworkRequest(url)
|
||||||
self.nam.get(request)
|
reply = self.nam.get(request)
|
||||||
|
assert reply is not None
|
||||||
self._lastUrl = url
|
self._lastUrl = url
|
||||||
|
reply.finished.connect(self.finished)
|
||||||
return
|
return
|
||||||
for player in [self.localPlayer, self.virtualPlayer]:
|
for player in [self.localPlayer, self.virtualPlayer]:
|
||||||
if not player:
|
if not player:
|
||||||
@@ -151,20 +159,22 @@ class SoundOff(QObject):
|
|||||||
_buffer: dict[QMediaPlayer, QBuffer] = {}
|
_buffer: dict[QMediaPlayer, QBuffer] = {}
|
||||||
_lastUrl = QUrl()
|
_lastUrl = QUrl()
|
||||||
|
|
||||||
@pyqtSlot(QNetworkReply)
|
@pyqtSlot()
|
||||||
def finished(self, reply: QNetworkReply) -> None:
|
def finished(self) -> None:
|
||||||
storage = reply.readAll()
|
reply = trycast(QNetworkReply, self.sender())
|
||||||
|
assert reply is not None
|
||||||
crypto = QCryptographicHash(QCryptographicHash.Algorithm.Sha256)
|
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]:
|
for player in [self.localPlayer, self.virtualPlayer]:
|
||||||
if not player:
|
if not player:
|
||||||
continue
|
continue
|
||||||
self._storage[player] = QByteArray(storage)
|
self._storage[player] = QByteArray(self._reply)
|
||||||
crypto.addData(self._storage[player])
|
|
||||||
print(player, crypto.result().toHex())
|
|
||||||
crypto.reset()
|
|
||||||
self._buffer[player] = QBuffer(self._storage[player])
|
self._buffer[player] = QBuffer(self._storage[player])
|
||||||
url = reply.request().url()
|
|
||||||
player.setSourceDevice(self._buffer[player], url)
|
player.setSourceDevice(self._buffer[player], url)
|
||||||
player.setPosition(0)
|
player.setPosition(0)
|
||||||
if player.mediaStatus() == QMediaPlayer.MediaStatus.LoadedMedia:
|
if player.mediaStatus() == QMediaPlayer.MediaStatus.LoadedMedia:
|
||||||
|
|||||||
61
lib/utils.py
61
lib/utils.py
@@ -2,7 +2,7 @@
|
|||||||
from typing import NoReturn, Self
|
from typing import NoReturn, Self
|
||||||
|
|
||||||
from PyQt6.QtCore import QCoreApplication, QDir, QStandardPaths, Qt
|
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.QtNetwork import QNetworkAccessManager, QNetworkDiskCache
|
||||||
from PyQt6.QtSql import QSqlQuery
|
from PyQt6.QtSql import QSqlQuery
|
||||||
|
|
||||||
@@ -22,6 +22,7 @@ def query_error(query: QSqlQuery) -> NoReturn:
|
|||||||
)
|
)
|
||||||
raise Exception(translate("MainWindow", "SQL Error"))
|
raise Exception(translate("MainWindow", "SQL Error"))
|
||||||
|
|
||||||
|
|
||||||
class Resources:
|
class Resources:
|
||||||
_instance = None
|
_instance = None
|
||||||
nam = QNetworkAccessManager()
|
nam = QNetworkAccessManager()
|
||||||
@@ -38,11 +39,60 @@ class Resources:
|
|||||||
linkColor: QColor
|
linkColor: QColor
|
||||||
subduedColor: 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:
|
def __new__(cls: type[Self]) -> Self:
|
||||||
if cls._instance:
|
if cls._instance:
|
||||||
return cls._instance
|
return cls._instance
|
||||||
cls._instance = super(Resources, cls).__new__(cls)
|
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
|
# Fonts
|
||||||
#
|
#
|
||||||
cls.headerFont = QFontDatabase.font("OpenDyslexic", None, 10)
|
cls.headerFont = QFontDatabase.font("OpenDyslexic", None, 10)
|
||||||
@@ -59,21 +109,12 @@ class Resources:
|
|||||||
cls.headerFont.setWeight(QFont.Weight.Bold)
|
cls.headerFont.setWeight(QFont.Weight.Bold)
|
||||||
cls.boldFont.setBold(True)
|
cls.boldFont.setBold(True)
|
||||||
cls.italicFont.setItalic(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.capsFont.setCapitalization(QFont.Capitalization.AllUppercase)
|
||||||
cls.smallCapsFont.setCapitalization(QFont.Capitalization.SmallCaps)
|
cls.smallCapsFont.setCapitalization(QFont.Capitalization.SmallCaps)
|
||||||
|
|
||||||
cls.phonicFont = QFontDatabase.font("Gentium", None, 10)
|
cls.phonicFont = QFontDatabase.font("Gentium", None, 10)
|
||||||
cls.phonicFont.setPixelSize(20)
|
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
|
# Setup the Network Manager
|
||||||
#
|
#
|
||||||
|
|||||||
73
lib/words.py
73
lib/words.py
@@ -1,40 +1,47 @@
|
|||||||
import importlib
|
import importlib
|
||||||
import pkgutil
|
|
||||||
import json
|
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 (
|
from PyQt6.QtCore import Qt, pyqtSlot
|
||||||
Qt,
|
|
||||||
pyqtSlot,
|
|
||||||
)
|
|
||||||
from PyQt6.QtSql import QSqlQuery
|
from PyQt6.QtSql import QSqlQuery
|
||||||
from PyQt6.QtWidgets import QScrollArea
|
from PyQt6.QtWidgets import QScrollArea
|
||||||
|
from trycast import trycast
|
||||||
from lib.utils import query_error
|
|
||||||
from lib.sounds import SoundOff
|
|
||||||
from lib.definition import Definition, Line
|
|
||||||
|
|
||||||
import plugins
|
import plugins
|
||||||
def find_plugins(ns_pkg):
|
from lib.definition import Definition, Line
|
||||||
return pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + '.')
|
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 = {
|
discovered_plugins = {
|
||||||
# finder, name, ispkg
|
# 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}"
|
API = "https://api.dictionaryapi.dev/api/v2/entries/en/{word}"
|
||||||
|
|
||||||
|
|
||||||
class WordType(TypedDict):
|
class WordType(TypedDict):
|
||||||
word: str
|
word: str
|
||||||
source: str
|
source: str
|
||||||
definition: str
|
definition: str
|
||||||
|
|
||||||
|
|
||||||
class Word:
|
class Word:
|
||||||
"""All processing of a dictionary word."""
|
"""All processing of a dictionary word."""
|
||||||
|
|
||||||
_words: dict[str, WordType] = {}
|
_words: dict[str, WordType] = {}
|
||||||
|
_valid = False
|
||||||
|
|
||||||
def __init__(self, word: str) -> None:
|
def __init__(self, word: str) -> None:
|
||||||
#
|
#
|
||||||
# Have we already retrieved this word?
|
# Have we already retrieved this word?
|
||||||
@@ -56,14 +63,18 @@ class Word:
|
|||||||
"definition": json.loads(query.value("definition")),
|
"definition": json.loads(query.value("definition")),
|
||||||
}
|
}
|
||||||
self.current = Word._words[word]
|
self.current = Word._words[word]
|
||||||
|
self._valid = True
|
||||||
return
|
return
|
||||||
#
|
#
|
||||||
# The code should look at our settings to see if we have an API
|
# The code should look at our settings to see if we have an API
|
||||||
# key for MW to decide on the source to use.
|
# key for MW to decide on the source to use.
|
||||||
#
|
#
|
||||||
source = "mw"
|
source = "mw"
|
||||||
|
|
||||||
self._words[word] = discovered_plugins[source].fetch(word)
|
self._words[word] = discovered_plugins[source].fetch(word)
|
||||||
|
if self._words[word] is None:
|
||||||
|
self._valid = False
|
||||||
|
return
|
||||||
self.current = Word._words[word]
|
self.current = Word._words[word]
|
||||||
query.prepare(
|
query.prepare(
|
||||||
"INSERT INTO words "
|
"INSERT INTO words "
|
||||||
@@ -75,39 +86,41 @@ class Word:
|
|||||||
query.bindValue(":definition", json.dumps(self.current["definition"]))
|
query.bindValue(":definition", json.dumps(self.current["definition"]))
|
||||||
if not query.exec():
|
if not query.exec():
|
||||||
query_error(query)
|
query_error(query)
|
||||||
|
self._valid = True
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def isValid(self) -> bool:
|
||||||
|
return self._valid
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def playSound(self) -> None:
|
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():
|
if url.isValid():
|
||||||
snd = SoundOff()
|
snd = SoundOff()
|
||||||
snd.playSound(url)
|
snd.playSound(url)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def playPRS(self) -> None:
|
||||||
|
return
|
||||||
|
|
||||||
def getWord(self) -> str:
|
def getWord(self) -> str:
|
||||||
return cast(str, self.current["word"])
|
return self.current["word"]
|
||||||
|
|
||||||
def get_html(self) -> str | None:
|
def get_html(self) -> str | None:
|
||||||
src = self.current['source']
|
src = self.current["source"]
|
||||||
try:
|
try:
|
||||||
return discovered_plugins[src].getHtml(self.current)
|
return cast(str, discovered_plugins[src].getHtml(self.current))
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise Exception(f"Unknown source: {src}")
|
raise Exception(f"Unknown source: {src}")
|
||||||
|
|
||||||
def get_def(self) -> list[Line]:
|
def get_def(self) -> list[Line]:
|
||||||
src = self.current['source']
|
src = self.current["source"]
|
||||||
try:
|
try:
|
||||||
lines = discovered_plugins[src].getDef(self.current["definition"])
|
lines = discovered_plugins[src].getDef(self.current["definition"])
|
||||||
|
lines = trycast(list[Line], lines)
|
||||||
|
assert lines is not None
|
||||||
return lines
|
return lines
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise Exception(f"Unknown source: {self.current['source']}")
|
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