Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8307822e20 | ||
|
|
f1548dc8ea | ||
|
|
25b33ab4d3 | ||
|
|
4af98dcab5 | ||
|
|
3cf916c845 | ||
|
|
d944a70063 | ||
|
|
c20f970405 | ||
|
|
bec082c0ae | ||
|
|
83b244a8a3 | ||
|
|
bd82392d71 | ||
|
|
836a3d353a | ||
|
|
a4d2a9ea6f | ||
|
|
b0fc8d63a6 | ||
|
|
eda1a1c4dd | ||
|
|
238abdb567 | ||
|
|
c3f9ceb03a | ||
|
|
b95e9fcda3 | ||
|
|
b3f8a2c18c | ||
|
|
18bbf7d86f | ||
|
|
7a63ac83d2 | ||
|
|
8dc28ddbee | ||
|
|
15803cf0a3 | ||
|
|
5ac001781c | ||
|
|
66986b88f8 | ||
|
|
f4e12a63a5 | ||
|
|
095c005c3f | ||
|
|
e414b6458d | ||
|
|
ece73784c2 | ||
|
|
8dd9644c13 | ||
|
|
556e8db104 | ||
|
|
d2f422a441 | ||
|
|
e4daff8956 | ||
|
|
2e1b17dcdf | ||
|
|
2ef11f56e5 | ||
|
|
fdaeca78a7 | ||
|
|
93e7343fe9 | ||
|
|
5167608f7a | ||
|
|
d4a0881e77 | ||
|
|
7f42210f2f | ||
|
|
33497bc92d | ||
|
|
06aa2e6c07 | ||
|
|
3bffaa01dc | ||
|
|
66237c35ea | ||
|
|
5936e1a68b | ||
|
|
17b3b2b770 | ||
|
|
826e805b06 | ||
|
|
5419d76606 | ||
|
|
0d84653f7d | ||
|
|
7140788b48 | ||
|
|
a43bd5a50e |
9
.github/workflows/build_linux.yml
vendored
9
.github/workflows/build_linux.yml
vendored
@@ -32,12 +32,15 @@ jobs:
|
||||
path: novelwriter/assets
|
||||
|
||||
- name: Build AppImage
|
||||
run: python pkgutils.py build-appimage --linux-tag $LINUX_TAG --python-version $PYTHON_VERSION
|
||||
id: build
|
||||
run: |
|
||||
echo "BUILD_VERSION=$(python pkgutils.py version)" >> $GITHUB_OUTPUT
|
||||
python pkgutils.py build-appimage --linux-tag $LINUX_TAG --python-version $PYTHON_VERSION
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Linux-AppImage
|
||||
path: dist_appimage
|
||||
name: novelWriter-${{ steps.build.outputs.BUILD_VERSION }}-Linux-AppImage
|
||||
path: dist_appimage/*.AppImage*
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
18
.github/workflows/build_mac.yml
vendored
18
.github/workflows/build_mac.yml
vendored
@@ -25,13 +25,16 @@ jobs:
|
||||
path: novelwriter/assets
|
||||
|
||||
- name: Build App Bundle
|
||||
run: ./setup/macos/build.sh $PYTHON_VERSION $PACKAGE_ARCH $MINICONDA_ARCH
|
||||
id: build
|
||||
run: |
|
||||
echo "BUILD_VERSION=$(python pkgutils.py version)" >> $GITHUB_OUTPUT
|
||||
./setup/macos/build.sh $PYTHON_VERSION $PACKAGE_ARCH $MINICONDA_ARCH
|
||||
|
||||
- name: Upload DMG
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: MacOS-AMD64-DMG
|
||||
path: dist_macos
|
||||
name: novelWriter-${{ steps.build.outputs.BUILD_VERSION }}-MacOS-AMD64-DMG
|
||||
path: dist_macos/*.dmg*
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
@@ -53,12 +56,15 @@ jobs:
|
||||
path: novelwriter/assets
|
||||
|
||||
- name: Build App Bundle
|
||||
run: ./setup/macos/build.sh $PYTHON_VERSION $PACKAGE_ARCH $MINICONDA_ARCH
|
||||
id: build
|
||||
run: |
|
||||
echo "BUILD_VERSION=$(python pkgutils.py version)" >> $GITHUB_OUTPUT
|
||||
./setup/macos/build.sh $PYTHON_VERSION $PACKAGE_ARCH $MINICONDA_ARCH
|
||||
|
||||
- name: Upload DMG
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: MacOS-M1-DMG
|
||||
path: dist_macos
|
||||
name: novelWriter-${{ steps.build.outputs.BUILD_VERSION }}-MacOS-M1-DMG
|
||||
path: dist_macos/*.dmg*
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
31
.github/workflows/build_win.yml
vendored
31
.github/workflows/build_win.yml
vendored
@@ -26,12 +26,37 @@ jobs:
|
||||
path: novelwriter/assets
|
||||
|
||||
- name: Build Setup Installer
|
||||
run: python pkgutils.py build-win-exe
|
||||
id: build
|
||||
run: |
|
||||
echo "BUILD_VERSION=$(python pkgutils.py version)" >> $Env:GITHUB_OUTPUT
|
||||
python pkgutils.py build-win-exe
|
||||
|
||||
- name: Upload Artifacts
|
||||
- name: Upload Unsigned Artifacts
|
||||
id: upload-unsigned-artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Win-Setup
|
||||
name: novelWriter-${{ steps.build.outputs.BUILD_VERSION }}-Win-Setup
|
||||
path: dist/*.exe
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
- name: Submit Signing Request
|
||||
uses: signpath/github-action-submit-signing-request@v1
|
||||
with:
|
||||
api-token: "${{ secrets.SIGNPATH_API_TOKEN }}"
|
||||
organization-id: "0471e52c-66fa-4e9a-bfb9-36167095ca3f"
|
||||
project-slug: "novelWriter"
|
||||
signing-policy-slug: "release-signing "
|
||||
github-artifact-id: "${{ steps.upload-unsigned-artifact.outputs.artifact-id }}"
|
||||
wait-for-completion: true
|
||||
output-artifact-directory: "dist/"
|
||||
parameters: |
|
||||
version: "${{ steps.build.outputs.BUILD_VERSION }}"
|
||||
|
||||
- name: Upload Signed Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: novelWriter-${{ steps.build.outputs.BUILD_VERSION }}-Win-Setup-Signed
|
||||
path: dist/*.exe
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
2
.github/workflows/test_linux.yml
vendored
2
.github/workflows/test_linux.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
testLinux:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
fail-fast: false
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
97
CHANGELOG.md
97
CHANGELOG.md
@@ -1,5 +1,102 @@
|
||||
# novelWriter Changelog
|
||||
|
||||
## Version 2.5.3 [2024-11-26]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This is a patch release that fixes a few minor bugs in the user interface, and with the HTML
|
||||
manuscript format. This patch also adds a Russian translation, and updated the German and
|
||||
Portuguese translations.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* Fixed the red/green LEDs on the statusbar for the Default Light theme. The colours were swapped.
|
||||
Issue #2057. PR #2059.
|
||||
* Moved the styles tag into the head tag for HTML manuscript output, which is the correct placement
|
||||
for them. Issue #2080. PR #2085.
|
||||
* Fixed a bug in the project tree where it was possible to drag content to root level on some Qt
|
||||
versions where the disallow flag is not obeyed. The illegal action is now additionally blocked by
|
||||
code. Issue #2108. PR #2109.
|
||||
|
||||
**Documentation**
|
||||
|
||||
* Updated a dead link to the spell check library with the new one. Issue #2042. PR #2059.
|
||||
|
||||
**Internationalisation**
|
||||
|
||||
* Russian translation added by Konstantin Tan (@k1kimosha). PR #2126
|
||||
* Updated German and Portuguese translations. PR #2126
|
||||
|
||||
----
|
||||
|
||||
## Version 2.5.2 [2024-09-17]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This is a patch release that fixes a series of issues mostly affecting dialogue highlighting in the
|
||||
document viewer. The way the text is formatted in the viewer changed a lot in 2.5, and there were a
|
||||
few issues with the new implementation. The tab stop distance setting was also ignored, but has now
|
||||
been fixed as well.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* Fixed an issue where the tab stop distance setting was being ignored in the document viewer. It
|
||||
must be set again every time a document is loaded. Issue #1996. PR #2014.
|
||||
* Fixed an issue with overlapping formatting markers when processing text into new formats. This
|
||||
issues was introduced when dialogue highlighting was added to the document viewer and manuscript
|
||||
build. It arises because start and end of dialogue can occur on the same character as other
|
||||
formatting, and then the order these markers are processed matters. This was not properly handled
|
||||
in the code. Issue #2012. PR #2014.
|
||||
* Dialogue highlighting is now only applied to plain text paragraphs, not comments, headings, and
|
||||
other places where there should be no dialogue. Issue #2013. PR #2014.
|
||||
* Note documents no longer get dialogue highlighting. Issue #2011. PR #2014.
|
||||
|
||||
**Documentation**
|
||||
|
||||
* Added a section about spell checking in the documentation. PR #2015.
|
||||
|
||||
----
|
||||
|
||||
## Version 2.5.1 [2024-07-28]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This is a patch release that fixes an issue with the lock file being left in the project when the
|
||||
project open is cancelled, a few issues with custom input boxes in Preferences, and a usability
|
||||
issue when selecting to edit a tag or reference from the Tags and References panel as well as fixed
|
||||
an issue with the Importance labels not being updated in the tabs for tags.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* Fixes an issue where the project lock file was created before certain checks where the user could
|
||||
cancel the project open process, leaving a lock file in the project folder behind. Issue #1977.
|
||||
PR #1978.
|
||||
* Fixes an issue where the user could add spaces to the pad before and after Preference settings,
|
||||
which would pad every space entered into the text. The Preferences settings no longer accept any
|
||||
white spaces in these boxes (they are removed on save) and also strips duplicate character
|
||||
entries. Issue #1985. PR #1986.
|
||||
* The narrator break and dialog symbol Preference text boxes will now be cleared if the settings is
|
||||
a white space. PR #1986.
|
||||
* Fixed an issue where Importance labels were not updated in the viewer panel's tags tabs when they
|
||||
where changed in Project Settings. Issue #1992. PR #1993.
|
||||
|
||||
**Usability**
|
||||
|
||||
* When selecting to edit a tag or reference from the panel below the Document Viewer, the editor
|
||||
now scrolls to the relevant heading as it does in the viewer. Issue #1983. PR #1987.
|
||||
|
||||
**Internationalisation**
|
||||
|
||||
* Latin American Spanish translation updated by Tommy Marplatt (@tmarplatt). PR #1989.
|
||||
|
||||
----
|
||||
|
||||
## Version 2.5 [2024-07-10]
|
||||
|
||||
### Release Notes
|
||||
|
||||
@@ -30,6 +30,7 @@ translators for the languages currently available:
|
||||
* **Norwegian:** Veronica Berglyd Olsen (vkbo)
|
||||
* **Polish:** Anna Maria Polak (Nauthiz)
|
||||
* **Portuguese:** Bruno Meneguello (bkmeneguello)
|
||||
* **Russian:** Konstantin Tan (k1kimosha)
|
||||
* **Simplified Chinese:** Qianzhi Long (longqzh)
|
||||
|
||||
Additional larger translation contributions:
|
||||
|
||||
@@ -29,6 +29,13 @@ documentation.
|
||||
* PyPi Project: [pypi.org/project/novelWriter](https://pypi.org/project/novelWriter)
|
||||
* Social Media: [fosstodon.org/@novelwriter](https://fosstodon.org/@novelwriter)
|
||||
|
||||
## Sponsors
|
||||
|
||||
<img align="left" style="height: 1.5em; margin: 0 0.5em 0 0;" src="https://raw.githubusercontent.com/vkbo/novelWriter/main/setup/signpath_logo.png">
|
||||
|
||||
Free code signing on Windows provided by [SignPath.io](https://about.signpath.io/),
|
||||
certificate by [SignPath Foundation](https://signpath.org/).
|
||||
|
||||
## Implementation
|
||||
|
||||
novelWriter is written with Python 3 (3.9+) using Qt5 and PyQt5 (5.15 only), and is released on
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
Customisations
|
||||
**************
|
||||
|
||||
.. _Enchant: https://abiword.github.io/enchant
|
||||
.. _Enchant: https://rrthomas.github.io/enchant/
|
||||
.. _Free Desktop: https://cgit.freedesktop.org/libreoffice/dictionaries/tree/
|
||||
|
||||
There are a few ways you can customise novelWriter yourself. Currently, you can add new GUI themes,
|
||||
|
||||
@@ -40,6 +40,24 @@ Any :term:`references<reference>` in the editor can be opened in the viewer by m
|
||||
the label and pressing :kbd:`Ctrl+Return`. You can also control-click them with your mouse.
|
||||
|
||||
|
||||
Spell Checking
|
||||
--------------
|
||||
|
||||
A third party library called Enchant is used for spell checking in the editor. The controls for
|
||||
spell checking can be found in the **Tools** menu. You can also set spell checking language in
|
||||
**Project Settings**.
|
||||
|
||||
This spell checking library comes with support for custom words that you can add by selecting
|
||||
"Add Word to Dictionary" from the context menu when a word is highlighted by the spell checker as
|
||||
misspelled. The custom words are managed on a per-project basis, and can the list of words can be
|
||||
edited from the **Project Word List** tool available from the **Tools** menu.
|
||||
|
||||
.. note::
|
||||
|
||||
Generally, spell checking dictionaries are collected from your operating system, but on Windows
|
||||
they are not. See :ref:`a_custom_dict` for how to add spell checking dictionaries on Windows.
|
||||
|
||||
|
||||
Editor Auto-Completer
|
||||
---------------------
|
||||
|
||||
|
||||
621
i18n/nw_base.ts
621
i18n/nw_base.ts
File diff suppressed because it is too large
Load Diff
647
i18n/nw_de_DE.ts
647
i18n/nw_de_DE.ts
File diff suppressed because it is too large
Load Diff
621
i18n/nw_en_US.ts
621
i18n/nw_en_US.ts
File diff suppressed because it is too large
Load Diff
1021
i18n/nw_es_419.ts
1021
i18n/nw_es_419.ts
File diff suppressed because it is too large
Load Diff
621
i18n/nw_fr_FR.ts
621
i18n/nw_fr_FR.ts
File diff suppressed because it is too large
Load Diff
621
i18n/nw_it_IT.ts
621
i18n/nw_it_IT.ts
File diff suppressed because it is too large
Load Diff
621
i18n/nw_ja_JP.ts
621
i18n/nw_ja_JP.ts
File diff suppressed because it is too large
Load Diff
621
i18n/nw_nb_NO.ts
621
i18n/nw_nb_NO.ts
File diff suppressed because it is too large
Load Diff
621
i18n/nw_nl_NL.ts
621
i18n/nw_nl_NL.ts
File diff suppressed because it is too large
Load Diff
621
i18n/nw_pl_PL.ts
621
i18n/nw_pl_PL.ts
File diff suppressed because it is too large
Load Diff
635
i18n/nw_pt_BR.ts
635
i18n/nw_pt_BR.ts
File diff suppressed because it is too large
Load Diff
4991
i18n/nw_ru_RU.ts
Normal file
4991
i18n/nw_ru_RU.ts
Normal file
File diff suppressed because it is too large
Load Diff
621
i18n/nw_zh_CN.ts
621
i18n/nw_zh_CN.ts
File diff suppressed because it is too large
Load Diff
@@ -47,9 +47,9 @@ __license__ = "GPLv3"
|
||||
__author__ = "Veronica Berglyd Olsen"
|
||||
__maintainer__ = "Veronica Berglyd Olsen"
|
||||
__email__ = "code@vkbo.net"
|
||||
__version__ = "2.5"
|
||||
__hexversion__ = "0x020500f0"
|
||||
__date__ = "2024-07-10"
|
||||
__version__ = "2.5.3"
|
||||
__hexversion__ = "0x020503f0"
|
||||
__date__ = "2024-11-26"
|
||||
__status__ = "Stable"
|
||||
__domain__ = "novelwriter.io"
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
"Notes": "Notizen",
|
||||
"Tag": "Schlagwort",
|
||||
"Point of View": "Perspektive",
|
||||
"Focus": "Mittelpunkt",
|
||||
"Focus": "Fokus",
|
||||
"Characters": "Charaktere",
|
||||
"Plot": "Handlungen",
|
||||
"Plot": "Handlung",
|
||||
"Timeline": "Zeitleiste",
|
||||
"Locations": "Schauplätze",
|
||||
"Objects": "Objekte",
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
{
|
||||
"Synopsis": "Сводка",
|
||||
"Short Description": "Краткое описание",
|
||||
"Comment": "Комментарий",
|
||||
"Notes": "Заметки",
|
||||
"Tag": "Тэг",
|
||||
"Point of View": "Точка зрения",
|
||||
"Focus": "Фокус",
|
||||
"Characters": "Персонажей",
|
||||
"Plot": "Сюжет",
|
||||
"Timeline": "Хронология",
|
||||
"Locations": "Локации",
|
||||
"Objects": "Объекты",
|
||||
"Entities": "Сущности",
|
||||
"Custom": "Другое",
|
||||
"0": "Ноль",
|
||||
"1": "Один",
|
||||
"2": "Два",
|
||||
|
||||
@@ -35,6 +35,7 @@ translators for the languages currently available:</p>
|
||||
<li><b>Norwegian:</b> Veronica Berglyd Olsen (vkbo)</li>
|
||||
<li><b>Polish:</b> Anna Maria Polak (Nauthiz)</li>
|
||||
<li><b>Portuguese:</b> Bruno Meneguello (bkmeneguello)</li>
|
||||
<li><b>Russian:</b> Konstantin Tan (k1kimosha)</li>
|
||||
<li><b>Simplified Chinese:</b> Qianzhi Long (longqzh)</li>
|
||||
</ul>
|
||||
|
||||
|
||||
@@ -29,5 +29,5 @@ helptext = 92, 92, 92
|
||||
fadedtext = 108, 108, 108
|
||||
errortext = 255, 92, 92
|
||||
statusnone = 120, 120, 120
|
||||
statussaved = 200, 15, 39
|
||||
statusunsaved = 2, 133, 37
|
||||
statussaved = 2, 133, 37
|
||||
statusunsaved = 200, 15, 39
|
||||
|
||||
@@ -279,6 +279,16 @@ def simplified(text: str) -> str:
|
||||
return " ".join(str(text).strip().split())
|
||||
|
||||
|
||||
def compact(text: str) -> str:
|
||||
"""Compact a string by removing spaces."""
|
||||
return "".join(str(text).split())
|
||||
|
||||
|
||||
def uniqueCompact(text: str) -> str:
|
||||
"""Return a unique, compact and sorted string."""
|
||||
return "".join(sorted(set(compact(text))))
|
||||
|
||||
|
||||
def elide(text: str, length: int) -> str:
|
||||
"""Elide a piece of text to a maximum length."""
|
||||
if len(text) > (cut := max(4, length)):
|
||||
|
||||
@@ -356,6 +356,7 @@ class NWProject:
|
||||
self.setProjectChanged(False)
|
||||
self._valid = True
|
||||
self._state = NWProjectState.READY
|
||||
self._storage.lockSession() # Lock only after a successful open. See issue #1977.
|
||||
|
||||
SHARED.newStatusMessage(self.tr("Opened Project: {0}").format(self._data.name))
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ class NWStatus:
|
||||
return key
|
||||
|
||||
def update(self, update: list[tuple[str | None, StatusEntry]]) -> None:
|
||||
"""Update the list of statuses, and from removed list."""
|
||||
"""Update the list of statuses."""
|
||||
self._store.clear()
|
||||
for key, entry in update:
|
||||
self._store[self._checkKey(key)] = entry
|
||||
@@ -130,6 +130,9 @@ class NWStatus:
|
||||
if self._default not in self._store:
|
||||
self._default = next(iter(self._store)) if self._store else None
|
||||
|
||||
# Emit the change signal
|
||||
SHARED.projectSingalProxy({"event": "statusLabels", "kind": self._prefix})
|
||||
|
||||
return
|
||||
|
||||
def check(self, value: str) -> str:
|
||||
|
||||
@@ -235,7 +235,6 @@ class NWStorage:
|
||||
if child.is_dir() and child.name.startswith("data_"):
|
||||
legacy.legacyDataFolder(basePath, child)
|
||||
|
||||
self._writeLockFile()
|
||||
self._ready = True
|
||||
|
||||
return NWStorageOpen.READY
|
||||
@@ -249,6 +248,12 @@ class NWStorage:
|
||||
return True
|
||||
return True
|
||||
|
||||
def lockSession(self) -> None:
|
||||
"""Lock the session when the project is successfully opened."""
|
||||
if self._ready:
|
||||
self._writeLockFile()
|
||||
return
|
||||
|
||||
def closeSession(self) -> None:
|
||||
"""Run tasks related to closing the session."""
|
||||
self._clearLockFile()
|
||||
|
||||
@@ -299,10 +299,10 @@ class ToHtml(Tokenizer):
|
||||
"<head>\n"
|
||||
"<meta charset='utf-8'>\n"
|
||||
"<title>{title:s}</title>\n"
|
||||
"</head>\n"
|
||||
"<style>\n"
|
||||
"{style:s}\n"
|
||||
"</style>\n"
|
||||
"</head>\n"
|
||||
"<body>\n"
|
||||
"<article>\n"
|
||||
"{body:s}\n"
|
||||
|
||||
@@ -832,7 +832,7 @@ class Tokenizer(ABC):
|
||||
sAlign |= self.A_IND_R
|
||||
|
||||
# Process formats
|
||||
tLine, tFmt = self._extractFormats(aLine)
|
||||
tLine, tFmt = self._extractFormats(aLine, hDialog=self._isNovel)
|
||||
tokens.append((
|
||||
self.T_TEXT, nHead, tLine, tFmt, sAlign
|
||||
))
|
||||
@@ -1098,8 +1098,13 @@ class Tokenizer(ABC):
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _extractFormats(self, text: str, skip: int = 0) -> tuple[str, T_Formats]:
|
||||
"""Extract format markers from a text paragraph."""
|
||||
def _extractFormats(
|
||||
self, text: str, skip: int = 0, hDialog: bool = False
|
||||
) -> tuple[str, T_Formats]:
|
||||
"""Extract format markers from a text paragraph. In order to
|
||||
also process dialogue highlighting, the hDialog flag must be set
|
||||
to True. See issues #2011 and #2013.
|
||||
"""
|
||||
temp: list[tuple[int, int, int, str]] = []
|
||||
|
||||
# Match Markdown
|
||||
@@ -1137,7 +1142,7 @@ class Tokenizer(ABC):
|
||||
))
|
||||
|
||||
# Match Dialogue
|
||||
if self._rxDialogue:
|
||||
if self._rxDialogue and hDialog:
|
||||
for regEx, fmtB, fmtE in self._rxDialogue:
|
||||
rxItt = regEx.globalMatch(text, 0)
|
||||
while rxItt.hasNext():
|
||||
@@ -1150,8 +1155,9 @@ class Tokenizer(ABC):
|
||||
formats = []
|
||||
for pos, n, fmt, key in reversed(sorted(temp, key=lambda x: x[0])):
|
||||
if fmt > 0:
|
||||
result = result[:pos] + result[pos+n:]
|
||||
formats = [(p-n, f, k) for p, f, k in formats]
|
||||
if n > 0:
|
||||
result = result[:pos] + result[pos+n:]
|
||||
formats = [(p-n if p > pos else p, f, k) for p, f, k in formats]
|
||||
formats.insert(0, (pos, fmt, key))
|
||||
|
||||
return result, formats
|
||||
|
||||
@@ -34,7 +34,7 @@ from PyQt5.QtWidgets import (
|
||||
)
|
||||
|
||||
from novelwriter import CONFIG, SHARED
|
||||
from novelwriter.common import describeFont
|
||||
from novelwriter.common import describeFont, uniqueCompact
|
||||
from novelwriter.constants import nwUnicode
|
||||
from novelwriter.dialogs.quotes import GuiQuoteSelect
|
||||
from novelwriter.extensions.configlayout import NColourLabel, NScrollableForm
|
||||
@@ -952,8 +952,8 @@ class GuiPreferences(NDialog):
|
||||
# Text Highlighting
|
||||
dialogueStyle = self.dialogStyle.currentData()
|
||||
allowOpenDial = self.allowOpenDial.isChecked()
|
||||
narratorBreak = self.narratorBreak.text()
|
||||
dialogueLine = self.dialogLine.text()
|
||||
narratorBreak = self.narratorBreak.text().strip()
|
||||
dialogueLine = self.dialogLine.text().strip()
|
||||
altDialogOpen = self.altDialogOpen.text()
|
||||
altDialogClose = self.altDialogClose.text()
|
||||
highlightEmph = self.highlightEmph.isChecked()
|
||||
@@ -983,8 +983,8 @@ class GuiPreferences(NDialog):
|
||||
CONFIG.doReplaceDQuote = self.doReplaceDQuote.isChecked()
|
||||
CONFIG.doReplaceDash = self.doReplaceDash.isChecked()
|
||||
CONFIG.doReplaceDots = self.doReplaceDots.isChecked()
|
||||
CONFIG.fmtPadBefore = self.fmtPadBefore.text().strip()
|
||||
CONFIG.fmtPadAfter = self.fmtPadAfter.text().strip()
|
||||
CONFIG.fmtPadBefore = uniqueCompact(self.fmtPadBefore.text())
|
||||
CONFIG.fmtPadAfter = uniqueCompact(self.fmtPadAfter.text())
|
||||
CONFIG.fmtPadThin = self.fmtPadThin.isChecked()
|
||||
|
||||
# Quotation Style
|
||||
|
||||
@@ -58,7 +58,7 @@ class GuiProjectSettings(NDialog):
|
||||
PAGE_IMPORT = 2
|
||||
PAGE_REPLACE = 3
|
||||
|
||||
newProjectSettingsReady = pyqtSignal(bool)
|
||||
newProjectSettingsReady = pyqtSignal()
|
||||
|
||||
def __init__(self, parent: QWidget, gotoPage: int = PAGE_SETTINGS) -> None:
|
||||
super().__init__(parent=parent)
|
||||
@@ -175,7 +175,7 @@ class GuiProjectSettings(NDialog):
|
||||
projAuthor = self.settingsPage.projAuthor.text()
|
||||
projLang = self.settingsPage.projLang.currentData()
|
||||
spellLang = self.settingsPage.spellLang.currentData()
|
||||
doBackup = not self.settingsPage.doBackup.isChecked()
|
||||
doBackup = not self.settingsPage.noBackup.isChecked()
|
||||
|
||||
project.data.setName(projName)
|
||||
project.data.setAuthor(projAuthor)
|
||||
@@ -183,23 +183,19 @@ class GuiProjectSettings(NDialog):
|
||||
project.data.setSpellLang(spellLang)
|
||||
project.setProjectLang(projLang)
|
||||
|
||||
rebuildTrees = False
|
||||
|
||||
if self.statusPage.changed:
|
||||
logger.debug("Updating status labels")
|
||||
project.data.itemStatus.update(self.statusPage.getNewList())
|
||||
rebuildTrees = True
|
||||
|
||||
if self.importPage.changed:
|
||||
logger.debug("Updating importance labels")
|
||||
project.data.itemImport.update(self.importPage.getNewList())
|
||||
rebuildTrees = True
|
||||
|
||||
if self.replacePage.changed:
|
||||
logger.debug("Updating auto-replace settings")
|
||||
project.data.setAutoReplace(self.replacePage.getNewList())
|
||||
|
||||
self.newProjectSettingsReady.emit(rebuildTrees)
|
||||
self.newProjectSettingsReady.emit()
|
||||
QApplication.processEvents()
|
||||
self.close()
|
||||
|
||||
@@ -289,10 +285,10 @@ class _SettingsPage(NScrollableForm):
|
||||
self.spellLang.setCurrentIndex(idx)
|
||||
|
||||
# Backup on Close
|
||||
self.doBackup = NSwitch(self)
|
||||
self.doBackup.setChecked(not data.doBackup)
|
||||
self.noBackup = NSwitch(self)
|
||||
self.noBackup.setChecked(not data.doBackup)
|
||||
self.addRow(
|
||||
self.tr("Disable backup on close"), self.doBackup,
|
||||
self.tr("Disable backup on close"), self.noBackup,
|
||||
self.tr("Overrides main preferences.")
|
||||
)
|
||||
|
||||
|
||||
@@ -229,15 +229,13 @@ class GuiDocViewer(QTextBrowser):
|
||||
QApplication.restoreOverrideCursor()
|
||||
return False
|
||||
|
||||
# Refresh the tab stops
|
||||
self.setTabStopDistance(CONFIG.getTabWidth())
|
||||
|
||||
# Must be before setHtml
|
||||
# Must be before setDocument
|
||||
if updateHistory:
|
||||
self.docHistory.append(tHandle)
|
||||
|
||||
self.setDocumentTitle(tHandle)
|
||||
self.setDocument(qDoc.document)
|
||||
self.setTabStopDistance(CONFIG.getTabWidth())
|
||||
|
||||
if self._docHandle == tHandle:
|
||||
# This is a refresh, so we set the scrollbar back to where it was
|
||||
|
||||
@@ -189,6 +189,13 @@ class GuiDocViewerPanel(QWidget):
|
||||
self._updateTabVisibility()
|
||||
return
|
||||
|
||||
@pyqtSlot(str)
|
||||
def updateStatusLabels(self, kind: str) -> None:
|
||||
"""Update the importance labels."""
|
||||
if kind == "i":
|
||||
self._loadAllTags()
|
||||
return
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
##
|
||||
|
||||
@@ -166,6 +166,7 @@ class GuiProjectView(QWidget):
|
||||
|
||||
def openProjectTasks(self) -> None:
|
||||
"""Run open project tasks."""
|
||||
self.populateTree()
|
||||
self.projBar.buildQuickLinksMenu()
|
||||
self.projBar.setEnabled(True)
|
||||
return
|
||||
@@ -213,7 +214,8 @@ class GuiProjectView(QWidget):
|
||||
@pyqtSlot(str)
|
||||
def updateItemValues(self, tHandle: str) -> None:
|
||||
"""Update tree item."""
|
||||
self.projTree.setTreeItemValues(tHandle)
|
||||
if nwItem := SHARED.project.tree[tHandle]:
|
||||
self.projTree.setTreeItemValues(nwItem)
|
||||
return
|
||||
|
||||
@pyqtSlot(str)
|
||||
@@ -242,6 +244,12 @@ class GuiProjectView(QWidget):
|
||||
self.projTree.createNewNote(tag, itemClass)
|
||||
return
|
||||
|
||||
@pyqtSlot(str)
|
||||
def refreshUserLabels(self, kind: str) -> None:
|
||||
"""Refresh status or importance labels."""
|
||||
self.projTree.refreshUserLabels(kind)
|
||||
return
|
||||
|
||||
|
||||
class GuiProjectToolBar(QWidget):
|
||||
|
||||
@@ -792,11 +800,11 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
def renameTreeItem(self, tHandle: str, name: str = "") -> None:
|
||||
"""Open a dialog to edit the label of an item."""
|
||||
if tItem := SHARED.project.tree[tHandle]:
|
||||
newLabel, dlgOk = GuiEditLabel.getLabel(self, text=name or tItem.itemName)
|
||||
if nwItem := SHARED.project.tree[tHandle]:
|
||||
newLabel, dlgOk = GuiEditLabel.getLabel(self, text=name or nwItem.itemName)
|
||||
if dlgOk:
|
||||
tItem.setName(newLabel)
|
||||
self.setTreeItemValues(tHandle)
|
||||
nwItem.setName(newLabel)
|
||||
self.setTreeItemValues(nwItem)
|
||||
self._alertTreeChange(tHandle, flush=False)
|
||||
return
|
||||
|
||||
@@ -992,7 +1000,9 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
self.propagateCount(tHandle, 0)
|
||||
|
||||
trItemP = trItemS.parent()
|
||||
# If a non-root item ends up on root due to a bug, allow it
|
||||
# to still be deleted from invisible root (see #2108)
|
||||
trItemP = trItemS.parent() or self.invisibleRootItem()
|
||||
tIndex = trItemP.indexOfChild(trItemS)
|
||||
trItemP.takeChild(tIndex)
|
||||
|
||||
@@ -1010,44 +1020,52 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
return True
|
||||
|
||||
def setTreeItemValues(self, tHandle: str) -> None:
|
||||
"""Set the name and flag values for a tree item from a handle in
|
||||
the project tree. Does not trigger a tree change as the data is
|
||||
already coming from the project tree.
|
||||
def refreshUserLabels(self, kind: str) -> None:
|
||||
"""Refresh status or importance labels."""
|
||||
if kind == "s":
|
||||
for nwItem in SHARED.project.tree:
|
||||
if nwItem.isNovelLike():
|
||||
self.setTreeItemValues(nwItem)
|
||||
elif kind == "i":
|
||||
for nwItem in SHARED.project.tree:
|
||||
if not nwItem.isNovelLike():
|
||||
self.setTreeItemValues(nwItem)
|
||||
return
|
||||
|
||||
def setTreeItemValues(self, nwItem: NWItem | None) -> None:
|
||||
"""Set the name and flag values for a tree item in the project
|
||||
tree. Does not trigger a tree change as the data is already
|
||||
coming from project data.
|
||||
"""
|
||||
trItem = self._getTreeItem(tHandle)
|
||||
nwItem = SHARED.project.tree[tHandle]
|
||||
if trItem is None or nwItem is None:
|
||||
return
|
||||
if isinstance(nwItem, NWItem) and (trItem := self._getTreeItem(nwItem.itemHandle)):
|
||||
itemStatus, statusIcon = nwItem.getImportStatus()
|
||||
hLevel = nwItem.mainHeading
|
||||
itemIcon = SHARED.theme.getItemIcon(
|
||||
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel
|
||||
)
|
||||
|
||||
itemStatus, statusIcon = nwItem.getImportStatus()
|
||||
hLevel = nwItem.mainHeading
|
||||
itemIcon = SHARED.theme.getItemIcon(
|
||||
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel
|
||||
)
|
||||
trItem.setIcon(self.C_NAME, itemIcon)
|
||||
trItem.setText(self.C_NAME, nwItem.itemName)
|
||||
trItem.setIcon(self.C_STATUS, statusIcon)
|
||||
trItem.setToolTip(self.C_STATUS, itemStatus)
|
||||
|
||||
trItem.setIcon(self.C_NAME, itemIcon)
|
||||
trItem.setText(self.C_NAME, nwItem.itemName)
|
||||
trItem.setIcon(self.C_STATUS, statusIcon)
|
||||
trItem.setToolTip(self.C_STATUS, itemStatus)
|
||||
if nwItem.isFileType():
|
||||
iconName = "checked" if nwItem.isActive else "unchecked"
|
||||
toolTip = self.trActive if nwItem.isActive else self.trInactive
|
||||
trItem.setToolTip(self.C_ACTIVE, toolTip)
|
||||
else:
|
||||
iconName = "noncheckable"
|
||||
|
||||
if nwItem.isFileType():
|
||||
iconName = "checked" if nwItem.isActive else "unchecked"
|
||||
toolTip = self.trActive if nwItem.isActive else self.trInactive
|
||||
trItem.setToolTip(self.C_ACTIVE, toolTip)
|
||||
else:
|
||||
iconName = "noncheckable"
|
||||
trItem.setIcon(self.C_ACTIVE, SHARED.theme.getIcon(iconName))
|
||||
|
||||
trItem.setIcon(self.C_ACTIVE, SHARED.theme.getIcon(iconName))
|
||||
if CONFIG.emphLabels and nwItem.isDocumentLayout():
|
||||
trFont = trItem.font(self.C_NAME)
|
||||
trFont.setBold(hLevel == "H1" or hLevel == "H2")
|
||||
trFont.setUnderline(hLevel == "H1")
|
||||
trItem.setFont(self.C_NAME, trFont)
|
||||
|
||||
if CONFIG.emphLabels and nwItem.isDocumentLayout():
|
||||
trFont = trItem.font(self.C_NAME)
|
||||
trFont.setBold(hLevel == "H1" or hLevel == "H2")
|
||||
trFont.setUnderline(hLevel == "H1")
|
||||
trItem.setFont(self.C_NAME, trFont)
|
||||
|
||||
# Emit Refresh Signal
|
||||
self.itemRefreshed.emit(tHandle, nwItem, itemIcon)
|
||||
# Emit Refresh Signal
|
||||
self.itemRefreshed.emit(nwItem.itemHandle, nwItem, itemIcon)
|
||||
|
||||
return
|
||||
|
||||
@@ -1299,9 +1317,11 @@ class GuiProjectTree(QTreeWidget):
|
||||
action is allowed, and update relevant data.
|
||||
"""
|
||||
tItem = self.itemAt(event.pos())
|
||||
dropOn = self.dropIndicatorPosition() == QAbstractItemView.DropIndicatorPosition.OnItem
|
||||
onItem = self.dropIndicatorPosition() == QAbstractItemView.DropIndicatorPosition.OnItem
|
||||
onView = self.dropIndicatorPosition() == QAbstractItemView.DropIndicatorPosition.OnViewport
|
||||
# Make sure nothing can be dropped on invisible root (see #1569)
|
||||
if not tItem or tItem.parent() is None and not dropOn:
|
||||
# Make sure nothing can be dropped on the viewport (see #2108)
|
||||
if tItem is None or (tItem.parent() is None and not onItem) or onView:
|
||||
logger.error("Invalid drop location")
|
||||
event.ignore()
|
||||
return
|
||||
@@ -1353,7 +1373,8 @@ class GuiProjectTree(QTreeWidget):
|
||||
SHARED.project.index.deleteHandle(mHandle)
|
||||
else:
|
||||
SHARED.project.index.reIndexHandle(mHandle)
|
||||
self.setTreeItemValues(mHandle)
|
||||
if mItem := SHARED.project.tree[mHandle]:
|
||||
self.setTreeItemValues(mItem)
|
||||
|
||||
# Update word count
|
||||
self.propagateCount(tHandle, nwItemS.wordCount, countChildren=True)
|
||||
@@ -1594,7 +1615,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
self._treeMap[tHandle] = newItem
|
||||
self.propagateCount(tHandle, nwItem.wordCount, countChildren=True)
|
||||
self.setTreeItemValues(tHandle)
|
||||
self.setTreeItemValues(nwItem)
|
||||
newItem.setExpanded(nwItem.isExpanded)
|
||||
|
||||
return newItem
|
||||
@@ -1971,7 +1992,7 @@ class _TreeContextMenu(QMenu):
|
||||
def _toggleItemActive(self) -> None:
|
||||
"""Toggle the active status of an item."""
|
||||
self._item.setActive(not self._item.isActive)
|
||||
self.projTree.setTreeItemValues(self._handle)
|
||||
self.projTree.setTreeItemValues(self._item)
|
||||
self.projTree._alertTreeChange(self._handle, flush=False)
|
||||
return
|
||||
|
||||
@@ -1984,14 +2005,14 @@ class _TreeContextMenu(QMenu):
|
||||
for tItem in self._items:
|
||||
if tItem and tItem.isFileType():
|
||||
tItem.setActive(isActive)
|
||||
self.projTree.setTreeItemValues(tItem.itemHandle)
|
||||
self.projTree.setTreeItemValues(tItem)
|
||||
self.projTree._alertTreeChange(tItem.itemHandle, flush=False)
|
||||
return
|
||||
|
||||
def _changeItemStatus(self, key: str) -> None:
|
||||
"""Set a new status value of an item."""
|
||||
self._item.setStatus(key)
|
||||
self.projTree.setTreeItemValues(self._handle)
|
||||
self.projTree.setTreeItemValues(self._item)
|
||||
self.projTree._alertTreeChange(self._handle, flush=False)
|
||||
return
|
||||
|
||||
@@ -2000,14 +2021,14 @@ class _TreeContextMenu(QMenu):
|
||||
for tItem in self._items:
|
||||
if tItem and tItem.isNovelLike():
|
||||
tItem.setStatus(key)
|
||||
self.projTree.setTreeItemValues(tItem.itemHandle)
|
||||
self.projTree.setTreeItemValues(tItem)
|
||||
self.projTree._alertTreeChange(tItem.itemHandle, flush=False)
|
||||
return
|
||||
|
||||
def _changeItemImport(self, key: str) -> None:
|
||||
"""Set a new importance value of an item."""
|
||||
self._item.setImport(key)
|
||||
self.projTree.setTreeItemValues(self._handle)
|
||||
self.projTree.setTreeItemValues(self._item)
|
||||
self.projTree._alertTreeChange(self._handle, flush=False)
|
||||
return
|
||||
|
||||
@@ -2016,7 +2037,7 @@ class _TreeContextMenu(QMenu):
|
||||
for tItem in self._items:
|
||||
if tItem and not tItem.isNovelLike():
|
||||
tItem.setImport(key)
|
||||
self.projTree.setTreeItemValues(tItem.itemHandle)
|
||||
self.projTree.setTreeItemValues(tItem)
|
||||
self.projTree._alertTreeChange(tItem.itemHandle, flush=False)
|
||||
return
|
||||
|
||||
@@ -2024,11 +2045,11 @@ class _TreeContextMenu(QMenu):
|
||||
"""Set a new item layout value of an item."""
|
||||
if itemLayout == nwItemLayout.DOCUMENT and self._item.documentAllowed():
|
||||
self._item.setLayout(nwItemLayout.DOCUMENT)
|
||||
self.projTree.setTreeItemValues(self._handle)
|
||||
self.projTree.setTreeItemValues(self._item)
|
||||
self.projTree._alertTreeChange(self._handle, flush=False)
|
||||
elif itemLayout == nwItemLayout.NOTE:
|
||||
self._item.setLayout(nwItemLayout.NOTE)
|
||||
self.projTree.setTreeItemValues(self._handle)
|
||||
self.projTree.setTreeItemValues(self._item)
|
||||
self.projTree._alertTreeChange(self._handle, flush=False)
|
||||
return
|
||||
|
||||
@@ -2042,12 +2063,12 @@ class _TreeContextMenu(QMenu):
|
||||
if msgYes and itemLayout == nwItemLayout.DOCUMENT and self._item.documentAllowed():
|
||||
self._item.setType(nwItemType.FILE)
|
||||
self._item.setLayout(nwItemLayout.DOCUMENT)
|
||||
self.projTree.setTreeItemValues(self._handle)
|
||||
self.projTree.setTreeItemValues(self._item)
|
||||
self.projTree._alertTreeChange(self._handle, flush=False)
|
||||
elif msgYes and itemLayout == nwItemLayout.NOTE:
|
||||
self._item.setType(nwItemType.FILE)
|
||||
self._item.setLayout(nwItemLayout.NOTE)
|
||||
self.projTree.setTreeItemValues(self._handle)
|
||||
self.projTree.setTreeItemValues(self._item)
|
||||
self.projTree._alertTreeChange(self._handle, flush=False)
|
||||
else:
|
||||
logger.info("Folder conversion cancelled")
|
||||
|
||||
@@ -189,9 +189,6 @@ class GuiMain(QMainWindow):
|
||||
self.splitView.setVisible(False)
|
||||
self.docEditor.closeSearch()
|
||||
|
||||
# Initialise the Project Tree
|
||||
self.rebuildTrees()
|
||||
|
||||
# Assemble Main Window Elements
|
||||
self.mainBox = QHBoxLayout()
|
||||
self.mainBox.addWidget(self.sideBar)
|
||||
@@ -222,6 +219,8 @@ class GuiMain(QMainWindow):
|
||||
SHARED.projectStatusChanged.connect(self.mainStatus.updateProjectStatus)
|
||||
SHARED.projectStatusMessage.connect(self.mainStatus.setStatusMessage)
|
||||
SHARED.spellLanguageChanged.connect(self.mainStatus.setLanguage)
|
||||
SHARED.statusLabelsChanged.connect(self.docViewerPanel.updateStatusLabels)
|
||||
SHARED.statusLabelsChanged.connect(self.projView.refreshUserLabels)
|
||||
|
||||
self.mainMenu.requestDocAction.connect(self._passDocumentAction)
|
||||
self.mainMenu.requestDocInsert.connect(self._passDocumentInsert)
|
||||
@@ -461,7 +460,6 @@ class GuiMain(QMainWindow):
|
||||
|
||||
# Update GUI
|
||||
self._updateWindowTitle(SHARED.project.data.name)
|
||||
self.rebuildTrees()
|
||||
self.docEditor.toggleSpellCheck(SHARED.project.data.spellCheck)
|
||||
self.mainStatus.setRefTime(SHARED.project.projOpened)
|
||||
self.projView.openProjectTasks()
|
||||
@@ -526,8 +524,14 @@ class GuiMain(QMainWindow):
|
||||
self.novelView.setActiveHandle(None)
|
||||
return
|
||||
|
||||
def openDocument(self, tHandle: str | None, tLine: int | None = None,
|
||||
changeFocus: bool = True, doScroll: bool = False) -> bool:
|
||||
def openDocument(
|
||||
self,
|
||||
tHandle: str | None,
|
||||
tLine: int | None = None,
|
||||
sTitle: str | None = None,
|
||||
changeFocus: bool = True,
|
||||
doScroll: bool = False
|
||||
) -> bool:
|
||||
"""Open a specific document, optionally at a given line."""
|
||||
if not SHARED.hasProject:
|
||||
logger.error("No project open")
|
||||
@@ -537,9 +541,12 @@ class GuiMain(QMainWindow):
|
||||
logger.debug("Requested item '%s' is not a document", tHandle)
|
||||
return False
|
||||
|
||||
if sTitle and tLine is None:
|
||||
if hItem := SHARED.project.index.getItemHeading(tHandle, sTitle):
|
||||
tLine = hItem.line
|
||||
|
||||
self._changeView(nwView.EDITOR)
|
||||
cHandle = self.docEditor.docHandle
|
||||
if cHandle == tHandle:
|
||||
if tHandle == self.docEditor.docHandle:
|
||||
self.docEditor.setCursorLine(tLine)
|
||||
if changeFocus:
|
||||
self.docEditor.setFocus()
|
||||
@@ -711,7 +718,6 @@ class GuiMain(QMainWindow):
|
||||
if SHARED.hasProject:
|
||||
tHandle = None
|
||||
sTitle = None
|
||||
tLine = None
|
||||
if self.projView.treeHasFocus():
|
||||
tHandle = self.projView.getSelectedHandle()
|
||||
elif self.novelView.treeHasFocus():
|
||||
@@ -722,19 +728,11 @@ class GuiMain(QMainWindow):
|
||||
logger.warning("No item selected")
|
||||
return
|
||||
|
||||
if tHandle and sTitle:
|
||||
if hItem := SHARED.project.index.getItemHeading(tHandle, sTitle):
|
||||
tLine = hItem.line
|
||||
if tHandle:
|
||||
self.openDocument(tHandle, tLine=tLine, changeFocus=False, doScroll=False)
|
||||
self.openDocument(tHandle, sTitle=sTitle, changeFocus=False, doScroll=False)
|
||||
|
||||
return
|
||||
|
||||
def rebuildTrees(self) -> None:
|
||||
"""Rebuild the project tree."""
|
||||
self.projView.populateTree()
|
||||
return
|
||||
|
||||
def rebuildIndex(self, beQuiet: bool = False) -> None:
|
||||
"""Rebuild the entire index."""
|
||||
if SHARED.hasProject:
|
||||
@@ -1093,15 +1091,13 @@ class GuiMain(QMainWindow):
|
||||
|
||||
return
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def _processProjectSettingsChanges(self, rebuildTrees: bool) -> None:
|
||||
@pyqtSlot()
|
||||
def _processProjectSettingsChanges(self) -> 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()
|
||||
@@ -1118,7 +1114,7 @@ class GuiMain(QMainWindow):
|
||||
tHandle, sTitle = self._getTagSource(tag)
|
||||
if tHandle is not None:
|
||||
if mode == nwDocMode.EDIT:
|
||||
self.openDocument(tHandle)
|
||||
self.openDocument(tHandle, sTitle=sTitle)
|
||||
elif mode == nwDocMode.VIEW:
|
||||
self.viewDocument(tHandle=tHandle, sTitle=sTitle)
|
||||
return
|
||||
@@ -1137,11 +1133,7 @@ class GuiMain(QMainWindow):
|
||||
"""Handle an open document request."""
|
||||
if tHandle is not None:
|
||||
if mode == nwDocMode.EDIT:
|
||||
tLine = None
|
||||
hItem = SHARED.project.index.getItemHeading(tHandle, sTitle)
|
||||
if hItem is not None:
|
||||
tLine = hItem.line
|
||||
self.openDocument(tHandle, tLine=tLine, changeFocus=setFocus)
|
||||
self.openDocument(tHandle, sTitle=sTitle, changeFocus=setFocus)
|
||||
elif mode == nwDocMode.VIEW:
|
||||
self.viewDocument(tHandle=tHandle, sTitle=sTitle)
|
||||
return
|
||||
|
||||
@@ -64,6 +64,7 @@ class SharedData(QObject):
|
||||
indexCleared = pyqtSignal()
|
||||
indexAvailable = pyqtSignal()
|
||||
mainClockTick = pyqtSignal()
|
||||
statusLabelsChanged = pyqtSignal(str)
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
@@ -309,6 +310,14 @@ class SharedData(QObject):
|
||||
self.indexAvailable.emit()
|
||||
return
|
||||
|
||||
def projectSingalProxy(self, data: dict) -> None:
|
||||
"""Emit signals on project data change."""
|
||||
event = data.get("event")
|
||||
logger.debug("Received '%s' event from project data", event)
|
||||
if event == "statusLabels":
|
||||
self.statusLabelsChanged.emit(data.get("kind", ""))
|
||||
return
|
||||
|
||||
##
|
||||
# Alert Boxes
|
||||
##
|
||||
|
||||
@@ -823,6 +823,7 @@ class _PreviewWidget(QTextBrowser):
|
||||
|
||||
document.setDocumentMargin(CONFIG.getTextMargin())
|
||||
self.setDocument(document)
|
||||
self.setTabStopDistance(CONFIG.getTabWidth())
|
||||
|
||||
self._docTime = int(time())
|
||||
self._updateBuildAge()
|
||||
|
||||
@@ -753,8 +753,8 @@ def buildForLaunchpad(args: argparse.Namespace) -> None:
|
||||
|
||||
distLoop = [
|
||||
("22.04", "jammy", True),
|
||||
("23.10", "mantic", False),
|
||||
("24.04", "noble", False),
|
||||
("24.10", "oracular", False),
|
||||
]
|
||||
|
||||
print("Building Ubuntu packages for:")
|
||||
|
||||
@@ -16,6 +16,7 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="2.5" hexVersion="0x020500f0" fileVersion="1.5" fileRevision="4" timeStamp="2024-07-10 16:23:02">
|
||||
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1950" autoCount="277" editTime="90880">
|
||||
<novelWriterXML appVersion="2.5.3" hexVersion="0x020503f0" fileVersion="1.5" fileRevision="4" timeStamp="2024-11-26 22:14:35">
|
||||
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1958" autoCount="277" editTime="91015">
|
||||
<name>Sample Project</name>
|
||||
<author>Jane Smith</author>
|
||||
</project>
|
||||
@@ -118,7 +118,7 @@
|
||||
<name status="sf12341" import="i2d7a54" active="yes">Earth</name>
|
||||
</item>
|
||||
<item handle="f1471bef9f2ae" parent="15c4492bd5107" root="15c4492bd5107" order="1" type="FILE" class="WORLD" layout="NOTE">
|
||||
<meta expanded="no" heading="H1" charCount="115" wordCount="24" paraCount="1" cursorPos="135" />
|
||||
<meta expanded="no" heading="H1" charCount="115" wordCount="24" paraCount="1" cursorPos="0" />
|
||||
<name status="sf12341" import="i4a1d39" active="yes">Space</name>
|
||||
</item>
|
||||
<item handle="5eaea4e8cdee8" parent="15c4492bd5107" root="15c4492bd5107" order="2" type="FILE" class="WORLD" layout="NOTE">
|
||||
|
||||
@@ -15,6 +15,7 @@ classifiers =
|
||||
Programming Language :: Python :: 3.10
|
||||
Programming Language :: Python :: 3.11
|
||||
Programming Language :: Python :: 3.12
|
||||
Programming Language :: Python :: 3.13
|
||||
Programming Language :: Python :: Implementation :: CPython
|
||||
License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
||||
Development Status :: 5 - Production/Stable
|
||||
|
||||
BIN
setup/signpath_logo.png
Normal file
BIN
setup/signpath_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<title>Lorem Ipsum</title>
|
||||
</head>
|
||||
<style>
|
||||
body {font-family: 'Arial'; font-size: 12pt; font-weight: 400; font-style: normal;}
|
||||
p {text-align: justify; line-height: 150%; margin-top: 0.00em; margin-bottom: 0.76em;}
|
||||
@@ -22,6 +21,7 @@ mark {background: rgb(255, 255, 166);}
|
||||
.dialog {color: rgb(66, 113, 174);}
|
||||
.altdialog {color: rgb(129, 55, 9);}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<article>
|
||||
<h1 class='title' style='text-align: center;'>Lorem Ipsum</h1>
|
||||
|
||||
@@ -32,12 +32,12 @@ from PyQt5.QtGui import QColor, QDesktopServices, QFontDatabase
|
||||
|
||||
from novelwriter.common import (
|
||||
NWConfigParser, checkBool, checkFloat, checkInt, checkIntTuple, checkPath,
|
||||
checkString, checkStringNone, checkUuid, cssCol, describeFont, elide,
|
||||
formatFileFilter, formatInt, formatTime, formatTimeStamp, formatVersion,
|
||||
fuzzyTime, getFileSize, hexToInt, isHandle, isItemClass, isItemLayout,
|
||||
isItemType, isListInstance, isTitleTag, jsonEncode, makeFileNameSafe,
|
||||
minmax, numberToRoman, openExternalPath, readTextFile, simplified,
|
||||
transferCase, xmlIndent, yesNo
|
||||
checkString, checkStringNone, checkUuid, compact, cssCol, describeFont,
|
||||
elide, formatFileFilter, formatInt, formatTime, formatTimeStamp,
|
||||
formatVersion, fuzzyTime, getFileSize, hexToInt, isHandle, isItemClass,
|
||||
isItemLayout, isItemType, isListInstance, isTitleTag, jsonEncode,
|
||||
makeFileNameSafe, minmax, numberToRoman, openExternalPath, readTextFile,
|
||||
simplified, transferCase, uniqueCompact, xmlIndent, yesNo
|
||||
)
|
||||
|
||||
from tests.mocked import causeOSError
|
||||
@@ -346,6 +346,27 @@ def testBaseCommon_simplified():
|
||||
assert simplified("\tHello\n\r\tWorld") == "Hello World"
|
||||
|
||||
|
||||
@pytest.mark.base
|
||||
def testBaseCommon_compact():
|
||||
"""Test the compact function."""
|
||||
assert compact("! ! !") == "!!!"
|
||||
assert compact("1\t2\t3") == "123"
|
||||
assert compact("1\n2\n3") == "123"
|
||||
assert compact("1\r2\r3") == "123"
|
||||
assert compact("1\u00a02\u00a03") == "123"
|
||||
|
||||
|
||||
@pytest.mark.base
|
||||
def testBaseCommon_uniqueCompact():
|
||||
"""Test the uniqueCompact function."""
|
||||
assert uniqueCompact("! ! !") == "!"
|
||||
assert uniqueCompact("1\t2\t3") == "123"
|
||||
assert uniqueCompact("1\n2\n3") == "123"
|
||||
assert uniqueCompact("1\r2\r3") == "123"
|
||||
assert uniqueCompact("1\u00a02\u00a03") == "123"
|
||||
assert uniqueCompact("3 2 1") == "123"
|
||||
|
||||
|
||||
@pytest.mark.base
|
||||
def testBaseCommon_elide():
|
||||
"""Test the elide function."""
|
||||
|
||||
@@ -142,7 +142,9 @@ def testCoreStorage_InitProjectStorage(monkeypatch, mockGUI, fncPath, mockRnd):
|
||||
storage.clear()
|
||||
|
||||
# Open twice, where second should fail due to lockfile
|
||||
# Note that locking is only possible after a successful open
|
||||
assert storage.initProjectStorage(fncPath) == NWStorageOpen.READY
|
||||
storage.lockSession()
|
||||
assert storage.initProjectStorage(fncPath) == NWStorageOpen.LOCKED
|
||||
assert isinstance(storage.lockStatus, list)
|
||||
assert len(storage.lockStatus) == 4
|
||||
|
||||
@@ -628,10 +628,10 @@ def testCoreToHtml_Save(mockGUI, fncPath):
|
||||
"<head>\n"
|
||||
"<meta charset='utf-8'>\n"
|
||||
"<title></title>\n"
|
||||
"</head>\n"
|
||||
"<style>\n"
|
||||
"{htmlStyle:s}\n"
|
||||
"</style>\n"
|
||||
"</head>\n"
|
||||
"<body>\n"
|
||||
"<article>\n"
|
||||
"{bodyText:s}\n"
|
||||
|
||||
@@ -1100,6 +1100,7 @@ def testCoreToken_Dialogue(mockGUI):
|
||||
project = NWProject()
|
||||
tokens = BareTokenizer(project)
|
||||
tokens.setDialogueHighlight(True)
|
||||
tokens._isNovel = True
|
||||
|
||||
# Single quotes
|
||||
tokens._text = "Text with \u2018dialogue one,\u2019 and \u2018dialogue two.\u2019\n"
|
||||
@@ -1161,6 +1162,24 @@ def testCoreToken_Dialogue(mockGUI):
|
||||
Tokenizer.A_NONE
|
||||
)]
|
||||
|
||||
# Special Cases
|
||||
# =============
|
||||
|
||||
# Dialogue + formatting on same index (Issue #2012)
|
||||
tokens._text = "[i]\u201cDialogue text.\u201d[/i]\n"
|
||||
tokens.tokenizeText()
|
||||
assert tokens._tokens == [(
|
||||
Tokenizer.T_TEXT, 0,
|
||||
"\u201cDialogue text.\u201d",
|
||||
[
|
||||
(0, Tokenizer.FMT_I_B, ""),
|
||||
(0, Tokenizer.FMT_DL_B, ""),
|
||||
(16, Tokenizer.FMT_I_E, ""),
|
||||
(16, Tokenizer.FMT_DL_E, ""),
|
||||
],
|
||||
Tokenizer.A_NONE
|
||||
)]
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreToken_SpecialFormat(mockGUI):
|
||||
|
||||
@@ -379,7 +379,7 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, tstPaths):
|
||||
assert CONFIG.doReplaceDQuote is False
|
||||
assert CONFIG.doReplaceDash is False
|
||||
assert CONFIG.doReplaceDots is False
|
||||
assert CONFIG.fmtPadBefore == "!?:"
|
||||
assert CONFIG.fmtPadBefore == "!:?"
|
||||
assert CONFIG.fmtPadAfter == "¡¿"
|
||||
assert CONFIG.fmtPadThin is True
|
||||
|
||||
|
||||
@@ -113,13 +113,13 @@ def testDlgProjSettings_SettingsPage(qtbot, monkeypatch, nwGUI, fncPath, projPat
|
||||
assert settings.projAuthor.text() == "Jane Smith"
|
||||
assert settings.projLang.currentData() == "en"
|
||||
assert settings.spellLang.currentData() == "en"
|
||||
assert settings.doBackup.isChecked() is False
|
||||
assert settings.noBackup.isChecked() is False
|
||||
|
||||
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)
|
||||
settings.noBackup.setChecked(True)
|
||||
|
||||
projSettings._doSave()
|
||||
assert project.data.name == "Project Name"
|
||||
@@ -128,7 +128,7 @@ def testDlgProjSettings_SettingsPage(qtbot, monkeypatch, nwGUI, fncPath, projPat
|
||||
assert project.data.spellLang == "de"
|
||||
assert project.data.doBackup is False
|
||||
|
||||
nwGUI._processProjectSettingsChanges(False)
|
||||
nwGUI._processProjectSettingsChanges()
|
||||
assert nwGUI.windowTitle() == "Project Name - novelWriter"
|
||||
|
||||
# qtbot.stop()
|
||||
@@ -161,7 +161,7 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, projPath, mockRn
|
||||
project.tree[hCharNote].setImport(C.iMajor) # type: ignore
|
||||
project.tree[hWorldNote].setImport(C.iMain) # type: ignore
|
||||
|
||||
nwGUI.rebuildTrees()
|
||||
nwGUI.projView.populateTree()
|
||||
project.countStatus()
|
||||
|
||||
assert [e.count for _, e in project.data.itemStatus.iterItems()] == [2, 0, 2, 1]
|
||||
|
||||
@@ -221,8 +221,14 @@ def testGuiViewerPanel_Tags(qtbot, monkeypatch, caplog, nwGUI, projPath, mockRnd
|
||||
nwJohn = SHARED.project.tree[hJohn]
|
||||
assert isinstance(nwJohn, NWItem)
|
||||
nwJohn.setActive(False)
|
||||
projTree.setTreeItemValues(hJohn)
|
||||
projTree.setTreeItemValues(nwJohn)
|
||||
projTree._alertTreeChange(hJohn, flush=False)
|
||||
assert charTab.topLevelItemCount() == 1
|
||||
|
||||
# Update Labels
|
||||
assert charTab.topLevelItem(0).text(charTab.C_IMPORT) == "New"
|
||||
SHARED.project.data.itemImport.add(C.iNew, "Stuff", (100, 100, 100), "SQUARE", 0)
|
||||
viewPanel.updateStatusLabels("i")
|
||||
assert charTab.topLevelItem(0).text(charTab.C_IMPORT) == "Stuff"
|
||||
|
||||
# qtbot.stop()
|
||||
|
||||
@@ -39,6 +39,7 @@ from novelwriter.gui.outline import GuiOutlineView
|
||||
from novelwriter.gui.projtree import GuiProjectTree
|
||||
from novelwriter.tools.welcome import GuiWelcome
|
||||
|
||||
from tests.mocked import causeOSError
|
||||
from tests.tools import NWD_IGNORE, XML_IGNORE, C, buildTestProject, cmpFiles
|
||||
|
||||
KEY_DELAY = 1
|
||||
@@ -651,7 +652,7 @@ def testGuiMain_Viewing(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testGuiMain_Features(qtbot, nwGUI, projPath, mockRnd):
|
||||
def testGuiMain_Features(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
|
||||
"""Test various features of the main window."""
|
||||
buildTestProject(nwGUI, projPath)
|
||||
assert SHARED.focusMode is False
|
||||
@@ -672,6 +673,7 @@ def testGuiMain_Features(qtbot, nwGUI, projPath, mockRnd):
|
||||
|
||||
# Enable focus mode
|
||||
nwGUI.toggleFocusMode()
|
||||
assert SHARED.focusMode is True
|
||||
assert nwGUI.treePane.isVisible() is False
|
||||
assert nwGUI.mainStatus.isVisible() is False
|
||||
assert nwGUI.mainMenu.isVisible() is False
|
||||
@@ -680,12 +682,19 @@ def testGuiMain_Features(qtbot, nwGUI, projPath, mockRnd):
|
||||
|
||||
# Disable focus mode
|
||||
nwGUI.toggleFocusMode()
|
||||
assert SHARED.focusMode is False
|
||||
assert nwGUI.treePane.isVisible() is True
|
||||
assert nwGUI.mainStatus.isVisible() is True
|
||||
assert nwGUI.mainMenu.isVisible() is True
|
||||
assert nwGUI.sideBar.isVisible() is True
|
||||
assert nwGUI.splitView.isVisible() is True
|
||||
|
||||
# Closing editor disables focus mode
|
||||
nwGUI.toggleFocusMode()
|
||||
assert SHARED.focusMode is True
|
||||
nwGUI.closeDocument()
|
||||
assert SHARED.focusMode is False
|
||||
|
||||
# Full Screen Mode
|
||||
# ================
|
||||
|
||||
@@ -702,6 +711,17 @@ def testGuiMain_Features(qtbot, nwGUI, projPath, mockRnd):
|
||||
nwGUI.sideBar.mSettings.show()
|
||||
nwGUI.sideBar.mSettings.hide()
|
||||
|
||||
# Document Open Errors
|
||||
# ====================
|
||||
|
||||
# Cannot edit a folder
|
||||
assert nwGUI.openDocument(C.hChapterDir) is False
|
||||
|
||||
# Handle I/O error
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr("builtins.open", causeOSError)
|
||||
assert nwGUI.openDocument(C.hChapterDoc) is False
|
||||
|
||||
# qtbot.stop()
|
||||
|
||||
|
||||
|
||||
@@ -186,6 +186,10 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRn
|
||||
# Adding an invalid item directly to the tree should also fail
|
||||
assert projTree._addTreeItem(None) is None
|
||||
|
||||
# Setting values for a non-existing tree item should be handled
|
||||
projTree.setTreeItemValues(None)
|
||||
projTree.setTreeItemValues(C.hInvalid) # The function used to take handles
|
||||
|
||||
# Clean up
|
||||
# qtbot.stop()
|
||||
nwGUI.closeProject()
|
||||
|
||||
@@ -197,7 +197,7 @@ def buildTestProject(obj: object, projPath: Path) -> None:
|
||||
project._valid = True
|
||||
|
||||
if nwGUI is not None:
|
||||
nwGUI.rebuildTrees()
|
||||
nwGUI.projView.populateTree()
|
||||
|
||||
return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user