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
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.QtSql import QSqlDatabase, QSqlQuery
from PyQt6.QtWidgets import QApplication, QScrollArea
@@ -18,10 +18,18 @@ from lib.words import Definition
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.definition = Definition(w)
self.setWidget(self.definition)
self.setWidgetResizable(True)
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
def closeEvent(self, event):

View File

@@ -16,9 +16,15 @@ from PyQt6.QtGui import (
QTextLayout,
QTextOption,
)
from lib.sounds import SoundOff
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:
"""A fragment of text to be displayed"""
@@ -117,12 +123,17 @@ class Fragment:
painter.brush().setColor(Qt.GlobalColor.green)
for fmt in self._layout.formats():
if fmt.format.isAnchor():
#text = self._layout.text()[fmt.start:fmt.start+fmt.length]
runs = self._layout.glyphRuns(fmt.start, fmt.length)
bb = runs[0].boundingRect()
bb.moveTo(bb.topLeft() + self._layout.position())
painter.drawRect(bb)
#print(f"({bb.left()}-{bb.right()}, {bb.top()}-{bb.bottom()}): {text}")
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 QSize(int(size.width()), int(size.height()))
@@ -434,12 +445,16 @@ class Clickable(TypedDict):
fmt: QTextCharFormat
class Definition(QWidget):
pronounce = pyqtSignal(str)
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)
@@ -450,7 +465,6 @@ class Definition(QWidget):
lines: list[Line] = word.get_def()
assert lines is not None
self._lines = lines
self._buttons: list[Clickable] = []
base = 0
for line in self._lines:
@@ -474,12 +488,23 @@ class Definition(QWidget):
def mousePressEvent(self, event: Optional[QMouseEvent]) -> None:
if not event:
return super().mousePressEvent(event)
print(f"mousePressEvent: {event.position()}")
for clk in self._buttons:
if clk["bb"].contains(event.position()):
print("inside")
self._downClickable = clk
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:
@@ -490,9 +515,15 @@ class Definition(QWidget):
):
print(f"mousePressPseudoEvent: {event.position()}")
clk = self._downClickable
bb = clk['bb']
print(f"({bb.left()}-{bb.right()}, {bb.top()}-{bb.bottom()})", clk["fmt"].anchorHref(),)
#self.pronounce.emit(audio)
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())
else:
print(f"{clk['fmt'].anchorHref()}: {url.scheme()}")
self.alert.emit()
self._downClickable = None
return
self._downClickable = None
@@ -510,29 +541,7 @@ class Definition(QWidget):
#
# All text on this line needs to be on the same baseline
#
buildButtons = (len(self._buttons) < 1)
assert self._lines is not None
for line in self._lines:
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

View File

@@ -22,6 +22,7 @@ from PyQt6.QtNetwork import (
QNetworkReply,
QNetworkRequest,
)
from trycast import trycast
# from PyQt6.QtWidgets import QWidget
@@ -90,7 +91,6 @@ class SoundOff(QObject):
self.virtualPlayer.errorOccurred.connect(self.mediaError)
self.virtualPlayer.mediaStatusChanged.connect(self.mediaStatus)
self.virtualPlayer.playbackStateChanged.connect(self.playbackState)
self.nam.finished.connect(self.finished)
return
@pyqtSlot(QMediaPlayer.Error, str)
@@ -101,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)
@@ -122,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)
@@ -131,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:
@@ -154,23 +159,25 @@ class SoundOff(QObject):
_buffer: dict[QMediaPlayer, QBuffer] = {}
_lastUrl = QUrl()
@pyqtSlot(QNetworkReply)
def finished(self, reply: QNetworkReply) -> None:
storage = reply.readAll()
@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()
crypto = QCryptographicHash(QCryptographicHash.Algorithm.Sha256)
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:
player.play()
# print("play")
print("play")
return

View File

@@ -151,6 +151,17 @@ class DefinitionSection(TypedDict):
sls: NotRequired[list[str]]
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",
{
@@ -287,7 +298,7 @@ def fetch(word: str) -> WordType:
def soundUrl(sound: Sound, fmt="ogg") -> QUrl:
"""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"]
m = re.match(r"(bix|gg|[a-zA-Z])", audio)
if m:
@@ -345,10 +356,26 @@ def do_prs(frag: Fragment, prs: list[Pronunciation] | None) -> None:
return
def do_aq(aq: AttributionOfQuote | None) -> list[Line]:
def do_aq(aq: AttributionOfQuote | None) -> Line:
assert aq is not None
raise NotImplementedError("aq")
return []
r = Resources()
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]:
@@ -364,7 +391,7 @@ def do_vis(vis: list[VerbalIllustration] | None, indent=0) -> list[Line]:
line.addFragment(frag)
lines.append(line)
if "aq" in vi:
lines += do_aq(trycast(AttributionOfQuote, vi["aq"]))
lines.append(do_aq(trycast(AttributionOfQuote, vi["aq"])))
return lines
@@ -688,7 +715,32 @@ def do_dros(dros: list[DefinedRunOn]|None) -> list[Line]:
raise NotImplementedError(f"Key of {k}")
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]:
Line.setParseText(parseText)
workList = restructure(defines)
@@ -786,6 +838,10 @@ def getDef(defines: Any) -> list[Line]:
lines += do_def(define)
except NotImplementedError:
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:
dros = trycast(list[DefinedRunOn], work["dros"])
if len(phrases) < 1:
@@ -815,6 +871,7 @@ def getDef(defines: Any) -> list[Line]:
"shortdef",
"vrs",
"dros",
'uros',
]:
raise NotImplementedError(f"Unknown key {k} in work")
if len(phrases) > 0:
@@ -845,10 +902,10 @@ def replaceCode(code:str) -> tuple[str, QTextCharFormat]:
token = fields[0]
if token == 'a_link':
text = fields[1]
fmt.setAnchorHref(fields[1])
fmt.setAnchorHref('auto://'+fields[1])
elif token in ['d_link', 'et_link', 'mat', 'sx', 'i_link']:
text = fields[1]
pre = 'word://'
pre = 'word:///'
if fields[2] == '':
fmt.setAnchorHref(pre+fields[1])
else:
@@ -859,18 +916,18 @@ def replaceCode(code:str) -> tuple[str, QTextCharFormat]:
fmt.setFontCapitalization(QFont.Capitalization.SmallCaps)
elif token == 'dxt':
if fields[3] == 'illustration':
fmt.setAnchorHref('article://'+fields[2])
fmt.setAnchorHref('article:///'+fields[2])
elif fields[3] == 'table':
fmt.setAnchorHref('table://'+fields[2])
fmt.setAnchorHref('table:///'+fields[2])
elif fields[3] != "":
fmt.setAnchorHref('sense://'+fields[3])
fmt.setAnchorHref('sense:///'+fields[3])
else:
fmt.setAnchorHref('word://'+fields[1])
fmt.setAnchorHref('word:///'+fields[1])
elif token == 'et_link':
if fields[2] != '':
fmt.setAnchorHref('etymology://'+fields[2])
fmt.setAnchorHref('etymology:///'+fields[2])
else:
fmt.setAnchorHref('etymology://' + fields[1])
fmt.setAnchorHref('etymology:///' + fields[1])
else:
raise NotImplementedError(f"Token {code} not implimented")
fmt.setForeground(r.linkColor)