From ce2ac9b1a14f476c169f1bd8104139b292cc56dc Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 18 Jan 2024 23:34:27 +0100 Subject: [PATCH 01/34] Add a novel info dialog --- novelwriter/gui/mainmenu.py | 6 ++- novelwriter/guimain.py | 13 +++++++ novelwriter/tools/novelinfo.py | 71 ++++++++++++++++++++++++++++++++++ novelwriter/tools/welcome.py | 8 +--- 4 files changed, 91 insertions(+), 7 deletions(-) create mode 100644 novelwriter/tools/novelinfo.py diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 189cd72d..94cd1d20 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -157,9 +157,13 @@ class GuiMainMenu(QMenuBar): # Project > Project Details self.aProjectDetails = self.projMenu.addAction(self.tr("Project Details")) - self.aProjectDetails.setShortcut("Shift+F6") self.aProjectDetails.triggered.connect(self.mainGui.showProjectDetailsDialog) + # Project > Novel Info + self.aNovelInfo = self.projMenu.addAction(self.tr("Novel Info")) + self.aNovelInfo.setShortcut("Shift+F6") + self.aNovelInfo.triggered.connect(self.mainGui.showNovelInfoDialog) + # Project > Separator self.projMenu.addSeparator() diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 6717351a..fea65021 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -56,6 +56,7 @@ from novelwriter.dialogs.preferences import GuiPreferences from novelwriter.dialogs.projdetails import GuiProjectDetails from novelwriter.dialogs.projsettings import GuiProjectSettings from novelwriter.tools.welcome import GuiWelcome +from novelwriter.tools.novelinfo import GuiNovelInfo from novelwriter.tools.manuscript import GuiManuscript from novelwriter.tools.dictionaries import GuiDictionaries from novelwriter.tools.writingstats import GuiWritingStats @@ -836,6 +837,18 @@ class GuiMain(QMainWindow): dialog.updateValues() return + @pyqtSlot() + def showNovelInfoDialog(self) -> None: + """Open the novel info dialog.""" + if SHARED.hasProject: + dialog = GuiNovelInfo(self) + dialog.setModal(True) + dialog.show() + dialog.raise_() + qApp.processEvents() + # dialog.updateValues() + return + @pyqtSlot() def showBuildManuscriptDialog(self) -> None: """Open the build manuscript dialog.""" diff --git a/novelwriter/tools/novelinfo.py b/novelwriter/tools/novelinfo.py new file mode 100644 index 00000000..611c80b3 --- /dev/null +++ b/novelwriter/tools/novelinfo.py @@ -0,0 +1,71 @@ +""" +novelWriter – GUI Novel Info +============================ + +File History: +Created: 2024-01-18 [2.3b1] GuiNovelInfo + +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 + +import logging + +from PyQt5.QtGui import QCloseEvent +from PyQt5.QtWidgets import QDialog, QWidget + +logger = logging.getLogger(__name__) + + +class GuiNovelInfo(QDialog): + + def __init__(self, parent: QWidget) -> None: + super().__init__(parent=parent) + + logger.debug("Create: GuiNovelInfo") + self.setObjectName("GuiNovelInfo") + + self.setWindowTitle(self.tr("Novel Info")) + logger.debug("Ready: GuiNovelInfo") + + return + + def __del__(self) -> None: # pragma: no cover + logger.debug("Delete: GuiNovelInfo") + return + + ## + # Events + ## + + def closeEvent(self, event: QCloseEvent) -> None: + """Capture the user closing the window and save settings.""" + self._saveSettings() + event.accept() + self.deleteLater() + return + + ## + # Internal Functions + ## + + def _saveSettings(self) -> None: + """Save the user GUI settings.""" + logger.debug("Saving State: GuiNovelInfo") + return + +# END Class GuiNovelInfo diff --git a/novelwriter/tools/welcome.py b/novelwriter/tools/welcome.py index d136cec6..4a7d65e4 100644 --- a/novelwriter/tools/welcome.py +++ b/novelwriter/tools/welcome.py @@ -25,7 +25,6 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING from pathlib import Path from datetime import datetime @@ -48,9 +47,6 @@ from novelwriter.constants import nwUnicode from novelwriter.core.coretools import ProjectBuilder from novelwriter.extensions.switch import NSwitch -if TYPE_CHECKING: # pragma: no cover - from novelwriter.guimain import GuiMain - logger = logging.getLogger(__name__) @@ -58,8 +54,8 @@ class GuiWelcome(QDialog): openProjectRequest = pyqtSignal(Path) - def __init__(self, mainGui: GuiMain) -> None: - super().__init__(parent=mainGui) + def __init__(self, parent: QWidget) -> None: + super().__init__(parent=parent) logger.debug("Create: GuiWelcome") self.setObjectName("GuiWelcome") From 82c63723a486ebc5c874cf2c81cc0c65cf6bcc72 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 19 Jan 2024 16:13:12 +0100 Subject: [PATCH 02/34] Make all theme colours actual QColor objects --- novelwriter/dialogs/about.py | 20 +++--- novelwriter/dialogs/preferences.py | 4 +- novelwriter/extensions/configlayout.py | 15 ++-- novelwriter/extensions/pagedsidebar.py | 4 +- novelwriter/gui/doceditor.py | 33 ++++----- novelwriter/gui/dochighlight.py | 98 +++++++++----------------- novelwriter/gui/docviewer.py | 85 ++++++++++++---------- novelwriter/gui/statusbar.py | 7 +- novelwriter/gui/theme.py | 67 +++++++----------- novelwriter/tools/manussettings.py | 8 +-- novelwriter/tools/welcome.py | 2 +- 11 files changed, 149 insertions(+), 194 deletions(-) diff --git a/novelwriter/dialogs/about.py b/novelwriter/dialogs/about.py index 69e19785..1329b06e 100644 --- a/novelwriter/dialogs/about.py +++ b/novelwriter/dialogs/about.py @@ -220,23 +220,25 @@ class GuiAbout(QDialog): def _setStyleSheet(self) -> None: """Set stylesheet for all browser tabs.""" + colHead = SHARED.theme.colHead + colKey = SHARED.theme.colKey styleSheet = ( "h1, h2, h3, h4 {{" - " color: rgb({hColR},{hColG},{hColB});" + " color: rgb({hColR}, {hColG}, {hColB});" "}}\n" "a {{" - " color: rgb({hColR},{hColG},{hColB});" + " color: rgb({hColR}, {hColG}, {hColB});" "}}\n" ".alt {{" - " color: rgb({kColR},{kColG},{kColB});" + " color: rgb({kColR}, {kColG}, {kColB});" "}}\n" ).format( - hColR=SHARED.theme.colHead[0], - hColG=SHARED.theme.colHead[1], - hColB=SHARED.theme.colHead[2], - kColR=SHARED.theme.colKey[0], - kColG=SHARED.theme.colKey[1], - kColB=SHARED.theme.colKey[2], + hColR=colHead.red(), + hColG=colHead.green(), + hColB=colHead.blue(), + kColR=colKey.red(), + kColG=colKey.green(), + kColB=colKey.blue(), ) self.pageAbout.document().setDefaultStyleSheet(styleSheet) self.pageNotes.document().setDefaultStyleSheet(styleSheet) diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index 49e58e00..32769451 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -26,7 +26,7 @@ from __future__ import annotations import logging -from PyQt5.QtGui import QCloseEvent, QColor, QFont, QKeyEvent, QKeySequence, QPalette +from PyQt5.QtGui import QCloseEvent, QFont, QKeyEvent, QKeySequence, QPalette from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot from PyQt5.QtWidgets import ( QAbstractButton, QComboBox, QCompleter, QDialog, QDialogButtonBox, @@ -62,7 +62,7 @@ class GuiPreferences(QDialog): font.setPointSizeF(1.25*SHARED.theme.fontPointSize) palette = self.palette() - palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.helpText)) + palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.helpText) self.titleLabel = QLabel(self.tr("Preferences"), self) self.titleLabel.setFont(font) diff --git a/novelwriter/extensions/configlayout.py b/novelwriter/extensions/configlayout.py index d714cf00..54ecb3c8 100644 --- a/novelwriter/extensions/configlayout.py +++ b/novelwriter/extensions/configlayout.py @@ -76,9 +76,9 @@ class NScrollableForm(QScrollArea): # Setters ## - def setHelpTextStyle(self, color: QColor | list | tuple, scale: float = FONT_SCALE) -> None: + 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._helpCol = color self._fontScale = scale return @@ -187,7 +187,7 @@ class NConfigLayout(QGridLayout): # Getters and Setters ## - def setHelpTextStyle(self, color: QColor | list | tuple, scale: float = FONT_SCALE) -> None: + 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 @@ -328,18 +328,15 @@ class NSimpleLayout(QGridLayout): class NHelpLabel(QLabel): - def __init__(self, text: str, color: QColor | list | tuple, - fontSize: float = FONT_SCALE) -> None: + def __init__(self, text: str, color: QColor, scale: float = FONT_SCALE) -> None: super().__init__(text) - qCol = color if isinstance(color, QColor) else QColor(*color) - lblCol = self.palette() - lblCol.setColor(QPalette.WindowText, qCol) + lblCol.setColor(QPalette.WindowText, color) self.setPalette(lblCol) lblFont = self.font() - lblFont.setPointSizeF(fontSize*lblFont.pointSizeF()) + lblFont.setPointSizeF(scale*lblFont.pointSizeF()) self.setFont(lblFont) self.setWordWrap(True) diff --git a/novelwriter/extensions/pagedsidebar.py b/novelwriter/extensions/pagedsidebar.py index 2f417b44..185b4d8a 100644 --- a/novelwriter/extensions/pagedsidebar.py +++ b/novelwriter/extensions/pagedsidebar.py @@ -67,9 +67,9 @@ class NPagedSideBar(QToolBar): """Return a specific button.""" return self._buttons[buttonId] - def setLabelColor(self, color: list | QColor) -> None: + def setLabelColor(self, color: QColor) -> None: """Set the text color for the labels.""" - self._labelCol = color if isinstance(color, QColor) else QColor(*color) + self._labelCol = color return def addSeparator(self) -> None: diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index c7554591..6baddf67 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -276,14 +276,14 @@ class GuiDocEditor(QPlainTextEdit): def updateSyntaxColours(self) -> None: """Update the syntax highlighting theme.""" mainPalette = self.palette() - mainPalette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack)) - mainPalette.setColor(QPalette.ColorRole.Base, QColor(*SHARED.theme.colBack)) - mainPalette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText)) + mainPalette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack) + mainPalette.setColor(QPalette.ColorRole.Base, SHARED.theme.colBack) + mainPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText) self.setPalette(mainPalette) docPalette = self.viewport().palette() - docPalette.setColor(QPalette.ColorRole.Base, QColor(*SHARED.theme.colBack)) - docPalette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText)) + docPalette.setColor(QPalette.ColorRole.Base, SHARED.theme.colBack) + docPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText) self.viewport().setPalette(docPalette) self.docHeader.matchColours() @@ -2326,9 +2326,9 @@ class GuiDocToolBar(QWidget): def updateTheme(self) -> None: """Initialise GUI elements that depend on specific settings.""" palette = QPalette() - palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack)) - palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText)) - palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText)) + palette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack) + palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText) + palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText) self.setPalette(palette) self.tbBoldMD.setIcon(SHARED.theme.getIcon("fmt_bold-md")) @@ -2866,10 +2866,11 @@ class GuiDocEditHeader(QWidget): self.minmaxButton.setIcon(SHARED.theme.getIcon("maximise")) self.closeButton.setIcon(SHARED.theme.getIcon("close")) + colText = SHARED.theme.colText buttonStyle = ( "QToolButton {{border: none; background: transparent;}} " - "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}" - ).format(*SHARED.theme.colText) + "QToolButton:hover {{border: none; background: rgba({0}, {1}, {2}, 0.2);}}" + ).format(colText.red(), colText.green(), colText.blue()) self.tbButton.setStyleSheet(buttonStyle) self.searchButton.setStyleSheet(buttonStyle) @@ -2885,9 +2886,9 @@ class GuiDocEditHeader(QWidget): theme rather than the main GUI. """ palette = QPalette() - palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack)) - palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText)) - palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText)) + palette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack) + palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText) + palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText) self.setPalette(palette) self.itemTitle.setPalette(palette) @@ -3093,9 +3094,9 @@ class GuiDocEditFooter(QWidget): theme rather than the main GUI. """ palette = QPalette() - palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack)) - palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText)) - palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText)) + palette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack) + palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText) + palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText) self.setPalette(palette) self.statusText.setPalette(palette) diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py index 5dcece0f..e89ec5f6 100644 --- a/novelwriter/gui/dochighlight.py +++ b/novelwriter/gui/dochighlight.py @@ -65,22 +65,6 @@ class GuiDocHighlighter(QSyntaxHighlighter): self._hRules: list[tuple[str, dict]] = [] self._hStyles: dict[str, QTextCharFormat] = {} - self._colHead = QColor(0, 0, 0) - self._colHeadH = QColor(0, 0, 0) - self._colEmph = QColor(0, 0, 0) - self._colDialN = QColor(0, 0, 0) - self._colDialD = QColor(0, 0, 0) - self._colDialS = QColor(0, 0, 0) - self._colHidden = QColor(0, 0, 0) - self._colCode = QColor(0, 0, 0) - self._colKey = QColor(0, 0, 0) - self._colVal = QColor(0, 0, 0) - self._colSpell = QColor(0, 0, 0) - self._colError = QColor(0, 0, 0) - self._colRepTag = QColor(0, 0, 0) - self._colMod = QColor(0, 0, 0) - self._colBreak = QColor(0, 0, 0) - self.initHighlighter() logger.debug("Ready: GuiDocHighlighter") @@ -93,54 +77,37 @@ class GuiDocHighlighter(QSyntaxHighlighter): """ logger.debug("Setting up highlighting rules") - self._colHead = QColor(*SHARED.theme.colHead) - self._colHeadH = QColor(*SHARED.theme.colHeadH) - self._colDialN = QColor(*SHARED.theme.colDialN) - self._colDialD = QColor(*SHARED.theme.colDialD) - self._colDialS = QColor(*SHARED.theme.colDialS) - self._colHidden = QColor(*SHARED.theme.colHidden) - self._colCode = QColor(*SHARED.theme.colCode) - self._colKey = QColor(*SHARED.theme.colKey) - self._colVal = QColor(*SHARED.theme.colVal) - self._colSpell = QColor(*SHARED.theme.colSpell) - self._colError = QColor(*SHARED.theme.colError) - self._colRepTag = QColor(*SHARED.theme.colRepTag) - self._colMod = QColor(*SHARED.theme.colMod) - self._colBreak = QColor(*SHARED.theme.colEmph) - self._colBreak.setAlpha(64) - - self._colEmph = None - if CONFIG.highlightEmph: - self._colEmph = QColor(*SHARED.theme.colEmph) - - self._hStyles = { - "header1": self._makeFormat(self._colHead, "bold", 1.8), - "header2": self._makeFormat(self._colHead, "bold", 1.6), - "header3": self._makeFormat(self._colHead, "bold", 1.4), - "header4": self._makeFormat(self._colHead, "bold", 1.2), - "header1h": self._makeFormat(self._colHeadH, "bold", 1.8), - "header2h": self._makeFormat(self._colHeadH, "bold", 1.6), - "header3h": self._makeFormat(self._colHeadH, "bold", 1.4), - "header4h": self._makeFormat(self._colHeadH, "bold", 1.2), - "bold": self._makeFormat(self._colEmph, "bold"), - "italic": self._makeFormat(self._colEmph, "italic"), - "strike": self._makeFormat(self._colHidden, "strike"), - "mspaces": self._makeFormat(self._colError, "errline"), - "nobreak": self._makeFormat(self._colBreak, "background"), - "dialogue1": self._makeFormat(self._colDialN), - "dialogue2": self._makeFormat(self._colDialD), - "dialogue3": self._makeFormat(self._colDialS), - "replace": self._makeFormat(self._colRepTag), - "hidden": self._makeFormat(self._colHidden), - "code": self._makeFormat(self._colCode), - "keyword": self._makeFormat(self._colKey), - "modifier": self._makeFormat(self._colMod), - "value": self._makeFormat(self._colVal, "underline"), - "codevalue": self._makeFormat(self._colVal), - "codeinval": self._makeFormat(None, "errline"), - } + colEmph = SHARED.theme.colEmph if CONFIG.highlightEmph else None + colBreak = QColor(SHARED.theme.colEmph) + colBreak.setAlpha(64) self._hRules = [] + self._hStyles = { + "header1": self._makeFormat(SHARED.theme.colHead, "bold", 1.8), + "header2": self._makeFormat(SHARED.theme.colHead, "bold", 1.6), + "header3": self._makeFormat(SHARED.theme.colHead, "bold", 1.4), + "header4": self._makeFormat(SHARED.theme.colHead, "bold", 1.2), + "header1h": self._makeFormat(SHARED.theme.colHeadH, "bold", 1.8), + "header2h": self._makeFormat(SHARED.theme.colHeadH, "bold", 1.6), + "header3h": self._makeFormat(SHARED.theme.colHeadH, "bold", 1.4), + "header4h": self._makeFormat(SHARED.theme.colHeadH, "bold", 1.2), + "bold": self._makeFormat(colEmph, "bold"), + "italic": self._makeFormat(colEmph, "italic"), + "strike": self._makeFormat(SHARED.theme.colHidden, "strike"), + "mspaces": self._makeFormat(SHARED.theme.colError, "errline"), + "nobreak": self._makeFormat(colBreak, "background"), + "dialogue1": self._makeFormat(SHARED.theme.colDialN), + "dialogue2": self._makeFormat(SHARED.theme.colDialD), + "dialogue3": self._makeFormat(SHARED.theme.colDialS), + "replace": self._makeFormat(SHARED.theme.colRepTag), + "hidden": self._makeFormat(SHARED.theme.colHidden), + "code": self._makeFormat(SHARED.theme.colCode), + "keyword": self._makeFormat(SHARED.theme.colKey), + "modifier": self._makeFormat(SHARED.theme.colMod), + "value": self._makeFormat(SHARED.theme.colVal, "underline"), + "codevalue": self._makeFormat(SHARED.theme.colVal), + "codeinval": self._makeFormat(None, "errline"), + } # Multiple or Trailing Spaces if CONFIG.showMultiSpaces: @@ -318,7 +285,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): self.setFormat(xPos, xLen, self._hStyles["value"]) else: kwFmt = self.format(xPos) - kwFmt.setUnderlineColor(self._colError) + kwFmt.setUnderlineColor(SHARED.theme.colError) kwFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline) self.setFormat(xPos, xLen, kwFmt) @@ -402,8 +369,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): for xPos, xLen in data.spellCheck(text): for x in range(xPos, xPos+xLen): spFmt = self.format(x) - spFmt.setUnderlineColor(self._colSpell) - spFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline) + spFmt.merge(self._hStyles["codeinval"]) self.setFormat(x, 1, spFmt) return @@ -431,7 +397,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): if "strike" in styles: charFormat.setFontStrikeOut(True) if "errline" in styles: - charFormat.setUnderlineColor(self._colError) + charFormat.setUnderlineColor(SHARED.theme.colError) charFormat.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline) if "underline" in styles: charFormat.setFontUnderline(True) diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index 5a066ccc..3a567668 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -33,7 +33,7 @@ from typing import TYPE_CHECKING from PyQt5.QtCore import pyqtSignal, pyqtSlot, QPoint, QSize, Qt, QUrl from PyQt5.QtGui import ( - QColor, QCursor, QFont, QMouseEvent, QPalette, QResizeEvent, QTextCursor, + QCursor, QFont, QMouseEvent, QPalette, QResizeEvent, QTextCursor, QTextOption ) from PyQt5.QtWidgets import ( @@ -149,14 +149,14 @@ class GuiDocViewer(QTextBrowser): # Set the widget colours to match syntax theme mainPalette = self.palette() - mainPalette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack)) - mainPalette.setColor(QPalette.ColorRole.Base, QColor(*SHARED.theme.colBack)) - mainPalette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText)) + mainPalette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack) + mainPalette.setColor(QPalette.ColorRole.Base, SHARED.theme.colBack) + mainPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText) self.setPalette(mainPalette) docPalette = self.viewport().palette() - docPalette.setColor(QPalette.ColorRole.Base, QColor(*SHARED.theme.colBack)) - docPalette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText)) + docPalette.setColor(QPalette.ColorRole.Base, SHARED.theme.colBack) + docPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText) self.viewport().setPalette(docPalette) self.docHeader.matchColours() @@ -463,7 +463,13 @@ class GuiDocViewer(QTextBrowser): """Generate an appropriate style sheet for the document viewer, based on the current syntax highlighter theme, """ - pTheme = SHARED.theme + colText = SHARED.theme.colText + colHead = SHARED.theme.colHead + colVal = SHARED.theme.colVal + colEmph = SHARED.theme.colEmph + colKey = SHARED.theme.colKey + colHidden = SHARED.theme.colHidden + colMod = SHARED.theme.colMod styleSheet = ( "body {{" " color: rgb({tColR}, {tColG}, {tColB});" @@ -490,27 +496,27 @@ class GuiDocViewer(QTextBrowser): " text-align: center;" "}}\n" ).format( - tColR=pTheme.colText[0], - tColG=pTheme.colText[1], - tColB=pTheme.colText[2], - hColR=pTheme.colHead[0], - hColG=pTheme.colHead[1], - hColB=pTheme.colHead[2], - aColR=pTheme.colVal[0], - aColG=pTheme.colVal[1], - aColB=pTheme.colVal[2], - eColR=pTheme.colEmph[0], - eColG=pTheme.colEmph[1], - eColB=pTheme.colEmph[2], - kColR=pTheme.colKey[0], - kColG=pTheme.colKey[1], - kColB=pTheme.colKey[2], - cColR=pTheme.colHidden[0], - cColG=pTheme.colHidden[1], - cColB=pTheme.colHidden[2], - mColR=pTheme.colMod[0], - mColG=pTheme.colMod[1], - mColB=pTheme.colMod[2], + tColR=colText.red(), + tColG=colText.green(), + tColB=colText.blue(), + hColR=colHead.red(), + hColG=colHead.green(), + hColB=colHead.blue(), + aColR=colVal.red(), + aColG=colVal.green(), + aColB=colVal.blue(), + eColR=colEmph.red(), + eColG=colEmph.green(), + eColB=colEmph.blue(), + kColR=colKey.red(), + kColG=colKey.green(), + kColB=colKey.blue(), + cColR=colHidden.red(), + cColG=colHidden.green(), + cColB=colHidden.blue(), + mColR=colMod.red(), + mColG=colMod.green(), + mColB=colMod.blue(), ) self.document().setDefaultStyleSheet(styleSheet) @@ -744,10 +750,11 @@ class GuiDocViewHeader(QWidget): self.refreshButton.setIcon(SHARED.theme.getIcon("refresh")) self.closeButton.setIcon(SHARED.theme.getIcon("close")) + colText = SHARED.theme.colText buttonStyle = ( "QToolButton {{border: none; background: transparent;}} " - "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}" - ).format(*SHARED.theme.colText) + "QToolButton:hover {{border: none; background: rgba({0}, {1}, {2}, 0.2);}}" + ).format(colText.red(), colText.green(), colText.blue()) self.backButton.setStyleSheet(buttonStyle) self.forwardButton.setStyleSheet(buttonStyle) @@ -763,9 +770,9 @@ class GuiDocViewHeader(QWidget): theme rather than the main GUI. """ palette = QPalette() - palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack)) - palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText)) - palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText)) + palette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack) + palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText) + palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText) self.setPalette(palette) self.docTitle.setPalette(palette) return @@ -937,11 +944,11 @@ class GuiDocViewFooter(QWidget): self.showComments.setIcon(bulletIcon) self.showSynopsis.setIcon(bulletIcon) - # StyleSheets + colText = SHARED.theme.colText buttonStyle = ( "QToolButton {{border: none; background: transparent;}} " - "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}" - ).format(*SHARED.theme.colText) + "QToolButton:hover {{border: none; background: rgba({0}, {1}, {2}, 0.2);}}" + ).format(colText.red(), colText.green(), colText.blue()) self.showHide.setStyleSheet(buttonStyle) self.showComments.setStyleSheet(buttonStyle) @@ -956,9 +963,9 @@ class GuiDocViewFooter(QWidget): theme rather than the main GUI. """ palette = QPalette() - palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack)) - palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText)) - palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText)) + palette.setColor(QPalette.ColorRole.Window, SHARED.theme.colBack) + palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText) + palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText) self.setPalette(palette) return diff --git a/novelwriter/gui/statusbar.py b/novelwriter/gui/statusbar.py index fd95be74..61168f20 100644 --- a/novelwriter/gui/statusbar.py +++ b/novelwriter/gui/statusbar.py @@ -29,7 +29,6 @@ from time import time from typing import TYPE_CHECKING, Literal from datetime import datetime -from PyQt5.QtGui import QColor from PyQt5.QtCore import pyqtSlot, QLocale from PyQt5.QtWidgets import qApp, QStatusBar, QLabel @@ -55,9 +54,9 @@ class GuiMainStatus(QStatusBar): self._userIdle = False self._debugInfo = False - colNone = QColor(*SHARED.theme.statNone) - colSaved = QColor(*SHARED.theme.statSaved) - colUnsaved = QColor(*SHARED.theme.statUnsaved) + colNone = SHARED.theme.statNone + colSaved = SHARED.theme.statSaved + colUnsaved = SHARED.theme.statUnsaved iPx = SHARED.theme.baseIconSize diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 2e3e8b94..5f8dbfac 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -70,10 +70,10 @@ class GuiTheme: self.isLightTheme = True # GUI - self.statNone = [120, 120, 120] - self.statUnsaved = [200, 15, 39] - self.statSaved = [2, 133, 37] - self.helpText = [0, 0, 0] + self.statNone = QColor(120, 120, 120) + self.statUnsaved = QColor(200, 15, 39) + self.statSaved = QColor(2, 133, 37) + self.helpText = QColor(0, 0, 0) # Loaded Syntax Settings # ====================== @@ -88,23 +88,23 @@ class GuiTheme: self.syntaxLicenseUrl = "" # Colours - self.colBack = [255, 255, 255] - self.colText = [0, 0, 0] - self.colLink = [0, 0, 0] - self.colHead = [0, 0, 0] - self.colHeadH = [0, 0, 0] - self.colEmph = [0, 0, 0] - self.colDialN = [0, 0, 0] - self.colDialD = [0, 0, 0] - self.colDialS = [0, 0, 0] - self.colHidden = [0, 0, 0] - self.colCode = [0, 0, 0] - self.colKey = [0, 0, 0] - self.colVal = [0, 0, 0] - self.colSpell = [0, 0, 0] - self.colError = [0, 0, 0] - self.colRepTag = [0, 0, 0] - self.colMod = [0, 0, 0] + self.colBack = QColor(255, 255, 255) + self.colText = QColor(0, 0, 0) + self.colLink = QColor(0, 0, 0) + self.colHead = QColor(0, 0, 0) + self.colHeadH = QColor(0, 0, 0) + self.colEmph = QColor(0, 0, 0) + self.colDialN = QColor(0, 0, 0) + self.colDialD = QColor(0, 0, 0) + self.colDialS = QColor(0, 0, 0) + self.colHidden = QColor(0, 0, 0) + self.colCode = QColor(0, 0, 0) + self.colKey = QColor(0, 0, 0) + self.colVal = QColor(0, 0, 0) + self.colSpell = QColor(0, 0, 0) + self.colError = QColor(0, 0, 0) + self.colRepTag = QColor(0, 0, 0) + self.colMod = QColor(0, 0, 0) # Class Setup # =========== @@ -256,12 +256,12 @@ class GuiTheme: backLNess = backCol.lightnessF() textLNess = textCol.lightnessF() self.isLightTheme = backLNess > textLNess - if self.helpText == [0, 0, 0]: + if self.helpText == QColor(0, 0, 0): if self.isLightTheme: helpLCol = textLNess + 0.35*(backLNess - textLNess) else: helpLCol = backLNess + 0.65*(textLNess - backLNess) - self.helpText = [int(255*helpLCol)]*3 + self.helpText = QColor.fromHsl(0, 0, int(255*helpLCol)) # Icons defaultIcons = "typicons_light" if backLNess >= 0.5 else "typicons_dark" @@ -398,29 +398,14 @@ class GuiTheme: return True - def _parseColour(self, parser: NWConfigParser, section: str, name: str) -> list[int]: + def _parseColour(self, parser: NWConfigParser, section: str, name: str) -> QColor: """Parse a colour value from a config string.""" - if parser.has_option(section, name): - values = parser.get(section, name).split(",") - result = [] - try: - result.append(minmax(int(values[0]), 0, 255)) - result.append(minmax(int(values[1]), 0, 255)) - result.append(minmax(int(values[2]), 0, 255)) - except Exception: - logger.error("Could not load theme colours for '%s' from config file", name) - result = [0, 0, 0] - else: - logger.warning("Could not find theme colours for '%s' in config file", name) - result = [0, 0, 0] - return result + return QColor(*parser.rdIntList(section, name, [0, 0, 0, 255])) def _setPalette(self, parser: NWConfigParser, section: str, name: str, value: QPalette.ColorRole) -> None: """Set a palette colour value from a config string.""" - self._guiPalette.setColor( - value, QColor(*self._parseColour(parser, section, name)) - ) + self._guiPalette.setColor(value, self._parseColour(parser, section, name)) return # End Class GuiTheme diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index fb0324d1..e0c034af 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -27,9 +27,7 @@ import logging from typing import TYPE_CHECKING -from PyQt5.QtGui import ( - QColor, QFont, QIcon, QSyntaxHighlighter, QTextCharFormat, QTextDocument -) +from PyQt5.QtGui import QFont, QIcon, QSyntaxHighlighter, QTextCharFormat, QTextDocument from PyQt5.QtCore import QEvent, QSize, Qt, pyqtSignal, pyqtSlot from PyQt5.QtWidgets import ( QAbstractButton, QAbstractItemView, QComboBox, QDialog, QDialogButtonBox, @@ -862,9 +860,9 @@ class _HeadingSyntaxHighlighter(QSyntaxHighlighter): def __init__(self, document: QTextDocument) -> None: super().__init__(document) self._fmtSymbol = QTextCharFormat() - self._fmtSymbol.setForeground(QColor(*SHARED.theme.colHead)) + self._fmtSymbol.setForeground(SHARED.theme.colHead) self._fmtFormat = QTextCharFormat() - self._fmtFormat.setForeground(QColor(*SHARED.theme.colEmph)) + self._fmtFormat.setForeground(SHARED.theme.colEmph) return def highlightBlock(self, text: str) -> None: diff --git a/novelwriter/tools/welcome.py b/novelwriter/tools/welcome.py index d136cec6..ed065192 100644 --- a/novelwriter/tools/welcome.py +++ b/novelwriter/tools/welcome.py @@ -343,7 +343,7 @@ class _ProjectListItem(QStyledItemDelegate): self._dFont = qApp.font() self._dFont.setPointSizeF(fPt) - self._dPen = QPen(QColor(*SHARED.theme.helpText)) + self._dPen = QPen(SHARED.theme.helpText) self._icon = SHARED.theme.getPixmap("proj_nwx", (iPx, iPx)) From b92112e9337aa64de34c2ffb42e0470813a24335 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 19 Jan 2024 16:13:29 +0100 Subject: [PATCH 03/34] Update tests --- tests/test_gui/test_gui_guimain.py | 4 ++-- tests/test_gui/test_gui_theme.py | 27 ++++++++++----------------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 38cb7ef2..cb06a470 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -29,7 +29,7 @@ from tools import ( C, NWD_IGNORE, cmpFiles, buildTestProject, XML_IGNORE, getGuiItem ) -from PyQt5.QtGui import QColor, QPalette +from PyQt5.QtGui import QPalette from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QMenu, QInputDialog @@ -170,7 +170,7 @@ def testGuiMain_UpdateTheme(qtbot, nwGUI): mainTheme.loadSyntax() nwGUI._processConfigChanges(True, True, True, True) - syntaxBack = QColor(*SHARED.theme.colBack) + syntaxBack = SHARED.theme.colBack assert nwGUI.docEditor.palette().color(QPalette.ColorRole.Window) == syntaxBack assert nwGUI.docEditor.docHeader.palette().color(QPalette.ColorRole.Window) == syntaxBack diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index 08defddc..86a43cce 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -27,7 +27,7 @@ from pathlib import Path from mocked import causeOSError from tools import writeFile -from PyQt5.QtGui import QIcon, QPalette, QPixmap +from PyQt5.QtGui import QColor, QIcon, QPalette, QPixmap from PyQt5.QtWidgets import QApplication from novelwriter import CONFIG, SHARED @@ -95,22 +95,15 @@ def testGuiTheme_Main(qtbot, nwGUI, tstPaths): "colour4": "-10, 127, 300", } - # Test the parser for several valid and invalid values - assert mainTheme._parseColour(parser, "Palette", "colour1") == [100, 150, 200] - assert mainTheme._parseColour(parser, "Palette", "colour2") == [100, 150, 200] - assert mainTheme._parseColour(parser, "Palette", "colour3") == [0, 0, 0] - assert mainTheme._parseColour(parser, "Palette", "colour4") == [0, 127, 255] - assert mainTheme._parseColour(parser, "Palette", "colour5") == [0, 0, 0] - # The palette should load with the parsed values mainTheme._setPalette(parser, "Palette", "colour1", QPalette.Window) assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (100, 150, 200, 255) mainTheme._setPalette(parser, "Palette", "colour2", QPalette.Window) - assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (100, 150, 200, 255) + assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (100, 150, 200, 250) mainTheme._setPalette(parser, "Palette", "colour3", QPalette.Window) - assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (0, 0, 0, 255) + assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (250, 250, 0, 255) mainTheme._setPalette(parser, "Palette", "colour4", QPalette.Window) - assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (0, 127, 255, 255) + assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (0, 0, 0, 0) mainTheme._setPalette(parser, "Palette", "colour5", QPalette.Window) assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (0, 0, 0, 255) @@ -240,9 +233,9 @@ def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI): # Check some values assert mainTheme.syntaxName == "Default Light" - assert mainTheme.colBack == [255, 255, 255] - assert mainTheme.colText == [0, 0, 0] - assert mainTheme.colLink == [0, 0, 200] + assert mainTheme.colBack == QColor(255, 255, 255) + assert mainTheme.colText == QColor(0, 0, 0) + assert mainTheme.colLink == QColor(0, 0, 200) # Load Default Dark Theme # ======================= @@ -253,9 +246,9 @@ def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI): # Check some values assert mainTheme.syntaxName == "Default Dark" - assert mainTheme.colBack == [54, 54, 54] - assert mainTheme.colText == [199, 207, 208] - assert mainTheme.colLink == [184, 200, 0] + assert mainTheme.colBack == QColor(54, 54, 54) + assert mainTheme.colText == QColor(199, 207, 208) + assert mainTheme.colLink == QColor(184, 200, 0) # qtbot.stop() From e10140b1cd3c9eac1ea13c8267bd02520adb2286 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 19 Jan 2024 16:34:57 +0100 Subject: [PATCH 04/34] Fix spelling colour in highlighter and make a few improvements --- novelwriter/gui/dochighlight.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py index e89ec5f6..e24a7904 100644 --- a/novelwriter/gui/dochighlight.py +++ b/novelwriter/gui/dochighlight.py @@ -48,6 +48,8 @@ SPELLRX.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption) class GuiDocHighlighter(QSyntaxHighlighter): + __slots__ = ("_tItem", "_tHandle", "_spellCheck", "_hRules", "_hStyles", "_spellErr") + BLOCK_NONE = 0 BLOCK_TEXT = 1 BLOCK_META = 2 @@ -58,9 +60,10 @@ class GuiDocHighlighter(QSyntaxHighlighter): logger.debug("Create: GuiDocHighlighter") - self._tItem = None - self._tHandle = None + self._tItem = None + self._tHandle = None self._spellCheck = False + self._spellErr = QTextCharFormat() self._hRules: list[tuple[str, dict]] = [] self._hStyles: dict[str, QTextCharFormat] = {} @@ -109,6 +112,11 @@ class GuiDocHighlighter(QSyntaxHighlighter): "codeinval": self._makeFormat(None, "errline"), } + # Cache Spell Error Format + self._spellErr = QTextCharFormat() + self._spellErr.setUnderlineColor(SHARED.theme.colSpell) + self._spellErr.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline) + # Multiple or Trailing Spaces if CONFIG.showMultiSpaces: self._hRules.append(( @@ -272,12 +280,12 @@ class GuiDocHighlighter(QSyntaxHighlighter): self.setCurrentBlockState(self.BLOCK_META) if self._tItem: pIndex = SHARED.project.index - isValid, theBits, thePos = pIndex.scanThis(text) - isGood = pIndex.checkThese(theBits, self._tItem) + isValid, bits, pos = pIndex.scanThis(text) + isGood = pIndex.checkThese(bits, self._tItem) if isValid: - for n, theBit in enumerate(theBits): - xPos = thePos[n] - xLen = len(theBit) + for n, bit in enumerate(bits): + xPos = pos[n] + xLen = len(bit) if isGood[n]: if n == 0: self.setFormat(xPos, xLen, self._hStyles["keyword"]) @@ -285,8 +293,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): self.setFormat(xPos, xLen, self._hStyles["value"]) else: kwFmt = self.format(xPos) - kwFmt.setUnderlineColor(SHARED.theme.colError) - kwFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline) + kwFmt.merge(self._hStyles["codeinval"]) self.setFormat(xPos, xLen, kwFmt) # We never want to run the spell checker on keyword/values, @@ -369,7 +376,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): for xPos, xLen in data.spellCheck(text): for x in range(xPos, xPos+xLen): spFmt = self.format(x) - spFmt.merge(self._hStyles["codeinval"]) + spFmt.merge(self._spellErr) self.setFormat(x, 1, spFmt) return From ca02bbf20d4a2ec58e0b04231100503eb4c6a6ce Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 19 Jan 2024 16:35:49 +0100 Subject: [PATCH 05/34] Fix slots order --- novelwriter/gui/dochighlight.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py index e24a7904..3b4dc684 100644 --- a/novelwriter/gui/dochighlight.py +++ b/novelwriter/gui/dochighlight.py @@ -48,7 +48,7 @@ SPELLRX.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption) class GuiDocHighlighter(QSyntaxHighlighter): - __slots__ = ("_tItem", "_tHandle", "_spellCheck", "_hRules", "_hStyles", "_spellErr") + __slots__ = ("_tItem", "_tHandle", "_spellCheck", "_spellErr", "_hRules", "_hStyles") BLOCK_NONE = 0 BLOCK_TEXT = 1 From fe0098ebbcf00c898894f516e45968e82c0c5c82 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 19 Jan 2024 16:51:27 +0100 Subject: [PATCH 06/34] Update colour parsing tests --- tests/test_gui/test_gui_theme.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index 86a43cce..b7a77dcd 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -89,22 +89,38 @@ def testGuiTheme_Main(qtbot, nwGUI, tstPaths): parser = NWConfigParser() parser["Palette"] = { - "colour1": "100, 150, 200", - "colour2": "100, 150, 200, 250", - "colour3": "250, 250", - "colour4": "-10, 127, 300", + "colour1": "100, 150, 200", # Valid + "colour2": "100, 150, 200, 250", # With alpha + "colour3": "100, 150, 200, 250, 300", # Too many values + "colour4": "250, 250", # Missing blue + "colour5": "-10, 127, 300", # Invalid red and blue + "colour6": "bob, 127, 255", # Invalid red } + # Test the parser for several valid and invalid values + assert mainTheme._parseColour(parser, "Palette", "colour1").getRgb() == (100, 150, 200, 255) + assert mainTheme._parseColour(parser, "Palette", "colour2").getRgb() == (100, 150, 200, 250) + assert mainTheme._parseColour(parser, "Palette", "colour3").getRgb() == (100, 150, 200, 250) + assert mainTheme._parseColour(parser, "Palette", "colour4").getRgb() == (250, 250, 0, 255) + assert mainTheme._parseColour(parser, "Palette", "colour5").getRgb() == (0, 0, 0, 0) + assert mainTheme._parseColour(parser, "Palette", "colour6").getRgb() == (0, 127, 255, 255) + # The palette should load with the parsed values mainTheme._setPalette(parser, "Palette", "colour1", QPalette.Window) assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (100, 150, 200, 255) mainTheme._setPalette(parser, "Palette", "colour2", QPalette.Window) assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (100, 150, 200, 250) mainTheme._setPalette(parser, "Palette", "colour3", QPalette.Window) - assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (250, 250, 0, 255) + assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (100, 150, 200, 250) mainTheme._setPalette(parser, "Palette", "colour4", QPalette.Window) - assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (0, 0, 0, 0) + assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (250, 250, 0, 255) mainTheme._setPalette(parser, "Palette", "colour5", QPalette.Window) + assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (0, 0, 0, 0) + mainTheme._setPalette(parser, "Palette", "colour6", QPalette.Window) + assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (0, 127, 255, 255) + + # Non-existing value should return default colour + mainTheme._setPalette(parser, "Palette", "stuff", QPalette.Window) assert mainTheme._guiPalette.color(QPalette.Window).getRgb() == (0, 0, 0, 255) # qtbot.stop() From 9f2c5379b75fd94707090a21c1fc8a809fb91617 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 22 Jan 2024 23:55:37 +0100 Subject: [PATCH 07/34] Generalise the custom help label widget --- novelwriter/dialogs/docmerge.py | 9 +++---- novelwriter/dialogs/docsplit.py | 6 ++--- novelwriter/dialogs/preferences.py | 18 +++++--------- novelwriter/extensions/configlayout.py | 33 +++++++++++++++++--------- 4 files changed, 36 insertions(+), 30 deletions(-) diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py index 15b54660..a0c43611 100644 --- a/novelwriter/dialogs/docmerge.py +++ b/novelwriter/dialogs/docmerge.py @@ -35,7 +35,7 @@ from PyQt5.QtWidgets import ( from novelwriter import CONFIG, SHARED from novelwriter.extensions.switch import NSwitch -from novelwriter.extensions.configlayout import NHelpLabel +from novelwriter.extensions.configlayout import NColourLabel logger = logging.getLogger(__name__) @@ -54,9 +54,10 @@ class GuiDocMerge(QDialog): self._data = {} self.headLabel = QLabel("{0}".format(self.tr("Documents to Merge"))) - self.helpLabel = NHelpLabel(self.tr( - "Drag and drop items to change the order, or uncheck to exclude." - ), SHARED.theme.helpText) + self.helpLabel = NColourLabel( + self.tr("Drag and drop items to change the order, or uncheck to exclude."), + SHARED.theme.helpText, parent=self, wrap=True + ) iPx = SHARED.theme.baseIconSize hSp = CONFIG.pxInt(12) diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py index 602b860c..b8add4c0 100644 --- a/novelwriter/dialogs/docsplit.py +++ b/novelwriter/dialogs/docsplit.py @@ -35,7 +35,7 @@ from PyQt5.QtWidgets import ( from novelwriter import CONFIG, SHARED from novelwriter.extensions.switch import NSwitch -from novelwriter.extensions.configlayout import NHelpLabel +from novelwriter.extensions.configlayout import NColourLabel logger = logging.getLogger(__name__) @@ -58,9 +58,9 @@ class GuiDocSplit(QDialog): self.setWindowTitle(self.tr("Split Document")) self.headLabel = QLabel("{0}".format(self.tr("Document Headers"))) - self.helpLabel = NHelpLabel( + self.helpLabel = NColourLabel( self.tr("Select the maximum level to split into files."), - SHARED.theme.helpText + SHARED.theme.helpText, parent=self, wrap=True ) # Values diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index 32769451..41663a42 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -26,11 +26,11 @@ from __future__ import annotations import logging -from PyQt5.QtGui import QCloseEvent, QFont, QKeyEvent, QKeySequence, QPalette +from PyQt5.QtGui import QCloseEvent, QFont, QKeyEvent, QKeySequence from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot from PyQt5.QtWidgets import ( QAbstractButton, QComboBox, QCompleter, QDialog, QDialogButtonBox, - QDoubleSpinBox, QFileDialog, QFontDialog, QHBoxLayout, QLabel, QLineEdit, + QDoubleSpinBox, QFileDialog, QFontDialog, QHBoxLayout, QLineEdit, QPushButton, QSpinBox, QToolButton, QVBoxLayout, QWidget, qApp ) @@ -38,7 +38,7 @@ from novelwriter import CONFIG, SHARED from novelwriter.constants import nwConst, nwUnicode from novelwriter.dialogs.quotes import GuiQuoteSelect from novelwriter.extensions.switch import NSwitch -from novelwriter.extensions.configlayout import NScrollableForm +from novelwriter.extensions.configlayout import NColourLabel, NScrollableForm from novelwriter.extensions.pagedsidebar import NPagedSideBar logger = logging.getLogger(__name__) @@ -58,15 +58,9 @@ class GuiPreferences(QDialog): self.resize(*CONFIG.preferencesWinSize) # Title - font = self.font() - font.setPointSizeF(1.25*SHARED.theme.fontPointSize) - - palette = self.palette() - palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.helpText) - - self.titleLabel = QLabel(self.tr("Preferences"), self) - self.titleLabel.setFont(font) - self.titleLabel.setPalette(palette) + self.titleLabel = NColourLabel( + self.tr("Preferences"), SHARED.theme.helpText, parent=self, scale=1.25 + ) self.titleLabel.setIndent(CONFIG.pxInt(4)) # SideBar diff --git a/novelwriter/extensions/configlayout.py b/novelwriter/extensions/configlayout.py index 54ecb3c8..0ba9b259 100644 --- a/novelwriter/extensions/configlayout.py +++ b/novelwriter/extensions/configlayout.py @@ -3,7 +3,7 @@ novelWriter – Custom Widget: Config Layout ========================================== File History: -Created: 2020-05-03 [0.4.5] NConfigLayout, NHelpLabel +Created: 2020-05-03 [0.4.5] NConfigLayout, NColourLabel Created: 2023-05-23 [2.1b1] NSimpleLayout Created: 2024-01-08 [2.3b1] NScrollableForm @@ -40,6 +40,10 @@ LEFT_TOP = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop 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) @@ -48,7 +52,7 @@ class NScrollableForm(QScrollArea): self._first = True self._sections: dict[int, QLabel] = {} - self._editable: dict[str, NHelpLabel] = {} + self._editable: dict[str, NColourLabel] = {} self._index: dict[str, QWidget] = {} self._layout = QVBoxLayout() @@ -130,7 +134,7 @@ class NScrollableForm(QScrollArea): qLabel.setBuddy(widget) if helpText: - qHelp = NHelpLabel(str(helpText), self._helpCol, self._fontScale) + qHelp = NColourLabel(str(helpText), self._helpCol, scale=self._fontScale, wrap=True) qHelp.setIndent(mPx) labelBox = QVBoxLayout() labelBox.addWidget(qLabel) @@ -197,7 +201,7 @@ class NConfigLayout(QGridLayout): """Set the text for the help label.""" if row in self._itemMap: qHelp = self._itemMap[row][1] - if isinstance(qHelp, NHelpLabel): + if isinstance(qHelp, NColourLabel): qHelp.setText(text) return @@ -226,7 +230,7 @@ class NConfigLayout(QGridLayout): qHelp = None if helpText is not None: - qHelp = NHelpLabel(str(helpText), self._helpCol, self._fontScale) + qHelp = NColourLabel(str(helpText), self._helpCol, scale=self._fontScale, wrap=True) qHelp.setIndent(wSp) labelBox = QVBoxLayout() labelBox.addWidget(qLabel) @@ -326,10 +330,16 @@ class NSimpleLayout(QGridLayout): # END Class NSimpleLayout -class NHelpLabel(QLabel): +class NColourLabel(QLabel): + """Extension: A Coloured Label - def __init__(self, text: str, color: QColor, scale: float = FONT_SCALE) -> None: - super().__init__(text) + A custom widget that draws a label in a specific colour, and + optionally at a specific size, and word wrapped. + """ + + def __init__(self, text: str, color: QColor, parent: QWidget | None = None, + scale: float = FONT_SCALE, wrap: bool = False) -> None: + super().__init__(text, parent=parent) lblCol = self.palette() lblCol.setColor(QPalette.WindowText, color) @@ -339,9 +349,10 @@ class NHelpLabel(QLabel): lblFont.setPointSizeF(scale*lblFont.pointSizeF()) self.setFont(lblFont) - self.setWordWrap(True) - self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + if wrap: + self.setWordWrap(True) + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) return -# END Class NHelpLabel +# END Class NColourLabel From b33a3b480d9f103ea4bc231d0c5c8d4bc1900ddf Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 23 Jan 2024 19:54:19 +0100 Subject: [PATCH 08/34] Use short version format for app version --- novelwriter/__init__.py | 2 +- novelwriter/common.py | 5 +++++ novelwriter/tools/welcome.py | 5 +++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index 4ce39cc6..02b28bc7 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -42,7 +42,7 @@ __license__ = "GPLv3" __author__ = "Veronica Berglyd Olsen" __maintainer__ = "Veronica Berglyd Olsen" __email__ = "code@vkbo.net" -__version__ = "2.3-alpha1" +__version__ = "2.3a1" __hexversion__ = "0x020300a1" __date__ = "2023-12-17" __status__ = "Stable" diff --git a/novelwriter/common.py b/novelwriter/common.py index 988b6c78..59b6bbb5 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -250,6 +250,11 @@ def formatTime(t: int) -> str: return "ERROR" +def formatVersion(value: str) -> str: + """Format a version number into a more human readable form.""" + return value.lower().replace("a", " Alpha ").replace("b", " Beta ").replace("rc", " RC ") + + # =============================================================================================== # # String Functions # =============================================================================================== # diff --git a/novelwriter/tools/welcome.py b/novelwriter/tools/welcome.py index ed065192..0329434e 100644 --- a/novelwriter/tools/welcome.py +++ b/novelwriter/tools/welcome.py @@ -43,7 +43,7 @@ from PyQt5.QtWidgets import ( from novelwriter import CONFIG, SHARED, __version__, __date__ from novelwriter.enum import nwItemClass -from novelwriter.common import formatInt, makeFileNameSafe +from novelwriter.common import formatInt, formatVersion, makeFileNameSafe from novelwriter.constants import nwUnicode from novelwriter.core.coretools import ProjectBuilder from novelwriter.extensions.switch import NSwitch @@ -91,7 +91,8 @@ class GuiWelcome(QDialog): self.nwLabel.setPixmap(self.nwImage) self.nwInfo = QLabel(self.tr("Version {0} {1} Released on {2}").format( - __version__, nwUnicode.U_ENDASH, datetime.strptime(__date__, "%Y-%m-%d").strftime("%x") + formatVersion(__version__), nwUnicode.U_ENDASH, + datetime.strptime(__date__, "%Y-%m-%d").strftime("%x") )) self.tabOpen = _OpenProjectPage(self) From 415f1a1d1a4e83ed6dbacc41dc543d3ea662919e Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 23 Jan 2024 19:57:27 +0100 Subject: [PATCH 09/34] Update build scripts to handle new version format --- pkgutils.py | 39 +++++++++++++++++++-------------------- sample/nwProject.nwx | 4 ++-- setup/macos/build.sh | 2 +- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/pkgutils.py b/pkgutils.py index 2b821e1a..1787d516 100755 --- a/pkgutils.py +++ b/pkgutils.py @@ -73,9 +73,16 @@ def extractVersion(beQuiet=False): return numVers, hexVers, relDate -def compactVersion(version): - """Make the version number more compact.""" - return version.replace("-alpha", "a").replace("-beta", "b").replace("-rc", "rc") +def stripVersion(version: str) -> str: + """Strip the pre-release part from a version number.""" + if "a" in version: + return version.partition("a")[0] + elif "b" in version: + return version.partition("b")[0] + elif "rc" in version: + return version.partition("rc")[0] + else: + return version def sysCall(callArgs, cwd=None): @@ -392,16 +399,16 @@ def buildQtI18nTS(sysArgs): ## -# Generage MacOS PList +# Generate MacOS PList ## def genMacOSPlist(): """Set necessary values for .plist file for MacOS build.""" outDir = "setup/macos" - numVers = extractVersion()[0].partition("-")[0] + numVers = stripVersion(extractVersion()[0]) copyrightYear = datetime.datetime.now().year - # These keys are no longer used but are present for compatability + # These keys are no longer used but are present for compatibility pkgVersMaj, pkgVersMin = numVers.split(".")[:2] plistXML = readFile(f"{outDir}/Info.plist.template").format( @@ -598,8 +605,7 @@ def makeMinimalPackage(targetOS): # Build Minimal Zip # ================= - numVers, _, _ = extractVersion() - pkgVers = compactVersion(numVers) + pkgVers, _, _ = extractVersion() zipFile = f"novelwriter-{pkgVers}-minimal{targName}.zip" outFile = os.path.join(bldDir, zipFile) if os.path.isfile(outFile): @@ -691,8 +697,7 @@ def makeDebianPackage(signKey=None, sourceBuild=False, distName="unstable", buil # Version Info # ============ - numVers, hexVers, relDate = extractVersion() - pkgVers = compactVersion(numVers) + pkgVers, hexVers, relDate = extractVersion() relDate = datetime.datetime.strptime(relDate, "%Y-%m-%d") pkgDate = email.utils.format_datetime(relDate.replace(hour=12, tzinfo=None)) print("") @@ -968,8 +973,7 @@ def makeAppImage(sysArgs): # Version Info # ============ - numVers, _, relDate = extractVersion() - pkgVers = compactVersion(numVers) + pkgVers, _, relDate = extractVersion() relDate = datetime.datetime.strptime(relDate, "%Y-%m-%d") print("") @@ -1856,7 +1860,7 @@ if __name__ == "__main__": "", " help Print the help message.", " pip Install all package dependencies for novelWriter using pip.", - " version Print the novelWriter version. Add -c for short version.", + " version Print the novelWriter version.", " build-clean Will attempt to delete 'build' and 'dist' folders.", "", "Additional Builds:", @@ -1912,12 +1916,7 @@ if __name__ == "__main__": if "version" in sys.argv: sys.argv.remove("version") - numVers, _, _ = extractVersion(beQuiet=True) - if "-c" in sys.argv: - sys.argv.remove("-c") - print(compactVersion(numVers), end=None) - else: - print(numVers, end=None) + print(extractVersion(beQuiet=True)[0], end=None) sys.exit(0) if "pip" in sys.argv: @@ -1996,7 +1995,7 @@ if __name__ == "__main__": if "build-appimage" in sys.argv: sys.argv.remove("build-appimage") if hostOS == OS_LINUX: - sys.argv = makeAppImage(sys.argv) # Build appimage and prune its args + sys.argv = makeAppImage(sys.argv) else: print("ERROR: Command 'build-appimage' can only be used on Linux") sys.exit(1) diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 9206abf4..af18a5a6 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,6 +1,6 @@ - - + + Sample Project Sample Project Jane Smith diff --git a/setup/macos/build.sh b/setup/macos/build.sh index dbc9834e..1ffca83f 100755 --- a/setup/macos/build.sh +++ b/setup/macos/build.sh @@ -24,7 +24,7 @@ echo "Build Dir: $BUILD_DIR" pushd "$SRC_DIR" || exit 1 -VERSION="$(python3 pkgutils.py version -c)" +VERSION="$(python3 pkgutils.py version)" echo "novelWriter Version: $VERSION" # --- Prepare Files ----------------------------------------------------------------------------- # From 97384ee4bd9338640b10b9fcf063b876d2844e37 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 23 Jan 2024 20:48:39 +0100 Subject: [PATCH 10/34] Update tests and clean up packaging util --- pkgutils.py | 184 ++++++++++++++-------------- tests/test_base/test_base_common.py | 13 +- 2 files changed, 101 insertions(+), 96 deletions(-) diff --git a/pkgutils.py b/pkgutils.py index 1787d516..2dcfedd1 100755 --- a/pkgutils.py +++ b/pkgutils.py @@ -23,6 +23,7 @@ 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 import os import sys @@ -42,7 +43,7 @@ OS_DARWIN = 3 # Utilities # =============================================================================================== # -def extractVersion(beQuiet=False): +def extractVersion(beQuiet: bool = False) -> tuple[str, str, str]: """Extract the novelWriter version number without having to import anything else from the main package. """ @@ -85,29 +86,19 @@ def stripVersion(version: str) -> str: return version -def sysCall(callArgs, cwd=None): - """Wrapper function for system calls.""" - sysP = subprocess.Popen( - callArgs, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - shell=True, cwd=cwd - ) - stdOut, stdErr = sysP.communicate() - return stdOut.decode("utf-8"), stdErr.decode("utf-8"), sysP.returncode - - -def readFile(fileName): +def readFile(fileName: str) -> str: """Read an entire file and return as a string.""" with open(fileName, mode="r", encoding="utf-8") as inFile: return inFile.read() -def writeFile(fileName, writeText): +def writeFile(fileName: str, writeText: str) -> None: """Write string to file.""" with open(fileName, mode="w+", encoding="utf-8") as outFile: outFile.write(writeText) -def toUpload(srcPath, dstName=None): +def toUpload(srcPath: str, dstName: str | None = None) -> None: """Copy a file produced by one of the build functions to the upload directory. The file can optionally be given a new name. """ @@ -120,7 +111,7 @@ def toUpload(srcPath, dstName=None): return -def makeCheckSum(sumFile, cwd=None): +def makeCheckSum(sumFile: str, cwd: str | None = None) -> str: """Create a SHA256 checksum file.""" try: if cwd is None: @@ -146,7 +137,7 @@ def makeCheckSum(sumFile, cwd=None): # Package Installer (pip) ## -def installPackages(hostOS): +def installPackages(hostOS: int) -> None: """Install package dependencies both for this script and for running novelWriter itself. """ @@ -179,7 +170,7 @@ def installPackages(hostOS): # Clean Build and Dist Folders (build-clean) ## -def cleanBuildDirs(): +def cleanBuildDirs() -> None: """Recursively delete the 'build' and 'dist' folders.""" print("") print("Cleaning up build environment ...") @@ -215,7 +206,7 @@ def cleanBuildDirs(): # Build PDF Manual (manual) ## -def buildPdfManual(): +def buildPdfManual() -> None: """This function will build the documentation as manual.pdf.""" print("") print("Building PDF Manual") @@ -269,7 +260,7 @@ def buildPdfManual(): # Qt Linguist QM Builder (qtlrelease) ## -def buildQtI18n(): +def buildQtI18n() -> None: """Build the lang.qm files for Qt Linguist.""" print("") print("Building Qt Localisation Files") @@ -322,7 +313,7 @@ def buildQtI18n(): # Qt Linguist TS Builder (qtlupdate) ## -def buildQtI18nTS(sysArgs): +def buildQtI18nTS(sysArgs: list[str]) -> None: """Build the lang.ts files for Qt Linguist.""" print("") print("Building Qt Translation Files") @@ -402,7 +393,7 @@ def buildQtI18nTS(sysArgs): # Generate MacOS PList ## -def genMacOSPlist(): +def genMacOSPlist() -> None: """Set necessary values for .plist file for MacOS build.""" outDir = "setup/macos" numVers = stripVersion(extractVersion()[0]) @@ -429,7 +420,7 @@ def genMacOSPlist(): # Sample Project ZIP File Builder (sample) ## -def buildSampleZip(): +def buildSampleZip() -> None: """Bundle the sample project into a single zip file to be saved into the novelwriter/assets folder for further bundling into builds. """ @@ -466,7 +457,7 @@ def buildSampleZip(): return -def cleanBuiltAssets(): +def cleanBuiltAssets() -> None: """Remove assets built by this script.""" print("") print("Removing Built Assets") @@ -495,7 +486,7 @@ def cleanBuiltAssets(): return -def checkAssetsExist(): +def checkAssetsExist() -> bool: """Check that the necessary compiled assets exist ahead of a build. """ hasSample = False @@ -530,7 +521,7 @@ def checkAssetsExist(): # Import Translations (import-i18n) ## -def importI18nUpdates(sysArgs): +def importI18nUpdates(sysArgs: list[str]) -> None: """Import new translation files from a zip file.""" print("") print("Import Updated Translations") @@ -570,7 +561,7 @@ def importI18nUpdates(sysArgs): # Make Minimal Package (minimal-zip) ## -def makeMinimalPackage(targetOS): +def makeMinimalPackage(targetOS: int) -> None: """Pack the core source file in a single zip file.""" from zipfile import ZipFile, ZIP_DEFLATED @@ -685,7 +676,8 @@ def makeMinimalPackage(targetOS): # Make Debian Package (build-deb) ## -def makeDebianPackage(signKey=None, sourceBuild=False, distName="unstable", buildName=""): +def makeDebianPackage(signKey: str | None = None, sourceBuild: bool = False, + distName: str = "unstable", buildName: str = "") -> str: """Build a Debian package.""" print("") print("Build Debian Package") @@ -858,14 +850,14 @@ def makeDebianPackage(signKey=None, sourceBuild=False, distName="unstable", buil # Make Launchpad Package (build-ubuntu) ## -def makeForLaunchpad(doSign=False, isFirst=False, isSnapshot=False): +def makeForLaunchpad(sign: bool = False, first: bool = False, snapshot: bool = False) -> None: """Wrapper for building Debian packages for Launchpad.""" print("") print("Launchpad Packages") print("==================") print("") - if isFirst or isSnapshot: + if first or snapshot: bldNum = "0" else: bldNum = input("Build number [0]: ") @@ -880,7 +872,7 @@ def makeForLaunchpad(doSign=False, isFirst=False, isSnapshot=False): ] tStamp = datetime.datetime.now().strftime("%Y%m%d~%H%M%S") - if isSnapshot: + if snapshot: print(f"Building Ununtu SNAPSHOT~{tStamp} for:") print("") else: @@ -890,7 +882,7 @@ def makeForLaunchpad(doSign=False, isFirst=False, isSnapshot=False): print(f" * Ubuntu {distNum} {codeName.title()}") print("") - if doSign: + if sign: signKey = "D6A9F6B8F227CF7C6F6D1EE84DBBE4B734B0BD08" else: signKey = None @@ -900,7 +892,7 @@ def makeForLaunchpad(doSign=False, isFirst=False, isSnapshot=False): dputCmd = [] for distNum, codeName in distLoop: - if isSnapshot: + if snapshot: buildName = f"+SNAPSHOT~{tStamp}~ubuntu{distNum}.0" else: buildName = f"~ubuntu{distNum}.{bldNum}" @@ -927,7 +919,7 @@ def makeForLaunchpad(doSign=False, isFirst=False, isSnapshot=False): # Make AppImage (build-appimage) ## -def makeAppImage(sysArgs): +def makeAppImage(sysArgs: list[str]) -> list[str]: """Build an AppImage.""" import glob import argparse @@ -1143,7 +1135,7 @@ def makeAppImage(sysArgs): # Make Windows Setup EXE (build-win-exe) ## -def makeWindowsEmbedded(sysArgs): +def makeWindowsEmbedded(sysArgs: list[str]) -> None: """Set up a package with embedded Python and dependencies for Windows installation. """ @@ -1375,7 +1367,7 @@ def makeWindowsEmbedded(sysArgs): # XDG Installation (xdg-install) ## -def xdgInstall(): +def xdgInstall() -> None: """Will attempt to install icons and make a launcher.""" print("") print("XDG Install") @@ -1511,7 +1503,7 @@ def xdgInstall(): # XDG Uninstallation (xdg-uninstall) ## -def xdgUninstall(): +def xdgUninstall() -> None: """Will attempt to uninstall icons and the launcher.""" print("") print("XDG Uninstall") @@ -1581,7 +1573,7 @@ def xdgUninstall(): # WIN Installation (win-install) ## -def winInstall(): +def winInstall() -> None: """Will attempt to install icons and make a launcher for Windows.""" import winreg try: @@ -1708,7 +1700,7 @@ def winInstall(): # WIN Uninstallation (win-uninstall) ## -def winUninstall(): +def winUninstall() -> None: """Will attempt to uninstall icons previously installed.""" import winreg try: @@ -1809,36 +1801,38 @@ if __name__ == "__main__": else: hostOS = OS_NONE + sysArgs = sys.argv.copy() + # Set Target OS - if "--target-linux" in sys.argv: - sys.argv.remove("--target-linux") + if "--target-linux" in sysArgs: + sysArgs.remove("--target-linux") targetOS = OS_LINUX - elif "--target-darwin" in sys.argv: - sys.argv.remove("--target-darwin") + elif "--target-darwin" in sysArgs: + sysArgs.remove("--target-darwin") targetOS = OS_DARWIN - elif "--target-win" in sys.argv: - sys.argv.remove("--target-win") + elif "--target-win" in sysArgs: + sysArgs.remove("--target-win") targetOS = OS_WIN else: targetOS = hostOS # Sign package - if "--sign" in sys.argv: - sys.argv.remove("--sign") + if "--sign" in sysArgs: + sysArgs.remove("--sign") doSign = True else: doSign = False # First build - if "--first" in sys.argv: - sys.argv.remove("--first") + if "--first" in sysArgs: + sysArgs.remove("--first") isFirstBuild = True else: isFirstBuild = False # Build snapshot - if "--snapshot" in sys.argv: - sys.argv.remove("--snapshot") + if "--snapshot" in sysArgs: + sysArgs.remove("--snapshot") isSnapshot = True else: isSnapshot = False @@ -1909,66 +1903,66 @@ if __name__ == "__main__": # General # ======= - if "help" in sys.argv: - sys.argv.remove("help") + if "help" in sysArgs: + sysArgs.remove("help") print("\n".join(helpMsg)) sys.exit(0) - if "version" in sys.argv: - sys.argv.remove("version") + if "version" in sysArgs: + sysArgs.remove("version") print(extractVersion(beQuiet=True)[0], end=None) sys.exit(0) - if "pip" in sys.argv: - sys.argv.remove("pip") + if "pip" in sysArgs: + sysArgs.remove("pip") installPackages(hostOS) - if "build-clean" in sys.argv: - sys.argv.remove("build-clean") + if "build-clean" in sysArgs: + sysArgs.remove("build-clean") cleanBuildDirs() # Additional Builds # ================= - if "manual" in sys.argv: - sys.argv.remove("manual") + if "manual" in sysArgs: + sysArgs.remove("manual") buildPdfManual() - if "qtlrelease" in sys.argv: - sys.argv.remove("qtlrelease") + if "qtlrelease" in sysArgs: + sysArgs.remove("qtlrelease") buildQtI18n() - if "qtlupdate" in sys.argv: - sys.argv.remove("qtlupdate") - buildQtI18nTS(sys.argv) + if "qtlupdate" in sysArgs: + sysArgs.remove("qtlupdate") + buildQtI18nTS(sysArgs) sys.exit(0) # Don't continue execution - if "sample" in sys.argv: - sys.argv.remove("sample") + if "sample" in sysArgs: + sysArgs.remove("sample") buildSampleZip() - if "clean-assets" in sys.argv: - sys.argv.remove("clean-assets") + if "clean-assets" in sysArgs: + sysArgs.remove("clean-assets") cleanBuiltAssets() - if "gen-plist" in sys.argv: - sys.argv.remove("gen-plist") + if "gen-plist" in sysArgs: + sysArgs.remove("gen-plist") genMacOSPlist() # Python Packaging # ================ - if "import-i18n" in sys.argv: - sys.argv.remove("import-i18n") - importI18nUpdates(sys.argv) + if "import-i18n" in sysArgs: + sysArgs.remove("import-i18n") + importI18nUpdates(sysArgs) sys.exit(0) # Don't continue execution - if "minimal-zip" in sys.argv: - sys.argv.remove("minimal-zip") + if "minimal-zip" in sysArgs: + sysArgs.remove("minimal-zip") makeMinimalPackage(targetOS) - if "build-deb" in sys.argv: - sys.argv.remove("build-deb") + if "build-deb" in sysArgs: + sysArgs.remove("build-deb") if hostOS == OS_LINUX: if doSign: signKey = "D6A9F6B8F227CF7C6F6D1EE84DBBE4B734B0BD08" @@ -1979,23 +1973,23 @@ if __name__ == "__main__": print("ERROR: Command 'build-deb' can only be used on Linux") sys.exit(1) - if "build-ubuntu" in sys.argv: - sys.argv.remove("build-ubuntu") + if "build-ubuntu" in sysArgs: + sysArgs.remove("build-ubuntu") if hostOS == OS_LINUX: - makeForLaunchpad(doSign=doSign, isFirst=isFirstBuild, isSnapshot=isSnapshot) + makeForLaunchpad(sign=doSign, first=isFirstBuild, snapshot=isSnapshot) else: print("ERROR: Command 'build-ubuntu' can only be used on Linux") sys.exit(1) - if "build-win-exe" in sys.argv: - sys.argv.remove("build-win-exe") - makeWindowsEmbedded(sys.argv) + if "build-win-exe" in sysArgs: + sysArgs.remove("build-win-exe") + makeWindowsEmbedded(sysArgs) sys.exit(0) # Don't continue execution - if "build-appimage" in sys.argv: - sys.argv.remove("build-appimage") + if "build-appimage" in sysArgs: + sysArgs.remove("build-appimage") if hostOS == OS_LINUX: - sys.argv = makeAppImage(sys.argv) + sysArgs = makeAppImage(sysArgs) else: print("ERROR: Command 'build-appimage' can only be used on Linux") sys.exit(1) @@ -2003,32 +1997,32 @@ if __name__ == "__main__": # General Installers # ================== - if "xdg-install" in sys.argv: - sys.argv.remove("xdg-install") + if "xdg-install" in sysArgs: + sysArgs.remove("xdg-install") if hostOS == OS_WIN: print("ERROR: Command 'xdg-install' cannot be used on Windows") sys.exit(1) else: xdgInstall() - if "xdg-uninstall" in sys.argv: - sys.argv.remove("xdg-uninstall") + if "xdg-uninstall" in sysArgs: + sysArgs.remove("xdg-uninstall") if hostOS == OS_WIN: print("ERROR: Command 'xdg-uninstall' cannot be used on Windows") sys.exit(1) else: xdgUninstall() - if "win-install" in sys.argv: - sys.argv.remove("win-install") + if "win-install" in sysArgs: + sysArgs.remove("win-install") if hostOS == OS_WIN: winInstall() else: print("ERROR: Command 'win-install' can only be used on Windows") sys.exit(1) - if "win-uninstall" in sys.argv: - sys.argv.remove("win-uninstall") + if "win-uninstall" in sysArgs: + sysArgs.remove("win-uninstall") if hostOS == OS_WIN: winUninstall() else: diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py index 0a475c19..a45571cf 100644 --- a/tests/test_base/test_base_common.py +++ b/tests/test_base/test_base_common.py @@ -35,7 +35,7 @@ from PyQt5.QtCore import QUrl from novelwriter.common import ( checkBool, checkFloat, checkHandle, checkInt, checkIntTuple, checkPath, checkString, checkStringNone, checkUuid, formatInt, formatTime, - formatTimeStamp, fuzzyTime, getFileSize, hexToInt, isHandle, isItemClass, + formatTimeStamp, formatVersion, fuzzyTime, getFileSize, hexToInt, isHandle, isItemClass, isItemLayout, isItemType, isTitleTag, jsonEncode, makeFileNameSafe, minmax, numberToRoman, NWConfigParser, openExternalPath, readTextFile, simplified, transferCase, xmlIndent, yesNo @@ -348,6 +348,17 @@ def testBaseCommon_formatTime(): # END Test testBaseCommon_formatTime +@pytest.mark.base +def testBaseCommon_formatVersion(): + """Test the formatVersion function.""" + assert formatVersion("1.2") == "1.2" + assert formatVersion("1.2a1") == "1.2 Alpha 1" + assert formatVersion("1.2b2") == "1.2 Beta 2" + assert formatVersion("1.2rc3") == "1.2 RC 3" + +# END Test testBaseCommon_formatVersion + + @pytest.mark.base def testBaseCommon_simplified(): """Test the simplified function.""" From bd45619e2ff29cc4c47c9fd60d6d3d6d3ea813d0 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 23 Jan 2024 21:49:28 +0100 Subject: [PATCH 11/34] Modify Debian and Ubuntu packaging version format, and remove snapshots --- novelwriter/__init__.py | 2 +- pkgutils.py | 43 +++++++++++------------------------------ setup/make_snapshot.sh | 28 --------------------------- 3 files changed, 12 insertions(+), 61 deletions(-) delete mode 100755 setup/make_snapshot.sh diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index 02b28bc7..5b3fe49c 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -44,7 +44,7 @@ __maintainer__ = "Veronica Berglyd Olsen" __email__ = "code@vkbo.net" __version__ = "2.3a1" __hexversion__ = "0x020300a1" -__date__ = "2023-12-17" +__date__ = "2024-01-23" __status__ = "Stable" __domain__ = "novelwriter.io" diff --git a/pkgutils.py b/pkgutils.py index 2dcfedd1..0cf5fde3 100755 --- a/pkgutils.py +++ b/pkgutils.py @@ -689,13 +689,13 @@ def makeDebianPackage(signKey: str | None = None, sourceBuild: bool = False, # Version Info # ============ - pkgVers, hexVers, relDate = extractVersion() + numVers, hexVers, relDate = extractVersion() relDate = datetime.datetime.strptime(relDate, "%Y-%m-%d") pkgDate = email.utils.format_datetime(relDate.replace(hour=12, tzinfo=None)) print("") - if buildName: - pkgVers = f"{pkgVers}{buildName}" + pkgVers = numVers.replace("a", "~a").replace("b", "~b").replace("rc", "~rc") + pkgVers = f"{pkgVers}+{buildName}" if buildName else pkgVers # Set Up Folder # ============= @@ -836,11 +836,7 @@ def makeDebianPackage(signKey: str | None = None, sourceBuild: bool = False, print("") if sourceBuild: - if hexVers[-2] == "f": - ppaName = "novelwriter" - else: - ppaName = "novelwriter-pre" - + ppaName = "novelwriter" if hexVers[-2] == "f" else "novelwriter-pre" return f"dput {ppaName}/{distName} {bldDir}/{bldPkg}_source.changes" return "" @@ -850,14 +846,14 @@ def makeDebianPackage(signKey: str | None = None, sourceBuild: bool = False, # Make Launchpad Package (build-ubuntu) ## -def makeForLaunchpad(sign: bool = False, first: bool = False, snapshot: bool = False) -> None: +def makeForLaunchpad(doSign: bool = False, isFirst: bool = False) -> None: """Wrapper for building Debian packages for Launchpad.""" print("") print("Launchpad Packages") print("==================") print("") - if first or snapshot: + if isFirst: bldNum = "0" else: bldNum = input("Build number [0]: ") @@ -871,18 +867,13 @@ def makeForLaunchpad(sign: bool = False, first: bool = False, snapshot: bool = F ("23.10", "mantic"), ] - tStamp = datetime.datetime.now().strftime("%Y%m%d~%H%M%S") - if snapshot: - print(f"Building Ununtu SNAPSHOT~{tStamp} for:") - print("") - else: - print("Building Ubuntu packages for:") - print("") + print("Building Ubuntu packages for:") + print("") for distNum, codeName in distLoop: print(f" * Ubuntu {distNum} {codeName.title()}") print("") - if sign: + if doSign: signKey = "D6A9F6B8F227CF7C6F6D1EE84DBBE4B734B0BD08" else: signKey = None @@ -892,11 +883,7 @@ def makeForLaunchpad(sign: bool = False, first: bool = False, snapshot: bool = F dputCmd = [] for distNum, codeName in distLoop: - if snapshot: - buildName = f"+SNAPSHOT~{tStamp}~ubuntu{distNum}.0" - else: - buildName = f"~ubuntu{distNum}.{bldNum}" - + buildName = f"ubuntu{distNum}.{bldNum}" dCmd = makeDebianPackage( signKey=signKey, sourceBuild=True, @@ -1830,13 +1817,6 @@ if __name__ == "__main__": else: isFirstBuild = False - # Build snapshot - if "--snapshot" in sysArgs: - sysArgs.remove("--snapshot") - isSnapshot = True - else: - isSnapshot = False - helpMsg = [ "", "novelWriter Setup Tool", @@ -1878,7 +1858,6 @@ if __name__ == "__main__": " sign package.", " build-ubuntu Build a .deb packages Launchpad. Add --sign to ", " sign package. Add --first to set build number to 0.", - " Add --snapshot to make a snapshot package.", " build-win-exe Build a setup.exe file with Python embedded for Windows.", " The package must be built from a minimal windows zip file.", " build-appimage Build an AppImage. Argument --linux-tag defaults to", @@ -1976,7 +1955,7 @@ if __name__ == "__main__": if "build-ubuntu" in sysArgs: sysArgs.remove("build-ubuntu") if hostOS == OS_LINUX: - makeForLaunchpad(sign=doSign, first=isFirstBuild, snapshot=isSnapshot) + makeForLaunchpad(doSign=doSign, isFirst=isFirstBuild) else: print("ERROR: Command 'build-ubuntu' can only be used on Linux") sys.exit(1) diff --git a/setup/make_snapshot.sh b/setup/make_snapshot.sh deleted file mode 100755 index 5b92350c..00000000 --- a/setup/make_snapshot.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash -set -e - -ENVPATH=/tmp/nwBuild - -if [ ! -f pkgutils.py ]; then - echo "Must be called from the root folder of the source" - exit 1 -fi - -echo "" -echo " Building Dependencies" -echo "================================================================================" -echo "" -if [ ! -d $ENVPATH ]; then - python3 -m venv $ENVPATH -fi -source $ENVPATH/bin/activate -pip3 install -r docs/source/requirements.txt -python3 pkgutils.py clean-assets -python3 pkgutils.py qtlrelease manual sample -deactivate - -echo "" -echo " Building Linux Snapshots" -echo "================================================================================" -echo "" -python3 pkgutils.py build-ubuntu --sign --snapshot From cc4d92f0f73a35893d4ea2b24e75e32d168bf363 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 24 Jan 2024 17:05:14 +0100 Subject: [PATCH 12/34] Improve novel selector widget and add to novel details dialog --- novelwriter/dialogs/preferences.py | 18 ++-- novelwriter/dialogs/projdetails.py | 2 +- novelwriter/extensions/novelselector.py | 28 +++++- novelwriter/gui/mainmenu.py | 2 +- novelwriter/gui/noveltree.py | 7 +- novelwriter/gui/outline.py | 5 +- novelwriter/guimain.py | 8 +- novelwriter/tools/noveldetails.py | 122 ++++++++++++++++++++++++ novelwriter/tools/novelinfo.py | 71 -------------- 9 files changed, 167 insertions(+), 96 deletions(-) create mode 100644 novelwriter/tools/noveldetails.py delete mode 100644 novelwriter/tools/novelinfo.py diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index 41663a42..5ece68b1 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -63,11 +63,6 @@ class GuiPreferences(QDialog): ) self.titleLabel.setIndent(CONFIG.pxInt(4)) - # SideBar - self.sidebar = NPagedSideBar(self) - self.sidebar.setLabelColor(SHARED.theme.helpText) - self.sidebar.buttonClicked.connect(self._sidebarClicked) - # Search Box self.searchText = QLineEdit(self) self.searchText.setPlaceholderText(self.tr("Search")) @@ -77,10 +72,10 @@ class GuiPreferences(QDialog): ) self.searchAction.triggered.connect(self._gotoSearch) - self.searchBox = QHBoxLayout() - self.searchBox.addWidget(self.titleLabel) - self.searchBox.addStretch(1) - self.searchBox.addWidget(self.searchText, 1) + # SideBar + self.sidebar = NPagedSideBar(self) + self.sidebar.setLabelColor(SHARED.theme.helpText) + self.sidebar.buttonClicked.connect(self._sidebarClicked) # Form self.mainForm = NScrollableForm(self) @@ -95,6 +90,11 @@ class GuiPreferences(QDialog): self.buttonBox.clicked.connect(self._dialogButtonClicked) # Assemble + self.searchBox = QHBoxLayout() + self.searchBox.addWidget(self.titleLabel) + self.searchBox.addStretch(1) + self.searchBox.addWidget(self.searchText, 1) + self.mainBox = QHBoxLayout() self.mainBox.addWidget(self.sidebar) self.mainBox.addWidget(self.mainForm) diff --git a/novelwriter/dialogs/projdetails.py b/novelwriter/dialogs/projdetails.py index a6e12163..31ffea62 100644 --- a/novelwriter/dialogs/projdetails.py +++ b/novelwriter/dialogs/projdetails.py @@ -419,7 +419,7 @@ class GuiProjectDetailsContents(QWidget): def updateValues(self) -> None: """Populate the tree.""" self._currentRoot = None - self.novelValue.updateList() + self.novelValue.refreshNovelList() self.novelValue.setHandle(self.novelValue.firstHandle) self._prepareData(self.novelValue.firstHandle) self._populateTree() diff --git a/novelwriter/extensions/novelselector.py b/novelwriter/extensions/novelselector.py index 035ab570..793a475d 100644 --- a/novelwriter/extensions/novelselector.py +++ b/novelwriter/extensions/novelselector.py @@ -3,7 +3,7 @@ novelWriter – Custom Widget: Novel Selector =========================================== File History: -Created: 2022-11-17 [2.0] +Created: 2022-11-17 [2.0] NovelSelector This file is a part of novelWriter Copyright 2018–2024, Veronica Berglyd Olsen @@ -43,6 +43,8 @@ class NovelSelector(QComboBox): super().__init__(parent=parent) self._blockSignal = False self._firstHandle = None + self._includeAll = False + self._listFormat = None self.currentIndexChanged.connect(self._indexChanged) return @@ -74,7 +76,23 @@ class NovelSelector(QComboBox): self._blockSignal = False return - def updateList(self, includeAll: bool = False, prefix: str | None = None) -> None: + def setIncludeAll(self, value: bool) -> None: + """Set flag to add an "All Novel Folders" option.""" + self._includeAll = value + return + + def setListFormat(self, value: str | None) -> None: + """Set a format string for the list entries.""" + if value is None or "{0}" in value: + self._listFormat = value + return + + ## + # Public Slots + ## + + @pyqtSlot() + def refreshNovelList(self) -> None: """Rebuild the list of novel items.""" self._blockSignal = True self._firstHandle = None @@ -83,8 +101,8 @@ class NovelSelector(QComboBox): icon = SHARED.theme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL]) handle = self.currentData() for tHandle, nwItem in SHARED.project.tree.iterRoots(nwItemClass.NOVEL): - if prefix: - name = prefix.format(nwItem.itemName) + if self._listFormat: + name = self._listFormat.format(nwItem.itemName) self.addItem(name, tHandle) else: name = nwItem.itemName @@ -92,7 +110,7 @@ class NovelSelector(QComboBox): if self._firstHandle is None: self._firstHandle = tHandle - if includeAll: + if self._includeAll: self.insertSeparator(self.count()) self.addItem(icon, self.tr("All Novel Folders"), "") diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 94cd1d20..4ad1ab09 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -162,7 +162,7 @@ class GuiMainMenu(QMenuBar): # Project > Novel Info self.aNovelInfo = self.projMenu.addAction(self.tr("Novel Info")) self.aNovelInfo.setShortcut("Shift+F6") - self.aNovelInfo.triggered.connect(self.mainGui.showNovelInfoDialog) + self.aNovelInfo.triggered.connect(self.mainGui.showNovelDetailsDialog) # Project > Separator self.projMenu.addSeparator() diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 1569262b..959ec7ae 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -207,9 +207,10 @@ class GuiNovelToolBar(QWidget): # Novel Selector selFont = self.font() selFont.setWeight(QFont.Weight.Bold) - self.novelPrefix = self.tr("Outline of {0}") + self.novelValue = NovelSelector(self) self.novelValue.setFont(selFont) + self.novelValue.setListFormat(self.tr("Outline of {0}")) self.novelValue.setMinimumWidth(CONFIG.pxInt(150)) self.novelValue.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) self.novelValue.novelSelectionChanged.connect(self.setCurrentRoot) @@ -294,7 +295,7 @@ class GuiNovelToolBar(QWidget): "QComboBox {border-style: none; padding-left: 0;} " "QComboBox::drop-down {border-style: none}" ) - self.novelValue.updateList(prefix=self.novelPrefix) + self.novelValue.refreshNovelList() self.tbNovel.setVisible(self.novelValue.count() > 1) return @@ -307,7 +308,7 @@ class GuiNovelToolBar(QWidget): def buildNovelRootMenu(self) -> None: """Build the novel root menu.""" - self.novelValue.updateList(prefix=self.novelPrefix) + self.novelValue.refreshNovelList() self.tbNovel.setVisible(self.novelValue.count() > 1) return diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index db066cee..cd441bae 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -220,6 +220,7 @@ class GuiOutlineToolBar(QToolBar): self.novelLabel.setContentsMargins(0, 0, mPx, 0) self.novelValue = NovelSelector(self) + self.novelValue.setIncludeAll(True) self.novelValue.setMinimumWidth(CONFIG.pxInt(200)) self.novelValue.novelSelectionChanged.connect(self._novelValueChanged) @@ -258,7 +259,7 @@ class GuiOutlineToolBar(QToolBar): def updateTheme(self) -> None: """Update theme elements.""" self.setStyleSheet("QToolBar {border: 0px;}") - self.novelValue.updateList(includeAll=True) + self.novelValue.refreshNovelList() self.aRefresh.setIcon(SHARED.theme.getIcon("refresh")) self.tbColumns.setIcon(SHARED.theme.getIcon("menu")) self.tbColumns.setStyleSheet("QToolButton::menu-indicator {image: none;}") @@ -266,7 +267,7 @@ class GuiOutlineToolBar(QToolBar): def populateNovelList(self) -> None: """Reload the content of the novel list.""" - self.novelValue.updateList(includeAll=True) + self.novelValue.refreshNovelList() return def setCurrentRoot(self, rootHandle: str | None) -> None: diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index fea65021..8f4c3e4d 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -56,9 +56,9 @@ from novelwriter.dialogs.preferences import GuiPreferences from novelwriter.dialogs.projdetails import GuiProjectDetails from novelwriter.dialogs.projsettings import GuiProjectSettings from novelwriter.tools.welcome import GuiWelcome -from novelwriter.tools.novelinfo import GuiNovelInfo from novelwriter.tools.manuscript import GuiManuscript from novelwriter.tools.dictionaries import GuiDictionaries +from novelwriter.tools.noveldetails import GuiNovelDetails from novelwriter.tools.writingstats import GuiWritingStats from novelwriter.enum import ( @@ -838,10 +838,10 @@ class GuiMain(QMainWindow): return @pyqtSlot() - def showNovelInfoDialog(self) -> None: - """Open the novel info dialog.""" + def showNovelDetailsDialog(self) -> None: + """Open the novel details dialog.""" if SHARED.hasProject: - dialog = GuiNovelInfo(self) + dialog = GuiNovelDetails(self) dialog.setModal(True) dialog.show() dialog.raise_() diff --git a/novelwriter/tools/noveldetails.py b/novelwriter/tools/noveldetails.py new file mode 100644 index 00000000..66eec029 --- /dev/null +++ b/novelwriter/tools/noveldetails.py @@ -0,0 +1,122 @@ +""" +novelWriter – GUI Novel Info +============================ + +File History: +Created: 2024-01-18 [2.3b1] GuiNovelDetails + +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 + +import logging + +from PyQt5.QtGui import QCloseEvent +from PyQt5.QtWidgets import ( + QDialog, QDialogButtonBox, QHBoxLayout, QStackedWidget, QVBoxLayout, + QWidget +) + +from novelwriter import CONFIG, SHARED +from novelwriter.extensions.configlayout import NColourLabel +from novelwriter.extensions.novelselector import NovelSelector +from novelwriter.extensions.pagedsidebar import NPagedSideBar + +logger = logging.getLogger(__name__) + + +class GuiNovelDetails(QDialog): + + def __init__(self, parent: QWidget) -> None: + super().__init__(parent=parent) + + logger.debug("Create: GuiNovelDetails") + self.setObjectName("GuiNovelDetails") + self.setWindowTitle(self.tr("Novel Details")) + self.setMinimumSize(CONFIG.pxInt(600), CONFIG.pxInt(500)) + + # Title + self.titleLabel = NColourLabel( + self.tr("Novel Details"), SHARED.theme.helpText, parent=self, scale=1.25 + ) + self.titleLabel.setIndent(CONFIG.pxInt(4)) + + # Novel Selector + self.novelSelector = NovelSelector(self) + self.novelSelector.refreshNovelList() + + # SideBar + self.sidebar = NPagedSideBar(self) + self.sidebar.setLabelColor(SHARED.theme.helpText) + # self.sidebar.buttonClicked.connect(self._sidebarClicked) + + # Content + self.mainStack = QStackedWidget(self) + + # Buttons + self.buttonBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) + self.buttonBox.rejected.connect(self.close) + + # Assemble + self.topBox = QHBoxLayout() + self.topBox.addWidget(self.titleLabel) + self.topBox.addStretch(1) + self.topBox.addWidget(self.novelSelector, 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: GuiNovelDetails") + + return + + def __del__(self) -> None: # pragma: no cover + logger.debug("Delete: GuiNovelDetails") + return + + ## + # Events + ## + + def closeEvent(self, event: QCloseEvent) -> None: + """Capture the user closing the window and save settings.""" + self._saveSettings() + event.accept() + self.deleteLater() + return + + ## + # Internal Functions + ## + + def _saveSettings(self) -> None: + """Save the user GUI settings.""" + logger.debug("Saving State: GuiNovelDetails") + return + +# END Class GuiNovelDetails diff --git a/novelwriter/tools/novelinfo.py b/novelwriter/tools/novelinfo.py deleted file mode 100644 index 611c80b3..00000000 --- a/novelwriter/tools/novelinfo.py +++ /dev/null @@ -1,71 +0,0 @@ -""" -novelWriter – GUI Novel Info -============================ - -File History: -Created: 2024-01-18 [2.3b1] GuiNovelInfo - -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 - -import logging - -from PyQt5.QtGui import QCloseEvent -from PyQt5.QtWidgets import QDialog, QWidget - -logger = logging.getLogger(__name__) - - -class GuiNovelInfo(QDialog): - - def __init__(self, parent: QWidget) -> None: - super().__init__(parent=parent) - - logger.debug("Create: GuiNovelInfo") - self.setObjectName("GuiNovelInfo") - - self.setWindowTitle(self.tr("Novel Info")) - logger.debug("Ready: GuiNovelInfo") - - return - - def __del__(self) -> None: # pragma: no cover - logger.debug("Delete: GuiNovelInfo") - return - - ## - # Events - ## - - def closeEvent(self, event: QCloseEvent) -> None: - """Capture the user closing the window and save settings.""" - self._saveSettings() - event.accept() - self.deleteLater() - return - - ## - # Internal Functions - ## - - def _saveSettings(self) -> None: - """Save the user GUI settings.""" - logger.debug("Saving State: GuiNovelInfo") - return - -# END Class GuiNovelInfo From 6622a7e8e02232d4ebbd93941f2f11eafd73d4eb Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 24 Jan 2024 19:26:48 +0100 Subject: [PATCH 13/34] Begin to add content to the novel details overview page --- novelwriter/core/options.py | 5 +- novelwriter/guimain.py | 2 +- novelwriter/tools/noveldetails.py | 126 +++++++++++++++++++++++++++++- 3 files changed, 128 insertions(+), 5 deletions(-) diff --git a/novelwriter/core/options.py b/novelwriter/core/options.py index 8e50c30e..93ec4d8c 100644 --- a/novelwriter/core/options.py +++ b/novelwriter/core/options.py @@ -71,7 +71,10 @@ VALID_MAP: dict[str, set[str]] = { }, "GuiDocViewerPanel": { "colWidths", "hideInactive", - } + }, + "GuiNovelDetails": { + "winWidth", "winHeight", + }, } diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 8f4c3e4d..143d9c65 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -846,7 +846,7 @@ class GuiMain(QMainWindow): dialog.show() dialog.raise_() qApp.processEvents() - # dialog.updateValues() + dialog.updateValues() return @pyqtSlot() diff --git a/novelwriter/tools/noveldetails.py b/novelwriter/tools/noveldetails.py index 66eec029..fdced6b9 100644 --- a/novelwriter/tools/noveldetails.py +++ b/novelwriter/tools/noveldetails.py @@ -24,14 +24,16 @@ along with this program. If not, see . from __future__ import annotations import logging +from PyQt5.QtCore import pyqtSlot from PyQt5.QtGui import QCloseEvent from PyQt5.QtWidgets import ( - QDialog, QDialogButtonBox, QHBoxLayout, QStackedWidget, QVBoxLayout, + QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout, QLabel, QStackedWidget, QVBoxLayout, QWidget ) from novelwriter import CONFIG, SHARED +from novelwriter.common import formatTime from novelwriter.extensions.configlayout import NColourLabel from novelwriter.extensions.novelselector import NovelSelector from novelwriter.extensions.pagedsidebar import NPagedSideBar @@ -41,13 +43,25 @@ logger = logging.getLogger(__name__) class GuiNovelDetails(QDialog): + PAGE_OVERVIEW = 1 + PAGE_CONTENTS = 2 + def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) logger.debug("Create: GuiNovelDetails") self.setObjectName("GuiNovelDetails") self.setWindowTitle(self.tr("Novel Details")) - self.setMinimumSize(CONFIG.pxInt(600), CONFIG.pxInt(500)) + + wW = CONFIG.pxInt(500) + wH = CONFIG.pxInt(400) + options = SHARED.project.options + + self.setMinimumSize(wW, wH) + self.resize( + CONFIG.pxInt(options.getInt("GuiNovelDetails", "winWidth", wW)), + CONFIG.pxInt(options.getInt("GuiNovelDetails", "winHeight", wH)) + ) # Title self.titleLabel = NColourLabel( @@ -62,10 +76,16 @@ class GuiNovelDetails(QDialog): # SideBar self.sidebar = NPagedSideBar(self) self.sidebar.setLabelColor(SHARED.theme.helpText) - # self.sidebar.buttonClicked.connect(self._sidebarClicked) + self.sidebar.addButton(self.tr("Overview"), self.PAGE_OVERVIEW) + self.sidebar.addButton(self.tr("Contents"), self.PAGE_CONTENTS) + self.sidebar.setSelected(self.PAGE_OVERVIEW) + self.sidebar.buttonClicked.connect(self._sidebarClicked) # Content + self.overviewPage = _OverviewPage(self) + self.mainStack = QStackedWidget(self) + self.mainStack.addWidget(self.overviewPage) # Buttons self.buttonBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) @@ -99,6 +119,15 @@ class GuiNovelDetails(QDialog): logger.debug("Delete: GuiNovelDetails") return + ## + # Methods + ## + + def updateValues(self) -> None: + """Load the dialogs initial values.""" + self.overviewPage.updateProjectData() + return + ## # Events ## @@ -110,13 +139,104 @@ class GuiNovelDetails(QDialog): self.deleteLater() return + ## + # Private Slots + ## + + @pyqtSlot(int) + def _sidebarClicked(self, pageId: int) -> None: + """Process a user request to switch page.""" + if pageId == self.PAGE_OVERVIEW: + self.mainStack.setCurrentWidget(self.overviewPage) + return + ## # Internal Functions ## def _saveSettings(self) -> None: """Save the user GUI settings.""" + winWidth = CONFIG.rpxInt(self.width()) + winHeight = CONFIG.rpxInt(self.height()) + logger.debug("Saving State: GuiNovelDetails") + options = SHARED.project.options + options.setValue("GuiNovelDetails", "winWidth", winWidth) + options.setValue("GuiNovelDetails", "winHeight", winHeight) return # END Class GuiNovelDetails + + +class _OverviewPage(QWidget): + + def __init__(self, parent: QWidget) -> None: + super().__init__(parent=parent) + + mPx = CONFIG.pxInt(8) + sPx = CONFIG.pxInt(16) + hPx = CONFIG.pxInt(24) + vPx = CONFIG.pxInt(4) + + # Project Info + self.projLabel = NColourLabel(self.tr("Project"), SHARED.theme.helpText, self, 1.5) + + self.projName = QLabel("", self) + self.projWords = QLabel("", self) + self.projRevisions = QLabel("", self) + self.projEditTime = QLabel("", self) + + self.projForm = QFormLayout() + self.projForm.addRow("%s" % self.tr("Name"), self.projName) + self.projForm.addRow("%s" % self.tr("Word Count"), self.projWords) + self.projForm.addRow("%s" % self.tr("Revisions"), self.projRevisions) + self.projForm.addRow("%s" % self.tr("Editing Time"), self.projEditTime) + self.projForm.setContentsMargins(mPx, 0, 0, 0) + self.projForm.setHorizontalSpacing(hPx) + self.projForm.setVerticalSpacing(vPx) + + # Novel Info + self.novelLabel = NColourLabel(self.tr("Novel"), SHARED.theme.helpText, self, 1.5) + + self.novelName = QLabel("", self) + self.novelWords = QLabel("", self) + self.novelChapters = QLabel("", self) + self.novelScenes = QLabel("", self) + + self.novelForm = QFormLayout() + self.novelForm.addRow("%s" % self.tr("Name"), self.novelName) + self.novelForm.addRow("%s" % self.tr("Word Count"), self.novelWords) + self.novelForm.addRow("%s" % self.tr("Chapters"), self.novelChapters) + self.novelForm.addRow("%s" % self.tr("Scenes"), self.novelScenes) + self.novelForm.setContentsMargins(mPx, 0, 0, 0) + self.novelForm.setHorizontalSpacing(hPx) + self.novelForm.setVerticalSpacing(vPx) + + # Assemble + self.outerBox = QVBoxLayout() + self.outerBox.addWidget(self.projLabel) + 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) + + return + + ## + # Methods + ## + + def updateProjectData(self) -> None: + """Load information about the project.""" + project = SHARED.project + self.projName.setText(project.data.name) + self.projWords.setText(f"{project.index.getNovelWordCount():n}") + self.projRevisions.setText(f"{project.data.saveCount:n}") + self.projEditTime.setText(formatTime(project.currentEditTime)) + return + +# END Class _OverviewPage From 23713bb79192cc9d330035d7411d005fbc9727a6 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 25 Jan 2024 21:25:43 +0100 Subject: [PATCH 14/34] Add table of contents code to novel details panel --- novelwriter/core/index.py | 21 +- novelwriter/core/options.py | 4 +- novelwriter/tools/noveldetails.py | 322 +++++++++++++++++++++++++++-- tests/test_core/test_core_index.py | 5 + 4 files changed, 326 insertions(+), 26 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index f58145f9..7f1598b6 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -528,17 +528,20 @@ class NWIndex: yield f"{tHandle}:{sTitle}", tHandle, sTitle, hItem return - def getNovelWordCount(self, activeOnly: bool = True) -> int: - """Count the number of words in the novel project.""" - wCount = 0 - for _, _, hItem in self._itemIndex.iterNovelStructure(activeOnly=activeOnly): - wCount += hItem.wordCount - return wCount + def getNovelWordCount(self, rootHandle: str | None = None, activeOnly: bool = True) -> int: + """Count the number of words in one or all novel roots.""" + return sum(hItem.wordCount for _, _, hItem in self._itemIndex.iterNovelStructure( + rHandle=rootHandle, activeOnly=activeOnly + )) - def getNovelTitleCounts(self, activeOnly: bool = True) -> list[int]: - """Count the number of titles in the novel project.""" + def getNovelTitleCounts( + self, rootHandle: str | None = None, activeOnly: bool = True + ) -> list[int]: + """Count the number of titles in one or all novel roots.""" hCount = [0, 0, 0, 0, 0] - for _, _, hItem in self._itemIndex.iterNovelStructure(activeOnly=activeOnly): + for _, _, hItem in self._itemIndex.iterNovelStructure( + rHandle=rootHandle, activeOnly=activeOnly + ): iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0) hCount[iLevel] += 1 return hCount diff --git a/novelwriter/core/options.py b/novelwriter/core/options.py index 93ec4d8c..e2e71296 100644 --- a/novelwriter/core/options.py +++ b/novelwriter/core/options.py @@ -73,7 +73,9 @@ VALID_MAP: dict[str, set[str]] = { "colWidths", "hideInactive", }, "GuiNovelDetails": { - "winWidth", "winHeight", + "winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2", + "widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble", + "novelRoot", }, } diff --git a/novelwriter/tools/noveldetails.py b/novelwriter/tools/noveldetails.py index fdced6b9..9139a655 100644 --- a/novelwriter/tools/noveldetails.py +++ b/novelwriter/tools/noveldetails.py @@ -23,23 +23,29 @@ along with this program. If not, see . """ from __future__ import annotations +import math import logging -from PyQt5.QtCore import pyqtSlot from PyQt5.QtGui import QCloseEvent +from PyQt5.QtCore import QSize, Qt, pyqtSlot from PyQt5.QtWidgets import ( - QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout, QLabel, QStackedWidget, QVBoxLayout, - QWidget + QAbstractItemView, QDialog, QDialogButtonBox, QFormLayout, QGridLayout, + QHBoxLayout, QLabel, QSpinBox, QStackedWidget, QTreeWidget, + QTreeWidgetItem, QVBoxLayout, QWidget ) from novelwriter import CONFIG, SHARED -from novelwriter.common import formatTime +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.novelselector import NovelSelector from novelwriter.extensions.pagedsidebar import NPagedSideBar +from novelwriter.extensions.novelselector import NovelSelector logger = logging.getLogger(__name__) +HEADER_SIZE = 1.4 + class GuiNovelDetails(QDialog): @@ -72,6 +78,7 @@ class GuiNovelDetails(QDialog): # Novel Selector self.novelSelector = NovelSelector(self) self.novelSelector.refreshNovelList() + self.novelSelector.setHandle(options.getString("GuiNovelDetails", "novelRoot", "")) # SideBar self.sidebar = NPagedSideBar(self) @@ -83,9 +90,11 @@ class GuiNovelDetails(QDialog): # Content self.overviewPage = _OverviewPage(self) + self.contentsPage = _ContentsPage(self) self.mainStack = QStackedWidget(self) self.mainStack.addWidget(self.overviewPage) + self.mainStack.addWidget(self.contentsPage) # Buttons self.buttonBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) @@ -111,6 +120,10 @@ class GuiNovelDetails(QDialog): self.setLayout(self.outerBox) self.setSizeGripEnabled(True) + # Connect Signals + self.novelSelector.novelSelectionChanged.connect(self.overviewPage.novelValueChanged) + self.novelSelector.novelSelectionChanged.connect(self.contentsPage.novelValueChanged) + logger.debug("Ready: GuiNovelDetails") return @@ -126,6 +139,8 @@ class GuiNovelDetails(QDialog): def updateValues(self) -> None: """Load the dialogs initial values.""" self.overviewPage.updateProjectData() + self.overviewPage.novelValueChanged(self.novelSelector.handle) + self.contentsPage.novelValueChanged(self.novelSelector.handle) return ## @@ -148,6 +163,8 @@ class GuiNovelDetails(QDialog): """Process a user request to switch page.""" if pageId == self.PAGE_OVERVIEW: self.mainStack.setCurrentWidget(self.overviewPage) + elif pageId == self.PAGE_CONTENTS: + self.mainStack.setCurrentWidget(self.contentsPage) return ## @@ -158,11 +175,16 @@ class GuiNovelDetails(QDialog): """Save the user GUI settings.""" winWidth = CONFIG.rpxInt(self.width()) winHeight = CONFIG.rpxInt(self.height()) + novelRoot = self.novelSelector.handle logger.debug("Saving State: GuiNovelDetails") options = SHARED.project.options options.setValue("GuiNovelDetails", "winWidth", winWidth) options.setValue("GuiNovelDetails", "winHeight", winHeight) + options.setValue("GuiNovelDetails", "novelRoot", novelRoot) + + self.contentsPage.saveSettings() + return # END Class GuiNovelDetails @@ -179,24 +201,32 @@ class _OverviewPage(QWidget): vPx = CONFIG.pxInt(4) # Project Info - self.projLabel = NColourLabel(self.tr("Project"), SHARED.theme.helpText, self, 1.5) + self.projLabel = NColourLabel( + self.tr("Project"), SHARED.theme.helpText, parent=self, scale=HEADER_SIZE + ) self.projName = QLabel("", self) self.projWords = QLabel("", self) + self.projNovels = QLabel("", self) + self.projNotes = QLabel("", self) self.projRevisions = QLabel("", self) self.projEditTime = QLabel("", self) self.projForm = QFormLayout() - self.projForm.addRow("%s" % self.tr("Name"), self.projName) - self.projForm.addRow("%s" % self.tr("Word Count"), self.projWords) - self.projForm.addRow("%s" % self.tr("Revisions"), self.projRevisions) - self.projForm.addRow("%s" % self.tr("Editing Time"), self.projEditTime) + self.projForm.addRow("{0}".format(self.tr("Name")), self.projName) + self.projForm.addRow("{0}".format(self.tr("Revisions")), self.projRevisions) + self.projForm.addRow("{0}".format(self.tr("Editing Time")), self.projEditTime) + self.projForm.addRow("{0}".format(self.tr("Word Count")), self.projWords) + self.projForm.addRow("\u2026 {0}".format(self.tr("In Novels")), self.projNovels) + self.projForm.addRow("\u2026 {0}".format(self.tr("In Notes ")), self.projNotes) self.projForm.setContentsMargins(mPx, 0, 0, 0) self.projForm.setHorizontalSpacing(hPx) self.projForm.setVerticalSpacing(vPx) # Novel Info - self.novelLabel = NColourLabel(self.tr("Novel"), SHARED.theme.helpText, self, 1.5) + self.novelLabel = NColourLabel( + self.tr("Selected Novel"), SHARED.theme.helpText, parent=self, scale=HEADER_SIZE + ) self.novelName = QLabel("", self) self.novelWords = QLabel("", self) @@ -204,10 +234,10 @@ class _OverviewPage(QWidget): self.novelScenes = QLabel("", self) self.novelForm = QFormLayout() - self.novelForm.addRow("%s" % self.tr("Name"), self.novelName) - self.novelForm.addRow("%s" % self.tr("Word Count"), self.novelWords) - self.novelForm.addRow("%s" % self.tr("Chapters"), self.novelChapters) - self.novelForm.addRow("%s" % self.tr("Scenes"), self.novelScenes) + self.novelForm.addRow("{0}".format(self.tr("Name")), self.novelName) + self.novelForm.addRow("{0}".format(self.tr("Word Count")), self.novelWords) + self.novelForm.addRow("{0}".format(self.tr("Chapters")), self.novelChapters) + self.novelForm.addRow("{0}".format(self.tr("Scenes")), self.novelScenes) self.novelForm.setContentsMargins(mPx, 0, 0, 0) self.novelForm.setHorizontalSpacing(hPx) self.novelForm.setVerticalSpacing(vPx) @@ -233,10 +263,270 @@ class _OverviewPage(QWidget): def updateProjectData(self) -> None: """Load information about the project.""" project = SHARED.project + project.updateWordCounts() + wcNovel, wcNotes = project.data.currCounts + self.projName.setText(project.data.name) - self.projWords.setText(f"{project.index.getNovelWordCount():n}") self.projRevisions.setText(f"{project.data.saveCount:n}") self.projEditTime.setText(formatTime(project.currentEditTime)) + self.projWords.setText(f"{wcNovel + wcNotes:n}") + self.projNovels.setText(f"{wcNovel:n}") + self.projNotes.setText(f"{wcNotes:n}") + return + + ## + # Public Slots + ## + + @pyqtSlot(str) + def novelValueChanged(self, tHandle: str) -> None: + """Refresh the data for the selected novel.""" + project = SHARED.project + if nwItem := project.tree[tHandle]: + self.novelName.setText(nwItem.itemName) + + nwCount = project.index.getNovelWordCount(rootHandle=tHandle) + self.novelWords.setText(f"{nwCount:n}") + + hCounts = project.index.getNovelTitleCounts(rootHandle=tHandle) + self.novelChapters.setText(f"{hCounts[2]:n}") + self.novelScenes.setText(f"{hCounts[3]:n}") + return # END Class _OverviewPage + + +class _ContentsPage(QWidget): + + C_TITLE = 0 + C_WORDS = 1 + C_PAGES = 2 + C_PAGE = 3 + C_PROG = 4 + + def __init__(self, parent: QWidget) -> None: + super().__init__(parent=parent) + + self._data = [] + self._currentRoot = None + + iPx = SHARED.theme.baseIconSize + hPx = CONFIG.pxInt(12) + vPx = CONFIG.pxInt(4) + options = SHARED.project.options + + # Title + self.contentLabel = NColourLabel( + self.tr("Table of Contents"), SHARED.theme.helpText, parent=self, scale=HEADER_SIZE + ) + + # Contents Tree + self.tocTree = QTreeWidget(self) + self.tocTree.setIconSize(QSize(iPx, iPx)) + self.tocTree.setIndentation(0) + self.tocTree.setColumnCount(6) + self.tocTree.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection) + self.tocTree.setHeaderLabels([ + self.tr("Title"), + self.tr("Words"), + self.tr("Pages"), + self.tr("Page"), + self.tr("Progress"), + "", + ]) + + treeHeadItem = self.tocTree.headerItem() + if treeHeadItem: + treeHeadItem.setTextAlignment(self.C_WORDS, Qt.AlignmentFlag.AlignRight) + treeHeadItem.setTextAlignment(self.C_PAGES, Qt.AlignmentFlag.AlignRight) + treeHeadItem.setTextAlignment(self.C_PAGE, Qt.AlignmentFlag.AlignRight) + treeHeadItem.setTextAlignment(self.C_PROG, Qt.AlignmentFlag.AlignRight) + + treeHeader = self.tocTree.header() + treeHeader.setStretchLastSection(True) + treeHeader.setMinimumSectionSize(hPx) + + wCol0 = CONFIG.pxInt(options.getInt("GuiNovelDetails", "widthCol0", 200)) + wCol1 = CONFIG.pxInt(options.getInt("GuiNovelDetails", "widthCol1", 60)) + wCol2 = CONFIG.pxInt(options.getInt("GuiNovelDetails", "widthCol2", 60)) + wCol3 = CONFIG.pxInt(options.getInt("GuiNovelDetails", "widthCol3", 60)) + wCol4 = CONFIG.pxInt(options.getInt("GuiNovelDetails", "widthCol4", 90)) + + self.tocTree.setColumnWidth(0, wCol0) + self.tocTree.setColumnWidth(1, wCol1) + self.tocTree.setColumnWidth(2, wCol2) + self.tocTree.setColumnWidth(3, wCol3) + self.tocTree.setColumnWidth(4, wCol4) + self.tocTree.setColumnWidth(5, hPx) + + # Options + wordsPerPage = options.getInt("GuiNovelDetails", "wordsPerPage", 350) + countFrom = options.getInt("GuiNovelDetails", "countFrom", 1) + clearDouble = options.getBool("GuiNovelDetails", "clearDouble", True) + + self.wpLabel = QLabel(self.tr("Words per page")) + + self.wpValue = QSpinBox(self) + self.wpValue.setMinimum(10) + self.wpValue.setMaximum(1000) + self.wpValue.setSingleStep(10) + self.wpValue.setValue(wordsPerPage) + self.wpValue.valueChanged.connect(self._populateTree) + + self.poLabel = QLabel(self.tr("First page offset")) + + self.poValue = QSpinBox(self) + self.poValue.setMinimum(1) + self.poValue.setMaximum(9999) + self.poValue.setSingleStep(1) + self.poValue.setValue(countFrom) + self.poValue.valueChanged.connect(self._populateTree) + + self.dblLabel = QLabel(self.tr("Chapters on odd pages")) + + self.dblValue = NSwitch(self, 2*iPx, iPx) + self.dblValue.setChecked(clearDouble) + self.dblValue.clicked.connect(self._populateTree) + + self.optionsBox = QGridLayout() + self.optionsBox.addWidget(self.wpLabel, 0, 0) + self.optionsBox.addWidget(self.wpValue, 0, 1) + self.optionsBox.addWidget(self.dblLabel, 0, 3) + self.optionsBox.addWidget(self.dblValue, 0, 4) + self.optionsBox.addWidget(self.poLabel, 1, 0) + self.optionsBox.addWidget(self.poValue, 1, 1) + self.optionsBox.setHorizontalSpacing(hPx) + self.optionsBox.setVerticalSpacing(vPx) + self.optionsBox.setColumnStretch(2, 1) + + # Assemble + self.outerBox = QVBoxLayout() + 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) + + return + + def saveSettings(self) -> None: + """Save the user GUI settings.""" + 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", "wordsPerPage", self.wpValue.value()) + options.setValue("GuiNovelDetails", "countFrom", self.poValue.value()) + options.setValue("GuiNovelDetails", "clearDouble", self.dblValue.isChecked()) + return + + def getColumnSizes(self) -> list[int]: + """Return the column widths for the tree columns.""" + retVals = [ + self.tocTree.columnWidth(0), + self.tocTree.columnWidth(1), + self.tocTree.columnWidth(2), + self.tocTree.columnWidth(3), + self.tocTree.columnWidth(4), + ] + return retVals + + ## + # Public Slots + ## + + @pyqtSlot(str) + def novelValueChanged(self, tHandle: str) -> None: + """Refresh the tree with another root item.""" + if tHandle != self._currentRoot: + self._prepareData(tHandle) + self._populateTree() + self._currentRoot = tHandle + return + + ## + # Private Slots + ## + + @pyqtSlot() + def _populateTree(self) -> None: + """Set the content of the chapter/page tree.""" + dblPages = self.dblValue.isChecked() + wpPage = self.wpValue.value() + fstPage = self.poValue.value() - 1 + + pTotal = 0 + tPages = 1 + + theList = [] + for _, tLevel, tTitle, wCount in self._data: + pCount = math.ceil(wCount/wpPage) + if dblPages: + pCount += pCount%2 + + pTotal += pCount + theList.append((tLevel, tTitle, wCount, pCount)) + + pMax = pTotal - fstPage + + self.tocTree.clear() + for tLevel, tTitle, wCount, pCount in theList: + newItem = QTreeWidgetItem() + + if tPages <= fstPage: + progPage = numberToRoman(tPages, True) + progText = "" + else: + cPage = tPages - fstPage + pgProg = 100.0*(cPage - 1)/pMax if pMax > 0 else 0.0 + progPage = f"{cPage:n}" + progText = f"{pgProg:.1f}{nwUnicode.U_THSP}%" + + hDec = SHARED.theme.getHeaderDecoration(tLevel) + if tTitle.strip() == "": + tTitle = self.tr("Untitled") + + newItem.setData(self.C_TITLE, Qt.DecorationRole, hDec) + newItem.setText(self.C_TITLE, tTitle) + newItem.setText(self.C_WORDS, f"{wCount:n}") + newItem.setText(self.C_PAGES, f"{pCount:n}") + newItem.setText(self.C_PAGE, progPage) + newItem.setText(self.C_PROG, progText) + + newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) + newItem.setTextAlignment(self.C_PAGES, Qt.AlignRight) + newItem.setTextAlignment(self.C_PAGE, Qt.AlignRight) + newItem.setTextAlignment(self.C_PROG, Qt.AlignRight) + + # Make pages and titles/partitions stand out + if tLevel < 2: + bFont = newItem.font(self.C_TITLE) + if tLevel == 0: + bFont.setItalic(True) + else: + bFont.setBold(True) + bFont.setUnderline(True) + newItem.setFont(self.C_TITLE, bFont) + + tPages += pCount + + self.tocTree.addTopLevelItem(newItem) + + return + + ## + # Internal Functions + ## + + def _prepareData(self, rootHandle: str | None) -> None: + """Extract the information from the project index.""" + logger.debug("Populating ToC from handle '%s'", rootHandle) + self._data = SHARED.project.index.getTableOfContents(rootHandle, 2) + self._data.append(("", 0, self.tr("END"), 0)) + return + +# END Class _ContentsPage diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index 5e9dee3f..44836349 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -695,6 +695,7 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd): "This is a story about Jane Smith.\n\n" "Well, not really. She's still awesome though.\n" )) + # Whole document cC, wC, pC = index.getCounts(nHandle) assert cC == 152 @@ -798,6 +799,10 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd): # Extract stats assert index.getNovelWordCount(activeOnly=False) == 43 assert index.getNovelWordCount(activeOnly=True) == 15 + assert index.getNovelWordCount(rootHandle=C.hNovelRoot, activeOnly=False) == 43 + assert index.getNovelWordCount(rootHandle=C.hNovelRoot, activeOnly=True) == 15 + assert index.getNovelWordCount(rootHandle=C.hWorldRoot, activeOnly=False) == 0 + assert index.getNovelWordCount(rootHandle=C.hWorldRoot, activeOnly=True) == 0 assert index.getNovelTitleCounts(activeOnly=False) == [0, 3, 2, 3, 0] assert index.getNovelTitleCounts(activeOnly=True) == [0, 1, 2, 3, 0] From 4ef40f2c4d823aab2b1651b89e96ed7df6e0fd94 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 25 Jan 2024 21:30:53 +0100 Subject: [PATCH 15/34] Remove old project details dialog --- novelwriter/core/options.py | 4 - novelwriter/dialogs/projdetails.py | 518 --------------------- novelwriter/gui/mainmenu.py | 12 +- novelwriter/gui/sidebar.py | 4 +- novelwriter/guimain.py | 13 - tests/test_core/test_core_options.py | 32 +- tests/test_dialogs/test_dlg_projdetails.py | 109 ----- 7 files changed, 22 insertions(+), 670 deletions(-) delete mode 100644 novelwriter/dialogs/projdetails.py delete mode 100644 tests/test_dialogs/test_dlg_projdetails.py diff --git a/novelwriter/core/options.py b/novelwriter/core/options.py index e2e71296..c48aaa13 100644 --- a/novelwriter/core/options.py +++ b/novelwriter/core/options.py @@ -53,10 +53,6 @@ VALID_MAP: dict[str, set[str]] = { "GuiProjectSettings": { "winWidth", "winHeight", "replaceColW", "statusColW", "importColW", }, - "GuiProjectDetails": { - "winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2", - "widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble", - }, "GuiWordList": {"winWidth", "winHeight"}, "GuiNovelView": {"lastCol", "lastColSize"}, "GuiBuildSettings": { diff --git a/novelwriter/dialogs/projdetails.py b/novelwriter/dialogs/projdetails.py deleted file mode 100644 index 31ffea62..00000000 --- a/novelwriter/dialogs/projdetails.py +++ /dev/null @@ -1,518 +0,0 @@ -""" -novelWriter – GUI Project Details -================================= - -File History: -Created: 2021-01-03 [1.1rc1] GuiProjectDetails - -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 - -import math -import logging - -from PyQt5.QtGui import QCloseEvent, QFont -from PyQt5.QtCore import Qt, QSize, pyqtSlot -from PyQt5.QtWidgets import ( - QAbstractItemView, QDialogButtonBox, QGridLayout, QHBoxLayout, QLabel, - QLineEdit, QSpinBox, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget -) - -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.pageddialog import NPagedDialog -from novelwriter.extensions.novelselector import NovelSelector - -logger = logging.getLogger(__name__) - - -class GuiProjectDetails(NPagedDialog): - - def __init__(self, parent: QWidget) -> None: - super().__init__(parent=parent) - - logger.debug("Create: GuiProjectDetails") - self.setObjectName("GuiProjectDetails") - - self.setWindowTitle(self.tr("Project Details")) - - wW = CONFIG.pxInt(600) - wH = CONFIG.pxInt(400) - pOptions = SHARED.project.options - - self.setMinimumWidth(wW) - self.setMinimumHeight(wH) - self.resize( - CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "winWidth", wW)), - CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "winHeight", wH)) - ) - - self.tabMain = GuiProjectDetailsMain(self) - self.tabContents = GuiProjectDetailsContents(self) - - self.addTab(self.tabMain, self.tr("Overview")) - self.addTab(self.tabContents, self.tr("Contents")) - - self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) - self.buttonBox.rejected.connect(self.close) - self.rejected.connect(self.close) - self.addControls(self.buttonBox) - - logger.debug("Ready: GuiProjectDetails") - - return - - def __del__(self) -> None: # pragma: no cover - logger.debug("Delete: GuiProjectDetails") - return - - def updateValues(self) -> None: - """Set all the values of the pages.""" - self.tabMain.updateValues() - self.tabContents.updateValues() - return - - ## - # Events - ## - - def closeEvent(self, event: QCloseEvent) -> None: - """Capture the close event and perform cleanup.""" - self._saveGuiSettings() - event.accept() - self.deleteLater() - return - - ## - # Internal Functions - ## - - def _saveGuiSettings(self) -> None: - """Save GUI settings.""" - winWidth = CONFIG.rpxInt(self.width()) - winHeight = CONFIG.rpxInt(self.height()) - - cColWidth = self.tabContents.getColumnSizes() - widthCol0 = CONFIG.rpxInt(cColWidth[0]) - widthCol1 = CONFIG.rpxInt(cColWidth[1]) - widthCol2 = CONFIG.rpxInt(cColWidth[2]) - widthCol3 = CONFIG.rpxInt(cColWidth[3]) - widthCol4 = CONFIG.rpxInt(cColWidth[4]) - - wordsPerPage = self.tabContents.wpValue.value() - countFrom = self.tabContents.poValue.value() - clearDouble = self.tabContents.dblValue.isChecked() - - logger.debug("Saving State: GuiProjectDetails") - pOptions = SHARED.project.options - pOptions.setValue("GuiProjectDetails", "winWidth", winWidth) - pOptions.setValue("GuiProjectDetails", "winHeight", winHeight) - pOptions.setValue("GuiProjectDetails", "widthCol0", widthCol0) - pOptions.setValue("GuiProjectDetails", "widthCol1", widthCol1) - pOptions.setValue("GuiProjectDetails", "widthCol2", widthCol2) - pOptions.setValue("GuiProjectDetails", "widthCol3", widthCol3) - pOptions.setValue("GuiProjectDetails", "widthCol4", widthCol4) - pOptions.setValue("GuiProjectDetails", "wordsPerPage", wordsPerPage) - pOptions.setValue("GuiProjectDetails", "countFrom", countFrom) - pOptions.setValue("GuiProjectDetails", "clearDouble", clearDouble) - - return - -# END Class GuiProjectDetails - - -class GuiProjectDetailsMain(QWidget): - - def __init__(self, parent: QWidget) -> None: - super().__init__(parent=parent) - - fPx = SHARED.theme.fontPixelSize - fPt = SHARED.theme.fontPointSize - vPx = CONFIG.pxInt(4) - hPx = CONFIG.pxInt(12) - - # Header - # ====== - - self.bookTitle = QLabel("") - bookFont = self.bookTitle.font() - bookFont.setPointSizeF(2.2*fPt) - bookFont.setWeight(QFont.Bold) - self.bookTitle.setFont(bookFont) - self.bookTitle.setAlignment(Qt.AlignHCenter) - self.bookTitle.setWordWrap(True) - - self.projName = QLabel("") - workFont = self.projName.font() - workFont.setPointSizeF(0.8*fPt) - workFont.setItalic(True) - self.projName.setFont(workFont) - self.projName.setAlignment(Qt.AlignHCenter) - self.projName.setWordWrap(True) - - self.bookAuthors = QLabel("") - authFont = self.bookAuthors.font() - authFont.setPointSizeF(1.2*fPt) - self.bookAuthors.setFont(authFont) - self.bookAuthors.setAlignment(Qt.AlignHCenter) - self.bookAuthors.setWordWrap(True) - - # Stats - # ===== - - self.wordCountLbl = QLabel("%s:" % self.tr("Words")) - self.wordCountVal = QLabel("") - - self.chapCountLbl = QLabel("%s:" % self.tr("Chapters")) - self.chapCountVal = QLabel("") - - self.sceneCountLbl = QLabel("%s:" % self.tr("Scenes")) - self.sceneCountVal = QLabel("") - - self.revCountLbl = QLabel("%s:" % self.tr("Revisions")) - self.revCountVal = QLabel("") - - self.editTimeLbl = QLabel("%s:" % self.tr("Editing Time")) - self.editTimeVal = QLabel("") - - self.statsGrid = QGridLayout() - self.statsGrid.addWidget(self.wordCountLbl, 0, 0, 1, 1, Qt.AlignRight) - self.statsGrid.addWidget(self.wordCountVal, 0, 1, 1, 1, Qt.AlignLeft) - self.statsGrid.addWidget(self.chapCountLbl, 1, 0, 1, 1, Qt.AlignRight) - self.statsGrid.addWidget(self.chapCountVal, 1, 1, 1, 1, Qt.AlignLeft) - self.statsGrid.addWidget(self.sceneCountLbl, 2, 0, 1, 1, Qt.AlignRight) - self.statsGrid.addWidget(self.sceneCountVal, 2, 1, 1, 1, Qt.AlignLeft) - self.statsGrid.addWidget(self.revCountLbl, 3, 0, 1, 1, Qt.AlignRight) - self.statsGrid.addWidget(self.revCountVal, 3, 1, 1, 1, Qt.AlignLeft) - self.statsGrid.addWidget(self.editTimeLbl, 4, 0, 1, 1, Qt.AlignRight) - self.statsGrid.addWidget(self.editTimeVal, 4, 1, 1, 1, Qt.AlignLeft) - self.statsGrid.setHorizontalSpacing(hPx) - self.statsGrid.setVerticalSpacing(vPx) - - # Meta - # ==== - - self.projPathLbl = QLabel("%s:" % self.tr("Path")) - self.projPathVal = QLineEdit() - self.projPathVal.setReadOnly(True) - - self.projPathBox = QHBoxLayout() - self.projPathBox.addWidget(self.projPathLbl) - self.projPathBox.addWidget(self.projPathVal) - self.projPathBox.setSpacing(hPx) - - # Assemble - # ======== - - self.outerBox = QVBoxLayout() - self.outerBox.addSpacing(fPx) - self.outerBox.addWidget(self.bookTitle) - self.outerBox.addWidget(self.projName) - self.outerBox.addWidget(self.bookAuthors) - self.outerBox.addSpacing(2*fPx) - self.outerBox.addLayout(self.statsGrid) - self.outerBox.addSpacing(fPx) - self.outerBox.addStretch(1) - self.outerBox.addLayout(self.projPathBox) - - self.setLayout(self.outerBox) - - return - - def updateValues(self) -> None: - """Set all the values.""" - project = SHARED.project - pIndex = project.index - hCounts = pIndex.getNovelTitleCounts() - nwCount = pIndex.getNovelWordCount() - edTime = project.currentEditTime - - self.bookTitle.setText(project.data.title or project.data.name) - self.projName.setText(self.tr("Project: {0}").format(project.data.name)) - self.bookAuthors.setText(self.tr("By {0}").format(project.data.author)) - - self.wordCountVal.setText(f"{nwCount:n}") - self.chapCountVal.setText(f"{hCounts[2]:n}") - self.sceneCountVal.setText(f"{hCounts[3]:n}") - self.revCountVal.setText(f"{project.data.saveCount:n}") - self.editTimeVal.setText(formatTime(edTime)) - - self.projPathVal.setText(str(project.storage.storagePath)) - - return - -# END Class GuiProjectDetailsMain - - -class GuiProjectDetailsContents(QWidget): - - C_TITLE = 0 - C_WORDS = 1 - C_PAGES = 2 - C_PAGE = 3 - C_PROG = 4 - - def __init__(self, parent: QWidget) -> None: - super().__init__(parent=parent) - - # Internal - self._theToC = [] - self._currentRoot = None - - iPx = SHARED.theme.baseIconSize - hPx = CONFIG.pxInt(12) - vPx = CONFIG.pxInt(4) - pOptions = SHARED.project.options - - # Header - # ====== - - self.tocLabel = QLabel("%s" % self.tr("Table of Contents")) - - self.novelValue = NovelSelector(self) - self.novelValue.setMinimumWidth(CONFIG.pxInt(200)) - self.novelValue.novelSelectionChanged.connect(self._novelValueChanged) - - self.headBox = QHBoxLayout() - self.headBox.addWidget(self.tocLabel) - self.headBox.addWidget(self.novelValue) - - # Contents Tree - # ============= - - self.tocTree = QTreeWidget() - self.tocTree.setIconSize(QSize(iPx, iPx)) - self.tocTree.setIndentation(0) - self.tocTree.setColumnCount(6) - self.tocTree.setSelectionMode(QAbstractItemView.NoSelection) - self.tocTree.setHeaderLabels([ - self.tr("Title"), - self.tr("Words"), - self.tr("Pages"), - self.tr("Page"), - self.tr("Progress"), - "" - ]) - - treeHeadItem = self.tocTree.headerItem() - if treeHeadItem: - treeHeadItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) - treeHeadItem.setTextAlignment(self.C_PAGES, Qt.AlignRight) - treeHeadItem.setTextAlignment(self.C_PAGE, Qt.AlignRight) - treeHeadItem.setTextAlignment(self.C_PROG, Qt.AlignRight) - - treeHeader = self.tocTree.header() - treeHeader.setStretchLastSection(True) - treeHeader.setMinimumSectionSize(hPx) - - wCol0 = CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol0", 200)) - wCol1 = CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol1", 60)) - wCol2 = CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol2", 60)) - wCol3 = CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol3", 60)) - wCol4 = CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol4", 90)) - - self.tocTree.setColumnWidth(0, wCol0) - self.tocTree.setColumnWidth(1, wCol1) - self.tocTree.setColumnWidth(2, wCol2) - self.tocTree.setColumnWidth(3, wCol3) - self.tocTree.setColumnWidth(4, wCol4) - self.tocTree.setColumnWidth(5, hPx) - - # Options - # ======= - - wordsPerPage = pOptions.getInt("GuiProjectDetails", "wordsPerPage", 350) - countFrom = pOptions.getInt("GuiProjectDetails", "countFrom", 1) - clearDouble = pOptions.getBool("GuiProjectDetails", "clearDouble", True) - - wordsHelp = ( - self.tr("Typical word count for a 5 by 8 inch book page with 11 pt font is 350.") - ) - offsetHelp = ( - self.tr("Start counting page numbers from this page.") - ) - dblHelp = ( - self.tr("Assume a new chapter or partition always start on an odd numbered page.") - ) - - self.wpLabel = QLabel(self.tr("Words per page")) - self.wpLabel.setToolTip(wordsHelp) - - self.wpValue = QSpinBox() - self.wpValue.setMinimum(10) - self.wpValue.setMaximum(1000) - self.wpValue.setSingleStep(10) - self.wpValue.setValue(wordsPerPage) - self.wpValue.setToolTip(wordsHelp) - self.wpValue.valueChanged.connect(self._populateTree) - - self.poLabel = QLabel(self.tr("Count pages from")) - self.poLabel.setToolTip(offsetHelp) - - self.poValue = QSpinBox() - self.poValue.setMinimum(1) - self.poValue.setMaximum(9999) - self.poValue.setSingleStep(1) - self.poValue.setValue(countFrom) - self.poValue.setToolTip(offsetHelp) - self.poValue.valueChanged.connect(self._populateTree) - - self.dblLabel = QLabel(self.tr("Clear double pages")) - self.dblLabel.setToolTip(dblHelp) - - self.dblValue = NSwitch(self, 2*iPx, iPx) - self.dblValue.setChecked(clearDouble) - self.dblValue.setToolTip(dblHelp) - self.dblValue.clicked.connect(self._populateTree) - - self.optionsBox = QGridLayout() - self.optionsBox.addWidget(self.wpLabel, 0, 0) - self.optionsBox.addWidget(self.wpValue, 0, 1) - self.optionsBox.addWidget(self.dblLabel, 0, 3) - self.optionsBox.addWidget(self.dblValue, 0, 4) - self.optionsBox.addWidget(self.poLabel, 1, 0) - self.optionsBox.addWidget(self.poValue, 1, 1) - self.optionsBox.setHorizontalSpacing(hPx) - self.optionsBox.setVerticalSpacing(vPx) - self.optionsBox.setColumnStretch(2, 1) - - # Assemble - # ======== - - self.outerBox = QVBoxLayout() - self.outerBox.addLayout(self.headBox) - self.outerBox.addWidget(self.tocTree) - self.outerBox.addLayout(self.optionsBox) - - self.setLayout(self.outerBox) - - return - - def getColumnSizes(self) -> list[int]: - """Return the column widths for the tree columns.""" - retVals = [ - self.tocTree.columnWidth(0), - self.tocTree.columnWidth(1), - self.tocTree.columnWidth(2), - self.tocTree.columnWidth(3), - self.tocTree.columnWidth(4), - ] - return retVals - - def updateValues(self) -> None: - """Populate the tree.""" - self._currentRoot = None - self.novelValue.refreshNovelList() - self.novelValue.setHandle(self.novelValue.firstHandle) - self._prepareData(self.novelValue.firstHandle) - self._populateTree() - return - - ## - # Internal Functions - ## - - def _prepareData(self, rootHandle: str | None) -> None: - """Extract the information from the project index.""" - logger.debug("Populating ToC from handle '%s'", rootHandle) - self._theToC = SHARED.project.index.getTableOfContents(rootHandle, 2) - self._theToC.append(("", 0, self.tr("END"), 0)) - return - - ## - # Slots - ## - - @pyqtSlot(str) - def _novelValueChanged(self, tHandle: str) -> None: - """Refresh the tree with another root item.""" - if tHandle != self._currentRoot: - self._prepareData(tHandle) - self._populateTree() - self._currentRoot = self.novelValue.handle - return - - @pyqtSlot() - def _populateTree(self) -> None: - """Set the content of the chapter/page tree.""" - dblPages = self.dblValue.isChecked() - wpPage = self.wpValue.value() - fstPage = self.poValue.value() - 1 - - pTotal = 0 - tPages = 1 - - theList = [] - for _, tLevel, tTitle, wCount in self._theToC: - pCount = math.ceil(wCount/wpPage) - if dblPages: - pCount += pCount%2 - - pTotal += pCount - theList.append((tLevel, tTitle, wCount, pCount)) - - pMax = pTotal - fstPage - - self.tocTree.clear() - for tLevel, tTitle, wCount, pCount in theList: - newItem = QTreeWidgetItem() - - if tPages <= fstPage: - progPage = numberToRoman(tPages, True) - progText = "" - else: - cPage = tPages - fstPage - pgProg = 100.0*(cPage - 1)/pMax if pMax > 0 else 0.0 - progPage = f"{cPage:n}" - progText = f"{pgProg:.1f}{nwUnicode.U_THSP}%" - - hDec = SHARED.theme.getHeaderDecoration(tLevel) - if tTitle.strip() == "": - tTitle = self.tr("Untitled") - - newItem.setData(self.C_TITLE, Qt.DecorationRole, hDec) - newItem.setText(self.C_TITLE, tTitle) - newItem.setText(self.C_WORDS, f"{wCount:n}") - newItem.setText(self.C_PAGES, f"{pCount:n}") - newItem.setText(self.C_PAGE, progPage) - newItem.setText(self.C_PROG, progText) - - newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) - newItem.setTextAlignment(self.C_PAGES, Qt.AlignRight) - newItem.setTextAlignment(self.C_PAGE, Qt.AlignRight) - newItem.setTextAlignment(self.C_PROG, Qt.AlignRight) - - # Make pages and titles/partitions stand out - if tLevel < 2: - bFont = newItem.font(self.C_TITLE) - if tLevel == 0: - bFont.setItalic(True) - else: - bFont.setBold(True) - bFont.setUnderline(True) - newItem.setFont(self.C_TITLE, bFont) - - tPages += pCount - - self.tocTree.addTopLevelItem(newItem) - - return - -# END Class GuiProjectDetailsContents diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 4ad1ab09..279fce07 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -155,14 +155,10 @@ class GuiMainMenu(QMenuBar): self.aProjectSettings.setShortcut("Ctrl+Shift+,") self.aProjectSettings.triggered.connect(self.mainGui.showProjectSettingsDialog) - # Project > Project Details - self.aProjectDetails = self.projMenu.addAction(self.tr("Project Details")) - self.aProjectDetails.triggered.connect(self.mainGui.showProjectDetailsDialog) - - # Project > Novel Info - self.aNovelInfo = self.projMenu.addAction(self.tr("Novel Info")) - self.aNovelInfo.setShortcut("Shift+F6") - self.aNovelInfo.triggered.connect(self.mainGui.showNovelDetailsDialog) + # Project > Novel Details + self.aNovelDetails = self.projMenu.addAction(self.tr("Novel Details")) + self.aNovelDetails.setShortcut("Shift+F6") + self.aNovelDetails.triggered.connect(self.mainGui.showNovelDetailsDialog) # Project > Separator self.projMenu.addSeparator() diff --git a/novelwriter/gui/sidebar.py b/novelwriter/gui/sidebar.py index 92522542..d425ccf8 100644 --- a/novelwriter/gui/sidebar.py +++ b/novelwriter/gui/sidebar.py @@ -79,9 +79,9 @@ class GuiSideBar(QWidget): self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog) self.tbDetails = QToolButton(self) - self.tbDetails.setToolTip("{0} [Shift+F6]".format(self.tr("Project Details"))) + self.tbDetails.setToolTip("{0} [Shift+F6]".format(self.tr("Novel Details"))) self.tbDetails.setIconSize(iconSize) - self.tbDetails.clicked.connect(self.mainGui.showProjectDetailsDialog) + self.tbDetails.clicked.connect(self.mainGui.showNovelDetailsDialog) self.tbStats = QToolButton(self) self.tbStats.setToolTip("{0} [F6]".format(self.tr("Writing Statistics"))) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 143d9c65..ad29b741 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -53,7 +53,6 @@ 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.projdetails import GuiProjectDetails from novelwriter.dialogs.projsettings import GuiProjectSettings from novelwriter.tools.welcome import GuiWelcome from novelwriter.tools.manuscript import GuiManuscript @@ -825,18 +824,6 @@ class GuiMain(QMainWindow): dialog.exec_() return - @pyqtSlot() - def showProjectDetailsDialog(self) -> None: - """Open the project details dialog.""" - if SHARED.hasProject: - dialog = GuiProjectDetails(self) - dialog.setModal(True) - dialog.show() - dialog.raise_() - qApp.processEvents() - dialog.updateValues() - return - @pyqtSlot() def showNovelDetailsDialog(self) -> None: """Open the novel details dialog.""" diff --git a/tests/test_core/test_core_options.py b/tests/test_core/test_core_options.py index 7458c746..fd291d08 100644 --- a/tests/test_core/test_core_options.py +++ b/tests/test_core/test_core_options.py @@ -121,27 +121,27 @@ def testCoreOptions_SetGet(mockGUI): assert options.setValue("GuiProjectSettings", "winWidth", 100) is True # Set some values of different types - assert options.setValue("GuiProjectDetails", "winWidth", 100) is True - assert options.setValue("GuiProjectDetails", "winHeight", 12.34) is True - assert options.setValue("GuiProjectDetails", "clearDouble", True) is True + assert options.setValue("GuiNovelDetails", "winWidth", 100) is True + assert options.setValue("GuiNovelDetails", "winHeight", 12.34) is True + assert options.setValue("GuiNovelDetails", "clearDouble", True) is True assert options.setValue("GuiNovelView", "lastCol", nwColHidden) is True # Generic get, doesn't check type - assert options.getValue("GuiProjectDetails", "winWidth", None) == 100 - assert options.getValue("GuiProjectDetails", "winHeight", None) == 12.34 - assert options.getValue("GuiProjectDetails", "clearDouble", None) is True - assert options.getValue("GuiProjectDetails", "mockItem", None) is None + assert options.getValue("GuiNovelDetails", "winWidth", None) == 100 + assert options.getValue("GuiNovelDetails", "winHeight", None) == 12.34 + assert options.getValue("GuiNovelDetails", "clearDouble", None) is True + assert options.getValue("GuiNovelDetails", "mockItem", None) is None # Get type-specific - assert options.getString("GuiProjectDetails", "winWidth", None) is None # type: ignore - assert options.getString("GuiProjectDetails", "mockItem", None) is None # type: ignore - assert options.getInt("GuiProjectDetails", "winWidth", None) == 100 # type: ignore - assert options.getInt("GuiProjectDetails", "textFont", None) is None # type: ignore - assert options.getInt("GuiProjectDetails", "mockItem", None) is None # type: ignore - assert options.getFloat("GuiProjectDetails", "winWidth", None) == 100.0 # type: ignore - assert options.getFloat("GuiProjectDetails", "mockItem", None) is None # type: ignore - assert options.getBool("GuiProjectDetails", "clearDouble", None) is True # type: ignore - assert options.getBool("GuiProjectDetails", "mockItem", None) is None # type: ignore + assert options.getString("GuiNovelDetails", "winWidth", None) is None # type: ignore + assert options.getString("GuiNovelDetails", "mockItem", None) is None # type: ignore + assert options.getInt("GuiNovelDetails", "winWidth", None) == 100 # type: ignore + assert options.getInt("GuiNovelDetails", "textFont", None) is None # type: ignore + assert options.getInt("GuiNovelDetails", "mockItem", None) is None # type: ignore + assert options.getFloat("GuiNovelDetails", "winWidth", None) == 100.0 # type: ignore + assert options.getFloat("GuiNovelDetails", "mockItem", None) is None # type: ignore + assert options.getBool("GuiNovelDetails", "clearDouble", None) is True # type: ignore + assert options.getBool("GuiNovelDetails", "mockItem", None) is None # type: ignore assert options.getEnum("GuiNovelView", "lastCol", NovelTreeColumn, nwColHidden) == nwColHidden # Get from non-existent groups diff --git a/tests/test_dialogs/test_dlg_projdetails.py b/tests/test_dialogs/test_dlg_projdetails.py deleted file mode 100644 index 3d68f9d7..00000000 --- a/tests/test_dialogs/test_dlg_projdetails.py +++ /dev/null @@ -1,109 +0,0 @@ -""" -novelWriter – Project Details Dialog Class Tester -================================================= - -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 - -import pytest - -from tools import getGuiItem - -from PyQt5.QtWidgets import QAction - -from novelwriter import SHARED -from novelwriter.dialogs.projdetails import GuiProjectDetails - - -@pytest.mark.gui -def testDlgProjDetails_Dialog(qtbot, nwGUI, prjLipsum): - """Test the project details dialog. - """ - # Create a project to work on - assert nwGUI.openProject(prjLipsum) - assert nwGUI.rebuildIndex(beQuiet=True) - qtbot.wait(100) - - # Open the Writing Stats dialog - nwGUI.mainMenu.aProjectDetails.activate(QAction.Trigger) - qtbot.waitUntil(lambda: getGuiItem("GuiProjectDetails") is not None, timeout=1000) - - projDet = getGuiItem("GuiProjectDetails") - assert isinstance(projDet, GuiProjectDetails) - - # Overview Page - # ============= - - assert projDet.tabMain.bookTitle.text() == "Lorem Ipsum" - assert projDet.tabMain.projName.text()[-11:] == "Lorem Ipsum" - assert projDet.tabMain.bookAuthors.text()[-10:] == "lipsum.com" - - assert projDet.tabMain.wordCountVal.text() == f"{3000:n}" - assert projDet.tabMain.chapCountVal.text() == f"{3:n}" - assert projDet.tabMain.sceneCountVal.text() == f"{5:n}" - assert projDet.tabMain.revCountVal.text() == f"{SHARED.project.data.saveCount:n}" - - assert projDet.tabMain.projPathVal.text() == str(prjLipsum) - - # Contents Page - # ============= - - tocTab = projDet.tabContents - tocTree = tocTab.tocTree - assert tocTree.topLevelItemCount() == 7 - assert tocTree.topLevelItem(0).text(tocTab.C_TITLE) == "Lorem Ipsum" # type: ignore - assert tocTree.topLevelItem(2).text(tocTab.C_TITLE) == "Prologue" # type: ignore - assert tocTree.topLevelItem(3).text(tocTab.C_TITLE) == "Act One" # type: ignore - assert tocTree.topLevelItem(4).text(tocTab.C_TITLE) == "Chapter One" # type: ignore - assert tocTree.topLevelItem(5).text(tocTab.C_TITLE) == "Chapter Two" # type: ignore - assert tocTree.topLevelItem(6).text(tocTab.C_TITLE) == "END" # type: ignore - - # Count Pages - tocTab.wpValue.setValue(100) - tocTab.poValue.setValue(4) - tocTab.dblValue.setChecked(False) - tocTab._populateTree() - - thePages = ["1", "2", "1", "1", "11", "17", "0"] - thePage = ["i", "ii", "1", "2", "3", "14", "31"] - for i in range(7): - assert tocTree.topLevelItem(i).text(tocTab.C_PAGES) == thePages[i] # type: ignore - assert tocTree.topLevelItem(i).text(tocTab.C_PAGE) == thePage[i] # type: ignore - - tocTab.poValue.setValue(5) - tocTab.dblValue.setChecked(True) - tocTab._populateTree() - - thePages = ["2", "2", "2", "2", "12", "18", "0"] - thePage = ["i", "iii", "1", "3", "5", "17", "35"] - for i in range(7): - assert tocTree.topLevelItem(i).text(tocTab.C_PAGES) == thePages[i] # type: ignore - assert tocTree.topLevelItem(i).text(tocTab.C_PAGE) == thePage[i] # type: ignore - - # Re-populate - assert tocTab._currentRoot is None - tocTab._novelValueChanged("7a992350f3eb6") # Not a root - assert tocTab._currentRoot == "b3643d0f92e32" # The actual novel root - - # qtbot.stop() - - # Clean Up - projDet.close() - nwGUI.closeMain() - -# END Test testDlgProjDetails_Dialog From 483528b3b2c2f1ed17d1b23da9bbb453a98552b5 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 25 Jan 2024 22:59:06 +0100 Subject: [PATCH 16/34] Modify some of the custom widgets after writing tests --- novelwriter/extensions/novelselector.py | 10 +++------- novelwriter/extensions/pagedsidebar.py | 6 ++---- novelwriter/tools/noveldetails.py | 15 +++------------ 3 files changed, 8 insertions(+), 23 deletions(-) diff --git a/novelwriter/extensions/novelselector.py b/novelwriter/extensions/novelselector.py index 793a475d..44e5bf23 100644 --- a/novelwriter/extensions/novelselector.py +++ b/novelwriter/extensions/novelselector.py @@ -66,14 +66,10 @@ class NovelSelector(QComboBox): def setHandle(self, tHandle: str | None, blockSignal: bool = True) -> None: """Set the currently selected handle.""" - self._blockSignal = blockSignal - if tHandle is None: - index = self.count() - 1 - else: - index = self.findData(tHandle) - if index >= 0: + if (index := self.findData(tHandle) if tHandle else (self.count() - 1)) >= 0: + self._blockSignal = blockSignal self.setCurrentIndex(index) - self._blockSignal = False + self._blockSignal = False return def setIncludeAll(self, value: bool) -> None: diff --git a/novelwriter/extensions/pagedsidebar.py b/novelwriter/extensions/pagedsidebar.py index 185b4d8a..082e8a2a 100644 --- a/novelwriter/extensions/pagedsidebar.py +++ b/novelwriter/extensions/pagedsidebar.py @@ -45,10 +45,9 @@ class NPagedSideBar(QToolBar): def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) - self._buttons = [] - self._actions = [] self._labelCol = None self._spacerHeight = self.fontMetrics().height() // 2 + self._buttons: dict[int, _NPagedToolButton] = {} self._group = QButtonGroup(self) self._group.setExclusive(True) @@ -95,8 +94,7 @@ class NPagedSideBar(QToolBar): action = self.insertWidget(self._stretchAction, button) self._group.addButton(button, id=buttonId) - self._buttons.append(button) - self._actions.append(action) + self._buttons[buttonId] = button return action diff --git a/novelwriter/tools/noveldetails.py b/novelwriter/tools/noveldetails.py index 9139a655..3fcd54cc 100644 --- a/novelwriter/tools/noveldetails.py +++ b/novelwriter/tools/noveldetails.py @@ -78,7 +78,9 @@ class GuiNovelDetails(QDialog): # Novel Selector self.novelSelector = NovelSelector(self) self.novelSelector.refreshNovelList() - self.novelSelector.setHandle(options.getString("GuiNovelDetails", "novelRoot", "")) + self.novelSelector.setHandle( + options.getString("GuiNovelDetails", "novelRoot", self.novelSelector.firstHandle or "") + ) # SideBar self.sidebar = NPagedSideBar(self) @@ -424,17 +426,6 @@ class _ContentsPage(QWidget): options.setValue("GuiNovelDetails", "clearDouble", self.dblValue.isChecked()) return - def getColumnSizes(self) -> list[int]: - """Return the column widths for the tree columns.""" - retVals = [ - self.tocTree.columnWidth(0), - self.tocTree.columnWidth(1), - self.tocTree.columnWidth(2), - self.tocTree.columnWidth(3), - self.tocTree.columnWidth(4), - ] - return retVals - ## # Public Slots ## From b6c9f1160fa46808ec06d33e9139f8bb3314ca12 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 25 Jan 2024 22:59:22 +0100 Subject: [PATCH 17/34] Add test coverage --- tests/test_dialogs/test_dlg_preferences.py | 18 ++- tests/test_tools/test_tools_noveldetails.py | 136 ++++++++++++++++++++ tests/tools.py | 37 +++--- 3 files changed, 168 insertions(+), 23 deletions(-) create mode 100644 tests/test_tools/test_tools_noveldetails.py diff --git a/tests/test_dialogs/test_dlg_preferences.py b/tests/test_dialogs/test_dlg_preferences.py index c4c2e590..408865c7 100644 --- a/tests/test_dialogs/test_dlg_preferences.py +++ b/tests/test_dialogs/test_dlg_preferences.py @@ -22,9 +22,11 @@ from __future__ import annotations import pytest +from tools import getGuiItem + from PyQt5.QtGui import QFontDatabase, QKeyEvent from PyQt5.QtCore import QEvent, Qt -from PyQt5.QtWidgets import QDialogButtonBox, QFileDialog, QFontDialog +from PyQt5.QtWidgets import QAction, QDialogButtonBox, QFileDialog, QFontDialog from novelwriter import CONFIG, SHARED from novelwriter.constants import nwConst, nwUnicode @@ -38,9 +40,13 @@ KEY_DELAY = 1 def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, tstPaths): """Test the preferences dialog loading.""" monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")]) + monkeypatch.setattr(GuiPreferences, "exec_", lambda *a: None) # Load GUI with standard values - prefs = GuiPreferences(nwGUI) + nwGUI.mainMenu.aPreferences.activate(QAction.ActionEvent.Trigger) + qtbot.waitUntil(lambda: getGuiItem("GuiPreferences") is not None, timeout=1000) + prefs = getGuiItem("GuiPreferences") + assert isinstance(prefs, GuiPreferences) prefs.show() # Check Languages @@ -95,10 +101,6 @@ def testDlgPreferences_Actions(qtbot, monkeypatch, nwGUI): # Check Navigation vBar = prefs.mainForm.verticalScrollBar() old = -1 - with qtbot.waitSignal(vBar.valueChanged) as value: - prefs.sidebar.button(0).click() - assert value.args[0] > old - old = value.args[0] with qtbot.waitSignal(vBar.valueChanged) as value: prefs.sidebar.button(1).click() assert value.args[0] > old @@ -107,6 +109,10 @@ def testDlgPreferences_Actions(qtbot, monkeypatch, nwGUI): prefs.sidebar.button(2).click() assert value.args[0] > old old = value.args[0] + with qtbot.waitSignal(vBar.valueChanged) as value: + prefs.sidebar.button(3).click() + assert value.args[0] > old + old = value.args[0] # Check Search prefs.searchText.setText("Display language") diff --git a/tests/test_tools/test_tools_noveldetails.py b/tests/test_tools/test_tools_noveldetails.py new file mode 100644 index 00000000..c8925e53 --- /dev/null +++ b/tests/test_tools/test_tools_noveldetails.py @@ -0,0 +1,136 @@ +""" +novelWriter – Novel Details Tool Tester +======================================= + +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 + +import pytest + +from tools import getGuiItem + +from PyQt5.QtWidgets import QAction + +from novelwriter import SHARED +from novelwriter.enum import nwItemClass +from novelwriter.tools.noveldetails import GuiNovelDetails + + +@pytest.mark.gui +def testToolNovelDetails_Main(qtbot, nwGUI, prjLipsum, ipsumText): + """Test the Novel Details main dialog.""" + nwGUI.openProject(prjLipsum) + nHandle = "b3643d0f92e32" + + # Add a second Novel folder + project = SHARED.project + secondText = "#! Second\n\n" + "\n\n".join(ipsumText) + sHandle = project.newRoot(nwItemClass.NOVEL, "Second") + dHandle = project.newFile("Document", sHandle) + project.storage.getDocument(dHandle).writeDocument(secondText) + project.index.reIndexHandle(dHandle) + nwGUI.projView.projTree.revealNewTreeItem(sHandle) + nwGUI.projView.projTree.revealNewTreeItem(dHandle) + + # Create the dialog + nwGUI.mainMenu.aNovelDetails.activate(QAction.ActionEvent.Trigger) + qtbot.waitUntil(lambda: getGuiItem("GuiNovelDetails") is not None, timeout=1000) + details = getGuiItem("GuiNovelDetails") + assert isinstance(details, GuiNovelDetails) + + # Overview Page + # ============= + overview = details.overviewPage + + # The selector should default to the first entry + assert details.novelSelector.handle == nHandle + + # Check project data + assert overview.projName.text() == "Lorem Ipsum" + assert overview.projWords.text() == f"{4376:n}" + assert overview.projNovels.text() == f"{3638:n}" + assert overview.projNotes.text() == f"{738:n}" + assert overview.projRevisions.text() != "" + assert overview.projEditTime.text() != "" + + # Check novel data for "Novel" + assert overview.novelName.text() == "Novel" + assert overview.novelWords.text() == f"{3000:n}" + assert overview.novelChapters.text() == f"{3:n}" + assert overview.novelScenes.text() == f"{5:n}" + + # Check novel data for "Second" + details.novelSelector.setHandle(sHandle, blockSignal=False) + assert overview.novelName.text() == "Second" + assert overview.novelWords.text() == f"{529:n}" + assert overview.novelChapters.text() == f"{0:n}" + assert overview.novelScenes.text() == f"{0:n}" + + # Contents Page + # ============= + details.novelSelector.setHandle(nHandle, blockSignal=False) + details.sidebar.button(details.PAGE_CONTENTS).click() + assert details.mainStack.currentIndex() == 1 + contents = details.contentsPage + + # Check defaults + words = [f"{v:n}" for v in [40, 176, 92, 6, 1071, 1615, 0]] + pages = [f"{v:n}" for v in [2, 2, 2, 2, 4, 6, 0]] + page = [f"{v:n}" for v in [1, 3, 5, 7, 9, 13, 19]] + for i in range(6): + item = contents.tocTree.topLevelItem(i) + assert item is not None + assert item.text(contents.C_WORDS) == words[i] + assert item.text(contents.C_PAGES) == pages[i] + assert item.text(contents.C_PAGE) == page[i] + + # Change Settings + contents.poValue.setValue(7) + contents.wpValue.setValue(50) + words = [f"{v:n}" for v in [40, 176, 92, 6, 1071, 1615, 0]] + pages = [f"{v:n}" for v in [2, 4, 2, 2, 22, 34, 0]] + page = ["i", "iii"] + [f"{v:n}" for v in [1, 3, 5, 27, 61]] + for i in range(6): + item = contents.tocTree.topLevelItem(i) + assert item is not None + assert item.text(contents.C_WORDS) == words[i] + assert item.text(contents.C_PAGES) == pages[i] + assert item.text(contents.C_PAGE) == page[i] + + # Turn off use odd pages + contents.dblValue.setChecked(False) + contents.poValue.setValue(0) + contents.wpValue.setValue(100) + words = [f"{v:n}" for v in [40, 176, 92, 6, 1071, 1615, 0]] + pages = [f"{v:n}" for v in [1, 2, 1, 1, 11, 17, 0]] + page = [f"{v:n}" for v in [1, 2, 4, 5, 6, 17, 34]] + for i in range(6): + item = contents.tocTree.topLevelItem(i) + assert item is not None + assert item.text(contents.C_WORDS) == words[i] + assert item.text(contents.C_PAGES) == pages[i] + assert item.text(contents.C_PAGE) == page[i] + + # Revert to Overview page + details.sidebar.button(details.PAGE_OVERVIEW).click() + assert details.mainStack.currentIndex() == 0 + + # qtbot.stop() + details.close() + +# END Test testToolNovelDetails_Main diff --git a/tests/tools.py b/tests/tools.py index 6bc21966..51cdcc19 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -156,20 +156,23 @@ def cleanProject(path: str | Path): return -def buildTestProject(obj, projPath): +def buildTestProject(obj: object, projPath: Path) -> None: """Build a standard test project in projPath using the project object as the parent. """ from novelwriter.enum import nwItemClass + from novelwriter.guimain import GuiMain from novelwriter.core.project import NWProject if isinstance(obj, NWProject): nwGUI = None project = obj - else: + elif isinstance(obj, GuiMain): from novelwriter import SHARED nwGUI = obj project = SHARED.project + else: + return project.storage.createNewProject(projPath) project.setDefaultStatusImport() @@ -181,27 +184,27 @@ def buildTestProject(obj, projPath): # Creating a minimal project with a few root folders and a # single chapter folder with a single file. - xHandle = {} - xHandle[1] = project.newRoot(nwItemClass.NOVEL, "Novel") - xHandle[2] = project.newRoot(nwItemClass.PLOT, "Plot") - xHandle[3] = project.newRoot(nwItemClass.CHARACTER, "Characters") - xHandle[4] = project.newRoot(nwItemClass.WORLD, "World") - xHandle[5] = project.newFile("Title Page", xHandle[1]) - xHandle[6] = project.newFolder("New Chapter", xHandle[1]) - xHandle[7] = project.newFile("New Chapter", xHandle[6]) - xHandle[8] = project.newFile("New Scene", xHandle[6]) + nrHandle = project.newRoot(nwItemClass.NOVEL, "Novel") + project.newRoot(nwItemClass.PLOT, "Plot") + project.newRoot(nwItemClass.CHARACTER, "Characters") + project.newRoot(nwItemClass.WORLD, "World") - aDoc = project.storage.getDocument(xHandle[5]) + tdHandle = project.newFile("Title Page", nrHandle) + cfHandle = project.newFolder("New Chapter", nrHandle) or "" + cdHandle = project.newFile("New Chapter", cfHandle) + sdHandle = project.newFile("New Scene", cfHandle) + + aDoc = project.storage.getDocument(tdHandle) aDoc.writeDocument("#! New Novel\n\n>> By Jane Doe <<\n") - project.index.reIndexHandle(xHandle[5]) + project.index.reIndexHandle(tdHandle) - aDoc = project.storage.getDocument(xHandle[7]) + aDoc = project.storage.getDocument(cdHandle) aDoc.writeDocument("## %s\n\n" % project.tr("New Chapter")) - project.index.reIndexHandle(xHandle[7]) + project.index.reIndexHandle(cdHandle) - aDoc = project.storage.getDocument(xHandle[8]) + aDoc = project.storage.getDocument(sdHandle) aDoc.writeDocument("### %s\n\n" % project.tr("New Scene")) - project.index.reIndexHandle(xHandle[8]) + project.index.reIndexHandle(sdHandle) project.session.startSession() project.setProjectChanged(True) From 10012054be105f0a3e107c264345fef6e409948d Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 25 Jan 2024 23:07:10 +0100 Subject: [PATCH 18/34] Remove some unused code --- novelwriter/extensions/configlayout.py | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/novelwriter/extensions/configlayout.py b/novelwriter/extensions/configlayout.py index 0ba9b259..c71036be 100644 --- a/novelwriter/extensions/configlayout.py +++ b/novelwriter/extensions/configlayout.py @@ -197,14 +197,6 @@ class NConfigLayout(QGridLayout): self._fontScale = scale return - def setHelpText(self, row: int, text: str) -> None: - """Set the text for the help label.""" - if row in self._itemMap: - qHelp = self._itemMap[row][1] - if isinstance(qHelp, NColourLabel): - qHelp.setText(text) - return - ## # Class Methods ## @@ -310,17 +302,9 @@ class NSimpleLayout(QGridLayout): wSp = CONFIG.pxInt(8) qLabel = QLabel(label) qLabel.setIndent(wSp) - self.addWidget(qLabel, self._nextRow, 0, 1, 1, LEFT_TOP) - - 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) - qLabel.setBuddy(widget) - + self.addWidget(qLabel, self._nextRow, 0, 1, 1, LEFT_TOP) + self.addWidget(widget, self._nextRow, 1, 1, 1, RIGHT_TOP) self.setRowStretch(self._nextRow, 0) self.setRowStretch(self._nextRow+1, 1) self._nextRow += 1 From df4993c38ec47a8a80adb1901c3d5a96f36eed33 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 25 Jan 2024 23:20:47 +0100 Subject: [PATCH 19/34] Drop outdated info in docstring --- novelwriter/guimain.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index ad29b741..795c058a 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -79,13 +79,6 @@ class GuiMain(QMainWindow): function. Also, the project instance and theme instance are created here. These should be passed around to all other objects who need them and new instances of them should generally not be created. - - * All other GUI classes that depend on any components from the - main GUI should be passed a reference to the instance of this - class. - * All non-GUI classes can be passed a reference to the NWProject - instance if the Main GUI is not needed (which it generally - shouldn't need). """ def __init__(self) -> None: From ee21045ee2953c07cbe37149d26a4c430b7f633f Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jan 2024 19:50:17 +0100 Subject: [PATCH 20/34] Update the design on the Project Settings dialog --- .../{projsettings.py => projectsettings.py} | 318 ++++++++++-------- novelwriter/extensions/configlayout.py | 18 +- novelwriter/gui/projtree.py | 6 +- novelwriter/guimain.py | 12 +- 4 files changed, 195 insertions(+), 159 deletions(-) rename novelwriter/dialogs/{projsettings.py => projectsettings.py} (72%) diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projectsettings.py similarity index 72% rename from novelwriter/dialogs/projsettings.py rename to novelwriter/dialogs/projectsettings.py index 8a9354a0..51efce1b 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,102 @@ 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, QLabel, + 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, 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 + wW = CONFIG.pxInt(500) + wH = CONFIG.pxInt(400) + options = SHARED.project.options - self.setMinimumWidth(wW) - self.setMinimumHeight(wH) + self.setMinimumSize(wW, wH) self.resize( - CONFIG.pxInt(pOptions.getInt("GuiProjectSettings", "winWidth", wW)), - CONFIG.pxInt(pOptions.getInt("GuiProjectSettings", "winHeight", wH)) + CONFIG.pxInt(options.getInt("GuiProjectSettings", "winWidth", wW)), + CONFIG.pxInt(options.getInt("GuiProjectSettings", "winHeight", wH)) ) - 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=1.25 + ) + self.titleLabel.setIndent(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 + 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 +131,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 +146,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.colChanged: + newList, delList = self.statusPage.getNewList() project.setStatusColours(newList, delList) + rebuildTrees = True - if self.tabImport.colChanged: - newList, delList = self.tabImport.getNewList() + if self.importPage.colChanged: + 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.arChanged: + newList = self.replacePage.getNewList() project.data.setAutoReplace(newList) - self.newProjectSettingsReady.emit() + self.newProjectSettingsReady.emit(rebuildTrees) qApp.processEvents() self.close() @@ -157,134 +201,104 @@ 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) - - self.mainForm.addGroupLabel(self.tr("Project Settings")) - xW = CONFIG.pxInt(250) - pData = SHARED.project.data + data = SHARED.project.data + self.setHelpTextStyle(SHARED.theme.helpText) + self.setRowIndent(0) - self.editName = QLineEdit() - self.editName.setMaxLength(200) - self.editName.setMaximumWidth(xW) - self.editName.setText(pData.name) - self.mainForm.addRow( + # Project Name + self.projName = QLineEdit(self) + self.projName.setMaxLength(200) + self.projName.setMaximumWidth(xW) + self.projName.setText(data.name) + self.addRow( self.tr("Project name"), - self.editName, - self.tr("Should be set only once.") + self.projName, + self.tr("Changing this will affect the backup path.") ) - 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( + # Project Author + self.projAuthor = QLineEdit(self) + self.projAuthor.setMaxLength(200) + self.projAuthor.setMaximumWidth(xW) + self.projAuthor.setText(data.author) + self.addRow( self.tr("Author(s)"), - self.editAuthor, - self.tr("Change whenever you want!") + self.projAuthor, + self.tr("Only used when building the manuscript.") ) + # Project Language self.projLang = QComboBox(self) self.projLang.setMaximumWidth(xW) for tag, language in CONFIG.listLanguages(CONFIG.LANG_PROJ): self.projLang.addItem(language, tag) - self.mainForm.addRow( + self.addRow( self.tr("Project language"), self.projLang, - self.tr("Used when building the manuscript.") + self.tr("Only used when building the manuscript.") ) + 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.addItem(self.tr("Default"), "None") if CONFIG.hasEnchant: for tag, language in SHARED.spelling.listDictionaries(): self.spellLang.addItem(language, tag) - self.mainForm.addRow( + self.addRow( self.tr("Spell check language"), self.spellLang, self.tr("Overrides main preferences.") ) + 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.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(QWidget): COL_LABEL = 0 COL_USAGE = 1 @@ -318,7 +332,7 @@ class GuiProjectEditStatus(QWidget): # The List # ======== - self.listBox = QTreeWidget() + self.listBox = QTreeWidget(self) self.listBox.setHeaderLabels([ self.tr("Label"), self.tr("Usage"), ]) @@ -332,16 +346,16 @@ class GuiProjectEditStatus(QWidget): # 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 @@ -389,6 +403,7 @@ class GuiProjectEditStatus(QWidget): self.outerBox = QVBoxLayout() self.outerBox.addWidget(QLabel("%s" % pageLabel)) self.outerBox.addLayout(self.innerBox) + self.outerBox.setContentsMargins(0, 0, 0, 0) self.setLayout(self.outerBox) @@ -410,6 +425,10 @@ class GuiProjectEditStatus(QWidget): return [], [] + def columnWidth(self) -> int: + """Return the size of the header column.""" + return self.listBox.columnWidth(0) + ## # Private Slots ## @@ -553,10 +572,10 @@ class GuiProjectEditStatus(QWidget): else: return self.tr("Used by {0} items").format(nUse) -# END Class GuiProjectEditStatus +# END Class _StatusPage -class GuiProjectEditReplace(QWidget): +class _ReplacePage(QWidget): COL_KEY = 0 COL_REPL = 1 @@ -638,6 +657,7 @@ class GuiProjectEditReplace(QWidget): self.outerBox = QVBoxLayout() self.outerBox.addWidget(QLabel("%s" % pageLabel)) self.outerBox.addLayout(self.innerBox) + self.outerBox.setContentsMargins(0, 0, 0, 0) self.setLayout(self.outerBox) @@ -655,6 +675,10 @@ class GuiProjectEditReplace(QWidget): new[aKey] = aVal return new + def columnWidth(self) -> int: + """Return the size of the header column.""" + return self.listBox.columnWidth(0) + ## # Internal Functions ## @@ -725,4 +749,4 @@ class GuiProjectEditReplace(QWidget): result += c return result -# END Class GuiProjectEditReplace +# END Class _ReplacePage diff --git a/novelwriter/extensions/configlayout.py b/novelwriter/extensions/configlayout.py index c71036be..2db50ded 100644 --- a/novelwriter/extensions/configlayout.py +++ b/novelwriter/extensions/configlayout.py @@ -50,6 +50,7 @@ class NScrollableForm(QScrollArea): self._helpCol = QColor(0, 0, 0) self._fontScale = FONT_SCALE self._first = True + self._indent = CONFIG.pxInt(12) self._sections: dict[int, QLabel] = {} self._editable: dict[str, NColourLabel] = {} @@ -92,6 +93,16 @@ 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 + + def setLayoutMargins(self, left: int, top: int, right: int, bottom: int) -> None: + """Set the contents margins on the layout.""" + self._layout.setContentsMargins(left, top, right, bottom) + return + ## # Methods ## @@ -128,14 +139,13 @@ class NScrollableForm(QScrollArea): row = QHBoxLayout() row.setSpacing(CONFIG.pxInt(4)) - 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.setIndent(self._indent) labelBox = QVBoxLayout() labelBox.addWidget(qLabel) labelBox.addWidget(qHelp) @@ -147,7 +157,7 @@ class NScrollableForm(QScrollArea): else: row.addWidget(qLabel) - row.addSpacing(mPx) + row.addSpacing(CONFIG.pxInt(12)) row.addWidget(widget) if isinstance(unit, str): 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() From c6ee6746a295ca05fdc59ef0497c7d5635757b18 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jan 2024 19:51:31 +0100 Subject: [PATCH 21/34] Make layout improvements to Project Settings --- novelwriter/dialogs/projectsettings.py | 170 ++++++++++++------------- novelwriter/extensions/configlayout.py | 24 ++-- 2 files changed, 97 insertions(+), 97 deletions(-) diff --git a/novelwriter/dialogs/projectsettings.py b/novelwriter/dialogs/projectsettings.py index 51efce1b..57bd4237 100644 --- a/novelwriter/dialogs/projectsettings.py +++ b/novelwriter/dialogs/projectsettings.py @@ -177,17 +177,17 @@ class GuiProjectSettings(QDialog): rebuildTrees = False - if self.statusPage.colChanged: + if self.statusPage.wasChanged: newList, delList = self.statusPage.getNewList() project.setStatusColours(newList, delList) rebuildTrees = True - if self.importPage.colChanged: + if self.importPage.wasChanged: newList, delList = self.importPage.getNewList() project.setImportColours(newList, delList) rebuildTrees = True - if self.replacePage.arChanged: + if self.replacePage.wasChanged: newList = self.replacePage.getNewList() project.data.setAutoReplace(newList) @@ -227,7 +227,7 @@ class _SettingsPage(NScrollableForm): def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) - xW = CONFIG.pxInt(250) + xW = CONFIG.pxInt(200) data = SHARED.project.data self.setHelpTextStyle(SHARED.theme.helpText) self.setRowIndent(0) @@ -235,23 +235,23 @@ class _SettingsPage(NScrollableForm): # Project Name self.projName = QLineEdit(self) self.projName.setMaxLength(200) - self.projName.setMaximumWidth(xW) + 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.") + 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.setMaximumWidth(xW) + 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.") + self.tr("Author(s)"), self.projAuthor, + self.tr("Only used when building the manuscript."), + stretch=(2, 1) ) # Project Language @@ -260,8 +260,7 @@ class _SettingsPage(NScrollableForm): for tag, language in CONFIG.listLanguages(CONFIG.LANG_PROJ): self.projLang.addItem(language, tag) self.addRow( - self.tr("Project language"), - self.projLang, + self.tr("Project language"), self.projLang, self.tr("Only used when building the manuscript.") ) if (idx := self.projLang.findData(data.language)) != -1: @@ -275,8 +274,7 @@ class _SettingsPage(NScrollableForm): for tag, language in SHARED.spelling.listDictionaries(): self.spellLang.addItem(language, tag) self.addRow( - self.tr("Spell check language"), - self.spellLang, + self.tr("Spell check language"), self.spellLang, self.tr("Overrides main preferences.") ) if (idx := self.spellLang.findData(data.spellLang)) != -1: @@ -286,8 +284,7 @@ class _SettingsPage(NScrollableForm): self.doBackup = NSwitch(self) self.doBackup.setChecked(not data.doBackup) self.addRow( - self.tr("Disable backup on close"), - self.doBackup, + self.tr("Disable backup on close"), self.doBackup, self.tr("Overrides main preferences.") ) @@ -323,9 +320,9 @@ class _StatusPage(QWidget): 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 @@ -344,8 +341,6 @@ class _StatusPage(QWidget): self._addItem(key, entry["name"], entry["cols"], entry["count"]) # List Controls - # ============= - self.addButton = QPushButton(SHARED.theme.getIcon("add"), "", self) self.addButton.clicked.connect(self._newItem) @@ -359,8 +354,6 @@ class _StatusPage(QWidget): 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")) @@ -378,8 +371,6 @@ class _StatusPage(QWidget): self.saveButton.clicked.connect(self._saveItem) # Assemble - # ======== - self.listControls = QVBoxLayout() self.listControls.addWidget(self.addButton) self.listControls.addWidget(self.delButton) @@ -409,9 +400,18 @@ class _StatusPage(QWidget): 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) @@ -421,7 +421,7 @@ class _StatusPage(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 [], [] @@ -436,12 +436,12 @@ class _StatusPage(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)) @@ -452,7 +452,7 @@ class _StatusPage(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() @@ -465,8 +465,8 @@ class _StatusPage(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() @@ -477,9 +477,9 @@ class _StatusPage(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() @@ -493,7 +493,7 @@ class _StatusPage(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() @@ -504,7 +504,7 @@ class _StatusPage(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) @@ -552,7 +552,7 @@ class _StatusPage(QWidget): if cItem is not None: cItem.setSelected(True) - self.colChanged = True + self._changed = True return @@ -583,7 +583,7 @@ class _ReplacePage(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) @@ -591,16 +591,11 @@ class _ReplacePage(QWidget): pageLabel = self.tr("Text Replace List for Preview and Export") # 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]) @@ -610,8 +605,6 @@ class _ReplacePage(QWidget): self.listBox.setSortingEnabled(True) # List Controls - # ============= - self.addButton = QPushButton(SHARED.theme.getIcon("add"), "") self.addButton.clicked.connect(self._addEntry) @@ -619,8 +612,6 @@ class _ReplacePage(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) @@ -634,8 +625,6 @@ class _ReplacePage(QWidget): self.saveButton.clicked.connect(self._saveEntry) # Assemble - # ======== - self.listControls = QVBoxLayout() self.listControls.addWidget(self.addButton) self.listControls.addWidget(self.delButton) @@ -663,12 +652,20 @@ class _ReplacePage(QWidget): 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: @@ -680,30 +677,29 @@ class _ReplacePage(QWidget): 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) @@ -715,38 +711,36 @@ class _ReplacePage(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 _ReplacePage diff --git a/novelwriter/extensions/configlayout.py b/novelwriter/extensions/configlayout.py index 2db50ded..39e1e651 100644 --- a/novelwriter/extensions/configlayout.py +++ b/novelwriter/extensions/configlayout.py @@ -134,10 +134,11 @@ 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)) qLabel = QLabel(label, self) qLabel.setIndent(self._indent) @@ -151,19 +152,24 @@ class NScrollableForm(QScrollArea): 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(CONFIG.pxInt(12)) - 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 From 0dc47194007b27953d7de2fac3d695ee8d2546f5 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jan 2024 19:51:54 +0100 Subject: [PATCH 22/34] Fix log formatting for longer file names --- novelwriter/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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": From 3761b1729f2f4e466beddb65a69a82c95725dc70 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jan 2024 21:31:14 +0100 Subject: [PATCH 23/34] Make the style of config dialogs consistent --- novelwriter/dialogs/preferences.py | 4 +- novelwriter/dialogs/projectsettings.py | 82 ++++++++++++----------- novelwriter/extensions/configlayout.py | 91 ++++++++++++++++++++------ novelwriter/tools/noveldetails.py | 52 ++++++++------- 4 files changed, 144 insertions(+), 85 deletions(-) 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/projectsettings.py b/novelwriter/dialogs/projectsettings.py index 57bd4237..b7f50f24 100644 --- a/novelwriter/dialogs/projectsettings.py +++ b/novelwriter/dialogs/projectsettings.py @@ -29,15 +29,15 @@ import logging from PyQt5.QtGui import QCloseEvent, QColor, QIcon, QPixmap from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot from PyQt5.QtWidgets import ( - QColorDialog, QComboBox, QDialog, QDialogButtonBox, QHBoxLayout, QLabel, - QLineEdit, QPushButton, QStackedWidget, 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.configlayout import NColourLabel, NScrollableForm +from novelwriter.extensions.configlayout import NColourLabel, NFixedPage, NScrollableForm from novelwriter.extensions.pagedsidebar import NPagedSideBar logger = logging.getLogger(__name__) @@ -59,21 +59,18 @@ class GuiProjectSettings(QDialog): self.setObjectName("GuiProjectSettings") self.setWindowTitle(self.tr("Project Settings")) - 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("GuiProjectSettings", "winWidth", wW)), - CONFIG.pxInt(options.getInt("GuiProjectSettings", "winHeight", wH)) + CONFIG.pxInt(options.getInt("GuiProjectSettings", "winWidth", CONFIG.pxInt(650))), + CONFIG.pxInt(options.getInt("GuiProjectSettings", "winHeight", CONFIG.pxInt(500))) ) # Title self.titleLabel = NColourLabel( - self.tr("Project Settings"), SHARED.theme.helpText, parent=self, scale=1.25 + self.tr("Project Settings"), SHARED.theme.helpText, + parent=self, scale=NColourLabel.HEADER_SCALE, indent=CONFIG.pxInt(4) ) - self.titleLabel.setIndent(CONFIG.pxInt(4)) # SideBar self.sidebar = NPagedSideBar(self) @@ -251,31 +248,33 @@ class _SettingsPage(NScrollableForm): self.addRow( self.tr("Author(s)"), self.projAuthor, self.tr("Only used when building the manuscript."), - stretch=(2, 1) + 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.addRow( self.tr("Project language"), self.projLang, - self.tr("Only used when building the manuscript.") + self.tr("Only used when building the manuscript."), + stretch=(3, 2) ) if (idx := self.projLang.findData(data.language)) != -1: self.projLang.setCurrentIndex(idx) # 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.addRow( self.tr("Spell check language"), self.spellLang, - self.tr("Overrides main preferences.") + self.tr("Overrides main preferences."), + stretch=(3, 2) ) if (idx := self.spellLang.findData(data.spellLang)) != -1: self.spellLang.setCurrentIndex(idx) @@ -295,7 +294,7 @@ class _SettingsPage(NScrollableForm): # END Class _SettingsPage -class _StatusPage(QWidget): +class _StatusPage(NFixedPage): COL_LABEL = 0 COL_USAGE = 1 @@ -326,9 +325,13 @@ class _StatusPage(QWidget): self.iPx = SHARED.theme.baseIconSize - # The List - # ======== + # Title + self.pageTitle = NColourLabel( + pageLabel, SHARED.theme.helpText, parent=self, + scale=NColourLabel.HEADER_SCALE + ) + # List Box self.listBox = QTreeWidget(self) self.listBox.setHeaderLabels([ self.tr("Label"), self.tr("Usage"), @@ -392,11 +395,10 @@ class _StatusPage(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.outerBox.setContentsMargins(0, 0, 0, 0) - self.setLayout(self.outerBox) + self.setCentralLayout(self.outerBox) return @@ -422,7 +424,6 @@ class _StatusPage(QWidget): "cols": item.data(self.COL_LABEL, self.COL_ROLE), }) return newList, self._colDeleted - return [], [] def columnWidth(self) -> int: @@ -517,16 +518,16 @@ class _StatusPage(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)) @@ -558,24 +559,23 @@ class _StatusPage(QWidget): 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 _StatusPage -class _ReplacePage(QWidget): +class _ReplacePage(NFixedPage): COL_KEY = 0 COL_REPL = 1 @@ -588,7 +588,12 @@ class _ReplacePage(QWidget): 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() @@ -644,11 +649,10 @@ class _ReplacePage(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.outerBox.setContentsMargins(0, 0, 0, 0) - self.setLayout(self.outerBox) + self.setCentralLayout(self.outerBox) return diff --git a/novelwriter/extensions/configlayout.py b/novelwriter/extensions/configlayout.py index 39e1e651..809068bc 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, QLineEdit, + 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,7 +92,7 @@ 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) @@ -66,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 @@ -81,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 @@ -145,13 +191,14 @@ class NScrollableForm(QScrollArea): qLabel.setBuddy(widget) if helpText: - qHelp = NColourLabel(str(helpText), self._helpCol, scale=self._fontScale, wrap=True) - qHelp.setIndent(self._indent) + 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, stretch[0]) if editable: self._editable[editable] = qHelp @@ -193,7 +240,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) @@ -207,7 +254,7 @@ class NConfigLayout(QGridLayout): # Getters and 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 if isinstance(color, QColor) else QColor(*color) self._fontScale = scale @@ -238,8 +285,10 @@ class NConfigLayout(QGridLayout): qHelp = None if helpText is not None: - qHelp = NColourLabel(str(helpText), self._helpCol, scale=self._fontScale, wrap=True) - qHelp.setIndent(wSp) + qHelp = NColourLabel( + str(helpText), self._helpCol, + scale=self._fontScale, wrap=True, indent=wSp + ) labelBox = QVBoxLayout() labelBox.addWidget(qLabel) labelBox.addWidget(qHelp) @@ -337,17 +386,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/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()) From a7a2eaff557df34be89a4c7a38f3a9a899807548 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jan 2024 21:36:05 +0100 Subject: [PATCH 24/34] Delete the old config dialog base class --- novelwriter/extensions/pageddialog.py | 130 -------------------------- 1 file changed, 130 deletions(-) delete mode 100644 novelwriter/extensions/pageddialog.py 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 From 40a8d728bb205c912349c67b8901347c9d2be494 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jan 2024 22:32:20 +0100 Subject: [PATCH 25/34] Update project settings tests --- novelwriter/dialogs/projectsettings.py | 16 +- ...ettings.py => test_dlg_projectsettings.py} | 285 +++++++++--------- 2 files changed, 143 insertions(+), 158 deletions(-) rename tests/test_dialogs/{test_dlg_projsettings.py => test_dlg_projectsettings.py} (50%) diff --git a/novelwriter/dialogs/projectsettings.py b/novelwriter/dialogs/projectsettings.py index b7f50f24..f6ff7730 100644 --- a/novelwriter/dialogs/projectsettings.py +++ b/novelwriter/dialogs/projectsettings.py @@ -90,6 +90,8 @@ class GuiProjectSettings(QDialog): self.buttonBox.rejected.connect(self.close) # Content + SHARED.project.countStatus() + self.settingsPage = _SettingsPage(self) self.statusPage = _StatusPage(self, True) self.importPage = _StatusPage(self, False) @@ -307,12 +309,12 @@ class _StatusPage(NFixedPage): 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( @@ -333,14 +335,12 @@ class _StatusPage(NFixedPage): # List Box self.listBox = QTreeWidget(self) - self.listBox.setHeaderLabels([ - self.tr("Label"), self.tr("Usage"), - ]) + 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 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" } From 4c5f68242aab7b480e65aa91eb02c7adf0b331f1 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jan 2024 22:32:39 +0100 Subject: [PATCH 26/34] Remove unused code in old config layout classes --- novelwriter/extensions/configlayout.py | 42 +++----------------------- 1 file changed, 5 insertions(+), 37 deletions(-) diff --git a/novelwriter/extensions/configlayout.py b/novelwriter/extensions/configlayout.py index 809068bc..99d38c03 100644 --- a/novelwriter/extensions/configlayout.py +++ b/novelwriter/extensions/configlayout.py @@ -30,7 +30,7 @@ from __future__ import annotations from PyQt5.QtGui import QColor, QPalette from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( - QAbstractButton, QFrame, QGridLayout, QHBoxLayout, QLabel, QLayout, QLineEdit, + QAbstractButton, QFrame, QGridLayout, QHBoxLayout, QLabel, QLayout, QScrollArea, QSizePolicy, QVBoxLayout, QWidget ) @@ -144,11 +144,6 @@ class NScrollableForm(QScrollArea): self._indent = max(indent, 0) return - def setLayoutMargins(self, left: int, top: int, right: int, bottom: int) -> None: - """Set the contents margins on the layout.""" - self._layout.setContentsMargins(left, top, right, bottom) - return - ## # Methods ## @@ -250,16 +245,6 @@ class NConfigLayout(QGridLayout): return - ## - # Getters and Setters - ## - - def setHelpTextStyle(self, color: QColor, scale: float = DEFAULT_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 ## @@ -275,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) @@ -284,19 +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, indent=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() @@ -313,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) From 838a2a8062b9669297db22d762a6b360dc9c6d5e Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jan 2024 22:45:12 +0100 Subject: [PATCH 27/34] Drop the Novel Title data from the project and code --- novelwriter/core/coretools.py | 3 +-- novelwriter/core/projectdata.py | 15 +-------------- novelwriter/core/projectxml.py | 8 +++----- novelwriter/core/tohtml.py | 1 - novelwriter/core/tokenizer.py | 1 - novelwriter/core/toodt.py | 6 ++---- tests/tools.py | 1 - 7 files changed, 7 insertions(+), 28 deletions(-) diff --git a/novelwriter/core/coretools.py b/novelwriter/core/coretools.py index 029bf22d..c88369e8 100644 --- a/novelwriter/core/coretools.py +++ b/novelwriter/core/coretools.py @@ -374,7 +374,6 @@ class ProjectBuilder: project.data.setUuid(None) project.data.setName(projName) - project.data.setTitle(projName) project.data.setAuthor(projAuthor) project.data.setLanguage(projLang) project.setDefaultStatusImport() @@ -383,7 +382,7 @@ class ProjectBuilder: # Add Root Folders hNovelRoot = project.newRoot(nwItemClass.NOVEL) hTitlePage = project.newFile(lblTitlePage, hNovelRoot) - novelTitle = project.data.title if project.data.title else project.data.name + novelTitle = project.data.name titlePage = f"#! {novelTitle}\n\n" if project.data.author: diff --git a/novelwriter/core/projectdata.py b/novelwriter/core/projectdata.py index 4a6fcec1..07c16909 100644 --- a/novelwriter/core/projectdata.py +++ b/novelwriter/core/projectdata.py @@ -53,7 +53,6 @@ class NWProjectData: # Project Meta self._uuid = "" self._name = "" - self._title = "" self._author = "" self._saveCount = 0 self._autoCount = 0 @@ -102,11 +101,6 @@ class NWProjectData: """Return the project name.""" return self._name - @property - def title(self) -> str: - """Return the project title.""" - return self._title - @property def author(self) -> str: """Return the project author.""" @@ -228,16 +222,9 @@ class NWProjectData: self._project.setProjectChanged(True) return - def setTitle(self, value: str | None) -> None: - """Set a new novel title.""" - if value != self._title: - self._title = simplified(str(value or "")) - self._project.setProjectChanged(True) - return - def setAuthor(self, value: str | None) -> None: """Set the author value.""" - if value != self._title: + if value != self._author: self._author = simplified(str(value or "")) self._project.setProjectChanged(True) return diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py index 71e2608c..5c2b8d84 100644 --- a/novelwriter/core/projectxml.py +++ b/novelwriter/core/projectxml.py @@ -46,7 +46,7 @@ if TYPE_CHECKING: # pragma: no cover logger = logging.getLogger(__name__) FILE_VERSION = "1.5" # The current project file format version -FILE_REVISION = "1" # The current project file format revision +FILE_REVISION = "2" # The current project file format revision HEX_VERSION = 0x0105 NUM_VERSION = { @@ -106,7 +106,8 @@ class ProjectXMLReader: the project or the content into their respective section nodes as attributes. The id attribute was also added to the project. - Rev 1: Drops the titleFormat section of settings. + Rev 1: Drops the "titleFormat" section from "settings". + Rev 2: Drop the "title" setting from "project". """ def __init__(self, path: str | Path) -> None: @@ -247,8 +248,6 @@ class ProjectXMLReader: for xItem in xSection: if xItem.tag == "name": data.setName(xItem.text) - elif xItem.tag == "title": - data.setTitle(xItem.text) elif xItem.tag == "author": data.setAuthor(xItem.text) else: @@ -520,7 +519,6 @@ class ProjectXMLWriter: xProject = ET.SubElement(xRoot, "project", attrib=projAttr) self._packSingleValue(xProject, "name", data.name) - self._packSingleValue(xProject, "title", data.title) self._packSingleValue(xProject, "author", data.author) # Save Project Settings diff --git a/novelwriter/core/tohtml.py b/novelwriter/core/tohtml.py index c509a547..5598ece7 100644 --- a/novelwriter/core/tohtml.py +++ b/novelwriter/core/tohtml.py @@ -338,7 +338,6 @@ class ToHtml(Tokenizer): data = { "meta": { "projectName": self._project.data.name, - "novelTitle": self._project.data.title, "novelAuthor": self._project.data.author, "buildTime": int(timeStamp), "buildTimeStr": formatTimeStamp(timeStamp), diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py index f1421db1..5b222e73 100644 --- a/novelwriter/core/tokenizer.py +++ b/novelwriter/core/tokenizer.py @@ -759,7 +759,6 @@ class Tokenizer(ABC): data = { "meta": { "projectName": self._project.data.name, - "novelTitle": self._project.data.title, "novelAuthor": self._project.data.author, "buildTime": int(timeStamp), "buildTimeStr": formatTimeStamp(timeStamp), diff --git a/novelwriter/core/toodt.py b/novelwriter/core/toodt.py index b34a56bf..4409b595 100644 --- a/novelwriter/core/toodt.py +++ b/novelwriter/core/toodt.py @@ -283,9 +283,7 @@ class ToOdt(Tokenizer): # =============== if self._headerText == "": - theTitle = self._project.data.title or self._project.data.name - theAuth = self._project.data.author - self._headerText = f"{theTitle} / {theAuth} /" + self._headerText = f"{self._project.data.name} / {self._project.data.author} /" # Create Roots # ============ @@ -373,7 +371,7 @@ class ToOdt(Tokenizer): # Dublin Core Meta Data xMeta = ET.SubElement(self._xMeta, _mkTag("dc", "title")) - xMeta.text = self._project.data.title or self._project.data.name + xMeta.text = self._project.data.name xMeta = ET.SubElement(self._xMeta, _mkTag("dc", "date")) xMeta.text = timeStamp diff --git a/tests/tools.py b/tests/tools.py index 51cdcc19..2da23818 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -179,7 +179,6 @@ def buildTestProject(obj: object, projPath: Path) -> None: project.data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed") project.data.setName("New Project") - project.data.setTitle("New Novel") project.data.setAuthor("Jane Doe") # Creating a minimal project with a few root folders and a From de6fd6566432e9466facfccd6e97a4476a94833c Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jan 2024 22:45:28 +0100 Subject: [PATCH 28/34] Update sample project and tests --- sample/nwProject.nwx | 5 ++--- tests/test_core/test_core_project.py | 4 ---- tests/test_core/test_core_projectxml.py | 6 ------ tests/test_gui/test_gui_guimain.py | 2 -- 4 files changed, 2 insertions(+), 15 deletions(-) diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index af18a5a6..8d02b22d 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,8 +1,7 @@ - - + + Sample Project - Sample Project Jane Smith diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 538e179b..fdad6b77 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -482,10 +482,6 @@ def testCoreProject_Methods(monkeypatch, mockGUI, fncPath, mockRnd): project.data.setName(" A Name ") assert project.data.name == "A Name" - # Project Title - project.data.setTitle(" A Title ") - assert project.data.title == "A Title" - # Project Author project.data.setAuthor(" Jane\tDoe ") assert project.data.author == "Jane Doe" diff --git a/tests/test_core/test_core_projectxml.py b/tests/test_core/test_core_projectxml.py index 06dad380..19624e39 100644 --- a/tests/test_core/test_core_projectxml.py +++ b/tests/test_core/test_core_projectxml.py @@ -148,7 +148,6 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, tstPaths, fncPath): # Check loaded data assert data.name == "Sample Project" - assert data.title == "Sample Project" assert data.author == "Jane Smith" assert data.saveCount == 5 assert data.autoCount == 10 @@ -267,7 +266,6 @@ def testCoreProjectXML_ReadLegacy10(tstPaths, fncPath, mockRnd): # Check loaded data assert data.name == "Sample Project" - assert data.title == "Sample Project" assert data.author == "Jay Doh" # Only last author is preserved assert data.saveCount == 0 # Doesn't exist in 1.0 assert data.autoCount == 0 # Doesn't exist in 1.0 @@ -402,7 +400,6 @@ def testCoreProjectXML_ReadLegacy11(tstPaths, fncPath, mockRnd): # Check loaded data assert data.name == "Sample Project" - assert data.title == "Sample Project" assert data.author == "Jay Doh" # Only last author is preserved assert data.saveCount == 5 assert data.autoCount == 10 @@ -537,7 +534,6 @@ def testCoreProjectXML_ReadLegacy12(tstPaths, fncPath, mockRnd): # Check loaded data assert data.name == "Sample Project" - assert data.title == "Sample Project" assert data.author == "Jay Doh" # Only last author is preserved assert data.saveCount == 5 assert data.autoCount == 10 @@ -675,7 +671,6 @@ def testCoreProjectXML_ReadLegacy13(tstPaths, fncPath, mockRnd): # Check loaded data assert data.name == "Sample Project" - assert data.title == "Sample Project" assert data.author == "Jay Doh" # Only last author is preserved assert data.saveCount == 5 assert data.autoCount == 10 @@ -813,7 +808,6 @@ def testCoreProjectXML_ReadLegacy14(tstPaths, fncPath, mockRnd): # Check loaded data assert data.name == "Sample Project" - assert data.title == "Sample Project" assert data.author == "Jay Doh" # Only last author is preserved assert data.saveCount == 5 assert data.autoCount == 10 diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index cb06a470..5c15ed4a 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -200,7 +200,6 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): assert len(SHARED.project.tree._roots) == 0 assert SHARED.project.tree.trashRoot is None assert SHARED.project.data.name == "" - assert SHARED.project.data.title == "" assert SHARED.project.data.author == "" assert SHARED.project.data.spellCheck is False @@ -220,7 +219,6 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): assert len(SHARED.project.tree._roots) == 4 assert SHARED.project.tree.trashRoot is None assert SHARED.project.data.name == "New Project" - assert SHARED.project.data.title == "New Novel" assert SHARED.project.data.author == "Jane Doe" assert SHARED.project.data.spellCheck is False From 7a1fab0c0a5e98bc7db8aa138b7e08d339f4e933 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jan 2024 22:57:25 +0100 Subject: [PATCH 29/34] Update test reference files --- tests/files/nwProject-1.5.nwx | 3 +-- tests/reference/coreProject_NewFileFolder_nwProject.nwx | 3 +-- tests/reference/coreProject_NewRoot_nwProject.nwx | 3 +-- tests/reference/coreTools_DocDuplicator_nwProject.nwx | 3 +-- tests/reference/coreTools_ProjectBuilderA_nwProject.nwx | 3 +-- tests/reference/coreTools_ProjectBuilderB_nwProject.nwx | 3 +-- tests/reference/guiEditor_Main_Final_nwProject.nwx | 5 ++--- tests/reference/guiEditor_Main_Initial_nwProject.nwx | 3 +-- tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.json | 5 ++--- tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.json | 5 ++--- tests/reference/projectXML_ReadLegacy10.nwx | 3 +-- tests/reference/projectXML_ReadLegacy11.nwx | 3 +-- tests/reference/projectXML_ReadLegacy12.nwx | 3 +-- tests/reference/projectXML_ReadLegacy13.nwx | 3 +-- tests/reference/projectXML_ReadLegacy14.nwx | 3 +-- tests/test_core/test_core_docbuild.py | 4 ++-- tests/test_core/test_core_projectxml.py | 2 +- 17 files changed, 21 insertions(+), 36 deletions(-) diff --git a/tests/files/nwProject-1.5.nwx b/tests/files/nwProject-1.5.nwx index 1965de73..cc17f909 100644 --- a/tests/files/nwProject-1.5.nwx +++ b/tests/files/nwProject-1.5.nwx @@ -1,8 +1,7 @@ - + Sample Project - Sample Project Jane Smith diff --git a/tests/reference/coreProject_NewFileFolder_nwProject.nwx b/tests/reference/coreProject_NewFileFolder_nwProject.nwx index 6fd70fb2..99b5a57c 100644 --- a/tests/reference/coreProject_NewFileFolder_nwProject.nwx +++ b/tests/reference/coreProject_NewFileFolder_nwProject.nwx @@ -1,8 +1,7 @@ - + New Project - New Novel Jane Doe diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index 057e3742..028da14c 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,8 +1,7 @@ - + New Project - New Novel Jane Doe diff --git a/tests/reference/coreTools_DocDuplicator_nwProject.nwx b/tests/reference/coreTools_DocDuplicator_nwProject.nwx index 0ee99397..bab9a82d 100644 --- a/tests/reference/coreTools_DocDuplicator_nwProject.nwx +++ b/tests/reference/coreTools_DocDuplicator_nwProject.nwx @@ -1,8 +1,7 @@ - + New Project - New Novel Jane Doe diff --git a/tests/reference/coreTools_ProjectBuilderA_nwProject.nwx b/tests/reference/coreTools_ProjectBuilderA_nwProject.nwx index 02f11a29..ee5eeeea 100644 --- a/tests/reference/coreTools_ProjectBuilderA_nwProject.nwx +++ b/tests/reference/coreTools_ProjectBuilderA_nwProject.nwx @@ -1,8 +1,7 @@ - + Test Project A - Test Project A Jane Doe diff --git a/tests/reference/coreTools_ProjectBuilderB_nwProject.nwx b/tests/reference/coreTools_ProjectBuilderB_nwProject.nwx index 0df054a8..209c7809 100644 --- a/tests/reference/coreTools_ProjectBuilderB_nwProject.nwx +++ b/tests/reference/coreTools_ProjectBuilderB_nwProject.nwx @@ -1,8 +1,7 @@ - + Test Project B - Test Project B Jane Doe diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index c56d9782..c3ee941f 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,8 +1,7 @@ - - + + New Project - New Novel Jane Doe diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx index e266f65f..aa9c3a74 100644 --- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx @@ -1,8 +1,7 @@ - + New Project - New Novel Jane Doe diff --git a/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.json b/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.json index 429aff11..7c315e5a 100644 --- a/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.json +++ b/tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.json @@ -1,10 +1,9 @@ { "meta": { "projectName": "Lorem Ipsum", - "novelTitle": "Lorem Ipsum", "novelAuthor": "lipsum.com", - "buildTime": 1687620769, - "buildTimeStr": "2023-06-24 17:32:49" + "buildTime": 1706306020, + "buildTimeStr": "2024-01-26 22:53:40" }, "text": { "css": [ diff --git a/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.json b/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.json index 305b0006..06633aa2 100644 --- a/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.json +++ b/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.json @@ -1,10 +1,9 @@ { "meta": { "projectName": "Lorem Ipsum", - "novelTitle": "Lorem Ipsum", "novelAuthor": "lipsum.com", - "buildTime": 1687621222, - "buildTimeStr": "2023-06-24 17:40:22" + "buildTime": 1706306020, + "buildTimeStr": "2024-01-26 22:53:40" }, "text": { "nwd": [ diff --git a/tests/reference/projectXML_ReadLegacy10.nwx b/tests/reference/projectXML_ReadLegacy10.nwx index 9d04ef44..5161eb5b 100644 --- a/tests/reference/projectXML_ReadLegacy10.nwx +++ b/tests/reference/projectXML_ReadLegacy10.nwx @@ -1,8 +1,7 @@ - + Sample Project - Sample Project Jay Doh diff --git a/tests/reference/projectXML_ReadLegacy11.nwx b/tests/reference/projectXML_ReadLegacy11.nwx index 50924c78..7624a057 100644 --- a/tests/reference/projectXML_ReadLegacy11.nwx +++ b/tests/reference/projectXML_ReadLegacy11.nwx @@ -1,8 +1,7 @@ - + Sample Project - Sample Project Jay Doh diff --git a/tests/reference/projectXML_ReadLegacy12.nwx b/tests/reference/projectXML_ReadLegacy12.nwx index 7d36e380..58b29061 100644 --- a/tests/reference/projectXML_ReadLegacy12.nwx +++ b/tests/reference/projectXML_ReadLegacy12.nwx @@ -1,8 +1,7 @@ - + Sample Project - Sample Project Jay Doh diff --git a/tests/reference/projectXML_ReadLegacy13.nwx b/tests/reference/projectXML_ReadLegacy13.nwx index c75aa97c..28e24c69 100644 --- a/tests/reference/projectXML_ReadLegacy13.nwx +++ b/tests/reference/projectXML_ReadLegacy13.nwx @@ -1,8 +1,7 @@ - + Sample Project - Sample Project Jay Doh diff --git a/tests/reference/projectXML_ReadLegacy14.nwx b/tests/reference/projectXML_ReadLegacy14.nwx index aef603dd..26e7413c 100644 --- a/tests/reference/projectXML_ReadLegacy14.nwx +++ b/tests/reference/projectXML_ReadLegacy14.nwx @@ -1,8 +1,7 @@ - + Sample Project - Sample Project Jay Doh diff --git a/tests/test_core/test_core_docbuild.py b/tests/test_core/test_core_docbuild.py index 8890e2a8..b70269a8 100644 --- a/tests/test_core/test_core_docbuild.py +++ b/tests/test_core/test_core_docbuild.py @@ -230,7 +230,7 @@ def testCoreDocBuild_HTML(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths): assert error == [] copyfile(docFile, tstFile) - assert cmpFiles(tstFile, cmpFile, ignoreLines=[6, 7]) + assert cmpFiles(tstFile, cmpFile, ignoreLines=[5, 6]) # Check Error Handling # ==================== @@ -370,7 +370,7 @@ def testCoreDocBuild_NWD(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths): assert error == [] copyfile(docFile, tstFile) - assert cmpFiles(tstFile, cmpFile, ignoreLines=[6, 7]) + assert cmpFiles(tstFile, cmpFile, ignoreLines=[5, 6]) # Check Error Handling # ==================== diff --git a/tests/test_core/test_core_projectxml.py b/tests/test_core/test_core_projectxml.py index 19624e39..32458158 100644 --- a/tests/test_core/test_core_projectxml.py +++ b/tests/test_core/test_core_projectxml.py @@ -142,7 +142,7 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, tstPaths, fncPath): assert xmlReader.state == XMLReadState.PARSED_OK assert xmlReader.xmlRoot == "novelWriterXML" assert xmlReader.xmlVersion == 0x0105 - assert xmlReader.xmlRevision == 1 + assert xmlReader.xmlRevision == 2 assert xmlReader.appVersion == "2.0-rc1" assert xmlReader.hexVersion == 0x020000c1 From 119d572050d53523ad2415fbdc0f6d3c4e3d04d4 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jan 2024 23:00:36 +0100 Subject: [PATCH 30/34] Update file format spec document --- docs/FileFormatSpec-1.5.fodt | 743 +++++++++++++++---------------- docs/source/fileformatspec15.pdf | Bin 319906 -> 319730 bytes 2 files changed, 361 insertions(+), 382 deletions(-) diff --git a/docs/FileFormatSpec-1.5.fodt b/docs/FileFormatSpec-1.5.fodt index a57c8627..4e2d66ef 100644 --- a/docs/FileFormatSpec-1.5.fodt +++ b/docs/FileFormatSpec-1.5.fodt @@ -1,31 +1,30 @@ - Veronica Olsen2022-11-05T18:25:44.8874731592023-06-03T18:35:47.268848685Veronica OlsenPT4H19S53LibreOffice/7.4.5.1$Linux_X86_64 LibreOffice_project/40$Build-1 + Veronica Olsen2022-11-05T18:25:44.8874731592024-01-26T22:59:54.491819741Veronica OlsenPT4H2M7S55LibreOffice/7.4.7.2$Linux_X86_64 LibreOffice_project/40$Build-2 - 0 + 35971 0 - 35645 + 26083 20394 true false view2 - 15145 - 7216 + 4692 + 47456 0 - 0 - 35643 - 20392 + 35971 + 26081 + 56363 0 1 false 140 false false - false false @@ -91,7 +90,7 @@ true true - 805029 + 917862 true false @@ -117,7 +116,7 @@ false false - true + false false false false @@ -159,7 +158,7 @@ - + @@ -180,7 +179,7 @@ - + @@ -480,15 +479,6 @@ - - - - - - - - - @@ -1108,153 +1098,148 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1271,78 +1256,81 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + @@ -1420,17 +1408,20 @@ - novelWriter - Project File Format 1.5 Specification + novelWriter + Project File Format 1.5 Specification - This document covers the file format specification for the 1.5 file format for novelWriter project files. See the documentation1 - See: https://docs.novelwriter.io for a full description of the app’s functionality. The file format was intrioduced in Release 2.0. - The target audience of this document is developers who intend to write a tool or script that generates valid novelWriter project files from data from other applications, or for templating purposes. - Note that flags are generally written out as yes/no values, but also true/false and on/off are understood by the application. The None value that occasionally occurs in actual project files is the text representation of the Python None value, and indicates that there is no value set. - Revisions + This document covers the file format specification for the 1.5 file format for novelWriter project files. See the documentation1 + See: https://docs.novelwriter.io for a full description of the app’s functionality. The file format was intrioduced in Release 2.0. + The target audience of this document is developers who intend to write a tool or script that generates valid novelWriter project files from data from other applications, or for templating purposes. + Note that flags are generally written out as yes/no values, but also true/false and on/off are understood by the application. The None value that occasionally occurs in actual project files is the text representation of the Python None value, and indicates that there is no value set. + Revisions - From Release 2.1 Beta 1: Removed the titleFormat node from settings. + From Release 2.1 Beta 1: Removed the titleFormat node from settings. + + + From Release 2.3 Beta 1: Removed the title node from project. XML Root @@ -1454,7 +1445,7 @@ - appVersion + appVersion No @@ -1465,7 +1456,7 @@ - hexVersion + hexVersion No @@ -1476,29 +1467,29 @@ - fileVersion + fileVersion Yes - The file format version used when writing the project file. This determines how the file is parsed, so it is important that it is set correctly. Valid values are “1.0”, “1.1”, “1.2”, “1.3”, “1.4”, and “1.5”. + The file format version used when writing the project file. This determines how the file is parsed, so it is important that it is set correctly. Valid values are “1.0”, “1.1”, “1.2”, “1.3”, “1.4”, and “1.5”. - fileRevision + fileRevision - No + No - The revision of the file format used for non-breaking changes. This is an interger value. + The revision of the file format used for non-breaking changes. This is an interger value. - timeStamp + timeStamp No @@ -1509,12 +1500,12 @@ Example - <novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" - fileVersion="1.5" fileRevision="1" - timeStamp="2022-11-05 17:46:51"> + <novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" + fileVersion="1.5" fileRevision="1" + timeStamp="2022-11-05 17:46:51"> Project Node Tag: project - The project section is a level one node under the root. It stores the primary settings for the project. The only required values are the id attribute and the name value. All other entries are set to default values during parsing if ommitted from the XML. + The project section is a level one node under the root. It stores the primary settings for the project. The only required values are the id attribute and the name value. All other entries are set to default values during parsing if ommitted from the XML. Attributes @@ -1533,18 +1524,18 @@ - id + id - Yes + Yes - A UUID that is unique for the project and does not change during the lifetime of the project. It is used by novelWriter to keep track of temporary files and to associate cross-project settings with specific projects without relying on the project name. + A UUID that is unique for the project and does not change during the lifetime of the project. It is used by novelWriter to keep track of temporary files and to associate cross-project settings with specific projects without relying on the project name. - saveCount + saveCount No @@ -1555,7 +1546,7 @@ - autoCount + autoCount No @@ -1566,7 +1557,7 @@ - editTime + editTime No @@ -1594,49 +1585,37 @@ - name + name - Yes + Yes - The name the user has given to the project. + The name the user has given to the project. - title + author No - The title the user has given to the project, which may differ from the project name. The value is used in some places in the user interface, and can be added to manuscript builds. - - - - - author - - - No - - - The name of the novel author or authors. The value is used in some places in the user interface, and can be added to manuscript builds. + The name of the novel author or authors. The value is used in some places in the user interface, and can be added to manuscript builds. Example - <project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" - saveCount="5" autoCount="10" editTime="1000"> - <name>Sample Project</name> - <title>Sample Project</title> - <author>Jane Smith</author> + <project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" + saveCount="5" autoCount="10" editTime="1000"> + <name>Sample Project</name> + <author>Jane Smith</author> </project> - - Settings Node + + Settings Node Tag: settings - The settings section is a level one node under the root. It stores the various runtime values for the project, and the settings available from the Project Settings panel. None of these settings are required, and if missing, will be set to default values. The entire settings section can thus be ommitted. + The settings section is a level one node under the root. It stores the various runtime values for the project, and the settings available from the Project Settings panel. None of these settings are required, and if missing, will be set to default values. The entire settings section can thus be ommitted. Attributes None Values @@ -1657,18 +1636,18 @@ - doBackup + doBackup - No + No - User-controlled setting for whether or not to run backup when the project is closed. + User-controlled setting for whether or not to run backup when the project is closed. - language + language No @@ -1679,7 +1658,7 @@ - spellChecking + spellChecking No @@ -1692,18 +1671,18 @@ - lastHandle + lastHandle No - An auto-generated list of the last documents open in the editor and viewer, and which novel root folder was last viewed in the Novel Tree and Outline View. These are saved as key/value pairs. See separate section for how they are stored as XML. + An auto-generated list of the last documents open in the editor and viewer, and which novel root folder was last viewed in the Novel Tree and Outline View. These are saved as key/value pairs. See separate section for how they are stored as XML. - autoReplace + autoReplace No @@ -1714,50 +1693,50 @@ - titleFormat + titleFormat No - Removed in Revision 1. + Removed in Revision 1. - status + status No - The status labels as defined by the user in Project Settings. These are saved as special key/value pars. See separate section for how they are stored as XML. + The status labels as defined by the user in Project Settings. These are saved as special key/value pars. See separate section for how they are stored as XML. - importance + importance No - The importance labels as defined by the user in Project Settings. These are saved as special key/value pars. See separate section for how they are stored as XML. + The importance labels as defined by the user in Project Settings. These are saved as special key/value pars. See separate section for how they are stored as XML. Example - <settings> - <doBackup>yes</doBackup> - <language>en_GB</language> - <spellChecking auto="yes">en_GB</spellChecking> - <lastHandle /> - <autoReplace /> - <status /> - <importance /> - </settings> - - Key/Value Nodes + <settings> + <doBackup>yes</doBackup> + <language>en_GB</language> + <spellChecking auto="yes">en_GB</spellChecking> + <lastHandle /> + <autoReplace /> + <status /> + <importance /> + </settings> + + Key/Value Nodes The key/value nodes are used for lastHandle, autoReplace and titleFormat settings. The lookup key is stored as an attribute, and the value is the text of the node. Attributes @@ -1777,25 +1756,25 @@ - key + key - Yes + Yes - The lookup key. + The lookup key. Example - <lastHandle> - <entry key="editor">636b6aa9b697b</entry> - <entry key="viewer">636b6aa9b697b</entry> - <entry key="novelTree">7031beac91f75</entry> - <entry key="outline">7031beac91f75</entry> - </lastHandle> + <lastHandle> + <entry key="editor">636b6aa9b697b</entry> + <entry key="viewer">636b6aa9b697b</entry> + <entry key="novelTree">7031beac91f75</entry> + <entry key="outline">7031beac91f75</entry> + </lastHandle> Status/Importance Key/Value Nodes - The status and importance settings are stored as key/value nodes with additional data attributes. The label of the status or importance setting is saved as the node text value. + The status and importance settings are stored as key/value nodes with additional data attributes. The label of the status or importance setting is saved as the node text value. Attributes @@ -1814,18 +1793,18 @@ - key + key - Yes + Yes - The lookup key. For status labels, it must consist of an “s” followed by six hexadecimal numbers. For importance labels it must consist of an “i” followed by six hexadecimal numbers. Each key string must be unique. Internally, they are random numbers, but also sequential values are valid. + The lookup key. For status labels, it must consist of an “s” followed by six hexadecimal numbers. For importance labels it must consist of an “i” followed by six hexadecimal numbers. Each key string must be unique. Internally, they are random numbers, but also sequential values are valid. - count + count No @@ -1836,7 +1815,7 @@ - red + red Yes @@ -1847,48 +1826,48 @@ - green + green Yes - A number between 0 and 255 representing the green component of the label colour. + A number between 0 and 255 representing the green component of the label colour. - blue + blue Yes - A number between 0 and 255 representing the blue component of the label colour. + A number between 0 and 255 representing the blue component of the label colour. Example - <status> - <entry key="sf12341" count="4" - red="100" green="100" blue="100">New</entry> - <entry key="sd51c5b" count="0" - red="193" green="129" blue="0">Draft</entry> - <entry key="s78ea90" count="1" - red="58" green="180" blue="58">Finished</entry> - </status> - <importance> - <entry key="ia857f0" count="5" - red="100" green="100" blue="100">None</entry> - <entry key="icfb3a5" count="2" - red="0" green="122" blue="188">Minor</entry> - <entry key="i2d7a54" count="2" - red="21" green="0" blue="180">Major</entry> - </importance> - + <status> + <entry key="sf12341" count="4" + red="100" green="100" blue="100">New</entry> + <entry key="sd51c5b" count="0" + red="193" green="129" blue="0">Draft</entry> + <entry key="s78ea90" count="1" + red="58" green="180" blue="58">Finished</entry> + </status> + <importance> + <entry key="ia857f0" count="5" + red="100" green="100" blue="100">None</entry> + <entry key="icfb3a5" count="2" + red="0" green="122" blue="188">Minor</entry> + <entry key="i2d7a54" count="2" + red="21" green="0" blue="180">Major</entry> + </importance> + Content Node Tag: content - The content section is a level one node under the root. It stores all the project items of the project. The items are stored in the order in which they appear in the project tree, so altering the order will affect the project structure. The attribute with the order number is not used when processing the data. It is written to the XML for debugging purposes. + The content section is a level one node under the root. It stores all the project items of the project. The items are stored in the order in which they appear in the project tree, so altering the order will affect the project structure. The attribute with the order number is not used when processing the data. It is written to the XML for debugging purposes. Attributes @@ -1907,35 +1886,35 @@ - items + items No - The number of project items in the content section. + The number of project items in the content section. - novelWords + novelWords No - The number of words in total for all document nodes with layout DOCUMENT. The value defaults to 0, but this number forms the basis of computing writing statistics during a session, so the number should be properly set to ensure correct statistics. + The number of words in total for all document nodes with layout DOCUMENT. The value defaults to 0, but this number forms the basis of computing writing statistics during a session, so the number should be properly set to ensure correct statistics. - notesWords + notesWords No - The number of words in total for all document nodes with layout NOTE. The value defaults to 0, but this number forms the basis of computing writing statistics during a session, so the number should be properly set to ensure correct statistics. + The number of words in total for all document nodes with layout NOTE. The value defaults to 0, but this number forms the basis of computing writing statistics during a session, so the number should be properly set to ensure correct statistics. @@ -1957,31 +1936,31 @@ - item + item - No + No - A node representing a project item. + A node representing a project item. Example - <content items="27" novelWords="954" notesWords="409"> - <item handle="7031beac91f75" parent="None" root="7031beac91f75" - order="0" type="ROOT" class="NOVEL"> - <meta expanded="yes"/> - <name status="sc24b8f" import="ia857f0">Novel</name> - </item> - ... - </content> - Item Nodes - The item nodes make up the actual project content of the project. Each node represents either a root folder, a regular folder, or a document. Each node has a type, class and layout setting that determine its category. Each item is given a handle that is a random hexadecimal string of length 13. - For the items that are document files, the handle corresponds to its expected filename. It is therefore important that they match in generated projects. Each document node is expected to correspond to a file in the contents folder of the project named “content/7031beac91f75.nwd” for the item handle “7031beac91f75”. If there is no such file, the document item is assumed to contain no text, and a file is created when the user tries to save text to it. - Each item node has a meta data node and a name node. The meta data node contains only collected information, and is thus not strictly required. However, an accurate word count and setting the correct icon in the project tree depend on these values. These values are set by the indexer class, so rebuilding the index should restore the data. - The name node contains the primary user defined settings for an item, and is required for the entry to be valid. - + <content items="27" novelWords="954" notesWords="409"> + <item handle="7031beac91f75" parent="None" root="7031beac91f75" + order="0" type="ROOT" class="NOVEL"> + <meta expanded="yes"/> + <name status="sc24b8f" import="ia857f0">Novel</name> + </item> + ... + </content> + Item Nodes + The item nodes make up the actual project content of the project. Each node represents either a root folder, a regular folder, or a document. Each node has a type, class and layout setting that determine its category. Each item is given a handle that is a random hexadecimal string of length 13. + For the items that are document files, the handle corresponds to its expected filename. It is therefore important that they match in generated projects. Each document node is expected to correspond to a file in the contents folder of the project named “content/7031beac91f75.nwd” for the item handle “7031beac91f75”. If there is no such file, the document item is assumed to contain no text, and a file is created when the user tries to save text to it. + Each item node has a meta data node and a name node. The meta data node contains only collected information, and is thus not strictly required. However, an accurate word count and setting the correct icon in the project tree depend on these values. These values are set by the indexer class, so rebuilding the index should restore the data. + The name node contains the primary user defined settings for an item, and is required for the entry to be valid. + Attributes @@ -2000,79 +1979,79 @@ - handle + handle - Yes + Yes - The 13 value hexadecimal string that represents the item in the project. This is the primary identifier of a project item, and is required. It must be unique withing a project. Internally, it is generated as a random value, but it can also be sequential. + The 13 value hexadecimal string that represents the item in the project. This is the primary identifier of a project item, and is required. It must be unique withing a project. Internally, it is generated as a random value, but it can also be sequential. - parent + parent - Yes + Yes - The handle of the parent item in the tree. This value should only be None for root folders. All other items must have a parent handle set. If the parent handle is None for an item that isn’t a root folder, the item will be treated as orphaned during project loading. + The handle of the parent item in the tree. This value should only be None for root folders. All other items must have a parent handle set. If the parent handle is None for an item that isn’t a root folder, the item will be treated as orphaned during project loading. - root + root - No + No - The handle of the top of its hierarchy of parent items. That is, the root folder which it ultimately sits under. If the attribute is not set, it will be computed during loading. It is saved to the XML file for efficiency reasons. + The handle of the top of its hierarchy of parent items. That is, the root folder which it ultimately sits under. If the attribute is not set, it will be computed during loading. It is saved to the XML file for efficiency reasons. - order + order - No + No - The numerical order of the item under its parent item. This value is not used during the loading process as the physical item order in the content node is used instead. It is primarily saved to the XML for debugging purposes. + The numerical order of the item under its parent item. This value is not used during the loading process as the physical item order in the content node is used instead. It is primarily saved to the XML for debugging purposes. - type + type - Yes + Yes - The item type of the item node. Allowed values are ROOT, FOLDER and FILE. + The item type of the item node. Allowed values are ROOT, FOLDER and FILE. - class + class - Yes + Yes - The item class of the item node. There are a number of item classes available in the app, all corresponding to a specific type of root folder. Each item in a root folder should have the same class set as the root folder itself. The attribute is not strictly required for items that aren’t root folders as it will be automatically set to match during loading. Allowed values are NOVEL, PLOT, CHARACTER, WORLD, TIMELINE, OBJECT, ENTITY, CUSTOM, ARCHIVE and TRASH. + The item class of the item node. There are a number of item classes available in the app, all corresponding to a specific type of root folder. Each item in a root folder should have the same class set as the root folder itself. The attribute is not strictly required for items that aren’t root folders as it will be automatically set to match during loading. Allowed values are NOVEL, PLOT, CHARACTER, WORLD, TIMELINE, OBJECT, ENTITY, CUSTOM, ARCHIVE and TRASH. - layout + layout - Yes + Yes - The item layout of the item node. Allowed values are DOCUMENT and NOTE. This is the attribute that determines if a FILE type item is a Novel Document or a Project Note. + The item layout of the item node. Allowed values are DOCUMENT and NOTE. This is the attribute that determines if a FILE type item is a Novel Document or a Project Note. @@ -2094,29 +2073,29 @@ - meta + meta - No + No - A node of meta data attributes for the item. + A node of meta data attributes for the item. - name + name - Yes + Yes - A node of user settings for the item. + A node of user settings for the item. - Meta Nodes - The meta data collected for the current item. This data either represents the item’s last state in the app, or consists of data collected by the indexer. It can be restored by rebuilding the index, so the data is not essential. + Meta Nodes + The meta data collected for the current item. This data either represents the item’s last state in the app, or consists of data collected by the indexer. It can be restored by rebuilding the index, so the data is not essential. Attributes @@ -2135,73 +2114,73 @@ - expanded + expanded - No + No - Whether the tree node in the project tree was expanded or collapsed during the last session. Applies to all item types, but is ignored for items without child items. + Whether the tree node in the project tree was expanded or collapsed during the last session. Applies to all item types, but is ignored for items without child items. - heading + heading - No + No - The heading level of the first heading of the text of the item. Only applies to FILE item types. Allowed values are H0, H1, H2, H3, and H4. Other values are reset to H0. + The heading level of the first heading of the text of the item. Only applies to FILE item types. Allowed values are H0, H1, H2, H3, and H4. Other values are reset to H0. - charCount + charCount - No + No - The number of characters in the text of the item. Only applies to FILE item types. + The number of characters in the text of the item. Only applies to FILE item types. - wordCount + wordCount - No + No - The number of words in the text of the item. Only applies to FILE item types. + The number of words in the text of the item. Only applies to FILE item types. - paraCount + paraCount - No + No - The number of paragraphs in the text of the item. Only applies to FILE item types. + The number of paragraphs in the text of the item. Only applies to FILE item types. - cursorPos + cursorPos - No + No - The last cursor position in the text of the item from the last editing session. Only applies to FILE item types. The value is used to restore the cursor position when the document is opened in the editor. + The last cursor position in the text of the item from the last editing session. Only applies to FILE item types. The value is used to restore the cursor position when the document is opened in the editor. - Name Nodes - The name node contains information about an item that is set by the user. Including its label, which is the text value of the node. This is the label that is displayed in the project tree. + Name Nodes + The name node contains information about an item that is set by the user. Including its label, which is the text value of the node. This is the label that is displayed in the project tree. Attributes @@ -2220,45 +2199,45 @@ - status + status - No + No - The ID of the status label that has been set for this item. Defaults to the first status item defined in the status section of the settings node. + The ID of the status label that has been set for this item. Defaults to the first status item defined in the status section of the settings node. - import + import - No + No - The ID of the importance label that has been set for this item. Defaults to the first importance item defined in the importance section of the settings node. + The ID of the importance label that has been set for this item. Defaults to the first importance item defined in the importance section of the settings node. - active + active - No + No - The active/inactive status of the document. Only applies to FILE item types. + The active/inactive status of the document. Only applies to FILE item types. Example - <item handle="53b69b83cdafc" parent="7031beac91f75" root="7031beac91f75" - order="0" type="FILE" class="NOVEL" layout="DOCUMENT"> - <meta expanded="no" heading="H1" charCount="93" wordCount="19" - paraCount="2" cursorPos="119"/> - <name status="sc24b8f" import="ia857f0" active="yes">Title Page</name> - </item> + <item handle="53b69b83cdafc" parent="7031beac91f75" root="7031beac91f75" + order="0" type="FILE" class="NOVEL" layout="DOCUMENT"> + <meta expanded="no" heading="H1" charCount="93" wordCount="19" + paraCount="2" cursorPos="119"/> + <name status="sc24b8f" import="ia857f0" active="yes">Title Page</name> + </item> \ No newline at end of file diff --git a/docs/source/fileformatspec15.pdf b/docs/source/fileformatspec15.pdf index 5115b5b1f82eb1e5e398d2c487e3b20bd618463e..58b5be9d9e28f667487267af97e60e471d3581bd 100644 GIT binary patch literal 319730 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�+FmnnlimS zH!KhpcKuY@k->)<1DOu6%DDmfZ$_4-CR3)Tdg8KnhEqHxI~x{k+(ZMtePmx;YzI{> z{5#tnHv~bl0H2!~2ai8a+Q3VW>qcE1fR-Lvmaa@{R(50yy|UP3w~;OM%A$_lMz*LU zi!yc_IYAw{Njjyo;gFqc?}Ed0ph_H7cBZ$;G%U^X7QkQ!m^DoGclp_8UH{X8d?7xg+QBc+SwN zBNBn@gc86z^vPLL?K3OKq{A>y8csE!i68Su2D7YJ_;;M9Q6>FgnlgwbN4SqVl$MD%Y zy~3`UO19063~jxh+#1=DYU}ngO&EjmHicHj34FF&j;XXeBf04uwgcE>ag@ zGFC}~JMuaYD!FcR#_rL8itVRoZ}G8_Rh{Frm!4N4tzs@x0yoZlNUZoR z0r;27>PR@9uQp#?Zk?}M)dBV!-V8*CLZYw^pGw+V=HSoJ@T{?T6bl76^M)p zxlutsp)mWTfq5~Y1Oay&URHhsdNFlJE2ltqq zI0>4E1kofJ<Q%rIbOpdgTKLso_eq^8*za*g7U@UhHTs$yM%u~?%F8C7E5mpC z2WnaJ9Z5r6!RP@Y)h)|!r9FA%X+_H6f36sq@LygGp@{7RiJI$ym3`Kkvi(6$h@g|W zVvkP++n*bx)R`L%7ua8{>m(z%r=Q8xKZMZb$HvbsO2d%zX4Pm~RnI$Pf?G1VhaE0V>3 zlXb2$#r~{F)Fq?&h{O(ogjeui7Z%UgRNAb^w{lA2e`y0ABT^;JbP4!!a$FNmz0aZ1 zEED{fK;x08;nEstY$Mjz~n{rwdFoA@6zS~wSv5ssY1 zUpVCWa&lY~%85w^XyF2RHFY2_6Gmt`Szu1&6v&uxFBDIW+S3qJ^k@F6EV69OE90g7 z3y+u6fq5h}aF32usJR`X=Jo@_O^GMt1YUl_SG~Vk!%6-_!&kdn?zhnLKn-8(ZnfW< zhA&!mOvuFP_>zbmks6XywDAtfDPElLVI4KaUEs9Hc`2UMnQHaHxHw3gFfJHMjSVFs znXrP4%kbNDeASuWo2g0?B7oMw)OnyOtP2|=YO1oIbOBJHZ)YzO{^QyvEQnr{(FJ|z zcujo(nzDc_u@ebwgHD1?s1um&QKyk5amFVZv% zn?>q)Ua4fLRU@+qh1X}id>Vvgx?*AwB1Iifb(NH%s0X*sZ5T?)fjXh(c@bw~kgAm) zCokemV-Z!7Od$n1J$`LO3hp~G6P0a<-b7O|WZDa9VOdk}mWGzV`S?6Q7Y6cRWTcTb zH^?Xg>r9(R4>z*KBgP6=2hDCN5Hs6bWx7G44$2+Pn(jeySTXDYjP!||rn@3C#(|p` zEb&ZRYcbL1g510=lvA=z;C$>B}zLiswR6-}mq?aivs=&l#y#z3||2T3Q*3>x5 zWML^IDvvxJR%HA^o$1BO$!Tz6WCq547j_HP1>HjS6mzgA;hQ{0q+pWi;BoCK9;hi* z2%!aW1fwGE=uuDthNYgbtU8rL~JCcD{cUaN!Yz3(Ij}7A_GL zenFZDQ4lfQDw2@3^S0jL<57T8TGrr`Ow#OQ>+4LLmaj-`*pf6j=AbD36eIILV3>PpGuS#t9siWbL4N=HrY)A&M9(h_$7MK$`3Hb0xr%ckQoCa^t?Jh@$|z+zq8f34bW~?UAi@+OABjPG|ZTbJ)WG8Yba7F&Qp<#HKvXv>azv z7jYF3zz}Hz=Y`+_+@x%&<)-jr;{=*)9qe|=V`rw0!vgFNXVeVYQMf}c_sdZ+eQr7jZ|P3C3KsJp_WG~>?licP8uo-oA!!$ zVqpmP#!iVR7KUW!@UA?uFqG-qnx)?chU&rwwIY67yOiy)JMsjA7NaL3UfFeQW0w$G zilyQdG|xI4j956Xelu}+SekPKBEew5UyM)SCkK4om`G*||IRI=b=)fEzJ{7Rv`Hnx z_h=PeyNy_x+iQBAq?WTuJcqE~$X2j_O133q8|+#YHjvdNV;k&(Lyj^H5ESLlJiC!d zIW4`R6c(lmxPPVQMjK*{$?2{N8`oCyR-PCy7{3k6I3?#*VdGlS*anYRg^g=PV;ejM zITw~T%)$pE<<#Wdr-Vp^XBNcn@RJsVMiymwx}sLHX?r$42KZI9y+n)49nP&I;!cf& zeV%IN_sz)_`jnTLIJj$-X-=3rxU1y^o7kvc$D3RW~2WlFYVaIvD>MJD-d zeDo^QRWfI2>&|4w|D*^pE}NvQxiR~fybF}0}u;XHi&gqF&h#k5WIBQFs%grqIKofBd=r~ zsWWyzd#V7PHi5(o${n8Z*-MGzYbM|!Eva0^%3CRy;Q46N1wq1VbAT^omK1t7%kHF` zk_;G5MclZV40i^cJHGGm;QHf>58QjCR6EnOM0Iv6(X!N=-nENRk;{8t{Mw+?A(Y}e zU8RI8S>rh=Vn$WTNNeZTx64Y(UB@=g9i}F>jtkXCxG0C5xWg~XG*@(if>vx)AKbSh zu2NQK<+qH;O=yowtwwgEKUE_YIcC*JeA}BaR^(2p5iLxC*BB>8H9}3$SO$a+A+7VO z9x15sUo+IIkqNfo^+IqS6O+x_l`Ly@$ZZ{cTMiZz&r=N+nWvw3R895D3j99IaZ6#~yoY8xGiiLiX8CFiodQt8_ zdkUM%N!B(x45+VU)u=~$MovSPhFGoT1WGmEKME`H z2$}|F+R!32LLFL=Cc;YE>q}b6Vk_rugCgaD8CLMtqJ;T+l5!L&yk-L3oD%$1tcsI5 zj#!8OlN2ciNn*(YvQh3l#Urgq7MM*{9FZ|WIw=x$4mrS#KAwp6fLT_+Ov;HTEtz2q z9x^OePP3y2S)%2T;~G5mt2k31VhP#}v*gJ-2TzT(oGdUWauOElkv=Ua+7Ko=A*(g9 zE4kg}jNaQ*+*_QIlSxXf%a);)BZZLm6}KvG+Pr# zR;{cZ@yOJ=V8K7p1)=!f6f(LnL=+@WtqVYGCZb4rDvQrOf~LWrsuD;!8T`3PY~?i} ztz=@@dE20@?B%AE)g%~T@&d{|Nu`sQdoK<-u0qA_C`noXe>!HF6bb{vL^feg4A&lc zT9GvPSNk?QE_dZT(n*nc7xEw>c8DUpc8pcwT{J7|5zq*0!9PjX$n%puMmTa3e$$(* z=cL6oIsJkG<;2lCEJ*m`596D#xTIK48vGqOiHO7_JtLWYrUNnu>yYExYE0}V>OaIj+}1#|O~GHlbs?DogmxyAe3u9z zS2FwM3_l3G06@)hc_njKyN+#^c&kjA^BlS%L!>QsEz8CVr6jP}nd%&6qPK&a1VG^% zfryi^0P6E-LWI>icP)Z;1UjNf0Y>hecvG1K7@~!V$D$-4npOj=KF_O(7mLL*Udu`x zkHEVJ)Uf|V(AD2I((LTLs=#{J<>_B#L=wM%@QpnH)6HsF31O4ISKRj7?DO8G6Ut1(g1LjT=h z(5UT3J5I-LiqbNp_%?))?nPef+1CNA5AHi7Ax*K(1tBn zVb0rn!=^@qY2!1IWVOy+Hjn~vrX9rv;JDI zfToU19c%EitT`_y>nIaBWw4AK<*1z4+e4Voy%|bQO<^xaBYH9VfXT=ScHGU_(=I;h zK+)JqP@B79AhA_?&BVro;}jaYE>6P;>Hx=gH{U@VJhN!{f*i#HP?k(gr)}1PztIIE zXJQH&UFah`X1SIZ5G#C(r??4E2_7a0nqDPQp*C3(FW;y~3b^T%p*T96 z02SEyw~`WeRy6rW2E-0I9;h^;*QK{3RH#16pjjr-x2#|H$kU2s!QYhV5E&D4Lq%$+ ztH;s)K8he77-AdgM;*ota-vL;%JW)KG0`_DsqicwBN8QTWK83bky9Y!vgJgz;=#U%2Z@{t0XllTDSI+2nQATj3p_@oa*}SnR8C&k zQIHdr$K%w!wj20DCWg>*vcPQRBttWg^l5taS|ebulWefArq%)eX|b6 z>YMbY-c&NUFhngRR%;w(7a52xl}4_=Jc6bjOstmva=X(E~?#A1dM z!z%CMr$*B1Fs7GNoU|O@$|+g$%igInIZ4Sn6NRc+pJic!d9=Mu}u)$WO`g?@gxbnq%aHZLa3@Ee%~(8>uHvvT?{cNwVEwR1Hduz zEeOQf@Vk$1LSH=cwcX?#r349YY8kr;wBU9Gi&@(Z7OZAa&!j z$ex3y6fO!HJUw5dp@iBLHt99gIi?ZQvdpYGMsmZn9vm_CMN8;;I8n1b*$+SET{K_vnpd~@eBGU;rYit`N6*j#!)ZRC# zpHRG}=mwL{tVW`LnJH)CwH(!)X}`lFg?qUgZaQncPPs-NR_?sKmxb4pWb7Hr(iOiN zd7}8PW1GOQDeOsWO6WF@t0}x1sAz2C7Me^`)GU5m{4DJMDxn(+HH|EVFW*s?P!_Jn z2D>Us@y|Txk*0ZS_{l2(@QZxtIjJ|`D4h)tKQVrWpBT2nPlQJJ=|uibVGmeS&;w@A z`viW9CC=&jqf9T?Oq^mL4*cY1E~OJqrUbdS6S|OR633w{LX8dl$!uF(9*$>P?`HC6 zSXpv_ujbHX`k)5>-nf5fuw6!k}Fr2`=|`7_TZ9?<2yY~T-%Gm+%6 z=d1#ML@;@Z{1IGtNS?``VeQPt_e#c1VY^sU&@Kio6l!eXkBDfd_!Ca@+~IsuBJR`> zo+H_SNDgAdtb_~9&FZ?tyN)j%E|&Z1%(Eub`Eg@CkqV%2PM$N>V86%s{Cg#*{tyJo zCiaz?;LZB`!mg#JVCcl%%lQ9t899w5mXZCeL(-1oVa(%m!%V|nKuV39!Um{@uK$?i zu3Z6T>V^lwu@wV4^;0l{GtRBa^nlD6+W3Vg(-|{oXuDJ>>;Wp_wLzms9@*l+>Kv-= zLLt+FGG}PJPAF`)DPf3NCV&BCYN=!RqYhfu=hxDE$DwER^u}I|(GrKG1oz!FoW&$H zhTxhzks5BFB58)_7}vm0+CkNpTX+!&+@M4m?+NpQLN_%waIY{4pt+}y{-igLv(IKV zHP?nEzSqal6!xPu@jQj8*{9fDv{aLu<(bx-xZfg>9mthg+x0}d}gXdZF zc8c9DHic~*g+NcJp`oVsgG;A&Nn!ysMDc%k)))Ru*tMu z^uQz9EqWC){sW5cl!7;fT_#P**v2`*)I^Ny35cLasn4{hch!g0M6;M*PTK+8w?UIV zqd5Ry%*6LvjqGlAszxdt&Z?2HFvnlz)d*vTphlE`z{5jXT6&@l-c&KJv4a|MXe=3O z)yM=3@cN=~S&3Tk$(uPhDJZ0H0whEVjNmcGb)%dXx=HNQs-EgfvStb@+n7 zHjg~5gc)~5WK58byZ^1|>e0kvKt;r2fT6LX%`|*OtY}4(M?b#B3KOCOBb1{}Pg!&1 zB>uuPAy-a3MX5q4C-k>8b&Lr0)PcN=*V1yz_=7fAo}Q5t$E-oAVw@nXx6L$s400;K zbhF+8Mr0hLBd0Pu70Z53e)!FneBzPR%|^KFIU0inZ!8%3|6~M^!eRt`(HbmJcXt?4!JY!sL$Y#$$<0WW-U_1 z@H=@+5;6SF9ZkeKYj=gsLak`%8P#7C?X-o>LPcX6C(sr)3l)uRAhyLq&vAkEI#$3F zD7V#ElHcu`L!A_Ry5@8%mEAPr#uE;AMuNp50{{xMtcN`v!3N+`&ca z9qfx%5u`TZcM1ku?r~|o#+)|OT++)*G?a|l9keSbezt{uG;PV)7JoAp^0VkR_}j`J zm??i(Lbt^t+2*k8*cOWldsd40ZLvtU4D3j^Jz@pyPUHYi&+bq>z+-mYU?ERq#&yN+$J2<$GC4vUF_d2wc@1#PC?4Oud7ylSblVbO9Y z2&oO&8MisKeAi9uUC`6YimxetQcbu^bIWOxyttiAFix6Nag;c;Xv9z2*ezga$O0im5qvp^zSwTd_3N&}js%iRKSzzIe z-OoK%B26Sv;45nGtt>y-b!_Va3Y!7i61t5C0GSo#J54%KSDny{$9;2I)d`iDp!B#U zf|v?ez@^=W<08zM+0^_T(tFE$V1Hr^FxUz7@!%VK^x?M88hKHqzCU6vv~|ewkb*i198nLk zj#o?%%*n(fTEv-+M;K1I%B+Zmet8qkikK3v{Muf`C?zf8YAw&`E)#>SrSho5P0N;g zr&7+xO27bJL}tr1FRj=1p{{*)-q!12+z6b~!6Z(Xm5gPRms_X~IZ+4I?l(0bIIQT4 zW#j$O3TZ($JGiUll8QFdAVO#5w4yzvcL~9_Qh6r29AdRrq*kU(9mWiD;{AdtIVGzD ztT$qxQz$3lnjLanIngyVj+}-q4r-3jQwQE;yp{$F?GI8p$^BH1bgCv9mD7+H#@(Y} z{nB5`rKvOeAg4U0LyTp{7NEK7*mfl&7aO~dT^qV($iC~?W<5Z6?-qnh)UcwJ&*T}> z_H|h`Txb^})^Mik+XY4pnlyw2ehyGh9W#q~X*JzU4a+ZDi@;BWI?Z;l8$pamzP20f zWF5Q7c+HI6u)c@M;Kzj6qMVL2H8jz2(No#wxT6#y3Z*3A6ur>h09;1KYXVytlsb~_ z4V(Hbv^1@vnV26WB`n*MR2rQDt%+@AZ0L;M%P9u5k{Q;X z?Bb`B7ZsG*lZ?|Ha(p?lDoWB&PDAaJ*Ng&!6w#AMo|cmaW=BrKIz7@eavGv9VzrhN z5Sxj<9BdMCwMWq83WnatT8^}Vdn1q0atWd`%2qmQ=WT-`apWeYWfjS;U^=WAo@MYd zSary86-nI3Ar}Soc47th->9jBYyx8@$kU3H17^7l@JJ^`iYpb#N7Q;oEiicMbkd5X zuETUjALLX3%ytFS$;%!*i%OL=*HYru!_!SWiBF=pvz%zrZRM2l?^;edV3u*6!%1Fg zj>>89_I!VaB3519=!_oa)ZYmy^}yH@tYp>Lps)g~isYm&zI4d(?P&~{`T5Ov@THKJ zJXs{{k*DR917;aEd8ALgGs?98Zz@>nNp;2dtxu7XmA2mL>}TUzp|LGr&w_OssTJ zk@{;y6JmQ0&o@?TtzToe#rWuhobtd7D=1^|^5pMAIfd7ZljF;2Bw~(L*g;M%Fv~J@ zk34Pg8DLhIUV18)kyF3kr%MctQmGr=oY8wZMc|NnV1e1BQPd@AyN+!(E!WZ(b{*TU zoI6?T?+GvgL3MARLyoUsBMya1u@MKjF@!ICThOeH2mu#_a(m=zeako)b^oF_m5jcz zJ%e$O$|Sa%iCR+L%5|4V&~)Tah3tf?6#0`yomueBkD9=>WTx}B!5|!Tum+JV^<~3^ zC#iJuQWYI?T*adM4AO{_F8UzFLNV#7SDoy+@W|7Olyfi=20hYAks_K(UPUNt1GAn6yFVmC`6!ciYR)q>O)TK&0jtvQQH8v=B zCX!3_ON!hf$Hf7<$3(LfO>2nVxDA0g36SVsB-6o}9E>zBZz>sm>-Wl7WfIu@CgjfWTRe(A}g-rshW?C4$9)qY5GwX4nk8jdhq?pG__ z(MuZ|6Bsbv(c8sKKp;)S=dSXeG^E)OA7C{{KY2j`p$y7V^JZPx*hCk7O@jsOURk5d zgh`|rhVSMee09Z`qbpNIejWAnpr&wpkD^fB*W`@dyEN(v(spg261c9$2FNLjyUgq_ zqvLliU*ybUR(&&34T6Sn((8ay7biI{>seON?E@QGie>87qg}_g%a_6qsC5b5X8EFS z0os*r6Tz+vJD`f_Hc{8Qumh@SY#S*Qc0d)4Z6k$D%fuWVgv}DAgJ8BIuAFifv${NJ zn&WUk(?&VM1{>w>Dl#1iC%C(cR5JX|4wAd8=q{V^+{vS;t2-w3I3MA|j+x@?y08G; zM7PDHOz+Dqx-BLZ_WhL5UBO0yR&Z=Q8Ap#fgjcYmZw43T20L1P1(_o=`01Hu1^ex0 zaIvV{nR08KndXvtJU7-^b+X&`6upe_T@FL$d6^gqftTSPLIQE{9&|Aj{N(%|_Yl$r z1L5B(e?$kyhu_KD&{5*>J2g_lPbzq$vt`3`9*>U%HPRNVIA?ObK&YK(GFY!*$K4Dr zmMS|BZZQo@xC_j;h;j>Rna-ejY_X_T*agK$w@&jMveTO9nI4T;Ivr6#zQYV&2p~m+8$?$}1nx|^MW;-SY;P$=lv{MlbUe%(+@e#a z6J*}t7Mzb~m)PWEu=+Q4R@MGk9U-QX)`DRGYy=qHo;N=TUCZNgY12GtaGs z$`clL9^6`JVNVC0PMg6NNWU7Mi)7!pcU;j6suea$l(EI28eoG^m(#-rCp1w4nWiW$5xIYEsT)2nbu)tJEx zBbTDVEjndtr{`&lK_}J5wX?Pu3#G=Vag3Trxv@~y*t9d{#zIwF(yoKsW0+JM(awXL z_yOFFSh<{{y9qWoF3OqiW~heYMyoY8RN6E1hGNx@<I$}Ku&YJBGnZqZ4W9Pi8)i%zmYc;~?_ zIu)MM-xcxPqEq29{bhq&bSf-oEgIamy{dxFU3qR>AywJg&VySEmDL724{j|~ZddL+ zxV2E3oZfkGYoRi`we#S{Lc!gLh4tyV8??FcXl-WfjPW438(xG^7tn%m7u*hYa==e^ zI|w}CITaRiAv*j{iyE#UhTnNSsv=p?rZ)T?3N-wr@w6wA%Z(|4;5soAib5ja3@$?T zod>t5CS!}82e+suHEGwuA@^$7G9AD4KCp!6wgsRoJX@a@{uzPV3j40Gq_k{si)w|Z z>z55~Pz|s_F#YtfL8ea}J(-COXuyaKTtrNZ-C(s&-kvw`PaCPsn@zvH%@(YL~) z*N$q-rjwblC#03Rut4$Sx9C(@_*yo&MW@2b*P_8~Ieu4I z`C2x(SrK-H=hc@DZey;(1MABMx9C)OVtv`*2A#UX%Ga{Nt%YXF(0X@MV*YS$i8gtK=7PTrBR=##r<~Zr5YFp3W6;{5M z4Q^2_bJlyWr$v-oRFgN0Pgz*e;1<;~HAC|#x2PuX+TNLRi)xue*XL1gQBB@Hy))$o z)c_krhW_-h0p)*c*nk^ihz)3#V9Q)h-0_M09aj@~R4@tu&bE$%3EwB|Fg7lt@F~W6 zr%0QuXbsy;dho)^*RE)AgXmph%F-}ADQCddC94`I)tJR_pI|yW|?xcy6*6<<4cDyEp<~~ zCoddw>|pAqJbL{Zd8I^GSn}CXRS`ZJ5p_;o(&cNj_b@EhM1UVlR4UP&d?^}*~sJYcVviN zl7K0O_1j#8TdoYm37B*!EVM;jNfOp^$qg5Dc3jZeaX}}k$e9JnGyJ5)6mjZ|cEp>) zbCx$$1v;s!wiz1OSGX6gE{-+VZPJ^^)zY5Cr4g*(X{L$V*%uZeZt&!B+9>SDw;m$J zak0)H$8m8f4ZUW3s`2o@8xPN(zi@PS@QfQenOMsOvAf?#+i*#j@{N_+;_#@`XG> z&+4FwD`E1py%oMDuY>Zs%}qR-TABwc;oz=tmWdL|jWN$ev=V0A6_GJPIv!2C5rw9` z*AK+fZUD-S>(-PaZ3?SoH&omC6;8l2{*;%QdR4mK zaTJ(o8xOfhfVCh^gvAsWplX9JtVQ0$v(2QmtRk89ElgghMcL#tb=SVZGXP7{P?6Yo zfo%BDPc?NQPr37)B3WRDMX6dC$YW4F8z5j72P11>?ib0-YOP3{Or>H>Lu{x@Sfihj zlieHvM$C>LGKJu-oT!jllZJ8{R=QL&tEUd+W&FD~cni!{PMHFE2If&Y4c^`>$ler| zHE-y6SZ)fv5)6P2rKJ+N{zvFApnAa@)aVp7HAjZ7Tw zI=1!kGK;wD*w)9(T-~l?8y^pfC$!vCycE1MoeI8PjKum>HiD4AMfH>u~EFyA5pL3K$&`S;k!VU7^ z8!jm~1j<6Oab++!xy-shsIBGoL2c8C>x1CP!tk4-i5AQg>-_ZkAhkZD`Xf{@+S`Fd zCyEKy)%=shsTFG@Q^pSC2aIz7-(Vapj?|PYDu&`Y6|>n~7qrQ=fJ|;uQZ*Ktormy+ zmGv7cPM$<~h{){hGx@sI3S90*+xo0MEt$FB{mim*w^CFE+N8=2s8Uezl&)tOI= z*et*{GJmw|*h#=Giv-7wM{b$O#G^xwt3Di8C+aiAaa?#$lT8+IvZl_XQmc`T=TbFN zUOKBrLWjKxXVr+D1gKF22UH`>nQCj^$Q+OJszHs|H%f+DoJ?YNdE}?ZL%RMQlTlM8xtK&RY+%A{Gka@cs6zh-pd{S7An^ zAeU$+DhoQx#2|O}JnCqZGHymnod}$#%1VMRx=DV+T`wE2yJaJn;Lh869gKMc`97gsFS7(Mp(cR~wmjayZHHZ%l9{#4>1t z#Sp6vR>(E3Gx{JW23a}2krO1)EF+YoO*aKntdJQ2ha6u{h=D0sP?0LCELX%^^1_@Z zLOw00j6c}Otd2)It?x(WG&nIX<5$FPeUMWg)9u-`U{ArfOfbTcQyHdfdl~_C zd^?~|P*E4uW%X~~o-&}mk&A1GlR10hNd#hjU>2P5h;?{5UHi`K$x&ctfd_)9pMx|J zR_EORp%e*(opF&w;N`C;!A`11E-0H#yu!H*?xWqvk{eGZnFS@4yrKqC z91e3QC#B}TDQt_{#0BaU*qMY-rAo5L1#kFj7m#wX?=WI&0m%iw$OZ1~F|!wL$r-zc zkhmP1n!QO`ZRC<_*Rjpgd?ObZyN+!Zz8kqD*mZ2X;wn6Oe^bJ5v*OywD12ACO^Cmd zG4!ruo6v0|1L_^ehIF3RZy6HrI<`$E%ZPK=u?=!;3j32v^aS#=h9@Ed+m&u>mxV{# z7tw9(6411$zRjTNW!zs))QzuBvz>t=+rv~-D8p$VLPl`L+$l(i6Mn})0)A2yK~@LP zsRIvwvNNaFG58$=dH6{y{tch&ZNxpj0_`C_hFlX652;Kuwg_ydRH!9K zXWPP7sZ9yp2F|upOLwFj(u|7V+rnn461pMTXvQuK1qyfq8Cx@UA>@>dZQyKM+NM-8 zwt=%PmS>Lx!5rIxG^F9l>DeXZan0DLXP1!4QDXz36=hmxc4L#uj#-L_U@qL@= zXo+t?*wtuUUc1j5PLOD8uSoj*J} ziHBi4_vpNOAU4{FlvBf61k8|PAov5U>^gPoeG&80^ z)Q)-r`2sVx*woo|Y>PdGof$vtKqd_}^Rn$Z?pZtL1|)&7<{a!CEbFLM?DmYQwgcgkOF`51O$?HQwkg&if^c>eAL zR>6{9={N9ODER5|JJVn@liwKhpdUe2B>WK}-;_Rj)vd-)z}naHME_n<;plH@L;ZOjY=-!R>)_+e~f!yul3?ZVUSeiUbNtAFYb=xQks? z5i&_;aFICJd2oX*+rrxWGPYO?EiAq-8{8U^JbGbgwpb&Q#~JKAcw$7tEAKkEJ&9q9 z6=35yKg)<9DXCR4Q*8zTZAdO4L9<}gcA)N$q(I#DcfASa3oQ*fTR zV1l2#x}>T{_#FeN#g&s-d-kD5kBp}A(d#fsW3Vy8r)(}VTJd$XmA@HWQu9$vBd(L>?56m zha$=iuz?+jg=}#wY3%@Cy|W$kJJ_UVK^NH`xNv>TrMz3O=6IVa8XbhdtB{E-O9g2m zt(%24vs)_No>qL@{k_G~$8q+}q8t)figJ*!$gW(zgr|K}A<_-|W>j0e*m8+3vB0kX z3d?D?RAe^Jbg>Z#>=5BoF+0pLy!6I#iA@$L)0QCOJx(y0b>TXOO6kUD4cn6yq%o~F z2#_tF{;E~IquWd^^7Qo4rfK@*<`jk7!ur)MRS0ILpSpd)-0|rPg`xM1i#qn=h`zlo zmJWwTGJQmUe;g`gt+=jHO&xSIldIINIGL6y7*3`3GJShQ^~6N6Z*NP5ns#o`;Tz9O z<<|zS4jbfL$f+i}Exa&bOVytGWhOmW*&g<7VL@)$;3kZK$q67VR3L%Bj{o))$h#;3 zdhYrILjzxVBi1W`t?1BGmZe9fR-{Z%7YwI@<(wjAs>G=QbV8B%7*T$0uSi~IyzttB zEtSM&6v;$&@cyFMNP⪚8Iy52x`)TtYe`?kTqtzEt1P>ki3jD{b2AuZN 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 literal 319906 zcma&NW0YoHlO~)tDs9`gZQH1{txDf%+pctGrES}`Z5vZhzuoW5eBEn(^Kb9iabjN) z>#TLIz#&r<6{lmOXNMu{FYNE=Z|cv5VJ2iGv^Tbb;pJtJHMKK$u^?pmD^X?;x3qCF zbz%^=F>)~#HT`LCV#?1C3<07!^xib4tW|3%|%>`a*IpE zp*V#Y7yk`uCthN7w>yb3u%Nr5l#FDBJ-CUaPJ%r*62FbpXXx{~J<}ET0XiO77TH<)hr`(TbH1^Sh!2NP1)DrF|7p2@T4XT_>+7Ekl#dio*#8!u1^qhn5 zPo>c|I0g=zY5sow+Pr-^s~F$Lhh(2V9yZax0j=W|zzf;|sF`K?_m|{`XmIfR@A7rN zUX5^~$TG7Z)*W0)Ow z$nRgjE3wDGg0pfaZJp-BOstN$wsrDPn&bnmRg-Q%q}{*a%nH-#}#byW9E3 zoerhz?vBZSe~q{MnPofCXQZKz zktR{HK+;`L)lm7jdqwFO<(N*RMNLG}!;TToagRJ&$y3NQ*+-Tc-Y2~hzzAg*)e%U7 z6?P!`iYPscv9xx!N~;oO)IC|5AJP|Ju_p+(>P6DICUZJGzs)hCTy zCTD9P!S>7AS}nC44+u4r6E{ds`~b<|`oy79&5NAAxo#t9m+9j!tUgkH*Np{ym177B zer`X37qhSH;9dlGJ3Ldn=-K-QfD0;6-H7h*0~XkaULp^;WWH zKpd@LED0Tzaaj-)0#F(vl$V+HEAbn~y}fka{m_%-K4z$tpko#!j)t9`PV&anMr#5K ztON0Vo}3&hz3W!-)RvJ-nmSfR1(Ar3dbrqSOf)M*(`AWkU~$WB`Zx|ESA7^pMB~be zx{og;ah3(Mf_mVXOXCT}cAVAUE@Fsz^5w*{Z-L=t6+E+{(8%k^3P^*?8R@+pWsLgf zW9o;*ep4+HGmx4?c2bagVY}tb)-d3qO*Og`*l;W_~NSL)V!tc{lk&JqVSElq;a}N~H zKyz?QoDFZ?k!o?tzyoDz69^8+@JnjqKLUgMjMkO+Rr?zSj*5UuyBhWZxsglbj1@|L z*d>>gzmPBE`f#mifkG2yTTDQt)7+$Qa0|D?m`5sWJD|Hz7QZn*Nxvw!OuEDi|LL0? zlKQn)#b~jh%{kc021DP%o`|Gw`MtUgT9Hv~1F!9IO{x+5b9kTHD!>GvRPWYZlQbUS z(ABtXNaPiu^@NpDS}rkj86U3dbU+spv8{%yG?R2zLp-uW`6nnMo&7L|C{o_R#DX4y z81fNfNTKW66@jj1d;%aH@N6CN6xfP?e|eP{C|D4-Mw3*qMQmv2uAwA+rG)BHWy9o9 zGE{>GW)_J|@gs~gQYLoV4OR6>A1Px$Ob=B9{B#M1W!WZoJz98hQV+Etk%z11=Am$8 zmR7JBIak^Sna=>jOM}y?l59jkD&Isp4#LBlk+>=c98S}E$nM}w`AcD3HlDdv;ux!y zO80vx)^aw7*nB0=YkVykt^lvX(naS$L_4}=GK;|^#?dWSfEmSMh!AB4&ZN30pqX2^ zyaRiwgKnf_#J27REobb8mfm>YekxebktO_{JZuodZK!&D2@&M1=w{?MdoPrfvZ04S$o;I?;wE9MMfW{2=98uQofB0tYM0t5 z$66too&o1b-dtJ6BQ;pI8PYGOIg;6_iHDe2lQ;ux6%Cj6swi}n#PCEKSTgaSd*is& zPZo!|a zR+aqq%)|*)sj-7B!)kJMzV#SSi!QQi#9ck(Zq@Tm%mP@+D~gL?`15E1*`ZxY536`smr)-`nVvbQKe0uS8yM;7A&k(9a@#BjaTDKejOt!VSEo}e#5k{6P zRTpJ7nUW|$bs^&Z7~(Y%30JRZkWC}h)_@g9)JINsxv`;fGS#YTS{nI#$4-V)FTXH3n3NXnH5vNZd>Tu-&{p#cbm^wKL=od&#&>w zWWU$P%)scpmf5VH%$5j^j8sr0Z9Cl8j}dH~g~remo2j9o7Pf%d`t4ADRwZ2)d^=|6 z*hm)dl?Vctq!!1aYO-3T5J}Ui+5MH-(!5>i_DE{1`tzV`pFl^1>Mc(c&XE+6N9&H)8lf>SrFrT z(bZGf&heh4gBVCrSGB7{nL`TqUSR%slNlT_g3d&%tbQ0Opq9|TVLS^h(!H0V^C#Ft zC6Yo}u;BP@gU+NnjSt=Va#xR}1JsW$qN;>le*6>8?0s*LO&*(cza%2RPiqYtnud0IMXPR0OIVW-ILqejKD~&>QPwRPI%|z;m za>XQBrK$=@wZqt(b>T}AbG`wOvmS)n1f4lnKFm$Un{X}5!3(4ccW_W;IZ|O(!l7bl zwPqcWNcoi`olzL;wwM|Uc;zp(HWBRb<0WrA;;~F(qg$y??3Sk}nIg>3%C%QL|huv=Vf%dx7i%S=_vHKKCs<`p+wI1SeJ<@feMWiVs!dfyBv@n zr+mm?nBCJ*WEQAah*nk&Dn=zzBRi#AO!fWRV>0jr0Ui5~oHbLS(xv?`UzA^5xc%L3 zQ!u~TL(sS+dxE2e(^?zU%GTJiS{!!*egUyLB5ebm{BUhwV(>`hKPix!T&U!zW6Y(1 z!k``yIU-lEx!sfVSQTau2N&!C7V9_06Axh>RE52%*yZ>F<8s$j{G}O&Z>mQ(6)9x!E`TThK=BK85XcWsqW&?G&F<+{ zmDM=`kTZ12pX(%N4D(?9@)*P5Biw*2zMXtYm!x+EKIhs_tJ}0l(;vZuZtEWW$70sM z{gJ?Y_6dbpv(6$RAaQyGh>6p?O^DwNFX4SJb!YPMxpMU?!hi}den2%$7u%P7<_`Kx z8-e+V0vLfJX+}8k&+}t7KShv=E_8jN+pq7h{q|=91B?jLB$;6aj?d~z8p+I4Nl8;O zzD;+eFbYb{jGzZ3>W{S~y(YFMdqAs4RkGDqxKm}H7hE-QtH+Km*o>Be<8AP!Sv`^K zHoJr)NW)^jZAu=;BRba6g(*Y4V{usXNJ{ll2v%Pn>Fo_n?ICavKf#A}iSF@vvqSN`R0pa0gBIH(gkm)Bj6$mz-o~OuC()q}XnWQg+W{6;`w_h_K z4Ai1|&b(_*p$s(q7{g7*R;eTv-+f2HMu{rqA_qV$RX}A6Jtk>pydUpqu3?(SY(K3! z^cz%2qrw)()XwBT)1JSpe=;SQe-fmB7A!1`oc|pEhmeVt?Z0JGOswqx54qIk-&|_H z1*z*@18T%JV75lnThiaCC*_h5_QI$h{@}*}0{xO4qkH_<8>ea(!!@C9r_be=zi`PPDLmLk210dk){(aM+ z>x}=B-eu7#=0qaOPtWJ(H8h*O^AhfEx3<23qYZQ&R&Jmj(Ev>n(9oG-UYeikZmyYa zl-G-YCFiJ1B{EKa%%AjV96@5X6y?e*eX$2@`s)Vr zBkGY;e&w^J0-8EA#UDA_0mxPKiDhY@a~I)<0*+Ij10k@~Vnx=C7cRvHFw8u%VgU*Q z&=WC52mUa$yT8YpvDmI42yzYT;{R6IM;z&;Rjks=^nCNs^7)dEbD=lBi-gmS5Vye}fc7g6azloQufuL8mBj}+85hl;EdnBM z(y&l1!|$Gn=7ACHL+MGg7ZO#Y4?V@CXN3ZPMxno=sy^V+H1O2bC|%fVZHrSE>d(sbxH_-Yt~zyA42W#BuTm(S z_yj@WNODi$t+3OLU0Wgq$mr0|P{4y}26@?86JgD;j!sLOp!{_Y2S<5VN0pvCDwuTC#I4zkj#=hb2u+>41uYDpzcFYv4%jRJ#R;PTB7 zNLxq6a-UL%1>J2-8=d5fqKG5mYJ>SdbxG8xA< zjdF+HQK%-a(*k^lnG%6YQWU9eRQ`B!1NEJ;4V>4Hrc{QC*2u>KbX}#4$8` z+Oo)LEq;wPKwn4ANWX76;h%9gOtQnH{f-;!G2baslQ=o{_359dvmJ_=RcG1^vT$2o znQSJ|so*iOMX*eyA1ykJ{(zE4ROf+9I8oA^RTG}NQU zbvH--(e(Gq_s~Uh%(;DTfqH&MICXT@#%u8*)~H$bTTr9eKXvr%E%I~T^79F`($*= zPfs_u1-iZgsPhcDT^-*KhuigI5o967%J31?)+wJWBZ(PJ?7?k*QJD>SNjq^IuM^d6 z8}!seZH;Fe=%}Zl=}nbZ|@#P3ue%<)UE>wfU$MxT}rxra9i>RhW|g;Wd)8-2wc*Y)bN-euu?X_o@mxj7nJ zXJ7!Vu}f226xGft#B4kQ7@Gn<$n*)!YwK5jT1wx~Q%4K%7nVEAo1w_Aq3s%fh}jq$a!fIX`mkF|IC!kHS zJRVwdR~hGzbW}gTQz2tK%aaYf$sGklToWl1?X5YObwQFi&RWC44Ykv8Vle4250A<8 zPcmz6Y}js++uHP@3h+FvY_6H=XimB(p_k3k*I8`n9{y@kMLoDtNsSZ(UMK zWx}`pxi@|i^-ZcBI-Bgr8N``Ed^bV9T%)M}Jes4hj(^V4#y-Uz&VS%kHEn+pINKh) zOsSZi&2R~&ubz$1^>k3?*k@C8+5a60T!dAo-DgzzuLp6^mFb|Y;ci}*@PM=4*3lm@}d1tGk>6PbR zfyF%&KyMmJELy^7ri+q`u4GtMDzgK2pj$J8Ze^;i^xsud+-ID1p<*e5259DwEBKe zF$m4zSPg2xGV_A-)lj=h7d6;@tsxzC{Fyh&$J!MKWpE?~sIOHP0gABL3*B+dgpl6fE()F1;W_s(TPE_Iu`=Hji#`JgLC{MssM)bPqdDc z*H8NI?_&XO!xLY!R^Lsb=XyW2=5^4^WxphM`&CbR{13v00;yjm0 zXIw~P+d@{$Pi{?6D&6}IVLOr{snbrnj^<*A$$OaA=p^z#22j?7!PYq%BYzdCI$Iax zT1#-NJoSm1?V}gdBTaHOCf>NV-M)eKxu-_|2h#g5F8dGF zW#(Yy`Zw)mVdVO6w3mg6@&A$b9yI;aLYP_Y8Sz#D!v=Z78t_MqfxICa>6NULf{Tr( zg?qU!kwiB*BVJrjBuu?9{3UL+2zY!c*GyQMDB0HU;`jY}B*^vsxUZf+UB4giCTFir z4>z5YpYA)eRPxq`%jN%mJPfTaHqftfP?PC5qnPh5^SgPqXIPtXn_HTcZX2yzJqce= zw>0GUd5bWd49-?7Rvf2wqpGYjkt8wik^%9emv*uYR^q%DUB#4D3kal_X#K!D5Xu$#xVT)DmY|5ciKCKR)C@~Ls9^XxO61hqVO<#^`9u7B@h@-Gp z52A=5G}TtI{XZ0lTqW}+Jbqn|yGw@o zWe$Ev!Iaa>c>=_bwmVRjul_&Pug+S70gk>p28#i(-9^Z!~q-!^ko)zI8Q%uKHru zCutPIhYq&7#m-T&ea5FD!H$)A-q4Y0!AkorzM)k^8`O^13f!z^&EJCX5+Lvor^O`_ zz|y+DiQ&--Ba%~gx_Xyq)GJmicO~`fP6rT_r5EnN+ zfu4JjIZCg&Q9Y}x5EBO5B}_10?9tw=2{S5Rlp3y0u5=Yk=KXEEmvE+F)~&=o>Z}-> zpXj8ie{^MHRWL|#ArMP4lmlgrA)&qzL2iM>B61~);re5*1tVDFW9LJGgASSC9gj`5 z4ZmD4DI3W;h6`7b5V^oN%NUH%S?Bt{BRg>pCJhY4dSQmO26(*7(x7jeUC>ej)fsEa zNlWCWBP%0)2+RZqak&stK>&UYBo0+tXeN}vLB&|O++QWKr?_L?pC}up!r$m?D9x)D z!qI?_l`j!LAq(Y{&N8U!*zh}HI3fI*E4CS6@QDQ_#L&a%j<>kV_=dnBI5$1BV+5g< z7(FAZM7?;OnTxtnSnLM-#l7JGm#FE&E=|-R$@HF2COI)ML+q)37^e-6$wCXder&TX z7X>B}T2-Wqr~@n;J7|xF()!~LCIbGaMGCm zxMgVKRu`1Fnh%!oOT&w>jYZRrbsZj_wWa!wQ8K)orpYr{BzW;2i;%Mx|76}|lQmSq z^EMJy4FCod2QV(g*V{I>in}9_j8@|My{zgQ;pjO^cET8q@M9F`5`hAz=kkl_92CW=0unjiM^?G-y zk`BzJ(JsgxMLVwIoxRl&uClT?BTiL8O(Yh&LwekXYf71uNj6$E#podO!|q6SOj4;( z?FCNlsN(X>%amSp%0u1s1RE(Xx)N!^O;XD+m(6mg@09~E(-3;%%Q+#ab3gyr~^G{yID-}{jv+Kk5s_kU`EB1)PeI= zwDku@gb6=#o@ez0x>Hf5hoysR?Xto-Sh-h`9X9B^T!)BB#X@-|t<)@|jBXjOIiWzjl9ClZ9Po$scRMRXWoYMR zF_F?tYd6Q_`U4z@F-M55j^In#O4Y2P^EJ>cxmW|bL>R3j^1hoe{d9k~!*0xOP4T@9 zyS|lQIo(GK)J$TuCYa>vyO?0S4;Tc}Xg$xa-1_*1eQ*suv?>D`uUgztZZB(74q0RL zVcEkFMwU57^*7{I0I%14f8qVXjs()XwVF#|oxn*p!2N*Js|yM5*#7fN;5~Q5mn>)6 zcc`BQFVm(Bg3?v$9;UDAqLKDhGtbcF+hA5t2Mky_7*f!1!7q;YwJ7Kd3ErTusR82A zb$R1%7f+@og;Ft$0{j-O&ks0NL0=fU zi*+-z_z@p9z1UHyr{P8cL^EL_+5jI9RwI1aMeFdK+Gd3<;j16mP^Qr$W>iJ?LcL*S zZg)IkV{uNPR4gC}+C#Y-B*@u{4MH0Jie^Un`Bo6|ipE9#ZslIm<&VQrw>42+22kBY z(Uy@$jE~x}2_M#<8kweMHWzaVEA!^_HHv)e!bwAE zhGbH4ei7ViGs!vt^hB#PYid{5^b5YxrtIs)88Q?n)8w_aHGY3} zoM)kfGl4}ia0Z_Hw_TW13g%r$m~Ve@j;~ckyZHe>(#o~kHB%&J!J^P1u=JEZDNvtH z(k%K#6^WAfP^IARa43NoXy`3}UC;M2iY*_(58`nImdNfsL=4zOZ%|8h|R|*}6i^ z5pK&v;zj>5uc@{s2DHj|vGF;A8hY=#F4xwbTk!~_R3O%!i!|vAZp9H*nDB-D9$?Y( z=<7DsxIgVy8-|Y({sUcv?^&1G=c5NCmT0`SJ=3s3plI5hN9YWdp5G(Qu$AqCRmNv` zN*;WW(qD8TF$+qRe@lT`XQ5-DSjK-F@kZ~;pQ?XwGWx-N!NQYpj5-vwC^;lfz0W{+ zL)Phll1N++AQ$2n({dCc>qED_X^@hf5h)*VGsJ>}@P0y;>CaBnYgcpmFm|qxS@lL!Yh^&9#70;WZ8a9UGY-KO! zG5Icjr>RV|))m1K7qr}APZ~>xR&Y19(Q_2f@?WY=4Eyk;yKL7uqd%pBOFP(xpO*fu zdU@^vD)nDhsIT{<%Vd2e7^KVfSI=ouSGw$zqFJ})z9BUCbjSY(*8eXm{~xTMi=Fk~ zte=(RzmE;Ea{Rvx4lU~GMA9}R`EDC9O$V?nbuQMxO-x(`f!*!A2;$4)%OX=S;U{7O zzI~;Y%Zje()Z8m<9XdRw%O#2wirPw5m176Aa(sPX_ulpuzQ3L_yFXppK2N{ywi7wS z>lMFDBz~y__&iCbw?^kdl2}$6u{nEDk_pwSK@H#Asmpq_VM}C)aCvr_-;W`l2wO&Z^X)eta79m)R z5+AG@ES-z->|wU|F;oi!Ua^L1+4##yvQ=vXD+hBuL`z>r_%f$GUR9Bo{48jXRnvbN zFq;?&H`ba0qY(_KTpIew1L_qhu=Ds2fuy>>G|!8m(BS_KL5hc<{68QOX#rDT?ttc- zPd$qK8=@gd!tV_D#X4T38^LrNkn+D6^{2AVL;t}HW5Oo!Dg^AbZ&S*j#L^4%BLOXM z4uQYM_efXZpS>bsreo0GC}16lG}4*}qWw*91dH&z^DZYM{V4zL^90%Vp$Je3P5}77pDqYh`{1j0eOe+m6zw~+3n2?ElLd`+StlE z`a%*CvKs*<3`1s@f36-Xz|RGw_Qu>1;SK~NhQ)Jvg1dbzb&NXwQaC1!jnmm@3WHP(CL||YQP;73ub%WV2O|a>C!FOyeEEH#BpvMnXQ{adK>n9&v zpBN(ba7GWnyPV%njjmam1!Y3L+jrQ7S4oaI1Q!yq2x`U=ow?^DB+tX z*2QqV92Y;=Q3M+V*dI!dC=iS;l9>|&2Zd5tR`)5aoPe%x`-}D|Ka$^W0p+{8HF+Zi zU#fJQ3OBO2?{$~go1D*0$OpJZZ$I`2g<}(w0?!|3WdDv+m4(k_A*K|TH1`H==1q%) zuqSfA%HWn-8^+tFyIki@OzmxV1BlSxBA!GM|w5*!LKxVnS(MYK4lO&yF42=plWeGph! z2L>r^BQFdE{`TYN=?YHdJPTYP3S!xSTCCx%^y^N4lC{FB^n-hE%#{tl%PyW@Pi&xQ zRi8Cv%KVjoC3jw3n=tSQggq-*yx7;y22KsO8QJ7*S=&p}P}2{g3)=?j6_naeASiGY zTcs#||H(Te;0bmKwY~+AJ}?eyLN6dwYmrQTaic8@!}_zIqb<^`5^`FufNzO3c!ItB zs$GVcKMN!NE%Yf#%W`DFH?^e{<2w#L{2kGe{Xd^}-Pa+(+pP$Es-P|b!Uq>3k)TgP zfmld=oE6oK<&{1bo&)@pHu65KSPOYRanY_Tp-U1M0M)t4r~LN~xw=?x`m;tv+y5|P zM$k45neU?*R_Ox=c>w@pVNBtsSIqK)Lj2ZVlJ?G(9z6o8mlmx$PMVt>0+QS$)r4fc zi~HR+R^bgOi7H6zl9Y&SDz|Gwoj?PFAe3RyTg*Mbd^29?=SCf~*Vz zV-c=!$CQI~4&S32SmB37l2g|LFSysJ8rOHq{vIDm*RcS(ntdz?G;K zzgePMH5ERRgo!0h%={6PKCh4Lv(DDLp@>$jRqDcDAmolZP5j+L(_( zqt$O;#Lcmn6R-l3P;FQ{>7uPqZGOt`vtw`N4mHYd)dJti6g%3o!CuMkXE?YN(nMR2 zY9W$|H@C4xpO3tPg(li~DPUue->=4CvVem#WZCGEZTm-O)@-V@+h=CUl8(Dw&ZA%B z;UD_s9Li5MSQc%~CFO6`i-T#q)Ql)*$QIL?nC_u#ZD`1MZ4aeUsVmOYI@91}ui}{H z)YTu63SG}o{2dK(R3UYYJ<2&M(8(~r7j3|O_PI*Wv6~@VcCL)(r8hxZ3-3!CicFCx zO$B3~EW2(@Mv$WNNFOLGAd6L8R`{cVQ>SwvbnPbQd7`BBUKhp7N$8ZIG{<51@Ow*H zd=hk|Vy3brB^?;PoMklt=hfg~k@zwa$(ZNjq zD|B>e1Y*`Z;{k4sbrYL+{wDQGo$HEOUwHzKj@i7zFCj5j==LZv+(2CRsiYF_-^?BI zixqjz+{w(5ya~FvI9@GZpM4H7DTZ*_2bfsK{^@)oZKj%61B#)RiRlfe?2Rp)Y?d;Y z+OzIlZmC`Bg;L-AKUDRNxRsMwHPDvxG2Jx@XfT~+b<~xf5%mB@-g)--`2k~MK5651 zyJ&a^XvG4|vqQ<6>Kcyog*`E#W(6p1hm*6TJ)#uR#r0baE`p0Rv0n16M&5)3iwCGn zIM;Jh7;KK*tE$#Tqe%oBOt6iW!}U;Y6Qj?bGE#c?R%WF!Ev$f|jT6+gXdo*MbS}V! z1}+;iGtd@|I*I}=zjI{}#7dRGLvKQ%fXjTh-skQMX$jZmh1RDqGx2iNTIl|7)*dr( z1I`-*kzT#-z;SAGZHQ@ADlmxDoW}&?M0k4*o4X%y)a;uN-zCP0&al)^NUI)BX0!U> z!Q?n!@Kg;Yu`j_5N!Vgq_20x2dEVQtS!vH2t~MHXUqt&1_t)h^m&DCf$29IO?kX`5EXt!Cp2@o=K>>d0a$MW30J4nA zy1R8pRtnq2S-2R-WhB&)fHR&P(EUY{{@Qbo7-l)NWC*Xm5nJ9G0R;T&ERQe4hK6H!=w-=;Eo4 z2}*Ku_BC^_he@%4TekiXyAs!`6p`DD0#6eoA*JJvK>Bk+;mJM2Y0!qV~5ml-F?!|GvV9xH%hlrOZXs!80}XF_tfPvFbCqK7Ikj-LQ$r@8X;5!%z4{7+ zCLY>;sL?Ib$i!X9$SxY1T`gix5XXYrVyk(+eU{+G+jtCwmk3%^GTg#hINJHEN=6=oIGF95*W{S=8 z%qZIkc%JQW>8EX?&ZE%$gmqHlQ?`0`kuUPCh1usTT}a6+YY1@zGgH9%lC~iGxFd|rUJg2WV+7$ z?Y^zHbh^ucO&c!CyLM4=@a{eg`M;mzFrJm=<3hf;n)sXd8|$jMEBTeUmI|dhzJwYg zBCTdaL{b}z28X>@;Fw*#;7Ql5*7d2>Ua^10f`e1$pI9X5nePoX7;$m6bqz#YQd>D( zS6*x&m6(9vf0F6^Hfbh_1&~YaRf&yO-ACFdNGZp4P`1}`S|pIJrSS1DpStssC?MEP zrzF{r*{Nh*#CRWyhJLg)6)7V$8u*Hobl1vhs{8imT$ZMZkTJ0jjEa~$tuzlOCT`6% zV;yGF+9m4eMoRHqJd8LAiSKJRSaH+(01V}Boz7&zGs$6jh!1up$Hd?F4bVwLKdEqF z+}ZeX5sGXmC6{nN;*;RRA#U?=4>PeB<#^SN;kG^7#97l#O;6lO?HTz=wKk9m+;WhH zUL3k{-xD~B%Sv~aR_trzvqJ%iQXx282*aVi(@0$=38Yur-YcH$m~sh$2V$$qqe6+K zvrBHT+|y8@?{FD)COboXILac~>ytq+aX5Al8 zHsa~Uo3>%_Jh5OE`FuH*4{uEcA>@%%pj`=Km%R8Id9Ykl)3k4dY*h2%U3@221u{Y-2X(77>LUX*BOY@B6#C4MWYvp zq}e9J)DnGLg(7E{SpVzAX6`wul|ttb4K2T)VD!59bh;P=`F&k{IHDj`eqZXqEM=4i zzCLuK7|vJnk|34PLNl9x#!C@JE+te~kfaxG$SQ?Q7nZ=sn9n^8`}0CvCK0fn`1~k0 z)HLwdVj<(&x9{m=_D-VQqi*D-+A*+E=`uw-o~OZs8S0RiO({1bIL)%9yN}j(Kz;k- zJW~0+hr`>Mk(bG81;PkM3)38_>n=UM)y_|mMb8h7bM}j;6-B(2O?+2Rb03J#u1>nC z-M#6D?_58E*~3$F%sGLagHxUk63u`oS@-tVvX9SG8jC|vsn!sVfwB5?r+GauQZ%4s z?rGg2agRdbhK|tY=%jOAj1gL8F;)7BWE$Q>?rCu5SkGfDAXUBPaTkP4AA0Wf;&6?Ha_N5O^g{~uyGU5;&9RX~ z?pEhkB>b`;boPCwaWzxU0TU!>1>UBY{?AqaHa>etuL}YF4+dUkBI?v7=ASv*L;nS$ zdH3IXO07*xRX6_ywxI(tRjBVj)y4)Jty#pY?sW<8C4((IN$3GVt~szKe=kn1uwFKc z(*2hkvv>0U=S|C1lbEgliRti93mo!)xmQ(fdQ|*xZ&^1ZY1GMuI%qJCdrprw+@@AB@j`11-Clo&FECo_`f^{;Bn_vT`y0yVk?O$@$+pyd0eWbbbH-eiGZ%m5C*7 zhX32)wdmmhU0-?!hQmI&Iz=$<2_1m_JJ)D9d`=o-p3Mo%`TCqsT3%RQpgC`vyBAJIz&qN zxLQFy^tUa8FYf||wAtNFL4zj$GK7MkA2w4tJ1ifO_bil@5>+bt`0<({;0L>PkhZ{K zaLn#WTTJS-3TTd^-O_Yvmqk!uZmrd$E{@HQKC@`Wb4UaKf`P96LucC>AxgH)NhkwW z;i(C&X74I^b||BI@zO^CLnVLt|3@RQs~(Fs*BTHrv;6&Ww10n(VW8*t?ewE${tz7L zhp(ZL-WJ8)?#^zrf68??kGEqgBsA>T&DXnt0FATqv@o=%mwApD@2RL$WRdX)SO@mM0U$mvd@epZAsUr zPx{grgv*<^9;HilA>8j^4%1` z!O*E1=fOz2d0>!~sv;BIIneA7P(hKPdpkxAWexkvHvn1X4F+ zg6Gs(6Bbi$(a|N51z6KV+2jpU5caCKG*L7R(xzvSn`3NH32HsHdo|Y91rL6gsI51Z zeYrxD5@=bcT@I$vsQo$lw!!7_>ny!P^WpQ^tFP@9~D3^?RGWgSDg6qx}jj@Rr$fjJX1Ap12Vt zeTQFSg9gg6jKYxmR((S#fmADRaLNE(Vb&O3WZZeZ=UpZ?h--(H7t z@yAKQzzA`O*K3sK3`8$Znr@f$cV9sZuf`_kVc7w)%;JI}ox(1glgE`4Vo%XEBwn1a z(FV1|o3Bm{(s;|T&FjLDN{w^B8nJmLkAOI@1V+z7X(<|6l?JJWfu zw=tF;TMDcExD8(#(37+B4CzA~boZ5M@Aq+&j=Y@9@B4OhJOB3`iw~Kyl8B&ye*Z)y z(Mp`98e2={<0j@7gPPphree#5;!=%a|C4aNm$ZnIz?zoc!Mp)eic@3 zO=bZ|2E2Y`IdI)HU5xZaS&wIpae{-$K%p z7P#w=KIWH3BM+85QrH57o1rAvB zRi>DcrkKsyWWyw$yKU>>QTAqi0>hnIerxvRt&Dox{J4(($_A7EtwvHNtcIj;3XW+F4a)Xv z$QC=K&Y(9&$(nb9MOm`lT)S*1E1|0<76b|3h8vZ##!)NHLyCpML?%f6Q`v2qw+Kra zJSVoy^_K%c$Jow2k^1M@=DCu|+S2#!55jEIimO zIeR67Ya8;QMU}y)yg<&Z9At<_Ix6Kzuwo%lSdJ?)`3VCRx2H|HSrmi0T0jii6 zBHA0=1zRM{FF}9;`>r_kMx-b2xkr!b?j<<^6!yzC-W)A)n}~#RPOGbD9j8Q5{sI8b zg9}ZeL_y#WjuV=QMmVe30Thz9#?ff;EV*#1=Ri1QW&J?b&Xbayy9x>ARoYbfaj||F zS|gnR#mF={W=C*m(QM7e-7Vwx)oc)r$SDABD%RZGD7^Ybp)=jV##r1n4J&+{fDJwD z(mf6<+(8x|@OY4R{_H7uGCUV$R?4>noKGN=z-bNXFpNpxJ$(+WyGqumryrZ}6OHMv z%KokRs`BFLmG6W_fR^VuEPbxOOw57nYA2nMzhWChH-Gx0=An=NG?+6br|Tp$>5X1r z^Ys#d{wLI{|Mibiu1X@3nuJg=_h#$_&yu-=5GEQ7q6c@+4UrK|_3FCRSA`m!N2t%1 zd;op|dx@k`YBLFd0GWlmXucK)$tbrPG{r+yO-4QXDL`I{gg~1c?T`4*+Gahqqtyct zaz3_FP|?_%mpTVRWZzUYK1Qeu7us5|*dX@&Ko44?lD4U;s>bM^(n{;@?+}&4Ld-$X zCs9O-v^i#AxSqs)-QyPb-{z%FS*X9)OqK4c<^&^78DS6HLCs%-pHKFt;0gG$s39Ta zd5@^$QCGEMw(sys=TE2L@i+>p3v=>Qbz~!}Z6(xs7 z`){R%@ZH3mS^I&1zG`v*XgeBXRY6{95sID8QC$$lR?Br1%60vttat-Oo2`51Mo3Vk zZ_1i9KiNe{IaU9YJ$C!;LyMRrq?AY-_c7G1V>RZ``RKcoAX_`dq7m@rR;cO3VkW}K z#%Gu#I!^&!{uMXZ3E3mYd{8W$^H2(C2 zRl=QGZ}QH(c|GQzaBBP&AET~RKH~$0N_fVza1ov_j53Iuk}!V@A=1Y`vhKZvurCTn zWpTk%B;xDkKUs>SU6_(Uh7EiAo1Xp`WA7NFZIZN}3PIsatmHM|VgYCMCRQZMMhaT>cxl;*Oo6^Ja$ zr4N7M@VtyrPeV}1bIO=m!2@0JE3nwKjP6RMwG@DWs{hGK5im7?$}6;??m@uZxw@vL zXK9^wVl$7Pytp^Uw$)58uW9^9WjfXWxD|b+7HkTY~sHa!q(RIFK$QguvQI z<1}Q!+-_H03VBAA5$_OgxkmmoXVM=di}CRV;yLjINjIX*`5bqoh*26-?N6#F*LLD? zgVB^p?Bw>eLU`juk-E4vi0(@IX3m(dBYH_P-0JXg2%gi?T7$58fIbIIVq1YNHQe~l zp$eUW6T2UR1FN@h9It2dRA9Jc6I^uLD`VtkvWz9M@(Htjt z^QzK^pR$<OH>ki`;pv6c0Y$HlMw2FOh0@7AQ*7p62<}Q( zlTZ;;aA~=}8i@(qS0dWYi`+T!ae%7aJDR~q7o+Hf+8+K`qg(b!f6Aq3*HUZOM-&^2 zE32xV9@Hx=oIR?B8pl4pZ_-3SwQj~)*;rhV-CUo$wTNqSuPb&Q4SVT`q*p#%Z2!I> z`Cw>ZLdfauBrB`pCIcR&yS!x6(Kv>`#s;Y~8t!4gTDUC?B=SYFy^1hX59Msb5&;+3 zjPQB&%2kbMYS$~XVI-yI^ zw6!X)M#+|Oywa@cIf`NipMyW#0v({0Xo3PAWY#wRxH6)bM3*hQ&R|6lIugnUi<;5p zgmObRCOm&p8XiwdmdY2=Vxu&RhVJ}9=X-z2%-S^O0F#RuZ72GsW z$-23_Eu}EzKXJ#DEn-*uO1-Jsbj-9Von5k-1BPvN(F190{v4l(jWiXh{|)2tKWT*j z!8kB7GW}nS11AT||ATShWnkE{S?sk`VeTddqdDL{w2}V3}GSAn`*~e<#?)UTCL4IE@ z9e=g_4_{<*Mo5;tTSM~wGYf!oMlP?{&+GlHYcaj-#X;3SJ_B9%J7*NX_xow0b0WVx zC3$gbB6?yy*=5lP^ltIw25Ld+ZDNUgnx`V$-6MMCkDUDd3T-_#Z_3FiRrZ1?&ws6+b~rh87*xG&GC6rnBC6#cCr!KmRILvtuJLbOfS!z}n3mWWj!AuQ(LO<6m64+H;+4JMaCmF3m{4ybee%}xwNG$%!NM*gLoMz~^rxQFFA{VyTxod%$NM;d1mu`mWc{zom4#^!!qv!D=9no3aWro^x%m8GY zggCcbL^%i6ENI6Ab%IeRYE0M5Lp6jHeJEPI9ANAwFnV&iGC_*a`1r=BS!Y+oQq# zc+i9NVGHT10`a2Eua zUFCmX9|5E*{(f)IXH#uIDuGzYc-FYLyrlIM--!PBlKEj4lJ6U`PWBF>2FtyAD8B(K z>m%wY&97*Go!&bi1~ud%D@$CXKmfsjOOlWt^Zb6Ty1*GU_tLC|U$Vn+3YLF#pfIp0 zl{=_DwcXlp(63?C1N`sNOjfczcW>}szMKj}BEkA;qs*EjV76 z1BojCq;0eu1L`^`??#`E+=MMLmeGr)25T00fcrDlz#K!H@Z;6m`7o^lHD-Z`-nSwH zFW5>K7*(VP!0!^Mhs=f&39fXVB~i>xHdzQ{rO^uIbNR(&T`Uy`#u?!Aw??m?NZF)? zEOMI84%s^~v-y#GgW_`YCZk&Kx3DTkOI~bwmb%6Qksd-GT9{7KTJbegD;BxE0i%Id z0g86K4S^N*3MJKBy>fFP|5P6H8;~qw=167|vg}u@wF(>&;1aWA6b;5Od#lp1mtN;m zPHXWrDCo2PJ3z?az9q%}2njnDC?3Rov_9QvV+P6^t*mMehr)pZ+10d=LtZ5(3_b2v zkSRL1&Py{k+D=0AKdiA+HYek$sKX$x&E$?BpCK$cGfold1UcfyxTG%paD}$z$z? z3RSRbpXej+o{-Hw(Q&0cm}$Dca@7lu0ao4ZRH6sdd7nX3!6Un~+57fu3Q3ZmiKlTh z*)X)`Qb)-JLZ`qXuHrK$l?6ht8*!I%ZvBS26!5sAeS|Y|EzxWj{24m#mbKKv#%Ve+;U!iNMeyIf~S?ZJS#9t~$*FC0*E{vxqt#<7-DUO4N ze@iuwSI)MFGx@M5r;p0AjzA<46e zW)dB>rn#LZ) zEE_hy`U5&{D?6B+L5uDqHxgDIlI1jP2BLyH!vMiqXil6S1qt7ohPl3_I3r#)Kmc&t zws|zbL)v7}Yx9^bR?Vn$E}^l&2}z04q-D}rowc$hQD`!d`$*jR+^albSSkjtm7xx^ zz#kwY_z*R)4y3_9WraR~N6MdLG0p&cEpm|I_v2Dzcuott;K7qfC^Lq85X^)KRcEda z0f#utiCt>RqH&mx=g8d6b!nne~0e7hu|GmSuL^TkAZXT}t6t ztkWctM-HI8KG=0KFw5`5fg!9G)c$H0sVODteD! z6xX9UgU7&pD65!&6^?cD9u#LKnHWHN!p_Y)_KgC_|Fsq*dw}}dHa;&o3;x*bN<>pB z|I@zRaDfku&~C>x{-rUj)+bReW|L-&c^0CIBaG{KxuE20#Zfb$cuLliW$aH?6vWDL zY%PN0xmH-mgi&vDYZ-Y4-UtI2P^24T8?vk6QGWkz+co0ra7l82c$yD0DsF<`ZMTd( zr_k0u^QS|ho|1hnmPrAW=%_J`F1Mz3aaj|ErI)R(_h=*5=WoN8l9~{BQ$}>gj0h;$ z3|orWN>%tK*aWr_8>ojKbzG&D)L^4@Bw*y-s;Z;KS{;?x!EUtEL>!SY$c|$v_6sc! z!HMNhyrceENov&%L%>1U`s<+}KOt07R%=#C}|5U2K|rX%L4Ok1|C{QCF;_ z<5j{dBe+4wxk837F|W)`jxFX`v8>X^iX0!y-r|dQ_)(NZ(1~696I!=#{*vWK6r8Tf zmpHx@vp+c(2?rt?f(C_P1z+v5`r^BzHMQI+ZIcs$6X7Zj%u6xack`wWWV4IzzF$bJ zO&@~9mb7{45c{QQoR_ab73wdIXaiEiF4D%Pk(`yTnYBY z&JZn-`G~!;>9ysz@};31o+_;?`!@h1E{oQu(wg;7qn1AbJ$4`P7BFV^);UOcH*b>6 zDDov_5425<;b=ByS^kegkfV^5hLe0f0Q2cOU_VHD}AjQT<5X%3ZB(h#Ae)+kv!NbG2JaW{r3I;8 z{BE#Rz>^j$tN3*Nt!;AHRiZ5d+1-(Hd3;|DSOEuXx{)!&K<2?b8_ntZ>te7tW0+1< zh3;r$f=A0bnRe66Xs0X#gRc^jwf(xJQmp9f@DIzxJnqZbzm4spl#TQM24?uFct?{o8C>T=h^bzgGEFipSg#syY&UQSXI zv(x)~{C0j{rr)}*<0Yn^%$>dRBwTQ#cbi=geB`R0z3%VU{-NQ5;N&7rOPprd#GM7=?V5fU4XzP|l3j?Y}Ud ze}+BRER(os5`$lAwwtDdCx;;{-+aEk6O!0Yr)~89>r;!s&r0}w^YYFK-h~MMz8`MxzMH;3 z6nd@0(uCvDt@o;F^mO+m0(p|x;UUXN%ax**O=~{^G&;(JaWmhnO`kSBq>j4qg{D z0y9`fl|_kxAcL|#XU2qJuAQ(f6vHio!Yw9j3btv6D$Hms-xw2@;8sJw%6de_eUZ2I zFfg1Op-0JRtb+x&J6r%$8ttgPQtNEf6%s}jVi0Flz}VPMr>p(_i@p?_A1-o#EL96+%Xmy@^C&jtIstI?v+eLlZTO7HEA^ zhjcLPMd;WDPChU~tW|UT-mP%n56l=MrehD3bR0Ex5Jh|bG#t~5pnbTwUbE8iM@!x* z@plZO74+^?_yIC z?S2bBNkcQ}8J@&{Ms|0=Z8l~B)7IYGb)Ba3%o#-J{F<6(bY-oQU$dIol3$>+on$}6 zaqoXIe%Qrza|pPt^!UYPxx>k<`lsKA$XePLIX&eaTg(iijubauR6RJ!Wv3gZ<4fkF zMKGLnOr0<0EK{>9yU+eS&*uk&Tt0U?hL={#@Lu{U-3#VYYtN0lH6;3ULO*Pk6?zKI zge|6Y+@IM{A}T&-A>g*h6^I@jE6@S63W*Y>OZ_I9JIQg83QIl&lYb14$SE48#D}bi znG5yN6UuQrNi?WHkO79B^)PNUaLPb&Oz1?j7W-43YIIkd8|%)nxvv`*Ao(T&0h+oe z^%v!Uij?#j6?KFnMiRrD9R74{g;|%{ z6w|plCio`1HaG$8orF(PCHwvZMf|-`k9wRUs<74Xv)T}2)T1#h(ak3`Bb2jjLzFQg zBqql|LlnmlSWif#%*I*)^P zWn)6f0&7|v_w!IA^0M7htG$7K*{}mkd@Gvrfa~8*m+;G61}sB6&~-{mtiFc-=B9NC z!P&nF-3Y_hg))jaBhcxvDIXn#sbqu*w*oBT1eGG?5OmH;yQE)HbNM$63rw?&B4Yu-(Gj-y4wJ<=gA8k;u^FhpJCann*>R8rg zK&xHQc$hbad9Mwk@jnh_pnJCpJsD)*pM@2$1D@eS;F~GS@7urAyP;MNJetek8()oZ7leW8$r zLNs9|L~;<^g5TLKCb*@FwmY5VfDB}dR{7kG=%WU59M@y+YyjY<=&EX>>$l^Iol;@i z4NL$IerrP5+@hD^duX7C(?vMfY?vjoi6-;^`v`W9U+JhY#&sX&{r%kLQQe{ut%rUF z+LiWu2%UW11~#+&I8cz&Q3oiG{pV)jD{iv--#DKjG^Y1z5*}~l{UZ{b-ecV!HYMfw zOE7c>H*xE`kqy$G=N1u3jtr%I!@;(Wb>klgg`ClWmlp`ug9y6yjIBDpL-u%Uzh&rU_}&A_E+QqF%}tLT*|T7SwsI!X#@_F66O zYbSd4ZCYbF`8e|r75ek|->kF#Ywh%(1r^i(%^H!J_5VreGcvROzcKp%Wkk2dOz2!rIQiqd7frGkc>LEj>TH z`TF^PxjElV=Ww1(XELAUa5ArJ?7kxV z_~oG5N{AajW|(enGsJ;6qnIY#e(CvS7-Z zNhhV`7`041FR_F#re4q9%4G43RJ}5NDNp>&;op(okvhe;XL5|EUVgR2GyKa5j17tL zk?Bj=QJ6GG#p6Z2$u`waYme}nGJlLQogH5G%kvQSp2IWC(f|dEpvG9F?C%Xj1m771N=)q; zy*PJl%O}^hV;rCt9KfF3@(Q*`JG?VXpTm#~*<8N@P1FHS>Vcn@j5~hx>k8Ty@gKlB z(j%8W%s=ECFchfU-z8?drZ!sE3+{{kjD}-e-;`LWcRhRnV;(XyBzAbh=&Or_`amAcwXJFOItgj$RSK0TdJrt?^x#oit* zM?^!xJ2=-hmA^9i?7gyVno3E?HK)mb3!-`;NCYB>ubgw_myELt`nT|FXloRxl)0;+ zp{smYYaK`_L>d#Cd{|p!jpN5q*GTaIqW^L@SnG&R$3bUUfhgk+fn_trPyZHvO?{oG z>SF?jv^-kq480I&Ag4NI+H;Xl~dG(PPC@xx{TE5GI&jnqsp&-7LZlt-VhtY(A z9p{rqR!go&pL-I27Q6;AK%;B(n&pejNII+MSUflI?)LL^c1;~Sfkj29i7b1q(!kIh z))$^8Ukg9Umwp}@u}F}#p^ESoP|tu3#h}xY-gl98GHCtz3Z-$ zD?XSOz60@Bb>+!-((N>|$tgJr3u(_VeV_)-N6Cr%X_D_lfIdeOPP=jzs&ceb#8G4U zI2}_C?!?0^zoTXatpOb98Mdv-Wgst* z?=c%$@HoF$opGeA5<7z<0{fx*?A64}=Lu8((cK1H4w@8%|L{?P-wab}gaF)qAI(Iz z3axlNqqK1FBb{hj&h5?PiKASd;a_19+dmy_(AiqxMZjuGZ+tD6-`Ng${9rfNy1E#C zwt7rr;N|=G(@{4?gTko`R-`q%Q#vvNQbSY@p#RuWOa#QTD)maB{_}7PuvxF2ekKM? zmTl;7SN_b9ji+{30SKNflW^Jb-zI?aXIG*YZb|t#mKY{(rkkv1;&$i0FGSXa48FDYImtR{}I7|z(A`rX80VFwQd53kyw=ghu68-R~b|NU? z1v;&xO)dSba}$Yka?{(fSLR3_c2wLb;>XC7_REc^HxFvTXi}eA zC1N0{I5xkQ+k~$-9VvCVyf>@lUrML?-YXKi9M_tDDmp%>XAj>q)9o3jaU=p5cWp4v zCAcb-qa{0MELbuZn73M+S$g>r%#y3g_SbTOgRvZ_K=-i{%Eg)fnm|t8ONdH} ziuTG#jg6KNf0$(RFaGUsm}+$PQSMjl1d@`@LCDm9wi#pH2q!{Bw*O_u-+Wg{SPM>e zv(5F%qVMh4PVnUKSr^y+ym0Fz6DRRCUN%TO7)k=&bZBW6W|I&R*q&o%t=n&Nuy_vA z&AcC^hN0ufSusw2MlsegVZ3uXMZ(mJ24cGICo1QhMm0#gk~IR!A+A5$c+et6d<7TWA?SVAFuf!6&&v zDcA>QUCbA0ol>9_#01?)S}#o?;h+`NCv{s1=AaS8y8s+{kq$vxs~aVmL$xf8L$rx` ztR=!rhMV3Fy5Nl5rX6&V$h=A=n7Tya7zJjD+)b97&`yjDC)GuI?FFq>5;1)CwMc@% zD0e2|2z`plUBa7GAjP0cmCQjjAVWv=B<*-s?;?e)O+t_)t3>*p0p?Aq4kfKXFW8_$ z=$KuMyFhqW%t90i)hMM0b}zaA5Fl|UoJq?oKZ64z}+&u{{ z1JMzeJPX!9Wms4a_Lr+%0YpuU(T-eiOF257vdWaAX7VMZZtHu(##EVhA2Ivj)G7F{nHqV}tsCf11cP=TR+5kvQ$q)P&OY% zGfJ)vbZcK)KUMJ?Cy%eyu-hT9((kVeuw;uSc(P(+#!OPeIr8>ddPm_1ys6!7`(86( z$8BiJj^l5btsT;q^0w-=TU-5g3_#C$NZAh4W$4LHAj^wgrGqe^Xn@?#BMzD8^mt** z^jge)nl?u9(sf9V-DeJduMTB~@2}sC@5apTQCJ2bty8_D!_qF}JdA`r#|hLu2fA?+ z-u1fPJ3PCM@wUq@qRTKHKx9|ndGSxZv~$=DW5%aCnIyneXsD8sQk^!>Cyj#6RaufY zMZWQSXOVqgi9F+kllZ8~St;P9u!c!l*0og4!0^`U1@FGBOx{zkOT7#2UY{yU<(na= zZ&z`j&L8OhZGWJvZqtj6&W>;MrOb9{>tpCy>hq@Qtl2ZvOoj4n8Klh_Vb{U)c z?5%35Yt6|CI2-ypqTHFbuEp2zd;eTGzppKnqAfO7WY; zSo>ycwqden7OcUmaO5)%({TUZ-!9JpNd^0vS>{^Yq%VSmM%goAd+1)eKHm_m^DK}2 z(_g$dZuce*SmG}oSEou6%_P~0puaAp(GU%E280+FAD!WYITn zECXLK)yqcih6( zA*&r{L-fx04*c^D5 z*|n8$)%d_=y#NpGC3YE8-Ke@3BoFP7ndCOc2+dJkFs{Zf`7OkD9D8BJySIHI51v>H z8Fl<48ZwxFp$>Xuf7Ke@8XdPV^BekN;){K$xv$@S=S3i4s&XvDquB_!Y%_-M42y`i z?}v#;0%FqRUdGa(KgL$M$ZcqM_h-_Tf9fc5c>&yq^VK06kTX(~X48i;-TcrwEbMnB zUWR0^Ci9@jBbggj8>Yx&ceduai&PApYm@pcp0oMQ{x#em?mGwoel4$OV3$4lblwL( z{+BA*cP#bpaCeJJ#CO-J^PSlXyIj22{5yKW9mbPvXL3{4LPJ^_i^EF zfG5fCqdGNd!K6zw?m1XWv!omwbdPl9M(3O!(tbw~)T1<@na9hF=S#N%77Plya~GG{ z-vA8iUbFJX+}L=-Xek9Ejs-X|=9HsC`m|}JG;4W_7fXA`@VTI`Z(N61wMI@OR0__tB$%MP$;F)NW3T47*uyU9jm_DV$P?);OhU`r|bi(o{9HQY?r|R{> z5U{XK%gtVuO&!Jw#{ax^z~RRr^H71V(+{M*fnUxaA}EkPG`_ zx=b`w2(uQyQy*6#9fAw9H-0V46tR!l-=!Xs!*YNCtid6_QYf~e}S%MxpCg16IpzXVT2)#4h%IU5r_R6R#6|;{G_# z+=j0f=sG$;i-)OW(j;F8YPTH;m$^amhC{t@m+2Ly4%BEPv$+xVk7iGs z?N5ot19lKD<_@Yqn8En{g5hMQ$Za|3D`COu{969(_HyJ4Sbgc4dTUJ^+0!6a8`!N%ZgY$lyNUvyYjGWa}n#XiN+`Rp0L(cgboTDMYUqd!LJD3-5b!}hVvi!l*mT7VPU@$5ChxXEhd zxur&I_6lh8>c*QT^5CHZ*lXfRFw3zzi4osa^CRw`3^YFo@FGUdfm%UQ*je745BF{k zUdD%JuA{JtNT8Hoo67-n!A^@uwcj%>^)8yy1ckioDtr8^FZOw~J?Hd&rLoyp__06R zx9fg4(U$*Za{nWd?$+FCWHzwNeec&B+ri_A*S}#gdgT22XkPWl|3T40&fio8^=A0B!0Z!&wmbB-=bSV|q zX~LTmFV89^z8FiAX6RB}VamTgQMqgU(alOvF}j>4Mk6L07{qGzxIRx? zqq*Qlu=_UtNl50!3Y}h})7;$mOY%mT>T(!29D48{EU#Q<=b%2f9dK!DcG?wZn<3s) zl=VaOCna?@LcL`7s2Phaqsf_x!!y(Hnbd(rSj4(W4!OfY>kbEBI+o+XEgcVTjJ(Ov zF0t&9j($()z{9~i9gZ~EJDs&u*sqR1lkcNM=2PtiQ%p%P>SrA8_}giH54FRrUdBnM zY?Iy1WoL~I*~yJvoqaUy)7{Poyf2pcH=6UL3)(5-5TmsiYxhf-7!xqrFc) zyYw*d5Ldz|<3+!WFPX=h+je7Zw$?uI(ks4%y{l}WQn;H7ZrkwNHda%60vMlJT=+3| zy-#)YOpJSTrL2~Ds{U;ZFzh)LN3u-^itr$6XoHaL7~HrE39Dcz zb(b0nEH@D-$_I#SF5_Ph25)L_r4A8T+gs^d(hpr$)t>nG9K}9iN8R4x|5VR(Bb#D# z*6PCb8JLp*JO#In4wI83NhaK&<(Hm-2a-7`a9f495WKJc*oS*@mzSe)<1`Hw;A&wV z7ULpw1jCUtA;hj3u0-JOvQ&MV7BK;_)rYYUeHscJD#6I#@JB2EB&TK#J*>sG&`(a0jLm?Y#!6VbGBJvUQM!nnLZ% zB-cU1D+tq^I}(h`(JH{ElJc8=$bI9ky*>wabAVvFuk7ivDPxu2^JCeEk{7Q4o_&Ew zaLy9FJSw_vQ<*$lc_!13JMqs!b<)h=8rWM1az~O++C7JogIRblwFBxPr@n#wi3v9D zWmQXrsb7!PVdpXWWF*j4VtH@d+Vmp+dJ&cwrLg{*yY8Mh+c@PLoCaYg<5sro zh?}z%SK1suJLU*{HSphFF8RpTiuUMC(ob2m0&qNIdwNu)xM>C$Jysz`(9<~fK;HY$ z@m3drbdSF-ss;n|}xv;!7m{+aSdFw#^~;&k%H#G6TBpUI^h znrY#b`$nR1O619@(s$Ir^^ARqY>t=`;lzK9cNc>Sp*N@ zRn(aEjgMJCUtMk!}%kB{B3Q;m%-E=1yw5PCwIG zXPR%)LF|pLt8}Pp`+f(+ZzScjl5@=(-5~kFXNBqXG`AYKny$d2S2Bx88eGnTn%p8-_!E=--b8Q zdjpdNLSKo!wfsaaUE}BZ-VDjV@&9~MULn}7Dw|C2BGhx}R(i#FIJ5*nqco5(L$N>~ zMEbH+WO?`}IZzTmQDklxuvmwQH@DsTeDYf0^~P_GZodW_%5L#=XKu25{#oB8A8p#h zTZw#20nJkpPok+fMqRg)AHbbIOQKy-;bDt@0^aYK0cUR~I1ggO7^TKg=`0uqSJn;S zGjI-V+``v ztzQIR?J@|4<21!{lj`K{4)&t|0tfFl>WslEraU z-dJf`gfg!F&bYc}RV3hBzvOv>x`7D7|26tY7+K%S@qB& zHsM9!+UI3(JE4YE?=i&9@jklr@|dnr1I2sC`VCBM)`q_|dtG>AJX;5SGx;jsnW(Vs zq_|xO;$Qh$DMI*9|J9v26#gG7$u{+frk{M*^5w@_vEk2MKcB}&-vr$n^4i@&#_ZXC93mmqDv;aa^Er>1^>}@IfpHaQ6lHw?cfx3Es?^Kc^T-ou&|ji!IP$T0N1=FvxMW;ms%pqF#a4edWqU zJE2ak;q@p}7`}&?WUNmym;@vk7g+5Qp!`W||(60Z`~EG_^)(`GDthVOwCyUsL~?0DBUUi(HT%W3LVi8F7K?HIQ5>0>Ylz zWVnTrRq!OS+Qkc^W3ILfS5UJl@5jzW9Z`gHR}{(MGp}tKr2?%w?E+ZrM=#{eVP4*6 z>lvbJL@(aVln02OJRzpn4hm=}GaKGv_A^%kPoM0RJ5+7spE~2Rg$QQ7!;a)+$>6CkMbD_8Ol-{^vgL(vW zr0wlJKpVG^s02E=--YI9Yj;|tQYuNY&o9pZXi_xH8pOqknjq6jXq zwyOcUjB;Hb4PHP+p(zz;am$rTs>l~GcKU~@V_Kg=g?c#T>UrBstGcMec({Un2iR25 zt*WbNCsP8s6o>|P2G5^-_avY~yFKn2og*mJgc9c+UcwYo3}3>8t`s|wt~f^QNCpGD zhW4gTRk#LyDfGOnn;dewarbsGlfqIByt!ww(9rC(cZJiTSaASobZaXZqrDRD3}HH6 zMWVS48YXg%ccZa@=%Dw?=Di|R!jt`7y)f!sX?0s(Z7r_IFw&`% zPN?%Th)`vh5D?QPhB6G<)o=k;H&LVcA`5kQ#ddpJN8K4FrBGl?T0Taf+ChP3IaHsZ zH3XxY*bvDO9m#i0HF6=er0739G}7ZNOt|==G-A~Z&+f!MK>#t#JK{@R6Q7~tbkw~D zlX?FmwV)QXPSlo|P9SUPgkE?~8sSdXHSWARA?*5P>7ruf&25Peqzl`KN?{b7e9V@y(tWfz~xzSJQK-#yH)w)n-7(SrB520SWv#5CVSrjSs{G5@8g zAP>UUFvUyg1~u*jDNlAnG0>q(^Ar_&Mm8<_A*;`{Ul$I;2*vis7o1V+qaMm#5)KH) zJ<6-MT#b1(R;Y}W+oxMa9QW+22b;9Tfs$>3v7W%ei*6}Tsz%(EbJT@Rp*G!kHj277 zB9f==xz%_k?>6R3OAB7EldKlrlm0m3^OnKyosawMG40Tll(K=fHwe#hjgH=qaNrle zSCCB~E{!hO^k70QX~rM(H6HLTV;Q&DLfj8tq_!`-6!;?*S5N&~5N&V4$r*&y+Lh4Ep&v)SYh$6ny(+cl1q~#vXcu_HdG@T{24Wl@j{$c zg>F7tD#IW_XmOS%=+jd|?L;)A7|n-T9JFn0+c?ETD!h#SiN_0*74I=SaDOS2U$3FsNY9T z&IGa2&-iw^kF77pJg-irSPTv|M~g&6+VZh=ohQK%mek3_rq5`M{Cv@&tuF`NVkS^b zJ86uZHKM;P0EDmMwY4by66$F9IlF2j*(o+kJ@~%QdHM|;fq@@fI2F(p4 zzf2H&wd{r+2&s6)yJH7;vy5HW@}6 zlWEC^$lY^&UW#8=QKDXgQ6743@aOJ&-FzChworN((TZB_TBvNw!&-gkB1V7!M3}g2$(pEq*1^7} zt*vx88~9B2df}1Gm~z*o-xZq3)>=BU4SY&keZ`BuV&bk=%QMv7bjHQrDzZm%co|LV zwUhx-tv9IdVCa)f&&Cp{g|Mwa>|aUI$hxJ+tSL1g`;k_(&-+fv9WS3PS5H@+g4(J$ z07#dXM)Hxa(!uk}%4>JDB zclg+mR70fu76|47riGW(Sxg~0)SO zX)I`GZevQo07Eb6Y;0=h@}Jg!p7&o(VM7NAQ%iFTm;Vt1L$B;&YO6-T@$aVp%q{kR zQ1;G2k_Owl_q1)>w(V)#wry+Lc2C>3ZDZQDZCkfzpKl-Tb8mcqyipmeGP9y0o~*a> z{XMysxs@^F_muyzXemH`yRH7cqAe54Un~57^~W=@Gt+$!aWek?9#;BJ#y^b>ZH@_dq z$GcmtG@l#Yi)E#n&?>gdAoq?n&;b;i&YJ>6>n-ZF@XZN1*{myza%UYqa_R4^jz`_C zo$j^e%Q$N{kZ@UF2AVaUtzjFYI<3ym*+q2dJ=BEPby5w}t-P`i_gZk<@s>+oi;IO4$(X>e@f<4S zAP!;~X8WOg3=ZQNM3%ek0y6Bn1bhyY053f^;4dRDF(YKV$B`LKic?Irkg;?bvOG|e zanZ&-9}KiBd;)BvpW>n8p9q61e7#qHCbC~^x<$ZRU-n!{-i8Oxt7Asat4qHQV~$8y zboclq9%F9S_YY-X&Z}=Kkl#ef_9yVBBx<|+zL`4T_Y9CHg3amu<%2747#K6_mCk#D zKoXArNV8banLFkgHQ`yd@8#V=|5~a4DTCj?E0Rvp*}&IoC8UJ38|9R}+KL4D9{d?>`-usUI?f*Cacg#OyfBSy7 zA^*ny`I-NmSNq?T{vS*5uiXDGQUyplC0k`1^S=`Q8~ew}kN>s={%3hR1}4`3t-jrV zI2RchIsS`t@xa~F3v1!`H4l@=@~sfjnuj_nru)0t?l5~?CYZaLyw&hf^u>TnYzDk! zlaXX^Q(z%6G5=l@e1cmNe5N=61<>~ z7-9L z%LPh7O1a>QC_;i*-U?CfAU9$DK zY=TL_fx(k$rJtN1&AP%JS+V}@h&M*3?zdyYMm)ktNKA19&y8CiAokc^XtO`Qj)QJ< z^4W(i8#ujnx^i=BN|wn}Bu2h;lONF;5?WlC7|Nc;Xg`5#6UZRcz0wUNpPZ(4MZeNj z_G`Hkf2DVb@dCry?W>TJ^b7Ab8kavfhGD$|e}*km+`(sn`j@Am;|_ z54mEwhrQ1!CwGT=g>-nnV2XME;ue&V<8;cY;`2Mo1?wvbu(<49+bl2LecZdy!Did@ z4hQ<}fk1UwY()2jb4lZX=PuY-ozekg{d_J0l`W6rl;9^z&J(4AJ_5`kb@|iW#hgv| z6=PKQ-F4m>#~l_zdmq(-n(W3?&+;0yE+t<(8{|Io-RIIgTC)W&Zm;Bt9irFL?0~`N zO!?!EujQFQ0hsE=BHp)Ay($%b4$s~amF`w~fIk>@Yho6)9qlUTF|X&Rbjc=rQ`S$-1j@bCD*U&AP*- z|JB)2Q-y6b3g&^z!m_NqK%#j}#RM^OP#7fmox4+}e22m3ArG+EC8!!z8i$}%8`7C| zKH{RuOjF3aeI7w+Ahu*%NRi0dg)tskVIZeuew(Nv5?iTbgu@4hA{mY zF}|c@o;0jj->n#L$Ix1VUujeS8Wz;j0$m|6bS(;ToYJClxylJUayCcGcm{K~kXLw9sb@0fJ(a4%q%~ zz2xYK6Ti%WmI0NilY#U5r$Aad9d(9eLw>t>H=c5a&ih$Aj>IqzfH1ho&g!qW zhiL5$iIYhlpP=$f_6@R_L-llub!h(k@DJ$%JxW$k?mTpsV2v5en_t;TFn5yZsZ zEsphBtuXO*d@%ItP!;DO!@Rso0kYhA2Y_)bN=7pJf@jy%VJ=v$oFOGbn^-xRg*ncB})f3+&H&nWDwutCr?t)a{Rq}dcEE9F5%xMO zlAv=hi9mrbEIhb&RqUC6sjs7w+3Qs`WO2=Cm0CY7JrFgg9>}S9QsPMMT2+_1!_jaT zeo}Sbz@ALz3^y)+5MLIXbD#~GOL4Ak>rkV^52ew!Nd>7kTaj~g*r}s2K4V#mSW3lB z2J_6uU~^8^%n?1^C?Uq?1ow)0THE+?NYY|?)PZ)1iT=n(wY_p(CGeGiZMWv%)#HUS z`I3zSal=R{TAC<^5J#vqNBi>@y?alcj-cL3r5_v`j|(Pn=C+O*->|q9T6%<9sUP(g z5!RGB6~i`3GoYaZ+B#`wnMJl_tOK-?Z7ThWW2P0a2|JsDQ!_v(BBv=1SG3YtirG|@kKOe0qf+oCoXi1_Sy(rAV5}y+d zdfI>p6&mIf2&8}pozxr(GmlCMT4A6VS&Uc0w(x!tT3htQulLS*c+gHhyu````5 z6noA&=5#-#KVg5JnzlTgc!;Vd3pv(woShuaqPY};h@v&)Ub=7h^p)42mk4p9ZtZ-$ z-?>e?#K%5qw5w=p=#)?_Xzfuh-S5bR;-kBO0z=i_gM`}6fbco=VxVO>^~$3SXw(1o z9*#I^o(gN6fnQR$*Nzu0SJ|;yQp?z-k5vIor8yrO#8O&nq%CPsIh)8j67_UvzPPkC zm^);DO*)f-`M7@`;90Fc*T-{yzTkDWNj}dOdrjh8wmqTxJo)T1ZFM-7rLMx=4^-#s z0uJt1Jk@*9cBk~i6&iT+#++>%lZ0^}y_xd04I zo?+`6ED@tLl&YrhQdHl?H~jb|-T7^Bg6v^|z=z}qJ`IkG#IsST#^`u7&P*WC2Xs?| zRZI5oY7?gd5Lu~kHNw?e+qoyed*{B{w4B>Er`j8~=GA@at8Q`d`YXll3BN6|7xYeq zE7D#)e2vOMN{#f2j3rLX&Z$3i8aupa{fqS~n`X}Hdn#SO=x8v(6AwM_oICxzh-PkA zH-Fd;<>*5iEQ_^DKfiIJ=b~;g8&ZxA*f=XX)57GsSDHo*Y8CZw_m+k^jk>|}W`2%l zso3^(FK}^3F0Qm}t$9tiF5q1^T|(5bTYvwFc!T*OR2PWfjOOm#)Y`E_4>Mv-(>7Td zM!)&#kwWZ5fWGVgO& z8?x5>ELc>`_K;fXFkDQf={Fe8lz?({Wxr$P#0f!d%_!FQ^7c-`5eAUC>MCEn~4HfMHhxNtV?_og6I^4gLWHNqzjo~c4S9Fw@zwcpAtV%|$ z@cNw0NNqjpM}K7a!tTjS^IlyvuUZ9>dONewq22~HvZzooq*8hSks>jm7i0o-_U7$* z0&y}?oDH(dSmd^MqtedzSaF=Y)dzpQ?@fSxu4tq*-KKfh=K4@1C$vrJ%dqy$Z3YC1C7I2@jF)} z7!ZRuc83PzGaGsUe4e6c*98NX z@nwUNc}$KZKsZ=;&Lpg}WQ8X|d53tVJ<7z+fZJ*MX+ht5=BhFFK5o8QEmWAqkhoACuS?egeh6nx~D0W6>x%* zW~pXYJ)N|V=iBN7mL}fj`$14QpTl?7`Ok+#wO2ac#ww4U!9t_`ksn!xPrK^h`KR|) zu3*c%Qk0YuPbH~HB+YGqNcM$pWT0z#`w0f*P+Y;mM+J%DOW=$B>KSrGyqC070TlKR zArYgWrxHlG53|xLn;wIMOl3!QY#lFOfwn-#M?y-=U4MB<_rRR`xPEX{w8z2yAf1IP zxLQBD+S*v-!?r1{L)Cr%05cDH8|hy0!s`qi?pgXe9OS$Nxr<1BQd!s~xj^rdgc%T7 zbfXfhUDUFoZC;}$0!SxCuem8SdBubQm5{8lO7Tw4Gy;b0_L@$j`9a74F`!MuAZuAs zt<^!h{FX}Ry8R>z4R}&KCtIZHYrhNp!`#IL*ng67_hzV{wBj+rR~%glVKN+BVM;~-b(1ZP@PKS*3*#2 z81m;FDhAKS&Fp#)&i3_Y8uTy3aJeuw2NP%t-?>9E!IGzxy3 zfE^GZ)>Lcm7L4gePRouy8!`H+vrxiYjB!8NpLh>$$JOD2l0n<%!U0q>+o6eGRh#!e zP^O!0Eq>wSYa5G^o$vg_0BbJf?)E+4y3-IBiN*B2gPMQw+H6UjEg0W45h6BQ~O z&(haNX|j8~Y;IK0gM%Jfbd9Ub!oh^xNzPN_qDZgn#ElL9 z!{-c=sf&JiN!RyBm)!@?eqdcsosADPTvZ%8F2ti7Esc+4d||ZQZ3|@3sZ=Yr))^mE zbnCBXW&%z*GLIsO?k{J*c8fiovir+`<3sF6waANIi`SBUYUO+zHQOYdv91H3 zn}6=c#KFR7n*f<_XBt8Eb*2%khd4HZzd6MuIETXR+7ZwNf!S4skCKRUJh8g~d0de3 z{gQd+R2}jTZy#{I_AP$-?V|Q(sGWa_)uwxv{t$EGJ6J(Z5>=Loopo#UN(JAXU5}JC zp#wC>q3L>bK2)tT!2f$T)*j`smh`c_2DF?0N=~1Kf>2jkALnsJR9Q_nI<{MWhg$)Z zhB8AoJXbOZh8||A+zz00TW9=ZcV*;5D=0z?%2sK|Kw+|3{*2{5ISuKZ7X_5i0-AC> zj(dU<<#1GE0uyC1RynN%(o($jFoLpzh>Dp^Nllh_x+Aa8-6-hsN{KyN$!tqmALuE# zN-hX$`JbmEe=6S&A=Q?V(_<*TkshU^YOwENv6U#2G(hP>gLWK65VH8j8j99QLcIf!P zkoV2#_2Ryo5hr#k0@eUkarJ=kpKdP3gn~qtsB$dLBzANmAY(OB6v=P`1{Nep(IP=6 zw(y&qu?YlILZn(FO2&YJuA7@>wLpYnLZ(u9#$iSI z2KO(&R5eCyto(8kEJscC!$d!&eZ8OS2oi;aqt6X=i35$(b-l|@-0F0=;X^}tySZ`- z(4z6RVdfdwJk&b0M7vhtPmDY`e4&(ZBBwK#H>qNbnCV&Y3F8bH<_st>g~vt3gr%v= zYGw|%^ddHtehT^MWkR(|$Y@#N#Dkk71oo<4l*}J)hC|5r6YQKSk5kG?0yF#CiBn}k)CGpywhUWO{%A~XfLFyH zeQQWvr9R0}lzu2G;)uOda(eW!|6|P(k)u@0#a3MFB|*N`qX?S*47QHNm7a4j!D=$Z zO<_ZI$TVO)oL!-m4mOv>X@fjKXgph=RGP^~<2UvoHuBdoXC5V%!XD$qB72F3xbf+C zUfX)l_)4xQ%h2&It08J#O(P#jhH6tno}hY)A+F$i@prC@#dTy4lKRCG4doEED3Jvm zS%yx+lfff`BouqpW^-4Ph?{s-Mt7zvlZp`|B=F%C+BzRAz1m!^FnUP}WCL$KWD?Sq zRgASfYA!+QCIps6il%^y?!Hr*(gg?+bNmZxIA~s5++Jz-Dn5iu~ZKb*@c$=EHe(C68&MM7pNMXD~ep4=@v-}87nuDs!SDG zFnRH%7QK+5hoh~INzI^9G&79R_?T-gcl}_QI>04k-TVLenLjMW))?BoYQixKAFP!0r~>2TUQV z5qsYUj^STv#N>8!Lh^&x%SI@J7fYWhRk&+HY|H2~BfQMm))#v_p<%vCQyK{HMwdx>B(Z0h2)9_Zxa*-G_>cHl<5Bd7T5Zr~EmZ)gp| z6m5GSFcyS<1P|y1J^b@TFyc&l>Lu6rzJuZCWPasQ`s7n~HcxfNQWlucG0f&}csDc= zovv5vnJ0!uH&oSC*Nb-a=hsAb77McTeKtgvc*ah5z4z~k4A7U0t3Qnas1+&~P;GP^K3&VvN9 z`!L$hGmie!C|wVaH!WH#ksFxn7S47X(B&Jo7bwL{06sQ6oL%X59u-I3vBY+tJ@(uh znzl=fwb<28h|_ys1m^RxT7L1~Ik^_NfL*}~iy+D+&{fxaUUa_a)yCVjj?a``rX(&X zBR2NoC6T>_yx`_iqVY}z@O=D1rJTg)l7xYP>|!RPoJC2MNdnetl3x($ zuv;J4<&qUAi|03FP{~+4KSMK51?LxwW=H?`U z^Nqs0HE>ZE0Qd~r1qgZka2+4JE#F8R@#FnT6q}1<;b=9D!~G%OLhrD%cD0{*vjcFm zcR`I*u_`L;=kA7yFudHC{tLKX|ke(#x{C2a-TS zM@f0K=hT+;GQr$;K%KWr}UX=J&rgORzHi3l05W5cL0DO8=5UGqC?B2K|=_cmEdu%c=hrJO0Od_;2j^AHw=?_6gR1 zmGy@8|DE-Qm7SISKRYTGJYBtzjX$sNq}DbiSyza;#^M1_)CG}7evD#@{o)5V5)k{F z@Fo^!r7#?^=0`5kAA$@$)t@Ura0L+Hd1Se-soh+&=a42{Tortq6bTZxo>DWBr|&+z zHmBaE+VxGcKQF9J!WyU{327{ZnGr3}fYZEjq5P*X0&7}b>;tQ94LYW~e!)K&c;Bsq z#9+7XX$GdyfK$LuWp-EioRt_RBg~=_>NwbSKp4Hy3s!4SrCR@af+(dpIb?1oJ_D|5DWBzkOti_sit69o!t1 zEu=55^q3YQ7lFj6tCeKg%lOTeW!jcMR2$Y1{c}VVN;>+UB2AwoNa4FefwM=zj8T)C z^&`8emp>B&#mZp&oBL~VK~W(bdP-dC@63eshPqtL6Q8*djOYq82tn;+vFbAgRp$yC8MNZB?g2rJH_k{M;qP0>J!->ca`9rG z=d|MSzVw)E&fKwk0|1Ut`?Lu2aW}3f@}xRJV^jX=7c356grFIP2jcc7)KRCu$sVgz zJ|?n2xg1yzt?6p&(jcP~0^ZMRLzFfsQ#m*wSh)O{n`s zk+l-}k}!{OT9LC~eVAp?!Mb3}NHYm;q-ijyi{NVbW-RMYDwI69l>)8;mhxK|E)P*I z1t+hl^g1D|K%`M-1ZiPKBXeLBHHZyyE&wcadC3K}4;!AXN5>|o=!NI&VNHEoz|ZAE z&Tc5H0a++m0~=;&v_pet5zpRB-7^kNDQUkNv{je4)}KLHav7PyUZ%T0x!8=~C-M8E z_N%H(&%~yd<22rX6p;EJr;aSIz%`hgFR|2$RQzHA9h&4F#5K|t9g|~e4mamlIgE)P z^2VZzo28Or zk0HphD2!I@}$Zf)Y3YP>HBur+Ehn<>3(-HqGBP7qyhG zDB_Ez)lNpp*;vbBqxZ2OPR)3>vfS&eg+dS}N=K&`-A@5sm4r`zoLlEdtgSBol*X6= z$jUqZb&$>V>sF6>PEiSa0>3y(=EP5N6$(*w1vx*&_6-(*BWo<#1{Y$4NGtq?>t~w0 zg%z@FsY61{99V~q<(<2muBNnRPJX#_bO@Wr>T2U^)1iSL8)daW5oeo@&Q;_q>vR54 z8YuTVUBYF$0%oIBmO4tIf|r-j50P!%++m*i6V`_ciZ9za@MXYp0ND18K$tTwU}iAR znZ;4%THcsLb~r);vVOcmi;?lsOAJktRP&3hlNFskfR!W_rAFb@M0@8fgYD;6{sw1Q zvn6gq^caS+ug#OkhE(bI8tm?dB(RF11(sWRR+JRr_E(!9odlYzkqV+;-sfj^NIweF z#^34Tu!SZm9+8(uzEIiNw}0TGYsVuf29_$z=YnGj@+Ya;d)mOBgU|o!!Hgi*7jfjC zkS{ltigAL_7>q(uo<()noabZ>x3D_HnsPMwQ@it^*3q zl^9q`vZ1(Lq$ddR%F*-_ld!`HzVN7=TOej@u17TJmQoN)KW4x<6$5d~SY5iabSXr= zhjc&qfGd*&DfH527vq2jQSvA-qO+-0vN zSu>QI^Hn7)P81iG)MZ%R4mAED`35eh~LQ1K8l9EVxtWAeg>$$UV; z%2{e67_pC4*vJ9~<0o{HMjR!padep12iZiW;#(paJ*il*!(bfHE{SE)1 zDzBOoE|+5@Fh&3pWmXtYBJMW1l2K2}gv&a<&*FaP&`H`H=90cnWp42sT(gua)5OBh zj3f-x$S8(>F4a3!s2PGgfE-rfHX5N{&WZqSSH*#uEXA4x+xiCyKSzBB2S$Czp+1pg z$0jd6HOO*NUlaJpN>uR)#=N!Z&0b9C+CU{233>?rRg@_yA=n>;T`7q|NDd`A_>>Bj zqIFvq3m8hV1gJYGC?=_rCnblfHD>aKxtRJ??jiy6O(V)luH|&w=G5c1q3dv-kXCOw z8#$-Fua24+&br60WEW?f0xBJx5fj*Sr^nxlnH>D*r&W^U?7u1n?8&uj(l}^hPeWBP z{(MjpSyMgDxhSRhVgG1P#Q!QrCuH!pd*@5ol$3wsq-z(F!HndZa+ zg;HW}e7`WZ7!=Tn&Q*naP-kNLH)(&;qjT92jO?7p<3lFvjA|?r$YQqQnSj1RhU7%0 zDUyj(Lf?R6e*GmvCBdH%CD zZUhC~xk+jK&-d)D&b{o|#$Dcw@Cmg5?cL&CcQBu?9vNt3;s}?1NjA$$X{Yc;JyY`8{j3@W;}K;xm(OB?Dg z5oNEpMWISgu~?3v7FF!biX)1YPNYNu2;`|UgUfsKlqgEMQqlJDwX0%SQJn4JO{cGC zH8NrJD&!MCp#u-qNo@&2e<*c|;s+9JF7pwhvdGLCt_pe5JhsR$?>46IWh_g6-CsUK zV2Ha*ZhmYpfQTSv*S_=L6mZM-moJCEB)E3dsMHLy>2yBPS%=(|XYP{W?P1(6qi^a0 zZL-E@!@$5-ctk6b=NK$%QqLoI50)_4dH^Cp~ zF`FSv09zBs^iUPpTKg=YQjns(w_v(K`PvS~>mH;jo{#6f`%h_hfp-T*QzHfSZs(P= zCPhz!>o=gFDL)^xSo{`q*iv**MNF1LmAXj5QF3REf;(VEC^(^^q&W z>opx(D34_|qOV7Vr#FH1yTI=^>b25ZzNo&`GVGFQ@-2?SrUWc?ch09Qr@RoKl)rQ# zuW?F#)~q2okfWgH%M>qZVmk?P=g**1<7ktCRBkF6$WvZ`YQPNv_{?xogS80oKdqWn z2T%`W046f9OaQL(E7id(&Hm&Qo%25%b(T#hWAvoMYdjz}!u}|rtI*))C*_Zdz!b_FA zJa=yo3F+eC^Jo4d(5z^}2X*;8Vl}WmX^~Y9Fvmi@X=)Flr+0#scX$PmIqq(j0#qc0 z5G1i{r5IDYct*{t;;raToLX*iPXH4V;<|A2(!_qt%tblgM#NSJGhJPafHyT3o#)wo z&t&(4RknlIO}K%t*o>8xl@6=zSHBzT*t(0Yo5gR4C*MNtaoX(hjhhiY*`WZYHiZTs!vTuI!u?vS)@ z`#oh3Rptp z=#L=Xeh>`lsXXHV!O}Sxy;gL7@OS=vXvuq+>yfDrEscV~PqZd5=!Rk`CN#jS4vYo` z58^57tk7tqDe=cs$7L65!VTW-z<6uN5cgL-Y)H>G0@d5(X+*6!g4jXCYa>@!5cu$M z5t}s>AO2=*?Aw4mei|#S;Tq!mT_F3s{+WJ=y<=n~{3bcFN@@>_1xA^ZMhYPxq#!c< z5c+)Vg*JF}san-*Q3tUg`PZ@XEo*I|9;?vWrKcb7u(vMf_TZq#w@{Y+E`4 z+I%{^bvHM~&ActHQ!cfqgb#twc`Z;&yQOZZSp>WUyisrZZBSu&VcB7P1|Ru+pWqEL z16Q+8aufW*9XZ0x{Ra1Sd}Kwq423_oe-e-?v_5ZidWNlwZE_-enFCtD$2*Jl+M`m{ zaU!MDpsPnFVM^8)E;!8gm6}nQTu<&JMhx!I3)Y#E1s^C>C|RjYLjsWYj6}xAO`JNH z{v<$q{C2pD6PCgB#!Mym(86GaKXi2h|n`nLRd%=9Ud=pMuY(}xhy3PcZ7>T<+ zV6B#>ti1N}WhW;!b1Gjd@$PEX08huqfwJ?-vgC^oYA`L$!x_<0v{KZ%(gF&Ao)&m^ zA4y(@N7>y88n@A91$@2;h*#fH@x&46cx!N|KzX8oG*2eO;x5W9tCOvNZ{v_SgDyv* z96;Jb8`r8WV3rS=G?!UJjyz$2%-5ID9|m@^dFq9xsDL1&d2v79&tr?~D$GRb?yS&> z4}yA%X!&Taso!i)W?UKo=}m&BK;aKB#ZpU@DwLw6=$jSns3ql@N2wIcF{Q}YfpV(s zxp^W&LNM>6)w1JCk`(yfC+tGpEGW9gpnn2Q4gkqU=)V*5lA_9|ChBB_WuLw`-FXYz zrYFi~06e+}0doyu3!m&F%@;ZF-8@no*|=N_vWD$vlPRL-!5CTB5j7%|p4lV1td->T z@5-ilUf_{TK}+$*&(YJt=@vX~r9iqXzps379;m3KZ_rD2itloSy$~0R3n~k$bo3k4 zs#nH(tck0i)(wSxsFcr{L%(Ip44F6vey~OZROc%(-fY-(7hlKD-;#C{iHepZZ_EY5 zMq>Lt|8QJm*-q2h|H-u)2Rp`O^9qufGy5zF!K_%V^dj4 zA+ZAo5-j{RF_l8NYkJgJ7yk`CKd*iis^Z+NM~}{Cd}BsQ{g1^zoU}>yv7Cap=S8?g zIn?g?LE=;PaP~^w!C}gCG)f*N4)!1Q?)k$~IXYhZRSB{#ydoBtzgr*bElv+7mcMXQ zz#{qHs8hhUc_8ld0p49mG3g2%t@u0@@=$Ysa*Y`oKg|uz-FQXuQBY*%Dlho>Q5}&- z=hG-CV@g7NO7ix&xuq2V{UoR>ZLn&S}L+$dMAg)@MUIr-Oef&nQHGo zu60%f^|@PVg-RX9*1SaRCQoUtaQnDD&0}7ly@9QgRc|yCj;OXM8D_SVGM;-z(^rFA+boeQ(gT80HDi}^ZUm)Bgq*gF2Gn&?tTajT#}FZn znXv-!G^`tFFx>LqILTvUqH|So*6}+h{M8#EUz-Bn6*%z0+}k%p4J?9!OxA8v7kqi` zrU-%>5${B;pp$4oVjW9JI&;&i5GCrsi*pNMuAwq4fGISSGU!2m(EyD@d?rdoLRzyDzV1Q4TOdNZK?N#q zZ3C;-(@|)2(Zu-PIgf{e5ZG$}fCf@d`wyvkoqtI!?ih#DDBBTjn3_8bn#wsHdxN9y zX7BZ=FV2el)$^MKUc2c5tk-)+=!YEbx}R$3m)!+gcN=g0>n)e&W3;xyYdX`^=52>n z%J!*`&QL9~Fjmjho*=Ry)o8hQePy7Sr3x?o?P9U95TsJeFK(K2`#tm5dQq2McpHzO z8Qw|PmT~Nvw1C+*f#j@eV$AB5;N~2a6E>{M&l4j#bWd2&4Upju#Tj$!_vhi*Q$uwJ zfTP+jXopTSL4494jAuLT8fyb0W%rfMh`R24y$!ir-jc^#!<0^tjn3W_gR)#ocKtyG z?6eRk51^CVC$_aGaq)CdmWd*q0P${Z?MY!M%sktM%Bk-aBhv5Un9!Pg+_5#oS;T|y zY&=|7H^#Pj!GeQ-_8YhBdP2lN zQ(yZrcwR5bY?}luXvg#DJ#)e9#PwoE4<6v`obywLQ6Xt#7V`v+Q7!@k;$IpQAgR)6 zVBB?JE%P{^wzRFE&!^2y^xpZhbl%-Knw5dlK_FcoRazhe)=|$w~4l6v*9Q zkEMJ@=XmQkBT!1Or6c9KT3=Y7Z&;qHRX)!aqACVv>XXmY7 zq2q?dx?0^Y{5RU(1|QJO@HD6*#SbC(Tuc-k>b-*xSF*R)k>kwuqgH@Wptdk^@h)T} ztsDFant9@KR`PQDE-Pi!`D%hz-O*u6A#*&NQ}_j5dB z5=XErJQX{S?vhC%_jDN^2dN}8KZl&)q18Q_Aep&x%O*tc0`*}vlQ^Z5&CZJFjP0A4 zbJn=PQzJ?L@VA@&8R48iP4uGx>i$CcBtUX~VWV_XVIsV>sxX{k4UVmHVY)wVKx6)~UQH8UH zyY8Rt`u_5ftlDX0m_f%nwW4|8%iMA;%w=dfycLAn#2l0qQ7NffX{%me8#UrZFx{;- zyO$$p#)!!g)aTc*$__S*^^hoVpdinwVq5u`F69Ag-5%<* zc-3P$b;*;m zZklOhcJ5}x0>3eQfF=Pbm0X#gir%;!yuY}EjDy&1Pa)Xzgn4V!>xX@93dR7pt2gJ} z`sKz3hh^ja`>fz5xmjG?9V^wnTUR`J@EPkyPQQ3M}1iHpvr$gvrK&TFA5VwgW_D3}v^ zQuJ^^&MDIBZVg)VGzggO`{*B?XYneaeC_1pZh%dZk}+(Ldcm?)X_J7iRofhMXO>bB z8t*@TSx1#@JTId$!#!xPwpVz*pOHr>cZ6S_yXNmWKDV8|xZEci>*2Y(TQq!YsO2d} zMaX$VuxZpr^vszyr)b3rtx{F%+{L|V3YRhUm=CFPWGB{Q@|Q=cTEPbB^tXLp95?5! z4^h?(q%M1gl-KuND1LHaMW;cLD}S8Vh}D@P6&D zGq&t=I$lHX1MtMrG!9-#Fn&XwU>4(+Q5yLPjzy#I@3JHTOA0n_8R)AmQY%wWTJRZu zg0S5~9RQ5hY>=|S&6CG7;3lT|NypcZ##*2iDRmjhFUO z5cVm8M0r07yJ;B4uN`;qbMH>Eo~&|;B?3s+;6Q5d2lsOe`@FAUMwNO$jn41keWy)F zg4QrrUEjN1dqLl&hpNwUPwgi^WNZI$P(cQd!I;r}I%@M2!0~M3&v$!i>2V*_KT}=1 z0+X-o#c>NI))oVKz{Pd>XKC=&OuyN8%RXSi3YAXFJ}{%=bNP1@3%Oh`lUUm*tVtbY z0PXe?7tysq^dN zB$i<|6RVfDwbtLgA(4?kKv2%N{-(L~O2?$`tmxTq{w)=yP9Q!PI*%#{ckpk6QL*d07ugKj^|rD}VwoV`R1luzm zJc+tXFjbO`C@v%EXm>C8wnUWq>#G&an5kE8=B)g>afrlX7{d0hMx;~t{(V*Ml))Jbm|%3Xo&-CV;Iu~gaC{9(!(P@L zU_+H$et1-~CR_3VtK7mT6!@^jeQA7PCM+Fco)!0Dh*#P}j+u9}BU7tdWV*9no6|zm zAy&b1`EagBb`-f%12f6S?520ZN`{2%L!j1efI`zAsm|wYJRfA=KB*($n%jowrsu_c zuIy#l(P034uBzd=8zU_uUXsgvt4y@P>Rub{646CRV$T#>Xp`R28H+~184aBBipTb4 z>?FpX%GgR1)jF`{yNW*Hdg{JFwiJ=F%_kc+9~(6N6g%udAP%q=|$nN*MqxHcjZg7-pX8K6C9>L=d+( z+wX1lv?lvUY+WD619_sjn%>yHBtB0Q!jUH+Y&z`qN287nbdOcp;al!JnlCzy`U9>b zC~uv2nuf`Ib06u;RcG5bTLI)kKG4#m0V(e)&5owo*$caxJ|+>V^>RI;(*zBeDEV)r zwkXp}W=x#XuldUR7vvT}94sE7!Zk3~y-U-O+z5*{qM|ca^Pu%H`5Md+JdjL|GqCLD z2$8toIPB1;0bb>IZ#XJ)5EEXA7<M^{V4)wv&+aoKyO}7@y`B)(6?^6^wE4La=OP*pT^< z^^hZJhs-Q`Ny&@c5@xwK;kOPmjjbiK<=mBdK z;ScO~8x(WdgE=ltOe9WK!!`xfNW)iet ze#S8~Gcz;unAsjPGcz;W<1sTcjZU~@$NUFmH^)lnqVpFkM^4~UlYGuSR9t+w^PnK zQI|?52Th0*suM|T-t1CtwdGFsCdE!(b+G`rpJ2Xr~ z{f)h)ZH91pgIo36sh16gvgr+Xz`J@l?<0Gy73@QB-(m_SEL~*F)+D5bt!VHq)a*6v zSO^`%y1$Wt@!FBYG4KRK)lek%g&S2LPK8Q|Pk zYcRJUo|rp4GDmf>d^TB31$-_TI3A- z$&hzr!cdvN-!Y{{DS!TorrV?901xmD#Ajj}*>7)TW8zc%0s4Ku9vcemkGTViF}S!; zzKklNSVIJCK!3KOyvrjmkPy~_+=EY7ij&Morv`0NR^b{ZOP(jBU_8EAoC4<5-E;FETZE?p;~!RHVpNCYI7IBKTZU#bRFGV z2Hz7TK2C&{vk{r=Ok1Ld3y7pwB)MNx!_^uH?JQZ8Nwe+b-uU)VBh>~QtUsMuaQ5+Z z4D9@77jx7l-tnV~4kO0>#Qn~2!%8m34A01)uS!y&u+ZrH18b$$+K_ZjOe$iNs4o8j z`8x^3Sl}FxLcZKtug;rgDSEz-+<<3lKMAhTEQLKU{nlm$2fm+o_nGEaDB5TL7s4f& zUE$Tp${zl{pH+_~H-QaP`rRSesV|Nq5Fu!TxDgyx@vIo_w?D_f_M^a|n{L>sw5K1Y zE9}yVI`k%On&A>Hx=b7WsOyx0y<&XC$`i87sIl`~!H`T&48;G4DW4#j@e&pn`<{KD zbtuB_#rp1I-F!oMZw|1R>o016rDU&>&diU^&`ZmN z=H`b|SV5An<$?1Z$~u~Oa!7fj#uwu!mk=h@T^(xY$Wa7s(8rjHV75vHuh8en63am; zDnCY14Ye$3$VQqT%07p`Wp9uh=}^Uld2`FFPo$9yQs**x74!KN|6;PzkIr(NwbJ?u z+j>BCAZ4A84^<|3AKWLLt3y85BbR0|*JA06eUEkljqE1zG63#pkg_tuk;_`e-`C+F zM69zy)sY3PByv7B=4r2Z*PCU#d;*?OIx;B`9~%=>33V|+V$JG_EKmP-i~G^H>Y!q5 z$BdFs&Cs94DIVenjKK`J+dt%ZC0=!8{7L8J6cr-fK}+?_V3=XXI-~*(Wu=Kr72HL= zHf>gCxyH%)Lzf?$9Y}kF{^En1`=@#&jBaNQaw{nw%Q{VFB)fC=U5XtwF7OJa&Y#xbX1ULX{Kty{s&iYGGbcX*=)=eVab%p{cJ8mtGrhAlhN<3V3J=p}aTvL#~lcl_dx zJ~-CAspC;eDy#+3KzTj51p&Tv7b{W$qsE;j8e=f{2nQ@8HXKxR&=rewH}Y#yXlqJ2 zT7X9G|&YyJef;ynWlHCDw=z zH*5lAiPTbZHswh2v-377fk>lp#@rSvdc+L;7O{r_T#NG;B39T4k|Q%G42Emk+12x& z%;(;1oQ_be|2Veb(A8>3?dg&j0N|S==mguXo_lI782hrZX@m5(0WcZe)qNX5VSLFyiQ?p5WUV$`wx zdU`?36>1R%=S@bpW`;qT7FPhrk4xqVpPFCz!jI$?%PD5yFE^m!ER*Hz*z?9^;lCu-%KR^KR$Yl7nwW?K(jpBCI3#eVxFJq@%Jx zHP;K_nqXg}M*}QY5lX&E^VpW4E`9M*E1|P{EBqRgtU}_=G z%cGxoX)*NM62bp1_ULlxO13i^4vJ`0Dq2G`$`(32Mx!nfbn!3AJ?DaMrTV&1M4iY` z=KYY@=-S8MVV!=ajvbB-fn+QI@7kU9BhX|h?g>&I&8e+QYNu4~+Yc@fG;)?{yHY%O z_QhemqiB`!qo}<_JV!s=z9Eit3e<$rZSHsLiI{q;eJxifitTTb@#(6W4Mj*9@)e_i zObzetLd|hwOe>gHveUFOgb&B>Cl?#3TXEc+Hd8|HTUG-n5Zxxf*5@;Kg4&!FY$PQs zY7m>@C}FRp&BGbADWbvHXOzoDIariXK8VgxyTEq1-RgEC!%g~yI{_&Fvq=6TTjVy4 zELDVoJ5=n>hr5R)*_R-W^A{;K%P2VkC9fXyAy=;TA$~%|F;-9iT_@8FYjRQ z@ZSh>9RDLh?pyiH#m+*^!_EA^tG53h@;?*X|DRgt|6c$6KjPy!n7RH}V*6zIxK(E4 z@T(ULL3^aC0Ya*k6l7GmsDT0^+BG6%R57v?a@d`oZorn;hWaV!{Xhne5F!8mTK^^U zz3itQ;hKr&cq>sG&-&2Y`VSY_H2;xmHoYyz6Y1_X8#XhqaREVl*%rd^yB2M;m#62; zrg5aZhLa)H=i~!}BgXL$M;1k)rr~Gb4#hcuBSOFNAKOk&J?>%NysQ&h7lc-G0iw3& zHUq*bmIH&HVH4P)6JUu9pZg9chh2Ks4r;xca`l9Dw}FMx@u!bQiX2bQTHXY{m>oO= z>`E;e-@z#X{l365-4aI2zBf$mNTv>58pm_k4dVM~y2f?*8R7fTGuE~G%2f|v{i3gi zJEZ3nFK0c2Ce&H#C&fDbhYjeQfzsUUR`2kvLGsC!qrPmwOy<2uX4ZQn<%c-Iu|e}e%x#9XE0&ge)sqIX|A+)<*gZm?W8+hy=nnyW@(rSF@#Cu<;*-ygpzjYP zXFq0m@1HVd1CY3+pCp$~uAJPz?;D9iuVnX6+DV^;7B}}#%S&aSX}t7Sw9K68XF_|H zt8e_n%JVx*I0N?&V$W}1%rR*7_jh>b^59kfxjOzg$#ws;TKJEK0nYE@_`jfa-(|u1 z|LubP|2Xcy&G-MC@&D26@E<^Voc{ro$N8T?d0hWXP#!BA&;K*xccJ4u;&-8uyK9N( zZoFeMW1KB1gU3k9bDxDI2_!)hkpyOk1Rj|gT!xZ7=sP`$64VzBHmL&f^)W0P_o&p3jwDHZa=gIziW|Hv$duiK?j;k$3L9yTF z@yH+Bi*_3JJoCtFioaWPkd@~)7uVaI7i|VZtCz#=#_c70^iO024&w~{t$i%EF!Ffp zUtsTqhB-Ob+H^9RMmqN#LUn(CBy8kNXpqx#@ZpH6ORzAxa(FcPJ#5p}q$?sDpUS&F zeQ#^ChM^;KQpeB)>>@Fq?EF*W5pYG9#h}68sRG8+=l^+yFZ`DQf@SQ#91!>pQ|{b) z_HkUpX=g|fVI4FD6)%u;rBmrzn!j{W8FSPyX#nX07WM~_cZW=xniPp&-2n|yzmw)| zu#oYjr>|g$9;t5N!*xS-{qqc0M`Bs9gUS}A&-+YYm-|!|+wdD*ZT$mVki)VpUue=QR_=QR$(u@z*=1til@ah}+e+MdJn zrI;qTnUK*eFW>&{{yU+PRw|uTrI+vZ(f&HUQF=14YI;s~VqKxuuJQ(5yI4eRm1?_= zY@@qW=RR?IT$zGZtJ!61^^w@NnmM=-HBcD_cR1Ca?|hlPf&rB2rx+BUE5q1VM3Dan z=Z(*A0o0b$k_gc+Td0GNCnbToStZY6x^|l=SXFZIb4pM;vjWgCRw676CW!*$2hvK3 ze;Fu>6dV>KV8+{j9i=0lA| zZ&fq9;!8Oo*-d)o^a&U9ixMU2<|)tI8?LHi_ON%-X>6PURz8$hs)_1M(byrsqWoWPWNs;m97=>d;p9=e?iCWpA95G(E!x|9{o6P`5 z^7)IXD;q(<#cm#Q#{$3M@k75zZE~(iPGhv;;oP+=*~JZ9S?3$~7UEKcPWy>l)5p`8 z7-`CG1xp7B=uZIb!NkMCjaBw>GX@$t94Zt|b{bWZ(U((#?T}o3fLl=_)`bb7 z;x(zR^a_cdQQCAkwIy#dDZ_7M1V_IksJ`M=3|x6r5)7u4!n&#J(XT zatr82I}Y01?7Zo6b@Q^phl)j4XUsJ%-K(*v1Dd>D(jQTfjV9Gb@sZkjw4y9wkU#dP z1|QJL=Vr;+I;XL%;RZ!7L{*|WM_rQsiB(jihRo{fRfWT}!W)xhND*R|v9huxxrKGf z?NX*q`||1tRd`xeYA#zqGZ?fh+VdB4N;&S!CjB%wa$>wHsu$jzCQ0rDi_pomKlU5` zsk}LUfi)5nn%Z;;S7Vp0_*Xes_pDv!a!et$|2F(|mlWM@VOZ4h?$m^Iy~AB>4~V}S z#>K@h_zwigtxv$mi7|$ohoNMpL`^1UmYPZH;pYqA3SVzUf=N%c7imexs`Z`CHh2k*bM}+_rfs6$<#rGV%+NrJB+_Vq^?9ItQYFdw1eO-43}LSdEZ*E-akq6 zltZ=g9Whl~}A4h|P;oF9%5prW=Muex*IQ_hAg+X6?1rrF#eDU9ra2Y*tt6 zxCRN{FJXMkR=-fepgevmeQA6|z@RmUcT7;e5Ebqn8kG6^rsep#yE$95`WPV6sx^oh z9?FOsS{GRgnU=+c8ak4AAOsm97&~;97+M5+)|mcOF~xXBTAK|gGzi|4(#hMK5?MHh zZ?cV+l!`PN-o1RFmF~+tC0MK*nbeGtNarN4J`un|z$>N@JXD zn^W3++nqYKNw+Oi2FsU^kL1-YCYDczyyQpGinGj^=O3p%^g=)DrCMUj`_wKbdlGYC z?9m3}Vhg8)=CD+c)j`2>5K(*GWWnuw)dy>Ah9H>aF?X9AP|Qio(NE3sIn$4zvYBh&YBuvB z$GoiL+x!{2udWK9BsUlJrTl3T%#4+!HleT?}+$P~3Y*R#!jL0m|XN7^% zOsAr*Oc;(Dj<|FIFHZ+8Kf^=7=n-hgFt2G1k z-Uy?+T2{gU>#vD;e~fde_S&y>4$zmE-Qa)NAz?fc4rk;*JeH^bL}}>Sm)I4Z=qIxJ5gWWHJ8R*A>lZ$P-QenccUr zpxo+`&35oDTc1dGwYqg931VvAlY_$qVf-_mv@J|Jz5^!;c6T;*mFoWSIp1Wz5u!9l zC=dFomOdvoz%$wG)Ho&l~fSUgn}3>EesJL?6}SxTHU2 zBmYx&oiKz0m(4Mn><=0$zc4@Zc~Ci&j6jc7NKt;!dfCTl{FW+Cr& z<7)G5;tC4Telm2y2iV#`x{~R*e=(T)+;iymd1>82`uYeZZ8znu{>j@SrJEi1%h5_t z3KCC-4*bZuE_Q)sF^tS0ohJ!}7s?C_I-n0+o;%3tACRVVg*3xtqB*!DnRV`#bqD`HFZ@{Acrb$A;QEjPvFmY zQi(08dz6Gp1KtLqT@xLi){NO){z@IC`HrqRS?m^%aD_lJssDlFkT-H$;E9`TX=#x# z1d?f@fNC_VT4*Hzo`m*FIfG7(FeesCyBOtG zqqU`u8y9XBHb*~a($cj$5~tOwJbd|`gC%thCFlVqogk9svXIB9&b-Fd9A%}+bg z-W?T7Dl$h>usF$WWbt--usc%X;@9*lS7ziOPY7HUKW@v3xrW0X=zEhVVCMC86JM7%KYi2Hy*g%}PZ$OG(8ZO1LxH2rd;+vCpcEKzO1 z(}O`LG{Nk~#fIbC;B~CuR;O~im3`el+O+>mSc4e?|MhV~*=5R;95&7u68DlWE4jXG zJy3uWsC6~-2HtAvcCcE{vUmH_M2oRZZ<92;Lp&B@+Z99s0tBiEMwr4J5h|Yvqz2Ry zT38-r6r7m@L{K~Wml3N~a56VeAbacVwcMrEYj=Gg<*H#)OUYqho^JlKVbzkIFxmO+ z7!wp59#f@UmSK!d)-OD{MtOY8EOeV%$&A*Y*{NoYr$DX6??qg`yg$Y}3BgXo-z~a@ zuu!ya6x@jR&3hJ=pW*O4#mwv$xv|{wSxFq@XEut3RKiY6avkfY>qDcRTKC6A$1L1p zxI3SB6RQWhqR-n!_>5juWvh5Daf*ZKIa&-pNwa7(cd13R`jGWrwS<6o_)NOA$sAsf zG4b~4UVci*4@7Hj>LWv1C`3ed{Op}=cBnFL?%rNvC`oSa@m_nxF7SZoyk5Y2z)Ek= z%e5>IGWRz1C(Nq3P{boc;V_R<=;T3?pg@u!+CSbKqRb9?aBD zwAvUjI%JQ22HRpT3mBhMpDjPHAvUb^SM8UNj(H*UD5&6|#2<8M`K-b*LwhZ2)+cm* zC3o2sq+)AyANRd?u0tR|Mo)0b((O-CbDCcDGJ_g-({KY48qqjrfQ3h^X5LgzPRY$t zo3FkB`=J}EB&W8MZIgT)WsWqGppfOe;}DY2L%+Sy{Y$|DQkXx6|B97R_H93m5d7;7 z^I*^J0gfAC*>y}qY)GGhV(;IYA42oR+gR^moUQ{%5v*;rPyN6Ch`jeiKN~G0QGGCP zNB9eozZ1WTT!SA)kWVn>U*KXv!<9Xos+rFwz~XZFgH-7*Z(!>x7)mpE|*wnv1cF2 zF}%Khy~w?;stJkRhV~P>A51IWNjSO13qdLGUCk^O66N!Kahh{}@+@#y#jDk!7YZ4va1yL^-@${qqhz2lqI`Y$}`Azj<2@ zDC{e%u&I4!YQ5?-A?i!nw=*UsocZa>3k>7f>`s`VRI@$@#T$j@?@4JqIB0ZBklGjo zo}O8VpPtsmUgm8IC!sme6IM637 zc7ZoYmo3vu-zr=aKk{o#{Zcmyloo8Md zU3O|%Un{LlM1P0`z{P0QqC#eo$(~KOWy5DSJVdY9{^hpS((Y`c89LLd`seC^8{YyB$tgArp{_(8oDuC%R31Vy36a2isQS|K@=RmKvi0u#}794yAT zO?^y`phIb8f{)->N>ZglGVZeHl12H3MyA@rTGVQD{|2T>@r`YPN?}<1h`r(;)&x(8&~ZC@jv*q^mAXMvx&~sc$#D{4zTW zd3FV!T8*ay_Vqe9@qsVJHfMV z)Llp2a8R3dsXtwBGgX@k89=c4_~tf!s`iIFz0*L{{4WC2?uM5MKdW6n?-CtN1o_`o+BN|dMhRPK}iaE!E79Xb>aAX7m_B|Xf9MB zC1H#kJsyUOX0Zw&tc@Wj`)?zTuuTAG1W-|-StpUyaj=qsH7U;>dLGZ+eoM-$0r)lS zA0{Zv=u16pS}$O(*fX}=TlhEYJ=b>AR{U(qsuAVm6kFDLCUPA$@Zp&C$2Q!lc`j_v z6xg*KhFzHLsQl=*^y8pYu<1`T5P!75H1KOvkZHb70g!f@N6gdsMdfoE&^pzK zdpVF#shgP)0{9*>n)h4~Tg0BsWN;m15Di!%f!N;23&lPS3AQKMKRJXGJHO6$#WVIY zWQNIxp(MbV<#LS8gdmX-kd1wM%ycA1U-lm>Kj)pb% zEwxr@3%;H1Ft}+2D;4tH$cNNm^JN`9qa97o;@nK!WgSi@_$OW5@Pw&d9pIdHsrEYP zS4pSmfZ>fhkM(}aa-xNab&@w_3$D-WY1YEiPQ?Z$Vj`ZAtpVZG#lvadY)0{`k_LVB z#$5RYZ?4jYT(}ffgax^B9d<@;F^;)FIK+0tX1G^ND;jZb(V#zqNoor?m6y1)r?OI3 z)&5ums^0QJdBwDL#BTSCKZA}M>wGxni+xwG_$B$|=jBV2?QcbXmQ)Ni-zpL@B>Ar* zFbQ7wMj(8BJo?p@Z1mdV2~>L*`J^|^v}c@ENYbtW?xveHqnbj{7z!Dk3X=AGF^Te< znz=ps9A>P7!6JV6@*m#ea7Z}to6mC%M$;ql#?;arPA~I-D!|4|Wr+T&9o_p54AmA* z8Yf<{D`~=w=Bt`#oE>woM46Zu-By0>D+0uIeM~Ex->f980V2lilk}4xMHh2~{N9&U zJ=o~T5jYTt#w$F}U$XRRus@Oj!CIh8n?4`MbUvn=%Yp8vBJ*giC7e7%rHs^S#sm zsB(c7Cw7pX3VFkX{BN%=iUMt}!}1uBeM%2(?VCPdg}q6C)eryi`M-ap1vDOoXMN<7 z^`L*Q#i0T^y=@1FZh+|XkhfZYs4=7SO74f?y@zJ-OgfBD>CK2SoE4EC`KfT>r&h?! z+*8E@h*Q)R*%dlH-Yc{0k5A6F{19AB8wj-vKp8{LPYjBw@^PI@*k%=ri)IaoLuo5e z4j%b%i83TcBcYN0n27Jxtrd)E9p}lG>d+Sx7eBnX;17u}*ow0sGSZLFzQT&)az;&{ zdK&4oQ#$(MT#l^wT4R@o_b*HvAowjzYa<{mD5)YGEHF3X;xFV^gzs2$t09Oj1dd2j z-(E(Jz*1I(vn@DglIErVUiVhWO35gfRFTT?vzZi;@}N<{FTll)b>~xIP)LyUN{nTd zE_3~@K%1QYnIf{ECagD) zPH#u#9G26o@QUn$w^J;F?j_MkP7BpAO~Zq~EJkiG36nim_DTP;>i~dwG3q%BC)22S z4mN*quyZ-ICl*UJn>KB!6v|O^h`EiHVZaq$@D=Y zgE9;>I=&p-Vpm!MfHE5-zZ;tR9Ju%z@RZz4!^z zYXpeyZsWZ)5tF0>QmIUG_=Or&;i_G|1@ka>FN#@nKa4ZURF8Pk5U;73X43m@y%*1? zrxXS$U`tt4zU1#-opGD4+Ra_R7?2kmH`(roh&J`V4rQ=;1drL{+FG})WcUYFoF5BP z0zOTDz2<>Y95Uqb`bK+iNh(Cz@g{8;FCLomb>QYrotXj~J2s6+M<3n>4;Q^-wstH^ zU&lT;KGnbMzfitlzG%Lzx|PEf#q;5De{NfS;RL9c{4io&H3~f~Rk3XqJ`)n;yiXPa z==(>p*|Hy(-6f61ac=>@ijoJS(_GW9D}!?36_`xF*pGtp3a>f7|I&*z;kn z+KTIx%uB|6;tex&(rz2ZIVmYDO|lJHaDqNGwF55rW*=r1c?(2wPr`?5{a*JU>ddq5 zF}|U~SnWHaC+WbJ^bntIPOTkT0}v`YpA(jfp9@c!{QCjmH}NEY^SB$ev^dKchHd5x z?76hh-8l(?F7hmVS!TfP*t%t;!=76F3@&MN-xGzDKpwC^g z;l^tdNl0@zoZC!bB?Hcj13Q>Djj~0Z8^6uhIO92m8G;N%FsTkm))U$;{N{7Pkqg!W zD}-3DrRl;{RJn}LIkLDB!UD3C(Qf=goViC5;Q;45sBm?NMLD7v{(rk zJ(Dn9=8Qr)MJy+h=8OWHO6KL}%8xoaQc=y&6tr2?+z@+JY}4e?>Rsn5$0aY8L9A8f z^#$Mh{Jvff+mW``9^bRb-UvP4{{nN1Br`bCr<*sP@285p<4`JsK2A?Vi!ai2q3rQL zN-mq?(EB$1Ooz!@SH8UOWC17T0?*gW5spxAsH3i2(tk8EVg8=E5(4Jjza*&2LWLF0 zn15wvp{r+R{*$3%eZJFG|5UkykpRi&eRD+29uF$OmpbeLNg1ht zj8=+L$uTe*?W-7Hhd31+8EbHgCe{&tC%&x2e54k41ClcW0f`O`q`i>FAJHgO>BSw<+ z?(6p^JoTjB?hV_dL9+0CCPOOoe8xeVy1s@+;-l6UgT3J5&)y~h=CF$*AeogzKoBZn zArm$uB9&R0{9Yav^QsKQ*)d9wNaBaTiQg94FGx;1bHceMM*?qPmqD5Qdpk#xA)gj4 zSx5B(isl!XOqb~eF`zM8C_QzQGVw3A?zEDbZwsP-tp*NjrTYcCWXiXOs-;A4 zlvU}aKBLCu$P#b*aWbUkSqL{Ll0w0QaSY90W@P96+lM0I!CVfkUzFq;<>aZ#zzVJ) zaSYvNBJt#ca0n;H2__@q#t+^gS;h{|M1B@EgzYWa3f*>vRtPg>hVUb?MCj$$E`{mM zK<*$69=75LO`DVGmgevdR_`qw6P=Eq*@Y~#)M~3bXc)YA{BHa^(rZ}Ev(LDvIPojQ zEzOG2z>6?LG$At|QG9s$HGGvskH~~h|3)B^UZKB){!B|5KIc*J?)=3AM>~=SZ|I7& zo&k?eUrU2kG&{Iw^|;qLg$c;ca(pEy5CD_qbY2Y&~*6nDzj+TZ0LtpS8 z8~85aTZp<~8Zgn|&)Y9Zpd+)x??b5Ll6dN{RAVy(@#Mnz}XR>EpF(#i$p1#!gt|$rhmEh1cD>-Z{j=FOi64F>0if>$k;hOWXtl95{3eWu!5%+S{EqDQ77r#Kn>t4b71Yg?H~_UDiJ zou=X4HiFeZf$J!&?TulH0G1XDS5v0*FS7SG(Zyc|ekSo8nCsMfRaQhBwdbv7v#^hP zj|BHK;QO#n75$k>HA`&jl+@Rsmhhg8RCHsu?qghFIGyToU4@G1`FXkI8WJbU&H<;5(Q8vvW&+Yp-RPU-Ic;)+%?K_UNdf3)@eRC9eO7b;G5)t;2T;}bCdusN=wW{wd+A&?ZbY>g6 zmz@v9hUcZq+C%emB4XPH-$i34ctCgw_iKa9W;!w9HlP99?MmuG$%{2)-Qf$pCV@Cc zs8)QeGD(}CdkQ;C_7JD=i&BNPSn$!CNl$ac3}IWNiJ+_YMu%TfAgCiYD3mg&l+7}d zBBCBpoGX<@|KgqNjHPxuk~Y^m4C|3-@n@@EX3Pm1VwkOcNOX0pYOp!-Plp8bi3z#_ zO1fh|7xZ+`!}y+L42l75BmNJ|S{Lrg1`-W38o!NdLNxI)fVf=mY-r1hm9S`!DVi5#a!z$$!2wCheI^pug8Sq z{2@E*#9tHcO{_+h=WpYJu}Xbi8bM3z05XcQAu1}~Ka&=psBM(H)=9Roh&PNGX3mta zmg&Rk8xS4_My;Q#8&qX=l}fU$eyR%5;!bkUNfCLNjt)n!ouXCm$!WL*`;9Ml3V0u_ z%|^Fp@Ps%DxC;CT@nFs-pN)CMchOFbDe_=Vn@Mu@de1r`h{Z6aY}L{g*vQR`Gk1(% z(5m?<2^&>vu?D>8{OibRY7wD{x(ybcfB+#{kiDr1?Iz_wUvRa=#wl?8P_01^rUKC~ z&a4D+L)uf#nlS!`zvuK-w~JbuI$R5z^sB+`U0YH_QxHAyMl2`{Ie`rA7_a^xK+t!G zMb+)5Aq1{FV5w^jk+8#e?eX(ZSD}*ISOLX#l8=MmA+@XEhwMvzge>>{@ukjvIERGZ z7lIF<)B=*H<9_^;h&NlPu-<}~_Swh|;h*S4s|36b`;Td@AT##p7&glp<26DudN|5X zME*be_zEXK6J)0vWfJ1|dgVNLIlp`Tv`S4sv&)BXmop?KiA)e51<3@;Qy`w9OQo-R z8U)S-EPN2_&5F{f-QQtbq9WOh`V!v;?C58#1Nrenj$|8|)!+~rEE^mp;K~{uW#B@z zxeLQKw2$XRV=xhI3Jl=nf`9N9X(KlVEAD0izbC*PP-?^=xSFr%m_ zK=fpn2UniQ$yJE;_NuX=^WHuxY8(@Mpn7kjV{Okc%Q)7y>O92yHA zDenXx+7BgfQExeKY(|ku*_?vb5p)&NrayP@Y|qG-7<2%ZRyRt$mgGjv1M~tpYFbqM z*x=1{F$Z*S6A6GOY%zMctWiGtZb{(EJmbHX4UV0~-<8Ir=x|!GpGpXFQ`*T@Oc_EH zqjFo$D?_<90e4#}VgX0uIRGSqr=d@qr_0MVr~X_$wk9|Iw#G)bnO^@Dx2yXM#fNTY zh@}fJHX^UX6QBb;e%pbhP?wdEPTN)ZSy%JdXw`w6I|aRZZ@m< zQFFfBI8#sYHs-4_r^oa}x>j@6#Hc6^4N(~Eu`W>ECN)=FJD;B(LS z@SJQ<`8(XP8OA@D76zw5XuhiM7LqHxWHQLRrTFOloUYsdqFbo_vY*ePyM{M8jk@c@ z0RGE)f7xaBkgnNIe%*OX5Zm)i?00|tRxhNaDAAU2nue$8hfNWY@7D=gbDFe+%+>7H zBk`7iunCFB?;Bg7q{X6_%enS9eJRs8I);OHkHw^k5~@<$C7%S!+Bu&es`N0B4C;r= zwiF`%WY7lBbXu?rPvEkJvmrNBdQ!!*6e(1pwP*23wq26(q03c7)sZlwyi~e&fh|ey zm>90*<7kA)tk9%PK-wqC6_q89w-^r+?_2K?Szh;4>fC+%EWs4Nc=DE*wP2syLqeD_ z;S_FXHP;dV$T=v5iS0z;h6_kTZeWsXI*v$wMJUs-c^?g|2aK+-xavV1(0jUL{M00< zO3F<_?5DK!wfU(*Q0~LY7~^*vr|A7@qW?*{;e+^XM&uNh{ptNg6=rlIRH9M*RBSLw z^U`{6t#+zwCloU$K;mxJs@pMhX68ARXLCPb!fBG?l!CIsRg_@KlN7X2Xjx~e1&AsW zDt1TqdF5^P-FbuP+{x(z@gMr$XsV0gKJM)n2fHa#s4OKrAt`mHs&f(!sHB_=Rasu!0HQHQda@_e z6pI-%cDIGEOZ%@AD>s@+t!v}xSwR%$ljGQp=D?vNmjM$&%RevYxT@d4Rb$KLo#=m& z*5uy=CVm6*0Cg*jY_xBl&mDx9-JK(D1@&i%h%&s4M-XVbrJ36!mxI~B?bOKM_nD_w zKrvKgTX#BBc}_Yyz%kv8H;rv9w#o`YJA+=!+|)+-bX(mdWvit)}>;pH{K zHdU?1PZDQBz{bC!F+k0YxZVD+=Csu3rPRO{DSPE=(n~}jfFk;7^Q!wgzxE&J*A;pB zyIy(TQue#rsl#VmS+ZSBlQq=B5Dg187i@RIoLAU zJu=gTVPa#u*#fWbxitJL=CE;?c50LGi~Ph5vGg1`W+7z#9OX`I?EPiRYnAKZKYNRJ zhOs!;WWDy(a@2~_l^J*UWu>^>J?Y=`cl^f1V*VxgjMiiQqoZs!Yi}bhDQhn2F0V^s ztG^4_`gD-{=y3Z*Sx0{@tOUJD&g^Q2xg(THO~^0Vw{I-h(f0{&TNVTfS8D&xEid3(%pYtYU+s$X0h1dXdM&o)pAQ(=`lzBsNj zhdZ!RS=vZ03Dl53lM=oI=cu8wU}vhd0?(-Rlaa@hu6@+w$QkS?`-sToOlS=^sIp>f zUA9!2!*s4-Sx8Gs#@nhqSzeu4a#GW&nqFOBqHl1esy+XHv`HoBkU^51GD(Y(>gwUJ zu}DdOc`5Ds#Qx;w>XBl~kQ=Q|S=~!{n)UW4v_y}Af{vH7nWm+0wJA52!Dh9|ers_Z zSl(PxQ`J}_NsUQqA~y+eI6FyUGot{?&-?YZB>PKX*Py4dJgxJ;ReKBY$PA$zYj zx!*rFJ`mRx$!coUH~sw0`PAt405hRiBP!OmTgjlqtG!BHS5i|3z)5I-TXiZ$HPIux`m0X7$R0+^gcxI0{qCKJuwth61?WaiX`nzT-IQ9KN-DFn&uu-sQ$X2z#6H6ov22Hp#2IZB^k^B%9vN^5##Wo z`gKxt60FIz`80)VEYuKld|M3$%aHP_r0}dLzyv4K9fp3volwF!Q=AlNUO;1vxQ0&l zc;L`b0IyL-PzO$jZtXM;h@)6GLK^q4%vbHXPDGz*qf&i6Y|PTNi((6N7a9BJRjRjw z8LE4{v;MLJ^@6HP;t|n-&J#7Je6dA1BvDdKi`HV<>VHV$jeS3xIX|0kAJ`YN7-{;Q zCi=>DJ2<|Y-TCS$f>he#Qb))xii^78jW#r|+K)+y_3`vN_35pOTGdx8*B^NYG-dx5 zHk8Um3a;v44O(+6##Pk~GiY{))tbTr!M~ZY4f%bG)%$K=uSS()?1H1Ror(kz2nB05Yw4vit_~x*B|GPkrfljQdoW!z!@VINf2T#|E;kFG%*$2OR=ZNAJ}R$c zhC1{IXjHaWqg52%H)?H2@}1P$YFMmGuVIYH_+Az3&d4v-&^f^B?KxFa`vS!eRD%}f z4coFNNyUea+cL_rP7_9Ut3O`pR(gmHR# zC3Dqwf%C@Ih-#z!qbC2BwBS}TM~25p1@5*sgPzSoIfKUu^Z(%OEx@As*0yo!ZfWV1 z9Ga2tZjg}fZt3ps?i8d!KoF4b5+oFmZjcTo{{!mpSnoOCx!&*pT^F0#d(ECb_kQAj z)}Cjr^}usD>AsDQDoP%&2^i6%O{(l5e2O}WMP=Y_E57md5sN6cRS-M)JLG2)>~Mk^ zWdW2m$XKyZ#LLgRYRo+tdz!Bpe4z@64zY+mNY*#VJO~Fyh`m&oUcqPke=$8!07#;d zMeJ9lG4^<%O}SvS`tmZ@!si~jBQS^6Jh;07Pp;BRQr+MQ_rcBkDwL13$uMcvjAe`8s+k{d-!BL zo*cYn7(9bZ>DTeOwRN#R@Q2#3ysU)UFDKhy8JcJG%+@bArzm_ zzKak`Fdr`W=;7v@$WfawE6m!Lnnd^XSN$8C=sZ3hSHVAxffDabl&dyqr@{g*31%cA zwfcVvI8+amtYA<9zPRpcaH|qi9QwgM3j5y1kEWExhXapBH4N*TN~R3-Cri8)=tRGwlfa5eK*82 z6N{=$Ko}q2u=In9C5L;AXY3^YlrTj5hsPi5wioRz^>!>7~h6Z|pjb z`Y9zKJ^ri3nvLCQP{H0hx%z88gB2#9{qD0zoctx%gYiZd`0;k$maa9J7!g#_^Yd>7FMK-$rEPj`6dPJlebe1AsU?!*JG3`N&?cp51#%;DR>4{k)|estP9Ip< zY*QR1YpNA1$61`0437;AV9U6$UlWxSSZx1w3g-PBzWWKvtjqxRpC%}C{GXNu6aIaI zveI2yupcv&S%03P{I7~&%p4q`DauU${S;-U|8a`)U+RHz{8k|mW*XX_L(xKfeMmT)BfB(?lFDAPGwa$IcY8J?t)68b9X$GGGj;u`^-KFs(5Zw*wj|+76xwUSxw8LCp|0smVOftLV*C>0NJh z33t+N)}WieSOBhRUCb|@sq0)F(#d!uz{q&(_+sC(rQ8ti=PlX5M`oE`sCJxG79`zh zb8p!MZrLmuV0o`1T_ar~T@%b9UL#$YZR+R~3Eje*7;f$x$z5=rbiG@U$hyJW*Z<(n zSa;&gi6G=nh5YPo{y`KS?uGlV&DQ=qIjLJom#tg;lpEv?)2;n6S}E^vubINzxZKwE zSG&uMuAs}bk1upAj4yT`=z#D%Zg)KQFG{ED- zbxGxt*5nIlT?jDCkf&%}Aff=1`nzBJmle(aM=$vM++>hj{A+G96R2<*Xm0YIPuvwU zV`lqyeAjkgtPJ$Z0ebJp{@r9I|L&Uiub>4H?(X+*^OEn`=eHtk_ul;5A_N?NE5gR{pNp^oIGKMb!nWq> z?t!t9xaqu}8?58@p(em6AI%?0*o#CDJRk?mQg5mti3xfTvZ~5Zs=fZ91V6DA6Ny;} zTwra&vpCi&s~Q<doldi!S2bF5Vovd_FB{Fq&w^&=Sr#G!bItbl zo%gTzqj*?0xc2umjx+c1x#DLCDO?SGs)V<<5vU4hv0dRvY*4|cDhP0Wtu9=lI`RVm zmre0Jp3zyY$7F&HI3yPiJS|OcoISnl;7D8x10EXckJ%QK=QqX0#B~-M=t&Dm&lU9^ zmU3nh>-<>o#&F$&Pde&yyNPNu=%RloA7k7W|MH0&%5K!43I*AVm+TUBrRoV1Ims zAZ;98R~2$~>v(8$U=zc5;2s*en>UKjU=8<}lnX9Z`H1N=uzKmX92@%dc~rBsY&-@> z%vn6+^6>;xL-;DpD%V4|h(}k5b=Z9pNKXW#VqtD+7PJhBwq4Ob>$E;WYYO)upht14>rPvmwnhVi0hIObQ+*=xCLJ}UA;H8CkqyD=`qao|J zr-ifOp1C3JS}$|^z0*GUzikb~bwkTPd{Jv|AXeOBrL{AYqh9Z6 zB}HH%9aTzTq2Wt?!H26-r+ENG0{YLeL}l1Qepg_D`#Atr0hq6xwoM9tZiOzitbM)& z)$KvDCul+T4o|~q^MNXfhSXHp?V16rjr#Vm>kJU^00;aeM5eBo%9QR@_5d>bs=}7s z`7Nj55*pP(D19D^*hJ;RvS*KXUP_WwG{_>u3MG>HqoAbsPsz$@3r7U(TPA-53?SDa z`TL^`U`2Dwl_hU{zKMh|Axxz0zJUIiZ)&BHUiaSd6M3IUqxwT=K1xo4L;rJ|t^l@( zin%K$1^!g^$DK1yukC>lI6Lj_KGcxVTri!T@et2M+Ubyy*Vd3I@d0z>AEIc2sC2kN zIYE?QLkAPYq=4F&FjMUPf%N#ZMlZODyE;PJby_#IR~?_#aJRRLQsQ7CB8z_b{ItVX zSB>h?rB-K24iUY`GtTq!8;bg)VMq2Jl zr5`57$KHxN2vbmRHyj0}PlSAyRYAhav83jJ{TvaGM3Rq-Cino%u3OP#;KCka-WZXy zn%QZFAk&tfq6i!L0ot|N8av5!S=_v=#7J4^zH)7qM8r6#VSpq~ zLJqrZY?Ta2yU%H3O>AC*+LMGl?3wOj9CQOeI;h#i#>Y#!3R8Xp1{G`Opn?Bp?HvF# zj<_OdNx+&c(py>7Y zF^3uow)_Ym#gQ8^^(IkEc1^|#RM&YvUhDBXg4(d|PD*4Ic>e}dCGo?|Q^e4!RwHr0 z>vDq6*h4N!v#k}^h!8^qBbd3lAH^+NiBU-v=<;5H@jFmIg*^~v4BmxBBixdkUTO->+V{-7@o#2s5_7yzRW)>HH9Nlk{ zA(WFdLcA`*?T+W0G3@VQi<~>55k?r~OOn5;41|smQskLiaQfuNIoE6?sg!PI_aWV zV|kJ_SuF}NY9Df>Q_^Ty-Lc$_mEFw&TF=4C4qIa`NlNo%#=zo()YZJ)*}?Pa-MOZL zC7*T{jHP)YEnt^mlB8eg#`8vDv9dr)YpFiN<+&XMuuk3o_0atqTU#aotgP za#Ioduwk5IR%rCrj71!y5aG`j=hGY5pH!cczCQx9CBx{%KxyNR`QYt-dVTo%OUvyc zv|5k1XhNm`^?)i6moTfhMvKQY zR+!6lnZrj$pClkQB%DSWvRhai{k4+t9x?E}%RE*dIUkK|71(I>I`XVQv|5ePAE#E) z*rfZ6EF=Ih1iTVyM}6ke0TxUESoA4?ctU{c$X^ao!D&0PEpkP;eBl0~5!_fbqg!pr zx-wCVpXV{nnJc!vQTElIKg!Z)zLxVNhy|DBMc+!#;|DAb%N!W{zW8*~pSwPEF^Yj! zHD0*=s>AU9k@StBKEzzqhImp+KT)9)kghk@4?KL*aC`$=$c-vy3N?nK+;$8;xQxFn z2Ip0W(UXH`dgJg#<+d5f7*Am}1Zo7!F(GL+tNknkF`jZ-1{x1&3a#0Qr7}r+#oH6R zw!ur#Hk9e+;i-&^lxSdzu2R=MVUBc-cQZqiG^!j{CgnY^x&K)XuQS;WFrq1B@PjD z74qkb6lME9k1u*%HG-ZQRmCC>NZ^AgweLa9?^k|=m^viNj>C+l6)gy@E@O^g+C6(64|=h6X6Mw{By=eg_z8y|-M%$A;9B7HjHrOfz_`R4 zF@xSp!V~vW$d#>R95Rv0Fm?y5(#$A17H>+TsVW;x2ms}KCL`DpL#(mWy5u*lXE9E!Mv~;Wl@qw^|(0I3VbR>Qo|)?_TBQ zX~y^W*>%^0&$qz{HByqTR*UxQ>X)^nDsjsrYS1TW3Z8p}s(dhAF)|IYL6t`-^TWGJJ2T7mW7@|4gi{wApzhbg+C1UyX6PZ&kvoy< z&OQliL4`WcRx+OY#DE<%na#50wtXgRM=@Fjj$RjR4O$ewUK^|gS%WPLbH}7v0fs22 zWq`5d#b9DV(B_4PLNa`K6tCl6#v69gMv7^#fMY`8>?DP3Cvkc@YxGBZp7o{N5l6Ox z!Dxi7D2tpZ5ZvUhwuidQOWUmDaD>Rm?}j?bF&m{1)z@3=C;Af{BrVR=p+^sGJZf}H zDtCm-He3%x%*L6bQD2YIt09TDZ#EXza^rv&-J$C;UhyMI2ac|?qd)6{kRd#9Z(9LN zhLI-xQVvt$vPgmA(F*~?ZCeS0PS_Bs=)hx(9j8;Y`aYq#7XqK>-7Z)WOogj%1iT4u zlnX!GVlH9^4DV6fGo=y*r*s%Bd2~E&kH~TPBgn)d`AHe2iZw-ulCM-Rb!a9f61C4B zun1D1xaTMxiVp;8piH>AX(F{=MrWQ-=0Vkdr!F>^8#GRWuyg_!$3kMaP+6R-1l9j;fr zjRr)=a~ylIauGy^&vbg8zij}1s($`VXCrmG*W%&8Rx1Ht%I9ITp(b(j{^Y{bj%HW4 zmhv{`;Tp!xstm*T4_^2QgT)Un*PFESjOtUq@+VQ`UJk_2 z=G+YgV=2g$&rS2WvLbSFXtnN?m{$3t>0v;9u%SZ+|5*W@wnb}XIgg!wovWques`zS4X@XDd)_NlujigFSLk4VEPl_H zvA^lYdeX^r3H~fhmca8OBdY21&BP2c!jPAlY2K3$d2(qh{iEmH9}Xj@PH@X* zh3kWD#IuW?cpDvuM@lDe1ELw@UfW*J7CzFveI&P79T`we#rB53`g!w-|Lb|hYqiph z(09>_HR>g=u%7ISh!n_IIy_uQQ`9V888A*t8PAPFuP^S)g2Mz=fhE`;6RIX0zwmSu zjdm|v2v*^=s%$1%mONb&`e+h@KKm^3lW2IeS_plTR>)d!qRiFdYrZRa7rn#FXzs5Q zou#dt$rftoUwAjpHW+Tvv|E`aWIYGZqKl$=4h&DrhlrwN==Q(BdM*UKy$$7)EUD_y zbS)@TrkgtSuzS2@UUEDX*d1AVEa-a3J(U2ne6Xrw#*_!X>{k8E@K~e|H0{}`2T@Oo zS^+wj*^~w%DZw|EF}=OIhdx)9plnj01=-PZ=@fWT)m%>v&@$uvu==sK{}?+Me1yL0 z^$6sXX>(&)wr8((>$#d<&4d~Ic~XvEn=KEAE<{CFwh(Lbx-a$KOkiX#c(E(Ougo(Rh{|6Z%DtUgn&z>HQp>QS*$&m@fyHi{kZ>VA?RU%K zDso5W%qsQN-!J8Jb3KQcb^3ZvbUEHKRCYN1*5b>$aLBZ$I|)Z?6QIUyIX_hAYCAD1 zyw~ey2E{BgNl!?9pfzX#EVw|;i)7PB&oU$O74}BTL5vWr*9D|d;1i4wnrC?KuROy& zqpxGG8?zSWW8{iXDE+mohbw+M*6i(&6}A}aGhaA zo4o(@!8EawAo~s07T!?_L1LB~7*&)Bh9#BK0*TS$D}k%I=~l0WP0@47=P@LD44UixAKekKK2$x{WiVqw9`r?s(lO{Y_ zA#rkt%0&iU;Fw|`sa%2}C@YgB{d(yo&tlLhv*oq<$4?WN)zge`*GArXijA5LdwqQE zl<6AgYCRhTWs=n}zZ9PyG>=o6d)umK{AlNLeXySXMIjMJ@w=xA)>M?SGm8Skw6E!f7cN-8-PD$wwFnW;`b=^Mxed`>}jKzJyE zO~-W_H_|OXVTZeX(UDc#rCmWwz#L2m%~))pogHEa3~KnJm0NUhV09`?z0 z1Fykr+FWvZ=P^dRoW;ll$ur;(v_e-al$)uN(AJ%XF`Da+Edo_Sh2?CN)l$VNFkaeH zW^Gnt**~2h+Q6L3i|Vm`p{Vw0L8XQAEn5jQLiy6o$&Bgf*}Sc}>l@k`qRj;xsbQ_8TZYMl8)&5UUh-Ax@WElmJ3Pl<=0!qjJQN4p;G zEX~JHmSE!~Sx3A;-IscS&Q?v{{o|hE6;4;6H%n-xFjhlXSeo4!OoV@E_NfDPoWRr zVE1>Gy!bksxjC9+DhMk^%4veAoM+*l9Y-2rYdp|st4iZrYiPsiuW>OxSuIW3Sn$Gl zVHh&VK))XI!He@dmu!#3>8R*=;tfijQV>>N)WiVC(5i0XuW9t$ZEr$sK( zs2_sar`a{q`5GH#|M(EAk7iYpxCMI+%5n{ERc_oQWm_i3OEEIxk>sVNr*^=UxfiSA zSIK~<`9Ta5)zEA8q|e_N`_?WOXBWKGMEMxy6%cCx)PIBeKKji=IIw8Hj$6@A6QyY- z$tU6Gd1A+h6_?75qf@wIVW8TUxevR0dAHBmyC`g5eJSIF?ZYl~l_H~%6hkXf8T%CU zs62sIoFWm&u(-}^pl(lzjy=EII$=K#n6bQ@_a;$8o{b7M%51C|fh;qRP`G$*Vs6~@ zE2F{fmdco9%dCLayujfAr;Hh`ZN}7kVUH!SZaSEZ0*h-5quV;7Ll18hHu0%>sZRV{ z*d(RL3j%?7Bn#KA-fYOD5^WmhM2+UAEf$&2XZc8AdThPmr^+DYl#Gyg)qu#>v}4`^ zV`>}hW%omqc%gjn(K(Z4J>(>EX!jr!d1FFnxP6rh=CLJegeT;DNgT z{A0e!+KU&Ji40bS$`-uSS44x4y(r8eiz>sEr;>S3>>}&eFJD<$UDm2_C5ArT<+dZSwN`GP6QK@og>yvSdz=+iy#<*^+(s0^BH>ld#peU&tO&j} zjXg@yHAKX434FTu^R)b=B3)2}ho3vnt-4^OP+MUa#Hr9X6I(^zZ`dss!~>X-i+X({ zgk+D|P%GMd+Z!uTARM%AS!~01%-dFGnCndY0 zn71yvKda7EJeWNrdi~1kc0~4#G@tWzMlQ-0TQJMPI_<$xpW^hnKW4?gmraGm5jbRU zd?!Tb?!jsZE70;3T65!iBYl)=WFP2#b+vWu){79s?XVo-5yY11+d1lg#hsD`?oBY zUw%vg5P)hO$fTWkbw#Frfb=y6hv;tW1Uw?tnBG2QMcSd7X79gP6vaIoTq_??GT(}M zerYs5RbA}~yEvE&gG@+HJCJ5*W6?CHj%6N41}hE^@bPK1EKVtdEo9jma*DS1g^!8o zjhCfGr6!A(%tv(@7Z+uJ*dbudCE-4fD_&$*ZHSs3Gz?o*JxR|Bwe~ z0p-nr9KioN56%JnpYq`M8L{89WPdKz&BDa?pYq^wgVz24P#*jSo^e(rKt?+L^t;}3Dl8wauG~b-$U|#LB9-fUhSZmv) z8l^);B)pV}4Vz<>k;3eVl22~+wc0~E_i%Tm1C~~1o=4w^1bA`HUYnu_z z_6O{??E`TZn=2VbBRtL!h6NgIPps1cm3ga`OP)?`4kbo4d7?=1I}g8%X!~LcCNvj= zR)UzkJ+qG`PX?u|c-@jB2KwpX^KT5?i%eraxjh#B|*ybbVqpXN2?V?&$(hv2Z)W(=LTL;!WG$Y{Y>*jNd3HRn!Ww<%iZ2ZEfL_C;m z!0O`67who+n72m;TN-H4(bKWdw-MJhb@OQ8G>EUn3_w9d8 z&;DiEXwanr#J?NfPs>J&G7&Seu-%D4kx`Txw9oNfhp6t%H-y!-gK!5@tB_BIAeMvl)I<%LBVm5p2+wSM{Nzdonz zYHP$OuV-@i&)(?%8f@RbR?*18=DEFrkpt+m_ur(*C}U)3rYC6Q^6c)5*??>eEC3cD z3kLwe!3pvimorQw~$i~d6#VBBHZR6G0@?uOOe z@7#?|QM^ljE9>byP%AyYb7=eL29DnVIS_+%Dg?Sa5I6_o`*VL50_#tC@Bd%#HiHXmY-0uejB^+6DpAD zfb{>}ivJ&K5H#@Q9}RMF-h0Y&r(D9uKI9M2%enpzhHD$U;s5_D@nb#ebH&HqlnzawM^A^bzO_g40g&E5AMewFP{{P;IaY@o6n_u2(% z_0Ghz1WD(`17B(cxR(OnDAT2?iYJx`;!TOy{BK85P0t%ze@V2e)V5{ z{J&ztKM^u9{ekdajQ>R6Z=(7Y)1CDGSK9vr2Rkbp1LtqN^BuAT(wNb&dmhP&u}$4%ck>)syz z@7oO<$QOQ>#$BiYJ&^v8#$T-eKg6g1u21xbhFIBwe_;A%um3RDpAic9zm8CU;9vnT zF|hw`Dc>UH583<~q3((P7fWGc`aun#uyZfjZy|~ma4*?kO@;MN-FF=ykm`SQAAjus zNeMsb;M)_`FLdx9J4W|P{@K~Sq5B2;ujK#Vsr3&`e+x+8!s2(evI71HNZ*?N4W_?Q z=f7z34-9O~9Dl0xXLJ9dqHomC3iu-+eQWwRi2id|=nqt^?96|362D94o1uM|%s+Da z!zI7j(l4m)$`Rf_jIjcK4oIx`KUCaJFZk(k>W+l|u{IDH{b^~S_x}k#f8b{UN%i** z=5P4_BFF!$;pW@Z`_CT0#=-#n)p@@=+&@yhkIcV%z<&rg|K8gEz`)AP`nz0yVECce zZ>srWZGQx1R={1;FM9onO@0nMtbqHY-}DO7<&O^UUGVw$di?`G3+U&)-yTnY$A2%w ze&9*KZ3(Q;hXi_k7nP)H;Cfy82UFU-i6`2aQt1G z|6YpUf3f-56gmIsoBb}vzd*iYrT=S!`wJ*g*Zj9f=kL_;Lz~}3@r!@lH~l+M|FJXv zClEFu=kJvK1IQ02`7=~8{q&q7OU%M3ZDeiYXiCh)%Ji+5D{5xxXk^bQYN_XFBy410 zV`v1r4+lqkBRwm4myGf>73HT|mwOu;ava=_&Y(Z%*N)<#?J;cL_~X0;Sa{fwJRj;} z>X012u$(+_X#EFa;OXb?y@U(5y0@dCMUwXx8rCx`KCQf2Ujp}8;2GlJ#(Duk%uUMB zGoltf{joa{`xW@R2Vk2%#BDB@J)&DPAJlwq>*}+>HnVwYzm7KDoO|Aml79KxdjbAH z4^*mygNAa?CmcD2k%?iOXzz)x=mN1#E5uueHgqN!2*QINzBi-ZIHEV5zPHRfe2-Ri zi@e*=9~_`V+`?R5p`0Ow!NK_Gtm(Ecwz6Cm^;O@dEMgEn!lJ@`bm(0~?ysf`s;vh?51NY3&Tm=w_V=?O9#lVQ6YugK70ZI+H#+9$ zJJpe?0(&6I@ZkFOZRuBKaNARy2W6w4D+cH{lbbikY!4vF`3dGQ5Zl1Gi61QcN+tM< zxV1gN0mIORF7rXEf1$5YsD0i97_F2?p5X3mF3&$ImPd_L3N>z+XBBszw004??V+ zDFy|epM)Bmil2!SO4c7;9)3Uosu)rw2YSNqi98BkoBBK05nsx8h#LH~rVyVONjD#4 zE;4U|f69S5@#B4mQsHN@_yh-%qYV`YnrsmZ2g5ktqdA zu1yI5v)=A-08Zzls*BbPZPWJb0J#~4sEtMqf~ei(0LlXje-Zx+n3o@Y6_S@P!2!&! zA5RsE04gF0$-@BODpW#Je)14R1!A`lcqTz@0YN1D{*j*5Zf>$vQ$RXi`@=%e` zgULxAi$li+u985B!zubIlROuP8257|))FUT4u~Me=tE=_u?pTO zxEDj^h;tO=LE=q~3hyVF1E2;E3&_h=qb8K}iOV6Ar%6CI;#ZePPJkishtFA&M^i@3 z^pnhCE(R|ND9P0*CN%fK%RQE7nSj^wVGtyEM^TKh?dz87W%}R^@+Y6595mC1Zy+Lt z&U4^4Nm!v|gjn8TY~r$_wD@4@#l1)@ZSV~A*KZy3oMG;uy z05sMBNI`g#aCv@evI*2QSgQcS4pcoqfS4MIB^olUSO7u?!BXIXz!x$<)Z7qQVOCP) z*bq)XaUny}8Z_4s4`Gr#96LOPpkl!Z(h*`jG?|d6!d!V6V}T8Rn_`+mbNq;8qG$#o zmm$F+Vj;L)N?qK`6#8(7ScinwVP68}|-NxevJh|gjG=z(3NaN_WBVTOV=q`+8i zajyc@xS#}nNwT$ANpY0|N=2BL(X66rB=oWM;xGjm<49YfCju8_{xN!e;)Zn0V5t$( zLdK*HvAr>YefoxQHILLI%Y?E-vc&31XJeCNKK1dgJ~)E5$8p66MrH8Jk`R$)lWxQ! z^iB5}t->FX*%N#vXbioIoD%_ih#n|{CjamuL>O*&)UxPdet5O=xV(C?rZR5{>~naPpc28!+>yLAd8=aN z38M+j36%+AbDZb+&#_M*oWin&rwO$P)8{5B&=$)TR~Ba%>lC+|`iSOLo06s>4~H0w zIOKhOCuoYNfleR(RJb&+?wy+{SuO4iyj8T5U{h|(ySJOfjyO-?34&!si*jGRbKP9o z)OUn?gRT{B&CiiL`;K7K+L7o2&kdF*j8Ke^pHGlaU^E~5oz1)bF`}`Z&HYWQ6C%Vm zVkS;RIH7KU0VjYPM<9Y)6rC(wp`%!@mgxcqBZ8s_YbC_4+eOcVNg%~mnw1hP-mkxH zRUaNe1)xZgl%`*&dx7f!<^bma<$&n`;efyrmDAqitJ7_%qLps*TVO{EO$DUpR=UDu3WX4RgWjpYOOhxmb!y@c(?x(?M) zAM`CB*TX-H<_WgycG7LCzWyMv3+sf)E{`x)$z8oLT|d7ud21Gi4zhzoNTF> z6YCLn6cLqQd|Ur=f*l^GRtDM#xJkG}SL0H%9aw|BCMyG2vfp3_K{w{I>_SRUXV(G>%{dDJAOr5MUShvV=ZVb(Blg2hnEfT{2{JvC#7n zrLLl`MY@p90(I^Tc5|h~D{_a(KkPC7*X3ohF5~IZS_M!B@TE$-FSXDVv zJ;HKL_A5Z8R))t9OePhK&7)Ri1(o$=38U{$T^51M2~b+5ew<(_3AXmIFl?OPEZ0%d z?XiD?!T{tNlTlWU@K^q?f*%X63WeS+Z52COIyO6!I*vO^I;J^7J7PJWIaWCGI<`A1 zI7&F;I*M(DZ4GQ~Z>4QAjBDjK$#O_KXIrOPXIY0@2U=%Zhg&BTipUk16l~;R=V0e2 z=c^V@7jopj&UMZ^&qFDQ&XLQJ%OB0>6VMUW5nB-Uj=h1uCcdV=hP+0+cDUd;u{gm$ zX)X2%D^x6|Qzk5dRS7E*p3K8jC@&5!CRFw|=VZ%D6CEzZQWz|LK0#>?Q@1|tEf9}$ZS}n zZ?Au(53e7lFKIlW|71jYKzVSYYodLkm$i#^s zY3ix!!5uK@3}Po) zJ#7Q=OpSdNe2ru^&8zLIw3?afH?J;UiB?h87*`XT&>NK-Sn0J4J!yL~aN%?-XZ z@7jFidu3Cz@36>;!`Z+o%xTCu!HLLO#+d>1;OyXB1~PNPb4miYff~R!KxkkOC!rnp zhT8g}y3c|&`Xctd%Uc~jj!xV5?wyJGypci_EpOY4KFi@J!0iXs2-9cC4(E!F{M z$U{z2Tjj-C{ucgp{>rwhHV9u6a2u#8C@+LO$Sp`9lnB%sR2|eX6akbcBpGBp)G?$S z!W3LR+!;JF0xxO?h9;IK_8giZf4LD6$YI`49(dk( z-gX{k9&%o=NT*1=NOV|INETWPwVTRG)Mo7_?HJn_J)U&_J5JL)R;V@^l_@>HWIQDZxb}*=i&q<$E56}B*b4z;z|L>GUQbGjc^c22#7q8}jDRAOL5o_8{u9k7S{@o6`fJIH_!Ar4TEu+B z)`;4O7ZHsS;nF$M8PfGbk5gHg37Dr1_XfBV)swzbzG9S$GfKuxREcAYsf*{K!zW3n zao4PutQV#F5zA;M(Y*^I&wJTt8!|SOQjtZqzbWGu`i0tOba%1HcN5xQ(n3#R%=%a*~uTi zU2a|uTYiDA7%C7dCm|~FtY5NU>#_1yh@!7zp<=jV_BcVIdLf4-zU1YAp%H@-oY8Y5 zB_oqH{x!%o?ltDM;{l$5*9k?5$qCbm&MF}^k7)F1N@yr)WNE-?4rv-`3TZZ!->6u> zlrE)vSysARg02Eo?pB#pmQl%6fl$Fvi7rVhWq)~AGXFGF!889t2B}zK!eydhf_0K^ za&A&^QgI@xbh21W9$&txh`p#juf8zqsfi-CqN+TH{A<;7m1u?0r+nq}#q)){dA#Ls z3vN_A<^0Rq%Sm45zSMiURDt#)wnU*=q0~-c+Q;V7HFGyx$y5%S2C*KXd8wLykhO0hQ zwO0)uBrQcO)hx-8C7^}J7RQ!=f^xb-ibC3$ME)z$PonvvuSEAF>LZwX%6eE0o*N(- zpsc{INUyx_*@$qD?2X9At;D-!Z%?;P6HhZu|B&L6-j%kRrkqlnrpb~DPzQX~ud$Id z8L*+TF*n^d#WQs4s(n{Ge-gJ9vK6sav-JXZCSp>0Ncz*z_|PET4Bc$mP+9kG$w$vu z$;LWEXTw`VUxzV=B8M`E{fCu?EA4YyDO$!- zoKnqF_)>(Y{!indN`iBdp2n7j+3cc#|>4XRt3EmHu4^~47B^rNX=-ziLKc3qmWP<#h2#<@^ zd-HuRk61~5Onz8?V}7zhgh7@;ok8$Q)k@h4OfLad97bilUwn0Zx>A5ry;9?Qs3LsX z_p*bsvm-7h)+XvELMA2~fDP#l#f_&MJ0nUXHklk+whWpK%nZ;BBMi9=xD161Ey>+-=6SZBji+5&ma&QQD9JDKSY}#bs zW#6e^KI_d~W>~D)wchjkz%#cGPkde$;7HZ&ZC0bF@|4QCm>EMEj{WuXd<*ptebiTg#q% z%h81wj%UhB)rp>e=SXlTiV0B#r^n&J!4g&QH1fWiQj19QWQ$evbxX7hwtMzD;X&4k z*GkKp(81EF!U@yq;YsLe|M|=Ft;5Eh<8hNt?6tu?+Rqk?JInY>=_}9HX1BPuxi&68 zq8u5VVVu7|CNGT zM~t3>tBZ|_69byU{tnNUMVC^SO_w7Ej~cHS*OUn7VG5xdo(qXB^?}5q#-bOhL5y0A z)4SZ9n4F=Uu$*v#*M3=0p2$J|>V6ddr~aA#WPT3*rT&aC(NL|3^RNQJwn2S?DFH44 zT|o~6SOZQ1r30FgKf+T49R-&JXarsa62kFd&Y-)IIZ`^ZIWk5tNz+MF4KdNt(Q%d0 z>?(f9tismAU)r=CvHfcM-nR1K%R$FM&w=m3%LC^nF0>OgQnWO*Gc*#kR5VPqOtjZz zSz?yLd1OGcH8L486fsI+ERn3}W-K7VR}y=UFcL{RNveUzG~6^CC6pyx+tw5M6Qibr ztlqVfwa*>-9nBrh9NkaEPc%>55SfF>L>YR>duS<1)`g{&hE7v@nM#>1tB0!5t1qf6 zs>iCoRwGqkSI1ZH>pL%Rt_ZKxt~^@lSg~I*UvXRB>urgejiTyB=(XtK>I&{L=~eA@ z?p}!0iCB;%rhug^rusk;KyFB(Ora^;kY1Hil`1a%P+D28RtiyKNU}_FSF$<1A*CTv zSQ1@wT-;t9US>eHL}FX^P4>x?EmdX;b#isuk4!*PAY&tMBga?0Bm1LlGDNYis7(Sp z9(2!wMe&tbU$XaNPnG!LrNSJ;?7~zf1tskyr3T~&UL~9=E7Pc!=$9Ckc$;gOyPBJ9 zt8EKzkFx5g>86RM32CTlSZio$*vuHtT-JVaeBs#d*y$L3^5q0$Yj{g&Yjk{?Ts*}#;qGpRNb`{>{n~Qyp4k8%l za$Db=%zrO79o62;8TI(lD{d?ox?{D|>MR44s1~mJ^wBuRc$>8S`0O~HEbqs+);Bv> znczX-q2T%8m3{?&@1d{}_8!t2*V6q37>(V^Fc0jvRXi#Y?GD}*l0DwHTJEX*uCmt07?p*MIOPydPwD`>G@LwbwNIiQm^E#fQ&%o(PoCz>mv z>N4sE>YVD9>b};=)OFS|nse-I?vQUs?0D>q@09FZY`@syoiy0~y2G}kwY{<}H>p!b zRO$V!)jjBQL5lcgmeU_$OjPmEBDo{YBcmdjBD*6$N3KNL;5m<0XDV@DE z-u2^g(uV)&My3t7_wm*7ak|WjOq|RMnHw2(8I!aM4jzk(d1-4b3wG->s}a)*D?(GR z@#Zl>%iKDgdY9J)uP16xUJuoKSW(o5)#KF?TFY3JTYt96G`pS=HN~)EF+H}HvM4p% zu}QbObn|ubb!tEG8x1@`sKbV4;BGu9&zxhJvYy+U&ibgm;ML+;aosEOCOi&%m;qXQ zsL|zYc{rSr!P*(%)aR(?6yW%1$8@`4viieeQGA_J_4^8znpefgf*URyM-;Z9J8?4< zMN)Rjk_=jJPY%Xsj82Sv2cIPKFcxW6yqR-$Jv$P`ekoNpSTNX1+f}&R@!9%? zb)$9q-of6=C#z2ep9Xnyf}6xzdJCy?i^__cHWWrTXur^ssv=f?(d=)wUmsXZ+)%A| z!*ySCD|hd7M{u8Y>vdl^ZTIB%O!Kt%?DxdD|4;aoqP6ozvaK4@4ooF|MF)a{rQ)^ z_P%de{DVLGnjd=97an!)YrgSO|N2qi_vkPD+V9?V!{5I01OM??pZ`za{ifI6aLeb< zz3S|%?t0(DFZhj5z4s#@dhJV}{hT+w^*i2C{oLxqm;LzidoI6c|Dlin)$cy~6OVr9 zKlxyP)xqC9@Z4Xk?mGF|Z++pv`^?XN`i7VO#Q)tt|M2_Xym|4R&;LJ9Uw7X-4xaFW z5C6>{{mf6j^QVse_QNOs;+H;t_BYPH|LnUS`tU`gn8hj#KxY z`ux|w>ym$U^QYf<>%ptu`ezqD^2fjUsqgxepZlGk`|NYy@!a2h?q7cReSiFQZ~U$| z{tt&AdEnE(_Noh?|GNi&_=}nckFpLyx{3Sd+gP(_@-Ar`zPP~6aV=YfAZ>2yz=*c z{CPkAjj#Uaul$P_|IX*H`_Lc%^>@DFrGIewn?Cj7-*4OB?|-p>uzA&!Uv%)fPyhWV zPX5lx-+cSmy!4UBe(1>u_g(hIhkyGY+;;7$-@WHspZl#Z{@`!@r$=7#($_!yy&wPH zm%Qhf-t&)M|EkyD`1+3=d&03(ul|28`@#DF;Xd@vzx#KuIra^&`Cl%-@WNL<{Dx2b zm*05ThmPI->R-O%#utC^i{Jj>dtPz-Z~x)TKKL8o^uSNP|0h58?2kVClka`_y}x(a zU9Vmpt{!{obJ_iy!^=@`~ENf-Dh9-m@|8S`hCCpmg`>h zFFyFThhFmTy`Osd7xo_geV@Dkvd{g>gMa(T%OCrg|M6>o^lP8_wLkK;|KTw&`1)6W z`lrO#QiX*X{p(+J{him}|D4y~|L6BS_}NQ8aNz?V`ODWmaQ~I}|HBuZ{Pef}-S7Wk|MCYe zyYG`9xa3_o|Lq%h4`2H0zxe&%@%NwpRQ>DU{;Ox+@J(-c;R}E5{e|7v{nVdd z^X~uj=fCytr@!`7zwqSm{mU=Dm%m}}_WR%a#ZTP# z&2Qd)>%o8gmbZN0|8w7kN8a|ik3Q@Dulw9{zWS|S|H20!{@jN@@|j2f?4y6|(Qo|W zzyIOi{o!B!;Xgb4KMs#SaOVEEz3AsZw|e-FkAKJC-}2C#-}#w8`qNJo3el zJnHRV_uykMzv2`3J^SI0KlJwBJ@&6(^Utoh<;6ey$eAzP`=Jm0*Ka*>;|tz((42Vv z3qJY54}Rp5zj@g^UiR5%{>?Mr`OGi<`6qwzeRsU|jvsjN-#_@%55D%n_dNLEbH3^R zH{AExFI@V$pMT`tfBL$^`(JV2&wugL55MYDZ~p9O9(mw3cYpYXW|fAZ~ree6$O^QJ2v|Kj&Q^0qJj*u&5NWdFs(Pd)GxFMibx<1N4PmjC@N|LR4*wD*R?H$C>P&-s-H?*G!c7k}vi zIB$68D1}-rw;B%U);KX`{{!xKkj31zW;HH z)2E*NxMy`YEN?h__5O2DU-yD@`!~Me={KEx!ShdUPCf35%Xcoj=hA!bIC}@YWpU3P zXAbVX^qwa_?!>9XTlX)8&+hTw<1XJ>Tz2l%ZI>Ru=34#^{PyI>J@@?ivzPAe-F^4n zyLUIchv#nJt04T_Th@DZeF=Qyk~{A`IDg`vOAhXQyc>eXfL5lH=T4t>{93p%PuzNV z;rx>y_c$K-?5W#MIEuyCCr+Qf14noNws&yn-sw|F5)4S+zVz&gbLTH_F58p8!uy7Q zKY4g?+u^x8P8^)vzx=AcYwWbC*Bu^sVRikKK0L>6815 zez!%=j{mw1PeCJXa_%{C7RXt8a^QQHUQHam>G1H(<nI^v#Kx5VOH3z=-w+D`HtX@%}gdAJn)1 z#^-?Ke`O;7f0N=@B=QxBe3>}#71jDOG4`T|{fa~`nsr~MKm3YBzD$h0=wZJik&9;C zm+23`B9Si>V=sExuSn#gS@&i7!>>r>%f#4=9(HF;0`4C`9_?&fYT?*bfOGZeL)catHbf4{m?L{=p^Rc@>;;c0C-U79Pvi;>k+f zQg;l#eJ>>U;P20zIEl%yy{pa~o_s#M0rHglSM5VmZhx`dt>Gvucf{M3XHMUKuy-{i z&Gyg9)80fIt3fMv{yJi@`BV~nrOU8lFQTIrKZNv(K1yR?&jjv7on%544hrvde=(N{ z)jG`tF{VW;>xlW3xj^?YWJ$BVUJHB`9*5|o{{jg~^=oGEpqe!A|POcsE+zFS>M*6<{ z;MD%PGp7&sgITyPPX23S7Ee2U04DK_MLY|q!1!ut;YG`P!9Dk&-N8YNZH0ZLdSbO` zCB=8iiA8PedZ!~qfoZ}N48U2BExy>Df*3F^Iq_ogb$VDh@N(d2xw`gkK%9`NA(aw^ zMKVA(*WXMVIScC71swwN;AluhBt?4}@ZvFfF!H2Euhg&^cwjuw4%@3?+ghl|MRPg> z;04ftWDutb#}Q+-x@n^h)p<}if|+UFu0tkB<2MwgcvyEorZA4kQZJHPP`z+mVH?|! z3%VSRD>PxNahq^lfDW?YnZb?heL>0?&Z181xK|RK zYRawwGXkyycf`mS%N@z#i{*|Z11%5j;rC@S zG77(!81JbrfnQjxTZ4=iWTFpXx)eO7P~;ix3Vtf@DRbZTuHYp;W|&nM7~yq9xjEkaT_ z>#oc&=)!@7!NFSsgM(QMQ=Xo+Sd`#964F0$>zRE><6O6Y;?(Jb+ueLATOz4)Ktq1o zO&BMq5C;BTOxYsrsRx)up}Q~^)vog^d)MuszWuovYqXV=3k)Y9;F!CH!DYg3Jb4J! zl2;#|xp2pU{5n#cnB#RQM%Lj`#jaf3>?TH$2fxzI%gxcTS2M zQH#D%XO_F06&PefX9OejIwKU!nrd2SXjU)-KQE^r51W z$k@T+T`XG>uNb6QvW)Ck!kR@39CAh1&1dt27V@4(5fL_BJ=Ag-QVw z%)rmf36&lO23xh#rCM{0n2(puBoN9iF%~VpOWTrKHIRS3}HYbWqf*^)gu3Aqfep zQD^Y(u^_jHbfRKAogwyybmB#N(*qBZVZ|HqQiDaboRw=BYyOcgMt5I^iF8vkk#})3 z_Uk>Ev7w9b?-|!0N=C!DmW;qI0~K#dNXD<0flkBS$eRa^fd|R3cqb(T;a1=tJ)dY6xTKs>+B?5qXsHQy7fJ&xDuoi+s>=3L<-oSm-{^kTQ8QnDH8~@pNaL zgkyz85I_bR50Vi^S~O#P$FwqA8su-y8x0XPh!F~knhYBU%G^_akwsakGZ`--FRC*b zNLWNy%SrYjoec0mCkZIvAYM>2&5l7bkr8RhM9VN-^d;sWc?o$jnl(ow^uyaPBfm(? zP3ITs`khE;gaPIylkpPK1)R#ap*2rPhM5X<;-`>Kyud3j8M=8)3SCP^EyLt}TLzoG z#Pj2-MFif}brkffMFd`Dbrj=95vI&|S$Ys@DDq^Q6bn1(QZ^|bw8?YG-G+gmO+ZCNW3TuosdtDfh=n8%8ZOA%%n z3Ofo2tuQ4ZwYC5OW#Xg=E-Ji)D=|+aP^kO`amsv#*r>u8#sU&5pBIqE+(fLOQMI&aRj#7NrpzI@vY<*4^&fuX0!1rNdeJs2PY!uu zxu|C?hAx_Q6(RO^yyPqb zBFcRd5Lx((rQ_3M4-%qE^OSi<3p7k!&C$y$(|2Lwuid`j(`K$j-q`432aYx)JbYw&#dZKYh5aLUH}N85@VjE-y7VjJ|^{Yh3A6t}BtBLn(M zHVYc`C5p-@|Bl*$lhk-s+P6a84$^q_%vCx7Zao|)ZqPjPl-={_wDrh3As`#Owi{Wm z5M?9NRwIK71`1wfnk&k=0V`=Wa$(4yl_0E9)yN{^-;Nt`v>^B+E#*8(9O9tSH@X zWDP_u?i&@jji9|IYYmwOm08v>uBy0urQ(*DiVB&b!PR*HumWEW%bhr(uF8VV6c4F? zC*go#?W9I7gs=R_8ggZ!Ryr!Z7{p;m6qk1TTT zrs!+{S&(PUeCWZa9tvqBEKj1J{+B#&!=rU~OzSvYCX zdF|oh`Gdpr7|O%h1P&b{T6vKOv~~+vsX*y0f{3Cv9a}zqct@1(1?SC;CTc{nX=6}+ z&FQ<$xWEl(v$HJtrA`lG0=2y4k57?%%n3{3=$r0~3iyj@5X2^`qgqr} z??@$!U5u9ve5U4_B%igjz?zZisCmxN+FQs%kS(d!!Ktj|*m7tcoysDKEr-_ODYgR~ z4X<^4DmTuzq*@24a+7<@p&?rpVsWNoevbC(b1%UAiKwvL%)|D_O4h>}voExB3^>Tn z$;nmR?^AJ$Q^h?_6*p#7+&WTmn@q)-u8Qk_6&uqj&Vh}7Z`vhbJPxT9ratBa*eD!G zF&kB@WF`0G4K_w;El(YKAx_1C2R55HR)wo;Dwa;7BcxG7&bG1yYI^q4;o@tit=uQf zMbBhK9S|X9IP)N6;~-&IQ~xOP)=$7$QKwV!HbeUc@hc)vw%Q5xi!>Lc>1 z(r|pi7#I#`$SW=kRZ1=k+bk+WHPV}Iu^I_DDo~?z3X9tA5^BWtTA)UL{otxQNE4Jb z%iS7-x1$*HtHZ_zkHwe)jGgzWffc5@z?XFbOK9P|ioysq$-9!5yuownShZRV-vKX1iNy&+FEaCWCPEoXu#W_s6 z#fVr*qbyXxHoQ-LDLHk%G!CY2Hdl05)R!PHl#})cT23-)XmFA@;3wonw15dZVy*m( zT+ADz5B4Nm2E3Eha?(p5xq{eo?C7K|8CGvQwoabNpmp1^b@oJtoZF7Aqiq>~ZacOP z^JFBp?btfZ>twZTp*@%R{s4Dn&w^KBz+HMfn+SVKZh)z{_`2eDhRVm>9CBA?y?BQ; zgT?l*gN4ZcML1}1K4& zC?hZ9Z39605Kf}C0w~yA4kPzYRsokyUWTa#IevW+C>}-Dp^rnw8}pZDgG48boh~PdcYudOu`VW6)$D%WH4rw69;@LIqATxlck~-BVvay<5+_nTTVPh zNgB#&j8SoxIwtL839dz6C@1akBRPr1fkiq6W|MLnqe!l04s^bj!HF@tms9Pd9LWrA zPbx5jD*%%#vN>Z%r*CywlAJ)*NC#*N{1?VlZ5^`OcUWvquT^e=VJzMS4a~Lr_6HQ0P=kl{ZEou{vb%zn0#_nngw( zhS;ShiAJX7>@h*CVOj!}EP^ulKg1`Di{M3(M&PF3$Pn>=EtzS&ZPZs5kQAJXSDeUuB(%j0EYVNFMAYADsbE zB`<=Y)pk-#T9b0(Nz3uIoTB(&mdIO-h?O)`&TQMh;hJ#jTz~~~A}(k(l6CeLd7+#% z{zq~W7HM#j)@k*moQA%unhM!NV(Y@n{hBJZ!=OdWNyY!5Bmx>NM#OTGtg|L-*%K9$ zhoqsLFn@q5%Jt>io-$!XC?^ffT24|aEz&dggxMG(7DX1y33!#BD9Iuy4a`)6 zPXfQAUj}BL2&u=$`;az~(AFioD zKcoziWei%lW)am^%p91LaqFRoGcgDZ7dYXp zh{ekdzEdSh0x5h82)))L1viA~i3%B)i9tFzi#m*7YVV^X&f^k_pbOy|+No*B8``QJ z8Y#HJc7$o64?5`20GIF6-b(M5LId}>Oe~u`zGj*n@2%vxCj_On9Km(&KRI@CB#9%vp=PP)-?tP-mKscwspuC(wj4H{M=A*&;$ z5(Z@v8}m}4nMF_@V4a{omu+DW^ks`^fFTAXr7eQKRxyS8QfrqaPd!K5$Gb03k z?LebUl0*}W5wVhHy1Yy|SshCqjTcQ+CCAeC|#ypj(Zt*k?{?w33-iX1UjBZhL?^Zxyo#}KM zq6mLZY6AS>)xp@%EatKjiEt{Oe5S+B1QqHvixtX)4fsLoB#X!kmg`SDaqr;;cgV^a zXivTau7%wT6)6Y)nZ`qo_NYP5AeN(PN32mdTxY8OM>+9&I3=e#V#MqSMIMT3DXC@n z{s>n|nlpStd{kLZ0&6uACTiJGD5o6w%M)!a(#hZ_<-{SYlM}K! z7*S)RMnS}1GPYVl*O@-9QNP$zOChS@0&pgzyRaRuh$qoXMfO%(%3)xPo5D03ZH&Rl z-Y{E!?$iOp0=Pw)VV7IFYVKtVSOQ8fh_J{Gbvoz7Z zmRP2PHMu3`eLZl6dJFaJg{w%;zQ9eYHBz0a!yh?YFt(2Cu{3{LS>}ulEDEru&h*U9 z8aoQ{WWZLe5f^b@UY)roEPuw5G%g>_IyFLvz98vBiiDdah-+%?QD=I?Mpp7#CgCL^ zBn`!sX_=cfwvHSMy8~gKGUz3=QAZuu7D>xa*!tqZ9BG#9O}YOyds_`psWi! z@ruUQFjUxyS2VVUp~6nQlCdHDQTpxVUjlJ?J%Nxa9J{a+uZV7KmxY~pMPqBb%#`-e zv3bD9fG2Q7OVR3k=DDRb&1o^t#9=O5037C~w3@qSYi{)=cmZ7I>siQb-R~HA!%qxe z;U@=2+%i~m%V5ndgEiL^1w5)V-DLy+t&TDkEP)?UV;6=ZC1Y#s%3QrLi*AKopf@6d zOu6(~Cw#&QJ4y4OsW)S`gq!1P?&zzBGZ*AuSz)tWO&wfG{|C{Ol5-)hm5ioXT0kd!0C2M(nHAGi~+=B6cLjf&5j!iKm;UIXOgLS@f@Ljk`U z*=3xNMbX&G$277jc-yg+j{!LgAUZO8rfD9ToG}P@3w{#e@ddHl{X`YevPkbcc>HVV zPM)MagI@{c4Ez#Mszj|?@{d4cqMzoQg~G`bPQL*pB5`>c-$#XY1*_Vk3k&f zk9_8Z2!Ik_0Ml5VIFQU3^&!a&PP0N%V0e$>v%;b#(^WIz9JMnc&P@%i!qcX(f2b)K zT1BT#rcq-K)hak`GVLpKhE{Q0BYSDK6r&2`;FQduzC7M(qy)OkPgmjDo+OEFm6~=X z2s~)TL`nvpmV2#8eC~H#oWV~FwiUP(c3w21^I}Rfp$$=aV7Xyb3<2s4PkQq-yL6g) zpw5{-?b#rgtj>!jQ#m`Rp{Si5##bf}o20a~hBN;BS~oYh?iiXV0-lOpT7##9ol`}A$VhGH+P7zlJZFuqoI6ZS z5I$`@!Bm9|jH#OH!>hKC#r$$CWNf%6qv5Sft>nTwEhMKR*i%XTNBQz z5xGy8F2;#ojc^?nS~Cz@79sljbjeVyMk?5Xleddv3n^+@gm29}Zoh%+G++j<@=V08 z4k;*Wiw0zOKFD3Iyohz3LnLCkk(F)KBdWJR3eH8tG$|A!q#!AUB8IONxhp9KM5qLH zSm@VK|D(bA(K~U=SHo>LEUb{gMiyclLLcD9r^BSAwGOJN940^8Tj6UGlQ#;;8eeHI zNkb(Z-IZ0~4&(__&nscZT@e`x(y@T7@jVO?u?(8va){MhP688?a>}nJVPUEOSMEz% zjED+(8d(=^kmKEgY?EqyA$v6SF-OHEESkKG*P7)Ih>VMu6Z6-}2|tFsY-C}t!I;6G ze1))NhPJ0@y(sr#%j6`Bq78CvIZc5&dNu@gDkl!74)zo{+ITq|$|(ctO;Ojxv_0`8 zKu*MJO|0A-F5{(aezJI(24>oLB$tR74JQ{-P(U+P!E}RV+0$w|DiWhyLXltvuPjP# zUYAWi(`8~RlIS=Y)^`q=Y44R331KfHFSFDQ6-fiLD&=o-9VrCNzARkw5s5_~PsAdt zLt-0Q1Zil+D5pFyLxN1pX?FA=3rG!eY&kIiM^hhT2`ase$jkWmP)<5fkL2XYnAUiz zly4^GGXbzlPY zY5yG6pBoSwyK6$$9#~|Cx}d>7(gop4tSMw{c8n-UoT0n`fie+A9Pr4V6^oz@_>WNo z_l7mx7t~nQx{cgxG~PBUD@TnfWmO3Vn7jxjElI_bms`aKIi^BU8$`T7p$&UdDDr=S zEdhB#^cH!cB8A{z?Jk>eZ&Fq>SfrC8i5`^DQ;kuC)q~O$wq7)TZ!RS#ovc~ODi4bh zhMa_Ct;t%eC(#umr%rDNt%d-KTNgq(t>g*D7I~qZLhv`_BrMV*Jt-%mg^9%|H8l7p zQ(HdRlTYxcZhDtOIhB#30ACRloZqrwy?Wy`9)fIsXOC zR>i9D#o2L+Ok{6OCdDZ-i6T?S#_*$13IJ3smm8Tx-F9rX#A^zR@*7H4hv=%J6vz;1 zi_L^lFdNihN4&L!S;_OVE#hNd%lb--KzREz7E^gYdLJVoGJ4T)(^f+Q36-Id8B^nJ zgMm=4Gp%TP*|L%aL1mMd`Cx+_6SaJjIYO;5*cuYF0viCWgqyU;3l%GjFqXUOVvt5A z+AY$_bBPv;(9NQ|LO084<-lkgnWHpDALYc>C7GcCK`mQ=5t&dcR#KVKHOR5$#LGxY z8hV#8xWZXRUPT_coR?D=VHk1}KFT6JEhjn@ndC(FrQ{@Z${2l=6Dy}=hL)3B0yZ+Q zw(ZzzHPFa3*tTP<2)2<~rftX8QCDG$L_>W5Y5ayalIHBf z*EK@q&D{&60pM%6dx1FXe)k(t0lvusN{jpu-$KV}*iBJdW(wb^6p`!(RX_AYFk4}k z7O^p}@l}bSB<(1vCXxuECgO%KJZiXiBs6T9*GMU`Pn4t#TSbyEc?Tstm9ojpY>+{Y z3Bc3~VJI5=3{W(yJp#xR;h#lbs7TsT>YPrdtA-;m(f1NKN`HF0Bi4wx8ksgQ7&8E{ z&p)K(q@r?Qgs=gN5wViWB#S|gEhob_ARvtCEbDF-AWxu=MP4W;?I-#B`x9Xz*9MK;$H*kkN%9!eeoU@`C%+XBVkt`o-u3iOE$G z)v_Y^bAwnzJyO6;at2DF(xxyfD_dv+RABco7#k)p%6|$KO2Ckz-xjE}qSqx*C^}5u zv7+URKQwt_Q^O)JR3r`lszis#$SG1Qu4oF09X8=wtBs|VD+PlwqnubFQ*u(#H^^yj zgwo1&l|hazCpK_NnlpSVvqm}58YEJqObijqNrQhRr}R38obSGf z5tygtG(;3Fu{t7F0G~x{%*#x#Vh)&xsDS|s0GXnPmKYuq*vg2$`xI5Fl&3ZB$wV9h5;p{h^-x>_|nrB!e7VvDl|Z ziZwgE51gJOD~&XgNhoer!{j z1ppHB4RTC5c}F5i!>Nxk5ts67H4-ktvZ2u6b>y!NUV2lDbP5C}<-~RXaw1l1ISG|F zMjz$GD+tL9Ehn{-Zl%6%JGNT=wNeGP9b2tf+QL?^)?-(SCn|_-<<%nF(j5n00vs*H zi@{sMy+Mv`7wkjiw9zg&R&=VyP);bZAZdYvQ)o9iM=9N|HMO+e zP}dOpB@%OJH$d#{EK#n#p<7Z+2R|!v`P*cfL#MxW&1j@pLvB9p2Qv7r6D$^%3)2_x9Ft^RL zd=C_!2cS;hpR$EP5*aOxU^ngJD@zXWgE_RBKB$pD`B*7Bmw6=}^bzt$ z1jJkOhdQZov1+wLwem1Y<7Fd%*k2|>d#I`pcn=rB7cGS6;#H>8Zl3;-vnT!_bcJ(h z3){uof_5=zq0s!v)N6SB+46i+BJQXnCm$(5xDw51!WFH^g5xi*`|acYMB5AAYh}|{=)NX>Alm?J9&CzXo+J|g8S~sG9Y-S zjw<*`EpDVB?m1^Sk@fC(93pf;ZQ%tZt!R4~=u)@{g>C^V0QU-$0HGV$Gy!tUV~z}^ z)LG4eI>fJWiO*L{1QfP~{V1(J`vgLtVSGLN7`uy>YN7{(UX#!iE zd|ZUt?Iv;sY-x75ouapkRbd;5I#?&*&d~I!k3@B1qIg_iOSgZ7-U3g3T=9C5U+Z$! zt|FGC6e?kxX}uVLN3>f^idg%m!Y-4xWNhV}s+C+rS^|o<>vF9cmiiz@XcqI!X*+=X zHd?#cL@v%PDnm8WyV;Q%$%SF|NWeB%R-RKMUQWYwao57D5v~KZ8VUV2P-s-6G08ir zMk-i<*B9;n1V|y$76v(fXEs0zMg)x%v}Ps)!j*J5bD&NHka-d7jn`PjflJLIg#b}& zqFE7>Fku$n`yo=Gl%k0ixhpA#Ophm(&$iSLXc*b(ow!AiG#Y$0_csLIsw-@i>9ASH z?H4D!m5M5XKZ4Pglq0yI*CasPXq4$hf6!na&RzNQOs>C~X6YbZFdDuf6y72)#F>n{ zA~F)BZaooMnH#@A7W3ekZP0*EX|7;+K@XOQEu zLZ15AiI$FmG?Wt}gaZqjWh5uLY`4e@<&^OUM8-wSiH=YpV!ibTauTdG7&FR=<(;x8 zwce>#@L6bo4dIdp^-ks(;8<@i2~ei*P8Y!L7h86FXsagjuzD&Lq!*H--T#7fJ~ZP!gk_vn!wV1whH2Eio;>GHqtb1tev?P&KWNmaechsuQhI1D7?_N6817SS+6XMU=%5b%iYbZ`VZ-(@;_W{s`Z^Ic){QYV@@2EHg!>!T<;*`;j2 z)hxQzLbA(r&&(QItz5doMxi2}sED-7v@*=1JBp&@X<#YSnpK8G(>yXeV%H8XoK8_# zka{17pX7u{ZQSqV;z=CvyVV=f6*dcXqFHECe^s>86*dbMjjfzOSJ*66G`51+E>m{DZ7JO^H9eM`)In7u{Hi?D&%L;t?;+Y zG#1PnTVoM+7fdU|tg$r~751zY@mph2Vb4m**iZ-_=n2@JSn3{~-Jx1dvwCLSh)xVc zZLZ(%xPhwU76wuqusd!b>9~QUj3@F*riI6JFFH zd7j+vLICid!yDG_(?m;9d#_g?^ae#R;U`p4YCp!%S`z@s5}qs4)|GE zPPS#aR42>VjY-VpFmB8vhph&JPS#i&FB>^lfF{bT0LNw&n2lFO%jA294FJp^=-%29za&OZ9BG#z(Hn3`++*`72Fxx$--2F zz_O|nDj`AXaZ5x;wSS9_OS=`vMZV5>>7dEJt}G=@<-33@7nNiUt4TUpP-jq>Oa@XR z4ROOlKDlFWj6KetBc0Ujm3M{N3)W7161lx0OEL^{{Cv2-73J%`mKoz;ub z$(?$Gr-O(^t^Jg9gVM6lc;r6&wqvUx2c{-MhBEb$r5FY|mikaVg=R6ooc2?=Lchbw zs--e~(OJeI_xNm}P>nLXht*0Rlbly0S;}HfIIBkF-}tp|Zf=`&C@qnuDL31VSB+}K zx%3oLL@}p4z`0P=!g>vIOi?2M#-jF>a&G3{n#9b3Il?Jh)P*9>Y&^nnfvZGHr|ZtT zOe;thDY%r5Upt7H@FTEh_)3wx61*XVO5hl7T6WYsWpJMNOMvrS9@lYw6HQwg8(oV& z{kA~|{VsY+2ctM$R^3@rj(2Qj;XpzMgDM4TJ}_9p$_cEXjqng@K{i6+E%HJo4Bb_9 zNku11GAz<*_gl1w3@*X>R;sH*tRPm0#0Hm#8>5eM!o^w043%pDm!_38aI`Vt8gdc_ zXOQEuB9N2c;Fl_dQ%8iLsiXErlW9yZSObu>3cCF}^>5py4R_lRIrUJGdJBqgDx#Si=7l9Y>;CrnqS9D(g1+z?ZgVJI|RNU(B86x5P(BRx!hH?zrnTY7U`r&K4laU%MK2# zf><5$vXe_3WAsr@>~-Y$T26ZX)XRf%Ek+n}60X%C$Ci`f8&FPTY)3U~;nYD+!bC0d zLOF$w(vVXI%QA8rqpx8>Ev;9sPYlKkaOg3A*%mDKk zVR7$T$Eg{*q+GeVPA)Qxw~dO#)+42*6-lpPdU+bWC8>Dwa=~YiV=5AzKo$n=s7PZ3 z!2LJMZ^)uXi@Z>ga= z*@Qj8N>(duCX$Hc6x^h1kYmef0+`49qxja@h&-pb7a1|fz$}*!7U>x|4c?wv9TA)H z_7N{TnRT-WN(1wFi&IB{wdlATknVj}z#QBGYP@Y!q&zS~{zfa32u>`EiYC93v714T zrATx)uH%a6p>n`X=jA&a;@d?d@&vkApjj#v1wo3~DKDoCFb5aMTBK*>GPqwda-qV;qn%lR=KHUlR_6 zE{{MQ;KmT*5#Eh&LmDSx{>c8}nA?F)V`8 z4#w9KJD{T6DqoGfpqVOe23O}9ZyS}?KnD$6(NbR)%9l+(({^GimiMXBR-J>Pnq!iU z5v0KvW<@GF2b1Z>p(P~UcxiY;qy?UX+^maaf}7%!ax$z;%PCsw%f^60ISI@+$g$iAtr=rt%z75Vkl zdw`n4?L7g7!98xq*yHRCzoDQKxE_pM^|GI*SnDpL<2Sf0&zQxm`eveK!z;gpSv|I1z7%#q716DhFRQ{1sG_k|1iLEifGQeWMP0!y zQ`_=eM+$`NTKl7o|3U)vqYP1QEqeQA-6=zG~Ca$(P8tH%n`x}?yjO6=-lrF z_M|@Ucf@b_Nz0HGU(XZFJ-C%8sSo)GA9l>t*uocVJGO$_RbeO83cJYCm;@qFZB9Vc$;)-4#^) zgi4s{|CmE~1r<7K@T0TL3NlA(a8VMorTJ8lO;Up&oo!Z-Q&)qF)XdhDE9->dP>h}m z4riTNLCSSxUItwV2G86>NH?K@H{l*azGw=5XK%wjgmhty`yP86x^)JAr+ft+v=9Hz zeMl?rLt1ek5+7Bq1tM}VDS_ZIziE`Opt3|s+fh2rxxvDU2G^J-k7wPQat&&k&Y*de zYg8-jf}*39GdzckeaQ1nkH##@A#bk+F9eS+63%otn7zPIl#hO1;jKZ=y>N)h*>5^h7oKzb z6&>B`ey6NB9|sD*bI8hF0xNE3SaFXA9d70RjzZQ@AcgHJbYe(O+8Wg|b+?20`m;U~ zS0hIy20R4eS18D3Ofvn zcn%2=#g;9eGSYW9#b7Cx%MwS^W|gqHDKTWDcPXxZS}LJMm` z%LZ2#3hqXvVUEt-WO_$tx*JT2le@v_ZqZXO9Q@=(2$%eGD-%4YMM%$m3)C3{?uqNG z$w=JqJRa5RF1QpMeo`L3=VRbQPa=1|Qx-|mZ9TX~HEF$D53W!Rut8)|j}9AT zQJF#WOl-h76S0A>XrP@!ux&0PrgV+_oo$`V#3;~)=UgTh>RVyaE3qhJhLF%#gO}Bh zlnt&By)P_#EgM{;Q(@g}+29(T3JYJ$2G{6RSovBsxGu-<3oBpC23IS>zVN*IvcWYv zVNGK}%tiMwqsxjer&XjP)V$UZmhn}Rc^0lXv<&)-H>w5mau=2HRaE)r2v)%^_ zE23PZTH%@bWt8ifD^oKxk8+J_nd8Fe4X#lQtC0$@p@`=i)d~-oFXOpFHNXZDG94W@ zWUA6#AHEW+k7H_&sP!jQ~?XmY0s_aGO^w|zrHW5eC?^WEMwyu(F-eI z%LZ47-WOKB77ecJ^83Qd*RsJiIu%yFmJP1asj%|3Y;cWE^8RYW-3kBJidn$8GyD8X|3I+#(h|Fb5AY0zTN7PM+ zNHax*d_@s)QKz0&VI`59m3>oQFR#He>>zbh9xqYk#e02W$!AYhMJUCdj@;haQ>{%Z zg$ptiIozBK$Jw?aeeUqMY580S2i!bk!HYB~IUi_qM+DJI48_64P=PpltcaQ(dq7%o zDDg19n!84;%t6g_hK{gGCZCGgS_N14^|`}Z_wPCX_O9y;Q+y!9c$zBN^WCJjtCLj%!;d(ojg z46oayH&3%qpTre%ZP;n1iR#%G79ppG;#v!O&Gc0L;eX<~ z%{5-IpeRFq8TQfET+$WzMxZW}oDI+V%bWoU!wDtN-jhgFCnk#bB(CwMi)e0e-KNj2 z@@t(`!AY@{l!6x)RIaJeEv?|nJFN@rDT~Hd?g^$=v}BR$gHuRw)gA5!USFz@zkEQt zWK^GFF|y{$KbovEAfPf6$A@Z^IieJX3j%0V`02YC*GPS0!dW$YP^DkuyW z)Wc>)EO5n|XimhOx#iamA|@9ah#0<7710kbh_Bcm)bK^OLSEJ&ll zXLEUNK;SxapsJ6e)>KTD)U31G6w3&A4e57wDVX*#lcT25?tj#vzbRA^9Gw!6mK zVHJmHIjI#7Fe2j^4LJ$cSzuzyiRB?x2<>UeRIRy0Qp+iDwDGO2p`0?HzRr|x=j4Ro zM@oev3*`j7obMmS^ekRxfLXXi{J0=Z1Youf&R!SRBCo{>vIC%KMN;eAs+T)@mPN&i zgs+)v@H}O$NR)y`+0e1ZS~hY|&>}BXBn`~4D3$5JHP-JYNGD*%bIO%LI;}@q2%We9 zIMDiarc!Zmpj0IsqMwqJ-W-9K&yF54h2W+Gr9$SAG@Sal(xqbGNKP659?D4rvzAk) zK%NbLQcmOI)GEkc%kv)$>mB99056qR%SmN!*M(KhYdX3%WV9Z;4w<}`eXHBjt=F2e z<#XGy)mn31Skhd?lPIv1yW82EJ&j*yClLvf*934*T%5M<7D#sG{DR}LT!U`Q~}A~t5Xy!F8%DD5lh1S4|Z%usH& zSaSh)K;T*y{upl?Y@5pQLV;@gi-KcWa!@vTS)*u>V`8wVKposT>e?7|aS>rKb&!pS zN-Xk1MaucgOd%vI5>>;8d_-c$po=RX17g>O#oKEtj||7xa*BduSzuu?B34qlUtBCF z!#&{CIo5%70T*=!Q?GhiqG6F2$|>h7W$l4QI>j_oa-uzZv@fni4TxPAR+F#&W?sry zs@QWa^Y;ejgRK^f4e^+wTf`&Vj;(#X%x!Nww)XKd(Y@{1%EyD^1-Y?!RTkTY+$=W8 zvGHpn)~B)%R5dPYCJg{z$wwrwxu|)~Ma_Xof&5Ge9NJCJQOfcHYib$083STc&rtt> zSfPNHk;lBwG-uKML6g}&DzYI%#`qV)4Fo9oNsd!spc*45~afE6gVP*!WX$zW*0h(iY8D_;bQ zqhLy)2O=Ju)&s?6bzRU}Zq^tSCO0Xml!(lZxDrsfmT75Y>|yqF)N#@iLqryg4YQve zF3SA9L5@cpxywXJ_>eN^i0Nu}oH2m(Wu4>t$_qx2y|0E!I@a9KKKMpjwZ>lW!0|M~=dL@Zm9Bi4$kWS+B$5loL-{>VdK+Nb?3`S3xytp`3(d4RUNb*`Usais;U`LqU_5Sq_JC%7FS> zCI>CjlXBu}N+K3n9q}@=_%(DsUHi`K$q8WQvQ8q1PR$`rgw;Cte`szNEqK*JXU2LG z?4)Ytg0fw$cWcP9(|xpE)$DH6eY9&??`6p(y`bchR^$Q2;V^frB17g?jMofC}OgS19rWTMxZ7$#)clHF5U&~B~G4?onPMM@;uToa)!o&C1 zbU1p5)f%4E(tIsf=G)S(7QSn_?%j55z2Yi7dB2D!YQ?pdOW1AcRw4dcF6Oo!TZL|G zxdhvCY)I#Y`YqRE+m5YVbQpLNh9Q-y#!iQPgZn5MTf&Q8IA}{9gbah?i9m&I$5wE*!Sd{B zAef`ykcL!rADvx79#@Thban}uoM3DKw4zMQ%-bMIt)^K$6LZIO47b9qxfO2Bt#AYZ z#}{O-?k8nJ$PC=?lyBRh({aC3x?sb*Qyc283VE|BY#Q4XG>yRl0s-GQnT{5(i$!B= z%*wO}%^4dOS&DA4woKS(3EhzPQe%tt)wW}6%ql#zzKCv(S%qiT7mckktFRrZXl#vH znX`Up>50ZH?EjbsfLUWJ%-Ur7QD%*;?Xs}-rikClE&*$WlSqZUvu@=kQl?ow6KgSO zb%_>&Kfp?A9fY5ZsoW66&W{as1A)GxNobuCI}0gaWmU#&ebu;}Y3mquisz6><3aW-w1vVB zmQ4xW8kaM@0<-AWxSVOBm^HS-`@o%`@V8h3-Z@7(uSbJRQNnuCHhI%iO`6x*J1UgvJffgLu938(i4L0-m zO=oT(zmXLYe+t)2R#mw26R`Hx{K;&a(oW>ewfiPh{W_CBxMNBDq5NhrhfQIZ$flr6 z1abnw*q}c}JGGQzzVf#7`{x$R-Q{`(rGoP!@Jw&XOpalgO&p`hI+)ca(;zW;LaI4V zTb%ZUg}oq~g0U-oD)lB)<~@h-3evIxS3uUn&{9|t9zi6_Mrl^l3n_gfU$8Ggiz1R5 z#fuwmIiY!LTqx}3*c5bJRFLcnn6W8r=O`OodvAH#k>RceF&0s-+&73(Onia!5*ss+cLtoj15PqD-;!yupqajk)-UPXmE`^g%#eLqQP}^kSV4eRJCYu4Q#Sc zervWUU;{f4QPV}+0nTe{JHV|Gv;#iMeZw{28!lYma4GMGk8%&yCsQ;!3V{@UFOdGwBh)m)%JH(&R@Y8eG)yM-0(0wtS#s*u3Thn%Kmv;0ppyEb78uj!bV93>W0^G7Wk}^|U_U zgbjKds=N%De&qsMd5L(4D*Q2U*n`PJ(h?TflR`F8(Kw~vDm;X@i$#{j!>Gk%gKtSp zoZH_533G53Nf`6A2xVMu71%XUCn6<_x=_NIt|}N##75aJ+l_3CG}xY(FeVzDgef3p z2~+JtIMHQcVd;jd&_aDu2o?qDa&zBeq9I~|dDhG=5sy2>gYKab5Ag`3X^lP_;EvF4 zi@Z?AnT{+NF7QlQD;D3jW}3BPbjD7`IDar)SI@$=f}xCqg9xpO8gdW-XHkc3wi~MQ z4s`-qKROPA*afLHEKj(oF(hzVSZxb}WV@C2th7)qODa03l3}B0_WYVW(#Qr*_#@ub zkhD2&(B7mI4}6C#Gn{^K!tHj z?xt-!xLR+57H8sz2y&VGv$c4_`_Z|nq{XyT-C*hKhKEN~9E=j`B>EQf%sz?&TJ}{N*?%{*?$N@;nb$n9YsKzw( zstPpqUJK}(fMiQ#1`A~B$RNlsm_jEJ5b6k1?W7@}s@$;kw;MLjDO z74RDLvfVfd62p~JE#(T-c)^H0;u?S<0M+t}*0-{T%i@M5FQ@x+@j)E>KM;rVVl;O1 zr{%7i|5}z1Srmry%6d7Ow=vl1z1)<%d~FrV%L0L=Tu&M=8096(ln0h*dFj>aQmzNL z9$hb#mU4Nx_2^MVCF|ELNgBE)GZF?tHa1Yvdu(e|G)CCKg~*_Eyhd#SIfrGc1WYJ&^Fcrgi zW!1~fne@U+7$6g|AmFBE!ThL4u9__tn6W#Rs~ehzO2wLHp+s>G+z?>`a)Lfee}AdK(%15?Cq| z&R^tcxMVk+Kb^>&pfHqI)(asa2_Kr1mxyw>)HtMfi3P^fFA(x#Rh2B!@`{!$h1KxY z(ue=4(e;{TDPzdAuH|Wmm?Gp%a0a(!d6upTI57yC&^2BU%c-NTiHMKOz=2Pc6NL|E z%qq({Et*5^%etUUgNKiSCecE~Y$RaKW2{F^Sc}19nWMFMoOVIJ01~|-u@HDk*;e>q ztuB``95LQFnuw42q$HJzfRj^dCW}rjkr^zIk*-0|gqpE1Bymp7sF0E%-Kv&{Z-}IZ zMPaB^Sr;T%5*GCgY1H8m`Uj*JV;QE`N|P@$mES^MhUIB_>A;!}?%tB5g$-^N(*)Vr;KRB|*F+|Z?>~Sk`+(g%I6XLw+oCx{ zi>wRE^rB?7PXufv;0S+(_F42AJkIpWWpZ&?+Krp_;BYAB2d4U@9Em~^xh$;jhaz=G zM=42dBGD>H)`J(aK>CDYNb{~n5{Hm3A|0!aA0SPbnMGl!RN4jU1BRE<=UUXW3-ZB2 zL@&Ei2WiCcNM30x994HRa!GO0;UQ`ZBE|^P4 zRZt^K=`9LFd1)6E%S(7Pi~5YbsBD$#rMyE(?^5cQ@d6<)R#mAIT3&h;6x<BVnwYe69kGCe4E zY~YPY5{F79Vgs%N*Fq8?Xi*p{RS4Sf+h!#j0js6Nl!Q`waEj;^k$`#M#j>zn1SsLD z%U39Q!IDyQL)e7H4MSx!%`p@5;&@vUhw^go=dgoo^EHJ6b1Vu&d4-^D$xGPi#mI}1 z);U>XxZ>!t@WKW7a~?{vM9WLB%a?_XEM=qXRphd;lcj9*Xw6gD%2GDEu1{KKs_zA^ z74TiJd6t<2-RF(2L)~TOr2BcJ>%eST*z{4xcO6D73%fqbM%VsiS=jbbG&&@xG`3^{ zcnkdm99j5bhOE?W6voqtej;S&J4`AvaaUEsJxK7uFT<#F#{HA`@!T5%PtcLLe?mlc zA5hQrlqG=V;WUh?EI1YZ!(s==z*FuIsqye{(<>@WEP5>wZ52}y*D1`SCPCsRl+=$nLT9!|6uJyrg2K>8 zhcVT1x9jULrpoj%`9Pg6h%o3Cxk&5qdk}*Ql}ch@e>+`DUfPQWmwyzhZ05-P33>56 zC2TVW(^)w5U9f97_ zI?ZMV2J(TYY10`2&tJusf+W4%pc`T4K~k=liXllfjlm=))W-)wIDOM*WISbwDD+P{ zp3sTE;If-++ZfY5En4augRhzH*R=+6kOUv_E1=%kwPM}U0KCyGtFSzN)l9K_z_?`rFO+_;Nu5)cZIzvSwk5Tnz z$Xsqg_+o3MZxKf+p>{?TI$kCy3^g+6&WMnN1S#hNyZ00HvSg874=1u@9K<-%n{r8@ zCGjg%`~}-mT3!O4EGUkh2E8XSV{1VK@pw!eYB2k4H-GNw(iA$*70WB#I;?951`ggILjhW3?1 ze9($f{02KY{WNV~3}UEXXloI@0uL>EEejATY0;EJZRs^RmS6*cbFui^hR_PC(oQ>f zu2wFLfclwmbh?%&VPgglE~eA2n(DOk@h(cqM(x1y#~sw6R%KDUtkbTV>fG`NM zYL|A}!BRFaFP+8;ZcZ!IS$Xn~rL!h#PjuSxwN1E?s?1Ki;Qlp>!cbmW)Gq6^o01n_ zFGchc!<7Y8rJZ(FL8l!QQib*v+!$wZ!?3T5=~k;Iy4CnvEtVI0+=xQqwJZuld1X<% ztTAm`ULqI_k~=hmD%12gZ*;w`s!Dqt-La0jcnwO? zH0=Vd4QD=&b{!4U<%)ArD?G~`L@EL{gT%>mXk`^KU-L+q>pQSWa87-=yAnQFF0CqU zTdWEi3!&s76an2lIhRTGrU`8eUp*!37`buGd*3u6qOqn)kM?KaFd^l!KtfG6DTt$7J*?*lUp+cSAQ1d9&;TalzXhG zNi$?Uo@was@vHnrezbAnygLAfnroHm9~(t&^^7SG3#Ff-sH@U;xvFGz?P2MH!mKF; zij-?OGd3euTg2WA(y74nHS7R}i z>8q#G4e-o{ZlKL^I@p1?uSj46b;f6UvrI?UsE}6Ah=#Z*RF?S>%d`z1;Cu2485}lM zQAec4dyo^yfsP~GJ))!}Q%V+vX?LdU`iPJO^_&Q&R?e7SO8bxnaVbeiZzX$-lK?{f zeBmpUyr48S9Gz~Xou{*z4oM$!>Pn8x`H)9VJI){EFyX3+9`AL z5*tls17%fspAe|0Vboe)(Sk@ekfybvVDxA~RN9O#lF32K@+@p{vq)H%nQpZ@ zJyF+0I*j|sKnlDyU778AbSrML_G$kWYoGAfhWPw8%p_W%g)my6fFpYrFDT@3rfomx zgD4-tF#rYCz*X8Kf)Az-D(%;+3i|b+GFW4pP?|y>Tui@SHPNrfHzyNE%ziz(Q@L2H z^pYylsUtr#+{(o zVr{x?Cg&U|x5&)ovTC+NSfH4VM^$NCVkPce8q%FQfYLh!bnWvz9%Q;OZT0dEMjC7= z$x0mIjQBI|p6sS7ka>eaPWk#3Fh{$ic08A-z>;vt&i>eCY*Bw4jvf zXNwz#&SrW;C**~JN=Tdw8tFVZmX}Wg1Pc^-@}5OuC@;N0iRC3CI19|!IZVn69T?MV zIWxG&eMAA{1tWa>mI$d5%D!N%P#fsl^qKZ0ysJUbguFQQA&EnIp^Rw$)RPcVD7-~p zUK+Jyd5PgH>T~kqc{tQIJ4fi^`I#KeRC`fn)nr=9?_(fMK9cucxs}<23kQ}KYL&mn zOA1bFm0tF4qP=&R;qSTDsnXDnAWN&5rYRA&f`JhRefP{d7`glLkB4mYMZz9ZDj5%uT@#p zMnV!2WYqTE6%ta>3PdbG3|CfAVN=TV3xvE1P&-P*$Rjo_ZWt(#Y3j6NH><%D=Z}`e z@-k6d?iE=ShA5ClZP^B3fT~A^D-XLd!}K@M1k7s)I_ZsV}aVd6mKJs z*t94NwMwJ5zJ!VhNl;H(RZnzp5;fqPL4wS1#nHO7SFkR)6c7BkmX`>eEpEg*OYcd{ z*jgLa6TO0br9IEzMs2ayuqX`WrBOSUS5by!T3$#a(~E9WakQ2dZ-!eKylh?2D+u9u zD6cxxiZyF=h?Vu|BFDQe?cn$H=x&`dtvK^&*Q>@l(^@cZ^e7sYTj!P*T6S^~-m;$P z(cI8ZGwB`oXhOdOC&g=OV_d)!sxE{04?ZIlDj{s-F1YF@&(k(;Q~RH`EX8E4uwyIl)4{#RXfc+bt< zYPVmkSGzT!1088oubSOf9cjaLU(@D>eic^wQ#~ergWAOwe2WQGg;7WWyDu&e?3RZ9 zOg^|*o}W;_>sUokGzz56Vzg7y_y)?%Yi1SJMhGSgw2}x`GP*{vjoj&<7PCyf)S0e1 z-MKN&)I~GCg2AaG_sl`_8?j!oHPfp$veg$&K|DUbOtCcrS6O|NWNXRjI)PMYx?ASa z9znOP7nv5CvQ5=WQRkZJ_U}3W_XcqqMLH$RdJ1$Z)0$VJ zQy4jdj~uFNNT&!)wN9m@R3UX<-UXahI;GcS8w`PFN?j~!4K=i-K4xV@0P(x|E=u4+ z{&4;qS;mnUwhsDZVLK)d6Xrm2vS+52H4fHfsgWWDAXE?Igzd#k!uD_?!FeM~fULBRp%%Hwa};tx?m`jYvgFC^AMri>t9IcLJtxzEsM5Xft6&YIR;Au?Y21@F*)B3iS{ zYPnL8iDnAd8jJ(xePm7HCj3TofTP@S# z27`pi*ySWergIFn%wlgpyk6CAu<9#eAqJGm4Gn{$2_v?gBpR`Zowyecj+n6$l^a<* zm{*OgpU8Af=2e5IgsMRvF0|&2toE`9wwUW?J`Dib^#s*hk@F(pgj}t^#;!K6v8!b! z&k*RS!PHfz!aofN5R==-UhZgfD$^ zwl$>wg0;G=dCQAT`-xa&88?>|8M<>88S-4|SS|BhX_2wI25O;120J+lYh(qZ#Sa5( zGVOX3R%UrIXJu45NE{e4YoW+Qe%2x}v@(s~vB(@H38>79Or&1;#xTuOp%fOu7Ei^} zz>snlzkQ7&D!xwxaknLyg6U>tG2}FvYMW}t7IMA2?ACG%d9`eJyI~KxU-rALKBtgY zG70H8JV zS|)!DBGX%6bWA*3o4J&b$xLH-!2>I)qwyxnU&wo}wjW;SFKU_L+kSYxcCAaB2W!#n zIeI9~9KFt}WwL8~>Z3J*Ozvzyyj~O3G9R=3@H!r>GyMiL6;sD9wM5%Z=wxoIC&2gH!tl=NHRexc_Ts z?FNZj)YXQ6U26X_w;Ig!TT2Mjpe6El$@m$ymTP^VUU83uc}TC>vO(x|va z!$*)OS9#l0A3>g6p>02W1bK47v;FW9h{A&R$F>wTA66I)i$MlNM*xot8K7MblM5b zgEZLh3l+1$PLU%UUR!Nbdbd~+b6`OfQooU+P5I)Pj#nm(4TYTRctvUs{U7>C?_JTS zxF?hlb055SrQS>T1XnNagZHk!i>c<`IQS>;x4Ewgp0M904TV>bF5w5t^t z?I;CG(TF>vCRYxSq1D5u(`bWAmkqDMuqkZ`DjQydVS^>^N5&ith7I<;9NF+144cya zn=JR5W}9 zVwoz?naqhmOdeO4jI@fVk3cL_VPp1g1e z>^Zv4LAb#N1wb2X@MpN7BAHMEk&}j8e$UdPl@Et69J8>Fuwt+huvD2yx)T}{!LOLO zMGVxyy&9o53@0{BVa_dVd$^?iInH8Acc|2uD0`h=-Ziu%MSuN!y# zYSFI<{qnEjwVn^ue0b)AG9QYyezx4X)v|iGJnxq2Z@I>|rLxUMOY_)ix#hRzTBnw4 z$y%OA%bTz*Z8j}hK4GxqrEJFw*p8R29fz77hl+6S1@gCJadb>q$0gt$7t3~ZPs(D& zB3*Itx#DqFY~w4wYGTE5Sn*Eqij&tponN$oDk*$#Plxv`dR{X3EbpH1V5jFI=$_}_ zQ_1|I=lS-W3-37t-gCNp&1Y7v=`g3on(1HjO(ScjYfW7fi#2D*`6j*vU3hbRXT$TP zvu}?t=uB1jlk{lAeA{pd7M-{t-XPp*Ui!tabp!w0C2ID6s%{|{apS2nmoZoF{oc|YJy$M;;F zp}3^eedFne=lAjdcJ`ij`r!Hae?UwH|Ai-4!TA&P|M0AV|L*Kv3)4Jse*}Mw)1d!{ zFYi4K{_m;qU;YQEu<)&&?#tJ8@LzZN9k^1GV}QuaE#~dhFWhnG%{$A*%?lD0S*K4& zv4$YV0|XK>vP@a(1dE(*VlAN*!-7qp*O3*L-15NytbOd@88F!1*^zs6 zBg#C+kxO9z%RtBYz~;!-%~joubzFVQNXI}&IJe`?mbBFyFY}7#&Pc#JVZ0BYW`<|r5&S*TyPd(-u+cE4!%OZ8!;&f^SL=Os(- zo349xVUagjmhKQzF}=)pzN!*bf&_5OP6Zk?95Up~LwyGXcZboXX7Co43Ew9Lzw_G| zEBR8Uo#Ct^t}?AyZ2q2$;Rqw&W$sIcd|42StD=IRoyx4C({E++ zzNJ4G2`mVuz-Ji({t(yvL0JA&FMrIJKisQkAMOtV@(0NH17P?Rlts-^uRjT*=0L_b zH`lE1HOD43^Vv6=aG%Le&HSW>2#7_vklEKU*R1$d8}DKj(sHi4gx|@axB_d(7hw7N zE2>n4zvH4SUvuS4u4?w3zTnEcgPQoiQ#1eFc~~yy^83two4IDo@_WlQ%fs(1*Iq7c zWqvoADhG}){1!5AJ;3iIr(1Sv&{m&n_nCH|U-#*CpHy!+LgbYC!n-h^MB^OVj?bL= ze3{RZ`K3$4QuNCdzb5hP4q9LU)g;ZGhOMMwDK=~c4M%JZqgTUHY}g7K_P!0*%{7c< z4Oh)IY!wZc!8I&}hNaN36*O!G4NIZnh^1j}w~T2mZ`8LOVYM6yw!DMUvevicBNi=V zSIgMd5_avhj8!c=gO;QCmNBXI;XGp!oSTMP(|VV{Sk%%wZm0DDKU+!55q!(&)UwsI zj7}{_o-OmeWtY(sI_Nq;=*dui;<&Hg3$I|W?^E!6^9ZSC>>F;zb{f?#H z@j9nt>38gz;Iuu|ijFa|<5hjfTF{Xe>~t*sZU__j?}m7RJZ8tz?Rb6Bv2;7e%8tT? zosQQ<9m}<2xppkqj^*01)pdT|#8%d^{5rz4ofV(Kx8jIl#m5(}SZ*th99C>)E0))a z<+EbBtVk|9D~=FWERPk-Va0M-u^d(`hZW0V#d27&6|P7QJ1e%r70Y48a#*n(RxF1V z%VEWGSg}>ESRN}{|Lv^UI#+C+E0)iS<+EZdU9pv}{Q8h%hZS4rimY>I#p~M@Tj`2X zb2Ws1s?V)xU*t;uNY-ljIt{Z7gucGE4J?y>%)rmVa58eB0bny zv3;-DzE^DDE7p${$0{qf^A+pMiqUz+db1+?-&t|^x#IY9#rm{jJXmoN){1@Rit(YR zSO(U|yBw`r0#| z^t@>3S$})Rm7evtCtTU-S&w_hm7evv=Y>Vjdfl^L_pH}F>vPZg+_OIStj|5;OV4`U zvtIYC*FEcV&-&c6KKHE8J?nGN`rNZV_pHx7>vPZg+_OISj5|H+bA zjBh>bb4h&04W$tyr`6tXVtOtPN|}(iSHZ0E#%agC5U(iMKklkYW(q-)KcZv-+j4uAd=?$X_UGNLv zV^rbGTj6(FiEbE8_*%#X-T4ULXEdSv1R-0;`oj0c!N1cAgYH9f-=};E-R1?qv%YND zJ8oES{GBCqGl}~i)6b|uH!8q$)+0WB9)9Qdu^#bh)9@VserLm|LB|}!-?N@=7&SJG z8XMNTA)Cnh=g)oIkXMB9m@dwh&0)Z2L?gOU|maI9|k{uF!;2g)2lxGK?_%E|sKw5_I13qwa zYsvlxp3oHF1I@Wwvhd-9|0fbRjLdjZEm^?u!4Jre0iJMfZOOugWRQP5ixHmqcaeZ# zWR?Vc@c+cTua*qq@WIcNK^&g=x3eJN326#^KqkP*EQ@N%kPRRFycj0niGL{r1SGEf zIBfbA$N(`IKHzkA-~;m+KJh=X{Z`|)ix&ZO74vsI!=bC#Kk?&`lZTPnyTb=gA49Yx=KG8lJnkJQgw@+8tl$YU6GmnS3LpG{>_Xv*pZg$d_NMSpEFGW8O7um*Kd}gWS__`AUQr_e zaCJtd>CSeQrtmJWNV=#DM)s!dcP-(GC)H=FeYy;GhM24QDgYRn9WH$E1G3A7C!U!c zw85?xt1;EOIR3C7h7T+z&c?yW9N5DL!tGmjdQbufHalzhOZ>3$8StH3WP5Cbz%F;M zo^j&#{X1cQYX8o|3+GPm-w6Y-3*5V5|J3Od?s@mf~08#E`-ul3vL5A;02GTjh1Al+@;RWdP?E{k0DM^yA zJb>M&y=xaOZf@OscAxgGt~xw-YX98Lj@IQZFrky@Z{9S!j*)P^f!lQj2-{tQCa%`I zaA3qO@UnyRuv2&EW!FCC+H03f*q{aP#Qy__4hyzrb0w6hz`wcwK|*1Jb>T3Ax7z8K@3t}A4J$Jw3_u<*<+ROK@bQ^(=@+(iCKYa*WczfS{ z{HgALPk8S6^Jnk8bZ_s};mJF94-W6zKXdoF(@<2gd-`ziiEiuc#`EVcoIFpPjEhac zDO}NA9H$(M02v@T;dldZ50^nWR`qZV1RUWJ@k;W-&HEbopZf{?a6DYO_{aUk`MT#v zH|J10&`oYNXVwmw=?XmTgmN_1<$Krc-+BIK)a||R`QCqyJOa&S7J<%%vTz1@adbK6 zyU1V^#%9+=E>prVXv4|yT%0uoO0yP(f?hhXmV{n?;{1s-hquE%HXsAQ0mXuDU;Flb zIxlqZ#?uG4pV@a0cJ_{)-9I2LCf+Reu04JJIzWdS?D^P*^Jh*U5UL>Oy#D&_azF{r z!|dVV83(ss^7La*+}XR~#JT4`_29NcuoL%j|I`B1e&6Xo40-kfoCtuXdo%nw0^Tiv zR_9Ng0@B@x2t45|oonM+UU}jCa}Uq4)pu;UXtB<^okcsX1q*jt3=CSf?zyvU_YAch z=(zga{)zMBFs9MN*Pe0i@YIEq`~Gz;41>eNxcEvK4u^MSxc~WA!*lm)hqewln%2J( z;NqGS=l7p*&84u*2Of3_+d<80**|Gnf7^2TZ4M*$4^FwM?%V?<0kDJaUbO>b@qZRj znF2x5Z3}t^`4Q(l+`-dEf+rPaXlYCTcbNx z&oJy&&v1t!dXSg%1 zo-N%%Bz(5S-4pc;Gym!tRz2xwE+kdYFn<(2gU$GuzSivmsqbOsl6rfGz)id0?44=XFY4na3vj^e#U^_ROM~C^Z z@O!Yq7|jie0wdnTc44{0yr~!oOW%?ngbtOuTxAyMF2!?oWr$VACObFKmoNa)ZsFh*uCdMm&Jcnn=eX zB^v!L6e~vWfJDE-b_jmJ&1s&lv3?<($GUQPZS~BsUq}N-v_Ju5BwvVP;`ibXwR%Sv z>(Cl^$b#=d8qMVnf@e!eiAL{$bZn%jU>#Y(8?_$5 zJEFNk?#Jaf)ZgMMPwE-&cc^E$QyD&km`(E@qLK*fAlis<2r^SH)27D5-7@v;|10cn zTJ<`zGYrr5D>Uh`EuV9wBQOTZwjiTGXBaYQ8aLe}-3GSWUoMJYtxwaeRK-M=tR5jzsECQ-aUv=#_0uB+7<7(|DMF_jAP%G_U^%2 zE=cSCd(Rh2cBhf!%l9(tB0GJZ@5FKD*(>HVv;mv%)`@6UQPydm)-JkHb$|4B5Ny@z{TS?^!ZaZK~rlPuuKE9Oh|mOR7U^LI$$ ztC!d4TC97J;jhMJ{3z?Vq zc|tTZj?Ws}mDM@po;vThSDxF9yRxQ+RvEz@Kk?uDILW-bS7H-;r=gS=lz)HLHQ&AB z24tV**T&Y9TN^*hO+9h8nd2&h7}|H?HDQkRFOLQdU-|gniSf_x#pgVMl<*q&qj!)S zpZ$COHa3mboj}8VUa6wlzo+_TqB4=4zHPGd*+xB=*Pbt&{64Rupzwi5vpz*d87DC> z?@Mgt+$1Ju-1{rcbzh4_y1iR!r(!FUe$f>?_Q+yks^|q@^X;?bY4+ar$a_c-TU{Yf zyZ8LVl{s{OH%HkyikApV#g-EJij3vG#V3*LT{-k>G0STTo>mqMs+~NC`k*9#JeK#?;*ghh2@zuZrxeF)GvuA!F zg*{6d>L+vI_HH;WGEuZIeCo;Qc$x0q1G0GESFK?%6j8nh&tjsiuQ-DY`pA3P^qtoN zvj>v>v-eL|R^feLEP8oJ74c$>jF;!gmnT;tb4ZZx@6|sinYzzX<5&MOW8<@1O3(NC zirwDdO>t+=h%D|LSC;WSdu5~!t+pd69!%>=a(U)>sjd~nDBnxW&iy6Z^B&y6?tL*b zn!9@i(a#)AsnrXRv=hd-@4*9m{E0oD`zB#F>l4e&dq{Add0A^$b?w*l3PHqg^b%Kv z&e!9(h*#$R&=@uQ9tv4!Ue8|nh4XCfFqI;GUvfNrFHJLZNWIKnNxRHmflG2mu%+B{ zSWxzq8=v=my)uOF@5U$T99QnyjJs-AhE_~Q%|LipbxUP7v77f0hseH2>sxuxjnCb( zy68bwH_>@lY^T_#vT9*HwpDXKl`{7gSBiOJ=M|hV7SKjp@lB`()(8^fLi(VDMh@HbS z>%P1Y2+JGX(9SD3n{ii_!~J}vjIWH)^B}U5#Ey>$HH$x}KSKAE-JU&AD3UW0ug#r- zm*m|Z#0ZF{lS6lZH##8q2*%TSi`P-{@xerd0`~@!x%q7nuI?6}Hb^g^ouy(eT-W z7e2(E8;>~HgFYL%gL-g8?z(QdGpybCR_gyEcipz!8TEd#JN5cV+~Bz9d%nEY*e@)? z=oB@4(G_+jKA3Q0l zK=rZz)LKT?smIJ+eNi;H@&LLP-vG=NyB)3k?ATTwyfBqBhhnenp8~Y-z2d2yo0x0% zRGLE0^21fLbLRtYcjS;#r>svoQP#&N$-7ZobLIzW-WbO5Q^0q6X0^qUR@l{I&;J{NzR7 zX&kP74`$u^jrQQGAG<%x#Oz*ORps|_QX6;wguG&JxE--`AW*>s&6;z2?|3`MRf&CH z3%Q#$3+*iLs#%Ua0ObY)Rl6Lyrmi`5r`}PqF-fano6Nf086qrx52zrrjz*4-5)lax zsgI7lBCetv)k}vbnWC}LS*+bZ#%TCTV}X@9GM*x@Kpy2?nEBorb=a}d)nrG1$bO7} zgcjYXj(cUvI|;&(lP4USdn6|&eB~l#4<6_vb>Q6t8Kml^rte7}!Rv?@?Uf(58iyG`6C~wVL_yMI|b*A^bujDmr7G}?y z9dFi5f0TCB{@vyfc8<;JCd39-3*UK(lJK~)(A_H`o7fh-%kqX2$o?LnYjnVI8+ZTI z+9wVRxyjwPQ?Yl|-v^(RrVwoXuwUMdxw~h?NnXh-Msc7RCBq>+&OD8L;T`pTnS|ku2eMtbvG2>V&AXig&b!rT*W)01S|5bo z1vY=qh|!RdXPGrq${km1NydBPmrV5Nef<;S$4k9T?t=ac<$KtA-i<*X z-Vi>|IKy(5@SMEc1FW9fihU&~D|QKwDfkc$kDNSVgYdYL_dO$SPkEBNmG^jnx9b!U zABW|W{loQ zXl!)Ho4X}_H#i7dd;=0Y@nr60>=I@~@T*t2&+6`1xKH+q$saxtch0^Dd94nWN!+v4 zye@M{a@c!*V9dP>ubS~LuSrFW4i)_^zbG8d`#xRWrfxbw5mC=qie}gHnwSXhJn~OR zlK7@dMuSU{bdfJ;!8~MD#ov;?k$cW0j4cXMh|hjRPWJRT&!ZP~Oet@OCFI;-TG=b$ zT<5qtjNSMC?C;Ps(xb0=1PrJ;w5$hi%E8%4N!XsA3`fa5}(c-u4xe!V?TP-?^(XfOYL@$<8&K|t&Oo^zQpTcFWa~wC z>KYW;DK#Ln(`n>9L~A2ExuKDrr_5&Wpv;uWPSN<>RZ=`YiT>ZQ;e@n=6Fn;n!0nD76Alk|e zVk7wxGPmOUfx?6P$W}g0RczT^pOtez19j}Em1l|x$C&9kigZtqmS^B-J z^Y+jr4BYq6vseGP8J8SBzju9=W5%V{Lv=;40>b5Lj&y$(4mam^GJ4lW_w3azbe?_i zz8ah7-A)qezONYl*g57%T;tI8%+Z2ppK6i^^Cnh#vJ^Tm{W15~?_K=?@9!(VpK&L_ zY2QtlE_({*$~Z{`*(;oMou^{qZ21eh_6f3WsQ11dfENcb}WDf+7 zvj@aW?uQ^y#|4iSUYf%JTCp9?~%NJ`;z(qGy(5@4`55)18|Y|D2AuKs^YZ#-qkn!eh0y^ z`+E%h+#`ne;*;j64T6&QV93UXJE538Uq;v7H~w@m%i}w*96#9+(KE+DSRH>1gyo?l zWcPeMoAq%GqF3Ggjh`P46E!O`^Xp3^F4FRlZCr;Fy*rcjYoMl@u>5<-P6XS z1T+&*wteZt;`f3@qE%+w`o0_86aE9Yhfn1dWDg{B_SwWpt+}#^Hjll_g^he+BE?rz ze;yqr^jY4J7?kz3KE*yK_K9_`p8j`!DF$VnqNL>w@t*9J4_jnU#Vf+Q;F633m6isC z-+8(FS&L9~X%Fr`a5Hmgm|a(iMk*yhF7kE)2hs}gi`1B|=A&UNEw)ydI4Zd>0&jmh#o zZ=rl3`7QTNh;;2W)dA7*tc}Q*1A&Iu#2i*eh;nXR?Woo08JW7V|8y*T@O$amN_D3$ z5K?-5IrzS(XRA14&JcZ($uIW9W0xlGKL>l+TYz30Z~ES~{!yMMf%$Vpc# zzMsY?&@}w>uvE@~V1_PD(5xb5y>hDnuIU}%qXciqFI)(XTgUCF}{`vA`WGt~88A}qE zC)+!fIgZ(OULRy%c_kFxeHLR5--CCv&+OCef!d7Ht}iRz?}B)D_N8@v-Xr<)6tb+b zeK+;kE5mGQ_ez)Wm47NYBAaYge3w%{ojnjs$ormv@~%$^vb5BfaY-JWuVl`g2f3T` z{g`^IgDM9%J8gQi7B$s554c6<<=U5~)#AP{xf@<1cS|eY4kpYvaN_2Y$Y4pn8CvSC z&>HtsKOOrD;t_k2DHWSRgf#f6If6YXdx~!$Y85<8NmuSA1UWJrR2rL_)ZLs)61F}L zNHu&)Y;>R7=Z+OyImG?VeN$^59V&9NI`3PxH!EWg7PS1#iK-v3s=VjM5L@0AdFojb z_-mW7I%DU!m6037edLBJmiVwF-R6F%C`=8iMhwL*gpMP3rQO80vh$ID5Tn>;$1L8v zs*O^xNLkYH!FX@6H$Wrh7bZjY)JJDR^U>0kyHbV9FEENc3!_-su9Z||tUBx7EhhTP zx|5Dn+5_IO%`30C2Elklf6FgitE^d)fB5u4e3f;YxUO`mu32tM_LK?;jcbs3Px9r+ zY!WeYT$WK{+f;IJB$3m3(ehfwYmoo-mWjT85F?vS(W?xj)vQ_v@`v^ z#v4_527@}L+48-r_xQiUV6mUka({X8@pZM+-+V4mu7vb`59Mkr4^I7b$6eo}yzfhU zIvoua*>u`M16}`Wwb;rtL7DM?S zbl7p}!ATiG6*5Qqc^H>|+-QwOYqK3a<0kYhh3y@r@U*HKGbok8DP8= zgBM-xS+;+ZXNymY7*-b3TUj4BH0x78*l~~MNN@j>wH}^4q3zX;+_%Vx^O>YQbEd`m z&c|k5T}A<kl4pX7LiV>wDy1^XW+!UEd0gQ+Iz4 z3keQ_rkF9~?99^il$bq`ekz+J<8nsECAStWIgo^SzLkfT9QA``++87mD>_oKo zqd2KTBkxbj?BXS8?faqrWA&bJz6DzBFtpZ@o}Aw0T_v6?Cw;spxk8fb zl6#K!;5$E9GW&<`_dH~)goju|8;5Wak|&6km=~H39K(~R+%)r2m@DhfmrJwe2j65k z?Le2uX^-3)T($m(Y`x$iNc=PNv`HtDq(RaXyC@xhtP=Z2|0(TuHRV9MCFX zSTq|`H_j!Ty@&9u-LC35813oO@!;9yeuzx89#Q^Z_W79dWAl5~Vea~hg$wu#_GVmg z5aU{pDEPGgxhtq~%KqL7M1&R_*t5y$Kug{nw7tEB0Y8f~7#G_XEwLE1-Ps$LTyf)ak`o9KEJA_U7C`vl?N&# z*Z)vnQGM=Hd290Fq!QzqkZqep9DdFG_?X!o=Lu=i;ytK$fd0*j| zQxcN(orKYy!*T5Xfl8zE4la;!Y^k*)WW+3PK{D00;6-ih{rZI9-Z$DKHrLZ>47Tq< ze`NpQC96N=9<9xPs+7X_5crJ&oy?T(seX7Xn;?R@Tf|j(*DcT8lI@tgg|aag06=o? z4lR2sJh(U^M0U^3y!W6Cd2%5}-zg8tx>p%pg9>?IPjzRHhQ=E#2uA3(En7qm5v-NY;F`d*S(?=;#AL`&43QS(L= zi1quhmK=U2?PCu)p~c}8;r7mOt$IdM^0QZ5)0G<}=f<{~tjXu67~8)SvL63U6n60; z&nm}3YO|hj%I=?t;l`s_b%~1~ z^fZ3d3B|0OWWEF|&{kt(i!K{0WcLcdoBcZ>^o$Jp__{iZMfaRMfUHFh z!Rnrq2axqKRaRc9HjUmttsVNT4ifur)OmgnoS1#)C+AuC(B1`>XzU!((43{%@#556 zum?S54+=VK21i5((9)5AXt~p3%-MS}=J*gIF?nAWaL&jr+L(w;`|5L6XJn@scy#D# zMYH@T{<}E(d5ev+qopnyt-VD~Th2XSsd>?5?@waE?!6Y`YlHYwS?-`1T7M6KnrGQB zYv-I8RrWxw$MTxkU3@yU$Ovj7=cbB2y5fW?cMcFhuohN!WU*@*eXdPgY(3^*{0hB* zH&??ai-Sp{B~SHq0?t0mM%tVm_IpJqbiDVMb)UUDrdh^0-lZLgY~`e3@fV1u$ZVgqh+YsOjU2)S zeO8Wj)&i0 zdiQCMwN=ir#U9Xt;h{aK7l%PDUx{I?p5~pmHcf%!;zK9PW9><__D3TA)^{;3-w8gi zySf%lLhJA@7&toMv{TJEcvIf@I9L07=i@b@u`-v&VJPjvSuzScPWDWGuYJbE7b5RL z9&774-e7oWET^60G=U8**uNV0;PR*Lef2q1R0_=e9;9@AA>)!0gU0gAvy6+Si5_PT zX33uAX=S^*OcrxY!I*t@>_qPWG;za)Z+HO&_86_I`R0Cpp<`>%XBT#|y3Xk}T@3nXR1l z9l&nSSJL~+Q*n~TPeroor}(UNc$eoF-aRcPGKbQy(x{MKA6Iwp!h?8V;;F4ESfZxrrZzH<&6LEB9=2 zInmnh8e$aNn`;_B52_M>2J{gBlIhcDKlt1CVA}3pF^4k8`Ig__8OIx&)#|>!m%sn^ zr@#HzFTcI{{`+tK?Qj44%UgKahj0G)>#x6`f040o|M=#cfBDB>em(#7^VffWdi(Eh zzyI>(yPscw{^|4c=bztydiM!ie}4J=<=v;(*Dp}a56?fm`TvK!{f{@_{PgRefBfq& zfBw&Z{`cSCe)Ib2{pZuS@%g9EA76g_@b2>`zT3yAPhWog@#+1S@4kEU!w+x%`TL)K Z`~81@`{gfh>@@xT<_~}P`Cq@h`9G8OYT*C? From d6eafccde6f92d06f205b1769ac5aebda78558c9 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jan 2024 23:02:35 +0100 Subject: [PATCH 31/34] Update the docstring in the project XML reader --- novelwriter/core/projectxml.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py index 5c2b8d84..1b6bef79 100644 --- a/novelwriter/core/projectxml.py +++ b/novelwriter/core/projectxml.py @@ -106,8 +106,8 @@ class ProjectXMLReader: the project or the content into their respective section nodes as attributes. The id attribute was also added to the project. - Rev 1: Drops the "titleFormat" section from "settings". - Rev 2: Drop the "title" setting from "project". + Rev 1: Drops the titleFormat node from settings. 2.1 Beta 1. + Rev 2: Drops the title node from project. 2.3 Beta 1. """ def __init__(self, path: str | Path) -> None: From e752a97ea707410045b1493d2830347370568f90 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jan 2024 23:11:13 +0100 Subject: [PATCH 32/34] Bump dev version to 2.3 Alpha 2 --- novelwriter/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index 164c5b35..a91f0b7f 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -42,9 +42,9 @@ __license__ = "GPLv3" __author__ = "Veronica Berglyd Olsen" __maintainer__ = "Veronica Berglyd Olsen" __email__ = "code@vkbo.net" -__version__ = "2.3a1" -__hexversion__ = "0x020300a1" -__date__ = "2024-01-23" +__version__ = "2.3a2" +__hexversion__ = "0x020300a2" +__date__ = "2024-01-26" __status__ = "Stable" __domain__ = "novelwriter.io" From 7d9ca66fd61dc04656bbac58466470e29e587600 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jan 2024 23:12:47 +0100 Subject: [PATCH 33/34] Remove Ubuntu 23.04 release --- pkgutils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pkgutils.py b/pkgutils.py index 0cf5fde3..2ba23bc0 100755 --- a/pkgutils.py +++ b/pkgutils.py @@ -863,7 +863,6 @@ def makeForLaunchpad(doSign: bool = False, isFirst: bool = False) -> None: distLoop = [ ("20.04", "focal"), ("22.04", "jammy"), - ("23.04", "lunar"), ("23.10", "mantic"), ] From a76268743f0bdb2f96d6cc39ee01b5b2c4ad6622 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 26 Jan 2024 23:18:36 +0100 Subject: [PATCH 34/34] Update Ubuntu releases --- pkgutils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pkgutils.py b/pkgutils.py index 2ba23bc0..fe648e8f 100755 --- a/pkgutils.py +++ b/pkgutils.py @@ -864,6 +864,7 @@ def makeForLaunchpad(doSign: bool = False, isFirst: bool = False) -> None: ("20.04", "focal"), ("22.04", "jammy"), ("23.10", "mantic"), + ("24.04", "noble"), ] print("Building Ubuntu packages for:")