import json import os import re from typing import cast import requests from PyQt6.QtCore import ( QFile, QIODeviceBase, QPoint, QRect, QResource, Qt, QTimer, pyqtSignal, 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 paragraphs = True sessionSignal = pyqtSignal() displayedWord = pyqtSignal(int) newParagraph = pyqtSignal(int, int) def __init__(self, session, person_id: int) -> None: self.session = session 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 # # # 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) # # Connect widgets # self.defineBtn.clicked.connect(self.defineAction) self.printBtn.clicked.connect(self.printAction) self.scrollBtn.clicked.connect(self.scrollAction) self.nextBtn.clicked.connect(self.nextAction) self.prevBtn.clicked.connect(self.prevAction) # self.sessionSignal.connect(self.session.timerAction) # self.sessionBtn.clicked.connect(self.sessionAction) self.sessionBtn.clicked.connect(self.session.timerAction) self.paraEdit.verticalScrollBar().valueChanged.connect(self.scrollSlot) # # Connect signals # self.displayedWord.connect(self.session.addWord) self.newParagraph.connect(self.session.addBlock) return # # slots # @pyqtSlot() def sessionAction(self) -> None: self.sessionSignal.emit() self.session.addParagraph(self.section_id, self.block) return @pyqtSlot() def printAction(self) -> None: html = "\n\n\n" html += ( '' + "\n" ) html += ( '' + "\n" ) html += '\n\n\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 COLLATE NOCASE" ) 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 += "
\n" html += '
' + "\n" text = self.sections[self.section_map[self.section_id]] text = re.sub(r"", "", text) html += text html += "\n
\n" html += "\n\n\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 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 @pyqtSlot() def nextAction(self) -> None: if self.stackedWidget.currentIndex() == 1: print("Next Definition") self.nextDefinition() elif self.paragraphs: self.nextParagraph() else: self.nextSection() return @pyqtSlot() def prevAction(self) -> None: if self.stackedWidget.currentIndex() == 1: print("Previous Definition") self.prevDefinition() elif self.paragraphs: self.prevParagraph() else: self.prevSection() return @pyqtSlot() def defineAction(self) -> None: if self.paragraphs: self.showDefinition() else: self.addWord() return def defToHtml(self, word: str, definition) -> str: html = f'

{word}

' + "\n" try: html += f"

{definition['phonetic']}

" + "\n" except Exception: pass html += '\n

\n" return html 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.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'{word}:
{phonetic}
' ) cursor.mergeCharFormat(def_format) except: pass return # # Event handlers # def keyReleaseEvent(self, event: QKeyEvent) -> None: self.nextBtn.setText("Next Para") self.prevBtn.setText("Prev Para") self.defineBtn.setText("Show Def") self.paragraphs = True super().keyReleaseEvent(event) return def keyPressEvent(self, event: QKeyEvent) -> None: self.nextBtn.setText("Next Sect") self.prevBtn.setText("Prev Sect") self.defineBtn.setText("Add Def") self.paragraphs = False 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 def nextParagraph(self) -> None: self.block += 1 if self.block >= self.paraEdit.document().blockCount(): self.nextSection() return 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 def addWord(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: cursor.select(QTextCursor.SelectionType.WordUnderCursor) word = cursor.selectedText() word = word.strip() start = cursor.selectionStart() end = cursor.selectionEnd() if start > end: tmp = start start = end end = tmp # # Find the block # textBlock = self.paraEdit.document().findBlock(cursor.position()) blockNum = textBlock.blockNumber() start = start - textBlock.position() end = end - textBlock.position() 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(f"{word}: {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) 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 # XXX - rename def display_definition(self) -> None: cursor = self.paraEdit.textCursor() fmt = cursor.charFormat() if not fmt.fontUnderline(): self.addWord() 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) self.displayedWord.emit(query.value("word_id")) return def showDefinition(self) -> None: idx = self.stackedWidget.currentIndex() if idx == 0: self.defineBtn.setText("Read") self.nextBtn.setText("Next Def") self.prevBtn.setText("Prev Def") self.display_definition() else: self.defineBtn.setText("Definition") if self.paragraphs: self.nextBtn.setText("Next Para") self.prevBtn.setText("Prev Para") else: self.nextBtn.setText("Next Sect") self.prevBtn.setText("Prev Sect") self.stackedWidget.setCurrentIndex(1 - idx) self.update() return def nextDefinition(self) -> None: cursor = self.paraEdit.textCursor() formats = self.paraEdit.document().allFormats() found = None 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) 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 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