diff --git a/novelwriter/dialogs/wordlist.py b/novelwriter/dialogs/wordlist.py index 72e19bb0..3532fa74 100644 --- a/novelwriter/dialogs/wordlist.py +++ b/novelwriter/dialogs/wordlist.py @@ -26,12 +26,13 @@ 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, + QLabel, QLineEdit, QListWidget, QPushButton, QVBoxLayout, qApp ) from novelwriter import CONFIG, SHARED @@ -57,30 +58,44 @@ class GuiWordList(QDialog): mS = CONFIG.pxInt(250) wW = CONFIG.pxInt(320) wH = CONFIG.pxInt(340) - pOptions = SHARED.project.options + options = 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(options.getInt("GuiWordList", "winWidth", wW)), + CONFIG.pxInt(options.getInt("GuiWordList", "winHeight", wH)) ) - # Main Widgets - # ============ - + # Header self.headLabel = QLabel("%s" % self.tr("Project Word List")) - self.listBox = QListWidget() + self.importButton = QPushButton(SHARED.theme.getIcon("add_document"), "", self) + self.importButton.setToolTip(self.tr("Import words from text file")) + self.importButton.clicked.connect(self._importWords) + + 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 +103,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 +148,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 +224,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 +239,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 diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 9480d5da..0f3fa719 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -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)