Merge 2.6.1 release into main
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"Synopsis": "Sinopsis",
|
||||
"Short Description": "Breve Descripción",
|
||||
"Footnotes": "Notas al pie",
|
||||
"Comment": "Comentario",
|
||||
"Notes": "Notas",
|
||||
"Tag": "Etiqueta",
|
||||
@@ -13,6 +14,7 @@
|
||||
"Objects": "Objetos",
|
||||
"Entities": "Entidades",
|
||||
"Custom": "Otros",
|
||||
"New Page": "Nueva Página",
|
||||
"0": "Cero",
|
||||
"1": "Uno",
|
||||
"2": "Dos",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"Synopsis": "Synopsis",
|
||||
"Short Description": "Description sommaire",
|
||||
"Footnotes": "Notes de bas de page",
|
||||
"Comment": "Commentaire",
|
||||
"Notes": "Notes",
|
||||
"Tag": "Étiquette",
|
||||
@@ -13,7 +14,8 @@
|
||||
"Objects": "Objets",
|
||||
"Entities": "Entités",
|
||||
"Custom": "Personnalisé",
|
||||
"0": "Zero",
|
||||
"New Page": "Nouvelle page",
|
||||
"0": "Zéro",
|
||||
"1": "Un",
|
||||
"2": "Deux",
|
||||
"3": "Trois",
|
||||
|
||||
Binary file not shown.
+48
-25
@@ -32,6 +32,7 @@ import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from time import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from PyQt6.QtCore import (
|
||||
PYQT_VERSION, PYQT_VERSION_STR, QT_VERSION, QT_VERSION_STR, QLibraryInfo,
|
||||
@@ -47,6 +48,9 @@ from novelwriter.common import (
|
||||
from novelwriter.constants import nwFiles, nwUnicode
|
||||
from novelwriter.error import formatException, logException
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from novelwriter.core.projectdata import NWProjectData
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEF_GUI = "default"
|
||||
@@ -60,14 +64,15 @@ class Config:
|
||||
__slots__ = (
|
||||
"_confPath", "_dataPath", "_homePath", "_backPath", "_appPath", "_appRoot", "_hasError",
|
||||
"_errData", "_nwLangPath", "_qtLangPath", "_qLocale", "_dLocale", "_dShortDate",
|
||||
"_dShortDateTime", "_qtTrans", "_recentProjects", "_recentPaths", "_backupPath",
|
||||
"_dShortDateTime", "_qtTrans", "_manuals", "_recentProjects", "_recentPaths",
|
||||
"_backupPath",
|
||||
|
||||
"appName", "appHandle", "pdfDocs", "guiLocale", "guiTheme", "guiSyntax", "guiFont",
|
||||
"hideVScroll", "hideHScroll", "lastNotes", "nativeFont", "iconTheme", "iconColTree",
|
||||
"iconColDocs", "mainWinSize", "welcomeWinSize", "prefsWinSize", "mainPanePos",
|
||||
"viewPanePos", "outlinePanePos", "autoSaveProj", "autoSaveDoc", "emphLabels",
|
||||
"backupOnClose", "askBeforeBackup", "textFont", "textWidth", "textMargin", "tabWidth",
|
||||
"focusWidth", "hideFocusFooter", "showFullPath", "autoSelect", "doJustify",
|
||||
"appName", "appHandle", "guiLocale", "guiTheme", "guiSyntax", "guiFont", "hideVScroll",
|
||||
"hideHScroll", "lastNotes", "nativeFont", "iconTheme", "iconColTree", "iconColDocs",
|
||||
"mainWinSize", "welcomeWinSize", "prefsWinSize", "mainPanePos", "viewPanePos",
|
||||
"outlinePanePos", "autoSaveProj", "autoSaveDoc", "emphLabels", "backupOnClose",
|
||||
"askBeforeBackup", "askBeforeExit", "textFont", "textWidth", "textMargin", "tabWidth",
|
||||
"focusWidth", "hideFocusFooter", "showFullPath", "autoSelect", "doJustify",
|
||||
"showTabsNSpaces", "showLineEndings", "showMultiSpaces", "doReplace", "doReplaceSQuote",
|
||||
"doReplaceDQuote", "doReplaceDash", "doReplaceDots", "autoScroll", "autoScrollPos",
|
||||
"scrollPastEnd", "dialogStyle", "allowOpenDial", "dialogLine", "narratorBreak",
|
||||
@@ -131,8 +136,11 @@ class Config:
|
||||
self._qtTrans = {}
|
||||
|
||||
# PDF Manual
|
||||
pdfDocs = self._appPath / "assets" / "manual.pdf"
|
||||
self.pdfDocs = pdfDocs if pdfDocs.is_file() else None
|
||||
self._manuals: dict[str, Path] = {}
|
||||
if (assets := self._appPath / "assets").is_dir():
|
||||
for item in assets.iterdir():
|
||||
if item.is_file() and item.stem.startswith("manual") and item.suffix == ".pdf":
|
||||
self._manuals[item.stem] = item
|
||||
|
||||
# User Settings
|
||||
# =============
|
||||
@@ -169,6 +177,7 @@ class Config:
|
||||
self.emphLabels = True # Add emphasis to H1 and H2 item labels
|
||||
self.backupOnClose = False # Flag for running automatic backups
|
||||
self.askBeforeBackup = True # Flag for asking before running automatic backup
|
||||
self.askBeforeExit = True # Flag for asking before exiting the app
|
||||
|
||||
# Text Editor Settings
|
||||
self.textFont = QFont() # Editor font
|
||||
@@ -291,6 +300,10 @@ class Config:
|
||||
def hasError(self) -> bool:
|
||||
return self._hasError
|
||||
|
||||
@property
|
||||
def pdfDocs(self) -> Path | None:
|
||||
return self._manuals.get(f"manual_{self.locale.name()}", self._manuals.get("manual"))
|
||||
|
||||
@property
|
||||
def locale(self) -> QLocale:
|
||||
return self._dLocale
|
||||
@@ -605,6 +618,7 @@ class Config:
|
||||
self._backupPath = conf.rdPath(sec, "backuppath", self._backupPath)
|
||||
self.backupOnClose = conf.rdBool(sec, "backuponclose", self.backupOnClose)
|
||||
self.askBeforeBackup = conf.rdBool(sec, "askbeforebackup", self.askBeforeBackup)
|
||||
self.askBeforeExit = conf.rdBool(sec, "askbeforeexit", self.askBeforeExit)
|
||||
|
||||
# Editor
|
||||
sec = "Editor"
|
||||
@@ -719,6 +733,7 @@ class Config:
|
||||
"backuppath": str(self._backupPath),
|
||||
"backuponclose": str(self.backupOnClose),
|
||||
"askbeforebackup": str(self.askBeforeBackup),
|
||||
"askbeforeexit": str(self.askBeforeExit),
|
||||
}
|
||||
|
||||
conf["Editor"] = {
|
||||
@@ -822,29 +837,30 @@ class RecentProjects:
|
||||
|
||||
def __init__(self, config: Config) -> None:
|
||||
self._conf = config
|
||||
self._data = {}
|
||||
self._data: dict[str, dict[str, str | int]] = {}
|
||||
self._map: dict[str, str] = {}
|
||||
return
|
||||
|
||||
def loadCache(self) -> bool:
|
||||
"""Load the cache file for recent projects."""
|
||||
self._data = {}
|
||||
|
||||
self._map = {}
|
||||
cacheFile = self._conf.dataPath(nwFiles.RECENT_FILE)
|
||||
if cacheFile.is_file():
|
||||
try:
|
||||
with open(cacheFile, mode="r", encoding="utf-8") as inFile:
|
||||
data = json.load(inFile)
|
||||
for path, entry in data.items():
|
||||
self._data[path] = {
|
||||
"title": entry.get("title", ""),
|
||||
"words": entry.get("words", 0),
|
||||
"time": entry.get("time", 0),
|
||||
}
|
||||
puuid = str(entry.get("uuid", ""))
|
||||
title = str(entry.get("title", ""))
|
||||
words = checkInt(entry.get("words", 0), 0)
|
||||
saved = checkInt(entry.get("time", 0), 0)
|
||||
if path and title:
|
||||
self._setEntry(puuid, path, title, words, saved)
|
||||
except Exception:
|
||||
logger.error("Could not load recent project cache")
|
||||
logException()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def saveCache(self) -> bool:
|
||||
@@ -859,7 +875,6 @@ class RecentProjects:
|
||||
logger.error("Could not save recent project cache")
|
||||
logException()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def listEntries(self) -> list[tuple[str, str, int, int]]:
|
||||
@@ -869,14 +884,15 @@ class RecentProjects:
|
||||
for k, e in self._data.items()
|
||||
]
|
||||
|
||||
def update(self, path: str | Path, title: str, words: int, saved: float | int) -> None:
|
||||
def update(self, path: str | Path, data: NWProjectData, saved: float | int) -> None:
|
||||
"""Add or update recent cache information on a given project."""
|
||||
self._data[str(path)] = {
|
||||
"title": title,
|
||||
"words": int(words),
|
||||
"time": int(saved),
|
||||
}
|
||||
self.saveCache()
|
||||
try:
|
||||
if (remove := self._map.get(data.uuid)) and (remove != str(path)):
|
||||
self.remove(remove)
|
||||
self._setEntry(data.uuid, str(path), data.name, sum(data.currCounts), int(saved))
|
||||
self.saveCache()
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
|
||||
def remove(self, path: str | Path) -> None:
|
||||
@@ -886,6 +902,13 @@ class RecentProjects:
|
||||
self.saveCache()
|
||||
return
|
||||
|
||||
def _setEntry(self, puuid: str, path: str, title: str, words: int, saved: int) -> None:
|
||||
"""Set an entry in the recent projects record."""
|
||||
self._data[path] = {"uuid": puuid, "title": title, "words": words, "time": saved}
|
||||
if puuid:
|
||||
self._map[puuid] = path
|
||||
return
|
||||
|
||||
|
||||
class RecentPaths:
|
||||
|
||||
|
||||
@@ -173,6 +173,10 @@ class nwKeyWords:
|
||||
TAG_KEY, POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY, WORLD_KEY,
|
||||
OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY, STORY_KEY, MENTION_KEY,
|
||||
]
|
||||
CAN_CREATE = [
|
||||
POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY, WORLD_KEY,
|
||||
OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY,
|
||||
]
|
||||
|
||||
# Set of Valid Keys
|
||||
VALID_KEYS = set(ALL_KEYS)
|
||||
|
||||
@@ -349,9 +349,7 @@ class NWProject:
|
||||
|
||||
# Update recent projects
|
||||
if storePath := self._storage.storagePath:
|
||||
CONFIG.recentProjects.update(
|
||||
storePath, self._data.name, sum(self._data.initCounts), time()
|
||||
)
|
||||
CONFIG.recentProjects.update(storePath, self._data, time())
|
||||
|
||||
# Check the project tree consistency
|
||||
# This also handles any orphaned files found
|
||||
@@ -417,9 +415,7 @@ class NWProject:
|
||||
|
||||
# Update recent projects
|
||||
if storagePath := self._storage.storagePath:
|
||||
CONFIG.recentProjects.update(
|
||||
storagePath, self._data.name, sum(self._data.currCounts), saveTime
|
||||
)
|
||||
CONFIG.recentProjects.update(storagePath, self._data, saveTime)
|
||||
|
||||
SHARED.newStatusMessage(self.tr("Saved Project: {0}").format(self._data.name))
|
||||
self.setProjectChanged(False)
|
||||
|
||||
@@ -302,10 +302,10 @@ class GuiPreferences(NDialog):
|
||||
self.tr("Include project notes in status bar word count"), self.incNotesWCount
|
||||
)
|
||||
|
||||
# Auto Save
|
||||
# Behaviour
|
||||
# =========
|
||||
|
||||
title = self.tr("Auto Save")
|
||||
title = self.tr("Behaviour")
|
||||
section += 1
|
||||
self.sidebar.addButton(title, section)
|
||||
self.mainForm.addGroupLabel(title, section)
|
||||
@@ -332,6 +332,14 @@ class GuiPreferences(NDialog):
|
||||
self.tr("How often the project is automatically saved."), unit=self.tr("seconds")
|
||||
)
|
||||
|
||||
# Ask before exiting novelWriter
|
||||
self.askBeforeExit = NSwitch(self)
|
||||
self.askBeforeExit.setChecked(CONFIG.askBeforeExit)
|
||||
self.mainForm.addRow(
|
||||
self.tr("Ask before exiting novelWriter"), self.askBeforeExit,
|
||||
self.tr("Only applies when a project is open.")
|
||||
)
|
||||
|
||||
# Project Backup
|
||||
# ==============
|
||||
|
||||
@@ -965,9 +973,10 @@ class GuiPreferences(NDialog):
|
||||
CONFIG.incNotesWCount = self.incNotesWCount.isChecked()
|
||||
CONFIG.setTextFont(self._textFont)
|
||||
|
||||
# Auto Save
|
||||
CONFIG.autoSaveDoc = self.autoSaveDoc.value()
|
||||
CONFIG.autoSaveProj = self.autoSaveProj.value()
|
||||
# Behaviour
|
||||
CONFIG.autoSaveDoc = self.autoSaveDoc.value()
|
||||
CONFIG.autoSaveProj = self.autoSaveProj.value()
|
||||
CONFIG.askBeforeExit = self.askBeforeExit.isChecked()
|
||||
|
||||
# Project Backup
|
||||
CONFIG.setBackupPath(self.backupPath)
|
||||
|
||||
@@ -68,13 +68,6 @@ class nwComment(Enum):
|
||||
STORY = 7
|
||||
|
||||
|
||||
class nwTrinary(Enum):
|
||||
|
||||
NEGATIVE = -1
|
||||
NEUTRAL = 0
|
||||
POSITIVE = 1
|
||||
|
||||
|
||||
class nwChange(Enum):
|
||||
|
||||
CREATE = 0
|
||||
|
||||
@@ -28,7 +28,6 @@ import logging
|
||||
from PyQt6.QtGui import QColor, QPainter, QPaintEvent
|
||||
from PyQt6.QtWidgets import QAbstractButton, QWidget
|
||||
|
||||
from novelwriter.enum import nwTrinary
|
||||
from novelwriter.types import QtBlack, QtPaintAntiAlias
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -44,13 +43,13 @@ class StatusLED(QAbstractButton):
|
||||
self._postitve = QtBlack
|
||||
self._negative = QtBlack
|
||||
self._color = QtBlack
|
||||
self._state = nwTrinary.NEUTRAL
|
||||
self._state = None
|
||||
self.setFixedWidth(sW)
|
||||
self.setFixedHeight(sH)
|
||||
return
|
||||
|
||||
@property
|
||||
def state(self) -> nwTrinary:
|
||||
def state(self) -> bool | None:
|
||||
"""The current state of the LED."""
|
||||
return self._state
|
||||
|
||||
@@ -62,11 +61,11 @@ class StatusLED(QAbstractButton):
|
||||
self.setState(self._state)
|
||||
return
|
||||
|
||||
def setState(self, state: nwTrinary) -> None:
|
||||
def setState(self, state: bool | None) -> None:
|
||||
"""Set the colour state."""
|
||||
if state == nwTrinary.POSITIVE:
|
||||
if state is True:
|
||||
self._color = self._postitve
|
||||
elif state == nwTrinary.NEGATIVE:
|
||||
elif state is False:
|
||||
self._color = self._negative
|
||||
else:
|
||||
self._color = self._neutral
|
||||
|
||||
@@ -34,7 +34,7 @@ from __future__ import annotations
|
||||
import bisect
|
||||
import logging
|
||||
|
||||
from enum import Enum
|
||||
from enum import Enum, IntFlag
|
||||
from time import time
|
||||
|
||||
from PyQt6.QtCore import (
|
||||
@@ -59,7 +59,7 @@ from novelwriter.constants import nwConst, nwKeyWords, nwShortcode, nwUnicode
|
||||
from novelwriter.core.document import NWDocument
|
||||
from novelwriter.enum import (
|
||||
nwChange, nwComment, nwDocAction, nwDocInsert, nwDocMode, nwItemClass,
|
||||
nwItemType, nwTrinary
|
||||
nwItemType
|
||||
)
|
||||
from novelwriter.extensions.configlayout import NColorLabel
|
||||
from novelwriter.extensions.eventfilters import WheelEventFilter
|
||||
@@ -86,6 +86,13 @@ class _SelectAction(Enum):
|
||||
MOVE_AFTER = 3
|
||||
|
||||
|
||||
class _TagAction(IntFlag):
|
||||
|
||||
NONE = 0b00
|
||||
FOLLOW = 0b01
|
||||
CREATE = 0b10
|
||||
|
||||
|
||||
class GuiDocEditor(QPlainTextEdit):
|
||||
"""Gui Widget: Main Document Editor"""
|
||||
|
||||
@@ -1146,11 +1153,11 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
|
||||
# Follow
|
||||
status = self._processTag(cursor=pCursor, follow=False)
|
||||
if status == nwTrinary.POSITIVE:
|
||||
if status & _TagAction.FOLLOW:
|
||||
action = qtAddAction(ctxMenu, self.tr("Follow Tag"))
|
||||
action.triggered.connect(qtLambda(self._processTag, cursor=pCursor, follow=True))
|
||||
ctxMenu.addSeparator()
|
||||
elif status == nwTrinary.NEGATIVE:
|
||||
elif status & _TagAction.CREATE:
|
||||
action = qtAddAction(ctxMenu, self.tr("Create Note for Tag"))
|
||||
action.triggered.connect(qtLambda(self._processTag, cursor=pCursor, create=True))
|
||||
ctxMenu.addSeparator()
|
||||
@@ -1916,7 +1923,7 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
|
||||
def _processTag(
|
||||
self, cursor: QTextCursor | None = None, follow: bool = True, create: bool = False
|
||||
) -> nwTrinary:
|
||||
) -> _TagAction:
|
||||
"""Activated by Ctrl+Enter. Checks that we're in a block
|
||||
starting with '@'. We then find the tag under the cursor and
|
||||
check that it is not the tag itself. If all this is fine, we
|
||||
@@ -1926,19 +1933,22 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
if cursor is None:
|
||||
cursor = self.textCursor()
|
||||
|
||||
status = _TagAction.NONE
|
||||
block = cursor.block()
|
||||
text = block.text()
|
||||
if len(text) == 0:
|
||||
return nwTrinary.NEUTRAL
|
||||
return status
|
||||
|
||||
if text.startswith("@") and self._docHandle:
|
||||
|
||||
isGood, tBits, tPos = SHARED.project.index.scanThis(text)
|
||||
if (
|
||||
not isGood or not tBits or tBits[0] == nwKeyWords.TAG_KEY
|
||||
or tBits[0] not in nwKeyWords.VALID_KEYS
|
||||
not isGood
|
||||
or not tBits
|
||||
or (key := tBits[0]) == nwKeyWords.TAG_KEY
|
||||
or key not in nwKeyWords.VALID_KEYS
|
||||
):
|
||||
return nwTrinary.NEUTRAL
|
||||
return status
|
||||
|
||||
tag = ""
|
||||
exist = False
|
||||
@@ -1955,7 +1965,14 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
|
||||
if not tag or tag.startswith("@"):
|
||||
# The keyword cannot be looked up, so we ignore that
|
||||
return nwTrinary.NEUTRAL
|
||||
return status
|
||||
|
||||
if not exist and key in nwKeyWords.CAN_CREATE:
|
||||
# Must only be set if we have a tag selected
|
||||
status |= _TagAction.CREATE
|
||||
|
||||
if exist:
|
||||
status |= _TagAction.FOLLOW
|
||||
|
||||
if follow and exist:
|
||||
logger.debug("Attempting to follow tag '%s'", tag)
|
||||
@@ -1967,9 +1984,7 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
itemClass = nwKeyWords.KEY_CLASS.get(tBits[0], nwItemClass.NO_CLASS)
|
||||
self.requestNewNoteCreation.emit(tag, itemClass)
|
||||
|
||||
return nwTrinary.POSITIVE if exist else nwTrinary.NEGATIVE
|
||||
|
||||
return nwTrinary.NEUTRAL
|
||||
return status
|
||||
|
||||
def _emitRenameItem(self, block: QTextBlock) -> None:
|
||||
"""Emit a signal to request an item be renamed."""
|
||||
|
||||
@@ -34,7 +34,6 @@ from PyQt6.QtWidgets import QApplication, QLabel, QStatusBar, QWidget
|
||||
from novelwriter import CONFIG, SHARED
|
||||
from novelwriter.common import formatTime
|
||||
from novelwriter.constants import nwConst
|
||||
from novelwriter.enum import nwTrinary
|
||||
from novelwriter.extensions.modified import NClickableLabel
|
||||
from novelwriter.extensions.statusled import StatusLED
|
||||
|
||||
@@ -119,8 +118,8 @@ class GuiMainStatus(QStatusBar):
|
||||
self.setRefTime(-1.0)
|
||||
self.setLanguage(*SHARED.spelling.describeDict())
|
||||
self.setProjectStats(0, 0)
|
||||
self.setProjectStatus(nwTrinary.NEUTRAL)
|
||||
self.setDocumentStatus(nwTrinary.NEUTRAL)
|
||||
self.setProjectStatus(None)
|
||||
self.setDocumentStatus(None)
|
||||
self.updateTime()
|
||||
return
|
||||
|
||||
@@ -150,12 +149,12 @@ class GuiMainStatus(QStatusBar):
|
||||
self._refTime = refTime
|
||||
return
|
||||
|
||||
def setProjectStatus(self, state: nwTrinary) -> None:
|
||||
def setProjectStatus(self, state: bool | None) -> None:
|
||||
"""Set the project status colour icon."""
|
||||
self.projIcon.setState(state)
|
||||
return
|
||||
|
||||
def setDocumentStatus(self, state: nwTrinary) -> None:
|
||||
def setDocumentStatus(self, state: bool | None) -> None:
|
||||
"""Set the document status colour icon."""
|
||||
self.docIcon.setState(state)
|
||||
return
|
||||
@@ -218,13 +217,13 @@ class GuiMainStatus(QStatusBar):
|
||||
@pyqtSlot(bool)
|
||||
def updateProjectStatus(self, status: bool) -> None:
|
||||
"""Update the project status."""
|
||||
self.setProjectStatus(nwTrinary.NEGATIVE if status else nwTrinary.POSITIVE)
|
||||
self.setProjectStatus(not status)
|
||||
return
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def updateDocumentStatus(self, status: bool) -> None:
|
||||
"""Update the document status."""
|
||||
self.setDocumentStatus(nwTrinary.NEGATIVE if status else nwTrinary.POSITIVE)
|
||||
self.setDocumentStatus(not status)
|
||||
return
|
||||
|
||||
##
|
||||
|
||||
@@ -847,7 +847,7 @@ class GuiMain(QMainWindow):
|
||||
|
||||
def closeMain(self) -> bool:
|
||||
"""Save everything, and close novelWriter."""
|
||||
if SHARED.hasProject and not SHARED.question("%s<br>%s" % (
|
||||
if SHARED.hasProject and CONFIG.askBeforeExit and not SHARED.question("%s<br>%s" % (
|
||||
self.tr("Do you want to exit novelWriter?"),
|
||||
self.tr("Changes are saved automatically.")
|
||||
)):
|
||||
|
||||
Reference in New Issue
Block a user