This commit is contained in:
Christopher T. Johnson
2024-04-16 11:50:26 -04:00
parent f1ad24d70a
commit 51b1121176
6 changed files with 422 additions and 274 deletions

View File

@@ -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

View File

@@ -1,27 +1,40 @@
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
from typing import Any, Callable, Optional, Self, cast, overload
from PyQt6.QtCore import QMargins, QPoint, QRect, QSize, Qt, QUrl, pyqtSignal
from PyQt6.QtGui import (
QBrush,
QColor,
QFont,
QFontMetrics,
QMouseEvent,
QPainter,
QPaintEvent,
QResizeEvent,
QTextOption,
QTransform,
)
from PyQt6.QtWidgets import QWidget
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,
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._text: str = which
if font is None:
raise TypeError("Missing required parameter 'font'")
self._font = font
@@ -63,13 +76,14 @@ class Fragment:
return f"({self._position.x()}, {self._position.y()}): {self._text}"
@overload
def paintEvent(self, widthSrc:int) -> QSize:
def paintEvent(self, widthSrc: int) -> QSize:
...
@overload
def paintEvent(self, widthSrc: QPainter) -> int:
...
def paintEvent(self, widthSrc) -> int|QSize:
def paintEvent(self, widthSrc: QPainter | int) -> int | QSize:
if isinstance(widthSrc, QPainter):
viewportWidth = widthSrc.viewport().width()
painter = widthSrc
@@ -77,28 +91,21 @@ class Fragment:
viewportWidth = widthSrc
painter = None
fm = QFontMetrics(self._font)
top = (
self._position.y()
+ fm.descent()
- fm.height()
)
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
)
flags = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignBaseline
boundingNoWrap = fm.boundingRect(
rect, flags|Qt.TextFlag.TextSingleLine, self._text
rect, flags | Qt.TextFlag.TextSingleLine, self._text
)
bounding = fm.boundingRect(
rect, flags|Qt.TextFlag.TextWordWrap, self._text
rect, flags | Qt.TextFlag.TextWordWrap, self._text
)
text = self._text
remainingText = ''
remainingText = ""
if boundingNoWrap.height() < bounding.height():
#
# This is not optimal, but it is only a few iterations
@@ -107,39 +114,29 @@ class Fragment:
char = 0
pos = rect.x()
while pos < rect.right():
if text[char] == ' ':
if text[char] == " ":
lastSpace = char
pos += fm.horizontalAdvance(
text[char]
)
pos += fm.horizontalAdvance(text[char])
char += 1
if lastSpace > 0:
remainingText = text[lastSpace+1:]
remainingText = text[lastSpace + 1 :]
text = text[:lastSpace]
size = boundingNoWrap.size()
boundingNoWrap = fm.boundingRect(
rect, flags|Qt.TextFlag.TextSingleLine, text
rect, flags | Qt.TextFlag.TextSingleLine, text
)
rect.setSize(boundingNoWrap.size())
if remainingText != '':
if remainingText != "":
top += size.height()
remainingRect = QRect(
indent, top,
viewportWidth - indent, 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()
)
)
size = size.grownBy(QMargins(0, 0, 0, boundingRemaingRect.height()))
remainingRect.setSize(boundingRemaingRect.size())
size = size.grownBy(self._margin)
size = size.grownBy(self._border)
@@ -163,12 +160,14 @@ class Fragment:
brush.setColor(self._background)
brush.setStyle(Qt.BrushStyle.SolidPattern)
painter.setBrush(brush)
painter.fillRect(rect,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)
painter.drawText(
remainingRect, flags | Qt.TextFlag.TextWordWrap, remainingText
)
painter.restore()
return size.height()
@@ -310,6 +309,7 @@ class Fragment:
def setBackground(self, color: QColor) -> None:
self._background = color
return
def setIndent(self, indent: int) -> None:
self._indent = indent
return
@@ -365,8 +365,10 @@ class Fragment:
def pixelIndent(self) -> int:
return self._indent * self._indentAmount
class Line:
parseText = None
def __init__(self) -> None:
self._maxHeight = -1
self._baseLine = -1
@@ -379,11 +381,12 @@ 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
@@ -395,12 +398,16 @@ class Line:
lineSpacing = ls
return lineSpacing
def addFragment(self, frags: Fragment|list[Fragment],) -> None:
def addFragment(
self,
frags: Fragment | list[Fragment],
) -> None:
SPEAKER = "\U0001F508"
if not isinstance(frags, list):
frags = [frags, ]
frags = [
frags,
]
for frag in frags:
if frag.audio().isValid():
frag.setText(frag.text() + " " + SPEAKER)
@@ -485,6 +492,7 @@ class Line:
def getLineSpacing(self) -> int:
return self._leading + self._maxHeight
class Definition(QWidget):
pronounce = pyqtSignal(str)
@@ -499,7 +507,7 @@ 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] = []

View File

@@ -101,7 +101,7 @@ class SoundOff(QObject):
@pyqtSlot(QMediaPlayer.MediaStatus)
def mediaStatus(self, status: QMediaPlayer.MediaStatus) -> None:
#print(f"mediaStatus: {status}")
# print(f"mediaStatus: {status}")
if status == QMediaPlayer.MediaStatus.LoadedMedia:
player: Optional[QMediaPlayer] = cast(QMediaPlayer, self.sender())
assert player is not None
@@ -110,7 +110,7 @@ class SoundOff(QObject):
@pyqtSlot(QMediaPlayer.PlaybackState)
def playbackState(self, state: QMediaPlayer.PlaybackState) -> None:
#print(f"playbackState: {state}")
# print(f"playbackState: {state}")
return
#
@@ -164,7 +164,7 @@ class SoundOff(QObject):
continue
self._storage[player] = QByteArray(storage)
crypto.addData(self._storage[player])
#print(player, crypto.result().toHex())
# print(player, crypto.result().toHex())
crypto.reset()
self._buffer[player] = QBuffer(self._storage[player])
url = reply.request().url()
@@ -172,5 +172,5 @@ class SoundOff(QObject):
player.setPosition(0)
if player.mediaStatus() == QMediaPlayer.MediaStatus.LoadedMedia:
player.play()
#print("play")
# print("play")
return

View File

@@ -22,6 +22,7 @@ def query_error(query: QSqlQuery) -> NoReturn:
)
raise Exception(translate("MainWindow", "SQL Error"))
class Resources:
_instance = None
nam = QNetworkAccessManager()
@@ -39,7 +40,7 @@ class Resources:
subduedColor: QColor
subduedBackground: QColor
def __new__(cls: type[Self]) -> Self:
if cls._instance:
return cls._instance

View File

@@ -1,40 +1,46 @@
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] = {}
def __init__(self, word: str) -> None:
#
# Have we already retrieved this word?
@@ -79,30 +85,38 @@ class Word:
@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)