Merge branch 'main' into update_docs
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+1123
-939
File diff suppressed because it is too large
Load Diff
+1116
-932
File diff suppressed because it is too large
Load Diff
+1116
-932
File diff suppressed because it is too large
Load Diff
+1116
-932
File diff suppressed because it is too large
Load Diff
+1116
-932
File diff suppressed because it is too large
Load Diff
+1116
-932
File diff suppressed because it is too large
Load Diff
+1116
-932
File diff suppressed because it is too large
Load Diff
+1116
-932
File diff suppressed because it is too large
Load Diff
+4988
File diff suppressed because it is too large
Load Diff
+1116
-932
File diff suppressed because it is too large
Load Diff
+1116
-932
File diff suppressed because it is too large
Load Diff
@@ -47,9 +47,9 @@ __license__ = "GPLv3"
|
||||
__author__ = "Veronica Berglyd Olsen"
|
||||
__maintainer__ = "Veronica Berglyd Olsen"
|
||||
__email__ = "code@vkbo.net"
|
||||
__version__ = "2.5b1"
|
||||
__hexversion__ = "0x020500b1"
|
||||
__date__ = "2024-05-26"
|
||||
__version__ = "2.5rc1"
|
||||
__hexversion__ = "0x020500c1"
|
||||
__date__ = "2024-06-22"
|
||||
__status__ = "Stable"
|
||||
__domain__ = "novelwriter.io"
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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>
|
||||
• <b>Concept:</b> Marian Lückhof (<a href="https://github.com/Number042">@Number042</a>)<br>
|
||||
• <b>Internationalisation:</b> Bruno Meneguello (<a href="https://github.com/bkmeneguello">@bkmeneguello</a>)<br>
|
||||
• <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>
|
||||
• <b>Dutch:</b> Martijn van der Kleijn (<a href="https://github.com/mvdkleijn">@mvdkleijn</a>)<br>
|
||||
• <b>French:</b> Jan Lüdke (<a href="https://github.com/jyhelle">@jyhelle</a>)<br>
|
||||
• <b>German:</b> Myian (<a href="https://github.com/heymyian">@heymyian</a>)<br>
|
||||
• <b>Italian:</b> Riccardo Mangili<br>
|
||||
• <b>Japanese:</b> hebekeg (<a href="https://github.com/hebekeg">@hebekeg</a>)<br>
|
||||
• <b>Latin American Spanish:</b> Tommy Marplatt (<a href="https://github.com/tmarplatt">@tmarplatt</a>)<br>
|
||||
• <b>Norwegian:</b> Veronica Berglyd Olsen (<a href="https://github.com/vkbo">@vkbo</a>)<br>
|
||||
• <b>Portuguese:</b> Bruno Meneguello (<a href="https://github.com/bkmeneguello">@bkmeneguello</a>)<br>
|
||||
• <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>
|
||||
• <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>
|
||||
• <a href="https://www.qt.io">Qt5</a> by Qt Company<br>
|
||||
• <a href="https://www.riverbankcomputing.com/software/pyqt">PyQt5</a> by Riverbank Computing<br>
|
||||
• <a href="https://abiword.github.io/enchant">Enchant</a> by Dom Lachowicz<br>
|
||||
• <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>
|
||||
• <a href="https://github.com/stephenhutchings/typicons.font">Typicons</a> icons by Stephen Hutchings (CC BY-SA 4.0)<br>
|
||||
• <a href="https://github.com/chriskempson/base16">Tomorrow</a> syntax themes by Chris Kempson (MIT License)<br>
|
||||
• <a href="https://github.com/sdras/night-owl-vscode-theme">Owl</a> syntax themes by Sarah Drasner (MIT License)<br>
|
||||
• <a href="https://github.com/altercation/solarized">Solarized</a> themes by Ethan Schoonover (MIT License)<br>
|
||||
• <a href="https://github.com/alemvigh">Cyberpunk Night</a> theme by Anders Lemvigh (CC BY-SA 4.0)<br>
|
||||
• <a href="https://draculatheme.com">Dracula</a> theme by Zeno Rocha (MIT License)<br>
|
||||
• <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>
|
||||
• Pridi by Cadson Demak (Open Font License, Version 1.1)<br>
|
||||
• 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
|
||||
|
||||
@@ -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
@@ -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 2018–2024, 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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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", ""))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
|
||||
# =================
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
+26
-3
@@ -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 2018–2024, 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
|
||||
@@ -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 2018–2024, Veronica Berglyd Olsen
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <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
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
##
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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 ""
|
||||
|
||||
@@ -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 "================================================================================"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
"""
|
||||
novelWriter – Progress Bar Tester
|
||||
=================================
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2024, Veronica Berglyd Olsen
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <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()
|
||||
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
novelWriter – Switch Tester
|
||||
===========================
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2024, Veronica Berglyd Olsen
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user