diff --git a/novelwriter/common.py b/novelwriter/common.py
index f914a6d1b..2627a1b09 100644
--- a/novelwriter/common.py
+++ b/novelwriter/common.py
@@ -487,6 +487,14 @@ def makeFileNameSafe(text: str) -> str:
return "".join(c for c in text if c.isalnum() or c in allowed)
+def getFileSize(path: Path) -> int:
+ """Return the size of a file."""
+ try:
+ return path.stat().st_size
+ except Exception:
+ return -1
+
+
# =============================================================================================== #
# Other Functions
# =============================================================================================== #
diff --git a/novelwriter/config.py b/novelwriter/config.py
index ba422efae..390353714 100644
--- a/novelwriter/config.py
+++ b/novelwriter/config.py
@@ -225,6 +225,7 @@ def __init__(self) -> None:
# Other System Info
self.hostName = QSysInfo.machineHostName()
self.kernelVer = QSysInfo.kernelVersion()
+ self.isDebug = False
# Packages
self.hasEnchant = False # The pyenchant package
@@ -484,6 +485,7 @@ def initConfig(self, confPath: str | Path | None = None,
self._recentObj.loadCache()
self._checkOptionalPackages()
+ self.isDebug = logger.getEffectiveLevel() == logging.DEBUG
logger.debug("Config instance initialised")
diff --git a/novelwriter/constants.py b/novelwriter/constants.py
index d8ce3562c..4afa77810 100644
--- a/novelwriter/constants.py
+++ b/novelwriter/constants.py
@@ -50,6 +50,9 @@ class nwConst:
URL_HELP = "https://github.com/vkbo/novelWriter/discussions"
URL_RELEASE = "https://github.com/vkbo/novelWriter/releases/latest"
+ # Requests
+ USER_AGENT = "Mozilla/5.0 (compatible; novelWriter (Python))"
+
# Gui Settings
STATUS_MSG_TIMEOUT = 15000 # milliseconds
diff --git a/novelwriter/dialogs/updates.py b/novelwriter/dialogs/updates.py
index c39a31cca..bbdaf0384 100644
--- a/novelwriter/dialogs/updates.py
+++ b/novelwriter/dialogs/updates.py
@@ -29,10 +29,10 @@
from datetime import datetime
from urllib.request import Request, urlopen
-from PyQt5.QtGui import QCursor
-from PyQt5.QtCore import Qt
+from PyQt5.QtGui import QCloseEvent, QCursor
+from PyQt5.QtCore import Qt, pyqtSlot
from PyQt5.QtWidgets import (
- qApp, QDialog, QHBoxLayout, QVBoxLayout, QDialogButtonBox, QLabel
+ QWidget, qApp, QDialog, QHBoxLayout, QVBoxLayout, QDialogButtonBox, QLabel
)
from novelwriter import CONFIG, SHARED, __version__, __date__
@@ -44,8 +44,8 @@
class GuiUpdates(QDialog):
- def __init__(self, mainGui):
- super().__init__(parent=mainGui)
+ def __init__(self, parent: QWidget) -> None:
+ super().__init__(parent=parent)
logger.debug("Create: GuiUpdates")
self.setObjectName("GuiUpdates")
@@ -94,8 +94,8 @@ def __init__(self, mainGui):
self.latestLabel.setFont(hFont)
# Buttons
- self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok)
- self.buttonBox.accepted.connect(self._doClose)
+ self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close)
+ self.buttonBox.rejected.connect(self._doClose)
# Assemble
self.innerBox = QHBoxLayout()
@@ -114,17 +114,16 @@ def __init__(self, mainGui):
return
- def __del__(self): # pragma: no cover
+ def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiUpdates")
return
- def checkLatest(self):
- """Check for latest release.
- """
+ def checkLatest(self) -> None:
+ """Check for latest release."""
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
urlReq = Request("https://api.github.com/repos/vkbo/novelwriter/releases/latest")
- urlReq.add_header("User-Agent", "Mozilla/5.0 (compatible; novelWriter (Python))")
+ urlReq.add_header("User-Agent", nwConst.USER_AGENT)
urlReq.add_header("Accept", "application/vnd.github.v3+json")
rawData = {}
@@ -161,10 +160,22 @@ def checkLatest(self):
return
##
- # Internal Functions
+ # Events
##
- def _doClose(self):
+ def closeEvent(self, event: QCloseEvent) -> None:
+ """Capture the user closing the window."""
+ event.accept()
+ self.deleteLater()
+ return
+
+ ##
+ # Private Slots
+ ##
+
+ @pyqtSlot()
+ def _doClose(self) -> None:
+ """Close the dialog."""
self.close()
return
diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index 8992505db..a0e2c3b90 100644
--- a/novelwriter/gui/docviewer.py
+++ b/novelwriter/gui/docviewer.py
@@ -620,7 +620,7 @@ def _dumpHistory(self) -> None:
"""Debug function to dump history to the logger. Since it is a
for loop, it is skipped entirely if log level isn't DEBUG.
"""
- if logger.getEffectiveLevel() == logging.DEBUG: # pragma: no cover
+ if CONFIG.isDebug: # pragma: no cover
for i, (h, p) in enumerate(zip(self._navHistory, self._posHistory)):
logger.debug(
"History %02d: %s %13s [x:%d]" % (
diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py
index a0420144a..8c74f2705 100644
--- a/novelwriter/gui/mainmenu.py
+++ b/novelwriter/gui/mainmenu.py
@@ -870,6 +870,11 @@ def _buildToolsMenu(self) -> None:
self.aEditWordList = self.toolsMenu.addAction(self.tr("Project Word List"))
self.aEditWordList.triggered.connect(lambda: self.mainGui.showProjectWordListDialog())
+ # Tools > Add Dictionaries
+ if CONFIG.osWindows or CONFIG.isDebug:
+ self.aAddDicts = self.toolsMenu.addAction(self.tr("Add Dictionaries"))
+ self.aAddDicts.triggered.connect(self.mainGui.showDictionariesDialog)
+
# Tools > Separator
self.toolsMenu.addSeparator()
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index 8e02e0e6b..635e9b8f0 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -59,6 +59,7 @@
from novelwriter.tools.lipsum import GuiLipsum
from novelwriter.tools.manuscript import GuiManuscript
from novelwriter.tools.projwizard import GuiProjectWizard
+from novelwriter.tools.dictionaries import GuiDictionaries
from novelwriter.tools.writingstats import GuiWritingStats
from novelwriter.core.coretools import ProjectBuilder
@@ -340,7 +341,7 @@ def __init__(self) -> None:
logger.debug("Ready: GUI")
- if __hexversion__[-2] == "a" and logger.getEffectiveLevel() > logging.DEBUG:
+ if __hexversion__[-2] == "a" and not CONFIG.isDebug:
SHARED.warn(self.tr(
"You are running an untested development version of novelWriter. "
"Please be careful when working on a live project "
@@ -1065,6 +1066,20 @@ def showUpdatesDialog(self) -> None:
return
+ @pyqtSlot()
+ def showDictionariesDialog(self) -> None:
+ """Show the download dictionaries dialog."""
+ dlgDicts = GuiDictionaries(self)
+ dlgDicts.setModal(True)
+ dlgDicts.show()
+ dlgDicts.raise_()
+ qApp.processEvents()
+ if not dlgDicts.initDialog():
+ dlgDicts.close()
+ SHARED.error(self.tr("Could not initialise the dialog."))
+
+ return
+
def reportConfErr(self) -> bool:
"""Checks if the Config module has any errors to report, and let
the user know if this is the case. The Config module caches
diff --git a/novelwriter/tools/dictionaries.py b/novelwriter/tools/dictionaries.py
new file mode 100644
index 000000000..847925a02
--- /dev/null
+++ b/novelwriter/tools/dictionaries.py
@@ -0,0 +1,275 @@
+"""
+novelWriter – GUI Dictionary Downloader
+=======================================
+
+File History:
+Created: 2023-11-19 [2.2rc1]
+
+This file is a part of novelWriter
+Copyright 2018–2023, Veronica Berglyd Olsen
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+"""
+from __future__ import annotations
+
+import logging
+
+from pathlib import Path
+from zipfile import ZipFile
+from urllib.parse import urljoin
+from urllib.request import pathname2url
+
+from PyQt5.QtGui import QCloseEvent, QDesktopServices, QTextCursor
+from PyQt5.QtCore import QUrl, pyqtSlot
+from PyQt5.QtWidgets import (
+ QDialog, QDialogButtonBox, QFileDialog, QFrame, QHBoxLayout, QLabel,
+ QLineEdit, QPlainTextEdit, QPushButton, QVBoxLayout, QWidget, qApp
+)
+
+from novelwriter import CONFIG, SHARED
+from novelwriter.common import formatInt, getFileSize
+from novelwriter.error import formatException
+
+logger = logging.getLogger(__name__)
+
+
+class GuiDictionaries(QDialog):
+
+ def __init__(self, parent: QWidget) -> None:
+ super().__init__(parent=parent)
+
+ logger.debug("Create: GuiDictionaries")
+ self.setObjectName("GuiDictionaries")
+ self.setWindowTitle(self.tr("Add Dictionaries"))
+
+ self._installPath = None
+ self._currDicts = set()
+
+ iPx = CONFIG.pxInt(4)
+ mPx = CONFIG.pxInt(8)
+ sPx = CONFIG.pxInt(16)
+
+ self.setMinimumWidth(CONFIG.pxInt(500))
+ self.setMinimumHeight(CONFIG.pxInt(300))
+
+ # Hunspell Dictionaries
+ foUrl = "https://www.freeoffice.com/en/download/dictionaries"
+ loUrl = "https://extensions.libreoffice.org"
+ self.huInfo = QLabel("
".join([
+ self.tr("Download a dictionary from one of the links, and add it below."),
+ f" \u203a {foUrl}",
+ f" \u203a {loUrl}",
+ ]))
+ self.huInfo.setOpenExternalLinks(True)
+ self.huInfo.setWordWrap(True)
+ self.huInput = QLineEdit(self)
+ self.huBrowse = QPushButton(self)
+ self.huBrowse.setIcon(SHARED.theme.getIcon("browse"))
+ self.huBrowse.clicked.connect(self._doBrowseHunspell)
+ self.huImport = QPushButton(self.tr("Add Dictionary"), self)
+ self.huImport.setIcon(SHARED.theme.getIcon("add"))
+ self.huImport.clicked.connect(self._doImportHunspell)
+
+ self.huPathBox = QHBoxLayout()
+ self.huPathBox.addWidget(self.huInput)
+ self.huPathBox.addWidget(self.huBrowse)
+ self.huPathBox.setSpacing(iPx)
+ self.huAddBox = QHBoxLayout()
+ self.huAddBox.addStretch(1)
+ self.huAddBox.addWidget(self.huImport)
+
+ # Install Path
+ self.inInfo = QLabel(self.tr("Dictionary install location"))
+ self.inPath = QLineEdit(self)
+ self.inPath.setReadOnly(True)
+ self.inBrowse = QPushButton(self)
+ self.inBrowse.setIcon(SHARED.theme.getIcon("browse"))
+ self.inBrowse.clicked.connect(self._doOpenInstallLocation)
+
+ self.inBox = QHBoxLayout()
+ self.inBox.addWidget(self.inPath)
+ self.inBox.addWidget(self.inBrowse)
+ self.inBox.setSpacing(iPx)
+
+ # Info Box
+ self.infoBox = QPlainTextEdit(self)
+ self.infoBox.setReadOnly(True)
+ self.infoBox.setFixedHeight(4*SHARED.theme.fontPixelSize)
+ self.infoBox.setFrameStyle(QFrame.Shape.NoFrame)
+
+ # Buttons
+ self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close)
+ self.buttonBox.rejected.connect(self._doClose)
+
+ # Assemble
+ self.innerBox = QVBoxLayout()
+ self.innerBox.addWidget(self.huInfo)
+ self.innerBox.addLayout(self.huPathBox)
+ self.innerBox.addLayout(self.huAddBox)
+ self.innerBox.addSpacing(mPx)
+ self.innerBox.addWidget(self.inInfo)
+ self.innerBox.addLayout(self.inBox)
+ self.innerBox.addWidget(self.infoBox)
+ self.innerBox.setSpacing(iPx)
+
+ self.outerBox = QVBoxLayout()
+ self.outerBox.addLayout(self.innerBox, 0)
+ self.outerBox.addStretch(1)
+ self.outerBox.addWidget(self.buttonBox, 0)
+ self.outerBox.setSpacing(sPx)
+
+ self.setLayout(self.outerBox)
+
+ logger.debug("Ready: GuiDictionaries")
+
+ return
+
+ def __del__(self) -> None: # pragma: no cover
+ logger.debug("Delete: GuiDictionaries")
+ return
+
+ def initDialog(self) -> bool:
+ """Prepare and check that we can proceed."""
+ try:
+ import enchant
+ path = Path(enchant.get_user_config_dir())
+ except Exception:
+ logger.error("Could not get enchant path")
+ return False
+
+ self._installPath = Path(path).resolve()
+ if path.is_dir():
+ self.inPath.setText(str(path))
+ hunspell = path / "hunspell"
+ if hunspell.is_dir():
+ self._currDicts = set(
+ i.stem for i in hunspell.iterdir() if i.is_file() and i.suffix == ".aff"
+ )
+ self._appendLog(self.tr(
+ "Additional dictionaries found: {0}"
+ ).format(len(self._currDicts)))
+
+ qApp.processEvents()
+ self.adjustSize()
+
+ return True
+
+ ##
+ # Events
+ ##
+
+ def closeEvent(self, event: QCloseEvent) -> None:
+ """Capture the user closing the window."""
+ event.accept()
+ self.deleteLater()
+ return
+
+ ##
+ # Private Slots
+ ##
+
+ @pyqtSlot()
+ def _doBrowseHunspell(self):
+ """Browse for a Free/Libre Office dictionary."""
+ extFilter = [
+ self.tr("Free or Libre Office extension ({0})").format("*.sox *.oxt"),
+ self.tr("All files ({0})").format("*"),
+ ]
+ soxFile, _ = QFileDialog.getOpenFileName(
+ self, self.tr("Browse Files"), "", filter=";;".join(extFilter)
+ )
+ if soxFile:
+ path = Path(soxFile).absolute()
+ self.huInput.setText(str(path))
+ return
+
+ @pyqtSlot()
+ def _doImportHunspell(self):
+ """Import a hunspell dictionary from .sox or .oxt file."""
+ procErr = self.tr("Could not process dictionary file")
+ if self._installPath:
+ temp = self.huInput.text()
+ if temp and (path := Path(temp)).is_file():
+ hunspell = self._installPath / "hunspell"
+ hunspell.mkdir(exist_ok=True)
+ try:
+ nAff, nDic = self._extractDicts(path, hunspell)
+ if nAff == 0 or nDic == 0:
+ self._appendLog(procErr, err=True)
+ except Exception as exc:
+ self._appendLog(procErr, err=True)
+ self._appendLog(formatException(exc), err=True)
+ else:
+ self._appendLog(procErr, err=True)
+ return
+
+ @pyqtSlot()
+ def _doOpenInstallLocation(self) -> None:
+ """Open the dictionary folder."""
+ path = self.inPath.text()
+ if Path(path).is_dir():
+ QDesktopServices.openUrl(
+ QUrl(urljoin("file:", pathname2url(path)))
+ )
+ else:
+ SHARED.error("Path not found.")
+ return
+
+ @pyqtSlot()
+ def _doClose(self) -> None:
+ """Close the dialog."""
+ self.close()
+ return
+
+ ##
+ # Internal Functions
+ ##
+
+ def _extractDicts(self, path: Path, output: Path) -> tuple[int, int]:
+ """Extract a zip archive and return the number of .aff and .dic
+ files found in it.
+ """
+ nAff = nDic = 0
+ with ZipFile(path, mode="r") as zipObj:
+ for item in zipObj.namelist():
+ zPath = Path(item)
+ if zPath.suffix not in (".aff", ".dic"):
+ continue
+ nAff += 1 if zPath.suffix == ".aff" else 0
+ nDic += 1 if zPath.suffix == ".dic" else 0
+ with zipObj.open(item) as zF:
+ oPath = output / zPath.name
+ oPath.write_bytes(zF.read())
+ size = getFileSize(oPath)
+ self._appendLog(self.tr(
+ "Added: {0} [{1}B]"
+ ).format(zPath.name, formatInt(size)))
+ return nAff, nDic
+
+ def _appendLog(self, text: str, err: bool = False) -> None:
+ """Append a line to the log output."""
+ cursor = self.infoBox.textCursor()
+ cursor.movePosition(QTextCursor.MoveOperation.End)
+ if cursor.position() > 0:
+ cursor.insertText("\n")
+ if err:
+ cursor.insertHtml(f"{text}")
+ else:
+ cursor.insertText(text)
+ cursor.movePosition(QTextCursor.MoveOperation.End)
+ cursor.deleteChar()
+ self.infoBox.setTextCursor(cursor)
+ return
+
+# END Class GuiDictionaries
diff --git a/novelwriter/tools/manusbuild.py b/novelwriter/tools/manusbuild.py
index 390d626cd..70235ebf1 100644
--- a/novelwriter/tools/manusbuild.py
+++ b/novelwriter/tools/manusbuild.py
@@ -27,6 +27,7 @@
from pathlib import Path
+from PyQt5.QtGui import QCloseEvent
from PyQt5.QtCore import QSize, QTimer, Qt, pyqtSlot
from PyQt5.QtWidgets import (
QAbstractButton, QAbstractItemView, QDialog, QDialogButtonBox, QFileDialog,
@@ -227,7 +228,7 @@ def __init__(self, parent: QWidget, build: BuildSettings):
return
- def __del__(self): # pragma: no cover
+ def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiManuscriptBuild")
return
@@ -235,7 +236,7 @@ def __del__(self): # pragma: no cover
# Events
##
- def closeEvent(self, event):
+ def closeEvent(self, event: QCloseEvent) -> None:
"""Capture the user closing the window so we can save GUI
settings.
"""
diff --git a/tests/conftest.py b/tests/conftest.py
index 1969567e6..1886bfd40 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -159,6 +159,7 @@ def nwGUI(qtbot, monkeypatch, functionFixture):
nwGUI = main(["--testmode", f"--config={_TMP_CONF}", f"--data={_TMP_CONF}"])
qtbot.addWidget(nwGUI)
resetConfigVars()
+ SHARED._alert = None
nwGUI.docEditor.initEditor()
nwGUI.show()
diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py
index e66e5dc61..c00fe9da5 100644
--- a/tests/test_gui/test_gui_mainmenu.py
+++ b/tests/test_gui/test_gui_mainmenu.py
@@ -35,8 +35,7 @@
@pytest.mark.gui
def testGuiMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum):
- """Test the main menu Edit and Format entries.
- """
+ """Test the main menu Edit and Format entries."""
monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *a: True)
# Test Document Action with No Project
@@ -343,8 +342,7 @@ def testGuiMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum):
@pytest.mark.gui
def testGuiMenu_ContextMenus(qtbot, nwGUI, prjLipsum):
- """Test the context menus.
- """
+ """Test the context menus."""
assert nwGUI.openProject(prjLipsum)
assert nwGUI.openDocument("4c4f28287af27")
@@ -423,8 +421,7 @@ def testGuiMenu_ContextMenus(qtbot, nwGUI, prjLipsum):
@pytest.mark.gui
def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd):
- """Test the Insert menu.
- """
+ """Test the Insert menu."""
buildTestProject(nwGUI, projPath)
assert nwGUI.projView.projTree._getTreeItem(C.hSceneDoc) is not None
diff --git a/tests/test_tools/test_tools_dictionaries.py b/tests/test_tools/test_tools_dictionaries.py
new file mode 100644
index 000000000..5c3b471e0
--- /dev/null
+++ b/tests/test_tools/test_tools_dictionaries.py
@@ -0,0 +1,159 @@
+"""
+novelWriter – Dictionary Downloader Tester
+==========================================
+
+This file is a part of novelWriter
+Copyright 2018–2023, Veronica Berglyd Olsen
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+"""
+
+import pytest
+import enchant
+
+from zipfile import ZipFile
+
+from tools import getGuiItem
+from mocked import causeException
+
+from PyQt5.QtGui import QDesktopServices
+from PyQt5.QtWidgets import QFileDialog
+
+from novelwriter import SHARED
+from novelwriter.tools.dictionaries import GuiDictionaries
+
+
+@pytest.mark.gui
+def testToolDictionaries_Main(qtbot, monkeypatch, nwGUI, fncPath):
+ """Test the Dictionaries downloader tool."""
+ monkeypatch.setattr(enchant, "get_user_config_dir", lambda *a: str(fncPath))
+
+ # Fail to open
+ with monkeypatch.context() as mp:
+ mp.setattr(enchant, "get_user_config_dir", lambda *a: causeException)
+ nwGUI.showDictionariesDialog()
+ assert SHARED.alert is not None
+ assert SHARED.alert.logMessage == "Could not initialise the dialog."
+
+ # Open the tool
+ nwGUI.showDictionariesDialog()
+ qtbot.waitUntil(lambda: getGuiItem("GuiDictionaries") is not None, timeout=1000)
+
+ nwDicts = getGuiItem("GuiDictionaries")
+ assert isinstance(nwDicts, GuiDictionaries)
+ assert nwDicts.isVisible()
+ assert nwDicts.inPath.text() == str(fncPath)
+
+ # Allow Open Dir
+ SHARED._alert = None
+ with monkeypatch.context() as mp:
+ mp.setattr(QDesktopServices, "openUrl", lambda *a: None)
+ nwDicts._doOpenInstallLocation()
+ assert SHARED.alert is None
+
+ # Fail Open Dir
+ nwDicts.inPath.setText("/foo/bar")
+ nwDicts._doOpenInstallLocation()
+ assert SHARED.alert is not None
+ assert SHARED.alert.logMessage == "Path not found."
+ nwDicts.inPath.setText(str(fncPath))
+
+ # Create Mock Dicts
+ foDict = fncPath / "freeoffice.sox"
+ with ZipFile(foDict, mode="w") as zipObj:
+ zipObj.writestr("en_GB.aff", "foobar")
+ zipObj.writestr("en_GB.dic", "foobar")
+ zipObj.writestr("README.txt", "foobar")
+
+ loDict = fncPath / "libreoffice.oxt"
+ with ZipFile(loDict, mode="w") as zipObj:
+ zipObj.writestr("en_US/en_US.aff", "foobar")
+ zipObj.writestr("en_US/en_US.dic", "foobar")
+ zipObj.writestr("README.txt", "foobar")
+
+ emDict = fncPath / "empty.oxt"
+ with ZipFile(emDict, mode="w") as zipObj:
+ zipObj.writestr("README.txt", "foobar")
+
+ noFile = fncPath / "foobar.oxt"
+ noDict = fncPath / "foobar.sox"
+ noDict.write_bytes(b"foobar")
+
+ assert nwDicts.infoBox.toPlainText().splitlines()[-1] == (
+ "Additional dictionaries found: 0"
+ )
+
+ # Import Free Office Dictionary
+ with monkeypatch.context() as mp:
+ mp.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (str(foDict), ""))
+ nwDicts._doBrowseHunspell()
+ assert nwDicts.huInput.text() == str(foDict)
+ nwDicts._doImportHunspell()
+ assert (fncPath / "hunspell").is_dir()
+ assert (fncPath / "hunspell" / "en_GB.aff").is_file()
+ assert (fncPath / "hunspell" / "en_GB.dic").is_file()
+ assert nwDicts.infoBox.blockCount() == 3
+
+ # Import Libre Office Dictionary
+ with monkeypatch.context() as mp:
+ mp.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (str(loDict), ""))
+ nwDicts._doBrowseHunspell()
+ assert nwDicts.huInput.text() == str(loDict)
+ nwDicts._doImportHunspell()
+ assert (fncPath / "hunspell").is_dir()
+ assert (fncPath / "hunspell" / "en_US.aff").is_file()
+ assert (fncPath / "hunspell" / "en_US.dic").is_file()
+ assert nwDicts.infoBox.blockCount() == 5
+
+ # Handle Unreadable File
+ with monkeypatch.context() as mp:
+ mp.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (str(noDict), ""))
+ nwDicts._doBrowseHunspell()
+ assert nwDicts.huInput.text() == str(noDict)
+ nwDicts._doImportHunspell()
+ assert nwDicts.infoBox.blockCount() == 7
+
+ # Handle File w/No Dicts
+ with monkeypatch.context() as mp:
+ mp.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (str(emDict), ""))
+ nwDicts._doBrowseHunspell()
+ assert nwDicts.huInput.text() == str(emDict)
+ nwDicts._doImportHunspell()
+ assert nwDicts.infoBox.blockCount() == 8
+ assert nwDicts.infoBox.toPlainText().splitlines()[-1] == (
+ "Could not process dictionary file"
+ )
+
+ # Handle Non-Existing File
+ with monkeypatch.context() as mp:
+ mp.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (str(noFile), ""))
+ nwDicts._doBrowseHunspell()
+ nwDicts._doImportHunspell()
+ assert nwDicts.infoBox.blockCount() == 9
+ assert nwDicts.infoBox.toPlainText().splitlines()[-1] == (
+ "Could not process dictionary file"
+ )
+
+ # Re-init
+ nwDicts.initDialog()
+ assert nwDicts.infoBox.blockCount() == 10
+ assert nwDicts.infoBox.toPlainText().splitlines()[-1] == (
+ "Additional dictionaries found: 2"
+ )
+
+ # Close
+ nwDicts._doClose()
+ # qtbot.stop()
+
+# END Test testToolDictionaries_Main