Compare commits
247 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4e87435dd | ||
|
|
9980d225f5 | ||
|
|
9c0f596c0d | ||
|
|
ff8cca6d8b | ||
|
|
0e5e263dea | ||
|
|
166732946e | ||
|
|
cccdab45dc | ||
|
|
190ca7cf2d | ||
|
|
009160b6bf | ||
|
|
851f67d86f | ||
|
|
b8be2277c4 | ||
|
|
14e266b938 | ||
|
|
94f070f096 | ||
|
|
0c28664ced | ||
|
|
754863e42c | ||
|
|
fb5e4a5f17 | ||
|
|
03e9fb93c7 | ||
|
|
3e77e24b1f | ||
|
|
d97809e4b0 | ||
|
|
551fd0aaf9 | ||
|
|
2fc36f8355 | ||
|
|
13cc2feb3f | ||
|
|
e2b62fc9f6 | ||
|
|
6ecf0ecd88 | ||
|
|
c940d9d8a4 | ||
|
|
efef172658 | ||
|
|
70425c77ea | ||
|
|
8520409459 | ||
|
|
67a81400ff | ||
|
|
2a0183c838 | ||
|
|
e222915a37 | ||
|
|
b698ccbc1d | ||
|
|
03a80beebe | ||
|
|
1c072e9f32 | ||
|
|
01576f941f | ||
|
|
4af80286d5 | ||
|
|
c9785bc57b | ||
|
|
71f9f3d230 | ||
|
|
500a01b46d | ||
|
|
dea2ac22e1 | ||
|
|
1cae46a936 | ||
|
|
ac22838ded | ||
|
|
93875d667c | ||
|
|
41043e4dc1 | ||
|
|
2e8425d4b1 | ||
|
|
fcac28c69f | ||
|
|
a14b319ada | ||
|
|
6763ebdd66 | ||
|
|
421f2dd688 | ||
|
|
19b21a3e8a | ||
|
|
f02bba5630 | ||
|
|
12fa295506 | ||
|
|
1054bbdf8b | ||
|
|
b47436ec59 | ||
|
|
856534cb81 | ||
|
|
3ee07c7da7 | ||
|
|
96fb892465 | ||
|
|
3a48a124e2 | ||
|
|
b391d47a40 | ||
|
|
a3c4d7c7b6 | ||
|
|
2ce1134b8b | ||
|
|
641ff441b5 | ||
|
|
7687259381 | ||
|
|
7484542af2 | ||
|
|
1e64a3f94c | ||
|
|
923e00ad19 | ||
|
|
518be4e630 | ||
|
|
72aa82c8ed | ||
|
|
b8a136e0ad | ||
|
|
38ba214327 | ||
|
|
c23ec06ce6 | ||
|
|
c495098582 | ||
|
|
499d095f81 | ||
|
|
2ed9367dc8 | ||
|
|
820e6fcfa3 | ||
|
|
f92ef05ee2 | ||
|
|
4b3795671d | ||
|
|
bff5ff79bd | ||
|
|
21056c5cb8 | ||
|
|
a4567939d6 | ||
|
|
90871d5859 | ||
|
|
cb299d145b | ||
|
|
6afc34e47e | ||
|
|
f6bcbbd962 | ||
|
|
55f1129a24 | ||
|
|
a2c48492ee | ||
|
|
cb0591fe5f | ||
|
|
2f269c3e17 | ||
|
|
d1a15f07d5 | ||
|
|
3cb0c95278 | ||
|
|
74910a6b56 | ||
|
|
5ab32326c1 | ||
|
|
7dc184f05f | ||
|
|
dbdb3931f4 | ||
|
|
8798afc41c | ||
|
|
e67c911c97 | ||
|
|
88d69f6fd4 | ||
|
|
99555f7dd0 | ||
|
|
b3ec77c631 | ||
|
|
d4c61e7ec9 | ||
|
|
48bbe288b7 | ||
|
|
a1f7ab1562 | ||
|
|
5f95c745e2 | ||
|
|
1f735d8739 | ||
|
|
551fa28b27 | ||
|
|
a4e55a0664 | ||
|
|
86e196808e | ||
|
|
0a4e15de31 | ||
|
|
cd1f52c6b5 | ||
|
|
b63449bffd | ||
|
|
93965138da | ||
|
|
fae1bd791f | ||
|
|
35855d2d71 | ||
|
|
c374534d9e | ||
|
|
f6c92dab24 | ||
|
|
4ee81501a9 | ||
|
|
3b92c3d75c | ||
|
|
567d1ae104 | ||
|
|
7b7f028d74 | ||
|
|
0e53524f2e | ||
|
|
0f0e0951ec | ||
|
|
a172c4b725 | ||
|
|
9f979c02f8 | ||
|
|
1d59a67d6e | ||
|
|
5837a4602a | ||
|
|
e07a7ac496 | ||
|
|
de57d0b027 | ||
|
|
2f790140e0 | ||
|
|
a8bf247d01 | ||
|
|
e2b3e06077 | ||
|
|
43332d8752 | ||
|
|
b410ef5d08 | ||
|
|
1e97a9a872 | ||
|
|
26c49e7c22 | ||
|
|
89269b848d | ||
|
|
dd10486f27 | ||
|
|
81302e3ca5 | ||
|
|
cf99b4f8b9 | ||
|
|
12d39550a8 | ||
|
|
9c3e063635 | ||
|
|
bc0f3e343b | ||
|
|
d3eabd8d8f | ||
|
|
bfa8f97e7e | ||
|
|
4136328a36 | ||
|
|
2fd2c18dfc | ||
|
|
0a32f9405a | ||
|
|
7fe3238339 | ||
|
|
f491f5bf49 | ||
|
|
17070b88d3 | ||
|
|
36fd23b264 | ||
|
|
fcd1d400f8 | ||
|
|
dbbec86a13 | ||
|
|
2f0388d692 | ||
|
|
d7922c6330 | ||
|
|
b383142c14 | ||
|
|
c42b0caf0f | ||
|
|
320643d2da | ||
|
|
2560b4d336 | ||
|
|
66282d4ed0 | ||
|
|
2152f54066 | ||
|
|
c89cb32871 | ||
|
|
59eaf9177e | ||
|
|
0cc1573503 | ||
|
|
2ad3f2c1ab | ||
|
|
6cca26b914 | ||
|
|
7b417929e6 | ||
|
|
58eaddf753 | ||
|
|
89ab97076c | ||
|
|
dd444ccc8a | ||
|
|
c9cab47ce0 | ||
|
|
b868477406 | ||
|
|
71ad3fc51d | ||
|
|
837ad7e39c | ||
|
|
898c589e2b | ||
|
|
49c90a4455 | ||
|
|
d315f37c88 | ||
|
|
799c1dc804 | ||
|
|
3d457e757d | ||
|
|
f74d575d6a | ||
|
|
d4e0bacde9 | ||
|
|
844c3ab26f | ||
|
|
97c884de42 | ||
|
|
6cc974a8e9 | ||
|
|
5f3822ac1a | ||
|
|
e9763f4ec6 | ||
|
|
e4d03f9581 | ||
|
|
8b0ac6e81e | ||
|
|
a73eba8916 | ||
|
|
c74d761a4b | ||
|
|
e60cc7b544 | ||
|
|
515e53b220 | ||
|
|
8948c3f435 | ||
|
|
3a1367a1d8 | ||
|
|
e114873e4c | ||
|
|
be8f98f6c7 | ||
|
|
5a685474ff | ||
|
|
185974a3ea | ||
|
|
6c87711595 | ||
|
|
35b8e389a3 | ||
|
|
51564d1adc | ||
|
|
88df06ee47 | ||
|
|
770c746444 | ||
|
|
47215ddb13 | ||
|
|
fb09066679 | ||
|
|
7591095f2a | ||
|
|
48575c09ca | ||
|
|
5fc01fe685 | ||
|
|
df94dd2cf5 | ||
|
|
ae53ce5544 | ||
|
|
1f0e6c0426 | ||
|
|
7f84c3c71c | ||
|
|
8a050810c1 | ||
|
|
4033f7a47d | ||
|
|
62cc64d912 | ||
|
|
290ca2178d | ||
|
|
b935f8a3f3 | ||
|
|
65e31115a1 | ||
|
|
19d536ea8b | ||
|
|
8aa2401134 | ||
|
|
4699d9404b | ||
|
|
b50132e255 | ||
|
|
8f1c4af9fa | ||
|
|
d5bd816676 | ||
|
|
8093528694 | ||
|
|
7b7bf5d390 | ||
|
|
8db140fe89 | ||
|
|
2e4148a9eb | ||
|
|
ed356ba85d | ||
|
|
cf79fef0bc | ||
|
|
d4b4451a48 | ||
|
|
2f57a4f86f | ||
|
|
31b665d344 | ||
|
|
6d56bc7c0c | ||
|
|
89cf28fade | ||
|
|
8087e8ad75 | ||
|
|
df19ced50c | ||
|
|
dba8865d75 | ||
|
|
d506cf2d2a | ||
|
|
5e30e1a2ee | ||
|
|
af6d96843d | ||
|
|
c0acc1d087 | ||
|
|
98e3343808 | ||
|
|
861a847692 | ||
|
|
1917c2ce38 | ||
|
|
f9e97735e6 | ||
|
|
d0f11db700 | ||
|
|
27de8138f5 |
@@ -1,4 +1,4 @@
|
||||
name: Linux (3.8)
|
||||
name: Linux
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -7,13 +7,16 @@ on:
|
||||
branches: [ main, testing, dev ]
|
||||
|
||||
jobs:
|
||||
testLinux38:
|
||||
testLinux:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.6, 3.7, 3.8, 3.9]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Python Setup
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: ${{ matrix.python-version }}
|
||||
architecture: x64
|
||||
- name: Install Packages
|
||||
run: |
|
||||
33
.github/workflows/test_linux_3.6.yml
vendored
33
.github/workflows/test_linux_3.6.yml
vendored
@@ -1,33 +0,0 @@
|
||||
name: Linux (3.6)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, testing, dev ]
|
||||
pull_request:
|
||||
branches: [ main, testing, dev ]
|
||||
|
||||
jobs:
|
||||
testLinux36:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Python Setup
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.6
|
||||
architecture: x64
|
||||
- name: Install Packages
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install libenchant-dev qt5-default
|
||||
- name: Checkout Source
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install pytest-timeout
|
||||
pip install pytest-qt
|
||||
- name: Run Tests
|
||||
run: |
|
||||
export QT_QPA_PLATFORM=offscreen
|
||||
pytest -v --timeout=60
|
||||
33
.github/workflows/test_linux_3.7.yml
vendored
33
.github/workflows/test_linux_3.7.yml
vendored
@@ -1,33 +0,0 @@
|
||||
name: Linux (3.7)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, testing, dev ]
|
||||
pull_request:
|
||||
branches: [ main, testing, dev ]
|
||||
|
||||
jobs:
|
||||
testLinux37:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Python Setup
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.7
|
||||
architecture: x64
|
||||
- name: Install Packages
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install libenchant-dev qt5-default
|
||||
- name: Checkout Source
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install pytest-timeout
|
||||
pip install pytest-qt
|
||||
- name: Run Tests
|
||||
run: |
|
||||
export QT_QPA_PLATFORM=offscreen
|
||||
pytest -v --timeout=60
|
||||
37
.github/workflows/test_linux_3.9.yml
vendored
37
.github/workflows/test_linux_3.9.yml
vendored
@@ -1,37 +0,0 @@
|
||||
name: Linux (3.9)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, testing, dev ]
|
||||
pull_request:
|
||||
branches: [ main, testing, dev ]
|
||||
|
||||
jobs:
|
||||
testLinux39:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Python Setup
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
architecture: x64
|
||||
- name: Install Packages
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install libenchant-dev qt5-default
|
||||
- name: Checkout Source
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install pytest-timeout
|
||||
pip install pytest-cov
|
||||
pip install pytest-qt
|
||||
pip install codecov
|
||||
- name: Run Tests
|
||||
run: |
|
||||
export QT_QPA_PLATFORM=offscreen
|
||||
pytest -v --cov=nw --timeout=60
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
4
.github/workflows/test_mac.yml
vendored
4
.github/workflows/test_mac.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: macOS (3.9)
|
||||
name: macOS
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
branches: [ main, testing, dev ]
|
||||
|
||||
jobs:
|
||||
testMac38:
|
||||
testMac:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Python Setup
|
||||
|
||||
4
.github/workflows/test_win.yml
vendored
4
.github/workflows/test_win.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Windows (3.9)
|
||||
name: Windows
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
branches: [ main, testing, dev ]
|
||||
|
||||
jobs:
|
||||
testWin38:
|
||||
testWin:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Python Setup
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -7,6 +7,11 @@
|
||||
*.egg-info
|
||||
setup.iss
|
||||
novelwriter.desktop
|
||||
novelWriter.pyw
|
||||
|
||||
# Translations
|
||||
/nw/assets/i18n/*.qm
|
||||
.qmake.stash
|
||||
i18n/*.qm
|
||||
i18n/*.qph
|
||||
|
||||
@@ -41,3 +46,4 @@ ToC.txt
|
||||
|
||||
# Coverage
|
||||
/.coverage
|
||||
/coverage.*
|
||||
|
||||
223
CHANGELOG.md
223
CHANGELOG.md
@@ -1,5 +1,228 @@
|
||||
# novelWriter Changelog
|
||||
|
||||
## Version 1.3 [2021-05-02]
|
||||
|
||||
### Release Notes
|
||||
|
||||
The main feature of the 1.3 release is internationalisation. The release introduces support for
|
||||
Portuguese, French, and Norwegian for the main GUI, in addition to the default English. The
|
||||
settings for choosing the GUI language can be found in the main Preferences.
|
||||
|
||||
The implementation is such that the projects themselves can use a different language than the one
|
||||
selected for the GUI. To allow for better support for multiple language projects, the Build Novel
|
||||
Project tool has the added option to select the language of the injected text into exported
|
||||
documents. In particularly for the number words that can be added for chapter headers. Available
|
||||
languages here are the same as mentioned above for the main GUI, plus German.
|
||||
|
||||
Settings have also been added in Preferences to allow for automatic injection of spaces or non-
|
||||
breaking spaces in front of, or behind, certain characters. These are features generally available
|
||||
in text editors for French, but the implementation in novelWriter simply makes them a free text box
|
||||
where you can list all characters or symbols where you want a space added automatically.
|
||||
|
||||
The translation files for this release have been provided by Bruno Meneguello for the Portuguese
|
||||
translation, by Jan Lüdke (jyhelle) for the French translation, by Veronica Berglyd Olsen for the
|
||||
Norwegian translation as well as the minor modifications for US English, and Marian Lückhof
|
||||
provided the translation file for chapter numbers in German.
|
||||
|
||||
The work to rewrite novelWriter to allow for internationalisation was done by Bruno Meneguello and
|
||||
Veronica Berglyd Olsen.
|
||||
|
||||
_These Release Notes also include the changes from 1.3 Beta 1 and RC 1._
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* Fixed an issue when saving a document from the Build Novel Project tool when the previous path
|
||||
used for a file dialog no longer existed. The dialog defaults to show the same folder as last
|
||||
time it was opened, but should default to the user's home folder if that path no longer exists.
|
||||
There was a bug in this default behaviour that resulted in a critical error. Issue #761. PR #762.
|
||||
* Remove a duplicate error message triggered by a broken cached index file. The two error messages
|
||||
reported on the same error. The first of them was also not included in the translation framework.
|
||||
PR #759.
|
||||
|
||||
**Installation**
|
||||
|
||||
* The setup script, and file locations for the translation files, have been updated such that the
|
||||
translation files are available in both the PyPi package and in the Minimal Install packages.
|
||||
PRs #753, #764 and #765.
|
||||
|
||||
**Code Maintenance**
|
||||
|
||||
* The source code files for dialogs and tools have been moved out of the main `gui` source folder.
|
||||
The `gui` folder now only contains sub-elements of the main GUI class. PR #754.
|
||||
|
||||
----
|
||||
|
||||
## Version 1.3 RC 1 [2021-04-18]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This is a release candidate of 1.3. The primary feature of release 1.3 is the addition of
|
||||
internationalisation (i18n) of novelWriter. The release introduces support for Portuguese, French,
|
||||
and Norwegian in addition to the default English.
|
||||
|
||||
There have been no major changes since the release of 1.3 Beta 1, but the bugfixes and improvements
|
||||
from version 1.2.3 have been included.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Internationalisation**
|
||||
|
||||
* The Portuguese translation has been updated and should now be complete. Thanks again to Bruno
|
||||
Meneguello (@bkmeneguello). PR #742.
|
||||
|
||||
*The changes since 1.3 Beta 1 also include the changes of release 1.2.3.*
|
||||
|
||||
----
|
||||
|
||||
## Version 1.3 Beta 1 [2021-03-28]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This is a pre-release of 1.3. The primary feature of release 1.3 is the addition of
|
||||
internationalisation (i18n) of novelWriter. The release introduces support for Portuguese, French,
|
||||
and Norwegian in addition to the default English.
|
||||
|
||||
This is a beta release. Use with caution on live project.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Internationalisation**
|
||||
|
||||
* Added support to the source code for internationalisation of the GUI. Thanks to Bruno Meneguello
|
||||
(@bkmeneguello) for doing most of the work. Issue #93. PRs #673, #680 and #684.
|
||||
* Build Novel Project localisation has been added as well. This is separate from the GUI
|
||||
localisation as the project may not be written in the same language as the GUI is set to. PRs
|
||||
#676 and #682.
|
||||
* The text editor's auto-replace features now support inserting spaces automatically when replacing
|
||||
quotes, as well as in front of major punctuation. These features are common for writing tools
|
||||
supporting French and Spanish for instance. Issue #703. PR #704.
|
||||
|
||||
**User Interface**
|
||||
|
||||
* The Preferences dialog has been updated to be more responsive to varying text label lengths due
|
||||
to different needs for different languages. PRs #687 and #711.
|
||||
* The Project Settings dialog has been improved to be more consistent and user friendly across the
|
||||
Status, Importance and Auto-Replace tabs. Issue #691. PR #695.
|
||||
* The About dialog has been updated to contain more information on contributions. PR #698.
|
||||
|
||||
**Translations**
|
||||
|
||||
* Portuguese translation added by Bruno Meneguello (@bkmeneguello). PRs #673, #681, #686 and #697.
|
||||
* Norwegian translation added by Veronica Olsen (@vkbo). PR #679.
|
||||
* German translation added (Build tool only) by Marian Lückhof (@Number042). PR #683.
|
||||
* French translation added by Jan Lüdke (@jyhelle). PRs #692, #711 and #713.
|
||||
* American English added by Veronica Olsen (@vkbo). PR #693.
|
||||
|
||||
**Installation**
|
||||
|
||||
* Added i18n support to the setup script. PRs #673 and #729.
|
||||
|
||||
**Code Maintenance**
|
||||
|
||||
* The ISO lookup dictionary for language codes has been removed and replaced with the lookup
|
||||
features available in Qt5 through QLocal. The remaining two source files in the constants folder
|
||||
have been moved up a level as well. PRs #673 and #730.
|
||||
|
||||
----
|
||||
|
||||
## Version 1.2.3 [2021-04-18]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This patch fixes a bug where the user's word list (personal dictionary) was not saved properly for
|
||||
new projects. The added words were thus "forgotten" the next time the project was opened.
|
||||
|
||||
In addition, uninstall commands have been added to the main setup script to make it easier to clean
|
||||
up icons (Linux and Windows) and registry keys (Windows) if the user wishes to uninstall
|
||||
novelWriter.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* Fixed an issue where the initial file for the user dictionary for a new project would not be
|
||||
created the first time a word was added to it. This caused the words the user added to the
|
||||
dictionary to not be loaded the next time the project was opened. Issue #733. PR #734.
|
||||
* Some of the `setup.py` commands could not run if the PyQt5 packages were missing due to an import
|
||||
command extracting the version number from the main `nw` package. This was not technically a bug
|
||||
as the choice to do it this way was made deliberately, but a function has been added to the
|
||||
`setup.py` script to allow reading the version number without having to import the `nw` package.
|
||||
PR #749.
|
||||
|
||||
**Installation**
|
||||
|
||||
* An `xdg-uninstall` option has been added to `setup.py` to uninstall the icons installed into the
|
||||
system by the `xdg-install` command. PR #736.
|
||||
* Likewise, a `win-uninstall` option has been added to `setup.py` to uninstall the icons installed
|
||||
into the system by the `win-install` command. Issue #743 and discussion #739. PR #749.
|
||||
* Improvements have also been made to the `setup_windows.bat` script, and a complementary
|
||||
`uninstall_windows.bat` has been added as well. PR #749.
|
||||
|
||||
----
|
||||
|
||||
## Version 1.2.2 [2021-03-28]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This patch release is a bug fix release addressing some inconsistencies and issues with the
|
||||
document header buttons when Focus Mode is active. The keyboard shortcuts for search and replace
|
||||
should now also work in Focus Mode. In addition, the setup script for novelWriter has been improved
|
||||
when installing on Windows.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* The way Focus Mode worked when activated through the menu and through the document header button
|
||||
were inconsistent. The header button would deactivate the edit, search and close buttons, while
|
||||
the menu entry would not. These two methods now call the same set of functions to ensure the
|
||||
behaviour is consistent. PR #717.
|
||||
* Closing the document while in Focus Mode now ends Focus Mode. Previously, the editor would be
|
||||
left stuck in Focus Mode with no way to exit. PR #717.
|
||||
|
||||
**User Interface**
|
||||
|
||||
* The keyboard shortcuts for the search and replace tool now also work in Focus Mode. Previously,
|
||||
the menu entries and their shortcuts were deactivated in this mode. Issue #716. PR #717.
|
||||
|
||||
**Installation**
|
||||
|
||||
* The setup script command do build minimal install archive files now also generate SHA 256 sum
|
||||
files. PR #724.
|
||||
* The setup script will now copy the `novelWriter.py` file to `novelWriter.pyw` if it doesn't exist
|
||||
when the `win-install` command is run. Issue #727. PR #728.
|
||||
|
||||
----
|
||||
|
||||
## Version 1.2.1 [2021-03-21]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This patch release is a bug fix release addressing issues with the document editor's search and
|
||||
replace tool. Due to some recently added restrictions on when various tools are active, depending
|
||||
on which part of the main window has the user's focus, the search tool keyboard shortcuts and
|
||||
buttons were blocked when they shouldn't. This release resolves these issues.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* Fixed an issue with the search bar where the button and shortcut actions would be blocked unless
|
||||
the document itself had focus. This focus check was added for all the text altering functions,
|
||||
but should not affect search and replace. The search and replace actions now bypass the regular
|
||||
action pipeline with the focus check. Issue #708. PR #709.
|
||||
|
||||
**Documentation**
|
||||
|
||||
* Improve the instructions for setup on Linux when manually installing using the `setup.py` script.
|
||||
Previously, the documentation wasn't very clear on the difference between a user space install
|
||||
and a system wide install. Neither did it explain how to install `setuptools` if the package is
|
||||
missing. Issue #714. PR #715.
|
||||
|
||||
----
|
||||
|
||||
## Version 1.2 [2021-03-14]
|
||||
|
||||
### Release Notes
|
||||
|
||||
191
README.md
191
README.md
@@ -1,53 +1,53 @@
|
||||
# novelWriter
|
||||
|
||||
[/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions)
|
||||
[/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions)
|
||||
[/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions)
|
||||
[/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions)
|
||||
[/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions)
|
||||
[/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions)
|
||||
[](https://github.com/vkbo/novelWriter/actions/workflows/test_linux.yml)
|
||||
[](https://github.com/vkbo/novelWriter/actions/workflows/test_win.yml)
|
||||
[](https://github.com/vkbo/novelWriter/actions/workflows/test_mac.yml)
|
||||
[](https://github.com/vkbo/novelWriter/actions)
|
||||
[](https://codecov.io/gh/vkbo/novelWriter)
|
||||
[](https://novelwriter.readthedocs.io/en/latest/?badge=latest)
|
||||
[](https://novelwriter.readthedocs.io/en/latest/?badge=latest)
|
||||
[](https://github.com/vkbo/novelWriter/releases)
|
||||
[](https://pypi.org/project/novelWriter)
|
||||
[](https://pypi.org/project/novelWriter)
|
||||
|
||||
<img align="left" style="margin: 0 16px 4px 0;" src="https://raw.githubusercontent.com/vkbo/novelWriter/main/setup/icons/scaled/icon-novelwriter-96.png">
|
||||
|
||||
novelWriter is a Markdown-like text editor designed for writing novels assembled from many smaller
|
||||
text documents. It uses a minimal formatting syntax inspired by Markdown, and adds a meta data
|
||||
syntax for comments, synopsis, and cross-referencing between files. It's designed to be a simple
|
||||
text editor that allows for easy organisation of text files and notes, built on plain text files
|
||||
for robustness.
|
||||
novelWriter is a plain text editor designed for writing novels assembled from many smaller text
|
||||
documents. It uses a minimal formatting syntax inspired by Markdown, and adds a meta data syntax
|
||||
for comments, synopsis, and cross-referencing. It's designed to be a simple text editor that allows
|
||||
for easy organisation of text and notes, using human readable text files as storage for robustness.
|
||||
|
||||
The plain text files are suitable for version control software, and also well suited for file
|
||||
synchronisation tools. The core project structure is stored in a single project XML file. Other
|
||||
meta data is primarily saved in JSON files.
|
||||
The project storage is suitable for version control software, and also well suited for file
|
||||
synchronisation tools. All text is saved as plain text files with a meta data header, the core
|
||||
project structure is stored in a single project XML file, and other meta data is primarily saved as
|
||||
JSON files.
|
||||
|
||||
The full documentation is available at
|
||||
[novelwriter.readthedocs.io](https://novelwriter.readthedocs.io).
|
||||
The full documentation is available on [readthedocs.io](https://novelwriter.readthedocs.io).
|
||||
|
||||
## Implementation
|
||||
|
||||
The application is written in Python 3 using Qt5 via PyQt5. It is developed on Linux, but it should
|
||||
in principle work fine on other operating systems as well, as long as dependencies are met. The
|
||||
unit tests are run on the latest versions of Ubuntu Linux, Windows Server and macOS.
|
||||
The application is written in Python 3 (3.6+) using Qt5 and PyQt5 (5.3+). It is developed on Linux,
|
||||
but should in principle work fine on other operating systems as well as long as dependencies are
|
||||
met. It is regularly tested on Debian and Ubuntu Linux, Windows, and macOS.
|
||||
|
||||
## Project Contributions
|
||||
|
||||
Contributions to this project are welcome. However, please read the
|
||||
[Contributing Guide](https://github.com/vkbo/novelWriter/blob/main/CONTRIBUTING.md) before
|
||||
submitting larger additions or changes to this project.
|
||||
submitting larger additions or changes.
|
||||
|
||||
# Key Features
|
||||
If you want to help translating novelWriter into another language, please see the
|
||||
[README](https://github.com/vkbo/novelWriter/blob/main/i18n/README.md) in the `nw/assets/i18n`
|
||||
folder.
|
||||
|
||||
Some features of novelWriter are listed below. Consult the
|
||||
## Key Features
|
||||
|
||||
Some key features of novelWriter are listed below. Consult the
|
||||
[documentation](https://novelwriter.readthedocs.io) for more information.
|
||||
|
||||
### Markdown Flavour
|
||||
|
||||
novelWriter is _not_ a full-feature Markdown editor. It is a plain text editor that uses
|
||||
Note that novelWriter is _not_ a proper Markdown editor. It is a plain text editor that uses
|
||||
Markdown-like syntax to allow for a minimal set of formatting that is useful for the specific task
|
||||
of writing novels. The formatting is currently limited to:
|
||||
|
||||
@@ -58,30 +58,30 @@ of writing novels. The formatting is currently limited to:
|
||||
|
||||
That is it. Features not supported in the editor are also not exported when using the export tool.
|
||||
|
||||
In addition, novelWriter adds the following, which is otherwise not supported by Markdown:
|
||||
In addition, novelWriter adds the following syntax used for its additional features:
|
||||
|
||||
* A line starting with `%` is treated as a comment and not rendered on exports unless requested.
|
||||
Comments do not count towards the word count.
|
||||
Comments do not count towards word counts and other statistics.
|
||||
* If the first word of the comment is `synopsis:`, the comment is indexed and treated as the
|
||||
synopsis for the section of text under the same header. These synopsis comments can be used to
|
||||
build an outline and exported to external documents.
|
||||
* A set of meta data keyword/values starting with the character `@`. This is used for tagging
|
||||
and inter-linking documents, and can also be included when generate a project outline.
|
||||
synopsis for the section of text where it occurs. These synopsis comments can be used to build an
|
||||
outline and exported to external documents.
|
||||
* A set of meta data keyword/values starting with the character `@`. These are used for tagging
|
||||
and inter-linking documents, and can also be included when generating a project outline.
|
||||
* A variety of thin and non-breaking spaces are supported. Some of them depend on the system
|
||||
running at least Qt 5.9. Earlier versions of Qt will unfortunately strip them out when saving.
|
||||
* Tabs can be used in the text, and should be properly aligned in both editor and viewer. This can
|
||||
be used to make simple tables and lists. Full Markdown tables and lists are not supported. Note
|
||||
that for HTML exports, most browsers will treat a tab as a space, so it may not show up like
|
||||
expected. Open Document exports should produce the expected result.
|
||||
be used to make simple tables and lists. Note that for HTML exports, most browsers will treat a
|
||||
tab as a space, so it may not show up like expected. Open Document exports should produce the
|
||||
expected result.
|
||||
|
||||
The core export formats of novelWriter are Open Document and HTML5. Open Document is an open
|
||||
standard for office type documents that is supported by most office applications. See
|
||||
[Open Document > Application Support](https://en.wikipedia.org/wiki/OpenDocument#Application_support)
|
||||
for more details.
|
||||
|
||||
You can also export the entire project as a single novelWriter Markdown-flavour document. These can
|
||||
later be imported again into novelWriter. In addition, printing and export to PDF is offered
|
||||
through the Qt library, although with limitations to formatting.
|
||||
You can also export the entire project as a single novelWriter-flavour document. These can later be
|
||||
imported again into novelWriter. In addition, printing and export to PDF is offered through the Qt
|
||||
library, although with limitations to formatting.
|
||||
|
||||
### Colour Themes
|
||||
|
||||
@@ -92,14 +92,14 @@ syntax highlighting themes. Optional GUI themes are also available, including da
|
||||
|
||||
The structure of the project is shown on the left hand side of the main window. Project files are
|
||||
organised into root folders, indicating what class of file they are. The most important root folder
|
||||
is the `Novel` folder, which contains all of the files that make up the finished novel. Each root
|
||||
is the `Novel` folder, which contains all of the files that make up the novel itself. Each root
|
||||
folder can have subfolders. Subfolders have no impact on the final project structure, they are
|
||||
there for you to organise your files in whatever way you want.
|
||||
|
||||
The editor supports four levels of headings, which determines what level the following text belongs
|
||||
The editor supports four levels of headings, which determine what level the following text belongs
|
||||
to. Headings of level one signify a book or partition title. Headings of level two signify the
|
||||
start of a new chapter. Headings of level three signify the start of a new scene. Headings of level
|
||||
four can be used internally in each scene to separate sections.
|
||||
four can be used internally in each scene to create separate sections.
|
||||
|
||||
Each novel file can be assigned a layout format, which shows up as a flag next to the item in the
|
||||
project tree. These are mostly to help the user track what they contain, but they also have some
|
||||
@@ -108,104 +108,91 @@ impact on the format of the exported document. See the
|
||||
|
||||
### Project Notes
|
||||
|
||||
Supporting note files can be added for the story plot, characters, locations, story timeline, etc.
|
||||
These have their separate root folders. These are optional files.
|
||||
Supporting notes can be added for the story plot, characters, locations, story timeline, etc. These
|
||||
have their separate root folders and are optional to use.
|
||||
|
||||
### Visualisation of Story Elements
|
||||
|
||||
The different notes can be assigned tags, which other files can refer back to using the `@` meta
|
||||
keywords. This information can be used to display an outline of the story, showing where each scene
|
||||
connects to the plot, and which characters, etc. occur in them. In addition, the tags themselves
|
||||
are clickable in the document view pane, and control-clickable in the editor. They make it possible
|
||||
to quickly navigate between the documents while editing.
|
||||
The different notes can be assigned tags, which other files can refer back to using `@`-prefixed
|
||||
meta keywords. This information can be used to display an outline of the story, showing where each
|
||||
scene connects to the plot, and which characters, etc. occur in them. In addition, the tags
|
||||
themselves are clickable in the document view pane, and control-clickable in the editor. They make
|
||||
it possible to quickly navigate between the documents while writing.
|
||||
|
||||
# Installing and Running
|
||||
## Standard Installation
|
||||
|
||||
For install instructions, please check the
|
||||
[Getting Started](https://novelwriter.readthedocs.io/en/latest/int_started.html) section in
|
||||
the [documentation](https://novelwriter.readthedocs.io/).
|
||||
For a regular installation, it is recommended that you download one of the minimal zip files from
|
||||
the [Releases](https://github.com/vkbo/novelWriter/releases) page or the
|
||||
[novelwriter.io](https://novelwriter.io/) website.
|
||||
The [documentation](https://novelwriter.readthedocs.io/) has detailed install instructions for
|
||||
[Linux](https://novelwriter.readthedocs.io/en/latest/setup_linux.html),
|
||||
[Windows](https://novelwriter.readthedocs.io/en/latest/setup_windows.html), and
|
||||
[macOS](https://novelwriter.readthedocs.io/en/latest/setup_mac.html).
|
||||
They are pretty straightforward.
|
||||
|
||||
## TLDR Instructions
|
||||
## Running from Source
|
||||
|
||||
If you want to run novelWriter directly from the source code, you must run the `novelWriter.py`
|
||||
file from command line. For installations on Linux, macOS or Windows, see below.
|
||||
file from command line.
|
||||
|
||||
**Note:** You may need to replace `python` with `python3` and `pip` with `pip3` in the instructions
|
||||
below on some systems. You may also want to add the `--user` flag for `pip` to install in your user
|
||||
space only.
|
||||
|
||||
### Install from PyPi
|
||||
### Dependencies
|
||||
|
||||
novelWriter is available on [pypi.org](https://pypi.org/project/novelWriter/), and can be installed
|
||||
with:
|
||||
```bash
|
||||
pip install novelwriter
|
||||
```
|
||||
Dependencies should be installed automatically, but can also be installed directly with:
|
||||
```bash
|
||||
pip install pyqt5 lxml pyenchant
|
||||
```
|
||||
When installing via pip, novelWriter can be launched from command line with:
|
||||
```bash
|
||||
novelWriter
|
||||
```
|
||||
|
||||
Make sure the install location for pip is in your PATH variable. This is not always the case by
|
||||
default.
|
||||
|
||||
### Setup on Linux
|
||||
|
||||
If you're installing from source, the following commands will set up novelWriter on Linux:
|
||||
Dependencies can generally be installed from PyPi with:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
python setup.py install
|
||||
```
|
||||
|
||||
### Additional Steps for Linux
|
||||
|
||||
On Linux, you can most likely find the dependencies in your distribution's repository. On Ubuntu
|
||||
and Debian, run:
|
||||
```bash
|
||||
sudo apt install python3-pyqt5 python3-lxml python3-enchant
|
||||
```
|
||||
|
||||
If you want to set up a launcher and icons on Linux, you can run:
|
||||
```bash
|
||||
python setup.py xdg-install
|
||||
```
|
||||
|
||||
This should make novelWriter available as a regular application on your system, with a launceher
|
||||
icon, and file association with novelWriter project files.
|
||||
### Additional Steps for macOS
|
||||
|
||||
**Note:** If you don't want to install the dependencies from pip and instead use your distro's own
|
||||
packages, you must manually install `python3-pyqt5`, `python3-lxml` and `python3-enchant`
|
||||
(Debian/Ubuntu), or the corresponding packages for your distro.
|
||||
|
||||
### Setup on macOS
|
||||
|
||||
If you're installing from source, the following commands will set up novelWriter on macOS:
|
||||
First, make sure you have properly set up Python3 with Homebrew. If not, check their
|
||||
[documentation](https://docs.brew.sh/Homebrew-and-Python).
|
||||
In addition, the following steps are necessary to install all dependencies:
|
||||
```bash
|
||||
brew install enchant
|
||||
pip3 install --user -r requirements.txt
|
||||
pip3 install --user pyobjc
|
||||
python3 setup.py install
|
||||
```
|
||||
|
||||
At present, novelWriter isn't further integrated into the OS, so you must launch it from command
|
||||
line with:
|
||||
```bash
|
||||
novelWriter
|
||||
```
|
||||
### Additional Steps for Windows
|
||||
|
||||
If you want to help improve the setup process on macOS, it would be much appreciated.
|
||||
Windows does not by default come with Python installed. If you haven't installed it already, get it
|
||||
from [python.org/downloads](https://www.python.org/downloads/). Remember to select "Add Python to
|
||||
PATH" during the installation.
|
||||
|
||||
### Setup on Windows
|
||||
The script `windows_install.bat` in the `setup` folder can be used to create desktop and start menu
|
||||
icons for novelWriter. The script will also install dependencies for you from PyPi.
|
||||
|
||||
For Windows, first ensure that you have Python installed. If not, get it from
|
||||
[python.org/downloads](https://www.python.org/downloads/). Remember to select "Add Python to PATH"
|
||||
during the installation, otherwise novelWriter will not start.
|
||||
## Internationalisation
|
||||
|
||||
Then, download the `novelWriter-x.y.z-minimal-win.zip` file, where `x.y.z` is the version number,
|
||||
from the [releases](https://github.com/vkbo/novelWriter/releases) page. You can extract it to
|
||||
wherever you want to keep novelWriter on your PC, and run the `setup_windows.bat` file in it
|
||||
(double-clicking it should work). This will install the necessary dependencies from
|
||||
[pypi.org](https://pypi.org/) and create desktop and start menu icons.
|
||||
If you install from source, you must build the translation files yourself if you want to switch to
|
||||
a different GUI language than British English. This requires that you have the Qt translation
|
||||
framework installed. Check the specific instruction in the README in the `i18n` source folder for
|
||||
how to build the translation files.
|
||||
|
||||
## Debugging
|
||||
|
||||
If you need to debug novelWriter, you must run it from command line. It takes a few parameters,
|
||||
If you need to debug novelWriter, you must run it from the command line. It takes a few parameters,
|
||||
which can be listed with the switch `--help`. The `--info`, `--debug` or `--verbose` flags are
|
||||
particularly useful for increasing logging output for debugging.
|
||||
|
||||
# Licenses
|
||||
## Licenses
|
||||
|
||||
This is Open Source software, and novelWriter is licensed under GPLv3. See the
|
||||
[GNU General Public License website](https://www.gnu.org/licenses/gpl-3.0.en.html) for more
|
||||
@@ -231,7 +218,7 @@ Bundled assets have the following licenses:
|
||||
the main repo is available at
|
||||
[sdras/night-owl-vscode-theme](https://github.com/sdras/night-owl-vscode-theme).
|
||||
|
||||
# Screenshot
|
||||
## Screenshots
|
||||
|
||||
**novelWriter with default system theme:**
|
||||

|
||||
|
||||
@@ -25,10 +25,9 @@ copyright = "2018–2021, Veronica Berglyd Olsen"
|
||||
author = "Veronica Berglyd Olsen"
|
||||
|
||||
# The short X.Y version
|
||||
version = "1.2"
|
||||
version = "1.3"
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = "1.2"
|
||||
|
||||
release = "1.3"
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
@@ -47,7 +46,6 @@ language = None
|
||||
exclude_patterns = []
|
||||
pygments_style = None
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
@@ -75,13 +73,11 @@ html_css_files = [
|
||||
# "searchbox.html"]``.
|
||||
# html_sidebars = {}
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ---------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = "novelWriterDoc"
|
||||
|
||||
|
||||
# -- Options for LaTeX output ------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
@@ -110,7 +106,6 @@ latex_documents = [(
|
||||
author, "manual"
|
||||
)]
|
||||
|
||||
|
||||
# -- Options for manual page output ------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
@@ -119,7 +114,6 @@ man_pages = [(
|
||||
master_doc, "novelwriter", "novelWriter Documentation", [author], 1
|
||||
)]
|
||||
|
||||
|
||||
# -- Options for Texinfo output ----------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
@@ -130,7 +124,6 @@ texinfo_documents = [(
|
||||
"novelWriter", "Markdown-like editor for novels.", "Miscellaneous"
|
||||
)]
|
||||
|
||||
|
||||
# -- Options for Epub output -------------------------------------------------
|
||||
|
||||
# Bibliographic Dublin Core info.
|
||||
|
||||
@@ -99,6 +99,30 @@ Make sure the install location for pip is in your PATH variable. This is not alw
|
||||
default.
|
||||
|
||||
|
||||
.. _a_started_i18n:
|
||||
|
||||
Building the Translation Files
|
||||
==============================
|
||||
|
||||
If you installed novelWriter from a package, the translation files should be pre-built and
|
||||
included. If you're running novelWriter from the source code, you will need to generate the files
|
||||
yourself. The files you need will be written to the ``i18n`` folder, and will have the ``.qm`` file
|
||||
extension.
|
||||
|
||||
You can build the ``.qm`` files with:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
python3 setup.py qtlrelease
|
||||
|
||||
This requires that the Python package ``pylupdate5`` to be installed.
|
||||
|
||||
.. note::
|
||||
If you want to improve novelWriter with translation files for another language, or update an
|
||||
existing translation, instructions for how to contribute can be found in the README file in the
|
||||
``i18n`` folder of the source code.
|
||||
|
||||
|
||||
.. _a_started_docs:
|
||||
|
||||
Building the Documentation
|
||||
@@ -109,13 +133,14 @@ you're running novelWriter from the source code, a local copy of this documentat
|
||||
generated. It requires the following Python packages on Debian and Ubuntu.
|
||||
|
||||
* ``python3-sphinx``
|
||||
* ``python3-sphinx-rtd-theme``
|
||||
* ``python3-sphinxcontrib.qthelp``
|
||||
|
||||
Or from PyPi:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
pip install sphinx sphinxcontrib-qthelp
|
||||
pip install sphinx sphinx-rtd-theme sphinxcontrib-qthelp
|
||||
|
||||
The documentation can then be built from the ``docs`` folder in the source code by running:
|
||||
|
||||
|
||||
@@ -42,34 +42,69 @@ can install them with:
|
||||
Step 2: Install Package (Optional)
|
||||
----------------------------------
|
||||
|
||||
You can install novelWriter to the correct location for Python packages with:
|
||||
You can install novelWriter to the default location for Python packages using ``setuptools``. This
|
||||
step is optional as you can also just put the novelWriter program folder wherever you like
|
||||
yourself. For instance in ``/opt/novelWriter``, and then run Step 3 to set up icons and launcher.
|
||||
|
||||
To install novelWriter to the default location, it requires that you have ``setuptools`` installed
|
||||
on your system. If you don't have it installed, it can usually be installed from your distro's
|
||||
repository. For Debian and Ubuntu this is achieved with:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
./setup.py install
|
||||
sudo apt install python3-setuptools
|
||||
|
||||
The package is also available from PyPi:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
pip3 install --user setuptools
|
||||
|
||||
With ``setuptools`` in place, novelWriter can be installed to the user space with:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
./setup.py install --user
|
||||
|
||||
This should install novelWriter as ``~/.local/bin/novelWriter``. If you instead want to install for
|
||||
all users, i.e. as ``/usr/local/bin/novelWriter``, run:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
sudo ./setup.py install
|
||||
|
||||
This is equivalent to what the ``pip`` installer does. It puts novelWriter in the location on your
|
||||
system where Python packages are usually kept. This is not really the best suited location for a
|
||||
GUI application like novelWriter, so you may instead copy the entire source to a suiteable location
|
||||
yourself.
|
||||
|
||||
This should install novelWriter to either ``~/.local/bin/novelWriter`` if installed for local user
|
||||
only, or to ``/usr/local/bin/novelWriter`` if installed for all users (with the ``sudo`` command).
|
||||
|
||||
|
||||
Step 3: Create Launcher Icons
|
||||
-----------------------------
|
||||
|
||||
Regardless of where you extract or install the source files, you can easily set up a standard icon
|
||||
and launcher. To set up this desktop launcher, the needed icons, and the project file association,
|
||||
run:
|
||||
Regardless of where you extract or install the source files, you can set up a standard icon and a
|
||||
launcher. To set up this desktop launcher, the needed icons, and the project file association,
|
||||
run the following from inside the novelWriter folder at the installed or final location:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
./setup.py xdg-install
|
||||
|
||||
By default, these commands install novelWriter and its icons for the current user only. To install
|
||||
for all users, run the script with the ``sudo`` command.
|
||||
By default, this command installs the launcher and icons for the current user only. To install for
|
||||
all users, run the script with the ``sudo`` command.
|
||||
|
||||
.. tip::
|
||||
All options of the setup script can be listed with: ``./setup.py help``.
|
||||
|
||||
|
||||
Uninstalling Icons
|
||||
==================
|
||||
|
||||
The steps taken by the ``xdg-install`` step can be reversed by running:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
./setup.py xdg-uninstall
|
||||
|
||||
This will remove the desktop launcher and icons from the system. As above, whether this is done on
|
||||
the current user, or system wide, depends on whether this command is called with ``sudo`` or not.
|
||||
|
||||
@@ -52,12 +52,19 @@ You can install novelWriter to the correct location for Python packages with:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
./setup.py install
|
||||
./setup.py install --user
|
||||
|
||||
This is equivalent to what the ``pip`` installer does. It puts novelWriter in the location on your
|
||||
system where Python packages are usually kept. This is not really the best suited location for a
|
||||
GUI application like novelWriter, so you may instead copy the entire source to a suiteable location
|
||||
yourself.
|
||||
This requires that the package ``setuptools`` is installed on your system. If not, it can be
|
||||
installed with:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
pip3 install --user setuptools
|
||||
|
||||
This is method of install is equivalent to what the ``pip`` installer does. It puts novelWriter in
|
||||
the location on your system where Python packages are usually kept. This is not really the best
|
||||
suited location for a GUI application like novelWriter, so you may instead copy the entire source
|
||||
to a suiteable location yourself.
|
||||
|
||||
After this, you should be able to launch novelWriter by running ``novelWriter`` in a command line
|
||||
window.
|
||||
|
||||
@@ -75,10 +75,13 @@ Step 2: Dependencies and Icons
|
||||
|
||||
**Alternative A: By Script**
|
||||
|
||||
Open the folder where you extracted the novelWriter source, and double-click the file named
|
||||
``setup_windows.bat``. This should open a command line window and run the setup script to install
|
||||
Open the folder where you extracted novelWriter, and double-click the file named
|
||||
``windows_install.bat``. This should open a command line window and run the setup script to install
|
||||
dependencies, and add desktop and start menu icons.
|
||||
|
||||
.. note::
|
||||
If you downloaded the full source package, the file may be in the ``setup`` subfolder.
|
||||
|
||||
The script will also check that it can find Python on your system and alert you if it cannot run
|
||||
it. If you are sure you have installed it, but the script cannot find it, you probably didn't
|
||||
install it with the "Add Python to PATH" option mentioned in Step 1.
|
||||
@@ -100,7 +103,7 @@ and run the following commands:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
python setup.py pip
|
||||
pip install --user pywin32 -r requirements.txt
|
||||
python setup.py win-install
|
||||
|
||||
The first command will install the dependencies on your system from the `Python Package Index`_,
|
||||
@@ -108,3 +111,39 @@ and the second command will create a desktop icon and a start menu icon. That sh
|
||||
you need.
|
||||
|
||||
.. _Python Package Index: https://pypi.org/
|
||||
|
||||
|
||||
Uninstalling
|
||||
============
|
||||
|
||||
**Alternative A: By Script**
|
||||
|
||||
Open the folder where you keep the novelWriter files, and double-click the file named
|
||||
``windows_uninstall.bat``. This should open a command line window and run the setup script to
|
||||
remove the main dependency packages and remove desktop and start menu icons.
|
||||
|
||||
.. note::
|
||||
If you downloaded the full source package, the file may be in the ``setup`` subfolder.
|
||||
|
||||
If you plan to also remove Python from your system, you must run the above script first as it needs
|
||||
Python in order to run.
|
||||
|
||||
.. note::
|
||||
Due to limitations of the ``pip`` installer, dependencies of the dependencies will not be
|
||||
removed, only the ones the setup script directly installed.
|
||||
|
||||
**Alternative B: Manual Uninstallation**
|
||||
|
||||
Like for the install process, the script just runs two commands. You can of course run them
|
||||
yourself if you wish. They are:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
python setup.py win-uninstall
|
||||
pip uninstall -r requirements.txt
|
||||
|
||||
There may be other packages on your system installed by ``pip``. To list all packages, run:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
pip freeze --user
|
||||
|
||||
@@ -152,6 +152,29 @@ convenient if you want to quickly look through all documents in the list in the
|
||||
pressing :kbd:`F9`.
|
||||
|
||||
|
||||
.. _a_ui_edit_search:
|
||||
|
||||
Search & Replace
|
||||
----------------
|
||||
|
||||
The document editor has a search and replace bar that can be activated with :kbd:`Ctrl`:kbd:`F` for
|
||||
search mode or :kbd:`Ctrl`:kbd:`H` for search/replace mode.
|
||||
|
||||
Pressing :kbd:`Return` while in the search box will search for the next occurrence of the word, and
|
||||
:kbd:`Shift`:kbd:`Return` for the previous. Pressing :kbd:`Return` in the replace box, will replace
|
||||
the highlighted text and move to the next word.
|
||||
|
||||
There are a number of settings for the search bar available as toggle switches above the search
|
||||
box. They allows you to search for, in order:,: matched case only, whole word results only, search
|
||||
using regular expressions, loop search when reaching the end of the document, and move to the next
|
||||
document when reaching the end. There is also a switch that will try to match the case of the word
|
||||
when the replacement is made. That is, it will try to keep the word upper, lower, or capitalised to
|
||||
match the word being replaced.
|
||||
|
||||
The regular expression search is somewhat dependant on which version of Qt your system has. If you
|
||||
have Qt 5.13 or higher, there is better support for unicode symbols in the search.
|
||||
|
||||
|
||||
.. _a_ui_edit_auto:
|
||||
|
||||
Auto-Replace as You Type
|
||||
@@ -432,7 +455,7 @@ Most features are available as keyboard shortcuts. These are as follows:
|
||||
":kbd:`Ctrl`:kbd:`Backspace`", "Delete previous word in editor."
|
||||
":kbd:`Ctrl`:kbd:`'`", "Wrap selected text, or word under cursor, in single quotes."
|
||||
":kbd:`Ctrl`:kbd:`""`", "Wrap selected text, or word under cursor, in double quotes."
|
||||
":kbd:`Ctrl`:kbd:`Enter`", "Open the tag or reference under the cursor in the Viewer."
|
||||
":kbd:`Ctrl`:kbd:`Retrun`", "Open the tag or reference under the cursor in the Viewer."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`,`", "Open the :guilabel:`Project Settings` dialog."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`/`", "Remove block formatting for block under cursor."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`1`", "Replace occurrence of search word in current document, and search for next occurrence."
|
||||
|
||||
72
i18n/README.md
Normal file
72
i18n/README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# novelWriter Internationalisation
|
||||
|
||||
The `i18n` folder contains the translation files for the Qt5 GUI. There are two types of files
|
||||
involved: the `nw_XX.ts` and the `nw_XX.qm` files. The `nw_XX.ts` files are located in the `i18n`
|
||||
folder at the root of the repository, and the `nw_XX.qm` together with the `project_XX.json` files
|
||||
are located in `nw/assets/i18n`. The latter are JSON files translation maps for the novelWriter
|
||||
projects, used by the Build Novel Project tool.
|
||||
|
||||
## Qt GUI Localisation
|
||||
|
||||
The `.qm` files are the actual files used by the application to translate the GUI into another
|
||||
language other than the default British English. The files are not generated by default. They can
|
||||
be built with:
|
||||
```bash
|
||||
python3 setup.py qtlrelease
|
||||
```
|
||||
|
||||
To add a new translation file, first add a new entry in the `novelWriter.pro` file under the
|
||||
`TRANSLATIONS` variable. Please use the underscore syntax for file names as the importer expects
|
||||
this format.
|
||||
|
||||
After adding the entry in the `.pro` file, run:
|
||||
```bash
|
||||
python3 setup.py qtlupdate
|
||||
```
|
||||
|
||||
This will build a new `i18n/nw_XX.ts` file for the language you just added. The file can then be
|
||||
edited with the Qt 5 Linguist application provided by Qt. This is by far easier than manually
|
||||
editing the `.ts` file. Please select "English" and "United Kingdom" as the source langauge when
|
||||
prompted by Qt 5 Linguist.
|
||||
|
||||
When you're done editing, you can build the `nw/assets/i18n/nw_XX.qm` file and test it in
|
||||
novelWriter. The Preferences dialog should list the newly added language, and you can select it and
|
||||
test it.
|
||||
|
||||
Please do not submit the `.qm` files to the repository. Only the `.ts` files in the `i18n` folder
|
||||
are needed.
|
||||
|
||||
**Note:** This requires that you have the tools Qt 5 Linguist and the PyQt5 tool `pylupdate5`
|
||||
installed on your system.
|
||||
|
||||
### Missing QtBase Translations
|
||||
|
||||
The default Qt dialogs also have translations, for instance for standard buttons for "Yes", "No",
|
||||
"Ok", "Cancel", etc. Generally, these translations files are installed with the Qt libraries on
|
||||
your system, and novelWriter will collect those translations from there. However, these
|
||||
translations are missing for many languages.
|
||||
|
||||
As a starting point, there is no need to translate any entries in the `.ts` files that are under
|
||||
elements starting with the letter "Q", like "QPlatformTheme", "QWizard", etc. If these turn up in
|
||||
English in novelWriter after activating a translation, it means they are probably missing in the Qt
|
||||
library, and you may also need to translate these.
|
||||
|
||||
These additional translation entries are generated from a file named `i18n/qtbase.py`, which is not
|
||||
a file that novelWriter uses. It is there only to generate these additional entries for the `.ts`
|
||||
files.
|
||||
|
||||
## Project Localisation
|
||||
|
||||
Projects can have a different language setting than the GUI itself. The files with format
|
||||
`project_XX.json` in `nw/assets/i18n` are simple translation maps for text that goes into the
|
||||
exported documents generated by the Build Novel Project tool.
|
||||
|
||||
The files are loaded based on the language setting in the build tool. At the present time, no other
|
||||
parts of the application use these files. Since these are used as replacement lookups, they are
|
||||
maintained as plain JSON files. The main usage is to generate chapter headers as number words.
|
||||
|
||||
Adding new translations for these files is easy. Just copy the `project_en.json` file, rename it to
|
||||
the appropriate language coded filename, and edit it. Please use the underscore as separator if a
|
||||
language needs to be localised to a specific country under a language. The importer will for
|
||||
instance look for `en_US` first, then use `en` if it is not found. If there are no matching files,
|
||||
the project falls back to using English.
|
||||
4931
i18n/nw_en_US.ts
Normal file
4931
i18n/nw_en_US.ts
Normal file
File diff suppressed because it is too large
Load Diff
4931
i18n/nw_fr.ts
Normal file
4931
i18n/nw_fr.ts
Normal file
File diff suppressed because it is too large
Load Diff
4931
i18n/nw_nb_NO.ts
Normal file
4931
i18n/nw_nb_NO.ts
Normal file
File diff suppressed because it is too large
Load Diff
4931
i18n/nw_pt.ts
Normal file
4931
i18n/nw_pt.ts
Normal file
File diff suppressed because it is too large
Load Diff
74
i18n/qtbase.py
Normal file
74
i18n/qtbase.py
Normal file
@@ -0,0 +1,74 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Qt Base Translation File
|
||||
========================
|
||||
|
||||
This file causes Qt Linguist to generate translation entries for the Qt
|
||||
elements that need translation in novelWriter for those languages who do
|
||||
not yet have a qtbase_xx.qm file shipped with Qt.
|
||||
|
||||
If a qtbase_xx.qm file already exists, do not add a translation for the
|
||||
entries generated from this file.
|
||||
"""
|
||||
|
||||
from PyQt5.QtCore import QT_TRANSLATE_NOOP
|
||||
|
||||
# QDialogButtonBox
|
||||
# ================
|
||||
|
||||
QT_TRANSLATE_NOOP("QDialogButtonBox", "OK")
|
||||
|
||||
# QGnomeTheme
|
||||
# ===========
|
||||
|
||||
QT_TRANSLATE_NOOP("QGnomeTheme", "&OK")
|
||||
QT_TRANSLATE_NOOP("QGnomeTheme", "&Save")
|
||||
QT_TRANSLATE_NOOP("QGnomeTheme", "&Cancel")
|
||||
QT_TRANSLATE_NOOP("QGnomeTheme", "&Close")
|
||||
QT_TRANSLATE_NOOP("QGnomeTheme", "Close without Saving")
|
||||
|
||||
# QGuiApplication
|
||||
# ===============
|
||||
|
||||
QT_TRANSLATE_NOOP("QGuiApplication", (
|
||||
"Translate this string to the string 'LTR' in left-to-right languages or to "
|
||||
"'RTL' in right-to-left languages (such as Hebrew and Arabic) to get proper widget layout."
|
||||
))
|
||||
|
||||
# QPlatformTheme
|
||||
# ==============
|
||||
|
||||
QT_TRANSLATE_NOOP("QPlatformTheme", "OK")
|
||||
QT_TRANSLATE_NOOP("QPlatformTheme", "Save")
|
||||
QT_TRANSLATE_NOOP("QPlatformTheme", "Save All")
|
||||
QT_TRANSLATE_NOOP("QPlatformTheme", "Open")
|
||||
QT_TRANSLATE_NOOP("QPlatformTheme", "&Yes")
|
||||
QT_TRANSLATE_NOOP("QPlatformTheme", "Yes to &All")
|
||||
QT_TRANSLATE_NOOP("QPlatformTheme", "&No")
|
||||
QT_TRANSLATE_NOOP("QPlatformTheme", "N&o to All")
|
||||
QT_TRANSLATE_NOOP("QPlatformTheme", "Abort")
|
||||
QT_TRANSLATE_NOOP("QPlatformTheme", "Retry")
|
||||
QT_TRANSLATE_NOOP("QPlatformTheme", "Ignore")
|
||||
QT_TRANSLATE_NOOP("QPlatformTheme", "Close")
|
||||
QT_TRANSLATE_NOOP("QPlatformTheme", "Cancel")
|
||||
QT_TRANSLATE_NOOP("QPlatformTheme", "Discard")
|
||||
QT_TRANSLATE_NOOP("QPlatformTheme", "Help")
|
||||
QT_TRANSLATE_NOOP("QPlatformTheme", "Apply")
|
||||
QT_TRANSLATE_NOOP("QPlatformTheme", "Reset")
|
||||
QT_TRANSLATE_NOOP("QPlatformTheme", "Restore Defaults")
|
||||
|
||||
# QWizard
|
||||
# =======
|
||||
|
||||
QT_TRANSLATE_NOOP("QWizard", "Go Back")
|
||||
QT_TRANSLATE_NOOP("QWizard", "< &Back")
|
||||
QT_TRANSLATE_NOOP("QWizard", "Continue")
|
||||
QT_TRANSLATE_NOOP("QWizard", "&Next")
|
||||
QT_TRANSLATE_NOOP("QWizard", "&Next >")
|
||||
QT_TRANSLATE_NOOP("QWizard", "Commit")
|
||||
QT_TRANSLATE_NOOP("QWizard", "Done")
|
||||
QT_TRANSLATE_NOOP("QWizard", "&Finish")
|
||||
QT_TRANSLATE_NOOP("QWizard", "Cancel")
|
||||
QT_TRANSLATE_NOOP("QWizard", "Cancel")
|
||||
QT_TRANSLATE_NOOP("QWizard", "Help")
|
||||
QT_TRANSLATE_NOOP("QWizard", "&Help")
|
||||
38
novelWriter.pro
Normal file
38
novelWriter.pro
Normal file
@@ -0,0 +1,38 @@
|
||||
SOURCES += \
|
||||
i18n/qtbase.py \
|
||||
nw/core/document.py \
|
||||
nw/core/project.py \
|
||||
nw/core/tokenizer.py \
|
||||
nw/dialogs/about.py \
|
||||
nw/dialogs/docmerge.py \
|
||||
nw/dialogs/docsplit.py \
|
||||
nw/dialogs/itemeditor.py \
|
||||
nw/dialogs/preferences.py \
|
||||
nw/dialogs/projload.py \
|
||||
nw/dialogs/projsettings.py \
|
||||
nw/dialogs/wordlist.py \
|
||||
nw/gui/custom.py \
|
||||
nw/gui/doceditor.py \
|
||||
nw/gui/docviewer.py \
|
||||
nw/gui/itemdetails.py \
|
||||
nw/gui/mainmenu.py \
|
||||
nw/gui/noveltree.py \
|
||||
nw/gui/outline.py \
|
||||
nw/gui/outlinedetails.py \
|
||||
nw/gui/projdetails.py \
|
||||
nw/gui/projtree.py \
|
||||
nw/gui/statusbar.py \
|
||||
nw/gui/theme.py \
|
||||
nw/tools/build.py \
|
||||
nw/tools/projwizard.py \
|
||||
nw/tools/writingstats.py \
|
||||
nw/common.py \
|
||||
nw/constants.py \
|
||||
nw/error.py \
|
||||
nw/guimain.py
|
||||
|
||||
TRANSLATIONS += \
|
||||
i18n/nw_en_US.ts \
|
||||
i18n/nw_fr.ts \
|
||||
i18n/nw_nb_NO.ts \
|
||||
i18n/nw_pt.ts
|
||||
@@ -62,9 +62,9 @@ __license__ = "GPLv3"
|
||||
__author__ = "Veronica Berglyd Olsen"
|
||||
__maintainer__ = "Veronica Berglyd Olsen"
|
||||
__email__ = "code@vkbo.net"
|
||||
__version__ = "1.2"
|
||||
__hexversion__ = "0x010200f0"
|
||||
__date__ = "2021-03-14"
|
||||
__version__ = "1.3"
|
||||
__hexversion__ = "0x010300f0"
|
||||
__date__ = "2021-05-02"
|
||||
__status__ = "Stable"
|
||||
__domain__ = "novelwriter.io"
|
||||
__url__ = "https://novelwriter.io"
|
||||
@@ -73,10 +73,6 @@ __issuesurl__ = "https://github.com/vkbo/novelWriter/issues"
|
||||
__helpurl__ = "https://github.com/vkbo/novelWriter/discussions"
|
||||
__releaseurl__ = "https://github.com/vkbo/novelWriter/releases/latest"
|
||||
__docurl__ = "https://novelwriter.readthedocs.io"
|
||||
__credits__ = [
|
||||
"Veronica Berglyd Olsen (developer)",
|
||||
"Marian Lückhof (contributor, tester)"
|
||||
]
|
||||
|
||||
##
|
||||
# Logging
|
||||
@@ -137,7 +133,7 @@ def main(sysArgs=None):
|
||||
"This program is distributed in the hope that it will be useful,\n"
|
||||
"but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
|
||||
"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n"
|
||||
"GNU General Public License for more details.\n"
|
||||
"GNU General Public Licence for more details.\n"
|
||||
"\n"
|
||||
"Usage:\n"
|
||||
" -h, --help Print this message.\n"
|
||||
@@ -221,14 +217,14 @@ def main(sysArgs=None):
|
||||
"At least Python 3.6.0 is required, found %s." % CONFIG.verPyString
|
||||
)
|
||||
errorCode |= 4
|
||||
if CONFIG.verQtValue < 50200:
|
||||
if CONFIG.verQtValue < 50300:
|
||||
errorData.append(
|
||||
"At least Qt5 version 5.2 is required, found %s." % CONFIG.verQtString
|
||||
"At least Qt5 version 5.3 is required, found %s." % CONFIG.verQtString
|
||||
)
|
||||
errorCode |= 8
|
||||
if CONFIG.verPyQtValue < 50200:
|
||||
if CONFIG.verPyQtValue < 50300:
|
||||
errorData.append(
|
||||
"At least PyQt5 version 5.2 is required, found %s." % CONFIG.verPyQtString
|
||||
"At least PyQt5 version 5.3 is required, found %s." % CONFIG.verPyQtString
|
||||
)
|
||||
errorCode |= 16
|
||||
|
||||
@@ -285,6 +281,7 @@ def main(sysArgs=None):
|
||||
sys.excepthook = exceptionHandler
|
||||
|
||||
# Launch main GUI
|
||||
CONFIG.initLocalisation(nwApp)
|
||||
nwGUI = GuiMain()
|
||||
if not nwGUI.hasProject:
|
||||
nwGUI.showProjectLoadDialog()
|
||||
|
||||
105
nw/assets/i18n/project_de.json
Normal file
105
nw/assets/i18n/project_de.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"Synopsis": "Synopsis",
|
||||
"Comment": "Kommentar",
|
||||
"Notes": "Notizen",
|
||||
"0": "Null",
|
||||
"1": "Eins",
|
||||
"2": "Zwei",
|
||||
"3": "Drei",
|
||||
"4": "Vier",
|
||||
"5": "Fünf",
|
||||
"6": "Sechs",
|
||||
"7": "Sieben",
|
||||
"8": "Acht",
|
||||
"9": "Neun",
|
||||
"10": "Zehn",
|
||||
"11": "Elf",
|
||||
"12": "Zwölf",
|
||||
"13": "Dreizehen",
|
||||
"14": "Vierzehn",
|
||||
"15": "Fünfzehn",
|
||||
"16": "Sechzehn",
|
||||
"17": "Siebzehn",
|
||||
"18": "Achtzehn",
|
||||
"19": "Neunzehn",
|
||||
"20": "Zwanzig",
|
||||
"21": "Einundzwanzig",
|
||||
"22": "Zweiundzwanzig",
|
||||
"23": "Dreiundzwanzig",
|
||||
"24": "Vierundzwanzig",
|
||||
"25": "Fünfundzwanzig",
|
||||
"26": "Sechsundzwanzig",
|
||||
"27": "Siebenundzwanzig",
|
||||
"28": "Achtundzwanzig",
|
||||
"29": "Neunundzwanzig",
|
||||
"30": "Dreißig",
|
||||
"31": "Einunddreißig",
|
||||
"32": "Zweiunddreißig",
|
||||
"33": "Dreiunddreißig",
|
||||
"34": "Vierunddreißig",
|
||||
"35": "Fünfunddreißig",
|
||||
"36": "Sechsunddreißig",
|
||||
"37": "Siebenunddreißig",
|
||||
"38": "Achtunddreißig",
|
||||
"39": "Neununddreißig",
|
||||
"40": "Vierzig",
|
||||
"41": "Einundvierzig",
|
||||
"42": "Zweiundvierzig",
|
||||
"43": "Dreiundvierzig",
|
||||
"44": "Vierundvierzig",
|
||||
"45": "Fünfundvierzig",
|
||||
"46": "Sechsundvierzig",
|
||||
"47": "Siebenundvierzig",
|
||||
"48": "Achtundvierzig",
|
||||
"49": "Neunundvierzig",
|
||||
"50": "Fünfzig",
|
||||
"51": "Einundfünfzig",
|
||||
"52": "Zweiundfünfzig",
|
||||
"53": "Dreiundfünfzig",
|
||||
"54": "Vierundfünfzig",
|
||||
"55": "Fünfundfünfzig",
|
||||
"56": "Sechsundfünfzig",
|
||||
"57": "Siebenundfünfzig",
|
||||
"58": "Achtundfünfzig",
|
||||
"59": "Neunundfünfzig",
|
||||
"60": "Sechzig",
|
||||
"61": "Einundsechzig",
|
||||
"62": "Zweiundsechzig",
|
||||
"63": "Dreiundsechzig",
|
||||
"64": "Vierundsechzig",
|
||||
"65": "Fünfundsechzig",
|
||||
"66": "Sechsundsechzig",
|
||||
"67": "Siebenundsechzig",
|
||||
"68": "Achtundsechzig",
|
||||
"69": "Neunundsechzig",
|
||||
"70": "Siebzig",
|
||||
"71": "Einundsiebzig",
|
||||
"72": "Zweiundsiebzig",
|
||||
"73": "Dreiundsiebzig",
|
||||
"74": "Vierundsiebzig",
|
||||
"75": "Fünfundsiebzig",
|
||||
"76": "Sechsundsiebzig",
|
||||
"77": "Siebenundsiebzig",
|
||||
"78": "Achtundsiebzig",
|
||||
"79": "Neunundsiebzig",
|
||||
"80": "Achtzig",
|
||||
"81": "Einundachtzig",
|
||||
"82": "Zweiundachtzig",
|
||||
"83": "Dreiundachtzig",
|
||||
"84": "Vierundachtzig",
|
||||
"85": "Fünfundachtzig",
|
||||
"86": "Sechsundachtzig",
|
||||
"87": "Siebenundachtzig",
|
||||
"88": "Achtundachtzig",
|
||||
"89": "Neunundachtzig",
|
||||
"90": "Neunzig",
|
||||
"91": "Einundneunzig",
|
||||
"92": "Zweiundneunzig",
|
||||
"93": "Dreiundneunzig",
|
||||
"94": "Vierundneunzig",
|
||||
"95": "Fünfundneunzig",
|
||||
"96": "Sechsundneunzig",
|
||||
"97": "Siebenundneunzig",
|
||||
"98": "Achtundneunzig",
|
||||
"99": "Neunundneunzig"
|
||||
}
|
||||
105
nw/assets/i18n/project_en.json
Normal file
105
nw/assets/i18n/project_en.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"Synopsis": "Synopsis",
|
||||
"Comment": "Comment",
|
||||
"Notes": "Notes",
|
||||
"0": "Zero",
|
||||
"1": "One",
|
||||
"2": "Two",
|
||||
"3": "Three",
|
||||
"4": "Four",
|
||||
"5": "Five",
|
||||
"6": "Six",
|
||||
"7": "Seven",
|
||||
"8": "Eight",
|
||||
"9": "Nine",
|
||||
"10": "Ten",
|
||||
"11": "Eleven",
|
||||
"12": "Twelve",
|
||||
"13": "Thirteen",
|
||||
"14": "Fourteen",
|
||||
"15": "Fifteen",
|
||||
"16": "Sixteen",
|
||||
"17": "Seventeen",
|
||||
"18": "Eighteen",
|
||||
"19": "Nineteen",
|
||||
"20": "Twenty",
|
||||
"21": "Twenty-One",
|
||||
"22": "Twenty-Two",
|
||||
"23": "Twenty-Three",
|
||||
"24": "Twenty-Four",
|
||||
"25": "Twenty-Five",
|
||||
"26": "Twenty-Six",
|
||||
"27": "Twenty-Seven",
|
||||
"28": "Twenty-Eight",
|
||||
"29": "Twenty-Nine",
|
||||
"30": "Thirty",
|
||||
"31": "Thirty-One",
|
||||
"32": "Thirty-Two",
|
||||
"33": "Thirty-Three",
|
||||
"34": "Thirty-Four",
|
||||
"35": "Thirty-Five",
|
||||
"36": "Thirty-Six",
|
||||
"37": "Thirty-Seven",
|
||||
"38": "Thirty-Eight",
|
||||
"39": "Thirty-Nine",
|
||||
"40": "Forty",
|
||||
"41": "Forty-One",
|
||||
"42": "Forty-Two",
|
||||
"43": "Forty-Three",
|
||||
"44": "Forty-Four",
|
||||
"45": "Forty-Five",
|
||||
"46": "Forty-Six",
|
||||
"47": "Forty-Seven",
|
||||
"48": "Forty-Eight",
|
||||
"49": "Forty-Nine",
|
||||
"50": "Fifty",
|
||||
"51": "Fifty-One",
|
||||
"52": "Fifty-Two",
|
||||
"53": "Fifty-Three",
|
||||
"54": "Fifty-Four",
|
||||
"55": "Fifty-Five",
|
||||
"56": "Fifty-Six",
|
||||
"57": "Fifty-Seven",
|
||||
"58": "Fifty-Eight",
|
||||
"59": "Fifty-Nine",
|
||||
"60": "Sixty",
|
||||
"61": "Sixty-One",
|
||||
"62": "Sixty-Two",
|
||||
"63": "Sixty-Three",
|
||||
"64": "Sixty-Four",
|
||||
"65": "Sixty-Five",
|
||||
"66": "Sixty-Six",
|
||||
"67": "Sixty-Seven",
|
||||
"68": "Sixty-Eight",
|
||||
"69": "Sixty-Nine",
|
||||
"70": "Seventy",
|
||||
"71": "Seventy-One",
|
||||
"72": "Seventy-Two",
|
||||
"73": "Seventy-Three",
|
||||
"74": "Seventy-Four",
|
||||
"75": "Seventy-Five",
|
||||
"76": "Seventy-Six",
|
||||
"77": "Seventy-Seven",
|
||||
"78": "Seventy-Eight",
|
||||
"79": "Seventy-Nine",
|
||||
"80": "Eighty",
|
||||
"81": "Eighty-One",
|
||||
"82": "Eighty-Two",
|
||||
"83": "Eighty-Three",
|
||||
"84": "Eighty-Four",
|
||||
"85": "Eighty-Five",
|
||||
"86": "Eighty-Six",
|
||||
"87": "Eighty-Seven",
|
||||
"88": "Eighty-Eight",
|
||||
"89": "Eighty-Nine",
|
||||
"90": "Ninety",
|
||||
"91": "Ninety-One",
|
||||
"92": "Ninety-Two",
|
||||
"93": "Ninety-Three",
|
||||
"94": "Ninety-Four",
|
||||
"95": "Ninety-Five",
|
||||
"96": "Ninety-Six",
|
||||
"97": "Ninety-Seven",
|
||||
"98": "Ninety-Eight",
|
||||
"99": "Ninety-Nine"
|
||||
}
|
||||
105
nw/assets/i18n/project_fr.json
Normal file
105
nw/assets/i18n/project_fr.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"Synopsis": "Synopsis",
|
||||
"Comment": "Commentaire",
|
||||
"Notes": "Notes",
|
||||
"0": "Zero",
|
||||
"1": "Un",
|
||||
"2": "Deux",
|
||||
"3": "Trois",
|
||||
"4": "Quatre",
|
||||
"5": "Cinq",
|
||||
"6": "Six",
|
||||
"7": "Sept",
|
||||
"8": "Huit",
|
||||
"9": "Neuf",
|
||||
"10": "Dix",
|
||||
"11": "Onze",
|
||||
"12": "Douze",
|
||||
"13": "Treize",
|
||||
"14": "Quatorze",
|
||||
"15": "Quinze",
|
||||
"16": "Seize",
|
||||
"17": "Dix-sept",
|
||||
"18": "Dix-huit",
|
||||
"19": "Dix-neuf",
|
||||
"20": "Vingt",
|
||||
"21": "Vingt et un",
|
||||
"22": "Vingt-deux",
|
||||
"23": "Vingt-trois",
|
||||
"24": "Vingt-quatre",
|
||||
"25": "Vingt-cinq",
|
||||
"26": "Vingt-six",
|
||||
"27": "Vingt-sept",
|
||||
"28": "Vingt-huit",
|
||||
"29": "Vingt-neuf",
|
||||
"30": "Trente",
|
||||
"31": "Trente et un",
|
||||
"32": "Trente-deux",
|
||||
"33": "Trente-trois",
|
||||
"34": "Trente-quatre",
|
||||
"35": "Trente-cinq",
|
||||
"36": "Trente-six",
|
||||
"37": "Trente-sept",
|
||||
"38": "Trente-huit",
|
||||
"39": "Trente-neuf",
|
||||
"40": "Quarante",
|
||||
"41": "Quarante et un",
|
||||
"42": "Quarante-deux",
|
||||
"43": "Quarante-trois",
|
||||
"44": "Quarante-quatre",
|
||||
"45": "Quarante-cinq",
|
||||
"46": "Quarante-six",
|
||||
"47": "Quarante-sept",
|
||||
"48": "Quarante-huit",
|
||||
"49": "Quarante-neuf",
|
||||
"50": "Cinquante",
|
||||
"51": "Cinquante et un",
|
||||
"52": "Cinquante-deux",
|
||||
"53": "Cinquante-trois",
|
||||
"54": "Cinquante-quatre",
|
||||
"55": "Cinquante-cinq",
|
||||
"56": "Cinquante-six",
|
||||
"57": "Cinquante-sept",
|
||||
"58": "Cinquante-huit",
|
||||
"59": "Cinquante-neuf",
|
||||
"60": "Soixante",
|
||||
"61": "Soixante et un",
|
||||
"62": "Soixante-deux",
|
||||
"63": "Soixante-trois",
|
||||
"64": "Soixante-quatre",
|
||||
"65": "Soixante-cinq",
|
||||
"66": "Soixante-six",
|
||||
"67": "Soixante-sept",
|
||||
"68": "Soixante-huit",
|
||||
"69": "Soixante-neuf",
|
||||
"70": "Soixante-dix",
|
||||
"71": "Soixante et onze",
|
||||
"72": "Soixante-douze",
|
||||
"73": "Soixante-treize",
|
||||
"74": "Soixante-quatorze",
|
||||
"75": "Soixante-quinze",
|
||||
"76": "Soixante-seize",
|
||||
"77": "Soixante-dix-sept",
|
||||
"78": "Soixante-dix-huit",
|
||||
"79": "Soixante-dix-neuf",
|
||||
"80": "Quatre-vingts",
|
||||
"81": "Quatre-vingt-un",
|
||||
"82": "Quatre-vingt-deux",
|
||||
"83": "Quatre-vingt-trois",
|
||||
"84": "Quatre-vingt-quatre",
|
||||
"85": "Quatre-vingt-cinq",
|
||||
"86": "Quatre-vingt-six",
|
||||
"87": "Quatre-vingt-sept",
|
||||
"88": "Quatre-vingt-huit",
|
||||
"89": "Quatre-vingt-neuf",
|
||||
"90": "Quatre-vingt-dix",
|
||||
"91": "Quatre-vingt-onze",
|
||||
"92": "Quatre-vingt-douze",
|
||||
"93": "Quatre-vingt-treize",
|
||||
"94": "Quatre-vingt-quatorze",
|
||||
"95": "Quatre-vingt-quinze",
|
||||
"96": "Quatre-vingt-seize",
|
||||
"97": "Quatre-vingt-dix-sept",
|
||||
"98": "Quatre-vingt-dix-huit",
|
||||
"99": "Quatre-vingt-dix-neuf"
|
||||
}
|
||||
105
nw/assets/i18n/project_nb_NO.json
Normal file
105
nw/assets/i18n/project_nb_NO.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"Synopsis": "Sammendrag",
|
||||
"Comment": "Kommentar",
|
||||
"Notes": "Notat",
|
||||
"0": "null",
|
||||
"1": "én",
|
||||
"2": "to",
|
||||
"3": "tre",
|
||||
"4": "fire",
|
||||
"5": "fem",
|
||||
"6": "seks",
|
||||
"7": "sju",
|
||||
"8": "åtte",
|
||||
"9": "ni",
|
||||
"10": "ti",
|
||||
"11": "elleve",
|
||||
"12": "tolv",
|
||||
"13": "tretten",
|
||||
"14": "fjorten",
|
||||
"15": "femten",
|
||||
"16": "seksten",
|
||||
"17": "sytten",
|
||||
"18": "atten",
|
||||
"19": "nitten",
|
||||
"20": "tjue",
|
||||
"21": "tjueén",
|
||||
"22": "tjueto",
|
||||
"23": "tjuetre",
|
||||
"24": "tjuefire",
|
||||
"25": "tjuefem",
|
||||
"26": "tjueseks",
|
||||
"27": "tjuesju",
|
||||
"28": "tjueåtte",
|
||||
"29": "tjueni",
|
||||
"30": "tretti",
|
||||
"31": "trettién",
|
||||
"32": "trettito",
|
||||
"33": "trettitre",
|
||||
"34": "trettifire",
|
||||
"35": "trettifem",
|
||||
"36": "trettiseks",
|
||||
"37": "trettisju",
|
||||
"38": "trettiåtte",
|
||||
"39": "trettini",
|
||||
"40": "førti",
|
||||
"41": "førtién",
|
||||
"42": "førtito",
|
||||
"43": "førtitre",
|
||||
"44": "førtifire",
|
||||
"45": "førtifem",
|
||||
"46": "førtiseks",
|
||||
"47": "førtisju",
|
||||
"48": "førtiåtte",
|
||||
"49": "førtini",
|
||||
"50": "femti",
|
||||
"51": "femtién",
|
||||
"52": "femtito",
|
||||
"53": "femtitre",
|
||||
"54": "femtifire",
|
||||
"55": "femtifem",
|
||||
"56": "femtiseks",
|
||||
"57": "femtisju",
|
||||
"58": "femtiåtte",
|
||||
"59": "femtini",
|
||||
"60": "seksti",
|
||||
"61": "sekstién",
|
||||
"62": "sekstito",
|
||||
"63": "sekstitre",
|
||||
"64": "sekstifire",
|
||||
"65": "sekstifem",
|
||||
"66": "sekstiseks",
|
||||
"67": "sekstisju",
|
||||
"68": "sekstiåtte",
|
||||
"69": "sekstini",
|
||||
"70": "sytti",
|
||||
"71": "syttién",
|
||||
"72": "syttito",
|
||||
"73": "syttitre",
|
||||
"74": "syttifire",
|
||||
"75": "syttifem",
|
||||
"76": "syttiseks",
|
||||
"77": "syttisju",
|
||||
"78": "syttiåtte",
|
||||
"79": "syttini",
|
||||
"80": "åtti",
|
||||
"81": "åttién",
|
||||
"82": "åttito",
|
||||
"83": "åttitre",
|
||||
"84": "åttifire",
|
||||
"85": "åttifem",
|
||||
"86": "åttiseks",
|
||||
"87": "åttisju",
|
||||
"88": "åttiåtte",
|
||||
"89": "åttini",
|
||||
"90": "nitti",
|
||||
"91": "nittién",
|
||||
"92": "nittito",
|
||||
"93": "nittitre",
|
||||
"94": "nittifire",
|
||||
"95": "nittifem",
|
||||
"96": "nittiseks",
|
||||
"97": "nittisju",
|
||||
"98": "nittiåtte",
|
||||
"99": "nittini"
|
||||
}
|
||||
105
nw/assets/i18n/project_nn_NO.json
Normal file
105
nw/assets/i18n/project_nn_NO.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"Synopsis": "Samandrag",
|
||||
"Comment": "Kommentar",
|
||||
"Notes": "Notat",
|
||||
"0": "null",
|
||||
"1": "ein",
|
||||
"2": "to",
|
||||
"3": "tre",
|
||||
"4": "fire",
|
||||
"5": "fem",
|
||||
"6": "seks",
|
||||
"7": "sju",
|
||||
"8": "åtte",
|
||||
"9": "ni",
|
||||
"10": "ti",
|
||||
"11": "elleve",
|
||||
"12": "tolv",
|
||||
"13": "tretten",
|
||||
"14": "fjorten",
|
||||
"15": "femten",
|
||||
"16": "seksten",
|
||||
"17": "sytten",
|
||||
"18": "atten",
|
||||
"19": "nitten",
|
||||
"20": "tjue",
|
||||
"21": "tjueein",
|
||||
"22": "tjueto",
|
||||
"23": "tjuetre",
|
||||
"24": "tjuefire",
|
||||
"25": "tjuefem",
|
||||
"26": "tjueseks",
|
||||
"27": "tjuesju",
|
||||
"28": "tjueåtte",
|
||||
"29": "tjueni",
|
||||
"30": "tretti",
|
||||
"31": "trettiein",
|
||||
"32": "trettito",
|
||||
"33": "trettitre",
|
||||
"34": "trettifire",
|
||||
"35": "trettifem",
|
||||
"36": "trettiseks",
|
||||
"37": "trettisju",
|
||||
"38": "trettiåtte",
|
||||
"39": "trettini",
|
||||
"40": "førti",
|
||||
"41": "førtiein",
|
||||
"42": "førtito",
|
||||
"43": "førtitre",
|
||||
"44": "førtifire",
|
||||
"45": "førtifem",
|
||||
"46": "førtiseks",
|
||||
"47": "førtisju",
|
||||
"48": "førtiåtte",
|
||||
"49": "førtini",
|
||||
"50": "femti",
|
||||
"51": "femtiein",
|
||||
"52": "femtito",
|
||||
"53": "femtitre",
|
||||
"54": "femtifire",
|
||||
"55": "femtifem",
|
||||
"56": "femtiseks",
|
||||
"57": "femtisju",
|
||||
"58": "femtiåtte",
|
||||
"59": "femtini",
|
||||
"60": "seksti",
|
||||
"61": "sekstiein",
|
||||
"62": "sekstito",
|
||||
"63": "sekstitre",
|
||||
"64": "sekstifire",
|
||||
"65": "sekstifem",
|
||||
"66": "sekstiseks",
|
||||
"67": "sekstisju",
|
||||
"68": "sekstiåtte",
|
||||
"69": "sekstini",
|
||||
"70": "sytti",
|
||||
"71": "syttiein",
|
||||
"72": "syttito",
|
||||
"73": "syttitre",
|
||||
"74": "syttifire",
|
||||
"75": "syttifem",
|
||||
"76": "syttiseks",
|
||||
"77": "syttisju",
|
||||
"78": "syttiåtte",
|
||||
"79": "syttini",
|
||||
"80": "åtti",
|
||||
"81": "åttiein",
|
||||
"82": "åttito",
|
||||
"83": "åttitre",
|
||||
"84": "åttifire",
|
||||
"85": "åttifem",
|
||||
"86": "åttiseks",
|
||||
"87": "åttisju",
|
||||
"88": "åttiåtte",
|
||||
"89": "åttini",
|
||||
"90": "nitti",
|
||||
"91": "nittiein",
|
||||
"92": "nittito",
|
||||
"93": "nittitre",
|
||||
"94": "nittifire",
|
||||
"95": "nittifem",
|
||||
"96": "nittiseks",
|
||||
"97": "nittisju",
|
||||
"98": "nittiåtte",
|
||||
"99": "nittini"
|
||||
}
|
||||
105
nw/assets/i18n/project_pt.json
Normal file
105
nw/assets/i18n/project_pt.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"Synopsis": "Sinopse",
|
||||
"Comment": "Comentários",
|
||||
"Notes": "Notas",
|
||||
"0": "Zero",
|
||||
"1": "Um",
|
||||
"2": "Dois",
|
||||
"3": "Três",
|
||||
"4": "Quatro",
|
||||
"5": "Cinco",
|
||||
"6": "Seis",
|
||||
"7": "Sete",
|
||||
"8": "Oito",
|
||||
"9": "Nove",
|
||||
"10": "Dez",
|
||||
"11": "Onze",
|
||||
"12": "Doze",
|
||||
"13": "Treze",
|
||||
"14": "Quatorze",
|
||||
"15": "Quinze",
|
||||
"16": "Dezesseis",
|
||||
"17": "Dezesete",
|
||||
"18": "Dezoito",
|
||||
"19": "Dezenove",
|
||||
"20": "Vinte",
|
||||
"21": "Vinte e Um",
|
||||
"22": "Vinte e Dois",
|
||||
"23": "Vinte e Três",
|
||||
"24": "Vinte e Quatro",
|
||||
"25": "Vinte e Cinco",
|
||||
"26": "Vinte e Seis",
|
||||
"27": "Vinte e Sete",
|
||||
"28": "Vinte e Oito",
|
||||
"29": "Vinte e Nove",
|
||||
"30": "Trinta",
|
||||
"31": "Trinta e Um",
|
||||
"32": "Trinta e Dois",
|
||||
"33": "Trinta e Três",
|
||||
"34": "Trinta e Quatro",
|
||||
"35": "Trinta e Cinco",
|
||||
"36": "Trinta e Seis",
|
||||
"37": "Trinta e Sete",
|
||||
"38": "Trinta e Oito",
|
||||
"39": "Trinta e Nove",
|
||||
"40": "Quarenta",
|
||||
"41": "Quarenta e Um",
|
||||
"42": "Quarenta e Dois",
|
||||
"43": "Quarenta e Três",
|
||||
"44": "Quarenta e Quatro",
|
||||
"45": "Quarenta e Cinco",
|
||||
"46": "Quarenta e Seis",
|
||||
"47": "Quarenta e Sete",
|
||||
"48": "Quarenta e Oito",
|
||||
"49": "Quarenta e Nove",
|
||||
"50": "Cinquenta",
|
||||
"51": "Cinquenta e Um",
|
||||
"52": "Cinquenta e Dois",
|
||||
"53": "Cinquenta e Três",
|
||||
"54": "Cinquenta e Quatro",
|
||||
"55": "Cinquenta e Cinco",
|
||||
"56": "Cinquenta e Seis",
|
||||
"57": "Cinquenta e Sete",
|
||||
"58": "Cinquenta e Oito",
|
||||
"59": "Cinquenta e Nove",
|
||||
"60": "Sessenta",
|
||||
"61": "Sessenta e Um",
|
||||
"62": "Sessenta e Dois",
|
||||
"63": "Sessenta e Três",
|
||||
"64": "Sessenta e Quatro",
|
||||
"65": "Sessenta e Cinco",
|
||||
"66": "Sessenta e Seis",
|
||||
"67": "Sessenta e Sete",
|
||||
"68": "Sessenta e Oito",
|
||||
"69": "Sessenta e Nove",
|
||||
"70": "Setenta",
|
||||
"71": "Setenta e Um",
|
||||
"72": "Setenta e Dois",
|
||||
"73": "Setenta e Três",
|
||||
"74": "Setenta e Quatro",
|
||||
"75": "Setenta e Cinco",
|
||||
"76": "Setenta e Seis",
|
||||
"77": "Setenta e Sete",
|
||||
"78": "Setenta e Oito",
|
||||
"79": "Setenta e Nove",
|
||||
"80": "Oitenta",
|
||||
"81": "Oitenta e Um",
|
||||
"82": "Oitenta e Dois",
|
||||
"83": "Oitenta e Três",
|
||||
"84": "Oitenta e Quatro",
|
||||
"85": "Oitenta e Cinco",
|
||||
"86": "Oitenta e Seis",
|
||||
"87": "Oitenta e Sete",
|
||||
"88": "Oitenta e Oito",
|
||||
"89": "Oitenta e Nove",
|
||||
"90": "Noventa",
|
||||
"91": "Noventa e Um",
|
||||
"92": "Noventa e Dois",
|
||||
"93": "Noventa e Três",
|
||||
"94": "Noventa e Quatro",
|
||||
"95": "Noventa e Cinco",
|
||||
"96": "Noventa e Seis",
|
||||
"97": "Noventa e Sete",
|
||||
"98": "Noventa e Oito",
|
||||
"99": "Noventa e Nove"
|
||||
}
|
||||
@@ -2,82 +2,27 @@
|
||||
<html>
|
||||
<body>
|
||||
|
||||
<h2>Release Notes for 1.2</h2>
|
||||
<p><i>Released on 14 March 2021</i></p>
|
||||
<p>This release is mainly focused on the Build Novel Project tool. Completely new export classes
|
||||
have been written to support Open Document and Markdown exports. In addition, the way document
|
||||
layouts are handled have been automated a little to assist the user in keeping header levels and
|
||||
document layout flags in sync. The third new additoion is the ability to record and log idle time
|
||||
during a writing session to improve the writing statistics information as requested by several
|
||||
users. Finally, it is now possible to directly edit the project dictionary via a new, simple GUI
|
||||
dialog.</p>
|
||||
|
||||
<h3>The Build Novel Project Tool</h3>
|
||||
<p>The main changes for this release are to the Build Novel Project tool. The Open Document export,
|
||||
as well as the Markdown export, is now handled entirely by code written for novelWriter.
|
||||
Previously, these export features depended on the underlying Qt library's save routines connected
|
||||
to the preview document shown in the build dialog. Using this method of export both meant that the
|
||||
content of the document was dependent on the preview being generated first, and it also meant that
|
||||
the exported document had limited support for novelWriter-specific features and custom formatting.
|
||||
The new export class should generate a much better result, especially for the Open Document
|
||||
formats. The Open Document standard is supported by Open Office, Libre Office, Google Docs,
|
||||
Microsoft Word, and probably a number of other applications too. The Markdown export hasn't changed
|
||||
a lot, but should be a slight improvement on the previous export feature.</p>
|
||||
<p>These changes to the build tool also imply that the saving process is now independent of the
|
||||
content of the preview window, meaning you don't have to rebuild the preview before saving, which
|
||||
was previously the case. To make this more consistent, the PDF export option has been moved to the
|
||||
print button as it is actually a print-to-file feature under the hood, not technically a proper PDF
|
||||
export format. It is exactly the same as printing to file from the print preview dialog.</p>
|
||||
<p>In addition to the changes to the export features, the Build Novel Project tool now also has
|
||||
controls for line height, which applies to all rich text export formats, and the option to replace
|
||||
unicode characters with appropriate html entities for html export.</p>
|
||||
|
||||
<h3>Document Layout Automation</h3>
|
||||
<p>Among other changes in this release are a few improvements to the process of creating and
|
||||
changing documents. When a new document is first created, the header is generated from the assigned
|
||||
label and layout.</p>
|
||||
<p>In addition, for some document layouts, when the user changes the header level of the first
|
||||
header of the document, the document layout setting is updated accordingly. This should reduce the
|
||||
need for the user to maintain two ways of assigning the role of a given document. This automation
|
||||
only applies to combinations of header level and current document layouts where there is no
|
||||
ambiguity. For instance, changing the header level in a "Scene" document from level 3 to 2 changes
|
||||
the document layout automatically to "Chapter". But changing the first header of a "Book" layout
|
||||
document from 1 to 2 does not change the document's layout as the "Book" layout is a generic
|
||||
document layout and it's perfectly reasonable for its first header to be a chapter header.</p>
|
||||
<p>Keep in mind that novelWriter treats documents with layout "Book", "Chapter" and "Scene" exactly
|
||||
the same during exports. The distinction is only meant as a way to indicate the purpose of a
|
||||
document in the project tree. This new automation is meant to assist in keeping this information up
|
||||
to date. The other layouts do have an effect on formatting during export, and are generally left
|
||||
alone.</p>
|
||||
|
||||
<h3>The Session Timer and Idle Time</h3>
|
||||
<p>Another change that has been requested by a couple of users is to have the session timer in the
|
||||
status bar stop counting when the user is inactive (idle). This feature is optional, and can be
|
||||
controlled from Preferences. The definition of "idle" in this context is either that the user is
|
||||
active in a different application than novelWriter (loss of focus) or that the user has not made
|
||||
any changes to the current document for a given amount of time. The time threshold is by default
|
||||
five minutes, but can be altered in Preferences.</p>
|
||||
<p>In addition, the idle time is also recorded in the session log, and can be viewed in the Writing
|
||||
Statistics dialog and exported with the rest of the information. The idle time is recorder in the
|
||||
logs regardless of whether the status bar clock takes idle time into consideration or not. So even
|
||||
if you turn off the idle time switch in Preferences, the other idle time setting still affects the
|
||||
writing stats log entries.</p>
|
||||
|
||||
<h3>Other Changes</h3>
|
||||
<p>The user dictionary of the project, where words added to the dictionary from the document editor
|
||||
go, can now be viewed and edited with a new "Project Word List" tool in the "Tools" menu.</p>
|
||||
<p>A small additional feature added is also the ability to undo the last move of an item in the
|
||||
project tree. The keyboard shortcut for this is `Ctrl+Shift+Z`, or it can be accessed from the
|
||||
menu. The feature can only undo the last move, but it includes both documents moved to trash, moves
|
||||
by up/down keypress or menu entries, and drag and drop moves.</p>
|
||||
<p>A new keyword has been added to mark characters in the story. The new keyword is intended to tag
|
||||
a character as the focus character for a chapter or scene. This is useful for stories where the
|
||||
point-of-view character and the focus character are different.</p>
|
||||
<p>Lastly, two bugfixes have been made as well. The Empty Trash feature was no longer working due
|
||||
to an earlier fix solving another issue. The feature has now been restored. In addition, the
|
||||
indexer now checks that a keyword (tag or reference) is valid before saving it to the index.
|
||||
Previously, an invalid keyword could be saved to the index and potentially crash the application.
|
||||
</p>
|
||||
<h2>Release Notes for 1.3</h2>
|
||||
<p><i>Released on 2 May 2021</i></p>
|
||||
<p>The main feature of the 1.3 release is internationalisation. The release introduces support for
|
||||
Portuguese, French, and Norwegian for the main GUI, in addition to the default English. The
|
||||
settings for choosing the GUI language can be found in the main Preferences.</p>
|
||||
<p>The implementation is such that the projects themselves can use a different language than the
|
||||
one selected for the GUI. To allow for better support for multiple language projects, the Build
|
||||
Novel Project tool has the added option to select the language of the injected text into exported
|
||||
documents. In particularly for the number words that can be added for chapter headers. Available
|
||||
languages here are the same as mentioned above for the main GUI, plus German.</p>
|
||||
<p>Settings have also been added in Preferences to allow for automatic injection of spaces or
|
||||
non-breaking spaces in front of, or behind, certain characters. These are features generally
|
||||
available in text editors for French, but the implementation in novelWriter simply makes them a
|
||||
free text box where you can list all characters or symbols where you want a space added
|
||||
automatically.</p>
|
||||
<p>The translation files for this release have been provided by Bruno Meneguello for the Portuguese
|
||||
translation, by Jan Lüdke (jyhelle) for the French translation, by Veronica Berglyd Olsen for the
|
||||
Norwegian translation as well as the minor modifications for US English, and Marian Lückhof
|
||||
provided the translation file for chapter numbers in German.</p>
|
||||
<p>The work to rewrite novelWriter to allow for internationalisation was done by Bruno Meneguello
|
||||
and Veronica Berglyd Olsen.</p>
|
||||
|
||||
<p><i>See also the <a href="https://github.com/vkbo/novelWriter/releases">Releases</a> page.</i></p>
|
||||
|
||||
|
||||
104
nw/assets/text/release_notes_1.2.htm
Normal file
104
nw/assets/text/release_notes_1.2.htm
Normal file
@@ -0,0 +1,104 @@
|
||||
<!DOCTYPE html public "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
|
||||
<html>
|
||||
<body>
|
||||
|
||||
|
||||
<h2>Release Notes for 1.2.2</h2>
|
||||
<p><i>Released on 28 March 2021</i></p>
|
||||
<p>This patch release is a bug fix release addressing some inconsistencies and issues with the
|
||||
document header buttons when Focus Mode is active. The keyboard shortcuts for search and replace
|
||||
should now also work in Focus Mode. In addition, the setup script for novelWriter has been improved
|
||||
when installing on Windows.</p>
|
||||
|
||||
<p><i>See also the <a href="https://github.com/vkbo/novelWriter/releases">Releases</a> page.</i></p>
|
||||
|
||||
<hr/>
|
||||
|
||||
<h2>Release Notes for 1.2.1</h2>
|
||||
<p><i>Released on 21 March 2021</i></p>
|
||||
<p>This patch release is a bug fix release addressing issues with the document editor's search and
|
||||
replace tool. Due to some recently added restrictions on when various tools are active, depending
|
||||
on which part of the main window has the user's focus, the search tool keyboard shortcuts and
|
||||
buttons were blocked when they shouldn't. This release resolves these issues.</p>
|
||||
|
||||
<hr/>
|
||||
|
||||
<h2>Release Notes for 1.2</h2>
|
||||
<p><i>Released on 14 March 2021</i></p>
|
||||
<p>This release is mainly focused on the Build Novel Project tool. Completely new export classes
|
||||
have been written to support Open Document and Markdown exports. In addition, the way document
|
||||
layouts are handled have been automated a little to assist the user in keeping header levels and
|
||||
document layout flags in sync. The third new additoion is the ability to record and log idle time
|
||||
during a writing session to improve the writing statistics information as requested by several
|
||||
users. Finally, it is now possible to directly edit the project dictionary via a new, simple GUI
|
||||
dialog.</p>
|
||||
|
||||
<h3>The Build Novel Project Tool</h3>
|
||||
<p>The main changes for this release are to the Build Novel Project tool. The Open Document export,
|
||||
as well as the Markdown export, is now handled entirely by code written for novelWriter.
|
||||
Previously, these export features depended on the underlying Qt library's save routines connected
|
||||
to the preview document shown in the build dialog. Using this method of export both meant that the
|
||||
content of the document was dependent on the preview being generated first, and it also meant that
|
||||
the exported document had limited support for novelWriter-specific features and custom formatting.
|
||||
The new export class should generate a much better result, especially for the Open Document
|
||||
formats. The Open Document standard is supported by Open Office, Libre Office, Google Docs,
|
||||
Microsoft Word, and probably a number of other applications too. The Markdown export hasn't changed
|
||||
a lot, but should be a slight improvement on the previous export feature.</p>
|
||||
<p>These changes to the build tool also imply that the saving process is now independent of the
|
||||
content of the preview window, meaning you don't have to rebuild the preview before saving, which
|
||||
was previously the case. To make this more consistent, the PDF export option has been moved to the
|
||||
print button as it is actually a print-to-file feature under the hood, not technically a proper PDF
|
||||
export format. It is exactly the same as printing to file from the print preview dialog.</p>
|
||||
<p>In addition to the changes to the export features, the Build Novel Project tool now also has
|
||||
controls for line height, which applies to all rich text export formats, and the option to replace
|
||||
unicode characters with appropriate html entities for html export.</p>
|
||||
|
||||
<h3>Document Layout Automation</h3>
|
||||
<p>Among other changes in this release are a few improvements to the process of creating and
|
||||
changing documents. When a new document is first created, the header is generated from the assigned
|
||||
label and layout.</p>
|
||||
<p>In addition, for some document layouts, when the user changes the header level of the first
|
||||
header of the document, the document layout setting is updated accordingly. This should reduce the
|
||||
need for the user to maintain two ways of assigning the role of a given document. This automation
|
||||
only applies to combinations of header level and current document layouts where there is no
|
||||
ambiguity. For instance, changing the header level in a "Scene" document from level 3 to 2 changes
|
||||
the document layout automatically to "Chapter". But changing the first header of a "Book" layout
|
||||
document from 1 to 2 does not change the document's layout as the "Book" layout is a generic
|
||||
document layout and it's perfectly reasonable for its first header to be a chapter header.</p>
|
||||
<p>Keep in mind that novelWriter treats documents with layout "Book", "Chapter" and "Scene" exactly
|
||||
the same during exports. The distinction is only meant as a way to indicate the purpose of a
|
||||
document in the project tree. This new automation is meant to assist in keeping this information up
|
||||
to date. The other layouts do have an effect on formatting during export, and are generally left
|
||||
alone.</p>
|
||||
|
||||
<h3>The Session Timer and Idle Time</h3>
|
||||
<p>Another change that has been requested by a couple of users is to have the session timer in the
|
||||
status bar stop counting when the user is inactive (idle). This feature is optional, and can be
|
||||
controlled from Preferences. The definition of "idle" in this context is either that the user is
|
||||
active in a different application than novelWriter (loss of focus) or that the user has not made
|
||||
any changes to the current document for a given amount of time. The time threshold is by default
|
||||
five minutes, but can be altered in Preferences.</p>
|
||||
<p>In addition, the idle time is also recorded in the session log, and can be viewed in the Writing
|
||||
Statistics dialog and exported with the rest of the information. The idle time is recorder in the
|
||||
logs regardless of whether the status bar clock takes idle time into consideration or not. So even
|
||||
if you turn off the idle time switch in Preferences, the other idle time setting still affects the
|
||||
writing stats log entries.</p>
|
||||
|
||||
<h3>Other Changes</h3>
|
||||
<p>The user dictionary of the project, where words added to the dictionary from the document editor
|
||||
go, can now be viewed and edited with a new "Project Word List" tool in the "Tools" menu.</p>
|
||||
<p>A small additional feature added is also the ability to undo the last move of an item in the
|
||||
project tree. The keyboard shortcut for this is `Ctrl+Shift+Z`, or it can be accessed from the
|
||||
menu. The feature can only undo the last move, but it includes both documents moved to trash, moves
|
||||
by up/down keypress or menu entries, and drag and drop moves.</p>
|
||||
<p>A new keyword has been added to mark characters in the story. The new keyword is intended to tag
|
||||
a character as the focus character for a chapter or scene. This is useful for stories where the
|
||||
point-of-view character and the focus character are different.</p>
|
||||
<p>Lastly, two bugfixes have been made as well. The Empty Trash feature was no longer working due
|
||||
to an earlier fix solving another issue. The feature has now been restored. In addition, the
|
||||
indexer now checks that a keyword (tag or reference) is valid before saving it to the index.
|
||||
Previously, an invalid keyword could be saved to the index and potentially crash the application.
|
||||
</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
116
nw/common.py
116
nw/common.py
@@ -29,10 +29,10 @@ import logging
|
||||
from datetime import datetime
|
||||
|
||||
from PyQt5.QtWidgets import qApp
|
||||
from PyQt5.QtCore import QCoreApplication
|
||||
|
||||
from nw.constants import (
|
||||
nwConst, nwUnicode, nwItemClass, nwItemType, nwItemLayout
|
||||
)
|
||||
from nw.enum import nwItemClass, nwItemType, nwItemLayout
|
||||
from nw.constants import nwConst, nwUnicode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -149,37 +149,6 @@ def hexToInt(value, default=0):
|
||||
return default
|
||||
return default
|
||||
|
||||
def colRange(rgbStart, rgbEnd, nStep):
|
||||
"""Generate a range of colours from one RGB value to another.
|
||||
"""
|
||||
if len(rgbStart) != 3 and len(rgbEnd) != 3 and nStep < 1:
|
||||
logger.error("Cannot create colour range from given parameters")
|
||||
return None
|
||||
|
||||
if nStep == 1:
|
||||
return rgbStart
|
||||
elif nStep == 2:
|
||||
return [rgbStart, rgbEnd]
|
||||
|
||||
dC = [0, 0, 0]
|
||||
for c in range(3):
|
||||
cA = rgbStart[c]
|
||||
cB = rgbEnd[c]
|
||||
dC[c] = (cB-cA)/(nStep-1)
|
||||
print(dC)
|
||||
retCol = [rgbStart]
|
||||
for n in range(nStep):
|
||||
if n > 0 and n < nStep:
|
||||
retCol.append([
|
||||
int(retCol[n-1][0] + dC[0]),
|
||||
int(retCol[n-1][1] + dC[1]),
|
||||
int(retCol[n-1][2] + dC[2]),
|
||||
])
|
||||
retCol[-1] = rgbEnd
|
||||
print(retCol)
|
||||
|
||||
return retCol
|
||||
|
||||
def formatInt(theInt):
|
||||
"""Formats an integer with k, M, G etc.
|
||||
"""
|
||||
@@ -267,33 +236,61 @@ def fuzzyTime(secDiff):
|
||||
"""Converts a time difference in seconds into a fuzzy time string.
|
||||
"""
|
||||
if secDiff < 0:
|
||||
return "in the future"
|
||||
return QCoreApplication.translate(
|
||||
"Common", "in the future"
|
||||
)
|
||||
elif secDiff < 30:
|
||||
return "just now"
|
||||
return QCoreApplication.translate(
|
||||
"Common", "just now"
|
||||
)
|
||||
elif secDiff < 90:
|
||||
return "a minute ago"
|
||||
return QCoreApplication.translate(
|
||||
"Common", "a minute ago"
|
||||
)
|
||||
elif secDiff < 3300: # 55 minutes
|
||||
return "%d minutes ago" % int(round(secDiff/60))
|
||||
return QCoreApplication.translate(
|
||||
"Common", "{0} minutes ago"
|
||||
).format(int(round(secDiff/60)))
|
||||
elif secDiff < 5400: # 90 minutes
|
||||
return "an hour ago"
|
||||
return QCoreApplication.translate(
|
||||
"Common", "an hour ago"
|
||||
)
|
||||
elif secDiff < 84600: # 23.5 hours
|
||||
return "%d hours ago" % int(round(secDiff/3600))
|
||||
return QCoreApplication.translate(
|
||||
"Common", "{0} hours ago"
|
||||
).format(int(round(secDiff/3600)))
|
||||
elif secDiff < 129600: # 1.5 days
|
||||
return "a day ago"
|
||||
return QCoreApplication.translate(
|
||||
"Common", "a day ago"
|
||||
)
|
||||
elif secDiff < 561600: # 6.5 days
|
||||
return "%d days ago" % int(round(secDiff/86400))
|
||||
return QCoreApplication.translate(
|
||||
"Common", "{0} days ago"
|
||||
).format(int(round(secDiff/86400)))
|
||||
elif secDiff < 907200: # 10.5 days
|
||||
return "a week ago"
|
||||
return QCoreApplication.translate(
|
||||
"Common", "a week ago"
|
||||
)
|
||||
elif secDiff < 2419200: # 28 days
|
||||
return "%d weeks ago" % int(round(secDiff/604800))
|
||||
return QCoreApplication.translate(
|
||||
"Common", "{0} weeks ago"
|
||||
).format(int(round(secDiff/604800)))
|
||||
elif secDiff < 3888000: # 45 days
|
||||
return "a month ago"
|
||||
return QCoreApplication.translate(
|
||||
"Common", "a month ago"
|
||||
)
|
||||
elif secDiff < 29808000: # 345 days
|
||||
return "%d months ago" % int(round(secDiff/2592000))
|
||||
return QCoreApplication.translate(
|
||||
"Common", "{0} months ago"
|
||||
).format(int(round(secDiff/2592000)))
|
||||
elif secDiff < 47336400: # 1.5 years
|
||||
return "a year ago"
|
||||
return QCoreApplication.translate(
|
||||
"Common", "a year ago"
|
||||
)
|
||||
else:
|
||||
return "%d years ago" % int(round(secDiff/31557600))
|
||||
return QCoreApplication.translate(
|
||||
"Common", "{0} years ago"
|
||||
).format(int(round(secDiff/31557600)))
|
||||
|
||||
def makeFileNameSafe(theText):
|
||||
"""Returns a filename safe version of the text.
|
||||
@@ -311,3 +308,26 @@ def getGuiItem(theName):
|
||||
if qWidget.objectName() == theName:
|
||||
return qWidget
|
||||
return None
|
||||
|
||||
def numberToRoman(numVal, isLower=False):
|
||||
"""Convert an integer to a roman number.
|
||||
"""
|
||||
if not isinstance(numVal, int):
|
||||
return "NAN"
|
||||
if numVal < 1 or numVal > 4999:
|
||||
return "OOR"
|
||||
|
||||
theValues = [
|
||||
(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"), (100, "C"), (90, "XC"),
|
||||
(50, "L"), (40, "XL"), (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"),
|
||||
]
|
||||
|
||||
romNum = ""
|
||||
for theDiv, theSym in theValues:
|
||||
n = numVal//theDiv
|
||||
romNum += n*theSym
|
||||
numVal -= n*theDiv
|
||||
if numVal <= 0:
|
||||
break
|
||||
|
||||
return romNum.lower() if isLower else romNum
|
||||
|
||||
113
nw/config.py
113
nw/config.py
@@ -34,11 +34,14 @@ import os
|
||||
from time import time
|
||||
|
||||
from PyQt5.Qt import PYQT_VERSION_STR
|
||||
from PyQt5.QtCore import QT_VERSION_STR, QStandardPaths, QSysInfo
|
||||
from PyQt5.QtCore import (
|
||||
QT_VERSION_STR, QStandardPaths, QSysInfo, QLocale, QLibraryInfo,
|
||||
QTranslator
|
||||
)
|
||||
|
||||
from nw.constants import nwConst, nwFiles, nwUnicode
|
||||
from nw.common import splitVersionNumber, formatTimeStamp
|
||||
from nw.error import logException
|
||||
from nw.common import splitVersionNumber, formatTimeStamp
|
||||
from nw.constants import nwConst, nwFiles, nwUnicode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -50,6 +53,9 @@ class Config:
|
||||
CNF_S_LST = 3
|
||||
CNF_I_LST = 4
|
||||
|
||||
LANG_NW = 1
|
||||
LANG_PROJ = 2
|
||||
|
||||
def __init__(self):
|
||||
|
||||
# Set Application Variables
|
||||
@@ -84,14 +90,21 @@ class Config:
|
||||
self.guiSyntax = "default_light"
|
||||
self.guiIcons = "typicons_colour_light"
|
||||
self.guiDark = False # Load icons for dark backgrounds, if available
|
||||
self.guiLang = "en" # Hardcoded for now since the GUI is only in English
|
||||
self.guiFont = "" # Defaults to system default font
|
||||
self.guiFontSize = 11
|
||||
self.guiFontSize = 11 # Is overridden if system default is loaded
|
||||
self.guiScale = 1.0 # Set automatically by Theme class
|
||||
self.lastNotes = "0x0" # The latest release notes that have been shown
|
||||
|
||||
## Localisation
|
||||
self.qLocal = QLocale.system()
|
||||
self.guiLang = self.qLocal.name()
|
||||
self.qtLangPath = QLibraryInfo.location(QLibraryInfo.TranslationsPath)
|
||||
self.nwLangPath = None
|
||||
self.qtTrans = {}
|
||||
|
||||
## Sizes
|
||||
self.winGeometry = [1200, 650]
|
||||
self.prefGeometry = [700, 615]
|
||||
self.treeColWidth = [200, 50, 30]
|
||||
self.novelColWidth = [200, 50]
|
||||
self.projColWidth = [200, 60, 140]
|
||||
@@ -151,6 +164,9 @@ class Config:
|
||||
self.fmtApostrophe = nwUnicode.U_RSQUO
|
||||
self.fmtSingleQuotes = [nwUnicode.U_LSQUO, nwUnicode.U_RSQUO]
|
||||
self.fmtDoubleQuotes = [nwUnicode.U_LDQUO, nwUnicode.U_RDQUO]
|
||||
self.fmtPadBefore = ""
|
||||
self.fmtPadAfter = ""
|
||||
self.fmtPadThin = False
|
||||
|
||||
## Spell Checking
|
||||
self.spellTool = None
|
||||
@@ -290,6 +306,9 @@ class Config:
|
||||
self.iconPath = os.path.join(self.assetPath, "icons")
|
||||
self.appIcon = os.path.join(self.iconPath, "novelwriter.svg")
|
||||
|
||||
# Internationalisation
|
||||
self.nwLangPath = os.path.join(self.assetPath, "i18n")
|
||||
|
||||
logger.verbose("App path: %s" % self.appPath)
|
||||
logger.verbose("Last path: %s" % self.lastPath)
|
||||
|
||||
@@ -354,6 +373,57 @@ class Config:
|
||||
|
||||
return True
|
||||
|
||||
def initLocalisation(self, nwApp):
|
||||
"""Initialise the localisation of the GUI.
|
||||
"""
|
||||
self.qLocal = QLocale(self.guiLang)
|
||||
QLocale.setDefault(self.qLocal)
|
||||
self.qtTrans = {}
|
||||
|
||||
langList = [
|
||||
(self.qtLangPath, "qtbase"), # Qt 5.x
|
||||
(self.nwLangPath, "qtbase"), # Alternative Qt 5.x
|
||||
(self.nwLangPath, "nw"), # novelWriter
|
||||
]
|
||||
for lngPath, lngBase in langList:
|
||||
for lngCode in self.qLocal.uiLanguages():
|
||||
qTrans = QTranslator()
|
||||
lngFile = "%s_%s" % (lngBase, lngCode.replace("-", "_"))
|
||||
if lngFile not in self.qtTrans:
|
||||
if qTrans.load(lngFile, lngPath):
|
||||
logger.debug("Loaded: %s" % qTrans.filePath())
|
||||
nwApp.installTranslator(qTrans)
|
||||
self.qtTrans[lngFile] = qTrans
|
||||
|
||||
return
|
||||
|
||||
def listLanguages(self, lngSet):
|
||||
"""List localisation files in the i18n folder. The default GUI
|
||||
language 'en_GB' is British English.
|
||||
"""
|
||||
if lngSet == self.LANG_NW:
|
||||
fPre = "nw_"
|
||||
fExt = ".qm"
|
||||
langList = {"en_GB": QLocale("en_GB").nativeLanguageName().title()}
|
||||
elif lngSet == self.LANG_PROJ:
|
||||
fPre = "project_"
|
||||
fExt = ".json"
|
||||
langList = {"en": "English"}
|
||||
else:
|
||||
return []
|
||||
|
||||
for qmFile in os.listdir(self.nwLangPath):
|
||||
if not os.path.isfile(os.path.join(self.nwLangPath, qmFile)):
|
||||
continue
|
||||
if not qmFile.startswith(fPre) or not qmFile.endswith(fExt):
|
||||
continue
|
||||
qmLang = qmFile[len(fPre):-len(fExt)]
|
||||
qmName = QLocale(qmLang).nativeLanguageName().title()
|
||||
if qmLang and qmName and qmLang != "en":
|
||||
langList[qmLang] = qmName
|
||||
|
||||
return sorted(langList.items(), key=lambda x: x[0])
|
||||
|
||||
def loadConfig(self):
|
||||
"""Load preferences from file and replace default settings.
|
||||
"""
|
||||
@@ -397,12 +467,18 @@ class Config:
|
||||
self.lastNotes = self._parseLine(
|
||||
cnfParse, cnfSec, "lastnotes", self.CNF_STR, self.lastNotes
|
||||
)
|
||||
self.guiLang = self._parseLine(
|
||||
cnfParse, cnfSec, "guilang", self.CNF_STR, self.guiLang
|
||||
)
|
||||
|
||||
## Sizes
|
||||
cnfSec = "Sizes"
|
||||
self.winGeometry = self._parseLine(
|
||||
cnfParse, cnfSec, "geometry", self.CNF_I_LST, self.winGeometry
|
||||
)
|
||||
self.prefGeometry = self._parseLine(
|
||||
cnfParse, cnfSec, "preferences", self.CNF_I_LST, self.prefGeometry
|
||||
)
|
||||
self.treeColWidth = self._parseLine(
|
||||
cnfParse, cnfSec, "treecols", self.CNF_I_LST, self.treeColWidth
|
||||
)
|
||||
@@ -505,6 +581,15 @@ class Config:
|
||||
self.fmtDoubleQuotes = self._parseLine(
|
||||
cnfParse, cnfSec, "fmtdoublequote", self.CNF_S_LST, self.fmtDoubleQuotes
|
||||
)
|
||||
self.fmtPadBefore = self._parseLine(
|
||||
cnfParse, cnfSec, "fmtpadbefore", self.CNF_STR, self.fmtPadBefore
|
||||
)
|
||||
self.fmtPadAfter = self._parseLine(
|
||||
cnfParse, cnfSec, "fmtpadafter", self.CNF_STR, self.fmtPadAfter
|
||||
)
|
||||
self.fmtPadThin = self._parseLine(
|
||||
cnfParse, cnfSec, "fmtpadthin", self.CNF_BOOL, self.fmtPadThin
|
||||
)
|
||||
self.spellTool = self._parseLine(
|
||||
cnfParse, cnfSec, "spelltool", self.CNF_STR, self.spellTool
|
||||
)
|
||||
@@ -626,11 +711,13 @@ class Config:
|
||||
cnfParse.set(cnfSec, "guifont", str(self.guiFont))
|
||||
cnfParse.set(cnfSec, "guifontsize", str(self.guiFontSize))
|
||||
cnfParse.set(cnfSec, "lastnotes", str(self.lastNotes))
|
||||
cnfParse.set(cnfSec, "guilang", str(self.guiLang))
|
||||
|
||||
## Sizes
|
||||
cnfSec = "Sizes"
|
||||
cnfParse.add_section(cnfSec)
|
||||
cnfParse.set(cnfSec, "geometry", self._packList(self.winGeometry))
|
||||
cnfParse.set(cnfSec, "preferences", self._packList(self.prefGeometry))
|
||||
cnfParse.set(cnfSec, "treecols", self._packList(self.treeColWidth))
|
||||
cnfParse.set(cnfSec, "novelcols", self._packList(self.novelColWidth))
|
||||
cnfParse.set(cnfSec, "projcols", self._packList(self.projColWidth))
|
||||
@@ -671,6 +758,9 @@ class Config:
|
||||
cnfParse.set(cnfSec, "autoscrollpos", str(self.autoScrollPos))
|
||||
cnfParse.set(cnfSec, "fmtsinglequote", self._packList(self.fmtSingleQuotes))
|
||||
cnfParse.set(cnfSec, "fmtdoublequote", self._packList(self.fmtDoubleQuotes))
|
||||
cnfParse.set(cnfSec, "fmtpadbefore", str(self.fmtPadBefore))
|
||||
cnfParse.set(cnfSec, "fmtpadafter", str(self.fmtPadAfter))
|
||||
cnfParse.set(cnfSec, "fmtpadthin", str(self.fmtPadThin))
|
||||
cnfParse.set(cnfSec, "spelltool", str(self.spellTool))
|
||||
cnfParse.set(cnfSec, "spellcheck", str(self.spellLanguage))
|
||||
cnfParse.set(cnfSec, "showtabsnspaces", str(self.showTabsNSpaces))
|
||||
@@ -851,6 +941,12 @@ class Config:
|
||||
self.confChanged = True
|
||||
return True
|
||||
|
||||
def setPreferencesSize(self, newWidth, newHeight):
|
||||
self.prefGeometry[0] = int(newWidth/self.guiScale)
|
||||
self.prefGeometry[1] = int(newHeight/self.guiScale)
|
||||
self.confChanged = True
|
||||
return True
|
||||
|
||||
def setTreeColWidths(self, colWidths):
|
||||
self.treeColWidth = [int(x/self.guiScale) for x in colWidths]
|
||||
self.confChanged = True
|
||||
@@ -914,6 +1010,9 @@ class Config:
|
||||
def getWinSize(self):
|
||||
return [int(x*self.guiScale) for x in self.winGeometry]
|
||||
|
||||
def getPreferencesSize(self):
|
||||
return [int(x*self.guiScale) for x in self.prefGeometry]
|
||||
|
||||
def getTreeColWidths(self):
|
||||
return [int(x*self.guiScale) for x in self.treeColWidth]
|
||||
|
||||
@@ -1017,7 +1116,7 @@ class Config:
|
||||
try:
|
||||
import enchant # noqa: F401
|
||||
self.hasEnchant = True
|
||||
logger.debug("Checking package 'pyenchant': Ok")
|
||||
logger.debug("Checking package 'pyenchant': OK")
|
||||
except Exception:
|
||||
self.hasEnchant = False
|
||||
logger.debug("Checking package 'pyenchant': Missing")
|
||||
@@ -1025,7 +1124,7 @@ class Config:
|
||||
assistPath = shutil.which("assistant")
|
||||
self.hasAssistant = assistPath is not None
|
||||
if self.hasAssistant:
|
||||
logger.debug("Checking executable 'assistant': Ok")
|
||||
logger.debug("Checking executable 'assistant': OK")
|
||||
else:
|
||||
logger.debug("Checking executable 'assistant': Missing")
|
||||
|
||||
|
||||
@@ -24,9 +24,14 @@ 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 nw.constants.enum import (
|
||||
nwItemClass, nwItemLayout, nwItemType, nwOutline
|
||||
)
|
||||
from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP
|
||||
|
||||
from nw.enum import nwItemClass, nwItemLayout, nwItemType, nwOutline
|
||||
|
||||
def trConst(tString):
|
||||
"""Wrapper function for locally translating constants.
|
||||
"""
|
||||
return QCoreApplication.translate("Constant", tString)
|
||||
|
||||
class nwConst():
|
||||
|
||||
@@ -119,17 +124,17 @@ class nwKeyWords:
|
||||
class nwLabels():
|
||||
|
||||
CLASS_NAME = {
|
||||
nwItemClass.NO_CLASS : "None",
|
||||
nwItemClass.NOVEL : "Novel",
|
||||
nwItemClass.PLOT : "Plot",
|
||||
nwItemClass.CHARACTER : "Characters",
|
||||
nwItemClass.WORLD : "Locations",
|
||||
nwItemClass.TIMELINE : "Timeline",
|
||||
nwItemClass.OBJECT : "Objects",
|
||||
nwItemClass.ENTITY : "Entity",
|
||||
nwItemClass.CUSTOM : "Custom",
|
||||
nwItemClass.ARCHIVE : "Outtakes",
|
||||
nwItemClass.TRASH : "Trash",
|
||||
nwItemClass.NO_CLASS : QT_TRANSLATE_NOOP("Constant", "None"),
|
||||
nwItemClass.NOVEL : QT_TRANSLATE_NOOP("Constant", "Novel"),
|
||||
nwItemClass.PLOT : QT_TRANSLATE_NOOP("Constant", "Plot"),
|
||||
nwItemClass.CHARACTER : QT_TRANSLATE_NOOP("Constant", "Characters"),
|
||||
nwItemClass.WORLD : QT_TRANSLATE_NOOP("Constant", "Locations"),
|
||||
nwItemClass.TIMELINE : QT_TRANSLATE_NOOP("Constant", "Timeline"),
|
||||
nwItemClass.OBJECT : QT_TRANSLATE_NOOP("Constant", "Objects"),
|
||||
nwItemClass.ENTITY : QT_TRANSLATE_NOOP("Constant", "Entity"),
|
||||
nwItemClass.CUSTOM : QT_TRANSLATE_NOOP("Constant", "Custom"),
|
||||
nwItemClass.ARCHIVE : QT_TRANSLATE_NOOP("Constant", "Outtakes"),
|
||||
nwItemClass.TRASH : QT_TRANSLATE_NOOP("Constant", "Trash"),
|
||||
}
|
||||
CLASS_FLAG = {
|
||||
nwItemClass.NO_CLASS : "0",
|
||||
@@ -158,15 +163,15 @@ class nwLabels():
|
||||
nwItemClass.TRASH : "cls_trash",
|
||||
}
|
||||
LAYOUT_NAME = {
|
||||
nwItemLayout.NO_LAYOUT : "None",
|
||||
nwItemLayout.TITLE : "Title Page",
|
||||
nwItemLayout.BOOK : "Book",
|
||||
nwItemLayout.PAGE : "Plain Page",
|
||||
nwItemLayout.PARTITION : "Partition",
|
||||
nwItemLayout.UNNUMBERED : "Unnumbered",
|
||||
nwItemLayout.CHAPTER : "Chapter",
|
||||
nwItemLayout.SCENE : "Scene",
|
||||
nwItemLayout.NOTE : "Note",
|
||||
nwItemLayout.NO_LAYOUT : QT_TRANSLATE_NOOP("Constant", "None"),
|
||||
nwItemLayout.TITLE : QT_TRANSLATE_NOOP("Constant", "Title Page"),
|
||||
nwItemLayout.BOOK : QT_TRANSLATE_NOOP("Constant", "Book"),
|
||||
nwItemLayout.PAGE : QT_TRANSLATE_NOOP("Constant", "Plain Page"),
|
||||
nwItemLayout.PARTITION : QT_TRANSLATE_NOOP("Constant", "Partition"),
|
||||
nwItemLayout.UNNUMBERED : QT_TRANSLATE_NOOP("Constant", "Unnumbered"),
|
||||
nwItemLayout.CHAPTER : QT_TRANSLATE_NOOP("Constant", "Chapter"),
|
||||
nwItemLayout.SCENE : QT_TRANSLATE_NOOP("Constant", "Scene"),
|
||||
nwItemLayout.NOTE : QT_TRANSLATE_NOOP("Constant", "Note"),
|
||||
}
|
||||
LAYOUT_FLAG = {
|
||||
nwItemLayout.NO_LAYOUT : "Xo",
|
||||
@@ -180,27 +185,27 @@ class nwLabels():
|
||||
nwItemLayout.NOTE : "Nt",
|
||||
}
|
||||
KEY_NAME = {
|
||||
nwKeyWords.TAG_KEY : "Tag",
|
||||
nwKeyWords.POV_KEY : "Point of View",
|
||||
nwKeyWords.FOCUS_KEY : "Focus",
|
||||
nwKeyWords.CHAR_KEY : "Characters",
|
||||
nwKeyWords.PLOT_KEY : "Plot",
|
||||
nwKeyWords.TIME_KEY : "Timeline",
|
||||
nwKeyWords.WORLD_KEY : "Locations",
|
||||
nwKeyWords.OBJECT_KEY : "Objects",
|
||||
nwKeyWords.ENTITY_KEY : "Entities",
|
||||
nwKeyWords.CUSTOM_KEY : "Custom",
|
||||
nwKeyWords.TAG_KEY : QT_TRANSLATE_NOOP("Constant", "Tag"),
|
||||
nwKeyWords.POV_KEY : QT_TRANSLATE_NOOP("Constant", "Point of View"),
|
||||
nwKeyWords.FOCUS_KEY : QT_TRANSLATE_NOOP("Constant", "Focus"),
|
||||
nwKeyWords.CHAR_KEY : QT_TRANSLATE_NOOP("Constant", "Characters"),
|
||||
nwKeyWords.PLOT_KEY : QT_TRANSLATE_NOOP("Constant", "Plot"),
|
||||
nwKeyWords.TIME_KEY : QT_TRANSLATE_NOOP("Constant", "Timeline"),
|
||||
nwKeyWords.WORLD_KEY : QT_TRANSLATE_NOOP("Constant", "Locations"),
|
||||
nwKeyWords.OBJECT_KEY : QT_TRANSLATE_NOOP("Constant", "Objects"),
|
||||
nwKeyWords.ENTITY_KEY : QT_TRANSLATE_NOOP("Constant", "Entities"),
|
||||
nwKeyWords.CUSTOM_KEY : QT_TRANSLATE_NOOP("Constant", "Custom"),
|
||||
}
|
||||
OUTLINE_COLS = {
|
||||
nwOutline.TITLE : "Title",
|
||||
nwOutline.LEVEL : "Level",
|
||||
nwOutline.LABEL : "Document",
|
||||
nwOutline.LINE : "Line",
|
||||
nwOutline.CCOUNT : "Chars",
|
||||
nwOutline.WCOUNT : "Words",
|
||||
nwOutline.PCOUNT : "Pars",
|
||||
nwOutline.POV : "POV",
|
||||
nwOutline.FOCUS : "Focus",
|
||||
nwOutline.TITLE : QT_TRANSLATE_NOOP("Constant", "Title"),
|
||||
nwOutline.LEVEL : QT_TRANSLATE_NOOP("Constant", "Level"),
|
||||
nwOutline.LABEL : QT_TRANSLATE_NOOP("Constant", "Document"),
|
||||
nwOutline.LINE : QT_TRANSLATE_NOOP("Constant", "Line"),
|
||||
nwOutline.CCOUNT : QT_TRANSLATE_NOOP("Constant", "Chars"),
|
||||
nwOutline.WCOUNT : QT_TRANSLATE_NOOP("Constant", "Words"),
|
||||
nwOutline.PCOUNT : QT_TRANSLATE_NOOP("Constant", "Pars"),
|
||||
nwOutline.POV : QT_TRANSLATE_NOOP("Constant", "POV"),
|
||||
nwOutline.FOCUS : QT_TRANSLATE_NOOP("Constant", "Focus"),
|
||||
nwOutline.CHAR : KEY_NAME[nwKeyWords.CHAR_KEY],
|
||||
nwOutline.PLOT : KEY_NAME[nwKeyWords.PLOT_KEY],
|
||||
nwOutline.TIME : KEY_NAME[nwKeyWords.TIME_KEY],
|
||||
@@ -208,7 +213,7 @@ class nwLabels():
|
||||
nwOutline.OBJECT : KEY_NAME[nwKeyWords.OBJECT_KEY],
|
||||
nwOutline.ENTITY : KEY_NAME[nwKeyWords.ENTITY_KEY],
|
||||
nwOutline.CUSTOM : KEY_NAME[nwKeyWords.CUSTOM_KEY],
|
||||
nwOutline.SYNOP : "Synopsis",
|
||||
nwOutline.SYNOP : QT_TRANSLATE_NOOP("Constant", "Synopsis"),
|
||||
}
|
||||
|
||||
# END Class nwLabels
|
||||
@@ -218,28 +223,28 @@ class nwQuotes():
|
||||
Source: https://en.wikipedia.org/wiki/Quotation_mark
|
||||
"""
|
||||
SYMBOLS = {
|
||||
"\u0027" : "Straight single quotation mark",
|
||||
"\u0022" : "Straight double quotation mark",
|
||||
"\u0027" : QT_TRANSLATE_NOOP("Constant", "Straight single quotation mark"),
|
||||
"\u0022" : QT_TRANSLATE_NOOP("Constant", "Straight double quotation mark"),
|
||||
|
||||
"\u2018" : "Left single quotation mark",
|
||||
"\u2019" : "Right single quotation mark",
|
||||
"\u201a" : "Single low-9 quotation mark",
|
||||
"\u201b" : "Single high-reversed-9 quotation mark",
|
||||
"\u201c" : "Left double quotation mark",
|
||||
"\u201d" : "Right double quotation mark",
|
||||
"\u201e" : "Double low-9 quotation mark",
|
||||
"\u201f" : "Double high-reversed-9 quotation mark",
|
||||
"\u2e42" : "Double low-reversed-9 quotation mark",
|
||||
"\u2018" : QT_TRANSLATE_NOOP("Constant", "Left single quotation mark"),
|
||||
"\u2019" : QT_TRANSLATE_NOOP("Constant", "Right single quotation mark"),
|
||||
"\u201a" : QT_TRANSLATE_NOOP("Constant", "Single low-9 quotation mark"),
|
||||
"\u201b" : QT_TRANSLATE_NOOP("Constant", "Single high-reversed-9 quotation mark"),
|
||||
"\u201c" : QT_TRANSLATE_NOOP("Constant", "Left double quotation mark"),
|
||||
"\u201d" : QT_TRANSLATE_NOOP("Constant", "Right double quotation mark"),
|
||||
"\u201e" : QT_TRANSLATE_NOOP("Constant", "Double low-9 quotation mark"),
|
||||
"\u201f" : QT_TRANSLATE_NOOP("Constant", "Double high-reversed-9 quotation mark"),
|
||||
"\u2e42" : QT_TRANSLATE_NOOP("Constant", "Double low-reversed-9 quotation mark"),
|
||||
|
||||
"\u2039" : "Single left-pointing angle quotation mark",
|
||||
"\u203a" : "Single right-pointing angle quotation mark",
|
||||
"\u00ab" : "Left-pointing double angle quotation mark",
|
||||
"\u00bb" : "Right-pointing double angle quotation mark",
|
||||
"\u2039" : QT_TRANSLATE_NOOP("Constant", "Single left-pointing angle quotation mark"),
|
||||
"\u203a" : QT_TRANSLATE_NOOP("Constant", "Single right-pointing angle quotation mark"),
|
||||
"\u00ab" : QT_TRANSLATE_NOOP("Constant", "Double left-pointing angle quotation mark"),
|
||||
"\u00bb" : QT_TRANSLATE_NOOP("Constant", "Double right-pointing angle quotation mark"),
|
||||
|
||||
"\u300c" : "Left corner bracket",
|
||||
"\u300d" : "Right corner bracket",
|
||||
"\u300e" : "Left white corner bracket",
|
||||
"\u300f" : "Right white corner bracket",
|
||||
"\u300c" : QT_TRANSLATE_NOOP("Constant", "Left corner bracket"),
|
||||
"\u300d" : QT_TRANSLATE_NOOP("Constant", "Right corner bracket"),
|
||||
"\u300e" : QT_TRANSLATE_NOOP("Constant", "Left white corner bracket"),
|
||||
"\u300f" : QT_TRANSLATE_NOOP("Constant", "Right white corner bracket"),
|
||||
}
|
||||
|
||||
# END Class nwQuotes
|
||||
@@ -1,31 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from nw.constants.iso import isoLanguage, isoCountry
|
||||
from nw.constants.constants import (
|
||||
nwConst, nwLists, nwRegEx, nwFiles, nwKeyWords, nwLabels, nwQuotes,
|
||||
nwUnicode, nwHtmlUnicode
|
||||
)
|
||||
from nw.constants.enum import (
|
||||
nwAlert, nwDocAction, nwItemClass, nwItemLayout, nwItemType, nwOutline,
|
||||
nwDocInsert
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"isoCountry",
|
||||
"isoLanguage",
|
||||
"nwConst",
|
||||
"nwLists",
|
||||
"nwRegEx",
|
||||
"nwFiles",
|
||||
"nwKeyWords",
|
||||
"nwLabels",
|
||||
"nwQuotes",
|
||||
"nwUnicode",
|
||||
"nwHtmlUnicode",
|
||||
"nwAlert",
|
||||
"nwDocAction",
|
||||
"nwItemClass",
|
||||
"nwItemLayout",
|
||||
"nwItemType",
|
||||
"nwOutline",
|
||||
"nwDocInsert",
|
||||
]
|
||||
@@ -1,472 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – ISO Codes
|
||||
=======================
|
||||
Handles translating language codes to language names
|
||||
|
||||
File History:
|
||||
Created: 2019-11-05 [0.4.0]
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, 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/>.
|
||||
"""
|
||||
|
||||
class isoLanguage():
|
||||
|
||||
ISO_639_1 = {
|
||||
"aa" : "Afar",
|
||||
"ab" : "Abkhazian",
|
||||
"ae" : "Avestan",
|
||||
"af" : "Afrikaans",
|
||||
"ak" : "Akan",
|
||||
"am" : "Amharic",
|
||||
"an" : "Aragonese",
|
||||
"ar" : "Arabic",
|
||||
"as" : "Assamese",
|
||||
"av" : "Avaric",
|
||||
"ay" : "Aymara",
|
||||
"az" : "Azerbaijani",
|
||||
"ba" : "Bashkir",
|
||||
"be" : "Belarusian",
|
||||
"bg" : "Bulgarian",
|
||||
"bh" : "Bihari languages",
|
||||
"bi" : "Bislama",
|
||||
"bm" : "Bambara",
|
||||
"bn" : "Bengali",
|
||||
"bo" : "Tibetan",
|
||||
"br" : "Breton",
|
||||
"bs" : "Bosnian",
|
||||
"ca" : "Catalan",
|
||||
"ce" : "Chechen",
|
||||
"ch" : "Chamorro",
|
||||
"co" : "Corsican",
|
||||
"cr" : "Cree",
|
||||
"cs" : "Czech",
|
||||
"cu" : "Church Slavic",
|
||||
"cv" : "Chuvash",
|
||||
"cy" : "Welsh",
|
||||
"da" : "Danish",
|
||||
"de" : "German",
|
||||
"dv" : "Divehi",
|
||||
"dz" : "Dzongkha",
|
||||
"ee" : "Ewe",
|
||||
"el" : "Modern Greek",
|
||||
"en" : "English",
|
||||
"eo" : "Esperanto",
|
||||
"es" : "Spanish",
|
||||
"et" : "Estonian",
|
||||
"eu" : "Basque",
|
||||
"fa" : "Persian",
|
||||
"ff" : "Fulah",
|
||||
"fi" : "Finnish",
|
||||
"fj" : "Fijian",
|
||||
"fo" : "Faroese",
|
||||
"fr" : "French",
|
||||
"fy" : "Western Frisian",
|
||||
"ga" : "Irish",
|
||||
"gd" : "Gaelic",
|
||||
"gl" : "Galician",
|
||||
"gn" : "Guarani",
|
||||
"gu" : "Gujarati",
|
||||
"gv" : "Manx",
|
||||
"ha" : "Hausa",
|
||||
"he" : "Hebrew",
|
||||
"hi" : "Hindi",
|
||||
"ho" : "Hiri Motu",
|
||||
"hr" : "Croatian",
|
||||
"ht" : "Haitian",
|
||||
"hu" : "Hungarian",
|
||||
"hy" : "Armenian",
|
||||
"hz" : "Herero",
|
||||
"ia" : "Interlingua",
|
||||
"id" : "Indonesian",
|
||||
"ie" : "Interlingue",
|
||||
"ig" : "Igbo",
|
||||
"ii" : "Sichuan Yi",
|
||||
"ik" : "Inupiaq",
|
||||
"io" : "Ido",
|
||||
"is" : "Icelandic",
|
||||
"it" : "Italian",
|
||||
"iu" : "Inuktitut",
|
||||
"ja" : "Japanese",
|
||||
"jv" : "Javanese",
|
||||
"ka" : "Georgian",
|
||||
"kg" : "Kongo",
|
||||
"ki" : "Kikuyu",
|
||||
"kj" : "Kuanyama",
|
||||
"kk" : "Kazakh",
|
||||
"kl" : "Kalaallisut",
|
||||
"km" : "Central Khmer",
|
||||
"kn" : "Kannada",
|
||||
"ko" : "Korean",
|
||||
"kr" : "Kanuri",
|
||||
"ks" : "Kashmiri",
|
||||
"ku" : "Kurdish",
|
||||
"kv" : "Komi",
|
||||
"kw" : "Cornish",
|
||||
"ky" : "Kirghiz",
|
||||
"la" : "Latin",
|
||||
"lb" : "Luxembourgish",
|
||||
"lg" : "Ganda",
|
||||
"li" : "Limburgan",
|
||||
"ln" : "Lingala",
|
||||
"lo" : "Lao",
|
||||
"lt" : "Lithuanian",
|
||||
"lu" : "Luba-Katanga",
|
||||
"lv" : "Latvian",
|
||||
"mg" : "Malagasy",
|
||||
"mh" : "Marshallese",
|
||||
"mi" : "Maori",
|
||||
"mk" : "Macedonian",
|
||||
"ml" : "Malayalam",
|
||||
"mn" : "Mongolian",
|
||||
"mr" : "Marathi",
|
||||
"ms" : "Malay",
|
||||
"mt" : "Maltese",
|
||||
"my" : "Burmese",
|
||||
"na" : "Nauru",
|
||||
"nb" : "Norwegian Bokmål",
|
||||
"nd" : "North Ndebele",
|
||||
"ne" : "Nepali",
|
||||
"ng" : "Ndonga",
|
||||
"nl" : "Dutch",
|
||||
"nn" : "Norwegian Nynorsk",
|
||||
"no" : "Norwegian",
|
||||
"nr" : "South Ndebele",
|
||||
"nv" : "Navajo",
|
||||
"ny" : "Chichewa",
|
||||
"oc" : "Occitan",
|
||||
"oj" : "Ojibwa",
|
||||
"om" : "Oromo",
|
||||
"or" : "Oriya",
|
||||
"os" : "Ossetian",
|
||||
"pa" : "Panjabi",
|
||||
"pi" : "Pali",
|
||||
"pl" : "Polish",
|
||||
"ps" : "Pushto",
|
||||
"pt" : "Portuguese",
|
||||
"qu" : "Quechua",
|
||||
"rm" : "Romansh",
|
||||
"rn" : "Rundi",
|
||||
"ro" : "Romanian",
|
||||
"ru" : "Russian",
|
||||
"rw" : "Kinyarwanda",
|
||||
"sa" : "Sanskrit",
|
||||
"sc" : "Sardinian",
|
||||
"sd" : "Sindhi",
|
||||
"se" : "Northern Sami",
|
||||
"sg" : "Sango",
|
||||
"si" : "Sinhala",
|
||||
"sk" : "Slovak",
|
||||
"sl" : "Slovenian",
|
||||
"sm" : "Samoan",
|
||||
"sn" : "Shona",
|
||||
"so" : "Somali",
|
||||
"sq" : "Albanian",
|
||||
"sr" : "Serbian",
|
||||
"ss" : "Swati",
|
||||
"st" : "Southern Sotho",
|
||||
"su" : "Sundanese",
|
||||
"sv" : "Swedish",
|
||||
"sw" : "Swahili",
|
||||
"ta" : "Tamil",
|
||||
"te" : "Telugu",
|
||||
"tg" : "Tajik",
|
||||
"th" : "Thai",
|
||||
"ti" : "Tigrinya",
|
||||
"tk" : "Turkmen",
|
||||
"tl" : "Tagalog",
|
||||
"tn" : "Tswana",
|
||||
"to" : "Tonga",
|
||||
"tr" : "Turkish",
|
||||
"ts" : "Tsonga",
|
||||
"tt" : "Tatar",
|
||||
"tw" : "Twi",
|
||||
"ty" : "Tahitian",
|
||||
"ug" : "Uighur",
|
||||
"uk" : "Ukrainian",
|
||||
"ur" : "Urdu",
|
||||
"uz" : "Uzbek",
|
||||
"ve" : "Venda",
|
||||
"vi" : "Vietnamese",
|
||||
"vo" : "Volapük",
|
||||
"wa" : "Walloon",
|
||||
"wo" : "Wolof",
|
||||
"xh" : "Xhosa",
|
||||
"yi" : "Yiddish",
|
||||
"yo" : "Yoruba",
|
||||
"za" : "Zhuang",
|
||||
"zh" : "Chinese",
|
||||
"zu" : "Zulu",
|
||||
}
|
||||
|
||||
# END Class isoLanguage
|
||||
|
||||
class isoCountry():
|
||||
|
||||
ISO_3166_1_alpha_2 = {
|
||||
"AD" : "Andorra",
|
||||
"AE" : "United Arab Emirates",
|
||||
"AF" : "Afghanistan",
|
||||
"AG" : "Antigua and Barbuda",
|
||||
"AI" : "Anguilla",
|
||||
"AL" : "Albania",
|
||||
"AM" : "Armenia",
|
||||
"AO" : "Angola",
|
||||
"AQ" : "Antarctica",
|
||||
"AR" : "Argentina",
|
||||
"AS" : "American Samoa",
|
||||
"AT" : "Austria",
|
||||
"AU" : "Australia",
|
||||
"AW" : "Aruba",
|
||||
"AX" : "Åland Islands",
|
||||
"AZ" : "Azerbaijan",
|
||||
"BA" : "Bosnia and Herzegovina",
|
||||
"BB" : "Barbados",
|
||||
"BD" : "Bangladesh",
|
||||
"BE" : "Belgium",
|
||||
"BF" : "Burkina Faso",
|
||||
"BG" : "Bulgaria",
|
||||
"BH" : "Bahrain",
|
||||
"BI" : "Burundi",
|
||||
"BJ" : "Benin",
|
||||
"BL" : "Saint Barthélemy",
|
||||
"BM" : "Bermuda",
|
||||
"BN" : "Brunei Darussalam",
|
||||
"BO" : "Plurinational State of Bolivia",
|
||||
"BQ" : "Sint Eustatius and Saba Bonaire",
|
||||
"BR" : "Brazil",
|
||||
"BS" : "Bahamas",
|
||||
"BT" : "Bhutan",
|
||||
"BV" : "Bouvet Island",
|
||||
"BW" : "Botswana",
|
||||
"BY" : "Belarus",
|
||||
"BZ" : "Belize",
|
||||
"CA" : "Canada",
|
||||
"CC" : "Cocos (Keeling) Islands",
|
||||
"CD" : "The Democratic Republic of the Congo",
|
||||
"CF" : "Central African Republic",
|
||||
"CG" : "Congo",
|
||||
"CH" : "Switzerland",
|
||||
"CI" : "Côte d'Ivoire",
|
||||
"CK" : "Cook Islands",
|
||||
"CL" : "Chile",
|
||||
"CM" : "Cameroon",
|
||||
"CN" : "China",
|
||||
"CO" : "Colombia",
|
||||
"CR" : "Costa Rica",
|
||||
"CU" : "Cuba",
|
||||
"CV" : "Cape Verde",
|
||||
"CW" : "Curaçao",
|
||||
"CX" : "Christmas Island",
|
||||
"CY" : "Cyprus",
|
||||
"CZ" : "Czech Republic",
|
||||
"DE" : "Germany",
|
||||
"DJ" : "Djibouti",
|
||||
"DK" : "Denmark",
|
||||
"DM" : "Dominica",
|
||||
"DO" : "Dominican Republic",
|
||||
"DZ" : "Algeria",
|
||||
"EC" : "Ecuador",
|
||||
"EE" : "Estonia",
|
||||
"EG" : "Egypt",
|
||||
"EH" : "Western Sahara",
|
||||
"ER" : "Eritrea",
|
||||
"ES" : "Spain",
|
||||
"ET" : "Ethiopia",
|
||||
"FI" : "Finland",
|
||||
"FJ" : "Fiji",
|
||||
"FK" : "Falkland Islands (Malvinas)",
|
||||
"FM" : "Federated States of Micronesia",
|
||||
"FO" : "Faroe Islands",
|
||||
"FR" : "France",
|
||||
"GA" : "Gabon",
|
||||
"GB" : "United Kingdom",
|
||||
"GD" : "Grenada",
|
||||
"GE" : "Georgia",
|
||||
"GF" : "French Guiana",
|
||||
"GG" : "Guernsey",
|
||||
"GH" : "Ghana",
|
||||
"GI" : "Gibraltar",
|
||||
"GL" : "Greenland",
|
||||
"GM" : "Gambia",
|
||||
"GN" : "Guinea",
|
||||
"GP" : "Guadeloupe",
|
||||
"GQ" : "Equatorial Guinea",
|
||||
"GR" : "Greece",
|
||||
"GS" : "South Georgia and the South Sandwich Islands",
|
||||
"GT" : "Guatemala",
|
||||
"GU" : "Guam",
|
||||
"GW" : "Guinea-Bissau",
|
||||
"GY" : "Guyana",
|
||||
"HK" : "Hong Kong",
|
||||
"HM" : "Heard Island and McDonald Islands",
|
||||
"HN" : "Honduras",
|
||||
"HR" : "Croatia",
|
||||
"HT" : "Haiti",
|
||||
"HU" : "Hungary",
|
||||
"ID" : "Indonesia",
|
||||
"IE" : "Ireland",
|
||||
"IL" : "Israel",
|
||||
"IM" : "Isle of Man",
|
||||
"IN" : "India",
|
||||
"IO" : "British Indian Ocean Territory",
|
||||
"IQ" : "Iraq",
|
||||
"IR" : "Islamic Republic of Iran",
|
||||
"IS" : "Iceland",
|
||||
"IT" : "Italy",
|
||||
"JE" : "Jersey",
|
||||
"JM" : "Jamaica",
|
||||
"JO" : "Jordan",
|
||||
"JP" : "Japan",
|
||||
"KE" : "Kenya",
|
||||
"KG" : "Kyrgyzstan",
|
||||
"KH" : "Cambodia",
|
||||
"KI" : "Kiribati",
|
||||
"KM" : "Comoros",
|
||||
"KN" : "Saint Kitts and Nevis",
|
||||
"KP" : "Democratic People's Republic of Korea",
|
||||
"KR" : "Republic of Korea",
|
||||
"KW" : "Kuwait",
|
||||
"KY" : "Cayman Islands",
|
||||
"KZ" : "Kazakhstan",
|
||||
"LA" : "Lao People's Democratic Republic",
|
||||
"LB" : "Lebanon",
|
||||
"LC" : "Saint Lucia",
|
||||
"LI" : "Liechtenstein",
|
||||
"LK" : "Sri Lanka",
|
||||
"LR" : "Liberia",
|
||||
"LS" : "Lesotho",
|
||||
"LT" : "Lithuania",
|
||||
"LU" : "Luxembourg",
|
||||
"LV" : "Latvia",
|
||||
"LY" : "Libya",
|
||||
"MA" : "Morocco",
|
||||
"MC" : "Monaco",
|
||||
"MD" : "Republic of Moldova",
|
||||
"ME" : "Montenegro",
|
||||
"MF" : "Saint Martin (French part)",
|
||||
"MG" : "Madagascar",
|
||||
"MH" : "Marshall Islands",
|
||||
"MK" : "The Former Yugoslav Republic of Macedonia",
|
||||
"ML" : "Mali",
|
||||
"MM" : "Myanmar",
|
||||
"MN" : "Mongolia",
|
||||
"MO" : "Macao",
|
||||
"MP" : "Northern Mariana Islands",
|
||||
"MQ" : "Martinique",
|
||||
"MR" : "Mauritania",
|
||||
"MS" : "Montserrat",
|
||||
"MT" : "Malta",
|
||||
"MU" : "Mauritius",
|
||||
"MV" : "Maldives",
|
||||
"MW" : "Malawi",
|
||||
"MX" : "Mexico",
|
||||
"MY" : "Malaysia",
|
||||
"MZ" : "Mozambique",
|
||||
"NA" : "Namibia",
|
||||
"NC" : "New Caledonia",
|
||||
"NE" : "Niger",
|
||||
"NF" : "Norfolk Island",
|
||||
"NG" : "Nigeria",
|
||||
"NI" : "Nicaragua",
|
||||
"NL" : "Netherlands",
|
||||
"NO" : "Norway",
|
||||
"NP" : "Nepal",
|
||||
"NR" : "Nauru",
|
||||
"NU" : "Niue",
|
||||
"NZ" : "New Zealand",
|
||||
"OM" : "Oman",
|
||||
"PA" : "Panama",
|
||||
"PE" : "Peru",
|
||||
"PF" : "French Polynesia",
|
||||
"PG" : "Papua New Guinea",
|
||||
"PH" : "Philippines",
|
||||
"PK" : "Pakistan",
|
||||
"PL" : "Poland",
|
||||
"PM" : "Saint Pierre and Miquelon",
|
||||
"PN" : "Pitcairn",
|
||||
"PR" : "Puerto Rico",
|
||||
"PS" : "State of Palestine",
|
||||
"PT" : "Portugal",
|
||||
"PW" : "Palau",
|
||||
"PY" : "Paraguay",
|
||||
"QA" : "Qatar",
|
||||
"RE" : "Réunion",
|
||||
"RO" : "Romania",
|
||||
"RS" : "Serbia",
|
||||
"RU" : "Russian Federation",
|
||||
"RW" : "Rwanda",
|
||||
"SA" : "Saudi Arabia",
|
||||
"SB" : "Solomon Islands",
|
||||
"SC" : "Seychelles",
|
||||
"SD" : "Sudan",
|
||||
"SE" : "Sweden",
|
||||
"SG" : "Singapore",
|
||||
"SH" : "Saint Helena, Ascension and Tristan da Cunha",
|
||||
"SI" : "Slovenia",
|
||||
"SJ" : "Svalbard and Jan Mayen",
|
||||
"SK" : "Slovakia",
|
||||
"SL" : "Sierra Leone",
|
||||
"SM" : "San Marino",
|
||||
"SN" : "Senegal",
|
||||
"SO" : "Somalia",
|
||||
"SR" : "Suriname",
|
||||
"SS" : "South Sudan",
|
||||
"ST" : "Sao Tome and Principe",
|
||||
"SV" : "El Salvador",
|
||||
"SX" : "Sint Maarten",
|
||||
"SY" : "Syrian Arab Republic",
|
||||
"SZ" : "Swaziland",
|
||||
"TC" : "Turks and Caicos Islands",
|
||||
"TD" : "Chad",
|
||||
"TF" : "French Southern Territories",
|
||||
"TG" : "Togo",
|
||||
"TH" : "Thailand",
|
||||
"TJ" : "Tajikistan",
|
||||
"TK" : "Tokelau",
|
||||
"TL" : "Timor-Leste",
|
||||
"TM" : "Turkmenistan",
|
||||
"TN" : "Tunisia",
|
||||
"TO" : "Tonga",
|
||||
"TR" : "Turkey",
|
||||
"TT" : "Trinidad and Tobago",
|
||||
"TV" : "Tuvalu",
|
||||
"TW" : "Taiwan, Province of China",
|
||||
"TZ" : "United Republic of Tanzania",
|
||||
"UA" : "Ukraine",
|
||||
"UG" : "Uganda",
|
||||
"UM" : "United States Minor Outlying Islands",
|
||||
"US" : "United States",
|
||||
"UY" : "Uruguay",
|
||||
"UZ" : "Uzbekistan",
|
||||
"VA" : "Holy See (Vatican City State)",
|
||||
"VC" : "Saint Vincent and the Grenadines",
|
||||
"VE" : "Bolivarian Republic of Venezuela",
|
||||
"VG" : "British Virgin Islands",
|
||||
"VI" : "U.S. Virgin Islands",
|
||||
"VN" : "Viet Nam",
|
||||
"VU" : "Vanuatu",
|
||||
"WF" : "Wallis and Futuna",
|
||||
"WS" : "Samoa",
|
||||
"YE" : "Yemen",
|
||||
"YT" : "Mayotte",
|
||||
"ZA" : "South Africa",
|
||||
"ZM" : "Zambia",
|
||||
"ZW" : "Zimbabwe",
|
||||
}
|
||||
|
||||
# END Class isoCountry
|
||||
@@ -1,18 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from nw.core.document import NWDoc
|
||||
from nw.core.index import NWIndex
|
||||
from nw.core.index import NWIndex, countWords
|
||||
from nw.core.project import NWProject
|
||||
from nw.core.spellcheck import NWSpellCheck, NWSpellEnchant, NWSpellSimple
|
||||
from nw.core.tohtml import ToHtml
|
||||
from nw.core.toodt import ToOdt
|
||||
from nw.core.tomd import ToMarkdown
|
||||
from nw.core.tools import countWords, numberToRoman, numberToWord
|
||||
|
||||
__all__ = [
|
||||
"countWords",
|
||||
"numberToRoman",
|
||||
"numberToWord",
|
||||
"NWDoc",
|
||||
"NWIndex",
|
||||
"NWProject",
|
||||
|
||||
@@ -27,9 +27,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import logging
|
||||
import os
|
||||
|
||||
from nw.constants import nwAlert
|
||||
from functools import partial
|
||||
|
||||
from PyQt5.QtCore import QCoreApplication
|
||||
|
||||
from nw.enum import nwAlert, nwItemLayout, nwItemClass
|
||||
from nw.common import isHandle
|
||||
from nw.constants import nwItemLayout, nwItemClass
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -48,6 +51,7 @@ class NWDoc():
|
||||
|
||||
# Internal Mapping
|
||||
self.makeAlert = self.theParent.makeAlert
|
||||
self.tr = partial(QCoreApplication.translate, "NWDoc")
|
||||
|
||||
return
|
||||
|
||||
@@ -111,7 +115,7 @@ class NWDoc():
|
||||
theText += inFile.read()
|
||||
|
||||
except Exception as e:
|
||||
self.makeAlert(["Failed to open document file.", str(e)], nwAlert.ERROR)
|
||||
self.makeAlert([self.tr("Failed to open document file."), str(e)], nwAlert.ERROR)
|
||||
# Note: Document must be cleared in case of an io error,
|
||||
# or else the auto-save or save will try to overwrite it
|
||||
# with an empty file. Return None to alert the caller.
|
||||
@@ -124,7 +128,9 @@ class NWDoc():
|
||||
return ""
|
||||
|
||||
if showStatus and not isOrphan:
|
||||
self.theParent.setStatus("Opened Document: %s" % self._theItem.itemName)
|
||||
self.theParent.setStatus(
|
||||
self.tr("Opened Document: {0}").format(self._theItem.itemName)
|
||||
)
|
||||
|
||||
return theText
|
||||
|
||||
@@ -158,7 +164,7 @@ class NWDoc():
|
||||
outFile.write(docMeta)
|
||||
outFile.write(docText)
|
||||
except Exception as e:
|
||||
self.makeAlert(["Could not save document.", str(e)], nwAlert.ERROR)
|
||||
self.makeAlert([self.tr("Could not save document."), str(e)], nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
# If we're here, the file was successfully saved, so we can
|
||||
@@ -168,7 +174,9 @@ class NWDoc():
|
||||
os.rename(docTemp, docPath)
|
||||
|
||||
if self._theItem is not None:
|
||||
self.theParent.setStatus("Saved Document: %s" % self._theItem.itemName)
|
||||
self.theParent.setStatus(
|
||||
self.tr("Saved Document: {0}").format(self._theItem.itemName)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@@ -191,7 +199,9 @@ class NWDoc():
|
||||
os.unlink(chkFile)
|
||||
logger.debug("Deleted: %s" % chkFile)
|
||||
except Exception as e:
|
||||
self.makeAlert(["Could not delete document file.", str(e)], nwAlert.ERROR)
|
||||
self.makeAlert(
|
||||
[self.tr("Could not delete document file."), str(e)], nwAlert.ERROR
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -5,7 +5,8 @@ novelWriter – Project Index
|
||||
Data class for the project index of tags, headers and references
|
||||
|
||||
File History:
|
||||
Created: 2019-05-27 [0.1.4]
|
||||
Created: 2019-04-22 [0.0.1] countWords
|
||||
Created: 2019-05-27 [0.1.4] NWIndex
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
@@ -31,12 +32,10 @@ import os
|
||||
|
||||
from time import time
|
||||
|
||||
from nw.constants import (
|
||||
nwFiles, nwKeyWords, nwItemType, nwItemClass, nwItemLayout, nwAlert
|
||||
)
|
||||
from nw.core.document import NWDoc
|
||||
from nw.core.tools import countWords
|
||||
from nw.enum import nwItemType, nwItemClass, nwItemLayout
|
||||
from nw.common import isHandle, isTitleTag, isItemClass, isItemLayout
|
||||
from nw.constants import nwFiles, nwKeyWords, nwUnicode
|
||||
from nw.core.document import NWDoc
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -158,10 +157,6 @@ class NWIndex():
|
||||
logger.error("Failed to load index file")
|
||||
nw.logException()
|
||||
self.indexBroken = True
|
||||
self.theParent.makeAlert(
|
||||
"Could not load cached index file. Rebuilding index.",
|
||||
nwAlert.WARN
|
||||
)
|
||||
return False
|
||||
|
||||
self._tagIndex = theData.get("tagIndex", {})
|
||||
@@ -228,10 +223,6 @@ class NWIndex():
|
||||
|
||||
if self.indexBroken:
|
||||
self.clearIndex()
|
||||
self.theParent.makeAlert(
|
||||
"The project index is outdated or broken. Rebuilding index.",
|
||||
nwAlert.WARN
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
@@ -905,3 +896,62 @@ class NWIndex():
|
||||
return
|
||||
|
||||
# END Class NWIndex
|
||||
|
||||
# =============================================================================================== #
|
||||
# Simple Word Counter
|
||||
# =============================================================================================== #
|
||||
|
||||
def countWords(theText):
|
||||
"""Count words in a piece of text, skipping special syntax and
|
||||
comments.
|
||||
"""
|
||||
charCount = 0
|
||||
wordCount = 0
|
||||
paraCount = 0
|
||||
prevEmpty = True
|
||||
|
||||
# We need to treat dashes as word separators for counting words.
|
||||
# The check+replace apprach is much faster that direct replace for
|
||||
# large texts, and a bit slower for small texts, but in the latter
|
||||
# case it doesn't matter.
|
||||
if nwUnicode.U_ENDASH in theText:
|
||||
theText = theText.replace(nwUnicode.U_ENDASH, " ")
|
||||
if nwUnicode.U_EMDASH in theText:
|
||||
theText = theText.replace(nwUnicode.U_EMDASH, " ")
|
||||
|
||||
for aLine in theText.splitlines():
|
||||
|
||||
countPara = True
|
||||
theLen = len(aLine)
|
||||
|
||||
if theLen == 0:
|
||||
prevEmpty = True
|
||||
continue
|
||||
if aLine[0] == "@" or aLine[0] == "%":
|
||||
continue
|
||||
|
||||
if aLine[0:5] == "#### ":
|
||||
wordCount -= 1
|
||||
charCount -= 5
|
||||
countPara = False
|
||||
elif aLine[0:4] == "### ":
|
||||
wordCount -= 1
|
||||
charCount -= 4
|
||||
countPara = False
|
||||
elif aLine[0:3] == "## ":
|
||||
wordCount -= 1
|
||||
charCount -= 3
|
||||
countPara = False
|
||||
elif aLine[0:2] == "# ":
|
||||
wordCount -= 1
|
||||
charCount -= 2
|
||||
countPara = False
|
||||
|
||||
wordCount += len(aLine.split())
|
||||
charCount += theLen
|
||||
if countPara and prevEmpty:
|
||||
paraCount += 1
|
||||
|
||||
prevEmpty = not countPara
|
||||
|
||||
return charCount, wordCount, paraCount
|
||||
|
||||
@@ -28,10 +28,8 @@ import logging
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from nw.constants import nwItemType, nwItemClass, nwItemLayout
|
||||
from nw.common import (
|
||||
checkInt, isHandle, isItemClass, isItemLayout, isItemType
|
||||
)
|
||||
from nw.enum import nwItemType, nwItemClass, nwItemLayout
|
||||
from nw.common import checkInt, isHandle, isItemClass, isItemLayout, isItemType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -91,6 +91,8 @@ class OptionState():
|
||||
"winWidth",
|
||||
"winHeight",
|
||||
"replaceColW",
|
||||
"statusColW",
|
||||
"importColW",
|
||||
},
|
||||
"GuiProjectDetails": {
|
||||
"winWidth",
|
||||
|
||||
@@ -27,10 +27,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import nw
|
||||
import logging
|
||||
import os
|
||||
import json
|
||||
import shutil
|
||||
|
||||
from lxml import etree
|
||||
from time import time
|
||||
from functools import partial
|
||||
|
||||
from PyQt5.QtCore import QCoreApplication
|
||||
|
||||
from nw.core.tree import NWTree
|
||||
from nw.core.item import NWItem
|
||||
@@ -41,9 +45,8 @@ from nw.common import (
|
||||
checkString, checkBool, checkInt, isHandle, formatTimeStamp,
|
||||
makeFileNameSafe, hexToInt
|
||||
)
|
||||
from nw.constants import (
|
||||
nwFiles, nwItemType, nwItemClass, nwItemLayout, nwLabels, nwAlert
|
||||
)
|
||||
from nw.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert
|
||||
from nw.constants import trConst, nwFiles, nwLabels
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -58,6 +61,7 @@ class NWProject():
|
||||
# Core Elements
|
||||
self.optState = OptionState(self) # Project-specific GUI options
|
||||
self.projTree = NWTree(self) # The project tree
|
||||
self.langData = {} # Localisation data
|
||||
|
||||
# Project Status
|
||||
self.projOpened = 0 # The time stamp of when the project file was opened
|
||||
@@ -74,7 +78,8 @@ class NWProject():
|
||||
self.projCache = None # The full path to the project's cache folder
|
||||
self.projContent = None # The full path to the project's content folder
|
||||
self.projDict = None # The spell check dictionary
|
||||
self.projLang = None # The spell check language, if different than default
|
||||
self.projSpell = None # The spell check language, if different than default
|
||||
self.projLang = None # The project language, used for builds
|
||||
self.projFile = None # The file name of the project main XML file
|
||||
|
||||
# Project Meta
|
||||
@@ -97,11 +102,12 @@ class NWProject():
|
||||
self.notesWCount = 0 # Total number of words in note files
|
||||
self.doBackup = True # Run project backup on exit
|
||||
|
||||
# Set Defaults
|
||||
self.clearProject()
|
||||
|
||||
# Internal Mapping
|
||||
self.makeAlert = self.theParent.makeAlert
|
||||
self.tr = partial(QCoreApplication.translate, "NWProject")
|
||||
|
||||
# Set Defaults
|
||||
self.clearProject()
|
||||
|
||||
return
|
||||
|
||||
@@ -157,7 +163,7 @@ class NWProject():
|
||||
trashHandle = self.projTree.trashRoot()
|
||||
if trashHandle is None:
|
||||
newItem = NWItem(self)
|
||||
newItem.setName("Trash")
|
||||
newItem.setName(self.tr("Trash"))
|
||||
newItem.setType(nwItemType.TRASH)
|
||||
newItem.setClass(nwItemClass.TRASH)
|
||||
self.projTree.append(None, None, newItem)
|
||||
@@ -189,6 +195,7 @@ class NWProject():
|
||||
self.projCache = None
|
||||
self.projContent = None
|
||||
self.projDict = None
|
||||
self.projSpell = None
|
||||
self.projLang = None
|
||||
self.projFile = nwFiles.PROJ_FILE
|
||||
self.projName = ""
|
||||
@@ -197,7 +204,7 @@ class NWProject():
|
||||
self.autoReplace = {}
|
||||
self.titleFormat = {
|
||||
"title" : r"%title%",
|
||||
"chapter" : r"Chapter %ch%: %title%",
|
||||
"chapter" : self.tr("Chapter")+r" %ch%: %title%",
|
||||
"unnumbered" : r"%title%",
|
||||
"scene" : r"* * *",
|
||||
"section" : r"",
|
||||
@@ -205,15 +212,15 @@ class NWProject():
|
||||
self.spellCheck = False
|
||||
self.autoOutline = True
|
||||
self.statusItems = NWStatus()
|
||||
self.statusItems.addEntry("New", (100, 100, 100))
|
||||
self.statusItems.addEntry("Note", (200, 50, 0))
|
||||
self.statusItems.addEntry("Draft", (200, 150, 0))
|
||||
self.statusItems.addEntry("Finished", (50, 200, 0))
|
||||
self.statusItems.addEntry(self.tr("New"), (100, 100, 100))
|
||||
self.statusItems.addEntry(self.tr("Note"), (200, 50, 0))
|
||||
self.statusItems.addEntry(self.tr("Draft"), (200, 150, 0))
|
||||
self.statusItems.addEntry(self.tr("Finished"), (50, 200, 0))
|
||||
self.importItems = NWStatus()
|
||||
self.importItems.addEntry("New", (100, 100, 100))
|
||||
self.importItems.addEntry("Minor", (200, 50, 0))
|
||||
self.importItems.addEntry("Major", (200, 150, 0))
|
||||
self.importItems.addEntry("Main", (50, 200, 0))
|
||||
self.importItems.addEntry(self.tr("New"), (100, 100, 100))
|
||||
self.importItems.addEntry(self.tr("Minor"), (200, 50, 0))
|
||||
self.importItems.addEntry(self.tr("Major"), (200, 150, 0))
|
||||
self.importItems.addEntry(self.tr("Main"), (50, 200, 0))
|
||||
self.lastEdited = None
|
||||
self.lastViewed = None
|
||||
self.lastWCount = 0
|
||||
@@ -239,7 +246,7 @@ class NWProject():
|
||||
|
||||
# Project Settings
|
||||
projPath = projData.get("projPath", None)
|
||||
projName = projData.get("projName", "New Project")
|
||||
projName = projData.get("projName", self.tr("New Project"))
|
||||
projTitle = projData.get("projTitle", "")
|
||||
projAuthors = projData.get("projAuthors", "")
|
||||
|
||||
@@ -256,7 +263,7 @@ class NWProject():
|
||||
|
||||
titlePage = "# %s\n\n" % (self.bookTitle if self.bookTitle else self.projName)
|
||||
if self.bookAuthors:
|
||||
titlePage = "%sBy %s\n" % (titlePage, ", ".join(self.bookAuthors))
|
||||
titlePage = "%s%s %s\n" % (titlePage, self.tr("By"), self.getAuthors())
|
||||
|
||||
# Document object for writing files
|
||||
aDoc = NWDoc(self, self.theParent)
|
||||
@@ -265,14 +272,14 @@ class NWProject():
|
||||
# Creating a minimal project with a few root folders and a
|
||||
# single chapter folder with a single file.
|
||||
xHandle = {}
|
||||
xHandle[1] = self.newRoot("Novel", nwItemClass.NOVEL)
|
||||
xHandle[2] = self.newRoot("Plot", nwItemClass.PLOT)
|
||||
xHandle[3] = self.newRoot("Characters", nwItemClass.CHARACTER)
|
||||
xHandle[4] = self.newRoot("World", nwItemClass.WORLD)
|
||||
xHandle[5] = self.newFile("Title Page", nwItemClass.NOVEL, xHandle[1])
|
||||
xHandle[6] = self.newFolder("New Chapter", nwItemClass.NOVEL, xHandle[1])
|
||||
xHandle[7] = self.newFile("New Chapter", nwItemClass.NOVEL, xHandle[6])
|
||||
xHandle[8] = self.newFile("New Scene", nwItemClass.NOVEL, xHandle[6])
|
||||
xHandle[1] = self.newRoot(self.tr("Novel"), nwItemClass.NOVEL)
|
||||
xHandle[2] = self.newRoot(self.tr("Plot"), nwItemClass.PLOT)
|
||||
xHandle[3] = self.newRoot(self.tr("Characters"), nwItemClass.CHARACTER)
|
||||
xHandle[4] = self.newRoot(self.tr("World"), nwItemClass.WORLD)
|
||||
xHandle[5] = self.newFile(self.tr("Title Page"), nwItemClass.NOVEL, xHandle[1])
|
||||
xHandle[6] = self.newFolder(self.tr("New Chapter"), nwItemClass.NOVEL, xHandle[1])
|
||||
xHandle[7] = self.newFile(self.tr("New Chapter"), nwItemClass.NOVEL, xHandle[6])
|
||||
xHandle[8] = self.newFile(self.tr("New Scene"), nwItemClass.NOVEL, xHandle[6])
|
||||
|
||||
self.projTree.setFileItemLayout(xHandle[5], nwItemLayout.TITLE)
|
||||
self.projTree.setFileItemLayout(xHandle[7], nwItemLayout.CHAPTER)
|
||||
@@ -282,11 +289,11 @@ class NWProject():
|
||||
aDoc.clearDocument()
|
||||
|
||||
aDoc.openDocument(xHandle[7], showStatus=False)
|
||||
aDoc.saveDocument("## New Chapter\n\n")
|
||||
aDoc.saveDocument("## %s\n\n" % self.tr("New Chapter"))
|
||||
aDoc.clearDocument()
|
||||
|
||||
aDoc.openDocument(xHandle[8], showStatus=False)
|
||||
aDoc.saveDocument("### New Scene\n\n")
|
||||
aDoc.saveDocument("### %s\n\n" % self.tr("New Scene"))
|
||||
aDoc.clearDocument()
|
||||
|
||||
elif popCustom:
|
||||
@@ -295,13 +302,13 @@ class NWProject():
|
||||
# wizard's custom page.
|
||||
|
||||
# Create root folders
|
||||
nHandle = self.newRoot("Novel", nwItemClass.NOVEL)
|
||||
nHandle = self.newRoot(self.tr("Novel"), nwItemClass.NOVEL)
|
||||
for newRoot in projData.get("addRoots", []):
|
||||
if newRoot in nwItemClass:
|
||||
self.newRoot(nwLabels.CLASS_NAME[newRoot], newRoot)
|
||||
self.newRoot(trConst(nwLabels.CLASS_NAME[newRoot]), newRoot)
|
||||
|
||||
# Create a title page
|
||||
tHandle = self.newFile("Title Page", nwItemClass.NOVEL, nHandle)
|
||||
tHandle = self.newFile(self.tr("Title Page"), nwItemClass.NOVEL, nHandle)
|
||||
self.projTree.setFileItemLayout(tHandle, nwItemLayout.TITLE)
|
||||
|
||||
aDoc.openDocument(tHandle, showStatus=False)
|
||||
@@ -316,7 +323,7 @@ class NWProject():
|
||||
# Create chapters
|
||||
if numChapters > 0:
|
||||
for ch in range(numChapters):
|
||||
chTitle = "Chapter %d" % (ch+1)
|
||||
chTitle = self.tr("Chapter {0}").format(f"{ch+1:d}")
|
||||
pHandle = nHandle
|
||||
if chFolders:
|
||||
pHandle = self.newFolder(chTitle, nwItemClass.NOVEL, nHandle)
|
||||
@@ -331,7 +338,7 @@ class NWProject():
|
||||
# Create chapter scenes
|
||||
if numScenes > 0:
|
||||
for sc in range(numScenes):
|
||||
scTitle = "Scene %d.%d" % (ch+1, sc+1)
|
||||
scTitle = self.tr("Scene {0}").format(f"{ch+1:d}.{sc+1:d}")
|
||||
sHandle = self.newFile(scTitle, nwItemClass.NOVEL, pHandle)
|
||||
|
||||
aDoc.openDocument(sHandle, showStatus=False)
|
||||
@@ -341,7 +348,7 @@ class NWProject():
|
||||
# Create scenes (no chapters)
|
||||
elif numScenes > 0:
|
||||
for sc in range(numScenes):
|
||||
scTitle = "Scene %d" % (sc+1)
|
||||
scTitle = self.tr("Scene {0}").format(f"{sc+1:d}")
|
||||
sHandle = self.newFile(scTitle, nwItemClass.NOVEL, nHandle)
|
||||
|
||||
aDoc.openDocument(sHandle, showStatus=False)
|
||||
@@ -365,7 +372,7 @@ class NWProject():
|
||||
if not os.path.isfile(fileName):
|
||||
fileName = os.path.join(fileName, nwFiles.PROJ_FILE)
|
||||
if not os.path.isfile(fileName):
|
||||
self.makeAlert("File not found: %s" % fileName, nwAlert.ERROR)
|
||||
self.makeAlert(self.tr("File not found: {0}").format(fileName), nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
self.clearProject()
|
||||
@@ -414,16 +421,20 @@ class NWProject():
|
||||
try:
|
||||
nwXML = etree.parse(fileName)
|
||||
except Exception as e:
|
||||
self.makeAlert(["Failed to parse project xml.", str(e)], nwAlert.ERROR)
|
||||
self.makeAlert([self.tr("Failed to parse project xml."), str(e)], nwAlert.ERROR)
|
||||
|
||||
# Trying to open backup file instead
|
||||
backFile = fileName[:-3]+"bak"
|
||||
if os.path.isfile(backFile):
|
||||
self.makeAlert("Attempting to open backup project file instead.", nwAlert.INFO)
|
||||
self.makeAlert(
|
||||
self.tr("Attempting to open backup project file instead."), nwAlert.INFO
|
||||
)
|
||||
try:
|
||||
nwXML = etree.parse(backFile)
|
||||
except Exception as e:
|
||||
self.makeAlert(["Failed to parse project xml.", str(e)], nwAlert.ERROR)
|
||||
self.makeAlert(
|
||||
[self.tr("Failed to parse project xml."), str(e)], nwAlert.ERROR
|
||||
)
|
||||
self.clearProject()
|
||||
return False
|
||||
else:
|
||||
@@ -433,15 +444,9 @@ class NWProject():
|
||||
xRoot = nwXML.getroot()
|
||||
nwxRoot = xRoot.tag
|
||||
|
||||
appVersion = xRoot.attrib.get("appVersion", "Unknown")
|
||||
hexVersion = xRoot.attrib.get("hexVersion", "0x0")
|
||||
fileVersion = xRoot.attrib.get("fileVersion", "Unknown")
|
||||
|
||||
# The following are deprecated and will be removed
|
||||
# The settings have been moved to the <project> tag
|
||||
self.saveCount = checkInt(xRoot.attrib.get("saveCount", 0), 0, False)
|
||||
self.autoCount = checkInt(xRoot.attrib.get("autoCount", 0), 0, False)
|
||||
self.editTime = checkInt(xRoot.attrib.get("editTime", 0), 0, False)
|
||||
appVersion = xRoot.attrib.get("appVersion", self.tr("Unknown"))
|
||||
hexVersion = xRoot.attrib.get("hexVersion", "0x0")
|
||||
fileVersion = xRoot.attrib.get("fileVersion", self.tr("Unknown"))
|
||||
|
||||
logger.verbose("XML root is %s" % nwxRoot)
|
||||
logger.verbose("File version is %s" % fileVersion)
|
||||
@@ -451,7 +456,7 @@ class NWProject():
|
||||
|
||||
if not nwxRoot == "novelWriterXML":
|
||||
self.makeAlert(
|
||||
"Project file does not appear to be a novelWriterXML file.",
|
||||
self.tr("Project file does not appear to be a novelWriterXML file."),
|
||||
nwAlert.ERROR
|
||||
)
|
||||
self.clearProject()
|
||||
@@ -469,25 +474,13 @@ class NWProject():
|
||||
# parser will lose the autoReplace settings if allowed to
|
||||
# read the file. Introduced in version 0.10.
|
||||
|
||||
if fileVersion == "1.0":
|
||||
msgYes = self.theParent.askQuestion("Old Project Version", (
|
||||
"The project file and data is created by a novelWriter version "
|
||||
"lower than 0.7. Do you want to upgrade the project to the "
|
||||
"most recent format?<br><br>Note that after the upgrade, you "
|
||||
"cannot open the project with an older version of novelWriter "
|
||||
"any more, so make sure you have a recent backup."
|
||||
))
|
||||
if not msgYes:
|
||||
self.clearProject()
|
||||
return False
|
||||
|
||||
elif fileVersion != "1.1" and fileVersion != "1.2":
|
||||
if fileVersion not in ("1.0", "1.1", "1.2"):
|
||||
self.makeAlert((
|
||||
"Unknown or unsupported novelWriter project file format. "
|
||||
"The project cannot be opened by this version of novelWriter. "
|
||||
"The file was saved with novelWriter version {vers:s}."
|
||||
).format(
|
||||
vers = appVersion,
|
||||
self.tr(
|
||||
"Unknown or unsupported novelWriter project file format. "
|
||||
"The project cannot be opened by this version of novelWriter. "
|
||||
"The file was saved with novelWriter version {0}."
|
||||
).format(appVersion)
|
||||
), nwAlert.ERROR)
|
||||
self.clearProject()
|
||||
return False
|
||||
@@ -496,14 +489,15 @@ class NWProject():
|
||||
# =========================
|
||||
|
||||
if hexToInt(hexVersion) > hexToInt(nw.__hexversion__):
|
||||
msgYes = self.theParent.askQuestion("Version Conflict", (
|
||||
"This project was saved by a newer version of novelWriter, version %s. "
|
||||
"This is version %s. If you continue to open the project, some attributes "
|
||||
"and settings may not be preserved, but the overall project should be fine. "
|
||||
"Continue opening the project?"
|
||||
) % (
|
||||
appVersion, nw.__version__
|
||||
))
|
||||
msgYes = self.theParent.askQuestion(
|
||||
self.tr("Version Conflict"),
|
||||
self.tr(
|
||||
"This project was saved by a newer version of novelWriter, version "
|
||||
"{0}. This is version {1}. If you continue to open the project, "
|
||||
"some attributes and settings may not be preserved, but the "
|
||||
"overall project should be fine. Continue opening the project?"
|
||||
).format(appVersion, nw.__version__)
|
||||
)
|
||||
if not msgYes:
|
||||
self.clearProject()
|
||||
return False
|
||||
@@ -533,10 +527,6 @@ class NWProject():
|
||||
elif xItem.tag == "editTime":
|
||||
self.editTime = checkInt(xItem.text, 0)
|
||||
|
||||
# The following is deprecated, and will be removed
|
||||
elif xItem.tag == "backup":
|
||||
self.doBackup = checkBool(xItem.text, False)
|
||||
|
||||
elif xChild.tag == "settings":
|
||||
logger.debug("Found project settings")
|
||||
for xItem in xChild:
|
||||
@@ -544,10 +534,12 @@ class NWProject():
|
||||
continue
|
||||
if xItem.tag == "doBackup":
|
||||
self.doBackup = checkBool(xItem.text, False)
|
||||
elif xItem.tag == "language":
|
||||
self.projLang = checkString(xItem.text, None, True)
|
||||
elif xItem.tag == "spellCheck":
|
||||
self.spellCheck = checkBool(xItem.text, False)
|
||||
elif xItem.tag == "spellLang":
|
||||
self.projLang = checkString(xItem.text, None, True)
|
||||
self.projSpell = checkString(xItem.text, None, True)
|
||||
elif xItem.tag == "autoOutline":
|
||||
self.autoOutline = checkBool(xItem.text, True)
|
||||
elif xItem.tag == "lastEdited":
|
||||
@@ -566,13 +558,8 @@ class NWProject():
|
||||
self.importItems.unpackXML(xItem)
|
||||
elif xItem.tag == "autoReplace":
|
||||
for xEntry in xItem:
|
||||
if xEntry.tag == "entry":
|
||||
if "key" in xEntry.attrib:
|
||||
self.autoReplace[xEntry.attrib["key"]] = checkString(
|
||||
xEntry.text, None, False
|
||||
)
|
||||
else: # Old format
|
||||
self.autoReplace[xEntry.tag] = checkString(
|
||||
if xEntry.tag == "entry" and "key" in xEntry.attrib:
|
||||
self.autoReplace[xEntry.attrib["key"]] = checkString(
|
||||
xEntry.text, None, False
|
||||
)
|
||||
elif xItem.tag == "titleFormat":
|
||||
@@ -595,16 +582,17 @@ class NWProject():
|
||||
if errList:
|
||||
self.makeAlert(errList, nwAlert.ERROR)
|
||||
|
||||
# Clean up old files
|
||||
# Clean up no longer used files
|
||||
self._deprecatedFiles()
|
||||
|
||||
# Update recent projects
|
||||
self.mainConf.updateRecentCache(self.projPath, self.projName, self.lastWCount, time())
|
||||
self.mainConf.saveRecentCache()
|
||||
|
||||
self.theParent.setStatus("Opened Project: %s" % self.projName)
|
||||
self.theParent.setStatus(self.tr("Opened Project: {0}").format(self.projName))
|
||||
|
||||
self._scanProjectFolder()
|
||||
self._loadProjectLocalisation()
|
||||
|
||||
self.currWCount = self.lastWCount
|
||||
self.projOpened = time()
|
||||
@@ -623,7 +611,7 @@ class NWProject():
|
||||
"""
|
||||
if self.projPath is None:
|
||||
self.makeAlert(
|
||||
"Project path not set, cannot save project.", nwAlert.ERROR
|
||||
self.tr("Project path not set, cannot save project."), nwAlert.ERROR
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -665,8 +653,9 @@ class NWProject():
|
||||
# Save Project Settings
|
||||
xSettings = etree.SubElement(nwXML, "settings")
|
||||
self._packProjectValue(xSettings, "doBackup", self.doBackup)
|
||||
self._packProjectValue(xSettings, "language", self.projLang)
|
||||
self._packProjectValue(xSettings, "spellCheck", self.spellCheck)
|
||||
self._packProjectValue(xSettings, "spellLang", self.projLang)
|
||||
self._packProjectValue(xSettings, "spellLang", self.projSpell)
|
||||
self._packProjectValue(xSettings, "autoOutline", self.autoOutline)
|
||||
self._packProjectValue(xSettings, "lastEdited", self.lastEdited)
|
||||
self._packProjectValue(xSettings, "lastViewed", self.lastViewed)
|
||||
@@ -702,7 +691,7 @@ class NWProject():
|
||||
xml_declaration = True
|
||||
))
|
||||
except Exception as e:
|
||||
self.makeAlert(["Failed to save project.", str(e)], nwAlert.ERROR)
|
||||
self.makeAlert([self.tr("Failed to save project."), str(e)], nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
# If we're here, the file was successfully saved,
|
||||
@@ -721,7 +710,7 @@ class NWProject():
|
||||
self.mainConf.saveRecentCache()
|
||||
|
||||
self._writeLockFile()
|
||||
self.theParent.setStatus("Saved Project: %s" % self.projName)
|
||||
self.theParent.setStatus(self.tr("Saved Project: {0}").format(self.projName))
|
||||
self.setProjectChanged(False)
|
||||
|
||||
return True
|
||||
@@ -773,27 +762,33 @@ class NWProject():
|
||||
return False
|
||||
|
||||
logger.info("Backing up project")
|
||||
self.theParent.setStatus("Backing up project ...")
|
||||
self.theParent.setStatus(self.tr("Backing up project ..."))
|
||||
|
||||
if self.mainConf.backupPath is None or self.mainConf.backupPath == "":
|
||||
self.theParent.makeAlert((
|
||||
"Cannot backup project because no backup path is set. "
|
||||
"Please set a valid backup location in Tools > Preferences."
|
||||
), nwAlert.ERROR)
|
||||
self.theParent.makeAlert(
|
||||
self.tr(
|
||||
"Cannot backup project because no backup path is set. "
|
||||
"Please set a valid backup location in Tools > Preferences."
|
||||
), nwAlert.ERROR
|
||||
)
|
||||
return False
|
||||
|
||||
if self.projName is None or self.projName == "":
|
||||
self.theParent.makeAlert((
|
||||
"Cannot backup project because no project name is set. "
|
||||
"Please set a Working Title in Project > Project Settings."
|
||||
), nwAlert.ERROR)
|
||||
self.theParent.makeAlert(
|
||||
self.tr(
|
||||
"Cannot backup project because no project name is set. "
|
||||
"Please set a Working Title in Project > Project Settings."
|
||||
), nwAlert.ERROR
|
||||
)
|
||||
return False
|
||||
|
||||
if not os.path.isdir(self.mainConf.backupPath):
|
||||
self.theParent.makeAlert((
|
||||
"Cannot backup project because the backup path does not exist. "
|
||||
"Please set a valid backup location in Tools > Preferences."
|
||||
), nwAlert.ERROR)
|
||||
self.theParent.makeAlert(
|
||||
self.tr(
|
||||
"Cannot backup project because the backup path does not exist. "
|
||||
"Please set a valid backup location in Tools > Preferences."
|
||||
), nwAlert.ERROR
|
||||
)
|
||||
return False
|
||||
|
||||
cleanName = makeFileNameSafe(self.projName)
|
||||
@@ -804,20 +799,22 @@ class NWProject():
|
||||
logger.debug("Created folder %s" % baseDir)
|
||||
except Exception as e:
|
||||
self.theParent.makeAlert(
|
||||
["Could not create backup folder.", str(e)],
|
||||
[self.tr("Could not create backup folder."), str(e)],
|
||||
nwAlert.ERROR
|
||||
)
|
||||
return False
|
||||
|
||||
if os.path.commonpath([self.projPath, baseDir]) == self.projPath:
|
||||
self.theParent.makeAlert((
|
||||
"Cannot backup project because the backup path is within the "
|
||||
"project folder to be backed up. Please choose a different "
|
||||
"backup path in Tools > Preferences."
|
||||
), nwAlert.ERROR)
|
||||
self.theParent.makeAlert(
|
||||
self.tr(
|
||||
"Cannot backup project because the backup path is within the "
|
||||
"project folder to be backed up. Please choose a different "
|
||||
"backup path in Tools > Preferences."
|
||||
), nwAlert.ERROR
|
||||
)
|
||||
return False
|
||||
|
||||
archName = "Backup from %s" % formatTimeStamp(time(), fileSafe=True)
|
||||
archName = self.tr("Backup from {0}").format(formatTimeStamp(time(), fileSafe=True))
|
||||
baseName = os.path.join(baseDir, archName)
|
||||
|
||||
try:
|
||||
@@ -827,18 +824,21 @@ class NWProject():
|
||||
logger.info("Backup written to: %s" % archName)
|
||||
if doNotify:
|
||||
self.theParent.makeAlert(
|
||||
"Backup archive file written to: %s.zip" % os.path.join(cleanName, archName),
|
||||
nwAlert.INFO
|
||||
self.tr(
|
||||
"Backup archive file written to: {0}"
|
||||
).format(
|
||||
f"{os.path.join(cleanName, archName)}.zip"
|
||||
), nwAlert.INFO
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.theParent.makeAlert(
|
||||
["Could not write backup archive.", str(e)],
|
||||
[self.tr("Could not write backup archive."), str(e)],
|
||||
nwAlert.ERROR
|
||||
)
|
||||
return False
|
||||
|
||||
self.theParent.setStatus("Project backed up to '%s.zip'" % baseName)
|
||||
self.theParent.setStatus(self.tr("Project backed up to '{0}'").format(f"{baseName}.zip"))
|
||||
|
||||
return True
|
||||
|
||||
@@ -865,7 +865,7 @@ class NWProject():
|
||||
isSuccess = True
|
||||
except Exception as e:
|
||||
self.makeAlert(
|
||||
["Failed to create a new example project.", str(e)], nwAlert.ERROR
|
||||
[self.tr("Failed to create a new example project."), str(e)], nwAlert.ERROR
|
||||
)
|
||||
|
||||
elif os.path.isdir(srcSample):
|
||||
@@ -887,14 +887,16 @@ class NWProject():
|
||||
|
||||
except Exception as e:
|
||||
self.makeAlert(
|
||||
["Failed to create a new example project.", str(e)], nwAlert.ERROR
|
||||
[self.tr("Failed to create a new example project."), str(e)], nwAlert.ERROR
|
||||
)
|
||||
|
||||
else:
|
||||
self.makeAlert((
|
||||
"Failed to create a new example project. Could not find the "
|
||||
"necessary files. They seem to be missing from this installation."
|
||||
), nwAlert.ERROR)
|
||||
self.makeAlert(
|
||||
self.tr(
|
||||
"Failed to create a new example project. Could not find the "
|
||||
"necessary files. They seem to be missing from this installation."
|
||||
), nwAlert.ERROR
|
||||
)
|
||||
|
||||
if isSuccess:
|
||||
self.clearProject()
|
||||
@@ -925,16 +927,18 @@ class NWProject():
|
||||
logger.debug("Created folder %s" % projPath)
|
||||
except Exception as e:
|
||||
self.theParent.makeAlert((
|
||||
["Could not create new project folder.", str(e)]
|
||||
[self.tr("Could not create new project folder."), str(e)]
|
||||
), nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
if os.path.isdir(projPath):
|
||||
if os.listdir(self.projPath):
|
||||
self.theParent.makeAlert((
|
||||
"New project folder is not empty. "
|
||||
"Each project requires a dedicated project folder."
|
||||
), nwAlert.ERROR)
|
||||
self.theParent.makeAlert(
|
||||
self.tr(
|
||||
"New project folder is not empty. "
|
||||
"Each project requires a dedicated project folder."
|
||||
), nwAlert.ERROR
|
||||
)
|
||||
return False
|
||||
|
||||
self.ensureFolderStructure()
|
||||
@@ -981,17 +985,21 @@ class NWProject():
|
||||
self.doBackup = doBackup
|
||||
if doBackup:
|
||||
if not os.path.isdir(self.mainConf.backupPath):
|
||||
self.theParent.makeAlert((
|
||||
"You must set a valid backup path in preferences to use "
|
||||
"the automatic project backup feature."
|
||||
), nwAlert.WARN)
|
||||
self.theParent.makeAlert(
|
||||
self.tr(
|
||||
"You must set a valid backup path in Preferences to use "
|
||||
"the automatic project backup feature."
|
||||
), nwAlert.WARN
|
||||
)
|
||||
return False
|
||||
|
||||
if self.projName == "":
|
||||
self.theParent.makeAlert((
|
||||
"You must set a valid project name in project settings to "
|
||||
"use the automatic project backup feature."
|
||||
), nwAlert.WARN)
|
||||
self.theParent.makeAlert(
|
||||
self.tr(
|
||||
"You must set a valid project name in Project Settings to "
|
||||
"use the automatic project backup feature."
|
||||
), nwAlert.WARN
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -1008,8 +1016,18 @@ class NWProject():
|
||||
"""Set the project-specific spell check language.
|
||||
"""
|
||||
theLang = checkString(theLang, None, True)
|
||||
if self.projSpell != theLang:
|
||||
self.projSpell = theLang
|
||||
self.setProjectChanged(True)
|
||||
return True
|
||||
|
||||
def setProjectLang(self, theLang):
|
||||
"""Set the project-specific language.
|
||||
"""
|
||||
theLang = checkString(theLang, None, True)
|
||||
if self.projLang != theLang:
|
||||
self.projLang = theLang
|
||||
self._loadProjectLocalisation()
|
||||
self.setProjectChanged(True)
|
||||
return True
|
||||
|
||||
@@ -1120,8 +1138,8 @@ class NWProject():
|
||||
if nAuth == 1:
|
||||
authString = self.bookAuthors[0]
|
||||
elif nAuth > 1:
|
||||
authString = "%s and %s" % (
|
||||
", ".join(self.bookAuthors[0:-1]), self.bookAuthors[-1]
|
||||
authString = "%s %s %s" % (
|
||||
", ".join(self.bookAuthors[0:-1]), self.tr("and"), self.bookAuthors[-1]
|
||||
)
|
||||
|
||||
return authString
|
||||
@@ -1197,10 +1215,47 @@ class NWProject():
|
||||
self.importItems.countEntry(nwItem.itemStatus)
|
||||
return
|
||||
|
||||
def localLookup(self, theWord):
|
||||
"""Look up a word in the translation map for the project and
|
||||
return it. The variable is cast to a string before lookup. If
|
||||
the word does not exist, it returns itself.
|
||||
"""
|
||||
theValue = str(theWord)
|
||||
return self.langData.get(theValue, theValue)
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _loadProjectLocalisation(self):
|
||||
"""Load the language data for the current project language.
|
||||
"""
|
||||
if self.projLang is None:
|
||||
self.langData = {}
|
||||
return False
|
||||
|
||||
lngShort = self.projLang.split("_")[0]
|
||||
loadFile = os.path.join(self.mainConf.nwLangPath, "project_en.json")
|
||||
chkFile1 = os.path.join(self.mainConf.nwLangPath, "project_%s.json" % self.projLang)
|
||||
chkFile2 = os.path.join(self.mainConf.nwLangPath, "project_%s.json" % lngShort)
|
||||
|
||||
if os.path.isfile(chkFile1):
|
||||
loadFile = chkFile1
|
||||
elif os.path.isfile(chkFile2):
|
||||
loadFile = chkFile2
|
||||
|
||||
try:
|
||||
with open(loadFile, mode="r", encoding="utf8") as inFile:
|
||||
self.langData = json.load(inFile)
|
||||
logger.debug("Loaded project language file: %s" % os.path.basename(loadFile))
|
||||
|
||||
except Exception:
|
||||
logger.error("Failed to project language file")
|
||||
nw.logException()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _readLockFile(self):
|
||||
"""Reads the lock file in the project folder.
|
||||
"""
|
||||
@@ -1330,7 +1385,7 @@ class NWProject():
|
||||
# Report status
|
||||
if len(orphanFiles) > 0:
|
||||
self.makeAlert(
|
||||
"Found %d orphaned file(s) in project folder." % len(orphanFiles),
|
||||
self.tr("Found {0} orphaned file(s) in project folder.").format(len(orphanFiles)),
|
||||
nwAlert.WARN
|
||||
)
|
||||
else:
|
||||
@@ -1341,6 +1396,7 @@ class NWProject():
|
||||
aDoc = NWDoc(self, self.theParent)
|
||||
nOrph = 0
|
||||
noWhere = False
|
||||
oPrefix = self.tr("Recovered")
|
||||
for oHandle in orphanFiles:
|
||||
|
||||
# Look for meta data
|
||||
@@ -1352,10 +1408,12 @@ class NWProject():
|
||||
oName, oParent, oClass, oLayout = aDoc.getMeta()
|
||||
|
||||
if oName:
|
||||
oName = "Recovered: %s" % oName.lstrip("Recovered: ")
|
||||
oName = self.tr("[{0}] {1}").format(
|
||||
oPrefix, oName.replace("[%s]" % oPrefix, "").strip()
|
||||
)
|
||||
else:
|
||||
nOrph += 1
|
||||
oName = "Recovered File %d" % nOrph
|
||||
oName = self.tr("Recovered File {0}").format(nOrph)
|
||||
|
||||
# Recover file meta data
|
||||
if oClass is None:
|
||||
@@ -1382,10 +1440,12 @@ class NWProject():
|
||||
self.projTree.append(oHandle, oParent, orphItem)
|
||||
|
||||
if noWhere:
|
||||
self.makeAlert((
|
||||
"One or more orphaned files could not be added back into the "
|
||||
"project. Make sure at least a Novel root folder exists."
|
||||
), nwAlert.WARN)
|
||||
self.makeAlert(
|
||||
self.tr(
|
||||
"One or more orphaned files could not be added back into the "
|
||||
"project. Make sure at least a Novel root folder exists."
|
||||
), nwAlert.WARN
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@@ -1441,7 +1501,7 @@ class NWProject():
|
||||
"""
|
||||
theData = os.path.join(self.projPath, theFolder)
|
||||
if not os.path.isdir(theData):
|
||||
errList.append("Not a folder: %s" % theData)
|
||||
errList.append(self.tr("Not a folder: {0}").format(theData))
|
||||
return errList
|
||||
|
||||
logger.info("Old data folder %s found" % theFolder)
|
||||
@@ -1464,7 +1524,7 @@ class NWProject():
|
||||
logger.info("Moved file: %s" % theFile)
|
||||
logger.info("New location: %s" % newPath)
|
||||
except Exception:
|
||||
errList.append("Could not move: %s" % theFile)
|
||||
errList.append(self.tr("Could not move: {0}").format(theFile))
|
||||
logger.error("Could not move: %s" % theFile)
|
||||
nw.logException()
|
||||
|
||||
@@ -1473,7 +1533,7 @@ class NWProject():
|
||||
os.unlink(theFile)
|
||||
logger.info("Deleted file: %s" % theFile)
|
||||
except Exception:
|
||||
errList.append("Could not delete: %s" % theFile)
|
||||
errList.append(self.tr("Could not delete: {0}").format(theFile))
|
||||
logger.error("Could not delete: %s" % theFile)
|
||||
nw.logException()
|
||||
|
||||
@@ -1486,10 +1546,10 @@ class NWProject():
|
||||
# ==================
|
||||
try:
|
||||
os.rmdir(theData)
|
||||
logger.info("Removed folder: %s" % theFolder)
|
||||
logger.info("Deleted folder: %s" % theFolder)
|
||||
except Exception:
|
||||
errList.append("Failed to remove: %s" % theFolder)
|
||||
logger.error("Failed to remove: %s" % theFolder)
|
||||
errList.append(self.tr("Could not delete: {0}").format(theFolder))
|
||||
logger.error("Could not delete: %s" % theFolder)
|
||||
nw.logException()
|
||||
|
||||
return errList
|
||||
@@ -1500,7 +1560,7 @@ class NWProject():
|
||||
"""
|
||||
theJunk = os.path.join(self.projPath, "junk")
|
||||
if not self._checkFolder(theJunk):
|
||||
return "Could not make folder: %s" % theJunk
|
||||
return self.tr("Could not make folder: {0}").format(theJunk)
|
||||
|
||||
theSrc = os.path.join(theDir, theItem)
|
||||
theDst = os.path.join(theJunk, theItem)
|
||||
@@ -1511,7 +1571,7 @@ class NWProject():
|
||||
except Exception:
|
||||
logger.error("Could not move item %s to junk." % theSrc)
|
||||
nw.logException()
|
||||
return "Could not move item %s to junk." % theSrc
|
||||
return self.tr("Could not move item {0} to {1}.").format(theSrc, theJunk)
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
@@ -29,8 +29,6 @@ import logging
|
||||
import os
|
||||
import difflib
|
||||
|
||||
from nw.constants import nwConst, isoLanguage
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# =============================================================================================== #
|
||||
@@ -89,16 +87,6 @@ class NWSpellCheck():
|
||||
"""
|
||||
return "", ""
|
||||
|
||||
@staticmethod
|
||||
def expandLanguage(spTag):
|
||||
"""Translate a language tag to something more user friendly.
|
||||
"""
|
||||
spBits = spTag.split("_")
|
||||
spLang = isoLanguage.ISO_639_1.get(spBits[0], spBits[0])
|
||||
if len(spBits) > 1:
|
||||
spLang += " (%s)" % spBits[1]
|
||||
return spLang
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
@@ -108,26 +96,27 @@ class NWSpellCheck():
|
||||
lookup lists.
|
||||
"""
|
||||
self.projDict = []
|
||||
if projectDict is not None:
|
||||
if not os.path.isfile(projectDict):
|
||||
self.projectDict = None
|
||||
return False
|
||||
else:
|
||||
self.projectDict = projectDict
|
||||
self.projectDict = projectDict
|
||||
|
||||
try:
|
||||
logger.debug("Loading project word list")
|
||||
with open(projectDict, mode="r", encoding="utf-8") as wordsFile:
|
||||
for theLine in wordsFile:
|
||||
theLine = theLine.strip()
|
||||
if len(theLine) > 0 and theLine not in self.projDict:
|
||||
self.projDict.append(theLine)
|
||||
logger.debug("Project word list contains %d words" % len(self.projDict))
|
||||
if projectDict is None:
|
||||
return False
|
||||
|
||||
except Exception:
|
||||
logger.error("Failed to load project word list")
|
||||
nw.logException()
|
||||
return False
|
||||
if not os.path.isfile(projectDict):
|
||||
return False
|
||||
|
||||
try:
|
||||
logger.debug("Loading project word list")
|
||||
with open(projectDict, mode="r", encoding="utf-8") as wordsFile:
|
||||
for theLine in wordsFile:
|
||||
theLine = theLine.strip()
|
||||
if len(theLine) > 0 and theLine not in self.projDict:
|
||||
self.projDict.append(theLine)
|
||||
logger.debug("Project word list contains %d words" % len(self.projDict))
|
||||
|
||||
except Exception:
|
||||
logger.error("Failed to load project word list")
|
||||
nw.logException()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -196,10 +185,10 @@ class NWSpellEnchant(NWSpellCheck):
|
||||
try:
|
||||
import enchant
|
||||
for spTag, spProvider in enchant.list_dicts():
|
||||
spName = "%s [%s]" % (self.expandLanguage(spTag), spProvider.name)
|
||||
retList.append((spTag, spName))
|
||||
retList.append((spTag, spProvider.name))
|
||||
except Exception:
|
||||
logger.error("Failed to list languages for enchant spell checking")
|
||||
|
||||
return retList
|
||||
|
||||
def describeDict(self):
|
||||
@@ -332,8 +321,7 @@ class NWSpellSimple(NWSpellCheck):
|
||||
if fExt != ".dict":
|
||||
continue
|
||||
|
||||
spName = "%s [%s]" % (self.expandLanguage(fRoot), nwConst.SP_INTERNAL)
|
||||
retList.append((fRoot, spName))
|
||||
retList.append((fRoot, "difflib"))
|
||||
|
||||
return retList
|
||||
|
||||
@@ -341,6 +329,6 @@ class NWSpellSimple(NWSpellCheck):
|
||||
"""Return the tag and provider of the currently loaded
|
||||
dictionary.
|
||||
"""
|
||||
return self.theLang, nwConst.SP_INTERNAL
|
||||
return self.theLang, ""
|
||||
|
||||
# END Class NWSpellSimple
|
||||
|
||||
@@ -27,7 +27,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import logging
|
||||
|
||||
from nw.core.tokenizer import Tokenizer
|
||||
from nw.constants import nwLabels, nwKeyWords, nwHtmlUnicode
|
||||
from nw.constants import nwKeyWords, nwLabels, nwHtmlUnicode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -406,17 +406,20 @@ class ToHtml(Tokenizer):
|
||||
"""Apply HTML formatting to synopsis.
|
||||
"""
|
||||
if self.genMode == self.M_PREVIEW:
|
||||
return "<p class='comment'><span class='synopsis'>Synopsis:</span> %s</p>\n" % tText
|
||||
sSynop = self._trSynopsis
|
||||
return f"<p class='comment'><span class='synopsis'>{sSynop}:</span> {tText}</p>\n"
|
||||
else:
|
||||
return "<p class='synopsis'><strong>Synopsis:</strong> %s</p>\n" % tText
|
||||
sSynop = self._localLookup("Synopsis")
|
||||
return f"<p class='synopsis'><strong>{sSynop}:</strong> {tText}</p>\n"
|
||||
|
||||
def _formatComments(self, tText):
|
||||
"""Apply HTML formatting to comments.
|
||||
"""
|
||||
if self.genMode == self.M_PREVIEW:
|
||||
return "<p class='comment'>%s</p>\n" % tText
|
||||
return f"<p class='comment'>{tText}</p>\n"
|
||||
else:
|
||||
return "<p class='comment'><strong>Comment:</strong> %s</p>\n" % tText
|
||||
sComm = self._localLookup("Comment")
|
||||
return f"<p class='comment'><strong>{sComm}:</strong> {tText}</p>\n"
|
||||
|
||||
def _formatKeywords(self, tText):
|
||||
"""Apply HTML formatting to keywords.
|
||||
|
||||
@@ -28,11 +28,14 @@ import logging
|
||||
import re
|
||||
|
||||
from operator import itemgetter
|
||||
from PyQt5.QtCore import QRegularExpression
|
||||
from functools import partial
|
||||
|
||||
from PyQt5.QtCore import QCoreApplication, QRegularExpression
|
||||
|
||||
from nw.enum import nwItemLayout, nwItemType
|
||||
from nw.common import numberToRoman
|
||||
from nw.constants import nwConst, nwRegEx, nwUnicode
|
||||
from nw.core.document import NWDoc
|
||||
from nw.core.tools import numberToWord, numberToRoman
|
||||
from nw.constants import nwConst, nwUnicode, nwItemLayout, nwItemType, nwRegEx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -141,6 +144,13 @@ class Tokenizer():
|
||||
# Error Handling
|
||||
self.errData = []
|
||||
|
||||
# Function Mapping
|
||||
self._localLookup = self.theProject.localLookup
|
||||
self.tr = partial(QCoreApplication.translate, "Tokenizer")
|
||||
|
||||
# Cached Translations
|
||||
self._trSynopsis = self.tr("Synopsis")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
@@ -249,7 +259,7 @@ class Tokenizer():
|
||||
if theItem.itemType != nwItemType.ROOT:
|
||||
return False
|
||||
|
||||
theTitle = "Notes: %s" % theItem.itemName
|
||||
theTitle = "%s: %s" % (self._localLookup("Notes"), theItem.itemName)
|
||||
self.theTokens = []
|
||||
self.theTokens.append((
|
||||
self.T_TITLE, 0, theTitle, None, self.A_PBB | self.A_CENTRE
|
||||
@@ -278,10 +288,10 @@ class Tokenizer():
|
||||
|
||||
docSize = len(self.theText)
|
||||
if docSize > nwConst.MAX_DOCSIZE:
|
||||
errVal = "Document '%s' is too big (%.2f MB). Skipping." % (
|
||||
self.theItem.itemName, docSize/1.0e6
|
||||
errVal = self.tr("Document '{0}' is too big ({1} MB). Skipping.").format(
|
||||
self.theItem.itemName, f"{docSize/1.0e6:.2f}"
|
||||
)
|
||||
self.theText = "# ERROR\n\n%s\n\n" % errVal
|
||||
self.theText = "# %s\n\n%s\n\n" % (self.tr("ERROR"), errVal)
|
||||
self.errData.append(errVal)
|
||||
|
||||
self.isNone = self.theItem.itemLayout == nwItemLayout.NO_LAYOUT
|
||||
@@ -653,11 +663,12 @@ class Tokenizer():
|
||||
theTitle = theTitle.replace(r"%sc%", str(self.numChScene))
|
||||
theTitle = theTitle.replace(r"%sca%", str(self.numAbsScene))
|
||||
if r"%chw%" in theTitle:
|
||||
theTitle = theTitle.replace(r"%chw%", numberToWord(self.numChapter, "en"))
|
||||
theTitle = theTitle.replace(r"%chw%", self._localLookup(self.numChapter))
|
||||
if r"%chi%" in theTitle:
|
||||
theTitle = theTitle.replace(r"%chi%", numberToRoman(self.numChapter, True))
|
||||
if r"%chI%" in theTitle:
|
||||
theTitle = theTitle.replace(r"%chI%", numberToRoman(self.numChapter, False))
|
||||
return theTitle
|
||||
|
||||
return theTitle[:1].upper() + theTitle[1:]
|
||||
|
||||
# END Class Tokenizer
|
||||
|
||||
@@ -26,8 +26,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
|
||||
from nw.core.tokenizer import Tokenizer
|
||||
from nw.constants import nwLabels
|
||||
from nw.core.tokenizer import Tokenizer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -77,7 +77,7 @@ class ToMarkdown(Tokenizer):
|
||||
self.FMT_D_B : "", self.FMT_D_E : "",
|
||||
}
|
||||
else:
|
||||
# GitHub and novelWriter
|
||||
# GitHub
|
||||
mdTags = {
|
||||
self.FMT_B_B : "**", self.FMT_B_E : "**",
|
||||
self.FMT_I_B : "_", self.FMT_I_E : "_",
|
||||
@@ -134,10 +134,10 @@ class ToMarkdown(Tokenizer):
|
||||
thisPar.append(tTemp.rstrip() + " ")
|
||||
|
||||
elif tType == self.T_SYNOPSIS and self.doSynopsis:
|
||||
tmpResult.append("**Synopsis:** %s\n\n" % tText)
|
||||
tmpResult.append("**%s:** %s\n\n" % (self._localLookup("Synopsis"), tText))
|
||||
|
||||
elif tType == self.T_COMMENT and self.doComments:
|
||||
tmpResult.append("**Comment:** %s\n\n" % tText)
|
||||
tmpResult.append("**%s:** %s\n\n" % (self._localLookup("Comment"), tText))
|
||||
|
||||
elif tType == self.T_KEYWORD and self.doKeywords:
|
||||
tmpResult.append(self._formatKeywords(tText, tStyle))
|
||||
|
||||
@@ -29,11 +29,11 @@ import logging
|
||||
|
||||
from lxml import etree
|
||||
from hashlib import sha256
|
||||
from datetime import datetime
|
||||
from zipfile import ZipFile
|
||||
from datetime import datetime
|
||||
|
||||
from nw.constants import nwKeyWords, nwLabels
|
||||
from nw.core.tokenizer import Tokenizer
|
||||
from nw.constants import nwLabels, nwKeyWords
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -298,7 +298,7 @@ class ToOdt(Tokenizer):
|
||||
|
||||
# Meta Data
|
||||
xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "creation-date"))
|
||||
xMeta.text = datetime.now().isoformat()
|
||||
xMeta.text = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
|
||||
|
||||
xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "generator"))
|
||||
xMeta.text = f"novelWriter/{nw.__version__}"
|
||||
@@ -507,15 +507,17 @@ class ToOdt(Tokenizer):
|
||||
def _formatSynopsis(self, tText):
|
||||
"""Apply formatting to synopsis lines.
|
||||
"""
|
||||
rTxt = "**Synopsis:** %s" % tText
|
||||
rFmt = "_B b_ %s" % (" "*len(tText))
|
||||
sSynop = self._localLookup("Synopsis")
|
||||
rTxt = "**%s:** %s" % (sSynop, tText)
|
||||
rFmt = "_B%s b_ %s" % (" "*len(sSynop), " "*len(tText))
|
||||
return rTxt, rFmt
|
||||
|
||||
def _formatComments(self, tText):
|
||||
"""Apply formatting to comments.
|
||||
"""
|
||||
rTxt = "**Comment:** %s" % tText
|
||||
rFmt = "_B b_ %s" % (" "*len(tText))
|
||||
sComm = self._localLookup("Comment")
|
||||
rTxt = "**%s:** %s" % (sComm, tText)
|
||||
rFmt = "_B%s b_ %s" % (" "*len(sComm), " "*len(tText))
|
||||
return rTxt, rFmt
|
||||
|
||||
def _formatKeywords(self, tText):
|
||||
@@ -530,7 +532,7 @@ class ToOdt(Tokenizer):
|
||||
if theBits[0] in nwLabels.KEY_NAME:
|
||||
tText = nwLabels.KEY_NAME[theBits[0]]
|
||||
rTxt += "**%s:** " % tText
|
||||
rFmt += "_B%s b_ " % (" "*len(tText))
|
||||
rFmt += "_B%s b_ " % (" "*len(tText))
|
||||
if len(theBits) > 1:
|
||||
if theBits[0] == nwKeyWords.TAG_KEY:
|
||||
rTxt += "%s" % theBits[1]
|
||||
@@ -561,7 +563,7 @@ class ToOdt(Tokenizer):
|
||||
##
|
||||
|
||||
if len(theText) != len(theFmt):
|
||||
# Genrate dummy format if there isn't any
|
||||
# Generate dummy format if there isn't any or it doesn't match
|
||||
theFmt = " "*len(theText)
|
||||
|
||||
# XML functions
|
||||
|
||||
193
nw/core/tools.py
193
nw/core/tools.py
@@ -1,193 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – Various Tools
|
||||
===========================
|
||||
Various core tool functions
|
||||
|
||||
File History:
|
||||
Created: 2019-04-22 [0.0.1] countWords
|
||||
Created: 2019-10-13 [0.2.3] numberToWord, _numberToWordEN
|
||||
Merged: 2020-05-08 [0.4.5] All of the above into this file
|
||||
Created: 2020-07-05 [0.10.0] numberToRoman
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, 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/>.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from nw.constants import nwUnicode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# =============================================================================================== #
|
||||
# Simple Word Counter
|
||||
# =============================================================================================== #
|
||||
|
||||
def countWords(theText):
|
||||
"""Count words in a piece of text, skipping special syntax and
|
||||
comments.
|
||||
"""
|
||||
charCount = 0
|
||||
wordCount = 0
|
||||
paraCount = 0
|
||||
prevEmpty = True
|
||||
|
||||
# We need to treat dashes as word separators for counting words.
|
||||
# The check+replace apprach is much faster that direct replace for
|
||||
# large texts, and a bit slower for small texts, but in the latter
|
||||
# case it doesn't matter.
|
||||
if nwUnicode.U_ENDASH in theText:
|
||||
theText = theText.replace(nwUnicode.U_ENDASH, " ")
|
||||
if nwUnicode.U_EMDASH in theText:
|
||||
theText = theText.replace(nwUnicode.U_EMDASH, " ")
|
||||
|
||||
for aLine in theText.splitlines():
|
||||
|
||||
countPara = True
|
||||
theLen = len(aLine)
|
||||
|
||||
if theLen == 0:
|
||||
prevEmpty = True
|
||||
continue
|
||||
if aLine[0] == "@" or aLine[0] == "%":
|
||||
continue
|
||||
|
||||
if aLine[0:5] == "#### ":
|
||||
wordCount -= 1
|
||||
charCount -= 5
|
||||
countPara = False
|
||||
elif aLine[0:4] == "### ":
|
||||
wordCount -= 1
|
||||
charCount -= 4
|
||||
countPara = False
|
||||
elif aLine[0:3] == "## ":
|
||||
wordCount -= 1
|
||||
charCount -= 3
|
||||
countPara = False
|
||||
elif aLine[0:2] == "# ":
|
||||
wordCount -= 1
|
||||
charCount -= 2
|
||||
countPara = False
|
||||
|
||||
wordCount += len(aLine.split())
|
||||
charCount += theLen
|
||||
if countPara and prevEmpty:
|
||||
paraCount += 1
|
||||
|
||||
prevEmpty = not countPara
|
||||
|
||||
return charCount, wordCount, paraCount
|
||||
|
||||
# =============================================================================================== #
|
||||
# Convert an Integer to a Roman Number
|
||||
# =============================================================================================== #
|
||||
|
||||
def numberToRoman(numVal, isLower=False):
|
||||
"""Convert an integer to a roman number.
|
||||
"""
|
||||
if not isinstance(numVal, int):
|
||||
return "NAN"
|
||||
if numVal < 1 or numVal > 4999:
|
||||
return "OOR"
|
||||
|
||||
theValues = [
|
||||
(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"), (100, "C"), (90, "XC"),
|
||||
(50, "L"), (40, "XL"), (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"),
|
||||
]
|
||||
|
||||
romNum = ""
|
||||
for theDiv, theSym in theValues:
|
||||
n = numVal//theDiv
|
||||
romNum += n*theSym
|
||||
numVal -= n*theDiv
|
||||
if numVal <= 0:
|
||||
break
|
||||
|
||||
return romNum.lower() if isLower else romNum
|
||||
|
||||
# =============================================================================================== #
|
||||
# Convert an Integer to a Word Number
|
||||
# =============================================================================================== #
|
||||
|
||||
def numberToWord(numVal, theLanguage):
|
||||
"""Wrapper for converting numbers to words for chapter headings.
|
||||
"""
|
||||
numWord = ""
|
||||
if theLanguage == "en":
|
||||
numWord = _numberToWordEN(numVal)
|
||||
else:
|
||||
numWord = _numberToWordEN(numVal)
|
||||
return numWord
|
||||
|
||||
def _numberToWordEN(numVal):
|
||||
"""Convert numbers to English words.
|
||||
"""
|
||||
oneWord = ""
|
||||
tenWord = ""
|
||||
hunWord = ""
|
||||
|
||||
if not isinstance(numVal, int):
|
||||
return "[NaN]"
|
||||
|
||||
if numVal < 0:
|
||||
return "[Negative]"
|
||||
|
||||
if numVal > 999:
|
||||
return "[Out of Range]"
|
||||
|
||||
if numVal == 0:
|
||||
return "Zero"
|
||||
|
||||
oneVal = numVal % 10
|
||||
tenVal = (numVal - oneVal) % 100
|
||||
hunVal = (numVal - tenVal - oneVal) % 1000
|
||||
|
||||
theHundreds = {
|
||||
100: "One Hundred", 200: "Two Hundred", 300: "Three Hundred",
|
||||
400: "Four Hundred", 500: "Five Hundred", 600: "Six Hundred",
|
||||
700: "Seven Hundred", 800: "Eight Hundred", 900: "Nine Hundred",
|
||||
}
|
||||
theTens = {
|
||||
20: "Twenty", 30: "Thirty", 40: "Forty", 50: "Fifty",
|
||||
60: "Sixty", 70: "Seventy", 80: "Eighty", 90: "Ninety",
|
||||
}
|
||||
theTeens = {
|
||||
0: "Ten", 1: "Eleven", 2: "Twelve", 3: "Thirteen", 4: "Fourteen",
|
||||
5: "Fifteen", 6: "Sixteen", 7: "Seventeen", 8: "Eighteen", 9: "Nineteen",
|
||||
}
|
||||
theOnes = {
|
||||
0: "", 1: "One", 2: "Two", 3: "Three", 4: "Four",
|
||||
5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine",
|
||||
}
|
||||
|
||||
retVale = ""
|
||||
hunWord = theHundreds.get(hunVal, "")
|
||||
if tenVal == 10:
|
||||
oneWord = theTeens.get(oneVal, "")
|
||||
retVale = f"{hunWord} {oneWord}".strip()
|
||||
else:
|
||||
oneWord = theOnes.get(oneVal, "")
|
||||
if tenVal == 0:
|
||||
retVale = f"{hunWord} {oneWord}".strip()
|
||||
else:
|
||||
tenWord = theTens.get(tenVal, "")
|
||||
if oneVal == 0:
|
||||
retVale = f"{hunWord} {tenWord}".strip()
|
||||
else:
|
||||
retVale = f"{hunWord} {tenWord}-{oneWord}".strip()
|
||||
|
||||
return retVale
|
||||
@@ -29,14 +29,13 @@ import logging
|
||||
import os
|
||||
|
||||
from lxml import etree
|
||||
from hashlib import sha256
|
||||
from time import time
|
||||
from hashlib import sha256
|
||||
|
||||
from nw.core.item import NWItem
|
||||
from nw.enum import nwItemType, nwItemClass, nwItemLayout
|
||||
from nw.common import checkHandle
|
||||
from nw.constants import (
|
||||
nwFiles, nwItemType, nwItemClass, nwItemLayout, nwConst, nwLists
|
||||
)
|
||||
from nw.constants import nwConst, nwLists, nwFiles
|
||||
from nw.core.item import NWItem
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
23
nw/dialogs/__init__.py
Normal file
23
nw/dialogs/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from nw.dialogs.about import GuiAbout
|
||||
from nw.dialogs.docmerge import GuiDocMerge
|
||||
from nw.dialogs.docsplit import GuiDocSplit
|
||||
from nw.dialogs.itemeditor import GuiItemEditor
|
||||
from nw.dialogs.preferences import GuiPreferences
|
||||
from nw.dialogs.projload import GuiProjectLoad
|
||||
from nw.dialogs.projsettings import GuiProjectSettings
|
||||
from nw.dialogs.quotes import GuiQuoteSelect
|
||||
from nw.dialogs.wordlist import GuiWordList
|
||||
|
||||
__all__ = [
|
||||
"GuiAbout",
|
||||
"GuiDocMerge",
|
||||
"GuiDocSplit",
|
||||
"GuiItemEditor",
|
||||
"GuiPreferences",
|
||||
"GuiProjectLoad",
|
||||
"GuiProjectSettings",
|
||||
"GuiQuoteSelect",
|
||||
"GuiWordList",
|
||||
]
|
||||
@@ -55,7 +55,7 @@ class GuiAbout(QDialog):
|
||||
self.innerBox = QHBoxLayout()
|
||||
self.innerBox.setSpacing(self.mainConf.pxInt(16))
|
||||
|
||||
self.setWindowTitle("About novelWriter")
|
||||
self.setWindowTitle(self.tr("About novelWriter"))
|
||||
self.setMinimumWidth(self.mainConf.pxInt(650))
|
||||
self.setMinimumHeight(self.mainConf.pxInt(600))
|
||||
|
||||
@@ -90,9 +90,9 @@ class GuiAbout(QDialog):
|
||||
|
||||
# Main Tab Area
|
||||
self.tabBox = QTabWidget()
|
||||
self.tabBox.addTab(self.pageAbout, "About")
|
||||
self.tabBox.addTab(self.pageNotes, "Release")
|
||||
self.tabBox.addTab(self.pageLicense, "License")
|
||||
self.tabBox.addTab(self.pageAbout, self.tr("About"))
|
||||
self.tabBox.addTab(self.pageNotes, self.tr("Release"))
|
||||
self.tabBox.addTab(self.pageLicense, self.tr("Licence"))
|
||||
self.innerBox.addWidget(self.tabBox)
|
||||
|
||||
# OK Button
|
||||
@@ -131,81 +131,92 @@ class GuiAbout(QDialog):
|
||||
def _fillAboutPage(self):
|
||||
"""Generate the content for the About page.
|
||||
"""
|
||||
listPrefix = " • "
|
||||
aboutMsg = (
|
||||
"<h2>About novelWriter</h2>"
|
||||
"<p>{copyright:s}.</p>"
|
||||
"<p>Website: <a href='{website:s}'>{domain:s}</a></p>"
|
||||
"<p>novelWriter is a markdown-like text editor designed for "
|
||||
"organising and writing novels. It is written in Python 3 with a "
|
||||
"Qt5 GUI, using PyQt5.</p>"
|
||||
"<p>novelWriter 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.</p>"
|
||||
"<p>novelWriter 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.</p>"
|
||||
"<p>See the License tab for the full license text, or visit the "
|
||||
"GNU website at "
|
||||
"<a href='https://www.gnu.org/licenses/gpl-3.0.html'>GPL v3.0</a> "
|
||||
"for more details.</p>"
|
||||
"<h3>Credits</h3>"
|
||||
"<p>{credits:s}</p>"
|
||||
aboutMsg = (
|
||||
"<h2>{title1}</h2>"
|
||||
"<p>{copy}</p>"
|
||||
"<p>{link}</p>"
|
||||
"<p>{intro}</p>"
|
||||
"<p>{license1}</p>"
|
||||
"<p>{license2}</p>"
|
||||
"<p>{license3}</p>"
|
||||
"<h3>{title2}</h3>"
|
||||
"<p>{credits}</p>"
|
||||
).format(
|
||||
copyright = nw.__copyright__,
|
||||
website = nw.__url__,
|
||||
domain = nw.__domain__,
|
||||
credits = "<br/>".join(["%s%s" % (listPrefix, x) for x in nw.__credits__]),
|
||||
title1 = self.tr("About novelWriter"),
|
||||
copy = nw.__copyright__,
|
||||
link = self.tr("Website: {0}").format(f"<a href='{nw.__url__}'>{nw.__domain__}</a>"),
|
||||
title2 = self.tr("Credits"),
|
||||
credits = self._wrapTable([
|
||||
(self.tr("Developer"), "Veronica Berglyd Olsen"),
|
||||
(self.tr("Concept"), "Veronica Berglyd Olsen, Marian Lückhof"),
|
||||
(self.tr("i18n"), "Bruno Meneguello"),
|
||||
]),
|
||||
intro = self.tr(
|
||||
"novelWriter is a markdown-like text editor designed for organising and "
|
||||
"writing novels. It is written in Python 3 with a Qt5 GUI, using PyQt5."
|
||||
),
|
||||
license1 = self.tr(
|
||||
"novelWriter 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."
|
||||
),
|
||||
license2 = self.tr(
|
||||
"novelWriter 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."
|
||||
),
|
||||
license3 = self.tr(
|
||||
"See the Licence tab for the full licence text, or visit the "
|
||||
"GNU website at {0} for more details."
|
||||
).format(
|
||||
"<a href='https://www.gnu.org/licenses/gpl-3.0.html'>GPL v3.0</a>"
|
||||
),
|
||||
)
|
||||
|
||||
aboutMsg += "<h4>%s</h4><p>%s</p>" % (
|
||||
self.tr("Translations"),
|
||||
self._wrapTable([
|
||||
("English", "Veronica Berglyd Olsen"),
|
||||
("Français", "Jan Lüdke (jyhelle)"),
|
||||
("Norsk Bokmål", "Veronica Berglyd Olsen"),
|
||||
("Português", "Bruno Meneguello"),
|
||||
])
|
||||
)
|
||||
|
||||
theTheme = self.theParent.theTheme
|
||||
theIcons = self.theParent.theTheme.theIcons
|
||||
if theTheme.themeName:
|
||||
aboutMsg += (
|
||||
"<h4>Theme: {name:s}</h4>"
|
||||
"<p>"
|
||||
"<b>Author:</b> {author:s}<br/>"
|
||||
"<b>Credit:</b> {credit:s}<br/>"
|
||||
"<b>License:</b> <a href='{lic_url:s}'>{license:s}</a>"
|
||||
"</p>"
|
||||
).format(
|
||||
name = theTheme.themeName,
|
||||
author = theTheme.themeAuthor,
|
||||
credit = theTheme.themeCredit,
|
||||
license = theTheme.themeLicense,
|
||||
lic_url = theTheme.themeLicenseUrl,
|
||||
if theTheme.themeName and theTheme.themeAuthor != "N/A":
|
||||
licURL = f"<a href='{theTheme.themeLicenseUrl}'>{theTheme.themeLicense}</a>"
|
||||
aboutMsg += "<h4>%s</h4><p>%s</p>" % (
|
||||
self.tr("Theme: {0}").format(theTheme.themeName),
|
||||
self._wrapTable([
|
||||
(self.tr("Author"), theTheme.themeAuthor),
|
||||
(self.tr("Credit"), theTheme.themeCredit),
|
||||
(self.tr("Licence"), licURL),
|
||||
])
|
||||
)
|
||||
|
||||
if theIcons.themeName:
|
||||
aboutMsg += (
|
||||
"<h4>Icons: {name:s}</h4>"
|
||||
"<p>"
|
||||
"<b>Author:</b> {author:s}<br/>"
|
||||
"<b>Credit:</b> {credit:s}<br/>"
|
||||
"<b>License:</b> <a href='{lic_url:s}'>{license:s}</a>"
|
||||
"</p>"
|
||||
).format(
|
||||
name = theIcons.themeName,
|
||||
author = theIcons.themeAuthor,
|
||||
credit = theIcons.themeCredit,
|
||||
license = theIcons.themeLicense,
|
||||
lic_url = theIcons.themeLicenseUrl,
|
||||
licURL = f"<a href='{theIcons.themeLicenseUrl}'>{theIcons.themeLicense}</a>"
|
||||
aboutMsg += "<h4>%s</h4><p>%s</p>" % (
|
||||
self.tr("Icons: {0}").format(theIcons.themeName),
|
||||
self._wrapTable([
|
||||
(self.tr("Author"), theIcons.themeAuthor),
|
||||
(self.tr("Credit"), theIcons.themeCredit),
|
||||
(self.tr("Licence"), licURL),
|
||||
])
|
||||
)
|
||||
|
||||
if theTheme.syntaxName:
|
||||
aboutMsg += (
|
||||
"<h4>Syntax: {name:s}</h4>"
|
||||
"<p>"
|
||||
"<b>Author:</b> {author:s}<br/>"
|
||||
"<b>Credit:</b> {credit:s}<br/>"
|
||||
"<b>License:</b> <a href='{lic_url:s}'>{license:s}</a>"
|
||||
"</p>"
|
||||
).format(
|
||||
name = theTheme.syntaxName,
|
||||
author = theTheme.syntaxAuthor,
|
||||
credit = theTheme.syntaxCredit,
|
||||
license = theTheme.syntaxLicense,
|
||||
lic_url = theTheme.syntaxLicenseUrl,
|
||||
licURL = f"<a href='{theTheme.syntaxLicenseUrl}'>{theTheme.syntaxLicense}</a>"
|
||||
aboutMsg += "<h4>%s</h4><p>%s</p>" % (
|
||||
self.tr("Syntax: {0}").format(theTheme.syntaxName),
|
||||
self._wrapTable([
|
||||
(self.tr("Author"), theTheme.syntaxAuthor),
|
||||
(self.tr("Credit"), theTheme.syntaxCredit),
|
||||
(self.tr("Licence"), licURL),
|
||||
])
|
||||
)
|
||||
|
||||
self.pageAbout.setHtml(aboutMsg)
|
||||
@@ -225,7 +236,7 @@ class GuiAbout(QDialog):
|
||||
return
|
||||
|
||||
def _fillLicensePage(self):
|
||||
"""Load the content for the License page.
|
||||
"""Load the content for the Licence page.
|
||||
"""
|
||||
docPath = os.path.join(self.mainConf.assetPath, "text", "gplv3_en.htm")
|
||||
if os.path.isfile(docPath):
|
||||
@@ -233,9 +244,19 @@ class GuiAbout(QDialog):
|
||||
helpText = inFile.read()
|
||||
self.pageLicense.setHtml(helpText)
|
||||
else:
|
||||
self.pageLicense.setHtml("Error loading license text ...")
|
||||
self.pageLicense.setHtml("Error loading licence text ...")
|
||||
return
|
||||
|
||||
def _wrapTable(self, theData):
|
||||
"""Wrap a list of label/value tuples in a html table.
|
||||
"""
|
||||
theTable = []
|
||||
for aLabel, aValue in theData:
|
||||
theTable.append(
|
||||
f"<tr><td><b>{aLabel}:</b></td><td>{aValue}</td></tr>"
|
||||
)
|
||||
return "<table>%s</table>" % "".join(theTable)
|
||||
|
||||
def _setStyleSheet(self):
|
||||
"""Set stylesheet for all browser tabs
|
||||
"""
|
||||
@@ -246,6 +267,9 @@ class GuiAbout(QDialog):
|
||||
"a {{"
|
||||
" color: rgb({hColR},{hColG},{hColB});"
|
||||
"}}\n"
|
||||
"td {{"
|
||||
" padding-right: 0.8em;"
|
||||
"}}\n"
|
||||
).format(
|
||||
hColR = self.theParent.theTheme.colHead[0],
|
||||
hColG = self.theParent.theTheme.colHead[1],
|
||||
@@ -33,9 +33,9 @@ from PyQt5.QtWidgets import (
|
||||
QListWidgetItem, QDialogButtonBox
|
||||
)
|
||||
|
||||
from nw.constants import nwAlert, nwItemType
|
||||
from nw.gui.custom import QHelpLabel
|
||||
from nw.core import NWDoc
|
||||
from nw.enum import nwAlert, nwItemType
|
||||
from nw.gui.custom import QHelpLabel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -53,11 +53,11 @@ class GuiDocMerge(QDialog):
|
||||
self.sourceItem = None
|
||||
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.setWindowTitle("Merge Documents")
|
||||
self.setWindowTitle(self.tr("Merge Documents"))
|
||||
|
||||
self.headLabel = QLabel("<b>Documents to Merge</b>")
|
||||
self.headLabel = QLabel("<b>%s</b>" % self.tr("Documents to Merge"))
|
||||
self.helpLabel = QHelpLabel(
|
||||
"Drag and drop items to change the order.", self.theParent.theTheme.helpText
|
||||
self.tr("Drag and drop items to change the order."), self.theParent.theTheme.helpText
|
||||
)
|
||||
|
||||
self.listBox = QListWidget()
|
||||
@@ -102,9 +102,9 @@ class GuiDocMerge(QDialog):
|
||||
finalOrder.append(self.listBox.item(i).data(Qt.UserRole))
|
||||
|
||||
if len(finalOrder) == 0:
|
||||
self.theParent.makeAlert((
|
||||
"No source documents found. Nothing to do."
|
||||
), nwAlert.ERROR)
|
||||
self.theParent.makeAlert(
|
||||
self.tr("No source documents found. Nothing to do."), nwAlert.ERROR
|
||||
)
|
||||
return
|
||||
|
||||
theDoc = NWDoc(self.theProject, self.theParent)
|
||||
@@ -114,16 +114,16 @@ class GuiDocMerge(QDialog):
|
||||
theText += "\n\n"
|
||||
|
||||
if self.sourceItem is None:
|
||||
self.theParent.makeAlert((
|
||||
"No source document selected. Nothing to do."
|
||||
), nwAlert.ERROR)
|
||||
self.theParent.makeAlert(
|
||||
self.tr("No source document selected. Nothing to do."), nwAlert.ERROR
|
||||
)
|
||||
return
|
||||
|
||||
srcItem = self.theProject.projTree[self.sourceItem]
|
||||
if srcItem is None:
|
||||
self.theParent.makeAlert((
|
||||
"Could not parse source document."
|
||||
), nwAlert.ERROR)
|
||||
self.theParent.makeAlert(
|
||||
self.tr("Could not parse source document."), nwAlert.ERROR
|
||||
)
|
||||
return
|
||||
|
||||
nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemClass, srcItem.itemParent)
|
||||
@@ -164,9 +164,9 @@ class GuiDocMerge(QDialog):
|
||||
if nwItem is None:
|
||||
return
|
||||
if nwItem.itemType is not nwItemType.FOLDER:
|
||||
self.theParent.makeAlert((
|
||||
"Element selected in the project tree must be a folder."
|
||||
), nwAlert.ERROR)
|
||||
self.theParent.makeAlert(
|
||||
self.tr("Element selected in the project tree must be a folder."), nwAlert.ERROR
|
||||
)
|
||||
return
|
||||
|
||||
for sHandle in self.theParent.treeView.getTreeFromHandle(tHandle):
|
||||
@@ -34,10 +34,9 @@ from PyQt5.QtWidgets import (
|
||||
)
|
||||
|
||||
from nw.core import NWDoc
|
||||
from nw.enum import nwAlert, nwItemType, nwItemClass, nwItemLayout
|
||||
from nw.constants import nwConst
|
||||
from nw.gui.custom import QHelpLabel
|
||||
from nw.constants import (
|
||||
nwAlert, nwItemType, nwItemClass, nwItemLayout, nwConst
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -56,11 +55,12 @@ class GuiDocSplit(QDialog):
|
||||
self.sourceItem = None
|
||||
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.setWindowTitle("Split Document")
|
||||
self.setWindowTitle(self.tr("Split Document"))
|
||||
|
||||
self.headLabel = QLabel("<b>Document Headers</b>")
|
||||
self.headLabel = QLabel("<b>%s</b>" % self.tr("Document Headers"))
|
||||
self.helpLabel = QHelpLabel(
|
||||
"Select the maximum level to split into files.", self.theParent.theTheme.helpText
|
||||
self.tr("Select the maximum level to split into files."),
|
||||
self.theParent.theTheme.helpText
|
||||
)
|
||||
|
||||
self.listBox = QListWidget()
|
||||
@@ -69,10 +69,10 @@ class GuiDocSplit(QDialog):
|
||||
self.listBox.setMinimumHeight(self.mainConf.pxInt(180))
|
||||
|
||||
self.splitLevel = QComboBox(self)
|
||||
self.splitLevel.addItem("Split on Header Level 1 (Title)", 1)
|
||||
self.splitLevel.addItem("Split up to Header Level 2 (Chapter)", 2)
|
||||
self.splitLevel.addItem("Split up to Header Level 3 (Scene)", 3)
|
||||
self.splitLevel.addItem("Split up to Header Level 4 (Section)", 4)
|
||||
self.splitLevel.addItem(self.tr("Split on Header Level 1 (Title)"), 1)
|
||||
self.splitLevel.addItem(self.tr("Split up to Header Level 2 (Chapter)"), 2)
|
||||
self.splitLevel.addItem(self.tr("Split up to Header Level 3 (Scene)"), 3)
|
||||
self.splitLevel.addItem(self.tr("Split up to Header Level 4 (Section)"), 4)
|
||||
spIndex = self.splitLevel.findData(
|
||||
self.optState.getInt("GuiDocSplit", "spLevel", 3)
|
||||
)
|
||||
@@ -115,16 +115,16 @@ class GuiDocSplit(QDialog):
|
||||
logger.verbose("GuiDocSplit split button clicked")
|
||||
|
||||
if self.sourceItem is None:
|
||||
self.theParent.makeAlert((
|
||||
"No source document selected. Nothing to do."
|
||||
), nwAlert.ERROR)
|
||||
self.theParent.makeAlert(
|
||||
self.tr("No source document selected. Nothing to do."), nwAlert.ERROR
|
||||
)
|
||||
return
|
||||
|
||||
srcItem = self.theProject.projTree[self.sourceItem]
|
||||
if srcItem is None:
|
||||
self.theParent.makeAlert((
|
||||
"Could not parse source document."
|
||||
), nwAlert.ERROR)
|
||||
self.theParent.makeAlert(
|
||||
self.tr("Could not parse source document."), nwAlert.ERROR
|
||||
)
|
||||
return
|
||||
|
||||
theDoc = NWDoc(self.theProject, self.theParent)
|
||||
@@ -147,26 +147,34 @@ class GuiDocSplit(QDialog):
|
||||
|
||||
nFiles = len(finalOrder)
|
||||
if nFiles == 0:
|
||||
self.theParent.makeAlert((
|
||||
"No headers found. Nothing to do."
|
||||
), nwAlert.ERROR)
|
||||
self.theParent.makeAlert(
|
||||
self.tr("No headers found. Nothing to do."), nwAlert.ERROR
|
||||
)
|
||||
return
|
||||
|
||||
# Check that another folder can be created
|
||||
parTree = self.theProject.projTree.getItemPath(srcItem.itemParent)
|
||||
if len(parTree) >= nwConst.MAX_DEPTH - 1:
|
||||
self.theParent.makeAlert((
|
||||
"Cannot add new folder for the document split. "
|
||||
"Maximum folder depth has been reached. "
|
||||
"Please move the file to another level in the project tree."
|
||||
), nwAlert.ERROR)
|
||||
self.theParent.makeAlert(
|
||||
self.tr(
|
||||
"Cannot add new folder for the document split. "
|
||||
"Maximum folder depth has been reached. "
|
||||
"Please move the file to another level in the project tree."
|
||||
), nwAlert.ERROR
|
||||
)
|
||||
return
|
||||
|
||||
msgYes = self.theParent.askQuestion("Split Document", (
|
||||
"The document will be split into %d file(s) in a new folder. "
|
||||
"The original document will remain intact.<br><br>"
|
||||
"Continue with the splitting process?"
|
||||
) % nFiles)
|
||||
msgYes = self.theParent.askQuestion(
|
||||
self.tr("Split Document"),
|
||||
"%s<br><br>%s" % (
|
||||
self.tr(
|
||||
"The document will be split into {0} file(s) in a new folder. "
|
||||
"The original document will remain intact.").format(nFiles),
|
||||
self.tr(
|
||||
"Continue with the splitting process?"
|
||||
)
|
||||
)
|
||||
)
|
||||
if not msgYes:
|
||||
return
|
||||
|
||||
@@ -242,9 +250,9 @@ class GuiDocSplit(QDialog):
|
||||
if nwItem is None:
|
||||
return
|
||||
if nwItem.itemType is not nwItemType.FILE:
|
||||
self.theParent.makeAlert((
|
||||
"Element selected in the project tree must be a file."
|
||||
), nwAlert.ERROR)
|
||||
self.theParent.makeAlert(
|
||||
self.tr("Element selected in the project tree must be a file."), nwAlert.ERROR
|
||||
)
|
||||
return
|
||||
|
||||
self.listBox.clear()
|
||||
@@ -33,8 +33,9 @@ from PyQt5.QtWidgets import (
|
||||
QDialogButtonBox
|
||||
)
|
||||
|
||||
from nw.enum import nwItemLayout, nwItemType
|
||||
from nw.constants import trConst, nwLists, nwLabels
|
||||
from nw.gui.custom import QSwitch
|
||||
from nw.constants import nwLabels, nwItemLayout, nwItemType, nwLists
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -58,7 +59,7 @@ class GuiItemEditor(QDialog):
|
||||
if self.theItem is None:
|
||||
self._doClose()
|
||||
|
||||
self.setWindowTitle("Item Settings")
|
||||
self.setWindowTitle(self.tr("Item Settings"))
|
||||
|
||||
mVd = self.mainConf.pxInt(220)
|
||||
mSp = self.mainConf.pxInt(16)
|
||||
@@ -103,10 +104,10 @@ class GuiItemEditor(QDialog):
|
||||
|
||||
for itemLayout in nwItemLayout:
|
||||
if itemLayout in validLayouts:
|
||||
self.editLayout.addItem(nwLabels.LAYOUT_NAME[itemLayout], itemLayout)
|
||||
self.editLayout.addItem(trConst(nwLabels.LAYOUT_NAME[itemLayout]), itemLayout)
|
||||
|
||||
# Export Switch
|
||||
self.textExport = QLabel("Include when building project")
|
||||
self.textExport = QLabel(self.tr("Include when building project"))
|
||||
self.editExport = QSwitch()
|
||||
if self.theItem.itemType == nwItemType.FILE:
|
||||
self.editExport.setEnabled(True)
|
||||
@@ -136,17 +137,21 @@ class GuiItemEditor(QDialog):
|
||||
# Assemble
|
||||
##
|
||||
|
||||
nameLabel = QLabel(self.tr("Label"))
|
||||
statusLabel = QLabel(self.tr("Status"))
|
||||
layoutLabel = QLabel(self.tr("Layout"))
|
||||
|
||||
self.mainForm = QGridLayout()
|
||||
self.mainForm.setVerticalSpacing(vSp)
|
||||
self.mainForm.setHorizontalSpacing(mSp)
|
||||
self.mainForm.addWidget(QLabel("Label"), 0, 0, 1, 1)
|
||||
self.mainForm.addWidget(self.editName, 0, 1, 1, 2)
|
||||
self.mainForm.addWidget(QLabel("Status"), 1, 0, 1, 1)
|
||||
self.mainForm.addWidget(self.editStatus, 1, 1, 1, 2)
|
||||
self.mainForm.addWidget(QLabel("Layout"), 2, 0, 1, 1)
|
||||
self.mainForm.addWidget(self.editLayout, 2, 1, 1, 2)
|
||||
self.mainForm.addWidget(self.textExport, 3, 0, 1, 2)
|
||||
self.mainForm.addWidget(self.editExport, 3, 2, 1, 1)
|
||||
self.mainForm.addWidget(nameLabel, 0, 0, 1, 1)
|
||||
self.mainForm.addWidget(self.editName, 0, 1, 1, 2)
|
||||
self.mainForm.addWidget(statusLabel, 1, 0, 1, 1)
|
||||
self.mainForm.addWidget(self.editStatus, 1, 1, 1, 2)
|
||||
self.mainForm.addWidget(layoutLabel, 2, 0, 1, 1)
|
||||
self.mainForm.addWidget(self.editLayout, 2, 1, 1, 2)
|
||||
self.mainForm.addWidget(self.textExport, 3, 0, 1, 2)
|
||||
self.mainForm.addWidget(self.editExport, 3, 2, 1, 1)
|
||||
self.mainForm.setColumnStretch(0, 0)
|
||||
self.mainForm.setColumnStretch(1, 1)
|
||||
self.mainForm.setColumnStretch(2, 0)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -74,7 +74,7 @@ class GuiProjectLoad(QDialog):
|
||||
self.outerBox.setSpacing(sPx)
|
||||
self.innerBox.setSpacing(sPx)
|
||||
|
||||
self.setWindowTitle("Open Project")
|
||||
self.setWindowTitle(self.tr("Open Project"))
|
||||
self.setMinimumWidth(self.mainConf.pxInt(650))
|
||||
self.setMinimumHeight(self.mainConf.pxInt(400))
|
||||
self.setModal(True)
|
||||
@@ -90,7 +90,11 @@ class GuiProjectLoad(QDialog):
|
||||
self.listBox.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
|
||||
self.listBox.setColumnCount(3)
|
||||
self.listBox.setHeaderLabels(["Working Title", "Words", "Last Opened"])
|
||||
self.listBox.setHeaderLabels([
|
||||
self.tr("Working Title"),
|
||||
self.tr("Words"),
|
||||
self.tr("Last Opened"),
|
||||
])
|
||||
self.listBox.setRootIsDecorated(False)
|
||||
self.listBox.itemSelectionChanged.connect(self._doSelectRecent)
|
||||
self.listBox.itemDoubleClicked.connect(self._doOpenRecent)
|
||||
@@ -100,8 +104,8 @@ class GuiProjectLoad(QDialog):
|
||||
treeHead.setTextAlignment(self.C_COUNT, Qt.AlignRight)
|
||||
treeHead.setTextAlignment(self.C_TIME, Qt.AlignRight)
|
||||
|
||||
self.lblRecent = QLabel("<b>Recently Opened Projects</b>")
|
||||
self.lblPath = QLabel("<b>Path</b>")
|
||||
self.lblRecent = QLabel("<b>%s</b>" % self.tr("Recently Opened Projects"))
|
||||
self.lblPath = QLabel("<b>%s</b>" % self.tr("Path"))
|
||||
self.selPath = QLineEdit("")
|
||||
self.selPath.setReadOnly(True)
|
||||
|
||||
@@ -126,10 +130,10 @@ class GuiProjectLoad(QDialog):
|
||||
self.buttonBox.accepted.connect(self._doOpenRecent)
|
||||
self.buttonBox.rejected.connect(self._doCancel)
|
||||
|
||||
self.newButton = self.buttonBox.addButton("New", QDialogButtonBox.ActionRole)
|
||||
self.newButton = self.buttonBox.addButton(self.tr("New"), QDialogButtonBox.ActionRole)
|
||||
self.newButton.clicked.connect(self._doNewProject)
|
||||
|
||||
self.delButton = self.buttonBox.addButton("Remove", QDialogButtonBox.ActionRole)
|
||||
self.delButton = self.buttonBox.addButton(self.tr("Remove"), QDialogButtonBox.ActionRole)
|
||||
self.delButton.clicked.connect(self._doDeleteRecent)
|
||||
|
||||
self.outerBox.addLayout(self.innerBox)
|
||||
@@ -180,12 +184,12 @@ class GuiProjectLoad(QDialog):
|
||||
"""Browse for a folder path.
|
||||
"""
|
||||
logger.verbose("GuiProjectLoad browse button clicked")
|
||||
dlgOpt = QFileDialog.Options()
|
||||
dlgOpt |= QFileDialog.DontUseNativeDialog
|
||||
extFilter = [
|
||||
self.tr("novelWriter Project File ({0})").format(nwFiles.PROJ_FILE),
|
||||
self.tr("All files ({0})").format("*"),
|
||||
]
|
||||
projFile, _ = QFileDialog.getOpenFileName(
|
||||
self, "Open novelWriter Project", "",
|
||||
"novelWriter Project File (%s);;All Files (*)" % nwFiles.PROJ_FILE,
|
||||
options=dlgOpt
|
||||
self, self.tr("Open Project"), "", filter=";;".join(extFilter)
|
||||
)
|
||||
if projFile:
|
||||
thePath = os.path.abspath(os.path.dirname(projFile))
|
||||
@@ -221,10 +225,13 @@ class GuiProjectLoad(QDialog):
|
||||
selList = self.listBox.selectedItems()
|
||||
if selList:
|
||||
projName = selList[0].text(self.C_NAME)
|
||||
msgYes = self.theParent.askQuestion("Remove Entry", (
|
||||
"Remove '%s' from the recent projects list? "
|
||||
"The project files will not be deleted."
|
||||
) % projName)
|
||||
msgYes = self.theParent.askQuestion(
|
||||
self.tr("Remove Entry"),
|
||||
self.tr(
|
||||
"Remove '{0}' from the recent projects list? "
|
||||
"The project files will not be deleted."
|
||||
).format(projName)
|
||||
)
|
||||
if msgYes:
|
||||
self.mainConf.removeFromRecentCache(
|
||||
selList[0].data(self.C_NAME, Qt.UserRole)
|
||||
@@ -27,15 +27,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import nw
|
||||
import logging
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtCore import Qt, QLocale
|
||||
from PyQt5.QtGui import QIcon, QPixmap, QColor, QBrush
|
||||
from PyQt5.QtWidgets import (
|
||||
QHBoxLayout, QVBoxLayout, QLineEdit, QPlainTextEdit, QLabel, QWidget,
|
||||
QDialogButtonBox, QListWidget, QPushButton, QListWidgetItem, QColorDialog,
|
||||
QAbstractItemView, QTreeWidget, QTreeWidgetItem, QComboBox
|
||||
QDialogButtonBox, QPushButton, QColorDialog, QTreeWidget, QTreeWidgetItem,
|
||||
QComboBox
|
||||
)
|
||||
|
||||
from nw.constants import nwAlert
|
||||
from nw.enum import nwAlert
|
||||
from nw.gui.custom import QSwitch, PagedDialog, QConfigLayout
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -54,7 +54,7 @@ class GuiProjectSettings(PagedDialog):
|
||||
self.optState = theProject.optState
|
||||
|
||||
self.theProject.countStatus()
|
||||
self.setWindowTitle("Project Settings")
|
||||
self.setWindowTitle(self.tr("Project Settings"))
|
||||
|
||||
wW = self.mainConf.pxInt(570)
|
||||
wH = self.mainConf.pxInt(375)
|
||||
@@ -71,10 +71,10 @@ class GuiProjectSettings(PagedDialog):
|
||||
self.tabImport = GuiProjectEditStatus(self.theParent, self.theProject, False)
|
||||
self.tabReplace = GuiProjectEditReplace(self.theParent, self.theProject)
|
||||
|
||||
self.addTab(self.tabMain, "Settings")
|
||||
self.addTab(self.tabStatus, "Status")
|
||||
self.addTab(self.tabImport, "Importance")
|
||||
self.addTab(self.tabReplace, "Auto-Replace")
|
||||
self.addTab(self.tabMain, self.tr("Settings"))
|
||||
self.addTab(self.tabStatus, self.tr("Status"))
|
||||
self.addTab(self.tabImport, self.tr("Importance"))
|
||||
self.addTab(self.tabReplace, self.tr("Auto-Replace"))
|
||||
|
||||
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
self.buttonBox.accepted.connect(self._doSave)
|
||||
@@ -143,10 +143,14 @@ class GuiProjectSettings(PagedDialog):
|
||||
winWidth = self.mainConf.rpxInt(self.width())
|
||||
winHeight = self.mainConf.rpxInt(self.height())
|
||||
replaceColW = self.mainConf.rpxInt(self.tabReplace.listBox.columnWidth(0))
|
||||
statusColW = self.mainConf.rpxInt(self.tabStatus.listBox.columnWidth(0))
|
||||
importColW = self.mainConf.rpxInt(self.tabImport.listBox.columnWidth(0))
|
||||
|
||||
self.optState.setValue("GuiProjectSettings", "winWidth", winWidth)
|
||||
self.optState.setValue("GuiProjectSettings", "winHeight", winHeight)
|
||||
self.optState.setValue("GuiProjectSettings", "replaceColW", replaceColW)
|
||||
self.optState.setValue("GuiProjectSettings", "statusColW", statusColW)
|
||||
self.optState.setValue("GuiProjectSettings", "importColW", importColW)
|
||||
|
||||
return
|
||||
|
||||
@@ -166,69 +170,69 @@ class GuiProjectEditMain(QWidget):
|
||||
self.mainForm.setHelpTextStyle(self.theParent.theTheme.helpText)
|
||||
self.setLayout(self.mainForm)
|
||||
|
||||
self.mainForm.addGroupLabel("Project Settings")
|
||||
self.mainForm.addGroupLabel(self.tr("Project Settings"))
|
||||
|
||||
xW = self.mainConf.pxInt(250)
|
||||
xH = self.mainConf.pxInt(100)
|
||||
xH = round(4.8*self.theParent.theTheme.fontPixelSize)
|
||||
|
||||
self.editName = QLineEdit()
|
||||
self.editName.setMaxLength(200)
|
||||
self.editName.setFixedWidth(xW)
|
||||
self.editName.setMaximumWidth(xW)
|
||||
self.editName.setText(self.theProject.projName)
|
||||
self.mainForm.addRow(
|
||||
"Working title",
|
||||
self.tr("Working title"),
|
||||
self.editName,
|
||||
"Should be set only once."
|
||||
self.tr("Should be set only once.")
|
||||
)
|
||||
|
||||
self.editTitle = QLineEdit()
|
||||
self.editTitle.setMaxLength(200)
|
||||
self.editTitle.setFixedWidth(xW)
|
||||
self.editTitle.setMaximumWidth(xW)
|
||||
self.editTitle.setText(self.theProject.bookTitle)
|
||||
self.mainForm.addRow(
|
||||
"Novel title",
|
||||
self.tr("Novel title"),
|
||||
self.editTitle,
|
||||
"Change whenever you want!"
|
||||
self.tr("Change whenever you want!")
|
||||
)
|
||||
|
||||
self.editAuthors = QPlainTextEdit()
|
||||
bookAuthors = ""
|
||||
for bookAuthor in self.theProject.bookAuthors:
|
||||
bookAuthors += bookAuthor+"\n"
|
||||
self.editAuthors.setPlainText(bookAuthors)
|
||||
self.editAuthors.setFixedHeight(xH)
|
||||
self.editAuthors.setFixedWidth(xW)
|
||||
self.editAuthors.setMaximumHeight(xH)
|
||||
self.editAuthors.setMaximumWidth(xW)
|
||||
self.editAuthors.setPlainText("\n".join(self.theProject.bookAuthors))
|
||||
self.mainForm.addRow(
|
||||
"Author(s)",
|
||||
self.tr("Author(s)"),
|
||||
self.editAuthors,
|
||||
"One name per line."
|
||||
self.tr("One name per line.")
|
||||
)
|
||||
|
||||
self.spellLang = QComboBox(self)
|
||||
self.spellLang.setMaximumWidth(xW)
|
||||
theDict = self.theParent.docEditor.theDict
|
||||
self.spellLang.addItem("Default", "None")
|
||||
self.spellLang.addItem(self.tr("Default"), "None")
|
||||
if theDict is not None:
|
||||
for spTag, spName in theDict.listDictionaries():
|
||||
self.spellLang.addItem(spName, spTag)
|
||||
for spTag, spProv in theDict.listDictionaries():
|
||||
qLocal = QLocale(spTag)
|
||||
spLang = qLocal.nativeLanguageName().title()
|
||||
self.spellLang.addItem("%s [%s]" % (spLang, spProv), spTag)
|
||||
|
||||
self.mainForm.addRow(
|
||||
"Spell check language",
|
||||
self.tr("Spell check language"),
|
||||
self.spellLang,
|
||||
"Overrides main preferences."
|
||||
self.tr("Overrides main preferences.")
|
||||
)
|
||||
|
||||
spellIdx = 0
|
||||
if self.theProject.projLang is not None:
|
||||
spellIdx = self.spellLang.findData(self.theProject.projLang)
|
||||
if self.theProject.projSpell is not None:
|
||||
spellIdx = self.spellLang.findData(self.theProject.projSpell)
|
||||
if spellIdx != -1:
|
||||
self.spellLang.setCurrentIndex(spellIdx)
|
||||
|
||||
self.doBackup = QSwitch(self)
|
||||
self.doBackup.setChecked(not self.theProject.doBackup)
|
||||
self.mainForm.addRow(
|
||||
"No backup on close",
|
||||
self.tr("No backup on close"),
|
||||
self.doBackup,
|
||||
"Overrides main preferences."
|
||||
self.tr("Overrides main preferences.")
|
||||
)
|
||||
|
||||
return
|
||||
@@ -237,17 +241,30 @@ class GuiProjectEditMain(QWidget):
|
||||
|
||||
class GuiProjectEditStatus(QWidget):
|
||||
|
||||
COL_LABEL = 0
|
||||
COL_USAGE = 1
|
||||
|
||||
def __init__(self, theParent, theProject, isStatus):
|
||||
QWidget.__init__(self, theParent)
|
||||
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theParent = theParent
|
||||
self.theProject = theProject
|
||||
self.optState = theProject.optState
|
||||
self.theTheme = theParent.theTheme
|
||||
|
||||
if isStatus:
|
||||
self.theStatus = self.theProject.statusItems
|
||||
pageLabel = self.tr("Novel File Status Levels")
|
||||
colSetting = "statusColW"
|
||||
else:
|
||||
self.theStatus = self.theProject.importItems
|
||||
pageLabel = self.tr("Note File Importance Levels")
|
||||
colSetting = "importColW"
|
||||
|
||||
wCol0 = self.mainConf.pxInt(
|
||||
self.optState.getInt("GuiProjectSettings", colSetting, 130)
|
||||
)
|
||||
|
||||
self.colData = []
|
||||
self.colCounts = []
|
||||
@@ -256,64 +273,89 @@ class GuiProjectEditStatus(QWidget):
|
||||
|
||||
self.iPx = self.theTheme.baseIconSize
|
||||
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.mainBox = QHBoxLayout()
|
||||
self.mainForm = QVBoxLayout()
|
||||
# The List
|
||||
# ========
|
||||
|
||||
self.listBox = QListWidget()
|
||||
self.listBox.setDragDropMode(QAbstractItemView.InternalMove)
|
||||
self.listBox = QTreeWidget()
|
||||
self.listBox.setHeaderLabels([
|
||||
self.tr("Label"),
|
||||
self.tr("Usage"),
|
||||
])
|
||||
self.listBox.itemSelectionChanged.connect(self._selectedItem)
|
||||
self.listBox.model().rowsMoved.connect(self._rowsMoved)
|
||||
self.listBox.setColumnWidth(self.COL_LABEL, wCol0)
|
||||
self.listBox.setIndentation(0)
|
||||
|
||||
for iName, iCol, nUse in self.theStatus:
|
||||
self._addItem(iName, iCol, iName, nUse)
|
||||
|
||||
# List Controls
|
||||
# =============
|
||||
|
||||
self.addButton = QPushButton(self.theTheme.getIcon("add"), "")
|
||||
self.addButton.setToolTip(self.tr("Add new entry"))
|
||||
self.addButton.clicked.connect(self._newItem)
|
||||
|
||||
self.delButton = QPushButton(self.theTheme.getIcon("remove"), "")
|
||||
self.delButton.setToolTip(self.tr("Delete selected entry"))
|
||||
self.delButton.clicked.connect(self._delItem)
|
||||
|
||||
# Edit Form
|
||||
# =========
|
||||
|
||||
self.editName = QLineEdit()
|
||||
self.editName.setMaxLength(40)
|
||||
self.editName.setEnabled(False)
|
||||
self.newButton = QPushButton("New")
|
||||
self.delButton = QPushButton("Delete")
|
||||
self.saveButton = QPushButton("Save")
|
||||
self.colPixmap = QPixmap(self.iPx, self.iPx)
|
||||
self.colPixmap.fill(QColor(120, 120, 120))
|
||||
self.colButton = QPushButton(QIcon(self.colPixmap), "Colour")
|
||||
self.colButton.setIconSize(self.colPixmap.rect().size())
|
||||
self.editName.setPlaceholderText(self.tr("Select item to edit"))
|
||||
|
||||
self.newButton.clicked.connect(self._newItem)
|
||||
self.delButton.clicked.connect(self._delItem)
|
||||
self.saveButton.clicked.connect(self._saveItem)
|
||||
self.colPixmap = QPixmap(self.iPx, self.iPx)
|
||||
self.colPixmap.fill(QColor(120, 120, 120))
|
||||
self.colButton = QPushButton(QIcon(self.colPixmap), self.tr("Colour"))
|
||||
self.colButton.setIconSize(self.colPixmap.rect().size())
|
||||
self.colButton.clicked.connect(self._selectColour)
|
||||
|
||||
self.mainForm.addWidget(self.newButton)
|
||||
self.mainForm.addWidget(self.delButton)
|
||||
self.mainForm.addStretch(1)
|
||||
self.mainForm.addWidget(QLabel("<b>Name</b>"))
|
||||
self.mainForm.addWidget(self.editName)
|
||||
self.mainForm.addWidget(self.colButton)
|
||||
self.mainForm.addStretch(1)
|
||||
self.mainForm.addWidget(self.saveButton)
|
||||
self.saveButton = QPushButton(self.tr("Save"))
|
||||
self.saveButton.clicked.connect(self._saveItem)
|
||||
|
||||
# Assemble
|
||||
# ========
|
||||
|
||||
self.listControls = QVBoxLayout()
|
||||
self.listControls.addWidget(self.addButton)
|
||||
self.listControls.addWidget(self.delButton)
|
||||
self.listControls.addStretch(1)
|
||||
|
||||
self.editBox = QHBoxLayout()
|
||||
self.editBox.addWidget(self.editName)
|
||||
self.editBox.addWidget(self.colButton)
|
||||
self.editBox.addWidget(self.saveButton)
|
||||
|
||||
self.mainBox = QVBoxLayout()
|
||||
self.mainBox.addWidget(self.listBox)
|
||||
self.mainBox.addLayout(self.mainForm)
|
||||
self.mainBox.addLayout(self.editBox)
|
||||
|
||||
if isStatus:
|
||||
self.outerBox.addWidget(QLabel("<b>Novel File Status Levels</b>"))
|
||||
else:
|
||||
self.outerBox.addWidget(QLabel("<b>Note File Importance Levels</b>"))
|
||||
self.outerBox.addLayout(self.mainBox)
|
||||
self.innerBox = QHBoxLayout()
|
||||
self.innerBox.addLayout(self.mainBox)
|
||||
self.innerBox.addLayout(self.listControls)
|
||||
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.outerBox.addWidget(QLabel("<b>%s</b>" % pageLabel))
|
||||
self.outerBox.addLayout(self.innerBox)
|
||||
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
return
|
||||
|
||||
def getNewList(self):
|
||||
"""Return list of entries.
|
||||
"""
|
||||
if self.colChanged:
|
||||
newList = []
|
||||
for n in range(self.listBox.count()):
|
||||
nItem = self.listBox.item(n)
|
||||
nIdx = nItem.data(Qt.UserRole)
|
||||
for n in range(self.listBox.topLevelItemCount()):
|
||||
nItem = self.listBox.topLevelItem(n)
|
||||
nIdx = nItem.data(self.COL_LABEL, Qt.UserRole)
|
||||
newList.append(self.colData[nIdx])
|
||||
return newList
|
||||
|
||||
return None
|
||||
|
||||
##
|
||||
@@ -325,7 +367,7 @@ class GuiProjectEditStatus(QWidget):
|
||||
"""
|
||||
if self.selColour is not None:
|
||||
newCol = QColorDialog.getColor(
|
||||
self.selColour, self, "Select Colour", QColorDialog.DontUseNativeDialog
|
||||
self.selColour, self, self.tr("Select Colour")
|
||||
)
|
||||
if newCol.isValid():
|
||||
self.selColour = newCol
|
||||
@@ -338,8 +380,9 @@ class GuiProjectEditStatus(QWidget):
|
||||
def _newItem(self):
|
||||
"""Create a new status item.
|
||||
"""
|
||||
newItem = self._addItem("New Item", (0, 0, 0), None, 0)
|
||||
newItem.setBackground(QBrush(QColor(0, 255, 0, 80)))
|
||||
newItem = self._addItem(self.tr("New Item"), (0, 0, 0), None, 0)
|
||||
newItem.setBackground(self.COL_LABEL, QBrush(QColor(0, 255, 0, 70)))
|
||||
newItem.setBackground(self.COL_USAGE, QBrush(QColor(0, 255, 0, 70)))
|
||||
self.colChanged = True
|
||||
return
|
||||
|
||||
@@ -348,14 +391,14 @@ class GuiProjectEditStatus(QWidget):
|
||||
"""
|
||||
selItem = self._getSelectedItem()
|
||||
if selItem is not None:
|
||||
iRow = self.listBox.row(selItem)
|
||||
selIdx = selItem.data(Qt.UserRole)
|
||||
iRow = self.listBox.indexOfTopLevelItem(selItem)
|
||||
selIdx = selItem.data(self.COL_LABEL, Qt.UserRole)
|
||||
if self.colCounts[selIdx] == 0:
|
||||
self.listBox.takeItem(iRow)
|
||||
self.listBox.takeTopLevelItem(iRow)
|
||||
self.colChanged = True
|
||||
else:
|
||||
self.theParent.makeAlert(
|
||||
"Cannot delete status item that is in use.", nwAlert.ERROR
|
||||
self.tr("Cannot delete a status item that is in use."), nwAlert.ERROR
|
||||
)
|
||||
return
|
||||
|
||||
@@ -364,7 +407,7 @@ class GuiProjectEditStatus(QWidget):
|
||||
"""
|
||||
selItem = self._getSelectedItem()
|
||||
if selItem is not None:
|
||||
selIdx = selItem.data(Qt.UserRole)
|
||||
selIdx = selItem.data(self.COL_LABEL, Qt.UserRole)
|
||||
self.colData[selIdx] = (
|
||||
self.editName.text().strip(),
|
||||
self.selColour.red(),
|
||||
@@ -372,10 +415,12 @@ class GuiProjectEditStatus(QWidget):
|
||||
self.selColour.blue(),
|
||||
self.colData[selIdx][4]
|
||||
)
|
||||
selItem.setText("%s [%d]" % (self.colData[selIdx][0], self.colCounts[selIdx]))
|
||||
selItem.setIcon(self.colButton.icon())
|
||||
selItem.setText(self.COL_LABEL, self.colData[selIdx][0])
|
||||
selItem.setText(self.COL_USAGE, self._usageString(self.colCounts[selIdx]))
|
||||
selItem.setIcon(self.COL_LABEL, self.colButton.icon())
|
||||
self.editName.setEnabled(False)
|
||||
self.colChanged = True
|
||||
|
||||
return
|
||||
|
||||
def _addItem(self, iName, iCol, oName, nUse):
|
||||
@@ -383,11 +428,12 @@ class GuiProjectEditStatus(QWidget):
|
||||
"""
|
||||
newIcon = QPixmap(self.iPx, self.iPx)
|
||||
newIcon.fill(QColor(*iCol))
|
||||
newItem = QListWidgetItem()
|
||||
newItem.setText("%s [%d]" % (iName, nUse))
|
||||
newItem.setIcon(QIcon(newIcon))
|
||||
newItem.setData(Qt.UserRole, len(self.colData))
|
||||
self.listBox.addItem(newItem)
|
||||
newItem = QTreeWidgetItem()
|
||||
newItem.setText(self.COL_LABEL, iName)
|
||||
newItem.setText(self.COL_USAGE, self._usageString(nUse))
|
||||
newItem.setIcon(self.COL_LABEL, QIcon(newIcon))
|
||||
newItem.setData(self.COL_LABEL, Qt.UserRole, len(self.colData))
|
||||
self.listBox.addTopLevelItem(newItem)
|
||||
self.colData.append((iName, iCol[0], iCol[1], iCol[2], oName))
|
||||
self.colCounts.append(nUse)
|
||||
return newItem
|
||||
@@ -398,7 +444,7 @@ class GuiProjectEditStatus(QWidget):
|
||||
"""
|
||||
selItem = self._getSelectedItem()
|
||||
if selItem is not None:
|
||||
selIdx = selItem.data(Qt.UserRole)
|
||||
selIdx = selItem.data(self.COL_LABEL, Qt.UserRole)
|
||||
selVal = self.colData[selIdx]
|
||||
self.selColour = QColor(selVal[1], selVal[2], selVal[3])
|
||||
newIcon = QPixmap(self.iPx, self.iPx)
|
||||
@@ -408,6 +454,7 @@ class GuiProjectEditStatus(QWidget):
|
||||
self.editName.setEnabled(True)
|
||||
self.editName.selectAll()
|
||||
self.editName.setFocus()
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
@@ -423,16 +470,28 @@ class GuiProjectEditStatus(QWidget):
|
||||
return None
|
||||
|
||||
def _rowsMoved(self):
|
||||
"""A row has been moved, so sett the changed flag.
|
||||
"""A row has been moved, so set the changed flag.
|
||||
"""
|
||||
logger.verbose("A drag move event occurred")
|
||||
self.colChanged = True
|
||||
return
|
||||
|
||||
def _usageString(self, nUse):
|
||||
"""Generate usage string.
|
||||
"""
|
||||
if nUse == 0:
|
||||
return self.tr("Not in use")
|
||||
elif nUse == 1:
|
||||
return self.tr("Used once")
|
||||
else:
|
||||
return self.tr("Used by {0} items").format(nUse)
|
||||
|
||||
# END Class GuiProjectEditStatus
|
||||
|
||||
class GuiProjectEditReplace(QWidget):
|
||||
|
||||
COL_KEY = 0
|
||||
COL_REPL = 1
|
||||
|
||||
def __init__(self, theParent, theProject):
|
||||
QWidget.__init__(self, theParent)
|
||||
|
||||
@@ -443,52 +502,82 @@ class GuiProjectEditReplace(QWidget):
|
||||
self.optState = theProject.optState
|
||||
self.arChanged = False
|
||||
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.bottomBox = QHBoxLayout()
|
||||
|
||||
wCol0 = self.mainConf.pxInt(
|
||||
self.optState.getInt("GuiProjectSettings", "replaceColW", 100)
|
||||
self.optState.getInt("GuiProjectSettings", "replaceColW", 130)
|
||||
)
|
||||
pageLabel = self.tr("Text Replace List for Preview and Export")
|
||||
|
||||
# List Box
|
||||
# ========
|
||||
|
||||
self.listBox = QTreeWidget()
|
||||
self.listBox.setHeaderLabels(["Keyword", "Replace With"])
|
||||
self.listBox.setHeaderLabels([
|
||||
self.tr("Keyword"),
|
||||
self.tr("Replace With"),
|
||||
])
|
||||
self.listBox.itemSelectionChanged.connect(self._selectedItem)
|
||||
self.listBox.setColumnWidth(0, wCol0)
|
||||
self.listBox.setColumnWidth(self.COL_KEY, wCol0)
|
||||
self.listBox.setIndentation(0)
|
||||
|
||||
for aKey, aVal in self.theProject.autoReplace.items():
|
||||
newItem = QTreeWidgetItem(["<%s>" % aKey, aVal])
|
||||
self.listBox.addTopLevelItem(newItem)
|
||||
|
||||
self.listBox.sortByColumn(0, Qt.AscendingOrder)
|
||||
self.listBox.sortByColumn(self.COL_KEY, Qt.AscendingOrder)
|
||||
self.listBox.setSortingEnabled(True)
|
||||
|
||||
self.editKey = QLineEdit()
|
||||
self.editValue = QLineEdit()
|
||||
self.saveButton = QPushButton(self.theTheme.getIcon("done"), "")
|
||||
self.addButton = QPushButton(self.theTheme.getIcon("add"), "")
|
||||
self.delButton = QPushButton(self.theTheme.getIcon("remove"), "")
|
||||
self.saveButton.setToolTip("Save entry")
|
||||
self.addButton.setToolTip("Add new entry")
|
||||
self.delButton.setToolTip("Delete selected entry")
|
||||
# List Controls
|
||||
# =============
|
||||
|
||||
self.addButton = QPushButton(self.theTheme.getIcon("add"), "")
|
||||
self.addButton.setToolTip(self.tr("Add new entry"))
|
||||
self.addButton.clicked.connect(self._addEntry)
|
||||
|
||||
self.delButton = QPushButton(self.theTheme.getIcon("remove"), "")
|
||||
self.delButton.setToolTip(self.tr("Delete selected entry"))
|
||||
self.delButton.clicked.connect(self._delEntry)
|
||||
|
||||
# Edit Form
|
||||
# =========
|
||||
|
||||
self.editKey = QLineEdit()
|
||||
self.editKey.setPlaceholderText(self.tr("Select item to edit"))
|
||||
self.editKey.setEnabled(False)
|
||||
self.editKey.setMaxLength(40)
|
||||
|
||||
self.editValue = QLineEdit()
|
||||
self.editValue.setEnabled(False)
|
||||
self.editValue.setMaxLength(80)
|
||||
|
||||
self.saveButton = QPushButton("Save")
|
||||
self.saveButton.setToolTip(self.tr("Save entry"))
|
||||
self.saveButton.clicked.connect(self._saveEntry)
|
||||
self.addButton.clicked.connect(self._addEntry)
|
||||
self.delButton.clicked.connect(self._delEntry)
|
||||
|
||||
self.bottomBox.addWidget(self.editKey, 2)
|
||||
self.bottomBox.addWidget(self.editValue, 3)
|
||||
self.bottomBox.addWidget(self.saveButton)
|
||||
self.bottomBox.addWidget(self.addButton)
|
||||
self.bottomBox.addWidget(self.delButton)
|
||||
# Assemble
|
||||
# ========
|
||||
|
||||
self.listControls = QVBoxLayout()
|
||||
self.listControls.addWidget(self.addButton)
|
||||
self.listControls.addWidget(self.delButton)
|
||||
self.listControls.addStretch(1)
|
||||
|
||||
self.editBox = QHBoxLayout()
|
||||
self.editBox.addWidget(self.editKey, 4)
|
||||
self.editBox.addWidget(self.editValue, 5)
|
||||
self.editBox.addWidget(self.saveButton, 0)
|
||||
|
||||
self.mainBox = QVBoxLayout()
|
||||
self.mainBox.addWidget(self.listBox)
|
||||
self.mainBox.addLayout(self.editBox)
|
||||
|
||||
self.innerBox = QHBoxLayout()
|
||||
self.innerBox.addLayout(self.mainBox)
|
||||
self.innerBox.addLayout(self.listControls)
|
||||
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.outerBox.addWidget(QLabel("<b>%s</b>" % pageLabel))
|
||||
self.outerBox.addLayout(self.innerBox)
|
||||
|
||||
self.outerBox.addWidget(QLabel("<b>Text Replace List for Preview and Export</b>"))
|
||||
self.outerBox.addWidget(self.listBox)
|
||||
self.outerBox.addLayout(self.bottomBox)
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
return
|
||||
@@ -503,6 +592,7 @@ class GuiProjectEditReplace(QWidget):
|
||||
aVal = tItem.text(1)
|
||||
if len(aKey) > 0:
|
||||
newList[aKey] = aVal
|
||||
|
||||
return newList
|
||||
|
||||
##
|
||||
@@ -538,8 +628,8 @@ class GuiProjectEditReplace(QWidget):
|
||||
saveKey = self._stripNotAllowed(newKey)
|
||||
|
||||
if len(saveKey) > 0 and len(newVal) > 0:
|
||||
selItem.setText(0, "<%s>" % saveKey)
|
||||
selItem.setText(1, newVal)
|
||||
selItem.setText(self.COL_KEY, "<%s>" % saveKey)
|
||||
selItem.setText(self.COL_REPL, newVal)
|
||||
self.editKey.clear()
|
||||
self.editValue.clear()
|
||||
self.editKey.setEnabled(False)
|
||||
133
nw/dialogs/quotes.py
Normal file
133
nw/dialogs/quotes.py
Normal file
@@ -0,0 +1,133 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – GUI Quotes Dialog
|
||||
===============================
|
||||
GUI class for quotes dialog
|
||||
|
||||
File History:
|
||||
Created: 2020-06-18 [0.9]
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, 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/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
import logging
|
||||
|
||||
from PyQt5.QtGui import QFontMetrics
|
||||
from PyQt5.QtCore import Qt, QSize
|
||||
from PyQt5.QtWidgets import (
|
||||
QLabel, QVBoxLayout, QHBoxLayout, QDialog, QDialogButtonBox,
|
||||
QListWidget, QListWidgetItem, QFrame
|
||||
)
|
||||
|
||||
from nw.constants import trConst, nwQuotes
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class GuiQuoteSelect(QDialog):
|
||||
|
||||
selectedQuote = ""
|
||||
|
||||
def __init__(self, theParent=None, currentQuote="\""):
|
||||
QDialog.__init__(self, parent=theParent)
|
||||
|
||||
self.mainConf = nw.CONFIG
|
||||
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.innerBox = QHBoxLayout()
|
||||
self.labelBox = QVBoxLayout()
|
||||
|
||||
self.selectedQuote = currentQuote
|
||||
|
||||
qMetrics = QFontMetrics(self.font())
|
||||
pxW = 7*qMetrics.boundingRectChar("M").width()
|
||||
pxH = 7*qMetrics.boundingRectChar("M").height()
|
||||
pxH = 7*qMetrics.boundingRectChar("M").height()
|
||||
|
||||
lblFont = self.font()
|
||||
lblFont.setPointSizeF(4*lblFont.pointSizeF())
|
||||
|
||||
# Preview Label
|
||||
self.previewLabel = QLabel(currentQuote)
|
||||
self.previewLabel.setFont(lblFont)
|
||||
self.previewLabel.setFixedSize(QSize(pxW, pxH))
|
||||
self.previewLabel.setAlignment(Qt.AlignCenter)
|
||||
self.previewLabel.setFrameStyle(QFrame.Box | QFrame.Plain)
|
||||
|
||||
# Quote Symbols
|
||||
self.listBox = QListWidget()
|
||||
self.listBox.itemSelectionChanged.connect(self._selectedSymbol)
|
||||
|
||||
minSize = 100
|
||||
for sKey, sLabel in nwQuotes.SYMBOLS.items():
|
||||
theText = "[ %s ] %s" % (sKey, trConst(sLabel))
|
||||
minSize = max(minSize, qMetrics.boundingRect(theText).width())
|
||||
qtItem = QListWidgetItem(theText)
|
||||
qtItem.setData(Qt.UserRole, sKey)
|
||||
self.listBox.addItem(qtItem)
|
||||
if sKey == currentQuote:
|
||||
self.listBox.setCurrentItem(qtItem)
|
||||
|
||||
self.listBox.setMinimumWidth(minSize + self.mainConf.pxInt(40))
|
||||
self.listBox.setMinimumHeight(self.mainConf.pxInt(150))
|
||||
|
||||
# Buttons
|
||||
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
self.buttonBox.accepted.connect(self._doAccept)
|
||||
self.buttonBox.rejected.connect(self._doReject)
|
||||
|
||||
# Assemble
|
||||
self.labelBox.addWidget(self.previewLabel, 0, Qt.AlignTop)
|
||||
self.labelBox.addStretch(1)
|
||||
|
||||
self.innerBox.addLayout(self.labelBox)
|
||||
self.innerBox.addWidget(self.listBox)
|
||||
|
||||
self.outerBox.addLayout(self.innerBox)
|
||||
self.outerBox.addWidget(self.buttonBox)
|
||||
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Slots
|
||||
##
|
||||
|
||||
def _selectedSymbol(self):
|
||||
"""Update the preview label and the selected quote style.
|
||||
"""
|
||||
selItems = self.listBox.selectedItems()
|
||||
if selItems:
|
||||
theSymbol = selItems[0].data(Qt.UserRole)
|
||||
self.previewLabel.setText(theSymbol)
|
||||
self.selectedQuote = theSymbol
|
||||
return
|
||||
|
||||
def _doAccept(self):
|
||||
"""Ok button clicked.
|
||||
"""
|
||||
self.accept()
|
||||
return
|
||||
|
||||
def _doReject(self):
|
||||
"""Cancel button clicked.
|
||||
"""
|
||||
self.reject()
|
||||
return
|
||||
|
||||
# END Class GuiQuoteSelect
|
||||
@@ -34,7 +34,8 @@ from PyQt5.QtWidgets import (
|
||||
QAbstractItemView, QPushButton, QLineEdit, QLabel
|
||||
)
|
||||
|
||||
from nw.constants import nwFiles, nwAlert
|
||||
from nw.enum import nwAlert
|
||||
from nw.constants import nwFiles
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -52,7 +53,7 @@ class GuiWordList(QDialog):
|
||||
self.theProject = theProject
|
||||
self.optState = theProject.optState
|
||||
|
||||
self.setWindowTitle("Project Word List")
|
||||
self.setWindowTitle(self.tr("Project Word List"))
|
||||
|
||||
mS = self.mainConf.pxInt(250)
|
||||
wW = self.mainConf.pxInt(320)
|
||||
@@ -68,7 +69,7 @@ class GuiWordList(QDialog):
|
||||
# Main Widgets
|
||||
# ============
|
||||
|
||||
self.headLabel = QLabel("<b>Project Word List</b>")
|
||||
self.headLabel = QLabel("<b>%s</b>" % self.tr("Project Word List"))
|
||||
|
||||
self.listBox = QListWidget()
|
||||
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
|
||||
@@ -77,11 +78,11 @@ class GuiWordList(QDialog):
|
||||
self.newEntry = QLineEdit()
|
||||
|
||||
self.addButton = QPushButton(self.theTheme.getIcon("add"), "")
|
||||
self.addButton.setToolTip("Add new entry")
|
||||
self.addButton.setToolTip(self.tr("Add new entry"))
|
||||
self.addButton.clicked.connect(self._doAdd)
|
||||
|
||||
self.delButton = QPushButton(self.theTheme.getIcon("remove"), "")
|
||||
self.delButton.setToolTip("Delete selected entry")
|
||||
self.delButton.setToolTip(self.tr("Delete selected entry"))
|
||||
self.delButton.clicked.connect(self._doDelete)
|
||||
|
||||
self.editBox = QHBoxLayout()
|
||||
@@ -121,12 +122,13 @@ class GuiWordList(QDialog):
|
||||
"""
|
||||
newWord = self.newEntry.text().strip()
|
||||
if newWord == "":
|
||||
self.theParent.makeAlert("Cannot add a blank word.", nwAlert.ERROR)
|
||||
self.theParent.makeAlert(self.tr("Cannot add a blank word."), nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
if self.listBox.findItems(newWord, Qt.MatchExactly):
|
||||
self.theParent.makeAlert(
|
||||
"The word '%s' is already in the word list." % newWord, nwAlert.ERROR
|
||||
self.tr("The word '{0}' is already in the word list.").format(newWord),
|
||||
nwAlert.ERROR
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -81,19 +81,14 @@ class nwDocAction(Enum):
|
||||
D_QUOTE = 10
|
||||
SEL_ALL = 11
|
||||
SEL_PARA = 12
|
||||
FIND = 13
|
||||
REPLACE = 14
|
||||
GO_NEXT = 15
|
||||
GO_PREV = 16
|
||||
REPL_NEXT = 17
|
||||
BLOCK_H1 = 18
|
||||
BLOCK_H2 = 19
|
||||
BLOCK_H3 = 20
|
||||
BLOCK_H4 = 21
|
||||
BLOCK_COM = 22
|
||||
BLOCK_TXT = 23
|
||||
REPL_SNG = 24
|
||||
REPL_DBL = 25
|
||||
BLOCK_H1 = 13
|
||||
BLOCK_H2 = 14
|
||||
BLOCK_H3 = 15
|
||||
BLOCK_H4 = 16
|
||||
BLOCK_COM = 17
|
||||
BLOCK_TXT = 18
|
||||
REPL_SNG = 19
|
||||
REPL_DBL = 20
|
||||
|
||||
# END Enum nwDocAction
|
||||
|
||||
@@ -117,6 +112,15 @@ class nwAlert(Enum):
|
||||
|
||||
# END Enum nwAlert
|
||||
|
||||
class nwWidget(Enum):
|
||||
|
||||
TREE = 1
|
||||
EDITOR = 2
|
||||
VIEWER = 3
|
||||
OUTLINE = 4
|
||||
|
||||
# END Enum nwWidget
|
||||
|
||||
class nwOutline(Enum):
|
||||
|
||||
TITLE = 0
|
||||
@@ -1,50 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from nw.gui.about import GuiAbout
|
||||
from nw.gui.build import GuiBuildNovel
|
||||
from nw.gui.doceditor import GuiDocEditor
|
||||
from nw.gui.docmerge import GuiDocMerge
|
||||
from nw.gui.docsplit import GuiDocSplit
|
||||
from nw.gui.docviewer import GuiDocViewer, GuiDocViewDetails
|
||||
from nw.gui.itemdetails import GuiItemDetails
|
||||
from nw.gui.itemeditor import GuiItemEditor
|
||||
from nw.gui.mainmenu import GuiMainMenu
|
||||
from nw.gui.noveltree import GuiNovelTree
|
||||
from nw.gui.outline import GuiOutline
|
||||
from nw.gui.outlinedetails import GuiOutlineDetails
|
||||
from nw.gui.preferences import GuiPreferences
|
||||
from nw.gui.projdetails import GuiProjectDetails
|
||||
from nw.gui.projload import GuiProjectLoad
|
||||
from nw.gui.projsettings import GuiProjectSettings
|
||||
from nw.gui.projtree import GuiProjectTree
|
||||
from nw.gui.projwizard import GuiProjectWizard
|
||||
from nw.gui.statusbar import GuiMainStatus
|
||||
from nw.gui.theme import GuiTheme
|
||||
from nw.gui.wordlist import GuiWordList
|
||||
from nw.gui.writingstats import GuiWritingStats
|
||||
|
||||
__all__ = [
|
||||
"GuiAbout",
|
||||
"GuiBuildNovel",
|
||||
"GuiDocEditor",
|
||||
"GuiDocMerge",
|
||||
"GuiDocSplit",
|
||||
"GuiDocViewDetails",
|
||||
"GuiDocViewer",
|
||||
"GuiItemDetails",
|
||||
"GuiItemEditor",
|
||||
"GuiMainMenu",
|
||||
"GuiNovelTree",
|
||||
"GuiMainStatus",
|
||||
"GuiNovelTree",
|
||||
"GuiOutline",
|
||||
"GuiOutlineDetails",
|
||||
"GuiPreferences",
|
||||
"GuiProjectDetails",
|
||||
"GuiProjectLoad",
|
||||
"GuiProjectSettings",
|
||||
"GuiProjectTree",
|
||||
"GuiProjectWizard",
|
||||
"GuiTheme",
|
||||
"GuiWordList",
|
||||
"GuiWritingStats",
|
||||
]
|
||||
|
||||
128
nw/gui/custom.py
128
nw/gui/custom.py
@@ -29,17 +29,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import nw
|
||||
import logging
|
||||
|
||||
from PyQt5.QtGui import QColor, QPalette, QPainter, QFontMetrics
|
||||
from PyQt5.QtGui import QColor, QPalette, QPainter
|
||||
from PyQt5.QtCore import (
|
||||
Qt, QRect, QPoint, QSize, QRectF, QPropertyAnimation, pyqtProperty
|
||||
Qt, QRect, QPoint, QRectF, QPropertyAnimation, pyqtProperty
|
||||
)
|
||||
from PyQt5.QtWidgets import (
|
||||
QGridLayout, QLabel, QWidget, QVBoxLayout, QHBoxLayout, QSizePolicy,
|
||||
QAbstractButton, QDialog, QTabWidget, QTabBar, QStyle, QDialogButtonBox,
|
||||
QStylePainter, QStyleOptionTab, QListWidget, QListWidgetItem, QFrame
|
||||
QAbstractButton, QDialog, QTabWidget, QTabBar, QStyle, QStylePainter,
|
||||
QStyleOptionTab, QLineEdit
|
||||
)
|
||||
|
||||
from nw.constants import nwUnicode, nwQuotes
|
||||
from nw.constants import nwUnicode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -61,6 +61,7 @@ class QConfigLayout(QGridLayout):
|
||||
wSp = nw.CONFIG.pxInt(8)
|
||||
self.setHorizontalSpacing(wSp)
|
||||
self.setVerticalSpacing(wSp)
|
||||
self.setColumnStretch(0, 1)
|
||||
|
||||
return
|
||||
|
||||
@@ -151,6 +152,7 @@ class QConfigLayout(QGridLayout):
|
||||
labelBox.addWidget(qLabel)
|
||||
labelBox.addWidget(qHelp)
|
||||
labelBox.setSpacing(0)
|
||||
labelBox.addStretch(1)
|
||||
|
||||
thisEntry["help"] = qHelp
|
||||
self.addLayout(labelBox, self._nextRow, 0, 1, 1, Qt.AlignLeft | Qt.AlignTop)
|
||||
@@ -163,15 +165,22 @@ class QConfigLayout(QGridLayout):
|
||||
controlBox.addWidget(qWidget, 0, Qt.AlignVCenter)
|
||||
controlBox.addWidget(QLabel(theUnit), 0, Qt.AlignVCenter)
|
||||
controlBox.setSpacing(wSp)
|
||||
self.addLayout(controlBox, self._nextRow, 1, 1, 1, Qt.AlignRight | Qt.AlignVCenter)
|
||||
self.addLayout(controlBox, self._nextRow, 1, 1, 1, Qt.AlignRight | Qt.AlignTop)
|
||||
|
||||
elif theButton is not None:
|
||||
controlBox = QHBoxLayout()
|
||||
controlBox.addWidget(qWidget, 0, Qt.AlignVCenter)
|
||||
controlBox.addWidget(theButton, 0, Qt.AlignVCenter)
|
||||
controlBox.setSpacing(wSp)
|
||||
self.addLayout(controlBox, self._nextRow, 1, 1, 1, Qt.AlignRight | Qt.AlignVCenter)
|
||||
self.addLayout(controlBox, self._nextRow, 1, 1, 1, Qt.AlignRight | Qt.AlignTop)
|
||||
|
||||
else:
|
||||
self.addWidget(qWidget, self._nextRow, 1, 1, 1, Qt.AlignRight | Qt.AlignVCenter)
|
||||
if isinstance(theWidget, QLineEdit):
|
||||
qLayout = QHBoxLayout()
|
||||
qLayout.addWidget(theWidget)
|
||||
self.addLayout(qLayout, self._nextRow, 1, 1, 1, Qt.AlignRight | Qt.AlignTop)
|
||||
else:
|
||||
self.addWidget(qWidget, self._nextRow, 1, 1, 1, Qt.AlignRight | Qt.AlignTop)
|
||||
|
||||
qLabel.setBuddy(qWidget)
|
||||
|
||||
@@ -207,6 +216,9 @@ class QHelpLabel(QLabel):
|
||||
lblFont.setPointSizeF(fontSize*lblFont.pointSizeF())
|
||||
self.setFont(lblFont)
|
||||
|
||||
self.setWordWrap(True)
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
|
||||
return
|
||||
|
||||
# END Class QHelpLabel
|
||||
@@ -409,6 +421,7 @@ class VerticalTabBar(QTabBar):
|
||||
|
||||
def __init__(self, theParent=None):
|
||||
QTabBar.__init__(self, parent=theParent)
|
||||
self._mW = nw.CONFIG.pxInt(150)
|
||||
return
|
||||
|
||||
def tabSizeHint(self, theIndex):
|
||||
@@ -416,6 +429,7 @@ class VerticalTabBar(QTabBar):
|
||||
"""
|
||||
tSize = QTabBar.tabSizeHint(self, theIndex)
|
||||
tSize.transpose()
|
||||
tSize.setWidth(min(tSize.width(), self._mW))
|
||||
return tSize
|
||||
|
||||
def paintEvent(self, theEvent):
|
||||
@@ -446,101 +460,3 @@ class VerticalTabBar(QTabBar):
|
||||
return
|
||||
|
||||
# END Class VerticalTabBar
|
||||
|
||||
# =============================================================================================== #
|
||||
# Quotes Dialog
|
||||
# =============================================================================================== #
|
||||
|
||||
class QuotesDialog(QDialog):
|
||||
|
||||
selectedQuote = ""
|
||||
|
||||
def __init__(self, theParent=None, currentQuote="\""):
|
||||
QDialog.__init__(self, parent=theParent)
|
||||
|
||||
self.mainConf = nw.CONFIG
|
||||
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.innerBox = QHBoxLayout()
|
||||
self.labelBox = QVBoxLayout()
|
||||
|
||||
self.selectedQuote = currentQuote
|
||||
|
||||
qMetrics = QFontMetrics(self.font())
|
||||
pxW = 7*qMetrics.boundingRectChar("M").width()
|
||||
pxH = 7*qMetrics.boundingRectChar("M").height()
|
||||
pxH = 7*qMetrics.boundingRectChar("M").height()
|
||||
|
||||
lblFont = self.font()
|
||||
lblFont.setPointSizeF(4*lblFont.pointSizeF())
|
||||
|
||||
# Preview Label
|
||||
self.previewLabel = QLabel(currentQuote)
|
||||
self.previewLabel.setFont(lblFont)
|
||||
self.previewLabel.setFixedSize(QSize(pxW, pxH))
|
||||
self.previewLabel.setAlignment(Qt.AlignCenter)
|
||||
self.previewLabel.setFrameStyle(QFrame.Box | QFrame.Plain)
|
||||
|
||||
# Quote Symbols
|
||||
self.listBox = QListWidget()
|
||||
self.listBox.itemSelectionChanged.connect(self._selectedSymbol)
|
||||
|
||||
minSize = 100
|
||||
for sKey, sLabel in nwQuotes.SYMBOLS.items():
|
||||
theText = "[ %s ] %s" % (sKey, sLabel)
|
||||
minSize = max(minSize, qMetrics.boundingRect(theText).width())
|
||||
qtItem = QListWidgetItem(theText)
|
||||
qtItem.setData(Qt.UserRole, sKey)
|
||||
self.listBox.addItem(qtItem)
|
||||
if sKey == currentQuote:
|
||||
self.listBox.setCurrentItem(qtItem)
|
||||
|
||||
self.listBox.setMinimumWidth(minSize + self.mainConf.pxInt(40))
|
||||
self.listBox.setMinimumHeight(self.mainConf.pxInt(150))
|
||||
|
||||
# Buttons
|
||||
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
self.buttonBox.accepted.connect(self._doAccept)
|
||||
self.buttonBox.rejected.connect(self._doReject)
|
||||
|
||||
# Assemble
|
||||
self.labelBox.addWidget(self.previewLabel, 0, Qt.AlignTop)
|
||||
self.labelBox.addStretch(1)
|
||||
|
||||
self.innerBox.addLayout(self.labelBox)
|
||||
self.innerBox.addWidget(self.listBox)
|
||||
|
||||
self.outerBox.addLayout(self.innerBox)
|
||||
self.outerBox.addWidget(self.buttonBox)
|
||||
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Slots
|
||||
##
|
||||
|
||||
def _selectedSymbol(self):
|
||||
"""Update the preview label and the selected quote style.
|
||||
"""
|
||||
selItems = self.listBox.selectedItems()
|
||||
if selItems:
|
||||
theSymbol = selItems[0].data(Qt.UserRole)
|
||||
self.previewLabel.setText(theSymbol)
|
||||
self.selectedQuote = theSymbol
|
||||
return
|
||||
|
||||
def _doAccept(self):
|
||||
"""Ok button clicked.
|
||||
"""
|
||||
self.accept()
|
||||
return
|
||||
|
||||
def _doReject(self):
|
||||
"""Cancel button clicked.
|
||||
"""
|
||||
self.reject()
|
||||
return
|
||||
|
||||
# END Class QuotesDialog
|
||||
|
||||
@@ -50,12 +50,10 @@ from PyQt5.QtWidgets import (
|
||||
)
|
||||
|
||||
from nw.core import NWDoc, NWSpellSimple, countWords
|
||||
from nw.gui.dochighlight import GuiDocHighlighter
|
||||
from nw.enum import nwAlert, nwDocAction, nwDocInsert, nwItemClass
|
||||
from nw.common import transferCase
|
||||
from nw.constants import (
|
||||
nwConst, nwAlert, nwUnicode, nwDocAction, nwDocInsert, nwItemClass,
|
||||
nwKeyWords, nwLabels
|
||||
)
|
||||
from nw.constants import nwConst, trConst, nwKeyWords, nwLabels, nwUnicode
|
||||
from nw.gui.dochighlight import GuiDocHighlighter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -98,10 +96,12 @@ class GuiDocEditor(QTextEdit):
|
||||
self.queuePos = None # Used for delayed change of cursor position
|
||||
|
||||
# Typography
|
||||
self.typDQOpen = self.mainConf.fmtDoubleQuotes[0]
|
||||
self.typDQClose = self.mainConf.fmtDoubleQuotes[1]
|
||||
self.typSQOpen = self.mainConf.fmtSingleQuotes[0]
|
||||
self.typSQClose = self.mainConf.fmtSingleQuotes[1]
|
||||
self.typDQOpen = '"'
|
||||
self.typDQClose = '"'
|
||||
self.typSQOpen = "'"
|
||||
self.typSQClose = "'"
|
||||
self.typPadChar = " "
|
||||
self.addPadding = False
|
||||
|
||||
# Core Elements and Signals
|
||||
self.qDocument = self.document()
|
||||
@@ -197,6 +197,17 @@ class GuiDocEditor(QTextEdit):
|
||||
self.nonWord += "".join(self.mainConf.fmtDoubleQuotes)
|
||||
self.nonWord += "".join(self.mainConf.fmtSingleQuotes)
|
||||
|
||||
# Typography
|
||||
if self.mainConf.fmtPadThin:
|
||||
self.typPadChar = nwUnicode.U_THNBSP
|
||||
else:
|
||||
self.typPadChar = nwUnicode.U_NBSP
|
||||
|
||||
self.typSQOpen = self.mainConf.fmtSingleQuotes[0]
|
||||
self.typSQClose = self.mainConf.fmtSingleQuotes[1]
|
||||
self.typDQOpen = self.mainConf.fmtDoubleQuotes[0]
|
||||
self.typDQClose = self.mainConf.fmtDoubleQuotes[1]
|
||||
|
||||
# Reload spell check and dictionaries
|
||||
self._setupSpellChecking()
|
||||
self.setDictionaries()
|
||||
@@ -206,6 +217,7 @@ class GuiDocEditor(QTextEdit):
|
||||
if self.mainConf.textFont is None:
|
||||
# If none is defined, set the default back to config
|
||||
self.mainConf.textFont = self.qDocument.defaultFont().family()
|
||||
|
||||
theFont.setFamily(self.mainConf.textFont)
|
||||
theFont.setPointSize(self.mainConf.textSize)
|
||||
self.setFont(theFont)
|
||||
@@ -293,11 +305,17 @@ class GuiDocEditor(QTextEdit):
|
||||
|
||||
docSize = len(theDoc)
|
||||
if docSize > nwConst.MAX_DOCSIZE:
|
||||
self.theParent.makeAlert((
|
||||
"The document you are trying to open is too big. "
|
||||
"The document size is %.2f\u202fMB. "
|
||||
"The maximum size allowed is %.2f\u202fMB."
|
||||
) % (docSize/1.0e6, nwConst.MAX_DOCSIZE/1.0e6), nwAlert.ERROR)
|
||||
self.theParent.makeAlert(
|
||||
self.tr(
|
||||
"The document you are trying to open is too big. "
|
||||
"The document size is {0} MB. "
|
||||
"The maximum size allowed is {1} MB."
|
||||
).format(
|
||||
f"{docSize/1.0e6:.2f}",
|
||||
f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}"
|
||||
),
|
||||
nwAlert.ERROR
|
||||
)
|
||||
self.clearEditor()
|
||||
return False
|
||||
|
||||
@@ -380,11 +398,17 @@ class GuiDocEditor(QTextEdit):
|
||||
"""
|
||||
docSize = len(theText)
|
||||
if docSize > nwConst.MAX_DOCSIZE:
|
||||
self.theParent.makeAlert((
|
||||
"The text you are trying to add is too big. "
|
||||
"The text size is %.2f\u202fMB. "
|
||||
"The maximum size allowed is %.2f\u202fMB."
|
||||
) % (docSize/1.0e6, nwConst.MAX_DOCSIZE/1.0e6), nwAlert.ERROR)
|
||||
self.theParent.makeAlert(
|
||||
self.tr(
|
||||
"The text you are trying to add is too big. "
|
||||
"The text size is {0} MB. "
|
||||
"The maximum size allowed is {1} MB."
|
||||
).format(
|
||||
f"{docSize/1.0e6:.2f}",
|
||||
f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}"
|
||||
),
|
||||
nwAlert.ERROR
|
||||
)
|
||||
return False
|
||||
|
||||
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
|
||||
@@ -568,12 +592,14 @@ class GuiDocEditor(QTextEdit):
|
||||
"""
|
||||
if not isinstance(theLine, int):
|
||||
return False
|
||||
|
||||
if theLine >= 0:
|
||||
theBlock = self.qDocument.findBlockByLineNumber(theLine)
|
||||
if theBlock:
|
||||
self.setCursorPosition(theBlock.position())
|
||||
self.docFooter.updateLineCount()
|
||||
logger.verbose("Cursor moved to line %d" % theLine)
|
||||
|
||||
return True
|
||||
|
||||
##
|
||||
@@ -585,17 +611,15 @@ class GuiDocEditor(QTextEdit):
|
||||
status bar to show the one actually loaded by the spell checker
|
||||
class.
|
||||
"""
|
||||
if self.theProject.projLang is None:
|
||||
if self.theProject.projSpell is None:
|
||||
theLang = self.mainConf.spellLanguage
|
||||
else:
|
||||
theLang = self.theProject.projLang
|
||||
theLang = self.theProject.projSpell
|
||||
|
||||
self.theDict.setLanguage(theLang, self.theProject.projDict)
|
||||
theTag, theProvider = self.theDict.describeDict()
|
||||
|
||||
aLang, aName = self.theDict.describeDict()
|
||||
self.theParent.statusBar.setLanguage(
|
||||
aLang, "%s [%s]" % (self.mainConf.spellTool.title(), aName.title())
|
||||
)
|
||||
self.theParent.statusBar.setLanguage(theLang, theProvider)
|
||||
|
||||
if not self.bigDoc:
|
||||
self.spellCheckDocument()
|
||||
@@ -641,10 +665,8 @@ class GuiDocEditor(QTextEdit):
|
||||
self.hLight.rehighlight()
|
||||
qApp.restoreOverrideCursor()
|
||||
afTime = time()
|
||||
logger.debug(
|
||||
"Document highlighted in %.3f ms" % (1000*(afTime-bfTime))
|
||||
)
|
||||
self.theParent.statusBar.showMessage("Spell check complete")
|
||||
logger.debug("Document highlighted in %.3f ms" % (1000*(afTime-bfTime)))
|
||||
self.theParent.statusBar.showMessage(self.tr("Spell check complete"))
|
||||
|
||||
return True
|
||||
|
||||
@@ -660,10 +682,6 @@ class GuiDocEditor(QTextEdit):
|
||||
this class when calling these actions from other classes.
|
||||
"""
|
||||
logger.verbose("Requesting action: %s" % theAction.name)
|
||||
if not self.hasFocus():
|
||||
logger.verbose("Editor does not have focus")
|
||||
return False
|
||||
|
||||
if self.theHandle is None:
|
||||
logger.error("No document open")
|
||||
return False
|
||||
@@ -693,16 +711,6 @@ class GuiDocEditor(QTextEdit):
|
||||
self._makeSelection(QTextCursor.Document)
|
||||
elif theAction == nwDocAction.SEL_PARA:
|
||||
self._makeSelection(QTextCursor.BlockUnderCursor)
|
||||
elif theAction == nwDocAction.FIND:
|
||||
self._beginSearch()
|
||||
elif theAction == nwDocAction.REPLACE:
|
||||
self._beginReplace()
|
||||
elif theAction == nwDocAction.GO_NEXT:
|
||||
self._findNext()
|
||||
elif theAction == nwDocAction.GO_PREV:
|
||||
self._findNext(isBackward=True)
|
||||
elif theAction == nwDocAction.REPL_NEXT:
|
||||
self._replaceNext()
|
||||
elif theAction == nwDocAction.BLOCK_H1:
|
||||
self._formatBlock(nwDocAction.BLOCK_H1)
|
||||
elif theAction == nwDocAction.BLOCK_H2:
|
||||
@@ -734,6 +742,15 @@ class GuiDocEditor(QTextEdit):
|
||||
"""
|
||||
return self.qDocument.isEmpty()
|
||||
|
||||
def anyFocus(self):
|
||||
"""Check if any widget or child widget has focus.
|
||||
"""
|
||||
if self.hasFocus():
|
||||
return True
|
||||
if self.isAncestorOf(qApp.focusWidget()):
|
||||
return True
|
||||
return False
|
||||
|
||||
def revealLocation(self):
|
||||
"""Tell the user where on the file system the file in the editor
|
||||
is saved.
|
||||
@@ -743,14 +760,14 @@ class GuiDocEditor(QTextEdit):
|
||||
return False
|
||||
|
||||
msgBox = QMessageBox()
|
||||
msgBox.information(self, "File Location", (
|
||||
"File details for the currently open file<br>"
|
||||
"Handle: {handle:s}<br>"
|
||||
"Location: {fileLoc:s}"
|
||||
).format(
|
||||
handle = self.theHandle,
|
||||
fileLoc = str(self.nwDocument.getFileLocation())
|
||||
))
|
||||
msgBox.information(
|
||||
self,
|
||||
self.tr("File Location"),
|
||||
"%s<br>%s" % (
|
||||
self.tr("The currently open file is saved in:"),
|
||||
self.nwDocument.getFileLocation()
|
||||
),
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
@@ -799,7 +816,7 @@ class GuiDocEditor(QTextEdit):
|
||||
theCursor = self.textCursor()
|
||||
theBlock = theCursor.block()
|
||||
if not theBlock.isValid():
|
||||
logger.error("Filed to insert keyword '%s'" % keyWord)
|
||||
logger.error("Failed to insert keyword '%s'" % keyWord)
|
||||
return False
|
||||
|
||||
theCursor.beginEditBlock()
|
||||
@@ -827,7 +844,7 @@ class GuiDocEditor(QTextEdit):
|
||||
if self.docSearch.isVisible():
|
||||
self.docSearch.closeSearch()
|
||||
else:
|
||||
self._beginSearch()
|
||||
self.beginSearch()
|
||||
return
|
||||
|
||||
##
|
||||
@@ -933,10 +950,15 @@ class GuiDocEditor(QTextEdit):
|
||||
self.lastFind = None
|
||||
|
||||
if self.qDocument.characterCount() > nwConst.MAX_DOCSIZE:
|
||||
self.theParent.makeAlert((
|
||||
"The document has grown too big and you cannot add more text to it. "
|
||||
"The maximum size of a single novelWriter document is %.2f\u202fMB."
|
||||
) % (nwConst.MAX_DOCSIZE/1.0e6), nwAlert.ERROR)
|
||||
self.theParent.makeAlert(
|
||||
self.tr(
|
||||
"The document has grown too big and you cannot add more text to it. "
|
||||
"The maximum size of a single novelWriter document is {0} MB."
|
||||
).format(
|
||||
f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}"
|
||||
),
|
||||
nwAlert.ERROR
|
||||
)
|
||||
self.undo()
|
||||
return
|
||||
|
||||
@@ -966,21 +988,21 @@ class GuiDocEditor(QTextEdit):
|
||||
# ===========================
|
||||
|
||||
if self._followTag(theCursor=posCursor, loadTag=False):
|
||||
mnuTag = QAction("Follow Tag", mnuContext)
|
||||
mnuTag = QAction(self.tr("Follow Tag"), mnuContext)
|
||||
mnuTag.triggered.connect(lambda: self._followTag(theCursor=posCursor))
|
||||
mnuContext.addAction(mnuTag)
|
||||
mnuContext.addSeparator()
|
||||
|
||||
if userSelection:
|
||||
mnuCut = QAction("Cut", mnuContext)
|
||||
mnuCut = QAction(self.tr("Cut"), mnuContext)
|
||||
mnuCut.triggered.connect(lambda: self.docAction(nwDocAction.CUT))
|
||||
mnuContext.addAction(mnuCut)
|
||||
|
||||
mnuCopy = QAction("Copy", mnuContext)
|
||||
mnuCopy = QAction(self.tr("Copy"), mnuContext)
|
||||
mnuCopy.triggered.connect(lambda: self.docAction(nwDocAction.COPY))
|
||||
mnuContext.addAction(mnuCopy)
|
||||
|
||||
mnuPaste = QAction("Paste", mnuContext)
|
||||
mnuPaste = QAction(self.tr("Paste"), mnuContext)
|
||||
mnuPaste.triggered.connect(lambda: self.docAction(nwDocAction.PASTE))
|
||||
mnuContext.addAction(mnuPaste)
|
||||
|
||||
@@ -989,17 +1011,17 @@ class GuiDocEditor(QTextEdit):
|
||||
# Selections
|
||||
# ==========
|
||||
|
||||
mnuSelAll = QAction("Select All", mnuContext)
|
||||
mnuSelAll = QAction(self.tr("Select All"), mnuContext)
|
||||
mnuSelAll.triggered.connect(lambda: self.docAction(nwDocAction.SEL_ALL))
|
||||
mnuContext.addAction(mnuSelAll)
|
||||
|
||||
mnuSelWord = QAction("Select Word", mnuContext)
|
||||
mnuSelWord = QAction(self.tr("Select Word"), mnuContext)
|
||||
mnuSelWord.triggered.connect(
|
||||
lambda: self._makePosSelection(QTextCursor.WordUnderCursor, thePos)
|
||||
)
|
||||
mnuContext.addAction(mnuSelWord)
|
||||
|
||||
mnuSelPara = QAction("Select Paragraph", mnuContext)
|
||||
mnuSelPara = QAction(self.tr("Select Paragraph"), mnuContext)
|
||||
mnuSelPara.triggered.connect(
|
||||
lambda: self._makePosSelection(QTextCursor.BlockUnderCursor, thePos)
|
||||
)
|
||||
@@ -1025,7 +1047,7 @@ class GuiDocEditor(QTextEdit):
|
||||
|
||||
if spellCheck:
|
||||
mnuContext.addSeparator()
|
||||
mnuHead = QAction("Spelling Suggestion(s)", mnuContext)
|
||||
mnuHead = QAction(self.tr("Spelling Suggestion(s)"), mnuContext)
|
||||
mnuContext.addAction(mnuHead)
|
||||
|
||||
theSuggest = self.theDict.suggestWords(theWord)[:15]
|
||||
@@ -1037,11 +1059,13 @@ class GuiDocEditor(QTextEdit):
|
||||
)
|
||||
mnuContext.addAction(mnuWord)
|
||||
else:
|
||||
mnuHead = QAction("%s No Suggestions" % nwUnicode.U_ENDASH, mnuContext)
|
||||
mnuHead = QAction(
|
||||
"%s %s" % (nwUnicode.U_ENDASH, self.tr("No Suggestions")), mnuContext
|
||||
)
|
||||
mnuContext.addAction(mnuHead)
|
||||
|
||||
mnuContext.addSeparator()
|
||||
mnuAdd = QAction("Add Word to Dictionary", mnuContext)
|
||||
mnuAdd = QAction(self.tr("Add Word to Dictionary"), mnuContext)
|
||||
mnuAdd.triggered.connect(lambda thePos: self._addWord(posCursor))
|
||||
mnuContext.addAction(mnuAdd)
|
||||
|
||||
@@ -1131,15 +1155,149 @@ class GuiDocEditor(QTextEdit):
|
||||
QPointF(theSize.width(), theSize.height()), Qt.FuzzyHit
|
||||
)
|
||||
if self.queuePos <= thePos:
|
||||
logger.verbose(
|
||||
"Allowed cursor move to %d <= %d" % (self.queuePos, thePos)
|
||||
)
|
||||
logger.verbose("Allowed cursor move to %d <= %d" % (self.queuePos, thePos))
|
||||
self.setCursorPosition(self.queuePos)
|
||||
self.queuePos = None
|
||||
else:
|
||||
logger.verbose(
|
||||
"Denied cursor move to %d > %d" % (self.queuePos, thePos)
|
||||
logger.verbose("Denied cursor move to %d > %d" % (self.queuePos, thePos))
|
||||
return
|
||||
|
||||
##
|
||||
# Search & Replace
|
||||
##
|
||||
|
||||
def beginSearch(self):
|
||||
"""Sets the selected text as the search text for the search bar.
|
||||
"""
|
||||
theCursor = self.textCursor()
|
||||
if theCursor.hasSelection():
|
||||
self.docSearch.setSearchText(theCursor.selectedText())
|
||||
else:
|
||||
self.docSearch.setSearchText(None)
|
||||
self.updateDocMargins()
|
||||
return
|
||||
|
||||
def beginReplace(self):
|
||||
"""Opens the replace line of the search bar and sets the find
|
||||
text if a selection has been made, and resets the replace text.
|
||||
"""
|
||||
theCursor = self.textCursor()
|
||||
if theCursor.hasSelection():
|
||||
self.docSearch.setSearchText(theCursor.selectedText())
|
||||
else:
|
||||
self.docSearch.setSearchText(None)
|
||||
self.docSearch.setReplaceText("")
|
||||
self.updateDocMargins()
|
||||
return
|
||||
|
||||
def findNext(self, goBack=False):
|
||||
"""Searches for the next or previous occurrence of the search
|
||||
bar text in the document. Wraps around if not found and loop is
|
||||
enabled, or continues to next file if next file is enabled.
|
||||
"""
|
||||
if not self.anyFocus():
|
||||
logger.debug("Editor does not have focus")
|
||||
return False
|
||||
|
||||
if not self.docSearch.isVisible():
|
||||
self.beginSearch()
|
||||
return
|
||||
|
||||
findOpt = QTextDocument.FindFlag(0)
|
||||
if goBack:
|
||||
findOpt |= QTextDocument.FindBackward
|
||||
if self.docSearch.isCaseSense:
|
||||
findOpt |= QTextDocument.FindCaseSensitively
|
||||
if self.docSearch.isWholeWord:
|
||||
findOpt |= QTextDocument.FindWholeWords
|
||||
|
||||
searchFor = self.docSearch.getSearchObject()
|
||||
wasFound = self.find(searchFor, findOpt)
|
||||
if not wasFound:
|
||||
if self.docSearch.doNextFile and not goBack:
|
||||
self.theParent.openNextDocument(
|
||||
self.theHandle, wrapAround=self.docSearch.doLoop
|
||||
)
|
||||
elif self.docSearch.doLoop:
|
||||
theCursor = self.textCursor()
|
||||
theCursor.movePosition(
|
||||
QTextCursor.End if goBack else QTextCursor.Start
|
||||
)
|
||||
self.setTextCursor(theCursor)
|
||||
wasFound = self.find(searchFor, findOpt)
|
||||
|
||||
if wasFound:
|
||||
theCursor = self.textCursor()
|
||||
self.lastFind = (theCursor.selectionStart(), theCursor.selectionEnd())
|
||||
|
||||
return
|
||||
|
||||
def replaceNext(self):
|
||||
"""Searches for the next occurrence of the search bar text in
|
||||
the document and replaces it with the replace text. Calls search
|
||||
next automatically when done.
|
||||
"""
|
||||
if not self.anyFocus():
|
||||
logger.debug("Editor does not have focus")
|
||||
return False
|
||||
|
||||
if not self.docSearch.isVisible():
|
||||
# The search tool is not active, so we activate it.
|
||||
self.beginSearch()
|
||||
return
|
||||
|
||||
theCursor = self.textCursor()
|
||||
if not theCursor.hasSelection():
|
||||
# We have no text selected at all, so just make this a
|
||||
# regular find next call.
|
||||
self.findNext()
|
||||
return
|
||||
|
||||
if self.lastFind is None and theCursor.hasSelection():
|
||||
# If we have a selection but no search, it may have been the
|
||||
# text we triggered the search with, in which case we search
|
||||
# again from the beginning of that selection to make sure we
|
||||
# have a valid result.
|
||||
sPos = theCursor.selectionStart()
|
||||
theCursor.clearSelection()
|
||||
theCursor.setPosition(sPos)
|
||||
self.setTextCursor(theCursor)
|
||||
self.findNext()
|
||||
theCursor = self.textCursor()
|
||||
|
||||
if self.lastFind is None:
|
||||
# In case the above didn't find a result, we give up here.
|
||||
return
|
||||
|
||||
searchFor = self.docSearch.getSearchText()
|
||||
replWith = self.docSearch.getReplaceText()
|
||||
|
||||
if self.docSearch.doMatchCap:
|
||||
replWith = transferCase(theCursor.selectedText(), replWith)
|
||||
|
||||
# Make sure the selected text was selected by an actual find
|
||||
# call, and not the user.
|
||||
try:
|
||||
isFind = self.lastFind[0] == theCursor.selectionStart()
|
||||
isFind &= self.lastFind[1] == theCursor.selectionEnd()
|
||||
except Exception:
|
||||
isFind = False
|
||||
|
||||
if isFind:
|
||||
theCursor.beginEditBlock()
|
||||
theCursor.removeSelectedText()
|
||||
theCursor.insertText(replWith)
|
||||
theCursor.endEditBlock()
|
||||
theCursor.setPosition(theCursor.selectionEnd())
|
||||
self.setTextCursor(theCursor)
|
||||
logger.verbose("Replaced occurrence of '%s' with '%s' on line %d" % (
|
||||
searchFor, replWith, theCursor.blockNumber()
|
||||
))
|
||||
else:
|
||||
logger.error("The selected text is not a search result, skipping replace")
|
||||
|
||||
self.findNext()
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
@@ -1189,7 +1347,7 @@ class GuiDocEditor(QTextEdit):
|
||||
return
|
||||
|
||||
def _docAutoReplace(self, theBlock):
|
||||
"""Autoreplace text elements based on main configuration.
|
||||
"""Auto-replace text elements based on main configuration.
|
||||
"""
|
||||
if not theBlock.isValid():
|
||||
return
|
||||
@@ -1206,43 +1364,62 @@ class GuiDocEditor(QTextEdit):
|
||||
theTwo = theText[thePos-2:thePos]
|
||||
theThree = theText[thePos-3:thePos]
|
||||
|
||||
if self.mainConf.doReplaceDQuote and theTwo == " \"":
|
||||
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1)
|
||||
theCursor.insertText(self.typDQOpen)
|
||||
if not theOne: # Makes Neo sad
|
||||
return
|
||||
|
||||
elif self.mainConf.doReplaceDQuote and theOne == "\"":
|
||||
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1)
|
||||
nDelete = 0
|
||||
tInsert = theOne
|
||||
|
||||
if self.mainConf.doReplaceDQuote and theTwo == ' "':
|
||||
nDelete = 1
|
||||
tInsert = self.typDQOpen
|
||||
|
||||
elif self.mainConf.doReplaceDQuote and theOne == '"':
|
||||
nDelete = 1
|
||||
if thePos == 1:
|
||||
theCursor.insertText(self.typDQOpen)
|
||||
tInsert = self.typDQOpen
|
||||
else:
|
||||
theCursor.insertText(self.typDQClose)
|
||||
tInsert = self.typDQClose
|
||||
|
||||
elif self.mainConf.doReplaceSQuote and theTwo == " '":
|
||||
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1)
|
||||
theCursor.insertText(self.typSQOpen)
|
||||
nDelete = 1
|
||||
tInsert = self.typSQOpen
|
||||
|
||||
elif self.mainConf.doReplaceSQuote and theOne == "'":
|
||||
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1)
|
||||
nDelete = 1
|
||||
if thePos == 1:
|
||||
theCursor.insertText(self.typSQOpen)
|
||||
tInsert = self.typSQOpen
|
||||
else:
|
||||
theCursor.insertText(self.typSQClose)
|
||||
tInsert = self.typSQClose
|
||||
|
||||
elif self.mainConf.doReplaceDash and theThree == "---":
|
||||
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 3)
|
||||
theCursor.insertText(nwUnicode.U_EMDASH)
|
||||
nDelete = 3
|
||||
tInsert = nwUnicode.U_EMDASH
|
||||
|
||||
elif self.mainConf.doReplaceDash and theTwo == "--":
|
||||
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 2)
|
||||
theCursor.insertText(nwUnicode.U_ENDASH)
|
||||
nDelete = 2
|
||||
tInsert = nwUnicode.U_ENDASH
|
||||
|
||||
elif self.mainConf.doReplaceDash and theTwo == nwUnicode.U_ENDASH+"-":
|
||||
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 2)
|
||||
theCursor.insertText(nwUnicode.U_EMDASH)
|
||||
elif self.mainConf.doReplaceDash and theTwo == nwUnicode.U_ENDASH + "-":
|
||||
nDelete = 2
|
||||
tInsert = nwUnicode.U_EMDASH
|
||||
|
||||
elif self.mainConf.doReplaceDots and theThree == "...":
|
||||
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 3)
|
||||
theCursor.insertText(nwUnicode.U_HELLIP)
|
||||
nDelete = 3
|
||||
tInsert = nwUnicode.U_HELLIP
|
||||
|
||||
tCheck = tInsert
|
||||
if tCheck in self.mainConf.fmtPadBefore:
|
||||
nDelete = max(nDelete, 1)
|
||||
tInsert = self.typPadChar + tInsert
|
||||
|
||||
if tCheck in self.mainConf.fmtPadAfter:
|
||||
nDelete = max(nDelete, 1)
|
||||
tInsert = tInsert + self.typPadChar
|
||||
|
||||
if nDelete > 0:
|
||||
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, nDelete)
|
||||
theCursor.insertText(tInsert)
|
||||
|
||||
return
|
||||
|
||||
@@ -1317,7 +1494,8 @@ class GuiDocEditor(QTextEdit):
|
||||
|
||||
else:
|
||||
self.theParent.makeAlert(
|
||||
"Please selection some text before calling replace quotes.", nwAlert.ERROR
|
||||
self.tr("Please select some text before calling replace quotes."),
|
||||
nwAlert.ERROR
|
||||
)
|
||||
|
||||
return
|
||||
@@ -1530,9 +1708,7 @@ class GuiDocEditor(QTextEdit):
|
||||
theText = newText
|
||||
cOffset -= 0
|
||||
else:
|
||||
logger.error(
|
||||
"Unknown or unsupported block format requested: %s" % str(docAction)
|
||||
)
|
||||
logger.error("Unknown or unsupported block format requested: %s" % str(docAction))
|
||||
return False
|
||||
|
||||
# Replace the block text
|
||||
@@ -1585,132 +1761,6 @@ class GuiDocEditor(QTextEdit):
|
||||
self._makeSelection(selMode)
|
||||
return
|
||||
|
||||
def _beginSearch(self):
|
||||
"""Sets the selected text as the search text for the search bar.
|
||||
"""
|
||||
theCursor = self.textCursor()
|
||||
if theCursor.hasSelection():
|
||||
self.docSearch.setSearchText(theCursor.selectedText())
|
||||
else:
|
||||
self.docSearch.setSearchText(None)
|
||||
self.updateDocMargins()
|
||||
return
|
||||
|
||||
def _beginReplace(self):
|
||||
"""Opens the replace line of the search bar and sets the find
|
||||
text if a selection has been made, and resets the replace text.
|
||||
"""
|
||||
theCursor = self.textCursor()
|
||||
if theCursor.hasSelection():
|
||||
self.docSearch.setSearchText(theCursor.selectedText())
|
||||
else:
|
||||
self.docSearch.setSearchText(None)
|
||||
self.docSearch.setReplaceText("")
|
||||
self.updateDocMargins()
|
||||
return
|
||||
|
||||
def _findNext(self, isBackward=False):
|
||||
"""Searches for the next or previous occurrence of the search
|
||||
bar text in the document. Wraps around if not found and loop is
|
||||
enabled, or continues to next file if next file is enabled.
|
||||
"""
|
||||
if not self.docSearch.isVisible():
|
||||
self._beginSearch()
|
||||
return
|
||||
|
||||
findOpt = QTextDocument.FindFlag(0)
|
||||
if isBackward:
|
||||
findOpt |= QTextDocument.FindBackward
|
||||
if self.docSearch.isCaseSense:
|
||||
findOpt |= QTextDocument.FindCaseSensitively
|
||||
if self.docSearch.isWholeWord:
|
||||
findOpt |= QTextDocument.FindWholeWords
|
||||
|
||||
searchFor = self.docSearch.getSearchObject()
|
||||
wasFound = self.find(searchFor, findOpt)
|
||||
if not wasFound:
|
||||
if self.docSearch.doNextFile and not isBackward:
|
||||
self.theParent.openNextDocument(
|
||||
self.theHandle, wrapAround=self.docSearch.doLoop
|
||||
)
|
||||
elif self.docSearch.doLoop:
|
||||
theCursor = self.textCursor()
|
||||
theCursor.movePosition(
|
||||
QTextCursor.End if isBackward else QTextCursor.Start
|
||||
)
|
||||
self.setTextCursor(theCursor)
|
||||
wasFound = self.find(searchFor, findOpt)
|
||||
|
||||
if wasFound:
|
||||
theCursor = self.textCursor()
|
||||
self.lastFind = (theCursor.selectionStart(), theCursor.selectionEnd())
|
||||
|
||||
return
|
||||
|
||||
def _replaceNext(self):
|
||||
"""Searches for the next occurrence of the search bar text in
|
||||
the document and replaces it with the replace text. Calls search
|
||||
next automatically when done.
|
||||
"""
|
||||
if not self.docSearch.isVisible():
|
||||
# The search tool is not active, so we activate it.
|
||||
self._beginSearch()
|
||||
return
|
||||
|
||||
theCursor = self.textCursor()
|
||||
if not theCursor.hasSelection():
|
||||
# We have no text selected at all, so just make this a
|
||||
# regular find next call.
|
||||
self._findNext()
|
||||
return
|
||||
|
||||
if self.lastFind is None and theCursor.hasSelection():
|
||||
# If we have a selection but no search, it may have been the
|
||||
# text we triggered the search with, in which case we search
|
||||
# again from the beginning of that selection to make sure we
|
||||
# have a valid result.
|
||||
sPos = theCursor.selectionStart()
|
||||
theCursor.clearSelection()
|
||||
theCursor.setPosition(sPos)
|
||||
self.setTextCursor(theCursor)
|
||||
self._findNext()
|
||||
theCursor = self.textCursor()
|
||||
|
||||
if self.lastFind is None:
|
||||
# In case the above didn't find a result, we give up here.
|
||||
return
|
||||
|
||||
searchFor = self.docSearch.getSearchText()
|
||||
replWith = self.docSearch.getReplaceText()
|
||||
|
||||
if self.docSearch.doMatchCap:
|
||||
replWith = transferCase(theCursor.selectedText(), replWith)
|
||||
|
||||
# Make sure the selected text was selected by an actual find
|
||||
# call, and not the user.
|
||||
try:
|
||||
isFind = self.lastFind[0] == theCursor.selectionStart()
|
||||
isFind &= self.lastFind[1] == theCursor.selectionEnd()
|
||||
except Exception:
|
||||
isFind = False
|
||||
|
||||
if isFind:
|
||||
theCursor.beginEditBlock()
|
||||
theCursor.removeSelectedText()
|
||||
theCursor.insertText(replWith)
|
||||
theCursor.endEditBlock()
|
||||
theCursor.setPosition(theCursor.selectionEnd())
|
||||
self.setTextCursor(theCursor)
|
||||
logger.verbose("Replaced occurrence of '%s' with '%s' on line %d" % (
|
||||
searchFor, replWith, theCursor.blockNumber()
|
||||
))
|
||||
else:
|
||||
logger.error("The selected text is not a search result, skipping replace")
|
||||
|
||||
self._findNext()
|
||||
|
||||
return
|
||||
|
||||
def _setupSpellChecking(self):
|
||||
"""Create the spell checking object based on the spellTool
|
||||
setting in config.
|
||||
@@ -1817,12 +1867,12 @@ class GuiDocEditSearch(QFrame):
|
||||
# ==========
|
||||
self.searchBox = QLineEdit(self)
|
||||
self.searchBox.setFont(boxFont)
|
||||
self.searchBox.setPlaceholderText("Search")
|
||||
self.searchBox.setPlaceholderText(self.tr("Search"))
|
||||
self.searchBox.returnPressed.connect(self._doSearch)
|
||||
|
||||
self.replaceBox = QLineEdit(self)
|
||||
self.replaceBox.setFont(boxFont)
|
||||
self.replaceBox.setPlaceholderText("Replace")
|
||||
self.replaceBox.setPlaceholderText(self.tr("Replace"))
|
||||
self.replaceBox.returnPressed.connect(self._doReplace)
|
||||
|
||||
self.searchOpt = QToolBar(self)
|
||||
@@ -1831,44 +1881,44 @@ class GuiDocEditSearch(QFrame):
|
||||
self.searchOpt.setContentsMargins(0, 0, 0, 0)
|
||||
self.searchOpt.setStyleSheet(r"QToolBar {padding: 0;}")
|
||||
|
||||
self.searchLabel = QLabel("Search")
|
||||
self.searchLabel = QLabel(self.tr("Search"))
|
||||
self.searchLabel.setFont(boxFont)
|
||||
self.searchLabel.setIndent(self.mainConf.pxInt(6))
|
||||
|
||||
self.toggleCase = QAction("Case Sensitive", self)
|
||||
self.toggleCase.setToolTip("Match case")
|
||||
self.toggleCase = QAction(self.tr("Case Sensitive"), self)
|
||||
self.toggleCase.setToolTip(self.tr("Match case"))
|
||||
self.toggleCase.setIcon(self.theTheme.getIcon("search_case"))
|
||||
self.toggleCase.setCheckable(True)
|
||||
self.toggleCase.setChecked(self.isCaseSense)
|
||||
self.toggleCase.toggled.connect(self._doToggleCase)
|
||||
self.searchOpt.addAction(self.toggleCase)
|
||||
|
||||
self.toggleWord = QAction("Whole Words Only", self)
|
||||
self.toggleWord.setToolTip("Match whole words")
|
||||
self.toggleWord = QAction(self.tr("Whole Words Only"), self)
|
||||
self.toggleWord.setToolTip(self.tr("Match whole words"))
|
||||
self.toggleWord.setIcon(self.theTheme.getIcon("search_word"))
|
||||
self.toggleWord.setCheckable(True)
|
||||
self.toggleWord.setChecked(self.isWholeWord)
|
||||
self.toggleWord.toggled.connect(self._doToggleWord)
|
||||
self.searchOpt.addAction(self.toggleWord)
|
||||
|
||||
self.toggleRegEx = QAction("RegEx Mode", self)
|
||||
self.toggleRegEx.setToolTip("Use regular expressions (requires Qt 5.3)")
|
||||
self.toggleRegEx = QAction(self.tr("RegEx Mode"), self)
|
||||
self.toggleRegEx.setToolTip(self.tr("Search using regular expressions"))
|
||||
self.toggleRegEx.setIcon(self.theTheme.getIcon("search_regex"))
|
||||
self.toggleRegEx.setCheckable(True)
|
||||
self.toggleRegEx.setChecked(self.isRegEx)
|
||||
self.toggleRegEx.toggled.connect(self._doToggleRegEx)
|
||||
self.searchOpt.addAction(self.toggleRegEx)
|
||||
|
||||
self.toggleLoop = QAction("Loop Search", self)
|
||||
self.toggleLoop.setToolTip("Loop the search when reaching the end")
|
||||
self.toggleLoop = QAction(self.tr("Loop Search"), self)
|
||||
self.toggleLoop.setToolTip(self.tr("Loop the search when reaching the end"))
|
||||
self.toggleLoop.setIcon(self.theTheme.getIcon("search_loop"))
|
||||
self.toggleLoop.setCheckable(True)
|
||||
self.toggleLoop.setChecked(self.doLoop)
|
||||
self.toggleLoop.toggled.connect(self._doToggleLoop)
|
||||
self.searchOpt.addAction(self.toggleLoop)
|
||||
|
||||
self.toggleProject = QAction("Search Next File", self)
|
||||
self.toggleProject.setToolTip("Continue searching in the next file")
|
||||
self.toggleProject = QAction(self.tr("Search Next File"), self)
|
||||
self.toggleProject.setToolTip(self.tr("Continue searching in the next file"))
|
||||
self.toggleProject.setIcon(self.theTheme.getIcon("search_project"))
|
||||
self.toggleProject.setCheckable(True)
|
||||
self.toggleProject.setChecked(self.doNextFile)
|
||||
@@ -1877,8 +1927,8 @@ class GuiDocEditSearch(QFrame):
|
||||
|
||||
self.searchOpt.addSeparator()
|
||||
|
||||
self.toggleMatchCap = QAction("Preserve Case", self)
|
||||
self.toggleMatchCap.setToolTip("Preserve case on replace")
|
||||
self.toggleMatchCap = QAction(self.tr("Preserve Case"), self)
|
||||
self.toggleMatchCap.setToolTip(self.tr("Preserve case on replace"))
|
||||
self.toggleMatchCap.setIcon(self.theTheme.getIcon("search_preserve"))
|
||||
self.toggleMatchCap.setCheckable(True)
|
||||
self.toggleMatchCap.setChecked(self.doMatchCap)
|
||||
@@ -1887,8 +1937,8 @@ class GuiDocEditSearch(QFrame):
|
||||
|
||||
self.searchOpt.addSeparator()
|
||||
|
||||
self.cancelSearch = QAction("Close Search", self)
|
||||
self.cancelSearch.setToolTip("Close the search box [Esc]")
|
||||
self.cancelSearch = QAction(self.tr("Close Search"), self)
|
||||
self.cancelSearch.setToolTip(self.tr("Close the search box [{0}]").format("Esc"))
|
||||
self.cancelSearch.setIcon(self.theTheme.getIcon("search_cancel"))
|
||||
self.cancelSearch.triggered.connect(self._doClose)
|
||||
self.searchOpt.addAction(self.cancelSearch)
|
||||
@@ -1900,18 +1950,18 @@ class GuiDocEditSearch(QFrame):
|
||||
self.showReplace = QToolButton(self)
|
||||
self.showReplace.setArrowType(Qt.RightArrow)
|
||||
self.showReplace.setCheckable(True)
|
||||
self.showReplace.setToolTip("Show/hide the replace text box")
|
||||
self.showReplace.setToolTip(self.tr("Show/hide the replace text box"))
|
||||
self.showReplace.setStyleSheet(r"QToolButton {border: none; background: transparent;}")
|
||||
self.showReplace.toggled.connect(self._doToggleReplace)
|
||||
|
||||
self.searchButton = QPushButton(self.theTheme.getIcon("search"), "")
|
||||
self.searchButton.setFixedSize(QSize(bPx, bPx))
|
||||
self.searchButton.setToolTip("Find in current document")
|
||||
self.searchButton.setToolTip(self.tr("Find in current document"))
|
||||
self.searchButton.clicked.connect(self._doSearch)
|
||||
|
||||
self.replaceButton = QPushButton(self.theTheme.getIcon("search-replace"), "")
|
||||
self.replaceButton.setFixedSize(QSize(bPx, bPx))
|
||||
self.replaceButton.setToolTip("Find and replace in current document")
|
||||
self.replaceButton.setToolTip(self.tr("Find and replace in current document"))
|
||||
self.replaceButton.clicked.connect(self._doReplace)
|
||||
|
||||
self.mainBox.addWidget(self.searchLabel, 0, 0, 1, 2, Qt.AlignLeft)
|
||||
@@ -2038,7 +2088,7 @@ class GuiDocEditSearch(QFrame):
|
||||
self._alertSearchValid(theRegEx.isValid())
|
||||
return theRegEx
|
||||
|
||||
elif self.mainConf.verQtValue >= 50300:
|
||||
else: # >= 50300 to < 51300
|
||||
if self.isCaseSense:
|
||||
rxOpt = Qt.CaseSensitive
|
||||
else:
|
||||
@@ -2074,15 +2124,15 @@ class GuiDocEditSearch(QFrame):
|
||||
"""
|
||||
modKey = qApp.keyboardModifiers()
|
||||
if modKey == Qt.ShiftModifier:
|
||||
self.docEditor.docAction(nwDocAction.GO_PREV)
|
||||
self.docEditor.findNext(goBack=True)
|
||||
else:
|
||||
self.docEditor.docAction(nwDocAction.GO_NEXT)
|
||||
self.docEditor.findNext()
|
||||
return
|
||||
|
||||
def _doReplace(self):
|
||||
"""Call the replace action function for the document editor.
|
||||
"""
|
||||
self.docEditor.docAction(nwDocAction.REPL_NEXT)
|
||||
self.docEditor.replaceNext()
|
||||
return
|
||||
|
||||
def _doToggleReplace(self, theState):
|
||||
@@ -2171,7 +2221,6 @@ class GuiDocEditHeader(QWidget):
|
||||
|
||||
fPx = int(0.9*self.theTheme.fontPixelSize)
|
||||
hSp = self.mainConf.pxInt(6)
|
||||
self.buttonSize = fPx + hSp
|
||||
|
||||
# Main Widget Settings
|
||||
self.setAutoFillBackground(True)
|
||||
@@ -2204,7 +2253,7 @@ class GuiDocEditHeader(QWidget):
|
||||
self.editButton.setStyleSheet(buttonStyle)
|
||||
self.editButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.editButton.setVisible(False)
|
||||
self.editButton.setToolTip("Edit document meta")
|
||||
self.editButton.setToolTip(self.tr("Edit document meta"))
|
||||
self.editButton.clicked.connect(self._editDocument)
|
||||
|
||||
self.searchButton = QToolButton(self)
|
||||
@@ -2215,7 +2264,7 @@ class GuiDocEditHeader(QWidget):
|
||||
self.searchButton.setStyleSheet(buttonStyle)
|
||||
self.searchButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.searchButton.setVisible(False)
|
||||
self.searchButton.setToolTip("Search document")
|
||||
self.searchButton.setToolTip(self.tr("Search document"))
|
||||
self.searchButton.clicked.connect(self._searchDocument)
|
||||
|
||||
self.minmaxButton = QToolButton(self)
|
||||
@@ -2226,7 +2275,7 @@ class GuiDocEditHeader(QWidget):
|
||||
self.minmaxButton.setStyleSheet(buttonStyle)
|
||||
self.minmaxButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.minmaxButton.setVisible(False)
|
||||
self.minmaxButton.setToolTip("Toggle Focus Mode")
|
||||
self.minmaxButton.setToolTip(self.tr("Toggle Focus Mode"))
|
||||
self.minmaxButton.clicked.connect(self._minmaxDocument)
|
||||
|
||||
self.closeButton = QToolButton(self)
|
||||
@@ -2237,7 +2286,7 @@ class GuiDocEditHeader(QWidget):
|
||||
self.closeButton.setStyleSheet(buttonStyle)
|
||||
self.closeButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.closeButton.setVisible(False)
|
||||
self.closeButton.setToolTip("Close the document")
|
||||
self.closeButton.setToolTip(self.tr("Close the document"))
|
||||
self.closeButton.clicked.connect(self._closeDocument)
|
||||
|
||||
# Assemble Layout
|
||||
@@ -2317,6 +2366,17 @@ class GuiDocEditHeader(QWidget):
|
||||
|
||||
return True
|
||||
|
||||
def updateFocusMode(self):
|
||||
"""Update the minimise/maximise icon of the Focus Mode button.
|
||||
This function is called by the GuiMain class via the
|
||||
toggleFocusMode function and should not be activated directly.
|
||||
"""
|
||||
if self.theParent.isFocusMode:
|
||||
self.minmaxButton.setIcon(self.theTheme.getIcon("minimise"))
|
||||
else:
|
||||
self.minmaxButton.setIcon(self.theTheme.getIcon("maximise"))
|
||||
return
|
||||
|
||||
##
|
||||
# Slots
|
||||
##
|
||||
@@ -2347,18 +2407,6 @@ class GuiDocEditHeader(QWidget):
|
||||
"""Switch on or off Focus Mode.
|
||||
"""
|
||||
self.theParent.toggleFocusMode()
|
||||
if self.theParent.isFocusMode:
|
||||
self.minmaxButton.setIcon(self.theTheme.getIcon("minimise"))
|
||||
self.setContentsMargins(self.buttonSize, 0, 0, 0)
|
||||
self.editButton.setVisible(False)
|
||||
self.searchButton.setVisible(False)
|
||||
self.closeButton.setVisible(False)
|
||||
else:
|
||||
self.minmaxButton.setIcon(self.theTheme.getIcon("maximise"))
|
||||
self.setContentsMargins(0, 0, 0, 0)
|
||||
self.editButton.setVisible(True)
|
||||
self.searchButton.setVisible(True)
|
||||
self.closeButton.setVisible(True)
|
||||
return
|
||||
|
||||
##
|
||||
@@ -2413,7 +2461,7 @@ class GuiDocEditFooter(QWidget):
|
||||
self.statusIcon.setFixedHeight(self.sPx)
|
||||
self.statusIcon.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
||||
|
||||
self.statusText = QLabel("Status")
|
||||
self.statusText = QLabel(self.tr("Status"))
|
||||
self.statusText.setIndent(0)
|
||||
self.statusText.setMargin(0)
|
||||
self.statusText.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -2429,7 +2477,7 @@ class GuiDocEditFooter(QWidget):
|
||||
self.linesIcon.setFixedHeight(self.sPx)
|
||||
self.linesIcon.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
||||
|
||||
self.linesText = QLabel("Line: 0")
|
||||
self.linesText = QLabel("")
|
||||
self.linesText.setIndent(0)
|
||||
self.linesText.setMargin(0)
|
||||
self.linesText.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -2445,7 +2493,7 @@ class GuiDocEditFooter(QWidget):
|
||||
self.wordsIcon.setFixedHeight(self.sPx)
|
||||
self.wordsIcon.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
||||
|
||||
self.wordsText = QLabel("Words: 0")
|
||||
self.wordsText = QLabel("")
|
||||
self.wordsText.setIndent(0)
|
||||
self.wordsText.setMargin(0)
|
||||
self.wordsText.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -2476,6 +2524,8 @@ class GuiDocEditFooter(QWidget):
|
||||
|
||||
# Fix the Colours
|
||||
self.matchColours()
|
||||
self.updateLineCount()
|
||||
self.updateCounts()
|
||||
|
||||
logger.debug("GuiDocEditFooter initialisation complete")
|
||||
|
||||
@@ -2532,8 +2582,8 @@ class GuiDocEditFooter(QWidget):
|
||||
theIcon = self.theParent.importIcons[iStatus]
|
||||
|
||||
sIcon = theIcon.pixmap(self.sPx, self.sPx)
|
||||
sClass = nwLabels.CLASS_NAME[self.theItem.itemClass]
|
||||
sLayout = nwLabels.LAYOUT_NAME[self.theItem.itemLayout]
|
||||
sClass = trConst(nwLabels.CLASS_NAME[self.theItem.itemClass])
|
||||
sLayout = trConst(nwLabels.LAYOUT_NAME[self.theItem.itemLayout])
|
||||
sText = f"{self.theItem.itemStatus} / {sClass} / {sLayout}"
|
||||
|
||||
self.statusIcon.setPixmap(sIcon)
|
||||
@@ -2552,7 +2602,9 @@ class GuiDocEditFooter(QWidget):
|
||||
iLine = theCursor.blockNumber() + 1
|
||||
iDist = 100*iLine/self.docEditor.qDocument.blockCount()
|
||||
|
||||
self.linesText.setText(f"Line: {iLine:n} ({iDist:.0f}\u202f%)")
|
||||
self.linesText.setText(
|
||||
self.tr("Line: {0} ({1})").format(f"{iLine:n}", f"{iDist:.0f} %")
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
@@ -2566,10 +2618,14 @@ class GuiDocEditFooter(QWidget):
|
||||
wCount = self.theItem.wordCount
|
||||
wDiff = wCount - self.theItem.initCount
|
||||
|
||||
self.wordsText.setText(f"Words: {wCount:n} ({wDiff:+n})")
|
||||
self.wordsText.setText(
|
||||
self.tr("Words: {0} ({1})").format(f"{wCount:n}", f"{wDiff:+n}")
|
||||
)
|
||||
|
||||
byteSize = self.docEditor.qDocument.characterCount()
|
||||
self.wordsText.setToolTip(f"Document size is {byteSize:n} bytes")
|
||||
self.wordsText.setToolTip(
|
||||
self.tr("Document size is {0} bytes").format(f"{byteSize:n}")
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ from PyQt5.QtGui import (
|
||||
QColor, QTextCharFormat, QFont, QSyntaxHighlighter, QBrush
|
||||
)
|
||||
|
||||
from nw.constants import nwUnicode, nwRegEx
|
||||
from nw.constants import nwRegEx, nwUnicode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -41,7 +41,8 @@ from PyQt5.QtWidgets import (
|
||||
)
|
||||
|
||||
from nw.core import ToHtml
|
||||
from nw.constants import nwAlert, nwItemType, nwDocAction, nwUnicode
|
||||
from nw.enum import nwAlert, nwItemType, nwDocAction
|
||||
from nw.constants import nwUnicode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -183,7 +184,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
except Exception:
|
||||
logger.error("Failed to generate preview for document with handle '%s'" % tHandle)
|
||||
nw.logException()
|
||||
self.setText("An error occurred while generating the preview.")
|
||||
self.setText(self.tr("An error occurred while generating the preview."))
|
||||
return False
|
||||
|
||||
# Refresh the tab stops
|
||||
@@ -243,11 +244,14 @@ class GuiDocViewer(QTextBrowser):
|
||||
logger.debug("Loading document from tag '%s'" % theTag)
|
||||
tHandle, _, sTitle = self.theParent.theIndex.getTagSource(theTag)
|
||||
if tHandle is None:
|
||||
self.theParent.makeAlert((
|
||||
"Could not find the reference for tag '%s'. It either doesn't "
|
||||
"exist, or the index is out of date. The index can be updated "
|
||||
"from the Tools menu, or by pressing F9."
|
||||
) % theTag, nwAlert.ERROR)
|
||||
self.theParent.makeAlert(
|
||||
self.tr(
|
||||
"Could not find the reference for tag '{0}'. It either doesn't "
|
||||
"exist, or the index is out of date. The index can be updated "
|
||||
"from the Tools menu, or by pressing {1}."
|
||||
).format(theTag, "F9"),
|
||||
nwAlert.ERROR
|
||||
)
|
||||
return False
|
||||
else:
|
||||
# Let the parent handle the opening as it also ensures that
|
||||
@@ -415,7 +419,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
# ===================
|
||||
|
||||
if userSelection:
|
||||
mnuCopy = QAction("Copy", mnuContext)
|
||||
mnuCopy = QAction(self.tr("Copy"), mnuContext)
|
||||
mnuCopy.triggered.connect(lambda: self.docAction(nwDocAction.COPY))
|
||||
mnuContext.addAction(mnuCopy)
|
||||
|
||||
@@ -424,17 +428,17 @@ class GuiDocViewer(QTextBrowser):
|
||||
# Selections
|
||||
# ==========
|
||||
|
||||
mnuSelAll = QAction("Select All", mnuContext)
|
||||
mnuSelAll = QAction(self.tr("Select All"), mnuContext)
|
||||
mnuSelAll.triggered.connect(lambda: self.docAction(nwDocAction.SEL_ALL))
|
||||
mnuContext.addAction(mnuSelAll)
|
||||
|
||||
mnuSelWord = QAction("Select Word", mnuContext)
|
||||
mnuSelWord = QAction(self.tr("Select Word"), mnuContext)
|
||||
mnuSelWord.triggered.connect(
|
||||
lambda: self._makePosSelection(QTextCursor.WordUnderCursor, thePos)
|
||||
)
|
||||
mnuContext.addAction(mnuSelWord)
|
||||
|
||||
mnuSelPara = QAction("Select Paragraph", mnuContext)
|
||||
mnuSelPara = QAction(self.tr("Select Paragraph"), mnuContext)
|
||||
mnuSelPara.triggered.connect(
|
||||
lambda: self._makePosSelection(QTextCursor.BlockUnderCursor, thePos)
|
||||
)
|
||||
@@ -740,7 +744,7 @@ class GuiDocViewHeader(QWidget):
|
||||
self.backButton.setStyleSheet(buttonStyle)
|
||||
self.backButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.backButton.setVisible(False)
|
||||
self.backButton.setToolTip("Go backward")
|
||||
self.backButton.setToolTip(self.tr("Go backward"))
|
||||
self.backButton.clicked.connect(self.docViewer.navBackward)
|
||||
|
||||
self.forwardButton = QToolButton(self)
|
||||
@@ -751,7 +755,7 @@ class GuiDocViewHeader(QWidget):
|
||||
self.forwardButton.setStyleSheet(buttonStyle)
|
||||
self.forwardButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.forwardButton.setVisible(False)
|
||||
self.forwardButton.setToolTip("Go forward")
|
||||
self.forwardButton.setToolTip(self.tr("Go forward"))
|
||||
self.forwardButton.clicked.connect(self.docViewer.navForward)
|
||||
|
||||
self.refreshButton = QToolButton(self)
|
||||
@@ -762,7 +766,7 @@ class GuiDocViewHeader(QWidget):
|
||||
self.refreshButton.setStyleSheet(buttonStyle)
|
||||
self.refreshButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.refreshButton.setVisible(False)
|
||||
self.refreshButton.setToolTip("Reload the document")
|
||||
self.refreshButton.setToolTip(self.tr("Reload the document"))
|
||||
self.refreshButton.clicked.connect(self._refreshDocument)
|
||||
|
||||
self.closeButton = QToolButton(self)
|
||||
@@ -773,7 +777,7 @@ class GuiDocViewHeader(QWidget):
|
||||
self.closeButton.setStyleSheet(buttonStyle)
|
||||
self.closeButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.closeButton.setVisible(False)
|
||||
self.closeButton.setToolTip("Close the document")
|
||||
self.closeButton.setToolTip(self.tr("Close the document"))
|
||||
self.closeButton.clicked.connect(self._closeDocument)
|
||||
|
||||
# Assemble Layout
|
||||
@@ -944,7 +948,7 @@ class GuiDocViewFooter(QWidget):
|
||||
self.showHide.setIconSize(QSize(fPx, fPx))
|
||||
self.showHide.setFixedSize(QSize(fPx, fPx))
|
||||
self.showHide.clicked.connect(self._doShowHide)
|
||||
self.showHide.setToolTip("Show/hide the references panel")
|
||||
self.showHide.setToolTip(self.tr("Show/hide the references panel"))
|
||||
|
||||
# Sticky Button
|
||||
self.stickyRefs = QToolButton(self)
|
||||
@@ -955,9 +959,9 @@ class GuiDocViewFooter(QWidget):
|
||||
self.stickyRefs.setIconSize(QSize(fPx, fPx))
|
||||
self.stickyRefs.setFixedSize(QSize(fPx, fPx))
|
||||
self.stickyRefs.toggled.connect(self._doToggleSticky)
|
||||
self.stickyRefs.setToolTip(
|
||||
self.stickyRefs.setToolTip(self.tr(
|
||||
"Activate to freeze the content of the references panel when changing document"
|
||||
)
|
||||
))
|
||||
|
||||
# Show Comments
|
||||
self.showComments = QToolButton(self)
|
||||
@@ -969,7 +973,7 @@ class GuiDocViewFooter(QWidget):
|
||||
self.showComments.setIconSize(QSize(fPx, fPx))
|
||||
self.showComments.setFixedSize(QSize(fPx, fPx))
|
||||
self.showComments.toggled.connect(self._doToggleComments)
|
||||
self.showComments.setToolTip("Show comments")
|
||||
self.showComments.setToolTip(self.tr("Show comments"))
|
||||
|
||||
# Show Synopsis
|
||||
self.showSynopsis = QToolButton(self)
|
||||
@@ -981,10 +985,10 @@ class GuiDocViewFooter(QWidget):
|
||||
self.showSynopsis.setIconSize(QSize(fPx, fPx))
|
||||
self.showSynopsis.setFixedSize(QSize(fPx, fPx))
|
||||
self.showSynopsis.toggled.connect(self._doToggleSynopsis)
|
||||
self.showSynopsis.setToolTip("Show synopsis comments")
|
||||
self.showSynopsis.setToolTip(self.tr("Show synopsis comments"))
|
||||
|
||||
# Labels
|
||||
self.lblRefs = QLabel("References")
|
||||
self.lblRefs = QLabel(self.tr("References"))
|
||||
self.lblRefs.setBuddy(self.showHide)
|
||||
self.lblRefs.setIndent(0)
|
||||
self.lblRefs.setMargin(0)
|
||||
@@ -993,7 +997,7 @@ class GuiDocViewFooter(QWidget):
|
||||
self.lblRefs.setFixedHeight(fPx)
|
||||
self.lblRefs.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
||||
|
||||
self.lblSticky = QLabel("Sticky")
|
||||
self.lblSticky = QLabel(self.tr("Sticky"))
|
||||
self.lblSticky.setBuddy(self.stickyRefs)
|
||||
self.lblSticky.setIndent(0)
|
||||
self.lblSticky.setMargin(0)
|
||||
@@ -1002,7 +1006,7 @@ class GuiDocViewFooter(QWidget):
|
||||
self.lblSticky.setFixedHeight(fPx)
|
||||
self.lblSticky.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
||||
|
||||
self.lblComments = QLabel("Comments")
|
||||
self.lblComments = QLabel(self.tr("Comments"))
|
||||
self.lblComments.setBuddy(self.showComments)
|
||||
self.lblComments.setIndent(0)
|
||||
self.lblComments.setMargin(0)
|
||||
@@ -1011,7 +1015,7 @@ class GuiDocViewFooter(QWidget):
|
||||
self.lblComments.setFixedHeight(fPx)
|
||||
self.lblComments.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
||||
|
||||
self.lblSynopsis = QLabel("Synopsis")
|
||||
self.lblSynopsis = QLabel(self.tr("Synopsis"))
|
||||
self.lblSynopsis.setBuddy(self.showSynopsis)
|
||||
self.lblSynopsis.setIndent(0)
|
||||
self.lblSynopsis.setMargin(0)
|
||||
|
||||
@@ -31,9 +31,8 @@ from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QFont, QPixmap
|
||||
from PyQt5.QtWidgets import QWidget, QGridLayout, QLabel
|
||||
|
||||
from nw.constants import (
|
||||
nwLabels, nwItemClass, nwItemType, nwItemLayout
|
||||
)
|
||||
from nw.enum import nwItemClass, nwItemType, nwItemLayout
|
||||
from nw.constants import trConst, nwLabels
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -67,7 +66,7 @@ class GuiItemDetails(QWidget):
|
||||
self.fntValue.setPointSizeF(0.9*fPt)
|
||||
|
||||
# Label
|
||||
self.labelName = QLabel("Label")
|
||||
self.labelName = QLabel(self.tr("Label"))
|
||||
self.labelName.setFont(self.fntLabel)
|
||||
self.labelName.setAlignment(Qt.AlignLeft | Qt.AlignBaseline)
|
||||
|
||||
@@ -80,7 +79,7 @@ class GuiItemDetails(QWidget):
|
||||
self.labelData.setWordWrap(True)
|
||||
|
||||
# Status
|
||||
self.statusName = QLabel("Status")
|
||||
self.statusName = QLabel(self.tr("Status"))
|
||||
self.statusName.setFont(self.fntLabel)
|
||||
self.statusName.setAlignment(Qt.AlignLeft)
|
||||
|
||||
@@ -92,7 +91,7 @@ class GuiItemDetails(QWidget):
|
||||
self.statusData.setAlignment(Qt.AlignLeft)
|
||||
|
||||
# Class
|
||||
self.className = QLabel("Class")
|
||||
self.className = QLabel(self.tr("Class"))
|
||||
self.className.setFont(self.fntLabel)
|
||||
self.className.setAlignment(Qt.AlignLeft)
|
||||
|
||||
@@ -105,7 +104,7 @@ class GuiItemDetails(QWidget):
|
||||
self.classData.setAlignment(Qt.AlignLeft)
|
||||
|
||||
# Layout
|
||||
self.layoutName = QLabel("Layout")
|
||||
self.layoutName = QLabel(self.tr("Layout"))
|
||||
self.layoutName.setFont(self.fntLabel)
|
||||
self.layoutName.setAlignment(Qt.AlignLeft)
|
||||
|
||||
@@ -118,7 +117,7 @@ class GuiItemDetails(QWidget):
|
||||
self.layoutData.setAlignment(Qt.AlignLeft)
|
||||
|
||||
# Character Count
|
||||
self.cCountName = QLabel(" Characters")
|
||||
self.cCountName = QLabel(" "+self.tr("Characters"))
|
||||
self.cCountName.setFont(self.fntLabel)
|
||||
self.cCountName.setAlignment(Qt.AlignRight)
|
||||
|
||||
@@ -127,7 +126,7 @@ class GuiItemDetails(QWidget):
|
||||
self.cCountData.setAlignment(Qt.AlignRight)
|
||||
|
||||
# Word Count
|
||||
self.wCountName = QLabel(" Words")
|
||||
self.wCountName = QLabel(" "+self.tr("Words"))
|
||||
self.wCountName.setFont(self.fntLabel)
|
||||
self.wCountName.setAlignment(Qt.AlignRight)
|
||||
|
||||
@@ -136,7 +135,7 @@ class GuiItemDetails(QWidget):
|
||||
self.wCountData.setAlignment(Qt.AlignRight)
|
||||
|
||||
# Paragraph Count
|
||||
self.pCountName = QLabel(" Paragraphs")
|
||||
self.pCountName = QLabel(" "+self.tr("Paragraphs"))
|
||||
self.pCountName.setFont(self.fntLabel)
|
||||
self.pCountName.setAlignment(Qt.AlignRight)
|
||||
|
||||
@@ -259,17 +258,17 @@ class GuiItemDetails(QWidget):
|
||||
|
||||
iPx = int(round(0.8*self.theTheme.baseIconSize))
|
||||
self.statusFlag.setPixmap(flagIcon.pixmap(iPx, iPx))
|
||||
self.classFlag.setText(nwLabels.CLASS_FLAG[nwItem.itemClass])
|
||||
self.classFlag.setText(nwLabels.CLASS_FLAG[nwItem.itemClass]) # NO-I18N
|
||||
|
||||
if nwItem.itemLayout == nwItemLayout.NO_LAYOUT:
|
||||
self.layoutFlag.setText("-")
|
||||
else:
|
||||
self.layoutFlag.setText(nwLabels.LAYOUT_FLAG[nwItem.itemLayout])
|
||||
self.layoutFlag.setText(nwLabels.LAYOUT_FLAG[nwItem.itemLayout]) # NO-I18N
|
||||
|
||||
self.labelData.setText(theLabel)
|
||||
self.statusData.setText(nwItem.itemStatus)
|
||||
self.classData.setText(nwLabels.CLASS_NAME[nwItem.itemClass])
|
||||
self.layoutData.setText(nwLabels.LAYOUT_NAME[nwItem.itemLayout])
|
||||
self.classData.setText(trConst(nwLabels.CLASS_NAME[nwItem.itemClass]))
|
||||
self.layoutData.setText(trConst(nwLabels.LAYOUT_NAME[nwItem.itemLayout]))
|
||||
|
||||
if nwItem.itemType == nwItemType.FILE:
|
||||
self.cCountData.setText(f"{nwItem.charCount:n}")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,8 +32,8 @@ from time import time
|
||||
from PyQt5.QtCore import Qt, QSize
|
||||
from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QAbstractItemView
|
||||
|
||||
from nw.constants import nwKeyWords
|
||||
from nw.common import checkInt
|
||||
from nw.constants import nwKeyWords
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -63,7 +63,11 @@ class GuiNovelTree(QTreeWidget):
|
||||
self.setIconSize(QSize(iPx, iPx))
|
||||
self.setIndentation(iPx)
|
||||
self.setColumnCount(3)
|
||||
self.setHeaderLabels(["Title", "Words", "POV"])
|
||||
self.setHeaderLabels([
|
||||
self.tr("Title"),
|
||||
self.tr("Words"),
|
||||
self.tr("POV")
|
||||
])
|
||||
self.itemDoubleClicked.connect(self._treeDoubleClick)
|
||||
self.itemSelectionChanged.connect(self._itemSelected)
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
@@ -73,9 +77,9 @@ class GuiNovelTree(QTreeWidget):
|
||||
|
||||
treeHeadItem = self.headerItem()
|
||||
treeHeadItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
|
||||
treeHeadItem.setToolTip(self.C_TITLE, "Section title")
|
||||
treeHeadItem.setToolTip(self.C_WORDS, "Word count")
|
||||
treeHeadItem.setToolTip(self.C_POV, "Point-of-view character")
|
||||
treeHeadItem.setToolTip(self.C_TITLE, self.tr("Section title"))
|
||||
treeHeadItem.setToolTip(self.C_WORDS, self.tr("Word count"))
|
||||
treeHeadItem.setToolTip(self.C_POV, self.tr("Point-of-view character"))
|
||||
|
||||
treeHeader = self.header()
|
||||
treeHeader.setStretchLastSection(True)
|
||||
|
||||
@@ -34,7 +34,8 @@ from PyQt5.QtWidgets import (
|
||||
QTreeWidget, QTreeWidgetItem, QMenu, QAction, QAbstractItemView
|
||||
)
|
||||
|
||||
from nw.constants import nwKeyWords, nwLabels, nwOutline
|
||||
from nw.enum import nwOutline
|
||||
from nw.constants import trConst, nwKeyWords, nwLabels
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -149,7 +150,7 @@ class GuiOutline(QTreeWidget):
|
||||
"""
|
||||
self.clear()
|
||||
self.setColumnCount(1)
|
||||
self.setHeaderLabel(nwLabels.OUTLINE_COLS[nwOutline.TITLE])
|
||||
self.setHeaderLabel(trConst(nwLabels.OUTLINE_COLS[nwOutline.TITLE]))
|
||||
|
||||
self.treeOrder = []
|
||||
self.colWidth = {}
|
||||
@@ -355,7 +356,7 @@ class GuiOutline(QTreeWidget):
|
||||
if self.firstView:
|
||||
theLabels = []
|
||||
for i, hItem in enumerate(self.treeOrder):
|
||||
theLabels.append(nwLabels.OUTLINE_COLS[hItem])
|
||||
theLabels.append(trConst(nwLabels.OUTLINE_COLS[hItem]))
|
||||
self.colIndex[hItem] = i
|
||||
|
||||
self.setHeaderLabels(theLabels)
|
||||
@@ -474,7 +475,7 @@ class GuiOutlineHeaderMenu(QMenu):
|
||||
self.theParent = theParent
|
||||
self.acceptToggle = True
|
||||
|
||||
mnuHead = QAction("Select Columns", self)
|
||||
mnuHead = QAction(self.tr("Select Columns"), self)
|
||||
self.addAction(mnuHead)
|
||||
self.addSeparator()
|
||||
|
||||
@@ -482,7 +483,7 @@ class GuiOutlineHeaderMenu(QMenu):
|
||||
for hItem in nwOutline:
|
||||
if hItem == nwOutline.TITLE:
|
||||
continue
|
||||
self.actionMap[hItem] = QAction(nwLabels.OUTLINE_COLS[hItem], self)
|
||||
self.actionMap[hItem] = QAction(trConst(nwLabels.OUTLINE_COLS[hItem]), self)
|
||||
self.actionMap[hItem].setCheckable(True)
|
||||
self.actionMap[hItem].toggled.connect(
|
||||
lambda isChecked, tItem=hItem : self._columnToggled(isChecked, tItem)
|
||||
|
||||
@@ -27,23 +27,23 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import nw
|
||||
import logging
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtCore import Qt, QT_TRANSLATE_NOOP
|
||||
from PyQt5.QtWidgets import (
|
||||
QScrollArea, QWidget, QGridLayout, QHBoxLayout, QGroupBox, QLabel
|
||||
)
|
||||
|
||||
from nw.constants import nwLabels, nwKeyWords
|
||||
from nw.common import checkInt
|
||||
from nw.constants import trConst, nwKeyWords, nwLabels
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class GuiOutlineDetails(QScrollArea):
|
||||
|
||||
LVL_MAP = {
|
||||
"H1" : "Title",
|
||||
"H2" : "Chapter",
|
||||
"H3" : "Scene",
|
||||
"H4" : "Section"
|
||||
"H1" : QT_TRANSLATE_NOOP("GuiOutlineDetails", "Title"),
|
||||
"H2" : QT_TRANSLATE_NOOP("GuiOutlineDetails", "Chapter"),
|
||||
"H3" : QT_TRANSLATE_NOOP("GuiOutlineDetails", "Scene"),
|
||||
"H4" : QT_TRANSLATE_NOOP("GuiOutlineDetails", "Section"),
|
||||
}
|
||||
|
||||
def __init__(self, theParent):
|
||||
@@ -66,9 +66,9 @@ class GuiOutlineDetails(QScrollArea):
|
||||
vSpace = int(self.mainConf.pxInt(4))
|
||||
|
||||
# Details Area
|
||||
self.titleLabel = QLabel("<b>Title</b>")
|
||||
self.fileLabel = QLabel("<b>Document</b>")
|
||||
self.itemLabel = QLabel("<b>Status</b>")
|
||||
self.titleLabel = QLabel("<b>%s</b>" % self.tr("Title"))
|
||||
self.fileLabel = QLabel("<b>%s</b>" % self.tr("Document"))
|
||||
self.itemLabel = QLabel("<b>%s</b>" % self.tr("Status"))
|
||||
self.titleValue = QLabel("")
|
||||
self.fileValue = QLabel("")
|
||||
self.itemValue = QLabel("")
|
||||
@@ -81,9 +81,9 @@ class GuiOutlineDetails(QScrollArea):
|
||||
self.itemValue.setMaximumWidth(maxTitle)
|
||||
|
||||
# Stats Area
|
||||
self.cCLabel = QLabel("<b>Characters</b>")
|
||||
self.wCLabel = QLabel("<b>Words</b>")
|
||||
self.pCLabel = QLabel("<b>Paragraphs</b>")
|
||||
self.cCLabel = QLabel("<b>%s</b>" % self.tr("Characters"))
|
||||
self.wCLabel = QLabel("<b>%s</b>" % self.tr("Words"))
|
||||
self.pCLabel = QLabel("<b>%s</b>" % self.tr("Paragraphs"))
|
||||
self.cCValue = QLabel("")
|
||||
self.wCValue = QLabel("")
|
||||
self.pCValue = QLabel("")
|
||||
@@ -96,7 +96,7 @@ class GuiOutlineDetails(QScrollArea):
|
||||
self.pCValue.setAlignment(Qt.AlignRight)
|
||||
|
||||
# Synopsis
|
||||
self.synopLabel = QLabel("<b>Synopsis</b>")
|
||||
self.synopLabel = QLabel("<b>%s</b>" % self.tr("Synopsis"))
|
||||
self.synopValue = QLabel("")
|
||||
self.synopLWrap = QHBoxLayout()
|
||||
self.synopValue.setWordWrap(True)
|
||||
@@ -104,15 +104,15 @@ class GuiOutlineDetails(QScrollArea):
|
||||
self.synopLWrap.addWidget(self.synopValue, 1)
|
||||
|
||||
# Tags
|
||||
self.povKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.POV_KEY])
|
||||
self.focKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY])
|
||||
self.chrKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.CHAR_KEY])
|
||||
self.pltKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY])
|
||||
self.timKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.TIME_KEY])
|
||||
self.wldKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.WORLD_KEY])
|
||||
self.objKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.OBJECT_KEY])
|
||||
self.entKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.ENTITY_KEY])
|
||||
self.cstKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.CUSTOM_KEY])
|
||||
self.povKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY]))
|
||||
self.focKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY]))
|
||||
self.chrKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.CHAR_KEY]))
|
||||
self.pltKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY]))
|
||||
self.timKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.TIME_KEY]))
|
||||
self.wldKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.WORLD_KEY]))
|
||||
self.objKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.OBJECT_KEY]))
|
||||
self.entKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.ENTITY_KEY]))
|
||||
self.cstKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.CUSTOM_KEY]))
|
||||
|
||||
self.povKeyLWrap = QHBoxLayout()
|
||||
self.focKeyLWrap = QHBoxLayout()
|
||||
@@ -165,7 +165,7 @@ class GuiOutlineDetails(QScrollArea):
|
||||
self.cstKeyLWrap.addWidget(self.cstKeyValue, 1)
|
||||
|
||||
# Selected Item Details
|
||||
self.mainGroup = QGroupBox("Title Details", self)
|
||||
self.mainGroup = QGroupBox(self.tr("Title Details"), self)
|
||||
self.mainForm = QGridLayout()
|
||||
self.mainGroup.setLayout(self.mainForm)
|
||||
|
||||
@@ -190,7 +190,7 @@ class GuiOutlineDetails(QScrollArea):
|
||||
self.mainForm.setVerticalSpacing(vSpace)
|
||||
|
||||
# Selected Item Tags
|
||||
self.tagsGroup = QGroupBox("Reference Tags", self)
|
||||
self.tagsGroup = QGroupBox(self.tr("Reference Tags"), self)
|
||||
self.tagsForm = QGridLayout()
|
||||
self.tagsGroup.setLayout(self.tagsForm)
|
||||
|
||||
@@ -256,7 +256,7 @@ class GuiOutlineDetails(QScrollArea):
|
||||
def clearDetails(self):
|
||||
"""Clear all the data labels.
|
||||
"""
|
||||
self.titleLabel.setText("<b>Title</b>")
|
||||
self.titleLabel.setText("<b>%s</b>" % self.tr("Title"))
|
||||
self.titleValue.setText("")
|
||||
self.fileValue.setText("")
|
||||
self.itemValue.setText("")
|
||||
@@ -286,9 +286,9 @@ class GuiOutlineDetails(QScrollArea):
|
||||
return False
|
||||
|
||||
if novIdx["level"] in self.LVL_MAP:
|
||||
self.titleLabel.setText("<b>%s</b>" % self.LVL_MAP[novIdx["level"]])
|
||||
self.titleLabel.setText("<b>%s</b>" % self.tr(self.LVL_MAP[novIdx["level"]]))
|
||||
else:
|
||||
self.titleLabel.setText("<b>Title</b>")
|
||||
self.titleLabel.setText("<b>%s</b>" % self.tr("Title"))
|
||||
self.titleValue.setText(novIdx["title"])
|
||||
|
||||
self.fileValue.setText(nwItem.itemName)
|
||||
|
||||
@@ -35,9 +35,9 @@ from PyQt5.QtWidgets import (
|
||||
QLabel, QSpinBox, QGridLayout, QHBoxLayout, QLineEdit, QAbstractItemView
|
||||
)
|
||||
|
||||
from nw.gui.custom import PagedDialog, QSwitch
|
||||
from nw.common import numberToRoman
|
||||
from nw.constants import nwUnicode
|
||||
from nw.core import numberToRoman
|
||||
from nw.gui.custom import PagedDialog, QSwitch
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -54,7 +54,7 @@ class GuiProjectDetails(PagedDialog):
|
||||
self.theProject = theProject
|
||||
self.optState = theProject.optState
|
||||
|
||||
self.setWindowTitle("Project Details")
|
||||
self.setWindowTitle(self.tr("Project Details"))
|
||||
|
||||
wW = self.mainConf.pxInt(600)
|
||||
wH = self.mainConf.pxInt(400)
|
||||
@@ -69,10 +69,11 @@ class GuiProjectDetails(PagedDialog):
|
||||
self.tabMain = GuiProjectDetailsMain(self.theParent, self.theProject)
|
||||
self.tabContents = GuiProjectDetailsContents(self.theParent, self.theProject)
|
||||
|
||||
self.addTab(self.tabMain, "Overview")
|
||||
self.addTab(self.tabContents, "Contents")
|
||||
self.addTab(self.tabMain, self.tr("Overview"))
|
||||
self.addTab(self.tabContents, self.tr("Contents"))
|
||||
|
||||
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close)
|
||||
self.buttonBox.button(QDialogButtonBox.Close).setText(self.tr("Close"))
|
||||
self.buttonBox.rejected.connect(self._doClose)
|
||||
self.addControls(self.buttonBox)
|
||||
|
||||
@@ -154,7 +155,9 @@ class GuiProjectDetailsMain(QWidget):
|
||||
self.bookTitle.setAlignment(Qt.AlignHCenter)
|
||||
self.bookTitle.setWordWrap(True)
|
||||
|
||||
self.projName = QLabel("Working Title: %s" % self.theProject.projName)
|
||||
self.projName = QLabel(
|
||||
self.tr("Working Title: {0}").format(self.theProject.projName)
|
||||
)
|
||||
workFont = self.projName.font()
|
||||
workFont.setPointSizeF(0.8*fPt)
|
||||
workFont.setItalic(True)
|
||||
@@ -162,7 +165,7 @@ class GuiProjectDetailsMain(QWidget):
|
||||
self.projName.setAlignment(Qt.AlignHCenter)
|
||||
self.projName.setWordWrap(True)
|
||||
|
||||
self.bookAuthors = QLabel("By %s" % self.theProject.getAuthors())
|
||||
self.bookAuthors = QLabel(self.tr("By {0}").format(self.theProject.getAuthors()))
|
||||
authFont = self.bookAuthors.font()
|
||||
authFont.setPointSizeF(1.2*fPt)
|
||||
self.bookAuthors.setFont(authFont)
|
||||
@@ -175,20 +178,20 @@ class GuiProjectDetailsMain(QWidget):
|
||||
hCounts = self.theIndex.getNovelTitleCounts()
|
||||
nwCount = self.theIndex.getNovelWordCount()
|
||||
|
||||
self.wordCountLbl = QLabel("<b>Words:</b>")
|
||||
self.wordCountLbl = QLabel("<b>%s:</b>" % self.tr("Words"))
|
||||
self.wordCountVal = QLabel(f"{nwCount:n}")
|
||||
|
||||
self.chapCountLbl = QLabel("<b>Chapters:</b>")
|
||||
self.chapCountLbl = QLabel("<b>%s:</b>" % self.tr("Chapters"))
|
||||
self.chapCountVal = QLabel(f"{hCounts[2]:n}")
|
||||
|
||||
self.sceneCountLbl = QLabel("<b>Scenes:</b>")
|
||||
self.sceneCountLbl = QLabel("<b>%s:</b>" % self.tr("Scenes"))
|
||||
self.sceneCountVal = QLabel(f"{hCounts[3]:n}")
|
||||
|
||||
self.revCountLbl = QLabel("<b>Revisions:</b>")
|
||||
self.revCountLbl = QLabel("<b>%s:</b>" % self.tr("Revisions"))
|
||||
self.revCountVal = QLabel(f"{self.theProject.saveCount:n}")
|
||||
|
||||
edTime = self.theProject.getCurrentEditTime()
|
||||
self.editTimeLbl = QLabel("<b>Editing Time:</b>")
|
||||
self.editTimeLbl = QLabel("<b>%s:</b>" % self.tr("Editing Time"))
|
||||
self.editTimeVal = QLabel(f"{edTime//3600:02d}:{edTime%3600//60:02d}")
|
||||
|
||||
self.statsGrid = QGridLayout()
|
||||
@@ -208,7 +211,7 @@ class GuiProjectDetailsMain(QWidget):
|
||||
# Meta
|
||||
# ====
|
||||
|
||||
self.projPathLbl = QLabel("<b>Path:</b>")
|
||||
self.projPathLbl = QLabel("<b>%s:</b>" % self.tr("Path"))
|
||||
self.projPathVal = QLineEdit()
|
||||
self.projPathVal.setText(self.theProject.projPath)
|
||||
self.projPathVal.setReadOnly(True)
|
||||
@@ -271,7 +274,14 @@ class GuiProjectDetailsContents(QWidget):
|
||||
self.tocTree.setIndentation(0)
|
||||
self.tocTree.setColumnCount(6)
|
||||
self.tocTree.setSelectionMode(QAbstractItemView.NoSelection)
|
||||
self.tocTree.setHeaderLabels(["Title", "Words", "Pages", "Page", "Progress", ""])
|
||||
self.tocTree.setHeaderLabels([
|
||||
self.tr("Title"),
|
||||
self.tr("Words"),
|
||||
self.tr("Pages"),
|
||||
self.tr("Page"),
|
||||
self.tr("Progress"),
|
||||
""
|
||||
])
|
||||
|
||||
treeHeadItem = self.tocTree.headerItem()
|
||||
treeHeadItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
|
||||
@@ -304,16 +314,16 @@ class GuiProjectDetailsContents(QWidget):
|
||||
clearDouble = self.optState.getInt("GuiProjectDetails", "clearDouble", True)
|
||||
|
||||
wordsHelp = (
|
||||
"Typical word count for a 5 by 8 inch book page with 11 pt font is 350."
|
||||
self.tr("Typical word count for a 5 by 8 inch book page with 11 pt font is 350.")
|
||||
)
|
||||
offsetHelp = (
|
||||
"Start counting page numbers from this page."
|
||||
self.tr("Start counting page numbers from this page.")
|
||||
)
|
||||
dblHelp = (
|
||||
"Assume a new chapter or partition always start on an odd numbered page."
|
||||
self.tr("Assume a new chapter or partition always start on an odd numbered page.")
|
||||
)
|
||||
|
||||
self.wpLabel = QLabel("Words per page")
|
||||
self.wpLabel = QLabel(self.tr("Words per page"))
|
||||
self.wpLabel.setToolTip(wordsHelp)
|
||||
|
||||
self.wpValue = QSpinBox()
|
||||
@@ -324,7 +334,7 @@ class GuiProjectDetailsContents(QWidget):
|
||||
self.wpValue.setToolTip(wordsHelp)
|
||||
self.wpValue.valueChanged.connect(self._populateTree)
|
||||
|
||||
self.poLabel = QLabel("Count pages from")
|
||||
self.poLabel = QLabel(self.tr("Count pages from"))
|
||||
self.poLabel.setToolTip(offsetHelp)
|
||||
|
||||
self.poValue = QSpinBox()
|
||||
@@ -335,7 +345,7 @@ class GuiProjectDetailsContents(QWidget):
|
||||
self.poValue.setToolTip(offsetHelp)
|
||||
self.poValue.valueChanged.connect(self._populateTree)
|
||||
|
||||
self.dblLabel = QLabel("Clear double pages")
|
||||
self.dblLabel = QLabel(self.tr("Clear double pages"))
|
||||
self.dblLabel.setToolTip(dblHelp)
|
||||
|
||||
self.dblValue = QSwitch(self, 2*iPx, iPx)
|
||||
@@ -358,7 +368,7 @@ class GuiProjectDetailsContents(QWidget):
|
||||
# ========
|
||||
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.outerBox.addWidget(QLabel("<b>Table of Contents</b>"))
|
||||
self.outerBox.addWidget(QLabel("<b>%s</b>" % self.tr("Table of Contents")))
|
||||
self.outerBox.addWidget(self.tocTree)
|
||||
self.outerBox.addLayout(self.optionsBox)
|
||||
|
||||
@@ -390,7 +400,7 @@ class GuiProjectDetailsContents(QWidget):
|
||||
"""
|
||||
self._theToC = []
|
||||
self._theToC = self.theIndex.getTableOfContents(2)
|
||||
self._theToC.append(("", 0, "END", 0))
|
||||
self._theToC.append(("", 0, self.tr("END"), 0))
|
||||
return
|
||||
|
||||
##
|
||||
|
||||
@@ -37,9 +37,8 @@ from PyQt5.QtWidgets import (
|
||||
)
|
||||
|
||||
from nw.core import NWDoc
|
||||
from nw.constants import (
|
||||
nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert, nwConst, nwLists
|
||||
)
|
||||
from nw.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert
|
||||
from nw.constants import nwConst, trConst, nwLists, nwLabels
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -85,14 +84,19 @@ class GuiProjectTree(QTreeWidget):
|
||||
self.setExpandsOnDoubleClick(True)
|
||||
self.setIndentation(iPx)
|
||||
self.setColumnCount(4)
|
||||
self.setHeaderLabels(["Label", "Words", "Inc", "Flags"])
|
||||
self.setHeaderLabels([
|
||||
self.tr("Label"),
|
||||
self.tr("Words"),
|
||||
self.tr("Inc"),
|
||||
self.tr("Flags")
|
||||
])
|
||||
|
||||
treeHeadItem = self.headerItem()
|
||||
treeHeadItem.setTextAlignment(self.C_COUNT, Qt.AlignRight)
|
||||
treeHeadItem.setToolTip(self.C_NAME, "Item label")
|
||||
treeHeadItem.setToolTip(self.C_COUNT, "Word count")
|
||||
treeHeadItem.setToolTip(self.C_EXPORT, "Include in build")
|
||||
treeHeadItem.setToolTip(self.C_FLAGS, "Status, class, and layout flags")
|
||||
treeHeadItem.setToolTip(self.C_NAME, self.tr("Item label"))
|
||||
treeHeadItem.setToolTip(self.C_COUNT, self.tr("Word count"))
|
||||
treeHeadItem.setToolTip(self.C_EXPORT, self.tr("Include in build"))
|
||||
treeHeadItem.setToolTip(self.C_FLAGS, self.tr("Status, class, and layout flags"))
|
||||
|
||||
# Let the last column stretch, and set the minimum size to the
|
||||
# size of the icon as the default Qt font metrics approach fails
|
||||
@@ -193,12 +197,12 @@ class GuiProjectTree(QTreeWidget):
|
||||
if itemClass is None:
|
||||
if itemType == nwItemType.FILE:
|
||||
self.makeAlert(
|
||||
"Please select a valid location in the tree to add the document.",
|
||||
self.tr("Please select a valid location in the tree to add the document."),
|
||||
nwAlert.ERROR
|
||||
)
|
||||
else:
|
||||
self.makeAlert(
|
||||
"Please select a valid location in the tree to add the folder.",
|
||||
self.tr("Please select a valid location in the tree to add the folder."),
|
||||
nwAlert.ERROR
|
||||
)
|
||||
return False
|
||||
@@ -209,7 +213,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
)
|
||||
|
||||
if itemType == nwItemType.ROOT:
|
||||
tHandle = self.theProject.newRoot(nwLabels.CLASS_NAME[itemClass], itemClass)
|
||||
tHandle = self.theProject.newRoot(trConst(nwLabels.CLASS_NAME[itemClass]), itemClass)
|
||||
if tHandle is None:
|
||||
logger.error("No root item added")
|
||||
return False
|
||||
@@ -223,7 +227,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
# If still nothing, give up
|
||||
if pHandle is None:
|
||||
self.makeAlert(
|
||||
"Did not find anywhere to add the file or folder!", nwAlert.ERROR
|
||||
self.tr("Did not find anywhere to add the file or folder!"), nwAlert.ERROR
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -237,15 +241,13 @@ class GuiProjectTree(QTreeWidget):
|
||||
# If we again have no home, give up
|
||||
if pHandle is None:
|
||||
self.makeAlert(
|
||||
"Did not find anywhere to add the file or folder!", nwAlert.ERROR
|
||||
self.tr("Did not find anywhere to add the file or folder!"), nwAlert.ERROR
|
||||
)
|
||||
return False
|
||||
|
||||
if self.theProject.projTree.isTrashRoot(pHandle):
|
||||
self.makeAlert(
|
||||
"Cannot add new files or folders to the %s folder." % (
|
||||
nwLabels.CLASS_NAME[nwItemClass.TRASH]
|
||||
), nwAlert.ERROR
|
||||
self.tr("Cannot add new files or folders to the Trash folder."), nwAlert.ERROR
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -253,18 +255,18 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
# If we're still here, add the file or folder
|
||||
if itemType == nwItemType.FILE:
|
||||
tHandle = self.theProject.newFile("New File", itemClass, pHandle)
|
||||
tHandle = self.theProject.newFile(self.tr("New File"), itemClass, pHandle)
|
||||
|
||||
elif itemType == nwItemType.FOLDER:
|
||||
if len(parTree) >= nwConst.MAX_DEPTH - 1:
|
||||
# Folders cannot be deeper than MAX_DEPTH - 1, leaving room
|
||||
# for one more level of files.
|
||||
self.makeAlert((
|
||||
"Cannot add new folder to this item. "
|
||||
"Maximum folder depth has been reached."
|
||||
self.tr("Cannot add new folder to this item."),
|
||||
self.tr("Maximum folder depth has been reached.")
|
||||
), nwAlert.ERROR)
|
||||
return False
|
||||
tHandle = self.theProject.newFolder("New Folder", itemClass, pHandle)
|
||||
tHandle = self.theProject.newFolder(self.tr("New Folder"), itemClass, pHandle)
|
||||
|
||||
else:
|
||||
logger.error("Failed to add new item")
|
||||
@@ -428,7 +430,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
logger.debug("Emptying Trash folder")
|
||||
if trashHandle is None:
|
||||
self.makeAlert(
|
||||
"There is currently no Trash folder in this project.", nwAlert.INFO
|
||||
self.tr("There is currently no Trash folder in this project."), nwAlert.INFO
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -438,11 +440,12 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
nTrash = len(theTrash)
|
||||
if nTrash == 0:
|
||||
self.makeAlert("The Trash folder is already empty.", nwAlert.INFO)
|
||||
self.makeAlert(self.tr("The Trash folder is already empty."), nwAlert.INFO)
|
||||
return False
|
||||
|
||||
msgYes = self.askQuestion(
|
||||
"Empty Trash", "Permanently delete %d file(s) from Trash?" % nTrash
|
||||
self.tr("Empty Trash"),
|
||||
self.tr("Permanently delete {0} file(s) from Trash?").format(nTrash)
|
||||
)
|
||||
if not msgYes:
|
||||
return False
|
||||
@@ -503,7 +506,8 @@ class GuiProjectTree(QTreeWidget):
|
||||
doPermanent = False
|
||||
if not alreadyAsked:
|
||||
msgYes = self.askQuestion(
|
||||
"Delete File", "Permanently delete file '%s'?" % nwItemS.itemName
|
||||
self.tr("Delete File"),
|
||||
self.tr("Permanently delete file '{0}'?").format(nwItemS.itemName)
|
||||
)
|
||||
if msgYes:
|
||||
doPermanent = True
|
||||
@@ -530,7 +534,8 @@ class GuiProjectTree(QTreeWidget):
|
||||
# The file is not already in the trash folder, so we
|
||||
# move it there.
|
||||
msgYes = self.askQuestion(
|
||||
"Delete File", "Move file '%s' to Trash?" % nwItemS.itemName
|
||||
self.tr("Delete File"),
|
||||
self.tr("Move file '{0}' to Trash?").format(nwItemS.itemName),
|
||||
)
|
||||
if msgYes:
|
||||
if pHandle is None:
|
||||
@@ -561,7 +566,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
self._deleteTreeItem(tHandle)
|
||||
self._setTreeChanged(True)
|
||||
else:
|
||||
self.makeAlert((
|
||||
self.makeAlert(self.tr(
|
||||
"Cannot delete folder. It is not empty. "
|
||||
"Recursive deletion is not supported. "
|
||||
"Please delete the content first."
|
||||
@@ -577,7 +582,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
self.theParent.mainMenu.setAvailableRoot()
|
||||
self._setTreeChanged(True)
|
||||
else:
|
||||
self.makeAlert((
|
||||
self.makeAlert(self.tr(
|
||||
"Cannot delete root folder. It is not empty. "
|
||||
"Recursive deletion is not supported. "
|
||||
"Please delete the content first."
|
||||
@@ -833,7 +838,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
snItem = self.theProject.projTree[sHandle]
|
||||
dnItem = self.theProject.projTree[dHandle]
|
||||
if dnItem is None:
|
||||
self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR)
|
||||
self.makeAlert(self.tr("The item cannot be moved to that location."), nwAlert.ERROR)
|
||||
return
|
||||
|
||||
pItem = sItem.parent()
|
||||
@@ -864,7 +869,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
else:
|
||||
theEvent.ignore()
|
||||
logger.debug("Drag'n'drop of item %s not accepted" % sHandle)
|
||||
self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR)
|
||||
self.makeAlert(self.tr("The item cannot be moved to that location."), nwAlert.ERROR)
|
||||
|
||||
return
|
||||
|
||||
@@ -962,7 +967,9 @@ class GuiProjectTree(QTreeWidget):
|
||||
self.addTopLevelItem(newItem)
|
||||
else:
|
||||
self.makeAlert(
|
||||
"There is nowhere to add item with name '%s'" % nwItem.itemName, nwAlert.ERROR
|
||||
self.tr(
|
||||
"There is nowhere to add item with name '{0}'."
|
||||
).format(nwItem.itemName), nwAlert.ERROR
|
||||
)
|
||||
del self._treeMap[tHandle]
|
||||
return None
|
||||
@@ -1081,43 +1088,43 @@ class GuiProjectTreeMenu(QMenu):
|
||||
self.theTree = theTree
|
||||
self.theItem = None
|
||||
|
||||
self.editItem = QAction("Edit Project Item", self)
|
||||
self.editItem = QAction(self.tr("Edit Project Item"), self)
|
||||
self.editItem.triggered.connect(self._doEditItem)
|
||||
self.addAction(self.editItem)
|
||||
|
||||
self.openItem = QAction("Open Document", self)
|
||||
self.openItem = QAction(self.tr("Open Document"), self)
|
||||
self.openItem.triggered.connect(self._doOpenItem)
|
||||
self.addAction(self.openItem)
|
||||
|
||||
self.viewItem = QAction("View Document", self)
|
||||
self.viewItem = QAction(self.tr("View Document"), self)
|
||||
self.viewItem.triggered.connect(self._doViewItem)
|
||||
self.addAction(self.viewItem)
|
||||
|
||||
self.toggleExp = QAction("Toggle Included Flag", self)
|
||||
self.toggleExp = QAction(self.tr("Toggle Included Flag"), self)
|
||||
self.toggleExp.triggered.connect(self._doToggleExported)
|
||||
self.addAction(self.toggleExp)
|
||||
|
||||
self.newFile = QAction("New File", self)
|
||||
self.newFile = QAction(self.tr("New File"), self)
|
||||
self.newFile.triggered.connect(self._doMakeFile)
|
||||
self.addAction(self.newFile)
|
||||
|
||||
self.newFolder = QAction("New Folder", self)
|
||||
self.newFolder = QAction(self.tr("New Folder"), self)
|
||||
self.newFolder.triggered.connect(self._doMakeFolder)
|
||||
self.addAction(self.newFolder)
|
||||
|
||||
self.deleteItem = QAction("Delete Item", self)
|
||||
self.deleteItem = QAction(self.tr("Delete Item"), self)
|
||||
self.deleteItem.triggered.connect(self._doDeleteItem)
|
||||
self.addAction(self.deleteItem)
|
||||
|
||||
self.emptyTrash = QAction("Empty Trash", self)
|
||||
self.emptyTrash = QAction(self.tr("Empty Trash"), self)
|
||||
self.emptyTrash.triggered.connect(self._doEmptyTrash)
|
||||
self.addAction(self.emptyTrash)
|
||||
|
||||
self.moveUp = QAction("Move Item Up", self)
|
||||
self.moveUp = QAction(self.tr("Move Item Up"), self)
|
||||
self.moveUp.triggered.connect(self._doMoveUp)
|
||||
self.addAction(self.moveUp)
|
||||
|
||||
self.moveDown = QAction("Move Item Down", self)
|
||||
self.moveDown = QAction(self.tr("Move Item Down"), self)
|
||||
self.moveDown.triggered.connect(self._doMoveDown)
|
||||
self.addAction(self.moveDown)
|
||||
|
||||
|
||||
@@ -29,10 +29,10 @@ import logging
|
||||
|
||||
from time import time
|
||||
|
||||
from PyQt5.QtCore import QLocale
|
||||
from PyQt5.QtGui import QColor, QPainter
|
||||
from PyQt5.QtWidgets import qApp, QStatusBar, QLabel, QAbstractButton
|
||||
|
||||
from nw.core import NWSpellCheck
|
||||
from nw.common import formatTime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -63,7 +63,7 @@ class GuiMainStatus(QStatusBar):
|
||||
|
||||
## The Spell Checker Language
|
||||
self.langIcon = QLabel("")
|
||||
self.langText = QLabel("None")
|
||||
self.langText = QLabel(self.tr("None"))
|
||||
self.langIcon.setPixmap(self.theTheme.getPixmap("status_lang", (iPx, iPx)))
|
||||
self.langIcon.setContentsMargins(0, 0, 0, 0)
|
||||
self.langText.setContentsMargins(0, 0, xM, 0)
|
||||
@@ -72,7 +72,7 @@ class GuiMainStatus(QStatusBar):
|
||||
|
||||
## The Editor Status
|
||||
self.docIcon = StatusLED(colNone, colTrue, colFalse, iPx, iPx, self)
|
||||
self.docText = QLabel("Editor")
|
||||
self.docText = QLabel(self.tr("Editor"))
|
||||
self.docIcon.setContentsMargins(0, 0, 0, 0)
|
||||
self.docText.setContentsMargins(0, 0, xM, 0)
|
||||
self.addPermanentWidget(self.docIcon)
|
||||
@@ -80,7 +80,7 @@ class GuiMainStatus(QStatusBar):
|
||||
|
||||
## The Project Status
|
||||
self.projIcon = StatusLED(colNone, colTrue, colFalse, iPx, iPx, self)
|
||||
self.projText = QLabel("Project")
|
||||
self.projText = QLabel(self.tr("Project"))
|
||||
self.projIcon.setContentsMargins(0, 0, 0, 0)
|
||||
self.projText.setContentsMargins(0, 0, xM, 0)
|
||||
self.addPermanentWidget(self.projIcon)
|
||||
@@ -103,7 +103,7 @@ class GuiMainStatus(QStatusBar):
|
||||
self.timeIcon = QLabel()
|
||||
self.timeText = QLabel("")
|
||||
self.timeIcon.setPixmap(self.timePixmap)
|
||||
self.timeText.setToolTip("Session Time")
|
||||
self.timeText.setToolTip(self.tr("Session Time"))
|
||||
self.timeText.setMinimumWidth(self.theTheme.getTextWidth("00:00:00:"))
|
||||
self.timeIcon.setContentsMargins(0, 0, 0, 0)
|
||||
self.timeText.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -151,13 +151,17 @@ class GuiMainStatus(QStatusBar):
|
||||
"""Set the language code for the spell checker.
|
||||
"""
|
||||
if theLanguage is None:
|
||||
self.langText.setText("None")
|
||||
self.langText.setText(self.tr("None"))
|
||||
self.langText.setToolTip("")
|
||||
else:
|
||||
self.langText.setText(NWSpellCheck.expandLanguage(theLanguage))
|
||||
self.langText.setToolTip(
|
||||
"Provider: %s" % (theProvider if theProvider else "unknown")
|
||||
)
|
||||
qLocal = QLocale(theLanguage)
|
||||
spLang = qLocal.nativeLanguageName().title()
|
||||
self.langText.setText(spLang)
|
||||
if theProvider:
|
||||
self.langText.setToolTip("%s (%s)" % (theLanguage, theProvider))
|
||||
else:
|
||||
self.langText.setToolTip(theLanguage)
|
||||
|
||||
return
|
||||
|
||||
def setProjectStatus(self, isChanged):
|
||||
@@ -175,8 +179,8 @@ class GuiMainStatus(QStatusBar):
|
||||
def setStats(self, pWC, sWC):
|
||||
"""Set the current project statistics.
|
||||
"""
|
||||
self.statsText.setText(f"Words: {pWC:n} ({sWC:+n})")
|
||||
self.statsText.setToolTip("Project word count (session change)")
|
||||
self.statsText.setText(self.tr("Words: {0} ({1})").format(f"{pWC:n}", f"{sWC:+n}"))
|
||||
self.statsText.setToolTip(self.tr("Project word count (session change)"))
|
||||
return
|
||||
|
||||
def setUserIdle(self, userIdle):
|
||||
|
||||
@@ -31,14 +31,15 @@ import configparser
|
||||
import os
|
||||
|
||||
from math import ceil
|
||||
from functools import partial
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtCore import QCoreApplication, Qt
|
||||
from PyQt5.QtWidgets import QStyle, qApp
|
||||
from PyQt5.QtGui import (
|
||||
QPalette, QColor, QIcon, QFont, QFontMetrics, QFontDatabase, QPixmap
|
||||
)
|
||||
|
||||
from nw.constants import nwAlert
|
||||
from nw.enum import nwAlert
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -157,6 +158,10 @@ class GuiTheme:
|
||||
logger.verbose("Text 'N' Height: %d" % self.textNHeight)
|
||||
logger.verbose("Text 'N' Width: %d" % self.textNWidth)
|
||||
|
||||
# Internal Mapping
|
||||
self.makeAlert = self.theParent.makeAlert
|
||||
self.tr = partial(QCoreApplication.translate, "GuiTheme")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
@@ -392,8 +397,8 @@ class GuiTheme:
|
||||
with open(themeConf, mode="r", encoding="utf8") as inFile:
|
||||
confParser.read_file(inFile)
|
||||
except Exception as e:
|
||||
self.theParent.makeAlert(
|
||||
["Could not load theme config file.", str(e)], nwAlert.ERROR
|
||||
self.makeAlert(
|
||||
[self.tr("Could not load theme config file."), str(e)], nwAlert.ERROR
|
||||
)
|
||||
continue
|
||||
themeName = ""
|
||||
@@ -425,8 +430,8 @@ class GuiTheme:
|
||||
with open(syntaxPath, mode="r", encoding="utf8") as inFile:
|
||||
confParser.read_file(inFile)
|
||||
except Exception as e:
|
||||
self.theParent.makeAlert(
|
||||
["Could not load syntax file.", str(e)], nwAlert.ERROR
|
||||
self.makeAlert(
|
||||
[self.tr("Could not load syntax file."), str(e)], nwAlert.ERROR
|
||||
)
|
||||
return []
|
||||
syntaxName = ""
|
||||
@@ -740,8 +745,8 @@ class GuiIcons:
|
||||
with open(themeConf, mode="r", encoding="utf8") as inFile:
|
||||
confParser.read_file(inFile)
|
||||
except Exception as e:
|
||||
self.theParent.makeAlert(
|
||||
["Could not load theme config file.", str(e)], nwAlert.ERROR
|
||||
self.makeAlert(
|
||||
[self.tr("Could not load theme config file."), str(e)], nwAlert.ERROR
|
||||
)
|
||||
continue
|
||||
themeName = ""
|
||||
|
||||
223
nw/guimain.py
223
nw/guimain.py
@@ -39,15 +39,19 @@ from PyQt5.QtWidgets import (
|
||||
)
|
||||
|
||||
from nw.gui import (
|
||||
GuiAbout, GuiBuildNovel, GuiDocEditor, GuiDocMerge, GuiDocSplit,
|
||||
GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiItemEditor,
|
||||
GuiMainMenu, GuiMainStatus, GuiNovelTree, GuiOutline, GuiOutlineDetails,
|
||||
GuiPreferences, GuiProjectDetails, GuiProjectLoad, GuiProjectSettings,
|
||||
GuiProjectTree, GuiProjectWizard, GuiTheme, GuiWordList, GuiWritingStats
|
||||
GuiDocEditor, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiMainMenu,
|
||||
GuiMainStatus, GuiNovelTree, GuiOutline, GuiOutlineDetails,
|
||||
GuiProjectDetails, GuiProjectTree, GuiTheme
|
||||
)
|
||||
from nw.dialogs import (
|
||||
GuiAbout, GuiDocMerge, GuiDocSplit, GuiItemEditor, GuiPreferences,
|
||||
GuiProjectLoad, GuiProjectSettings, GuiWordList
|
||||
)
|
||||
from nw.tools import GuiBuildNovel, GuiProjectWizard, GuiWritingStats
|
||||
from nw.core import NWProject, NWDoc, NWIndex
|
||||
from nw.constants import nwItemType, nwItemClass, nwAlert, nwLists
|
||||
from nw.enum import nwItemType, nwItemClass, nwAlert, nwWidget
|
||||
from nw.common import getGuiItem, hexToInt
|
||||
from nw.constants import nwLists
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -76,6 +80,7 @@ class GuiMain(QMainWindow):
|
||||
logger.info("Python Version: %s (0x%x)" % (
|
||||
self.mainConf.verPyString, self.mainConf.verPyHexVal)
|
||||
)
|
||||
logger.info("GUI Language: %s" % self.mainConf.guiLang)
|
||||
|
||||
# Core Classes
|
||||
# ============
|
||||
@@ -122,8 +127,8 @@ class GuiMain(QMainWindow):
|
||||
self.projTabs = QTabWidget()
|
||||
self.projTabs.setTabPosition(QTabWidget.South)
|
||||
self.projTabs.setStyleSheet(r"QTabWidget::pane {border: 0;};")
|
||||
self.projTabs.addTab(self.treeView, "Project")
|
||||
self.projTabs.addTab(self.novelView, "Novel")
|
||||
self.projTabs.addTab(self.treeView, self.tr("Project"))
|
||||
self.projTabs.addTab(self.novelView, self.tr("Novel"))
|
||||
self.projTabs.currentChanged.connect(self._projTabsChanged)
|
||||
|
||||
tabFont = self.projTabs.tabBar().font()
|
||||
@@ -139,17 +144,17 @@ class GuiMain(QMainWindow):
|
||||
self.treeButtons.setStyleSheet(r"QToolBar {padding: 0;}")
|
||||
self.projTabs.setCornerWidget(self.treeButtons, Qt.BottomRightCorner)
|
||||
|
||||
self.projDetailsBtn = QAction("Project Details")
|
||||
self.projDetailsBtn = QAction(self.tr("Project Details"))
|
||||
self.projDetailsBtn.setIcon(self.theTheme.getIcon("status_lines"))
|
||||
self.projDetailsBtn.triggered.connect(lambda: self.showProjectDetailsDialog())
|
||||
self.treeButtons.addAction(self.projDetailsBtn)
|
||||
|
||||
self.projStatsBtn = QAction("Writing Statistics")
|
||||
self.projStatsBtn = QAction(self.tr("Writing Statistics"))
|
||||
self.projStatsBtn.setIcon(self.theTheme.getIcon("status_stats"))
|
||||
self.projStatsBtn.triggered.connect(lambda: self.showWritingStatsDialog())
|
||||
self.treeButtons.addAction(self.projStatsBtn)
|
||||
|
||||
self.projSettingsBtn = QAction("Project Settings")
|
||||
self.projSettingsBtn = QAction(self.tr("Project Settings"))
|
||||
self.projSettingsBtn.setIcon(self.theTheme.getIcon("settings"))
|
||||
self.projSettingsBtn.triggered.connect(lambda: self.showProjectSettingsDialog())
|
||||
self.treeButtons.addAction(self.projSettingsBtn)
|
||||
@@ -184,8 +189,8 @@ class GuiMain(QMainWindow):
|
||||
self.mainTabs = QTabWidget()
|
||||
self.mainTabs.setTabPosition(QTabWidget.East)
|
||||
self.mainTabs.setStyleSheet(r"QTabWidget::pane {border: 0;}")
|
||||
self.mainTabs.addTab(self.splitDocs, "Editor")
|
||||
self.mainTabs.addTab(self.splitOutline, "Outline")
|
||||
self.mainTabs.addTab(self.splitDocs, self.tr("Editor"))
|
||||
self.mainTabs.addTab(self.splitOutline, self.tr("Outline"))
|
||||
self.mainTabs.currentChanged.connect(self._mainTabChanged)
|
||||
|
||||
# Splitter : Project Tree / Main Tabs
|
||||
@@ -289,7 +294,7 @@ class GuiMain(QMainWindow):
|
||||
self.openProject(self.mainConf.cmdOpen)
|
||||
|
||||
logger.debug("novelWriter is ready ...")
|
||||
self.setStatus("novelWriter is ready ...")
|
||||
self.setStatus(self.tr("novelWriter is ready ..."))
|
||||
|
||||
return
|
||||
|
||||
@@ -339,7 +344,7 @@ class GuiMain(QMainWindow):
|
||||
if self.hasProject:
|
||||
if not self.closeProject():
|
||||
self.makeAlert(
|
||||
"Cannot create new project when another project is open.",
|
||||
self.tr("Cannot create new project when another project is open."),
|
||||
nwAlert.ERROR
|
||||
)
|
||||
return False
|
||||
@@ -357,8 +362,10 @@ class GuiMain(QMainWindow):
|
||||
|
||||
if os.path.isfile(os.path.join(projPath, self.theProject.projFile)):
|
||||
self.makeAlert(
|
||||
"A project already exists in that location. Please choose another folder.",
|
||||
nwAlert.ERROR
|
||||
self.tr(
|
||||
"A project already exists in that location. "
|
||||
"Please choose another folder."
|
||||
), nwAlert.ERROR
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -372,7 +379,7 @@ class GuiMain(QMainWindow):
|
||||
self.statusBar.setRefTime(self.theProject.projOpened)
|
||||
self.statusBar.setProjectStatus(True)
|
||||
self.statusBar.setDocumentStatus(None)
|
||||
self.statusBar.setStatus("New project created ...")
|
||||
self.statusBar.setStatus(self.tr("New project created ..."))
|
||||
self._updateWindowTitle(self.theProject.projName)
|
||||
else:
|
||||
self.theProject.clearProject()
|
||||
@@ -391,8 +398,11 @@ class GuiMain(QMainWindow):
|
||||
|
||||
if not isYes:
|
||||
msgYes = self.askQuestion(
|
||||
"Close Project",
|
||||
"Close the current project?<br>Changes are saved automatically."
|
||||
self.tr("Close Project"),
|
||||
"%s<br>%s" % (
|
||||
self.tr("Close the current project?"),
|
||||
self.tr("Changes are saved automatically.")
|
||||
)
|
||||
)
|
||||
if not msgYes:
|
||||
return False
|
||||
@@ -407,7 +417,8 @@ class GuiMain(QMainWindow):
|
||||
doBackup = True
|
||||
if self.mainConf.askBeforeBackup:
|
||||
msgYes = self.askQuestion(
|
||||
"Backup Project", "Backup the current project?"
|
||||
self.tr("Backup Project"),
|
||||
self.tr("Backup the current project?")
|
||||
)
|
||||
if not msgYes:
|
||||
doBackup = False
|
||||
@@ -457,29 +468,35 @@ class GuiMain(QMainWindow):
|
||||
|
||||
try:
|
||||
lockDetails = (
|
||||
"<br><br>The project was locked by the computer "
|
||||
"'%s' (%s %s), last active on %s"
|
||||
) % (
|
||||
"<br>%s" % self.tr(
|
||||
"The project was locked by the computer "
|
||||
"'{0}' ({1} {2}), last active on {3}."
|
||||
)
|
||||
).format(
|
||||
self.theProject.lockedBy[0],
|
||||
self.theProject.lockedBy[1],
|
||||
self.theProject.lockedBy[2],
|
||||
datetime.fromtimestamp(
|
||||
int(self.theProject.lockedBy[3])
|
||||
).strftime("%x %X")
|
||||
datetime.fromtimestamp(int(self.theProject.lockedBy[3])).strftime("%x %X")
|
||||
)
|
||||
except Exception:
|
||||
lockDetails = ""
|
||||
|
||||
msgBox = QMessageBox()
|
||||
msgRes = msgBox.warning(
|
||||
self, "Project Locked", (
|
||||
"The project is already open by another instance of novelWriter, and "
|
||||
"is therefore locked. Override lock and continue anyway?<br><br>"
|
||||
"Note: If the program or the computer previously crashed, the lock "
|
||||
"can safely be overridden. If, however, another instance of "
|
||||
"novelWriter has the project open, overriding the lock may corrupt "
|
||||
"the project, and is not recommended.%s"
|
||||
) % lockDetails,
|
||||
self, self.tr("Project Locked"),
|
||||
"%s<br><br>%s<br>%s" % (
|
||||
self.tr(
|
||||
"The project is already open by another instance of novelWriter, and "
|
||||
"is therefore locked. Override lock and continue anyway?"
|
||||
),
|
||||
self.tr(
|
||||
"Note: If the program or the computer previously crashed, the lock "
|
||||
"can safely be overridden. If, however, another instance of "
|
||||
"novelWriter has the project open, overriding the lock may corrupt "
|
||||
"the project, and is not recommended."
|
||||
),
|
||||
lockDetails
|
||||
),
|
||||
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
|
||||
)
|
||||
if msgRes == QMessageBox.Yes:
|
||||
@@ -514,6 +531,10 @@ class GuiMain(QMainWindow):
|
||||
|
||||
# Check if we need to rebuild the index
|
||||
if self.theIndex.indexBroken:
|
||||
self.makeAlert(
|
||||
self.tr("The project index is outdated or broken. Rebuilding index."),
|
||||
nwAlert.WARN
|
||||
)
|
||||
self.rebuildIndex()
|
||||
|
||||
# Make sure the changed status is set to false on all that was
|
||||
@@ -533,17 +554,9 @@ class GuiMain(QMainWindow):
|
||||
logger.error("No project open")
|
||||
return False
|
||||
|
||||
# If the project is new, it may not have a path, so we need one
|
||||
if self.theProject.projPath is None:
|
||||
projPath = self.selectProjectPath()
|
||||
self.theProject.setProjectPath(projPath)
|
||||
|
||||
if self.theProject.projPath is None:
|
||||
return False
|
||||
|
||||
self.treeView.saveTreeOrder()
|
||||
self.theProject.saveProject(autoSave=autoSave)
|
||||
self.theIndex.saveIndex()
|
||||
if self.theProject.saveProject(autoSave=autoSave):
|
||||
self.theIndex.saveIndex()
|
||||
|
||||
return True
|
||||
|
||||
@@ -558,6 +571,10 @@ class GuiMain(QMainWindow):
|
||||
logger.error("No project open")
|
||||
return False
|
||||
|
||||
# Disable focus mode if it is active
|
||||
if self.isFocusMode:
|
||||
self.toggleFocusMode()
|
||||
|
||||
self.docEditor.saveCursorPosition()
|
||||
if self.docEditor.docChanged:
|
||||
self.saveDocument()
|
||||
@@ -685,15 +702,13 @@ class GuiMain(QMainWindow):
|
||||
|
||||
lastPath = self.mainConf.lastPath
|
||||
extFilter = [
|
||||
"Text files (*.txt)",
|
||||
"Markdown files (*.md)",
|
||||
"novelWriter files (*.nwd)",
|
||||
"All files (*.*)",
|
||||
self.tr("Text files ({0})").format("*.txt"),
|
||||
self.tr("Markdown files ({0})").format("*.md"),
|
||||
self.tr("novelWriter files ({0})").format("*.nwd"),
|
||||
self.tr("All files ({0})").format("*"),
|
||||
]
|
||||
dlgOpt = QFileDialog.Options()
|
||||
dlgOpt |= QFileDialog.DontUseNativeDialog
|
||||
loadFile, _ = QFileDialog.getOpenFileName(
|
||||
self, "Import File", lastPath, options=dlgOpt, filter=";;".join(extFilter)
|
||||
self, self.tr("Import File"), lastPath, filter=";;".join(extFilter)
|
||||
)
|
||||
if not loadFile:
|
||||
return False
|
||||
@@ -707,24 +722,26 @@ class GuiMain(QMainWindow):
|
||||
theText = inFile.read()
|
||||
self.mainConf.setLastPath(loadFile)
|
||||
except Exception as e:
|
||||
self.makeAlert(
|
||||
["Could not read file. The file must be an existing text file.", str(e)],
|
||||
nwAlert.ERROR
|
||||
)
|
||||
self.makeAlert([
|
||||
self.tr("Could not read file. The file must be an existing text file."), str(e)
|
||||
], nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
if self.docEditor.theHandle is None:
|
||||
self.makeAlert(
|
||||
"Please open a document to import the text file into.",
|
||||
self.tr("Please open a document to import the text file into."),
|
||||
nwAlert.ERROR
|
||||
)
|
||||
return False
|
||||
|
||||
if not self.docEditor.isEmpty():
|
||||
msgYes = self.askQuestion("Import Document", (
|
||||
"Importing the file will overwrite the current content of the document. "
|
||||
"Do you want to proceed?"
|
||||
))
|
||||
msgYes = self.askQuestion(
|
||||
self.tr("Import Document"),
|
||||
self.tr(
|
||||
"Importing the file will overwrite the current content of the document. "
|
||||
"Do you want to proceed?"
|
||||
)
|
||||
)
|
||||
if not msgYes:
|
||||
return False
|
||||
|
||||
@@ -757,14 +774,18 @@ class GuiMain(QMainWindow):
|
||||
return True
|
||||
|
||||
def passDocumentAction(self, theAction):
|
||||
"""Pass on document action theAction to the document viewer if
|
||||
it has focus, otherwise pass it to the document editor.
|
||||
"""Pass on document action to the document viewer if it has
|
||||
focus, or pass it to the document editor if it or any of
|
||||
its clid widgets have focus. If neither has focus, ignore the
|
||||
action.
|
||||
"""
|
||||
if self.docViewer.hasFocus():
|
||||
self.docViewer.docAction(theAction)
|
||||
else:
|
||||
elif self.docEditor.hasFocus():
|
||||
self.docEditor.docAction(theAction)
|
||||
return True
|
||||
else:
|
||||
logger.debug("Action cancelled as neither editor nor viewer has focus")
|
||||
return
|
||||
|
||||
##
|
||||
# Tree Item Actions
|
||||
@@ -800,10 +821,10 @@ class GuiMain(QMainWindow):
|
||||
return False
|
||||
|
||||
if tHandle is None:
|
||||
if self.treeView.hasFocus():
|
||||
tHandle = self.treeView.getSelectedHandle()
|
||||
elif self.docEditor.hasFocus():
|
||||
if self.docEditor.anyFocus() or self.isFocusMode:
|
||||
tHandle = self.docEditor.theHandle
|
||||
else:
|
||||
tHandle = self.treeView.getSelectedHandle()
|
||||
|
||||
if tHandle is None:
|
||||
logger.warning("No item selected")
|
||||
@@ -861,9 +882,9 @@ class GuiMain(QMainWindow):
|
||||
for nDone, tItem in enumerate(self.theProject.projTree):
|
||||
|
||||
if tItem is not None:
|
||||
self.setStatus("Indexing: '%s'" % tItem.itemName)
|
||||
self.setStatus(self.tr("Indexing: '{0}'").format(tItem.itemName))
|
||||
else:
|
||||
self.setStatus("Indexing: Unknown item")
|
||||
self.setStatus(self.tr("Indexing: '{0}'").format(self.tr("Unknown item")))
|
||||
|
||||
if tItem is not None and tItem.itemType == nwItemType.FILE:
|
||||
logger.verbose("Scanning: %s" % tItem.itemName)
|
||||
@@ -881,12 +902,16 @@ class GuiMain(QMainWindow):
|
||||
self.treeView.projectWordCount()
|
||||
|
||||
tEnd = time()
|
||||
self.setStatus("Indexing completed in %.1f ms" % ((tEnd - tStart)*1000.0))
|
||||
self.setStatus(
|
||||
self.tr("Indexing completed in {0} ms").format(f"{(tEnd - tStart)*1000.0:.1f}")
|
||||
)
|
||||
self.docEditor.updateTagHighLighting()
|
||||
qApp.restoreOverrideCursor()
|
||||
|
||||
if not beQuiet:
|
||||
self.makeAlert("The project index has been successfully rebuilt.", nwAlert.INFO)
|
||||
self.makeAlert(
|
||||
self.tr("The project index has been successfully rebuilt."), nwAlert.INFO
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@@ -907,19 +932,6 @@ class GuiMain(QMainWindow):
|
||||
# Main Dialogs
|
||||
##
|
||||
|
||||
def selectProjectPath(self):
|
||||
"""Select where to save project.
|
||||
"""
|
||||
dlgOpt = QFileDialog.Options()
|
||||
dlgOpt |= QFileDialog.ShowDirsOnly
|
||||
dlgOpt |= QFileDialog.DontUseNativeDialog
|
||||
projPath = QFileDialog.getExistingDirectory(
|
||||
self, "Save novelWriter Project", "", options=dlgOpt
|
||||
)
|
||||
if projPath:
|
||||
return projPath
|
||||
return None
|
||||
|
||||
def showProjectLoadDialog(self):
|
||||
"""Opens the projects dialog for selecting either existing
|
||||
projects from a cache of recently opened projects, or provide a
|
||||
@@ -1100,14 +1112,14 @@ class GuiMain(QMainWindow):
|
||||
# Popup
|
||||
msgBox = QMessageBox()
|
||||
if theLevel == nwAlert.INFO:
|
||||
msgBox.information(self, "Information", popMsg)
|
||||
msgBox.information(self, self.tr("Information"), popMsg)
|
||||
elif theLevel == nwAlert.WARN:
|
||||
msgBox.warning(self, "Warning", popMsg)
|
||||
msgBox.warning(self, self.tr("Warning"), popMsg)
|
||||
elif theLevel == nwAlert.ERROR:
|
||||
msgBox.critical(self, "Error", popMsg)
|
||||
msgBox.critical(self, self.tr("Error"), popMsg)
|
||||
elif theLevel == nwAlert.BUG:
|
||||
popMsg += "<br>This is a bug!"
|
||||
msgBox.critical(self, "Internal Error", popMsg)
|
||||
popMsg += "<br>%s" % self.tr("This is a bug!")
|
||||
msgBox.critical(self, self.tr("Internal Error"), popMsg)
|
||||
|
||||
return
|
||||
|
||||
@@ -1115,7 +1127,7 @@ class GuiMain(QMainWindow):
|
||||
"""Ask the user a Yes/No question.
|
||||
"""
|
||||
msgBox = QMessageBox()
|
||||
msgRes = msgBox.question(self, theTitle, theQuestion)
|
||||
msgRes = msgBox.question(self, theTitle, theQuestion, QMessageBox.Yes | QMessageBox.No)
|
||||
return msgRes == QMessageBox.Yes
|
||||
|
||||
def reportConfErr(self):
|
||||
@@ -1137,8 +1149,11 @@ class GuiMain(QMainWindow):
|
||||
"""
|
||||
if self.hasProject:
|
||||
msgYes = self.askQuestion(
|
||||
"Exit",
|
||||
"Do you want to exit novelWriter?<br>Changes are saved automatically."
|
||||
self.tr("Exit"),
|
||||
"%s<br>%s" % (
|
||||
self.tr("Do you want to exit novelWriter?"),
|
||||
self.tr("Changes are saved automatically.")
|
||||
)
|
||||
)
|
||||
if not msgYes:
|
||||
return False
|
||||
@@ -1169,18 +1184,18 @@ class GuiMain(QMainWindow):
|
||||
|
||||
return True
|
||||
|
||||
def setFocus(self, paneNo):
|
||||
def switchFocus(self, paneNo):
|
||||
"""Switch focus between main GUI views.
|
||||
"""
|
||||
if paneNo == 1:
|
||||
if paneNo == nwWidget.TREE:
|
||||
self.treeView.setFocus()
|
||||
elif paneNo == 2:
|
||||
elif paneNo == nwWidget.EDITOR:
|
||||
self.mainTabs.setCurrentWidget(self.splitDocs)
|
||||
self.docEditor.setFocus()
|
||||
elif paneNo == 3:
|
||||
elif paneNo == nwWidget.VIEWER:
|
||||
self.mainTabs.setCurrentWidget(self.splitDocs)
|
||||
self.docViewer.setFocus()
|
||||
elif paneNo == 4:
|
||||
elif paneNo == nwWidget.OUTLINE:
|
||||
self.mainTabs.setCurrentWidget(self.splitOutline)
|
||||
self.projView.setFocus()
|
||||
return
|
||||
@@ -1217,6 +1232,7 @@ class GuiMain(QMainWindow):
|
||||
if self.isFocusMode:
|
||||
logger.debug("Activating Focus Mode")
|
||||
self.mainTabs.setCurrentWidget(self.splitDocs)
|
||||
self.switchFocus(nwWidget.EDITOR)
|
||||
else:
|
||||
logger.debug("Deactivating Focus Mode")
|
||||
|
||||
@@ -1228,6 +1244,7 @@ class GuiMain(QMainWindow):
|
||||
|
||||
hideDocFooter = self.isFocusMode and self.mainConf.hideFocusFooter
|
||||
self.docEditor.docFooter.setVisible(not hideDocFooter)
|
||||
self.docEditor.docHeader.updateFocusMode()
|
||||
|
||||
if self.splitView.isVisible():
|
||||
self.splitView.setVisible(False)
|
||||
@@ -1265,11 +1282,12 @@ class GuiMain(QMainWindow):
|
||||
"""
|
||||
# Project
|
||||
self.addAction(self.mainMenu.aSaveProject)
|
||||
self.addAction(self.mainMenu.aEditItem)
|
||||
self.addAction(self.mainMenu.aExitNW)
|
||||
|
||||
# Document
|
||||
self.addAction(self.mainMenu.aSaveDoc)
|
||||
self.addAction(self.mainMenu.aFileDetails)
|
||||
self.addAction(self.mainMenu.aCloseDoc)
|
||||
|
||||
# Edit
|
||||
self.addAction(self.mainMenu.aEditUndo)
|
||||
@@ -1313,6 +1331,13 @@ class GuiMain(QMainWindow):
|
||||
for mAction, _ in self.mainMenu.mInsKWItems.values():
|
||||
self.addAction(mAction)
|
||||
|
||||
# Search
|
||||
self.addAction(self.mainMenu.aFind)
|
||||
self.addAction(self.mainMenu.aReplace)
|
||||
self.addAction(self.mainMenu.aFindNext)
|
||||
self.addAction(self.mainMenu.aFindPrev)
|
||||
self.addAction(self.mainMenu.aReplaceNext)
|
||||
|
||||
# Format
|
||||
self.addAction(self.mainMenu.aFmtEmph)
|
||||
self.addAction(self.mainMenu.aFmtStrong)
|
||||
|
||||
11
nw/tools/__init__.py
Normal file
11
nw/tools/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from nw.tools.build import GuiBuildNovel
|
||||
from nw.tools.projwizard import GuiProjectWizard
|
||||
from nw.tools.writingstats import GuiWritingStats
|
||||
|
||||
__all__ = [
|
||||
"GuiBuildNovel",
|
||||
"GuiProjectWizard",
|
||||
"GuiWritingStats",
|
||||
]
|
||||
@@ -41,15 +41,14 @@ from PyQt5.QtWidgets import (
|
||||
qApp, QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QLabel,
|
||||
QLineEdit, QGroupBox, QGridLayout, QProgressBar, QMenu, QAction,
|
||||
QFileDialog, QFontDialog, QSpinBox, QScrollArea, QSplitter, QWidget,
|
||||
QSizePolicy, QDoubleSpinBox
|
||||
QSizePolicy, QDoubleSpinBox, QComboBox
|
||||
)
|
||||
|
||||
from nw.common import fuzzyTime, makeFileNameSafe
|
||||
from nw.gui.custom import QSwitch
|
||||
from nw.core import ToHtml, ToOdt, ToMarkdown
|
||||
from nw.constants import (
|
||||
nwConst, nwAlert, nwFiles, nwItemType, nwItemLayout, nwItemClass
|
||||
)
|
||||
from nw.enum import nwAlert, nwItemType, nwItemLayout, nwItemClass
|
||||
from nw.common import fuzzyTime, makeFileNameSafe
|
||||
from nw.constants import nwConst, nwFiles
|
||||
from nw.gui.custom import QSwitch
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -84,7 +83,7 @@ class GuiBuildNovel(QDialog):
|
||||
self.htmlSize = 0 # Size of the html document
|
||||
self.buildTime = 0 # The timestamp of the last build
|
||||
|
||||
self.setWindowTitle("Build Novel Project")
|
||||
self.setWindowTitle(self.tr("Build Novel Project"))
|
||||
self.setMinimumWidth(self.mainConf.pxInt(700))
|
||||
self.setMinimumHeight(self.mainConf.pxInt(600))
|
||||
|
||||
@@ -101,27 +100,26 @@ class GuiBuildNovel(QDialog):
|
||||
# Title Formats
|
||||
# =============
|
||||
|
||||
self.titleGroup = QGroupBox("Title Formats for Novel Files", self)
|
||||
self.titleGroup = QGroupBox(self.tr("Title Formats for Novel Files"), self)
|
||||
self.titleForm = QGridLayout(self)
|
||||
self.titleGroup.setLayout(self.titleForm)
|
||||
|
||||
fmtHelp = (
|
||||
r"<b>Formatting Codes:</b><br>"
|
||||
r"%title% for the title as set in the document<br>"
|
||||
r"%ch% for chapter number (1, 2, 3)<br>"
|
||||
r"%chw% for chapter number as a word (one, two)<br>"
|
||||
r"%chI% for chapter number in upper case Roman<br>"
|
||||
r"%chi% for chapter number in lower case Roman<br>"
|
||||
r"%sc% for scene number within chapter<br>"
|
||||
r"%sca% for scene number within novel"
|
||||
)
|
||||
fmtScHelp = (
|
||||
r"<br><br>"
|
||||
r"Leave blank to skip this heading, or set to a static text, like "
|
||||
r"for instance '* * *', to make a separator. The separator will "
|
||||
r"be centred automatically and only appear between sections of "
|
||||
r"the same type."
|
||||
)
|
||||
fmtHelp = "<br>".join([
|
||||
"<b>%s</b>" % self.tr("Formatting Codes:"),
|
||||
self.tr("{0} for the title as set in the document").format(r"%title%"),
|
||||
self.tr("{0} for chapter number (1, 2, 3)").format(r"%ch%"),
|
||||
self.tr("{0} for chapter number as a word (one, two)").format(r"%chw%"),
|
||||
self.tr("{0} for chapter number in upper case Roman").format(r"%chI%"),
|
||||
self.tr("{0} for chapter number in lower case Roman").format(r"%chi%"),
|
||||
self.tr("{0} for scene number within chapter").format(r"%sc%"),
|
||||
self.tr("{0} for scene number within novel").format(r"%sca%"),
|
||||
])
|
||||
fmtScHelp = "<br><br>%s" % self.tr(
|
||||
"Leave blank to skip this heading, or set to a static text, like "
|
||||
"for instance '{0}', to make a separator. The separator will "
|
||||
"be centred automatically and only appear between sections of "
|
||||
"the same type."
|
||||
).format("* * *")
|
||||
xFmt = self.mainConf.pxInt(100)
|
||||
|
||||
self.fmtTitle = QLineEdit()
|
||||
@@ -164,6 +162,17 @@ class GuiBuildNovel(QDialog):
|
||||
self._reFmtCodes(self.theProject.titleFormat["section"])
|
||||
)
|
||||
|
||||
self.buildLang = QComboBox()
|
||||
self.buildLang.setMinimumWidth(xFmt)
|
||||
theLangs = self.mainConf.listLanguages(self.mainConf.LANG_PROJ)
|
||||
self.buildLang.addItem("[%s]" % self.tr("Not Set"), "None")
|
||||
for langID, langName in theLangs:
|
||||
self.buildLang.addItem(langName, langID)
|
||||
|
||||
langIdx = self.buildLang.findData(self.theProject.projLang)
|
||||
if langIdx != -1:
|
||||
self.buildLang.setCurrentIndex(langIdx)
|
||||
|
||||
# Dummy boxes due to QGridView and QLineEdit expand bug
|
||||
self.boxTitle = QHBoxLayout()
|
||||
self.boxTitle.addWidget(self.fmtTitle)
|
||||
@@ -176,11 +185,12 @@ class GuiBuildNovel(QDialog):
|
||||
self.boxSection = QHBoxLayout()
|
||||
self.boxSection.addWidget(self.fmtSection)
|
||||
|
||||
titleLabel = QLabel("Title")
|
||||
chapterLabel = QLabel("Chapter")
|
||||
unnumbLabel = QLabel("Unnumbered")
|
||||
sceneLabel = QLabel("Scene")
|
||||
sectionLabel = QLabel("Section")
|
||||
titleLabel = QLabel(self.tr("Title"))
|
||||
chapterLabel = QLabel(self.tr("Chapter"))
|
||||
unnumbLabel = QLabel(self.tr("Unnumbered"))
|
||||
sceneLabel = QLabel(self.tr("Scene"))
|
||||
sectionLabel = QLabel(self.tr("Section"))
|
||||
langLabel = QLabel(self.tr("Language"))
|
||||
|
||||
self.titleForm.addWidget(titleLabel, 0, 0, 1, 1, Qt.AlignLeft)
|
||||
self.titleForm.addLayout(self.boxTitle, 0, 1, 1, 1, Qt.AlignRight)
|
||||
@@ -192,6 +202,8 @@ class GuiBuildNovel(QDialog):
|
||||
self.titleForm.addLayout(self.boxScene, 3, 1, 1, 1, Qt.AlignRight)
|
||||
self.titleForm.addWidget(sectionLabel, 4, 0, 1, 1, Qt.AlignLeft)
|
||||
self.titleForm.addLayout(self.boxSection, 4, 1, 1, 1, Qt.AlignRight)
|
||||
self.titleForm.addWidget(langLabel, 5, 0, 1, 1, Qt.AlignLeft)
|
||||
self.titleForm.addWidget(self.buildLang, 5, 1, 1, 1, Qt.AlignRight)
|
||||
|
||||
self.titleForm.setColumnStretch(0, 0)
|
||||
self.titleForm.setColumnStretch(1, 1)
|
||||
@@ -199,7 +211,7 @@ class GuiBuildNovel(QDialog):
|
||||
# Font Options
|
||||
# ============
|
||||
|
||||
self.fontGroup = QGroupBox("Font Options", self)
|
||||
self.fontGroup = QGroupBox(self.tr("Font Options"), self)
|
||||
self.fontForm = QGridLayout(self)
|
||||
self.fontGroup.setLayout(self.fontForm)
|
||||
|
||||
@@ -237,11 +249,11 @@ class GuiBuildNovel(QDialog):
|
||||
self.boxFont = QHBoxLayout()
|
||||
self.boxFont.addWidget(self.textFont)
|
||||
|
||||
fontFamilyLabel = QLabel("Font family")
|
||||
fontSizeLabel = QLabel("Font size")
|
||||
lineHeightLabel = QLabel("Line height")
|
||||
justifyLabel = QLabel("Justify text")
|
||||
stylingLabel = QLabel("Disable styling")
|
||||
fontFamilyLabel = QLabel(self.tr("Font family"))
|
||||
fontSizeLabel = QLabel(self.tr("Font size"))
|
||||
lineHeightLabel = QLabel(self.tr("Line height"))
|
||||
justifyLabel = QLabel(self.tr("Justify text"))
|
||||
stylingLabel = QLabel(self.tr("Disable styling"))
|
||||
|
||||
self.fontForm.addWidget(fontFamilyLabel, 0, 0, 1, 1, Qt.AlignLeft)
|
||||
self.fontForm.addLayout(self.boxFont, 0, 1, 1, 1, Qt.AlignRight)
|
||||
@@ -258,7 +270,7 @@ class GuiBuildNovel(QDialog):
|
||||
# Styling Options
|
||||
# ===============
|
||||
|
||||
self.styleGroup = QGroupBox("Styling Options", self)
|
||||
self.styleGroup = QGroupBox(self.tr("Styling Options"), self)
|
||||
self.styleForm = QGridLayout(self)
|
||||
self.styleGroup.setLayout(self.styleForm)
|
||||
|
||||
@@ -283,7 +295,7 @@ class GuiBuildNovel(QDialog):
|
||||
# Include Options
|
||||
# ===============
|
||||
|
||||
self.textGroup = QGroupBox("Include Options", self)
|
||||
self.textGroup = QGroupBox(self.tr("Include Options"), self)
|
||||
self.textForm = QGridLayout(self)
|
||||
self.textGroup.setLayout(self.textForm)
|
||||
|
||||
@@ -307,10 +319,10 @@ class GuiBuildNovel(QDialog):
|
||||
self.optState.getBool("GuiBuildNovel", "incBodyText", True)
|
||||
)
|
||||
|
||||
synopsisLabel = QLabel("Include synopsis")
|
||||
commentsLabel = QLabel("Include comments")
|
||||
keywordsLabel = QLabel("Include keywords")
|
||||
bodyLabel = QLabel("Include body text")
|
||||
synopsisLabel = QLabel(self.tr("Include synopsis"))
|
||||
commentsLabel = QLabel(self.tr("Include comments"))
|
||||
keywordsLabel = QLabel(self.tr("Include keywords"))
|
||||
bodyLabel = QLabel(self.tr("Include body text"))
|
||||
|
||||
self.textForm.addWidget(synopsisLabel, 0, 0, 1, 1, Qt.AlignLeft)
|
||||
self.textForm.addWidget(self.includeSynopsis, 0, 1, 1, 1, Qt.AlignRight)
|
||||
@@ -327,37 +339,34 @@ class GuiBuildNovel(QDialog):
|
||||
# File Filter Options
|
||||
# ===================
|
||||
|
||||
self.fileGroup = QGroupBox("File Filter Options", self)
|
||||
self.fileGroup = QGroupBox(self.tr("File Filter Options"), self)
|
||||
self.fileForm = QGridLayout(self)
|
||||
self.fileGroup.setLayout(self.fileForm)
|
||||
|
||||
self.novelFiles = QSwitch(width=wS, height=hS)
|
||||
self.novelFiles.setToolTip(
|
||||
"Include files with layouts 'Book', 'Page', 'Partition', "
|
||||
"'Chapter', 'Unnumbered', and 'Scene'."
|
||||
)
|
||||
self.novelFiles.setToolTip(self.tr("Include files with layouts other than 'Note'."))
|
||||
self.novelFiles.setChecked(
|
||||
self.optState.getBool("GuiBuildNovel", "addNovel", True)
|
||||
)
|
||||
|
||||
self.noteFiles = QSwitch(width=wS, height=hS)
|
||||
self.noteFiles.setToolTip("Include files with layout 'Note'.")
|
||||
self.noteFiles.setToolTip(self.tr("Include files with layout 'Note'."))
|
||||
self.noteFiles.setChecked(
|
||||
self.optState.getBool("GuiBuildNovel", "addNotes", False)
|
||||
)
|
||||
|
||||
self.ignoreFlag = QSwitch(width=wS, height=hS)
|
||||
self.ignoreFlag.setToolTip(
|
||||
self.ignoreFlag.setToolTip(self.tr(
|
||||
"Ignore the 'Include when building project' setting and include "
|
||||
"all files in the output."
|
||||
)
|
||||
))
|
||||
self.ignoreFlag.setChecked(
|
||||
self.optState.getBool("GuiBuildNovel", "ignoreFlag", False)
|
||||
)
|
||||
|
||||
novelLabel = QLabel("Include novel files")
|
||||
notesLabel = QLabel("Include note files")
|
||||
exportLabel = QLabel("Ignore export flag")
|
||||
novelLabel = QLabel(self.tr("Include novel files"))
|
||||
notesLabel = QLabel(self.tr("Include note files"))
|
||||
exportLabel = QLabel(self.tr("Ignore export flag"))
|
||||
|
||||
self.fileForm.addWidget(novelLabel, 0, 0, 1, 1, Qt.AlignLeft)
|
||||
self.fileForm.addWidget(self.novelFiles, 0, 1, 1, 1, Qt.AlignRight)
|
||||
@@ -372,7 +381,7 @@ class GuiBuildNovel(QDialog):
|
||||
# Export Options
|
||||
# ==============
|
||||
|
||||
self.exportGroup = QGroupBox("Export Options", self)
|
||||
self.exportGroup = QGroupBox(self.tr("Export Options"), self)
|
||||
self.exportForm = QGridLayout(self)
|
||||
self.exportGroup.setLayout(self.exportForm)
|
||||
|
||||
@@ -386,8 +395,8 @@ class GuiBuildNovel(QDialog):
|
||||
self.optState.getBool("GuiBuildNovel", "replaceUCode", False)
|
||||
)
|
||||
|
||||
tabsLabel = QLabel("Replace tabs with spaces")
|
||||
uCodeLabel = QLabel("Replace Unicode in HTML")
|
||||
tabsLabel = QLabel(self.tr("Replace tabs with spaces"))
|
||||
uCodeLabel = QLabel(self.tr("Replace Unicode in HTML"))
|
||||
|
||||
self.exportForm.addWidget(tabsLabel, 0, 0, 1, 1, Qt.AlignLeft)
|
||||
self.exportForm.addWidget(self.replaceTabs, 0, 1, 1, 1, Qt.AlignRight)
|
||||
@@ -402,7 +411,7 @@ class GuiBuildNovel(QDialog):
|
||||
|
||||
self.buildProgress = QProgressBar()
|
||||
|
||||
self.buildNovel = QPushButton("Build Preview")
|
||||
self.buildNovel = QPushButton(self.tr("Build Preview"))
|
||||
self.buildNovel.clicked.connect(self._buildPreview)
|
||||
|
||||
# Action Buttons
|
||||
@@ -413,56 +422,56 @@ class GuiBuildNovel(QDialog):
|
||||
# Printing
|
||||
|
||||
self.printMenu = QMenu(self)
|
||||
self.btnPrint = QPushButton("Print")
|
||||
self.btnPrint = QPushButton(self.tr("Print"))
|
||||
self.btnPrint.setMenu(self.printMenu)
|
||||
|
||||
self.printSend = QAction("Print Preview", self)
|
||||
self.printSend = QAction(self.tr("Print Preview"), self)
|
||||
self.printSend.triggered.connect(self._printDocument)
|
||||
self.printMenu.addAction(self.printSend)
|
||||
|
||||
self.printFile = QAction("Print to PDF", self)
|
||||
self.printFile = QAction(self.tr("Print to PDF"), self)
|
||||
self.printFile.triggered.connect(lambda: self._saveDocument(self.FMT_PDF))
|
||||
self.printMenu.addAction(self.printFile)
|
||||
|
||||
# Saving to File
|
||||
|
||||
self.saveMenu = QMenu(self)
|
||||
self.btnSave = QPushButton("Save As")
|
||||
self.btnSave = QPushButton(self.tr("Save As"))
|
||||
self.btnSave.setMenu(self.saveMenu)
|
||||
|
||||
self.saveODT = QAction("Open Document (.odt)", self)
|
||||
self.saveODT = QAction(self.tr("Open Document (.odt)"), self)
|
||||
self.saveODT.triggered.connect(lambda: self._saveDocument(self.FMT_ODT))
|
||||
self.saveMenu.addAction(self.saveODT)
|
||||
|
||||
self.saveFODT = QAction("Flat Open Document (.fodt)", self)
|
||||
self.saveFODT = QAction(self.tr("Flat Open Document (.fodt)"), self)
|
||||
self.saveFODT.triggered.connect(lambda: self._saveDocument(self.FMT_FODT))
|
||||
self.saveMenu.addAction(self.saveFODT)
|
||||
|
||||
self.saveHTM = QAction("novelWriter HTML (.htm)", self)
|
||||
self.saveHTM = QAction(self.tr("novelWriter HTML (.htm)"), self)
|
||||
self.saveHTM.triggered.connect(lambda: self._saveDocument(self.FMT_HTM))
|
||||
self.saveMenu.addAction(self.saveHTM)
|
||||
|
||||
self.saveNWD = QAction("novelWriter Markdown (.nwd)", self)
|
||||
self.saveNWD = QAction(self.tr("novelWriter Markdown (.nwd)"), self)
|
||||
self.saveNWD.triggered.connect(lambda: self._saveDocument(self.FMT_NWD))
|
||||
self.saveMenu.addAction(self.saveNWD)
|
||||
|
||||
self.saveMD = QAction("Standard Markdown (.md)", self)
|
||||
self.saveMD = QAction(self.tr("Standard Markdown (.md)"), self)
|
||||
self.saveMD.triggered.connect(lambda: self._saveDocument(self.FMT_MD))
|
||||
self.saveMenu.addAction(self.saveMD)
|
||||
|
||||
self.saveGH = QAction("GitHub Markdown (.md)", self)
|
||||
self.saveGH = QAction(self.tr("GitHub Markdown (.md)"), self)
|
||||
self.saveGH.triggered.connect(lambda: self._saveDocument(self.FMT_GH))
|
||||
self.saveMenu.addAction(self.saveGH)
|
||||
|
||||
self.saveJsonH = QAction("JSON + novelWriter HTML (.json)", self)
|
||||
self.saveJsonH = QAction(self.tr("JSON + novelWriter HTML (.json)"), self)
|
||||
self.saveJsonH.triggered.connect(lambda: self._saveDocument(self.FMT_JSON_H))
|
||||
self.saveMenu.addAction(self.saveJsonH)
|
||||
|
||||
self.saveJsonM = QAction("JSON + novelWriters Markdown (.json)", self)
|
||||
self.saveJsonM = QAction(self.tr("JSON + novelWriter Markdown (.json)"), self)
|
||||
self.saveJsonM.triggered.connect(lambda: self._saveDocument(self.FMT_JSON_M))
|
||||
self.saveMenu.addAction(self.saveJsonM)
|
||||
|
||||
self.btnClose = QPushButton("Close")
|
||||
self.btnClose = QPushButton(self.tr("Close"))
|
||||
self.btnClose.clicked.connect(self._doClose)
|
||||
|
||||
self.buttonBox.addWidget(self.btnSave)
|
||||
@@ -567,7 +576,7 @@ class GuiBuildNovel(QDialog):
|
||||
self.docView.setContent(self.htmlText, self.buildTime)
|
||||
else:
|
||||
self.docView.setText(
|
||||
"Failed to generate preview. The result is too big."
|
||||
self.tr("Failed to generate preview. The result is too big.")
|
||||
)
|
||||
|
||||
else:
|
||||
@@ -655,6 +664,9 @@ class GuiBuildNovel(QDialog):
|
||||
includeBody = self.includeBody.isChecked()
|
||||
replaceUCode = self.replaceUCode.isChecked()
|
||||
|
||||
# The language lookup dict is reloaded if needed
|
||||
self.theProject.setProjectLang(self.buildLang.currentData())
|
||||
|
||||
# Get font information
|
||||
fontInfo = QFontInfo(QFont(textFont, textSize))
|
||||
textFixed = fontInfo.fixedPitch()
|
||||
@@ -736,10 +748,10 @@ class GuiBuildNovel(QDialog):
|
||||
logger.debug("Built project in %.3f ms" % (1000*(tEnd - tStart)))
|
||||
|
||||
if bldObj.errData:
|
||||
self.theParent.makeAlert((
|
||||
"There were problems when building the project:"
|
||||
"<br>- %s"
|
||||
) % "<br>- ".join(bldObj.errData), nwAlert.ERROR)
|
||||
self.theParent.makeAlert("%s:<br>- %s" % (
|
||||
self.tr("There were problems when building the project"),
|
||||
"<br>- ".join(bldObj.errData)), nwAlert.ERROR
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
@@ -796,39 +808,39 @@ class GuiBuildNovel(QDialog):
|
||||
|
||||
if theFmt == self.FMT_ODT:
|
||||
fileExt = "odt"
|
||||
textFmt = "Open Document"
|
||||
textFmt = self.tr("Open Document")
|
||||
|
||||
elif theFmt == self.FMT_FODT:
|
||||
fileExt = "fodt"
|
||||
textFmt = "Flat Open Document"
|
||||
textFmt = self.tr("Flat Open Document")
|
||||
|
||||
elif theFmt == self.FMT_HTM:
|
||||
fileExt = "htm"
|
||||
textFmt = "Plain HTML"
|
||||
textFmt = self.tr("Plain HTML")
|
||||
|
||||
elif theFmt == self.FMT_NWD:
|
||||
fileExt = "nwd"
|
||||
textFmt = "novelWriter Markdown"
|
||||
textFmt = self.tr("novelWriter Markdown")
|
||||
|
||||
elif theFmt == self.FMT_MD:
|
||||
fileExt = "md"
|
||||
textFmt = "Standard Markdown"
|
||||
textFmt = self.tr("Standard Markdown")
|
||||
|
||||
elif theFmt == self.FMT_GH:
|
||||
fileExt = "md"
|
||||
textFmt = "GitHub Markdown"
|
||||
textFmt = self.tr("GitHub Markdown")
|
||||
|
||||
elif theFmt == self.FMT_JSON_H:
|
||||
fileExt = "json"
|
||||
textFmt = "JSON + novelWriter HTML"
|
||||
textFmt = self.tr("JSON + novelWriter HTML")
|
||||
|
||||
elif theFmt == self.FMT_JSON_M:
|
||||
fileExt = "json"
|
||||
textFmt = "JSON + novelWriter Markdown"
|
||||
textFmt = self.tr("JSON + novelWriter Markdown")
|
||||
|
||||
elif theFmt == self.FMT_PDF:
|
||||
fileExt = "pdf"
|
||||
textFmt = "PDF"
|
||||
textFmt = self.tr("PDF")
|
||||
|
||||
else:
|
||||
return False
|
||||
@@ -836,28 +848,21 @@ class GuiBuildNovel(QDialog):
|
||||
# Generate File Name
|
||||
# ==================
|
||||
|
||||
if fileExt:
|
||||
cleanName = makeFileNameSafe(self.theProject.projName)
|
||||
fileName = "%s.%s" % (cleanName, fileExt)
|
||||
saveDir = self.mainConf.lastPath
|
||||
if not os.path.isdir(saveDir):
|
||||
saveDir = os.path.expanduser("~")
|
||||
|
||||
cleanName = makeFileNameSafe(self.theProject.projName)
|
||||
fileName = "%s.%s" % (cleanName, fileExt)
|
||||
saveDir = self.mainConf.lastPath
|
||||
savePath = os.path.join(saveDir, fileName)
|
||||
if not os.path.isdir(saveDir):
|
||||
saveDir = self.mainConf.homePath
|
||||
|
||||
dlgOpt = QFileDialog.Options()
|
||||
dlgOpt |= QFileDialog.DontUseNativeDialog
|
||||
savePath, _ = QFileDialog.getSaveFileName(
|
||||
self, "Save Document As", savePath, options=dlgOpt
|
||||
)
|
||||
if not savePath:
|
||||
return False
|
||||
|
||||
self.mainConf.setLastPath(savePath)
|
||||
|
||||
else:
|
||||
savePath = os.path.join(saveDir, fileName)
|
||||
savePath, _ = QFileDialog.getSaveFileName(
|
||||
self, self.tr("Save Document As"), savePath
|
||||
)
|
||||
if not savePath:
|
||||
return False
|
||||
|
||||
self.mainConf.setLastPath(savePath)
|
||||
|
||||
# Build and Write
|
||||
# ===============
|
||||
|
||||
@@ -985,20 +990,24 @@ class GuiBuildNovel(QDialog):
|
||||
errMsg - str(e)
|
||||
|
||||
else:
|
||||
errMsg = "Unknown format"
|
||||
# If the if statements above and here match, it should not
|
||||
# be possible to reach this else statement.
|
||||
return False # pragma: no cover
|
||||
|
||||
# Report to User
|
||||
# ==============
|
||||
|
||||
# Report to user
|
||||
if wSuccess:
|
||||
self.theParent.makeAlert(
|
||||
"%s file successfully written to:<br> %s" % (
|
||||
textFmt, savePath
|
||||
), nwAlert.INFO
|
||||
"%s<br>%s" % (
|
||||
self.tr("{0} file successfully written to:").format(textFmt), savePath
|
||||
),
|
||||
nwAlert.INFO
|
||||
)
|
||||
else:
|
||||
self.theParent.makeAlert(
|
||||
"Failed to write %s file. %s" % (
|
||||
textFmt, errMsg
|
||||
), nwAlert.ERROR
|
||||
self.tr("Failed to write {0} file. {1}").format(textFmt, errMsg),
|
||||
nwAlert.ERROR
|
||||
)
|
||||
|
||||
return wSuccess
|
||||
@@ -1119,6 +1128,7 @@ class GuiBuildNovel(QDialog):
|
||||
"section" : self.fmtSection.text().strip(),
|
||||
})
|
||||
|
||||
buildLang = self.buildLang.currentData()
|
||||
winWidth = self.mainConf.rpxInt(self.width())
|
||||
winHeight = self.mainConf.rpxInt(self.height())
|
||||
justifyText = self.justifyText.isChecked()
|
||||
@@ -1140,6 +1150,8 @@ class GuiBuildNovel(QDialog):
|
||||
boxWidth = self.mainConf.rpxInt(mainSplit[0])
|
||||
docWidth = self.mainConf.rpxInt(mainSplit[1])
|
||||
|
||||
self.theProject.setProjectLang(buildLang)
|
||||
|
||||
# GUI Settings
|
||||
self.optState.setValue("GuiBuildNovel", "winWidth", winWidth)
|
||||
self.optState.setValue("GuiBuildNovel", "winHeight", winHeight)
|
||||
@@ -1193,11 +1205,11 @@ class GuiBuildNovelDocView(QTextBrowser):
|
||||
|
||||
self.qDocument = self.document()
|
||||
self.qDocument.setDocumentMargin(self.mainConf.getTextMargin())
|
||||
self.setPlaceholderText(
|
||||
self.setPlaceholderText(self.tr(
|
||||
"This area will show the content of the document to be "
|
||||
"exported or printed. Press the \"Build Preview\" button "
|
||||
"to generate content."
|
||||
)
|
||||
))
|
||||
|
||||
theFont = QFont()
|
||||
if self.mainConf.textFont is None:
|
||||
@@ -1227,7 +1239,7 @@ class GuiBuildNovelDocView(QTextBrowser):
|
||||
|
||||
fPx = int(1.1*self.theTheme.fontPixelSize)
|
||||
|
||||
self.theTitle = QLabel("<b>Build Time:</b> Unknown", self)
|
||||
self.theTitle = QLabel("", self)
|
||||
self.theTitle.setIndent(0)
|
||||
self.theTitle.setAutoFillBackground(True)
|
||||
self.theTitle.setAlignment(Qt.AlignCenter)
|
||||
@@ -1236,6 +1248,7 @@ class GuiBuildNovelDocView(QTextBrowser):
|
||||
self.theTitle.setFont(lblFont)
|
||||
|
||||
self._updateDocMargins()
|
||||
self._updateBuildAge()
|
||||
self.setStyleSheet()
|
||||
|
||||
# Age Timer
|
||||
@@ -1341,8 +1354,11 @@ class GuiBuildNovelDocView(QTextBrowser):
|
||||
fuzzyTime(time() - self.buildTime)
|
||||
)
|
||||
else:
|
||||
strBuildTime = "Unknown"
|
||||
self.theTitle.setText("<b>Build Time:</b> %s" % strBuildTime)
|
||||
strBuildTime = self.tr("Unknown")
|
||||
|
||||
self.theTitle.setText(self.tr("<b>Build Time:</b> {0}").format(strBuildTime))
|
||||
|
||||
return
|
||||
|
||||
def _updateDocMargins(self):
|
||||
"""Automatically adjust the header to fill the top of the
|
||||
@@ -35,8 +35,9 @@ from PyQt5.QtWidgets import (
|
||||
QGroupBox, QGridLayout, QSpinBox
|
||||
)
|
||||
|
||||
from nw.enum import nwItemClass
|
||||
from nw.common import makeFileNameSafe
|
||||
from nw.constants import nwLabels, nwItemClass
|
||||
from nw.constants import trConst, nwLabels
|
||||
from nw.gui.custom import QSwitch
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -94,16 +95,18 @@ class ProjWizardIntroPage(QWizardPage):
|
||||
self.theWizard = theWizard
|
||||
self.theTheme = theWizard.theTheme
|
||||
|
||||
self.setTitle("Create New Project")
|
||||
self.theText = QLabel(
|
||||
self.setTitle(self.tr("Create New Project"))
|
||||
self.theText = QLabel(self.tr(
|
||||
"Provide at least a working title. The working title should not "
|
||||
"be change beyond this point as it is used by the application for "
|
||||
"generating file names for for instance backups. The other fields "
|
||||
"are optional and can be changed at any time in Project Settings."
|
||||
)
|
||||
))
|
||||
self.theText.setWordWrap(True)
|
||||
|
||||
self.imgCredit = QLabel("Side image by Peter Mitterhofer, CC BY-SA 4.0")
|
||||
self.imgCredit = QLabel(self.tr("Side image by {0}, {1}").format(
|
||||
"Peter Mitterhofer", "CC BY-SA 4.0"
|
||||
))
|
||||
lblFont = self.imgCredit.font()
|
||||
lblFont.setPointSizeF(0.6*self.theTheme.fontPointSize)
|
||||
self.imgCredit.setFont(lblFont)
|
||||
@@ -117,22 +120,22 @@ class ProjWizardIntroPage(QWizardPage):
|
||||
self.projName = QLineEdit()
|
||||
self.projName.setMaxLength(200)
|
||||
self.projName.setFixedWidth(xW)
|
||||
self.projName.setPlaceholderText("Required")
|
||||
self.projName.setPlaceholderText(self.tr("Required"))
|
||||
|
||||
self.projTitle = QLineEdit()
|
||||
self.projTitle.setMaxLength(200)
|
||||
self.projTitle.setFixedWidth(xW)
|
||||
self.projTitle.setPlaceholderText("Optional")
|
||||
self.projTitle.setPlaceholderText(self.tr("Optional"))
|
||||
|
||||
self.projAuthors = QPlainTextEdit()
|
||||
self.projAuthors.setFixedHeight(xH)
|
||||
self.projAuthors.setFixedWidth(xW)
|
||||
self.projAuthors.setPlaceholderText("Optional. One name per line.")
|
||||
self.projAuthors.setPlaceholderText(self.tr("Optional. One name per line."))
|
||||
|
||||
self.mainForm = QFormLayout()
|
||||
self.mainForm.addRow("Working Title", self.projName)
|
||||
self.mainForm.addRow("Novel Title", self.projTitle)
|
||||
self.mainForm.addRow("Author(s)", self.projAuthors)
|
||||
self.mainForm.addRow(self.tr("Working Title"), self.projName)
|
||||
self.mainForm.addRow(self.tr("Novel Title"), self.projTitle)
|
||||
self.mainForm.addRow(self.tr("Author(s)"), self.projAuthors)
|
||||
self.mainForm.setVerticalSpacing(fS)
|
||||
|
||||
self.registerField("projName*", self.projName)
|
||||
@@ -161,11 +164,11 @@ class ProjWizardFolderPage(QWizardPage):
|
||||
self.theWizard = theWizard
|
||||
self.theTheme = theWizard.theTheme
|
||||
|
||||
self.setTitle("Select Project Folder")
|
||||
self.theText = QLabel(
|
||||
self.setTitle(self.tr("Select Project Folder"))
|
||||
self.theText = QLabel(self.tr(
|
||||
"Select a location to store the project. A new project folder "
|
||||
"will be created in the selected location."
|
||||
)
|
||||
))
|
||||
self.theText.setWordWrap(True)
|
||||
|
||||
xW = self.mainConf.pxInt(300)
|
||||
@@ -174,14 +177,14 @@ class ProjWizardFolderPage(QWizardPage):
|
||||
|
||||
self.projPath = QLineEdit("")
|
||||
self.projPath.setFixedWidth(xW)
|
||||
self.projPath.setPlaceholderText("Required")
|
||||
self.projPath.setPlaceholderText(self.tr("Required"))
|
||||
|
||||
self.browseButton = QPushButton("...")
|
||||
self.browseButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("...")))
|
||||
self.browseButton.clicked.connect(self._doBrowse)
|
||||
|
||||
self.mainForm = QHBoxLayout()
|
||||
self.mainForm.addWidget(QLabel("Project Path"), 0)
|
||||
self.mainForm.addWidget(QLabel(self.tr("Project Path")), 0)
|
||||
self.mainForm.addWidget(self.projPath, 1)
|
||||
self.mainForm.addWidget(self.browseButton, 0)
|
||||
self.mainForm.setSpacing(fS)
|
||||
@@ -209,11 +212,8 @@ class ProjWizardFolderPage(QWizardPage):
|
||||
if not os.path.isdir(lastPath):
|
||||
lastPath = ""
|
||||
|
||||
dlgOpt = QFileDialog.Options()
|
||||
dlgOpt |= QFileDialog.ShowDirsOnly
|
||||
dlgOpt |= QFileDialog.DontUseNativeDialog
|
||||
projDir = QFileDialog.getExistingDirectory(
|
||||
self, "Select Project Folder", lastPath, options=dlgOpt
|
||||
self, self.tr("Select Project Folder"), lastPath, options=QFileDialog.ShowDirsOnly
|
||||
)
|
||||
if projDir:
|
||||
projName = self.field("projName")
|
||||
@@ -235,20 +235,20 @@ class ProjWizardPopulatePage(QWizardPage):
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theWizard = theWizard
|
||||
|
||||
self.setTitle("Populate Project")
|
||||
self.theText = QLabel(
|
||||
self.setTitle(self.tr("Populate Project"))
|
||||
self.theText = QLabel(self.tr(
|
||||
"Choose how to pre-fill the project. Either with a minimal set of "
|
||||
"starter items, an example project explaining and showing many of "
|
||||
"the features, or show further custom options on the next page."
|
||||
)
|
||||
))
|
||||
self.theText.setWordWrap(True)
|
||||
|
||||
vS = self.mainConf.pxInt(12)
|
||||
fS = self.mainConf.pxInt(4)
|
||||
|
||||
self.popMinimal = QRadioButton("Fill the project with a minimal set of items")
|
||||
self.popSample = QRadioButton("Fill the project with example files")
|
||||
self.popCustom = QRadioButton("Show detailed options for filling the project")
|
||||
self.popMinimal = QRadioButton(self.tr("Fill the project with a minimal set of items"))
|
||||
self.popSample = QRadioButton(self.tr("Fill the project with example files"))
|
||||
self.popCustom = QRadioButton(self.tr("Show detailed options for filling the project"))
|
||||
self.popMinimal.setChecked(True)
|
||||
|
||||
self.popBox = QVBoxLayout()
|
||||
@@ -290,27 +290,39 @@ class ProjWizardCustomPage(QWizardPage):
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theWizard = theWizard
|
||||
|
||||
self.setTitle("Custom Project Options")
|
||||
self.theText = QLabel(
|
||||
self.setTitle(self.tr("Custom Project Options"))
|
||||
self.theText = QLabel(self.tr(
|
||||
"Select which additional root folders to make, and how to populate "
|
||||
"the Novel folder. If you don't want to add chapters or scenes, set "
|
||||
"the values to 0. You can add scenes without chapters."
|
||||
)
|
||||
))
|
||||
self.theText.setWordWrap(True)
|
||||
|
||||
vS = self.mainConf.pxInt(12)
|
||||
|
||||
# Root Folders
|
||||
self.rootGroup = QGroupBox("Additional Root Folders")
|
||||
self.rootGroup = QGroupBox(self.tr("Additional Root Folders"))
|
||||
self.rootForm = QGridLayout()
|
||||
self.rootGroup.setLayout(self.rootForm)
|
||||
|
||||
self.lblPlot = QLabel("%s folder" % nwLabels.CLASS_NAME[nwItemClass.PLOT])
|
||||
self.lblChar = QLabel("%s folder" % nwLabels.CLASS_NAME[nwItemClass.CHARACTER])
|
||||
self.lblWorld = QLabel("%s folder" % nwLabels.CLASS_NAME[nwItemClass.WORLD])
|
||||
self.lblTime = QLabel("%s folder" % nwLabels.CLASS_NAME[nwItemClass.TIMELINE])
|
||||
self.lblObject = QLabel("%s folder" % nwLabels.CLASS_NAME[nwItemClass.OBJECT])
|
||||
self.lblEntity = QLabel("%s folder" % nwLabels.CLASS_NAME[nwItemClass.ENTITY])
|
||||
self.lblPlot = QLabel(self.tr("{0} folder").format(
|
||||
trConst(nwLabels.CLASS_NAME[nwItemClass.PLOT]))
|
||||
)
|
||||
self.lblChar = QLabel(self.tr("{0} folder").format(
|
||||
trConst(nwLabels.CLASS_NAME[nwItemClass.CHARACTER]))
|
||||
)
|
||||
self.lblWorld = QLabel(self.tr("{0} folder").format(
|
||||
trConst(nwLabels.CLASS_NAME[nwItemClass.WORLD]))
|
||||
)
|
||||
self.lblTime = QLabel(self.tr("{0} folder").format(
|
||||
trConst(nwLabels.CLASS_NAME[nwItemClass.TIMELINE]))
|
||||
)
|
||||
self.lblObject = QLabel(self.tr("{0} folder").format(
|
||||
trConst(nwLabels.CLASS_NAME[nwItemClass.OBJECT]))
|
||||
)
|
||||
self.lblEntity = QLabel(self.tr("{0} folder").format(
|
||||
trConst(nwLabels.CLASS_NAME[nwItemClass.ENTITY]))
|
||||
)
|
||||
|
||||
self.addPlot = QSwitch()
|
||||
self.addChar = QSwitch()
|
||||
@@ -338,7 +350,7 @@ class ProjWizardCustomPage(QWizardPage):
|
||||
self.rootForm.setRowStretch(6, 1)
|
||||
|
||||
# Novel Options
|
||||
self.novelGroup = QGroupBox("Populate Novel Folder")
|
||||
self.novelGroup = QGroupBox(self.tr("Populate Novel Folder"))
|
||||
self.novelForm = QGridLayout()
|
||||
self.novelGroup.setLayout(self.novelForm)
|
||||
|
||||
@@ -353,9 +365,9 @@ class ProjWizardCustomPage(QWizardPage):
|
||||
self.chFolders = QSwitch()
|
||||
self.chFolders.setChecked(True)
|
||||
|
||||
self.novelForm.addWidget(QLabel("Add chapters"), 0, 0)
|
||||
self.novelForm.addWidget(QLabel("Scenes (per chapter)"), 1, 0)
|
||||
self.novelForm.addWidget(QLabel("Add chapter folders"), 2, 0)
|
||||
self.novelForm.addWidget(QLabel(self.tr("Add chapters")), 0, 0)
|
||||
self.novelForm.addWidget(QLabel(self.tr("Scenes (per chapter)")), 1, 0)
|
||||
self.novelForm.addWidget(QLabel(self.tr("Add chapter folders")), 2, 0)
|
||||
self.novelForm.addWidget(self.numChapters, 0, 1, 1, 1, Qt.AlignRight)
|
||||
self.novelForm.addWidget(self.numScenes, 1, 1, 1, 1, Qt.AlignRight)
|
||||
self.novelForm.addWidget(self.chFolders, 2, 1, 1, 1, Qt.AlignRight)
|
||||
@@ -396,13 +408,15 @@ class ProjWizardFinalPage(QWizardPage):
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theWizard = theWizard
|
||||
|
||||
self.setTitle("Finished")
|
||||
self.theText = QLabel((
|
||||
"<p>All done.</p>"
|
||||
"<p>Press '{finish}' to create the new project.</p>"
|
||||
).format(
|
||||
finish = "Done" if self.mainConf.osDarwin else "Finish"
|
||||
))
|
||||
self.setTitle(self.tr("Finished"))
|
||||
self.theText = QLabel(
|
||||
"<p>%s</p><p>%s</p>" % (
|
||||
self.tr("All done."),
|
||||
self.tr("Press '{0}' to create the new project.").format(
|
||||
self.tr("Done") if self.mainConf.osDarwin else self.tr("Finish")
|
||||
)
|
||||
)
|
||||
)
|
||||
self.theText.setWordWrap(True)
|
||||
|
||||
# Assemble
|
||||
@@ -38,8 +38,9 @@ from PyQt5.QtWidgets import (
|
||||
QLabel, QGroupBox, QMenu, QAction, QFileDialog, QSpinBox, QHBoxLayout
|
||||
)
|
||||
|
||||
from nw.enum import nwAlert
|
||||
from nw.common import formatTime, checkInt
|
||||
from nw.constants import nwConst, nwFiles, nwAlert
|
||||
from nw.constants import nwConst, nwFiles
|
||||
from nw.gui.custom import QSwitch
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -72,7 +73,7 @@ class GuiWritingStats(QDialog):
|
||||
self.timeFilter = 0.0
|
||||
self.wordOffset = 0
|
||||
|
||||
self.setWindowTitle("Writing Statistics")
|
||||
self.setWindowTitle(self.tr("Writing Statistics"))
|
||||
self.setMinimumWidth(self.mainConf.pxInt(420))
|
||||
self.setMinimumHeight(self.mainConf.pxInt(400))
|
||||
self.resize(
|
||||
@@ -95,7 +96,13 @@ class GuiWritingStats(QDialog):
|
||||
)
|
||||
|
||||
self.listBox = QTreeWidget()
|
||||
self.listBox.setHeaderLabels(["Session Start", "Length", "Idle", "Words", "Histogram"])
|
||||
self.listBox.setHeaderLabels([
|
||||
self.tr("Session Start"),
|
||||
self.tr("Length"),
|
||||
self.tr("Idle"),
|
||||
self.tr("Words"),
|
||||
self.tr("Histogram"),
|
||||
])
|
||||
self.listBox.setIndentation(0)
|
||||
self.listBox.setColumnWidth(self.C_TIME, wCol0)
|
||||
self.listBox.setColumnWidth(self.C_LENGTH, wCol1)
|
||||
@@ -125,7 +132,7 @@ class GuiWritingStats(QDialog):
|
||||
self.barImage.fill(self.palette().highlight().color())
|
||||
|
||||
# Session Info
|
||||
self.infoBox = QGroupBox("Sum Totals", self)
|
||||
self.infoBox = QGroupBox(self.tr("Sum Totals"), self)
|
||||
self.infoForm = QGridLayout(self)
|
||||
self.infoBox.setLayout(self.infoForm)
|
||||
|
||||
@@ -153,24 +160,33 @@ class GuiWritingStats(QDialog):
|
||||
self.totalWords.setFont(self.theTheme.guiFontFixed)
|
||||
self.totalWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
|
||||
|
||||
self.infoForm.addWidget(QLabel("Total Time:"), 0, 0)
|
||||
self.infoForm.addWidget(QLabel("Idle Time:"), 1, 0)
|
||||
self.infoForm.addWidget(QLabel("Filtered Time:"), 2, 0)
|
||||
self.infoForm.addWidget(QLabel("Novel Word Count:"), 3, 0)
|
||||
self.infoForm.addWidget(QLabel("Notes Word Count:"), 4, 0)
|
||||
self.infoForm.addWidget(QLabel("Total Word Count:"), 5, 0)
|
||||
lblTTime = QLabel(self.tr("Total Time:"))
|
||||
lblITime = QLabel(self.tr("Idle Time:"))
|
||||
lblFTime = QLabel(self.tr("Filtered Time:"))
|
||||
lblNvCount = QLabel(self.tr("Novel Word Count:"))
|
||||
lblNtCount = QLabel(self.tr("Notes Word Count:"))
|
||||
lblTtCount = QLabel(self.tr("Total Word Count:"))
|
||||
|
||||
self.infoForm.addWidget(lblTTime, 0, 0)
|
||||
self.infoForm.addWidget(lblITime, 1, 0)
|
||||
self.infoForm.addWidget(lblFTime, 2, 0)
|
||||
self.infoForm.addWidget(lblNvCount, 3, 0)
|
||||
self.infoForm.addWidget(lblNtCount, 4, 0)
|
||||
self.infoForm.addWidget(lblTtCount, 5, 0)
|
||||
|
||||
self.infoForm.addWidget(self.labelTotal, 0, 1)
|
||||
self.infoForm.addWidget(self.labelIdleT, 1, 1)
|
||||
self.infoForm.addWidget(self.labelFilter, 2, 1)
|
||||
self.infoForm.addWidget(self.novelWords, 3, 1)
|
||||
self.infoForm.addWidget(self.notesWords, 4, 1)
|
||||
self.infoForm.addWidget(self.totalWords, 5, 1)
|
||||
|
||||
self.infoForm.setRowStretch(6, 1)
|
||||
|
||||
# Filter Options
|
||||
sPx = self.theTheme.baseIconSize
|
||||
|
||||
self.filterBox = QGroupBox("Filters", self)
|
||||
self.filterBox = QGroupBox(self.tr("Filters"), self)
|
||||
self.filterForm = QGridLayout(self)
|
||||
self.filterBox.setLayout(self.filterForm)
|
||||
|
||||
@@ -210,12 +226,12 @@ class GuiWritingStats(QDialog):
|
||||
)
|
||||
self.showIdleTime.clicked.connect(self._updateListBox)
|
||||
|
||||
self.filterForm.addWidget(QLabel("Count novel files"), 0, 0)
|
||||
self.filterForm.addWidget(QLabel("Count note files"), 1, 0)
|
||||
self.filterForm.addWidget(QLabel("Hide zero word count"), 2, 0)
|
||||
self.filterForm.addWidget(QLabel("Hide negative word count"), 3, 0)
|
||||
self.filterForm.addWidget(QLabel("Group entries by day"), 4, 0)
|
||||
self.filterForm.addWidget(QLabel("Show idle time"), 5, 0)
|
||||
self.filterForm.addWidget(QLabel(self.tr("Count novel files")), 0, 0)
|
||||
self.filterForm.addWidget(QLabel(self.tr("Count note files")), 1, 0)
|
||||
self.filterForm.addWidget(QLabel(self.tr("Hide zero word count")), 2, 0)
|
||||
self.filterForm.addWidget(QLabel(self.tr("Hide negative word count")), 3, 0)
|
||||
self.filterForm.addWidget(QLabel(self.tr("Group entries by day")), 4, 0)
|
||||
self.filterForm.addWidget(QLabel(self.tr("Show idle time")), 5, 0)
|
||||
self.filterForm.addWidget(self.incNovel, 0, 1)
|
||||
self.filterForm.addWidget(self.incNotes, 1, 1)
|
||||
self.filterForm.addWidget(self.hideZeros, 2, 1)
|
||||
@@ -236,7 +252,7 @@ class GuiWritingStats(QDialog):
|
||||
|
||||
self.optsBox = QHBoxLayout()
|
||||
self.optsBox.addStretch(1)
|
||||
self.optsBox.addWidget(QLabel("Word count cap for the histogram"), 0)
|
||||
self.optsBox.addWidget(QLabel(self.tr("Word count cap for the histogram")), 0)
|
||||
self.optsBox.addWidget(self.histMax, 0)
|
||||
|
||||
# Buttons
|
||||
@@ -246,17 +262,17 @@ class GuiWritingStats(QDialog):
|
||||
self.btnClose = self.buttonBox.addButton(QDialogButtonBox.Close)
|
||||
self.btnClose.setAutoDefault(False)
|
||||
|
||||
self.btnSave = self.buttonBox.addButton("Save As", QDialogButtonBox.ActionRole)
|
||||
self.btnSave = self.buttonBox.addButton(self.tr("Save As"), QDialogButtonBox.ActionRole)
|
||||
self.btnSave.setAutoDefault(False)
|
||||
|
||||
self.saveMenu = QMenu(self)
|
||||
self.btnSave.setMenu(self.saveMenu)
|
||||
|
||||
self.saveJSON = QAction("JSON Data File (.json)", self)
|
||||
self.saveJSON = QAction(self.tr("JSON Data File (.json)"), self)
|
||||
self.saveJSON.triggered.connect(lambda: self._saveData(self.FMT_JSON))
|
||||
self.saveMenu.addAction(self.saveJSON)
|
||||
|
||||
self.saveCSV = QAction("CSV Data File (.csv)", self)
|
||||
self.saveCSV = QAction(self.tr("CSV Data File (.csv)"), self)
|
||||
self.saveCSV.triggered.connect(lambda: self._saveData(self.FMT_CSV))
|
||||
self.saveMenu.addAction(self.saveCSV)
|
||||
|
||||
@@ -338,10 +354,10 @@ class GuiWritingStats(QDialog):
|
||||
|
||||
if dataFmt == self.FMT_JSON:
|
||||
fileExt = "json"
|
||||
textFmt = "JSON Data File"
|
||||
textFmt = self.tr("JSON Data File")
|
||||
elif dataFmt == self.FMT_CSV:
|
||||
fileExt = "csv"
|
||||
textFmt = "CSV Data File"
|
||||
textFmt = self.tr("CSV Data File")
|
||||
else:
|
||||
return False
|
||||
|
||||
@@ -353,10 +369,8 @@ class GuiWritingStats(QDialog):
|
||||
fileName = "sessionStats.%s" % fileExt
|
||||
savePath = os.path.join(saveDir, fileName)
|
||||
|
||||
dlgOpt = QFileDialog.Options()
|
||||
dlgOpt |= QFileDialog.DontUseNativeDialog
|
||||
savePath, _ = QFileDialog.getSaveFileName(
|
||||
self, "Save Document As", savePath, options=dlgOpt
|
||||
self, self.tr("Save Data As"), savePath, "%s (*.%s)" % (textFmt, fileExt)
|
||||
)
|
||||
if not savePath:
|
||||
return False
|
||||
@@ -474,7 +488,7 @@ class GuiWritingStats(QDialog):
|
||||
|
||||
except Exception as e:
|
||||
self.theParent.makeAlert(
|
||||
["Failed to read session log file.", str(e)], nwAlert.ERROR
|
||||
[self.tr("Failed to read session log file."), str(e)], nwAlert.ERROR
|
||||
)
|
||||
return False
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="1.2rc1" hexVersion="0x010200c1" fileVersion="1.2" timeStamp="2021-02-20 17:08:19">
|
||||
<novelWriterXML appVersion="1.3" hexVersion="0x010300f0" fileVersion="1.2" timeStamp="2021-05-02 16:48:34">
|
||||
<project>
|
||||
<name>Sample Project</name>
|
||||
<title>Sample Project</title>
|
||||
<author>Jane Smith</author>
|
||||
<author>Jay Doh</author>
|
||||
<saveCount>937</saveCount>
|
||||
<autoCount>161</autoCount>
|
||||
<editTime>46797</editTime>
|
||||
<saveCount>1060</saveCount>
|
||||
<autoCount>166</autoCount>
|
||||
<editTime>49679</editTime>
|
||||
</project>
|
||||
<settings>
|
||||
<doBackup>False</doBackup>
|
||||
<language>en</language>
|
||||
<spellCheck>True</spellCheck>
|
||||
<spellLang>None</spellLang>
|
||||
<autoOutline>True</autoOutline>
|
||||
@@ -26,7 +27,7 @@
|
||||
</autoReplace>
|
||||
<titleFormat>
|
||||
<title>%title%</title>
|
||||
<chapter>Chapter %ch%: %title%</chapter>
|
||||
<chapter>Chapter %chw%: %title%</chapter>
|
||||
<unnumbered>%title%</unnumbered>
|
||||
<scene>Scene %ch%.%sc%: %title%</scene>
|
||||
<section></section>
|
||||
@@ -120,7 +121,7 @@
|
||||
<charCount>1810</charCount>
|
||||
<wordCount>318</wordCount>
|
||||
<paraCount>8</paraCount>
|
||||
<cursorPos>3</cursorPos>
|
||||
<cursorPos>1505</cursorPos>
|
||||
</item>
|
||||
<item handle="bc0cbd2a407f3" order="2" parent="e7ded148d6e4a">
|
||||
<name>Another Scene</name>
|
||||
|
||||
@@ -5,7 +5,7 @@ author = Veronica Berglyd Olsen
|
||||
author_email = code@vkbo.net
|
||||
description = A markdown-like document editor for writing novels
|
||||
url = https://novelwriter.io
|
||||
long_description = file: README.md
|
||||
long_description = file: setup/long_description.md
|
||||
long_description_content_type = text/markdown
|
||||
license_file = LICENSE.md
|
||||
license = GNU General Public License v3
|
||||
@@ -25,7 +25,7 @@ classifiers =
|
||||
python_requires = >=3.6
|
||||
project_urls =
|
||||
Bug Tracker = https://github.com/vkbo/novelWriter/issues
|
||||
Documentation = https://github.com/vkbo/novelWriter/issues
|
||||
Documentation = https://novelwriter.readthedocs.io/en/stable
|
||||
Source Code = https://github.com/vkbo/novelWriter
|
||||
|
||||
[options]
|
||||
|
||||
430
setup.py
430
setup.py
@@ -35,6 +35,30 @@ OS_LINUX = 1
|
||||
OS_WIN = 2
|
||||
OS_DARWIN = 3
|
||||
|
||||
# =============================================================================================== #
|
||||
# Utilities
|
||||
# =============================================================================================== #
|
||||
|
||||
def extractVersion():
|
||||
"""Extract the novelWriter version number without having to import
|
||||
anything else from the main package.
|
||||
"""
|
||||
def getValue(theString):
|
||||
theBits = theString.partition("=")
|
||||
return theBits[2].strip().strip('"')
|
||||
|
||||
numVers = "Unknown"
|
||||
hexVers = "Unknown"
|
||||
initFile = os.path.join("nw", "__init__.py")
|
||||
with open(initFile, mode="r") as inFile:
|
||||
for aLine in inFile:
|
||||
if aLine.startswith("__version__"):
|
||||
numVers = getValue((aLine))
|
||||
if aLine.startswith("__hexversion__"):
|
||||
hexVers = getValue((aLine))
|
||||
|
||||
return numVers, hexVers
|
||||
|
||||
# =============================================================================================== #
|
||||
# General
|
||||
# =============================================================================================== #
|
||||
@@ -186,6 +210,65 @@ def buildQtDocs():
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Qt Linguist QM Builder (qtlrelease)
|
||||
##
|
||||
|
||||
def buildQtI18n():
|
||||
"""Build the lang.qm files for Qt Linguist.
|
||||
"""
|
||||
print("")
|
||||
print("Building Qt Localisation Files")
|
||||
print("==============================")
|
||||
print("")
|
||||
|
||||
try:
|
||||
subprocess.call(["lrelease", "-verbose", "novelWriter.pro"])
|
||||
except Exception as e:
|
||||
print("QtI18n Release Error:")
|
||||
print(str(e))
|
||||
|
||||
print("")
|
||||
print("Moving QM Files to Assets")
|
||||
print("")
|
||||
|
||||
langDir = os.path.join("nw", "assets", "i18n")
|
||||
for langFile in os.listdir("i18n"):
|
||||
langPath = os.path.join("i18n", langFile)
|
||||
if not os.path.isfile(langPath):
|
||||
continue
|
||||
|
||||
if langFile.endswith(".qm"):
|
||||
destPath = os.path.join(langDir, langFile)
|
||||
os.rename(langPath, destPath)
|
||||
print("Moved: %s -> %s" % (langPath, destPath))
|
||||
|
||||
print("")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Qt Linguist TS Builder (qtlupdate)
|
||||
##
|
||||
|
||||
def buildQtI18nTS():
|
||||
"""Build the lang.ts files for Qt Linguist.
|
||||
"""
|
||||
print("")
|
||||
print("Building Qt Translation Files")
|
||||
print("=============================")
|
||||
print("")
|
||||
|
||||
try:
|
||||
subprocess.call(["pylupdate5", "-verbose", "-noobsolete", "novelWriter.pro"])
|
||||
except Exception as e:
|
||||
print("QtI18n Release Error:")
|
||||
print(str(e))
|
||||
|
||||
print("")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Sample Project ZIP File Builder (sample)
|
||||
##
|
||||
@@ -237,9 +320,11 @@ def buildSampleZip():
|
||||
def makeMinimalPackage(targetOS):
|
||||
"""Pack the core source file in a single zip file.
|
||||
"""
|
||||
from nw import __version__
|
||||
from zipfile import ZipFile, ZIP_DEFLATED
|
||||
|
||||
# Get the version
|
||||
numVers, _ = extractVersion()
|
||||
|
||||
# Make sample.zip first
|
||||
try:
|
||||
buildSampleZip()
|
||||
@@ -248,24 +333,36 @@ def makeMinimalPackage(targetOS):
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
|
||||
# Make translation files
|
||||
try:
|
||||
buildQtI18n()
|
||||
except Exception as e:
|
||||
print("Failed with error:")
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
|
||||
print("")
|
||||
print("Building Minimal ZIP File")
|
||||
print("=========================")
|
||||
print("")
|
||||
|
||||
if not os.path.isdir("dist"):
|
||||
os.mkdir("dist")
|
||||
|
||||
if targetOS == OS_LINUX:
|
||||
targName = "-linux"
|
||||
print("Target OS: Linux")
|
||||
elif targetOS == OS_DARWIN:
|
||||
targName = "-darwin"
|
||||
print("Target OS: Darwin")
|
||||
elif targetOS == OS_WIN:
|
||||
targName = "-win"
|
||||
print("Target OS: Windows")
|
||||
else:
|
||||
targName = ""
|
||||
print("")
|
||||
|
||||
outFile = os.path.join("dist", f"novelWriter-{__version__}-minimal{targName}.zip")
|
||||
zipFile = f"novelWriter-{numVers}-minimal{targName}.zip"
|
||||
outFile = os.path.join("dist", zipFile)
|
||||
if os.path.isfile(outFile):
|
||||
os.unlink(outFile)
|
||||
|
||||
@@ -274,6 +371,7 @@ def makeMinimalPackage(targetOS):
|
||||
with ZipFile(outFile, "w", compression=ZIP_DEFLATED, compresslevel=9) as zipObj:
|
||||
|
||||
if targetOS != OS_WIN:
|
||||
# Not needed for Windows as the icons are in nw/assets/icons
|
||||
for nRoot, _, nFiles in os.walk("setup"):
|
||||
print("Adding Folder: %s [%d files]" % (nRoot, len(nFiles)))
|
||||
for aFile in nFiles:
|
||||
@@ -294,19 +392,30 @@ def makeMinimalPackage(targetOS):
|
||||
if targetOS == OS_WIN:
|
||||
zipObj.write("novelWriter.py", "novelWriter.pyw")
|
||||
print("Adding File: novelWriter.pyw")
|
||||
zipObj.write("setup_windows.bat")
|
||||
print("Adding File: setup_windows.bat")
|
||||
zipObj.write(os.path.join("setup", "windows_install.bat"), "windows_install.bat")
|
||||
print("Adding File: windows_install.bat")
|
||||
zipObj.write(os.path.join("setup", "windows_uninstall.bat"), "windows_uninstall.bat")
|
||||
print("Adding File: windows_uninstall.bat")
|
||||
else:
|
||||
zipObj.write("novelWriter.py")
|
||||
print("Adding File: novelWriter.py")
|
||||
|
||||
for aFile in rootFiles:
|
||||
assert os.path.isfile(aFile)
|
||||
print("Adding File: %s" % aFile)
|
||||
zipObj.write(aFile)
|
||||
|
||||
print("")
|
||||
print("Built file: %s" % outFile)
|
||||
print("Created File: %s" % outFile)
|
||||
|
||||
try:
|
||||
shaFile = open(outFile+".sha256", mode="w")
|
||||
subprocess.call(["sha256sum", zipFile], stdout=shaFile, cwd="dist")
|
||||
shaFile.close()
|
||||
print("SHA256 Sum: %s" % (outFile+".sha256"))
|
||||
except Exception as e:
|
||||
print("Could not generate sha256 file")
|
||||
print(str(e))
|
||||
|
||||
print("")
|
||||
|
||||
return
|
||||
@@ -566,14 +675,6 @@ def xdgInstall():
|
||||
else:
|
||||
print(f"Error {exCode}: Could not install menu desktop file")
|
||||
|
||||
exCode = subprocess.call(
|
||||
["xdg-desktop-icon", "install", "--novendor", "./novelwriter.desktop"]
|
||||
)
|
||||
if exCode == 0:
|
||||
print("Installed icon desktop file")
|
||||
else:
|
||||
print(f"Error {exCode}: Could not install icon desktop file")
|
||||
|
||||
print("")
|
||||
|
||||
# Install MimeType
|
||||
@@ -632,6 +733,10 @@ def xdgInstall():
|
||||
else:
|
||||
print(f"Error {exCode}: Could not update icon cache")
|
||||
|
||||
# Clean up
|
||||
if os.path.isfile("./novelwriter.desktop"):
|
||||
os.unlink("./novelwriter.desktop")
|
||||
|
||||
print("")
|
||||
print("Done!")
|
||||
print("")
|
||||
@@ -639,14 +744,71 @@ def xdgInstall():
|
||||
return
|
||||
|
||||
##
|
||||
# WIN Installation (win-install, launcher)
|
||||
# XDG Uninstallation (xdg-uninstall)
|
||||
##
|
||||
|
||||
def xdgUninstall():
|
||||
"""Will attempt to uninstall icons and make a launcher.
|
||||
"""
|
||||
print("")
|
||||
print("XDG Uninstall")
|
||||
print("=============")
|
||||
print("")
|
||||
|
||||
exCode = subprocess.call(
|
||||
["xdg-desktop-menu", "uninstall", "novelwriter.desktop"]
|
||||
)
|
||||
if exCode == 0:
|
||||
print("Uninstalled menu desktop file")
|
||||
else:
|
||||
print(f"Error {exCode}: Could not uninstall menu desktop file")
|
||||
|
||||
sizeArr = ["16", "22", "24", "32", "48", "64", "96", "128", "256", "512"]
|
||||
|
||||
# App Icon
|
||||
for aSize in sizeArr:
|
||||
exCode = subprocess.call([
|
||||
"xdg-icon-resource", "uninstall", "--noupdate",
|
||||
"--context", "apps", "--size", aSize, "novelwriter"
|
||||
])
|
||||
if exCode == 0:
|
||||
print(f"Uninstalled app icon size {aSize}")
|
||||
else:
|
||||
print(f"Error {exCode}: Could not uninstall app icon size {aSize}")
|
||||
|
||||
# Mimetype
|
||||
for aSize in sizeArr:
|
||||
exCode = subprocess.call([
|
||||
"xdg-icon-resource", "uninstall", "--noupdate",
|
||||
"--context", "mimetypes", "--size", aSize,
|
||||
"application-x-novelwriter-project"
|
||||
])
|
||||
if exCode == 0:
|
||||
print(f"Uninstalled mime icon size {aSize}")
|
||||
else:
|
||||
print(f"Error {exCode}: Could not uninstall mime icon size {aSize}")
|
||||
|
||||
# Update Cache
|
||||
exCode = subprocess.call(["xdg-icon-resource", "forceupdate"])
|
||||
if exCode == 0:
|
||||
print("Updated icon cache")
|
||||
else:
|
||||
print(f"Error {exCode}: Could not update icon cache")
|
||||
|
||||
print("")
|
||||
print("Done!")
|
||||
print("")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# WIN Installation (win-install)
|
||||
##
|
||||
|
||||
def winInstall():
|
||||
"""Will attempt to install icons and make a launcher for Windows.
|
||||
"""
|
||||
import winreg
|
||||
from nw import __version__, __hexversion__
|
||||
try:
|
||||
import win32com.client
|
||||
except ImportError:
|
||||
@@ -662,13 +824,14 @@ def winInstall():
|
||||
print("===============")
|
||||
print("")
|
||||
|
||||
nwTesting = not __hexversion__[-2] == "f"
|
||||
numVers, hexVers = extractVersion()
|
||||
nwTesting = not hexVers[-2] == "f"
|
||||
wShell = win32com.client.Dispatch("WScript.Shell")
|
||||
|
||||
if nwTesting:
|
||||
linkName = "novelWriter Testing %s.lnk" % __version__
|
||||
linkName = "novelWriter Testing %s.lnk" % numVers
|
||||
else:
|
||||
linkName = "novelWriter %s.lnk" % __version__
|
||||
linkName = "novelWriter %s.lnk" % numVers
|
||||
|
||||
desktopDir = wShell.SpecialFolders("Desktop")
|
||||
desktopIcon = os.path.join(desktopDir, linkName)
|
||||
@@ -684,6 +847,9 @@ def winInstall():
|
||||
targetPy = os.path.join(targetDir, "novelWriter.pyw")
|
||||
targetIcon = os.path.join(targetDir, "nw", "assets", "icons", "novelwriter.ico")
|
||||
|
||||
if not os.path.isfile(targetPy):
|
||||
shutil.copy2(os.path.join(targetDir, "novelWriter.py"), targetPy)
|
||||
|
||||
print("Collecting Info ...")
|
||||
print("Desktop Folder: %s" % desktopDir)
|
||||
print("Start Menu Folder: %s" % startMenuDir)
|
||||
@@ -759,6 +925,92 @@ def winInstall():
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# WIN Uninstallation (win-uninstall)
|
||||
##
|
||||
|
||||
def winUninstall():
|
||||
"""Will attempt to uninstall icons previously installed.
|
||||
"""
|
||||
import winreg
|
||||
try:
|
||||
import win32com.client
|
||||
except ImportError:
|
||||
print(
|
||||
"ERROR: Package 'pywin32' is missing on this system.\n"
|
||||
" Please run 'pip install --user pywin32' to install it."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
print("")
|
||||
print("Windows Uninstall")
|
||||
print("=================")
|
||||
print("")
|
||||
|
||||
numVers, hexVers = extractVersion()
|
||||
nwTesting = not hexVers[-2] == "f"
|
||||
wShell = win32com.client.Dispatch("WScript.Shell")
|
||||
|
||||
if nwTesting:
|
||||
linkName = "novelWriter Testing %s.lnk" % numVers
|
||||
else:
|
||||
linkName = "novelWriter %s.lnk" % numVers
|
||||
|
||||
desktopDir = wShell.SpecialFolders("Desktop")
|
||||
desktopIcon = os.path.join(desktopDir, linkName)
|
||||
|
||||
startMenuDir = wShell.SpecialFolders("StartMenu")
|
||||
startMenuProg = os.path.join(startMenuDir, "Programs", "novelWriter")
|
||||
startMenuIcon = os.path.join(startMenuProg, linkName)
|
||||
|
||||
print("Deleting Links ...")
|
||||
if os.path.isfile(desktopIcon):
|
||||
os.unlink(desktopIcon)
|
||||
print("Deleted: %s" % desktopIcon)
|
||||
else:
|
||||
print("Not Found: %s" % desktopIcon)
|
||||
|
||||
if os.path.isfile(startMenuIcon):
|
||||
os.unlink(startMenuIcon)
|
||||
print("Deleted: %s" % startMenuIcon)
|
||||
else:
|
||||
print("Not Found: %s" % startMenuIcon)
|
||||
|
||||
if os.path.isdir(startMenuProg):
|
||||
if not os.listdir(startMenuProg):
|
||||
os.rmdir(startMenuProg)
|
||||
print("Deleted: %s" % startMenuProg)
|
||||
else:
|
||||
print("Not Empty: %s" % startMenuProg)
|
||||
|
||||
print("")
|
||||
print("Removing registry keys ...")
|
||||
|
||||
theKeys = [
|
||||
r"Software\Classes\novelWriterProject.nwx\shell\open\command",
|
||||
r"Software\Classes\novelWriterProject.nwx\shell\open",
|
||||
r"Software\Classes\novelWriterProject.nwx\shell",
|
||||
r"Software\Classes\novelWriterProject.nwx\DefaultIcon",
|
||||
r"Software\Classes\novelWriterProject.nwx",
|
||||
r"Software\Classes\.nwx\OpenWithProgids",
|
||||
r"Software\Classes\.nwx",
|
||||
r"Software\Classes\Applications\novelWriter.pyw\SupportedTypes",
|
||||
r"Software\Classes\Applications\novelWriter.pyw",
|
||||
]
|
||||
|
||||
for aKey in theKeys:
|
||||
try:
|
||||
winreg.DeleteKey(winreg.HKEY_CURRENT_USER, aKey)
|
||||
print("Deleted: HKEY_CURRENT_USER\\%s" % aKey)
|
||||
except WindowsError:
|
||||
print("Not Found: HKEY_CURRENT_USER\\%s" % aKey)
|
||||
|
||||
print("")
|
||||
print("Done!")
|
||||
print("")
|
||||
|
||||
return
|
||||
|
||||
# =============================================================================================== #
|
||||
# Windows Installers
|
||||
# =============================================================================================== #
|
||||
@@ -780,8 +1032,8 @@ def innoSetup():
|
||||
with open(os.path.join("setup", "win_setup_pyz.iss"), mode="r") as inFile:
|
||||
issData = inFile.read()
|
||||
|
||||
import nw # noqa: E402
|
||||
issData = issData.replace(r"%%version%%", nw.__version__)
|
||||
numVers, _ = extractVersion()
|
||||
issData = issData.replace(r"%%version%%", numVers)
|
||||
issData = issData.replace(r"%%dir%%", os.getcwd())
|
||||
|
||||
with open("setup.iss", mode="w+") as outFile:
|
||||
@@ -828,47 +1080,61 @@ if __name__ == "__main__":
|
||||
else:
|
||||
targetOS = hostOS
|
||||
|
||||
helpMsg = (
|
||||
"\n"
|
||||
"novelWriter Setup Tool\n"
|
||||
"======================\n"
|
||||
"\n"
|
||||
"This tool provides setup and build commands for installing or distibuting novelWriter\n"
|
||||
"as a package on Linux, Mac and Windows. The available options are as follows:\n"
|
||||
"\n"
|
||||
"Some of the commands can be targeted towards a different OS than the host OS. To target\n"
|
||||
"the command, add one of '--target-linux', '--target-darwin' or '--target-win'.\n"
|
||||
"\n"
|
||||
"General:\n"
|
||||
"\n"
|
||||
" help Print the help message.\n"
|
||||
" pip Install all package dependencies for novelWriter using pip.\n"
|
||||
" clean Will attempt to delete the 'build' and 'dist' folders.\n"
|
||||
"\n"
|
||||
"Additional Builds:\n"
|
||||
"\n"
|
||||
" qthelp Build the help documentation for use with the Qt Assistant. Run before\n"
|
||||
" install to have local help enable in the the installed version.\n"
|
||||
" sample Build the sample project as a zip file. Run before install to enable\n"
|
||||
" creating sample projects in the in-app New Project Wizard.\n"
|
||||
"\n"
|
||||
"Python Packaging:\n"
|
||||
"\n"
|
||||
" minimal-zip Creates a minimal zip file of the core application without all the\n"
|
||||
" other source files. Accepts a target OS flag.\n"
|
||||
" pack-pyz Creates a pyz package in a folder with all dependencies using the\n"
|
||||
" zipapp tool. On Windows, python embeddable is added to the folder.\n"
|
||||
" setup-pyz Build a Windows installer from a zipapp package using Inno Setup.\n"
|
||||
"\n"
|
||||
"System Install:\n"
|
||||
"\n"
|
||||
" install Installs novelWriter to the system's Python install location. Run as \n"
|
||||
" root or with sudo for system-wide install, or as user for single user \n"
|
||||
" install.\n"
|
||||
" xdg-install Install launcher and icons for freedesktop systems. Run as root or \n"
|
||||
" with sudo for system-wide install, or as user for single user install.\n"
|
||||
" win-install Install desktop and start menu icons for Windows systems.\n"
|
||||
)
|
||||
helpMsg = [
|
||||
"",
|
||||
"novelWriter Setup Tool",
|
||||
"======================",
|
||||
"",
|
||||
"This tool provides setup and build commands for installing or distibuting",
|
||||
"novelWriter as a package on Linux, Mac and Windows. The available options",
|
||||
"are as follows:",
|
||||
"",
|
||||
"Some of the commands can be targeted towards a different OS than the host OS.",
|
||||
"To target the command, add one of '--target-linux', '--target-darwin' or",
|
||||
"'--target-win'.",
|
||||
"",
|
||||
"General:",
|
||||
"",
|
||||
" help Print the help message.",
|
||||
" pip Install all package dependencies for novelWriter using pip.",
|
||||
" clean Will attempt to delete the 'build' and 'dist' folders.",
|
||||
"",
|
||||
"Additional Builds:",
|
||||
"",
|
||||
" qthelp Build the help documentation for use with the Qt Assistant.",
|
||||
" qtlupdate Update the translation files for internationalisation.",
|
||||
" qtlrelease Build the language files for internationalisation.",
|
||||
" sample Build the sample project zip file and add it to assets.",
|
||||
"",
|
||||
"Python Packaging:",
|
||||
"",
|
||||
" minimal-zip Creates a minimal zip file of the core application without",
|
||||
" all the other source files. Defaults to tailor the zip file",
|
||||
" for the current OS, but accepts a target OS flag to build",
|
||||
" for another OS.",
|
||||
" pack-pyz Creates a .pyz package in a folder with all dependencies",
|
||||
" using the zipapp tool. On Windows, python embeddable is",
|
||||
" added to the folder.",
|
||||
" setup-pyz Build a Windows executable installer from a zipapp package",
|
||||
" using Inno Setup. Must run 'pack-pyz' first.",
|
||||
"",
|
||||
"System Install:",
|
||||
"",
|
||||
" install Installs novelWriter to the system's Python install",
|
||||
" location. Run as root or with sudo for system-wide install,",
|
||||
" or as user for single user install.",
|
||||
" xdg-install Install launcher and icons for freedesktop systems. Run as",
|
||||
" root or with sudo for system-wide install, or as user for",
|
||||
" single user install.",
|
||||
" xdg-uninstall Remove the launcher and icons for the current system as",
|
||||
" installed by the 'xdg-install' command.",
|
||||
" win-install Install desktop icon, start menu icon, and registry entries",
|
||||
" for file association with .nwx files for Windows systems.",
|
||||
" win-uninstall Remove desktop icon, start menu icon, and registry keys,",
|
||||
" for the current system. Note that it only removes icons for",
|
||||
" the version number of the package the command is run from.",
|
||||
"",
|
||||
]
|
||||
|
||||
# Flags and Variables
|
||||
makeSetupPyz = False
|
||||
@@ -880,7 +1146,14 @@ if __name__ == "__main__":
|
||||
|
||||
if "help" in sys.argv:
|
||||
sys.argv.remove("help")
|
||||
print(helpMsg)
|
||||
print("\n".join(helpMsg))
|
||||
sys.exit(0)
|
||||
|
||||
if "version" in sys.argv:
|
||||
sys.argv.remove("version")
|
||||
numVers, hexVers = extractVersion()
|
||||
print("Semantic Version: %s" % numVers)
|
||||
print("Hexadecimal Version: %s" % hexVers)
|
||||
sys.exit(0)
|
||||
|
||||
if "pip" in sys.argv:
|
||||
@@ -898,6 +1171,14 @@ if __name__ == "__main__":
|
||||
sys.argv.remove("qthelp")
|
||||
buildQtDocs()
|
||||
|
||||
if "qtlrelease" in sys.argv:
|
||||
sys.argv.remove("qtlrelease")
|
||||
buildQtI18n()
|
||||
|
||||
if "qtlupdate" in sys.argv:
|
||||
sys.argv.remove("qtlupdate")
|
||||
buildQtI18nTS()
|
||||
|
||||
if "sample" in sys.argv:
|
||||
sys.argv.remove("sample")
|
||||
buildSampleZip()
|
||||
@@ -918,11 +1199,6 @@ if __name__ == "__main__":
|
||||
# General Installers
|
||||
# ==================
|
||||
|
||||
if "launcher" in sys.argv:
|
||||
sys.argv.remove("launcher")
|
||||
print("The 'launcher' command has been replaced by 'xdg-install'.")
|
||||
sys.exit(1)
|
||||
|
||||
if "xdg-install" in sys.argv:
|
||||
sys.argv.remove("xdg-install")
|
||||
if hostOS == OS_WIN:
|
||||
@@ -931,6 +1207,14 @@ if __name__ == "__main__":
|
||||
else:
|
||||
xdgInstall()
|
||||
|
||||
if "xdg-uninstall" in sys.argv:
|
||||
sys.argv.remove("xdg-uninstall")
|
||||
if hostOS == OS_WIN:
|
||||
print("ERROR: Command 'xdg-uninstall' cannot be used on Windows")
|
||||
sys.exit(1)
|
||||
else:
|
||||
xdgUninstall()
|
||||
|
||||
if "win-install" in sys.argv:
|
||||
sys.argv.remove("win-install")
|
||||
if hostOS == OS_WIN:
|
||||
@@ -939,6 +1223,14 @@ if __name__ == "__main__":
|
||||
print("ERROR: Command 'win-install' can only be used on Windows")
|
||||
sys.exit(1)
|
||||
|
||||
if "win-uninstall" in sys.argv:
|
||||
sys.argv.remove("win-uninstall")
|
||||
if hostOS == OS_WIN:
|
||||
winUninstall()
|
||||
else:
|
||||
print("ERROR: Command 'win-uninstall' can only be used on Windows")
|
||||
sys.exit(1)
|
||||
|
||||
# Windows Setup Installer
|
||||
# =======================
|
||||
|
||||
|
||||
@@ -1,50 +1,28 @@
|
||||
# Build and Install novelWriter
|
||||
|
||||
The root folder of the repository contains two scripts for setup and install:
|
||||
The root folder of the repository contains a standard `setup.py` script. The `setup` folder
|
||||
contains necessary files for the setup process, like icons and other files, and it also contains
|
||||
additional scripts that are included in the minimal zip packages.
|
||||
|
||||
## Setup Script
|
||||
|
||||
## Script `setup.py`
|
||||
The `setup.py` script in the root folder has all the necessary commands for generating packages,
|
||||
installing icons, launcher, and file associations on Linux and Windows (no macOS support yet).
|
||||
|
||||
The `setup.py` is a standard Python setup script with a couple of additional
|
||||
options:
|
||||
In addition, it is a wrapper for `setuptools` and accept all commands associated with this tool.
|
||||
|
||||
Some of the commands can be targeted towards a different OS than the host OS.
|
||||
To target the command, add one of `--target-linux`, `--target-darwin` or
|
||||
`--target-win`.
|
||||
To list all custom package and install options in this script, run:
|
||||
```
|
||||
./setup.py help
|
||||
```
|
||||
## Windows Scripts
|
||||
|
||||
### General
|
||||
The `windows_install.bat` and `windows_uninstall.bat` are provided as convenience scripts for
|
||||
Windows users who aren't used to running commands from command line. They are both located in the
|
||||
`setup` folder in the source package, but in the minimal zip package for Windows, they can be
|
||||
found in the root folder.
|
||||
|
||||
`help` – Print the help message.
|
||||
|
||||
`pip` – Install all package dependencies for novelWriter using pip.
|
||||
|
||||
`clean` – Will attempt to delete the `build` and `dist` folders.
|
||||
|
||||
### Additional Builds
|
||||
|
||||
`qthelp` – Build the help documentation for use with the Qt Assistant. Run
|
||||
before install to have local help enable in the the installed version
|
||||
|
||||
`sample` – Build the sample project as a zip file. Run before install to enable
|
||||
creating sample projects in the in-app New Project Wizard.
|
||||
|
||||
### Python Packaging
|
||||
|
||||
`minimal-zip` – Creates a minimal zip file of the core application without all
|
||||
the other source files.
|
||||
|
||||
`pack-pyz` – Creates a pyz package in a folder with all dependencies using the
|
||||
zipapp tool. On Windows, python embeddable is added to the folder.
|
||||
|
||||
`setup-pyz` – Build a Windows installer from a zipapp package using Inno Setup.
|
||||
|
||||
### System Install
|
||||
|
||||
`install` – Installs novelWriter to the system's Python install location. Run
|
||||
as root or with sudo for system-wide install, or as user for single user
|
||||
install.
|
||||
|
||||
`xdg-install` – Install launcher and icons for freedesktop systems. Run as root
|
||||
or with sudo for system-wide install, or as user for single user install.
|
||||
|
||||
`win-install` – Install desktop and start menu icons for Windows systems.
|
||||
These scripts are designed to be double-clicked by the user, but can also be run from the command
|
||||
line tool. In either case, they will check if Python is properly installed, and then run the
|
||||
commands to install or uninstall the core dependencies of novelWriter, and then setup or remove the
|
||||
desktop icon, start menu icon and registry entries.
|
||||
|
||||
21
setup/long_description.md
Normal file
21
setup/long_description.md
Normal file
@@ -0,0 +1,21 @@
|
||||
<img src="https://raw.githubusercontent.com/vkbo/novelWriter/main/setup/novelwriter.png">
|
||||
|
||||
novelWriter is a plain text editor designed for writing novels assembled from many smaller text
|
||||
documents. It uses a minimal formatting syntax inspired by Markdown, and adds a meta data syntax
|
||||
for comments, synopsis, and cross-referencing. It's designed to be a simple text editor that allows
|
||||
for easy organisation of text and notes, using human readable text files as storage for robustness.
|
||||
|
||||
The project storage is suitable for version control software, and also well suited for file
|
||||
synchronisation tools. All text is saved as plain text files with a meta data header, the core
|
||||
project structure is stored in a single project XML file, and other meta data is primarily saved as
|
||||
JSON files.
|
||||
|
||||
The application is written in Python 3 (3.6+) using Qt5 and PyQt5 (5.3+). It is developed on Linux,
|
||||
but should in principle work fine on other operating systems as well as long as dependencies are
|
||||
met. It is regularly tested on Debian and Ubuntu Linux, Windows, and macOS.
|
||||
|
||||
novelWriter is developed and maintained by [Veronica Berglyd Olsen](https://github.com/vkbo).
|
||||
|
||||
**Website:** https://novelwriter.io
|
||||
**Documentation:** https://novelwriter.readthedocs.io
|
||||
**Source Code:** https://github.com/vkbo/novelWriter
|
||||
BIN
setup/novelwriter.png
Normal file
BIN
setup/novelwriter.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -6,10 +6,21 @@ echo Looking for Python
|
||||
python --version 2>NUL
|
||||
if errorlevel 1 goto errorNoPython
|
||||
echo Python found. OK.
|
||||
|
||||
if exist setup.py (
|
||||
echo setup.py found. OK.
|
||||
) else (
|
||||
cd ..
|
||||
if exist setup.py (
|
||||
echo setup.py found. OK.
|
||||
) else (
|
||||
goto errorNoSetup
|
||||
)
|
||||
)
|
||||
echo.
|
||||
|
||||
:: Install the PyQt5, lxml and pyenchant dependencies
|
||||
python setup.py pip
|
||||
pip install --user pywin32 -r requirements.txt
|
||||
|
||||
:: Create the desktop and start menu icons
|
||||
python setup.py win-install
|
||||
@@ -24,5 +35,13 @@ echo.
|
||||
echo Please download and install it from https://www.python.org/downloads/
|
||||
echo Also make sure the "Add Python to PATH" option is selected during installation.
|
||||
echo.
|
||||
goto:eof
|
||||
|
||||
:errorNoSetup
|
||||
echo.
|
||||
echo ERROR^: Could not find the setup.py script.
|
||||
echo.
|
||||
echo Make sure you run setup.py from the novelWriter root folder.
|
||||
echo.
|
||||
|
||||
pause
|
||||
57
setup/windows_uninstall.bat
Normal file
57
setup/windows_uninstall.bat
Normal file
@@ -0,0 +1,57 @@
|
||||
@echo off
|
||||
|
||||
:: Check for Python Installation
|
||||
echo.
|
||||
echo Looking for Python
|
||||
python --version 2>NUL
|
||||
if errorlevel 1 goto errorNoPython
|
||||
echo Python found. OK.
|
||||
|
||||
if exist setup.py (
|
||||
echo setup.py found. OK.
|
||||
) else (
|
||||
cd ..
|
||||
if exist setup.py (
|
||||
echo setup.py found. OK.
|
||||
) else (
|
||||
goto errorNoSetup
|
||||
)
|
||||
)
|
||||
echo.
|
||||
|
||||
echo Ready to remove the novelWriter icons, registry keys, and package dependencies.
|
||||
set /P c=Are you sure you want to continue [y/n]?
|
||||
if /I "%c%" EQU "y" goto doUninst
|
||||
goto afterUninst
|
||||
|
||||
:doUninst
|
||||
|
||||
:: Remove the desktop and start menu icons
|
||||
python setup.py win-uninstall
|
||||
|
||||
:: Remove the PyQt5, lxml and pyenchant dependencies
|
||||
pip uninstall --yes -r requirements.txt
|
||||
|
||||
:afterUninst
|
||||
|
||||
echo.
|
||||
pause
|
||||
goto:eof
|
||||
|
||||
:errorNoPython
|
||||
echo.
|
||||
echo ERROR^: Python is not installed on your system, or cannot be found.
|
||||
echo.
|
||||
echo Please download and install it from https://www.python.org/downloads/
|
||||
echo Also make sure the "Add Python to PATH" option is selected during installation.
|
||||
echo.
|
||||
goto:eof
|
||||
|
||||
:errorNoSetup
|
||||
echo.
|
||||
echo ERROR^: Could not find the setup.py script.
|
||||
echo.
|
||||
echo Make sure you run setup.py from the novelWriter root folder.
|
||||
echo.
|
||||
|
||||
pause
|
||||
@@ -52,45 +52,10 @@ pytest-3 -v -m core
|
||||
|
||||
Available markers are:
|
||||
|
||||
* `base` for unit tests covering the non-gui classes of the `bw` folder..
|
||||
* `base` for unit tests covering the non-gui classes of the `nw` folder..
|
||||
* `core` for unit tests covering the classes in the `nw/core` folder.
|
||||
* `gui` for unit and integrations tests covering the classes in the `nw/gui` folder.
|
||||
|
||||
## Tests
|
||||
|
||||
To filter specific groups of tests, use the `-k` switch.
|
||||
The commands for the respective test categories are listed below.
|
||||
|
||||
| Type | Test Target | Source File(s) | Marker | Filter |
|
||||
| :---------- | :----------------------- | :--------------------- | :-------- | :----------------------- |
|
||||
| Unit | Main function | nw/\_\_init\_\_.py | `-m base` | `-k testBaseInit` |
|
||||
| Unit | Common functions | nw/common.py | `-m base` | `-k testBaseCommon` |
|
||||
| Unit | Config class | nw/config.py | `-m base` | `-k testBaseConfig` |
|
||||
| Unit | Error handlers | nw/error.py | `-m base` | `-k testBaseError` |
|
||||
| Unit | Core functions | nw/core/tools.py | `-m core` | `-k testCoreTools` |
|
||||
| Unit | NWDoc class | nw/core/document.py | `-m core` | `-k testCoreDocument` |
|
||||
| Unit | NWIndex class | nw/core/index.py | `-m core` | `-k testCoreIndex` |
|
||||
| Unit | NWItem class | nw/core/item.py | `-m core` | `-k testCoreItem` |
|
||||
| Unit | NWProject class | nw/core/project.py | `-m core` | `-k testCoreProject` |
|
||||
| Unit | NWSpell* classes | nw/core/spellcheck.py | `-m core` | `-k testCoreSpell` |
|
||||
| Unit | NWStatus class | nw/core/status.py | `-m core` | `-k testCoreStatus` |
|
||||
| Unit | NWTree class | nw/core/tree.py | `-m core` | `-k testCoreTree` |
|
||||
| Unit | OptionsState class | nw/core/options.py | `-m core` | `-k testCoreOptions` |
|
||||
| Unit | ToHtml class | nw/core/tohtml.py | `-m core` | `-k testCoreToHtml` |
|
||||
| Unit | Tokenizer class | nw/core/tokenizer.py | `-m core` | `-k testCoreToken` |
|
||||
| Integration | About Dialogs | nw/gui/about.py | `-m gui` | `-k testGuiAbout` |
|
||||
| Integration | Build Novel Project Tool | nw/gui/build.py | `-m gui` | `-k testGuiBuild` |
|
||||
| Integration | Document Editor Widget | nw/gui/doceditor.py | `-m gui` | `-k testGuiEditor` |
|
||||
| Integration | Document Viewer Widget | nw/gui/docviewer.py | `-m gui` | `-k testGuiViewer` |
|
||||
| Integration | Item Editor Dialog | nw/gui/itemeditor.py | `-m gui` | `-k testGuiItemEditor` |
|
||||
| Integration | Menu Widgets | nw/gui/mainmenu.py | `-m gui` | `-k testGuiMenu` |
|
||||
| Integration | Merge Tool | nw/gui/docmerge.py | `-m gui` | `-k testGuiMergeSplit` |
|
||||
| Integration | Outline Widget | nw/gui/outline.py | `-m gui` | `-k testGuiOutline` |
|
||||
| Integration | Preferences Dialog | nw/gui/preferences.py | `-m gui` | `-k testGuiPreferences` |
|
||||
| Integration | Project Load Dialog | nw/gui/projload.py | `-m gui` | `-k testGuiProjLoad` |
|
||||
| Integration | Project Settings Dialog | nw/gui/projsettings.py | `-m gui` | `-k testGuiProjSettings` |
|
||||
| Integration | Project Tree Widget | nw/gui/projtree.py | `-m gui` | `-k testGuiProjTree` |
|
||||
| Integration | Theme/Icon Classes | nw/gui/theme.py | `-m gui` | `-k testGuiTheme` |
|
||||
| Integration | Split Tool | nw/gui/docsplit.py | `-m gui` | `-k testGuiMergeSplit` |
|
||||
| Integration | Writing Stats Dialog | nw/gui/writingstats.py | `-m gui` | `-k testGuiWritingStats` |
|
||||
| Integration | Various Dialogs | N/A | `-m gui` | `-k testGuiDialogs` |
|
||||
You can filter tests further with the `-k` switch, all the way down to a single test. You can for
|
||||
instance run only dialog tests with `-k testDlg` or tools with `-k testTool`.
|
||||
|
||||
@@ -61,6 +61,14 @@ def refDir():
|
||||
theDir = os.path.join(testDir, "reference")
|
||||
return theDir
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def filesDir():
|
||||
"""The folder where additional test files are stored.
|
||||
"""
|
||||
testDir = os.path.dirname(__file__)
|
||||
theDir = os.path.join(testDir, "files")
|
||||
return theDir
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def outDir(tmpDir):
|
||||
"""An output folder for test results
|
||||
@@ -110,20 +118,36 @@ def tmpConf(tmpDir):
|
||||
theConf = Config()
|
||||
theConf.initConfig(tmpDir, tmpDir)
|
||||
theConf.setLastPath("")
|
||||
theConf.guiLang = "en_GB"
|
||||
return theConf
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def dummyGUI(tmpConf):
|
||||
def fncConf(fncDir):
|
||||
"""Create a temporary novelWriter configuration object.
|
||||
"""
|
||||
confFile = os.path.join(fncDir, "novelwriter.conf")
|
||||
if os.path.isfile(confFile):
|
||||
os.unlink(confFile)
|
||||
theConf = Config()
|
||||
theConf.initConfig(fncDir, fncDir)
|
||||
theConf.setLastPath("")
|
||||
theConf.guiLang = "en_GB"
|
||||
return theConf
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def dummyGUI(monkeypatch, tmpConf):
|
||||
"""Create a dummy instance of novelWriter's main GUI class.
|
||||
"""
|
||||
monkeypatch.setattr("nw.CONFIG", tmpConf)
|
||||
theDummy = DummyMain()
|
||||
theDummy.mainConf = tmpConf
|
||||
return theDummy
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def nwGUI(qtbot, fncDir):
|
||||
def nwGUI(qtbot, monkeypatch, fncDir, fncConf):
|
||||
"""Create an instance of the novelWriter GUI.
|
||||
"""
|
||||
monkeypatch.setattr("nw.CONFIG", fncConf)
|
||||
nwGUI = nw.main(["--testmode", "--config=%s" % fncDir, "--data=%s" % fncDir])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
|
||||
@@ -91,6 +91,16 @@ class DummyStatusBar():
|
||||
|
||||
# END Class DummyStatusBar
|
||||
|
||||
class DummyApp:
|
||||
|
||||
def __init__(self):
|
||||
return
|
||||
|
||||
def installTranslator(self, theLang):
|
||||
return
|
||||
|
||||
# END Class DummyApp
|
||||
|
||||
# =========================================================================== #
|
||||
# Error Functions
|
||||
# Dummy functions that will raise errors instead.
|
||||
|
||||
1
tests/files/nw_en_GB.qm
Normal file
1
tests/files/nw_en_GB.qm
Normal file
@@ -0,0 +1 @@
|
||||
<<3C>d<18><EFBFBD>!<1C>`<60><><EFBFBD>
|
||||
@@ -10,6 +10,7 @@
|
||||
</project>
|
||||
<settings>
|
||||
<doBackup>False</doBackup>
|
||||
<language>en</language>
|
||||
<spellCheck>False</spellCheck>
|
||||
<spellLang>None</spellLang>
|
||||
<autoOutline>True</autoOutline>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
</project>
|
||||
<settings>
|
||||
<doBackup>True</doBackup>
|
||||
<language>en</language>
|
||||
<spellCheck>False</spellCheck>
|
||||
<spellLang>None</spellLang>
|
||||
<autoOutline>True</autoOutline>
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import pstats
|
||||
import os
|
||||
|
||||
profDir = os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir, "prof"))
|
||||
|
||||
print("")
|
||||
print("Profiles directory: %s" % profDir)
|
||||
print("")
|
||||
|
||||
profMainWindows = pstats.Stats(os.path.join(profDir, "testMainWindows.prof"))
|
||||
profMainWindows.sort_stats("cumtime")
|
||||
profMainWindows.print_stats("nw/")
|
||||
@@ -1,5 +1,5 @@
|
||||
[Main]
|
||||
timestamp = 2021-02-09 21:07:03
|
||||
timestamp = 2021-02-15 16:31:24
|
||||
theme = default
|
||||
syntax = default_light
|
||||
icons = typicons_colour_light
|
||||
@@ -7,9 +7,11 @@ guidark = False
|
||||
guifont =
|
||||
guifontsize = 11
|
||||
lastnotes = 0x0
|
||||
guilang = en-GB
|
||||
|
||||
[Sizes]
|
||||
geometry = 1200, 650
|
||||
preferences = 700, 615
|
||||
treecols = 200, 50, 30
|
||||
novelcols = 200, 50
|
||||
projcols = 200, 60, 140
|
||||
@@ -46,6 +48,9 @@ autoscroll = False
|
||||
autoscrollpos = 30
|
||||
fmtsinglequote = ‘, ’
|
||||
fmtdoublequote = “, ”
|
||||
fmtpadbefore =
|
||||
fmtpadafter =
|
||||
fmtpadthin = False
|
||||
spelltool = internal
|
||||
spellcheck = en
|
||||
showtabsnspaces = False
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
</project>
|
||||
<settings>
|
||||
<doBackup>True</doBackup>
|
||||
<language>None</language>
|
||||
<spellCheck>False</spellCheck>
|
||||
<spellLang>None</spellLang>
|
||||
<autoOutline>True</autoOutline>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
</project>
|
||||
<settings>
|
||||
<doBackup>True</doBackup>
|
||||
<language>None</language>
|
||||
<spellCheck>False</spellCheck>
|
||||
<spellLang>None</spellLang>
|
||||
<autoOutline>True</autoOutline>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
</project>
|
||||
<settings>
|
||||
<doBackup>True</doBackup>
|
||||
<language>None</language>
|
||||
<spellCheck>False</spellCheck>
|
||||
<spellLang>None</spellLang>
|
||||
<autoOutline>True</autoOutline>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
</project>
|
||||
<settings>
|
||||
<doBackup>True</doBackup>
|
||||
<language>None</language>
|
||||
<spellCheck>False</spellCheck>
|
||||
<spellLang>None</spellLang>
|
||||
<autoOutline>True</autoOutline>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
</project>
|
||||
<settings>
|
||||
<doBackup>True</doBackup>
|
||||
<language>None</language>
|
||||
<spellCheck>False</spellCheck>
|
||||
<spellLang>None</spellLang>
|
||||
<autoOutline>True</autoOutline>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user