Merge branch 'main' into update_docs

This commit is contained in:
Veronica Berglyd Olsen
2024-07-09 19:58:44 +02:00
87 changed files with 19423 additions and 11704 deletions
-131
View File
@@ -1,131 +0,0 @@
name: Build
on: workflow_dispatch
jobs:
buildAssets:
runs-on: ubuntu-latest
steps:
- name: Python Setup
uses: actions/setup-python@v5
with:
python-version: "3.12"
architecture: x64
- name: Install Packages (apt)
run: |
sudo apt update
sudo apt install qttools5-dev-tools latexmk texlive texlive-latex-extra
- name: Checkout Source
uses: actions/checkout@v4
- name: Install Packages (pip)
run: pip install -r docs/source/requirements.txt
- name: Build Assets
run: python pkgutils.py qtlrelease sample manual
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: nw-assets
path: |
novelwriter/assets/sample.zip
novelwriter/assets/manual.pdf
novelwriter/assets/i18n/*.qm
if-no-files-found: error
retention-days: 14
buildLinux:
needs: buildAssets
runs-on: ubuntu-latest
env:
PYTHON_VERSION: "3.12"
LINUX_TAG: "manylinux_2_28_x86_64"
steps:
- name: Python Setup
uses: actions/setup-python@v5
with:
python-version: "3.11"
architecture: x64
- name: Install Packages (pip)
run: pip install python-appimage
- name: Checkout Source
uses: actions/checkout@v4
- name: Download Artifacts
uses: actions/download-artifact@v4
with:
name: nw-assets
path: novelwriter/assets
- name: Build AppImage
run: 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
if-no-files-found: error
retention-days: 14
buildMac-AMD64:
needs: buildAssets
# Stay on macos-12 due to https://github.com/create-dmg/create-dmg/issues/143
runs-on: macos-12
env:
PYTHON_VERSION: "3.12"
PACKAGE_ARCH: x86_64
MINICONDA_ARCH: x86_64
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download Artifacts
uses: actions/download-artifact@v4
with:
name: nw-assets
path: novelwriter/assets
- name: Build App Bundle
run: ./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
if-no-files-found: error
retention-days: 14
buildMac-M1:
needs: buildAssets
runs-on: macos-14
env:
PYTHON_VERSION: "3.12"
PACKAGE_ARCH: aarch64
MINICONDA_ARCH: arm64
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download Artifacts
uses: actions/download-artifact@v4
with:
name: nw-assets
path: novelwriter/assets
- name: Build App Bundle
run: ./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
if-no-files-found: error
retention-days: 14
+39
View File
@@ -0,0 +1,39 @@
name: BuildAssets
on: workflow_call
jobs:
buildAssets:
runs-on: ubuntu-latest
steps:
- name: Python Setup
uses: actions/setup-python@v5
with:
python-version: "3.12"
architecture: x64
- name: Install Packages (apt)
run: |
sudo apt update
sudo apt install qttools5-dev-tools latexmk texlive texlive-latex-extra
- name: Checkout Source
uses: actions/checkout@v4
- name: Install Packages (pip)
run: pip install -r docs/source/requirements.txt
- name: Build Assets
run: |
python pkgutils.py build-assets
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: nw-assets
path: |
novelwriter/assets/manual.pdf
novelwriter/assets/sample.zip
novelwriter/assets/i18n/*.qm
if-no-files-found: error
retention-days: 14
+43
View File
@@ -0,0 +1,43 @@
name: BuildLinux
on: workflow_dispatch
jobs:
buildAssets:
uses: ./.github/workflows/build_assets.yml
buildLinux-AppImage:
needs: buildAssets
runs-on: ubuntu-latest
env:
PYTHON_VERSION: "3.12"
LINUX_TAG: "manylinux_2_28_x86_64"
steps:
- name: Python Setup
uses: actions/setup-python@v5
with:
python-version: "3.11"
architecture: x64
- name: Install Packages (pip)
run: pip install python-appimage
- name: Checkout Source
uses: actions/checkout@v4
- name: Download Artifacts
uses: actions/download-artifact@v4
with:
name: nw-assets
path: novelwriter/assets
- name: Build AppImage
run: 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
if-no-files-found: error
retention-days: 14
+64
View File
@@ -0,0 +1,64 @@
name: BuildMacOS
on: workflow_dispatch
jobs:
buildAssets:
uses: ./.github/workflows/build_assets.yml
buildMac-AMD64:
needs: buildAssets
# Stay on macos-12 due to https://github.com/create-dmg/create-dmg/issues/143
runs-on: macos-12
env:
PYTHON_VERSION: "3.12"
PACKAGE_ARCH: x86_64
MINICONDA_ARCH: x86_64
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download Artifacts
uses: actions/download-artifact@v4
with:
name: nw-assets
path: novelwriter/assets
- name: Build App Bundle
run: ./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
if-no-files-found: error
retention-days: 14
buildMac-M1:
needs: buildAssets
runs-on: macos-14
env:
PYTHON_VERSION: "3.12"
PACKAGE_ARCH: aarch64
MINICONDA_ARCH: arm64
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download Artifacts
uses: actions/download-artifact@v4
with:
name: nw-assets
path: novelwriter/assets
- name: Build App Bundle
run: ./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
if-no-files-found: error
retention-days: 14
+37
View File
@@ -0,0 +1,37 @@
name: BuildWindows
on: workflow_dispatch
jobs:
buildAssets:
uses: ./.github/workflows/build_assets.yml
buildWin64:
needs: buildAssets
runs-on: windows-latest
steps:
- name: Python Setup
uses: actions/setup-python@v5
with:
python-version: "3.12"
architecture: x64
- name: Checkout Source
uses: actions/checkout@v4
- name: Download Artifacts
uses: actions/download-artifact@v4
with:
name: nw-assets
path: novelwriter/assets
- name: Build Setup Installer
run: python pkgutils.py build-win-exe
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: Win-Setup
path: dist/*.exe
if-no-files-found: error
retention-days: 14
+4 -1
View File
@@ -15,6 +15,7 @@ jobs:
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
fail-fast: false
runs-on: ubuntu-latest
steps:
- name: Python Setup
@@ -32,7 +33,9 @@ jobs:
run: |
pip install -U -r requirements.txt -r tests/requirements.txt
- name: Run Build Commands
run: python pkgutils.py qtlrelease sample
run: |
python pkgutils.py qtlrelease
python pkgutils.py sample
- name: Run Tests
run: |
export QT_QPA_PLATFORM=offscreen
+65
View File
@@ -1,5 +1,70 @@
# novelWriter Changelog
## Version 2.5 RC 1 [2024-06-22]
### Release Notes
This is a release candidate of the next release version, and is intended for testing purposes.
Please be careful when using this version on live writing projects, and make sure you take frequent
backups.
### Detailed Changelog
**Bugfixes**
* The Status bar LEDs are now properly updated when the theme changes. Issue #1893. PR #1906.
* All HTML tags are now properly closed at the end of each paragraph in HTML output. After
shortcodes were introduced, it was possible to leave formatting tags open. Issue #1919. PR #1926.
**Improvements**
* Move the first line indent setting from being an Open Document feature to a general build
settings feature that also applies to HTML, and make it visible in the Manuscript build tool
preview. Issue #1839 and #1858. PR #1898.
* Make sure the document in the editor is saved before the same document is opened in the viewer.
Issue #1884. PR #1902.
* The project name now appears before "novelWriter" in the main window title, which improves the
task bar label on at least Linux Mint Cinnamon, and Windows. Issue #1910. PR #1911.
* Dialogue highlighting now only applies to novel documents, not notes. It is also possible to
apply it to HTML and ODT manuscripts, and it also shows up in the preview and in the document
viewer. Issue #1774. PR #1908.
* The Welcome dialog and other tools that use the project name to generate files of folder names
are now less restrictive on what characters it allows in the file or folder names. Issue #1917.
PR #1922.
* Last used folder paths are now remembered individually for each tool or feature that requires
path input from the user. Issues #1930 and #1933. PR #1934.
* The Manuscript preview will now show line height as set in build settings. Issue #1920. PR #1935.
* Global search now refreshes if any of the search option buttons are toggled, and the search
result will show the complete word if the search term only matches part of a word. Issue #1830.
PR #1936.
* When a new note is created from a reference tag in the editor, the syntax highlighting is
properly updated to indicate the tag is now valid. Issue #1916. PR #1938.
* The `Ctrl+E` shortcut now toggles focus between editor and viewer instead of just going to the
editor. The header text colour changes to indicate which panel has focus. This should make it
easier to scroll the content of the viewer without having to click it with the mouse first.
Issue #1387. PRs #1940 and #1941.
**Code Improvements**
* Change how dialogs are handled in memory, and drop the calls to deleteLater for the underlying Qt
object as it caused problems in some cases. Instead, the dialog is disconnected from the parent
object, which seems to let the Python and Qt garbage collectors to kick in.
PRs #1899. #1913 and #1921.
* Overload the reject call for dialogs rather to call close, which the default implementation does
not. This simplifies the logic when closing dialogs, as reject() is also a slot, which close() is
not. Issue #1915. PR #1918.
* Processing of dialogue highlighting has been added to the Tokenizer class, and the RegEx handling
moved to a separate factory class. PR #1908.
* The progress bar widgets have been moved to a single module, and test coverage added. PR #1937.
**Packaging**
* The Windows installer is now built with Inno Setup 6.3, and uses zip compression rather than
lzma. It also properly sets the undelete icon, and the undelete process is better at cleaning up
files. PR #1932.
----
## Version 2.5 Beta 1 [2024-05-26]
### Release Notes
+32 -30
View File
@@ -2,72 +2,74 @@
## Main Developer
* Veronica Berglyd Olsen (@vkbo)
* Veronica Berglyd Olsen
## Contributors
* Concept: Marian Lückhof (@Number042)
* Internationalisation: Bruno Meneguello (@bkmeneguello)
* Setup and Packaging: Rachel Powers (@Ryex)
* Early Concept: Marian Lückhof
* Internationalisation: Bruno Meneguello
* Setup and Packaging: Rachel Powers
For other contributions, see the project's [Contributors](https://github.com/vkbo/novelWriter/graphs/contributors) page.
## Artwork
The artwork on the Welcome dialog was created by [Louis Durrant](https://louisdurrant.art).
The artwork on the Welcome dialog was created by Louis Durrant.
## Translations
The default language is English (UK) with English (US) as an option. These are the original
translators for the languages currently available:
* Dutch: Martijn van der Kleijn (@mvdkleijn)
* French: Jan Lüdke (@jyhelle)
* German: Myian (@heymyian)
* Italian: Riccardo Mangili
* Japanese: hebekeg (@hebekeg)
* Latin American Spanish: Tommy Marplatt (@tmarplatt)
* Norwegian: Veronica Berglyd Olsen (@vkbo)
* Portuguese: Bruno Meneguello (@bkmeneguello)
* Simplified Chinese: Qianzhi Long (@longqzh)
* **Dutch:** Martijn van der Kleijn (mvdkleijn)
* **French:** Jan Lüdke (jyhelle)
* **German:** Myian (HeyMyian)
* **Italian:** Riccardo Mangili
* **Japanese:** hebekeg
* **Latin American Spanish:** Tommy Marplatt (tmarplatt)
* **Norwegian:** Veronica Berglyd Olsen (vkbo)
* **Polish:** Anna Maria Polak (Nauthiz)
* **Portuguese:** Bruno Meneguello (bkmeneguello)
* **Simplified Chinese:** Qianzhi Long (longqzh)
Additional larger translation contributions:
* French: Albert Aribaud (@aaribaud)
* **French:** Albert Aribaud (aaribaud)
* **Portuguese:** Oli Maia (olimaia)
Translations are managed on [Crowdin](https://crowdin.com/project/novelwriter), and more
contributions are listed on the project's [Members](https://crowdin.com/project/novelwriter/members) page.
contributions are listed on the project's Members page.
## Libraries
The following libraries are dependencies of novelWriter:
* [Qt5](https://www.qt.io) by Qt Company
* [PyQt5](https://www.riverbankcomputing.com/software/pyqt) by Riverbank Computing
* [Enchant](https://abiword.github.io/enchant) by Dom Lachowicz
* [PyEnchant](https://pyenchant.github.io/pyenchant) by Dimitri Merejkowsky
* **Qt5** by Qt Company
* **PyQt5** by Riverbank Computing
* **Enchant** by Dom Lachowicz
* **PyEnchant** by Dimitri Merejkowsky
## Assets
Some of the assets bundled with novelWriter were adapted from the following sources:
* [Typicons](https://github.com/stephenhutchings/typicons.font) icons by Stephen Hutchings (CC BY-SA 4.0)
* [Tomorrow](https://github.com/chriskempson/base16) syntax themes by Chris Kempson (MIT License)
* [Owl](https://github.com/sdras/night-owl-vscode-theme) syntax themes by Sarah Drasner (MIT License)
* [Solarized](https://github.com/altercation/solarized) themes by Ethan Schoonover (MIT License)
* [Cyberpunk Night](https://github.com/alemvigh) theme by Anders Lemvigh (CC BY-SA 4.0)
* [Dracula](https://draculatheme.com) theme by Zeno Rocha (MIT License)
* [Snazzy Light](https://github.com/loilo/vscode-snazzy-light) theme by Florian Reuschel (MIT License)
* **Typicons** icons by Stephen Hutchings (CC BY-SA 4.0)
* **Tomorrow** syntax themes by Chris Kempson (MIT License)
* **Owl** syntax themes by Sarah Drasner (MIT License)
* **Solarized** themes by Ethan Schoonover (MIT License)
* **Cyberpunk Night** theme by Anders Lemvigh (CC BY-SA 4.0)
* **Dracula** theme by Zeno Rocha (MIT License)
* **Snazzy Light** theme by Florian Reuschel (MIT License)
## Fonts
The font used for the main novelWriter logo, mimetype and text banners is Pridi. Other fonts are
used on buttons and icons.
* Pridi by Cadson Demak (Open Font License, Version 1.1)
* Source Sans Pro by Paul D. Hunt (SIL Open Font License)
* **Pridi** by Cadson Demak (Open Font License, Version 1.1)
* **Source Sans Pro** by Paul D. Hunt (SIL Open Font License)
## Special Mentions
Additional thanks to @johnblommers who was an early user and who has provided a lot of very useful
Additional thanks to John Blommers who was an early user and who has provided a lot of very useful
feedback over the years.
+2 -1
View File
@@ -132,6 +132,7 @@ A GUI theme ``.conf`` file consists of the following settings:
[GUI]
helptext = 0, 0, 0
fadedtext = 128, 128, 128
errortext = 255, 0, 0
statusnone = 120, 120, 120
statussaved = 2, 133, 37
@@ -149,7 +150,7 @@ colour values are RGB numbers on the format ``r, g, b`` where each is an integer
not defined, it is computed as a colour between the ``window`` and ``windowtext`` colour.
.. versionadded:: 2.5
The ``errortext`` theme colour entry was added.
The ``fadedtext`` and ``errortext`` theme colour entries were added.
Custom Syntax Theme
+1119 -935
View File
File diff suppressed because it is too large Load Diff
+1123 -939
View File
File diff suppressed because it is too large Load Diff
+1116 -932
View File
File diff suppressed because it is too large Load Diff
+1116 -932
View File
File diff suppressed because it is too large Load Diff
+1116 -932
View File
File diff suppressed because it is too large Load Diff
+1116 -932
View File
File diff suppressed because it is too large Load Diff
+1116 -932
View File
File diff suppressed because it is too large Load Diff
+1116 -932
View File
File diff suppressed because it is too large Load Diff
+1116 -932
View File
File diff suppressed because it is too large Load Diff
+4988
View File
File diff suppressed because it is too large Load Diff
+1116 -932
View File
File diff suppressed because it is too large Load Diff
+1116 -932
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -47,9 +47,9 @@ __license__ = "GPLv3"
__author__ = "Veronica Berglyd Olsen"
__maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net"
__version__ = "2.5b1"
__hexversion__ = "0x020500b1"
__date__ = "2024-05-26"
__version__ = "2.5rc1"
__hexversion__ = "0x020500c1"
__date__ = "2024-06-22"
__status__ = "Stable"
__domain__ = "novelwriter.io"
+116
View File
@@ -0,0 +1,116 @@
{
"Synopsis": "Streszczenie",
"Short Description": "Krótki opis",
"Comment": "Komentarz",
"Notes": "Notatki",
"Tag": "Znacznik",
"Point of View": "Punkt widzenia",
"Focus": "Skupienie",
"Characters": "Postacie",
"Plot": "Fabuła",
"Timeline": "Oś czasu",
"Locations": "Miejsca",
"Objects": "Obiekty",
"Entities": "Podmioty",
"Custom": "Różne",
"0": "zero",
"1": "pierwszy",
"2": "drugi",
"3": "trzeci",
"4": "czwarty",
"5": "piąty",
"6": "szósty",
"7": "siódmy",
"8": "ósmy",
"9": "dziewiąty",
"10": "dziesiąty",
"11": "jedenasty",
"12": "dwunasty",
"13": "trzynasty",
"14": "czternasty",
"15": "piętnasty",
"16": "szesnasty",
"17": "siedemnasty",
"18": "osiemnasty",
"19": "dziewiętnasty",
"20": "dwudziesty",
"21": "dwudziesty pierwszy",
"22": "dwudziesty drugi",
"23": "dwudziesty Trzeci",
"24": "dwudziesty czwarty",
"25": "dwudziesty piąty",
"26": "dwudziesty szósty",
"27": "dwudziesty siódmy",
"28": "dwudziesty ósmy",
"29": "dwudziesty dziewiąty",
"30": "trzydziesty",
"31": "trzydziesty pierwszy",
"32": "trzydziesty drugi",
"33": "trzydziesty trzeci",
"34": "trzydziesty czwarty",
"35": "trzydziesty piąty",
"36": "trzydziesty szósty",
"37": "trzydziesty siódmy",
"38": "trzydziesty ósmy",
"39": "trzydziesty dziewiaty",
"40": "czterdziesty",
"41": "czterdziesty pierwszy",
"42": "czterdziesty drugi",
"43": "czterdziesty Trzeci",
"44": "czterdziesty czwarty",
"45": "czterdziesty piąty",
"46": "czterdziesty szósty",
"47": "czterdziesty siódmy",
"48": "czterdziesty ósmy",
"49": "czterdziesty dziewiąty",
"50": "pięćdziesiąty",
"51": "pięćdziesiąty pierwszy",
"52": "pięćdziesiąty drugi",
"53": "pięćdziesiąty trzeci",
"54": "pięćdziesiąty czwarty",
"55": "pięćdziesiąty piąty",
"56": "pięćdziesiąty szósty",
"57": "pięćdziesiąty siódmy",
"58": "pięćdziesiąty ósmy",
"59": "pięćdziesiąty dziewiąty",
"60": "sześćdziesiąty",
"61": "sześćdziesiąty pierwszy",
"62": "sześćdziesiąty drugi",
"63": "sześćdziesiąty trzeci",
"64": "sześćdziesiąty czwarty",
"65": "sześćdziesiąty piąty",
"66": "sześćdziesiąty szósty",
"67": "sześćdziesiąty siódmy",
"68": "sześćdziesiąty ósmy",
"69": "sześćdziesiąty dziewiąty",
"70": "siedemdziesiąty",
"71": "siedemdziesiąty pierwszy",
"72": "siedemdziesiąty drugi",
"73": "siedemdziesiąty trzeci",
"74": "siedemdziesiąty czwarty",
"75": "siedemdziesiąty piąty",
"76": "siedemdziesiąty szósty",
"77": "siedemdziesiąty siódmy",
"78": "siedemdziesiąty ósmy",
"79": "siedemdziesiąty dziewiąty",
"80": "osiemdziesiąty",
"81": "osiemdziesiąty pierwszy",
"82": "osiemdziesiąty drugi",
"83": "osiemdziesiąty trzeci",
"84": "osiemdziesiąty czwarty",
"85": "osiemdziesiąty piąty",
"86": "osiemdziesiąty szósty",
"87": "osiemdziesiąty siódmy",
"88": "osiemdziesiąty ósmy",
"89": "osiemdziesiąty dziewiąty",
"90": "dziewięćdziesiąty",
"91": "dziewięćdziesiąty pierwszy",
"92": "dziewięćdziesiąty drugi",
"93": "dziewięćdziesiąty trzeci",
"94": "dziewięćdziesiąty czwarty",
"95": "dziewięćdziesiąty piąty",
"96": "dziewięćdziesiąty szósty",
"97": "dziewięćdziesiąty siódmy",
"98": "dziewięćdziesiąty ósmy",
"99": "dziewięćdziesiąty dziewiąty"
}
+52 -44
View File
@@ -1,84 +1,92 @@
<!DOCTYPE html public "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<body>
<h3>Main Developer</h3>
<p>Veronica Berglyd Olsen (<a href="https://github.com/vkbo">@vkbo</a>)</p>
<p>Veronica Berglyd Olsen</p>
<h3>Contributors</h3>
<p>
&nbsp;&bull;&nbsp;<b>Concept:</b> Marian Lückhof (<a href="https://github.com/Number042">@Number042</a>)<br>
&nbsp;&bull;&nbsp;<b>Internationalisation:</b> Bruno Meneguello (<a href="https://github.com/bkmeneguello">@bkmeneguello</a>)<br>
&nbsp;&bull;&nbsp;<b>Setup and Packaging:</b> Rachel Powers (<a href="https://github.com/Ryex">@Ryex</a>)
</p>
<ul>
<li><b>Early Concept:</b> Marian Lückhof</li>
<li><b>Internationalisation:</b> Bruno Meneguello</li>
<li><b>Setup and Packaging:</b> Rachel Powers</li>
</ul>
<p>For other contributions, see the project's
<a href="https://github.com/vkbo/novelWriter/graphs/contributors">Contributors</a> page.</p>
<h3>Artwork</h3>
<p>The artwork on the Welcome dialog was created by <a href="https://louisdurrant.art">Louis Durrant</a>.</p>
<p>The artwork on the Welcome dialog was created by Louis Durrant.</p>
<h3>Translations</h3>
<p>The default language is English (UK) with English (US) as an option. These are the original
translators for the languages currently available:</p>
<p>
&nbsp;&bull;&nbsp;<b>Dutch:</b> Martijn van der Kleijn (<a href="https://github.com/mvdkleijn">@mvdkleijn</a>)<br>
&nbsp;&bull;&nbsp;<b>French:</b> Jan Lüdke (<a href="https://github.com/jyhelle">@jyhelle</a>)<br>
&nbsp;&bull;&nbsp;<b>German:</b> Myian (<a href="https://github.com/heymyian">@heymyian</a>)<br>
&nbsp;&bull;&nbsp;<b>Italian:</b> Riccardo Mangili<br>
&nbsp;&bull;&nbsp;<b>Japanese:</b> hebekeg (<a href="https://github.com/hebekeg">@hebekeg</a>)<br>
&nbsp;&bull;&nbsp;<b>Latin American Spanish:</b> Tommy Marplatt (<a href="https://github.com/tmarplatt">@tmarplatt</a>)<br>
&nbsp;&bull;&nbsp;<b>Norwegian:</b> Veronica Berglyd Olsen (<a href="https://github.com/vkbo">@vkbo</a>)<br>
&nbsp;&bull;&nbsp;<b>Portuguese:</b> Bruno Meneguello (<a href="https://github.com/bkmeneguello">@bkmeneguello</a>)<br>
&nbsp;&bull;&nbsp;<b>Simplified Chinese:</b> Qianzhi Long (<a href="https://github.com/longqzh">@longqzh</a>)
</p>
<ul>
<li><b>Dutch:</b> Martijn van der Kleijn (mvdkleijn)</li>
<li><b>French:</b> Jan Lüdke (jyhelle)</li>
<li><b>German:</b> Myian (HeyMyian)</li>
<li><b>Italian:</b> Riccardo Mangili</li>
<li><b>Japanese:</b> hebekeg</li>
<li><b>Latin American Spanish:</b> Tommy Marplatt (tmarplatt)</li>
<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>Simplified Chinese:</b> Qianzhi Long (longqzh)</li>
</ul>
<p>Additional larger translation contributions:</p>
<p>
&nbsp;&bull;&nbsp;<b>French:</b> Albert Aribaud (<a href="https://github.com/aaribaud">@aaribaud</a>)
</p>
<ul>
<li><b>French:</b> Albert Aribaud (aaribaud)</li>
<li><b>Portuguese:</b> Oli Maia (olimaia)</li>
</ul>
<p>Translations are managed on <a href="https://crowdin.com/project/novelwriter">Crowdin</a>, and
more contributions are listed on the project's <a href="https://crowdin.com/project/novelwriter/members">Members</a> page.</p>
more contributions are listed on the project's Members page.</p>
<h3>Libraries</h3>
<p>The following libraries are dependencies of novelWriter:</p>
<p>
&nbsp;&bull;&nbsp;<a href="https://www.qt.io">Qt5</a> by Qt Company<br>
&nbsp;&bull;&nbsp;<a href="https://www.riverbankcomputing.com/software/pyqt">PyQt5</a> by Riverbank Computing<br>
&nbsp;&bull;&nbsp;<a href="https://abiword.github.io/enchant">Enchant</a> by Dom Lachowicz<br>
&nbsp;&bull;&nbsp;<a href="https://pyenchant.github.io/pyenchant">PyEnchant</a> by Dimitri Merejkowsky
</p>
<ul>
<li><b>Qt5</b> by Qt Company</li>
<li><b>PyQt5</b> by Riverbank Computing</li>
<li><b>Enchant</b> by Dom Lachowicz</li>
<li><b>PyEnchant</b> by Dimitri Merejkowsky</li>
</ul>
<h3>Assets</h3>
<p>Some of the assets bundled with novelWriter were adapted from the following sources:</p>
<p>
&nbsp;&bull;&nbsp;<a href="https://github.com/stephenhutchings/typicons.font">Typicons</a> icons by Stephen Hutchings (CC BY-SA 4.0)<br>
&nbsp;&bull;&nbsp;<a href="https://github.com/chriskempson/base16">Tomorrow</a> syntax themes by Chris Kempson (MIT License)<br>
&nbsp;&bull;&nbsp;<a href="https://github.com/sdras/night-owl-vscode-theme">Owl</a> syntax themes by Sarah Drasner (MIT License)<br>
&nbsp;&bull;&nbsp;<a href="https://github.com/altercation/solarized">Solarized</a> themes by Ethan Schoonover (MIT License)<br>
&nbsp;&bull;&nbsp;<a href="https://github.com/alemvigh">Cyberpunk Night</a> theme by Anders Lemvigh (CC BY-SA 4.0)<br>
&nbsp;&bull;&nbsp;<a href="https://draculatheme.com">Dracula</a> theme by Zeno Rocha (MIT License)<br>
&nbsp;&bull;&nbsp;<a href="https://github.com/loilo/vscode-snazzy-light">Snazzy Light</a> theme by Florian Reuschel (MIT License)
</p>
<ul>
<li><b>Typicons</b> icons by Stephen Hutchings (CC BY-SA 4.0)</li>
<li><b>Tomorrow</b> syntax themes by Chris Kempson (MIT License)</li>
<li><b>Owl</b> syntax themes by Sarah Drasner (MIT License)</li>
<li><b>Solarized</b> themes by Ethan Schoonover (MIT License)</li>
<li><b>Cyberpunk Night</b> theme by Anders Lemvigh (CC BY-SA 4.0)</li>
<li><b>Dracula</b> theme by Zeno Rocha (MIT License)</li>
<li><b>Snazzy Light</b> theme by Florian Reuschel (MIT License)</li>
</ul>
<h3>Fonts</h3>
<p>The font used for the main novelWriter logo, mimetype and text banners is Pridi. Other fonts are
used on buttons and icons.</p>
<p>
&nbsp;&bull;&nbsp;Pridi by Cadson Demak (Open Font License, Version 1.1)<br>
&nbsp;&bull;&nbsp;Source Sans Pro by Paul D. Hunt (SIL Open Font License)
</p>
<ul>
<li><b>Pridi</b> by Cadson Demak (Open Font License, Version 1.1)</li>
<li><b>Source Sans Pro</b> by Paul D. Hunt (SIL Open Font License)</li>
</ul>
<h3>Special Mentions</h3>
<p>Additional thanks to <a href="https://github.com/johnblommers">@johnblommers</a> who was an early user and who has provided a lot
of very useful feedback over the years.</p>
<p>Additional thanks to John Blommers who was an early user and who has provided a lot of very
useful feedback over the years.</p>
</body>
</html>
@@ -25,6 +25,7 @@ linkvisited = 50, 0, 80
[GUI]
helptext = 97, 97, 97
fadedtext = 97, 97, 97
errortext = 255, 77, 77
statusnone = 50, 50, 50
statussaved = 77, 255, 77
@@ -26,6 +26,7 @@ linkvisited = 102, 153, 204
[GUI]
helptext = 164, 164, 164
fadedtext = 148, 148, 148
errortext = 255, 164, 164
statusnone = 150, 152, 150
statussaved = 39, 135, 78
@@ -26,6 +26,7 @@ linkvisited = 66, 113, 174
[GUI]
helptext = 92, 92, 92
fadedtext = 108, 108, 108
errortext = 255, 92, 92
statusnone = 120, 120, 120
statussaved = 200, 15, 39
+1
View File
@@ -41,6 +41,7 @@ linkvisited = 139, 233, 253
[GUI]
helptext = 204, 172, 249
fadedtext = 98, 114, 164
errortext = 255, 85, 85
statusnone = 98, 114, 164
statussaved = 80, 250, 123
@@ -25,6 +25,7 @@ linkvisited = 38, 139, 210
[GUI]
helptext = 166, 161, 149
fadedtext = 166, 161, 149
errortext = 255, 161, 149
statusnone = 88, 110, 117
statussaved = 42, 161, 152
@@ -25,6 +25,7 @@ linkvisited = 38, 139, 210
[GUI]
helptext = 78, 91, 95
fadedtext = 78, 91, 95
errortext = 255, 91, 95
statusnone = 88, 110, 117
statussaved = 42, 161, 152
+67 -15
View File
@@ -5,6 +5,7 @@ novelWriter Config Class
File History:
Created: 2018-09-22 [0.0.1] Config
Created: 2022-11-09 [2.0rc2] RecentProjects
Created: 2024-06-16 [2.5rc1] RecentPaths
This file is a part of novelWriter
Copyright 20182024, Veronica Berglyd Olsen
@@ -103,7 +104,8 @@ class Config:
# User Settings
# =============
self._recentObj = RecentProjects(self)
self._recentProjects = RecentProjects(self)
self._recentPaths = RecentPaths(self)
# General GUI Settings
self.guiLocale = self._qLocale.name()
@@ -180,7 +182,6 @@ class Config:
self.fmtPadThin = False
# User Paths
self._lastPath = self._homePath # The user's last used path
self._backupPath = self._backPath # Backup path to use, can be none
# Spell Checking Settings
@@ -253,7 +254,7 @@ class Config:
@property
def recentProjects(self) -> RecentProjects:
return self._recentObj
return self._recentProjects
@property
def mainWinSize(self) -> list[int]:
@@ -343,7 +344,7 @@ class Config:
self._outlnPanePos = [int(x/self.guiScale) for x in pos]
return
def setLastPath(self, path: str | Path) -> None:
def setLastPath(self, key: str, path: str | Path) -> None:
"""Set the last used path. Only the folder is saved, so if the
path is not a folder, the parent of the path is used instead.
"""
@@ -352,8 +353,7 @@ class Config:
if not path.is_dir():
path = path.parent
if path.is_dir():
self._lastPath = path
logger.debug("Last path updated: %s" % self._lastPath)
self._recentPaths.setPath(key, path)
return
def setBackupPath(self, path: Path | str) -> None:
@@ -438,11 +438,12 @@ class Config:
return self._appPath / "assets" / target
return self._appPath / "assets"
def lastPath(self) -> Path:
def lastPath(self, key: str) -> Path:
"""Return the last path used by the user, if it exists."""
if isinstance(self._lastPath, Path):
if self._lastPath.is_dir():
return self._lastPath
if path := self._recentPaths.getPath(key):
asPath = Path(path)
if asPath.is_dir():
return asPath
return self._homePath
def backupPath(self) -> Path:
@@ -516,7 +517,6 @@ class Config:
logger.debug("Data Path: %s", self._dataPath)
logger.debug("App Root: %s", self._appRoot)
logger.debug("App Path: %s", self._appPath)
logger.debug("Last Path: %s", self._lastPath)
logger.debug("PDF Manual: %s", self.pdfDocs)
# If the config and data folders don't exist, create them
@@ -531,7 +531,8 @@ class Config:
(self._dataPath / "syntax").mkdir(exist_ok=True)
(self._dataPath / "themes").mkdir(exist_ok=True)
self._recentObj.loadCache()
self._recentPaths.loadCache()
self._recentProjects.loadCache()
self._checkOptionalPackages()
logger.debug("Config instance initialised")
@@ -600,7 +601,6 @@ class Config:
self.hideHScroll = conf.rdBool(sec, "hidehscroll", self.hideHScroll)
self.lastNotes = conf.rdStr(sec, "lastnotes", self.lastNotes)
self.nativeFont = conf.rdBool(sec, "nativefont", self.nativeFont)
self._lastPath = conf.rdPath(sec, "lastpath", self._lastPath)
# Sizes
sec = "Sizes"
@@ -710,7 +710,6 @@ class Config:
"hidehscroll": str(self.hideHScroll),
"lastnotes": str(self.lastNotes),
"nativefont": str(self.nativeFont),
"lastpath": str(self._lastPath),
}
conf["Sizes"] = {
@@ -811,7 +810,7 @@ class Config:
"""Pack a list of items into a comma-separated string for saving
to the config file.
"""
return ", ".join([str(inVal) for inVal in data])
return ", ".join(str(inVal) for inVal in data)
def _checkOptionalPackages(self) -> None:
"""Check optional packages used by some features."""
@@ -893,3 +892,56 @@ class RecentProjects:
logger.debug("Removed recent: %s", path)
self.saveCache()
return
class RecentPaths:
KEYS = ["default", "project", "import", "outline", "stats"]
def __init__(self, config: Config) -> None:
self._conf = config
self._data = {}
return
def setPath(self, key: str, path: Path | str) -> None:
"""Set a path for a given key, and save the cache."""
if key in self.KEYS:
self._data[key] = str(path)
self.saveCache()
return
def getPath(self, key: str) -> str | None:
"""Get a path for a given key, or return None."""
return self._data.get(key)
def loadCache(self) -> bool:
"""Load the cache file for recent paths."""
self._data = {}
cacheFile = self._conf.dataPath(nwFiles.RECENT_PATH)
if cacheFile.is_file():
try:
with open(cacheFile, mode="r", encoding="utf-8") as inFile:
data = json.load(inFile)
if isinstance(data, dict):
for key, path in data.items():
if key in self.KEYS and isinstance(path, str):
self._data[key] = path
except Exception:
logger.error("Could not load recent paths cache")
logException()
return False
return True
def saveCache(self) -> bool:
"""Save the cache dictionary of recent paths."""
cacheFile = self._conf.dataPath(nwFiles.RECENT_PATH)
cacheTemp = cacheFile.with_suffix(".tmp")
try:
with open(cacheTemp, mode="w+", encoding="utf-8") as outFile:
json.dump(self._data, outFile, indent=2)
cacheTemp.replace(cacheFile)
except Exception:
logger.error("Could not save recent paths cache")
logException()
return False
return True
+1
View File
@@ -104,6 +104,7 @@ class nwFiles:
# Config Files
CONF_FILE = "novelwriter.conf"
RECENT_FILE = "recentProjects.json"
RECENT_PATH = "recentPaths.json"
# Project Root Files
PROJ_FILE = "nwProject.nwx"
+3 -3
View File
@@ -219,7 +219,7 @@ class BuildSettings:
return self._order
@property
def lastPath(self) -> Path:
def lastBuildPath(self) -> Path:
"""The last used build path."""
if self._path.is_dir():
return self._path
@@ -293,7 +293,7 @@ class BuildSettings:
self._order = value
return
def setLastPath(self, path: Path | str | None) -> None:
def setLastBuildPath(self, path: Path | str | None) -> None:
"""Set the last used build path."""
if isinstance(path, str):
path = Path(path)
@@ -461,7 +461,7 @@ class BuildSettings:
self.setName(data.get("name", ""))
self.setBuildID(data.get("uuid", ""))
self.setOrder(data.get("order", 0))
self.setLastPath(data.get("path", None))
self.setLastBuildPath(data.get("path", None))
self.setLastBuildName(data.get("build", ""))
buildFmt = str(data.get("format", ""))
+3 -1
View File
@@ -348,7 +348,9 @@ class DocSearch:
rxMatch = rxItt.next()
pos = rxMatch.capturedStart()
num = rxMatch.capturedLength()
context = text[pos:pos+100].partition("\n")[0]
lim = text[:pos].rfind("\n") + 1
cut = text[lim:pos].rfind(" ") + lim + 1
context = text[cut:cut+100].partition("\n")[0]
if context:
results.append((pos, num, context))
count += 1
+14 -4
View File
@@ -254,13 +254,23 @@ class NWProject:
status = self._storage.initProjectStorage(projPath, clearLock)
if status != NWStorageOpen.READY:
if status == NWStorageOpen.UNKOWN:
SHARED.error(self.tr("Not a known project file format."))
SHARED.error(
self.tr("Not a known project file format."),
info=self.tr("Path: {0}").format(str(projPath))
)
elif status == NWStorageOpen.NOT_FOUND:
SHARED.error(self.tr("Project file not found."))
SHARED.error(
self.tr("Project file not found."),
info=self.tr("Path: {0}").format(str(projPath))
)
elif status == NWStorageOpen.FAILED:
SHARED.error(
self.tr("Failed to open project."),
info=self.tr("Path: {0}").format(str(projPath)),
exc=self._storage.exc
)
elif status == NWStorageOpen.LOCKED:
self._state = NWProjectState.LOCKED
elif status == NWStorageOpen.FAILED:
SHARED.error(self.tr("Failed to open project."), exc=self._storage.exc)
return False
# Read Project XML
+62 -28
View File
@@ -37,28 +37,35 @@ from novelwriter.types import FONT_STYLE, FONT_WEIGHTS
logger = logging.getLogger(__name__)
HTML5_TAGS = {
Tokenizer.FMT_B_B: "<strong>",
Tokenizer.FMT_B_E: "</strong>",
Tokenizer.FMT_I_B: "<em>",
Tokenizer.FMT_I_E: "</em>",
Tokenizer.FMT_D_B: "<del>",
Tokenizer.FMT_D_E: "</del>",
Tokenizer.FMT_U_B: "<span style='text-decoration: underline;'>",
Tokenizer.FMT_U_E: "</span>",
Tokenizer.FMT_M_B: "<mark>",
Tokenizer.FMT_M_E: "</mark>",
Tokenizer.FMT_SUP_B: "<sup>",
Tokenizer.FMT_SUP_E: "</sup>",
Tokenizer.FMT_SUB_B: "<sub>",
Tokenizer.FMT_SUB_E: "</sub>",
Tokenizer.FMT_DL_B: "<span class='dialog'>",
Tokenizer.FMT_DL_E: "</span>",
Tokenizer.FMT_ADL_B: "<span class='altdialog'>",
Tokenizer.FMT_ADL_E: "</span>",
Tokenizer.FMT_STRIP: "",
# Each opener tag, with the id of its corresponding closer and tag format
HTML_OPENER: dict[int, tuple[int, str]] = {
Tokenizer.FMT_B_B: (Tokenizer.FMT_B_E, "<strong>"),
Tokenizer.FMT_I_B: (Tokenizer.FMT_I_E, "<em>"),
Tokenizer.FMT_D_B: (Tokenizer.FMT_D_E, "<del>"),
Tokenizer.FMT_U_B: (Tokenizer.FMT_U_E, "<span style='text-decoration: underline;'>"),
Tokenizer.FMT_M_B: (Tokenizer.FMT_M_E, "<mark>"),
Tokenizer.FMT_SUP_B: (Tokenizer.FMT_SUP_E, "<sup>"),
Tokenizer.FMT_SUB_B: (Tokenizer.FMT_SUB_E, "<sub>"),
Tokenizer.FMT_DL_B: (Tokenizer.FMT_DL_E, "<span class='dialog'>"),
Tokenizer.FMT_ADL_B: (Tokenizer.FMT_ADL_E, "<span class='altdialog'>"),
}
# Each closer tag, with the id of its corresponding opener and tag format
HTML_CLOSER: dict[int, tuple[int, str]] = {
Tokenizer.FMT_B_E: (Tokenizer.FMT_B_B, "</strong>"),
Tokenizer.FMT_I_E: (Tokenizer.FMT_I_B, "</em>"),
Tokenizer.FMT_D_E: (Tokenizer.FMT_D_B, "</del>"),
Tokenizer.FMT_U_E: (Tokenizer.FMT_U_B, "</span>"),
Tokenizer.FMT_M_E: (Tokenizer.FMT_M_B, "</mark>"),
Tokenizer.FMT_SUP_E: (Tokenizer.FMT_SUP_B, "</sup>"),
Tokenizer.FMT_SUB_E: (Tokenizer.FMT_SUB_B, "</sub>"),
Tokenizer.FMT_DL_E: (Tokenizer.FMT_DL_B, "</span>"),
Tokenizer.FMT_ADL_E: (Tokenizer.FMT_ADL_B, "</span>"),
}
# Empty HTML tag record
HTML_NONE = (0, "")
class ToHtml(Tokenizer):
"""Core: HTML Document Writer
@@ -447,19 +454,46 @@ class ToHtml(Tokenizer):
def _formatText(self, text: str, tFmt: T_Formats) -> str:
"""Apply formatting tags to text."""
temp = text
for pos, fmt, data in reversed(tFmt):
html = ""
if fmt == self.FMT_FNOTE:
# Build a list of all html tags that need to be inserted in the text.
# This is done in the forward direction, and a tag is only opened if it
# isn't already open, and only closed if it has previously been opened.
tags: list[tuple[int, str]] = []
state = dict.fromkeys(HTML_OPENER, False)
for pos, fmt, data in tFmt:
if m := HTML_OPENER.get(fmt):
if not state.get(fmt, True):
tags.append((pos, m[1]))
state[fmt] = True
elif m := HTML_CLOSER.get(fmt):
if state.get(m[0], False):
tags.append((pos, m[1]))
state[m[0]] = False
elif fmt == self.FMT_FNOTE:
if data in self._footnotes:
index = len(self._usedNotes) + 1
self._usedNotes[data] = index
html = f"<sup><a href='#footnote_{index}'>{index}</a></sup>"
tags.append((pos, f"<sup><a href='#footnote_{index}'>{index}</a></sup>"))
else:
html = "<sup>ERR</sup>"
else:
html = HTML5_TAGS.get(fmt, "")
temp = f"{temp[:pos]}{html}{temp[pos:]}"
tags.append((pos, "<sup>ERR</sup>"))
# Check all format types and close any tag that is still open. This
# ensures that unclosed tags don't spill over to the next paragraph.
end = len(text)
for opener, active in state.items():
if active:
closer = HTML_OPENER.get(opener, HTML_NONE)[0]
tags.append((end, HTML_CLOSER.get(closer, HTML_NONE)[1]))
# Insert all tags at their correct position, starting from the back.
# The reverse order ensures that the positions are not shifted while we
# insert tags.
for pos, tag in reversed(tags):
temp = f"{temp[:pos]}{tag}{temp[pos:]}"
# Replace all line breaks with proper HTML break tags
temp = temp.replace("\n", "<br>")
return stripEscape(temp)
def _formatSynopsis(self, text: str, synopsis: bool) -> str:
+5 -5
View File
@@ -198,6 +198,7 @@ class Tokenizer(ABC):
# Instance Variables
self._hFormatter = HeadingFormatter(self._project)
self._noSep = True # Flag to indicate that we don't want a scene separator
self._noIndent = False # Flag to disable text indent on next paragraph
self._showDialog = False # Flag for dialogue highlighting
# This File
@@ -873,7 +874,6 @@ class Tokenizer(ABC):
pLines: list[T_Token] = []
tCount = len(tokens)
pIndent = True
for n, cToken in enumerate(tokens):
if n > 0:
@@ -881,11 +881,11 @@ class Tokenizer(ABC):
if n < tCount - 1:
nToken = tokens[n+1] # Look ahead
if not self._indentFirst and cToken[0] in self.L_SKIP_INDENT:
if cToken[0] in self.L_SKIP_INDENT and not self._indentFirst:
# Unless the indentFirst flag is set, we set up the next
# paragraph to not be indented if we see a block of a
# specific type
pIndent = False
self._noIndent = True
if cToken[0] == self.T_EMPTY:
# We don't need to keep the empty lines after this pass
@@ -910,7 +910,7 @@ class Tokenizer(ABC):
# Next token is not text, so we add the buffer to tokens
nLines = len(pLines)
cStyle = pLines[0][4]
if self._firstIndent and pIndent and not cStyle & self.M_ALIGNED:
if self._firstIndent and not (self._noIndent or cStyle & self.M_ALIGNED):
# If paragraph indentation is enabled, not temporarily
# turned off, and the block is not aligned, we add the
# text indentation flag
@@ -938,7 +938,7 @@ class Tokenizer(ABC):
# Reset buffer and make sure text indent is on for next pass
pLines = []
pIndent = True
self._noIndent = False
else:
self._tokens.append(cToken)
+16 -13
View File
@@ -26,7 +26,7 @@ from __future__ import annotations
import logging
from PyQt5.QtGui import (
QColor, QFont, QFontMetrics, QTextBlockFormat, QTextCharFormat,
QColor, QFont, QFontMetricsF, QTextBlockFormat, QTextCharFormat,
QTextCursor, QTextDocument
)
@@ -99,19 +99,19 @@ class ToQTextDocument(Tokenizer):
self._document.clear()
self._document.setDefaultFont(self._textFont)
qMetric = QFontMetrics(self._textFont)
mScale = qMetric.height()
qMetric = QFontMetricsF(self._textFont)
mPx = qMetric.ascent() # 1 em in pixels
fPt = self._textFont.pointSizeF()
# Scaled Sizes
# ============
self._mHead = {
self.T_TITLE: (mScale * self._marginTitle[0], mScale * self._marginTitle[1]),
self.T_HEAD1: (mScale * self._marginHead1[0], mScale * self._marginHead1[1]),
self.T_HEAD2: (mScale * self._marginHead2[0], mScale * self._marginHead2[1]),
self.T_HEAD3: (mScale * self._marginHead3[0], mScale * self._marginHead3[1]),
self.T_HEAD4: (mScale * self._marginHead4[0], mScale * self._marginHead4[1]),
self.T_TITLE: (mPx * self._marginTitle[0], mPx * self._marginTitle[1]),
self.T_HEAD1: (mPx * self._marginHead1[0], mPx * self._marginHead1[1]),
self.T_HEAD2: (mPx * self._marginHead2[0], mPx * self._marginHead2[1]),
self.T_HEAD3: (mPx * self._marginHead3[0], mPx * self._marginHead3[1]),
self.T_HEAD4: (mPx * self._marginHead4[0], mPx * self._marginHead4[1]),
}
self._sHead = {
@@ -122,12 +122,12 @@ class ToQTextDocument(Tokenizer):
self.T_HEAD4: nwHeaders.H_SIZES.get(4, 1.0) * fPt,
}
self._mText = (mScale * self._marginText[0], mScale * self._marginText[1])
self._mMeta = (mScale * self._marginMeta[0], mScale * self._marginMeta[1])
self._mSep = (mScale * self._marginSep[0], mScale * self._marginSep[1])
self._mText = (mPx * self._marginText[0], mPx * self._marginText[1])
self._mMeta = (mPx * self._marginMeta[0], mPx * self._marginMeta[1])
self._mSep = (mPx * self._marginSep[0], mPx * self._marginSep[1])
self._mIndent = mScale * 2.0
self._tIndent = mScale * self._firstWidth
self._mIndent = mPx * 2.0
self._tIndent = mPx * self._firstWidth
# Block Format
# ============
@@ -136,6 +136,9 @@ class ToQTextDocument(Tokenizer):
self._blockFmt.setTopMargin(self._mText[0])
self._blockFmt.setBottomMargin(self._mText[1])
self._blockFmt.setAlignment(QtAlignJustify if self._doJustify else QtAlignAbsolute)
self._blockFmt.setLineHeight(
100*self._lineHeight, QTextBlockFormat.LineHeightTypes.ProportionalHeight
)
# Character Formats
# =================
+1 -1
View File
@@ -75,7 +75,7 @@ class GuiAbout(NDialog):
# Credits
self.lblCredits = NColourLabel(
self.tr("Credits"), scale=1.6, parent=self, bold=True
self.tr("Credits"), self, scale=1.6, bold=True
)
self.txtCredits = QTextBrowser(self)
+1 -1
View File
@@ -58,7 +58,7 @@ class GuiDocMerge(NDialog):
self.headLabel.setFont(SHARED.theme.guiFontB)
self.helpLabel = NColourLabel(
self.tr("Drag and drop items to change the order, or uncheck to exclude."),
SHARED.theme.helpText, parent=self, wrap=True
self, color=SHARED.theme.helpText, wrap=True
)
iPx = SHARED.theme.baseIconHeight
+1 -1
View File
@@ -62,7 +62,7 @@ class GuiDocSplit(NDialog):
self.headLabel.setFont(SHARED.theme.guiFontB)
self.helpLabel = NColourLabel(
self.tr("Select the maximum level to split into files."),
SHARED.theme.helpText, parent=self, wrap=True
self, color=SHARED.theme.helpText, wrap=True
)
# Values
+11 -24
View File
@@ -29,8 +29,8 @@ import logging
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QCloseEvent, QKeyEvent, QKeySequence
from PyQt5.QtWidgets import (
QAbstractButton, QCompleter, QDialogButtonBox, QFileDialog, QHBoxLayout,
QLineEdit, QPushButton, QVBoxLayout, QWidget
QCompleter, QDialogButtonBox, QFileDialog, QHBoxLayout, QLineEdit,
QPushButton, QVBoxLayout, QWidget
)
from novelwriter import CONFIG, SHARED
@@ -43,10 +43,7 @@ from novelwriter.extensions.modified import (
)
from novelwriter.extensions.pagedsidebar import NPagedSideBar
from novelwriter.extensions.switch import NSwitch
from novelwriter.types import (
QtAlignCenter, QtDialogApply, QtDialogClose, QtDialogSave, QtRoleAccept,
QtRoleApply, QtRoleReject
)
from novelwriter.types import QtAlignCenter, QtDialogCancel, QtDialogSave
logger = logging.getLogger(__name__)
@@ -66,8 +63,8 @@ class GuiPreferences(NDialog):
# Title
self.titleLabel = NColourLabel(
self.tr("Preferences"), SHARED.theme.helpText,
parent=self, scale=NColourLabel.HEADER_SCALE, indent=CONFIG.pxInt(4)
self.tr("Preferences"), self, color=SHARED.theme.helpText,
scale=NColourLabel.HEADER_SCALE, indent=CONFIG.pxInt(4)
)
# Search Box
@@ -89,8 +86,9 @@ class GuiPreferences(NDialog):
self.mainForm.setHelpTextStyle(SHARED.theme.helpText)
# Buttons
self.buttonBox = QDialogButtonBox(QtDialogApply | QtDialogSave | QtDialogClose, self)
self.buttonBox.clicked.connect(self._dialogButtonClicked)
self.buttonBox = QDialogButtonBox(QtDialogSave | QtDialogCancel, self)
self.buttonBox.accepted.connect(self._doSave)
self.buttonBox.rejected.connect(self.reject)
# Assemble
self.searchBox = QHBoxLayout()
@@ -784,19 +782,6 @@ class GuiPreferences(NDialog):
# Private Slots
##
@pyqtSlot("QAbstractButton*")
def _dialogButtonClicked(self, button: QAbstractButton) -> None:
"""Handle button clicks from the dialog button box."""
role = self.buttonBox.buttonRole(button)
if role == QtRoleApply:
self._saveValues()
elif role == QtRoleAccept:
self._saveValues()
self.close()
elif role == QtRoleReject:
self.close()
return
@pyqtSlot(int)
def _sidebarClicked(self, section: int) -> None:
"""Process a user request to switch page."""
@@ -897,7 +882,7 @@ class GuiPreferences(NDialog):
CONFIG.setPreferencesWinSize(self.width(), self.height())
return
def _saveValues(self) -> None:
def _doSave(self) -> None:
"""Save the values set in the form."""
updateTheme = False
needsRestart = False
@@ -1012,4 +997,6 @@ class GuiPreferences(NDialog):
CONFIG.saveConfig()
self.newPreferencesReady.emit(needsRestart, refreshTree, updateTheme, updateSyntax)
self.close()
return
+5 -5
View File
@@ -76,8 +76,8 @@ class GuiProjectSettings(NDialog):
# Title
self.titleLabel = NColourLabel(
self.tr("Project Settings"), SHARED.theme.helpText,
parent=self, scale=NColourLabel.HEADER_SCALE, indent=CONFIG.pxInt(4)
self.tr("Project Settings"), self, color=SHARED.theme.helpText,
scale=NColourLabel.HEADER_SCALE, indent=CONFIG.pxInt(4)
)
# SideBar
@@ -345,7 +345,7 @@ class _StatusPage(NFixedPage):
# Title
self.pageTitle = NColourLabel(
pageLabel, SHARED.theme.helpText, parent=self,
pageLabel, self, color=SHARED.theme.helpText,
scale=NColourLabel.HEADER_SCALE
)
@@ -637,8 +637,8 @@ class _ReplacePage(NFixedPage):
# Title
self.pageTitle = NColourLabel(
self.tr("Text Auto-Replace for Preview and Build"),
SHARED.theme.helpText, parent=self, scale=NColourLabel.HEADER_SCALE
self.tr("Text Auto-Replace for Preview and Build"), self,
color=SHARED.theme.helpText, scale=NColourLabel.HEADER_SCALE
)
# List Box
+1 -1
View File
@@ -69,7 +69,7 @@ class GuiWordList(NDialog):
# Header
self.headLabel = NColourLabel(
self.tr("Project Word List"), SHARED.theme.helpText, parent=self,
self.tr("Project Word List"), self, color=SHARED.theme.helpText,
scale=NColourLabel.HEADER_SCALE
)
+4 -5
View File
@@ -148,12 +148,11 @@ class nwView(Enum):
SEARCH = 4
class nwWidget(Enum):
class nwFocus(Enum):
TREE = 1
EDITOR = 2
VIEWER = 3
OUTLINE = 4
TREE = 1
DOCUMENT = 2
OUTLINE = 3
class nwOutline(Enum):
+38 -4
View File
@@ -203,7 +203,7 @@ class NScrollableForm(QScrollArea):
if helpText:
qHelp = NColourLabel(
str(helpText), color=self._helpCol, parent=self,
str(helpText), self, color=self._helpCol,
scale=self._fontScale, wrap=True, indent=self._indent
)
labelBox = QVBoxLayout()
@@ -252,11 +252,20 @@ class NColourLabel(QLabel):
HELP_SCALE = DEFAULT_SCALE
HEADER_SCALE = 1.25
def __init__(self, text: str, color: QColor | None = None, parent: QWidget | None = None,
scale: float = HELP_SCALE, wrap: bool = False, indent: int = 0,
bold: bool = False) -> None:
_state = None
def __init__(
self, text: str, parent: QWidget, *,
color: QColor | None = None, faded: QColor | None = None,
scale: float = HELP_SCALE, wrap: bool = False, indent: int = 0,
bold: bool = False
) -> None:
super().__init__(text, parent=parent)
default = self.palette().windowText().color()
self._color = color or default
self._faded = faded or default
font = self.font()
font.setPointSizeF(scale*font.pointSizeF())
font.setWeight(QFont.Weight.Bold if bold else QFont.Weight.Normal)
@@ -268,9 +277,34 @@ class NColourLabel(QLabel):
self.setFont(font)
self.setIndent(indent)
self.setWordWrap(wrap)
self.setColorState(True)
return
def setTextColors(self, *, color: QColor | None = None, faded: QColor | None = None) -> None:
"""Set or update the text colours."""
self._color = color or self._color
self._faded = faded or self._faded
self._refeshTextColor()
return
def setColorState(self, state: bool) -> None:
"""Change the colour state."""
if self._state is not state:
self._state = state
self._refeshTextColor()
return
def _refeshTextColor(self) -> None:
"""Refresh the colour of the text on the label."""
palette = self.palette()
palette.setColor(
QPalette.ColorRole.WindowText,
self._color if self._state else self._faded,
)
self.setPalette(palette)
return
class NWrappedWidgetBox(QHBoxLayout):
"""Extension: A Text-Wrapped Widget Box
@@ -1,9 +1,10 @@
"""
novelWriter Custom Widget: Progress Circle
============================================
novelWriter Custom Widget: Progress Bars
==========================================
File History:
Created: 2023-06-07 [2.1b1]
Created: 2023-06-07 [2.1b1] NProgressCircle
Created: 2023-06-09 [2.1b1] NProgressSimple
This file is a part of novelWriter
Copyright 20182024, Veronica Berglyd Olsen
@@ -101,3 +102,25 @@ class NProgressCircle(QProgressBar):
painter.setPen(self._tColor)
painter.drawText(self._cRect, QtAlignCenter, self._text or f"{progress:.1f} %")
return
class NProgressSimple(QProgressBar):
"""Extension: Simple Progress Widget
A custom widget that paints a plain bar with no other styling.
"""
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
return
def paintEvent(self, event: QPaintEvent) -> None:
"""Custom painter for the progress bar."""
if (value := self.value()) > 0:
progress = ceil(self.width()*float(value)/self.maximum())
painter = QPainter(self)
painter.setRenderHint(QtPaintAnitAlias, True)
painter.setPen(self.palette().highlight().color())
painter.setBrush(self.palette().highlight())
painter.drawRect(0, 0, progress, self.height())
return
-53
View File
@@ -1,53 +0,0 @@
"""
novelWriter Custom Widget: Progress Simple
============================================
File History:
Created: 2023-06-09 [2.1b1]
This file is a part of novelWriter
Copyright 20182024, 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 <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
from math import ceil
from PyQt5.QtGui import QPainter, QPaintEvent
from PyQt5.QtWidgets import QProgressBar, QWidget
from novelwriter.types import QtPaintAnitAlias
class NProgressSimple(QProgressBar):
"""Extension: Simple Progress Widget
A custom widget that paints a plain bar with no other styling.
"""
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
return
def paintEvent(self, event: QPaintEvent) -> None:
"""Custom painter for the progress bar."""
if (value := self.value()) > 0:
progress = ceil(self.width()*float(value)/self.maximum())
painter = QPainter(self)
painter.setRenderHint(QtPaintAnitAlias, True)
painter.setPen(self.palette().highlight().color())
painter.setBrush(self.palette().highlight())
painter.drawRect(0, 0, progress, self.height())
return
+12 -2
View File
@@ -28,6 +28,7 @@ import logging
from PyQt5.QtGui import QColor, QPainter, QPaintEvent
from PyQt5.QtWidgets import QAbstractButton, QWidget
from novelwriter import CONFIG
from novelwriter.enum import nwTrinary
from novelwriter.types import QtBlack, QtPaintAnitAlias
@@ -36,6 +37,10 @@ logger = logging.getLogger(__name__)
class StatusLED(QAbstractButton):
__slots__ = (
"_neutral", "_postitve", "_negative", "_color", "_state", "_bPx"
)
def __init__(self, sW: int, sH: int, parent: QWidget | None = None) -> None:
super().__init__(parent=parent)
self._neutral = QtBlack
@@ -43,6 +48,7 @@ class StatusLED(QAbstractButton):
self._negative = QtBlack
self._color = QtBlack
self._state = nwTrinary.NEUTRAL
self._bPx = CONFIG.pxInt(1)
self.setFixedWidth(sW)
self.setFixedHeight(sH)
return
@@ -76,8 +82,12 @@ class StatusLED(QAbstractButton):
"""Draw the LED."""
painter = QPainter(self)
painter.setRenderHint(QtPaintAnitAlias, True)
painter.setPen(self.palette().dark().color())
painter.setPen(self.palette().windowText().color())
painter.setBrush(self._color)
painter.setOpacity(1.0)
painter.drawEllipse(1, 1, self.width() - 2, self.height() - 2)
painter.drawEllipse(
self._bPx, self._bPx,
self.width() - 2*self._bPx,
self.height() - 2*self._bPx
)
return
+17 -11
View File
@@ -55,6 +55,7 @@ from novelwriter.common import minmax, transferCase
from novelwriter.constants import nwConst, nwKeyWords, nwShortcode, nwUnicode
from novelwriter.core.document import NWDocument
from novelwriter.enum import nwComment, nwDocAction, nwDocInsert, nwDocMode, nwItemClass, nwTrinary
from novelwriter.extensions.configlayout import NColourLabel
from novelwriter.extensions.eventfilters import WheelEventFilter
from novelwriter.extensions.modified import NIconToggleButton, NIconToolButton
from novelwriter.gui.dochighlight import BLOCK_META, BLOCK_TITLE
@@ -210,6 +211,7 @@ class GuiDocEditor(QPlainTextEdit):
# Function Mapping
self.closeSearch = self.docSearch.closeSearch
self.searchVisible = self.docSearch.isVisible
self.changeFocusState = self.docHeader.changeFocusState
# Finalise
self.updateSyntaxColours()
@@ -436,11 +438,6 @@ class GuiDocEditor(QPlainTextEdit):
return True
def updateTagHighLighting(self) -> None:
"""Rerun the syntax highlighter on all meta data lines."""
self._qDocument.syntaxHighlighter.rehighlightByType(BLOCK_META)
return
def replaceText(self, text: str) -> None:
"""Replace the text of the current document with the provided
text. This also clears undo history.
@@ -1034,6 +1031,13 @@ class GuiDocEditor(QPlainTextEdit):
self.beginSearch()
return
@pyqtSlot(list, list)
def updateChangedTags(self, updated: list[str], deleted: list[str]) -> None:
"""Tags have changed, so just in case we rehighlight them."""
if updated or deleted:
self._qDocument.syntaxHighlighter.rehighlightByType(BLOCK_META)
return
##
# Private Slots
##
@@ -1922,8 +1926,6 @@ class GuiDocEditor(QPlainTextEdit):
).format(tag)):
itemClass = nwKeyWords.KEY_CLASS.get(tBits[0], nwItemClass.NO_CLASS)
self.requestNewNoteCreation.emit(tag, itemClass)
QApplication.processEvents()
self._qDocument.syntaxHighlighter.rehighlightBlock(block)
return nwTrinary.POSITIVE if exist else nwTrinary.NEGATIVE
@@ -2785,8 +2787,7 @@ class GuiDocEditHeader(QWidget):
self.setAutoFillBackground(True)
# Title Label
self.itemTitle = QLabel("", self)
self.itemTitle.setIndent(0)
self.itemTitle = NColourLabel("", self, faded=SHARED.theme.fadedText)
self.itemTitle.setMargin(0)
self.itemTitle.setContentsMargins(0, 0, 0, 0)
self.itemTitle.setAutoFillBackground(True)
@@ -2918,10 +2919,15 @@ class GuiDocEditHeader(QWidget):
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)
self.itemTitle.setTextColors(
color=palette.windowText().color(), faded=SHARED.theme.fadedText
)
return
def changeFocusState(self, state: bool) -> None:
"""Toggle focus state."""
self.itemTitle.setColorState(state)
return
def setHandle(self, tHandle: str) -> None:
+18 -5
View File
@@ -33,7 +33,7 @@ from enum import Enum
from PyQt5.QtCore import QPoint, Qt, QUrl, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QCursor, QMouseEvent, QPalette, QResizeEvent, QTextCursor
from PyQt5.QtWidgets import (
QAction, QApplication, QFrame, QHBoxLayout, QLabel, QMenu, QTextBrowser,
QAction, QApplication, QFrame, QHBoxLayout, QMenu, QTextBrowser,
QToolButton, QWidget
)
@@ -42,6 +42,7 @@ from novelwriter.constants import nwHeaders, nwUnicode
from novelwriter.core.toqdoc import TextDocumentTheme, ToQTextDocument
from novelwriter.enum import nwDocAction, nwDocMode, nwItemType
from novelwriter.error import logException
from novelwriter.extensions.configlayout import NColourLabel
from novelwriter.extensions.eventfilters import WheelEventFilter
from novelwriter.extensions.modified import NIconToolButton
from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
@@ -92,6 +93,9 @@ class GuiDocViewer(QTextBrowser):
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.customContextMenuRequested.connect(self._openContextMenu)
# Function Mapping
self.changeFocusState = self.docHeader.changeFocusState
self.initViewer()
logger.debug("Ready: GuiDocViewer")
@@ -278,6 +282,10 @@ class GuiDocViewer(QTextBrowser):
return False
return True
def anyFocus(self) -> bool:
"""Check if any widget or child widget has focus."""
return self.hasFocus() or self.isAncestorOf(QApplication.focusWidget())
def clearNavHistory(self) -> None:
"""Clear the navigation history."""
self.docHistory.clear()
@@ -597,9 +605,7 @@ class GuiDocViewHeader(QWidget):
self.setAutoFillBackground(True)
# Title Label
self.itemTitle = QLabel(self)
self.itemTitle.setText("")
self.itemTitle.setIndent(0)
self.itemTitle = NColourLabel("", self, faded=SHARED.theme.fadedText)
self.itemTitle.setMargin(0)
self.itemTitle.setContentsMargins(0, 0, 0, 0)
self.itemTitle.setAutoFillBackground(True)
@@ -735,7 +741,14 @@ class GuiDocViewHeader(QWidget):
palette.setColor(QPalette.ColorRole.WindowText, SHARED.theme.colText)
palette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
self.setPalette(palette)
self.itemTitle.setPalette(palette)
self.itemTitle.setTextColors(
color=palette.windowText().color(), faded=SHARED.theme.fadedText
)
return
def changeFocusState(self, state: bool) -> None:
"""Toggle focus state."""
self.itemTitle.setColorState(state)
return
def setHandle(self, tHandle: str) -> None:
+9 -9
View File
@@ -35,7 +35,7 @@ from PyQt5.QtWidgets import QAction, QMenuBar
from novelwriter import CONFIG, SHARED
from novelwriter.common import openExternalPath
from novelwriter.constants import nwConst, nwKeyWords, nwLabels, nwUnicode, trConst
from novelwriter.enum import nwDocAction, nwDocInsert, nwView, nwWidget
from novelwriter.enum import nwDocAction, nwDocInsert, nwFocus, nwView
from novelwriter.extensions.eventfilters import StatusTipFilter
if TYPE_CHECKING: # pragma: no cover
@@ -54,7 +54,7 @@ class GuiMainMenu(QMenuBar):
requestDocInsert = pyqtSignal(nwDocInsert)
requestDocInsertText = pyqtSignal(str)
requestDocKeyWordInsert = pyqtSignal(str)
requestFocusChange = pyqtSignal(nwWidget)
requestFocusChange = pyqtSignal(nwFocus)
requestViewChange = pyqtSignal(nwView)
def __init__(self, mainGui: GuiMain) -> None:
@@ -303,24 +303,24 @@ class GuiMainMenu(QMenuBar):
self.viewMenu = self.addMenu(self.tr("&View"))
# View > TreeView
self.aFocusTree = self.viewMenu.addAction(self.tr("Go to Project Tree"))
self.aFocusTree = self.viewMenu.addAction(self.tr("Go to Tree View"))
self.aFocusTree.setShortcut("Ctrl+T")
self.aFocusTree.triggered.connect(
lambda: self.requestFocusChange.emit(nwWidget.TREE)
lambda: self.requestFocusChange.emit(nwFocus.TREE)
)
# View > Document Editor
self.aFocusEditor = self.viewMenu.addAction(self.tr("Go to Document Editor"))
self.aFocusEditor.setShortcut("Ctrl+E")
self.aFocusEditor.triggered.connect(
lambda: self.requestFocusChange.emit(nwWidget.EDITOR)
self.aFocusDocument = self.viewMenu.addAction(self.tr("Go to Document"))
self.aFocusDocument.setShortcut("Ctrl+E")
self.aFocusDocument.triggered.connect(
lambda: self.requestFocusChange.emit(nwFocus.DOCUMENT)
)
# View > Outline
self.aFocusOutline = self.viewMenu.addAction(self.tr("Go to Outline"))
self.aFocusOutline.setShortcut("Ctrl+Shift+T")
self.aFocusOutline.triggered.connect(
lambda: self.requestFocusChange.emit(nwWidget.OUTLINE)
lambda: self.requestFocusChange.emit(nwFocus.OUTLINE)
)
# View > Separator
+4 -3
View File
@@ -215,7 +215,7 @@ class GuiOutlineToolBar(QToolBar):
# Novel Selector
self.novelLabel = NColourLabel(
self.tr("Outline of"), parent=self, scale=NColourLabel.HEADER_SCALE, bold=True
self.tr("Outline of"), self, scale=NColourLabel.HEADER_SCALE, bold=True
)
self.novelLabel.setContentsMargins(0, 0, CONFIG.pxInt(12), 0)
@@ -268,6 +268,7 @@ class GuiOutlineToolBar(QToolBar):
self.aExport.setIcon(SHARED.theme.getIcon("export"))
self.tbColumns.setIcon(SHARED.theme.getIcon("menu"))
self.tbColumns.setStyleSheet("QToolButton::menu-indicator {image: none;}")
self.novelLabel.setTextColors(color=self.palette().windowText().color())
return
def populateNovelList(self) -> None:
@@ -523,12 +524,12 @@ class GuiOutlineTree(QTreeWidget):
@pyqtSlot()
def exportOutline(self) -> None:
"""Export the outline as a CSV file."""
path = CONFIG.lastPath() / f"{makeFileNameSafe(SHARED.project.data.name)}.csv"
path = CONFIG.lastPath("outline") / f"{makeFileNameSafe(SHARED.project.data.name)}.csv"
path, _ = QFileDialog.getSaveFileName(
self, self.tr("Save Outline As"), str(path), formatFileFilter(["*.csv", "*"])
)
if path:
CONFIG.setLastPath(path)
CONFIG.setLastPath("outline", path)
logger.info("Writing CSV file: %s", path)
cols = [col for col in self._treeOrder if not self._colHidden[col]]
order = [self._colIdx[col] for col in cols]
+3 -3
View File
@@ -997,7 +997,7 @@ class GuiProjectTree(QTreeWidget):
trItemP.takeChild(tIndex)
for dHandle in reversed(self.getTreeFromHandle(tHandle)):
SHARED.closeDocument(dHandle)
SHARED.closeEditor(dHandle)
SHARED.project.removeItem(dHandle)
self._treeMap.pop(dHandle, None)
@@ -1404,7 +1404,7 @@ class GuiProjectTree(QTreeWidget):
return False
# Save the open document first, in case it's part of merge
SHARED.saveDocument()
SHARED.saveEditor()
# Create merge object, and append docs
docMerger = DocMerger(SHARED.project)
@@ -1805,7 +1805,7 @@ class _TreeContextMenu(QMenu):
def _itemHeader(self) -> None:
"""Check if there is a header that can be used for rename."""
SHARED.ensureEditorSaved(self._handle)
SHARED.saveEditor()
if hItem := SHARED.project.index.getItemHeading(self._handle, "T0001"):
action = self.addAction(self.tr("Rename to Heading"))
action.triggered.connect(
+10 -1
View File
@@ -208,6 +208,12 @@ class GuiProjectSearch(QWidget):
self.searchResult.clear()
return
def refreshCurrentSearch(self) -> None:
"""Refresh the search if there is one."""
if self.searchResult.topLevelItemCount() > 0:
self._processSearch()
return
##
# Events
##
@@ -259,7 +265,7 @@ class GuiProjectSearch(QWidget):
if not self._blocked:
QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
start = time()
SHARED.saveDocument()
SHARED.saveEditor()
self._blocked = True
self._map = {}
self.searchResult.clear()
@@ -298,18 +304,21 @@ class GuiProjectSearch(QWidget):
def _toggleCase(self, state: bool) -> None:
"""Enable/disable case sensitive mode."""
CONFIG.searchProjCase = state
self.refreshCurrentSearch()
return
@pyqtSlot(bool)
def _toggleWord(self, state: bool) -> None:
"""Enable/disable whole word search mode."""
CONFIG.searchProjWord = state
self.refreshCurrentSearch()
return
@pyqtSlot(bool)
def _toggleRegEx(self, state: bool) -> None:
"""Enable/disable regular expression search mode."""
CONFIG.searchProjRegEx = state
self.refreshCurrentSearch()
return
##
+3
View File
@@ -75,6 +75,7 @@ class GuiTheme:
self.statUnsaved = QColor(0, 0, 0)
self.statSaved = QColor(0, 0, 0)
self.helpText = QColor(0, 0, 0)
self.fadedText = QColor(0, 0, 0)
self.errorText = QColor(255, 0, 0)
# Loaded Syntax Settings
@@ -263,6 +264,7 @@ class GuiTheme:
sec = "GUI"
if parser.has_section(sec):
self.helpText = self._parseColour(parser, sec, "helptext")
self.fadedText = self._parseColour(parser, sec, "fadedtext")
self.errorText = self._parseColour(parser, sec, "errortext")
self.statNone = self._parseColour(parser, sec, "statusnone")
self.statUnsaved = self._parseColour(parser, sec, "statusunsaved")
@@ -405,6 +407,7 @@ class GuiTheme:
self.statUnsaved = QColor(200, 15, 39)
self.statSaved = QColor(2, 133, 37)
self.helpText = QColor(0, 0, 0)
self.fadedText = QColor(128, 128, 128)
self.errorText = QColor(255, 0, 0)
return
+109 -70
View File
@@ -44,7 +44,7 @@ from novelwriter.dialogs.about import GuiAbout
from novelwriter.dialogs.preferences import GuiPreferences
from novelwriter.dialogs.projectsettings import GuiProjectSettings
from novelwriter.dialogs.wordlist import GuiWordList
from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemType, nwView, nwWidget
from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwFocus, nwItemType, nwView
from novelwriter.gui.doceditor import GuiDocEditor
from novelwriter.gui.docviewer import GuiDocViewer
from novelwriter.gui.docviewerpanel import GuiDocViewerPanel
@@ -210,67 +210,68 @@ class GuiMain(QMainWindow):
# Connect Signals
# ===============
SHARED.focusModeChanged.connect(self._focusModeChanged)
SHARED.indexAvailable.connect(self.docViewerPanel.indexHasAppeared)
SHARED.indexChangedTags.connect(self.docEditor.updateChangedTags)
SHARED.indexChangedTags.connect(self.docViewerPanel.updateChangedTags)
SHARED.indexCleared.connect(self.docViewerPanel.indexWasCleared)
SHARED.indexScannedText.connect(self.docViewerPanel.projectItemChanged)
SHARED.indexScannedText.connect(self.itemDetails.updateViewBox)
SHARED.indexScannedText.connect(self.projView.updateItemValues)
SHARED.mainClockTick.connect(self._timeTick)
SHARED.projectStatusChanged.connect(self.mainStatus.updateProjectStatus)
SHARED.projectStatusMessage.connect(self.mainStatus.setStatusMessage)
SHARED.spellLanguageChanged.connect(self.mainStatus.setLanguage)
SHARED.focusModeChanged.connect(self._focusModeChanged)
SHARED.indexChangedTags.connect(self.docViewerPanel.updateChangedTags)
SHARED.indexScannedText.connect(self.docViewerPanel.projectItemChanged)
SHARED.indexScannedText.connect(self.projView.updateItemValues)
SHARED.indexScannedText.connect(self.itemDetails.updateViewBox)
SHARED.indexCleared.connect(self.docViewerPanel.indexWasCleared)
SHARED.indexAvailable.connect(self.docViewerPanel.indexHasAppeared)
SHARED.mainClockTick.connect(self._timeTick)
self.mainMenu.requestDocAction.connect(self._passDocumentAction)
self.mainMenu.requestDocInsert.connect(self._passDocumentInsert)
self.mainMenu.requestDocInsertText.connect(self._passDocumentInsert)
self.mainMenu.requestDocKeyWordInsert.connect(self.docEditor.insertKeyWord)
self.mainMenu.requestFocusChange.connect(self.switchFocus)
self.mainMenu.requestFocusChange.connect(self._switchFocus)
self.mainMenu.requestViewChange.connect(self._changeView)
self.sideBar.requestViewChange.connect(self._changeView)
self.projView.selectedItemChanged.connect(self.itemDetails.updateViewBox)
self.projView.openDocumentRequest.connect(self._openDocument)
self.projView.wordCountsChanged.connect(self._updateStatusWordCount)
self.projView.projectSettingsRequest.connect(self.showProjectSettingsDialog)
self.projView.rootFolderChanged.connect(self.novelView.updateRootItem)
self.projView.rootFolderChanged.connect(self.outlineView.updateRootItem)
self.projView.rootFolderChanged.connect(self.projView.updateRootItem)
self.projView.selectedItemChanged.connect(self.itemDetails.updateViewBox)
self.projView.treeItemChanged.connect(self.docEditor.updateDocInfo)
self.projView.treeItemChanged.connect(self.docViewer.updateDocInfo)
self.projView.treeItemChanged.connect(self.itemDetails.updateViewBox)
self.projView.treeItemChanged.connect(self.docViewerPanel.projectItemChanged)
self.projView.rootFolderChanged.connect(self.outlineView.updateRootItem)
self.projView.rootFolderChanged.connect(self.novelView.updateRootItem)
self.projView.rootFolderChanged.connect(self.projView.updateRootItem)
self.projView.projectSettingsRequest.connect(self.showProjectSettingsDialog)
self.projView.treeItemChanged.connect(self.itemDetails.updateViewBox)
self.projView.wordCountsChanged.connect(self._updateStatusWordCount)
self.novelView.selectedItemChanged.connect(self.itemDetails.updateViewBox)
self.novelView.openDocumentRequest.connect(self._openDocument)
self.novelView.selectedItemChanged.connect(self.itemDetails.updateViewBox)
self.projSearch.openDocumentSelectRequest.connect(self._openDocumentSelection)
self.projSearch.selectedItemChanged.connect(self.itemDetails.updateViewBox)
self.docEditor.editedStatusChanged.connect(self.mainStatus.updateDocumentStatus)
self.docEditor.closeDocumentRequest.connect(self.closeDocEditor)
self.docEditor.docCountsChanged.connect(self.itemDetails.updateCounts)
self.docEditor.docCountsChanged.connect(self.projView.updateCounts)
self.docEditor.loadDocumentTagRequest.connect(self._followTag)
self.docEditor.novelStructureChanged.connect(self.novelView.refreshTree)
self.docEditor.novelItemMetaChanged.connect(self.novelView.updateNovelItemMeta)
self.docEditor.statusMessage.connect(self.mainStatus.setStatusMessage)
self.docEditor.spellCheckStateChanged.connect(self.mainMenu.setSpellCheckState)
self.docEditor.closeDocumentRequest.connect(self.closeDocEditor)
self.docEditor.toggleFocusModeRequest.connect(self.toggleFocusMode)
self.docEditor.requestProjectItemSelected.connect(self.projView.setSelectedHandle)
self.docEditor.requestProjectItemRenamed.connect(self.projView.renameTreeItem)
self.docEditor.requestNewNoteCreation.connect(self.projView.createNewNote)
self.docEditor.docTextChanged.connect(self.projSearch.textChanged)
self.docEditor.editedStatusChanged.connect(self.mainStatus.updateDocumentStatus)
self.docEditor.loadDocumentTagRequest.connect(self._followTag)
self.docEditor.novelItemMetaChanged.connect(self.novelView.updateNovelItemMeta)
self.docEditor.novelStructureChanged.connect(self.novelView.refreshTree)
self.docEditor.requestNewNoteCreation.connect(self.projView.createNewNote)
self.docEditor.requestNextDocument.connect(self.openNextDocument)
self.docEditor.requestProjectItemRenamed.connect(self.projView.renameTreeItem)
self.docEditor.requestProjectItemSelected.connect(self.projView.setSelectedHandle)
self.docEditor.spellCheckStateChanged.connect(self.mainMenu.setSpellCheckState)
self.docEditor.statusMessage.connect(self.mainStatus.setStatusMessage)
self.docEditor.toggleFocusModeRequest.connect(self.toggleFocusMode)
self.docViewer.closeDocumentRequest.connect(self.closeDocViewer)
self.docViewer.documentLoaded.connect(self.docViewerPanel.updateHandle)
self.docViewer.loadDocumentTagRequest.connect(self._followTag)
self.docViewer.closeDocumentRequest.connect(self.closeDocViewer)
self.docViewer.reloadDocumentRequest.connect(self._reloadViewer)
self.docViewer.togglePanelVisibility.connect(self._toggleViewerPanelVisibility)
self.docViewer.requestProjectItemSelected.connect(self.projView.setSelectedHandle)
self.docViewer.togglePanelVisibility.connect(self._toggleViewerPanelVisibility)
self.docViewerPanel.loadDocumentTagRequest.connect(self._followTag)
self.docViewerPanel.openDocumentRequest.connect(self._openDocument)
@@ -325,6 +326,11 @@ class GuiMain(QMainWindow):
def postLaunchTasks(self, cmdOpen: str | None) -> None:
"""Process tasks after the main window has been created."""
QApplication.processEvents()
app = QApplication.instance()
if isinstance(app, QApplication):
app.focusChanged.connect(self._appFocusChanged)
# Check that config loaded fine
if CONFIG.hasError:
SHARED.error(CONFIG.errorText())
@@ -652,7 +658,7 @@ class GuiMain(QMainWindow):
logger.error("No project open")
return False
lastPath = CONFIG.lastPath()
lastPath = CONFIG.lastPath("import")
ffilter = formatFileFilter(["*.txt", "*.md", "*.nwd", "*"])
loadFile, _ = QFileDialog.getOpenFileName(
self, self.tr("Import File"), str(lastPath), filter=ffilter
@@ -667,7 +673,7 @@ class GuiMain(QMainWindow):
try:
with open(loadFile, mode="rt", encoding="utf-8") as inFile:
text = inFile.read()
CONFIG.setLastPath(loadFile)
CONFIG.setLastPath("import", loadFile)
except Exception as exc:
SHARED.error(self.tr(
"Could not read file. The file must be an existing text file."
@@ -745,7 +751,6 @@ class GuiMain(QMainWindow):
self.mainStatus.setStatusMessage(
self.tr("Indexing completed in {0} ms").format(f"{(tEnd - tStart)*1000.0:.1f}")
)
self.docEditor.updateTagHighLighting()
self._updateStatusWordCount()
QApplication.restoreOverrideCursor()
@@ -944,14 +949,37 @@ class GuiMain(QMainWindow):
SHARED.setFocusMode(not SHARED.focusMode)
return
##
# Private Slots
##
@pyqtSlot("QWidget*", "QWidget*")
def _appFocusChanged(self, old: QWidget, new: QWidget) -> None:
"""Alert main widgets that they have received or lost focus."""
if isinstance(new, QWidget):
docEditor = False
docViewer = False
if self.docEditor.isAncestorOf(new):
docEditor = True
elif self.docViewer.isAncestorOf(new):
docViewer = True
self.docEditor.changeFocusState(docEditor)
self.docViewer.changeFocusState(docViewer)
logger.debug("Main focus switched to: %s", type(new).__name__)
return
@pyqtSlot(bool)
def _focusModeChanged(self, focusMode: bool) -> None:
"""Handle change of focus mode. The Main GUI Focus Mode hides tree,
view, statusbar and menu.
"""Handle change of focus mode. The Main GUI Focus Mode hides
tree, view, statusbar and menu.
"""
if focusMode:
logger.debug("Activating Focus Mode")
self.switchFocus(nwWidget.EDITOR)
self._changeView(nwView.EDITOR)
self.docEditor.setFocus()
else:
logger.debug("Deactivating Focus Mode")
@@ -974,39 +1002,53 @@ class GuiMain(QMainWindow):
self.docEditor.ensureCursorVisibleNoCentre()
return
@pyqtSlot(nwWidget)
def switchFocus(self, paneNo: nwWidget) -> None:
@pyqtSlot(nwFocus)
def _switchFocus(self, paneNo: nwFocus) -> None:
"""Switch focus between main GUI views."""
if paneNo == nwWidget.TREE:
if self.projStack.currentWidget() is self.projView:
if self.projView.treeHasFocus():
self._changeView(nwView.NOVEL)
self.novelView.setTreeFocus()
else:
self.projView.setTreeFocus()
elif self.projStack.currentWidget() is self.novelView:
if self.novelView.treeHasFocus():
self._changeView(nwView.PROJECT)
self.projView.setTreeFocus()
else:
self.novelView.setTreeFocus()
if paneNo == nwFocus.TREE:
# Decision Matrix
# vM | vP | fP | vN | fN | Focus
# ----|----|----|----|----|---------
# T | T | T | F | F | Novel
# T | T | F | F | F | Project
# T | F | F | T | T | Project
# T | F | F | T | F | Novel
# T | F | F | F | F | Project
# F | T | T | F | F | Project
# F | T | F | F | F | Project
# F | F | F | T | T | Novel
# F | F | F | T | F | Novel
# F | F | F | F | F | Project
vM = self.mainStack.currentWidget() is self.splitMain
vP = self.projStack.currentWidget() is self.projView
vN = self.projStack.currentWidget() is self.novelView
fP = self.projView.treeHasFocus()
fN = self.novelView.treeHasFocus()
self._changeView(nwView.EDITOR)
if (vM and (vP and fP or vN and not fN)) or (not vM and vN):
self._changeView(nwView.NOVEL)
self.novelView.setTreeFocus()
else:
self._changeView(nwView.PROJECT)
self.projView.setTreeFocus()
elif paneNo == nwWidget.EDITOR:
elif paneNo == nwFocus.DOCUMENT:
self._changeView(nwView.EDITOR)
self.docEditor.setFocus()
elif paneNo == nwWidget.VIEWER:
self._changeView(nwView.EDITOR)
self.docViewer.setFocus()
elif paneNo == nwWidget.OUTLINE:
hasViewer = self.splitView.isVisible()
if hasViewer and self.docEditor.anyFocus():
self.docViewer.setFocus()
elif hasViewer and self.docViewer.anyFocus():
self.docEditor.setFocus()
else:
self.docEditor.setFocus()
elif paneNo == nwFocus.OUTLINE:
self._changeView(nwView.OUTLINE)
self.outlineView.setTreeFocus()
return
##
# Private Slots
##
return
@pyqtSlot(bool, bool, bool, bool)
def _processConfigChanges(self, restart: bool, tree: bool, theme: bool, syntax: bool) -> None:
@@ -1146,16 +1188,13 @@ class GuiMain(QMainWindow):
@pyqtSlot(nwDocAction)
def _passDocumentAction(self, action: nwDocAction) -> None:
"""Pass on a document action to the document viewer if it has
focus, or pass it to the document editor if it or any of its
child widgets have focus. If neither has focus, ignore it.
"""Pass on a document action to the editor or viewer based on
which one has focus, or if neither has focus, ignore it.
"""
if self.docViewer.hasFocus():
self.docViewer.docAction(action)
elif self.docEditor.hasFocus():
if self.docEditor.hasFocus():
self.docEditor.docAction(action)
else:
logger.debug("Action cancelled as neither editor nor viewer has focus")
elif self.docViewer.hasFocus():
self.docViewer.docAction(action)
return
@pyqtSlot(str)
+10 -17
View File
@@ -172,15 +172,21 @@ class SharedData(QObject):
logger.debug("Thread Pool Max Count: %d", QThreadPool.globalInstance().maxThreadCount())
return
def closeDocument(self, tHandle: str | None = None) -> None:
def closeEditor(self, tHandle: str | None = None) -> None:
"""Close the document editor, optionally a specific document."""
if tHandle is None or tHandle == self.mainGui.docEditor.docHandle:
self.mainGui.closeDocument()
return
def saveDocument(self) -> None:
"""Forward save document call to main GUI."""
self.mainGui.saveDocument()
def saveEditor(self, tHandle: str | None = None) -> None:
"""Save the editor content, optionally a specific document."""
docEditor = self.mainGui.docEditor
if (
self.hasProject and docEditor.docHandle
and (tHandle is None or tHandle == docEditor.docHandle)
):
logger.debug("Saving editor document before action")
docEditor.saveText()
return
def openProject(self, path: str | Path, clearLock: bool = False) -> bool:
@@ -216,19 +222,6 @@ class SharedData(QObject):
self._resetIdleTimer()
return
def ensureEditorSaved(self, tHandle: str | None) -> None:
"""Ensure that the editor content is saved. Optionally, only if
it is a specific handle.
"""
docEditor = self.mainGui.docEditor
if (
self.hasProject and docEditor.docHandle
and (tHandle is None or tHandle == docEditor.docHandle)
):
logger.debug("Saving editor document before action")
docEditor.saveText()
return
def updateSpellCheckLanguage(self, reload: bool = False) -> None:
"""Update the active spell check language from settings."""
from novelwriter import CONFIG
+5 -5
View File
@@ -43,7 +43,7 @@ from novelwriter.core.docbuild import NWBuildDocument
from novelwriter.core.item import NWItem
from novelwriter.enum import nwBuildFmt
from novelwriter.extensions.modified import NDialog, NIconToolButton
from novelwriter.extensions.simpleprogress import NProgressSimple
from novelwriter.extensions.progressbars import NProgressSimple
from novelwriter.types import QtAlignCenter, QtDialogClose, QtRoleAction, QtRoleReject, QtUserRole
logger = logging.getLogger(__name__)
@@ -220,7 +220,7 @@ class GuiManuscriptBuild(NDialog):
self.btnBuild.setFocus()
self._populateContentList()
self.buildPath.setText(str(self._build.lastPath))
self.buildPath.setText(str(self._build.lastBuildPath))
if self._build.lastBuildName:
self.buildName.setText(self._build.lastBuildName)
else:
@@ -274,7 +274,7 @@ class GuiManuscriptBuild(NDialog):
def _doSelectPath(self) -> None:
"""Select a folder for output."""
bPath = Path(self.buildPath.text())
bPath = bPath if bPath.is_dir() else self._build.lastPath
bPath = bPath if bPath.is_dir() else self._build.lastBuildPath
savePath = QFileDialog.getExistingDirectory(
self, self.tr("Select Folder"), str(bPath)
)
@@ -327,7 +327,7 @@ class GuiManuscriptBuild(NDialog):
return False
# Make sure editor content is saved before we start
SHARED.saveDocument()
SHARED.saveEditor()
docBuild = NWBuildDocument(SHARED.project, self._build)
docBuild.queueAll()
@@ -336,7 +336,7 @@ class GuiManuscriptBuild(NDialog):
for i, _ in docBuild.iterBuild(buildPath, bFormat):
self.buildProgress.setValue(i+1)
self._build.setLastPath(bPath)
self._build.setLastBuildPath(bPath)
self._build.setLastBuildName(bName)
self._build.setLastFormat(bFormat)
+2 -2
View File
@@ -44,8 +44,8 @@ from novelwriter.core.buildsettings import BuildCollection, BuildSettings
from novelwriter.core.docbuild import NWBuildDocument
from novelwriter.core.tokenizer import HeadingFormatter
from novelwriter.core.toqdoc import TextDocumentTheme, ToQTextDocument
from novelwriter.extensions.circularprogress import NProgressCircle
from novelwriter.extensions.modified import NIconToggleButton, NIconToolButton, NToolDialog
from novelwriter.extensions.progressbars import NProgressCircle
from novelwriter.gui.theme import STYLES_FLAT_TABS, STYLES_MIN_TOOLBUTTON
from novelwriter.tools.manusbuild import GuiManuscriptBuild
from novelwriter.tools.manussettings import GuiBuildSettings
@@ -325,7 +325,7 @@ class GuiManuscript(NToolDialog):
start = time()
# Make sure editor content is saved before we start
SHARED.ensureEditorSaved(None)
SHARED.saveEditor()
docBuild = NWBuildDocument(SHARED.project, build)
docBuild.queueAll()
+2 -2
View File
@@ -98,8 +98,8 @@ class GuiBuildSettings(NToolDialog):
# Title
self.titleLabel = NColourLabel(
self.tr("Manuscript Build Settings"), SHARED.theme.helpText,
parent=self, scale=NColourLabel.HEADER_SCALE, indent=CONFIG.pxInt(4)
self.tr("Manuscript Build Settings"), self, color=SHARED.theme.helpText,
scale=NColourLabel.HEADER_SCALE, indent=CONFIG.pxInt(4)
)
# Settings Name
+8 -8
View File
@@ -68,8 +68,8 @@ class GuiNovelDetails(NNonBlockingDialog):
# Title
self.titleLabel = NColourLabel(
self.tr("Novel Details"), SHARED.theme.helpText,
parent=self, scale=NColourLabel.HEADER_SCALE, indent=CONFIG.pxInt(4)
self.tr("Novel Details"), self, color=SHARED.theme.helpText,
scale=NColourLabel.HEADER_SCALE, indent=CONFIG.pxInt(4)
)
# Novel Selector
@@ -199,8 +199,8 @@ class _OverviewPage(NScrollablePage):
# Project Info
self.projLabel = NColourLabel(
self.tr("Project"), SHARED.theme.helpText,
parent=self, scale=NColourLabel.HEADER_SCALE
self.tr("Project"), self, color=SHARED.theme.helpText,
scale=NColourLabel.HEADER_SCALE
)
self.projName = QLabel("", self)
@@ -223,8 +223,8 @@ class _OverviewPage(NScrollablePage):
# Novel Info
self.novelLabel = NColourLabel(
self.tr("Selected Novel"), SHARED.theme.helpText,
parent=self, scale=NColourLabel.HEADER_SCALE
self.tr("Selected Novel"), self, color=SHARED.theme.helpText,
scale=NColourLabel.HEADER_SCALE
)
self.novelName = QLabel("", self)
@@ -315,8 +315,8 @@ class _ContentsPage(NFixedPage):
# Title
self.contentLabel = NColourLabel(
self.tr("Table of Contents"), SHARED.theme.helpText,
parent=self, scale=NColourLabel.HEADER_SCALE
self.tr("Table of Contents"), self, color=SHARED.theme.helpText,
scale=NColourLabel.HEADER_SCALE
)
# Contents Tree
+14 -5
View File
@@ -208,6 +208,7 @@ class GuiWelcome(NDialog):
"""Show the create new project page."""
self.mainStack.setCurrentWidget(self.tabNew)
self._setButtonVisibility()
self.tabNew.enterForm()
return
@pyqtSlot()
@@ -220,8 +221,7 @@ class GuiWelcome(NDialog):
@pyqtSlot()
def _browseForProject(self) -> None:
"""Browse for a project to open."""
if path := SHARED.getProjectPath(self, path=CONFIG.lastPath(), allowZip=False):
CONFIG.setLastPath(path)
if path := SHARED.getProjectPath(self, path=CONFIG.homePath(), allowZip=False):
self._openProjectPath(path)
return
@@ -504,6 +504,8 @@ class _NewProjectPage(QWidget):
self.scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.enterForm = self.projectForm.enterForm
# Assemble
# ========
@@ -550,7 +552,7 @@ class _NewProjectForm(QWidget):
def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
self._basePath = CONFIG.homePath()
self._basePath = CONFIG.lastPath("project")
self._fillMode = self.FILL_BLANK
self._copyPath = None
@@ -697,6 +699,12 @@ class _NewProjectForm(QWidget):
return
def enterForm(self) -> None:
"""Focus the project name field when entering the form."""
self.projName.setFocus()
self.projName.selectAll()
return
def getProjectData(self) -> dict:
"""Collect form data and return it as a dictionary."""
roots = []
@@ -726,12 +734,13 @@ class _NewProjectForm(QWidget):
@pyqtSlot()
def _doBrowse(self) -> None:
"""Select a project folder."""
if projDir := QFileDialog.getExistingDirectory(
if path := QFileDialog.getExistingDirectory(
self, self.tr("Select Project Folder"),
str(self._basePath), options=QFileDialog.Option.ShowDirsOnly
):
self._basePath = Path(projDir)
self._basePath = Path(path)
self._updateProjPath()
CONFIG.setLastPath("project", path)
return
@pyqtSlot()
+2 -2
View File
@@ -384,14 +384,14 @@ class GuiWritingStats(NToolDialog):
return False
# Generate the file name
savePath = CONFIG.lastPath() / f"sessionStats.{fileExt}"
savePath = CONFIG.lastPath("stats") / f"sessionStats.{fileExt}"
savePath, _ = QFileDialog.getSaveFileName(
self, self.tr("Save Data As"), str(savePath), f"{textFmt} (*.{fileExt})"
)
if not savePath:
return False
CONFIG.setLastPath(savePath)
CONFIG.setLastPath("stats", savePath)
# Do the actual writing
wSuccess = False
+653 -825
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.5b1" hexVersion="0x020500b1" fileVersion="1.5" fileRevision="4" timeStamp="2024-05-27 12:11:38">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1948" autoCount="277" editTime="90871">
<novelWriterXML appVersion="2.5rc1" hexVersion="0x020500c1" fileVersion="1.5" fileRevision="4" timeStamp="2024-06-16 21:29:46">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1949" autoCount="277" editTime="90875">
<name>Sample Project</name>
<author>Jane Smith</author>
</project>
+1 -1
View File
@@ -18,7 +18,7 @@ if [ ! -d $ENVPATH ]; then
fi
source $ENVPATH/bin/activate
pip3 install -r docs/source/requirements.txt
python3 pkgutils.py qtlrelease manual sample
python3 pkgutils.py build-assets
deactivate
echo ""
+1 -8
View File
@@ -17,16 +17,9 @@ if [ ! -d $ENVPATH ]; then
fi
source $ENVPATH/bin/activate
pip3 install -r docs/source/requirements.txt
python3 pkgutils.py clean-assets
python3 pkgutils.py qtlrelease manual sample
python3 pkgutils.py build-assets
deactivate
echo ""
echo " Building Windows Source Zip"
echo "================================================================================"
echo ""
python3 pkgutils.py windows-zip
echo ""
echo " Building Linux Packages"
echo "================================================================================"
+9 -6
View File
@@ -1,7 +1,6 @@
; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
; Script for building setup.exe installer with Inno Setup
#define nwAppDir "%%dir%%\dist"
#define nwAppDir "%%dist%%"
#define nwAppName "novelWriter"
#define nwAppVersion "%%version%%"
#define nwAppPublisher "novelWriter"
@@ -18,6 +17,7 @@ AppPublisherURL={#nwAppURL}
AppSupportURL={#nwAppURL}
AppUpdatesURL={#nwAppURL}
SetupIconFile=setup\icons\novelwriter.ico
UninstallDisplayIcon={app}\novelwriter.ico
DefaultDirName={autopf}\{#nwAppName}
LicenseFile=setup\iss_license.txt
DisableProgramGroupPage=yes
@@ -25,10 +25,10 @@ UsedUserAreasWarning=no
PrivilegesRequiredOverridesAllowed=dialog
OutputDir={#nwAppDir}
OutputBaseFilename=novelwriter-{#nwAppVersion}-amd64-setup
Compression=lzma
Compression=zip
SolidCompression=yes
WizardStyle=modern
ArchitecturesInstallIn64BitMode=x64
ArchitecturesInstallIn64BitMode=x64compatible
ChangesAssociations=yes
[Languages]
@@ -36,11 +36,14 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 6.1; Check: not IsAdminInstallMode
Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; Check: not IsAdminInstallMode
[InstallDelete]
Type: filesandordirs; Name: "{app}\novelwriter\*"
[UninstallDelete]
Type: filesandordirs; Name: "{app}\novelwriter\*"
[Files]
Source: "{#nwAppDir}\novelWriter\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
-1
View File
@@ -50,7 +50,6 @@ def resetConfigVars():
"""Reset the CONFIG object and set various values for testing to
prevent interfering with local OS.
"""
CONFIG.setLastPath(_TMP_ROOT)
CONFIG.setBackupPath(_TMP_ROOT)
CONFIG.setGuiFont(None)
CONFIG.setTextFont(None)
+1 -2
View File
@@ -1,5 +1,5 @@
[Meta]
timestamp = 2024-05-20 16:48:20
timestamp = 2024-06-16 00:36:27
[Main]
font =
@@ -10,7 +10,6 @@ hidevscroll = False
hidehscroll = False
lastnotes = 0x0
nativefont = True
lastpath =
[Sizes]
mainwindow = 1200, 650
+63 -7
View File
@@ -20,6 +20,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
@@ -28,7 +29,7 @@ from shutil import copyfile
import pytest
from novelwriter import CONFIG
from novelwriter.config import Config, RecentProjects
from novelwriter.config import Config, RecentPaths, RecentProjects
from novelwriter.constants import nwFiles
from tests.mocked import MockApp, causeOSError
@@ -213,21 +214,21 @@ def testBaseConfig_Methods(fncPath):
assert tstConf.assetPath("stuff") == appPath / "assets" / "stuff"
# Last Path
assert tstConf.lastPath() == Path.home().absolute()
assert tstConf.lastPath("project") == Path.home().absolute()
tmpStuff = fncPath / "stuff"
tmpStuff.mkdir()
tstConf.setLastPath(tmpStuff)
assert tstConf.lastPath() == tmpStuff
tstConf.setLastPath("project", tmpStuff)
assert tstConf.lastPath("project") == tmpStuff
fileStuff = tmpStuff / "more_stuff.txt"
fileStuff.write_text("Stuff")
tstConf.setLastPath(fileStuff)
assert tstConf.lastPath() == tmpStuff
tstConf.setLastPath("project", fileStuff)
assert tstConf.lastPath("project") == tmpStuff
fileStuff.unlink()
tmpStuff.rmdir()
assert tstConf.lastPath() == Path.home().absolute()
assert tstConf.lastPath("project") == Path.home().absolute()
# Backup Path
assert tstConf.backupPath() == tstConf._backPath
@@ -440,3 +441,58 @@ def testBaseConfig_RecentCache(monkeypatch, tstPaths):
assert recent.listEntries() == [
(str(pathOne), "Proj One", 100, 1600002000),
]
@pytest.mark.base
def testBaseConfig_RecentPaths(monkeypatch, tstPaths):
"""Test recent paths file."""
cacheFile = tstPaths.cnfDir / nwFiles.RECENT_PATH
recent = RecentPaths(CONFIG)
# Load when there is no file should pass, but load nothing
assert not cacheFile.exists()
assert recent.loadCache() is True
assert recent._data == {}
# Set valid paths
recent.setPath("default", tstPaths.cnfDir / "default")
recent.setPath("project", tstPaths.cnfDir / "project")
recent.setPath("import", tstPaths.cnfDir / "import")
recent.setPath("outline", tstPaths.cnfDir / "outline")
recent.setPath("stats", tstPaths.cnfDir / "stats")
# Set invalid path
recent.setPath("foobar", tstPaths.cnfDir / "foobar")
# Check valid paths
assert recent.getPath("default") == str(tstPaths.cnfDir / "default")
assert recent.getPath("project") == str(tstPaths.cnfDir / "project")
assert recent.getPath("import") == str(tstPaths.cnfDir / "import")
assert recent.getPath("outline") == str(tstPaths.cnfDir / "outline")
assert recent.getPath("stats") == str(tstPaths.cnfDir / "stats")
# Check invalid path
assert recent.getPath("foobar") is None
# Check file
expected = {
"default": str(tstPaths.cnfDir / "default"),
"project": str(tstPaths.cnfDir / "project"),
"import": str(tstPaths.cnfDir / "import"),
"outline": str(tstPaths.cnfDir / "outline"),
"stats": str(tstPaths.cnfDir / "stats"),
}
assert cacheFile.exists()
assert json.loads(cacheFile.read_text()) == expected
# Clear and reload
recent._data = {}
recent.loadCache()
assert recent._data == expected
# Check error handling
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
assert recent.saveCache() is False
assert recent.loadCache() is False
+11 -9
View File
@@ -62,15 +62,17 @@ def testBaseInit_Launch(caplog, monkeypatch, fncPath):
CONFIG.osWindows = osWindows
# Normal Launch
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.__init__", lambda *a: None)
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.setApplicationName", lambda *a: None)
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.setApplicationVersion", lambda *a: None)
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.setWindowIcon", lambda *a: None)
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.setOrganizationDomain", lambda *a: None)
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.exec", lambda *a: 0)
with pytest.raises(SystemExit) as ex:
main([f"--config={fncPath}", f"--data={fncPath}"])
assert ex.value.code == 0
with monkeypatch.context() as mp:
mp.setattr("PyQt5.QtWidgets.QApplication.__init__", lambda *a: None)
mp.setattr("PyQt5.QtWidgets.QApplication.setApplicationName", lambda *a: None)
mp.setattr("PyQt5.QtWidgets.QApplication.setApplicationVersion", lambda *a: None)
mp.setattr("PyQt5.QtWidgets.QApplication.setWindowIcon", lambda *a: None)
mp.setattr("PyQt5.QtWidgets.QApplication.setOrganizationDomain", lambda *a: None)
mp.setattr("PyQt5.QtWidgets.QApplication.exec", lambda *a: 0)
# mp.setattr("PyQt5.QtWidgets.QApplication.focusChange.connect", lambda *a: None)
with pytest.raises(SystemExit) as ex:
main([f"--config={fncPath}", f"--data={fncPath}"])
assert ex.value.code == 0
@pytest.mark.base
+14 -14
View File
@@ -73,29 +73,29 @@ def testCoreBuildSettings_ClassAttributes(fncPath: Path):
assert isUUID(build.buildID)
# Last path must be valid, if not it defaults to $HOME
build.setLastPath("/path/to/nowhere")
assert build.lastPath == CONFIG.homePath()
build.setLastBuildPath("/path/to/nowhere")
assert build.lastBuildPath == CONFIG.homePath()
build.setLastPath(None)
assert build.lastPath == CONFIG.homePath()
build.setLastBuildPath(None)
assert build.lastBuildPath == CONFIG.homePath()
(fncPath / "test.txt").write_text("foobar")
build.setLastPath(fncPath / "test.txt") # Can't be a file
assert build.lastPath == CONFIG.homePath()
build.setLastBuildPath(fncPath / "test.txt") # Can't be a file
assert build.lastBuildPath == CONFIG.homePath()
build.setLastPath(fncPath)
assert build.lastPath == fncPath
build.setLastBuildPath(fncPath)
assert build.lastBuildPath == fncPath
build.setLastPath(str(fncPath)) # String paths are also ok
assert build.lastPath == fncPath
build.setLastBuildPath(str(fncPath)) # String paths are also ok
assert build.lastBuildPath == fncPath
# Last path no longer exists -> fallback to $HOME
testDir = fncPath / "test_dir"
testDir.mkdir()
build.setLastPath(testDir)
assert build.lastPath == testDir
build.setLastBuildPath(testDir)
assert build.lastBuildPath == testDir
testDir.rmdir()
assert build.lastPath == CONFIG.homePath()
assert build.lastBuildPath == CONFIG.homePath()
# Last build name
build.setLastBuildName(None) # type: ignore
@@ -119,7 +119,7 @@ def testCoreBuildSettings_ClassAttributes(fncPath: Path):
# Set some sensible values
build.setName("Test Build")
build.setBuildID("5cf45d24-f496-42c9-8733-529a9e52a62b")
build.setLastPath(fncPath)
build.setLastBuildPath(fncPath)
build.setLastBuildName("Build Name")
build.setLastFormat(nwBuildFmt.HTML)
+3 -3
View File
@@ -449,8 +449,8 @@ def testCoreTools_DocSearch(monkeypatch, mockGUI, fncPath, mockRnd, ipsumText):
assert pruneResult(search.iterSearch(project, "Lor"), 2) == []
search.setWholeWords(False)
assert pruneResult(search.iterSearch(project, "Lor"), 2) == [
(15, 3, "Lorem"), (29, 3, "lor"), (754, 3, "lorem"), (2056, 3, "lorem,"),
(2209, 3, "lorem"), (2425, 3, "lorem"), (2840, 3, "lorem."), (3328, 3, "lor."),
(15, 3, "Lorem"), (29, 3, "dolor"), (754, 3, "lorem"), (2056, 3, "lorem,"),
(2209, 3, "lorem"), (2425, 3, "lorem"), (2840, 3, "lorem."), (3328, 3, "dolor."),
(3399, 3, "lorem"),
]
@@ -458,7 +458,7 @@ def testCoreTools_DocSearch(monkeypatch, mockGUI, fncPath, mockRnd, ipsumText):
search.setWholeWords(False)
search.setUserRegEx(True)
assert pruneResult(search.iterSearch(project, r"Lor\b"), 2) == [
(29, 3, "lor"), (3328, 3, "lor."),
(29, 3, "dolor"), (3328, 3, "dolor."),
]
# Max Results
+38 -2
View File
@@ -275,12 +275,12 @@ def testCoreToHtml_ConvertParagraphs(mockGUI):
CONFIG.altDialogOpen = "::"
CONFIG.altDialogClose = "::"
html.setDialogueHighlight(True)
html._text = "## Chapter\n\nThis text :: has alt dialogue :: in it.\n\n"
html._text = "## Chapter\n\nThis text ::has alt dialogue:: in it.\n\n"
html.tokenizeText()
html.doConvert()
assert html.result == (
"<h1 style='page-break-before: always;'>Chapter</h1>\n"
"<p>This text <span class='altdialog'>:: has alt dialogue ::</span> in it.</p>\n"
"<p>This text <span class='altdialog'>::has alt dialogue::</span> in it.</p>\n"
)
# Footnotes
@@ -308,6 +308,42 @@ def testCoreToHtml_ConvertParagraphs(mockGUI):
)
@pytest.mark.core
def testCoreToHtml_CloseTags(mockGUI):
"""Test automatic closing of HTML tags for shortcodes."""
project = NWProject()
html = ToHtml(project)
html._isNovel = True
html._isFirst = True
# Unclosed Shortcodes
html._text = "Text [b][i][s][u][m][sup][sub]text text text.\n"
html.tokenizeText()
html.doConvert()
assert html.result == (
"<p>Text <strong><em><del><span style='text-decoration: underline;'><mark><sup><sub>"
"text text text.</strong></em></del></span></mark></sup></sub></p>\n"
)
# Double Shortcodes
html._text = "Text [b][i][s][u][m][sup][sub]text [b][i][s][u][m][sup][sub]text text.\n"
html.tokenizeText()
html.doConvert()
assert html.result == (
"<p>Text <strong><em><del><span style='text-decoration: underline;'><mark><sup><sub>"
"text text text.</strong></em></del></span></mark></sup></sub></p>\n"
)
# Redundant Close Shortcodes
html._text = "Text text [/b][/i][/s][/u][/m][/sup][/sub]text text.\n"
html.tokenizeText()
html.doConvert()
assert html.result == (
"<p>Text text text text.</p>\n"
)
@pytest.mark.core
def testCoreToHtml_ConvertDirect(mockGUI):
"""Test the converter directly using the ToHtml class."""
+83
View File
@@ -1324,6 +1324,89 @@ def testCoreToken_SpecialFormat(mockGUI):
]
@pytest.mark.core
def testCoreToken_TextIndent(mockGUI):
"""Test the handling of text indent in the Tokenizer class."""
project = NWProject()
tokens = BareTokenizer(project)
# No First Indent
tokens.setFirstLineIndent(True, 1.0, False)
assert tokens._noIndent is False
assert tokens._firstIndent is True
assert tokens._firstWidth == 1.0
assert tokens._indentFirst is False
# Page One
# Two paragraphs in the same scene
tokens._text = (
"# Title One\n\n"
"### Scene One\n\n"
"First paragraph.\n\n"
"Second paragraph.\n\n"
)
tokens.tokenizeText()
assert tokens._tokens == [
(Tokenizer.T_HEAD1, 1, "Title One", [], Tokenizer.A_NONE),
(Tokenizer.T_HEAD3, 2, "Scene One", [], Tokenizer.A_NONE),
(Tokenizer.T_TEXT, 2, "First paragraph.", [], Tokenizer.A_NONE),
(Tokenizer.T_TEXT, 2, "Second paragraph.", [], Tokenizer.A_IND_T),
]
assert tokens._noIndent is False
# Page Two
# New scene with only a synopsis
tokens._text = (
"### Scene Two\n\n"
"%Synopsis: Stuff happens.\n\n"
)
tokens.tokenizeText()
assert tokens._tokens == [
(Tokenizer.T_HEAD3, 1, "Scene Two", [], Tokenizer.A_NONE),
(Tokenizer.T_SYNOPSIS, 1, "Stuff happens.", [], Tokenizer.A_NONE),
]
assert tokens._noIndent is True
# Page Three
# Two paragraphs for the scene on the previous page
tokens._text = (
"First paragraph.\n\n"
"Second paragraph.\n\n"
)
tokens.tokenizeText()
assert tokens._tokens == [
(Tokenizer.T_TEXT, 0, "First paragraph.", [], Tokenizer.A_NONE),
(Tokenizer.T_TEXT, 0, "Second paragraph.", [], Tokenizer.A_IND_T),
]
assert tokens._noIndent is False
# First Indent
tokens.setFirstLineIndent(True, 1.0, True)
assert tokens._noIndent is False
assert tokens._firstIndent is True
assert tokens._firstWidth == 1.0
assert tokens._indentFirst is True
# Page Four
# Two paragraphs in the same scene
tokens._text = (
"# Title One\n\n"
"### Scene One\n\n"
"First paragraph.\n\n"
"Second paragraph.\n\n"
)
tokens.tokenizeText()
assert tokens._tokens == [
(Tokenizer.T_HEAD1, 1, "Title One", [], Tokenizer.A_NONE),
(Tokenizer.T_HEAD3, 2, "Scene One", [], Tokenizer.A_NONE),
(Tokenizer.T_TEXT, 2, "First paragraph.", [], Tokenizer.A_IND_T),
(Tokenizer.T_TEXT, 2, "Second paragraph.", [], Tokenizer.A_IND_T),
]
assert tokens._noIndent is False
@pytest.mark.core
def testCoreToken_ProcessHeaders(mockGUI):
"""Test the header and page parser of the Tokenizer class."""
+7 -11
View File
@@ -30,7 +30,7 @@ from novelwriter import CONFIG, SHARED
from novelwriter.constants import nwUnicode
from novelwriter.dialogs.preferences import GuiPreferences
from novelwriter.dialogs.quotes import GuiQuoteSelect
from novelwriter.types import QtDialogApply, QtDialogClose, QtDialogSave, QtModNone
from novelwriter.types import QtDialogCancel, QtDialogSave, QtModNone
KEY_DELAY = 1
@@ -92,7 +92,8 @@ def testDlgPreferences_Actions(qtbot, monkeypatch, nwGUI):
"""Test the preferences dialog actions."""
monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")])
prefs = GuiPreferences(nwGUI)
prefs.show()
with qtbot.waitExposed(prefs):
prefs.show()
# Check Navigation
vBar = prefs.mainForm.verticalScrollBar()
@@ -116,12 +117,6 @@ def testDlgPreferences_Actions(qtbot, monkeypatch, nwGUI):
prefs._gotoSearch()
assert value.args[0] < old
# Check Apply Button
prefs.show()
with qtbot.waitSignal(prefs.newPreferencesReady) as signal:
prefs.buttonBox.button(QtDialogApply).click()
assert signal.args == [False, False, False, False]
# Check Save Button
prefs.show()
with qtbot.waitSignal(prefs.newPreferencesReady) as signal:
@@ -130,7 +125,7 @@ def testDlgPreferences_Actions(qtbot, monkeypatch, nwGUI):
# Check Close Button
prefs.show()
prefs.buttonBox.button(QtDialogClose).click()
prefs.buttonBox.button(QtDialogCancel).click()
assert prefs.isHidden() is True
# Close Using Escape Key
@@ -152,7 +147,8 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, tstPaths):
monkeypatch.setattr(CONFIG, "listLanguages", lambda *a: languages)
prefs = GuiPreferences(nwGUI)
prefs.show()
with qtbot.waitExposed(prefs):
prefs.show()
# Appearance
prefs.guiLocale.setCurrentIndex(prefs.guiLocale.findData("en_US"))
@@ -315,7 +311,7 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, tstPaths):
with monkeypatch.context() as mp:
mp.setattr(QFontDatabase, "families", lambda *a: ["TestFont"])
with qtbot.waitSignal(prefs.newPreferencesReady) as signal:
prefs.buttonBox.button(QtDialogApply).click()
prefs.buttonBox.button(QtDialogSave).click()
assert signal.args == [True, True, True, True]
# Check Settings
+78
View File
@@ -0,0 +1,78 @@
"""
novelWriter Progress Bar Tester
=================================
This file is a part of novelWriter
Copyright 20182024, 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 <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
from time import sleep
import pytest
from PyQt5.QtGui import QColor
from novelwriter.extensions.progressbars import NProgressCircle, NProgressSimple
from tests.tools import SimpleDialog
@pytest.mark.gui
def testExtProgressBars_NProgressCircle(qtbot):
"""Test the NProgressCircle class."""
dialog = SimpleDialog()
progress = NProgressCircle(dialog, 200, 16)
with qtbot.waitExposed(dialog):
# This ensures the paint event is executed
dialog.show()
dialog.resize(200, 200)
progress.setColours(
QColor(255, 255, 255), QColor(255, 192, 192),
QColor(255, 0, 0), QColor(0, 0, 0),
)
progress.setMaximum(100)
for i in range(1, 101):
progress.setValue(i)
sleep(0.0025)
assert progress.value() == i
progress.setCentreText("Done!")
assert progress._text == "Done!"
# qtbot.stop()
@pytest.mark.gui
def testExtProgressBars_NProgressSimple(qtbot):
"""Test the NProgressSimple class."""
dialog = SimpleDialog()
progress = NProgressSimple(dialog)
with qtbot.waitExposed(dialog):
# This ensures the paint event is executed
dialog.show()
progress.setMaximum(100)
for i in range(1, 101):
progress.setValue(i)
sleep(0.0025)
assert progress.value() == i
# qtbot.stop()
+74
View File
@@ -0,0 +1,74 @@
"""
novelWriter Switch Tester
===========================
This file is a part of novelWriter
Copyright 20182024, 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 <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import pytest
from PyQt5.QtCore import QEvent, QPoint
from PyQt5.QtGui import QMouseEvent
from novelwriter.extensions.switch import NSwitch
from novelwriter.types import QtModNone, QtMouseLeft
from tests.tools import SimpleDialog
@pytest.mark.gui
def testExtSwitch_Main(qtbot):
"""Test the NSwitch class. This is mostly a check that all the calls
work as the result is visual.
"""
dialog = SimpleDialog()
switch = NSwitch(dialog, 40)
with qtbot.waitExposed(dialog):
# This ensures the paint event is executed
dialog.show()
dialog.resize(200, 100)
switch.setEnabled(False)
switch.setChecked(False)
switch.repaint()
qtbot.wait(20)
switch.setChecked(True)
switch.repaint()
qtbot.wait(20)
switch.setEnabled(True)
switch.setChecked(False)
switch.repaint()
qtbot.wait(20)
switch.setChecked(True)
switch.repaint()
qtbot.wait(20)
button = QtMouseLeft
modifier = QtModNone
event = QMouseEvent(QEvent.Type.MouseButtonRelease, QPoint(), button, button, modifier)
switch.mouseReleaseEvent(event)
event = QEvent(QEvent.Type.Enter)
switch.enterEvent(event)
# qtbot.stop()
+2 -5
View File
@@ -29,9 +29,7 @@ from PyQt5.QtWidgets import QAction, QApplication, QMenu
from novelwriter import CONFIG, SHARED
from novelwriter.constants import nwKeyWords, nwUnicode
from novelwriter.dialogs.editlabel import GuiEditLabel
from novelwriter.enum import (
nwDocAction, nwDocInsert, nwItemClass, nwItemLayout, nwTrinary, nwWidget
)
from novelwriter.enum import nwDocAction, nwDocInsert, nwItemClass, nwItemLayout, nwTrinary
from novelwriter.gui.doceditor import GuiDocEditor
from novelwriter.text.counting import standardCounter
from novelwriter.types import (
@@ -1597,7 +1595,6 @@ def testGuiEditor_Tags(qtbot, nwGUI, projPath, ipsumText, mockRnd):
docEditor.replaceText(text)
nwGUI.saveDocument()
assert nwGUI.projView.projTree.revealNewTreeItem(cHandle)
docEditor.updateTagHighLighting()
# Follow Tag
# ==========
@@ -1679,7 +1676,7 @@ def testGuiEditor_Completer(qtbot, nwGUI, projPath, mockRnd):
completer = docEditor._completer
# Create Scene
nwGUI.switchFocus(nwWidget.EDITOR)
nwGUI.docEditor.setFocus()
for c in "### Scene One":
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
+141 -12
View File
@@ -32,7 +32,7 @@ from PyQt5.QtWidgets import QInputDialog, QMenu
from novelwriter import CONFIG, SHARED
from novelwriter.dialogs.editlabel import GuiEditLabel
from novelwriter.enum import nwItemType, nwView, nwWidget
from novelwriter.enum import nwDocAction, nwFocus, nwItemType, nwView
from novelwriter.gui.doceditor import GuiDocEditor
from novelwriter.gui.noveltree import GuiNovelView
from novelwriter.gui.outline import GuiOutlineView
@@ -64,7 +64,21 @@ def testGuiMain_Launch(qtbot, monkeypatch, nwGUI, projPath):
# Open Lipsum project
nwGUI.postLaunchTasks(projPath)
assert SHARED.hasProject is True
nwGUI.closeProject()
assert SHARED.hasProject is False
# Open as if called from Welcome
nwGUI._openProjectFromWelcome(projPath)
assert SHARED.hasProject is True
nwGUI.closeProject()
assert SHARED.hasProject is False
# Open as if called from Welcome, invalid path
with monkeypatch.context() as mp:
mp.setattr(nwGUI, "showWelcomeDialog", lambda *a: None)
nwGUI._openProjectFromWelcome(None)
assert SHARED.hasProject is False
# Project open fails
with monkeypatch.context() as mp:
@@ -113,7 +127,7 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
# Project Tree has focus
nwGUI._changeView(nwView.PROJECT)
nwGUI.switchFocus(nwWidget.TREE)
nwGUI._switchFocus(nwFocus.TREE)
nwGUI.projStack.setCurrentIndex(0)
with monkeypatch.context() as mp:
mp.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
@@ -137,7 +151,7 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
# Project Outline has focus
nwGUI._changeView(nwView.OUTLINE)
nwGUI.switchFocus(nwWidget.OUTLINE)
nwGUI._switchFocus(nwFocus.OUTLINE)
with monkeypatch.context() as mp:
mp.setattr(GuiOutlineView, "treeHasFocus", lambda *a: True)
assert nwGUI.docEditor.docHandle is None
@@ -230,7 +244,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
CONFIG.autoScroll = True
# Add a Character File
nwGUI.switchFocus(nwWidget.TREE)
nwGUI._switchFocus(nwFocus.TREE)
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem(C.hCharRoot).setSelected(True)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
@@ -250,7 +264,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
docEditor._qDocument.syntaxHighlighter.initHighlighter()
# Type something into the document
nwGUI.switchFocus(nwWidget.EDITOR)
nwGUI.docEditor.setFocus()
qtbot.keyClick(docEditor, "a", modifier=Qt.ControlModifier, delay=KEY_DELAY)
for c in "# Jane Doe":
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
@@ -265,14 +279,14 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
# Add a Plot File
nwGUI.switchFocus(nwWidget.TREE)
nwGUI._switchFocus(nwFocus.TREE)
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem(C.hPlotRoot).setSelected(True)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
nwGUI.openSelectedItem()
# Type something into the document
nwGUI.switchFocus(nwWidget.EDITOR)
nwGUI.docEditor.setFocus()
qtbot.keyClick(docEditor, "a", modifier=Qt.ControlModifier, delay=KEY_DELAY)
for c in "# Main Plot":
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
@@ -287,7 +301,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
qtbot.keyClick(docEditor, Qt.Key_Return, delay=KEY_DELAY)
# Add a World File
nwGUI.switchFocus(nwWidget.TREE)
nwGUI._switchFocus(nwFocus.TREE)
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem(C.hWorldRoot).setSelected(True)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None, isNote=True)
@@ -299,7 +313,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
docEditor.replaceText("")
# Type something into the document
nwGUI.switchFocus(nwWidget.EDITOR)
nwGUI.docEditor.setFocus()
qtbot.keyClick(docEditor, "a", modifier=Qt.ControlModifier, delay=KEY_DELAY)
for c in "# Main Location":
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
@@ -318,7 +332,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
nwGUI._autoSaveProject()
# Select the 'New Scene' file
nwGUI.switchFocus(nwWidget.TREE)
nwGUI._switchFocus(nwFocus.TREE)
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem(C.hNovelRoot).setExpanded(True)
nwGUI.projView.projTree._getTreeItem(C.hChapterDir).setExpanded(True)
@@ -326,7 +340,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
nwGUI.openSelectedItem()
# Type something into the document
nwGUI.switchFocus(nwWidget.EDITOR)
nwGUI.docEditor.setFocus()
qtbot.keyClick(docEditor, "a", modifier=Qt.ControlModifier, delay=KEY_DELAY)
for c in "# Novel":
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
@@ -535,7 +549,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
nwGUI.rebuildIndex()
# Open and view the edited document
nwGUI.switchFocus(nwWidget.VIEWER)
nwGUI.docViewer.setFocus()
assert nwGUI.openDocument(C.hSceneDoc)
assert nwGUI.viewDocument(C.hSceneDoc)
assert nwGUI.saveProject()
@@ -689,3 +703,118 @@ def testGuiMain_Features(qtbot, nwGUI, projPath, mockRnd):
nwGUI.sideBar.mSettings.hide()
# qtbot.stop()
@pytest.mark.gui
def testGuiMain_FocusView(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
"""Test switching focus and view of the main window."""
buildTestProject(nwGUI, projPath)
nwGUI.openDocument(C.hSceneDoc)
nwGUI.viewDocument(C.hSceneDoc)
# Toggle Focus
# ============
nwGUI.docEditor.setFocus()
assert nwGUI.docEditor.anyFocus()
# Simulate focus change to viewer
nwGUI._appFocusChanged(None, nwGUI.docViewer)
assert nwGUI.docEditor.docHeader.itemTitle._state is False
assert nwGUI.docViewer.docHeader.itemTitle._state is True
# Simulate focus change to editor
nwGUI._appFocusChanged(None, nwGUI.docEditor)
assert nwGUI.docEditor.docHeader.itemTitle._state is True
assert nwGUI.docViewer.docHeader.itemTitle._state is False
# Focus Tree
# ==========
assert nwGUI.projStack.currentWidget() == nwGUI.projView
# Switch from editor to project tree
nwGUI.docEditor.setFocus()
nwGUI._switchFocus(nwFocus.TREE)
assert nwGUI.projStack.currentWidget() == nwGUI.projView
# Triggering again should switch to novel view
nwGUI._switchFocus(nwFocus.TREE)
assert nwGUI.projStack.currentWidget() == nwGUI.novelView
# Switch from editor to novel view
nwGUI.docEditor.setFocus()
nwGUI._switchFocus(nwFocus.TREE)
assert nwGUI.projStack.currentWidget() == nwGUI.novelView
# Triggering again should switch back to project tree
nwGUI._switchFocus(nwFocus.TREE)
assert nwGUI.projStack.currentWidget() == nwGUI.projView
# If in search mode, should default to project tree
nwGUI._changeView(nwView.SEARCH)
nwGUI._switchFocus(nwFocus.TREE)
assert nwGUI.projStack.currentWidget() == nwGUI.projView
# Focus Document
# ==============
nwGUI._switchFocus(nwFocus.TREE)
def mockEmitEditorFocus(*a):
nwGUI._appFocusChanged(None, nwGUI.docEditor)
def mockEmitViewerFocus(*a):
nwGUI._appFocusChanged(None, nwGUI.docViewer)
# Switch to viewer
with monkeypatch.context() as mp:
mp.setattr(nwGUI.docEditor, "hasFocus", lambda *a: True)
mp.setattr(nwGUI.docViewer, "hasFocus", lambda *a: False)
mp.setattr(nwGUI.docEditor, "setFocus", mockEmitEditorFocus)
mp.setattr(nwGUI.docViewer, "setFocus", mockEmitViewerFocus)
nwGUI._switchFocus(nwFocus.DOCUMENT)
assert nwGUI.docEditor.docHeader.itemTitle._state is False
assert nwGUI.docViewer.docHeader.itemTitle._state is True
# Call again to switch to editor
with monkeypatch.context() as mp:
mp.setattr(nwGUI.docEditor, "hasFocus", lambda *a: False)
mp.setattr(nwGUI.docViewer, "hasFocus", lambda *a: True)
mp.setattr(nwGUI.docEditor, "setFocus", mockEmitEditorFocus)
mp.setattr(nwGUI.docViewer, "setFocus", mockEmitViewerFocus)
nwGUI._switchFocus(nwFocus.DOCUMENT)
assert nwGUI.docEditor.docHeader.itemTitle._state is True
assert nwGUI.docViewer.docHeader.itemTitle._state is False
# Default to editor
with monkeypatch.context() as mp:
mp.setattr(nwGUI.docEditor, "hasFocus", lambda *a: False)
mp.setattr(nwGUI.docViewer, "hasFocus", lambda *a: False)
mp.setattr(nwGUI.docEditor, "setFocus", mockEmitEditorFocus)
mp.setattr(nwGUI.docViewer, "setFocus", mockEmitViewerFocus)
nwGUI._switchFocus(nwFocus.DOCUMENT)
assert nwGUI.docEditor.docHeader.itemTitle._state is True
assert nwGUI.docViewer.docHeader.itemTitle._state is False
# Focus Outline
# =============
nwGUI._switchFocus(nwFocus.OUTLINE)
assert nwGUI.mainStack.currentWidget() == nwGUI.outlineView
# Pass Actions
# ============
# Pass to editor
with monkeypatch.context() as mp:
mp.setattr(nwGUI.docEditor, "hasFocus", lambda *a: True)
mp.setattr(nwGUI.docViewer, "hasFocus", lambda *a: False)
nwGUI._passDocumentAction(nwDocAction.SEL_ALL)
assert nwGUI.docEditor.textCursor().hasSelection() is True
# Pass to viewer
with monkeypatch.context() as mp:
mp.setattr(nwGUI.docEditor, "hasFocus", lambda *a: False)
mp.setattr(nwGUI.docViewer, "hasFocus", lambda *a: True)
nwGUI._passDocumentAction(nwDocAction.SEL_ALL)
assert nwGUI.docViewer.textCursor().hasSelection() is True
# qtbot.stop()
+2
View File
@@ -64,6 +64,8 @@ def testGuiI18n_Localisation(qtbot, monkeypatch, language, nwGUI, projPath):
qtbot.waitUntil(lambda: SHARED.findTopLevelWidget(dType) is not None, timeout=1000)
dialog = SHARED.findTopLevelWidget(dType)
assert isinstance(dialog, dType)
assert dialog is not None
dialog.deleteLater()
showDialog(nwGUI.showWelcomeDialog, GuiWelcome)
showDialog(nwGUI.showPreferencesDialog, GuiPreferences)
+2 -2
View File
@@ -30,7 +30,7 @@ from PyQt5.QtWidgets import QInputDialog, QToolTip
from novelwriter import CONFIG, SHARED
from novelwriter.dialogs.editlabel import GuiEditLabel
from novelwriter.enum import nwItemType, nwWidget
from novelwriter.enum import nwFocus, nwItemType
from novelwriter.gui.noveltree import GuiNovelTree, NovelTreeColumn
from novelwriter.types import QtMouseLeft
@@ -44,7 +44,7 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
buildTestProject(nwGUI, projPath)
nwGUI.switchFocus(nwWidget.TREE)
nwGUI._switchFocus(nwFocus.TREE)
nwGUI.projView.projTree.clearSelection()
nwGUI.projView.projTree._getTreeItem(C.hCharRoot).setSelected(True)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE)
+3 -3
View File
@@ -34,7 +34,7 @@ from novelwriter.core.project import NWProject
from novelwriter.dialogs.docmerge import GuiDocMerge
from novelwriter.dialogs.docsplit import GuiDocSplit
from novelwriter.dialogs.editlabel import GuiEditLabel
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwWidget
from novelwriter.enum import nwFocus, nwItemClass, nwItemLayout, nwItemType
from novelwriter.gui.projtree import GuiProjectTree, GuiProjectView, _TreeContextMenu
from novelwriter.guimain import GuiMain
from novelwriter.types import QtAccepted, QtModNone, QtMouseLeft, QtMouseMiddle, QtRejected
@@ -1111,7 +1111,7 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
# Create a project
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)
nwGUI.switchFocus(nwWidget.TREE)
nwGUI._switchFocus(nwFocus.TREE)
# Handles for new objects
hCharNote = "0000000000011"
@@ -1408,7 +1408,7 @@ def testGuiProjTree_Templates(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
# Create a project
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)
nwGUI.switchFocus(nwWidget.TREE)
nwGUI._switchFocus(nwFocus.TREE)
nwGUI.show()
project = SHARED.project
+2 -2
View File
@@ -47,7 +47,7 @@ def testToolManuscriptBuild_Main(
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)
build = BuildSettings()
build.setLastPath(fncPath)
build.setLastBuildPath(fncPath)
manus = GuiManuscriptBuild(nwGUI, build)
manus.show()
@@ -100,7 +100,7 @@ def testToolManuscriptBuild_Main(
assert build.lastBuildName == "TestBuild"
assert build.lastFormat == lastFmt
assert build.lastPath == fncPath
assert build.lastBuildPath == fncPath
# Error Handling
# ==============
+9 -5
View File
@@ -204,17 +204,21 @@ def buildTestProject(obj: object, projPath: Path) -> None:
class SimpleDialog(QDialog):
def __init__(self, widget: QWidget) -> None:
def __init__(self, widget: QWidget | None = None) -> None:
super().__init__()
self._widget = widget
layout = QVBoxLayout()
layout.addWidget(widget)
layout.setContentsMargins(40, 40, 40, 40)
self.setLayout(layout)
if widget:
layout.addWidget(widget)
return
@property
def widget(self) -> QWidget:
def widget(self) -> QWidget | None:
return self._widget
def addWidget(self, widget: QWidget) -> None:
self._widget = widget
self.layout().addWidget(widget)
return