"""
novelWriter – GUI Project Settings
==================================
File History:
Created: 2018-09-29 [0.0.1] GuiProjectSettings
Rewritten: 2024-01-26 [2.3b1] GuiProjectSettings
This file is a part of novelWriter
Copyright (C) 2018 Veronica Berglyd Olsen and novelWriter contributors
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 .
""" # noqa
from __future__ import annotations
import csv
import logging
from pathlib import Path
from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot
from PyQt6.QtGui import QCloseEvent, QColor
from PyQt6.QtWidgets import (
QAbstractItemView, QApplication, QColorDialog, QDialogButtonBox,
QFileDialog, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QMenu,
QStackedWidget, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget
)
from novelwriter import CONFIG, SHARED
from novelwriter.common import formatFileFilter, qtAddAction, qtLambda, simplified
from novelwriter.constants import nwLabels, trConst
from novelwriter.core.status import CUSTOM_COL, NWStatus, StatusEntry
from novelwriter.enum import nwStandardButton, nwStatusShape
from novelwriter.extensions.configlayout import NColorLabel, NFixedPage, NScrollableForm
from novelwriter.extensions.modified import NComboBox, NDialog, NIconToolButton
from novelwriter.extensions.pagedsidebar import NPagedSideBar
from novelwriter.extensions.switch import NSwitch
from novelwriter.types import (
QtRoleAccept, QtRoleReject, QtSizeMinimum, QtSizeMinimumExpanding,
QtUserRole
)
logger = logging.getLogger(__name__)
class GuiProjectSettings(NDialog):
"""GUI: Project Settings DIalog."""
PAGE_SETTINGS = 0
PAGE_STATUS = 1
PAGE_IMPORT = 2
PAGE_REPLACE = 3
newProjectSettingsReady = pyqtSignal()
def __init__(self, parent: QWidget, gotoPage: int = PAGE_SETTINGS) -> None:
super().__init__(parent=parent)
logger.debug("Create: GuiProjectSettings")
self.setObjectName("GuiProjectSettings")
self.setWindowTitle(self.tr("Project Settings"))
options = SHARED.project.options
self.setMinimumSize(500, 400)
self.resize(
options.getInt("GuiProjectSettings", "winWidth", 650),
options.getInt("GuiProjectSettings", "winHeight", 500),
)
# Title
self.titleLabel = NColorLabel(
self.tr("Project Settings"), self, color=SHARED.theme.helpText,
scale=NColorLabel.HEADER_SCALE, indent=4,
)
# SideBar
self.sidebar = NPagedSideBar(self)
self.sidebar.setLabelColor(SHARED.theme.helpText)
self.sidebar.setAccessibleName(self.titleLabel.text())
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.buttonClicked.connect(self._sidebarClicked)
# Buttons
self.btnSave = SHARED.theme.getStandardButton(nwStandardButton.SAVE, self)
self.btnSave.clicked.connect(self._doSave)
self.btnCancel = SHARED.theme.getStandardButton(nwStandardButton.CANCEL, self)
self.btnCancel.clicked.connect(self.reject)
self.btnBox = QDialogButtonBox(self)
self.btnBox.addButton(self.btnSave, QtRoleAccept)
self.btnBox.addButton(self.btnCancel, QtRoleReject)
# 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.btnBox)
self.outerBox.setSpacing(8)
self.setLayout(self.outerBox)
self.setSizeGripEnabled(True)
# Jump to Specified Page
self.sidebar.setSelected(gotoPage)
self._sidebarClicked(gotoPage)
logger.debug("Ready: GuiProjectSettings")
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: GuiProjectSettings")
##
# Events
##
def closeEvent(self, event: QCloseEvent) -> None:
"""Capture the user closing the window and save settings."""
self._saveSettings()
event.accept()
self.softDelete()
##
# 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)
@pyqtSlot()
def _doSave(self) -> None:
"""Save settings and close dialog."""
project = SHARED.project
projName = self.settingsPage.projName.text()
projAuthor = self.settingsPage.projAuthor.text()
projLang = self.settingsPage.projLang.currentData()
spellLang = self.settingsPage.spellLang.currentData()
doBackup = not self.settingsPage.noBackup.isChecked()
project.data.setName(projName)
project.data.setAuthor(projAuthor)
project.data.setDoBackup(doBackup)
project.data.setSpellLang(spellLang)
project.setProjectLang(projLang)
if self.statusPage.changed:
logger.debug("Updating status labels")
project.updateStatus("s", self.statusPage.getNewList())
if self.importPage.changed:
logger.debug("Updating importance labels")
project.updateStatus("i", self.importPage.getNewList())
if self.replacePage.changed:
logger.debug("Updating auto-replace settings")
project.data.setAutoReplace(self.replacePage.getNewList())
self.newProjectSettingsReady.emit()
QApplication.processEvents()
self.close()
##
# Internal Functions
##
def _saveSettings(self) -> None:
"""Save GUI settings."""
statusColW = self.statusPage.columnWidth()
importColW = self.importPage.columnWidth()
replaceColW = self.replacePage.columnWidth()
logger.debug("Saving State: GuiProjectSettings")
options = SHARED.project.options
options.setValue("GuiProjectSettings", "winWidth", self.width())
options.setValue("GuiProjectSettings", "winHeight", self.height())
options.setValue("GuiProjectSettings", "statusColW", statusColW)
options.setValue("GuiProjectSettings", "importColW", importColW)
options.setValue("GuiProjectSettings", "replaceColW", replaceColW)
class _SettingsPage(NScrollableForm):
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
data = SHARED.project.data
self.setHelpTextStyle(SHARED.theme.helpText)
self.setRowIndent(0)
# Project Name
self.projName = QLineEdit(self)
self.projName.setMaxLength(200)
self.projName.setMinimumWidth(200)
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)
)
# Project Author
self.projAuthor = QLineEdit(self)
self.projAuthor.setMaxLength(200)
self.projAuthor.setMinimumWidth(200)
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
projLang = data.language or CONFIG.guiLocale
self.projLang = NComboBox(self)
self.projLang.setMinimumWidth(200)
for tag, language in CONFIG.listLanguages(CONFIG.LANG_PROJ):
self.projLang.addItem(language, tag)
self.projLang.setCurrentData(projLang, projLang)
self.addRow(
self.tr("Project language"), self.projLang,
self.tr("Only used when building the manuscript."),
stretch=(3, 2)
)
# Spell Check Language
self.spellLang = NComboBox(self)
self.spellLang.setMinimumWidth(200)
self.spellLang.addItem(self.tr("Default"), "None")
if CONFIG.hasEnchant:
for tag, language in SHARED.spelling.listDictionaries():
self.spellLang.addItem(language, tag)
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)
# Backup on Close
self.noBackup = NSwitch(self)
self.noBackup.setChecked(not data.doBackup)
self.addRow(
self.tr("Disable backup on close"), self.noBackup,
self.tr("Overrides main preferences.")
)
self.finalise()
class _StatusPage(NFixedPage):
C_DATA = 0
C_LABEL = 0
C_USAGE = 1
D_KEY = QtUserRole
D_ENTRY = QtUserRole + 1
def __init__(self, parent: QWidget, isStatus: bool) -> None:
super().__init__(parent=parent)
if isStatus:
self._kind = self.tr("Status")
self._store = SHARED.project.data.itemStatus
pageLabel = self.tr("Novel Document Status Levels")
colSetting = "statusColW"
else:
self._kind = self.tr("Importance")
self._store = SHARED.project.data.itemImport
pageLabel = self.tr("Project Note Importance Levels")
colSetting = "importColW"
wCol0 = SHARED.project.options.getInt("GuiProjectSettings", colSetting, 130)
self._changed = False
self._color = QColor(100, 100, 100)
self._shape = nwStatusShape.SQUARE
self._theme = CUSTOM_COL
self._icons = {}
self._iPx = SHARED.theme.baseIconHeight
iSz = SHARED.theme.baseIconSize
iColor = self.palette().text().color()
# Labels
self.trCountNone = self.tr("Not in use")
self.trCountOne = self.tr("Used once")
self.trCountMore = self.tr("Used by {0} items")
self.trSelColor = self.tr("Select Colour")
# Title
self.pageTitle = NColorLabel(
pageLabel, self, color=SHARED.theme.helpText,
scale=NColorLabel.HEADER_SCALE
)
# List Box
self.listBox = QTreeWidget(self)
self.listBox.setHeaderLabels([self.tr("Label"), self.tr("Usage")])
self.listBox.setColumnWidth(self.C_LABEL, wCol0)
self.listBox.setIndentation(0)
self.listBox.setAccessibleName(pageLabel)
self.listBox.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.listBox.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.listBox.itemSelectionChanged.connect(self._onSelectionChanged)
for key, entry in self._store.iterItems():
self._addItem(key, StatusEntry.duplicate(entry))
# List Controls
self.addButton = NIconToolButton(self, iSz, "add", "add")
self.addButton.setToolTip(self.tr("Add Label"))
self.addButton.clicked.connect(self._onItemCreate)
self.delButton = NIconToolButton(self, iSz, "remove", "remove")
self.delButton.setToolTip(self.tr("Delete Label"))
self.delButton.clicked.connect(self._onItemDelete)
self.upButton = NIconToolButton(self, iSz, "chevron_up", "action")
self.upButton.setToolTip(self.tr("Move Up"))
self.upButton.clicked.connect(qtLambda(self._moveItem, -1))
self.downButton = NIconToolButton(self, iSz, "chevron_down", "action")
self.downButton.setToolTip(self.tr("Move Down"))
self.downButton.clicked.connect(qtLambda(self._moveItem, 1))
self.importButton = NIconToolButton(self, iSz, "import", "apply")
self.importButton.setToolTip(self.tr("Import Labels"))
self.importButton.clicked.connect(self._importLabels)
self.exportButton = NIconToolButton(self, iSz, "export", "action")
self.exportButton.setToolTip(self.tr("Export Labels"))
self.exportButton.clicked.connect(self._exportLabels)
# Edit Form
self.editName = QLineEdit(self)
self.editName.setMaxLength(40)
self.editName.setPlaceholderText(self.tr("Select item to edit"))
self.editName.setEnabled(False)
self.editName.textEdited.connect(self._onNameEdit)
self.labelName = QLabel(self.tr("Label"), self)
self.labelName.setBuddy(self.editName)
# Icon Colours
self.iconColor = NComboBox(self)
self.iconColor.setMinimumWidth(200)
self.iconColor.setEnabled(False)
self.iconColor.addItem(self.tr("Custom"), CUSTOM_COL)
for key, label in nwLabels.THEME_COLORS.items():
self.iconColor.addItem(trConst(label), key)
self.iconColor.currentIndexChanged.connect(self._onThemeSelect)
self.labelColor = QLabel(self.tr("Colour"), self)
self.labelColor.setBuddy(self.iconColor)
buttonStyle = (
"QToolButton {padding: 0 4px;} "
"QToolButton::menu-indicator {image: none;}"
)
self.colorButton = NIconToolButton(self, iSz)
self.colorButton.setToolTip(self.tr("Colour"))
self.colorButton.setSizePolicy(QtSizeMinimum, QtSizeMinimumExpanding)
self.colorButton.setStyleSheet(buttonStyle)
self.colorButton.setEnabled(False)
self.colorButton.clicked.connect(self._onColorSelect)
def buildMenu(menu: QMenu | None, items: dict[nwStatusShape, str]) -> None:
if menu is not None:
for shape, label in items.items():
icon = NWStatus.createIcon(self._iPx, iColor, shape)
action = qtAddAction(menu, trConst(label))
action.setIcon(icon)
action.triggered.connect(qtLambda(self._selectShape, shape))
menu.addAction(action)
self._icons[shape] = icon
self.shapeMenu = QMenu(self)
buildMenu(self.shapeMenu, nwLabels.SHAPES_PLAIN)
buildMenu(self.shapeMenu.addMenu(self.tr("Circles ...")), nwLabels.SHAPES_CIRCLE)
buildMenu(self.shapeMenu.addMenu(self.tr("Bars ...")), nwLabels.SHAPES_BARS)
buildMenu(self.shapeMenu.addMenu(self.tr("Blocks ...")), nwLabels.SHAPES_BLOCKS)
self.shapeButton = NIconToolButton(self, iSz)
self.shapeButton.setMenu(self.shapeMenu)
self.shapeButton.setToolTip(self.tr("Shape"))
self.shapeButton.setSizePolicy(QtSizeMinimum, QtSizeMinimumExpanding)
self.shapeButton.setStyleSheet(buttonStyle)
self.shapeButton.setEnabled(False)
self.labelShape = QLabel(self.tr("Shape"), self)
self.labelShape.setBuddy(self.iconColor)
# Assemble
self.listControls = QVBoxLayout()
self.listControls.addWidget(self.addButton)
self.listControls.addWidget(self.delButton)
self.listControls.addWidget(self.upButton)
self.listControls.addWidget(self.downButton)
self.listControls.addStretch(1)
self.listControls.addWidget(self.importButton)
self.listControls.addWidget(self.exportButton)
self.editBox = QGridLayout()
self.editBox.addWidget(self.labelName, 0, 0)
self.editBox.addWidget(self.editName, 0, 1, 1, 5)
self.editBox.addWidget(self.labelColor, 1, 0)
self.editBox.addWidget(self.iconColor, 1, 1)
self.editBox.addWidget(self.colorButton, 1, 2)
self.editBox.addWidget(self.labelShape, 1, 3)
self.editBox.addWidget(self.shapeButton, 1, 4)
self.editBox.setColumnStretch(5, 1)
self.innerBox = QGridLayout()
self.innerBox.addWidget(self.listBox, 0, 0)
self.innerBox.addLayout(self.listControls, 0, 1)
self.innerBox.addLayout(self.editBox, 1, 0)
self.innerBox.setRowStretch(0, 1)
self.innerBox.setColumnStretch(0, 1)
self.outerBox = QVBoxLayout()
self.outerBox.addWidget(self.pageTitle, 0)
self.outerBox.addLayout(self.innerBox, 1)
self.setCentralLayout(self.outerBox)
self._setButtonIcons()
@property
def changed(self) -> bool:
"""The user changed these settings."""
return self._changed
##
# Methods
##
def getNewList(self) -> list[tuple[str | None, StatusEntry]]:
"""Return list of entries."""
if self._changed:
update = []
for n in range(self.listBox.topLevelItemCount()):
if item := self.listBox.topLevelItem(n):
key = item.data(self.C_DATA, self.D_KEY)
entry = item.data(self.C_DATA, self.D_ENTRY)
update.append((key, entry))
return update
return []
def columnWidth(self) -> int:
"""Return the size of the header column."""
return self.listBox.columnWidth(0)
##
# Private Slots
##
@pyqtSlot(str)
def _onNameEdit(self, text: str) -> None:
"""Update the status label text."""
if item := self._getSelectedItem():
name = simplified(text)
entry: StatusEntry = item.data(self.C_DATA, self.D_ENTRY)
entry.name = name
item.setText(self.C_LABEL, name)
self._changed = True
@pyqtSlot(int)
def _onThemeSelect(self, index: int) -> None:
"""Update the colour handling on theme selection change."""
self._theme = str(self.iconColor.currentData())
self._setButtonIcons()
self._updateIcon()
@pyqtSlot()
def _onColorSelect(self) -> None:
"""Open a dialog to select the status icon colour."""
if (color := QColorDialog.getColor(self._color, self, self.trSelColor)).isValid():
self._color = color
self._theme = CUSTOM_COL
self._setButtonIcons()
self._updateIcon()
@pyqtSlot()
def _onItemCreate(self) -> None:
"""Create a new status item."""
color = QColor(100, 100, 100)
shape = nwStatusShape.SQUARE
icon = NWStatus.createIcon(self._iPx, color, shape)
theme = str(self.iconColor.currentData())
self._addItem(None, StatusEntry(self.tr("New Item"), color, theme, shape, icon, 0))
self._changed = True
@pyqtSlot()
def _onItemDelete(self) -> None:
"""Delete a status item."""
if item := self._getSelectedItem():
iRow = self.listBox.indexOfTopLevelItem(item)
entry: StatusEntry = item.data(self.C_DATA, self.D_ENTRY)
if entry.count > 0:
SHARED.error(self.tr("Cannot delete a status item that is in use."))
else:
self.listBox.takeTopLevelItem(iRow)
self._changed = True
@pyqtSlot()
def _onSelectionChanged(self) -> None:
"""Extract the info of a selected item and populate the settings
boxes and button. If no item is selected, clear the form.
"""
if item := self._getSelectedItem():
entry: StatusEntry = item.data(self.C_DATA, self.D_ENTRY)
self._color = entry.color
self._shape = entry.shape
self._theme = entry.theme
self._setButtonIcons()
self.editName.setText(entry.name)
self.editName.selectAll()
self.editName.setFocus()
self.editName.setEnabled(True)
self.iconColor.setEnabled(True)
self.colorButton.setEnabled(True)
self.shapeButton.setEnabled(True)
else:
self._color = QColor(100, 100, 100)
self._shape = nwStatusShape.SQUARE
self._theme = CUSTOM_COL
self._setButtonIcons()
self.editName.setText("")
self.editName.setEnabled(False)
self.iconColor.setEnabled(False)
self.colorButton.setEnabled(False)
self.shapeButton.setEnabled(False)
@pyqtSlot()
def _importLabels(self) -> None:
"""Import labels from file."""
if path := QFileDialog.getOpenFileName(
self, self.tr("Import File"),
str(CONFIG.homePath()), filter=formatFileFilter(["*.csv", "*"]),
)[0]:
try:
with open(path, mode="r", encoding="utf-8") as fo:
for row in csv.reader(fo):
if entry := self._store.fromRaw(row):
self._addItem(None, entry)
self._changed = True
except Exception as exc:
SHARED.error("Could not read file.", exc=exc)
return
return
@pyqtSlot()
def _exportLabels(self) -> None:
"""Export labels to file."""
name = f"{SHARED.project.data.fileSafeName} - {self._kind}.csv"
if path := QFileDialog.getSaveFileName(
self, self.tr("Export File"), str(CONFIG.homePath() / name),
)[0]:
try:
path = Path(path).with_suffix(".csv")
with open(path, mode="w", encoding="utf-8") as fo:
writer = csv.writer(fo)
for n in range(self.listBox.topLevelItemCount()):
if item := self.listBox.topLevelItem(n):
entry: StatusEntry = item.data(self.C_DATA, self.D_ENTRY)
writer.writerow([entry.shape.name, entry.color.name(), entry.name])
except Exception as exc:
SHARED.error("Could not write file.", exc=exc)
##
# Internal Functions
##
def _selectShape(self, shape: nwStatusShape) -> None:
"""Set the current shape."""
self._shape = shape
self._setButtonIcons()
self._updateIcon()
def _updateIcon(self) -> None:
"""Apply changes made to a status icon."""
if item := self._getSelectedItem():
icon = NWStatus.createIcon(self._iPx, self._pickColor(), self._shape)
entry: StatusEntry = item.data(self.C_DATA, self.D_ENTRY)
entry.color = self._color
entry.shape = self._shape
entry.theme = self._theme
entry.icon = icon
item.setIcon(self.C_LABEL, icon)
self._changed = True
def _addItem(self, key: str | None, entry: StatusEntry) -> None:
"""Add a status item to the list."""
item = QTreeWidgetItem()
item.setText(self.C_LABEL, entry.name)
item.setIcon(self.C_LABEL, entry.icon)
item.setText(self.C_USAGE, self._usageString(entry.count))
item.setData(self.C_DATA, self.D_KEY, key)
item.setData(self.C_DATA, self.D_ENTRY, entry)
self.listBox.addTopLevelItem(item)
def _moveItem(self, step: int) -> None:
"""Move and item up or down step."""
if item := self._getSelectedItem():
tIdx = self.listBox.indexOfTopLevelItem(item)
nItm = self.listBox.topLevelItemCount()
nIdx = tIdx + step
if (0 <= nIdx < nItm) and (cItem := self.listBox.takeTopLevelItem(tIdx)):
self.listBox.insertTopLevelItem(nIdx, cItem)
self.listBox.clearSelection()
cItem.setSelected(True)
self._changed = True
def _getSelectedItem(self) -> QTreeWidgetItem | None:
"""Get the currently selected item."""
if items := self.listBox.selectedItems():
return items[0]
return None
def _usageString(self, count: int) -> str:
"""Generate usage string."""
if count == 0:
return self.trCountNone
elif count == 1:
return self.trCountOne
else:
return self.trCountMore.format(count)
def _setButtonIcons(self) -> None:
"""Set the colour of the colour button."""
icon = NWStatus.createIcon(self._iPx, self._pickColor(), nwStatusShape.SQUARE)
self.iconColor.setCurrentData(self._theme, CUSTOM_COL)
self.colorButton.setIcon(icon)
self.shapeButton.setIcon(self._icons[self._shape])
def _pickColor(self) -> QColor:
"""Get the correct colour value based on selections."""
if self._theme == CUSTOM_COL:
return self._color
return SHARED.theme.getBaseColor(self._theme)
class _ReplacePage(NFixedPage):
C_KEY = 0
C_REPL = 1
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
self._changed = False
iSz = SHARED.theme.baseIconSize
wCol0 = SHARED.project.options.getInt("GuiProjectSettings", "replaceColW", 130)
# Title
self.pageTitle = NColorLabel(
self.tr("Text Auto-Replace for Preview and Build"), self,
color=SHARED.theme.helpText, scale=NColorLabel.HEADER_SCALE
)
# List Box
self.listBox = QTreeWidget(self)
self.listBox.setHeaderLabels([self.tr("Keyword"), self.tr("Replace With")])
self.listBox.setColumnWidth(self.C_KEY, wCol0)
self.listBox.setIndentation(0)
self.listBox.setAccessibleName(self.pageTitle.text())
self.listBox.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.listBox.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.listBox.itemSelectionChanged.connect(self._onSelectionChanged)
for aKey, aVal in SHARED.project.data.autoReplace.items():
newItem = QTreeWidgetItem([f"<{aKey}>", aVal])
self.listBox.addTopLevelItem(newItem)
self.listBox.sortByColumn(self.C_KEY, Qt.SortOrder.AscendingOrder)
self.listBox.setSortingEnabled(True)
# List Controls
self.addButton = NIconToolButton(self, iSz, "add", "add")
self.addButton.clicked.connect(self._onEntryCreated)
self.delButton = NIconToolButton(self, iSz, "remove", "remove")
self.delButton.clicked.connect(self._onEntryDeleted)
# Edit Form
self.editKey = QLineEdit(self)
self.editKey.setPlaceholderText(self.tr("Select item to edit"))
self.editKey.setEnabled(False)
self.editKey.setMaxLength(40)
self.editKey.textEdited.connect(self._onKeyEdit)
self.editValue = QLineEdit(self)
self.editValue.setEnabled(False)
self.editValue.setMaxLength(250)
self.editValue.textEdited.connect(self._onValueEdit)
# Assemble
self.listControls = QVBoxLayout()
self.listControls.addWidget(self.addButton)
self.listControls.addWidget(self.delButton)
self.listControls.addStretch(1)
self.editBox = QHBoxLayout()
self.editBox.addWidget(self.editKey, 4)
self.editBox.addWidget(self.editValue, 5)
self.mainBox = QVBoxLayout()
self.mainBox.addWidget(self.listBox, 1)
self.mainBox.addLayout(self.editBox, 0)
self.innerBox = QHBoxLayout()
self.innerBox.addLayout(self.mainBox)
self.innerBox.addLayout(self.listControls)
self.outerBox = QVBoxLayout()
self.outerBox.addWidget(self.pageTitle)
self.outerBox.addLayout(self.innerBox)
self.setCentralLayout(self.outerBox)
@property
def changed(self) -> bool:
"""The user changed these settings."""
return self._changed
##
# Methods
##
def getNewList(self) -> dict[str, str]:
"""Extract the list from the widget."""
new = {}
for n in range(self.listBox.topLevelItemCount()):
if item := self.listBox.topLevelItem(n):
if key := self._stripKey(item.text(self.C_KEY)):
new[key] = item.text(self.C_REPL)
return new
def columnWidth(self) -> int:
"""Return the size of the header column."""
return self.listBox.columnWidth(0)
##
# Private Slots
##
@pyqtSlot(str)
def _onKeyEdit(self, text: str) -> None:
"""Update the key text."""
if (item := self._getSelectedItem()) and (key := self._stripKey(text)):
item.setText(self.C_KEY, f"<{key}>")
self._changed = True
@pyqtSlot(str)
def _onValueEdit(self, text: str) -> None:
"""Update the value text."""
if item := self._getSelectedItem():
item.setText(self.C_REPL, text)
self._changed = True
@pyqtSlot()
def _onSelectionChanged(self) -> None:
"""Extract the details from the selected item and populate the
edit form.
"""
if item := self._getSelectedItem():
self.editKey.setText(self._stripKey(item.text(self.C_KEY)))
self.editValue.setText(item.text(self.C_REPL))
self.editKey.setEnabled(True)
self.editValue.setEnabled(True)
self.editKey.selectAll()
self.editKey.setFocus()
else:
self.editKey.setText("")
self.editValue.setText("")
self.editKey.setEnabled(False)
self.editValue.setEnabled(False)
@pyqtSlot()
def _onEntryCreated(self) -> None:
"""Add a new list entry."""
key = f""
self.listBox.addTopLevelItem(QTreeWidgetItem([key, ""]))
@pyqtSlot()
def _onEntryDeleted(self) -> None:
"""Delete the selected entry."""
if item := self._getSelectedItem():
self.listBox.takeTopLevelItem(self.listBox.indexOfTopLevelItem(item))
self._changed = True
##
# Internal Functions
##
def _getSelectedItem(self) -> QTreeWidgetItem | None:
"""Extract the currently selected item."""
if items := self.listBox.selectedItems():
return items[0]
return None
def _stripKey(self, key: str) -> str:
"""Clean up the replace key string."""
return "".join(c for c in key if c.isalnum())