""" novelWriter – Custom Widget: Config Layout ========================================== File History: Created: 2020-05-03 [0.4.5] NColourLabel Created: 2024-01-08 [2.3b1] NScrollableForm Created: 2024-01-26 [2.3b1] NScrollablePage Created: 2024-01-26 [2.3b1] NFixedPage Created: 2024-03-12 [2.4b1] NWrappedWidgetBox This file is a part of novelWriter Copyright (C) 2023 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 . """ from __future__ import annotations from PyQt6.QtGui import QColor, QFont, QPalette, QPixmap from PyQt6.QtWidgets import ( QAbstractButton, QFrame, QHBoxLayout, QLabel, QLayout, QScrollArea, QVBoxLayout, QWidget ) from novelwriter.types import QtScrollAsNeeded DEFAULT_SCALE = 0.9 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) return def setCentralLayout(self, layout: QLayout) -> None: """Set a layout as the central object.""" self.setLayout(layout) return def setCentralWidget(self, widget: QWidget) -> None: """Set a layout as the central object.""" layout = QHBoxLayout() layout.addWidget(widget) self.setLayout(layout) return 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(QtScrollAsNeeded) self.setVerticalScrollBarPolicy(QtScrollAsNeeded) 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 class NScrollableForm(QScrollArea): """Extension: Scrollable Form Widget A custom widget that creates a form within a scrollable area. """ def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) self._helpCol = QColor(0, 0, 0) self._fontScale = DEFAULT_SCALE self._first = True self._indent = 12 self._sections: dict[int, QLabel] = {} self._editable: dict[str, NColourLabel] = {} self._index: dict[str, QWidget] = {} self._layout = QVBoxLayout() self._layout.setSpacing(12) self._widget = QWidget(self) self._widget.setLayout(self._layout) self.setWidget(self._widget) self.setWidgetResizable(True) self.setHorizontalScrollBarPolicy(QtScrollAsNeeded) self.setVerticalScrollBarPolicy(QtScrollAsNeeded) self.setFrameShadow(QFrame.Shadow.Sunken) self.setFrameShape(QFrame.Shape.StyledPanel) return ## # Properties ## @property def labels(self) -> list[str]: return list(self._index.keys()) ## # Setters ## def setHelpTextStyle(self, color: QColor, scale: float = DEFAULT_SCALE) -> None: """Set the text color for the help text.""" self._helpCol = color self._fontScale = scale return def setHelpText(self, key: str, text: str) -> None: """Set the text for the help label.""" if qHelp := self._editable.get(key): qHelp.setText(text) return def setRowIndent(self, indent: int) -> None: """Set the indentation of each row.""" self._indent = max(indent, 0) return ## # Methods ## def scrollToSection(self, identifier: int) -> None: """Scroll to the requested section identifier.""" if identifier in self._sections: yPos = self._sections[identifier].pos().y() - 8 if vBar := self.verticalScrollBar(): vBar.setValue(yPos) return def scrollToLabel(self, label: str) -> None: """Scroll to the requested label.""" if label in self._index: yPos = self._index[label].pos().y() - 8 if vBar := self.verticalScrollBar(): vBar.setValue(yPos) return def addGroupLabel(self, label: str, identifier: int | None = None) -> None: """Add a text label to separate groups of settings.""" qLabel = QLabel(f"{label}", self) qLabel.setContentsMargins(0, 4, 0, 4) if not self._first: self._layout.addSpacing(20) self._layout.addWidget(qLabel) self._first = False if identifier is not None: self._sections[identifier] = qLabel return def addRow( self, label: str | None, widget: QWidget | list[QWidget | QPixmap | int], helpText: str = "", unit: str | None = None, button: QWidget | None = None, editable: str | None = None, stretch: tuple[int, int] = (1, 0), ) -> None: """Add a label and a widget as a new row of the form.""" row = QHBoxLayout() row.setSpacing(12) if isinstance(widget, list): wBox = QHBoxLayout() wBox.setContentsMargins(0, 0, 0, 0) for item in widget: if isinstance(item, QWidget): wBox.addWidget(item) elif isinstance(item, QPixmap): icon = QLabel(self) icon.setPixmap(item) wBox.addWidget(icon) elif isinstance(item, int): wBox.addSpacing(item) qWidget = QWidget(self) qWidget.setLayout(wBox) else: qWidget = widget qLabel = QLabel(label or "", self) qLabel.setIndent(self._indent) qLabel.setBuddy(qWidget) if helpText: qHelp = NColourLabel( str(helpText), self, color=self._helpCol, scale=self._fontScale, wrap=True, indent=self._indent ) labelBox = QVBoxLayout() labelBox.addWidget(qLabel, 0) labelBox.addWidget(qHelp, 1) labelBox.setSpacing(0) row.addLayout(labelBox, stretch[0]) if editable: self._editable[editable] = qHelp else: row.addWidget(qLabel, stretch[0]) if isinstance(unit, str): box = QHBoxLayout() box.addWidget(qWidget, 1) box.addWidget(QLabel(unit, self), 0) row.addLayout(box, stretch[1]) elif isinstance(button, QAbstractButton): box = QHBoxLayout() box.addWidget(qWidget, 1) box.addWidget(button, 0) row.addLayout(box, stretch[1]) else: row.addWidget(qWidget, stretch[1]) self._layout.addLayout(row) if label: self._index[label.strip()] = qWidget self._first = False return def finalise(self) -> None: """Finalise the layout when the form is built.""" self._layout.addSpacing(20) self._layout.addStretch(1) return class NColourLabel(QLabel): """Extension: A Coloured Label A custom widget that draws a label in a specific colour, and optionally at a specific size, and word wrapped. """ HELP_SCALE = DEFAULT_SCALE HEADER_SCALE = 1.25 _state = None def __init__( self, text: str, parent: QWidget, *, color: QColor | None = None, faded: QColor | None = None, scale: float = HELP_SCALE, wrap: bool = False, indent: int = 0, bold: bool = False ) -> None: super().__init__(text, parent=parent) default = self.palette().windowText().color() self._color = color or default self._faded = faded or default font = self.font() font.setPointSizeF(scale*font.pointSizeF()) font.setWeight(QFont.Weight.Bold if bold else QFont.Weight.Normal) if color: colour = self.palette() colour.setColor(QPalette.ColorRole.WindowText, color) self.setPalette(colour) self.setFont(font) self.setIndent(indent) self.setWordWrap(wrap) self.setColorState(True) return def setTextColors(self, *, color: QColor | None = None, faded: QColor | None = None) -> None: """Set or update the text colours.""" self._color = color or self._color self._faded = faded or self._faded self._refeshTextColor() return def setColorState(self, state: bool) -> None: """Change the colour state.""" if self._state is not state: self._state = state self._refeshTextColor() return def _refeshTextColor(self) -> None: """Refresh the colour of the text on the label.""" palette = self.palette() palette.setColor( QPalette.ColorRole.WindowText, self._color if self._state else self._faded, ) self.setPalette(palette) return class NWrappedWidgetBox(QHBoxLayout): """Extension: A Text-Wrapped Widget Box A custom layout box where a widget is wrapped in text labels on either side within a layout box. The widget is inserted at the {0} position so that it can be used for translation strings. """ def __init__(self, text: str, widget: QWidget) -> None: super().__init__() before, _, after = text.partition(r"{0}") if before: self.addWidget(QLabel(before.rstrip())) self.addWidget(widget) if after: self.addWidget(QLabel(after.lstrip())) return