diff --git a/lib/person.py b/lib/person.py
index 8a29a39..adf8fe5 100644
--- a/lib/person.py
+++ b/lib/person.py
@@ -7,6 +7,7 @@ from email import policy
from email.message import EmailMessage
from html.parser import HTMLParser
from io import StringIO
+from typing import Any, List
import css_inline
from PyQt6.QtCore import QResource, QSize, Qt, QUrl, pyqtSlot
@@ -21,7 +22,7 @@ from ui.PeopleDialog import Ui_Dialog
class blockHandler(HTMLParser):
text = ""
- blocks = []
+ blocks: List[str] = []
active = 0
tags = [
"h1",
@@ -39,7 +40,7 @@ class blockHandler(HTMLParser):
]
space = ["b", "i", "em", "st", "span"]
- def __init__(self):
+ def __init__(self) -> None:
super().__init__()
self.reset()
self.strict = False
@@ -49,7 +50,7 @@ class blockHandler(HTMLParser):
self.active = 0
return
- def handle_starttag(self, tag, attrs):
+ def handle_starttag(self, tag: str, attrs: Any) -> None:
if not tag in self.tags:
return
self.active += 1
@@ -58,7 +59,7 @@ class blockHandler(HTMLParser):
self.text += f"<{tag}>"
return
- def handle_endtag(self, tag):
+ def handle_endtag(self, tag: str) -> None:
if not tag in self.tags:
return
self.active -= 1
@@ -71,21 +72,21 @@ class blockHandler(HTMLParser):
self.active = 0
return
- def handle_data(self, data):
+ def handle_data(self, data: str) -> None:
self.text += data
return
- def get_block(self, block):
+ def get_block(self, block: int) -> str:
return self.blocks[block]
class MLStripper(HTMLParser):
- def __init__(self):
+ def __init__(self) -> None:
super().__init__()
self.reset()
return
- def reset(self):
+ def reset(self) -> None:
super().reset()
self.strict = False
self.convert_charrefs = True
@@ -93,13 +94,13 @@ class MLStripper(HTMLParser):
self.first = True
return
- def handle_data(self, d):
+ def handle_data(self, d: str) -> None:
if self.first:
self.text.write(d)
self.first = False
return
- def get_data(self):
+ def get_data(self) -> str:
return self.text.getvalue()
@@ -110,7 +111,7 @@ class PersonDialog(QDialog, Ui_Dialog):
person_id = 0
inliner = css_inline.CSSInliner(keep_style_tags=True, keep_link_tags=True)
- def __init__(self, *args, **kwargs):
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
self.person_id = kwargs.pop("person_id", 0)
super(PersonDialog, self).__init__(*args, **kwargs)
self.setupUi(self)
@@ -128,7 +129,7 @@ class PersonDialog(QDialog, Ui_Dialog):
self.bookCombo.setModel(model)
self.bookCombo.setModelColumn(1)
self.bookCombo.setCurrentIndex(-1)
- model = QStandardItemModel()
+ model: QStandardItemModel = QStandardItemModel() # type: ignore[no-redef]
self.sectionCombo.setPlaceholderText("Select A Section")
self.sectionCombo.setModel(model)
self.sectionCombo.setEnabled(False)
@@ -349,7 +350,7 @@ class PersonDialog(QDialog, Ui_Dialog):
return
@pyqtSlot()
- def checkLineEdits(self):
+ def checkLineEdits(self) -> None:
name = self.nameEdit.text().strip()
org = self.orgEdit.text().strip()
button = self.buttonBox.button(QDialogButtonBox.StandardButton.Ok)
diff --git a/lib/preferences.py b/lib/preferences.py
index 779def1..451acba 100644
--- a/lib/preferences.py
+++ b/lib/preferences.py
@@ -1,41 +1,42 @@
import json
import os
+from typing import Any, Dict, List, Optional, Type, cast
from PyQt6.QtCore import Qt, pyqtSlot
from PyQt6.QtMultimedia import QMediaDevices
-from PyQt6.QtWidgets import QAbstractItemView, QDialog, QListWidgetItem
+from PyQt6.QtWidgets import QAbstractItemView, QDialog, QListWidgetItem, QWidget
from ui.Preferences import Ui_Dialog
class Preferences(QDialog, Ui_Dialog):
_instance = None
+ preferences: Dict[str, str | List[str]]
- def __new__(cls):
+ def __new__(cls: Type[Preferences]) -> Preferences:
if cls._instance:
return cls._instance
cls._instance = super(Preferences, cls).__new__(cls)
return cls._instance
@pyqtSlot(int)
- def done(self, r):
+ def done(self, r: int) -> None:
self.hide()
super().done(r)
return
@pyqtSlot()
- def exec(self):
+ def exec(self) -> int:
self.show()
- super().exec()
- return
+ return super().exec()
@pyqtSlot()
- def open(self):
+ def open(self) -> None:
self.show()
super().open()
return
- def __init__(self, *args, **kwargs):
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
super(Preferences, self).__init__(*args, **kwargs)
self.setupUi(self)
self.hide()
@@ -61,7 +62,7 @@ class Preferences(QDialog, Ui_Dialog):
self.setCurrent()
return
- def setCurrent(self):
+ def setCurrent(self) -> None:
if os.path.exists("preferences.json"):
with open("preferences.json", "r") as f:
self.preferences = json.load(f)
@@ -94,12 +95,14 @@ class Preferences(QDialog, Ui_Dialog):
self.phoneticsCombo.setCurrentIndex(index)
return
- def get(self, name: str = None):
+ def get(
+ self, name: Optional[str] = None
+ ) -> str | List[str] | Dict[str, str | List[str]]:
if not name:
return self.preferences
return self.preferences[name]
- def accept(self):
+ def accept(self) -> None:
self.preferences["readerFont"] = self.readerCombo.currentFont().family()
self.preferences[
"phoneticFont"
diff --git a/lib/read.py b/lib/read.py
index 469e5eb..d407e50 100644
--- a/lib/read.py
+++ b/lib/read.py
@@ -1,6 +1,6 @@
import json
import re
-from typing import cast
+from typing import Any, Dict, List, Optional, cast
import requests
from PyQt6.QtCore import (
@@ -30,35 +30,38 @@ from PyQt6.QtGui import (
QTextListFormat,
)
from PyQt6.QtSql import QSqlDatabase, QSqlQuery, QSqlQueryModel
-from PyQt6.QtWidgets import QDialog, QPushButton
+from PyQt6.QtWidgets import QDialog, QPushButton, QTextEdit, QWidget
+from lib.preferences import Preferences
+from lib.session import SessionDialog
+from lib.sounds import SoundOff
from main import query_error
from ui.ReadDialog import Ui_Dialog
-from lib.preferences import Preferences
-from lib.sounds import SoundOff
+
class ReadDialog(QDialog, Ui_Dialog):
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, session, person_id: int) -> None:
+
+ 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 = Preferences().get()
+ self.preferences = cast(Dict[str, str | List[str]], Preferences().get())
self.sound = SoundOff()
styleSheet = QResource(":/display.css").data().decode("utf-8")
- styleSheet = styleSheet.replace(
- '{readerFont}',self.preferences['readerFont']
- )
- styleSheet = styleSheet.replace(
- '{phoneticFont}',self.preferences['phoneticFont']
- )
+ 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
@@ -88,7 +91,7 @@ class ReadDialog(QDialog, Ui_Dialog):
self.prevBtn.clicked.connect(self.prevAction)
self.sessionBtn.clicked.connect(self.timerAction)
self.paraEdit.verticalScrollBar().valueChanged.connect(self.scrollSlot)
- #self.defEdit.selectionChanged.connect(self.recursiveDef)
+ # self.defEdit.selectionChanged.connect(self.recursiveDef)
self.returnBtn.clicked.connect(self.returnAction)
#
# Connect signals
@@ -107,16 +110,17 @@ class ReadDialog(QDialog, Ui_Dialog):
# slots
#
@pyqtSlot()
- def timerAction(self):
- if self.session.isActive(): # We are stopping
+ def timerAction(self) -> None:
+ if self.session.isActive(): # We are stopping
self.sessionBtn.setText("Start")
else:
self.sessionBtn.setText("Stop")
self.session.timerAction()
self.newParagraph.emit(self.section_id, self.block)
return
+
@pyqtSlot()
- def recursiveDef(self):
+ def recursiveDef(self) -> None:
cursor = self.defEdit.textCursor()
selection = cursor.selectedText().strip()
if len(selection) <= 0:
@@ -125,7 +129,7 @@ class ReadDialog(QDialog, Ui_Dialog):
query.prepare("SELECT * FROM words " "WHERE word = :word")
query.bindValue(":word", selection)
if not query.exec():
- query_error()
+ query_error(query)
if not query.next():
response = requests.get(
f"https://api.dictionaryapi.dev/api/v2/entries/en/{selection}"
@@ -138,13 +142,13 @@ class ReadDialog(QDialog, Ui_Dialog):
else:
definition = query.value("definition")
word_id = query.value("word_id")
- self.setDefEdit(selection, word_id, definition)
+ self.setDefEdit(selection, word_id, definition)
return
@pyqtSlot()
def sessionAction(self) -> None:
self.sessionSignal.emit()
- self.session.addParagraph(self.section_id, self.block)
+ self.newParagraph.emit(self.section_id, self.block)
return
@pyqtSlot()
@@ -248,10 +252,7 @@ class ReadDialog(QDialog, Ui_Dialog):
@pyqtSlot()
def nextAction(self) -> None:
- if self.stackedWidget.currentIndex() == 1:
- print("Next Definition")
- self.nextDefinition()
- elif self.paragraphs:
+ if self.paragraphs:
self.nextParagraph()
else:
self.nextSection()
@@ -259,15 +260,15 @@ class ReadDialog(QDialog, Ui_Dialog):
@pyqtSlot()
def prevAction(self) -> None:
- if self.stackedWidget.currentIndex() == 1:
- print("Previous Definition")
- self.prevDefinition()
- elif self.paragraphs:
+ if self.paragraphs:
self.prevParagraph()
else:
self.prevSection()
return
+ #
+ # Called when the "define" button is pressed
+ #
@pyqtSlot()
def defineAction(self) -> None:
editor = self.paraEdit
@@ -277,10 +278,10 @@ class ReadDialog(QDialog, Ui_Dialog):
self.showDefinition()
return
- def defToHtml(self, word: str, definition) -> str:
+ def defToHtml(self, word: str, definition: Dict[str, Any]) -> str:
html = f'
{word}
' + "\n"
try:
- words = []
+ words: List[str] = []
for phonetic in definition["phonetics"]:
if phonetic["text"] in words:
continue
@@ -389,7 +390,7 @@ class ReadDialog(QDialog, Ui_Dialog):
#
# Event handlers
#
- def keyReleaseEvent(self, event: QKeyEvent) -> None:
+ def keyReleaseEvent(self, event: Optional[QKeyEvent]) -> None:
self.nextBtn.setText("Next Para")
self.prevBtn.setText("Prev Para")
self.defineBtn.setText("Show Def")
@@ -397,7 +398,7 @@ class ReadDialog(QDialog, Ui_Dialog):
super().keyReleaseEvent(event)
return
- def keyPressEvent(self, event: QKeyEvent) -> None:
+ def keyPressEvent(self, event: Optional[QKeyEvent]) -> None:
self.nextBtn.setText("Next Sect")
self.prevBtn.setText("Prev Sect")
self.defineBtn.setText("Add Def")
@@ -447,7 +448,7 @@ class ReadDialog(QDialog, Ui_Dialog):
self.savePosition()
return
- def addWord(self,editor) -> None:
+ def addWord(self, editor: QTextEdit) -> None:
#
# Find the word
#
@@ -470,7 +471,9 @@ class ReadDialog(QDialog, Ui_Dialog):
#
# Find the block
#
- textBlock = editor.document().findBlock(cursor.position())
+ document = editor.document()
+ assert document is not None
+ textBlock = document.findBlock(cursor.position())
blockNum = textBlock.blockNumber()
start = start - textBlock.position()
end = end - textBlock.position()
@@ -490,7 +493,7 @@ class ReadDialog(QDialog, Ui_Dialog):
f"https://api.dictionaryapi.dev/api/v2/entries/en/{word}"
)
if response.status_code != 200:
- print(f"{word}: {response.content}")
+ print(f"{word}: {response.content.decode('utf8')}")
self.playAlert.emit()
return
definitions = json.loads(response.content.decode("utf-8"))
@@ -551,6 +554,10 @@ class ReadDialog(QDialog, Ui_Dialog):
return
# XXX - rename
+ #
+ # Create a definition for the word under the cursor on the current
+ # panel.
+ #
def display_definition(self, idx: int) -> bool:
if idx == 0:
editor = self.paraEdit
@@ -559,14 +566,11 @@ class ReadDialog(QDialog, Ui_Dialog):
cursor = editor.textCursor()
cursor.select(QTextCursor.SelectionType.WordUnderCursor)
word = cursor.selectedText()
- fmt = cursor.charFormat()
- if not fmt.fontUnderline():
- self.addWord(editor)
+ # fmt = cursor.charFormat()
+ # if not fmt.fontUnderline():
+ # self.addWord(editor)
query = QSqlQuery()
- query.prepare(
- "SELECT w.* FROM words w "
- "WHERE word = :word"
- )
+ query.prepare("SELECT w.* FROM words w " "WHERE word = :word")
query.bindValue(":word", word)
if not query.exec():
query_error(query)
@@ -577,7 +581,9 @@ class ReadDialog(QDialog, Ui_Dialog):
self.setDefEdit(word, query.value("word_id"), definition)
return True
- def setDefEdit(self, word, word_id, definition):
+ def setDefEdit(
+ self, word: str, word_id: int, definition: Dict[str, str]
+ ) -> None:
if "phonetics" in definition:
self.phonetics = definition["phonetics"]
else:
@@ -615,34 +621,6 @@ class ReadDialog(QDialog, Ui_Dialog):
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(0)
- return
-
def scrollTo(self, position: int) -> None:
cursor = self.paraEdit.textCursor()
cursor.setPosition(position)
diff --git a/lib/session.py b/lib/session.py
index 3923f55..33170ec 100644
--- a/lib/session.py
+++ b/lib/session.py
@@ -1,4 +1,5 @@
from datetime import datetime, timedelta
+from typing import Optional, cast
from PyQt6.QtCore import QModelIndex, Qt, QTime, QTimer, pyqtSignal, pyqtSlot
from PyQt6.QtGui import (
@@ -11,7 +12,7 @@ from PyQt6.QtGui import (
QTextDocument,
)
from PyQt6.QtSql import QSqlQuery
-from PyQt6.QtWidgets import QDialog
+from PyQt6.QtWidgets import QCheckBox, QDialog, QListView, QMessageBox
from main import query_error
from ui.SessionDialog import Ui_Dialog
@@ -25,7 +26,7 @@ class SessionDialog(QDialog, Ui_Dialog):
timer = QTimer()
startTime = datetime.now()
- totalTime = 0 # seconds
+ totalTime = timedelta(seconds=0)
sessionStart = None
sessionEnd = None
blocks = QStandardItemModel()
@@ -49,8 +50,11 @@ class SessionDialog(QDialog, Ui_Dialog):
flag = index.data(SessionDialog.WordImportantRole)
flag = 1 - flag
model = index.model()
+ assert model is not None
+ model = cast(QStandardItemModel, model)
model.setData(index, flag, SessionDialog.WordImportantRole)
item = model.itemFromIndex(index)
+ assert item is not None
if flag:
item.setForeground(Qt.GlobalColor.red)
else:
@@ -62,13 +66,13 @@ class SessionDialog(QDialog, Ui_Dialog):
@pyqtSlot()
def resetForm(self) -> None:
self.nameLbl.setText("")
- self.totalTime = timedelta()
+ self.totalTime = timedelta(seconds=0)
self.wordView.model().clear()
self.textBrowser.document().clear()
return
@pyqtSlot(int)
- def setPerson(self, person_id) -> None:
+ def setPerson(self, person_id: int) -> None:
self.resetForm()
self.person_id = person_id
query = QSqlQuery()
@@ -79,12 +83,12 @@ class SessionDialog(QDialog, Ui_Dialog):
if not query.next():
raise Exception(f"Bad person_id: {person_id}")
self.nameLbl.setText(query.value("name"))
- self.totalTime = timedelta()
+ self.totalTime = timedelta(seconds=0)
self.wordView.model().clear()
return
@pyqtSlot()
- def rejected(self) -> None:
+ def rejected(self): # type: ignore[no-untyped-def]
msg = QMessageBox()
msg.setText("There is unsaved data.")
msg.setInformativeText("Do you want to save the session?")
@@ -100,7 +104,7 @@ class SessionDialog(QDialog, Ui_Dialog):
super().reject()
return
self.accept()
- self.done()
+ self.done(QDialog.DialogCode.Accepted)
return
@pyqtSlot()
@@ -164,7 +168,8 @@ class SessionDialog(QDialog, Ui_Dialog):
@pyqtSlot()
def tickAction(self) -> None:
- delta = self.totalTime + (datetime.now() - self.startTime)
+ td = datetime.now() - self.startTime
+ delta = self.totalTime + td
seconds = delta.seconds % 60
minutes = int(delta.seconds / 60) % 60
hours = int(delta.seconds / 3600)
@@ -211,7 +216,7 @@ class SessionDialog(QDialog, Ui_Dialog):
return
@pyqtSlot(int, int)
- def addBlock(self, section_id, block) -> None:
+ def addBlock(self, section_id: int, block: int) -> None:
if not self.activeBox.isChecked():
return
new_block = QStandardItem()
@@ -265,5 +270,6 @@ class SessionDialog(QDialog, Ui_Dialog):
#
# End Slots
#
- def isActive(self):
- return self.activeBox.isChecked()
+ def isActive(self) -> bool:
+ active: bool = self.activeBox.isChecked()
+ return active
diff --git a/lib/sounds.py b/lib/sounds.py
index c363c1f..87af0a2 100644
--- a/lib/sounds.py
+++ b/lib/sounds.py
@@ -1,3 +1,5 @@
+from typing import Optional, Type, cast
+
from PyQt6.QtCore import QObject, Qt, QUrl, pyqtSlot
from PyQt6.QtMultimedia import (
QAudioDevice,
@@ -13,13 +15,13 @@ from PyQt6.QtMultimedia import (
class SoundOff(QObject):
_instance = None
- def __new__(cls):
+ def __new__(cls: Type[SoundOff]) -> SoundOff:
if cls._instance:
return cls._instance
cls._instance = super(SoundOff, cls).__new__(cls)
return cls._instance
- def __init__(self):
+ def __init__(self) -> None:
super().__init__()
#
# Setup devices
@@ -61,36 +63,38 @@ class SoundOff(QObject):
self.virtualPlayer.playbackStateChanged.connect(self.playbackState)
@pyqtSlot()
- def alert(self):
+ def alert(self) -> None:
self.alertEffect.play()
return
@pyqtSlot(QMediaPlayer.Error, str)
- def mediaError(self, error, string):
+ def mediaError(self, error: QMediaPlayer.Error, string: str) -> None:
print(error)
print(str)
return
@pyqtSlot(QMediaPlayer.MediaStatus)
- def mediaStatus(self, status):
+ def mediaStatus(self, status: QMediaPlayer.MediaStatus) -> None:
if status == QMediaPlayer.MediaStatus.LoadedMedia:
- self.sender().play()
+ player: Optional[QMediaPlayer] = cast(QMediaPlayer, self.sender())
+ assert player is not None
+ player.play()
return
@pyqtSlot(QMediaPlayer.PlaybackState)
- def playbackState(self, state):
+ def playbackState(self, state: QMediaPlayer.PlaybackState) -> None:
return
#
# Communications slots
#
@pyqtSlot()
- def soundAlert(self):
+ def soundAlert(self) -> None:
self.alertEffect.play()
return
@pyqtSlot(str)
- def playSound(self, url):
+ def playSound(self, url: str) -> None:
src = QUrl(url)
if not self.localPlayer.audioOutput():
self.localPlayer.setAudioOutput(self.localOutput)
diff --git a/main.py b/main.py
index f062455..1daeb6b 100755
--- a/main.py
+++ b/main.py
@@ -16,7 +16,7 @@ import os
import re
import sys
from datetime import datetime, timedelta
-from typing import cast
+from typing import Optional
from PyQt6.QtCore import (
QModelIndex,
@@ -98,7 +98,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
return
@pyqtSlot()
- def editPreferences(self):
+ def editPreferences(self) -> None:
dlg = Preferences()
dlg.exec()
return
@@ -134,12 +134,13 @@ class MainWindow(QMainWindow, Ui_MainWindow):
@pyqtSlot()
@pyqtSlot(QModelIndex)
- def editPerson(self, index=None) -> None:
+ def editPerson(self, index: Optional[QModelIndex] = None) -> None:
if not index:
indexes = self.peopleView.selectedIndexes()
if len(indexes) < 1:
return
index = indexes[0]
+ assert index is not None
dlg = PersonDialog(person_id=index.siblingAtColumn(0).data())
dlg.exec()
return
@@ -324,7 +325,7 @@ if __name__ == "__main__":
uiName = "ui/" + fileName[:-3] + ".ui"
rccName = "ui/" + fileName[:-3] + ".qrc"
if not os.path.isfile(uiName) and not os.path.isfile(rccName):
- outOfDate.append(filenName)
+ outOfDate.append(fileName)
continue
if os.path.isfile(uiName) and os.path.getmtime(
uiName