Files
esl-reader/lib/read.py
2024-05-14 11:08:02 -04:00

461 lines
15 KiB
Python

from typing import Dict, List, Optional, cast
from PyQt6.QtCore import QPoint, QResource, Qt, QTimer, pyqtSignal, pyqtSlot
from PyQt6.QtGui import (
QBrush,
QColor,
QKeyEvent,
QPainter,
QPainterPath,
QPaintEvent,
QTextCharFormat,
QTextCursor,
)
from PyQt6.QtSql import QSqlQuery
from PyQt6.QtWidgets import QDialog, QWidget
from lib import query_error
from lib.preferences import Preferences
from lib.session import SessionDialog
from lib.sounds import SoundOff
from lib.words import Word
from ui.ReadDialog import Ui_ReadDialog
class ReadDialog(QDialog, Ui_ReadDialog):
playSound = pyqtSignal(str)
playAlert = pyqtSignal()
block: int
preferences: Dict[str, str | List[str]]
paragraphs = True
sessionSignal = pyqtSignal()
displayedWord = pyqtSignal(int)
newParagraph = pyqtSignal(int, int)
def __init__(
self, parent: Optional[QWidget], session: SessionDialog, person_id: int
) -> None:
self.session = session
super(ReadDialog, self).__init__(parent)
self.person_id = person_id
self.preferences = cast(Dict[str, str | List[str]], Preferences().get())
self.sound = SoundOff()
styleSheet = QResource(":/display.css").data().decode("utf-8")
readerFont = cast(str, self.preferences["readerFont"])
phoneticFont = cast(str, self.preferences["phoneticFont"])
styleSheet = styleSheet.replace("{readerFont}", readerFont)
styleSheet = styleSheet.replace("{phoneticFont}", phoneticFont)
self.setupUi(self)
#
# Override UI
#
self.returnBtn.setVisible(False)
#
# End overrides
#
self.load_book(self.person_id)
self.titleLbl.setText(self.book_title)
blockNumber = self.block
self.paraEdit.setReadOnly(True)
doc = self.paraEdit.document()
assert doc is not None
doc.setDefaultStyleSheet(styleSheet)
self.show_section(self.section_id)
self.block = blockNumber
self.savePosition()
self.stackedWidget.setCurrentIndex(0)
#
# Connect widgets
#
self.defineBtn.clicked.connect(self.defineAction)
# self.playBtn.clicked.connect(self.playAction)
self.scrollBtn.clicked.connect(self.scrollAction)
self.nextBtn.clicked.connect(self.nextAction)
self.prevBtn.clicked.connect(self.prevAction)
self.sessionBtn.clicked.connect(self.timerAction)
sb = self.paraEdit.verticalScrollBar()
assert sb is not None
sb.valueChanged.connect(self.scrollSlot)
# self.defEdit.selectionChanged.connect(self.recursiveDef)
self.returnBtn.clicked.connect(self.returnAction)
#
# Connect signals
#
self.displayedWord.connect(self.session.addWord)
self.newParagraph.connect(self.session.addBlock)
self.playSound.connect(self.sound.playSound)
self.playAlert.connect(self.sound.alert)
self.definition.pronounce.connect(self.sound.playSound)
self.definition.newWord.connect(self.newWord)
return
#
# Events
#
#
# slots
#
@pyqtSlot(str)
def newWord(self, word: str) -> None:
w = Word(word)
if not w.isValid():
self.playAlert.emit()
return
self.definition.setWord(w)
return
@pyqtSlot()
def timerAction(self) -> None:
if self.session.isActive(): # We are stopping
self.sessionBtn.setText(self.tr("Start"))
else:
self.sessionBtn.setText(self.tr("Stop"))
self.session.timerAction()
self.newParagraph.emit(self.section_id, self.block)
return
@pyqtSlot()
def sessionAction(self) -> None:
self.sessionSignal.emit()
self.newParagraph.emit(self.section_id, self.block)
return
@pyqtSlot()
def playAction(self) -> None:
if self.stackedWidget.currentIndex() != 0:
return
# find word
cursor = self.paraEdit.textCursor()
if cursor.hasSelection():
text = cursor.selectedText().strip()
else:
cursor.select(QTextCursor.SelectionType.WordUnderCursor)
text = cursor.selectedText().strip()
word = Word(text)
if not word.isValid():
self.playAlert.emit()
return
word.playPRS()
return
@pyqtSlot()
def scrollAction(self) -> None:
doc = self.paraEdit.document()
assert doc is not None
position = doc.findBlockByNumber(self.block).position()
cursor = self.paraEdit.textCursor()
cursor.setPosition(position)
pos = self.paraEdit.mapTo(
self, self.paraEdit.cursorRect(cursor).topLeft()
)
top = self.paraEdit.mapTo(self, QPoint(0, 0))
sb = self.paraEdit.verticalScrollBar()
assert sb is not None
value = sb.value()
#
# XXX - Where does "4" come from?
#
delta = pos.y() - top.y() - 4
self.pxPerTick = int(1000 / delta)
if self.pxPerTick < 1:
if delta < 0:
self.pxPerTick = -1
else:
self.pxPerTick = 1
steps = abs(delta / self.pxPerTick)
msPerTick = int(1000 / steps)
if msPerTick < 3:
msPerTick = 3
self.target = value + delta
if self.target > sb.maximum():
self.target = sb.maximum() - 1
timer = QTimer(self)
timer.timeout.connect(self.softTick)
timer.start(msPerTick)
return
@pyqtSlot()
def softTick(self) -> None:
sb = self.paraEdit.verticalScrollBar()
assert sb is not None
value = sb.value()
maxValue = sb.maximum()
sender: QTimer = cast(QTimer, self.sender())
if self.pxPerTick < 0: # moving content up
if value < self.target:
sender.stop()
return
else:
if value > self.target or value >= maxValue:
sender.stop()
return
value += self.pxPerTick
sb.setValue(value)
self.update()
return
@pyqtSlot(int)
def scrollSlot(self, _: int) -> None:
self.update()
return
@pyqtSlot()
def nextAction(self) -> None:
if self.paragraphs:
self.nextParagraph()
else:
self.nextSection()
return
@pyqtSlot()
def prevAction(self) -> None:
if self.paragraphs:
self.prevParagraph()
else:
self.prevSection()
return
#
# Called when the "define" button is pressed
#
@pyqtSlot()
def defineAction(self) -> None:
if self.stackedWidget.currentIndex() != 0:
return
cursor = self.paraEdit.textCursor()
if cursor.hasSelection():
text = cursor.selectedText().strip()
else:
cursor.select(cursor.SelectionType.WordUnderCursor)
text = cursor.selectedText().strip()
word = Word(text)
if not word.isValid():
self.playAlert.emit()
return
self.definition.setWord(word)
self.showDefinition()
return
def load_book(self, person_id: int) -> None:
query = QSqlQuery()
query.prepare(
"SELECT pb.*,b.title FROM people p "
"LEFT JOIN person_book pb "
"ON (p.book_id = pb.book_id "
"AND p.person_id = pb.person_id) "
"LEFT JOIN books b "
"ON (p.book_id = b.book_id) "
"WHERE p.person_id = :person_id"
)
query.bindValue(":person_id", person_id)
if not query.exec():
query_error(query)
if not query.next():
self.done(0)
self.book_id = query.value("book_id")
self.book_title = query.value("title")
self.section_id = query.value("section_id")
self.block = query.value("block")
self.sections = []
self.section_map = {}
self.sequence_map = {}
query.prepare(
"SELECT * FROM sections "
"WHERE book_id = :book_id "
"ORDER BY sequence"
)
query.bindValue(":book_id", self.book_id)
if not query.exec():
query_error(query)
while query.next():
self.sections.append(query.value("content"))
self.section_map[query.value("section_id")] = query.value(
"sequence"
)
self.sequence_map[query.value("sequence")] = query.value(
"section_id"
)
return
def show_section(self, section_id: int, start: bool = True) -> None:
sequence = self.section_map[section_id]
self.paraEdit.clear()
cursor = self.paraEdit.textCursor()
cursor.insertHtml(self.sections[sequence])
doc = self.paraEdit.document()
assert doc is not None
if start:
self.block = 0
else:
self.block = doc.blockCount() - 1
textBlock = doc.findBlockByNumber(self.block)
cursor.setPosition(textBlock.position())
self.paraEdit.setTextCursor(cursor)
self.paraEdit.ensureCursorVisible()
#
# Mark all the defined words with underlines
#
def_format = QTextCharFormat()
def_format.setFontUnderline(True)
cursor = QTextCursor(self.paraEdit.document())
query = QSqlQuery()
query.prepare(
"SELECT wb.*,w.word,w.definition FROM word_block wb "
"LEFT JOIN words w "
"ON (w.word_id = wb.word_id) "
"WHERE wb.section_id = :section_id"
)
query.bindValue(":section_id", section_id)
if not query.exec():
query_error(query)
while query.next():
#
# Define these variables so that the code matches
# the defining action
#
blockNum = query.value("block")
start = query.value("start")
end = query.value("end")
textBlock = doc.findBlockByNumber(blockNum)
cursor.setPosition(
start + textBlock.position(), QTextCursor.MoveMode.MoveAnchor
)
cursor.setPosition(
end + textBlock.position(), QTextCursor.MoveMode.KeepAnchor
)
cursor.mergeCharFormat(def_format)
return
#
# Event handlers
#
def keyReleaseEvent(self, event: Optional[QKeyEvent]) -> None:
self.nextBtn.setText(self.tr("Next Paragraph"))
self.prevBtn.setText(self.tr("Previous Paragraph"))
self.defineBtn.setText(self.tr("Definition"))
self.paragraphs = True
super().keyReleaseEvent(event)
return
def keyPressEvent(self, event: Optional[QKeyEvent]) -> None:
self.nextBtn.setText(self.tr("Next Section"))
self.prevBtn.setText(self.tr("Previous Secttion"))
self.defineBtn.setText(self.tr("Definition"))
self.paragraphs = False
super().keyPressEvent(event)
return
def paintEvent(self, _: QPaintEvent | None) -> None:
idx = self.stackedWidget.currentIndex()
if idx > 0:
return
doc = self.paraEdit.document()
assert doc is not None
position = doc.findBlockByNumber(self.block).position()
cursor = self.paraEdit.textCursor()
cursor.setPosition(position)
#
# rect is position in viewport coordenates.
rect = self.paraEdit.cursorRect(cursor)
c_pt = self.paraEdit.mapTo(self, rect.bottomLeft())
painter = QPainter(self)
brush = QBrush()
brush.setColor(QColor("green"))
brush.setStyle(Qt.BrushStyle.SolidPattern)
path = QPainterPath()
path.moveTo(0, 0)
path.lineTo(18, -rect.height() / 2.0)
path.lineTo(0, -rect.height())
path.lineTo(0, 0)
painter.translate(1.0, c_pt.y())
painter.fillPath(path, brush)
return
def nextParagraph(self) -> None:
self.block += 1
doc = self.paraEdit.document()
assert doc is not None
if self.block >= doc.blockCount():
self.nextSection()
self.savePosition()
self.newParagraph.emit(self.section_id, self.block)
return
def prevParagraph(self) -> None:
self.block -= 1
if self.block < 0:
self.prevSection()
return
self.savePosition()
return
@pyqtSlot()
def returnAction(self) -> None:
self.returnBtn.setVisible(False)
if self.paragraphs:
self.nextBtn.setText(self.tr("Next Paragraph"))
self.prevBtn.setText(self.tr("Previous Paragraph"))
else:
self.nextBtn.setText(self.tr("Next Section"))
self.prevBtn.setText(self.tr("Previous Section"))
self.stackedWidget.setCurrentIndex(0)
self.update()
return
def showDefinition(self) -> None:
self.returnBtn.setVisible(True)
self.nextBtn.setText(self.tr("Next Definition"))
self.prevBtn.setText(self.tr("Previous Definition"))
self.stackedWidget.setCurrentIndex(1)
self.update()
return
def scrollTo(self, position: int) -> None:
cursor = self.paraEdit.textCursor()
cursor.setPosition(position)
return
def savePosition(self) -> None:
cursor = self.paraEdit.textCursor()
doc = self.paraEdit.document()
assert doc is not None
cursor.setPosition(doc.findBlockByNumber(self.block).position())
self.paraEdit.setTextCursor(cursor)
self.scrollTo(cursor.position())
self.paraEdit.ensureCursorVisible()
self.update()
query = QSqlQuery()
query.prepare(
"UPDATE person_book SET section_id = :section_id, "
"block = :block "
"WHERE book_id = :book_id "
"AND person_id = :person_id"
)
query.bindValue(":section_id", self.section_id)
query.bindValue(":block", self.block)
query.bindValue(":book_id", self.book_id)
query.bindValue(":person_id", self.person_id)
if not query.exec():
query_error(query)
return
def nextSection(self) -> None:
sequence = self.section_map[self.section_id]
sequence += 1
self.section_id = self.sequence_map[sequence]
self.show_section(self.section_id)
self.savePosition()
return
def prevSection(self) -> None:
sequence = self.section_map[self.section_id]
if sequence < 1:
return
sequence -= 1
self.section_id = self.sequence_map[sequence]
self.show_section(self.section_id, False)
self.savePosition()
return