Mostly working!

This commit is contained in:
Christopher T. Johnson
2024-05-10 12:08:21 -04:00
parent f97305e36e
commit 7c65b466f1
4 changed files with 150 additions and 69 deletions

View File

@@ -5,7 +5,7 @@ import signal
import sys import sys
from typing import Any, cast from typing import Any, cast
from PyQt6.QtCore import QResource, QSettings, Qt 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, QScrollArea from PyQt6.QtWidgets import QApplication, QScrollArea
@@ -18,10 +18,18 @@ from lib.words import Definition
class DefinitionArea(QScrollArea): class DefinitionArea(QScrollArea):
def __init__(self, w: Word, *args: Any, **kwargs: Any) -> None: def __init__(self, w: Word, *args: Any, **kwargs: Any) -> None:
super(DefinitionArea, self).__init__(*args, *kwargs) super(DefinitionArea, self).__init__(*args, *kwargs)
d = Definition(w) self.definition = Definition(w)
self.setWidget(d) self.setWidget(self.definition)
self.setWidgetResizable(True) self.setWidgetResizable(True)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn) self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
self.definition.newWord.connect(self.newWord)
return
@pyqtSlot(str)
def newWord(self, word:str) -> None:
print(f"newWord: {word}")
w = Word(word)
self.definition.setWord(w)
return return
def closeEvent(self, event): def closeEvent(self, event):

View File

