858 lines
31 KiB
Python
858 lines
31 KiB
Python
"""
|
||
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 <https://www.gnu.org/licenses/>.
|
||
""" # 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, scrollable=True)
|
||
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, scrollable=True)
|
||
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"<keyword{self.listBox.topLevelItemCount() + 1:d}>"
|
||
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())
|