diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index 5b3fe49c..164c5b35 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -131,7 +131,7 @@ def main(sysArgs: list | None = None): elif inOpt == "--debug": CONFIG.isDebug = True logLevel = logging.DEBUG - logFormat = "[{asctime:}] {filename:>17}:{lineno:<4d} {levelname:8} {message:}" + logFormat = "[{asctime:}] {filename:>18}:{lineno:<4d} {levelname:8} {message:}" elif inOpt == "--style": qtStyle = inArg elif inOpt == "--config": diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index 5ece68b1..7653734e 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -59,9 +59,9 @@ class GuiPreferences(QDialog): # Title self.titleLabel = NColourLabel( - self.tr("Preferences"), SHARED.theme.helpText, parent=self, scale=1.25 + self.tr("Preferences"), SHARED.theme.helpText, + parent=self, scale=NColourLabel.HEADER_SCALE, indent=CONFIG.pxInt(4) ) - self.titleLabel.setIndent(CONFIG.pxInt(4)) # Search Box self.searchText = QLineEdit(self) diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projectsettings.py similarity index 57% rename from novelwriter/dialogs/projsettings.py rename to novelwriter/dialogs/projectsettings.py index 8a9354a0..f6ff7730 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projectsettings.py @@ -3,7 +3,8 @@ novelWriter – GUI Project Settings ================================== File History: -Created: 2018-09-29 [0.0.1] GuiProjectSettings +Created: 2018-09-29 [0.0.1] GuiProjectSettings +Rewritten: 2024-01-26 [2.3b1] GuiProjectSettings This file is a part of novelWriter Copyright 2018–2024, Veronica Berglyd Olsen @@ -25,75 +26,101 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING - -from PyQt5.QtGui import QCloseEvent, QIcon, QPixmap, QColor +from PyQt5.QtGui import QCloseEvent, QColor, QIcon, QPixmap from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot from PyQt5.QtWidgets import ( - QColorDialog, QComboBox, QDialogButtonBox, QHBoxLayout, QLabel, QLineEdit, - QPushButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget, qApp + QColorDialog, QComboBox, QDialog, QDialogButtonBox, QHBoxLayout, QLineEdit, + QPushButton, QStackedWidget, QTreeWidget, QTreeWidgetItem, QVBoxLayout, + QWidget, qApp ) from novelwriter import CONFIG, SHARED from novelwriter.common import simplified from novelwriter.extensions.switch import NSwitch -from novelwriter.extensions.pageddialog import NPagedDialog -from novelwriter.extensions.configlayout import NConfigLayout - -if TYPE_CHECKING: # pragma: no cover - from novelwriter.guimain import GuiMain +from novelwriter.extensions.configlayout import NColourLabel, NFixedPage, NScrollableForm +from novelwriter.extensions.pagedsidebar import NPagedSideBar logger = logging.getLogger(__name__) -class GuiProjectSettings(NPagedDialog): +class GuiProjectSettings(QDialog): - TAB_MAIN = 0 - TAB_STATUS = 1 - TAB_IMPORT = 2 - TAB_REPLACE = 3 + PAGE_SETTINGS = 0 + PAGE_STATUS = 1 + PAGE_IMPORT = 2 + PAGE_REPLACE = 3 - newProjectSettingsReady = pyqtSignal() + newProjectSettingsReady = pyqtSignal(bool) - def __init__(self, mainGui: GuiMain, focusTab: int = TAB_MAIN) -> None: - super().__init__(parent=mainGui) + def __init__(self, parent: QWidget, gotoPage: int = PAGE_SETTINGS) -> None: + super().__init__(parent=parent) logger.debug("Create: GuiProjectSettings") self.setObjectName("GuiProjectSettings") - - self.mainGui = mainGui - SHARED.project.countStatus() self.setWindowTitle(self.tr("Project Settings")) - wW = CONFIG.pxInt(570) - wH = CONFIG.pxInt(375) - pOptions = SHARED.project.options - - self.setMinimumWidth(wW) - self.setMinimumHeight(wH) + options = SHARED.project.options + self.setMinimumSize(CONFIG.pxInt(500), CONFIG.pxInt(400)) self.resize( - CONFIG.pxInt(pOptions.getInt("GuiProjectSettings", "winWidth", wW)), - CONFIG.pxInt(pOptions.getInt("GuiProjectSettings", "winHeight", wH)) + CONFIG.pxInt(options.getInt("GuiProjectSettings", "winWidth", CONFIG.pxInt(650))), + CONFIG.pxInt(options.getInt("GuiProjectSettings", "winHeight", CONFIG.pxInt(500))) ) - self.tabMain = GuiProjectEditMain(self) - self.tabStatus = GuiProjectEditStatus(self, True) - self.tabImport = GuiProjectEditStatus(self, False) - self.tabReplace = GuiProjectEditReplace(self) + # Title + self.titleLabel = NColourLabel( + self.tr("Project Settings"), SHARED.theme.helpText, + parent=self, scale=NColourLabel.HEADER_SCALE, indent=CONFIG.pxInt(4) + ) - self.addTab(self.tabMain, self.tr("Settings")) - self.addTab(self.tabStatus, self.tr("Status")) - self.addTab(self.tabImport, self.tr("Importance")) - self.addTab(self.tabReplace, self.tr("Auto-Replace")) + # SideBar + self.sidebar = NPagedSideBar(self) + self.sidebar.setLabelColor(SHARED.theme.helpText) + self.sidebar.addButton(self.tr("Settings"), self.PAGE_SETTINGS) + self.sidebar.addButton(self.tr("Status"), self.PAGE_STATUS) + self.sidebar.addButton(self.tr("Importance"), self.PAGE_IMPORT) + self.sidebar.addButton(self.tr("Auto-Replace"), self.PAGE_REPLACE) + self.sidebar.setSelected(gotoPage) + self.sidebar.buttonClicked.connect(self._sidebarClicked) - self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + # Buttons + self.buttonBox = QDialogButtonBox( + QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel + ) self.buttonBox.accepted.connect(self._doSave) self.buttonBox.rejected.connect(self.close) - self.rejected.connect(self.close) - self.addControls(self.buttonBox) - # Focus Tab - self._focusTab(focusTab) + # Content + SHARED.project.countStatus() + + self.settingsPage = _SettingsPage(self) + self.statusPage = _StatusPage(self, True) + self.importPage = _StatusPage(self, False) + self.replacePage = _ReplacePage(self) + + self.mainStack = QStackedWidget(self) + self.mainStack.addWidget(self.settingsPage) + self.mainStack.addWidget(self.statusPage) + self.mainStack.addWidget(self.importPage) + self.mainStack.addWidget(self.replacePage) + + # Assemble + self.topBox = QHBoxLayout() + self.topBox.addWidget(self.titleLabel) + self.topBox.addStretch(1) + + self.mainBox = QHBoxLayout() + self.mainBox.addWidget(self.sidebar) + self.mainBox.addWidget(self.mainStack) + self.mainBox.setContentsMargins(0, 0, 0, 0) + + self.outerBox = QVBoxLayout() + self.outerBox.addLayout(self.topBox) + self.outerBox.addLayout(self.mainBox) + self.outerBox.addWidget(self.buttonBox) + self.outerBox.setSpacing(CONFIG.pxInt(8)) + + self.setLayout(self.outerBox) + self.setSizeGripEnabled(True) logger.debug("Ready: GuiProjectSettings") @@ -103,9 +130,13 @@ class GuiProjectSettings(NPagedDialog): logger.debug("Delete: GuiProjectSettings") return + ## + # Events + ## + def closeEvent(self, event: QCloseEvent) -> None: - """Capture the close event and perform cleanup.""" - self._saveGuiSettings() + """Capture the user closing the window and save settings.""" + self._saveSettings() event.accept() self.deleteLater() return @@ -114,40 +145,52 @@ class GuiProjectSettings(NPagedDialog): # Private Slots ## + @pyqtSlot(int) + def _sidebarClicked(self, pageId: int) -> None: + """Process a user request to switch page.""" + if pageId == self.PAGE_SETTINGS: + self.mainStack.setCurrentWidget(self.settingsPage) + elif pageId == self.PAGE_STATUS: + self.mainStack.setCurrentWidget(self.statusPage) + elif pageId == self.PAGE_IMPORT: + self.mainStack.setCurrentWidget(self.importPage) + elif pageId == self.PAGE_REPLACE: + self.mainStack.setCurrentWidget(self.replacePage) + return + @pyqtSlot() def _doSave(self) -> None: """Save settings and close dialog.""" project = SHARED.project - projName = self.tabMain.editName.text() - bookTitle = self.tabMain.editTitle.text() - bookAuthor = self.tabMain.editAuthor.text() - projLang = self.tabMain.projLang.currentData() - spellLang = self.tabMain.spellLang.currentData() - doBackup = not self.tabMain.doBackup.isChecked() + projName = self.settingsPage.projName.text() + projAuthor = self.settingsPage.projAuthor.text() + projLang = self.settingsPage.projLang.currentData() + spellLang = self.settingsPage.spellLang.currentData() + doBackup = not self.settingsPage.doBackup.isChecked() project.data.setName(projName) - project.data.setTitle(bookTitle) - project.data.setAuthor(bookAuthor) + project.data.setAuthor(projAuthor) project.data.setDoBackup(doBackup) project.data.setSpellLang(spellLang) project.setProjectLang(projLang) - if self.tabStatus.colChanged: - newList, delList = self.tabStatus.getNewList() + rebuildTrees = False + + if self.statusPage.wasChanged: + newList, delList = self.statusPage.getNewList() project.setStatusColours(newList, delList) + rebuildTrees = True - if self.tabImport.colChanged: - newList, delList = self.tabImport.getNewList() + if self.importPage.wasChanged: + newList, delList = self.importPage.getNewList() project.setImportColours(newList, delList) + rebuildTrees = True - if self.tabStatus.colChanged or self.tabImport.colChanged: - self.mainGui.rebuildTrees() - - if self.tabReplace.arChanged: - newList = self.tabReplace.getNewList() + if self.replacePage.wasChanged: + newList = self.replacePage.getNewList() project.data.setAutoReplace(newList) - self.newProjectSettingsReady.emit() + self.newProjectSettingsReady.emit(rebuildTrees) qApp.processEvents() self.close() @@ -157,134 +200,103 @@ class GuiProjectSettings(NPagedDialog): # Internal Functions ## - def _focusTab(self, tab: int) -> None: - """Change which is the focused tab.""" - if tab == self.TAB_MAIN: - self.setCurrentWidget(self.tabMain) - elif tab == self.TAB_STATUS: - self.setCurrentWidget(self.tabStatus) - elif tab == self.TAB_IMPORT: - self.setCurrentWidget(self.tabImport) - elif tab == self.TAB_REPLACE: - self.setCurrentWidget(self.tabReplace) - return - - def _saveGuiSettings(self) -> None: + def _saveSettings(self) -> None: """Save GUI settings.""" winWidth = CONFIG.rpxInt(self.width()) winHeight = CONFIG.rpxInt(self.height()) - replaceColW = CONFIG.rpxInt(self.tabReplace.listBox.columnWidth(0)) - statusColW = CONFIG.rpxInt(self.tabStatus.listBox.columnWidth(0)) - importColW = CONFIG.rpxInt(self.tabImport.listBox.columnWidth(0)) + statusColW = CONFIG.rpxInt(self.statusPage.columnWidth()) + importColW = CONFIG.rpxInt(self.importPage.columnWidth()) + replaceColW = CONFIG.rpxInt(self.replacePage.columnWidth()) logger.debug("Saving State: GuiProjectSettings") - pOptions = SHARED.project.options - pOptions.setValue("GuiProjectSettings", "winWidth", winWidth) - pOptions.setValue("GuiProjectSettings", "winHeight", winHeight) - pOptions.setValue("GuiProjectSettings", "replaceColW", replaceColW) - pOptions.setValue("GuiProjectSettings", "statusColW", statusColW) - pOptions.setValue("GuiProjectSettings", "importColW", importColW) + options = SHARED.project.options + options.setValue("GuiProjectSettings", "winWidth", winWidth) + options.setValue("GuiProjectSettings", "winHeight", winHeight) + options.setValue("GuiProjectSettings", "statusColW", statusColW) + options.setValue("GuiProjectSettings", "importColW", importColW) + options.setValue("GuiProjectSettings", "replaceColW", replaceColW) return # END Class GuiProjectSettings -class GuiProjectEditMain(QWidget): +class _SettingsPage(NScrollableForm): def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) - # The Form - self.mainForm = NConfigLayout() - self.mainForm.setHelpTextStyle(SHARED.theme.helpText) - self.setLayout(self.mainForm) + xW = CONFIG.pxInt(200) + data = SHARED.project.data + self.setHelpTextStyle(SHARED.theme.helpText) + self.setRowIndent(0) - self.mainForm.addGroupLabel(self.tr("Project Settings")) - - xW = CONFIG.pxInt(250) - pData = SHARED.project.data - - self.editName = QLineEdit() - self.editName.setMaxLength(200) - self.editName.setMaximumWidth(xW) - self.editName.setText(pData.name) - self.mainForm.addRow( - self.tr("Project name"), - self.editName, - self.tr("Should be set only once.") + # Project Name + self.projName = QLineEdit(self) + self.projName.setMaxLength(200) + self.projName.setMinimumWidth(xW) + self.projName.setText(data.name) + self.addRow( + self.tr("Project name"), self.projName, + self.tr("Changing this will affect the backup path."), + stretch=(3, 2) ) - self.editTitle = QLineEdit() - self.editTitle.setMaxLength(200) - self.editTitle.setMaximumWidth(xW) - self.editTitle.setText(pData.title) - self.mainForm.addRow( - self.tr("Novel title"), - self.editTitle, - self.tr("Change whenever you want!") - ) - - self.editAuthor = QLineEdit() - self.editAuthor.setMaxLength(200) - self.editAuthor.setMaximumWidth(xW) - self.editAuthor.setText(pData.author) - self.mainForm.addRow( - self.tr("Author(s)"), - self.editAuthor, - self.tr("Change whenever you want!") + # Project Author + self.projAuthor = QLineEdit(self) + self.projAuthor.setMaxLength(200) + self.projAuthor.setMinimumWidth(xW) + self.projAuthor.setText(data.author) + self.addRow( + self.tr("Author(s)"), self.projAuthor, + self.tr("Only used when building the manuscript."), + stretch=(3, 2) ) + # Project Language self.projLang = QComboBox(self) - self.projLang.setMaximumWidth(xW) + self.projLang.setMinimumWidth(xW) for tag, language in CONFIG.listLanguages(CONFIG.LANG_PROJ): self.projLang.addItem(language, tag) - self.mainForm.addRow( - self.tr("Project language"), - self.projLang, - self.tr("Used when building the manuscript.") + self.addRow( + self.tr("Project language"), self.projLang, + self.tr("Only used when building the manuscript."), + stretch=(3, 2) ) + if (idx := self.projLang.findData(data.language)) != -1: + self.projLang.setCurrentIndex(idx) - langIdx = 0 - if pData.language is not None: - langIdx = self.projLang.findData(pData.language) - if langIdx == -1: - langIdx = self.projLang.findData("en_GB") - if langIdx != -1: - self.projLang.setCurrentIndex(langIdx) - + # Spell Check Language self.spellLang = QComboBox(self) - self.spellLang.setMaximumWidth(xW) + self.spellLang.setMinimumWidth(xW) self.spellLang.addItem(self.tr("Default"), "None") if CONFIG.hasEnchant: for tag, language in SHARED.spelling.listDictionaries(): self.spellLang.addItem(language, tag) - self.mainForm.addRow( - self.tr("Spell check language"), - self.spellLang, - self.tr("Overrides main preferences.") + self.addRow( + self.tr("Spell check language"), self.spellLang, + self.tr("Overrides main preferences."), + stretch=(3, 2) ) + if (idx := self.spellLang.findData(data.spellLang)) != -1: + self.spellLang.setCurrentIndex(idx) - langIdx = 0 - if pData.spellLang is not None: - langIdx = self.spellLang.findData(pData.spellLang) - if langIdx != -1: - self.spellLang.setCurrentIndex(langIdx) - + # Backup on Close self.doBackup = NSwitch(self) - self.doBackup.setChecked(not pData.doBackup) - self.mainForm.addRow( - self.tr("No backup on close"), - self.doBackup, + self.doBackup.setChecked(not data.doBackup) + self.addRow( + self.tr("Disable backup on close"), self.doBackup, self.tr("Overrides main preferences.") ) + self.finalise() + return -# END Class GuiProjectEditMain +# END Class _SettingsPage -class GuiProjectEditStatus(QWidget): +class _StatusPage(NFixedPage): COL_LABEL = 0 COL_USAGE = 1 @@ -297,56 +309,54 @@ class GuiProjectEditStatus(QWidget): super().__init__(parent=parent) if isStatus: - self.theStatus = SHARED.project.data.itemStatus - pageLabel = self.tr("Novel File Status Levels") + status = SHARED.project.data.itemStatus + pageLabel = self.tr("Novel Document Status Levels") colSetting = "statusColW" else: - self.theStatus = SHARED.project.data.itemImport - pageLabel = self.tr("Note File Importance Levels") + status = SHARED.project.data.itemImport + pageLabel = self.tr("Project Note Importance Levels") colSetting = "importColW" wCol0 = CONFIG.pxInt( SHARED.project.options.getInt("GuiProjectSettings", colSetting, 130) ) - self.colDeleted = [] - self.colChanged = False - self.selColour = QColor(100, 100, 100) + self._changed = False + self._colDeleted = [] + self._selColour = QColor(100, 100, 100) self.iPx = SHARED.theme.baseIconSize - # The List - # ======== + # Title + self.pageTitle = NColourLabel( + pageLabel, SHARED.theme.helpText, parent=self, + scale=NColourLabel.HEADER_SCALE + ) - self.listBox = QTreeWidget() - self.listBox.setHeaderLabels([ - self.tr("Label"), self.tr("Usage"), - ]) + # List Box + self.listBox = QTreeWidget(self) + self.listBox.setHeaderLabels([self.tr("Label"), self.tr("Usage")]) self.listBox.itemSelectionChanged.connect(self._selectedItem) self.listBox.setColumnWidth(self.COL_LABEL, wCol0) self.listBox.setIndentation(0) - for key, entry in self.theStatus.items(): + for key, entry in status.items(): self._addItem(key, entry["name"], entry["cols"], entry["count"]) # List Controls - # ============= - - self.addButton = QPushButton(SHARED.theme.getIcon("add"), "") + self.addButton = QPushButton(SHARED.theme.getIcon("add"), "", self) self.addButton.clicked.connect(self._newItem) - self.delButton = QPushButton(SHARED.theme.getIcon("remove"), "") + self.delButton = QPushButton(SHARED.theme.getIcon("remove"), "", self) self.delButton.clicked.connect(self._delItem) - self.upButton = QPushButton(SHARED.theme.getIcon("up"), "") + self.upButton = QPushButton(SHARED.theme.getIcon("up"), "", self) self.upButton.clicked.connect(lambda: self._moveItem(-1)) - self.dnButton = QPushButton(SHARED.theme.getIcon("down"), "") + self.dnButton = QPushButton(SHARED.theme.getIcon("down"), "", self) self.dnButton.clicked.connect(lambda: self._moveItem(1)) # Edit Form - # ========= - self.editName = QLineEdit() self.editName.setMaxLength(40) self.editName.setPlaceholderText(self.tr("Select item to edit")) @@ -364,8 +374,6 @@ class GuiProjectEditStatus(QWidget): self.saveButton.clicked.connect(self._saveItem) # Assemble - # ======== - self.listControls = QVBoxLayout() self.listControls.addWidget(self.addButton) self.listControls.addWidget(self.delButton) @@ -387,16 +395,25 @@ class GuiProjectEditStatus(QWidget): self.innerBox.addLayout(self.listControls) self.outerBox = QVBoxLayout() - self.outerBox.addWidget(QLabel("%s" % pageLabel)) + self.outerBox.addWidget(self.pageTitle) self.outerBox.addLayout(self.innerBox) - self.setLayout(self.outerBox) + self.setCentralLayout(self.outerBox) return + @property + def wasChanged(self) -> bool: + """The user changed these settings.""" + return self._changed + + ## + # Methods + ## + def getNewList(self) -> tuple[list, list]: """Return list of entries.""" - if self.colChanged: + if self._changed: newList = [] for n in range(self.listBox.topLevelItemCount()): item = self.listBox.topLevelItem(n) @@ -406,10 +423,13 @@ class GuiProjectEditStatus(QWidget): "name": item.text(self.COL_LABEL), "cols": item.data(self.COL_LABEL, self.COL_ROLE), }) - return newList, self.colDeleted - + return newList, self._colDeleted return [], [] + def columnWidth(self) -> int: + """Return the size of the header column.""" + return self.listBox.columnWidth(0) + ## # Private Slots ## @@ -417,12 +437,12 @@ class GuiProjectEditStatus(QWidget): @pyqtSlot() def _selectColour(self) -> None: """Open a dialog to select the status icon colour.""" - if self.selColour is not None: + if self._selColour is not None: newCol = QColorDialog.getColor( - self.selColour, self, self.tr("Select Colour") + self._selColour, self, self.tr("Select Colour") ) if newCol.isValid(): - self.selColour = newCol + self._selColour = newCol pixmap = QPixmap(self.iPx, self.iPx) pixmap.fill(newCol) self.colButton.setIcon(QIcon(pixmap)) @@ -433,7 +453,7 @@ class GuiProjectEditStatus(QWidget): def _newItem(self) -> None: """Create a new status item.""" self._addItem(None, self.tr("New Item"), (100, 100, 100), 0) - self.colChanged = True + self._changed = True return @pyqtSlot() @@ -446,8 +466,8 @@ class GuiProjectEditStatus(QWidget): SHARED.error(self.tr("Cannot delete a status item that is in use.")) else: self.listBox.takeTopLevelItem(iRow) - self.colDeleted.append(selItem.data(self.COL_LABEL, self.KEY_ROLE)) - self.colChanged = True + self._colDeleted.append(selItem.data(self.COL_LABEL, self.KEY_ROLE)) + self._changed = True return @pyqtSlot() @@ -458,9 +478,9 @@ class GuiProjectEditStatus(QWidget): selItem.setText(self.COL_LABEL, simplified(self.editName.text())) selItem.setIcon(self.COL_LABEL, self.colButton.icon()) selItem.setData(self.COL_LABEL, self.COL_ROLE, ( - self.selColour.red(), self.selColour.green(), self.selColour.blue() + self._selColour.red(), self._selColour.green(), self._selColour.blue() )) - self.colChanged = True + self._changed = True return @pyqtSlot() @@ -474,7 +494,7 @@ class GuiProjectEditStatus(QWidget): name = selItem.text(self.COL_LABEL) pixmap = QPixmap(self.iPx, self.iPx) pixmap.fill(QColor(*cols)) - self.selColour = QColor(*cols) + self._selColour = QColor(*cols) self.editName.setText(name) self.colButton.setIcon(QIcon(pixmap)) self.editName.selectAll() @@ -485,7 +505,7 @@ class GuiProjectEditStatus(QWidget): else: pixmap = QPixmap(self.iPx, self.iPx) pixmap.fill(QColor(100, 100, 100)) - self.selColour = QColor(100, 100, 100) + self._selColour = QColor(100, 100, 100) self.editName.setText("") self.colButton.setIcon(QIcon(pixmap)) self.editName.setEnabled(False) @@ -498,16 +518,16 @@ class GuiProjectEditStatus(QWidget): ## def _addItem(self, key: str | None, name: str, - cols: tuple[int, int, int], count: int) -> None: + colour: tuple[int, int, int], count: int) -> None: """Add a status item to the list.""" pixmap = QPixmap(self.iPx, self.iPx) - pixmap.fill(QColor(*cols)) + pixmap.fill(QColor(*colour)) item = QTreeWidgetItem() item.setText(self.COL_LABEL, name) item.setIcon(self.COL_LABEL, QIcon(pixmap)) item.setData(self.COL_LABEL, self.KEY_ROLE, key) - item.setData(self.COL_LABEL, self.COL_ROLE, cols) + item.setData(self.COL_LABEL, self.COL_ROLE, colour) item.setData(self.COL_LABEL, self.NUM_ROLE, count) item.setText(self.COL_USAGE, self._usageString(count)) @@ -533,30 +553,29 @@ class GuiProjectEditStatus(QWidget): if cItem is not None: cItem.setSelected(True) - self.colChanged = True + self._changed = True return def _getSelectedItem(self) -> QTreeWidgetItem | None: """Get the currently selected item.""" - selItem = self.listBox.selectedItems() - if len(selItem) > 0: - return selItem[0] + if items := self.listBox.selectedItems(): + return items[0] return None - def _usageString(self, nUse: int) -> str: + def _usageString(self, count: int) -> str: """Generate usage string.""" - if nUse == 0: + if count == 0: return self.tr("Not in use") - elif nUse == 1: + elif count == 1: return self.tr("Used once") else: - return self.tr("Used by {0} items").format(nUse) + return self.tr("Used by {0} items").format(count) -# END Class GuiProjectEditStatus +# END Class _StatusPage -class GuiProjectEditReplace(QWidget): +class _ReplacePage(NFixedPage): COL_KEY = 0 COL_REPL = 1 @@ -564,24 +583,24 @@ class GuiProjectEditReplace(QWidget): def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) - self.arChanged = False + self._changed = False wCol0 = CONFIG.pxInt( SHARED.project.options.getInt("GuiProjectSettings", "replaceColW", 130) ) - pageLabel = self.tr("Text Replace List for Preview and Export") + + # Title + self.pageTitle = NColourLabel( + self.tr("Text Auto-Replace for Preview and Build"), + SHARED.theme.helpText, parent=self, scale=NColourLabel.HEADER_SCALE + ) # List Box - # ======== - self.listBox = QTreeWidget() - self.listBox.setHeaderLabels([ - self.tr("Keyword"), - self.tr("Replace With"), - ]) - self.listBox.itemSelectionChanged.connect(self._selectedItem) + self.listBox.setHeaderLabels([self.tr("Keyword"), self.tr("Replace With")]) self.listBox.setColumnWidth(self.COL_KEY, wCol0) self.listBox.setIndentation(0) + self.listBox.itemSelectionChanged.connect(self._selectedItem) for aKey, aVal in SHARED.project.data.autoReplace.items(): newItem = QTreeWidgetItem(["<%s>" % aKey, aVal]) @@ -591,8 +610,6 @@ class GuiProjectEditReplace(QWidget): self.listBox.setSortingEnabled(True) # List Controls - # ============= - self.addButton = QPushButton(SHARED.theme.getIcon("add"), "") self.addButton.clicked.connect(self._addEntry) @@ -600,8 +617,6 @@ class GuiProjectEditReplace(QWidget): self.delButton.clicked.connect(self._delEntry) # Edit Form - # ========= - self.editKey = QLineEdit() self.editKey.setPlaceholderText(self.tr("Select item to edit")) self.editKey.setEnabled(False) @@ -615,8 +630,6 @@ class GuiProjectEditReplace(QWidget): self.saveButton.clicked.connect(self._saveEntry) # Assemble - # ======== - self.listControls = QVBoxLayout() self.listControls.addWidget(self.addButton) self.listControls.addWidget(self.delButton) @@ -636,50 +649,61 @@ class GuiProjectEditReplace(QWidget): self.innerBox.addLayout(self.listControls) self.outerBox = QVBoxLayout() - self.outerBox.addWidget(QLabel("%s" % pageLabel)) + self.outerBox.addWidget(self.pageTitle) self.outerBox.addLayout(self.innerBox) - self.setLayout(self.outerBox) + self.setCentralLayout(self.outerBox) return + @property + def wasChanged(self) -> bool: + """The user changed these settings.""" + return self._changed + + ## + # Methods + ## + def getNewList(self) -> dict: """Extract the list from the widget.""" new = {} for n in range(self.listBox.topLevelItemCount()): - tItem = self.listBox.topLevelItem(n) - if tItem is not None: + if tItem := self.listBox.topLevelItem(n): aKey = self._stripNotAllowed(tItem.text(0)) aVal = tItem.text(1) if len(aKey) > 0: new[aKey] = aVal return new + def columnWidth(self) -> int: + """Return the size of the header column.""" + return self.listBox.columnWidth(0) + ## - # Internal Functions + # Private Slots ## - def _selectedItem(self) -> bool: + @pyqtSlot() + def _selectedItem(self) -> None: """Extract the details from the selected item and populate the edit form. """ - selItem = self._getSelectedItem() - if selItem is None: - return False - editKey = self._stripNotAllowed(selItem.text(0)) - editVal = selItem.text(1) - self.editKey.setText(editKey) - self.editValue.setText(editVal) - self.editKey.setEnabled(True) - self.editValue.setEnabled(True) - self.editKey.selectAll() - self.editKey.setFocus() - return True + if selItem := self._getSelectedItem(): + editKey = self._stripNotAllowed(selItem.text(0)) + editVal = selItem.text(1) + self.editKey.setText(editKey) + self.editValue.setText(editVal) + self.editKey.setEnabled(True) + self.editValue.setEnabled(True) + self.editKey.selectAll() + self.editKey.setFocus() + return + @pyqtSlot() def _saveEntry(self) -> None: """Save the form data into the list widget.""" - selItem = self._getSelectedItem() - if selItem: + if selItem := self._getSelectedItem(): newKey = self.editKey.text() newVal = self.editValue.text() saveKey = self._stripNotAllowed(newKey) @@ -691,38 +715,36 @@ class GuiProjectEditReplace(QWidget): self.editKey.setEnabled(False) self.editValue.setEnabled(False) self.listBox.clearSelection() - self.arChanged = True + self._changed = True return + @pyqtSlot() def _addEntry(self) -> None: """Add a new list entry.""" saveKey = "" % (self.listBox.topLevelItemCount() + 1) - newVal = "" - newItem = QTreeWidgetItem([saveKey, newVal]) - self.listBox.addTopLevelItem(newItem) + self.listBox.addTopLevelItem(QTreeWidgetItem([saveKey, ""])) return + @pyqtSlot() def _delEntry(self) -> None: """Delete the selected entry.""" - selItem = self._getSelectedItem() - if selItem: + if selItem := self._getSelectedItem(): self.listBox.takeTopLevelItem(self.listBox.indexOfTopLevelItem(selItem)) - self.arChanged = True + self._changed = True return + ## + # Internal Functions + ## + def _getSelectedItem(self) -> QTreeWidgetItem | None: """Extract the currently selected item.""" - selItem = self.listBox.selectedItems() - if len(selItem) == 0: - return None - return selItem[0] + if items := self.listBox.selectedItems(): + return items[0] + return None def _stripNotAllowed(self, key: str) -> str: """Clean up the replace key string.""" - result = "" - for c in key: - if c.isalnum(): - result += c - return result + return "".join(c for c in key if c.isalnum()) -# END Class GuiProjectEditReplace +# END Class _ReplacePage diff --git a/novelwriter/extensions/configlayout.py b/novelwriter/extensions/configlayout.py index c71036be..99d38c03 100644 --- a/novelwriter/extensions/configlayout.py +++ b/novelwriter/extensions/configlayout.py @@ -6,6 +6,8 @@ File History: Created: 2020-05-03 [0.4.5] NConfigLayout, NColourLabel Created: 2023-05-23 [2.1b1] NSimpleLayout Created: 2024-01-08 [2.3b1] NScrollableForm +Created: 2024-01-26 [2.3b1] NScrollablePage +Created: 2024-01-26 [2.3b1] NFixedPage This file is a part of novelWriter Copyright 2018–2024, Veronica Berglyd Olsen @@ -28,17 +30,59 @@ from __future__ import annotations from PyQt5.QtGui import QColor, QPalette from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( - QAbstractButton, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QScrollArea, QSizePolicy, - QVBoxLayout, QWidget + QAbstractButton, QFrame, QGridLayout, QHBoxLayout, QLabel, QLayout, + QScrollArea, QSizePolicy, QVBoxLayout, QWidget ) from novelwriter import CONFIG -FONT_SCALE = 0.9 +DEFAULT_SCALE = 0.9 RIGHT_TOP = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop LEFT_TOP = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop +class NFixedPage(QFrame): + """Extension: Fixed Page Widget + + A custom widget that holds a layout. This is just a wrapper around a + QFrame that sets the same frame style as the other Page widgets. + """ + + def __init__(self, parent: QWidget) -> None: + super().__init__(parent=parent) + self.setFrameShadow(QFrame.Shadow.Sunken) + self.setFrameShape(QFrame.Shape.StyledPanel) + self.setCentralLayout = self.setLayout + return + +# END Class NFixedPage + + +class NScrollablePage(QScrollArea): + """Extension: Scrollable Page Widget + + A custom widget that holds a layout within a scrollable area. + """ + + def __init__(self, parent: QWidget) -> None: + super().__init__(parent=parent) + self._widget = QWidget(self) + self.setWidget(self._widget) + self.setWidgetResizable(True) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self.setFrameShadow(QFrame.Shadow.Sunken) + self.setFrameShape(QFrame.Shape.StyledPanel) + return + + def setCentralLayout(self, layout: QLayout) -> None: + """Set the central layout of the scroll page.""" + self._widget.setLayout(layout) + return + +# END Class NScrollablePage + + class NScrollableForm(QScrollArea): """Extension: Scrollable Form Widget @@ -48,8 +92,9 @@ class NScrollableForm(QScrollArea): def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) self._helpCol = QColor(0, 0, 0) - self._fontScale = FONT_SCALE + self._fontScale = DEFAULT_SCALE self._first = True + self._indent = CONFIG.pxInt(12) self._sections: dict[int, QLabel] = {} self._editable: dict[str, NColourLabel] = {} @@ -65,6 +110,8 @@ class NScrollableForm(QScrollArea): self.setWidgetResizable(True) self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self.setFrameShadow(QFrame.Shadow.Sunken) + self.setFrameShape(QFrame.Shape.StyledPanel) return @@ -80,7 +127,7 @@ class NScrollableForm(QScrollArea): # Setters ## - def setHelpTextStyle(self, color: QColor, scale: float = FONT_SCALE) -> None: + def setHelpTextStyle(self, color: QColor, scale: float = DEFAULT_SCALE) -> None: """Set the text color for the help text.""" self._helpCol = color self._fontScale = scale @@ -92,6 +139,11 @@ class NScrollableForm(QScrollArea): qHelp.setText(text) return + def setRowIndent(self, indent: int) -> None: + """Set the indentation of each row.""" + self._indent = max(indent, 0) + return + ## # Methods ## @@ -123,37 +175,43 @@ class NScrollableForm(QScrollArea): return def addRow(self, label: str, widget: QWidget, helpText: str = "", unit: str | None = None, - button: QWidget | None = None, editable: str | None = None) -> None: + button: QWidget | None = None, editable: str | None = None, + stretch: tuple[int, int] = (0, 0)) -> None: """Add a label and a widget as a new row of the form.""" row = QHBoxLayout() - row.setSpacing(CONFIG.pxInt(4)) + row.setSpacing(CONFIG.pxInt(12)) - mPx = CONFIG.pxInt(12) qLabel = QLabel(label, self) - qLabel.setIndent(mPx) + qLabel.setIndent(self._indent) qLabel.setBuddy(widget) if helpText: - qHelp = NColourLabel(str(helpText), self._helpCol, scale=self._fontScale, wrap=True) - qHelp.setIndent(mPx) + qHelp = NColourLabel( + str(helpText), self._helpCol, parent=self, + scale=self._fontScale, wrap=True, indent=self._indent + ) labelBox = QVBoxLayout() labelBox.addWidget(qLabel) labelBox.addWidget(qHelp) labelBox.setSpacing(0) - labelBox.addStretch(1) - row.addLayout(labelBox) + row.addLayout(labelBox, stretch[0]) if editable: self._editable[editable] = qHelp else: - row.addWidget(qLabel) - - row.addSpacing(mPx) - row.addWidget(widget) + row.addWidget(qLabel, stretch[0]) if isinstance(unit, str): - row.addWidget(QLabel(unit, self)) + box = QHBoxLayout() + box.addWidget(widget) + box.addWidget(QLabel(unit, self)) + row.addLayout(box, stretch[1]) elif isinstance(button, QAbstractButton): - row.addWidget(button) + box = QHBoxLayout() + box.addWidget(widget) + box.addWidget(button) + row.addLayout(box, stretch[1]) + else: + row.addWidget(widget, stretch[1]) self._layout.addLayout(row) self._index[label.strip()] = widget @@ -177,7 +235,7 @@ class NConfigLayout(QGridLayout): self._nextRow = 0 self._helpCol = QColor(0, 0, 0) - self._fontScale = FONT_SCALE + self._fontScale = DEFAULT_SCALE self._itemMap = {} wSp = CONFIG.pxInt(8) @@ -187,16 +245,6 @@ class NConfigLayout(QGridLayout): return - ## - # Getters and Setters - ## - - def setHelpTextStyle(self, color: QColor, scale: float = FONT_SCALE) -> None: - """Set the text color for the help text.""" - self._helpCol = color if isinstance(color, QColor) else QColor(*color) - self._fontScale = scale - return - ## # Class Methods ## @@ -212,8 +260,8 @@ class NConfigLayout(QGridLayout): self._nextRow += 1 return - def addRow(self, label: str, widget: QWidget, helpText: str | None = None, - unit: str | None = None, button: QWidget | None = None) -> int: + def addRow(self, label: str, widget: QWidget, unit: str | None = None, + button: QWidget | None = None) -> int: """Add a label and a widget as a new row of the grid.""" wSp = CONFIG.pxInt(8) qLabel = QLabel(label) @@ -221,17 +269,7 @@ class NConfigLayout(QGridLayout): qLabel.setBuddy(widget) qHelp = None - if helpText is not None: - qHelp = NColourLabel(str(helpText), self._helpCol, scale=self._fontScale, wrap=True) - qHelp.setIndent(wSp) - labelBox = QVBoxLayout() - labelBox.addWidget(qLabel) - labelBox.addWidget(qHelp) - labelBox.setSpacing(0) - labelBox.addStretch(1) - self.addLayout(labelBox, self._nextRow, 0, 1, 1, LEFT_TOP) - else: - self.addWidget(qLabel, self._nextRow, 0, 1, 1, LEFT_TOP) + self.addWidget(qLabel, self._nextRow, 0, 1, 1, LEFT_TOP) if isinstance(unit, str): controlBox = QHBoxLayout() @@ -248,12 +286,7 @@ class NConfigLayout(QGridLayout): self.addLayout(controlBox, self._nextRow, 1, 1, 1, RIGHT_TOP) else: - if isinstance(widget, QLineEdit): - qLayout = QHBoxLayout() - qLayout.addWidget(widget) - self.addLayout(qLayout, self._nextRow, 1, 1, 1, RIGHT_TOP) - else: - self.addWidget(widget, self._nextRow, 1, 1, 1, RIGHT_TOP) + self.addWidget(widget, self._nextRow, 1, 1, 1, RIGHT_TOP) self.setRowStretch(self._nextRow, 0) self.setRowStretch(self._nextRow+1, 1) @@ -321,17 +354,21 @@ class NColourLabel(QLabel): optionally at a specific size, and word wrapped. """ + HELP_SCALE = DEFAULT_SCALE + HEADER_SCALE = 1.25 + def __init__(self, text: str, color: QColor, parent: QWidget | None = None, - scale: float = FONT_SCALE, wrap: bool = False) -> None: + scale: float = HELP_SCALE, wrap: bool = False, indent: int = 0) -> None: super().__init__(text, parent=parent) - lblCol = self.palette() - lblCol.setColor(QPalette.WindowText, color) - self.setPalette(lblCol) + font = self.font() + font.setPointSizeF(scale*font.pointSizeF()) + colour = self.palette() + colour.setColor(QPalette.WindowText, color) - lblFont = self.font() - lblFont.setPointSizeF(scale*lblFont.pointSizeF()) - self.setFont(lblFont) + self.setPalette(colour) + self.setFont(font) + self.setIndent(indent) if wrap: self.setWordWrap(True) diff --git a/novelwriter/extensions/pageddialog.py b/novelwriter/extensions/pageddialog.py deleted file mode 100644 index a620cb74..00000000 --- a/novelwriter/extensions/pageddialog.py +++ /dev/null @@ -1,130 +0,0 @@ -""" -novelWriter – Custom Widget: Paged Dialog -========================================= - -File History: -Created: 2020-05-17 [0.5.1] - -This file is a part of novelWriter -Copyright 2018–2024, 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 - -from PyQt5.QtGui import QPaintEvent -from PyQt5.QtCore import QRect, QPoint, QSize -from PyQt5.QtWidgets import ( - QDialog, QHBoxLayout, QStyle, QStyleOptionTab, QStylePainter, QTabBar, - QTabWidget, QVBoxLayout, QWidget -) - -from novelwriter import CONFIG - - -class NPagedDialog(QDialog): - - def __init__(self, parent: QWidget) -> None: - super().__init__(parent=parent) - - self._tabBar = NVerticalTabBar(self) - self._tabBar.setExpanding(False) - - self._tabBox = QTabWidget(self) - self._tabBox.setTabBar(self._tabBar) - self._tabBox.setTabPosition(QTabWidget.West) - - self._buttonBox = QHBoxLayout() - - self._outerBox = QVBoxLayout() - self._outerBox.addWidget(self._tabBox) - self._outerBox.addLayout(self._buttonBox) - - # Default Margins - thisStyle = self.style() - mL = thisStyle.pixelMetric(QStyle.PM_LayoutLeftMargin) - mR = thisStyle.pixelMetric(QStyle.PM_LayoutRightMargin) - mT = thisStyle.pixelMetric(QStyle.PM_LayoutLeftMargin) - mB = thisStyle.pixelMetric(QStyle.PM_LayoutBottomMargin) - - # Set Margins - self.setContentsMargins(0, 0, 0, 0) - self._outerBox.setContentsMargins(0, 0, 0, mB) - self._buttonBox.setContentsMargins(mL, 0, mR, 0) - self._outerBox.setSpacing(mT) - - self.setLayout(self._outerBox) - - return - - def addTab(self, widget: QWidget, label: str) -> None: - """Forward the adding of tabs to the QTabWidget.""" - self._tabBox.addTab(widget, label) - return - - def addControls(self, buttonBar: QWidget) -> None: - """Add a button bar to the dialog.""" - self._buttonBox.addWidget(buttonBar) - return - - def setCurrentWidget(self, widget: QWidget) -> None: - """Forward the changing of tab to the QTabWidget.""" - self._tabBox.setCurrentWidget(widget) - return - -# END Class NPagedDialog - - -class NVerticalTabBar(QTabBar): - - def __init__(self, parent: QWidget) -> None: - super().__init__(parent=parent) - self._mW = CONFIG.pxInt(150) - return - - def tabSizeHint(self, index: int) -> QSize: - """Return a transposed size hint for the rotated bar.""" - tSize = super().tabSizeHint(index) - tSize.transpose() - tSize.setWidth(min(tSize.width(), self._mW)) - return tSize - - def paintEvent(self, event: QPaintEvent) -> None: - """Custom implementation of the label painter that rotates the - label 90 degrees. - """ - pObj = QStylePainter(self) - oObj = QStyleOptionTab() - - for i in range(self.count()): - self.initStyleOption(oObj, i) - pObj.drawControl(QStyle.CE_TabBarTabShape, oObj) - pObj.save() - - oSize = oObj.rect.size() - oSize.transpose() - oRect = QRect(QPoint(), oSize) - oRect.moveCenter(oObj.rect.center()) - oObj.rect = oRect - - oCenter = self.tabRect(i).center() - pObj.translate(oCenter) - pObj.rotate(90) - pObj.translate(-oCenter) - pObj.drawControl(QStyle.CE_TabBarTabLabel, oObj) - pObj.restore() - - return - -# END Class NVerticalTabBar diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 8c42a107..da050876 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -48,7 +48,7 @@ from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter from novelwriter.dialogs.docmerge import GuiDocMerge from novelwriter.dialogs.docsplit import GuiDocSplit from novelwriter.dialogs.editlabel import GuiEditLabel -from novelwriter.dialogs.projsettings import GuiProjectSettings +from novelwriter.dialogs.projectsettings import GuiProjectSettings from novelwriter.enum import ( nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwWidget ) @@ -1748,7 +1748,7 @@ class _TreeContextMenu(QMenu): menu.addSeparator() action = menu.addAction(self.tr("Manage Labels ...")) action.triggered.connect( - lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.TAB_STATUS) + lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.PAGE_STATUS) ) else: menu = self.addMenu(self.tr("Set Importance to ...")) @@ -1765,7 +1765,7 @@ class _TreeContextMenu(QMenu): menu.addSeparator() action = menu.addAction(self.tr("Manage Labels ...")) action.triggered.connect( - lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.TAB_IMPORT) + lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.PAGE_IMPORT) ) return diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 795c058a..0282b8d7 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -53,7 +53,7 @@ from novelwriter.dialogs.about import GuiAbout from novelwriter.dialogs.updates import GuiUpdates from novelwriter.dialogs.wordlist import GuiWordList from novelwriter.dialogs.preferences import GuiPreferences -from novelwriter.dialogs.projsettings import GuiProjectSettings +from novelwriter.dialogs.projectsettings import GuiProjectSettings from novelwriter.tools.welcome import GuiWelcome from novelwriter.tools.manuscript import GuiManuscript from novelwriter.tools.dictionaries import GuiDictionaries @@ -809,10 +809,10 @@ class GuiMain(QMainWindow): @pyqtSlot() @pyqtSlot(int) - def showProjectSettingsDialog(self, focusTab: int = GuiProjectSettings.TAB_MAIN) -> None: + def showProjectSettingsDialog(self, focusTab: int = GuiProjectSettings.PAGE_SETTINGS) -> None: """Open the project settings dialog.""" if SHARED.hasProject: - dialog = GuiProjectSettings(self, focusTab=focusTab) + dialog = GuiProjectSettings(self, gotoPage=focusTab) dialog.newProjectSettingsReady.connect(self._processProjectSettingsChanges) dialog.exec_() return @@ -1111,13 +1111,15 @@ class GuiMain(QMainWindow): return - @pyqtSlot() - def _processProjectSettingsChanges(self) -> None: + @pyqtSlot(bool) + def _processProjectSettingsChanges(self, rebuildTrees: bool) -> None: """Refresh data dependent on project settings.""" logger.debug("Applying new project settings") SHARED.updateSpellCheckLanguage() self.itemDetails.refreshDetails() self._updateWindowTitle(SHARED.project.data.name) + if rebuildTrees: + self.rebuildTrees() return @pyqtSlot() diff --git a/novelwriter/tools/noveldetails.py b/novelwriter/tools/noveldetails.py index 3fcd54cc..71375d7f 100644 --- a/novelwriter/tools/noveldetails.py +++ b/novelwriter/tools/noveldetails.py @@ -38,14 +38,12 @@ from novelwriter import CONFIG, SHARED from novelwriter.common import formatTime, numberToRoman from novelwriter.constants import nwUnicode from novelwriter.extensions.switch import NSwitch -from novelwriter.extensions.configlayout import NColourLabel +from novelwriter.extensions.configlayout import NColourLabel, NFixedPage, NScrollablePage from novelwriter.extensions.pagedsidebar import NPagedSideBar from novelwriter.extensions.novelselector import NovelSelector logger = logging.getLogger(__name__) -HEADER_SIZE = 1.4 - class GuiNovelDetails(QDialog): @@ -59,21 +57,18 @@ class GuiNovelDetails(QDialog): self.setObjectName("GuiNovelDetails") self.setWindowTitle(self.tr("Novel Details")) - wW = CONFIG.pxInt(500) - wH = CONFIG.pxInt(400) options = SHARED.project.options - - self.setMinimumSize(wW, wH) + self.setMinimumSize(CONFIG.pxInt(500), CONFIG.pxInt(400)) self.resize( - CONFIG.pxInt(options.getInt("GuiNovelDetails", "winWidth", wW)), - CONFIG.pxInt(options.getInt("GuiNovelDetails", "winHeight", wH)) + CONFIG.pxInt(options.getInt("GuiNovelDetails", "winWidth", CONFIG.pxInt(650))), + CONFIG.pxInt(options.getInt("GuiNovelDetails", "winHeight", CONFIG.pxInt(500))) ) # Title self.titleLabel = NColourLabel( - self.tr("Novel Details"), SHARED.theme.helpText, parent=self, scale=1.25 + self.tr("Novel Details"), SHARED.theme.helpText, + parent=self, scale=NColourLabel.HEADER_SCALE, indent=CONFIG.pxInt(4) ) - self.titleLabel.setIndent(CONFIG.pxInt(4)) # Novel Selector self.novelSelector = NovelSelector(self) @@ -192,7 +187,7 @@ class GuiNovelDetails(QDialog): # END Class GuiNovelDetails -class _OverviewPage(QWidget): +class _OverviewPage(NScrollablePage): def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) @@ -204,7 +199,8 @@ class _OverviewPage(QWidget): # Project Info self.projLabel = NColourLabel( - self.tr("Project"), SHARED.theme.helpText, parent=self, scale=HEADER_SIZE + self.tr("Project"), SHARED.theme.helpText, + parent=self, scale=NColourLabel.HEADER_SCALE ) self.projName = QLabel("", self) @@ -227,7 +223,8 @@ class _OverviewPage(QWidget): # Novel Info self.novelLabel = NColourLabel( - self.tr("Selected Novel"), SHARED.theme.helpText, parent=self, scale=HEADER_SIZE + self.tr("Selected Novel"), SHARED.theme.helpText, + parent=self, scale=NColourLabel.HEADER_SCALE ) self.novelName = QLabel("", self) @@ -250,11 +247,10 @@ class _OverviewPage(QWidget): self.outerBox.addLayout(self.projForm) self.outerBox.addWidget(self.novelLabel) self.outerBox.addLayout(self.novelForm) - self.outerBox.setContentsMargins(0, 0, 0, 0) self.outerBox.setSpacing(sPx) self.outerBox.addStretch(1) - self.setLayout(self.outerBox) + self.setCentralLayout(self.outerBox) return @@ -299,7 +295,7 @@ class _OverviewPage(QWidget): # END Class _OverviewPage -class _ContentsPage(QWidget): +class _ContentsPage(NFixedPage): C_TITLE = 0 C_WORDS = 1 @@ -320,7 +316,8 @@ class _ContentsPage(QWidget): # Title self.contentLabel = NColourLabel( - self.tr("Table of Contents"), SHARED.theme.helpText, parent=self, scale=HEADER_SIZE + self.tr("Table of Contents"), SHARED.theme.helpText, + parent=self, scale=NColourLabel.HEADER_SCALE ) # Contents Tree @@ -407,20 +404,25 @@ class _ContentsPage(QWidget): self.outerBox.addWidget(self.contentLabel) self.outerBox.addWidget(self.tocTree) self.outerBox.addLayout(self.optionsBox) - self.outerBox.setContentsMargins(0, 0, 0, 0) - self.setLayout(self.outerBox) + self.setCentralLayout(self.outerBox) return def saveSettings(self) -> None: """Save the user GUI settings.""" + widthCol0 = CONFIG.rpxInt(self.tocTree.columnWidth(0)) + widthCol1 = CONFIG.rpxInt(self.tocTree.columnWidth(1)) + widthCol2 = CONFIG.rpxInt(self.tocTree.columnWidth(2)) + widthCol3 = CONFIG.rpxInt(self.tocTree.columnWidth(3)) + widthCol4 = CONFIG.rpxInt(self.tocTree.columnWidth(4)) + options = SHARED.project.options - options.setValue("GuiNovelDetails", "widthCol0", self.tocTree.columnWidth(0)) - options.setValue("GuiNovelDetails", "widthCol1", self.tocTree.columnWidth(1)) - options.setValue("GuiNovelDetails", "widthCol2", self.tocTree.columnWidth(2)) - options.setValue("GuiNovelDetails", "widthCol3", self.tocTree.columnWidth(3)) - options.setValue("GuiNovelDetails", "widthCol4", self.tocTree.columnWidth(4)) + options.setValue("GuiNovelDetails", "widthCol0", widthCol0) + options.setValue("GuiNovelDetails", "widthCol1", widthCol1) + options.setValue("GuiNovelDetails", "widthCol2", widthCol2) + options.setValue("GuiNovelDetails", "widthCol3", widthCol3) + options.setValue("GuiNovelDetails", "widthCol4", widthCol4) options.setValue("GuiNovelDetails", "wordsPerPage", self.wpValue.value()) options.setValue("GuiNovelDetails", "countFrom", self.poValue.value()) options.setValue("GuiNovelDetails", "clearDouble", self.dblValue.isChecked()) diff --git a/tests/test_dialogs/test_dlg_projsettings.py b/tests/test_dialogs/test_dlg_projectsettings.py similarity index 50% rename from tests/test_dialogs/test_dlg_projsettings.py rename to tests/test_dialogs/test_dlg_projectsettings.py index 37556245..5b4d5499 100644 --- a/tests/test_dialogs/test_dlg_projsettings.py +++ b/tests/test_dialogs/test_dlg_projectsettings.py @@ -31,7 +31,7 @@ from PyQt5.QtWidgets import QDialog, QAction, QColorDialog from novelwriter import CONFIG, SHARED from novelwriter.enum import nwItemType from novelwriter.dialogs.editlabel import GuiEditLabel -from novelwriter.dialogs.projsettings import GuiProjectSettings +from novelwriter.dialogs.projectsettings import GuiProjectSettings KEY_DELAY = 1 @@ -63,17 +63,17 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI): qtbot.addWidget(projSettings) # Switch Tabs - projSettings._focusTab(GuiProjectSettings.TAB_REPLACE) - assert projSettings._tabBox.currentWidget() == projSettings.tabReplace + projSettings.sidebar.button(GuiProjectSettings.PAGE_SETTINGS).click() + assert projSettings.mainStack.currentWidget() == projSettings.settingsPage - projSettings._focusTab(GuiProjectSettings.TAB_IMPORT) - assert projSettings._tabBox.currentWidget() == projSettings.tabImport + projSettings.sidebar.button(GuiProjectSettings.PAGE_STATUS).click() + assert projSettings.mainStack.currentWidget() == projSettings.statusPage - projSettings._focusTab(GuiProjectSettings.TAB_STATUS) - assert projSettings._tabBox.currentWidget() == projSettings.tabStatus + projSettings.sidebar.button(GuiProjectSettings.PAGE_IMPORT).click() + assert projSettings.mainStack.currentWidget() == projSettings.importPage - projSettings._focusTab(GuiProjectSettings.TAB_MAIN) - assert projSettings._tabBox.currentWidget() == projSettings.tabMain + projSettings.sidebar.button(GuiProjectSettings.PAGE_REPLACE).click() + assert projSettings.mainStack.currentWidget() == projSettings.replacePage # Clean Up projSettings.close() @@ -83,9 +83,11 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI): @pytest.mark.gui -def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd): - """Test the main tab of the project settings dialog.""" - monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")]) +def testDlgProjSettings_SettingsPage(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd): + """Test the settings page of the dialog.""" + languages = [("en", "English"), ("de", "German")] + monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda *a: languages) + monkeypatch.setattr(CONFIG, "listLanguages", lambda *a: languages) # Create new project buildTestProject(nwGUI, projPath) @@ -94,72 +96,57 @@ def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockR # Set some values project = SHARED.project + project.data.setLanguage("en") project.data.setSpellLang("en") project.data.setAuthor("Jane Smith") project.data.setAutoReplace({"A": "B", "C": "D"}) # Create Dialog - projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.TAB_MAIN) + projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.PAGE_SETTINGS) projSettings.show() qtbot.addWidget(projSettings) # Settings Tab - # ============ + settings = projSettings.settingsPage - tabMain = projSettings.tabMain + assert settings.projName.text() == "New Project" + assert settings.projAuthor.text() == "Jane Smith" + assert settings.projLang.currentData() == "en" + assert settings.spellLang.currentData() == "en" + assert settings.doBackup.isChecked() is False - assert tabMain.editName.text() == "New Project" - assert tabMain.editTitle.text() == "New Novel" - assert tabMain.editAuthor.text() == "Jane Smith" - assert tabMain.spellLang.currentData() == "en" - assert tabMain.doBackup.isChecked() is False - - tabMain.editName.setText("") - for c in "Project Name": - qtbot.keyClick(tabMain.editName, c, delay=KEY_DELAY) - tabMain.editTitle.setText("") - for c in "Project Title": - qtbot.keyClick(tabMain.editTitle, c, delay=KEY_DELAY) - - tabMain.editAuthor.clear() - for c in "Jane Doe": - qtbot.keyClick(tabMain.editAuthor, c, delay=KEY_DELAY) - - assert tabMain.editName.text() == "Project Name" - assert tabMain.editTitle.text() == "Project Title" - assert tabMain.editAuthor.text() == "Jane Doe" + settings.projName.setText("Project Name") + settings.projAuthor.setText("Jane Doe") + settings.projLang.setCurrentIndex(settings.projLang.findData("de")) + settings.spellLang.setCurrentIndex(settings.spellLang.findData("de")) + settings.doBackup.setChecked(True) projSettings._doSave() assert project.data.name == "Project Name" - assert project.data.title == "Project Title" assert project.data.author == "Jane Doe" + assert project.data.language == "de" + assert project.data.spellLang == "de" + assert project.data.doBackup is False - nwGUI._processProjectSettingsChanges() + nwGUI._processProjectSettingsChanges(False) assert nwGUI.windowTitle() == "novelWriter - Project Name" # qtbot.stop() -# END Test testDlgProjSettings_Main +# END Test testDlgProjSettings_SettingsPage @pytest.mark.gui -def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd): - """Test the status and importance tabs of the project settings - dialog. - """ +def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, projPath, mockRnd): + """Test the status and importance pages of the dialog.""" monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) - monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")]) - - # Create new project - mockRnd.reset() buildTestProject(nwGUI, projPath) - CONFIG.setBackupPath(fncPath) # Set some values - theProject = SHARED.project - theProject.tree[C.hTitlePage].setStatus(C.sFinished) # type: ignore - theProject.tree[C.hChapterDoc].setStatus(C.sDraft) # type: ignore - theProject.tree[C.hSceneDoc].setStatus(C.sDraft) # type: ignore + project = SHARED.project + project.tree[C.hTitlePage].setStatus(C.sFinished) # type: ignore + project.tree[C.hChapterDoc].setStatus(C.sDraft) # type: ignore + project.tree[C.hSceneDoc].setStatus(C.sDraft) # type: ignore nwGUI.projView.projTree.setSelectedHandle(C.hPlotRoot) nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) @@ -172,51 +159,54 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat hCharNote = "0000000000011" hWorldNote = "0000000000012" - theProject.tree[hPlotNote].setImport(C.iMajor) # type: ignore - theProject.tree[hCharNote].setImport(C.iMajor) # type: ignore - theProject.tree[hWorldNote].setImport(C.iMain) # type: ignore + project.tree[hPlotNote].setImport(C.iMajor) # type: ignore + project.tree[hCharNote].setImport(C.iMajor) # type: ignore + project.tree[hWorldNote].setImport(C.iMain) # type: ignore + + nwGUI.rebuildTrees() + project.countStatus() + + assert [e["count"] for _, e in project.data.itemStatus.items()] == [2, 0, 2, 1] + assert [e["count"] for _, e in project.data.itemImport.items()] == [3, 0, 2, 1] # Create Dialog - projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.TAB_STATUS) + projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.PAGE_STATUS) projSettings.show() qtbot.addWidget(projSettings) # Status Tab # ========== - tabStatus = projSettings.tabStatus + status = projSettings.statusPage - assert tabStatus.colChanged is False - assert tabStatus.getNewList() == ([], []) - assert tabStatus.listBox.topLevelItemCount() == 4 + assert status.wasChanged is False + assert status.getNewList() == ([], []) + assert status.listBox.topLevelItemCount() == 4 # Can't delete the first item (it's in use) - tabStatus.listBox.clearSelection() - tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(0)) - qtbot.mouseClick(tabStatus.delButton, Qt.LeftButton) - assert tabStatus.listBox.topLevelItemCount() == 4 + status.listBox.clearSelection() + status.listBox.setCurrentItem(status.listBox.topLevelItem(0)) + qtbot.mouseClick(status.delButton, Qt.LeftButton) + assert status.listBox.topLevelItemCount() == 4 # Can delete the second item - tabStatus.listBox.clearSelection() - tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(1)) - qtbot.mouseClick(tabStatus.delButton, Qt.LeftButton) - assert tabStatus.listBox.topLevelItemCount() == 3 + status.listBox.clearSelection() + status.listBox.setCurrentItem(status.listBox.topLevelItem(1)) + qtbot.mouseClick(status.delButton, Qt.LeftButton) + assert status.listBox.topLevelItemCount() == 3 # Add a new item with monkeypatch.context() as mp: mp.setattr(QColorDialog, "getColor", lambda *a: QColor(20, 30, 40)) - qtbot.mouseClick(tabStatus.addButton, Qt.LeftButton) - tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(3)) - for _ in range(8): - qtbot.keyClick(tabStatus.editName, Qt.Key_Backspace, delay=KEY_DELAY) - for c in "Final": - qtbot.keyClick(tabStatus.editName, c, delay=KEY_DELAY) - qtbot.mouseClick(tabStatus.colButton, Qt.LeftButton) - qtbot.mouseClick(tabStatus.saveButton, Qt.LeftButton) - assert tabStatus.listBox.topLevelItemCount() == 4 + status.addButton.click() + status.listBox.setCurrentItem(status.listBox.topLevelItem(3)) + status.editName.setText("Final") + status.colButton.click() + status.saveButton.click() + assert status.listBox.topLevelItemCount() == 4 - assert tabStatus.colChanged is True - assert tabStatus.getNewList() == ( + assert status.wasChanged is True + assert status.getNewList() == ( [ { "key": C.sNew, @@ -241,62 +231,62 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat ) # Move items, none selected -> no change - tabStatus.listBox.clearSelection() - tabStatus._moveItem(1) - assert [x["key"] for x in tabStatus.getNewList()[0]] == [ + status.listBox.clearSelection() + status._moveItem(1) + assert [x["key"] for x in status.getNewList()[0]] == [ C.sNew, C.sDraft, C.sFinished, None ] # Move items, first selected, move up -> no change - tabStatus.listBox.clearSelection() - tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(0)) - tabStatus._moveItem(-1) - assert [x["key"] for x in tabStatus.getNewList()[0]] == [ + status.listBox.clearSelection() + status.listBox.setCurrentItem(status.listBox.topLevelItem(0)) + status._moveItem(-1) + assert [x["key"] for x in status.getNewList()[0]] == [ C.sNew, C.sDraft, C.sFinished, None ] # Move items, last selected, move up -> allowed - tabStatus.listBox.clearSelection() - tabStatus.listBox.setCurrentItem(tabStatus.listBox.topLevelItem(3)) - tabStatus._moveItem(-1) - assert [x["key"] for x in tabStatus.getNewList()[0]] == [ + status.listBox.clearSelection() + status.listBox.setCurrentItem(status.listBox.topLevelItem(3)) + status._moveItem(-1) + assert [x["key"] for x in status.getNewList()[0]] == [ C.sNew, C.sDraft, None, C.sFinished ] # Move items, same selected, move down -> allowed - tabStatus._moveItem(1) - assert [x["key"] for x in tabStatus.getNewList()[0]] == [ + status._moveItem(1) + assert [x["key"] for x in status.getNewList()[0]] == [ C.sNew, C.sDraft, C.sFinished, None ] # Importance Tab # ============== - tabImport = projSettings.tabImport - projSettings._focusTab(GuiProjectSettings.TAB_IMPORT) + importance = projSettings.importPage + projSettings._sidebarClicked(GuiProjectSettings.PAGE_IMPORT) # Delete unused entry - tabImport.listBox.clearSelection() - tabImport.listBox.setCurrentItem(tabImport.listBox.topLevelItem(1)) - qtbot.mouseClick(tabImport.delButton, Qt.LeftButton) - assert tabImport.listBox.topLevelItemCount() == 3 + importance.listBox.clearSelection() + importance.listBox.setCurrentItem(importance.listBox.topLevelItem(1)) + qtbot.mouseClick(importance.delButton, Qt.LeftButton) + assert importance.listBox.topLevelItemCount() == 3 # Add a new entry with monkeypatch.context() as mp: mp.setattr(QColorDialog, "getColor", lambda *a: QColor(20, 30, 40)) - qtbot.mouseClick(tabImport.addButton, Qt.LeftButton) - tabImport.listBox.clearSelection() - tabImport.listBox.setCurrentItem(tabImport.listBox.topLevelItem(3)) + qtbot.mouseClick(importance.addButton, Qt.LeftButton) + importance.listBox.clearSelection() + importance.listBox.setCurrentItem(importance.listBox.topLevelItem(3)) for _ in range(8): - qtbot.keyClick(tabImport.editName, Qt.Key_Backspace, delay=KEY_DELAY) + qtbot.keyClick(importance.editName, Qt.Key_Backspace, delay=KEY_DELAY) for c in "Final": - qtbot.keyClick(tabImport.editName, c, delay=KEY_DELAY) - qtbot.mouseClick(tabImport.colButton, Qt.LeftButton) - qtbot.mouseClick(tabImport.saveButton, Qt.LeftButton) - assert tabImport.listBox.topLevelItemCount() == 4 + qtbot.keyClick(importance.editName, c, delay=KEY_DELAY) + qtbot.mouseClick(importance.colButton, Qt.LeftButton) + qtbot.mouseClick(importance.saveButton, Qt.LeftButton) + assert importance.listBox.topLevelItemCount() == 4 - assert tabImport.colChanged is True - assert tabImport.getNewList() == ( + assert importance.wasChanged is True + assert importance.getNewList() == ( [ { "key": C.iNew, @@ -323,13 +313,13 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat # Check Project projSettings._doSave() - statusItems = dict(theProject.data.itemStatus.items()) + statusItems = dict(project.data.itemStatus.items()) assert statusItems[C.sNew]["name"] == "New" assert statusItems[C.sDraft]["name"] == "Draft" assert statusItems[C.sFinished]["name"] == "Finished" assert statusItems["s000013"]["name"] == "Final" - importItems = dict(theProject.data.itemImport.items()) + importItems = dict(project.data.itemImport.items()) assert importItems[C.iNew]["name"] == "New" assert importItems[C.iMajor]["name"] == "Major" assert importItems[C.iMain]["name"] == "Main" @@ -341,83 +331,78 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat @pytest.mark.gui -def testDlgProjSettings_Replace(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd): - """Test the auto-replace tab of the project settings dialog.""" +def testDlgProjSettings_Replace(qtbot, monkeypatch, nwGUI, projPath, mockRnd): + """Test the auto-replace page of the dialog.""" monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) - monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")]) - - # Create new project - mockRnd.reset() buildTestProject(nwGUI, projPath) - CONFIG.setBackupPath(fncPath) # Set some values - theProject = SHARED.project - theProject.data.setAutoReplace({ + project = SHARED.project + project.data.setAutoReplace({ "A": "B", "C": "D" }) # Create Dialog - projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.TAB_REPLACE) + projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.PAGE_REPLACE) projSettings.show() qtbot.addWidget(projSettings) # Auto-Replace Tab # ================ - tabReplace = projSettings.tabReplace + replace = projSettings.replacePage - assert tabReplace.listBox.topLevelItem(0).text(0) == "" # type: ignore - assert tabReplace.listBox.topLevelItem(0).text(1) == "B" # type: ignore - assert tabReplace.listBox.topLevelItem(1).text(0) == "" # type: ignore - assert tabReplace.listBox.topLevelItem(1).text(1) == "D" # type: ignore - assert tabReplace.listBox.topLevelItemCount() == 2 + assert replace.listBox.topLevelItem(0).text(0) == "" # type: ignore + assert replace.listBox.topLevelItem(0).text(1) == "B" # type: ignore + assert replace.listBox.topLevelItem(1).text(0) == "" # type: ignore + assert replace.listBox.topLevelItem(1).text(1) == "D" # type: ignore + assert replace.listBox.topLevelItemCount() == 2 # Nothing to save or delete - tabReplace.listBox.clearSelection() - tabReplace._saveEntry() - tabReplace._delEntry() - assert tabReplace.listBox.topLevelItemCount() == 2 + replace.listBox.clearSelection() + replace._saveEntry() + replace._delEntry() + assert replace.listBox.topLevelItemCount() == 2 # Create a new entry - qtbot.mouseClick(tabReplace.addButton, Qt.LeftButton) - assert tabReplace.listBox.topLevelItemCount() == 3 - assert tabReplace.listBox.topLevelItem(2).text(0) == "" # type: ignore - assert tabReplace.listBox.topLevelItem(2).text(1) == "" # type: ignore + qtbot.mouseClick(replace.addButton, Qt.LeftButton) + assert replace.listBox.topLevelItemCount() == 3 + assert replace.listBox.topLevelItem(2).text(0) == "" # type: ignore + assert replace.listBox.topLevelItem(2).text(1) == "" # type: ignore # Edit the entry - tabReplace.listBox.setCurrentItem(tabReplace.listBox.topLevelItem(2)) - tabReplace.editKey.setText("") + replace.listBox.setCurrentItem(replace.listBox.topLevelItem(2)) + replace.editKey.setText("") for c in "Th is ": - qtbot.keyClick(tabReplace.editKey, c, delay=KEY_DELAY) - tabReplace.editValue.setText("") + qtbot.keyClick(replace.editKey, c, delay=KEY_DELAY) + replace.editValue.setText("") for c in "With This Stuff ": - qtbot.keyClick(tabReplace.editValue, c, delay=KEY_DELAY) - qtbot.mouseClick(tabReplace.saveButton, Qt.LeftButton) - assert tabReplace.listBox.topLevelItem(2).text(0) == "" # type: ignore - assert tabReplace.listBox.topLevelItem(2).text(1) == "With This Stuff " # type: ignore + qtbot.keyClick(replace.editValue, c, delay=KEY_DELAY) + qtbot.mouseClick(replace.saveButton, Qt.LeftButton) + assert replace.listBox.topLevelItem(2).text(0) == "" # type: ignore + assert replace.listBox.topLevelItem(2).text(1) == "With This Stuff " # type: ignore # Create a new entry again - tabReplace.listBox.clearSelection() - qtbot.mouseClick(tabReplace.addButton, Qt.LeftButton) - assert tabReplace.listBox.topLevelItemCount() == 4 + replace.listBox.clearSelection() + qtbot.mouseClick(replace.addButton, Qt.LeftButton) + assert replace.listBox.topLevelItemCount() == 4 # The list is sorted, so we must find it newIdx = -1 - for i in range(tabReplace.listBox.topLevelItemCount()): - if tabReplace.listBox.topLevelItem(i).text(0) == "": # type: ignore + for i in range(replace.listBox.topLevelItemCount()): + if replace.listBox.topLevelItem(i).text(0) == "": # type: ignore newIdx = i break assert newIdx >= 0 # Then delete the new item - tabReplace.listBox.setCurrentItem(tabReplace.listBox.topLevelItem(newIdx)) - qtbot.mouseClick(tabReplace.delButton, Qt.LeftButton) - assert tabReplace.listBox.topLevelItemCount() == 3 + replace.listBox.setCurrentItem(replace.listBox.topLevelItem(newIdx)) + qtbot.mouseClick(replace.delButton, Qt.LeftButton) + assert replace.listBox.topLevelItemCount() == 3 # Check Project projSettings._doSave() - assert theProject.data.autoReplace == { + assert project.data.autoReplace == { "A": "B", "C": "D", "This": "With This Stuff" }