Add import and export to project word list (#1691)

This commit is contained in:
Veronica Berglyd Olsen
2024-02-11 19:37:35 +01:00
committed by GitHub
8 changed files with 160 additions and 51 deletions
@@ -59,6 +59,7 @@ fmt_subscript = nw_tb-subscript.svg
fmt_superscript = nw_tb-superscript.svg
fmt_underline = nw_tb-underline.svg
forward = typ_chevron-right.svg
import = mixed_import.svg
maximise = typ_arrow-maximise.svg
menu = typ_th-dot-menu.svg
minimise = typ_arrow-minimise.svg
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m3.1113 3.666c-0.61332 0-1.1094 0.49689-1.1094 1.1113v15.557h-0.0019531c0 0.61443 0.498 1.1094 1.1113 1.1094h15.557c0.61332 0 1.1113-0.49494 1.1113-1.1094v-8.2578c-0.61899 0.57364-1.3634 1.0131-2.252 1.2578-0.004058 2.0524-0.007162 4.0735 0.005859 5.8887h-13.311v-13.318c1.6116-0.00848 2.8992 0 4.4648 0 0.75712-0.75269 1.5907-1.5005 2.3418-2.2383h-7.918z" fill="#9c9"/>
<path d="m21.996 3.1106v-0.55556c-1.8789 2.8644-4 4.3922-6.6667 4.4444v-3.3333c0-0.61222-0.56778-1.1111-1.27-1.1111-0.40444 0-0.75 0.17556-0.98111 0.43444-2.1478 2.2544-6.6378 6.7878-6.6378 6.7878s4.49 4.5356 6.6389 6.8189c0.23111 0.22778 0.57556 0.40333 0.98 0.40333 0.70222 0 1.27-0.49667 1.27-1.1111v-3.3333c5.1778 0 6.6667-5.4122 6.6667-9.4444z" fill="#9c9" stroke-width="1.1111"/>
</svg>

After

Width:  |  Height:  |  Size: 912 B

@@ -59,6 +59,7 @@ fmt_subscript = nw_tb-subscript.svg
fmt_superscript = nw_tb-superscript.svg
fmt_underline = nw_tb-underline.svg
forward = typ_chevron-right.svg
import = mixed_import.svg
maximise = typ_arrow-maximise.svg
menu = typ_th-dot-menu.svg
minimise = typ_arrow-minimise.svg
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m3.1113 3.666c-0.61332 0-1.1094 0.49689-1.1094 1.1113v15.557h-0.0019531c0 0.61443 0.498 1.1094 1.1113 1.1094h15.557c0.61332 0 1.1113-0.49494 1.1113-1.1094v-8.2578c-0.61899 0.57364-1.3634 1.0131-2.252 1.2578-0.004058 2.0524-0.007162 4.0735 0.005859 5.8887h-13.311v-13.318c1.6116-0.00848 2.8992 0 4.4648 0 0.75712-0.75269 1.5907-1.5005 2.3418-2.2383h-7.918z" fill="#718c00"/>
<path d="m21.996 3.1106v-0.55556c-1.8789 2.8644-4 4.3922-6.6667 4.4444v-3.3333c0-0.61222-0.56778-1.1111-1.27-1.1111-0.40444 0-0.75 0.17556-0.98111 0.43444-2.1478 2.2544-6.6378 6.7878-6.6378 6.7878s4.49 4.5356 6.6389 6.8189c0.23111 0.22778 0.57556 0.40333 0.98 0.40333 0.70222 0 1.27-0.49667 1.27-1.1111v-3.3333c5.1778 0 6.6667-5.4122 6.6667-9.4444z" fill="#718c00" stroke-width="1.1111"/>
</svg>

After

Width:  |  Height:  |  Size: 918 B

+97 -39
View File
@@ -26,16 +26,18 @@ from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from pathlib import Path
from PyQt5.QtGui import QCloseEvent
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import (
QAbstractItemView, QDialog, QDialogButtonBox, QHBoxLayout, QLabel,
QLineEdit, QListWidget, QListWidgetItem, QPushButton, QVBoxLayout, qApp
QAbstractItemView, QDialog, QDialogButtonBox, QFileDialog, QHBoxLayout,
QLineEdit, QListWidget, QPushButton, QVBoxLayout, qApp
)
from novelwriter import CONFIG, SHARED
from novelwriter.core.spellcheck import UserDictionary
from novelwriter.extensions.configlayout import NColourLabel
if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
@@ -57,30 +59,46 @@ class GuiWordList(QDialog):
mS = CONFIG.pxInt(250)
wW = CONFIG.pxInt(320)
wH = CONFIG.pxInt(340)
pOptions = SHARED.project.options
self.setMinimumWidth(mS)
self.setMinimumHeight(mS)
self.resize(
CONFIG.pxInt(pOptions.getInt("GuiWordList", "winWidth", wW)),
CONFIG.pxInt(pOptions.getInt("GuiWordList", "winHeight", wH))
CONFIG.pxInt(SHARED.project.options.getInt("GuiWordList", "winWidth", wW)),
CONFIG.pxInt(SHARED.project.options.getInt("GuiWordList", "winHeight", wH))
)
# Main Widgets
# ============
# Header
self.headLabel = NColourLabel(
"Project Word List", SHARED.theme.helpText, parent=self,
scale=NColourLabel.HEADER_SCALE
)
self.headLabel = QLabel("<b>%s</b>" % self.tr("Project Word List"))
self.importButton = QPushButton(SHARED.theme.getIcon("import"), "", self)
self.importButton.setToolTip(self.tr("Import words from text file"))
self.importButton.clicked.connect(self._importWords)
self.listBox = QListWidget()
self.exportButton = QPushButton(SHARED.theme.getIcon("export"), "", self)
self.exportButton.setToolTip(self.tr("Export words to text file"))
self.exportButton.clicked.connect(self._exportWords)
self.headerBox = QHBoxLayout()
self.headerBox.addWidget(self.headLabel, 1)
self.headerBox.addWidget(self.importButton, 0)
self.headerBox.addWidget(self.exportButton, 0)
# List Box
self.listBox = QListWidget(self)
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
self.listBox.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
self.listBox.setSortingEnabled(True)
# Add/Remove Form
self.newEntry = QLineEdit(self)
self.addButton = QPushButton(SHARED.theme.getIcon("add"), "")
self.addButton = QPushButton(SHARED.theme.getIcon("add"), "", self)
self.addButton.clicked.connect(self._doAdd)
self.delButton = QPushButton(SHARED.theme.getIcon("remove"), "")
self.delButton = QPushButton(SHARED.theme.getIcon("remove"), "", self)
self.delButton.clicked.connect(self._doDelete)
self.editBox = QHBoxLayout()
@@ -88,20 +106,19 @@ class GuiWordList(QDialog):
self.editBox.addWidget(self.addButton, 0)
self.editBox.addWidget(self.delButton, 0)
# Buttons
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Close)
self.buttonBox.accepted.connect(self._doSave)
self.buttonBox.rejected.connect(self.close)
# Assemble
# ========
self.outerBox = QVBoxLayout()
self.outerBox.addWidget(self.headLabel)
self.outerBox.addSpacing(CONFIG.pxInt(8))
self.outerBox.addLayout(self.headerBox, 0)
self.outerBox.addWidget(self.listBox, 1)
self.outerBox.addLayout(self.editBox, 0)
self.outerBox.addSpacing(CONFIG.pxInt(12))
self.outerBox.addWidget(self.buttonBox, 0)
self.outerBox.setSpacing(CONFIG.pxInt(4))
self.setLayout(self.outerBox)
@@ -134,45 +151,72 @@ class GuiWordList(QDialog):
def _doAdd(self) -> None:
"""Add a new word to the word list."""
word = self.newEntry.text().strip()
if word == "":
SHARED.error(self.tr("Cannot add a blank word."))
return
if self.listBox.findItems(word, Qt.MatchExactly):
SHARED.error(self.tr(
"The word '{0}' is already in the word list."
).format(word))
return
self.listBox.addItem(word)
self.newEntry.setText("")
self.listBox.clearSelection()
self._addWord(word)
if items := self.listBox.findItems(word, Qt.MatchExactly):
self.listBox.setCurrentItem(items[0])
self.listBox.scrollToItem(items[0], QAbstractItemView.ScrollHint.PositionAtCenter)
return
@pyqtSlot()
def _doDelete(self) -> None:
"""Delete the selected item."""
selItem = self.listBox.selectedItems()
if selItem:
self.listBox.takeItem(self.listBox.row(selItem[0]))
"""Delete the selected items."""
for item in self.listBox.selectedItems():
self.listBox.takeItem(self.listBox.row(item))
return
@pyqtSlot()
def _doSave(self) -> None:
"""Save the new word list and close."""
userDict = UserDictionary(SHARED.project)
for i in range(self.listBox.count()):
item = self.listBox.item(i)
if isinstance(item, QListWidgetItem):
word = item.text().strip()
if word:
userDict.add(word)
for word in self._listWords():
userDict.add(word)
userDict.save()
self.newWordListReady.emit()
qApp.processEvents()
self.close()
return
@pyqtSlot()
def _importWords(self) -> None:
"""Import words from file."""
SHARED.info(self.tr(
"Note: The import file must be a plain text file with UTF-8 or ASCII encoding."
))
extFilter = [
"{0} (*.txt)".format(self.tr("Text files")),
"{0} (*)".format(self.tr("All files")),
]
path, _ = QFileDialog.getOpenFileName(
self, self.tr("Import File"), str(Path.home()), filter=";;".join(extFilter)
)
if path:
try:
with open(path, mode="r", encoding="utf-8") as fo:
words = set(w.strip() for w in fo.read().split())
except Exception as exc:
SHARED.error("Could not read file.", exc=exc)
return
for word in words:
self._addWord(word)
return
@pyqtSlot()
def _exportWords(self) -> None:
"""Export words to file."""
path, _ = QFileDialog.getSaveFileName(
self, self.tr("Export File"), str(Path.home())
)
if path:
try:
path = Path(path).with_suffix(".txt")
with open(path, mode="w", encoding="utf-8") as fo:
fo.write("\n".join(self._listWords()))
except Exception as exc:
SHARED.error("Could not write file.", exc=exc)
return
##
# Internal Functions
##
@@ -183,8 +227,7 @@ class GuiWordList(QDialog):
userDict.load()
self.listBox.clear()
for word in userDict:
if word:
self.listBox.addItem(word)
self.listBox.addItem(word)
return
def _saveGuiSettings(self) -> None:
@@ -199,4 +242,19 @@ class GuiWordList(QDialog):
return
def _addWord(self, word: str) -> None:
"""Add a single word to the list."""
if word and not self.listBox.findItems(word, Qt.MatchExactly):
self.listBox.addItem(word)
self._changed = True
return
def _listWords(self) -> list[str]:
"""List all words in the list box."""
result = []
for i in range(self.listBox.count()):
if (item := self.listBox.item(i)) and (word := item.text().strip()):
result.append(word)
return result
# END Class GuiWordList
+2 -2
View File
@@ -453,8 +453,8 @@ class GuiIcons:
# General Button Icons
"add", "add_document", "backward", "bookmark", "browse", "checked", "close", "cross",
"document", "down", "edit", "export", "forward", "maximise", "menu", "minimise", "more",
"noncheckable", "panel", "refresh", "remove", "revert", "search_replace", "search",
"document", "down", "edit", "export", "forward", "import", "maximise", "menu", "minimise",
"more", "noncheckable", "panel", "refresh", "remove", "revert", "search_replace", "search",
"settings", "star", "unchecked", "up", "view",
# Switches
+4 -4
View File
@@ -665,10 +665,10 @@ class GuiMain(QMainWindow):
lastPath = CONFIG.lastPath()
extFilter = [
self.tr("Text files ({0})").format("*.txt"),
self.tr("Markdown files ({0})").format("*.md"),
self.tr("novelWriter files ({0})").format("*.nwd"),
self.tr("All files ({0})").format("*"),
"{0} (*.txt)".format(self.tr("Text files")),
"{0} (*.md)".format(self.tr("Markdown files")),
"{0} (*.nwd)".format(self.tr("novelWriter files")),
"{0} (*)".format(self.tr("All files")),
]
loadFile, _ = QFileDialog.getOpenFileName(
self, self.tr("Import File"), str(lastPath), filter=";;".join(extFilter)
+45 -6
View File
@@ -23,9 +23,10 @@ from __future__ import annotations
import pytest
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QDialog, QAction
from PyQt5.QtWidgets import QDialog, QAction, QFileDialog
from tools import buildTestProject
from mocked import causeOSError
from novelwriter import SHARED
from novelwriter.core.spellcheck import UserDictionary
@@ -33,9 +34,8 @@ from novelwriter.dialogs.wordlist import GuiWordList
@pytest.mark.gui
def testDlgWordList_Dialog(qtbot, monkeypatch, nwGUI, projPath):
"""test the word list editor.
"""
def testDlgWordList_Dialog(qtbot, monkeypatch, nwGUI, fncPath, projPath):
"""test the word list editor."""
buildTestProject(nwGUI, projPath)
monkeypatch.setattr(GuiWordList, "exec_", lambda *a: None)
@@ -110,16 +110,55 @@ def testDlgWordList_Dialog(qtbot, monkeypatch, nwGUI, projPath):
assert wList.listBox.findItems("delete_me", Qt.MatchExactly) == []
assert wList.listBox.item(0).text() == "word_a" # type: ignore
# Save files
# Import/Export
# =============
expFile = fncPath / "wordlist_export.txt"
impFile = fncPath / "wordlist_import.txt"
wList.show()
# Export File, OS Error
with monkeypatch.context() as mp:
mp.setattr(QFileDialog, "getSaveFileName", lambda *a, **k: (str(expFile), ""))
mp.setattr("builtins.open", causeOSError)
wList.exportButton.click()
assert not expFile.exists()
# Export File, OK
with monkeypatch.context() as mp:
mp.setattr(QFileDialog, "getSaveFileName", lambda *a, **k: (str(expFile), ""))
wList.exportButton.click()
assert expFile.exists()
assert expFile.read_text() == "word_a\nword_b\nword_c\nword_d\nword_f\nword_g"
# Write File
impFile.write_text("word_d\nword_e\nword_f\tword_g word_h word_i\n\n\n")
# Import File, OS Error
with monkeypatch.context() as mp:
mp.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (str(impFile), ""))
mp.setattr("builtins.open", causeOSError)
wList.importButton.click()
assert wList.listBox.count() == 6
# Import File, OK
with monkeypatch.context() as mp:
mp.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (str(impFile), ""))
wList.importButton.click()
assert wList.listBox.count() == 9
# Save and Check List
wList._doSave()
userDict.load()
assert len(list(userDict)) == 6
assert len(list(userDict)) == 9
assert "word_a" in userDict
assert "word_b" in userDict
assert "word_c" in userDict
assert "word_d" in userDict
assert "word_e" in userDict
assert "word_f" in userDict
assert "word_g" in userDict
assert "word_h" in userDict
assert "word_i" in userDict
# qtbot.stop()