@@ -16,9 +16,15 @@ from PyQt6.QtGui import (
QTextLayout, QTextLayout,
QTextOption, QTextOption,
) )
from lib.sounds import SoundOff
from PyQt6.QtWidgets import QWidget from PyQt6.QtWidgets import QWidget
from trycast import trycast
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"""
@@ -117,12 +123,17 @@ class Fragment:
painter.brush().setColor(Qt.GlobalColor.green) painter.brush().setColor(Qt.GlobalColor.green)
for fmt in self._layout.formats(): for fmt in self._layout.formats():
if fmt.format.isAnchor(): if fmt.format.isAnchor():
#text = self._layout.text()[fmt.start:fmt.start+fmt.length]
runs = self._layout.glyphRuns(fmt.start, fmt.length) runs = self._layout.glyphRuns(fmt.start, fmt.length)
bb = runs[0].boundingRect() bb = runs[0].boundingRect()
bb.moveTo(bb.topLeft() + self._layout.position()) bb.moveTo(bb.topLeft() + self._layout.position())
painter.drawRect(bb) url = QUrl(fmt.format.anchorHref())
#print(f"({bb.left()}-{bb.right()}, {bb.top()}-{bb.bottom()}): {text}") 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() painter.restore()
return QSize(int(size.width()), int(size.height())) return QSize(int(size.width()), int(size.height()))
@@ -434,12 +445,16 @@ class Clickable(TypedDict):
fmt: QTextCharFormat fmt: QTextCharFormat
class Definition(QWidget): class Definition(QWidget):
pronounce = pyqtSignal(str) 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)
@@ -450,7 +465,6 @@ class Definition(QWidget):
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[Clickable] = []
base = 0 base = 0
for line in self._lines: for line in self._lines:
@@ -474,12 +488,23 @@ class Definition(QWidget):
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.position()}") position = MyPointF(event.position())
for clk in self._buttons: print(f"mousePressEvent: {position}")
if clk["bb"].contains(event.position()): for line in self._lines:
print("inside") for frag in line.getLine():
self._downClickable = clk 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:
@@ -490,9 +515,15 @@ class Definition(QWidget):
): ):
print(f"mousePressPseudoEvent: {event.position()}") print(f"mousePressPseudoEvent: {event.position()}")
clk = self._downClickable clk = self._downClickable
bb = clk['bb'] url = QUrl(clk['fmt'].anchorHref())
print(f"({bb.left()}-{bb.right()}, {bb.top()}-{bb.bottom()})", clk["fmt"].anchorHref(),) if url.scheme() == 'audio':
#self.pronounce.emit(audio) url.setScheme('https')
self.pronounce.emit(url)
elif url.scheme() == 'word':
self.newWord.emit(url.path())
else:
print(f"{clk['fmt'].anchorHref()}: {url.scheme()}")
self.alert.emit()
self._downClickable = None self._downClickable = None
return return
self._downClickable = None self._downClickable = None
@@ -510,29 +541,7 @@ 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
# #
buildButtons = (len(self._buttons) < 1)
assert self._lines is not None assert self._lines is not None
for line in self._lines: for line in self._lines:
line.paintEvent(painter) line.paintEvent(painter)
if not buildButtons:
continue
for frag in line.getLine():
for fmtRng in frag.layout().formats():
if fmtRng.format.isAnchor():
runs = frag.layout().glyphRuns(fmtRng.start,fmtRng.start+fmtRng.length)
run = runs[0]
bb = run.boundingRect()
bb.moveTo(bb.topLeft() + frag.layout().position())
self._buttons.append(
{
'bb': bb,
'frag': frag,
'fmt': fmtRng.format,
}
)
painter.setPen(QColor("cyan"))
for click in self._buttons:
if click['fmt'].isAnchor():
painter.drawRect(click['bb'])
return return

View File

@@ -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
@@ -90,7 +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)
self.nam.finished.connect(self.finished)
return return
@pyqtSlot(QMediaPlayer.Error, str) @pyqtSlot(QMediaPlayer.Error, str)
@@ -101,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)
@@ -122,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)
@@ -131,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:
@@ -154,23 +159,25 @@ 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
code = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute)
print(f"HttpStatusCodeAttribute: {code}, error: {reply.error()}")
self._reply = reply.readAll()
url = reply.request().url()
reply.close() reply.close()
crypto = QCryptographicHash(QCryptographicHash.Algorithm.Sha256) 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:
player.play() player.play()
# print("play") print("play")
return return

View File

@@ -151,6 +151,17 @@ class DefinitionSection(TypedDict):
sls: NotRequired[list[str]] sls: NotRequired[list[str]]
sseq: Any # list[list[Pair]] sseq: Any # list[list[Pair]]
class UndefinedRunOn(TypedDict):
ure: str
fl: str
utxt: NotRequired[list[list[Pair]]]
ins: NotRequired[list[Inflection]]
lbs: NotRequired[list[str]]
prs: NotRequired[list[Pronunciation]]
sls: NotRequired[list[str]]
vrs: NotRequired[list[Variant]]
DefinedRunOn = TypedDict( DefinedRunOn = TypedDict(
"DefinedRunOn", "DefinedRunOn",
{ {
@@ -287,7 +298,7 @@ def fetch(word: str) -> WordType:
def soundUrl(sound: Sound, fmt="ogg") -> QUrl: def soundUrl(sound: Sound, fmt="ogg") -> QUrl:
"""Create a URL from a PRS structure.""" """Create a URL from a PRS structure."""
base = f"https://media.merriam-webster.com/audio/prons/en/us/{fmt}" base = f"audio://media.merriam-webster.com/audio/prons/en/us/{fmt}"
audio = sound["audio"] audio = sound["audio"]
m = re.match(r"(bix|gg|[a-zA-Z])", audio) m = re.match(r"(bix|gg|[a-zA-Z])", audio)
if m: if m:
@@ -345,10 +356,26 @@ def do_prs(frag: Fragment, prs: list[Pronunciation] | None) -> None:
return return
def do_aq(aq: AttributionOfQuote | None) -> list[Line]: def do_aq(aq: AttributionOfQuote | None) -> Line:
assert aq is not None assert aq is not None
raise NotImplementedError("aq") r = Resources()
return [] frag = Fragment()
if 'auth' in aq:
frag.addText(aq['auth']+', ', r.subduedFormat)
if 'source' in aq:
frag.addText(aq['source'], r.subduedFormat)
if 'aqdate' in aq:
frag.addText(', '+aq['aqdate'], r.subduedFormat)
if 'subsource' in aq:
ss = trycast(SubSource, aq['subsource'])
assert ss is not None
if 'source' in ss:
frag.addText(', '+ss['source'], r.subduedFormat)
if 'aqdate' in ss:
frag.addText(', '+ss['aqdate'], r.subduedFormat)
line = Line()
line.addFragment(frag)
return line
def do_vis(vis: list[VerbalIllustration] | None, indent=0) -> list[Line]: def do_vis(vis: list[VerbalIllustration] | None, indent=0) -> list[Line]:
@@ -364,7 +391,7 @@ def do_vis(vis: list[VerbalIllustration] | None, indent=0) -> list[Line]:
line.addFragment(frag) line.addFragment(frag)
lines.append(line) lines.append(line)
if "aq" in vi: if "aq" in vi:
lines += do_aq(trycast(AttributionOfQuote, vi["aq"])) lines.append(do_aq(trycast(AttributionOfQuote, vi["aq"])))
return lines return lines
@@ -688,7 +715,32 @@ def do_dros(dros: list[DefinedRunOn]|None) -> list[Line]:
raise NotImplementedError(f"Key of {k}") raise NotImplementedError(f"Key of {k}")
return lines return lines
def do_uros(uros: list[UndefinedRunOn]|None) -> list[Line]:
assert uros is not None
r = Resources()
lines: list[Line] = []
for uro in uros:
frag = Fragment()
text = re.sub(r'\*', '', uro['ure'])
frag.addText(text, r.labelFormat)
if 'prs' in uro:
do_prs(frag, uro['prs'])
frag.addText(' '+uro['fl'],r.textFormat) # r.linkFormat
line = Line()
line.addFragment(frag)
lines.append(line)
if 'utxt' in uro:
for entry in uro['utxt']:
for pair in entry:
if pair['objType'] == 'vis':
lines += do_vis(trycast(list[VerbalIllustration], pair['obj']))
elif pair['objType'] == 'uns':
(newFrags, newLines) = do_uns(trycast(list[list[list[Pair]]],pair['obj']),0)
line = Line()
line.addFragment(newFrags)
lines.append(line)
lines += newLines
return lines
def getDef(defines: Any) -> list[Line]: def getDef(defines: Any) -> list[Line]:
Line.setParseText(parseText) Line.setParseText(parseText)
workList = restructure(defines) workList = restructure(defines)
@@ -786,6 +838,10 @@ def getDef(defines: Any) -> list[Line]:
lines += do_def(define) lines += do_def(define)
except NotImplementedError: except NotImplementedError:
raise raise
if "uros" in work:
print(json.dumps(work['uros'],indent=2))
uros = trycast(list[UndefinedRunOn], work['uros'])
lines += do_uros(uros)
if "dros" in work: if "dros" in work:
dros = trycast(list[DefinedRunOn], work["dros"]) dros = trycast(list[DefinedRunOn], work["dros"])
if len(phrases) < 1: if len(phrases) < 1:
@@ -815,6 +871,7 @@ def getDef(defines: Any) -> list[Line]:
"shortdef", "shortdef",
"vrs", "vrs",
"dros", "dros",
'uros',
]: ]:
raise NotImplementedError(f"Unknown key {k} in work") raise NotImplementedError(f"Unknown key {k} in work")
if len(phrases) > 0: if len(phrases) > 0:
@@ -845,10 +902,10 @@ def replaceCode(code:str) -> tuple[str, QTextCharFormat]:
token = fields[0] token = fields[0]
if token == 'a_link': if token == 'a_link':
text = fields[1] text = fields[1]
fmt.setAnchorHref(fields[1]) fmt.setAnchorHref('auto://'+fields[1])
elif token in ['d_link', 'et_link', 'mat', 'sx', 'i_link']: elif token in ['d_link', 'et_link', 'mat', 'sx', 'i_link']:
text = fields[1] text = fields[1]
pre = 'word://' pre = 'word:///'
if fields[2] == '': if fields[2] == '':
fmt.setAnchorHref(pre+fields[1]) fmt.setAnchorHref(pre+fields[1])
else: else:
@@ -859,18 +916,18 @@ def replaceCode(code:str) -> tuple[str, QTextCharFormat]:
fmt.setFontCapitalization(QFont.Capitalization.SmallCaps) fmt.setFontCapitalization(QFont.Capitalization.SmallCaps)
elif token == 'dxt': elif token == 'dxt':
if fields[3] == 'illustration': if fields[3] == 'illustration':
fmt.setAnchorHref('article://'+fields[2]) fmt.setAnchorHref('article:///'+fields[2])
elif fields[3] == 'table': elif fields[3] == 'table':
fmt.setAnchorHref('table://'+fields[2]) fmt.setAnchorHref('table:///'+fields[2])
elif fields[3] != "": elif fields[3] != "":
fmt.setAnchorHref('sense://'+fields[3]) fmt.setAnchorHref('sense:///'+fields[3])
else: else:
fmt.setAnchorHref('word://'+fields[1]) fmt.setAnchorHref('word:///'+fields[1])
elif token == 'et_link': elif token == 'et_link':
if fields[2] != '': if fields[2] != '':
fmt.setAnchorHref('etymology://'+fields[2]) fmt.setAnchorHref('etymology:///'+fields[2])
else: else:
fmt.setAnchorHref('etymology://' + fields[1]) fmt.setAnchorHref('etymology:///' + fields[1])
else: else:
raise NotImplementedError(f"Token {code} not implimented") raise NotImplementedError(f"Token {code} not implimented")
fmt.setForeground(r.linkColor) fmt.setForeground(r.linkColor)