Skip to content

Commit

Permalink
Add import and export to project word list (#1691)
Browse files Browse the repository at this point in the history
  • Loading branch information
vkbo committed Feb 11, 2024
2 parents 56cb545 + edfb4e6 commit dc661b7
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 51 deletions.
1 change: 1 addition & 0 deletions novelwriter/assets/icons/typicons_dark/icons.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions novelwriter/assets/icons/typicons_dark/mixed_import.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions novelwriter/assets/icons/typicons_light/icons.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions novelwriter/assets/icons/typicons_light/mixed_import.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
136 changes: 97 additions & 39 deletions novelwriter/dialogs/wordlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,18 @@
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
Expand All @@ -57,51 +59,66 @@ def __init__(self, mainGui: GuiMain) -> None:
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))
)

# Header
self.headLabel = NColourLabel(
"Project Word List", SHARED.theme.helpText, parent=self,
scale=NColourLabel.HEADER_SCALE
)

# Main Widgets
# ============
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.headLabel = QLabel("<b>%s</b>" % self.tr("Project Word List"))
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.listBox = QListWidget()
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()
self.editBox.addWidget(self.newEntry, 1)
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)

Expand Down Expand Up @@ -134,45 +151,72 @@ def closeEvent(self, event: QCloseEvent) -> None:
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
##
Expand All @@ -183,8 +227,7 @@ def _loadWordList(self) -> None:
userDict.load()
self.listBox.clear()
for word in userDict:
if word:
self.listBox.addItem(word)
self.listBox.addItem(word)
return

def _saveGuiSettings(self) -> None:
Expand All @@ -199,4 +242,19 @@ def _saveGuiSettings(self) -> None:

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
4 changes: 2 additions & 2 deletions novelwriter/gui/theme.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions novelwriter/guimain.py
Original file line number Diff line number Diff line change
Expand Up @@ -665,10 +665,10 @@ def importDocument(self) -> bool:

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)
Expand Down
51 changes: 45 additions & 6 deletions tests/test_dialogs/test_dlg_wordlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,19 @@
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
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)
Expand Down Expand Up @@ -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()

Expand Down

0 comments on commit dc661b7

Please sign in to comment.