571 lines
19 KiB
Python
571 lines
19 KiB
Python
import json
|
|
import os
|
|
import re
|
|
from typing import cast
|
|
|
|
import requests
|
|
from PyQt6.QtCore import (
|
|
QFile,
|
|
QIODeviceBase,
|
|
QPoint,
|
|
QRect,
|
|
QResource,
|
|
Qt,
|
|
QTimer,
|
|
pyqtSlot,
|
|
)
|
|
from PyQt6.QtGui import (
|
|
QBrush,
|
|
QColor,
|
|
QFont,
|
|
QKeyEvent,
|
|
QMouseEvent,
|
|
QPainter,
|
|
QPainterPath,
|
|
QPaintEvent,
|
|
QTextBlockFormat,
|
|
QTextCharFormat,
|
|
QTextCursor,
|
|
QTextDocument,
|
|
QTextListFormat,
|
|
)
|
|
from PyQt6.QtSql import QSqlDatabase, QSqlQuery, QSqlQueryModel
|
|
from PyQt6.QtWidgets import QDialog, QPushButton
|
|
|
|
from main import query_error
|
|
from ui.EditDialog import Ui_Dialog
|
|
|
|
|
|
class EditDialog(QDialog, Ui_Dialog):
|
|
block: int
|
|
|
|
def __init__(self, person_id: int) -> None:
|
|
super(EditDialog, self).__init__()
|
|
self.person_id = person_id
|
|
if not QResource.registerResource(
|
|
os.path.join(os.path.dirname(__file__), "../ui/resources.rcc"), "/"
|
|
):
|
|
raise Exception("Unable to register resources.rcc")
|
|
styleSheet = QResource(":/display.css").data().decode("utf-8")
|
|
self.setupUi(self)
|
|
#
|
|
# Override UI
|
|
#
|
|
self.printBtn = QPushButton()
|
|
self.printBtn.setText("Print")
|
|
self.printBtn.setObjectName("printBtn")
|
|
self.verticalLayout.addWidget(self.printBtn)
|
|
#
|
|
# End overrides
|
|
#
|
|
self.load_book(self.person_id)
|
|
blockNumber = self.block
|
|
self.paraEdit.setReadOnly(True)
|
|
self.paraEdit.document().setDefaultStyleSheet(styleSheet)
|
|
self.defEdit.setReadOnly(True)
|
|
self.defEdit.document().setDefaultStyleSheet(styleSheet)
|
|
self.show_section(self.section_id)
|
|
self.block = blockNumber
|
|
self.savePosition()
|
|
self.stackedWidget.setCurrentIndex(0)
|
|
self.addBtn.clicked.connect(self.defineAction)
|
|
self.showBtn.clicked.connect(self.showAction)
|
|
self.nextBtn.clicked.connect(self.nextAction)
|
|
self.prevBtn.clicked.connect(self.prevAction)
|
|
self.nextParaBtn.clicked.connect(self.nextParaAction)
|
|
self.prevParaBtn.clicked.connect(self.prevParaAction)
|
|
self.printBtn.clicked.connect(self.printAction)
|
|
self.paraEdit.verticalScrollBar().valueChanged.connect(self.scrollSlot)
|
|
self.scrollBtn.clicked.connect(self.scrollAction)
|
|
return
|
|
|
|
def defToHtml(self, word: str, definition) -> str:
|
|
html = f'<h1 class="def-word">{word}</h1>' + "\n"
|
|
try:
|
|
html += f"<p class=\"phonetic\">{definition['phonetic']}</p>" + "\n"
|
|
except Exception:
|
|
pass
|
|
html += '<ul class="outer">' + "\n"
|
|
for meaning in definition["meanings"]:
|
|
html += f"<li>{meaning['partOfSpeech']}"
|
|
html += '<ul class="inner">'
|
|
for a_def in meaning["definitions"]:
|
|
html += f"<li>{a_def['definition']}</li>\n"
|
|
html += "</ul>\n"
|
|
html += "</ul>\n<p/>\n"
|
|
return html
|
|
|
|
@pyqtSlot()
|
|
def printAction(self) -> None:
|
|
html = "<!DOCTYPE html>\n<html>\n<head>\n"
|
|
html += (
|
|
'<link href="https://fonts.cdnfonts.com/css/open-dyslexic" rel="stylesheet">'
|
|
+ "\n"
|
|
)
|
|
html += (
|
|
'<link href="https://fonts.cdnfonts.com/css/tex-gyre-heros" rel="stylesheet">'
|
|
+ "\n"
|
|
)
|
|
html += '<style type="text/css">\n'
|
|
style = QResource(":/print.css").data().decode("utf-8")
|
|
html += style
|
|
html += "</style>\n</head>\n<body>\n"
|
|
query = QSqlQuery()
|
|
query.prepare(
|
|
"SELECT w.* FROM word_block wb "
|
|
"LEFT JOIN words w "
|
|
"ON (w.word_id = wb.word_id) "
|
|
"WHERE wb.section_id = :section_id "
|
|
"ORDER BY w.word"
|
|
)
|
|
query.bindValue(":section_id", self.section_id)
|
|
if not query.exec():
|
|
query_error(query)
|
|
while query.next():
|
|
word = query.value("word")
|
|
definition = json.loads(query.value("definition"))
|
|
html += self.defToHtml(word, definition)
|
|
html += "\n"
|
|
html += "<hr/>\n"
|
|
html += '<div class="text">' + "\n"
|
|
text = self.sections[self.section_map[self.section_id]]
|
|
text = re.sub(r"</?body>", "", text)
|
|
html += text
|
|
html += "\n</div>\n"
|
|
html += "\n</body>\n</html>\n"
|
|
qf = QFile("out.html")
|
|
if qf.open(QIODeviceBase.OpenModeFlag.WriteOnly):
|
|
qf.write(html.encode("utf-8"))
|
|
qf.close()
|
|
print("Printed!")
|
|
return
|
|
|
|
@pyqtSlot()
|
|
def scrollAction(self) -> None:
|
|
position = (
|
|
self.paraEdit.document().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))
|
|
value = self.paraEdit.verticalScrollBar().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
|
|
print(f"delta: {delta}, pixels: {self.pxPerTick}, ms: {msPerTick}")
|
|
timer = QTimer(self)
|
|
timer.timeout.connect(self.softTick)
|
|
timer.start(msPerTick)
|
|
return
|
|
|
|
@pyqtSlot()
|
|
def softTick(self) -> None:
|
|
value = self.paraEdit.verticalScrollBar().value()
|
|
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:
|
|
sender.stop()
|
|
return
|
|
value += self.pxPerTick
|
|
self.paraEdit.verticalScrollBar().setValue(value)
|
|
self.update()
|
|
return
|
|
|
|
@pyqtSlot(int)
|
|
def scrollSlot(self, value: int) -> None:
|
|
self.update()
|
|
return
|
|
|
|
def load_book(self, person_id: int) -> None:
|
|
query = QSqlQuery()
|
|
query.prepare(
|
|
"SELECT pb.* FROM people p "
|
|
"LEFT JOIN person_book pb "
|
|
"ON (p.book_id = pb.book_id "
|
|
"AND p.person_id = pb.person_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.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.setWindowTitle(f"Section {sequence}")
|
|
self.paraEdit.clear()
|
|
cursor = self.paraEdit.textCursor()
|
|
cursor.insertHtml(self.sections[sequence])
|
|
if start:
|
|
self.block = 0
|
|
else:
|
|
self.block = self.paraEdit.document().blockCount() - 1
|
|
textBlock = self.paraEdit.document().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 = self.paraEdit.document().findBlockByNumber(blockNum)
|
|
cursor.setPosition(
|
|
start + textBlock.position(), QTextCursor.MoveMode.MoveAnchor
|
|
)
|
|
cursor.setPosition(
|
|
end + textBlock.position(), QTextCursor.MoveMode.KeepAnchor
|
|
)
|
|
word = query.value("word")
|
|
definition = json.loads(query.value("definition"))
|
|
try:
|
|
phonetic = definition["phonetic"]
|
|
def_format.setToolTip(
|
|
f'<font size="24">{word}:<br/><font family="Tex Gyre Heros">{phonetic}</font></font>'
|
|
)
|
|
cursor.mergeCharFormat(def_format)
|
|
except:
|
|
pass
|
|
|
|
return
|
|
|
|
#
|
|
# Event handlers
|
|
#
|
|
def mousePressEvent(self, event: QMouseEvent | None) -> None:
|
|
return
|
|
|
|
def keyPressEvent(self, event: QKeyEvent) -> None:
|
|
print(event)
|
|
super().keyPressEvent(event)
|
|
return
|
|
|
|
def paintEvent(self, e: QPaintEvent | None) -> None:
|
|
idx = self.stackedWidget.currentIndex()
|
|
if idx > 0:
|
|
return
|
|
position = (
|
|
self.paraEdit.document().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
|
|
|
|
@pyqtSlot()
|
|
def nextParaAction(self) -> None:
|
|
self.block += 1
|
|
if self.block >= self.paraEdit.document().blockCount():
|
|
self.nextAction()
|
|
return
|
|
self.savePosition()
|
|
return
|
|
|
|
@pyqtSlot()
|
|
def prevParaAction(self) -> None:
|
|
self.block -= 1
|
|
if self.block < 0:
|
|
self.prevAction()
|
|
return
|
|
self.savePosition()
|
|
return
|
|
|
|
@pyqtSlot()
|
|
def defineAction(self) -> None:
|
|
#
|
|
# Find the word
|
|
#
|
|
cursor = self.paraEdit.textCursor()
|
|
word = cursor.selectedText()
|
|
start = cursor.selectionStart()
|
|
end = cursor.selectionEnd()
|
|
if start != end:
|
|
word = word.strip()
|
|
if len(word) == 0 or word.find(" ") >= 0:
|
|
return
|
|
if start > end:
|
|
tmp = start
|
|
start = end
|
|
end = tmp
|
|
#
|
|
# Find the block
|
|
#
|
|
textBlock = self.paraEdit.document().findBlock(cursor.position())
|
|
blockNum = textBlock.blockNumber()
|
|
query = QSqlQuery()
|
|
query.prepare("SELECT * FROM words WHERE word = :word")
|
|
query.bindValue(":word", word)
|
|
if not query.exec():
|
|
query_error(query)
|
|
if query.next(): # we have something
|
|
self.defined(query.value("word_id"), blockNum, start, end)
|
|
return
|
|
#
|
|
# Get the defintion
|
|
#
|
|
response = requests.get(
|
|
f"https://api.dictionaryapi.dev/api/v2/entries/en/{word}"
|
|
)
|
|
if response.status_code != 200:
|
|
print(response.content)
|
|
return
|
|
definitions = json.loads(response.content.decode("utf-8"))
|
|
definition = definitions[0]
|
|
query.prepare(
|
|
"INSERT INTO words (word, definition) "
|
|
"VALUES (:word, :definition)"
|
|
)
|
|
query.bindValue(":word", word)
|
|
query.bindValue(":definition", json.dumps(definition))
|
|
if not query.exec():
|
|
query_error(query)
|
|
start = start - textBlock.position()
|
|
end = end - textBlock.position()
|
|
self.defined(query.lastInsertId(), blockNum, start, end)
|
|
return
|
|
|
|
def defined(
|
|
self, word_id: int, blockNum: int, start: int, end: int
|
|
) -> None:
|
|
query = QSqlQuery()
|
|
query.prepare(
|
|
"SELECT * FROM word_block "
|
|
"WHERE section_id = :section_id "
|
|
"AND block = :block "
|
|
"AND start = :start "
|
|
"AND end = :end"
|
|
)
|
|
query.bindValue(":word_id", word_id)
|
|
query.bindValue(":section_id", self.section_id)
|
|
query.bindValue(":block", blockNum)
|
|
query.bindValue(":start", start)
|
|
query.bindValue(":end", end)
|
|
if not query.exec():
|
|
query_error(query)
|
|
if not query.next():
|
|
query.prepare(
|
|
"INSERT INTO word_block VALUES "
|
|
"( :word_id, :section_id, :block, :start, :end)"
|
|
)
|
|
query.bindValue(":word_id", word_id)
|
|
query.bindValue(":section_id", self.section_id)
|
|
query.bindValue(":block", blockNum)
|
|
query.bindValue(":start", start)
|
|
query.bindValue(":end", end)
|
|
if not query.exec():
|
|
query_error(query)
|
|
|
|
def_format = QTextCharFormat()
|
|
def_format.setFontUnderline(True)
|
|
cursor = QTextCursor(self.paraEdit.document())
|
|
textBlock = self.paraEdit.document().findBlockByNumber(blockNum)
|
|
cursor.setPosition(
|
|
start + textBlock.position(), QTextCursor.MoveMode.MoveAnchor
|
|
)
|
|
cursor.setPosition(
|
|
end + textBlock.position(), QTextCursor.MoveMode.KeepAnchor
|
|
)
|
|
cursor.mergeCharFormat(def_format)
|
|
return
|
|
|
|
def display_definition(self) -> None:
|
|
cursor = self.paraEdit.textCursor()
|
|
textBlock = self.paraEdit.document().findBlock(cursor.position())
|
|
blockNum = textBlock.blockNumber()
|
|
query = QSqlQuery()
|
|
query.prepare(
|
|
"SELECT w.* FROM word_block wb "
|
|
"LEFT JOIN words w "
|
|
"ON (w.word_id = wb.word_id) "
|
|
"WHERE :position BETWEEN wb.start AND wb.end "
|
|
"AND wb.block = :block AND wb.section_id = :section_id"
|
|
)
|
|
query.bindValue(":position", cursor.position() - textBlock.position())
|
|
query.bindValue(":block", blockNum)
|
|
query.bindValue(":section_id", self.section_id)
|
|
if not query.exec():
|
|
query_error(query)
|
|
if not query.next():
|
|
return
|
|
word = query.value("word")
|
|
definition = json.loads(query.value("definition"))
|
|
self.defEdit.document().clear()
|
|
cursor = self.defEdit.textCursor()
|
|
cursor.insertHtml(self.defToHtml(word,definition))
|
|
cursor.setPosition(0)
|
|
self.defEdit.setTextCursor(cursor)
|
|
print(self.defEdit.document().toHtml())
|
|
return
|
|
|
|
@pyqtSlot()
|
|
def showAction(self) -> None:
|
|
idx = self.stackedWidget.currentIndex()
|
|
if idx == 0:
|
|
self.display_definition()
|
|
self.stackedWidget.setCurrentIndex(1 - idx)
|
|
self.update()
|
|
return
|
|
|
|
def nextDefinition(self) -> None:
|
|
cursor = self.paraEdit.textCursor()
|
|
position = cursor.position()
|
|
block = cursor.block()
|
|
found = None
|
|
while block and not found:
|
|
formats = block.textFormats()
|
|
for f in formats:
|
|
wc = QTextCursor(cursor)
|
|
wc.setPosition(f.start)
|
|
wc.movePosition(
|
|
QTextCursor.MoveOperation.Right,
|
|
QTextCursor.MoveMode.KeepAnchor,
|
|
f.length,
|
|
)
|
|
word = wc.selectedText()
|
|
cf = wc.charFormat()
|
|
if f.start <= position:
|
|
continue
|
|
if not cf.fontUnderline():
|
|
continue
|
|
if not found:
|
|
found = f
|
|
elif f.start < found.start:
|
|
found = f
|
|
if found:
|
|
cursor.setPosition(found.start)
|
|
self.paraEdit.setTextCursor(cursor)
|
|
block = block.next()
|
|
self.display_definition()
|
|
return
|
|
|
|
def scrollTo(self, position: int) -> None:
|
|
cursor = self.paraEdit.textCursor()
|
|
cursor.setPosition(position)
|
|
rect = self.paraEdit.cursorRect(cursor)
|
|
return
|
|
|
|
def savePosition(self) -> None:
|
|
cursor = self.paraEdit.textCursor()
|
|
cursor.setPosition(
|
|
self.paraEdit.document().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
|
|
|
|
@pyqtSlot()
|
|
def nextAction(self) -> None:
|
|
if self.stackedWidget.currentIndex() == 1:
|
|
self.nextDefinition()
|
|
return
|
|
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
|
|
|
|
@pyqtSlot()
|
|
def prevAction(self) -> None:
|
|
if self.stackedWidget.currentIndex() == 1:
|
|
return
|
|
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
|