Compare commits
409 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83e92994d1 | ||
|
|
33146f0a50 | ||
|
|
732450e163 | ||
|
|
df8e3dd82b | ||
|
|
75ddafd31d | ||
|
|
4f38a8d16c | ||
|
|
35746103c7 | ||
|
|
fb5347b8e4 | ||
|
|
6890b278ea | ||
|
|
9f667a0b35 | ||
|
|
36be7b0b33 | ||
|
|
1d16cc2a03 | ||
|
|
72fd2990e7 | ||
|
|
6d7ca2bb86 | ||
|
|
f10a8f9589 | ||
|
|
1881b3d32c | ||
|
|
1b89d0a4f0 | ||
|
|
671888b6ea | ||
|
|
50498a7462 | ||
|
|
ecd190987e | ||
|
|
2c000f6808 | ||
|
|
c472e40b2b | ||
|
|
0575f89ae9 | ||
|
|
e1dcfaa1e3 | ||
|
|
486af84107 | ||
|
|
8acbd8b978 | ||
|
|
7f9180e356 | ||
|
|
26912f226f | ||
|
|
0c286231cc | ||
|
|
991a61f2f6 | ||
|
|
edfd7c9770 | ||
|
|
ad62faaeea | ||
|
|
468903bbeb | ||
|
|
0723f44dc3 | ||
|
|
831bdabfc6 | ||
|
|
93a3eed530 | ||
|
|
307a52e7f4 | ||
|
|
3246f4bfcc | ||
|
|
fa4f4a8db4 | ||
|
|
cb797fe903 | ||
|
|
618461b1ff | ||
|
|
e2f08d3c76 | ||
|
|
f39cda729d | ||
|
|
b28d826d76 | ||
|
|
57ae559723 | ||
|
|
1d1be18689 | ||
|
|
141b5eb776 | ||
|
|
3f941bf9fd | ||
|
|
a48f302531 | ||
|
|
12d8872973 | ||
|
|
40ff865989 | ||
|
|
32e9c10702 | ||
|
|
804dd3bb7c | ||
|
|
9f2e3a66cd | ||
|
|
960d1967a3 | ||
|
|
ad025c3fe4 | ||
|
|
81212a108e | ||
|
|
8637a4eb12 | ||
|
|
0e4489d627 | ||
|
|
0bfb9d0700 | ||
|
|
efe759b94e | ||
|
|
9f89c1f7a4 | ||
|
|
530d92cc3f | ||
|
|
b0225515d2 | ||
|
|
d511c69ee7 | ||
|
|
6e1e40f13e | ||
|
|
70d3ee07bf | ||
|
|
e5477f314e | ||
|
|
80c4826253 | ||
|
|
e0de3ff191 | ||
|
|
239cff3dee | ||
|
|
6f9e3168b0 | ||
|
|
7d067f807d | ||
|
|
a7b43699ed | ||
|
|
7d11917d1c | ||
|
|
904ba9b1ca | ||
|
|
616fe35ed5 | ||
|
|
577a3c6c6a | ||
|
|
e937267eb0 | ||
|
|
2c1e1b94e7 | ||
|
|
60736ce04c | ||
|
|
03daa350f8 | ||
|
|
095cc38122 | ||
|
|
142d659b70 | ||
|
|
0826a2f8a3 | ||
|
|
a3819a7785 | ||
|
|
2314777b4b | ||
|
|
729e97a95d | ||
|
|
5ab60ca4a3 | ||
|
|
f1adff82a8 | ||
|
|
96b2d139c9 | ||
|
|
e1bc1222c4 | ||
|
|
0bff4a3d21 | ||
|
|
dfc775c925 | ||
|
|
dea304d1eb | ||
|
|
cfd80645e2 | ||
|
|
4b7f7887c8 | ||
|
|
9fc640aa59 | ||
|
|
d812aab173 | ||
|
|
39b58711c7 | ||
|
|
c6120fcf7c | ||
|
|
45b9215c87 | ||
|
|
394f6adf82 | ||
|
|
1a5076438d | ||
|
|
872b8e7902 | ||
|
|
0cad96fdca | ||
|
|
e9ef4fd091 | ||
|
|
4ce8b38acd | ||
|
|
24ab6f2d3b | ||
|
|
ba5e5f4bb5 | ||
|
|
6ef8611a0a | ||
|
|
486676baa5 | ||
|
|
542cbf13ff | ||
|
|
62630eb519 | ||
|
|
b1d597e63f | ||
|
|
887eee01fc | ||
|
|
f605f1fc75 | ||
|
|
cc8fc38734 | ||
|
|
5797b8655d | ||
|
|
fd4d663a1c | ||
|
|
88263d55f2 | ||
|
|
5c4398f948 | ||
|
|
9ff397dfbc | ||
|
|
1c9ebfd11c | ||
|
|
22e8d7ee71 | ||
|
|
35458a7e37 | ||
|
|
c1544ab374 | ||
|
|
d795eb6f5f | ||
|
|
ce8264b8f2 | ||
|
|
c6cbc8a8b1 | ||
|
|
6c79a1daba | ||
|
|
7867965989 | ||
|
|
348513a248 | ||
|
|
eb62253137 | ||
|
|
c4e87435dd | ||
|
|
9980d225f5 | ||
|
|
9c0f596c0d | ||
|
|
ff8cca6d8b | ||
|
|
0e5e263dea | ||
|
|
166732946e | ||
|
|
cccdab45dc | ||
|
|
190ca7cf2d | ||
|
|
009160b6bf | ||
|
|
851f67d86f | ||
|
|
b8be2277c4 | ||
|
|
14e266b938 | ||
|
|
94f070f096 | ||
|
|
0c28664ced | ||
|
|
754863e42c | ||
|
|
fb5e4a5f17 | ||
|
|
03e9fb93c7 | ||
|
|
3e77e24b1f | ||
|
|
d97809e4b0 | ||
|
|
551fd0aaf9 | ||
|
|
2fc36f8355 | ||
|
|
13cc2feb3f | ||
|
|
e2b62fc9f6 | ||
|
|
6b119cf4b8 | ||
|
|
88d198403e | ||
|
|
5ee39d7ecc | ||
|
|
e7102725bd | ||
|
|
bd28867804 | ||
|
|
0eb09a2136 | ||
|
|
470daf0036 | ||
|
|
fd28cbca4e | ||
|
|
6ecf0ecd88 | ||
|
|
c940d9d8a4 | ||
|
|
efef172658 | ||
|
|
e865b2c054 | ||
|
|
f8243f6934 | ||
|
|
746268c3ef | ||
|
|
48695651e1 | ||
|
|
45f726669e | ||
|
|
d473615ccf | ||
|
|
70425c77ea | ||
|
|
8520409459 | ||
|
|
1e48c6dcfe | ||
|
|
67a81400ff | ||
|
|
51431169dd | ||
|
|
2a0183c838 | ||
|
|
e222915a37 | ||
|
|
b698ccbc1d | ||
|
|
03a80beebe | ||
|
|
1c072e9f32 | ||
|
|
01576f941f | ||
|
|
4af80286d5 | ||
|
|
c9785bc57b | ||
|
|
81e72826ce | ||
|
|
71f9f3d230 | ||
|
|
500a01b46d | ||
|
|
8190c189e5 | ||
|
|
dea2ac22e1 | ||
|
|
1cae46a936 | ||
|
|
ac22838ded | ||
|
|
93875d667c | ||
|
|
41043e4dc1 | ||
|
|
2e8425d4b1 | ||
|
|
fcac28c69f | ||
|
|
a14b319ada | ||
|
|
6763ebdd66 | ||
|
|
421f2dd688 | ||
|
|
19b21a3e8a | ||
|
|
06dcaff806 | ||
|
|
f02bba5630 | ||
|
|
12fa295506 | ||
|
|
1054bbdf8b | ||
|
|
b47436ec59 | ||
|
|
856534cb81 | ||
|
|
3ee07c7da7 | ||
|
|
96fb892465 | ||
|
|
3a48a124e2 | ||
|
|
b391d47a40 | ||
|
|
a3c4d7c7b6 | ||
|
|
4ee5890d90 | ||
|
|
2ce1134b8b | ||
|
|
641ff441b5 | ||
|
|
7687259381 | ||
|
|
bd888451ec | ||
|
|
7484542af2 | ||
|
|
1e64a3f94c | ||
|
|
fd7319b295 | ||
|
|
923e00ad19 | ||
|
|
518be4e630 | ||
|
|
06a13557e3 | ||
|
|
72aa82c8ed | ||
|
|
b8a136e0ad | ||
|
|
38ba214327 | ||
|
|
c23ec06ce6 | ||
|
|
c495098582 | ||
|
|
499d095f81 | ||
|
|
2ed9367dc8 | ||
|
|
820e6fcfa3 | ||
|
|
f92ef05ee2 | ||
|
|
4b3795671d | ||
|
|
bff5ff79bd | ||
|
|
21056c5cb8 | ||
|
|
a4567939d6 | ||
|
|
e92ac2b30d | ||
|
|
90871d5859 | ||
|
|
cb299d145b | ||
|
|
6afc34e47e | ||
|
|
f6bcbbd962 | ||
|
|
55f1129a24 | ||
|
|
a2c48492ee | ||
|
|
cb0591fe5f | ||
|
|
2f269c3e17 | ||
|
|
d1a15f07d5 | ||
|
|
3cb0c95278 | ||
|
|
74910a6b56 | ||
|
|
5ab32326c1 | ||
|
|
7dc184f05f | ||
|
|
dbdb3931f4 | ||
|
|
56a5559024 | ||
|
|
8798afc41c | ||
|
|
e67c911c97 | ||
|
|
88d69f6fd4 | ||
|
|
99555f7dd0 | ||
|
|
b3ec77c631 | ||
|
|
d4c61e7ec9 | ||
|
|
48bbe288b7 | ||
|
|
3664266eed | ||
|
|
a1f7ab1562 | ||
|
|
5f95c745e2 | ||
|
|
1f735d8739 | ||
|
|
551fa28b27 | ||
|
|
909114e318 | ||
|
|
a4e55a0664 | ||
|
|
86e196808e | ||
|
|
0a4e15de31 | ||
|
|
cd1f52c6b5 | ||
|
|
b63449bffd | ||
|
|
93965138da | ||
|
|
fae1bd791f | ||
|
|
35855d2d71 | ||
|
|
c374534d9e | ||
|
|
f6c92dab24 | ||
|
|
03beae30ca | ||
|
|
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 |
24
.github/pull_request_template.md
vendored
Normal file
24
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
<!--
|
||||
Please check the following before you make a pull request:
|
||||
|
||||
* The branch you are making the pull request from (in your own fork) has a unique and descriptive
|
||||
name. Do not make a pull request directly from your copy of `main`.
|
||||
* If you are submitting translation files, only submit the files that you have added translations
|
||||
to, not the other .ts files that may have been updated in the process.
|
||||
* Make sure your contribution follows the style guide and other requirements mentioned in the
|
||||
CONTRIBUTING.md file in the repository.
|
||||
* Fill in the Summary section below, and if relevant, mention the issue numbers related to the PR.
|
||||
-->
|
||||
|
||||
**Summary:**
|
||||
|
||||
**Related Issue(s):**
|
||||
|
||||
**Reviewer's Checklist:**
|
||||
|
||||
* [ ] The header of all files contain a reference to the repository license
|
||||
* [ ] The overall test coverage is increased or remains the same as before
|
||||
* [ ] All tests are passing
|
||||
* [ ] All flake8 checks are passing and the style guide is followed
|
||||
* [ ] Documentation (as docstrings) is complete and understandable
|
||||
* [ ] Only files that have been actively changed are committed
|
||||
10
.github/workflows/syntax.yml
vendored
10
.github/workflows/syntax.yml
vendored
@@ -2,9 +2,11 @@ name: flake8
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, testing, dev ]
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches: [ main, testing, dev ]
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
checkSyntax:
|
||||
@@ -25,5 +27,5 @@ jobs:
|
||||
flake8 tests --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
- name: Coding Style Violations
|
||||
run: |
|
||||
flake8 nw --count --max-line-length=99 --ignore E203,E221,E226,E228,E241,E251,E261,E266,E302,E305 --show-source --statistics
|
||||
flake8 tests --count --max-line-length=99 --ignore E203,E221,E226,E228,E241,E251,E261,E266,E302,E305 --show-source --statistics
|
||||
flake8 nw --count --max-line-length=99 --ignore E221,E226,E228,E241 --show-source --statistics
|
||||
flake8 tests --count --max-line-length=99 --ignore E221,E226,E228,E241 --show-source --statistics
|
||||
|
||||
@@ -1,34 +1,38 @@
|
||||
name: Linux (3.8)
|
||||
name: Linux
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, testing, dev ]
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches: [ main, testing, dev ]
|
||||
branches:
|
||||
- main
|
||||
|
||||
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
|
||||
- name: Install Packages (apt)
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install libenchant-dev qt5-default
|
||||
sudo apt install libenchant-dev qttools5-dev-tools
|
||||
- name: Checkout Source
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Dependencies
|
||||
- name: Install Dependencies (pip)
|
||||
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
|
||||
pip install -r requirements-dev.txt
|
||||
- name: Run Build Commands
|
||||
run: python setup.py qtlrelease sample
|
||||
- name: Run Tests
|
||||
run: |
|
||||
export QT_QPA_PLATFORM=offscreen
|
||||
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
|
||||
15
.github/workflows/test_mac.yml
vendored
15
.github/workflows/test_mac.yml
vendored
@@ -1,13 +1,15 @@
|
||||
name: macOS (3.9)
|
||||
name: macOS
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, testing, dev ]
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches: [ main, testing, dev ]
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
testMac38:
|
||||
testMac:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Python Setup
|
||||
@@ -24,11 +26,8 @@ jobs:
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install -r requirements-dev.txt
|
||||
pip install pyobjc
|
||||
pip install pytest-timeout
|
||||
pip install pytest-cov
|
||||
pip install pytest-qt
|
||||
pip install codecov
|
||||
- name: Run Tests
|
||||
run: |
|
||||
export QT_QPA_PLATFORM=offscreen
|
||||
|
||||
15
.github/workflows/test_win.yml
vendored
15
.github/workflows/test_win.yml
vendored
@@ -1,13 +1,15 @@
|
||||
name: Windows (3.9)
|
||||
name: Windows
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, testing, dev ]
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches: [ main, testing, dev ]
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
testWin38:
|
||||
testWin:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Python Setup
|
||||
@@ -21,10 +23,7 @@ jobs:
|
||||
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
|
||||
pip install -r requirements-dev.txt
|
||||
- name: Run Tests
|
||||
run: |
|
||||
pytest -v --cov=nw --timeout=60
|
||||
|
||||
7
.gitignore
vendored
7
.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
|
||||
|
||||
@@ -23,6 +28,7 @@ __pycache__
|
||||
/nw/assets/sample.zip
|
||||
/sample/cache
|
||||
/sample/meta
|
||||
/sample/versions
|
||||
*.bak
|
||||
*.lock
|
||||
ToC.txt
|
||||
@@ -41,3 +47,4 @@ ToC.txt
|
||||
|
||||
# Coverage
|
||||
/.coverage
|
||||
/coverage.*
|
||||
|
||||
461
CHANGELOG.md
461
CHANGELOG.md
@@ -1,5 +1,466 @@
|
||||
# novelWriter Changelog
|
||||
|
||||
## Version 1.4 [2021-07-27]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This release contains some new features and a lot of code refactoring. Among the main new features
|
||||
is paragraph alignment and indentation. Regular line breaks within paragraphs are now also
|
||||
supported. The changes are described in more detail below.
|
||||
|
||||
#### Internationalisation
|
||||
|
||||
Translation files have been added for Simplified Chinese by Qianzhi Long.
|
||||
|
||||
#### Line Breaks
|
||||
|
||||
The way line breaks inside paragraphs work has been changed. A single line break is now treated as
|
||||
a proper line break and will show up in the document viewer and exported documents. A single line
|
||||
break does not start a new paragraph, but forces a break inside the paragraph like a Shift + Enter
|
||||
does in most rich text editors. Two line breaks is still needed to start a new paragraph.
|
||||
|
||||
The old syntax of adding two spaces at the end of a line to force a line break within a paragraph
|
||||
will still work as before, so there is no need to change your existing text if you've used this
|
||||
feature. However, there is a new highlighting feature that will show you where in the text you have
|
||||
redundant spaces. If you are used to having double spaces between sentences, you may want to switch
|
||||
off this highlighting feature in Preferences as it will also detect those.
|
||||
|
||||
A helper function has been added to the Format menu that can look through a paragraph and remove
|
||||
line breaks in case you've been using line breaks inside your existing text under the assumption
|
||||
that the exporter and viewer will ignore them.
|
||||
|
||||
I hope this change will not be too inconvenient. I believe the new behaviour will make more sense
|
||||
for most people. Especially considering some of the feedback I've gotten on how line breaks work.
|
||||
The original implementation was following the Markdown standard, but since novelWriter is not a
|
||||
proper Markdown editor and instead just borrows from Markdown, this behaviour always seemed a bit
|
||||
unnecessary.
|
||||
|
||||
#### Text Alignment and Indentation
|
||||
|
||||
The default text alignment is left or justified based on your preferences. For documents with the
|
||||
layout set to Title Page or Partitions, the default is centred. However, sometimes you may want to
|
||||
override this default. A new set of codes have therefore been added to allow specifying alignment
|
||||
as well as additional text margins on individual paragraphs.
|
||||
|
||||
The logic of the syntax is as follows:
|
||||
|
||||
A single angle bracket will push the text away from the edge it points away from. Therefore, a
|
||||
single `>` before the paragraph, or a single `<` after the paragraph, will add indentation on the
|
||||
respective side. It's perfectly valid to do this on both sides at the same time.
|
||||
|
||||
A double set of angle brackets will push the text all the way towards the opposite side. Therefore,
|
||||
a double set of `>>` before the paragraph will indicate right alignment, and a double set of `<<`
|
||||
after the paragraph will force left alignment. Also here both can be used at the same time, which
|
||||
results in the paragraph being centred.
|
||||
|
||||
Format menu entries and keyboard shortcuts have been added so that you don't have to memorise these
|
||||
codes.
|
||||
|
||||
_These Release Notes also include the changes from 1.4 Beta 1 and RC 1._
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* The way margins are determined in the paged dialogs used many places, including Preferences, has
|
||||
been improved. These margins would sometimes be set to zero when they shouldn't. PR #834.
|
||||
* Dialogs that are non-modal are no longer duplicated when opened multiple times. Instead, the
|
||||
existing dialog is moved to the front. PR #834.
|
||||
|
||||
**Internationalisation**
|
||||
|
||||
* US English and Norwegian translations have been updated by @vkbo. PR #825.
|
||||
* French translations have been updated by @jyhelle. PR #829.
|
||||
|
||||
----
|
||||
|
||||
## Version 1.4 RC 1 [2021-07-12]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This is a preview release of novelWriter 1.4. It contains some new features and a lot of code
|
||||
refactoring. This release is a testing release, and may contain bugs. Please be careful when using
|
||||
this version to work on your projects.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Internationalisation**
|
||||
|
||||
* Translation files have been added for Simplified Chinese. The translation has been provided by
|
||||
Qianzhi Long (@longqzh). PR #817.
|
||||
|
||||
**Code Improvements**
|
||||
|
||||
* Improve PEP8 compliance for the source code. PRs #816 and #820.
|
||||
* The way messages are written to the logging tool has been improved. PR #818.
|
||||
* Some improvements to parsing and encoding of HTML has been added. PR #821.
|
||||
* Test coverage of the document editor has been improved. PR #787.
|
||||
|
||||
----
|
||||
|
||||
## Version 1.4 Beta 1 [2021-06-13]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This is a preview release of novelWriter 1.4. It contains some new features and a lot of code
|
||||
refactoring. This release is a testing release, and may contain bugs. Please be careful when using
|
||||
this version to work on your projects.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* A number of calls for the pop-up alert box were missing translation wrappers. That means they
|
||||
could not be translated into other languages. The alerts have been fixed, but the PR does not add
|
||||
the missing translations. PR #806.
|
||||
* A duplicate error message from the index class has been removed. PR #758.
|
||||
|
||||
**Features**
|
||||
|
||||
* Single line breaks are now treated as proper line breaks within paragraphs. Paragraphs are still
|
||||
separated by two line breaks like before. This means that it is no longer necessary to leave two
|
||||
spaces at the end of the line to force a line break. This is a rather obscure and little known
|
||||
feature taken from Markdown, and it isn't very intuitive. Issue #785. PR #786.
|
||||
* It is now possible to specify text alignment and additional indentation on individual paragraphs
|
||||
in the text. Both features use a similar syntax that I hope is fairly intuitive. Menu entries and
|
||||
keyboard shortcuts have also been added to make it easier to use these features. Issues #595 and
|
||||
#803. PR #804.
|
||||
|
||||
**Code Improvements**
|
||||
|
||||
* Class initialisation has been made consistent. All GUI classes now inherit the main window as its
|
||||
parent class, and all other classes inherit the main project class as its parent. Since the
|
||||
project class and the main window class have pointers to each other, all needed pointers are
|
||||
available from their respective classes. PR #758.
|
||||
* The document class has been changed from a reusable class to a class that is intended to wrap a
|
||||
single document via its handle. The class was originally written for the document editor where
|
||||
the reusable approach made more sense. But it is much simple to create and destroy them in other
|
||||
parts of the code when they are not reusable. PRs #758 and #760.
|
||||
* The document class no longer generates any pop-up alerts. Errors are recorded and retrieved and
|
||||
displayed by the parent or caller class. PR #758.
|
||||
* Refactored the code of the editor class to make it more isolated by making most class variables
|
||||
private. PR #779.
|
||||
* Made similar changes to the viewer class and item details class. PR #780.
|
||||
|
||||
----
|
||||
|
||||
## Version 1.3.3 [2021-06-13]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This patch release fixes a potential file encoding issue when running setup on Windows, and a minor
|
||||
issue with the project word count not being updated immediately when a file is deleted. In
|
||||
addition, the keyboard shortcuts to change focus between the project tree, the editor, the viewer,
|
||||
and the outline panel, have been changed for Windows users. They keyboard shortcuts were
|
||||
interfering with the Alt codes used for special characters. The shortcuts are unchanged for Linux
|
||||
and macOS.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* Fix an issue with file encoding when extracting version information from the source code during
|
||||
setup on Windows. This seems to be a limited issue, but the changes make the relevant function
|
||||
more fault tolerant. Issue #805. PR #807.
|
||||
* The project word count on the status bar was not always updated when a file was permanently
|
||||
deleted from the project. This has now been resolved. Issue #799. PR #810.
|
||||
* The keyboard shortcuts to change focus will on Windows interfere with the alt key codes as the
|
||||
focus shortcuts used `Alt+` to `Alt+4`. On Windows, these are now instead `Ctrl+Alt+1` to
|
||||
`Ctrl+Alt+4`. Part of issue #740. PR #808.
|
||||
|
||||
**Source Code**
|
||||
|
||||
* Remove a redundant line in the source code. PR #802.
|
||||
* Make the XML parse for project items a little less panicky when encountering unexpected XML tags.
|
||||
Generally, this shouldn't be a problem, but the XML parser should silently ignore unexpected tags
|
||||
when parsing the project file. This may occur if a project is opened in an earlier version of
|
||||
novelWriter. If so, a warning is issued anyway, so it is safe to disregard unrecognised tags as
|
||||
the user has already actively selected to proceed and been sufficiently warned. PR #809.
|
||||
|
||||
----
|
||||
|
||||
## Version 1.3.2 [2021-05-30]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This is a patch release that fixes some minor issues. One issue was with the split tool, which
|
||||
would drop the last line from the source document during a split if it was missing a final line
|
||||
break. A minor issue with the display of word counts on the details panel under the project tree
|
||||
has also been fixed. In addition, the setup script commands for Linux have been improved a bit.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* The details panel under the tree would sometimes show character, word and paragraph counts even
|
||||
when there was no document selected. This has now been fixed. Issue #781. PR #782.
|
||||
* The split document tool would drop the last line of the source document. This is generally not a
|
||||
problem if the last line has a line break after it, but if it doesn't, it is lost in the split.
|
||||
This was caused by an offset error when calculating the split positions in the file and has been
|
||||
resolved. Issue #795. PR #796.
|
||||
|
||||
**Installation**
|
||||
|
||||
* Due to an error in the setup script in the past, a desktop icon was created for novelWriter when
|
||||
running the `xdg-install` command. This is no longer the case, but the old icon was still left on
|
||||
some user's desktops. The setup script will now remove that icon if it exists when the
|
||||
`xdg-install` or `xdg-uninstall` commands are run. PR #784.
|
||||
|
||||
----
|
||||
|
||||
## Version 1.3.1 [2021-05-06]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This is a patch release primarily to fix a problem with the Qt translation library used to make
|
||||
novelWriter available in multiple languages. One of the function calls made to the library was
|
||||
added recently, in Qt 5.15, which many users on Linux will not have installed on their system. The
|
||||
issue could be resolved by updating the library, but it's an unnecessary and inconvenient
|
||||
restriction.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* The code to load translation files for the main GUI was using a function in the QTranslate class
|
||||
that was added in version 5.15, which means novelWriter could not start on a lower version of the
|
||||
Qt library if translation files were present. We don't want to force users to use Qt 5.15, so the
|
||||
call has been removed. Issue #773. PR #774.
|
||||
|
||||
**Other Fixes**
|
||||
|
||||
* When generating a new project on Windows, the code that generates the new documents in the
|
||||
project would create duplicate handles, causing a warning to be printed. The warning is harmless
|
||||
as the collisions are handled. They are caused by the resolution of the clock available to Python
|
||||
on Windows being in the tens of millisecond range, much slower than the code generates the new
|
||||
project files. The solution was to append a counter to the timestamp from the clock, ensuring
|
||||
that the seed is always unique. Issue #769. PR #776.
|
||||
|
||||
----
|
||||
|
||||
## 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
|
||||
|
||||
166
CONTRIBUTING.md
166
CONTRIBUTING.md
@@ -1,39 +1,48 @@
|
||||
# Contributing Guide
|
||||
|
||||
When contributing to this repository, please first discuss the change you wish to make via the
|
||||
issue tracker or the discussions page with the owner of this repository before making a change. If
|
||||
you just want to make a minor correction, like fix a typo or similar, feel free to just make a pull
|
||||
request directly.
|
||||
issue tracker or the discussions page with the owner of this repository. Especially if it is
|
||||
regarding new features. If you just want to make a minor correction, like fix a typo or similar,
|
||||
feel free to just make a pull request directly.
|
||||
|
||||
## Branching Structure
|
||||
The following contributions are welcome:
|
||||
|
||||
There are three protected branches on this repository. They are used in the following way:
|
||||
* Bugfixes for new or existing bugs. Please also report new bugs in the issue tracker even if you
|
||||
also provide a fix. It makes it easier to keep track of what has been fixed and when.
|
||||
* New translations. You can read more about contributing translations
|
||||
[here](https://github.com/vkbo/novelWriter/blob/main/i18n/README.md).
|
||||
* Improvements to the documentation. Particularly if the documentation is unclear. Please don't
|
||||
make any larger changes to the documentation without discussing if with the maintainer first.
|
||||
* Adaptations, installation or packaging features targeting specific operating systems.
|
||||
|
||||
* `main` – This is the Stable branch. It is used for releases and subsequent patches. No
|
||||
development code should be merged into this branch starting from version 1.0.
|
||||
* `dev` – This is the Unstable (development) branch. It is where new features are merged, and where
|
||||
pre-releases are taken from.
|
||||
* `testing` – This is the Testing branch. It is populated from the `dev` branch for pre-releases
|
||||
that need a longer testing phase.
|
||||
Please do not:
|
||||
|
||||
Stable releases and patches will be tagged in the `main` branch, pre-releases may be tagged from
|
||||
either `testing` or `dev` branch.
|
||||
* Make a pull request that restructures or reformats existing code. If you think some parts of the
|
||||
code could be improved, please make an issue thread or start a discussion. The same applies to
|
||||
any text document in the repository.
|
||||
|
||||
### What Branch to Use for Contributions
|
||||
## Pull Requests
|
||||
|
||||
* If your contribution is a fix for the latest stable release, branch from the `main` branch.
|
||||
* If your contribution is a fix for the latest testing release, branch from the `testing` branch.
|
||||
* If your contribution is a new feature, branch from the `dev` branch.
|
||||
This project follows the [OneFlow](https://www.endoflineblog.com/oneflow-a-git-branching-model-and-workflow)
|
||||
model. The `main` branch is the default branch. For general changes, please make a new branch in
|
||||
your own fork from the current `main` branch. Do not make pull requests from your own `main`
|
||||
branch.
|
||||
|
||||
## Pull Request Process
|
||||
**Note:** If your contribution is a bugfix for a specific version, please make a bugfix branch from
|
||||
the tag you want the fix to be applied to, not directly from the `main` branch. This is important.
|
||||
|
||||
1. Make sure your code passes all tests and conforms to the style guide. You can check that the
|
||||
code conforms by running `flake8` from the root of the project folder.
|
||||
2. Please provide a complete description of the changes in the pull request, and a summary that can
|
||||
be copied into the [CHANGELOG](CHANGELOG.md). Remember to reference any issue related by
|
||||
providing the issue number.
|
||||
3. Do not change the version number. Version numbers are bumped in separate release pull requests
|
||||
by the maintainer.
|
||||
Also check the following:
|
||||
|
||||
* Make sure your code passes all tests and conforms to the style guide. You can check that the
|
||||
code generally conforms by running the Python linting tool `flake8` from the root of the project
|
||||
folder, although it doesn't check everything. The same check is also run on pull requests by the
|
||||
maintainer.
|
||||
* Please provide a description of the changes in the pull request under the summary section of the
|
||||
pull request template, and reference any related issues by providing the issue number.
|
||||
* Do not change the version number.
|
||||
* Do not submit files that were not actively changed but have otherwise been modifed. This is
|
||||
mostly an issue with translation files. The language tool may update all files in the `i18n`
|
||||
folder.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
@@ -52,92 +61,63 @@ Please see the [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) file for the full text.
|
||||
## Code Style Guide
|
||||
|
||||
The source code of novelWriter broadly follows the [PEP8](https://www.python.org/dev/peps/pep-0008)
|
||||
style guide, but with a few modifications and exceptions listed below.
|
||||
style guide, but with a few exceptions. Some key points are listed below.
|
||||
|
||||
### Line Length
|
||||
**Line Length:**
|
||||
|
||||
For this project, source lines should stay within the 79 and 99 character limits described by PEP8.
|
||||
79 characters is often too restrictive, so 99 character lines are acceptable when that is more
|
||||
practical. Readability has priority. Generally, if a code statement requires multiple lines, the
|
||||
lines should wrap at 79 characters, not 99. If wrapping can be avoided by going to 99, then that is
|
||||
generally preferrable.
|
||||
* Source code lines can extend to the upper limit of 99 characters allowed by PEP8. 79 characters
|
||||
is the recommended line length in PEP8, but this is often too restrictive. 99 characters are
|
||||
acceptable when that is more practical. Readability has priority. Generally, if a code statement
|
||||
requires multiple lines, the lines should wrap at 79 characters if possible. If wrapping can be
|
||||
avoided by going to 99, then that is generally preferrable.
|
||||
* For text files, the text should be wrapped at 99 character. The exception is markdown image tags
|
||||
and urls which can run past that limit.
|
||||
|
||||
For text files, the text should also be wrapped at 99 character. The exception is markdown image
|
||||
tags and urls.
|
||||
**Variable and Function Names**
|
||||
|
||||
Please do not submit pull requests that re-wrap existing source or text unless this has been
|
||||
discussed beforehand.
|
||||
* PEP8 allows for camelCase for consistency with existing code. The Qt library uses camelCase, so
|
||||
the Python the source code does too.
|
||||
* The exception to the above is for constants. They should always be in upper snake case, like PEP8
|
||||
states.
|
||||
|
||||
**Spaces, Indentation and Alignment**
|
||||
|
||||
* Only indentation by 4 spaces is allowed.
|
||||
* No trailing spaces should occur on any line in the source code, including empty lines.
|
||||
* Ideally, a function should end on the same indention level as it started. Exceptions are allowed
|
||||
if it makes the code easier to follow.
|
||||
* Aligning operators and attributes in columns with multiple spaces is not allowed by PEP8. The
|
||||
rule is relaxed a bit here. Alignment is allowed when populating large dictionaries or setting
|
||||
many class attributes. It does improve readability in such cases, but should not be overused.
|
||||
|
||||
### Linting with `flake8`
|
||||
|
||||
An excellent tool for checking Python code for errors and coding style is `flake8`. The
|
||||
documentation is available [here](https://flake8.pycqa.org/en/latest/).
|
||||
A good tool for checking Python code for errors and code style is `flake8`. The documentation is
|
||||
available [here](https://flake8.pycqa.org/en/latest/).
|
||||
|
||||
The `setup.cfg` file in the root of this project has the following settings for `flake8` that
|
||||
matches the coding standard:
|
||||
matches the above code style:
|
||||
```conf
|
||||
[flake8]
|
||||
ignore = E203,E221,E226,E228,E241,E251,E261,E266,E302,E305
|
||||
ignore = E221,E226,E228,E241
|
||||
max-line-length = 99
|
||||
exclude = docs/*
|
||||
```
|
||||
|
||||
The command line equivalent, with reporting, is:
|
||||
```bash
|
||||
flake8 . --count --ignore E203,E221,E226,E228,E241,E251,E261,E266,E302,E305 --max-line-length=99 --show-source --statistics
|
||||
flake8 . --count --ignore E221,E226,E228,E241 --max-line-length=99 --show-source --statistics
|
||||
```
|
||||
|
||||
Passing this check is required before contributions are merged into the `main` branch. This is
|
||||
checked automatically when you make a pull request. You can run the `flake8` command locally to
|
||||
check beforehand. The full command will give you a detailed description of the code lines that do
|
||||
not conform to the standard.
|
||||
Passing this check is required before contributions are merged. This is checked automatically when
|
||||
you make a pull request. You can run the `flake8` command locally to check beforehand. The full
|
||||
command will give you a detailed description of the code lines that do not conform to the standard.
|
||||
|
||||
## Ignored Errors
|
||||
Two of the ignored errors are due to the relaxed restriction on column alignment, these are the
|
||||
E221 and E241 error codes.
|
||||
|
||||
Some `flake8` error codes are ignored for this project for various reasons. The source also uses
|
||||
camelCase function and variable names. This is the standard for the Qt libraries novelWriter
|
||||
integrates with. It also happens to be the author's personal preferences.
|
||||
|
||||
The reason behind the other ignored error codes are listed below. Many of them are due to PEP8 not
|
||||
permitting column alignment as opposed to many other coding styles, like for instance for Go. I
|
||||
find them useful in regions of bulk value assignments. There's a reason why tables are more
|
||||
readable than lists. They should be used sparingly though.
|
||||
|
||||
The ignored errors are all `pycodestyle` errors, and they are documented
|
||||
[here](https://pycodestyle.pycqa.org/en/latest/intro.html#error-codes).
|
||||
|
||||
**E203:** whitespace before ‘:’
|
||||
**Reason:** Column alignment.
|
||||
|
||||
**E221:** multiple spaces before operator
|
||||
**Reason:** Column alignment.
|
||||
|
||||
**E226:** missing whitespace around arithmetic operator
|
||||
**Reason:** This doesn't actually follow the PEP8 recommendation of grouping longer equations by
|
||||
operator precedence like `2*a + 3*b` instead of `a * a + 3 * b`. Generally, don't use spaces around
|
||||
`*`, `/` and `**`, but _do_ use spaces around `+` and `-`. For appending strings, the spaces can be
|
||||
dropped. Don't use the `+` operator for appending multiple strings. Use formatting instead.
|
||||
|
||||
**E228** missing whitespace around modulo operator
|
||||
**Reason:** See reason for E226. Formatting `%` like `/` and `*` should be possible.
|
||||
|
||||
**E241:** multiple spaces after ‘,’
|
||||
**Reason:** Column alignment.
|
||||
|
||||
**E251:** unexpected spaces around keyword / parameter equals
|
||||
**Reason:** Column alignment.
|
||||
|
||||
**E261:** at least two spaces before inline comment
|
||||
**Reason:** With syntax highlighting, this one doesn't make much sense.
|
||||
|
||||
**E266:** too many leading ‘#’ for block comment
|
||||
**Reason:** In the source multiple `#`s is sometimes used to indicate importance or heading level,
|
||||
like markdown headers.
|
||||
|
||||
**E302:** expected 2 blank lines, found 0
|
||||
**Reason:** Applies to classes. Instead, end classes with a comment like `# END Class ClassName` to
|
||||
make it easier to see which class just ended. The double line break is then redundant.
|
||||
|
||||
**E305:** expected 2 blank lines after end of function or class
|
||||
**Reason:** Instead, _always_ end a function with a `return`, preferably indented at function
|
||||
level. The end of the function is then clear.
|
||||
The code E226 is ignored becuse it doesn't actually follow the
|
||||
[PEP8 recommendation](https://www.python.org/dev/peps/pep-0008/#other-recommendations)
|
||||
of grouping longer equations by operator precedence like `2*a + 3*b` instead of `a * a + 3 * b`.
|
||||
Generally, don't use spaces around `*`, `/` and `**`, but _do_ use spaces around `+` and `-`.
|
||||
The same applies to the modulo operator `%`, hence E228 is also ignored.
|
||||
|
||||
42
CREDITS.md
Normal file
42
CREDITS.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Credits
|
||||
|
||||
## Main Developer
|
||||
|
||||
* Veronica Berglyd Olsen (@vkbo)
|
||||
|
||||
## Main Contributors
|
||||
|
||||
* Concept: Marian Lückhof (@Number042)
|
||||
* Internationalisation: Bruno Meneguello (@bkmeneguello)
|
||||
|
||||
## Translations
|
||||
|
||||
* French: Jan Lüdke (@jyhelle)
|
||||
* Norwegian: Veronica Berglyd Olsen (@vkbo)
|
||||
* Portuguese: Bruno Meneguello (@bkmeneguello)
|
||||
* Simplified Chinese: Qianzhi Long (@longqzh)
|
||||
|
||||
## Libraries
|
||||
|
||||
The following libraries are dependencies of novelWriter:
|
||||
|
||||
* Qt5 by [Qt Company](https://www.qt.io/)
|
||||
* PyQt5 by [Riverbank Computing](https://www.riverbankcomputing.com/software/pyqt/)
|
||||
* lxml by [Martijn Faassen](https://lxml.de/)
|
||||
* Enchant by [Dom Lachowicz](https://abiword.github.io/enchant/)
|
||||
* PyEnchant by [Dimitri Merejkowsky](https://pyenchant.github.io/pyenchant/)
|
||||
|
||||
## Assets
|
||||
|
||||
Some of the assets bundled with novelWriter were adapted from the following sources:
|
||||
|
||||
* Typicons icons by Stephen Hutchings (CC BY-SA 4.0)
|
||||
* Cantarell font by Dave Crossland (OPEN FONT LICENSE Version 1.1)
|
||||
* Tomorrow syntax themes by Chris Kempson (MIT License)
|
||||
* Owl syntax themes by Sarah Drasner (MIT License)
|
||||
* Solarized themes by Ethan Schoonover, added by @nullbasis (MIT License)
|
||||
|
||||
## Special Mentions
|
||||
|
||||
Additional thanks to @johnblommers who was an early user and who has provided a lot of very useful
|
||||
feedback over the years.
|
||||
251
README.md
251
README.md
@@ -1,87 +1,100 @@
|
||||
# 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. 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).
|
||||
|
||||
The full credits are listed in [CREDITS.md](https://github.com/vkbo/novelWriter/blob/main/CREDITS.md).
|
||||
|
||||
## 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.
|
||||
Please don't make feature pull requests without first having discussed them with the maintainer.
|
||||
You can make a feature request in the issue tracker, or if the idea isn't fully formed, start a
|
||||
discussion on the discussion page. Please also don't make pull requests to reformat or rewrite
|
||||
existing code unless there is a very good reason for doing so.
|
||||
|
||||
# Key Features
|
||||
Fixes and patches are welcome. Contributions related to packaging and installing novelWriter will
|
||||
also be appreciated, but please make an issue or a discussion topic first. Before contributing any
|
||||
code, please read the full [Contributing Guide](https://github.com/vkbo/novelWriter/blob/main/CONTRIBUTING.md).
|
||||
|
||||
Some features of novelWriter are listed below. Consult the
|
||||
New translations are always welcome. Please read the additional
|
||||
[instructions](https://github.com/vkbo/novelWriter/blob/main/i18n/README.md) for further details.
|
||||
|
||||
## Key Features
|
||||
|
||||
Some key features of novelWriter are listed below. Consult the
|
||||
[documentation](https://novelwriter.readthedocs.io) for more information.
|
||||
|
||||
### Markdown Flavour
|
||||
### Formatting Codes
|
||||
|
||||
novelWriter is _not_ a full-feature 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:
|
||||
Although novelWriter is a plain text editor, it uses a Markdown-like syntax to allow for a minimal
|
||||
set of formatting that is useful for the specific task of writing novels.
|
||||
|
||||
* Headings levels 1 to 4 using the `#` syntax only.
|
||||
* Emphasised and strongly emphasised text. These are rendered as italicised and bold text.
|
||||
* Strikethrough text.
|
||||
* Hard line breaks using two or more spaces at the end of a line.
|
||||
| Code | Usage | Description |
|
||||
|------------|----------|-------------|
|
||||
| `#` | Prefix | Headings level 1 to 4. |
|
||||
| `_` | Wrapped | Emphasised (italicised) text. |
|
||||
| `**` | Wrapped | Strongly emphasised (bold) text. |
|
||||
| `~~` | Wrapped | Strikethrough text. |
|
||||
| `%` | Prefix | A comment; does not count towards the word count.<sup>1</sup> |
|
||||
| `@` | Prefix | The following text is parsed as a keyword/value command for meta data. |
|
||||
| `>` | Prefix | The paragraph is indented one tab width from the left.<sup>2</sup> |
|
||||
| `<` | Suffix | The paragraph is indented one tab width from the right.<sup>2</sup> |
|
||||
| `>>` | Prefix | The paragraph is right-aligned.<sup>2</sup> |
|
||||
| `<<` | Suffix | The paragraph is left-aligned.<sup>2</sup> |
|
||||
| `>>`, `<<` | Wrapped | The paragraph is centred.<sup>2</sup> |
|
||||
|
||||
That is it. Features not supported in the editor are also not exported when using the export tool.
|
||||
<sup>1</sup> If the first word of the comment is `synopsis:`, the comment is indexed and treated as the
|
||||
synopsis for the section of text where it occurs. These synopsis comments can be used to build an
|
||||
outline and exported to external documents.
|
||||
|
||||
In addition, novelWriter adds the following, which is otherwise not supported by Markdown:
|
||||
<sup>2</sup> The indent and alignment codes are available from version 1.4.
|
||||
|
||||
In additions:
|
||||
|
||||
* A line starting with `%` is treated as a comment and not rendered on exports unless requested.
|
||||
Comments do not count towards the word count.
|
||||
* 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.
|
||||
* 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.
|
||||
|
||||
### Export Formats
|
||||
|
||||
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,146 +105,116 @@ 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
|
||||
impact on the format of the exported document. See the
|
||||
impact on the layout of the exported document. See the
|
||||
[documentation](https://novelwriter.readthedocs.io) for further details.
|
||||
|
||||
### 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
|
||||
details, or consult the [LICENSE](https://github.com/vkbo/novelWriter/blob/main/LICENSE.md) file.
|
||||
|
||||
Bundled assets have the following licenses:
|
||||
Bundled assets and their licenses are listed in
|
||||
[CREDITS](https://github.com/vkbo/novelWriter/blob/main/CREDITS.md).
|
||||
|
||||
* The Typicons-based icon themes by Stephen Hutchings are licensed under
|
||||
[CC BY-SA 4.0](http://creativecommons.org/licenses/by-sa/4.0/). The icons have been altered in
|
||||
size and colour for use with novelWriter, and some additional icons added. The original icon set
|
||||
is available at
|
||||
[stephenhutchings/typicons.font](https://github.com/stephenhutchings/typicons.font).
|
||||
* The Cantarell font by Dave Crossland is licensed under
|
||||
[OPEN FONT LICENSE Version 1.1](http://scripts.sil.org/OFL).
|
||||
It is available at [Google Fonts](https://fonts.google.com/specimen/Cantarell).
|
||||
* The Tomorrow syntax themes use colour schemes taken from Chris Kempson's collection of code
|
||||
editor themes, licensed with the
|
||||
[MIT License](https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md),
|
||||
and the main repo is available at
|
||||
[chriskempson/tomorrow-theme](https://github.com/chriskempson/tomorrow-theme).
|
||||
* Likewise, the Owl syntax themes use colours from Sarah Drasner's code editor themes, licensed
|
||||
with the [MIT License](https://github.com/sdras/night-owl-vscode-theme/blob/master/LICENSE), and
|
||||
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:**
|
||||

|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
@@ -25,9 +24,9 @@ copyright = "2018–2021, Veronica Berglyd Olsen"
|
||||
author = "Veronica Berglyd Olsen"
|
||||
|
||||
# The short X.Y version
|
||||
version = "1.2"
|
||||
version = "1.4"
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = "1.2"
|
||||
release = "1.4"
|
||||
|
||||
|
||||
# -- 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.
|
||||
|
||||
@@ -71,6 +71,7 @@ operating system for further details.
|
||||
:caption: Usage
|
||||
|
||||
usage_interface
|
||||
usage_shortcuts
|
||||
usage_projects
|
||||
usage_structure
|
||||
usage_notes
|
||||
|
||||
@@ -38,14 +38,13 @@ at the same time provide a complete set of features needed for writing a novel.
|
||||
|
||||
.. tip::
|
||||
If you do need to align information in rows and columns in your notes, you can achieve this with
|
||||
tabs and hard line breaks. The tab stop width can be specified in :guilabel:`Preferences` and
|
||||
hard line breaks can be inserted by adding to spaces at the end of the line.
|
||||
tabs and line breaks. The tab stop width can be specified in :guilabel:`Preferences`.
|
||||
|
||||
The main window does not have a toolbar like many other applications do. This reduces clutter, and
|
||||
since the documents are formatted with markdown tags, is more or less redundant. However, all
|
||||
formatting features supported are available through convenient keyboard shortcuts. They are also
|
||||
available in the main menu so you don't have to look up the syntax every time until you've
|
||||
memorised it. A full list of shortcuts can be found in the :ref:`a_ui_shortcuts` section.
|
||||
memorised it. A full list of shortcuts can be found in the :ref:`a_kb` section.
|
||||
|
||||
In addition, novelWriter has a :guilabel:`Focus Mode` where all the user interface elements other
|
||||
than the document editor itself are hidden away.
|
||||
|
||||
@@ -99,6 +99,31 @@ 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 ``nw/assets/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 Qt Linguist tool is installed on your system. On Ubuntu and Debian, the
|
||||
needed package is called `qttools5-dev-tools`.
|
||||
|
||||
.. 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 +134,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
|
||||
@@ -163,8 +186,8 @@ you wish.
|
||||
|
||||
.. tip::
|
||||
If you don't like auto-replacement, all symbols inserted by this feature are also available in
|
||||
the :guilabel:`Insert` menu, and via convenient :ref:`a_ui_shortcuts_ins`. You may also be using
|
||||
a `Compose Key`_ setup, which means you may not need the auto-replace feature.
|
||||
the :guilabel:`Insert` menu, and via convenient :ref:`a_kb_ins`. You may also be using a
|
||||
`Compose Key`_ setup, which means you may not need the auto-replace feature.
|
||||
|
||||
.. _Compose Key: https://en.wikipedia.org/wiki/Compose_key
|
||||
|
||||
@@ -182,15 +205,16 @@ tricky for languages that use the same symbol for these.
|
||||
|
||||
.. _a_ui_md:
|
||||
|
||||
Markdown Format
|
||||
===============
|
||||
The Markdown-Like Format
|
||||
========================
|
||||
|
||||
The document editor uses a simplified markdown format. That is, it supports basic formatting like
|
||||
emphasis (italic), strong importance (bold) and strikethrough text, as well as four levels of
|
||||
headings.
|
||||
The editor itself is a plaintext editor that uses formatting codes for setting meta data values and
|
||||
allowing for some text formatting. The syntax is based on Markdown, but novelWriter is *not* a
|
||||
Markdown editor. It supports basic formatting like emphasis (italic), strong importance (bold)
|
||||
and strikethrough text, as well as four levels of headings.
|
||||
|
||||
Some non-standard markdown features have been added. For instance, novelWriter allows for comments,
|
||||
a synopsis tag, and a set of keyword and value sets used for tags and references.
|
||||
In addition to formatting codes, novelWriter allows for comments, a synopsis tag, and a set of
|
||||
keyword and value sets used for tags and references.
|
||||
|
||||
|
||||
.. _a_ui_md_head:
|
||||
@@ -230,6 +254,69 @@ level of the novel. See :ref:`a_struct_heads` for more details.
|
||||
itself. See :ref:`a_struct_heads_unnum` for more details.
|
||||
|
||||
|
||||
.. _a_ui_md_text:
|
||||
|
||||
Text Paragraphs
|
||||
---------------
|
||||
|
||||
A text paragraph is indicated by a blank line. That is, you need two line breaks to separate two
|
||||
fragments of text into two paragraphs. Single line breaks are treated as line breaks within a
|
||||
paragraph.
|
||||
|
||||
In addition, the editor supports a few additional types of whitespaces.
|
||||
|
||||
* A non-breaking space can be inserted with :kbd:`Ctrl`:kbd:`K`, :kbd:`Space`.
|
||||
* Thin spaces are also supported, and can be inserted with :kbd:`Ctrl`:kbd:`K`,
|
||||
:kbd:`Shift`:kbd:`Space`.
|
||||
* Non-breaking thin space can be inserted with :kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`Space`.
|
||||
|
||||
These are all insert features, and the :guilabel:`Insert` menu has more. They are also listed
|
||||
in :ref:`a_kb_ins`.
|
||||
|
||||
Non-breaking spaces are highlighted by the syntax highlighter with an alternate coloured
|
||||
background, depending on the selected theme.
|
||||
|
||||
.. tip::
|
||||
Non-breaking spaces are the correct type of space to separate a number from its unit. Generally,
|
||||
it prevents the line wrapping algorithms from adding line breaks where it shouldn't.
|
||||
|
||||
|
||||
.. _a_ui_md_align:
|
||||
|
||||
Paragraphs Alignment and Indentation
|
||||
------------------------------------
|
||||
|
||||
Aside from Title Page and Partition which by default have its text centred, all document layouts
|
||||
have the text by default aligned to the left ot justified, depending on your Preferences.
|
||||
|
||||
You can override the default text alignment on individual paragraphs by specifying alignment tags.
|
||||
These tags are double angle brackets. Either ``>>`` or ``<<``. You put them either before or after
|
||||
the paragraph, and they will "push" the text towards the edge the brackets point towards. This
|
||||
should be fairly intuitive.
|
||||
|
||||
Indentation uses a similar syntax. But here you use a single ``>`` or ``<`` to push the text away
|
||||
from the edge.
|
||||
|
||||
Examples:
|
||||
|
||||
.. csv-table:: Text Alignment and Indentation
|
||||
:header: "Syntax", "Description"
|
||||
:widths: 40, 60
|
||||
:class: "tight-table"
|
||||
|
||||
"``>> Right aligned text``", "The text paragraph is right-aligned."
|
||||
"``Left aligned text <<``", "The text paragraph is left-aligned."
|
||||
"``>> Centred text <<``", "The text paragraph is centred."
|
||||
"``> Indented text``", "The text has an increased left margin."
|
||||
"``Indented text <``", "The text has an increased right margin."
|
||||
"``> Indented text <``", "The text has an both margins increased."
|
||||
|
||||
.. note::
|
||||
The text editor will not show the alignment and indentation live. But the viewer will show them
|
||||
when you open the document in the viewer. It will of course also be reflected in the document
|
||||
generated from the build tool.
|
||||
|
||||
|
||||
.. _a_ui_md_emph:
|
||||
|
||||
Text Emphasis
|
||||
@@ -310,34 +397,6 @@ The available tag and reference keywords are listed in the :ref:`a_struct_tags`
|
||||
also be inserted at the cursor position in the editor via the :guilabel:`Insert` menu.
|
||||
|
||||
|
||||
.. _a_ui_md_add:
|
||||
|
||||
Additional Markdown and Non-Standard Features
|
||||
---------------------------------------------
|
||||
|
||||
The editor and viewer also support markdown standard hard line breaks, and preserve non-breaking
|
||||
spaces if running with Qt 5.9 or higher. For older versions, the non-breaking spaces are lost when
|
||||
the document is saved. This is unfortunately hard-coded into the Qt text editor.
|
||||
|
||||
* A hard line break can be achieved by leaving two or more spaces at the end of the line. This is
|
||||
standard markdown syntax. Alternatively, the user can press :kbd:`Ctrl`:kbd:`K`, :kbd:`Return` to
|
||||
insert this type of line break.
|
||||
* A non-breaking space can be inserted with :kbd:`Ctrl`:kbd:`K`, :kbd:`Space`.
|
||||
* Thin spaces are also supported, and can be inserted with :kbd:`Ctrl`:kbd:`K`,
|
||||
:kbd:`Shift`:kbd:`Space`.
|
||||
* Non-breaking thin space can be inserted with :kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`Space`.
|
||||
|
||||
These are all insert features, and the :guilabel:`Insert` menu has more. They are also listed
|
||||
in :ref:`a_ui_shortcuts_ins`.
|
||||
|
||||
Both hard line breaks and non-breaking spaces are highlighted by the syntax highlighter as an
|
||||
alternate coloured background, depending on the selected theme.
|
||||
|
||||
.. tip::
|
||||
Non-breaking spaces are the correct type of space to separate a number from its unit. Generally,
|
||||
it prevents the line wrapping algorithms from adding line breaks where it shouldn't.
|
||||
|
||||
|
||||
.. _a_ui_outline:
|
||||
|
||||
Project Outline View
|
||||
@@ -376,139 +435,3 @@ very large,
|
||||
|
||||
The :guilabel:`Synopsis` column of the outline view takes its information from a specially
|
||||
formatted comment. See :ref:`a_ui_md_comm`.
|
||||
|
||||
|
||||
.. _a_ui_shortcuts:
|
||||
|
||||
Keyboard Shortcuts
|
||||
==================
|
||||
|
||||
Most features are available as keyboard shortcuts. These are as follows:
|
||||
|
||||
.. csv-table:: Keyboard Shortcuts
|
||||
:header: "Shortcut", "Description"
|
||||
:widths: 30, 70
|
||||
:class: "tight-table"
|
||||
|
||||
":kbd:`Alt`:kbd:`1`", "Switch focus to the project tree."
|
||||
":kbd:`Alt`:kbd:`2`", "Switch focus to document editor."
|
||||
":kbd:`Alt`:kbd:`3`", "Switch focus to document viewer."
|
||||
":kbd:`Alt`:kbd:`4`", "Switch focus to outline view."
|
||||
":kbd:`Alt`:kbd:`Left`", "Move backward in the view history of the document viewer."
|
||||
":kbd:`Alt`:kbd:`Right`", "Move forward in the view history of the document viewer."
|
||||
":kbd:`Ctrl`:kbd:`.`", "Open menu to correct word under cursor."
|
||||
":kbd:`Ctrl`:kbd:`,`", "Open the :guilabel:`Preferences` dialog."
|
||||
":kbd:`Ctrl`:kbd:`/`", "Change block format to comment."
|
||||
":kbd:`Ctrl`:kbd:`0`", "Remove block formatting for block under cursor."
|
||||
":kbd:`Ctrl`:kbd:`1`", "Change block format to header level 1."
|
||||
":kbd:`Ctrl`:kbd:`2`", "Change block format to header level 2."
|
||||
":kbd:`Ctrl`:kbd:`3`", "Change block format to header level 3."
|
||||
":kbd:`Ctrl`:kbd:`4`", "Change block format to header level 4."
|
||||
":kbd:`Ctrl`:kbd:`A`", "Select all text in the document."
|
||||
":kbd:`Ctrl`:kbd:`B`", "Format selected text, or word under cursor, with strong emphasis (bold)."
|
||||
":kbd:`Ctrl`:kbd:`C`", "Copy selected text to clipboard."
|
||||
":kbd:`Ctrl`:kbd:`D`", "Strikethrough selected text, or word under cursor."
|
||||
":kbd:`Ctrl`:kbd:`E`", "If in the project tree, edit a document or folder settings. (Same as :kbd:`F2`.)"
|
||||
":kbd:`Ctrl`:kbd:`F`", "Open the search bar and search for the selected word, if any is selected."
|
||||
":kbd:`Ctrl`:kbd:`G`", "Find next occurrence of search word in current document. (Same as :kbd:`F3`.)"
|
||||
":kbd:`Ctrl`:kbd:`H`", "Open the search and replace bar and search for the selected word, if any is selected. (On Mac, this is :kbd:`Cmd`:kbd:`=`.)"
|
||||
":kbd:`Ctrl`:kbd:`I`", "Format selected text, or word under cursor, with emphasis (italic)."
|
||||
":kbd:`Ctrl`:kbd:`K`", "Activate the insert commands. The commands are listed in :ref:`a_ui_shortcuts_ins`."
|
||||
":kbd:`Ctrl`:kbd:`N`", "Create new document."
|
||||
":kbd:`Ctrl`:kbd:`O`", "Open selected document."
|
||||
":kbd:`Ctrl`:kbd:`Q`", "Exit novelWriter."
|
||||
":kbd:`Ctrl`:kbd:`R`", "If in the project tree, open a document for viewing. If the editor has focus, open current document for viewing."
|
||||
":kbd:`Ctrl`:kbd:`S`", "Save the current document in the document editor."
|
||||
":kbd:`Ctrl`:kbd:`V`", "Paste text from clipboard to cursor position."
|
||||
":kbd:`Ctrl`:kbd:`W`", "Close the current document in the document editor."
|
||||
":kbd:`Ctrl`:kbd:`X`", "Cut selected text to clipboard."
|
||||
":kbd:`Ctrl`:kbd:`Y`", "Redo latest undo."
|
||||
":kbd:`Ctrl`:kbd:`Z`", "Undo latest changes."
|
||||
":kbd:`Ctrl`:kbd:`F7`", "Toggle spell checking."
|
||||
":kbd:`Ctrl`:kbd:`F10`", "Toggle automatic updating of project outline."
|
||||
":kbd:`Ctrl`:kbd:`Up`", "Move item one step up in the project tree."
|
||||
":kbd:`Ctrl`:kbd:`Down`", "Move item one step down in the project tree."
|
||||
":kbd:`Ctrl`:kbd:`Del`", "Delete next word in editor."
|
||||
":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:`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."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`A`", "Select all text in current paragraph."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`G`", "Find previous occurrence of search word in current document. (Same as :kbd:`Shift`:kbd:`F3`.)"
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`I`", "Import text to the current document from a text file."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`N`", "Create new folder."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`O`", "Open a project."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`R`", "Close the document viewer."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`S`", "Save the current project."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`W`", "Close the current project."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`Z`", "Undo move of project tree item."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`Del`", "If in the project tree, move a document to trash, or delete a folder."
|
||||
":kbd:`F1`", "Open the documentation. This will either open the Qt Assistant, if available, or send you to the documentation website."
|
||||
":kbd:`F2`", "If in the project tree, edit a document or folder settings. (Same as :kbd:`Ctrl`:kbd:`E`)"
|
||||
":kbd:`F3`", "Find next occurrence of search word in current document. (Same as :kbd:`Ctrl`:kbd:`G`)"
|
||||
":kbd:`F5`", "Open the :guilabel:`Build Novel Project` dialog."
|
||||
":kbd:`F6`", "Open the :guilabel:`Writing Statistics` dialog."
|
||||
":kbd:`F7`", "Re-run spell checker."
|
||||
":kbd:`F8`", "Activate :guilabel:`Focus Mode`, hiding the project tree and document viewer."
|
||||
":kbd:`F9`", "Re-build the project index."
|
||||
":kbd:`F10`", "Re-build the project outline."
|
||||
":kbd:`F11`", "Activate full screen mode."
|
||||
":kbd:`Shift`:kbd:`F1`", "Open the online documentation in the system default browser."
|
||||
":kbd:`Shift`:kbd:`F3`", "Find previous occurrence of search word in current document. (Same as :kbd:`Ctrl`:kbd:`Shift`:kbd:`G`.)"
|
||||
":kbd:`Shift`:kbd:`F6`", "Open the :guilabel:`Project Details` dialog."
|
||||
":kbd:`Return`", "If in the project tree, open a document for editing."
|
||||
|
||||
.. note::
|
||||
On macOS, replace :kbd:`Ctrl` with :kbd:`Cmd`.
|
||||
|
||||
|
||||
.. _a_ui_shortcuts_ins:
|
||||
|
||||
Insert Shortcuts
|
||||
----------------
|
||||
|
||||
A set of insert features are also available through shortcuts, but they require a double
|
||||
combination of key sequences. The insert feature is activated with :kbd:`Ctrl`:kbd:`K`, followed by
|
||||
a key or key combination for the inserted content.
|
||||
|
||||
.. csv-table:: Keyboard Shortcuts
|
||||
:header: "Shortcut", "Description"
|
||||
:widths: 40, 60
|
||||
:class: "tight-table"
|
||||
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`−`", "Insert a short dash (en dash)."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`_`", "Insert a long dash (em dash)."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`_`", "Insert a horizontal bar (quotation dash)."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`~`", "Insert a figure dash (same width as a number)."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`1`", "Insert a left single quote."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`2`", "Insert a right single quote."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`3`", "Insert a left double quote."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`4`", "Insert a right double quote."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`'`", "Insert a modifier apostrophe."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`.`", "Insert an ellipsis."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`'`", "Insert a prime."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`""`", "Insert a double prime."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Return`", "Insert a hard line break."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Space`", "Insert a non-breaking space."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Shift`:kbd:`Space`", "Insert a thin space."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`Space`", "Insert a thin non-breaking space."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`*`", "Insert a list bullet."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`−`", "Insert a hyphen bullet (alternative bullet)."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`*`", "Insert a flower mark (alternative bullet)."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`%`", "Insert a per mille symbol."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`O`", "Insert a degree symbol."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`X`", "Insert a times sign."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`D`", "Insert a division sign."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`G`", "Insert a ``@tag`` keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`V`", "Insert a ``@pov`` keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`F`", "Insert a ``@focus`` keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`C`", "Insert a ``@char`` keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`P`", "Insert a ``@plot`` keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`T`", "Insert a ``@time`` keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`L`", "Insert a ``@location`` keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`O`", "Insert an ``@object`` keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`E`", "Insert an ``@entity`` keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`X`", "Insert a ``@custom`` keyword."
|
||||
|
||||
147
docs/source/usage_shortcuts.rst
Normal file
147
docs/source/usage_shortcuts.rst
Normal file
@@ -0,0 +1,147 @@
|
||||
.. _a_kb:
|
||||
|
||||
******************
|
||||
Keyboard Shortcuts
|
||||
******************
|
||||
|
||||
Most features in novelWriter are available as keyboard shortcuts.
|
||||
|
||||
|
||||
.. _a_kb_main:
|
||||
|
||||
Main Shortcuts
|
||||
==============
|
||||
|
||||
The main shorcuts are as follows:
|
||||
|
||||
.. csv-table:: Keyboard Shortcuts
|
||||
:header: "Shortcut", "Description"
|
||||
:widths: 30, 70
|
||||
:class: "tight-table"
|
||||
|
||||
":kbd:`Alt`:kbd:`1`", "Switch focus to the project tree. On Windows, use :kbd:`Ctrl`:kbd:`Alt`:kbd:`1`."
|
||||
":kbd:`Alt`:kbd:`2`", "Switch focus to document editor. On Windows, use :kbd:`Ctrl`:kbd:`Alt`:kbd:`2`."
|
||||
":kbd:`Alt`:kbd:`3`", "Switch focus to document viewer. On Windows, use :kbd:`Ctrl`:kbd:`Alt`:kbd:`3`."
|
||||
":kbd:`Alt`:kbd:`4`", "Switch focus to outline view. On Windows, use :kbd:`Ctrl`:kbd:`Alt`:kbd:`4`."
|
||||
":kbd:`Alt`:kbd:`Left`", "Move backward in the view history of the document viewer."
|
||||
":kbd:`Alt`:kbd:`Right`", "Move forward in the view history of the document viewer."
|
||||
":kbd:`Ctrl`:kbd:`.`", "Open menu to correct word under cursor."
|
||||
":kbd:`Ctrl`:kbd:`,`", "Open the :guilabel:`Preferences` dialog."
|
||||
":kbd:`Ctrl`:kbd:`/`", "Toggle block format as comment."
|
||||
":kbd:`Ctrl`:kbd:`0`", "Remove block formatting for block under cursor."
|
||||
":kbd:`Ctrl`:kbd:`1`", "Change block format to header level 1."
|
||||
":kbd:`Ctrl`:kbd:`2`", "Change block format to header level 2."
|
||||
":kbd:`Ctrl`:kbd:`3`", "Change block format to header level 3."
|
||||
":kbd:`Ctrl`:kbd:`4`", "Change block format to header level 4."
|
||||
":kbd:`Ctrl`:kbd:`5`", "Change block alignment to left-aligned."
|
||||
":kbd:`Ctrl`:kbd:`6`", "Change block alignment to centred."
|
||||
":kbd:`Ctrl`:kbd:`7`", "Change block alignment to right-aligned."
|
||||
":kbd:`Ctrl`:kbd:`8`", "Add a left margin to the block."
|
||||
":kbd:`Ctrl`:kbd:`9`", "Add a right margin to the block."
|
||||
":kbd:`Ctrl`:kbd:`A`", "Select all text in the document."
|
||||
":kbd:`Ctrl`:kbd:`B`", "Format selected text, or word under cursor, with strong emphasis (bold)."
|
||||
":kbd:`Ctrl`:kbd:`C`", "Copy selected text to clipboard."
|
||||
":kbd:`Ctrl`:kbd:`D`", "Strikethrough selected text, or word under cursor."
|
||||
":kbd:`Ctrl`:kbd:`E`", "If in the project tree, edit a document or folder settings. (Same as :kbd:`F2`.)"
|
||||
":kbd:`Ctrl`:kbd:`F`", "Open the search bar and search for the selected word, if any is selected."
|
||||
":kbd:`Ctrl`:kbd:`G`", "Find next occurrence of search word in current document. (Same as :kbd:`F3`.)"
|
||||
":kbd:`Ctrl`:kbd:`H`", "Open the search and replace bar and search for the selected word, if any is selected. (On Mac, this is :kbd:`Cmd`:kbd:`=`.)"
|
||||
":kbd:`Ctrl`:kbd:`I`", "Format selected text, or word under cursor, with emphasis (italic)."
|
||||
":kbd:`Ctrl`:kbd:`K`", "Activate the insert commands. The commands are listed in :ref:`a_kb_ins`."
|
||||
":kbd:`Ctrl`:kbd:`N`", "Create new document."
|
||||
":kbd:`Ctrl`:kbd:`O`", "Open selected document."
|
||||
":kbd:`Ctrl`:kbd:`Q`", "Exit novelWriter."
|
||||
":kbd:`Ctrl`:kbd:`R`", "If in the project tree, open a document for viewing. If the editor has focus, open current document for viewing."
|
||||
":kbd:`Ctrl`:kbd:`S`", "Save the current document in the document editor."
|
||||
":kbd:`Ctrl`:kbd:`V`", "Paste text from clipboard to cursor position."
|
||||
":kbd:`Ctrl`:kbd:`W`", "Close the current document in the document editor."
|
||||
":kbd:`Ctrl`:kbd:`X`", "Cut selected text to clipboard."
|
||||
":kbd:`Ctrl`:kbd:`Y`", "Redo latest undo."
|
||||
":kbd:`Ctrl`:kbd:`Z`", "Undo latest changes."
|
||||
":kbd:`Ctrl`:kbd:`F7`", "Toggle spell checking."
|
||||
":kbd:`Ctrl`:kbd:`F10`", "Toggle automatic updating of project outline."
|
||||
":kbd:`Ctrl`:kbd:`Up`", "Move item one step up in the project tree."
|
||||
":kbd:`Ctrl`:kbd:`Down`", "Move item one step down in the project tree."
|
||||
":kbd:`Ctrl`:kbd:`Del`", "Delete next word in editor."
|
||||
":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:`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."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`A`", "Select all text in current paragraph."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`G`", "Find previous occurrence of search word in current document. (Same as :kbd:`Shift`:kbd:`F3`.)"
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`I`", "Import text to the current document from a text file."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`N`", "Create new folder."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`O`", "Open a project."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`R`", "Close the document viewer."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`S`", "Save the current project."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`W`", "Close the current project."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`Z`", "Undo move of project tree item."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`Del`", "If in the project tree, move a document to trash, or delete a folder."
|
||||
":kbd:`F1`", "Open the documentation. This will either open the Qt Assistant, if available, or send you to the documentation website."
|
||||
":kbd:`F2`", "If in the project tree, edit a document or folder settings. (Same as :kbd:`Ctrl`:kbd:`E`)"
|
||||
":kbd:`F3`", "Find next occurrence of search word in current document. (Same as :kbd:`Ctrl`:kbd:`G`)"
|
||||
":kbd:`F5`", "Open the :guilabel:`Build Novel Project` dialog."
|
||||
":kbd:`F6`", "Open the :guilabel:`Writing Statistics` dialog."
|
||||
":kbd:`F7`", "Re-run spell checker."
|
||||
":kbd:`F8`", "Activate :guilabel:`Focus Mode`, hiding the project tree and document viewer."
|
||||
":kbd:`F9`", "Re-build the project index."
|
||||
":kbd:`F10`", "Re-build the project outline."
|
||||
":kbd:`F11`", "Activate full screen mode."
|
||||
":kbd:`Shift`:kbd:`F1`", "Open the online documentation in the system default browser."
|
||||
":kbd:`Shift`:kbd:`F3`", "Find previous occurrence of search word in current document. (Same as :kbd:`Ctrl`:kbd:`Shift`:kbd:`G`.)"
|
||||
":kbd:`Shift`:kbd:`F6`", "Open the :guilabel:`Project Details` dialog."
|
||||
":kbd:`Return`", "If in the project tree, open a document for editing."
|
||||
|
||||
.. note::
|
||||
On macOS, replace :kbd:`Ctrl` with :kbd:`Cmd`.
|
||||
|
||||
|
||||
.. _a_kb_ins:
|
||||
|
||||
Insert Shortcuts
|
||||
================
|
||||
|
||||
A set of insert features are also available through shortcuts, but they require a double
|
||||
combination of key sequences. The insert feature is activated with :kbd:`Ctrl`:kbd:`K`, followed by
|
||||
a key or key combination for the inserted content.
|
||||
|
||||
.. csv-table:: Keyboard Shortcuts
|
||||
:header: "Shortcut", "Description"
|
||||
:widths: 40, 60
|
||||
:class: "tight-table"
|
||||
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`−`", "Insert a short dash (en dash)."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`_`", "Insert a long dash (em dash)."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`_`", "Insert a horizontal bar (quotation dash)."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`~`", "Insert a figure dash (same width as a number)."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`1`", "Insert a left single quote."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`2`", "Insert a right single quote."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`3`", "Insert a left double quote."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`4`", "Insert a right double quote."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`'`", "Insert a modifier apostrophe."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`.`", "Insert an ellipsis."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`'`", "Insert a prime."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`""`", "Insert a double prime."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Space`", "Insert a non-breaking space."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Shift`:kbd:`Space`", "Insert a thin space."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`Space`", "Insert a thin non-breaking space."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`*`", "Insert a list bullet."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`−`", "Insert a hyphen bullet (alternative bullet)."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`*`", "Insert a flower mark (alternative bullet)."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`%`", "Insert a per mille symbol."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`O`", "Insert a degree symbol."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`X`", "Insert a times sign."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`D`", "Insert a division sign."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`G`", "Insert a ``@tag`` keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`V`", "Insert a ``@pov`` keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`F`", "Insert a ``@focus`` keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`C`", "Insert a ``@char`` keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`P`", "Insert a ``@plot`` keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`T`", "Insert a ``@time`` keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`L`", "Insert a ``@location`` keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`O`", "Insert an ``@object`` keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`E`", "Insert an ``@entity`` keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`X`", "Insert a ``@custom`` keyword."
|
||||
@@ -7,7 +7,7 @@ Typographical Notes
|
||||
novelWriter has some support for typographical symbols that are not usually easily available in
|
||||
many text editors. This includes for instance the proper unicode quotation marks, dashes, ellipsis,
|
||||
thin spaces, etc. All these symbols are available from the :guilabel:`Insert` menu, and via
|
||||
keyboard shortcuts. See :ref:`a_ui_shortcuts_ins`.
|
||||
keyboard shortcuts. See :ref:`a_kb_ins`.
|
||||
|
||||
This chapter provides some additional information on how novelWriter handles these symbols.
|
||||
|
||||
|
||||
85
i18n/README.md
Normal file
85
i18n/README.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# 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 translation maps for the novelWriter projects,
|
||||
used by the Build Novel Project tool.
|
||||
|
||||
**Note**
|
||||
|
||||
When making a new translation, or updating an existing one, only commit the `nw_XX.ts` you have
|
||||
made changes to. The `qtlupdate` command mentioned below will likely modify all `nw_XX.ts`
|
||||
slightly, but please _don't_ commit those changes to the pull request.
|
||||
|
||||
## 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` file you just added in the
|
||||
`i18n` folder is needed.
|
||||
|
||||
**Note**
|
||||
|
||||
These commands require that you have the tools Qt 5 Linguist and the PyQt5 tool `pylupdate5`
|
||||
installed on your system.
|
||||
|
||||
For Ubuntu/Debian, run:
|
||||
```bash
|
||||
sudo apt install qttools5-dev-tools pyqt5-dev-tools
|
||||
```
|
||||
|
||||
### Missing QtBase Translations
|
||||
|
||||
The default Qt dialogs also have translations, for instance for standard buttons like "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.
|
||||
5023
i18n/nw_en_US.ts
Normal file
5023
i18n/nw_en_US.ts
Normal file
File diff suppressed because it is too large
Load Diff
5024
i18n/nw_fr.ts
Normal file
5024
i18n/nw_fr.ts
Normal file
File diff suppressed because it is too large
Load Diff
5023
i18n/nw_nb_NO.ts
Normal file
5023
i18n/nw_nb_NO.ts
Normal file
File diff suppressed because it is too large
Load Diff
4943
i18n/nw_pt.ts
Normal file
4943
i18n/nw_pt.ts
Normal file
File diff suppressed because it is too large
Load Diff
5025
i18n/nw_zh_CN.ts
Normal file
5025
i18n/nw_zh_CN.ts
Normal file
File diff suppressed because it is too large
Load Diff
73
i18n/qtbase.py
Normal file
73
i18n/qtbase.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
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")
|
||||
39
novelWriter.pro
Normal file
39
novelWriter.pro
Normal file
@@ -0,0 +1,39 @@
|
||||
SOURCES += \
|
||||
i18n/qtbase.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 \
|
||||
i18n/nw_zh_CN.ts
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – Start Script
|
||||
==========================
|
||||
@@ -8,9 +7,9 @@ import os
|
||||
import sys
|
||||
|
||||
try:
|
||||
import PyQt5.QtWidgets # noqa: F401
|
||||
import PyQt5.QtGui # noqa: F401
|
||||
import PyQt5.QtCore # noqa: F401
|
||||
import PyQt5.QtWidgets # noqa: F401
|
||||
import PyQt5.QtGui # noqa: F401
|
||||
import PyQt5.QtCore # noqa: F401
|
||||
except Exception:
|
||||
print("ERROR: Failed to load dependency PyQt5")
|
||||
sys.exit(1)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – Init File
|
||||
=======================
|
||||
@@ -40,9 +39,9 @@ from nw.config import Config
|
||||
# Generally follows PEP 440
|
||||
# Hex Version:
|
||||
# - Digit 1,2 : Major Version (01-ff)
|
||||
# = Digit 3,4 : Minor Version (01-ff)
|
||||
# - Digit 3,4 : Minor Version (01-ff)
|
||||
# - Digit 5,6 : Patch Version (01-ff)
|
||||
# = Digit 7 : Release Type (a: aplha, b: beta, c: candidate, f: final)
|
||||
# - Digit 7 : Release Type (a: aplha, b: beta, c: candidate, f: final)
|
||||
# - Digit 8 : Release Number (0-f)
|
||||
#
|
||||
# Example : Full Short Description
|
||||
@@ -62,9 +61,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.4"
|
||||
__hexversion__ = "0x010400f0"
|
||||
__date__ = "2021-07-27"
|
||||
__status__ = "Stable"
|
||||
__domain__ = "novelwriter.io"
|
||||
__url__ = "https://novelwriter.io"
|
||||
@@ -73,10 +72,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
|
||||
@@ -94,15 +89,19 @@ __credits__ = [
|
||||
# Add verbose logging level
|
||||
VERBOSE = 5
|
||||
logging.addLevelName(VERBOSE, "VERBOSE")
|
||||
|
||||
|
||||
def logVerbose(self, message, *args, **kws):
|
||||
if self.isEnabledFor(VERBOSE):
|
||||
self._log(VERBOSE, message, args, **kws)
|
||||
|
||||
|
||||
logging.Logger.verbose = logVerbose
|
||||
|
||||
# Initiating logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
##
|
||||
# Main Program
|
||||
##
|
||||
@@ -110,8 +109,9 @@ logger = logging.getLogger(__name__)
|
||||
# Load the main config as a global object
|
||||
CONFIG = Config()
|
||||
|
||||
|
||||
def main(sysArgs=None):
|
||||
"""Parses command line, sets up logging, and launches main GUI.
|
||||
"""Parse command line, set up logging, and launches main GUI.
|
||||
"""
|
||||
if sysArgs is None:
|
||||
sysArgs = sys.argv[1:]
|
||||
@@ -137,7 +137,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"
|
||||
@@ -149,19 +149,19 @@ def main(sysArgs=None):
|
||||
" --config= Alternative config file.\n"
|
||||
" --data= Alternative user data path.\n"
|
||||
).format(
|
||||
version = __version__,
|
||||
copyright = __copyright__,
|
||||
date = __date__,
|
||||
version=__version__,
|
||||
copyright=__copyright__,
|
||||
date=__date__,
|
||||
)
|
||||
|
||||
# Defaults
|
||||
logLevel = logging.WARN
|
||||
logLevel = logging.WARN
|
||||
logFormat = "{levelname:8} {message:}"
|
||||
confPath = None
|
||||
dataPath = None
|
||||
testMode = False
|
||||
qtStyle = "Fusion"
|
||||
cmdOpen = None
|
||||
confPath = None
|
||||
dataPath = None
|
||||
testMode = False
|
||||
qtStyle = "Fusion"
|
||||
cmdOpen = None
|
||||
|
||||
# Parse Options
|
||||
try:
|
||||
@@ -209,9 +209,7 @@ def main(sysArgs=None):
|
||||
pkgLogger.addHandler(cHandle)
|
||||
pkgLogger.setLevel(logLevel)
|
||||
|
||||
logger.info("Starting novelWriter %s (%s) %s" % (
|
||||
__version__, __hexversion__, __date__
|
||||
))
|
||||
logger.info("Starting novelWriter %s (%s) %s", __version__, __hexversion__, __date__)
|
||||
|
||||
# Check Packages and Versions
|
||||
errorData = []
|
||||
@@ -221,19 +219,19 @@ 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
|
||||
|
||||
try:
|
||||
import lxml # noqa: F401
|
||||
import lxml # noqa: F401
|
||||
except ImportError:
|
||||
errorData.append("Python module 'lxml' is missing.")
|
||||
errorCode |= 32
|
||||
@@ -285,6 +283,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"
|
||||
}
|
||||
105
nw/assets/i18n/project_zh_CN.json
Normal file
105
nw/assets/i18n/project_zh_CN.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"Synopsis": "概要",
|
||||
"Comment": "注释",
|
||||
"Notes": "笔记",
|
||||
"0": "零",
|
||||
"1": "一",
|
||||
"2": "二",
|
||||
"3": "三",
|
||||
"4": "四",
|
||||
"5": "五",
|
||||
"6": "六",
|
||||
"7": "七",
|
||||
"8": "八",
|
||||
"9": "九",
|
||||
"10": "十",
|
||||
"11": "十一",
|
||||
"12": "十二",
|
||||
"13": "十三",
|
||||
"14": "十四",
|
||||
"15": "十五",
|
||||
"16": "十六",
|
||||
"17": "十七",
|
||||
"18": "十八",
|
||||
"19": "十九",
|
||||
"20": "二十",
|
||||
"21": "二十一",
|
||||
"22": "二十二",
|
||||
"23": "二十三",
|
||||
"24": "二十四",
|
||||
"25": "二十五",
|
||||
"26": "二十六",
|
||||
"27": "二十七",
|
||||
"28": "二十八",
|
||||
"29": "二十九",
|
||||
"30": "三十",
|
||||
"31": "三十一",
|
||||
"32": "三十二",
|
||||
"33": "三十三",
|
||||
"34": "三十四",
|
||||
"35": "三十五",
|
||||
"36": "三十六",
|
||||
"37": "三十七",
|
||||
"38": "三十八",
|
||||
"39": "三十九",
|
||||
"40": "四十",
|
||||
"41": "四十一",
|
||||
"42": "四十二",
|
||||
"43": "四十三",
|
||||
"44": "四十四",
|
||||
"45": "四十五",
|
||||
"46": "四十六",
|
||||
"47": "四十七",
|
||||
"48": "四十八",
|
||||
"49": "四十九",
|
||||
"50": "五十",
|
||||
"51": "五十一",
|
||||
"52": "五十二",
|
||||
"53": "五十三",
|
||||
"54": "五十四",
|
||||
"55": "五十五",
|
||||
"56": "五十六",
|
||||
"57": "五十七",
|
||||
"58": "五十八",
|
||||
"59": "五十九",
|
||||
"60": "六十",
|
||||
"61": "六十一",
|
||||
"62": "六十二",
|
||||
"63": "六十三",
|
||||
"64": "六十四",
|
||||
"65": "六十五",
|
||||
"66": "六十六",
|
||||
"67": "六十七",
|
||||
"68": "六十八",
|
||||
"69": "六十九",
|
||||
"70": "七十",
|
||||
"71": "七十一",
|
||||
"72": "七十二",
|
||||
"73": "七十三",
|
||||
"74": "七十四",
|
||||
"75": "七十五",
|
||||
"76": "七十六",
|
||||
"77": "七十七",
|
||||
"78": "七十八",
|
||||
"79": "七十九",
|
||||
"80": "八十",
|
||||
"81": "八十一",
|
||||
"82": "八十二",
|
||||
"83": "八十三",
|
||||
"84": "八十四",
|
||||
"85": "八十五",
|
||||
"86": "八十六",
|
||||
"87": "八十七",
|
||||
"88": "八十八",
|
||||
"89": "八十九",
|
||||
"90": "九十",
|
||||
"91": "九十一",
|
||||
"92": "九十二",
|
||||
"93": "九十三",
|
||||
"94": "九十四",
|
||||
"95": "九十五",
|
||||
"96": "九十六",
|
||||
"97": "九十七",
|
||||
"98": "九十八",
|
||||
"99": "九十九",
|
||||
}
|
||||
@@ -2,82 +2,50 @@
|
||||
<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>
|
||||
<h2>Release Notes for 1.4</h2>
|
||||
<p><i>Released on 27 July 2021</i></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>
|
||||
<p>This release contains some new features and a lot of code refactoring. Among the main new
|
||||
features is paragraph alignment and indentation. Regular line breaks within paragraphs are now also
|
||||
supported. The changes are described in more detail below.</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>Internationalisation</h3>
|
||||
<p>Translation files have been added for Simplified Chinese by Qianzhi Long.</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>Line Breaks</h3>
|
||||
<p>The way line breaks inside paragraphs work has been changed. A single line break is now treated
|
||||
as a proper line break and will show up in the document viewer and exported documents. A single
|
||||
line break does not start a new paragraph, but forces a break inside the paragraph like a Shift +
|
||||
Enter does in most rich text editors. Two line breaks is still needed to start a new paragraph.</p>
|
||||
<p>The old syntax of adding two spaces at the end of a line to force a line break within a
|
||||
paragraph will still work as before, so there is no need to change your existing text if you've
|
||||
used this feature. However, there is a new highlighting feature that will show you where in the
|
||||
text you have redundant spaces. If you are used to having double spaces between sentences, you may
|
||||
want to switch off this highlighting feature in Preferences as it will also detect those.</p>
|
||||
<p>A helper function has been added to the Format menu that can look through a paragraph and remove
|
||||
line breaks in case you've been using line breaks inside your existing text under the assumption
|
||||
that the exporter and viewer will ignore them.</p>
|
||||
<p>I hope this change will not be too inconvenient. I believe the new behaviour will make more
|
||||
sense for most people. Especially considering some of the feedback I've gotten on how line breaks
|
||||
work. The original implementation was following the Markdown standard, but since novelWriter is not
|
||||
a proper Markdown editor and instead just borrows from Markdown, this behaviour always seemed a bit
|
||||
unnecessary.</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>
|
||||
<h3>Text Alignment and Indentation</h3>
|
||||
<p>The default text alignment is left or justified based on your preferences. For documents with
|
||||
the layout set to Title Page or Partitions, the default is centred. However, sometimes you may
|
||||
want to override this default. A new set of codes have therefore been added to allow specifying
|
||||
alignment as well as additional text margins on individual paragraphs.</p>
|
||||
<p>The logic of the syntax is as follows:</p>
|
||||
<p>A single angle bracket will push the text away from the edge it points away from. Therefore, a
|
||||
single '>' before the paragraph, or a single '<' after the paragraph, will add indentation on
|
||||
the respective side. It's perfectly valid to do this on both sides at the same time.</p>
|
||||
<p>A double set of angle brackets will push the text all the way towards the opposite side.
|
||||
Therefore, a double set of '>>' before the paragraph will indicate right alignment, and a
|
||||
double set of '<<' after the paragraph will force left alignment. Also here both can be used
|
||||
at the same time, which results in the paragraph being centred.</p>
|
||||
<p>Format menu entries and keyboard shortcuts have been added so that you don't have to memorise
|
||||
these codes.</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>
|
||||
60
nw/assets/text/release_notes_1.3.htm
Normal file
60
nw/assets/text/release_notes_1.3.htm
Normal file
@@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html public "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
|
||||
<html>
|
||||
<body>
|
||||
|
||||
<h2>Release Notes for 1.3.3</h2>
|
||||
<p><i>Released on 13 June 2021</i></p>
|
||||
<p>This patch release fixes a potential file encoding issue when running setup on Windows, and a
|
||||
minor issue with the project word count not being updated immediately when a file is deleted. In
|
||||
addition, the keyboard shortcuts to change focus between the project tree, the editor, the viewer,
|
||||
and the outline panel, have been changed for Windows users. They keyboard shortcuts were
|
||||
interfering with the Alt codes used for special characters. The shortcuts are unchanged for Linux
|
||||
and macOS.</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.3.2</h2>
|
||||
<p><i>Released on 30 May 2021</i></p>
|
||||
<p>This is a patch release that fixes some minor issues. One issue was with the split tool, which
|
||||
would drop the last line from the source document during a split if it was missing a final line
|
||||
break. A minor issue with the display of word counts on the details panel under the project tree
|
||||
has also been fixed. In addition, the setup script commands for Linux have been improved a bit.</p>
|
||||
|
||||
<hr/>
|
||||
|
||||
<h2>Release Notes for 1.3.1</h2>
|
||||
<p><i>Released on 6 May 2021</i></p>
|
||||
<p>This is a patch release primarily to fix a problem with the Qt translation library used to make
|
||||
novelWriter available in multiple languages. One of the function calls made to the library was
|
||||
added recently, in Qt 5.15, which many users on Linux will not have installed on their system. The
|
||||
issue could be resolved by updating the library, but it's an unnecessary and inconvenient
|
||||
restriction.</p>
|
||||
|
||||
<hr/>
|
||||
|
||||
<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>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -20,6 +20,6 @@ hidden = 150, 150, 150
|
||||
keyword = 200, 46, 0
|
||||
value = 184, 200, 0
|
||||
spellcheckline = 200, 46, 0
|
||||
tagerror = 46, 200, 0
|
||||
errorline = 46, 200, 0
|
||||
replacetag = 0, 184, 46
|
||||
modifier = 200, 120, 0
|
||||
|
||||
@@ -20,6 +20,6 @@ hidden = 100, 100, 100
|
||||
keyword = 200, 50, 50
|
||||
value = 50, 150, 50
|
||||
spellcheckline = 200, 0, 0
|
||||
tagerror = 0, 150, 0
|
||||
errorline = 0, 150, 0
|
||||
replacetag = 0, 150, 0
|
||||
modifier = 150, 110, 30
|
||||
|
||||
@@ -20,6 +20,6 @@ hidden = 150, 150, 150
|
||||
keyword = 225, 225, 225
|
||||
value = 200, 200, 200
|
||||
spellcheckline = 200, 46, 0
|
||||
tagerror = 46, 200, 0
|
||||
errorline = 46, 200, 0
|
||||
replacetag = 225, 225, 225
|
||||
modifier = 225, 225, 225
|
||||
|
||||
@@ -20,6 +20,6 @@ hidden = 100, 100, 100
|
||||
keyword = 0, 0, 0
|
||||
value = 20, 20, 20
|
||||
spellcheckline = 200, 0, 0
|
||||
tagerror = 0, 150, 0
|
||||
errorline = 0, 150, 0
|
||||
replacetag = 0, 0, 0
|
||||
modifier = 0, 0, 0
|
||||
|
||||
@@ -40,6 +40,6 @@ hidden = 152, 159, 177
|
||||
keyword = 222, 61, 58
|
||||
value = 150, 74, 193
|
||||
spellcheckline = 222, 61, 58
|
||||
tagerror = 8, 145, 106
|
||||
errorline = 8, 145, 106
|
||||
replacetag = 42, 162, 152
|
||||
modifier = 224, 175, 5
|
||||
|
||||
@@ -40,6 +40,6 @@ hidden = 99, 119, 119
|
||||
keyword = 247, 140, 108
|
||||
value = 199, 146, 234
|
||||
spellcheckline = 247, 140, 108
|
||||
tagerror = 173, 219, 103
|
||||
errorline = 173, 219, 103
|
||||
replacetag = 127, 219, 202
|
||||
modifier = 236, 196, 141
|
||||
|
||||
@@ -20,6 +20,6 @@ hidden = 147, 161, 161
|
||||
keyword = 133, 153, 0
|
||||
value = 203, 75, 22
|
||||
spellcheckline = 203, 75, 22
|
||||
tagerror = 220, 50, 47
|
||||
errorline = 220, 50, 47
|
||||
replacetag = 133, 153, 0
|
||||
modifier = 181, 137, 0
|
||||
|
||||
@@ -20,6 +20,6 @@ hidden = 88, 110, 117
|
||||
keyword = 133, 153, 0
|
||||
value = 203, 75, 22
|
||||
spellcheckline = 203, 75, 22
|
||||
tagerror = 220, 50, 47
|
||||
errorline = 220, 50, 47
|
||||
replacetag = 133, 153, 0
|
||||
modifier = 181, 137, 0
|
||||
|
||||
@@ -40,6 +40,6 @@ hidden = 142, 144, 140
|
||||
keyword = 240, 40, 41
|
||||
value = 137, 89, 168
|
||||
spellcheckline = 240, 40, 41
|
||||
tagerror = 113, 140, 0
|
||||
errorline = 113, 140, 0
|
||||
replacetag = 62, 153, 159
|
||||
modifier = 245, 135, 31
|
||||
|
||||
@@ -40,6 +40,6 @@ hidden = 150, 152, 150
|
||||
keyword = 204, 102, 102
|
||||
value = 178, 148, 187
|
||||
spellcheckline = 204, 102, 102
|
||||
tagerror = 181, 189, 104
|
||||
errorline = 181, 189, 104
|
||||
replacetag = 138, 190, 183
|
||||
modifier = 222, 147, 95
|
||||
|
||||
@@ -40,6 +40,6 @@ hidden = 114, 133, 183
|
||||
keyword = 255, 157, 164
|
||||
value = 235, 187, 255
|
||||
spellcheckline = 255, 157, 164
|
||||
tagerror = 209, 241, 169
|
||||
errorline = 209, 241, 169
|
||||
replacetag = 153, 255, 255
|
||||
modifier = 255, 197, 143
|
||||
|
||||
@@ -40,6 +40,6 @@ hidden = 150, 152, 150
|
||||
keyword = 213, 78, 83
|
||||
value = 195, 151, 216
|
||||
spellcheckline = 213, 78, 83
|
||||
tagerror = 185, 202, 74
|
||||
errorline = 185, 202, 74
|
||||
replacetag = 112, 192, 177
|
||||
modifier = 231, 140, 69
|
||||
|
||||
@@ -40,6 +40,6 @@ hidden = 153, 153, 153
|
||||
keyword = 242, 119, 122
|
||||
value = 204, 153, 204
|
||||
spellcheckline = 242, 119, 122
|
||||
tagerror = 153, 204, 153
|
||||
errorline = 153, 204, 153
|
||||
replacetag = 102, 204, 204
|
||||
modifier = 249, 145, 57
|
||||
|
||||
156
nw/common.py
156
nw/common.py
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – Common Functions
|
||||
==============================
|
||||
@@ -29,13 +28,14 @@ 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__)
|
||||
|
||||
|
||||
def checkString(value, default, allowNone=False):
|
||||
"""Check if a variable is a string or a none.
|
||||
"""
|
||||
@@ -48,6 +48,7 @@ def checkString(value, default, allowNone=False):
|
||||
return str(value)
|
||||
return default
|
||||
|
||||
|
||||
def checkInt(value, default, allowNone=False):
|
||||
"""Check if a variable is an integer or a none.
|
||||
"""
|
||||
@@ -61,6 +62,7 @@ def checkInt(value, default, allowNone=False):
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def checkBool(value, default, allowNone=False):
|
||||
"""Check if a variable is a boolean or a none.
|
||||
"""
|
||||
@@ -85,6 +87,7 @@ def checkBool(value, default, allowNone=False):
|
||||
return default
|
||||
return default
|
||||
|
||||
|
||||
def checkHandle(value, default, allowNone=False):
|
||||
"""Check if a value is a handle.
|
||||
"""
|
||||
@@ -97,6 +100,7 @@ def checkHandle(value, default, allowNone=False):
|
||||
return str(value)
|
||||
return default
|
||||
|
||||
|
||||
def isHandle(theString):
|
||||
"""Check if a string is a valid novelWriter handle.
|
||||
Note: This is case sensitive. Must be lower case!
|
||||
@@ -110,6 +114,7 @@ def isHandle(theString):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def isTitleTag(theString):
|
||||
"""Check if a string is a valid title string.
|
||||
"""
|
||||
@@ -124,21 +129,25 @@ def isTitleTag(theString):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def isItemClass(theString):
|
||||
"""Check if an item is a calid nwItemClass identifier.
|
||||
"""
|
||||
return theString in nwItemClass.__members__
|
||||
|
||||
|
||||
def isItemType(theString):
|
||||
"""Check if an item is a calid nwItemType identifier.
|
||||
"""
|
||||
return theString in nwItemType.__members__
|
||||
|
||||
|
||||
def isItemLayout(theString):
|
||||
"""Check if an item is a calid nwItemLayout identifier.
|
||||
"""
|
||||
return theString in nwItemLayout.__members__
|
||||
|
||||
|
||||
def hexToInt(value, default=0):
|
||||
"""Convert a hex string to an integer.
|
||||
"""
|
||||
@@ -149,36 +158,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.
|
||||
@@ -199,6 +178,7 @@ def formatInt(theInt):
|
||||
|
||||
return str(theInt)
|
||||
|
||||
|
||||
def formatTimeStamp(theTime, fileSafe=False):
|
||||
"""Take a number (on the format returned by time.time()) and convert
|
||||
it to a timestamp string.
|
||||
@@ -208,6 +188,7 @@ def formatTimeStamp(theTime, fileSafe=False):
|
||||
else:
|
||||
return datetime.fromtimestamp(theTime).strftime(nwConst.FMT_TSTAMP)
|
||||
|
||||
|
||||
def formatTime(tS):
|
||||
"""Format a time in seconds in HH:MM:SS format or d-HH:MM:SS format
|
||||
if a full day or longer.
|
||||
@@ -219,6 +200,7 @@ def formatTime(tS):
|
||||
return f"{tS//3600:02d}:{tS%3600//60:02d}:{tS%60:02d}"
|
||||
return "ERROR"
|
||||
|
||||
|
||||
def splitVersionNumber(vString):
|
||||
""" Splits a version string on the form aa.bb.cc into major, minor
|
||||
and patch, and computes an integer value aabbcc.
|
||||
@@ -226,7 +208,7 @@ def splitVersionNumber(vString):
|
||||
vMajor = 0
|
||||
vMinor = 0
|
||||
vPatch = 0
|
||||
vInt = 0
|
||||
vInt = 0
|
||||
|
||||
vBits = vString.split(".")
|
||||
nBits = len(vBits)
|
||||
@@ -242,6 +224,7 @@ def splitVersionNumber(vString):
|
||||
|
||||
return [vMajor, vMinor, vPatch, vInt]
|
||||
|
||||
|
||||
def transferCase(theSource, theTarget):
|
||||
"""Transfers the case of the source word to the target word. This
|
||||
will consider all upper or lower, and first char capitalisation.
|
||||
@@ -263,37 +246,67 @@ def transferCase(theSource, theTarget):
|
||||
|
||||
return theResult
|
||||
|
||||
|
||||
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"
|
||||
elif secDiff < 3300: # 55 minutes
|
||||
return "%d minutes ago" % int(round(secDiff/60))
|
||||
elif secDiff < 5400: # 90 minutes
|
||||
return "an hour ago"
|
||||
elif secDiff < 84600: # 23.5 hours
|
||||
return "%d hours ago" % int(round(secDiff/3600))
|
||||
elif secDiff < 129600: # 1.5 days
|
||||
return "a day ago"
|
||||
elif secDiff < 561600: # 6.5 days
|
||||
return "%d days ago" % int(round(secDiff/86400))
|
||||
elif secDiff < 907200: # 10.5 days
|
||||
return "a week ago"
|
||||
elif secDiff < 2419200: # 28 days
|
||||
return "%d weeks ago" % int(round(secDiff/604800))
|
||||
elif secDiff < 3888000: # 45 days
|
||||
return "a month ago"
|
||||
elif secDiff < 29808000: # 345 days
|
||||
return "%d months ago" % int(round(secDiff/2592000))
|
||||
elif secDiff < 47336400: # 1.5 years
|
||||
return "a year ago"
|
||||
return QCoreApplication.translate(
|
||||
"Common", "a minute ago"
|
||||
)
|
||||
elif secDiff < 3300: # 55 minutes
|
||||
return QCoreApplication.translate(
|
||||
"Common", "{0} minutes ago"
|
||||
).format(int(round(secDiff/60)))
|
||||
elif secDiff < 5400: # 90 minutes
|
||||
return QCoreApplication.translate(
|
||||
"Common", "an hour ago"
|
||||
)
|
||||
elif secDiff < 84600: # 23.5 hours
|
||||
return QCoreApplication.translate(
|
||||
"Common", "{0} hours ago"
|
||||
).format(int(round(secDiff/3600)))
|
||||
elif secDiff < 129600: # 1.5 days
|
||||
return QCoreApplication.translate(
|
||||
"Common", "a day ago"
|
||||
)
|
||||
elif secDiff < 561600: # 6.5 days
|
||||
return QCoreApplication.translate(
|
||||
"Common", "{0} days ago"
|
||||
).format(int(round(secDiff/86400)))
|
||||
elif secDiff < 907200: # 10.5 days
|
||||
return QCoreApplication.translate(
|
||||
"Common", "a week ago"
|
||||
)
|
||||
elif secDiff < 2419200: # 28 days
|
||||
return QCoreApplication.translate(
|
||||
"Common", "{0} weeks ago"
|
||||
).format(int(round(secDiff/604800)))
|
||||
elif secDiff < 3888000: # 45 days
|
||||
return QCoreApplication.translate(
|
||||
"Common", "a month ago"
|
||||
)
|
||||
elif secDiff < 29808000: # 345 days
|
||||
return QCoreApplication.translate(
|
||||
"Common", "{0} months ago"
|
||||
).format(int(round(secDiff/2592000)))
|
||||
elif secDiff < 47336400: # 1.5 years
|
||||
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.
|
||||
@@ -304,6 +317,7 @@ def makeFileNameSafe(theText):
|
||||
cleanName += c
|
||||
return cleanName
|
||||
|
||||
|
||||
def getGuiItem(theName):
|
||||
"""Returns a QtWidget based on its objectName.
|
||||
"""
|
||||
@@ -311,3 +325,27 @@ 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
|
||||
|
||||
308
nw/config.py
308
nw/config.py
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – Config Class
|
||||
==========================
|
||||
@@ -24,24 +23,28 @@ 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 os
|
||||
import sys
|
||||
import json
|
||||
import shutil
|
||||
import logging
|
||||
import configparser
|
||||
import shutil
|
||||
import json
|
||||
import sys
|
||||
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__)
|
||||
|
||||
|
||||
class Config:
|
||||
|
||||
CNF_STR = 0
|
||||
@@ -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
|
||||
@@ -76,22 +82,29 @@ class Config:
|
||||
self.helpPath = None # The full path to the novelwriter .qhc help file
|
||||
|
||||
# Runtime Settings and Variables
|
||||
self.confChanged = False # True whenever the config has chenged, false after save
|
||||
self.hasHelp = False # True if the Qt help files are present in the assets folder
|
||||
self.confChanged = False # True whenever the config has chenged, false after save
|
||||
self.hasHelp = False # True if the Qt help files are present in the assets folder
|
||||
|
||||
## General
|
||||
# General
|
||||
self.guiTheme = "default"
|
||||
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.guiScale = 1.0 # Set automatically by Theme class
|
||||
self.lastNotes = "0x0" # The latest release notes that have been shown
|
||||
self.guiDark = False # Load icons for dark backgrounds, if available
|
||||
self.guiFont = "" # Defaults to system default font
|
||||
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
|
||||
|
||||
## Sizes
|
||||
# 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]
|
||||
@@ -101,62 +114,66 @@ class Config:
|
||||
self.outlnPanePos = [500, 150]
|
||||
self.isFullScreen = False
|
||||
|
||||
## Features
|
||||
self.hideVScroll = False # Hide vertical scroll bars on main widgets
|
||||
self.hideHScroll = False # Hide horizontal scroll bars on main widgets
|
||||
# Features
|
||||
self.hideVScroll = False # Hide vertical scroll bars on main widgets
|
||||
self.hideHScroll = False # Hide horizontal scroll bars on main widgets
|
||||
|
||||
## Project
|
||||
self.autoSaveProj = 60 # Interval for auto-saving project in seconds
|
||||
self.autoSaveDoc = 30 # Interval for auto-saving document in seconds
|
||||
# Project
|
||||
self.autoSaveProj = 60 # Interval for auto-saving project in seconds
|
||||
self.autoSaveDoc = 30 # Interval for auto-saving document in seconds
|
||||
|
||||
## Text Editor
|
||||
self.textFont = None # Editor font
|
||||
self.textSize = 12 # Editor font size
|
||||
self.textFixedW = True # Keep editor text fixed width
|
||||
self.textWidth = 600 # Editor text width
|
||||
self.textMargin = 40 # Editor/viewer text margin
|
||||
self.tabWidth = 40 # Editor tabulator width
|
||||
# Text Editor
|
||||
self.textFont = None # Editor font
|
||||
self.textSize = 12 # Editor font size
|
||||
self.textFixedW = True # Keep editor text fixed width
|
||||
self.textWidth = 600 # Editor text width
|
||||
self.textMargin = 40 # Editor/viewer text margin
|
||||
self.tabWidth = 40 # Editor tabulator width
|
||||
|
||||
self.focusWidth = 800 # Focus Mode text width
|
||||
self.hideFocusFooter = False # Hide document footer in Focus Mode
|
||||
self.showFullPath = True # Show full document path in editor header
|
||||
self.autoSelect = True # Auto-select word when applying format with no selection
|
||||
self.focusWidth = 800 # Focus Mode text width
|
||||
self.hideFocusFooter = False # Hide document footer in Focus Mode
|
||||
self.showFullPath = True # Show full document path in editor header
|
||||
self.autoSelect = True # Auto-select word when applying format with no selection
|
||||
|
||||
self.doJustify = False # Justify text
|
||||
self.showTabsNSpaces = False # Show tabs and spaces in edior
|
||||
self.showLineEndings = False # Show line endings in editor
|
||||
self.doJustify = False # Justify text
|
||||
self.showTabsNSpaces = False # Show tabs and spaces in edior
|
||||
self.showLineEndings = False # Show line endings in editor
|
||||
self.showMultiSpaces = True # Highlight multiple spaces in the text
|
||||
|
||||
self.doReplace = True # Enable auto-replace as you type
|
||||
self.doReplaceSQuote = True # Smart single quotes
|
||||
self.doReplaceDQuote = True # Smart double quotes
|
||||
self.doReplaceDash = True # Replace multiple hyphens with dashes
|
||||
self.doReplaceDots = True # Replace three dots with ellipsis
|
||||
self.doReplace = True # Enable auto-replace as you type
|
||||
self.doReplaceSQuote = True # Smart single quotes
|
||||
self.doReplaceDQuote = True # Smart double quotes
|
||||
self.doReplaceDash = True # Replace multiple hyphens with dashes
|
||||
self.doReplaceDots = True # Replace three dots with ellipsis
|
||||
|
||||
self.scrollPastEnd = True # Allow scrolling past end of document
|
||||
self.autoScroll = False # Typewriter-like scrolling
|
||||
self.autoScrollPos = 30 # Start point for typewriter-like scrolling
|
||||
self.scrollPastEnd = True # Allow scrolling past end of document
|
||||
self.autoScroll = False # Typewriter-like scrolling
|
||||
self.autoScrollPos = 30 # Start point for typewriter-like scrolling
|
||||
|
||||
self.wordCountTimer = 5.0 # Interval for word count update in seconds
|
||||
self.bigDocLimit = 800 # Size threshold for heavy editor features in kilobytes
|
||||
self.wordCountTimer = 5.0 # Interval for word count update in seconds
|
||||
self.bigDocLimit = 800 # Size threshold for heavy editor features in kilobytes
|
||||
|
||||
self.highlightQuotes = True # Highlight text in quotes
|
||||
self.allowOpenSQuote = False # Allow open-ended single quotes
|
||||
self.allowOpenDQuote = True # Allow open-ended double quotes
|
||||
self.highlightEmph = True # Add colour to text emphasis
|
||||
self.highlightQuotes = True # Highlight text in quotes
|
||||
self.allowOpenSQuote = False # Allow open-ended single quotes
|
||||
self.allowOpenDQuote = True # Allow open-ended double quotes
|
||||
self.highlightEmph = True # Add colour to text emphasis
|
||||
|
||||
self.stopWhenIdle = True # Stop the status bar clock when the user is idle
|
||||
self.userIdleTime = 300 # Time of inactivity to consider user idle
|
||||
self.stopWhenIdle = True # Stop the status bar clock when the user is idle
|
||||
self.userIdleTime = 300 # Time of inactivity to consider user idle
|
||||
|
||||
## User-Selected Symbols
|
||||
# User-Selected Symbols
|
||||
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
|
||||
# Spell Checking
|
||||
self.spellTool = None
|
||||
self.spellLanguage = None
|
||||
|
||||
## Search Bar Switches
|
||||
# Search Bar Switches
|
||||
self.searchCase = False
|
||||
self.searchWord = False
|
||||
self.searchRegEx = False
|
||||
@@ -164,12 +181,12 @@ class Config:
|
||||
self.searchNextFile = False
|
||||
self.searchMatchCap = False
|
||||
|
||||
## Backup
|
||||
# Backup
|
||||
self.backupPath = ""
|
||||
self.backupOnClose = False
|
||||
self.askBeforeBackup = True
|
||||
|
||||
## State
|
||||
# State
|
||||
self.showRefPanel = True
|
||||
self.viewComments = True
|
||||
self.viewSynopsis = True
|
||||
@@ -218,8 +235,8 @@ class Config:
|
||||
self.kernelVer = "Unknown"
|
||||
|
||||
# Packages
|
||||
self.hasEnchant = False # The pyenchant package
|
||||
self.hasAssistant = False # The Qt Assistant executable
|
||||
self.hasEnchant = False # The pyenchant package
|
||||
self.hasAssistant = False # The Qt Assistant executable
|
||||
|
||||
# Recent Cache
|
||||
self.recentProj = {}
|
||||
@@ -255,7 +272,7 @@ class Config:
|
||||
confRoot = QStandardPaths.writableLocation(QStandardPaths.ConfigLocation)
|
||||
self.confPath = os.path.join(os.path.abspath(confRoot), self.appHandle)
|
||||
else:
|
||||
logger.info("Setting config from alternative path: %s" % confPath)
|
||||
logger.info("Setting config from alternative path: %s", confPath)
|
||||
self.confPath = confPath
|
||||
|
||||
if dataPath is None:
|
||||
@@ -265,11 +282,11 @@ class Config:
|
||||
dataRoot = QStandardPaths.writableLocation(QStandardPaths.DataLocation)
|
||||
self.dataPath = os.path.join(os.path.abspath(dataRoot), self.appHandle)
|
||||
else:
|
||||
logger.info("Setting data path from alternative path: %s" % dataPath)
|
||||
logger.info("Setting data path from alternative path: %s", dataPath)
|
||||
self.dataPath = dataPath
|
||||
|
||||
logger.verbose("Config path: %s" % self.confPath)
|
||||
logger.verbose("Data path: %s" % self.dataPath)
|
||||
logger.verbose("Config path: %s", self.confPath)
|
||||
logger.verbose("Data path: %s", self.dataPath)
|
||||
|
||||
self.confFile = self.appHandle+".conf"
|
||||
self.lastPath = os.path.expanduser("~")
|
||||
@@ -290,8 +307,11 @@ class Config:
|
||||
self.iconPath = os.path.join(self.assetPath, "icons")
|
||||
self.appIcon = os.path.join(self.iconPath, "novelwriter.svg")
|
||||
|
||||
logger.verbose("App path: %s" % self.appPath)
|
||||
logger.verbose("Last path: %s" % self.lastPath)
|
||||
# Internationalisation
|
||||
self.nwLangPath = os.path.join(self.assetPath, "i18n")
|
||||
|
||||
logger.verbose("App path: %s", self.appPath)
|
||||
logger.verbose("Last path: %s", self.lastPath)
|
||||
|
||||
# If config folder does not exist, create it.
|
||||
# This assumes that the os config folder itself exists.
|
||||
@@ -299,7 +319,7 @@ class Config:
|
||||
try:
|
||||
os.mkdir(self.confPath)
|
||||
except Exception as e:
|
||||
logger.error("Could not create folder: %s" % self.confPath)
|
||||
logger.error("Could not create folder: %s", self.confPath)
|
||||
logException()
|
||||
self.hasError = True
|
||||
self.errData.append("Could not create folder: %s" % self.confPath)
|
||||
@@ -322,7 +342,7 @@ class Config:
|
||||
try:
|
||||
os.mkdir(self.dataPath)
|
||||
except Exception as e:
|
||||
logger.error("Could not create folder: %s" % self.dataPath)
|
||||
logger.error("Could not create folder: %s", self.dataPath)
|
||||
logException()
|
||||
self.hasError = True
|
||||
self.errData.append("Could not create folder: %s" % self.dataPath)
|
||||
@@ -354,6 +374,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", os.path.join(lngPath, lngFile))
|
||||
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.
|
||||
"""
|
||||
@@ -362,9 +433,9 @@ class Config:
|
||||
return False
|
||||
|
||||
cnfParse = configparser.ConfigParser()
|
||||
cnfPath = os.path.join(self.confPath, self.confFile)
|
||||
cnfPath = os.path.join(self.confPath, self.confFile)
|
||||
try:
|
||||
with open(cnfPath, mode="r", encoding="utf8") as inFile:
|
||||
with open(cnfPath, mode="r", encoding="utf-8") as inFile:
|
||||
cnfParse.read_file(inFile)
|
||||
except Exception as e:
|
||||
logger.error("Could not load config file")
|
||||
@@ -374,7 +445,7 @@ class Config:
|
||||
self.errData.append(str(e))
|
||||
return False
|
||||
|
||||
## Main
|
||||
# Main
|
||||
cnfSec = "Main"
|
||||
self.guiTheme = self._parseLine(
|
||||
cnfParse, cnfSec, "theme", self.CNF_STR, self.guiTheme
|
||||
@@ -397,12 +468,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
|
||||
# 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
|
||||
)
|
||||
@@ -434,7 +511,7 @@ class Config:
|
||||
cnfParse, cnfSec, "hidehscroll", self.CNF_BOOL, self.hideHScroll
|
||||
)
|
||||
|
||||
## Project
|
||||
# Project
|
||||
cnfSec = "Project"
|
||||
self.autoSaveProj = self._parseLine(
|
||||
cnfParse, cnfSec, "autosaveproject", self.CNF_INT, self.autoSaveProj
|
||||
@@ -443,7 +520,7 @@ class Config:
|
||||
cnfParse, cnfSec, "autosavedoc", self.CNF_INT, self.autoSaveDoc
|
||||
)
|
||||
|
||||
## Editor
|
||||
# Editor
|
||||
cnfSec = "Editor"
|
||||
self.textFont = self._parseLine(
|
||||
cnfParse, cnfSec, "textfont", self.CNF_STR, self.textFont
|
||||
@@ -505,6 +582,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
|
||||
)
|
||||
@@ -517,6 +603,9 @@ class Config:
|
||||
self.showLineEndings = self._parseLine(
|
||||
cnfParse, cnfSec, "showlineendings", self.CNF_BOOL, self.showLineEndings
|
||||
)
|
||||
self.showMultiSpaces = self._parseLine(
|
||||
cnfParse, cnfSec, "showmultispaces", self.CNF_BOOL, self.showMultiSpaces
|
||||
)
|
||||
self.bigDocLimit = self._parseLine(
|
||||
cnfParse, cnfSec, "bigdoclimit", self.CNF_INT, self.bigDocLimit
|
||||
)
|
||||
@@ -542,7 +631,7 @@ class Config:
|
||||
cnfParse, cnfSec, "useridletime", self.CNF_INT, self.userIdleTime
|
||||
)
|
||||
|
||||
## Backup
|
||||
# Backup
|
||||
cnfSec = "Backup"
|
||||
self.backupPath = self._parseLine(
|
||||
cnfParse, cnfSec, "backuppath", self.CNF_STR, self.backupPath
|
||||
@@ -554,7 +643,7 @@ class Config:
|
||||
cnfParse, cnfSec, "askbeforebackup", self.CNF_BOOL, self.askBeforeBackup
|
||||
)
|
||||
|
||||
## State
|
||||
# State
|
||||
cnfSec = "State"
|
||||
self.showRefPanel = self._parseLine(
|
||||
cnfParse, cnfSec, "showrefpanel", self.CNF_BOOL, self.showRefPanel
|
||||
@@ -584,7 +673,7 @@ class Config:
|
||||
cnfParse, cnfSec, "searchmatchcap", self.CNF_BOOL, self.searchMatchCap
|
||||
)
|
||||
|
||||
## Path
|
||||
# Path
|
||||
cnfSec = "Path"
|
||||
self.lastPath = self._parseLine(
|
||||
cnfParse, cnfSec, "lastpath", self.CNF_STR, self.lastPath
|
||||
@@ -615,7 +704,7 @@ class Config:
|
||||
|
||||
# Set options
|
||||
|
||||
## Main
|
||||
# Main
|
||||
cnfSec = "Main"
|
||||
cnfParse.add_section(cnfSec)
|
||||
cnfParse.set(cnfSec, "timestamp", formatTimeStamp(time()))
|
||||
@@ -626,11 +715,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
|
||||
# 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))
|
||||
@@ -642,13 +733,13 @@ class Config:
|
||||
cnfParse.set(cnfSec, "hidevscroll", str(self.hideVScroll))
|
||||
cnfParse.set(cnfSec, "hidehscroll", str(self.hideHScroll))
|
||||
|
||||
## Project
|
||||
# Project
|
||||
cnfSec = "Project"
|
||||
cnfParse.add_section(cnfSec)
|
||||
cnfParse.set(cnfSec, "autosaveproject", str(self.autoSaveProj))
|
||||
cnfParse.set(cnfSec, "autosavedoc", str(self.autoSaveDoc))
|
||||
|
||||
## Editor
|
||||
# Editor
|
||||
cnfSec = "Editor"
|
||||
cnfParse.add_section(cnfSec)
|
||||
cnfParse.set(cnfSec, "textfont", str(self.textFont))
|
||||
@@ -671,10 +762,14 @@ 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))
|
||||
cnfParse.set(cnfSec, "showlineendings", str(self.showLineEndings))
|
||||
cnfParse.set(cnfSec, "showmultispaces", str(self.showMultiSpaces))
|
||||
cnfParse.set(cnfSec, "bigdoclimit", str(self.bigDocLimit))
|
||||
cnfParse.set(cnfSec, "showfullpath", str(self.showFullPath))
|
||||
cnfParse.set(cnfSec, "highlightquotes", str(self.highlightQuotes))
|
||||
@@ -684,14 +779,14 @@ class Config:
|
||||
cnfParse.set(cnfSec, "stopwhenidle", str(self.stopWhenIdle))
|
||||
cnfParse.set(cnfSec, "useridletime", str(self.userIdleTime))
|
||||
|
||||
## Backup
|
||||
# Backup
|
||||
cnfSec = "Backup"
|
||||
cnfParse.add_section(cnfSec)
|
||||
cnfParse.set(cnfSec, "backuppath", str(self.backupPath))
|
||||
cnfParse.set(cnfSec, "backuponclose", str(self.backupOnClose))
|
||||
cnfParse.set(cnfSec, "askbeforebackup", str(self.askBeforeBackup))
|
||||
|
||||
## State
|
||||
# State
|
||||
cnfSec = "State"
|
||||
cnfParse.add_section(cnfSec)
|
||||
cnfParse.set(cnfSec, "showrefpanel", str(self.showRefPanel))
|
||||
@@ -704,7 +799,7 @@ class Config:
|
||||
cnfParse.set(cnfSec, "searchnextfile", str(self.searchNextFile))
|
||||
cnfParse.set(cnfSec, "searchmatchcap", str(self.searchMatchCap))
|
||||
|
||||
## Path
|
||||
# Path
|
||||
cnfSec = "Path"
|
||||
cnfParse.add_section(cnfSec)
|
||||
cnfParse.set(cnfSec, "lastpath", str(self.lastPath))
|
||||
@@ -712,7 +807,7 @@ class Config:
|
||||
# Write config file
|
||||
cnfPath = os.path.join(self.confPath, self.confFile)
|
||||
try:
|
||||
with open(cnfPath, mode="w", encoding="utf8") as outFile:
|
||||
with open(cnfPath, mode="w", encoding="utf-8") as outFile:
|
||||
cnfParse.write(outFile)
|
||||
self.confChanged = False
|
||||
except Exception as e:
|
||||
@@ -736,13 +831,13 @@ class Config:
|
||||
|
||||
if os.path.isfile(cacheFile):
|
||||
try:
|
||||
with open(cacheFile, mode="r", encoding="utf8") as inFile:
|
||||
with open(cacheFile, mode="r", encoding="utf-8") as inFile:
|
||||
theData = json.load(inFile)
|
||||
|
||||
for projPath in theData.keys():
|
||||
theEntry = theData[projPath]
|
||||
theTitle = ""
|
||||
lastTime = 0
|
||||
theEntry = theData[projPath]
|
||||
theTitle = ""
|
||||
lastTime = 0
|
||||
wordCount = 0
|
||||
if "title" in theEntry.keys():
|
||||
theTitle = theEntry["title"]
|
||||
@@ -751,9 +846,9 @@ class Config:
|
||||
if "words" in theEntry.keys():
|
||||
wordCount = int(theEntry["words"])
|
||||
self.recentProj[projPath] = {
|
||||
"title" : theTitle,
|
||||
"time" : lastTime,
|
||||
"words" : wordCount,
|
||||
"title": theTitle,
|
||||
"time": lastTime,
|
||||
"words": wordCount,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
@@ -774,7 +869,7 @@ class Config:
|
||||
cacheTemp = os.path.join(self.dataPath, nwFiles.RECENT_FILE+"~")
|
||||
|
||||
try:
|
||||
with open(cacheTemp, mode="w+", encoding="utf8") as outFile:
|
||||
with open(cacheTemp, mode="w+", encoding="utf-8") as outFile:
|
||||
json.dump(self.recentProj, outFile, indent=2)
|
||||
except Exception as e:
|
||||
self.hasError = True
|
||||
@@ -792,9 +887,9 @@ class Config:
|
||||
"""Add or update recent cache information o9n a given project.
|
||||
"""
|
||||
self.recentProj[os.path.abspath(projPath)] = {
|
||||
"title" : projTitle,
|
||||
"time" : int(saveTime),
|
||||
"words" : int(wordCount),
|
||||
"title": projTitle,
|
||||
"time": int(saveTime),
|
||||
"words": int(wordCount),
|
||||
}
|
||||
return True
|
||||
|
||||
@@ -803,10 +898,10 @@ class Config:
|
||||
"""
|
||||
if thePath in self.recentProj:
|
||||
del self.recentProj[thePath]
|
||||
logger.verbose("Removed recent: %s" % thePath)
|
||||
logger.verbose("Removed recent: %s", thePath)
|
||||
self.saveRecentCache()
|
||||
else:
|
||||
logger.error("Unknown recent: %s" % thePath)
|
||||
logger.error("Unknown recent: %s", thePath)
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -851,6 +946,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 +1015,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]
|
||||
|
||||
@@ -1015,9 +1119,9 @@ class Config:
|
||||
"""Cheks if we have the optional packages used by some features.
|
||||
"""
|
||||
try:
|
||||
import enchant # noqa: F401
|
||||
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 +1129,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")
|
||||
|
||||
|
||||
451
nw/constants.py
Normal file
451
nw/constants.py
Normal file
@@ -0,0 +1,451 @@
|
||||
"""
|
||||
novelWriter – Constants
|
||||
=======================
|
||||
Constants and maps for translating flags and enums to text
|
||||
|
||||
File History:
|
||||
Created: 2019-04-28 [0.0.1]
|
||||
|
||||
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/>.
|
||||
"""
|
||||
|
||||
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():
|
||||
|
||||
# Date and Time Formats
|
||||
FMT_TSTAMP = "%Y-%m-%d %H:%M:%S" # Default format
|
||||
FMT_FSTAMP = "%Y-%m-%d %H.%M.%S" # FileName safe format
|
||||
FMT_DSTAMP = "%Y-%m-%d" # Date only format
|
||||
|
||||
# Various Hard Limits
|
||||
MAX_DEPTH = 30 # Maximum folder depth of a project
|
||||
MAX_DOCSIZE = 5000000 # Maxium size of a single document
|
||||
MAX_BUILDSIZE = 10000000 # Maxium size of a project build
|
||||
|
||||
# Spell Check Providers
|
||||
SP_INTERNAL = "internal"
|
||||
SP_ENCHANT = "enchant"
|
||||
|
||||
# END Class nwConst
|
||||
|
||||
|
||||
class nwLists():
|
||||
"""Lists used for grouping various other constants.
|
||||
"""
|
||||
# Regular user-accessible item types
|
||||
REG_TYPES = {nwItemType.ROOT, nwItemType.FOLDER, nwItemType.FILE}
|
||||
|
||||
# Item classes where the full list of novel layouts are allowed
|
||||
CLS_NOVEL = {nwItemClass.NOVEL, nwItemClass.ARCHIVE}
|
||||
|
||||
# Item classes which do not require items to have same class
|
||||
FREE_CLASS = {nwItemClass.ARCHIVE, nwItemClass.TRASH}
|
||||
|
||||
# END Class nwLists
|
||||
|
||||
|
||||
class nwRegEx():
|
||||
|
||||
FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)"
|
||||
FMT_EB = r"(?<![\w\\])([\*]{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
|
||||
FMT_ST = r"(?<![\w\\])([~]{2})(?![\s~])(.+?)(?<![\s\\])(\1)(?!\w)"
|
||||
|
||||
# END Class nwRegEx
|
||||
|
||||
|
||||
class nwFiles():
|
||||
|
||||
PROJ_FILE = "nwProject.nwx"
|
||||
PROJ_DICT = "wordlist.txt"
|
||||
PROJ_LOCK = "nwProject.lock"
|
||||
TOC_TXT = "ToC.txt"
|
||||
SESS_STATS = "sessionStats.log"
|
||||
INDEX_FILE = "tagsIndex.json"
|
||||
OPTS_FILE = "guiOptions.json"
|
||||
RECENT_FILE = "recentProjects.json"
|
||||
BUILD_CACHE = "prevBuild.json"
|
||||
|
||||
# END Class nwFiles
|
||||
|
||||
|
||||
class nwKeyWords:
|
||||
|
||||
TAG_KEY = "@tag"
|
||||
POV_KEY = "@pov"
|
||||
FOCUS_KEY = "@focus"
|
||||
CHAR_KEY = "@char"
|
||||
PLOT_KEY = "@plot"
|
||||
TIME_KEY = "@time"
|
||||
WORLD_KEY = "@location"
|
||||
OBJECT_KEY = "@object"
|
||||
ENTITY_KEY = "@entity"
|
||||
CUSTOM_KEY = "@custom"
|
||||
|
||||
# Set of Valid Keys
|
||||
VALID_KEYS = {
|
||||
TAG_KEY, POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY,
|
||||
WORLD_KEY, OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY
|
||||
}
|
||||
|
||||
# Map from Keys to Item Class
|
||||
KEY_CLASS = {
|
||||
POV_KEY: nwItemClass.CHARACTER,
|
||||
FOCUS_KEY: nwItemClass.CHARACTER,
|
||||
CHAR_KEY: nwItemClass.CHARACTER,
|
||||
PLOT_KEY: nwItemClass.PLOT,
|
||||
TIME_KEY: nwItemClass.TIMELINE,
|
||||
WORLD_KEY: nwItemClass.WORLD,
|
||||
OBJECT_KEY: nwItemClass.OBJECT,
|
||||
ENTITY_KEY: nwItemClass.ENTITY,
|
||||
CUSTOM_KEY: nwItemClass.CUSTOM,
|
||||
}
|
||||
|
||||
# END Class nwKeyWords
|
||||
|
||||
|
||||
class nwLabels():
|
||||
|
||||
CLASS_NAME = {
|
||||
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",
|
||||
nwItemClass.NOVEL: "N",
|
||||
nwItemClass.PLOT: "P",
|
||||
nwItemClass.CHARACTER: "C",
|
||||
nwItemClass.WORLD: "L",
|
||||
nwItemClass.TIMELINE: "T",
|
||||
nwItemClass.OBJECT: "O",
|
||||
nwItemClass.ENTITY: "E",
|
||||
nwItemClass.CUSTOM: "X",
|
||||
nwItemClass.ARCHIVE: "U",
|
||||
nwItemClass.TRASH: "R",
|
||||
}
|
||||
CLASS_ICON = {
|
||||
nwItemClass.NO_CLASS: "cls_none",
|
||||
nwItemClass.NOVEL: "cls_novel",
|
||||
nwItemClass.PLOT: "cls_plot",
|
||||
nwItemClass.CHARACTER: "cls_character",
|
||||
nwItemClass.WORLD: "cls_world",
|
||||
nwItemClass.TIMELINE: "cls_timeline",
|
||||
nwItemClass.OBJECT: "cls_object",
|
||||
nwItemClass.ENTITY: "cls_entity",
|
||||
nwItemClass.CUSTOM: "cls_custom",
|
||||
nwItemClass.ARCHIVE: "cls_archive",
|
||||
nwItemClass.TRASH: "cls_trash",
|
||||
}
|
||||
LAYOUT_NAME = {
|
||||
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",
|
||||
nwItemLayout.TITLE: "Tt",
|
||||
nwItemLayout.BOOK: "Bk",
|
||||
nwItemLayout.PAGE: "Pg",
|
||||
nwItemLayout.PARTITION: "Pt",
|
||||
nwItemLayout.UNNUMBERED: "Un",
|
||||
nwItemLayout.CHAPTER: "Ch",
|
||||
nwItemLayout.SCENE: "Sc",
|
||||
nwItemLayout.NOTE: "Nt",
|
||||
}
|
||||
KEY_NAME = {
|
||||
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: 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],
|
||||
nwOutline.WORLD: KEY_NAME[nwKeyWords.WORLD_KEY],
|
||||
nwOutline.OBJECT: KEY_NAME[nwKeyWords.OBJECT_KEY],
|
||||
nwOutline.ENTITY: KEY_NAME[nwKeyWords.ENTITY_KEY],
|
||||
nwOutline.CUSTOM: KEY_NAME[nwKeyWords.CUSTOM_KEY],
|
||||
nwOutline.SYNOP: QT_TRANSLATE_NOOP("Constant", "Synopsis"),
|
||||
}
|
||||
|
||||
# END Class nwLabels
|
||||
|
||||
|
||||
class nwQuotes():
|
||||
"""Allowed quotation marks.
|
||||
Source: https://en.wikipedia.org/wiki/Quotation_mark
|
||||
"""
|
||||
SYMBOLS = {
|
||||
"\u0027": QT_TRANSLATE_NOOP("Constant", "Straight single quotation mark"),
|
||||
"\u0022": QT_TRANSLATE_NOOP("Constant", "Straight double 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": 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": 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
|
||||
|
||||
|
||||
class nwUnicode:
|
||||
"""Supported unicode character constants and their HTML equivalents.
|
||||
"""
|
||||
# Unicode Constants
|
||||
# =================
|
||||
|
||||
# Quotation Marks
|
||||
U_QUOT = "\u0022" # Quotation mark
|
||||
U_APOS = "\u0027" # Apostrophe
|
||||
U_LAQUO = "\u00ab" # Left-pointing double angle quotation mark
|
||||
U_RAQUO = "\u00bb" # Right-pointing double angle quotation mark
|
||||
U_LSQUO = "\u2018" # Left single quotation mark
|
||||
U_RSQUO = "\u2019" # Right single quotation mark
|
||||
U_SBQUO = "\u201a" # Single low-9 quotation mark
|
||||
U_SUQUO = "\u201b" # Single high-reversed-9 quotation mark
|
||||
U_LDQUO = "\u201c" # Left double quotation mark
|
||||
U_RDQUO = "\u201d" # Right double quotation mark
|
||||
U_BDQUO = "\u201e" # Double low-9 quotation mark
|
||||
U_UDQUO = "\u201f" # Double high-reversed-9 quotation mark
|
||||
U_LSAQUO = "\u2039" # Single left-pointing angle quotation mark
|
||||
U_RSAQUO = "\u203a" # Single right-pointing angle quotation mark
|
||||
U_BDRQUO = "\u2e42" # Double low-reversed-9 quotation mark
|
||||
U_LCQUO = "\u300c" # Left corner bracket
|
||||
U_RCQUO = "\u300d" # Right corner bracket
|
||||
U_LWCQUO = "\u300e" # Left white corner bracket
|
||||
U_RWCQUO = "\u300f" # Right white corner bracket
|
||||
|
||||
# Punctuation
|
||||
U_FGDASH = "\u2012" # Figure dash
|
||||
U_ENDASH = "\u2013" # Short dash
|
||||
U_EMDASH = "\u2014" # Long dash
|
||||
U_HBAR = "\u2015" # Horizontal bar
|
||||
U_HELLIP = "\u2026" # Ellipsis
|
||||
U_MAPOSS = "\u02bc" # Modifier letter single apostrophe
|
||||
U_PRIME = "\u2032" # Prime
|
||||
U_DPRIME = "\u2033" # Double prime
|
||||
|
||||
# Spaces and Lines
|
||||
U_NBSP = "\u00a0" # Non-breaking space
|
||||
U_THSP = "\u2009" # Thin space
|
||||
U_THNBSP = "\u202f" # Thin non-breaking space
|
||||
U_ENSP = "\u2002" # Short (en) space
|
||||
U_EMSP = "\u2003" # Long (em) space
|
||||
U_LSEP = "\u2028" # Line separator
|
||||
U_PSEP = "\u2029" # Paragraph separator
|
||||
|
||||
# Symbols
|
||||
U_CHECK = "\u2714" # Heavy check mark
|
||||
U_CROSS = "\u2715" # Heavy cross mark
|
||||
U_BULL = "\u2022" # List bullet
|
||||
U_TRBULL = "\u2023" # Triangle bullet
|
||||
U_HYBULL = "\u2043" # Hyphen bullet
|
||||
U_FLOWER = "\u2055" # Flower punctuation mark
|
||||
U_PERMIL = "\u2030" # Per mille sign
|
||||
U_DEGREE = "\u00b0" # Degree symbol
|
||||
U_MINUS = "\u2212" # Minus sign
|
||||
U_TIMES = "\u00d7" # Multiplaction sign
|
||||
U_DIVIDE = "\u00f7" # Division sign
|
||||
|
||||
# Arrows
|
||||
U_UTRI = "\u25b2" # Up-pointing triangle
|
||||
U_UTRIS = "\u25b4" # Up-pointing triangle, small
|
||||
U_RTRI = "\u25b6" # Right-pointing triangle
|
||||
U_RTRIS = "\u25b8" # Right-pointing triangle, small
|
||||
U_DTRI = "\u25bc" # Down-pointing triangle
|
||||
U_DTRIS = "\u25be" # Down-pointing triangle, small
|
||||
U_LTRI = "\u25c0" # Left-pointing triangle
|
||||
U_LTRIS = "\u25c2" # Left-pointing triangle, small
|
||||
|
||||
# HTML Equivalents
|
||||
# ================
|
||||
|
||||
# Quotes
|
||||
H_QUOT = """
|
||||
H_APOS = "'"
|
||||
H_LAQUO = "«"
|
||||
H_RAQUO = "»"
|
||||
H_LSQUO = "‘"
|
||||
H_RSQUO = "’"
|
||||
H_SBQUO = "‚"
|
||||
H_SUQUO = "‛"
|
||||
H_LDQUO = "“"
|
||||
H_RDQUO = "”"
|
||||
H_BDQUO = "„"
|
||||
H_UDQUO = "‟"
|
||||
H_LSAQUO = "‹"
|
||||
H_RSAQUO = "›"
|
||||
H_BDRQUO = "⹂"
|
||||
H_LCQUO = "「"
|
||||
H_RCQUO = "」"
|
||||
H_LWCQUO = "『"
|
||||
H_RWCQUO = "』"
|
||||
|
||||
# Punctuation
|
||||
H_FGDASH = "‒"
|
||||
H_ENDASH = "–"
|
||||
H_EMDASH = "—"
|
||||
H_HBAR = "―"
|
||||
H_HELLIP = "…"
|
||||
H_MAPOSS = "ʼ"
|
||||
H_PRIME = "′"
|
||||
H_DPRIME = "″"
|
||||
|
||||
# Spaces
|
||||
H_NBSP = " "
|
||||
H_THSP = " "
|
||||
H_THNBSP = " "
|
||||
H_ENSP = " "
|
||||
H_EMSP = " "
|
||||
|
||||
# Symbols
|
||||
H_CHECK = "✔"
|
||||
H_CROSS = "✕"
|
||||
H_BULL = "•"
|
||||
H_TRBULL = "‣"
|
||||
H_HYBULL = "⁃"
|
||||
H_FLOWER = "⁕"
|
||||
H_PERMIL = "‰"
|
||||
H_DEGREE = "°"
|
||||
H_MINUS = "−"
|
||||
H_TIMES = "×"
|
||||
H_DIVIDE = "÷"
|
||||
|
||||
# Arrows
|
||||
H_UTRI = "▲"
|
||||
H_UTRIS = "▴"
|
||||
H_RTRI = "▶"
|
||||
H_RTRIS = "▸"
|
||||
H_DTRI = "▼"
|
||||
H_DTRIS = "▾"
|
||||
H_LTRI = "◀"
|
||||
H_LTRIS = "◂"
|
||||
|
||||
# END Class nwUnicode
|
||||
|
||||
|
||||
class nwHtmlUnicode():
|
||||
|
||||
U_TO_H = {
|
||||
# Quotes
|
||||
nwUnicode.U_QUOT: nwUnicode.H_QUOT,
|
||||
nwUnicode.U_APOS: nwUnicode.H_APOS,
|
||||
nwUnicode.U_LAQUO: nwUnicode.H_LAQUO,
|
||||
nwUnicode.U_RAQUO: nwUnicode.H_RAQUO,
|
||||
nwUnicode.U_LSQUO: nwUnicode.H_LSQUO,
|
||||
nwUnicode.U_RSQUO: nwUnicode.H_RSQUO,
|
||||
nwUnicode.U_SBQUO: nwUnicode.H_SBQUO,
|
||||
nwUnicode.U_SUQUO: nwUnicode.H_SUQUO,
|
||||
nwUnicode.U_LDQUO: nwUnicode.H_LDQUO,
|
||||
nwUnicode.U_RDQUO: nwUnicode.H_RDQUO,
|
||||
nwUnicode.U_BDQUO: nwUnicode.H_BDQUO,
|
||||
nwUnicode.U_UDQUO: nwUnicode.H_UDQUO,
|
||||
nwUnicode.U_LSAQUO: nwUnicode.H_LSAQUO,
|
||||
nwUnicode.U_RSAQUO: nwUnicode.H_RSAQUO,
|
||||
nwUnicode.U_BDRQUO: nwUnicode.H_BDRQUO,
|
||||
nwUnicode.U_LCQUO: nwUnicode.H_LCQUO,
|
||||
nwUnicode.U_RCQUO: nwUnicode.H_RCQUO,
|
||||
nwUnicode.U_LWCQUO: nwUnicode.H_LWCQUO,
|
||||
nwUnicode.U_RWCQUO: nwUnicode.H_RWCQUO,
|
||||
|
||||
# Punctuation
|
||||
nwUnicode.U_FGDASH: nwUnicode.H_FGDASH,
|
||||
nwUnicode.U_ENDASH: nwUnicode.H_ENDASH,
|
||||
nwUnicode.U_EMDASH: nwUnicode.H_EMDASH,
|
||||
nwUnicode.U_HBAR: nwUnicode.H_HBAR,
|
||||
nwUnicode.U_HELLIP: nwUnicode.H_HELLIP,
|
||||
nwUnicode.U_MAPOSS: nwUnicode.H_MAPOSS,
|
||||
nwUnicode.U_PRIME: nwUnicode.H_PRIME,
|
||||
nwUnicode.U_DPRIME: nwUnicode.H_DPRIME,
|
||||
|
||||
# Spaces
|
||||
nwUnicode.U_NBSP: nwUnicode.H_NBSP,
|
||||
nwUnicode.U_THSP: nwUnicode.H_THSP,
|
||||
nwUnicode.U_THNBSP: nwUnicode.H_THNBSP,
|
||||
nwUnicode.U_ENSP: nwUnicode.H_ENSP,
|
||||
nwUnicode.U_EMSP: nwUnicode.H_EMSP,
|
||||
|
||||
# Symbols
|
||||
nwUnicode.U_CHECK: nwUnicode.H_CHECK,
|
||||
nwUnicode.U_CROSS: nwUnicode.H_CROSS,
|
||||
nwUnicode.U_BULL: nwUnicode.H_BULL,
|
||||
nwUnicode.U_TRBULL: nwUnicode.H_TRBULL,
|
||||
nwUnicode.U_HYBULL: nwUnicode.H_HYBULL,
|
||||
nwUnicode.U_FLOWER: nwUnicode.H_FLOWER,
|
||||
nwUnicode.U_PERMIL: nwUnicode.H_PERMIL,
|
||||
nwUnicode.U_DEGREE: nwUnicode.H_DEGREE,
|
||||
nwUnicode.U_MINUS: nwUnicode.H_MINUS,
|
||||
nwUnicode.U_TIMES: nwUnicode.H_TIMES,
|
||||
nwUnicode.U_DIVIDE: nwUnicode.H_DIVIDE,
|
||||
}
|
||||
|
||||
# END Class nwHtmlUnicode
|
||||
@@ -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,437 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – Constants
|
||||
=======================
|
||||
Constants and maps for translating flags and enums to text
|
||||
|
||||
File History:
|
||||
Created: 2019-04-28 [0.0.1]
|
||||
|
||||
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/>.
|
||||
"""
|
||||
|
||||
from nw.constants.enum import (
|
||||
nwItemClass, nwItemLayout, nwItemType, nwOutline
|
||||
)
|
||||
|
||||
class nwConst():
|
||||
|
||||
# Date and Time Formats
|
||||
FMT_TSTAMP = "%Y-%m-%d %H:%M:%S" # Default format
|
||||
FMT_FSTAMP = "%Y-%m-%d %H.%M.%S" # FileName safe format
|
||||
FMT_DSTAMP = "%Y-%m-%d" # Date only format
|
||||
|
||||
# Various Hard Limits
|
||||
MAX_DEPTH = 30 # Maximum folder depth of a project
|
||||
MAX_DOCSIZE = 5000000 # Maxium size of a single document
|
||||
MAX_BUILDSIZE = 10000000 # Maxium size of a project build
|
||||
|
||||
# Spell Check Providers
|
||||
SP_INTERNAL = "internal"
|
||||
SP_ENCHANT = "enchant"
|
||||
|
||||
# END Class nwConst
|
||||
|
||||
class nwLists():
|
||||
"""Lists used for grouping various other constants.
|
||||
"""
|
||||
# Regular user-accessible item types
|
||||
REG_TYPES = {nwItemType.ROOT, nwItemType.FOLDER, nwItemType.FILE}
|
||||
|
||||
# Item classes where the full list of novel layouts are allowed
|
||||
CLS_NOVEL = {nwItemClass.NOVEL, nwItemClass.ARCHIVE}
|
||||
|
||||
# Item classes which do not require items to have same class
|
||||
FREE_CLASS = {nwItemClass.ARCHIVE, nwItemClass.TRASH}
|
||||
|
||||
# END Class nwLists
|
||||
|
||||
class nwRegEx():
|
||||
|
||||
FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)"
|
||||
FMT_EB = r"(?<![\w\\])([\*]{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
|
||||
FMT_ST = r"(?<![\w\\])([~]{2})(?![\s~])(.+?)(?<![\s\\])(\1)(?!\w)"
|
||||
|
||||
# END Class nwRegEx
|
||||
|
||||
class nwFiles():
|
||||
|
||||
PROJ_FILE = "nwProject.nwx"
|
||||
PROJ_DICT = "wordlist.txt"
|
||||
PROJ_LOCK = "nwProject.lock"
|
||||
TOC_TXT = "ToC.txt"
|
||||
SESS_STATS = "sessionStats.log"
|
||||
INDEX_FILE = "tagsIndex.json"
|
||||
OPTS_FILE = "guiOptions.json"
|
||||
RECENT_FILE = "recentProjects.json"
|
||||
BUILD_CACHE = "prevBuild.json"
|
||||
|
||||
# END Class nwFiles
|
||||
|
||||
class nwKeyWords:
|
||||
|
||||
TAG_KEY = "@tag"
|
||||
POV_KEY = "@pov"
|
||||
FOCUS_KEY = "@focus"
|
||||
CHAR_KEY = "@char"
|
||||
PLOT_KEY = "@plot"
|
||||
TIME_KEY = "@time"
|
||||
WORLD_KEY = "@location"
|
||||
OBJECT_KEY = "@object"
|
||||
ENTITY_KEY = "@entity"
|
||||
CUSTOM_KEY = "@custom"
|
||||
|
||||
# Set of Valid Keys
|
||||
VALID_KEYS = {
|
||||
TAG_KEY, POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY,
|
||||
WORLD_KEY, OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY
|
||||
}
|
||||
|
||||
# Map from Keys to Item Class
|
||||
KEY_CLASS = {
|
||||
POV_KEY : nwItemClass.CHARACTER,
|
||||
FOCUS_KEY : nwItemClass.CHARACTER,
|
||||
CHAR_KEY : nwItemClass.CHARACTER,
|
||||
PLOT_KEY : nwItemClass.PLOT,
|
||||
TIME_KEY : nwItemClass.TIMELINE,
|
||||
WORLD_KEY : nwItemClass.WORLD,
|
||||
OBJECT_KEY : nwItemClass.OBJECT,
|
||||
ENTITY_KEY : nwItemClass.ENTITY,
|
||||
CUSTOM_KEY : nwItemClass.CUSTOM,
|
||||
}
|
||||
|
||||
# END 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",
|
||||
}
|
||||
CLASS_FLAG = {
|
||||
nwItemClass.NO_CLASS : "0",
|
||||
nwItemClass.NOVEL : "N",
|
||||
nwItemClass.PLOT : "P",
|
||||
nwItemClass.CHARACTER : "C",
|
||||
nwItemClass.WORLD : "L",
|
||||
nwItemClass.TIMELINE : "T",
|
||||
nwItemClass.OBJECT : "O",
|
||||
nwItemClass.ENTITY : "E",
|
||||
nwItemClass.CUSTOM : "X",
|
||||
nwItemClass.ARCHIVE : "U",
|
||||
nwItemClass.TRASH : "R",
|
||||
}
|
||||
CLASS_ICON = {
|
||||
nwItemClass.NO_CLASS : "cls_none",
|
||||
nwItemClass.NOVEL : "cls_novel",
|
||||
nwItemClass.PLOT : "cls_plot",
|
||||
nwItemClass.CHARACTER : "cls_character",
|
||||
nwItemClass.WORLD : "cls_world",
|
||||
nwItemClass.TIMELINE : "cls_timeline",
|
||||
nwItemClass.OBJECT : "cls_object",
|
||||
nwItemClass.ENTITY : "cls_entity",
|
||||
nwItemClass.CUSTOM : "cls_custom",
|
||||
nwItemClass.ARCHIVE : "cls_archive",
|
||||
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",
|
||||
}
|
||||
LAYOUT_FLAG = {
|
||||
nwItemLayout.NO_LAYOUT : "Xo",
|
||||
nwItemLayout.TITLE : "Tt",
|
||||
nwItemLayout.BOOK : "Bk",
|
||||
nwItemLayout.PAGE : "Pg",
|
||||
nwItemLayout.PARTITION : "Pt",
|
||||
nwItemLayout.UNNUMBERED : "Un",
|
||||
nwItemLayout.CHAPTER : "Ch",
|
||||
nwItemLayout.SCENE : "Sc",
|
||||
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",
|
||||
}
|
||||
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.CHAR : KEY_NAME[nwKeyWords.CHAR_KEY],
|
||||
nwOutline.PLOT : KEY_NAME[nwKeyWords.PLOT_KEY],
|
||||
nwOutline.TIME : KEY_NAME[nwKeyWords.TIME_KEY],
|
||||
nwOutline.WORLD : KEY_NAME[nwKeyWords.WORLD_KEY],
|
||||
nwOutline.OBJECT : KEY_NAME[nwKeyWords.OBJECT_KEY],
|
||||
nwOutline.ENTITY : KEY_NAME[nwKeyWords.ENTITY_KEY],
|
||||
nwOutline.CUSTOM : KEY_NAME[nwKeyWords.CUSTOM_KEY],
|
||||
nwOutline.SYNOP : "Synopsis",
|
||||
}
|
||||
|
||||
# END Class nwLabels
|
||||
|
||||
class nwQuotes():
|
||||
"""Allowed quotation marks.
|
||||
Source: https://en.wikipedia.org/wiki/Quotation_mark
|
||||
"""
|
||||
SYMBOLS = {
|
||||
"\u0027" : "Straight single quotation mark",
|
||||
"\u0022" : "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",
|
||||
|
||||
"\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",
|
||||
|
||||
"\u300c" : "Left corner bracket",
|
||||
"\u300d" : "Right corner bracket",
|
||||
"\u300e" : "Left white corner bracket",
|
||||
"\u300f" : "Right white corner bracket",
|
||||
}
|
||||
|
||||
# END Class nwQuotes
|
||||
|
||||
class nwUnicode:
|
||||
"""Supported unicode character constants and their HTML equivalents.
|
||||
"""
|
||||
# Unicode Constants
|
||||
# =================
|
||||
|
||||
## Quotation Marks
|
||||
U_QUOT = "\u0022" # Quotation mark
|
||||
U_APOS = "\u0027" # Apostrophe
|
||||
U_LAQUO = "\u00ab" # Left-pointing double angle quotation mark
|
||||
U_RAQUO = "\u00bb" # Right-pointing double angle quotation mark
|
||||
U_LSQUO = "\u2018" # Left single quotation mark
|
||||
U_RSQUO = "\u2019" # Right single quotation mark
|
||||
U_SBQUO = "\u201a" # Single low-9 quotation mark
|
||||
U_SUQUO = "\u201b" # Single high-reversed-9 quotation mark
|
||||
U_LDQUO = "\u201c" # Left double quotation mark
|
||||
U_RDQUO = "\u201d" # Right double quotation mark
|
||||
U_BDQUO = "\u201e" # Double low-9 quotation mark
|
||||
U_UDQUO = "\u201f" # Double high-reversed-9 quotation mark
|
||||
U_LSAQUO = "\u2039" # Single left-pointing angle quotation mark
|
||||
U_RSAQUO = "\u203a" # Single right-pointing angle quotation mark
|
||||
U_BDRQUO = "\u2e42" # Double low-reversed-9 quotation mark
|
||||
U_LCQUO = "\u300c" # Left corner bracket
|
||||
U_RCQUO = "\u300d" # Right corner bracket
|
||||
U_LWCQUO = "\u300e" # Left white corner bracket
|
||||
U_RWCQUO = "\u300f" # Right white corner bracket
|
||||
|
||||
## Punctuation
|
||||
U_FGDASH = "\u2012" # Figure dash
|
||||
U_ENDASH = "\u2013" # Short dash
|
||||
U_EMDASH = "\u2014" # Long dash
|
||||
U_HBAR = "\u2015" # Horizontal bar
|
||||
U_HELLIP = "\u2026" # Ellipsis
|
||||
U_MAPOSS = "\u02bc" # Modifier letter single apostrophe
|
||||
U_PRIME = "\u2032" # Prime
|
||||
U_DPRIME = "\u2033" # Double prime
|
||||
|
||||
## Spaces and Lines
|
||||
U_NBSP = "\u00a0" # Non-breaking space
|
||||
U_THSP = "\u2009" # Thin space
|
||||
U_THNBSP = "\u202f" # Thin non-breaking space
|
||||
U_ENSP = "\u2002" # Short (en) space
|
||||
U_EMSP = "\u2003" # Long (em) space
|
||||
U_LSEP = "\u2028" # Line separator
|
||||
U_PSEP = "\u2029" # Paragraph separator
|
||||
|
||||
## Symbols
|
||||
U_CHECK = "\u2714" # Heavy check mark
|
||||
U_CROSS = "\u2715" # Heavy cross mark
|
||||
U_BULL = "\u2022" # List bullet
|
||||
U_TRBULL = "\u2023" # Triangle bullet
|
||||
U_HYBULL = "\u2043" # Hyphen bullet
|
||||
U_FLOWER = "\u2055" # Flower punctuation mark
|
||||
U_PERMIL = "\u2030" # Per mille sign
|
||||
U_DEGREE = "\u00b0" # Degree symbol
|
||||
U_MINUS = "\u2212" # Minus sign
|
||||
U_TIMES = "\u00d7" # Multiplaction sign
|
||||
U_DIVIDE = "\u00f7" # Division sign
|
||||
|
||||
## Arrows
|
||||
U_UTRI = "\u25b2" # Up-pointing triangle
|
||||
U_UTRIS = "\u25b4" # Up-pointing triangle, small
|
||||
U_RTRI = "\u25b6" # Right-pointing triangle
|
||||
U_RTRIS = "\u25b8" # Right-pointing triangle, small
|
||||
U_DTRI = "\u25bc" # Down-pointing triangle
|
||||
U_DTRIS = "\u25be" # Down-pointing triangle, small
|
||||
U_LTRI = "\u25c0" # Left-pointing triangle
|
||||
U_LTRIS = "\u25c2" # Left-pointing triangle, small
|
||||
|
||||
# HTML Equivalents
|
||||
# ================
|
||||
|
||||
## Quotes
|
||||
H_QUOT = """
|
||||
H_APOS = "'"
|
||||
H_LAQUO = "«"
|
||||
H_RAQUO = "»"
|
||||
H_LSQUO = "‘"
|
||||
H_RSQUO = "’"
|
||||
H_SBQUO = "‚"
|
||||
H_SUQUO = "‛"
|
||||
H_LDQUO = "“"
|
||||
H_RDQUO = "”"
|
||||
H_BDQUO = "„"
|
||||
H_UDQUO = "‟"
|
||||
H_LSAQUO = "‹"
|
||||
H_RSAQUO = "›"
|
||||
H_BDRQUO = "⹂"
|
||||
H_LCQUO = "「"
|
||||
H_RCQUO = "」"
|
||||
H_LWCQUO = "『"
|
||||
H_RWCQUO = "』"
|
||||
|
||||
## Punctuation
|
||||
H_FGDASH = "‒"
|
||||
H_ENDASH = "–"
|
||||
H_EMDASH = "—"
|
||||
H_HBAR = "―"
|
||||
H_HELLIP = "…"
|
||||
H_MAPOSS = "ʼ"
|
||||
H_PRIME = "′"
|
||||
H_DPRIME = "″"
|
||||
|
||||
## Spaces
|
||||
H_NBSP = " "
|
||||
H_THSP = " "
|
||||
H_THNBSP = " "
|
||||
H_ENSP = " "
|
||||
H_EMSP = " "
|
||||
|
||||
## Symbols
|
||||
H_CHECK = "✔"
|
||||
H_CROSS = "✕"
|
||||
H_BULL = "•"
|
||||
H_TRBULL = "‣"
|
||||
H_HYBULL = "⁃"
|
||||
H_FLOWER = "⁕"
|
||||
H_PERMIL = "‰"
|
||||
H_DEGREE = "°"
|
||||
H_MINUS = "−"
|
||||
H_TIMES = "×"
|
||||
H_DIVIDE = "÷"
|
||||
|
||||
## Arrows
|
||||
H_UTRI = "▲"
|
||||
H_UTRIS = "▴"
|
||||
H_RTRI = "▶"
|
||||
H_RTRIS = "▸"
|
||||
H_DTRI = "▼"
|
||||
H_DTRIS = "▾"
|
||||
H_LTRI = "◀"
|
||||
H_LTRIS = "◂"
|
||||
|
||||
# END Class nwUnicode
|
||||
|
||||
class nwHtmlUnicode():
|
||||
|
||||
U_TO_H = {
|
||||
## Quotes
|
||||
nwUnicode.U_QUOT : nwUnicode.H_QUOT,
|
||||
nwUnicode.U_APOS : nwUnicode.H_APOS,
|
||||
nwUnicode.U_LAQUO : nwUnicode.H_LAQUO,
|
||||
nwUnicode.U_RAQUO : nwUnicode.H_RAQUO,
|
||||
nwUnicode.U_LSQUO : nwUnicode.H_LSQUO,
|
||||
nwUnicode.U_RSQUO : nwUnicode.H_RSQUO,
|
||||
nwUnicode.U_SBQUO : nwUnicode.H_SBQUO,
|
||||
nwUnicode.U_SUQUO : nwUnicode.H_SUQUO,
|
||||
nwUnicode.U_LDQUO : nwUnicode.H_LDQUO,
|
||||
nwUnicode.U_RDQUO : nwUnicode.H_RDQUO,
|
||||
nwUnicode.U_BDQUO : nwUnicode.H_BDQUO,
|
||||
nwUnicode.U_UDQUO : nwUnicode.H_UDQUO,
|
||||
nwUnicode.U_LSAQUO : nwUnicode.H_LSAQUO,
|
||||
nwUnicode.U_RSAQUO : nwUnicode.H_RSAQUO,
|
||||
nwUnicode.U_BDRQUO : nwUnicode.H_BDRQUO,
|
||||
nwUnicode.U_LCQUO : nwUnicode.H_LCQUO,
|
||||
nwUnicode.U_RCQUO : nwUnicode.H_RCQUO,
|
||||
nwUnicode.U_LWCQUO : nwUnicode.H_LWCQUO,
|
||||
nwUnicode.U_RWCQUO : nwUnicode.H_RWCQUO,
|
||||
|
||||
## Punctuation
|
||||
nwUnicode.U_FGDASH : nwUnicode.H_FGDASH,
|
||||
nwUnicode.U_ENDASH : nwUnicode.H_ENDASH,
|
||||
nwUnicode.U_EMDASH : nwUnicode.H_EMDASH,
|
||||
nwUnicode.U_HBAR : nwUnicode.H_HBAR,
|
||||
nwUnicode.U_HELLIP : nwUnicode.H_HELLIP,
|
||||
nwUnicode.U_MAPOSS : nwUnicode.H_MAPOSS,
|
||||
nwUnicode.U_PRIME : nwUnicode.H_PRIME,
|
||||
nwUnicode.U_DPRIME : nwUnicode.H_DPRIME,
|
||||
|
||||
## Spaces
|
||||
nwUnicode.U_NBSP : nwUnicode.H_NBSP,
|
||||
nwUnicode.U_THSP : nwUnicode.H_THSP,
|
||||
nwUnicode.U_THNBSP : nwUnicode.H_THNBSP,
|
||||
nwUnicode.U_ENSP : nwUnicode.H_ENSP,
|
||||
nwUnicode.U_EMSP : nwUnicode.H_EMSP,
|
||||
|
||||
## Symbols
|
||||
nwUnicode.U_CHECK : nwUnicode.H_CHECK,
|
||||
nwUnicode.U_CROSS : nwUnicode.H_CROSS,
|
||||
nwUnicode.U_BULL : nwUnicode.H_BULL,
|
||||
nwUnicode.U_TRBULL : nwUnicode.H_TRBULL,
|
||||
nwUnicode.U_HYBULL : nwUnicode.H_HYBULL,
|
||||
nwUnicode.U_FLOWER : nwUnicode.H_FLOWER,
|
||||
nwUnicode.U_PERMIL : nwUnicode.H_PERMIL,
|
||||
nwUnicode.U_DEGREE : nwUnicode.H_DEGREE,
|
||||
nwUnicode.U_MINUS : nwUnicode.H_MINUS,
|
||||
nwUnicode.U_TIMES : nwUnicode.H_TIMES,
|
||||
nwUnicode.U_DIVIDE : nwUnicode.H_DIVIDE,
|
||||
}
|
||||
|
||||
# END Class nwHtmlUnicode
|
||||
@@ -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,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – Core Init
|
||||
=======================
|
||||
|
||||
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/>.
|
||||
"""
|
||||
|
||||
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",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – Project Document
|
||||
==============================
|
||||
@@ -27,27 +26,30 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import logging
|
||||
import os
|
||||
|
||||
from nw.constants import nwAlert
|
||||
from nw.enum import nwItemLayout, nwItemClass
|
||||
from nw.common import isHandle
|
||||
from nw.constants import nwItemLayout, nwItemClass
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NWDoc():
|
||||
|
||||
def __init__(self, theProject, theParent):
|
||||
def __init__(self, theProject, theHandle):
|
||||
|
||||
self.theProject = theProject
|
||||
self.theParent = theParent
|
||||
|
||||
# Internal Variables
|
||||
self._theItem = None # The currently open item
|
||||
self._docHandle = None # The handle of the currently open item
|
||||
self._fileLoc = None # The file location of the currently open item
|
||||
self._docMeta = {} # The meta data of the currently open item
|
||||
self._theItem = None # The currently open item
|
||||
self._docHandle = None # The handle of the currently open item
|
||||
self._fileLoc = None # The file location of the currently open item
|
||||
self._docMeta = {} # The meta data of the currently open item
|
||||
self._docError = "" # The latest encountered IO error
|
||||
|
||||
# Internal Mapping
|
||||
self.makeAlert = self.theParent.makeAlert
|
||||
if isHandle(theHandle):
|
||||
self._docHandle = theHandle
|
||||
|
||||
if self._docHandle is not None:
|
||||
self._theItem = self.theProject.projTree[theHandle]
|
||||
|
||||
return
|
||||
|
||||
@@ -55,39 +57,23 @@ class NWDoc():
|
||||
# Class Methods
|
||||
##
|
||||
|
||||
def clearDocument(self):
|
||||
"""Clear the document contents.
|
||||
"""
|
||||
self._theItem = None
|
||||
self._docHandle = None
|
||||
self._fileLoc = None
|
||||
self._docMeta = {}
|
||||
return
|
||||
|
||||
def openDocument(self, tHandle, showStatus=True, isOrphan=False):
|
||||
"""Open a document from handle, capturing potential file system
|
||||
errors and parse meta data. If the document doesn't exist on
|
||||
disk, return an empty string. If something went wrong, return
|
||||
def readDocument(self, isOrphan=False):
|
||||
"""Read a document from set handle, capturing potential file
|
||||
system errors and parse meta data. If the document doesn't exist
|
||||
on disk, return an empty string. If something went wrong, return
|
||||
None.
|
||||
"""
|
||||
if not isHandle(tHandle):
|
||||
self._docError = ""
|
||||
if self._docHandle is None:
|
||||
logger.error("No document handle set")
|
||||
return None
|
||||
|
||||
# Always clear first, since the object will often be reused.
|
||||
self.clearDocument()
|
||||
|
||||
self._docHandle = tHandle
|
||||
if not isOrphan:
|
||||
self._theItem = self.theProject.projTree[tHandle]
|
||||
else:
|
||||
self._theItem = None
|
||||
|
||||
if self._theItem is None and not isOrphan:
|
||||
self.clearDocument()
|
||||
logger.error("Unknown novelWriter document")
|
||||
return None
|
||||
|
||||
docFile = self._docHandle+".nwd"
|
||||
logger.debug("Opening document %s" % docFile)
|
||||
logger.debug("Opening document: %s", docFile)
|
||||
|
||||
docPath = os.path.join(self.theProject.projContent, docFile)
|
||||
self._fileLoc = docPath
|
||||
@@ -96,7 +82,7 @@ class NWDoc():
|
||||
self._docMeta = {}
|
||||
if os.path.isfile(docPath):
|
||||
try:
|
||||
with open(docPath, mode="r", encoding="utf8") as inFile:
|
||||
with open(docPath, mode="r", encoding="utf-8") as inFile:
|
||||
|
||||
# Check the first <= 10 lines for metadata
|
||||
for i in range(10):
|
||||
@@ -111,34 +97,30 @@ class NWDoc():
|
||||
theText += inFile.read()
|
||||
|
||||
except Exception as e:
|
||||
self.makeAlert(["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.
|
||||
self.clearDocument()
|
||||
self._docError = str(e)
|
||||
return None
|
||||
|
||||
else:
|
||||
# The document file does not exist, so we assume it's a new
|
||||
# document and initialise an empty text string.
|
||||
logger.debug("The requested document does not exist.")
|
||||
return ""
|
||||
|
||||
if showStatus and not isOrphan:
|
||||
self.theParent.setStatus("Opened Document: %s" % self._theItem.itemName)
|
||||
|
||||
return theText
|
||||
|
||||
def saveDocument(self, docText):
|
||||
"""Save the document. The file is saved via a temp file in case
|
||||
def writeDocument(self, docText):
|
||||
"""Write the document. The file is saved via a temp file in case
|
||||
of save failure. Returns True if successful, False if not.
|
||||
"""
|
||||
self._docError = ""
|
||||
if self._docHandle is None:
|
||||
logger.error("No document handle set")
|
||||
return False
|
||||
|
||||
self.theProject.ensureFolderStructure()
|
||||
|
||||
docFile = self._docHandle+".nwd"
|
||||
logger.debug("Saving document %s" % docFile)
|
||||
logger.debug("Saving document: %s", docFile)
|
||||
|
||||
docPath = os.path.join(self.theProject.projContent, docFile)
|
||||
docTemp = os.path.join(self.theProject.projContent, docFile+"~")
|
||||
@@ -154,11 +136,11 @@ class NWDoc():
|
||||
)
|
||||
|
||||
try:
|
||||
with open(docTemp, mode="w", encoding="utf8") as outFile:
|
||||
with open(docTemp, mode="w", encoding="utf-8") as outFile:
|
||||
outFile.write(docMeta)
|
||||
outFile.write(docText)
|
||||
except Exception as e:
|
||||
self.makeAlert(["Could not save document.", str(e)], nwAlert.ERROR)
|
||||
self._docError = str(e)
|
||||
return False
|
||||
|
||||
# If we're here, the file was successfully saved, so we can
|
||||
@@ -167,19 +149,18 @@ class NWDoc():
|
||||
os.unlink(docPath)
|
||||
os.rename(docTemp, docPath)
|
||||
|
||||
if self._theItem is not None:
|
||||
self.theParent.setStatus("Saved Document: %s" % self._theItem.itemName)
|
||||
|
||||
return True
|
||||
|
||||
def deleteDocument(self, tHandle):
|
||||
def deleteDocument(self):
|
||||
"""Permanently delete a document source file and related files
|
||||
from the project data folder.
|
||||
"""
|
||||
if not isHandle(tHandle):
|
||||
self._docError = ""
|
||||
if self._docHandle is None:
|
||||
logger.error("No document handle set")
|
||||
return False
|
||||
|
||||
docFile = tHandle+".nwd"
|
||||
docFile = self._docHandle+".nwd"
|
||||
|
||||
chkList = []
|
||||
chkList.append(os.path.join(self.theProject.projContent, docFile))
|
||||
@@ -189,9 +170,9 @@ class NWDoc():
|
||||
if os.path.isfile(chkFile):
|
||||
try:
|
||||
os.unlink(chkFile)
|
||||
logger.debug("Deleted: %s" % chkFile)
|
||||
logger.debug("Deleted: %s", chkFile)
|
||||
except Exception as e:
|
||||
self.makeAlert(["Could not delete document file.", str(e)], nwAlert.ERROR)
|
||||
self._docError = str(e)
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -221,6 +202,11 @@ class NWDoc():
|
||||
|
||||
return theName, theParent, theClass, theLayout
|
||||
|
||||
def getError(self):
|
||||
"""Return the last recorded exception.
|
||||
"""
|
||||
return self._docError
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
@@ -251,7 +237,7 @@ class NWDoc():
|
||||
self._docMeta["layout"] = nwItemLayout[metaBits[1]]
|
||||
|
||||
else:
|
||||
logger.debug("Ignoring meta data: '%s'" % metaLine.strip())
|
||||
logger.debug("Ignoring meta data: '%s'", metaLine.strip())
|
||||
|
||||
return
|
||||
|
||||
|
||||
203
nw/core/index.py
203
nw/core/index.py
@@ -1,11 +1,11 @@
|
||||
# -*- coding: utf-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,26 +31,24 @@ 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__)
|
||||
|
||||
|
||||
class NWIndex():
|
||||
|
||||
H_VALID = ("H0", "H1", "H2", "H3", "H4")
|
||||
H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4}
|
||||
|
||||
def __init__(self, theProject, theParent):
|
||||
def __init__(self, theProject):
|
||||
|
||||
# Internal
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theProject = theProject
|
||||
self.theParent = theParent
|
||||
self.indexBroken = False
|
||||
|
||||
# Indices
|
||||
@@ -87,7 +85,7 @@ class NWIndex():
|
||||
def deleteHandle(self, tHandle):
|
||||
"""Delete all entries of a given document handle.
|
||||
"""
|
||||
logger.debug("Removing item %s from the index" % tHandle)
|
||||
logger.debug("Removing item '%s' from the index", tHandle)
|
||||
|
||||
delTags = []
|
||||
for tTag in self._tagIndex:
|
||||
@@ -109,7 +107,7 @@ class NWIndex():
|
||||
moved from the archive or trash folders back into the active
|
||||
project.
|
||||
"""
|
||||
logger.debug("Re-indexing item %s" % tHandle)
|
||||
logger.debug("Re-indexing item '%s'", tHandle)
|
||||
|
||||
tItem = self.theProject.projTree[tHandle]
|
||||
if tItem is None:
|
||||
@@ -117,8 +115,8 @@ class NWIndex():
|
||||
if tItem.itemType != nwItemType.FILE:
|
||||
return False
|
||||
|
||||
theDoc = NWDoc(self.theProject, self.theParent)
|
||||
theText = theDoc.openDocument(tHandle, showStatus=False)
|
||||
theDoc = NWDoc(self.theProject, tHandle)
|
||||
theText = theDoc.readDocument()
|
||||
if theText:
|
||||
self.scanText(tHandle, theText)
|
||||
|
||||
@@ -146,28 +144,24 @@ class NWIndex():
|
||||
def loadIndex(self):
|
||||
"""Load index from last session from the project meta folder.
|
||||
"""
|
||||
theData = {}
|
||||
theData = {}
|
||||
indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
|
||||
|
||||
if os.path.isfile(indexFile):
|
||||
logger.debug("Loading index file")
|
||||
try:
|
||||
with open(indexFile, mode="r", encoding="utf8") as inFile:
|
||||
with open(indexFile, mode="r", encoding="utf-8") as inFile:
|
||||
theData = json.load(inFile)
|
||||
except Exception:
|
||||
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", {})
|
||||
self._refIndex = theData.get("refIndex", {})
|
||||
self._tagIndex = theData.get("tagIndex", {})
|
||||
self._refIndex = theData.get("refIndex", {})
|
||||
self._novelIndex = theData.get("novelIndex", {})
|
||||
self._noteIndex = theData.get("noteIndex", {})
|
||||
self._noteIndex = theData.get("noteIndex", {})
|
||||
self._textCounts = theData.get("textCounts", {})
|
||||
|
||||
nowTime = round(time())
|
||||
@@ -187,13 +181,13 @@ class NWIndex():
|
||||
indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
|
||||
|
||||
try:
|
||||
with open(indexFile, mode="w+", encoding="utf8") as outFile:
|
||||
with open(indexFile, mode="w+", encoding="utf-8") as outFile:
|
||||
json.dump({
|
||||
"tagIndex" : self._tagIndex,
|
||||
"refIndex" : self._refIndex,
|
||||
"novelIndex" : self._novelIndex,
|
||||
"noteIndex" : self._noteIndex,
|
||||
"textCounts" : self._textCounts,
|
||||
"tagIndex": self._tagIndex,
|
||||
"refIndex": self._refIndex,
|
||||
"novelIndex": self._novelIndex,
|
||||
"noteIndex": self._noteIndex,
|
||||
"textCounts": self._textCounts,
|
||||
}, outFile, indent=2)
|
||||
except Exception:
|
||||
logger.error("Failed to save index file")
|
||||
@@ -223,15 +217,11 @@ class NWIndex():
|
||||
self.indexBroken = True
|
||||
|
||||
tEnd = time()
|
||||
logger.debug("Index check took %.3f ms" % ((tEnd - tStart)*1000))
|
||||
logger.debug("Index check took %.3f ms", (tEnd - tStart)*1000)
|
||||
logger.debug("Index check complete")
|
||||
|
||||
if self.indexBroken:
|
||||
self.clearIndex()
|
||||
self.theParent.makeAlert(
|
||||
"The project index is outdated or broken. Rebuilding index.",
|
||||
nwAlert.WARN
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
@@ -249,16 +239,16 @@ class NWIndex():
|
||||
theRoot = self.theProject.projTree.getRootItem(tHandle)
|
||||
|
||||
if theItem is None:
|
||||
logger.info("Not indexing unknown item %s" % tHandle)
|
||||
logger.info("Not indexing unknown item '%s'", tHandle)
|
||||
return False
|
||||
if theItem.itemType != nwItemType.FILE:
|
||||
logger.info("Not indexing non-file item %s" % tHandle)
|
||||
logger.info("Not indexing non-file item '%s'", tHandle)
|
||||
return False
|
||||
if theItem.itemLayout == nwItemLayout.NO_LAYOUT:
|
||||
logger.info("Not indexing no-layout item %s" % tHandle)
|
||||
logger.info("Not indexing no-layout item '%s'", tHandle)
|
||||
return False
|
||||
if theItem.itemParent is None:
|
||||
logger.info("Not indexing orphaned item %s" % tHandle)
|
||||
logger.info("Not indexing orphaned item '%s'", tHandle)
|
||||
return False
|
||||
|
||||
# Run word counter for the whole text
|
||||
@@ -267,23 +257,23 @@ class NWIndex():
|
||||
|
||||
# If the file is archived or trashed, we don't index the file itself
|
||||
if self.theProject.projTree.isTrashRoot(theItem.itemParent):
|
||||
logger.info("Not indexing trash item %s" % tHandle)
|
||||
logger.info("Not indexing trash item '%s'", tHandle)
|
||||
return False
|
||||
if theRoot.itemClass == nwItemClass.ARCHIVE:
|
||||
logger.info("Not indexing archived item %s" % tHandle)
|
||||
logger.info("Not indexing archived item '%s'", tHandle)
|
||||
return False
|
||||
|
||||
itemClass = theItem.itemClass
|
||||
itemLayout = theItem.itemLayout
|
||||
|
||||
logger.debug("Indexing item with handle %s" % tHandle)
|
||||
logger.debug("Indexing item with handle '%s'", tHandle)
|
||||
|
||||
# Check file type, and reset its old index
|
||||
# Also add a dummy entry T000000 in case the file has no title
|
||||
# Also add a default entry T000000 in case the file has no title
|
||||
self._refIndex[tHandle] = {}
|
||||
self._refIndex[tHandle]["T000000"] = {
|
||||
"tags" : [],
|
||||
"updated" : round(time()),
|
||||
"tags": [],
|
||||
"updated": round(time()),
|
||||
}
|
||||
if itemLayout == nwItemLayout.NOTE:
|
||||
self._novelIndex.pop(tHandle, None)
|
||||
@@ -302,13 +292,12 @@ class NWIndex():
|
||||
for aTag in clearTags:
|
||||
self._tagIndex.pop(aTag)
|
||||
|
||||
nLine = 0
|
||||
nLine = 0
|
||||
nTitle = 0
|
||||
theLines = theText.splitlines()
|
||||
for aLine in theLines:
|
||||
aLine = aLine
|
||||
nLine += 1
|
||||
nChar = len(aLine.strip())
|
||||
nChar = len(aLine.strip())
|
||||
if nChar == 0:
|
||||
continue
|
||||
|
||||
@@ -363,33 +352,33 @@ class NWIndex():
|
||||
"""
|
||||
if aLine.startswith("# "):
|
||||
hDepth = "H1"
|
||||
hText = aLine[2:].strip()
|
||||
hText = aLine[2:].strip()
|
||||
elif aLine.startswith("## "):
|
||||
hDepth = "H2"
|
||||
hText = aLine[3:].strip()
|
||||
hText = aLine[3:].strip()
|
||||
elif aLine.startswith("### "):
|
||||
hDepth = "H3"
|
||||
hText = aLine[4:].strip()
|
||||
hText = aLine[4:].strip()
|
||||
elif aLine.startswith("#### "):
|
||||
hDepth = "H4"
|
||||
hText = aLine[5:].strip()
|
||||
hText = aLine[5:].strip()
|
||||
else:
|
||||
return False
|
||||
|
||||
sTitle = "T%06d" % nLine
|
||||
self._refIndex[tHandle][sTitle] = {
|
||||
"tags" : [],
|
||||
"updated" : round(time()),
|
||||
"tags": [],
|
||||
"updated": round(time()),
|
||||
}
|
||||
theData = {
|
||||
"level" : hDepth,
|
||||
"title" : hText,
|
||||
"layout" : itemLayout.name,
|
||||
"synopsis" : "",
|
||||
"cCount" : 0,
|
||||
"wCount" : 0,
|
||||
"pCount" : 0,
|
||||
"updated" : round(time()),
|
||||
"level": hDepth,
|
||||
"title": hText,
|
||||
"layout": itemLayout.name,
|
||||
"synopsis": "",
|
||||
"cCount": 0,
|
||||
"wCount": 0,
|
||||
"pCount": 0,
|
||||
"updated": round(time()),
|
||||
}
|
||||
|
||||
if hText != "":
|
||||
@@ -406,14 +395,14 @@ class NWIndex():
|
||||
"""Index a page with no title.
|
||||
"""
|
||||
theData = {
|
||||
"level" : "H0",
|
||||
"title" : "Untitled Page",
|
||||
"layout" : itemLayout.name,
|
||||
"synopsis" : "",
|
||||
"cCount" : 0,
|
||||
"wCount" : 0,
|
||||
"pCount" : 0,
|
||||
"updated" : round(time()),
|
||||
"level": "H0",
|
||||
"title": "Untitled Page",
|
||||
"layout": itemLayout.name,
|
||||
"synopsis": "",
|
||||
"cCount": 0,
|
||||
"wCount": 0,
|
||||
"pCount": 0,
|
||||
"updated": round(time()),
|
||||
}
|
||||
|
||||
if isNovel:
|
||||
@@ -468,11 +457,11 @@ class NWIndex():
|
||||
"""
|
||||
isValid, theBits, _ = self.scanThis(aLine)
|
||||
if not isValid or len(theBits) < 2:
|
||||
logger.warning("Skipping keyword with %d value(s) in %s" % (len(theBits), tHandle))
|
||||
logger.warning("Skipping keyword with %d value(s) in '%s'", len(theBits), tHandle)
|
||||
return
|
||||
|
||||
if theBits[0] not in nwKeyWords.VALID_KEYS:
|
||||
logger.warning("Skipping invalid keyword '%s' in %s" % (theBits[0], tHandle))
|
||||
logger.warning("Skipping invalid keyword '%s' in '%s'", theBits[0], tHandle)
|
||||
return
|
||||
|
||||
sTitle = "T%06d" % nTitle
|
||||
@@ -493,10 +482,10 @@ class NWIndex():
|
||||
"""Scan a line starting with @ to check that it's valid. Then
|
||||
split it up into its elements and positions as two arrays.
|
||||
"""
|
||||
theBits = [] # The elements of the string
|
||||
thePos = [] # The absolute position of each element
|
||||
theBits = [] # The elements of the string
|
||||
thePos = [] # The absolute position of each element
|
||||
|
||||
aLine = aLine.rstrip() # Remove all trailing white spaces
|
||||
aLine = aLine.rstrip() # Remove all trailing white spaces
|
||||
nChar = len(aLine)
|
||||
if nChar < 2:
|
||||
return False, theBits, thePos
|
||||
@@ -905,3 +894,71 @@ 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
|
||||
|
||||
if not isinstance(theText, str):
|
||||
return charCount, wordCount, paraCount
|
||||
|
||||
# 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
|
||||
|
||||
if not aLine:
|
||||
prevEmpty = True
|
||||
continue
|
||||
if aLine[0] == "@" or aLine[0] == "%":
|
||||
continue
|
||||
|
||||
if aLine[0] == "#":
|
||||
if aLine[:5] == "#### ":
|
||||
aLine = aLine[5:]
|
||||
countPara = False
|
||||
elif aLine[:4] == "### ":
|
||||
aLine = aLine[4:]
|
||||
countPara = False
|
||||
elif aLine[:3] == "## ":
|
||||
aLine = aLine[3:]
|
||||
countPara = False
|
||||
elif aLine[:2] == "# ":
|
||||
aLine = aLine[2:]
|
||||
countPara = False
|
||||
elif aLine[0] == ">" or aLine[-1] == "<":
|
||||
if aLine[:2] == ">>":
|
||||
aLine = aLine[2:].lstrip(" ")
|
||||
elif aLine[:1] == ">":
|
||||
aLine = aLine[1:].lstrip(" ")
|
||||
if aLine[-2:] == "<<":
|
||||
aLine = aLine[:-2].rstrip(" ")
|
||||
elif aLine[-1:] == "<":
|
||||
aLine = aLine[:-1].rstrip(" ")
|
||||
|
||||
wordCount += len(aLine.split())
|
||||
charCount += len(aLine)
|
||||
if countPara and prevEmpty:
|
||||
paraCount += 1
|
||||
|
||||
prevEmpty = not countPara
|
||||
|
||||
return charCount, wordCount, paraCount
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – Project Item Class
|
||||
================================
|
||||
@@ -28,13 +27,12 @@ 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__)
|
||||
|
||||
|
||||
class NWItem():
|
||||
|
||||
def __init__(self, theProject):
|
||||
@@ -53,11 +51,11 @@ class NWItem():
|
||||
self.isExported = True
|
||||
|
||||
# Document Meta Data
|
||||
self.charCount = 0 # Current character count
|
||||
self.wordCount = 0 # Current word count
|
||||
self.paraCount = 0 # Current paragraph count
|
||||
self.initCount = 0 # Initial word count
|
||||
self.cursorPos = 0 # Last cursor position
|
||||
self.charCount = 0 # Current character count
|
||||
self.wordCount = 0 # Current word count
|
||||
self.paraCount = 0 # Current paragraph count
|
||||
self.initCount = 0 # Initial word count
|
||||
self.cursorPos = 0 # Last cursor position
|
||||
|
||||
return
|
||||
|
||||
@@ -69,9 +67,9 @@ class NWItem():
|
||||
"""Packs all the data in the class instance into an XML object.
|
||||
"""
|
||||
xPack = etree.SubElement(xParent, "item", attrib={
|
||||
"handle" : str(self.itemHandle),
|
||||
"order" : str(self.itemOrder),
|
||||
"parent" : str(self.itemParent),
|
||||
"handle": str(self.itemHandle),
|
||||
"order": str(self.itemOrder),
|
||||
"parent": str(self.itemParent),
|
||||
})
|
||||
self._subPack(xPack, "name", text=str(self.itemName))
|
||||
self._subPack(xPack, "type", text=str(self.itemType.name))
|
||||
@@ -108,7 +106,6 @@ class NWItem():
|
||||
if "order" in xItem.attrib:
|
||||
self.setOrder(xItem.attrib["order"])
|
||||
|
||||
retStatus = True
|
||||
for xValue in xItem:
|
||||
if xValue.tag == "name":
|
||||
self.setName(xValue.text)
|
||||
@@ -133,10 +130,12 @@ class NWItem():
|
||||
elif xValue.tag == "cursorPos":
|
||||
self.setCursorPos(xValue.text)
|
||||
else:
|
||||
logger.error("Unknown tag '%s'" % xValue.tag)
|
||||
retStatus = False
|
||||
# Sliently skip as we may otherwise cause orphaned
|
||||
# items if an otherwise valid file is opened by a
|
||||
# version of novelWriter that doesn't know the tag.
|
||||
logger.error("Unknown tag '%s'", xValue.tag)
|
||||
|
||||
return retStatus
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _subPack(xParent, name, attrib=None, text=None, none=True):
|
||||
@@ -206,7 +205,7 @@ class NWItem():
|
||||
elif isItemType(theType):
|
||||
self.itemType = nwItemType[theType]
|
||||
else:
|
||||
logger.error("Unrecognised item type '%s'" % theType)
|
||||
logger.error("Unrecognised item type '%s'", theType)
|
||||
self.itemType = nwItemType.NO_TYPE
|
||||
return
|
||||
|
||||
@@ -219,7 +218,7 @@ class NWItem():
|
||||
elif isItemClass(theClass):
|
||||
self.itemClass = nwItemClass[theClass]
|
||||
else:
|
||||
logger.error("Unrecognised item class '%s'" % theClass)
|
||||
logger.error("Unrecognised item class '%s'", theClass)
|
||||
self.itemClass = nwItemClass.NO_CLASS
|
||||
return
|
||||
|
||||
@@ -232,7 +231,7 @@ class NWItem():
|
||||
elif isItemLayout(theLayout):
|
||||
self.itemLayout = nwItemLayout[theLayout]
|
||||
else:
|
||||
logger.error("Unrecognised item layout '%s'" % theLayout)
|
||||
logger.error("Unrecognised item layout '%s'", theLayout)
|
||||
self.itemLayout = nwItemLayout.NO_LAYOUT
|
||||
return
|
||||
|
||||
@@ -252,7 +251,7 @@ class NWItem():
|
||||
if isinstance(expState, str):
|
||||
self.isExpanded = (expState == str(True))
|
||||
else:
|
||||
self.isExpanded = (expState == True) # noqa: E712
|
||||
self.isExpanded = (expState == True) # noqa: E712
|
||||
return
|
||||
|
||||
def setExported(self, expState):
|
||||
@@ -261,7 +260,7 @@ class NWItem():
|
||||
if isinstance(expState, str):
|
||||
self.isExported = (expState == str(True))
|
||||
else:
|
||||
self.isExported = (expState == True) # noqa: E712
|
||||
self.isExported = (expState == True) # noqa: E712
|
||||
return
|
||||
|
||||
##
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – Project Options Cache
|
||||
===================================
|
||||
@@ -34,6 +33,7 @@ from nw.constants import nwFiles
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OptionState():
|
||||
|
||||
def __init__(self, theProject):
|
||||
@@ -91,6 +91,8 @@ class OptionState():
|
||||
"winWidth",
|
||||
"winHeight",
|
||||
"replaceColW",
|
||||
"statusColW",
|
||||
"importColW",
|
||||
},
|
||||
"GuiProjectDetails": {
|
||||
"winWidth",
|
||||
@@ -123,12 +125,12 @@ class OptionState():
|
||||
return False
|
||||
|
||||
stateFile = os.path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
|
||||
theState = {}
|
||||
theState = {}
|
||||
|
||||
if os.path.isfile(stateFile):
|
||||
logger.debug("Loading GUI options file")
|
||||
try:
|
||||
with open(stateFile, mode="r", encoding="utf8") as inFile:
|
||||
with open(stateFile, mode="r", encoding="utf-8") as inFile:
|
||||
theState = json.load(inFile)
|
||||
except Exception:
|
||||
logger.error("Failed to load GUI options file")
|
||||
@@ -155,7 +157,7 @@ class OptionState():
|
||||
logger.debug("Saving GUI options file")
|
||||
|
||||
try:
|
||||
with open(stateFile, mode="w+", encoding="utf8") as outFile:
|
||||
with open(stateFile, mode="w+", encoding="utf-8") as outFile:
|
||||
json.dump(self.theState, outFile, indent=2)
|
||||
except Exception:
|
||||
logger.error("Failed to save GUI options file")
|
||||
@@ -172,11 +174,11 @@ class OptionState():
|
||||
"""Saves a value, with a given group and name.
|
||||
"""
|
||||
if setGroup not in self.validMap:
|
||||
logger.error("Unknown option group '%s'" % setGroup)
|
||||
logger.error("Unknown option group '%s'", setGroup)
|
||||
return False
|
||||
|
||||
if setName not in self.validMap[setGroup]:
|
||||
logger.error("Unknown option name '%s'" % setName)
|
||||
logger.error("Unknown option name '%s'", setName)
|
||||
return False
|
||||
|
||||
if setGroup not in self.theState:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – Spell Check Classes
|
||||
=================================
|
||||
@@ -29,10 +28,9 @@ import logging
|
||||
import os
|
||||
import difflib
|
||||
|
||||
from nw.constants import nwConst, isoLanguage
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# =============================================================================================== #
|
||||
# SpellChecking SuperClass
|
||||
# =============================================================================================== #
|
||||
@@ -49,17 +47,17 @@ class NWSpellCheck():
|
||||
return
|
||||
|
||||
def setLanguage(self, theLang, projectDict=None):
|
||||
"""Dummy function.
|
||||
"""Default function.
|
||||
"""
|
||||
return
|
||||
|
||||
def checkWord(self, theWord):
|
||||
"""Dummy function.
|
||||
"""Default function.
|
||||
"""
|
||||
return True
|
||||
|
||||
def suggestWords(self, theWord):
|
||||
"""Dummy function.
|
||||
"""Default function.
|
||||
"""
|
||||
return []
|
||||
|
||||
@@ -73,32 +71,22 @@ class NWSpellCheck():
|
||||
outFile.write("%s\n" % newWord)
|
||||
self.projDict.append(newWord)
|
||||
except Exception:
|
||||
logger.error("Failed to add word to project word list %s" % str(self.projectDict))
|
||||
logger.error("Failed to add word to project word list %s", str(self.projectDict))
|
||||
nw.logException()
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
|
||||
def listDictionaries(self):
|
||||
"""Dummy function.
|
||||
"""Default function.
|
||||
"""
|
||||
return []
|
||||
|
||||
def describeDict(self):
|
||||
"""Dummy function.
|
||||
"""Default function.
|
||||
"""
|
||||
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,31 +96,33 @@ 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
|
||||
|
||||
# END Class NWSpellCheck
|
||||
|
||||
|
||||
# =============================================================================================== #
|
||||
# Enchant Based SpellChecking
|
||||
# =============================================================================================== #
|
||||
@@ -147,7 +137,7 @@ class NWSpellEnchant(NWSpellCheck):
|
||||
|
||||
def setLanguage(self, theLang, projectDict=None):
|
||||
"""Load a dictionary for the language specified in the config.
|
||||
If that fails, we load a dummy dictionary so that lookups don't
|
||||
If that fails, we load a mock dictionary so that lookups don't
|
||||
crash.
|
||||
"""
|
||||
try:
|
||||
@@ -159,11 +149,11 @@ class NWSpellEnchant(NWSpellCheck):
|
||||
self.theBroker = enchant.Broker()
|
||||
self.theDict = self.theBroker.request_dict(theLang)
|
||||
self.spellLanguage = theLang
|
||||
logger.debug("Enchant spell checking for language %s loaded" % theLang)
|
||||
logger.debug("Enchant spell checking for language '%s' loaded", theLang)
|
||||
|
||||
except Exception:
|
||||
logger.error("Failed to load enchant spell checking for language %s" % theLang)
|
||||
self.theDict = NWSpellEnchantDummy()
|
||||
logger.error("Failed to load enchant spell checking for language '%s'", theLang)
|
||||
self.theDict = FakeEnchant()
|
||||
self.spellLanguage = None
|
||||
|
||||
self._readProjectDictionary(projectDict)
|
||||
@@ -196,10 +186,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):
|
||||
@@ -219,7 +209,8 @@ class NWSpellEnchant(NWSpellCheck):
|
||||
|
||||
# END Class NWSpellEnchant
|
||||
|
||||
class NWSpellEnchantDummy:
|
||||
|
||||
class FakeEnchant:
|
||||
"""Fallback for when Enchant is selected, but not installed.
|
||||
"""
|
||||
def __init__(self):
|
||||
@@ -234,7 +225,8 @@ class NWSpellEnchantDummy:
|
||||
def add_to_session(self, theWord):
|
||||
return
|
||||
|
||||
# END Class NWSpellEnchantDummy
|
||||
# END Class FakeEnchant
|
||||
|
||||
|
||||
# =============================================================================================== #
|
||||
# Fallback SpellChecking Using difflib
|
||||
@@ -243,8 +235,7 @@ class NWSpellEnchantDummy:
|
||||
class NWSpellSimple(NWSpellCheck):
|
||||
"""Internal spell check tool that uses standard Python packages with
|
||||
no other external dependencies. This is the fallback spell checker
|
||||
when no other is available. This method is fairly slow compared to
|
||||
other implementations.
|
||||
when no other is available. This method is slower than enchant.
|
||||
"""
|
||||
theWords = set()
|
||||
|
||||
@@ -267,12 +258,12 @@ class NWSpellSimple(NWSpellCheck):
|
||||
continue
|
||||
self.theWords.add(theLine.strip().lower())
|
||||
|
||||
logger.debug("Spell check dictionary for language %s loaded" % theLang)
|
||||
logger.debug("Dictionary contains %d words" % len(self.theWords))
|
||||
logger.debug("Spell check dictionary for language '%s' loaded", theLang)
|
||||
logger.debug("Dictionary contains %d words", len(self.theWords))
|
||||
self.spellLanguage = theLang
|
||||
|
||||
except Exception:
|
||||
logger.error("Failed to load spell check word list for language %s" % theLang)
|
||||
logger.error("Failed to load spell check word list for language '%s'", theLang)
|
||||
nw.logException()
|
||||
self.spellLanguage = None
|
||||
|
||||
@@ -332,8 +323,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 +331,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
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – Project Item Status Class
|
||||
=======================================
|
||||
@@ -32,15 +31,18 @@ from nw.common import checkInt
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NWStatus():
|
||||
|
||||
def __init__(self):
|
||||
self._theLabels = []
|
||||
|
||||
self._theLabels = []
|
||||
self._theColours = []
|
||||
self._theCounts = []
|
||||
self._theMap = {}
|
||||
self._theLength = 0
|
||||
self._theIndex = 0
|
||||
self._theCounts = []
|
||||
self._theMap = {}
|
||||
self._theLength = 0
|
||||
self._theIndex = 0
|
||||
|
||||
return
|
||||
|
||||
def addEntry(self, theLabel, theColours):
|
||||
@@ -87,12 +89,12 @@ class NWStatus():
|
||||
replaceMap = {}
|
||||
|
||||
if newList is not None:
|
||||
self._theLabels = []
|
||||
self._theLabels = []
|
||||
self._theColours = []
|
||||
self._theCounts = []
|
||||
self._theMap = {}
|
||||
self._theLength = 0
|
||||
self._theIndex = 0
|
||||
self._theCounts = []
|
||||
self._theMap = {}
|
||||
self._theLength = 0
|
||||
self._theIndex = 0
|
||||
|
||||
for nName, nR, nG, nB, oName in newList:
|
||||
self.addEntry(nName, (nR, nG, nB))
|
||||
@@ -122,9 +124,9 @@ class NWStatus():
|
||||
"""
|
||||
for n in range(self._theLength):
|
||||
xSub = etree.SubElement(xParent, "entry", attrib={
|
||||
"blue" : str(self._theColours[n][2]),
|
||||
"green" : str(self._theColours[n][1]),
|
||||
"red" : str(self._theColours[n][0]),
|
||||
"blue": str(self._theColours[n][2]),
|
||||
"green": str(self._theColours[n][1]),
|
||||
"red": str(self._theColours[n][0]),
|
||||
})
|
||||
xSub.text = self._theLabels[n]
|
||||
return True
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – HTML Text Converter
|
||||
=================================
|
||||
@@ -27,22 +26,23 @@ 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__)
|
||||
|
||||
|
||||
class ToHtml(Tokenizer):
|
||||
|
||||
M_PREVIEW = 0 # Tweak output for the DocViewer
|
||||
M_EXPORT = 1 # Tweak output for saving to HTML or printing
|
||||
M_EBOOK = 2 # Tweak output for converting to epub
|
||||
M_PREVIEW = 0 # Tweak output for the DocViewer
|
||||
M_EXPORT = 1 # Tweak output for saving to HTML or printing
|
||||
M_EBOOK = 2 # Tweak output for converting to epub
|
||||
|
||||
def __init__(self, theProject, theParent):
|
||||
Tokenizer.__init__(self, theProject, theParent)
|
||||
def __init__(self, theProject):
|
||||
Tokenizer.__init__(self, theProject)
|
||||
|
||||
self.genMode = self.M_EXPORT
|
||||
self.genMode = self.M_EXPORT
|
||||
self.cssStyles = True
|
||||
self.fullHTML = []
|
||||
self.fullHTML = []
|
||||
|
||||
# Internals
|
||||
self._trMap = {}
|
||||
@@ -59,7 +59,7 @@ class ToHtml(Tokenizer):
|
||||
need to make a few changes to formatting, which is managed by
|
||||
these flags.
|
||||
"""
|
||||
self.genMode = self.M_PREVIEW
|
||||
self.genMode = self.M_PREVIEW
|
||||
self.doKeywords = True
|
||||
self.doComments = doComments
|
||||
self.doSynopsis = doSynopsis
|
||||
@@ -74,16 +74,13 @@ class ToHtml(Tokenizer):
|
||||
return
|
||||
|
||||
def setReplaceUnicode(self, doReplace):
|
||||
"""Set the translation map to either minimal or full unicode to
|
||||
"""Set the translation map to either minimal or full unicode for
|
||||
html entities replacement.
|
||||
"""
|
||||
# Control characters must always be replaced
|
||||
self._trMap = str.maketrans({
|
||||
"<" : "<",
|
||||
">" : ">",
|
||||
"&" : "&",
|
||||
})
|
||||
|
||||
# Angle brackets are replaced later as they are also used in
|
||||
# formatting codes
|
||||
self._trMap = str.maketrans({"&": "&"})
|
||||
if doReplace:
|
||||
# Extend to all relevant Unicode characters
|
||||
self._trMap.update(str.maketrans(nwHtmlUnicode.U_TO_H))
|
||||
@@ -112,22 +109,22 @@ class ToHtml(Tokenizer):
|
||||
to theResult.
|
||||
"""
|
||||
if self.genMode == self.M_PREVIEW:
|
||||
htmlTags = { # HTML4 + CSS2
|
||||
self.FMT_B_B : "<b>",
|
||||
self.FMT_B_E : "</b>",
|
||||
self.FMT_I_B : "<i>",
|
||||
self.FMT_I_E : "</i>",
|
||||
self.FMT_D_B : "<span style='text-decoration: line-through;'>",
|
||||
self.FMT_D_E : "</span>",
|
||||
htmlTags = { # HTML4 + CSS2
|
||||
self.FMT_B_B: "<b>",
|
||||
self.FMT_B_E: "</b>",
|
||||
self.FMT_I_B: "<i>",
|
||||
self.FMT_I_E: "</i>",
|
||||
self.FMT_D_B: "<span style='text-decoration: line-through;'>",
|
||||
self.FMT_D_E: "</span>",
|
||||
}
|
||||
else:
|
||||
htmlTags = { # HTML5
|
||||
self.FMT_B_B : "<strong>",
|
||||
self.FMT_B_E : "</strong>",
|
||||
self.FMT_I_B : "<em>",
|
||||
self.FMT_I_E : "</em>",
|
||||
self.FMT_D_B : "<del>",
|
||||
self.FMT_D_E : "</del>",
|
||||
htmlTags = { # HTML5
|
||||
self.FMT_B_B: "<strong>",
|
||||
self.FMT_B_E: "</strong>",
|
||||
self.FMT_I_B: "<em>",
|
||||
self.FMT_I_E: "</em>",
|
||||
self.FMT_D_B: "<del>",
|
||||
self.FMT_D_E: "</del>",
|
||||
}
|
||||
|
||||
if self.isNovel and self.genMode != self.M_PREVIEW:
|
||||
@@ -151,10 +148,12 @@ class ToHtml(Tokenizer):
|
||||
thisPar = []
|
||||
parStyle = None
|
||||
tmpResult = []
|
||||
hasHardBreak = False
|
||||
|
||||
for tType, tLine, tText, tFormat, tStyle in self.theTokens:
|
||||
|
||||
# Replace < and > before adding html tags
|
||||
tText = tText.replace("<", "<").replace(">", ">")
|
||||
|
||||
# Styles
|
||||
aStyle = []
|
||||
if tStyle is not None and self.cssStyles:
|
||||
@@ -182,6 +181,11 @@ class ToHtml(Tokenizer):
|
||||
if tStyle & self.A_Z_TOPMRG:
|
||||
aStyle.append("margin-top: 0;")
|
||||
|
||||
if tStyle & self.A_IND_L:
|
||||
aStyle.append("margin-left: %dpx;" % self.mainConf.tabWidth)
|
||||
if tStyle & self.A_IND_R:
|
||||
aStyle.append("margin-right: %dpx;" % self.mainConf.tabWidth)
|
||||
|
||||
if len(aStyle) > 0:
|
||||
hStyle = " style='%s'" % (" ".join(aStyle))
|
||||
else:
|
||||
@@ -196,16 +200,15 @@ class ToHtml(Tokenizer):
|
||||
if tType == self.T_EMPTY:
|
||||
if parStyle is None:
|
||||
parStyle = ""
|
||||
if hasHardBreak and self.cssStyles:
|
||||
if len(thisPar) > 1 and self.cssStyles:
|
||||
parClass = " class='break'"
|
||||
else:
|
||||
parClass = ""
|
||||
if len(thisPar) > 0:
|
||||
tTemp = "".join(thisPar)
|
||||
tTemp = "<br/>".join(thisPar)
|
||||
tmpResult.append("<p%s%s>%s</p>\n" % (parStyle, parClass, tTemp.rstrip()))
|
||||
thisPar = []
|
||||
parStyle = None
|
||||
hasHardBreak = False
|
||||
|
||||
elif tType == self.T_TITLE:
|
||||
tHead = tText.replace(r"\\", "<br/>")
|
||||
@@ -239,11 +242,7 @@ class ToHtml(Tokenizer):
|
||||
parStyle = hStyle
|
||||
for xPos, xLen, xFmt in reversed(tFormat):
|
||||
tTemp = tTemp[:xPos] + htmlTags[xFmt] + tTemp[xPos+xLen:]
|
||||
if tText.endswith(" "):
|
||||
thisPar.append(tTemp.rstrip() + "<br/>")
|
||||
hasHardBreak = True
|
||||
else:
|
||||
thisPar.append(tTemp.rstrip() + " ")
|
||||
thisPar.append(tTemp.rstrip())
|
||||
|
||||
elif tType == self.T_SYNOPSIS and self.doSynopsis:
|
||||
tmpResult.append(self._formatSynopsis(tText))
|
||||
@@ -266,7 +265,7 @@ class ToHtml(Tokenizer):
|
||||
def saveHTML5(self, savePath):
|
||||
"""Save the data to an .html file.
|
||||
"""
|
||||
with open(savePath, mode="w", encoding="utf8") as outFile:
|
||||
with open(savePath, mode="w", encoding="utf-8") as outFile:
|
||||
theStyle = self.getStyleSheet()
|
||||
theStyle.append("article {width: 800px; margin: 40px auto;}")
|
||||
bodyText = "".join(self.fullHTML)
|
||||
@@ -289,9 +288,9 @@ class ToHtml(Tokenizer):
|
||||
"</body>\n"
|
||||
"</html>\n"
|
||||
).format(
|
||||
projTitle = self.theProject.projName,
|
||||
htmlStyle = "\n".join(theStyle),
|
||||
bodyText = bodyText,
|
||||
projTitle=self.theProject.projName,
|
||||
htmlStyle="\n".join(theStyle),
|
||||
bodyText=bodyText,
|
||||
)
|
||||
outFile.write(theHtml)
|
||||
|
||||
@@ -406,17 +405,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.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – Text Tokenizer
|
||||
============================
|
||||
@@ -24,82 +23,91 @@ 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
|
||||
import nw
|
||||
import re
|
||||
import logging
|
||||
|
||||
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__)
|
||||
|
||||
|
||||
class Tokenizer():
|
||||
|
||||
# In-Text Format
|
||||
FMT_B_B = 1 # Begin bold
|
||||
FMT_B_E = 2 # End bold
|
||||
FMT_I_B = 3 # Begin italics
|
||||
FMT_I_E = 4 # End italics
|
||||
FMT_D_B = 5 # Begin strikeout
|
||||
FMT_D_E = 6 # End strikeout
|
||||
FMT_B_B = 1 # Begin bold
|
||||
FMT_B_E = 2 # End bold
|
||||
FMT_I_B = 3 # Begin italics
|
||||
FMT_I_E = 4 # End italics
|
||||
FMT_D_B = 5 # Begin strikeout
|
||||
FMT_D_E = 6 # End strikeout
|
||||
|
||||
# Block Type
|
||||
T_EMPTY = 1 # Empty line (new paragraph)
|
||||
T_SYNOPSIS = 2 # Synopsis comment
|
||||
T_COMMENT = 3 # Comment line
|
||||
T_KEYWORD = 4 # Command line
|
||||
T_TITLE = 5 # Title
|
||||
T_HEAD1 = 6 # Header 1
|
||||
T_HEAD2 = 7 # Header 2
|
||||
T_HEAD3 = 8 # Header 3
|
||||
T_HEAD4 = 9 # Header 4
|
||||
T_TEXT = 10 # Text line
|
||||
T_SEP = 11 # Scene separator
|
||||
T_SKIP = 12 # Paragraph break
|
||||
T_EMPTY = 1 # Empty line (new paragraph)
|
||||
T_SYNOPSIS = 2 # Synopsis comment
|
||||
T_COMMENT = 3 # Comment line
|
||||
T_KEYWORD = 4 # Command line
|
||||
T_TITLE = 5 # Title
|
||||
T_HEAD1 = 6 # Header 1
|
||||
T_HEAD2 = 7 # Header 2
|
||||
T_HEAD3 = 8 # Header 3
|
||||
T_HEAD4 = 9 # Header 4
|
||||
T_TEXT = 10 # Text line
|
||||
T_SEP = 11 # Scene separator
|
||||
T_SKIP = 12 # Paragraph break
|
||||
|
||||
# Block Style
|
||||
A_NONE = 0x0000 # No special style
|
||||
A_LEFT = 0x0001 # Left aligned
|
||||
A_RIGHT = 0x0002 # Right aligned
|
||||
A_CENTRE = 0x0004 # Centred
|
||||
A_JUSTIFY = 0x0008 # Justified
|
||||
A_PBB = 0x0010 # Page break before always
|
||||
A_PBB_AUT = 0x0020 # Page break before auto
|
||||
A_PBA = 0x0040 # Page break after always
|
||||
A_PBA_AUT = 0x0080 # Page break after auto
|
||||
A_Z_TOPMRG = 0x0100 # Zero top margin
|
||||
A_Z_BTMMRG = 0x0200 # Zero bottom margin
|
||||
A_NONE = 0x0000 # No special style
|
||||
A_LEFT = 0x0001 # Left aligned
|
||||
A_RIGHT = 0x0002 # Right aligned
|
||||
A_CENTRE = 0x0004 # Centred
|
||||
A_JUSTIFY = 0x0008 # Justified
|
||||
A_PBB = 0x0010 # Page break before always
|
||||
A_PBB_AUT = 0x0020 # Page break before auto
|
||||
A_PBA = 0x0040 # Page break after always
|
||||
A_PBA_AUT = 0x0080 # Page break after auto
|
||||
A_Z_TOPMRG = 0x0100 # Zero top margin
|
||||
A_Z_BTMMRG = 0x0200 # Zero bottom margin
|
||||
A_IND_L = 0x0400 # Left indentation
|
||||
A_IND_R = 0x0800 # Right indentation
|
||||
|
||||
def __init__(self, theProject, theParent):
|
||||
def __init__(self, theProject):
|
||||
|
||||
self.theProject = theProject
|
||||
self.theParent = theParent
|
||||
self.theParent = theProject.theParent
|
||||
self.mainConf = nw.CONFIG
|
||||
|
||||
# Data Variables
|
||||
self.theText = "" # The raw text to be tokenized
|
||||
self.theHandle = None # The handle associated with the text
|
||||
self.theItem = None # The NWItem associated with the handle
|
||||
self.theTokens = [] # The list of the processed tokens
|
||||
self.theResult = "" # The result of the last document
|
||||
self.theText = "" # The raw text to be tokenized
|
||||
self.theHandle = None # The handle associated with the text
|
||||
self.theItem = None # The NWItem associated with the handle
|
||||
self.theTokens = [] # The list of the processed tokens
|
||||
self.theResult = "" # The result of the last document
|
||||
|
||||
self.keepMarkdown = False # Whether to keep the markdown text
|
||||
self.theMarkdown = [] # The result novelWriter markdown of all documents
|
||||
self.keepMarkdown = False # Whether to keep the markdown text
|
||||
self.theMarkdown = [] # The result novelWriter markdown of all documents
|
||||
|
||||
# User Settings
|
||||
self.textFont = "Serif" # Output text font
|
||||
self.textSize = 11 # Output text size
|
||||
self.textFixed = False # Fixed width text
|
||||
self.lineHeight = 1.15 # Line height
|
||||
self.doJustify = False # Justify text
|
||||
self.doBodyText = True # Include body text
|
||||
self.doSynopsis = False # Also process synopsis comments
|
||||
self.doComments = False # Also process comments
|
||||
self.doKeywords = False # Also process keywords like tags and references
|
||||
self.textFont = "Serif" # Output text font
|
||||
self.textSize = 11 # Output text size
|
||||
self.textFixed = False # Fixed width text
|
||||
self.lineHeight = 1.15 # Line height in units of em
|
||||
self.blockIndent = 4.00 # Block indent in units of em
|
||||
self.doJustify = False # Justify text
|
||||
self.doBodyText = True # Include body text
|
||||
self.doSynopsis = False # Also process synopsis comments
|
||||
self.doComments = False # Also process comments
|
||||
self.doKeywords = False # Also process keywords like tags and references
|
||||
|
||||
## Title Margins
|
||||
# Margins
|
||||
self.marginTitle = (1.000, 0.500)
|
||||
self.marginHead1 = (1.000, 0.500)
|
||||
self.marginHead2 = (0.834, 0.500)
|
||||
@@ -108,23 +116,23 @@ class Tokenizer():
|
||||
self.marginText = (0.000, 0.584)
|
||||
self.marginMeta = (0.000, 0.584)
|
||||
|
||||
## Title Formats
|
||||
self.fmtTitle = "%title%" # Formatting for titles
|
||||
self.fmtChapter = "%title%" # Formatting for numbered chapters
|
||||
self.fmtUnNum = "%title%" # Formatting for unnumbered chapters
|
||||
self.fmtScene = "%title%" # Formatting for scenes
|
||||
self.fmtSection = "%title%" # Formatting for sections
|
||||
# Title Formats
|
||||
self.fmtTitle = "%title%" # Formatting for titles
|
||||
self.fmtChapter = "%title%" # Formatting for numbered chapters
|
||||
self.fmtUnNum = "%title%" # Formatting for unnumbered chapters
|
||||
self.fmtScene = "%title%" # Formatting for scenes
|
||||
self.fmtSection = "%title%" # Formatting for sections
|
||||
|
||||
self.hideScene = False # Do not include scene headers
|
||||
self.hideSection = False # Do not include section headers
|
||||
self.hideScene = False # Do not include scene headers
|
||||
self.hideSection = False # Do not include section headers
|
||||
|
||||
self.linkHeaders = False # Add an anchor before headers
|
||||
self.linkHeaders = False # Add an anchor before headers
|
||||
|
||||
# Instance Variables
|
||||
self.numChapter = 0 # Counter for chapter numbers
|
||||
self.numChScene = 0 # Counter for scene number within chapter
|
||||
self.numAbsScene = 0 # Counter for scene number within novel
|
||||
self.firstScene = False # Flag to indicate that the first scene of the chapter
|
||||
self.numChapter = 0 # Counter for chapter numbers
|
||||
self.numChScene = 0 # Counter for scene number within chapter
|
||||
self.numAbsScene = 0 # Counter for scene number within novel
|
||||
self.firstScene = False # Flag to indicate that the first scene of the chapter
|
||||
|
||||
# This File
|
||||
self.isNone = False
|
||||
@@ -141,6 +149,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
|
||||
|
||||
##
|
||||
@@ -176,7 +191,11 @@ class Tokenizer():
|
||||
return
|
||||
|
||||
def setLineHeight(self, lineHeight):
|
||||
self.lineHeight = float(lineHeight)
|
||||
self.lineHeight = min(max(float(lineHeight), 0.5), 5.0)
|
||||
return
|
||||
|
||||
def setBlockIndent(self, blockIndent):
|
||||
self.blockIndent = min(max(float(blockIndent), 0.0), 10.0)
|
||||
return
|
||||
|
||||
def setJustify(self, doJustify):
|
||||
@@ -249,7 +268,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
|
||||
@@ -268,20 +287,23 @@ class Tokenizer():
|
||||
if self.theItem is None:
|
||||
return False
|
||||
|
||||
self.theText = ""
|
||||
if theText is not None:
|
||||
# If the text is set, just use that
|
||||
self.theText = theText
|
||||
else:
|
||||
# Otherwise, load it from file
|
||||
theDocument = NWDoc(self.theProject, self.theParent)
|
||||
self.theText = theDocument.openDocument(theHandle)
|
||||
theDoc = NWDoc(self.theProject, theHandle)
|
||||
theText = theDoc.readDocument()
|
||||
if theText:
|
||||
self.theText = theText
|
||||
|
||||
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
|
||||
@@ -298,7 +320,7 @@ class Tokenizer():
|
||||
return True
|
||||
|
||||
def doPreProcessing(self):
|
||||
"""Reun trough the various replace doctionaries.
|
||||
"""Run trough the various replace doctionaries.
|
||||
"""
|
||||
# Process the user's auto-replace dictionary
|
||||
if len(self.theProject.autoReplace) > 0:
|
||||
@@ -319,9 +341,9 @@ class Tokenizer():
|
||||
does the standard escaped characters.
|
||||
"""
|
||||
escapeDict = {
|
||||
r"\*" : "*",
|
||||
r"\~" : "~",
|
||||
r"\_" : "_",
|
||||
r"\*": "*",
|
||||
r"\~": "~",
|
||||
r"\_": "_",
|
||||
}
|
||||
escReplace = re.compile(
|
||||
"|".join([re.escape(k) for k in escapeDict.keys()]), flags=re.DOTALL
|
||||
@@ -353,6 +375,12 @@ class Tokenizer():
|
||||
(QRegularExpression(nwRegEx.FMT_ST), [None, self.FMT_D_B, None, self.FMT_D_E]),
|
||||
]
|
||||
|
||||
# Determine default text alignment
|
||||
if self.isTitle or self.isPart:
|
||||
defAlign = self.A_CENTRE
|
||||
else:
|
||||
defAlign = self.A_NONE
|
||||
|
||||
self.theTokens = []
|
||||
tmpMarkdown = []
|
||||
nLine = 0
|
||||
@@ -362,7 +390,7 @@ class Tokenizer():
|
||||
# Tag lines starting with specific characters
|
||||
if len(aLine.strip()) == 0:
|
||||
self.theTokens.append((
|
||||
self.T_EMPTY, nLine, "", None, self.A_NONE
|
||||
self.T_EMPTY, nLine, "", None, defAlign
|
||||
))
|
||||
if self.keepMarkdown:
|
||||
tmpMarkdown.append("\n")
|
||||
@@ -372,48 +400,48 @@ class Tokenizer():
|
||||
synTag = cLine[:9].lower()
|
||||
if synTag == "synopsis:":
|
||||
self.theTokens.append((
|
||||
self.T_SYNOPSIS, nLine, cLine[9:].strip(), None, self.A_NONE
|
||||
self.T_SYNOPSIS, nLine, cLine[9:].strip(), None, defAlign
|
||||
))
|
||||
if self.doSynopsis and self.keepMarkdown:
|
||||
tmpMarkdown.append("%s\n" % aLine)
|
||||
else:
|
||||
self.theTokens.append((
|
||||
self.T_COMMENT, nLine, aLine[1:].strip(), None, self.A_NONE
|
||||
self.T_COMMENT, nLine, aLine[1:].strip(), None, defAlign
|
||||
))
|
||||
if self.doComments and self.keepMarkdown:
|
||||
tmpMarkdown.append("%s\n" % aLine)
|
||||
|
||||
elif aLine[0] == "@":
|
||||
self.theTokens.append((
|
||||
self.T_KEYWORD, nLine, aLine[1:].strip(), None, self.A_NONE
|
||||
self.T_KEYWORD, nLine, aLine[1:].strip(), None, defAlign
|
||||
))
|
||||
if self.doKeywords and self.keepMarkdown:
|
||||
tmpMarkdown.append("%s\n" % aLine)
|
||||
|
||||
elif aLine[:2] == "# ":
|
||||
self.theTokens.append((
|
||||
self.T_HEAD1, nLine, aLine[2:].strip(), None, self.A_NONE
|
||||
self.T_HEAD1, nLine, aLine[2:].strip(), None, defAlign
|
||||
))
|
||||
if self.keepMarkdown:
|
||||
tmpMarkdown.append("%s\n" % aLine)
|
||||
|
||||
elif aLine[:3] == "## ":
|
||||
self.theTokens.append((
|
||||
self.T_HEAD2, nLine, aLine[3:].strip(), None, self.A_NONE
|
||||
self.T_HEAD2, nLine, aLine[3:].strip(), None, defAlign
|
||||
))
|
||||
if self.keepMarkdown:
|
||||
tmpMarkdown.append("%s\n" % aLine)
|
||||
|
||||
elif aLine[:4] == "### ":
|
||||
self.theTokens.append((
|
||||
self.T_HEAD3, nLine, aLine[4:].strip(), None, self.A_NONE
|
||||
self.T_HEAD3, nLine, aLine[4:].strip(), None, defAlign
|
||||
))
|
||||
if self.keepMarkdown:
|
||||
tmpMarkdown.append("%s\n" % aLine)
|
||||
|
||||
elif aLine[:5] == "#### ":
|
||||
self.theTokens.append((
|
||||
self.T_HEAD4, nLine, aLine[5:].strip(), None, self.A_NONE
|
||||
self.T_HEAD4, nLine, aLine[5:].strip(), None, defAlign
|
||||
))
|
||||
if self.keepMarkdown:
|
||||
tmpMarkdown.append("%s\n" % aLine)
|
||||
@@ -423,6 +451,38 @@ class Tokenizer():
|
||||
# Skip all body text
|
||||
continue
|
||||
|
||||
# Check Alignment and Indentation
|
||||
tagLeft = False
|
||||
tagRight = False
|
||||
indLeft = False
|
||||
indRight = False
|
||||
if aLine.startswith(">>"):
|
||||
tagRight = True
|
||||
aLine = aLine[2:].lstrip(" ")
|
||||
elif aLine.startswith(">"):
|
||||
indLeft = True
|
||||
aLine = aLine[1:].lstrip(" ")
|
||||
|
||||
if aLine.endswith("<<"):
|
||||
tagLeft = True
|
||||
aLine = aLine[:-2].rstrip(" ")
|
||||
elif aLine.endswith("<"):
|
||||
indRight = True
|
||||
aLine = aLine[:-1].rstrip(" ")
|
||||
|
||||
textAlign = defAlign
|
||||
if tagLeft and tagRight:
|
||||
textAlign = self.A_CENTRE
|
||||
elif tagLeft:
|
||||
textAlign = self.A_LEFT
|
||||
elif tagRight:
|
||||
textAlign = self.A_RIGHT
|
||||
|
||||
if indLeft:
|
||||
textAlign |= self.A_IND_L
|
||||
if indRight:
|
||||
textAlign |= self.A_IND_R
|
||||
|
||||
# Otherwise we use RegEx to find formatting tags within a line of text
|
||||
fmtPos = []
|
||||
for theRX, theKeys in rxFormats:
|
||||
@@ -439,14 +499,14 @@ class Tokenizer():
|
||||
# sorted by position
|
||||
fmtPos = sorted(fmtPos, key=itemgetter(0))
|
||||
self.theTokens.append((
|
||||
self.T_TEXT, nLine, aLine, fmtPos, self.A_NONE
|
||||
self.T_TEXT, nLine, aLine, fmtPos, textAlign
|
||||
))
|
||||
if self.keepMarkdown:
|
||||
tmpMarkdown.append("%s\n" % aLine)
|
||||
|
||||
# Always add an empty line at the end
|
||||
self.theTokens.append((
|
||||
self.T_EMPTY, nLine, "", None, self.A_NONE
|
||||
self.T_EMPTY, nLine, "", None, defAlign
|
||||
))
|
||||
if self.keepMarkdown:
|
||||
tmpMarkdown.append("\n")
|
||||
@@ -458,8 +518,8 @@ class Tokenizer():
|
||||
# ===========
|
||||
# Some items need a second pass
|
||||
|
||||
pToken = (self.T_EMPTY, 0, "", None, self.A_NONE)
|
||||
nToken = (self.T_EMPTY, 0, "", None, self.A_NONE)
|
||||
pToken = (self.T_EMPTY, 0, "", None, defAlign)
|
||||
nToken = (self.T_EMPTY, 0, "", None, defAlign)
|
||||
tCount = len(self.theTokens)
|
||||
for n, tToken in enumerate(self.theTokens):
|
||||
|
||||
@@ -588,55 +648,48 @@ class Tokenizer():
|
||||
tToken[0], tToken[1], tTemp, None, self.A_NONE
|
||||
)
|
||||
|
||||
# For title page and partitions, we need to centre all text.
|
||||
# For partition, we also add a page break before, and for
|
||||
# both types we always add a page break after the content.
|
||||
# We also swap header level 1 with a title type instead.
|
||||
# For title page we use a different title class, and we will set
|
||||
# an automatic page break before (i.e. added if needed). For
|
||||
# partitions we always need a page break before.
|
||||
if self.isTitle or self.isPart:
|
||||
for n, tToken in enumerate(self.theTokens):
|
||||
aStyle = tToken[4]
|
||||
if n == 0:
|
||||
if self.isTitle:
|
||||
aStyle |= self.A_PBB_AUT
|
||||
else:
|
||||
aStyle |= self.A_PBB
|
||||
|
||||
if tToken[0] == self.T_HEAD1:
|
||||
if self.isTitle:
|
||||
aStyle = self.A_PBB_AUT | self.A_CENTRE
|
||||
self.theTokens[n] = (
|
||||
self.T_TITLE, tToken[1], tToken[2], tToken[3], aStyle
|
||||
)
|
||||
else:
|
||||
aStyle = self.A_PBB | self.A_CENTRE
|
||||
self.theTokens[n] = (
|
||||
tToken[0], tToken[1], tToken[2], tToken[3], aStyle
|
||||
)
|
||||
else:
|
||||
self.theTokens[n] = (
|
||||
tToken[0], tToken[1], tToken[2], tToken[3], self.A_CENTRE
|
||||
)
|
||||
|
||||
# Add a page break after the last entry
|
||||
n = len(self.theTokens) - 1
|
||||
if n >= 0:
|
||||
tToken = self.theTokens[n]
|
||||
self.theTokens[n] = (
|
||||
if len(self.theTokens) > 0:
|
||||
tToken = self.theTokens[-1]
|
||||
self.theTokens[-1] = (
|
||||
tToken[0], tToken[1], tToken[2], tToken[3], tToken[4] | self.A_PBA
|
||||
)
|
||||
|
||||
# A single page is always left-aligned and starts on a fresh
|
||||
# page, unless it's empty.
|
||||
if self.isPage:
|
||||
for n, tToken in enumerate(self.theTokens):
|
||||
if n == 0:
|
||||
self.theTokens[n] = (
|
||||
tToken[0], tToken[1], tToken[2], tToken[3], self.A_LEFT | self.A_PBB
|
||||
)
|
||||
else:
|
||||
self.theTokens[n] = (
|
||||
tToken[0], tToken[1], tToken[2], tToken[3], self.A_LEFT
|
||||
)
|
||||
# A single page always starts on a fresh page, unless it's empty.
|
||||
if self.isPage and len(self.theTokens) > 0:
|
||||
tToken = self.theTokens[0]
|
||||
self.theTokens[0] = (
|
||||
tToken[0], tToken[1], tToken[2], tToken[3], tToken[4] | self.A_PBB
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def saveRawMarkdown(self, savePath):
|
||||
"""Save the data to a plain text file.
|
||||
"""
|
||||
with open(savePath, mode="w", encoding="utf8") as outFile:
|
||||
with open(savePath, mode="w", encoding="utf-8") as outFile:
|
||||
for nwdPage in self.theMarkdown:
|
||||
outFile.write(nwdPage)
|
||||
return
|
||||
@@ -653,11 +706,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
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – Markdown Text Converter
|
||||
=====================================
|
||||
@@ -26,18 +25,19 @@ 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__)
|
||||
|
||||
|
||||
class ToMarkdown(Tokenizer):
|
||||
|
||||
M_STD = 0 # Standard Markdown
|
||||
M_GH = 1 # GitHub Markdown
|
||||
M_STD = 0 # Standard Markdown
|
||||
M_GH = 1 # GitHub Markdown
|
||||
|
||||
def __init__(self, theProject, theParent):
|
||||
Tokenizer.__init__(self, theProject, theParent)
|
||||
def __init__(self, theProject):
|
||||
Tokenizer.__init__(self, theProject)
|
||||
|
||||
self.genMode = self.M_STD
|
||||
self.fullMD = []
|
||||
@@ -72,16 +72,22 @@ class ToMarkdown(Tokenizer):
|
||||
if self.genMode == self.M_STD:
|
||||
# Standard
|
||||
mdTags = {
|
||||
self.FMT_B_B : "**", self.FMT_B_E : "**",
|
||||
self.FMT_I_B : "_", self.FMT_I_E : "_",
|
||||
self.FMT_D_B : "", self.FMT_D_E : "",
|
||||
self.FMT_B_B: "**",
|
||||
self.FMT_B_E: "**",
|
||||
self.FMT_I_B: "_",
|
||||
self.FMT_I_E: "_",
|
||||
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 : "_",
|
||||
self.FMT_D_B : "~~", self.FMT_D_E : "~~",
|
||||
self.FMT_B_B: "**",
|
||||
self.FMT_B_E: "**",
|
||||
self.FMT_I_B: "_",
|
||||
self.FMT_I_E: "_",
|
||||
self.FMT_D_B: "~~",
|
||||
self.FMT_D_E: "~~",
|
||||
}
|
||||
|
||||
self.theResult = ""
|
||||
@@ -89,12 +95,12 @@ class ToMarkdown(Tokenizer):
|
||||
thisPar = []
|
||||
tmpResult = []
|
||||
|
||||
for tType, tLine, tText, tFormat, tStyle in self.theTokens:
|
||||
for tType, _, tText, tFormat, tStyle in self.theTokens:
|
||||
|
||||
# Process Text Type
|
||||
if tType == self.T_EMPTY:
|
||||
if len(thisPar) > 0:
|
||||
tTemp = "".join(thisPar)
|
||||
tTemp = " \n".join(thisPar)
|
||||
tmpResult.append("%s\n\n" % tTemp.rstrip(" "))
|
||||
thisPar = []
|
||||
|
||||
@@ -128,16 +134,13 @@ class ToMarkdown(Tokenizer):
|
||||
tTemp = tText
|
||||
for xPos, xLen, xFmt in reversed(tFormat):
|
||||
tTemp = tTemp[:xPos] + mdTags[xFmt] + tTemp[xPos+xLen:]
|
||||
if tText.endswith(" "):
|
||||
thisPar.append(tTemp.rstrip() + " \n")
|
||||
else:
|
||||
thisPar.append(tTemp.rstrip() + " ")
|
||||
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))
|
||||
@@ -152,7 +155,7 @@ class ToMarkdown(Tokenizer):
|
||||
def saveMarkdown(self, savePath):
|
||||
"""Save the data to a plain text file.
|
||||
"""
|
||||
with open(savePath, mode="w", encoding="utf8") as outFile:
|
||||
with open(savePath, mode="w", encoding="utf-8") as outFile:
|
||||
theText = "".join(self.fullMD)
|
||||
outFile.write(theText)
|
||||
|
||||
@@ -176,7 +179,7 @@ class ToMarkdown(Tokenizer):
|
||||
def _formatKeywords(self, tText, tStyle):
|
||||
"""Apply Markdown formatting to keywords.
|
||||
"""
|
||||
isValid, theBits, thePos = self.theParent.theIndex.scanThis("@"+tText)
|
||||
isValid, theBits, _ = self.theParent.theIndex.scanThis("@"+tText)
|
||||
if not isValid or not theBits:
|
||||
return ""
|
||||
|
||||
|
||||
194
nw/core/toodt.py
194
nw/core/toodt.py
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – ODT Text Converter
|
||||
================================
|
||||
@@ -29,22 +28,22 @@ 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__)
|
||||
|
||||
# Main XML NameSpaces
|
||||
XML_NS = {
|
||||
"office" : "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
|
||||
"style" : "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
|
||||
"loext" : "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0",
|
||||
"text" : "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
|
||||
"meta" : "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
|
||||
"fo" : "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
|
||||
"office": "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
|
||||
"style": "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
|
||||
"loext": "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0",
|
||||
"text": "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
|
||||
"meta": "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
|
||||
"fo": "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
|
||||
}
|
||||
|
||||
# Mimetype and Version
|
||||
@@ -57,38 +56,39 @@ TAG_TAB = "{%s}tab" % XML_NS["text"]
|
||||
TAG_SPAN = "{%s}span" % XML_NS["text"]
|
||||
TAG_STNM = "{%s}style-name" % XML_NS["text"]
|
||||
|
||||
|
||||
class ToOdt(Tokenizer):
|
||||
|
||||
X_BLD = 0x01 # Bold format
|
||||
X_ITA = 0x02 # Italic format
|
||||
X_DEL = 0x04 # Strikethrough format
|
||||
X_BRK = 0x08 # Line break
|
||||
X_TAB = 0x10 # Tab
|
||||
X_BLD = 0x01 # Bold format
|
||||
X_ITA = 0x02 # Italic format
|
||||
X_DEL = 0x04 # Strikethrough format
|
||||
X_BRK = 0x08 # Line break
|
||||
X_TAB = 0x10 # Tab
|
||||
|
||||
def __init__(self, theProject, theParent, isFlat):
|
||||
Tokenizer.__init__(self, theProject, theParent)
|
||||
def __init__(self, theProject, isFlat):
|
||||
Tokenizer.__init__(self, theProject)
|
||||
|
||||
self.mainConf = nw.CONFIG
|
||||
|
||||
self._isFlat = isFlat # Flat: .fodt, otherwise .odt
|
||||
self._isFlat = isFlat # Flat: .fodt, otherwise .odt
|
||||
|
||||
self._dFlat = None # FODT file XML root
|
||||
self._dCont = None # ODT content.xml root
|
||||
self._dMeta = None # ODT meta.xml root
|
||||
self._dStyl = None # ODT styles.xml root
|
||||
self._dFlat = None # FODT file XML root
|
||||
self._dCont = None # ODT content.xml root
|
||||
self._dMeta = None # ODT meta.xml root
|
||||
self._dStyl = None # ODT styles.xml root
|
||||
|
||||
self._xMeta = None # Office meta root
|
||||
self._xStyl = None # Office styles root
|
||||
self._xAuto = None # Office auto-styles root
|
||||
self._xMast = None # Office master-styles root
|
||||
self._xBody = None # Office body root
|
||||
self._xText = None # Office text root
|
||||
self._xMeta = None # Office meta root
|
||||
self._xStyl = None # Office styles root
|
||||
self._xAuto = None # Office auto-styles root
|
||||
self._xMast = None # Office master-styles root
|
||||
self._xBody = None # Office body root
|
||||
self._xText = None # Office text root
|
||||
|
||||
self._xAut2 = None # Page layout auto-styles for ODT file
|
||||
self._xAut2 = None # Page layout auto-styles for ODT file
|
||||
|
||||
self._mainPara = {} # User-accessible paragraph styles
|
||||
self._autoPara = {} # Auto-generated paragraph styles
|
||||
self._autoText = {} # Auto-generated text styles
|
||||
self._mainPara = {} # User-accessible paragraph styles
|
||||
self._autoPara = {} # Auto-generated paragraph styles
|
||||
self._autoText = {} # Auto-generated text styles
|
||||
|
||||
# Properties
|
||||
self.textFont = "Liberation Serif"
|
||||
@@ -99,21 +99,22 @@ class ToOdt(Tokenizer):
|
||||
self.headerText = ""
|
||||
|
||||
# Internal
|
||||
self._fontFamily = "'Liberation Sans'"
|
||||
self._fontPitch = "variable"
|
||||
self._fSizeTitle = "30pt"
|
||||
self._fSizeHead1 = "24pt"
|
||||
self._fSizeHead2 = "20pt"
|
||||
self._fSizeHead3 = "16pt"
|
||||
self._fSizeHead4 = "14pt"
|
||||
self._fSizeHead = "14pt"
|
||||
self._fSizeText = "12pt"
|
||||
self._lineHeight = "115%"
|
||||
self._textAlign = "left"
|
||||
self._dLanguage = "en"
|
||||
self._dCountry = "GB"
|
||||
self._fontFamily = "'Liberation Sans'"
|
||||
self._fontPitch = "variable"
|
||||
self._fSizeTitle = "30pt"
|
||||
self._fSizeHead1 = "24pt"
|
||||
self._fSizeHead2 = "20pt"
|
||||
self._fSizeHead3 = "16pt"
|
||||
self._fSizeHead4 = "14pt"
|
||||
self._fSizeHead = "14pt"
|
||||
self._fSizeText = "12pt"
|
||||
self._lineHeight = "115%"
|
||||
self._blockIndent = "1.693cm"
|
||||
self._textAlign = "left"
|
||||
self._dLanguage = "en"
|
||||
self._dCountry = "GB"
|
||||
|
||||
## Text Margings in Units of em
|
||||
# Text Margings in Units of em
|
||||
self._mTopTitle = "0.423cm"
|
||||
self._mTopHead1 = "0.423cm"
|
||||
self._mTopHead2 = "0.353cm"
|
||||
@@ -132,13 +133,13 @@ class ToOdt(Tokenizer):
|
||||
self._mBotText = "0.247cm"
|
||||
self._mBotMeta = "0.106cm"
|
||||
|
||||
## Document Margins
|
||||
# Document Margins
|
||||
self._mDocTop = "2.000cm"
|
||||
self._mDocBtm = "2.000cm"
|
||||
self._mDocLeft = "2.000cm"
|
||||
self._mDocRight = "2.000cm"
|
||||
|
||||
## Colour
|
||||
# Colour
|
||||
self._colHead12 = None
|
||||
self._opaHead12 = None
|
||||
self._colHead34 = None
|
||||
@@ -222,8 +223,9 @@ class ToOdt(Tokenizer):
|
||||
self._colMetaTx = "#813709"
|
||||
self._opaMetaTx = "100%"
|
||||
|
||||
self._lineHeight = f"{round(100 * self.lineHeight):d}%"
|
||||
self._textAlign = "justify" if self.doJustify else "left"
|
||||
self._lineHeight = f"{round(100 * self.lineHeight):d}%"
|
||||
self._blockIndent = self._emToCm(self.blockIndent)
|
||||
self._textAlign = "justify" if self.doJustify else "left"
|
||||
|
||||
# Document Header
|
||||
# ===============
|
||||
@@ -298,7 +300,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__}"
|
||||
@@ -313,22 +315,21 @@ class ToOdt(Tokenizer):
|
||||
def doConvert(self):
|
||||
"""Convert the list of text tokens into XML elements.
|
||||
"""
|
||||
self.theResult = "" # Not used, but cleared just in case
|
||||
self.theResult = "" # Not used, but cleared just in case
|
||||
|
||||
odtTags = {
|
||||
self.FMT_B_B : "_B", # Bold open format
|
||||
self.FMT_B_E : "b_", # Bold close format
|
||||
self.FMT_I_B : "I", # Italic open format
|
||||
self.FMT_I_E : "i", # Italic close format
|
||||
self.FMT_D_B : "_S", # Strikethrough open format
|
||||
self.FMT_D_E : "s_", # Strikethrough close format
|
||||
self.FMT_B_B: "_B", # Bold open format
|
||||
self.FMT_B_E: "b_", # Bold close format
|
||||
self.FMT_I_B: "I", # Italic open format
|
||||
self.FMT_I_E: "i", # Italic close format
|
||||
self.FMT_D_B: "_S", # Strikethrough open format
|
||||
self.FMT_D_E: "s_", # Strikethrough close format
|
||||
}
|
||||
|
||||
thisPar = []
|
||||
thisFmt = []
|
||||
parStyle = None
|
||||
hasHardBreak = False
|
||||
for tType, tLine, tText, tFormat, tStyle in self.theTokens:
|
||||
for tType, _, tText, tFormat, tStyle in self.theTokens:
|
||||
|
||||
# Styles
|
||||
oStyle = ODTParagraphStyle()
|
||||
@@ -357,15 +358,20 @@ class ToOdt(Tokenizer):
|
||||
if tStyle & self.A_Z_TOPMRG:
|
||||
oStyle.setMarginTop("0.000cm")
|
||||
|
||||
if tStyle & self.A_IND_L:
|
||||
oStyle.setMarginLeft(self._blockIndent)
|
||||
if tStyle & self.A_IND_R:
|
||||
oStyle.setMarginRight(self._blockIndent)
|
||||
|
||||
# Process Text Types
|
||||
if tType == self.T_EMPTY:
|
||||
if hasHardBreak and parStyle is not None:
|
||||
if len(thisPar) > 1 and parStyle is not None:
|
||||
if self.doJustify:
|
||||
parStyle.setTextAlign("left")
|
||||
|
||||
if len(thisPar) > 0:
|
||||
tTemp = "".join(thisPar)
|
||||
fTemp = "".join(thisFmt)
|
||||
tTemp = "\n".join(thisPar)
|
||||
fTemp = " ".join(thisFmt)
|
||||
tTxt = tTemp.rstrip()
|
||||
tFmt = fTemp[:len(tTxt)]
|
||||
self._addTextPar("Text_Body", parStyle, tTxt, theFmt=tFmt)
|
||||
@@ -373,7 +379,6 @@ class ToOdt(Tokenizer):
|
||||
thisPar = []
|
||||
thisFmt = []
|
||||
parStyle = None
|
||||
hasHardBreak = False
|
||||
|
||||
elif tType == self.T_TITLE:
|
||||
tHead = tText.replace(r"\\", "\n")
|
||||
@@ -402,23 +407,17 @@ class ToOdt(Tokenizer):
|
||||
self._addTextPar("Text_Body", oStyle, "")
|
||||
|
||||
elif tType == self.T_TEXT:
|
||||
tTemp = tText
|
||||
if parStyle is None:
|
||||
parStyle = oStyle
|
||||
|
||||
tFmt = " "*len(tTemp)
|
||||
tFmt = " "*len(tText)
|
||||
for xPos, xLen, xFmt in tFormat:
|
||||
tFmt = tFmt[:xPos] + odtTags[xFmt] + tFmt[xPos+xLen:]
|
||||
|
||||
tTxt = tTemp.rstrip()
|
||||
tTxt = tText.rstrip()
|
||||
tFmt = tFmt[:len(tTxt)]
|
||||
if tText.endswith(" "):
|
||||
thisPar.append(tTxt + "\n")
|
||||
thisFmt.append(tFmt + " ")
|
||||
hasHardBreak = True
|
||||
else:
|
||||
thisPar.append(tTxt + " ")
|
||||
thisFmt.append(tFmt + " ")
|
||||
thisPar.append(tTxt)
|
||||
thisFmt.append(tFmt)
|
||||
|
||||
elif tType == self.T_SYNOPSIS and self.doSynopsis:
|
||||
tTemp, fTemp = self._formatSynopsis(tText)
|
||||
@@ -451,16 +450,16 @@ class ToOdt(Tokenizer):
|
||||
with open(savePath, mode="wb") as outFile:
|
||||
outFile.write(etree.tostring(
|
||||
self._dFlat,
|
||||
pretty_print = True,
|
||||
encoding = "utf-8",
|
||||
xml_declaration = True
|
||||
pretty_print=True,
|
||||
encoding="utf-8",
|
||||
xml_declaration=True
|
||||
))
|
||||
return
|
||||
|
||||
def saveOpenDocText(self, savePath):
|
||||
"""Save the data to an .odt file.
|
||||
"""
|
||||
mMap = {"manifest" : "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"}
|
||||
mMap = {"manifest": "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"}
|
||||
mMani = "{%s}manifest" % mMap["manifest"]
|
||||
mVers = "{%s}version" % mMap["manifest"]
|
||||
mPath = "{%s}full-path" % mMap["manifest"]
|
||||
@@ -474,7 +473,7 @@ class ToOdt(Tokenizer):
|
||||
etree.SubElement(xMani, mFile, attrib={mPath: "meta.xml", mType: "text/xml"})
|
||||
etree.SubElement(xMani, mFile, attrib={mPath: "styles.xml", mType: "text/xml"})
|
||||
|
||||
sMap = {"office" : "urn:oasis:names:tc:opendocument:xmlns:office:1.0"}
|
||||
sMap = {"office": "urn:oasis:names:tc:opendocument:xmlns:office:1.0"}
|
||||
oRoot = "{%s}document-settings" % sMap["office"]
|
||||
oSett = "{%s}settings" % sMap["office"]
|
||||
xSett = etree.Element(oRoot, nsmap=sMap)
|
||||
@@ -507,21 +506,23 @@ 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):
|
||||
"""Apply formatting to keywords.
|
||||
"""
|
||||
isValid, theBits, thePos = self.theParent.theIndex.scanThis("@"+tText)
|
||||
isValid, theBits, _ = self.theParent.theIndex.scanThis("@"+tText)
|
||||
if not isValid or not theBits:
|
||||
return ""
|
||||
|
||||
@@ -530,7 +531,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 +562,7 @@ class ToOdt(Tokenizer):
|
||||
##
|
||||
|
||||
if len(theText) != len(theFmt):
|
||||
# Genrate dummy format if there isn't any
|
||||
# Generate an empty format if there isn't any or it doesn't match
|
||||
theFmt = " "*len(theText)
|
||||
|
||||
# XML functions
|
||||
@@ -643,7 +644,7 @@ class ToOdt(Tokenizer):
|
||||
"""
|
||||
refStyle = self._mainPara.get(parName, None)
|
||||
if refStyle is None:
|
||||
logger.error("Unknown paragraph style '%s'" % parName)
|
||||
logger.error("Unknown paragraph style '%s'", parName)
|
||||
return "Standard"
|
||||
|
||||
if not refStyle.checkNew(oStyle):
|
||||
@@ -984,6 +985,7 @@ class ToOdt(Tokenizer):
|
||||
|
||||
# END Class ToOdt
|
||||
|
||||
|
||||
# =============================================================================================== #
|
||||
# Auto-Style Classes
|
||||
# =============================================================================================== #
|
||||
@@ -1014,6 +1016,8 @@ class ODTParagraphStyle():
|
||||
self._pAttr = {
|
||||
"margin-top": ["fo", None],
|
||||
"margin-bottom": ["fo", None],
|
||||
"margin-left": ["fo", None],
|
||||
"margin-right": ["fo", None],
|
||||
"line-height": ["fo", None],
|
||||
"text-align": ["fo", None],
|
||||
"break-before": ["fo", None],
|
||||
@@ -1070,6 +1074,14 @@ class ODTParagraphStyle():
|
||||
self._pAttr["margin-bottom"][1] = str(theValue)
|
||||
return
|
||||
|
||||
def setMarginLeft(self, theValue):
|
||||
self._pAttr["margin-left"][1] = str(theValue)
|
||||
return
|
||||
|
||||
def setMarginRight(self, theValue):
|
||||
self._pAttr["margin-right"][1] = str(theValue)
|
||||
return
|
||||
|
||||
def setLineHeight(self, theValue):
|
||||
self._pAttr["line-height"][1] = str(theValue)
|
||||
return
|
||||
@@ -1148,13 +1160,13 @@ class ODTParagraphStyle():
|
||||
"""Check if there are new settings in refStyle that differ from
|
||||
those in the current object.
|
||||
"""
|
||||
for aName, (aNm, aVal) in refStyle._mAttr.items():
|
||||
for aName, (_, aVal) in refStyle._mAttr.items():
|
||||
if aVal is not None and aVal != self._mAttr[aName][1]:
|
||||
return True
|
||||
for aName, (aNm, aVal) in refStyle._pAttr.items():
|
||||
for aName, (_, aVal) in refStyle._pAttr.items():
|
||||
if aVal is not None and aVal != self._pAttr[aName][1]:
|
||||
return True
|
||||
for aName, (aNm, aVal) in refStyle._tAttr.items():
|
||||
for aName, (_, aVal) in refStyle._tAttr.items():
|
||||
if aVal is not None and aVal != self._tAttr[aName][1]:
|
||||
return True
|
||||
return False
|
||||
@@ -1201,6 +1213,7 @@ class ODTParagraphStyle():
|
||||
|
||||
# END Class ODTParagraphStyle
|
||||
|
||||
|
||||
class ODTTextStyle():
|
||||
"""Wrapper class for the text style setting used by the exporter.
|
||||
Only the used settings are exposed here to keep the class minimal
|
||||
@@ -1271,6 +1284,7 @@ class ODTTextStyle():
|
||||
|
||||
# END Class ODTTextStyle
|
||||
|
||||
|
||||
# =============================================================================================== #
|
||||
# Local Functions
|
||||
# =============================================================================================== #
|
||||
@@ -1281,5 +1295,5 @@ def _mkTag(nsName, tagName):
|
||||
theNS = XML_NS.get(nsName, "")
|
||||
if theNS:
|
||||
return "{%s}%s" % (theNS, tagName)
|
||||
logger.warning("Missing xml namespace '%s'" % nsName)
|
||||
logger.warning("Missing xml namespace '%s'", nsName)
|
||||
return tagName
|
||||
|
||||
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
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – Project Tree Class
|
||||
================================
|
||||
@@ -25,18 +24,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
import logging
|
||||
import os
|
||||
import logging
|
||||
|
||||
from time import time
|
||||
from lxml import etree
|
||||
from hashlib import sha256
|
||||
from time import time
|
||||
|
||||
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__)
|
||||
|
||||
@@ -63,21 +61,23 @@ LAYOUT_MAP = {
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class NWTree():
|
||||
|
||||
def __init__(self, theProject):
|
||||
|
||||
self.theProject = theProject
|
||||
|
||||
self._projTree = {} # Holds all the items of the project
|
||||
self._treeOrder = [] # The order of the tree items on the tree view
|
||||
self._treeRoots = [] # The root items of the tree
|
||||
self._trashRoot = None # The handle of the trash root folder
|
||||
self._archRoot = None # The handle of the archive root folder
|
||||
self._theIndex = 0 # The current iterator index
|
||||
self._treeChanged = False # True if tree structure has changed
|
||||
self._projTree = {} # Holds all the items of the project
|
||||
self._treeOrder = [] # The order of the tree items on the tree view
|
||||
self._treeRoots = [] # The root items of the tree
|
||||
self._trashRoot = None # The handle of the trash root folder
|
||||
self._archRoot = None # The handle of the archive root folder
|
||||
self._theIndex = 0 # The current iterator index
|
||||
self._treeChanged = False # True if tree structure has changed
|
||||
|
||||
self._handleSeed = None # Used for generating handles for testing
|
||||
self._handleSeed = None # Used for generating handles for testing
|
||||
self._handleCount = 0 # A counter that is added to the handle generator
|
||||
|
||||
return
|
||||
|
||||
@@ -111,24 +111,24 @@ class NWTree():
|
||||
tHandle = self._makeHandle()
|
||||
|
||||
if tHandle in self._projTree:
|
||||
logger.warning("Duplicate handle %s detected, skipping" % tHandle)
|
||||
logger.warning("Duplicate handle '%s' detected, skipping", tHandle)
|
||||
return False
|
||||
|
||||
logger.verbose("Adding item %s with parent %s" % (str(tHandle), str(pHandle)))
|
||||
logger.verbose("Adding item '%s' with parent '%s'", str(tHandle), str(pHandle))
|
||||
|
||||
nwItem.setHandle(tHandle)
|
||||
nwItem.setParent(pHandle)
|
||||
|
||||
if nwItem.itemType == nwItemType.ROOT:
|
||||
logger.verbose("Item %s is a root item" % str(tHandle))
|
||||
logger.verbose("Item '%s' is a root item", str(tHandle))
|
||||
self._treeRoots.append(tHandle)
|
||||
if nwItem.itemClass == nwItemClass.ARCHIVE:
|
||||
logger.verbose("Item %s is the archive folder" % str(tHandle))
|
||||
logger.verbose("Item '%s' is the archive folder", str(tHandle))
|
||||
self._archRoot = tHandle
|
||||
|
||||
if nwItem.itemType == nwItemType.TRASH:
|
||||
if self._trashRoot is None:
|
||||
logger.verbose("Item %s is the trash folder" % str(tHandle))
|
||||
logger.verbose("Item '%s' is the trash folder", str(tHandle))
|
||||
self._trashRoot = tHandle
|
||||
else:
|
||||
logger.error("Only one trash folder allowed")
|
||||
@@ -193,7 +193,7 @@ class NWTree():
|
||||
try:
|
||||
# Dump the text
|
||||
tocText = os.path.join(self.theProject.projPath, nwFiles.TOC_TXT)
|
||||
with open(tocText, mode="w", encoding="utf8") as outFile:
|
||||
with open(tocText, mode="w", encoding="utf-8") as outFile:
|
||||
outFile.write("\n")
|
||||
outFile.write("Table of Contents\n")
|
||||
outFile.write("=================\n")
|
||||
@@ -245,9 +245,10 @@ class NWTree():
|
||||
if iLayout in LAYOUT_MAP:
|
||||
if hLevel in LAYOUT_MAP[iLayout]:
|
||||
tItem.itemLayout = LAYOUT_MAP[iLayout][hLevel]
|
||||
logger.debug("Changed layout for %s from %s to %s" % (
|
||||
logger.debug(
|
||||
"Changed layout for %s from %s to %s",
|
||||
tHandle, iLayout.name, tItem.itemLayout.name
|
||||
))
|
||||
)
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -357,13 +358,13 @@ class NWTree():
|
||||
if tHandle in self._projTree:
|
||||
tmpOrder.append(tHandle)
|
||||
else:
|
||||
logger.error("Handle %s in new tree order is not in project tree" % tHandle)
|
||||
logger.error("Handle '%s' in new tree order is not in project tree", tHandle)
|
||||
|
||||
# Do a reverse lookup to check for items that will be lost
|
||||
# This is mainly for debugging purposes
|
||||
for tHandle in self._treeOrder:
|
||||
if tHandle not in tmpOrder:
|
||||
logger.warning("Handle %s in old tree order is not in new tree order" % tHandle)
|
||||
logger.warning("Handle '%s' in old tree order is not in new tree order", tHandle)
|
||||
|
||||
# Save the temp list
|
||||
self._treeOrder = tmpOrder
|
||||
@@ -387,7 +388,7 @@ class NWTree():
|
||||
if tItem is None:
|
||||
return False
|
||||
if tItem.itemType != nwItemType.FILE:
|
||||
logger.error("Item %s is not a file" % tHandle)
|
||||
logger.error("Item %s is not a file", tHandle)
|
||||
return False
|
||||
if not isinstance(itemLayout, nwItemLayout):
|
||||
return False
|
||||
@@ -444,7 +445,7 @@ class NWTree():
|
||||
"""
|
||||
if tHandle in self._projTree:
|
||||
return self._projTree[tHandle]
|
||||
logger.error("No tree item with handle %s" % str(tHandle))
|
||||
logger.error("No tree item with handle '%s'", str(tHandle))
|
||||
return None
|
||||
|
||||
def __delitem__(self, tHandle):
|
||||
@@ -454,7 +455,7 @@ class NWTree():
|
||||
self._treeOrder.remove(tHandle)
|
||||
del self._projTree[tHandle]
|
||||
else:
|
||||
logger.warning("Failed to delete item %s: item not found" % tHandle)
|
||||
logger.warning("Failed to delete item '%s': item not found", tHandle)
|
||||
return
|
||||
|
||||
if tHandle in self._treeRoots:
|
||||
@@ -516,13 +517,14 @@ class NWTree():
|
||||
handle requests come faster than the clock resolution.
|
||||
"""
|
||||
if self._handleSeed is None:
|
||||
newSeed = str(time()) + addSeed
|
||||
newSeed = "%s_%d_%s" % (str(time()), self._handleCount, addSeed)
|
||||
self._handleCount += 1
|
||||
else:
|
||||
# This is used for debugging
|
||||
newSeed = str(self._handleSeed)
|
||||
self._handleSeed += 1
|
||||
|
||||
logger.verbose("Generating handle with seed '%s'" % newSeed)
|
||||
logger.verbose("Generating handle with seed '%s'", newSeed)
|
||||
itemHandle = sha256(newSeed.encode()).hexdigest()[0:13]
|
||||
if itemHandle in self._projTree:
|
||||
logger.warning("Duplicate handle encountered! Retrying ...")
|
||||
|
||||
42
nw/dialogs/__init__.py
Normal file
42
nw/dialogs/__init__.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""
|
||||
novelWriter – Dialogs Init
|
||||
==========================
|
||||
|
||||
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/>.
|
||||
"""
|
||||
|
||||
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",
|
||||
]
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – GUI About Box
|
||||
===========================
|
||||
@@ -25,13 +24,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
import logging
|
||||
import os
|
||||
import logging
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QCursor
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import (
|
||||
qApp, QDialog, QHBoxLayout, QVBoxLayout, QDialogButtonBox, QTabWidget,
|
||||
QTextBrowser, QLabel
|
||||
@@ -39,6 +38,7 @@ from PyQt5.QtWidgets import (
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiAbout(QDialog):
|
||||
|
||||
def __init__(self, theParent):
|
||||
@@ -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,93 @@ 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"),
|
||||
("简体中文", "Qianzhi Long"),
|
||||
])
|
||||
)
|
||||
|
||||
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)
|
||||
@@ -217,7 +229,7 @@ class GuiAbout(QDialog):
|
||||
"""
|
||||
docPath = os.path.join(self.mainConf.assetPath, "text", "release_notes.htm")
|
||||
if os.path.isfile(docPath):
|
||||
with open(docPath, mode="r", encoding="utf8") as inFile:
|
||||
with open(docPath, mode="r", encoding="utf-8") as inFile:
|
||||
helpText = inFile.read()
|
||||
self.pageNotes.setHtml(helpText)
|
||||
else:
|
||||
@@ -225,17 +237,27 @@ 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):
|
||||
with open(docPath, mode="r", encoding="utf8") as inFile:
|
||||
with open(docPath, mode="r", encoding="utf-8") as inFile:
|
||||
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,10 +268,13 @@ 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],
|
||||
hColB = self.theParent.theTheme.colHead[2],
|
||||
hColR=self.theParent.theTheme.colHead[0],
|
||||
hColG=self.theParent.theTheme.colHead[1],
|
||||
hColB=self.theParent.theTheme.colHead[2],
|
||||
)
|
||||
self.pageAbout.document().setDefaultStyleSheet(styleSheet)
|
||||
self.pageNotes.document().setDefaultStyleSheet(styleSheet)
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – GUI Doc Merge Tool
|
||||
================================
|
||||
@@ -33,15 +32,16 @@ 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__)
|
||||
|
||||
|
||||
class GuiDocMerge(QDialog):
|
||||
|
||||
def __init__(self, theParent, theProject):
|
||||
def __init__(self, theParent):
|
||||
QDialog.__init__(self, theParent)
|
||||
|
||||
logger.debug("Initialising GuiDocMerge ...")
|
||||
@@ -49,15 +49,15 @@ class GuiDocMerge(QDialog):
|
||||
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theParent = theParent
|
||||
self.theProject = theProject
|
||||
self.theProject = theParent.theProject
|
||||
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,42 +102,51 @@ class GuiDocMerge(QDialog):
|
||||
finalOrder.append(self.listBox.item(i).data(Qt.UserRole))
|
||||
|
||||
if len(finalOrder) == 0:
|
||||
self.theParent.makeAlert((
|
||||
self.theParent.makeAlert(self.tr(
|
||||
"No source documents found. Nothing to do."
|
||||
), nwAlert.ERROR)
|
||||
return
|
||||
return False
|
||||
|
||||
theDoc = NWDoc(self.theProject, self.theParent)
|
||||
theText = ""
|
||||
for tHandle in finalOrder:
|
||||
theText += theDoc.openDocument(tHandle, False).rstrip("\n")
|
||||
theText += "\n\n"
|
||||
inDoc = NWDoc(self.theProject, tHandle)
|
||||
docText = inDoc.readDocument()
|
||||
docErr = inDoc.getError()
|
||||
if docText is None and docErr:
|
||||
self.theParent.makeAlert([
|
||||
self.tr("Failed to open document file."), docErr
|
||||
], nwAlert.ERROR)
|
||||
if docText:
|
||||
theText += docText.rstrip("\n")+"\n\n"
|
||||
|
||||
if self.sourceItem is None:
|
||||
self.theParent.makeAlert((
|
||||
"No source document selected. Nothing to do."
|
||||
self.theParent.makeAlert(self.tr(
|
||||
"No source folder selected. Nothing to do."
|
||||
), nwAlert.ERROR)
|
||||
return
|
||||
return False
|
||||
|
||||
srcItem = self.theProject.projTree[self.sourceItem]
|
||||
if srcItem is None:
|
||||
self.theParent.makeAlert((
|
||||
"Could not parse source document."
|
||||
), nwAlert.ERROR)
|
||||
return
|
||||
self.theParent.makeAlert(self.tr("Internal error."), nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemClass, srcItem.itemParent)
|
||||
newItem = self.theProject.projTree[nHandle]
|
||||
newItem.setStatus(srcItem.itemStatus)
|
||||
|
||||
theDoc.openDocument(nHandle, False)
|
||||
theDoc.saveDocument(theText)
|
||||
outDoc = NWDoc(self.theProject, nHandle)
|
||||
if not outDoc.writeDocument(theText):
|
||||
self.theParent.makeAlert([
|
||||
self.tr("Could not save document."), outDoc.getError()
|
||||
], nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
self.theParent.treeView.revealNewTreeItem(nHandle)
|
||||
self.theParent.openDocument(nHandle, doScroll=True)
|
||||
|
||||
self._doClose()
|
||||
|
||||
return
|
||||
return True
|
||||
|
||||
def _doClose(self):
|
||||
"""Close the dialog window without doing anything.
|
||||
@@ -158,16 +167,17 @@ class GuiDocMerge(QDialog):
|
||||
tHandle = self.theParent.treeView.getSelectedHandle()
|
||||
self.sourceItem = tHandle
|
||||
if tHandle is None:
|
||||
return
|
||||
return False
|
||||
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
if nwItem is None:
|
||||
return
|
||||
return False
|
||||
|
||||
if nwItem.itemType is not nwItemType.FOLDER:
|
||||
self.theParent.makeAlert((
|
||||
self.theParent.makeAlert(self.tr(
|
||||
"Element selected in the project tree must be a folder."
|
||||
), nwAlert.ERROR)
|
||||
return
|
||||
return False
|
||||
|
||||
for sHandle in self.theParent.treeView.getTreeFromHandle(tHandle):
|
||||
newItem = QListWidgetItem()
|
||||
@@ -178,6 +188,6 @@ class GuiDocMerge(QDialog):
|
||||
newItem.setData(Qt.UserRole, sHandle)
|
||||
self.listBox.addItem(newItem)
|
||||
|
||||
return
|
||||
return True
|
||||
|
||||
# END Class GuiDocMerge
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – GUI Doc Split Tool
|
||||
================================
|
||||
@@ -34,16 +33,16 @@ 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__)
|
||||
|
||||
|
||||
class GuiDocSplit(QDialog):
|
||||
|
||||
def __init__(self, theParent, theProject):
|
||||
def __init__(self, theParent):
|
||||
QDialog.__init__(self, theParent)
|
||||
|
||||
logger.debug("Initialising GuiDocSplit ...")
|
||||
@@ -51,16 +50,19 @@ class GuiDocSplit(QDialog):
|
||||
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theParent = theParent
|
||||
self.theProject = theProject
|
||||
self.optState = self.theProject.optState
|
||||
self.theProject = theParent.theProject
|
||||
self.optState = theParent.theProject.optState
|
||||
|
||||
self.sourceItem = None
|
||||
self.sourceText = []
|
||||
|
||||
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 +71,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,23 +117,31 @@ class GuiDocSplit(QDialog):
|
||||
logger.verbose("GuiDocSplit split button clicked")
|
||||
|
||||
if self.sourceItem is None:
|
||||
self.theParent.makeAlert((
|
||||
self.theParent.makeAlert(self.tr(
|
||||
"No source document selected. Nothing to do."
|
||||
), nwAlert.ERROR)
|
||||
return
|
||||
return False
|
||||
|
||||
srcItem = self.theProject.projTree[self.sourceItem]
|
||||
if srcItem is None:
|
||||
self.theParent.makeAlert((
|
||||
self.theParent.makeAlert(self.tr(
|
||||
"Could not parse source document."
|
||||
), nwAlert.ERROR)
|
||||
return
|
||||
return False
|
||||
|
||||
theDoc = NWDoc(self.theProject, self.theParent)
|
||||
theText = theDoc.openDocument(self.sourceItem, False)
|
||||
theLines = theText.splitlines()
|
||||
nLines = len(theLines)
|
||||
theLines.insert(0, "%Split Doc")
|
||||
inDoc = NWDoc(self.theProject, self.sourceItem)
|
||||
theText = inDoc.readDocument()
|
||||
|
||||
docErr = inDoc.getError()
|
||||
if theText is None and docErr:
|
||||
self.theParent.makeAlert([
|
||||
self.tr("Failed to open document file."), docErr
|
||||
], nwAlert.ERROR)
|
||||
|
||||
if theText is None:
|
||||
theText = ""
|
||||
|
||||
nLines = len(self.sourceText)
|
||||
logger.debug(
|
||||
"Splitting document %s with %d lines" % (self.sourceItem, nLines)
|
||||
)
|
||||
@@ -147,35 +157,41 @@ class GuiDocSplit(QDialog):
|
||||
|
||||
nFiles = len(finalOrder)
|
||||
if nFiles == 0:
|
||||
self.theParent.makeAlert((
|
||||
self.theParent.makeAlert(self.tr(
|
||||
"No headers found. Nothing to do."
|
||||
), nwAlert.ERROR)
|
||||
return
|
||||
return False
|
||||
|
||||
# Check that another folder can be created
|
||||
parTree = self.theProject.projTree.getItemPath(srcItem.itemParent)
|
||||
if len(parTree) >= nwConst.MAX_DEPTH - 1:
|
||||
self.theParent.makeAlert((
|
||||
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
|
||||
return False
|
||||
|
||||
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
|
||||
return False
|
||||
|
||||
# Create the folder
|
||||
fHandle = self.theProject.newFolder(
|
||||
srcItem.itemName, srcItem.itemClass, srcItem.itemParent
|
||||
)
|
||||
self.theParent.treeView.revealNewTreeItem(fHandle)
|
||||
logger.verbose("Creating folder %s" % fHandle)
|
||||
logger.verbose("Creating folder '%s'", fHandle)
|
||||
|
||||
# Loop through, and create the files
|
||||
for wTitle, iStart, iEnd in finalOrder:
|
||||
@@ -200,20 +216,25 @@ class GuiDocSplit(QDialog):
|
||||
newItem.setStatus(srcItem.itemStatus)
|
||||
logger.verbose(
|
||||
"Creating new document %s with text from line %d to %d" % (
|
||||
nHandle, iStart, iEnd-1
|
||||
nHandle, iStart+1, iEnd
|
||||
)
|
||||
)
|
||||
|
||||
theText = "\n".join(theLines[iStart:iEnd])
|
||||
theText = "\n".join(self.sourceText[iStart:iEnd])
|
||||
theText = theText.rstrip("\n") + "\n\n"
|
||||
theDoc.openDocument(nHandle, False)
|
||||
theDoc.saveDocument(theText)
|
||||
theDoc.clearDocument()
|
||||
|
||||
outDoc = NWDoc(self.theProject, nHandle)
|
||||
if not outDoc.writeDocument(theText):
|
||||
self.theParent.makeAlert([
|
||||
self.tr("Could not save document."), outDoc.getError()
|
||||
], nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
self.theParent.treeView.revealNewTreeItem(nHandle)
|
||||
|
||||
self._doClose()
|
||||
|
||||
return
|
||||
return True
|
||||
|
||||
def _doClose(self):
|
||||
"""Close the dialog window without doing anything.
|
||||
@@ -232,24 +253,28 @@ class GuiDocSplit(QDialog):
|
||||
are then added to the list view in order. The list itself can be
|
||||
reordered by the user.
|
||||
"""
|
||||
self.listBox.clear()
|
||||
if self.sourceItem is None:
|
||||
self.sourceItem = self.theParent.treeView.getSelectedHandle()
|
||||
|
||||
if self.sourceItem is None:
|
||||
return
|
||||
return False
|
||||
|
||||
nwItem = self.theProject.projTree[self.sourceItem]
|
||||
if nwItem is None:
|
||||
return
|
||||
return False
|
||||
|
||||
if nwItem.itemType is not nwItemType.FILE:
|
||||
self.theParent.makeAlert((
|
||||
self.theParent.makeAlert(self.tr(
|
||||
"Element selected in the project tree must be a file."
|
||||
), nwAlert.ERROR)
|
||||
return
|
||||
return False
|
||||
|
||||
self.listBox.clear()
|
||||
theDoc = NWDoc(self.theProject, self.theParent)
|
||||
theText = theDoc.openDocument(self.sourceItem, False)
|
||||
inDoc = NWDoc(self.theProject, self.sourceItem)
|
||||
theText = inDoc.readDocument()
|
||||
if theText is None:
|
||||
theText = ""
|
||||
return False
|
||||
|
||||
spLevel = self.splitLevel.currentData()
|
||||
self.optState.setValue("GuiDocSplit", "spLevel", spLevel)
|
||||
@@ -257,12 +282,10 @@ class GuiDocSplit(QDialog):
|
||||
"Scanning document %s for headings level <= %d" % (self.sourceItem, spLevel)
|
||||
)
|
||||
|
||||
lineNo = 0
|
||||
for aLine in theText.splitlines():
|
||||
|
||||
lineNo += 1
|
||||
onLine = 0
|
||||
self.sourceText = theText.splitlines()
|
||||
for lineNo, aLine in enumerate(self.sourceText):
|
||||
|
||||
onLine = -1
|
||||
if aLine.startswith("# ") and spLevel >= 1:
|
||||
onLine = lineNo
|
||||
elif aLine.startswith("## ") and spLevel >= 2:
|
||||
@@ -272,12 +295,12 @@ class GuiDocSplit(QDialog):
|
||||
elif aLine.startswith("#### ") and spLevel >= 4:
|
||||
onLine = lineNo
|
||||
|
||||
if onLine > 0:
|
||||
if onLine >= 0:
|
||||
newItem = QListWidgetItem()
|
||||
newItem.setText(aLine.strip())
|
||||
newItem.setData(Qt.UserRole, onLine)
|
||||
self.listBox.addItem(newItem)
|
||||
|
||||
return
|
||||
return True
|
||||
|
||||
# END Class GuiDocSplit
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – GUI Item Editor
|
||||
=============================
|
||||
@@ -33,22 +32,24 @@ 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__)
|
||||
|
||||
|
||||
class GuiItemEditor(QDialog):
|
||||
|
||||
def __init__(self, theParent, theProject, tHandle):
|
||||
def __init__(self, theParent, tHandle):
|
||||
QDialog.__init__(self, theParent)
|
||||
|
||||
logger.debug("Initialising GuiItemEditor ...")
|
||||
self.setObjectName("GuiItemEditor")
|
||||
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theProject = theProject
|
||||
self.theParent = theParent
|
||||
self.theProject = theParent.theProject
|
||||
|
||||
##
|
||||
# Build GUI
|
||||
@@ -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)
|
||||
@@ -174,7 +179,7 @@ class GuiItemEditor(QDialog):
|
||||
"""
|
||||
logger.verbose("ItemEditor save button clicked")
|
||||
|
||||
itemName = self.editName.text()
|
||||
itemName = self.editName.text()
|
||||
itemStatus = self.editStatus.currentData()
|
||||
itemLayout = self.editLayout.currentData()
|
||||
isExported = self.editExport.isChecked()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – GUI Open Project
|
||||
==============================
|
||||
@@ -25,13 +24,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
import logging
|
||||
import os
|
||||
import logging
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from PyQt5.QtCore import Qt, QSize
|
||||
from PyQt5.QtGui import QKeySequence
|
||||
from PyQt5.QtCore import Qt, QSize
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QPushButton, QTreeWidget,
|
||||
QAbstractItemView, QTreeWidgetItem, QDialogButtonBox, QLabel, QShortcut,
|
||||
@@ -43,6 +42,7 @@ from nw.constants import nwFiles
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiProjectLoad(QDialog):
|
||||
|
||||
NONE_STATE = 0
|
||||
@@ -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,9 +104,9 @@ 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.selPath = QLineEdit("")
|
||||
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)
|
||||
|
||||
self.browseButton = QPushButton("...")
|
||||
@@ -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)
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – GUI Project Settings
|
||||
==================================
|
||||
@@ -27,22 +26,23 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import nw
|
||||
import logging
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QIcon, QPixmap, QColor, QBrush
|
||||
from PyQt5.QtCore import Qt, QLocale
|
||||
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__)
|
||||
|
||||
|
||||
class GuiProjectSettings(PagedDialog):
|
||||
|
||||
def __init__(self, theParent, theProject):
|
||||
def __init__(self, theParent):
|
||||
PagedDialog.__init__(self, theParent)
|
||||
|
||||
logger.debug("Initialising GuiProjectSettings ...")
|
||||
@@ -50,11 +50,11 @@ class GuiProjectSettings(PagedDialog):
|
||||
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theParent = theParent
|
||||
self.theProject = theProject
|
||||
self.optState = theProject.optState
|
||||
self.theProject = theParent.theProject
|
||||
self.optState = theParent.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,15 +143,20 @@ 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
|
||||
|
||||
# END Class GuiProjectSettings
|
||||
|
||||
|
||||
class GuiProjectEditMain(QWidget):
|
||||
|
||||
def __init__(self, theParent, theProject):
|
||||
@@ -166,88 +171,102 @@ 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)
|
||||
theDict = self.theParent.docEditor.theDict
|
||||
self.spellLang.addItem("Default", "None")
|
||||
self.spellLang.setMaximumWidth(xW)
|
||||
theDict = self.theParent.docEditor.currentDictionary()
|
||||
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
|
||||
|
||||
# END Class GuiProjectEditMain
|
||||
|
||||
|
||||
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 +275,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 +369,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 +382,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,15 +393,15 @@ 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.theParent.makeAlert(self.tr(
|
||||
"Cannot delete a status item that is in use."
|
||||
), nwAlert.ERROR)
|
||||
return
|
||||
|
||||
def _saveItem(self):
|
||||
@@ -364,7 +409,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 +417,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 +430,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 +446,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 +456,7 @@ class GuiProjectEditStatus(QWidget):
|
||||
self.editName.setEnabled(True)
|
||||
self.editName.selectAll()
|
||||
self.editName.setFocus()
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
@@ -423,16 +472,29 @@ 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 +505,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 +595,7 @@ class GuiProjectEditReplace(QWidget):
|
||||
aVal = tItem.text(1)
|
||||
if len(aKey) > 0:
|
||||
newList[aKey] = aVal
|
||||
|
||||
return newList
|
||||
|
||||
##
|
||||
@@ -533,13 +626,13 @@ class GuiProjectEditReplace(QWidget):
|
||||
if selItem is None:
|
||||
return False
|
||||
|
||||
newKey = self.editKey.text()
|
||||
newVal = self.editValue.text()
|
||||
newKey = self.editKey.text()
|
||||
newVal = self.editValue.text()
|
||||
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 @@
|
||||
"""
|
||||
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
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – GUI User Wordlist
|
||||
===============================
|
||||
@@ -25,8 +24,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
import logging
|
||||
import os
|
||||
import logging
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import (
|
||||
@@ -34,13 +33,15 @@ 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__)
|
||||
|
||||
|
||||
class GuiWordList(QDialog):
|
||||
|
||||
def __init__(self, theParent, theProject):
|
||||
def __init__(self, theParent):
|
||||
QDialog.__init__(self, theParent)
|
||||
|
||||
logger.debug("Initialising GuiWordList ...")
|
||||
@@ -49,10 +50,10 @@ class GuiWordList(QDialog):
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theParent = theParent
|
||||
self.theTheme = theParent.theTheme
|
||||
self.theProject = theProject
|
||||
self.optState = theProject.optState
|
||||
self.theProject = theParent.theProject
|
||||
self.optState = theParent.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,13 +122,15 @@ 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.theParent.makeAlert(self.tr(
|
||||
"The word '{0}' is already in the word list."
|
||||
).format(newWord), nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
self.listBox.addItem(newWord)
|
||||
@@ -152,7 +155,7 @@ class GuiWordList(QDialog):
|
||||
tmpFile = dctFile + "~"
|
||||
|
||||
try:
|
||||
with open(tmpFile, mode="w", encoding="utf8") as outFile:
|
||||
with open(tmpFile, mode="w", encoding="utf-8") as outFile:
|
||||
for i in range(self.listBox.count()):
|
||||
outFile.write(self.listBox.item(i).text() + "\n")
|
||||
|
||||
@@ -190,7 +193,7 @@ class GuiWordList(QDialog):
|
||||
logger.debug("No project dictionary file found")
|
||||
return False
|
||||
|
||||
with open(wordList, mode="r", encoding="utf8") as inFile:
|
||||
with open(wordList, mode="r", encoding="utf-8") as inFile:
|
||||
for inLine in inFile:
|
||||
theWord = inLine.strip()
|
||||
if len(theWord) == 0:
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – Enums
|
||||
===================
|
||||
@@ -26,6 +25,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class nwItemType(Enum):
|
||||
|
||||
NO_TYPE = 0
|
||||
@@ -36,6 +36,7 @@ class nwItemType(Enum):
|
||||
|
||||
# END Enum nwItemType
|
||||
|
||||
|
||||
class nwItemClass(Enum):
|
||||
|
||||
NO_CLASS = 0
|
||||
@@ -52,6 +53,7 @@ class nwItemClass(Enum):
|
||||
|
||||
# END Enum nwItemClass
|
||||
|
||||
|
||||
class nwItemLayout(Enum):
|
||||
|
||||
NO_LAYOUT = 0
|
||||
@@ -66,6 +68,7 @@ class nwItemLayout(Enum):
|
||||
|
||||
# END Enum nwItemLayout
|
||||
|
||||
|
||||
class nwDocAction(Enum):
|
||||
|
||||
NO_ACTION = 0
|
||||
@@ -81,33 +84,35 @@ 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
|
||||
RM_BREAKS = 21
|
||||
ALIGN_L = 22
|
||||
ALIGN_C = 23
|
||||
ALIGN_R = 24
|
||||
INDENT_L = 25
|
||||
INDENT_R = 26
|
||||
|
||||
# END Enum nwDocAction
|
||||
|
||||
|
||||
class nwDocInsert(Enum):
|
||||
|
||||
NO_INSERT = 0
|
||||
HARD_BREAK = 1
|
||||
QUOTE_LS = 2
|
||||
QUOTE_RS = 3
|
||||
QUOTE_LD = 4
|
||||
QUOTE_RD = 5
|
||||
NO_INSERT = 0
|
||||
QUOTE_LS = 1
|
||||
QUOTE_RS = 2
|
||||
QUOTE_LD = 3
|
||||
QUOTE_RD = 4
|
||||
|
||||
# END Enum nwDocInsert
|
||||
|
||||
|
||||
class nwAlert(Enum):
|
||||
|
||||
INFO = 0
|
||||
@@ -117,6 +122,26 @@ class nwAlert(Enum):
|
||||
|
||||
# END Enum nwAlert
|
||||
|
||||
|
||||
class nwState(Enum):
|
||||
|
||||
NONE = 0
|
||||
BAD = 1
|
||||
GOOD = 2
|
||||
|
||||
# END Enum nwState
|
||||
|
||||
|
||||
class nwWidget(Enum):
|
||||
|
||||
TREE = 1
|
||||
EDITOR = 2
|
||||
VIEWER = 3
|
||||
OUTLINE = 4
|
||||
|
||||
# END Enum nwWidget
|
||||
|
||||
|
||||
class nwOutline(Enum):
|
||||
|
||||
TITLE = 0
|
||||
30
nw/error.py
30
nw/error.py
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – Exception Handling
|
||||
================================
|
||||
@@ -35,6 +34,7 @@ from PyQt5.QtWidgets import (
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# =============================================================================================== #
|
||||
# Utility Functions
|
||||
# =============================================================================================== #
|
||||
@@ -43,7 +43,8 @@ def logException():
|
||||
"""Log the content of an exception message.
|
||||
"""
|
||||
exType, exValue, _ = sys.exc_info()
|
||||
logger.error("%s: %s" % (exType.__name__, str(exValue).strip("'")))
|
||||
logger.error("%s: %s", exType.__name__, str(exValue).strip("'"))
|
||||
|
||||
|
||||
# =============================================================================================== #
|
||||
# Error Handler
|
||||
@@ -103,7 +104,7 @@ class NWErrorMessage(QDialog):
|
||||
"message and traceback shown below.</p>"
|
||||
"<p>URL: <a href='{issueUrl}'>{issueUrl}</a></p>"
|
||||
).format(
|
||||
issueUrl = __issuesurl__,
|
||||
issueUrl=__issuesurl__,
|
||||
))
|
||||
|
||||
try:
|
||||
@@ -123,16 +124,16 @@ class NWErrorMessage(QDialog):
|
||||
"\n"
|
||||
"Traceback:\n{exTrace}\n"
|
||||
).format(
|
||||
nwVersion = __version__,
|
||||
osType = sys.platform,
|
||||
osKernel = kernelVersion,
|
||||
pyVersion = sys.version.split()[0],
|
||||
pyHexVer = sys.hexversion,
|
||||
qtVers = QT_VERSION_STR,
|
||||
pyqtVers = PYQT_VERSION_STR,
|
||||
exType = exType.__name__,
|
||||
exMessage = str(exValue),
|
||||
exTrace = "\n".join(format_tb(exTrace)),
|
||||
nwVersion=__version__,
|
||||
osType=sys.platform,
|
||||
osKernel=kernelVersion,
|
||||
pyVersion=sys.version.split()[0],
|
||||
pyHexVer=sys.hexversion,
|
||||
qtVers=QT_VERSION_STR,
|
||||
pyqtVers=PYQT_VERSION_STR,
|
||||
exType=exType.__name__,
|
||||
exMessage=str(exValue),
|
||||
exTrace="\n".join(format_tb(exTrace)),
|
||||
))
|
||||
except Exception:
|
||||
self.msgBody.setPlainText("Failed to generate error report ...")
|
||||
@@ -151,13 +152,14 @@ class NWErrorMessage(QDialog):
|
||||
|
||||
# END Class NWErrorMessage
|
||||
|
||||
|
||||
def exceptionHandler(exType, exValue, exTrace):
|
||||
"""Function to catch unhandled global exceptions.
|
||||
"""
|
||||
from traceback import print_tb
|
||||
from PyQt5.QtWidgets import qApp
|
||||
|
||||
logger.critical("%s: %s" % (exType.__name__, str(exValue)))
|
||||
logger.critical("%s: %s", exType.__name__, str(exValue))
|
||||
print_tb(exTrace)
|
||||
|
||||
try:
|
||||
|
||||
@@ -1,50 +1,47 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – GUI Init
|
||||
======================
|
||||
|
||||
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/>.
|
||||
"""
|
||||
|
||||
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",
|
||||
]
|
||||
|
||||
167
nw/gui/custom.py
167
nw/gui/custom.py
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – Custom Widgets and Layouts
|
||||
========================================
|
||||
@@ -29,20 +28,21 @@ 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__)
|
||||
|
||||
|
||||
# =============================================================================================== #
|
||||
# Config Form Layout
|
||||
# =============================================================================================== #
|
||||
@@ -61,6 +61,7 @@ class QConfigLayout(QGridLayout):
|
||||
wSp = nw.CONFIG.pxInt(8)
|
||||
self.setHorizontalSpacing(wSp)
|
||||
self.setVerticalSpacing(wSp)
|
||||
self.setColumnStretch(0, 1)
|
||||
|
||||
return
|
||||
|
||||
@@ -122,9 +123,9 @@ class QConfigLayout(QGridLayout):
|
||||
"""Add a label and a widget as a new row of the grid.
|
||||
"""
|
||||
thisEntry = {
|
||||
"label" : None,
|
||||
"help" : None,
|
||||
"widget" : None,
|
||||
"label": None,
|
||||
"help": None,
|
||||
"widget": None,
|
||||
}
|
||||
|
||||
if isinstance(theLabel, QLabel):
|
||||
@@ -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)
|
||||
|
||||
@@ -189,6 +198,7 @@ class QConfigLayout(QGridLayout):
|
||||
|
||||
# END Class QConfigLayout
|
||||
|
||||
|
||||
class QHelpLabel(QLabel):
|
||||
|
||||
def __init__(self, theText, textCol, fontSize=0.9):
|
||||
@@ -207,10 +217,14 @@ class QHelpLabel(QLabel):
|
||||
lblFont.setPointSizeF(fontSize*lblFont.pointSizeF())
|
||||
self.setFont(lblFont)
|
||||
|
||||
self.setWordWrap(True)
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
|
||||
return
|
||||
|
||||
# END Class QHelpLabel
|
||||
|
||||
|
||||
# =============================================================================================== #
|
||||
# Switch Widget
|
||||
# =============================================================================================== #
|
||||
@@ -297,13 +311,13 @@ class QSwitch(QAbstractButton):
|
||||
if self.isChecked():
|
||||
trackBrush = qPalette.highlight()
|
||||
thumbBrush = qPalette.highlightedText()
|
||||
textColor = qPalette.highlight().color()
|
||||
thumbText = nwUnicode.U_CHECK
|
||||
textColor = qPalette.highlight().color()
|
||||
thumbText = nwUnicode.U_CHECK
|
||||
else:
|
||||
trackBrush = qPalette.dark()
|
||||
thumbBrush = qPalette.light()
|
||||
textColor = qPalette.dark().color()
|
||||
thumbText = nwUnicode.U_CROSS
|
||||
textColor = qPalette.dark().color()
|
||||
thumbText = nwUnicode.U_CROSS
|
||||
|
||||
if self.isEnabled():
|
||||
trackOpacity = 1.0
|
||||
@@ -311,7 +325,7 @@ class QSwitch(QAbstractButton):
|
||||
trackOpacity = 0.6
|
||||
trackBrush = qPalette.shadow()
|
||||
thumbBrush = qPalette.mid()
|
||||
textColor = qPalette.shadow().color()
|
||||
textColor = qPalette.shadow().color()
|
||||
|
||||
qPaint.setBrush(trackBrush)
|
||||
qPaint.setOpacity(trackOpacity)
|
||||
@@ -355,6 +369,7 @@ class QSwitch(QAbstractButton):
|
||||
|
||||
# END Class QSwitch
|
||||
|
||||
|
||||
# =============================================================================================== #
|
||||
# Paged Dialog w/Custom TabWidget
|
||||
# =============================================================================================== #
|
||||
@@ -364,18 +379,18 @@ class PagedDialog(QDialog):
|
||||
def __init__(self, theParent=None):
|
||||
QDialog.__init__(self, parent=theParent)
|
||||
|
||||
self._outerBox = QVBoxLayout()
|
||||
self._buttonBox = QHBoxLayout()
|
||||
self._tabBox = QTabWidget()
|
||||
|
||||
self._tabBar = VerticalTabBar(self)
|
||||
self._tabBox.setTabBar(self._tabBar)
|
||||
self._tabBox.setTabPosition(QTabWidget.West)
|
||||
self._tabBar.setExpanding(False)
|
||||
|
||||
self._tabBox = QTabWidget()
|
||||
self._tabBox.setTabBar(self._tabBar)
|
||||
self._tabBox.setTabPosition(QTabWidget.West)
|
||||
|
||||
self._buttonBox = QHBoxLayout()
|
||||
|
||||
self._outerBox = QVBoxLayout()
|
||||
self._outerBox.addWidget(self._tabBox)
|
||||
self._outerBox.addLayout(self._buttonBox)
|
||||
self.setLayout(self._outerBox)
|
||||
|
||||
# Default Margins
|
||||
qM = self._outerBox.contentsMargins()
|
||||
@@ -384,11 +399,14 @@ class PagedDialog(QDialog):
|
||||
mT = qM.top()
|
||||
mB = qM.bottom()
|
||||
|
||||
# Set Margins
|
||||
self.setContentsMargins(0, 0, 0, 0)
|
||||
self._outerBox.setContentsMargins(0, 0, 0, mB)
|
||||
self._buttonBox.setContentsMargins(mL, 0, mR, 0)
|
||||
self._outerBox.setSpacing(mT)
|
||||
|
||||
self.setLayout(self._outerBox)
|
||||
|
||||
return
|
||||
|
||||
def addTab(self, tabWidget, tabLabel):
|
||||
@@ -405,10 +423,12 @@ class PagedDialog(QDialog):
|
||||
|
||||
# END Class PagedDialog
|
||||
|
||||
|
||||
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 +436,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 +467,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
|
||||
|
||||
1741
nw/gui/doceditor.py
1741
nw/gui/doceditor.py
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – GUI Syntax Highlighter
|
||||
====================================
|
||||
@@ -34,10 +33,11 @@ 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__)
|
||||
|
||||
|
||||
class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
|
||||
BLOCK_NONE = 0
|
||||
@@ -71,7 +71,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
self.colKey = QColor(0, 0, 0)
|
||||
self.colVal = QColor(0, 0, 0)
|
||||
self.colSpell = QColor(0, 0, 0)
|
||||
self.colTagErr = QColor(0, 0, 0)
|
||||
self.colError = QColor(0, 0, 0)
|
||||
self.colRepTag = QColor(0, 0, 0)
|
||||
|
||||
self.initHighlighter()
|
||||
@@ -95,53 +95,54 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
self.colKey = QColor(*self.theTheme.colKey)
|
||||
self.colVal = QColor(*self.theTheme.colVal)
|
||||
self.colSpell = QColor(*self.theTheme.colSpell)
|
||||
self.colTagErr = QColor(*self.theTheme.colTagErr)
|
||||
self.colError = QColor(*self.theTheme.colError)
|
||||
self.colRepTag = QColor(*self.theTheme.colRepTag)
|
||||
self.colMod = QColor(*self.theTheme.colMod)
|
||||
self.colTrail = QColor(*self.theTheme.colEmph)
|
||||
self.colTrail.setAlpha(64)
|
||||
self.colBreak = QColor(*self.theTheme.colEmph)
|
||||
self.colBreak.setAlpha(64)
|
||||
|
||||
self.colEmph = None
|
||||
if self.mainConf.highlightEmph:
|
||||
self.colEmph = QColor(*self.theTheme.colEmph)
|
||||
|
||||
self.hStyles = {
|
||||
"header1" : self._makeFormat(self.colHead, "bold", 1.8),
|
||||
"header2" : self._makeFormat(self.colHead, "bold", 1.6),
|
||||
"header3" : self._makeFormat(self.colHead, "bold", 1.4),
|
||||
"header4" : self._makeFormat(self.colHead, "bold", 1.2),
|
||||
"header1h" : self._makeFormat(self.colHeadH, "bold", 1.8),
|
||||
"header2h" : self._makeFormat(self.colHeadH, "bold", 1.6),
|
||||
"header3h" : self._makeFormat(self.colHeadH, "bold", 1.4),
|
||||
"header4h" : self._makeFormat(self.colHeadH, "bold", 1.2),
|
||||
"bold" : self._makeFormat(self.colEmph, "bold"),
|
||||
"italic" : self._makeFormat(self.colEmph, "italic"),
|
||||
"strike" : self._makeFormat(self.colHidden, "strike"),
|
||||
"trailing" : self._makeFormat(self.colTrail, "background"),
|
||||
"nobreak" : self._makeFormat(self.colTrail, "background"),
|
||||
"dialogue1" : self._makeFormat(self.colDialN),
|
||||
"dialogue2" : self._makeFormat(self.colDialD),
|
||||
"dialogue3" : self._makeFormat(self.colDialS),
|
||||
"replace" : self._makeFormat(self.colRepTag),
|
||||
"hidden" : self._makeFormat(self.colHidden),
|
||||
"keyword" : self._makeFormat(self.colKey),
|
||||
"modifier" : self._makeFormat(self.colMod),
|
||||
"value" : self._makeFormat(self.colVal, "underline"),
|
||||
"header1": self._makeFormat(self.colHead, "bold", 1.8),
|
||||
"header2": self._makeFormat(self.colHead, "bold", 1.6),
|
||||
"header3": self._makeFormat(self.colHead, "bold", 1.4),
|
||||
"header4": self._makeFormat(self.colHead, "bold", 1.2),
|
||||
"header1h": self._makeFormat(self.colHeadH, "bold", 1.8),
|
||||
"header2h": self._makeFormat(self.colHeadH, "bold", 1.6),
|
||||
"header3h": self._makeFormat(self.colHeadH, "bold", 1.4),
|
||||
"header4h": self._makeFormat(self.colHeadH, "bold", 1.2),
|
||||
"bold": self._makeFormat(self.colEmph, "bold"),
|
||||
"italic": self._makeFormat(self.colEmph, "italic"),
|
||||
"strike": self._makeFormat(self.colHidden, "strike"),
|
||||
"mspaces": self._makeFormat(self.colError, "errline"),
|
||||
"nobreak": self._makeFormat(self.colBreak, "background"),
|
||||
"dialogue1": self._makeFormat(self.colDialN),
|
||||
"dialogue2": self._makeFormat(self.colDialD),
|
||||
"dialogue3": self._makeFormat(self.colDialS),
|
||||
"replace": self._makeFormat(self.colRepTag),
|
||||
"hidden": self._makeFormat(self.colHidden),
|
||||
"keyword": self._makeFormat(self.colKey),
|
||||
"modifier": self._makeFormat(self.colMod),
|
||||
"value": self._makeFormat(self.colVal, "underline"),
|
||||
}
|
||||
|
||||
self.hRules = []
|
||||
|
||||
# Trailing Spaces, 2+
|
||||
self.hRules.append((
|
||||
r"[ ]{2,}$", {
|
||||
0 : self.hStyles["trailing"],
|
||||
}
|
||||
))
|
||||
# Multiple or Trailing Spaces
|
||||
if self.mainConf.showMultiSpaces:
|
||||
self.hRules.append((
|
||||
r"[ ]{2,}|[ ]*$", {
|
||||
0: self.hStyles["mspaces"],
|
||||
}
|
||||
))
|
||||
|
||||
# Non-Breaking Spaces
|
||||
self.hRules.append((
|
||||
"[%s%s]+" % (nwUnicode.U_NBSP, nwUnicode.U_THNBSP), {
|
||||
0 : self.hStyles["nobreak"],
|
||||
0: self.hStyles["nobreak"],
|
||||
}
|
||||
))
|
||||
|
||||
@@ -154,7 +155,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
if fmtDbl != ["\"", "\""]:
|
||||
self.hRules.append((
|
||||
"(\\B\")(.*?)(\"\\B)", {
|
||||
0 : self.hStyles["dialogue1"],
|
||||
0: self.hStyles["dialogue1"],
|
||||
}
|
||||
))
|
||||
|
||||
@@ -162,7 +163,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
dblEnd = "|$" if self.mainConf.allowOpenDQuote else ""
|
||||
self.hRules.append((
|
||||
f"(\\B{fmtDbl[0]})(.*?)({fmtDbl[1]}\\B{dblEnd})", {
|
||||
0 : self.hStyles["dialogue2"],
|
||||
0: self.hStyles["dialogue2"],
|
||||
}
|
||||
))
|
||||
|
||||
@@ -170,37 +171,44 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
sngEnd = "|$" if self.mainConf.allowOpenSQuote else ""
|
||||
self.hRules.append((
|
||||
f"(\\B{fmtSng[0]})(.*?)({fmtSng[1]}\\B{sngEnd})", {
|
||||
0 : self.hStyles["dialogue3"],
|
||||
0: self.hStyles["dialogue3"],
|
||||
}
|
||||
))
|
||||
|
||||
# Markdown
|
||||
# Markdown Syntax
|
||||
self.hRules.append((
|
||||
nwRegEx.FMT_EI, {
|
||||
1 : self.hStyles["hidden"],
|
||||
2 : self.hStyles["italic"],
|
||||
3 : self.hStyles["hidden"],
|
||||
1: self.hStyles["hidden"],
|
||||
2: self.hStyles["italic"],
|
||||
3: self.hStyles["hidden"],
|
||||
}
|
||||
))
|
||||
self.hRules.append((
|
||||
nwRegEx.FMT_EB, {
|
||||
1 : self.hStyles["hidden"],
|
||||
2 : self.hStyles["bold"],
|
||||
3 : self.hStyles["hidden"],
|
||||
1: self.hStyles["hidden"],
|
||||
2: self.hStyles["bold"],
|
||||
3: self.hStyles["hidden"],
|
||||
}
|
||||
))
|
||||
self.hRules.append((
|
||||
nwRegEx.FMT_ST, {
|
||||
1 : self.hStyles["hidden"],
|
||||
2 : self.hStyles["strike"],
|
||||
3 : self.hStyles["hidden"],
|
||||
1: self.hStyles["hidden"],
|
||||
2: self.hStyles["strike"],
|
||||
3: self.hStyles["hidden"],
|
||||
}
|
||||
))
|
||||
|
||||
# Alignment Tags
|
||||
self.hRules.append((
|
||||
r"(^>{1,2}|<{1,2}$)", {
|
||||
1: self.hStyles["hidden"],
|
||||
}
|
||||
))
|
||||
|
||||
# Auto-Replace Tags
|
||||
self.hRules.append((
|
||||
r"<(\S+?)>", {
|
||||
0 : self.hStyles["replace"],
|
||||
0: self.hStyles["replace"],
|
||||
}
|
||||
))
|
||||
|
||||
@@ -253,11 +261,11 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
"""Loop through all blocks and rehighlight those of a given
|
||||
content type.
|
||||
"""
|
||||
qDocument = self.document()
|
||||
nBlocks = qDocument.blockCount()
|
||||
qDoc = self.document()
|
||||
nBlocks = qDoc.blockCount()
|
||||
bfTime = time()
|
||||
for i in range(nBlocks):
|
||||
theBlock = qDocument.findBlockByNumber(i)
|
||||
theBlock = qDoc.findBlockByNumber(i)
|
||||
if theBlock.userState() & theType > 0:
|
||||
self.rehighlightBlock(theBlock)
|
||||
afTime = time()
|
||||
@@ -280,7 +288,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
if self.theHandle is None or not theText:
|
||||
return
|
||||
|
||||
if theText.startswith("@"): # Keywords and commands
|
||||
if theText.startswith("@"): # Keywords and commands
|
||||
self.setCurrentBlockState(self.BLOCK_META)
|
||||
tItem = self.theParent.theProject.projTree[self.theHandle]
|
||||
isValid, theBits, thePos = self.theIndex.scanThis(theText)
|
||||
@@ -296,7 +304,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
self.setFormat(xPos, xLen, self.hStyles["value"])
|
||||
else:
|
||||
kwFmt = self.format(xPos)
|
||||
kwFmt.setUnderlineColor(self.colTagErr)
|
||||
kwFmt.setUnderlineColor(self.colError)
|
||||
kwFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
|
||||
self.setFormat(xPos, xLen, kwFmt)
|
||||
|
||||
@@ -304,27 +312,27 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
# so we force a return here
|
||||
return
|
||||
|
||||
elif theText.startswith("# "): # Header 1
|
||||
elif theText.startswith("# "): # Header 1
|
||||
self.setCurrentBlockState(self.BLOCK_TITLE)
|
||||
self.setFormat(0, 1, self.hStyles["header1h"])
|
||||
self.setFormat(1, len(theText), self.hStyles["header1"])
|
||||
|
||||
elif theText.startswith("## "): # Header 2
|
||||
elif theText.startswith("## "): # Header 2
|
||||
self.setCurrentBlockState(self.BLOCK_TITLE)
|
||||
self.setFormat(0, 2, self.hStyles["header2h"])
|
||||
self.setFormat(2, len(theText), self.hStyles["header2"])
|
||||
|
||||
elif theText.startswith("### "): # Header 3
|
||||
elif theText.startswith("### "): # Header 3
|
||||
self.setCurrentBlockState(self.BLOCK_TITLE)
|
||||
self.setFormat(0, 3, self.hStyles["header3h"])
|
||||
self.setFormat(3, len(theText), self.hStyles["header3"])
|
||||
|
||||
elif theText.startswith("#### "): # Header 4
|
||||
elif theText.startswith("#### "): # Header 4
|
||||
self.setCurrentBlockState(self.BLOCK_TITLE)
|
||||
self.setFormat(0, 4, self.hStyles["header4h"])
|
||||
self.setFormat(4, len(theText), self.hStyles["header4"])
|
||||
|
||||
elif theText.startswith("%"): # Comments
|
||||
elif theText.startswith("%"): # Comments
|
||||
self.setCurrentBlockState(self.BLOCK_TEXT)
|
||||
toCheck = theText[1:].lstrip()
|
||||
synTag = toCheck[:9].lower()
|
||||
@@ -337,7 +345,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
else:
|
||||
self.setFormat(0, tLen, self.hStyles["hidden"])
|
||||
|
||||
else: # Text Paragraph
|
||||
else: # Text Paragraph
|
||||
self.setCurrentBlockState(self.BLOCK_TEXT)
|
||||
for rX, xFmt in self.rxRules:
|
||||
rxItt = rX.globalMatch(theText, 0)
|
||||
@@ -391,6 +399,9 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
theFormat.setFontItalic(True)
|
||||
if "strike" in fmtStyle:
|
||||
theFormat.setFontStrikeOut(True)
|
||||
if "errline" in fmtStyle:
|
||||
theFormat.setUnderlineColor(self.colError)
|
||||
theFormat.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
|
||||
if "underline" in fmtStyle:
|
||||
theFormat.setFontUnderline(True)
|
||||
if "background" in fmtStyle:
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – GUI Document Viewer
|
||||
=================================
|
||||
@@ -41,10 +40,12 @@ 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__)
|
||||
|
||||
|
||||
class GuiDocViewer(QTextBrowser):
|
||||
|
||||
def __init__(self, theParent):
|
||||
@@ -57,9 +58,11 @@ class GuiDocViewer(QTextBrowser):
|
||||
self.theParent = theParent
|
||||
self.theTheme = theParent.theTheme
|
||||
self.theProject = theParent.theProject
|
||||
self.theHandle = None
|
||||
|
||||
self.qDocument = self.document()
|
||||
# Internal Variables
|
||||
self._docHandle = None
|
||||
|
||||
# Settings
|
||||
self.setMinimumWidth(self.mainConf.pxInt(300))
|
||||
self.setAutoFillBackground(True)
|
||||
self.setOpenExternalLinks(False)
|
||||
@@ -89,8 +92,8 @@ class GuiDocViewer(QTextBrowser):
|
||||
"""
|
||||
self.clear()
|
||||
self.setSearchPaths([""])
|
||||
self.theHandle = None
|
||||
self.docHeader.setTitleFromHandle(self.theHandle)
|
||||
self._docHandle = None
|
||||
self.docHeader.setTitleFromHandle(self._docHandle)
|
||||
return True
|
||||
|
||||
def initViewer(self):
|
||||
@@ -102,7 +105,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
theFont = QFont()
|
||||
if self.mainConf.textFont is None:
|
||||
# If none is defined, set the default back to config
|
||||
self.mainConf.textFont = self.qDocument.defaultFont().family()
|
||||
self.mainConf.textFont = self.document().defaultFont().family()
|
||||
theFont.setFamily(self.mainConf.textFont)
|
||||
theFont.setPointSize(self.mainConf.textSize)
|
||||
self.setFont(theFont)
|
||||
@@ -123,11 +126,11 @@ class GuiDocViewer(QTextBrowser):
|
||||
self.docFooter.matchColours()
|
||||
|
||||
# Set default text margins
|
||||
self.qDocument.setDocumentMargin(0)
|
||||
self.document().setDocumentMargin(0)
|
||||
theOpt = QTextOption()
|
||||
if self.mainConf.doJustify:
|
||||
theOpt.setAlignment(Qt.AlignJustify)
|
||||
self.qDocument.setDefaultTextOption(theOpt)
|
||||
self.document().setDefaultTextOption(theOpt)
|
||||
|
||||
# Scroll bars
|
||||
if self.mainConf.hideVScroll:
|
||||
@@ -147,7 +150,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
self.setTabStopWidth(self.mainConf.getTabWidth())
|
||||
|
||||
# If we have a document open, we should reload it in case the font changed
|
||||
if self.theHandle is not None:
|
||||
if self._docHandle is not None:
|
||||
self.reloadText()
|
||||
|
||||
return True
|
||||
@@ -163,11 +166,11 @@ class GuiDocViewer(QTextBrowser):
|
||||
if tItem.itemType != nwItemType.FILE:
|
||||
return False
|
||||
|
||||
logger.debug("Generating preview for item %s" % tHandle)
|
||||
logger.debug("Generating preview for item '%s'", tHandle)
|
||||
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
|
||||
|
||||
sPos = self.verticalScrollBar().value()
|
||||
aDoc = ToHtml(self.theProject, self.theParent)
|
||||
aDoc = ToHtml(self.theProject)
|
||||
aDoc.setPreview(self.mainConf.viewComments, self.mainConf.viewSynopsis)
|
||||
aDoc.setLinkHeaders(True)
|
||||
|
||||
@@ -181,10 +184,9 @@ class GuiDocViewer(QTextBrowser):
|
||||
aDoc.doConvert()
|
||||
aDoc.doPostProcessing()
|
||||
except Exception:
|
||||
logger.error("Failed to generate preview for document with handle '%s'" % tHandle)
|
||||
logger.error("Failed to generate preview for document with handle '%s'", tHandle)
|
||||
nw.logException()
|
||||
self.setText("An error occurred while generating the preview.")
|
||||
return False
|
||||
self.setText(self.tr("An error occurred while generating the preview."))
|
||||
|
||||
# Refresh the tab stops
|
||||
if self.mainConf.verQtValue >= 51000:
|
||||
@@ -205,11 +207,11 @@ class GuiDocViewer(QTextBrowser):
|
||||
theCursor = self.textCursor()
|
||||
theCursor.insertText("\t")
|
||||
|
||||
if self.theHandle == tHandle:
|
||||
if self._docHandle == tHandle:
|
||||
self.verticalScrollBar().setValue(sPos)
|
||||
self.theHandle = tHandle
|
||||
self._docHandle = tHandle
|
||||
self.theProject.setLastViewed(tHandle)
|
||||
self.docHeader.setTitleFromHandle(self.theHandle)
|
||||
self.docHeader.setTitleFromHandle(self._docHandle)
|
||||
self.updateDocMargins()
|
||||
|
||||
# Make sure the main GUI knows we changed the content
|
||||
@@ -225,13 +227,13 @@ class GuiDocViewer(QTextBrowser):
|
||||
def reloadText(self):
|
||||
"""Reload the text in the current document.
|
||||
"""
|
||||
self.loadText(self.theHandle, updateHistory=False)
|
||||
self.loadText(self._docHandle, updateHistory=False)
|
||||
return
|
||||
|
||||
def redrawText(self):
|
||||
"""Redraw the text by marking the document content as "dirty".
|
||||
"""
|
||||
self.qDocument.markContentsDirty(0, self.qDocument.characterCount())
|
||||
self.document().markContentsDirty(0, self.document().characterCount())
|
||||
self.updateDocMargins()
|
||||
return
|
||||
|
||||
@@ -240,20 +242,22 @@ class GuiDocViewer(QTextBrowser):
|
||||
tag rather than a known handle. This function depends on the
|
||||
index being up to date.
|
||||
"""
|
||||
logger.debug("Loading document from tag '%s'" % theTag)
|
||||
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 "
|
||||
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 F9."
|
||||
) % theTag, nwAlert.ERROR)
|
||||
"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
|
||||
# the doc view panel is visible in case this request comes
|
||||
# from outside this class.
|
||||
logger.verbose("Tag points to %s#%s" % (tHandle, sTitle))
|
||||
logger.verbose("Tag points to '%s#%s'", tHandle, sTitle)
|
||||
self.theParent.viewDocument(tHandle, "#%s" % sTitle)
|
||||
return True
|
||||
|
||||
@@ -261,8 +265,8 @@ class GuiDocViewer(QTextBrowser):
|
||||
"""Wrapper function for various document actions on the current
|
||||
document.
|
||||
"""
|
||||
logger.verbose("Requesting action: %s" % theAction.name)
|
||||
if self.theHandle is None:
|
||||
logger.verbose("Requesting action: '%s'", theAction.name)
|
||||
if self._docHandle is None:
|
||||
logger.error("No document open")
|
||||
return False
|
||||
if theAction == nwDocAction.CUT:
|
||||
@@ -274,7 +278,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
elif theAction == nwDocAction.SEL_PARA:
|
||||
self._makeSelection(QTextCursor.BlockUnderCursor)
|
||||
else:
|
||||
logger.debug("Unknown or unsupported document action %s" % str(theAction))
|
||||
logger.debug("Unknown or unsupported document action '%s'", str(theAction))
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -284,7 +288,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
if not isinstance(tAnchor, str):
|
||||
return False
|
||||
if tAnchor.startswith("#"):
|
||||
logger.verbose("Moving to anchor %s" % tAnchor)
|
||||
logger.verbose("Moving to anchor '%s'", tAnchor)
|
||||
self.setSource(QUrl(tAnchor))
|
||||
return True
|
||||
|
||||
@@ -307,9 +311,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
return
|
||||
|
||||
def updateDocMargins(self):
|
||||
"""Automatically adjust the margins so the text is centred if
|
||||
Config.textFixedW is enabled or we're in Focus Mode. Otherwise,
|
||||
just ensure the margins are set correctly.
|
||||
"""Automatically adjust the margins so the text is centred.
|
||||
"""
|
||||
vBar = self.verticalScrollBar()
|
||||
sW = vBar.width() if vBar.isVisible() else 0
|
||||
@@ -334,11 +336,21 @@ class GuiDocViewer(QTextBrowser):
|
||||
"""Called when an item label is changed to check if the document
|
||||
title bar needs updating,
|
||||
"""
|
||||
if tHandle == self.theHandle:
|
||||
self.docHeader.setTitleFromHandle(self.theHandle)
|
||||
if tHandle == self._docHandle:
|
||||
self.docHeader.setTitleFromHandle(self._docHandle)
|
||||
self.updateDocMargins()
|
||||
return
|
||||
|
||||
##
|
||||
# Properties
|
||||
##
|
||||
|
||||
def docHandle(self):
|
||||
"""Return the handle of the currently open document. Returns
|
||||
None if no document is open.
|
||||
"""
|
||||
return self._docHandle
|
||||
|
||||
##
|
||||
# Setters
|
||||
##
|
||||
@@ -360,10 +372,10 @@ class GuiDocViewer(QTextBrowser):
|
||||
if not isinstance(theLine, int):
|
||||
return False
|
||||
if theLine >= 0:
|
||||
theBlock = self.qDocument.findBlockByLineNumber(theLine)
|
||||
theBlock = self.document().findBlockByLineNumber(theLine)
|
||||
if theBlock:
|
||||
self.setCursorPosition(theBlock.position())
|
||||
logger.verbose("Cursor moved to line %d" % theLine)
|
||||
logger.verbose("Cursor moved to line %d", theLine)
|
||||
return True
|
||||
|
||||
def setScrollPosition(self, thePos):
|
||||
@@ -395,7 +407,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
"""Slot for a link in the document being clicked.
|
||||
"""
|
||||
theLink = theURL.url()
|
||||
logger.verbose("Clicked link: '%s'" % theLink)
|
||||
logger.verbose("Clicked link: '%s'", theLink)
|
||||
if len(theLink) > 0:
|
||||
theBits = theLink.split("=")
|
||||
if len(theBits) == 2:
|
||||
@@ -415,7 +427,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 +436,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)
|
||||
)
|
||||
@@ -528,34 +540,35 @@ class GuiDocViewer(QTextBrowser):
|
||||
" color: rgb({mColR}, {mColG}, {mColB});"
|
||||
"}}\n"
|
||||
).format(
|
||||
tColR = self.theTheme.colText[0],
|
||||
tColG = self.theTheme.colText[1],
|
||||
tColB = self.theTheme.colText[2],
|
||||
hColR = self.theTheme.colHead[0],
|
||||
hColG = self.theTheme.colHead[1],
|
||||
hColB = self.theTheme.colHead[2],
|
||||
aColR = self.theTheme.colVal[0],
|
||||
aColG = self.theTheme.colVal[1],
|
||||
aColB = self.theTheme.colVal[2],
|
||||
eColR = self.theTheme.colEmph[0],
|
||||
eColG = self.theTheme.colEmph[1],
|
||||
eColB = self.theTheme.colEmph[2],
|
||||
kColR = self.theTheme.colKey[0],
|
||||
kColG = self.theTheme.colKey[1],
|
||||
kColB = self.theTheme.colKey[2],
|
||||
cColR = self.theTheme.colHidden[0],
|
||||
cColG = self.theTheme.colHidden[1],
|
||||
cColB = self.theTheme.colHidden[2],
|
||||
mColR = self.theTheme.colMod[0],
|
||||
mColG = self.theTheme.colMod[1],
|
||||
mColB = self.theTheme.colMod[2],
|
||||
tColR=self.theTheme.colText[0],
|
||||
tColG=self.theTheme.colText[1],
|
||||
tColB=self.theTheme.colText[2],
|
||||
hColR=self.theTheme.colHead[0],
|
||||
hColG=self.theTheme.colHead[1],
|
||||
hColB=self.theTheme.colHead[2],
|
||||
aColR=self.theTheme.colVal[0],
|
||||
aColG=self.theTheme.colVal[1],
|
||||
aColB=self.theTheme.colVal[2],
|
||||
eColR=self.theTheme.colEmph[0],
|
||||
eColG=self.theTheme.colEmph[1],
|
||||
eColB=self.theTheme.colEmph[2],
|
||||
kColR=self.theTheme.colKey[0],
|
||||
kColG=self.theTheme.colKey[1],
|
||||
kColB=self.theTheme.colKey[2],
|
||||
cColR=self.theTheme.colHidden[0],
|
||||
cColG=self.theTheme.colHidden[1],
|
||||
cColB=self.theTheme.colHidden[2],
|
||||
mColR=self.theTheme.colMod[0],
|
||||
mColG=self.theTheme.colMod[1],
|
||||
mColB=self.theTheme.colMod[2],
|
||||
)
|
||||
self.qDocument.setDefaultStyleSheet(styleSheet)
|
||||
self.document().setDefaultStyleSheet(styleSheet)
|
||||
|
||||
return True
|
||||
|
||||
# END Class GuiDocViewer
|
||||
|
||||
|
||||
class GuiDocViewHistory():
|
||||
|
||||
def __init__(self, docViewer):
|
||||
@@ -601,7 +614,7 @@ class GuiDocViewHistory():
|
||||
|
||||
self._dumpHistory()
|
||||
|
||||
logger.verbose("Added %s to view history" % tHandle)
|
||||
logger.verbose("Added '%s' to view history", tHandle)
|
||||
|
||||
return True
|
||||
|
||||
@@ -687,6 +700,7 @@ class GuiDocViewHistory():
|
||||
|
||||
# END Class GuiDocViewHistory
|
||||
|
||||
|
||||
# =============================================================================================== #
|
||||
# The Embedded Document Header
|
||||
# Only used by DocViewer, and is at a fixed position in the QTextBrowser's viewport
|
||||
@@ -704,7 +718,9 @@ class GuiDocViewHeader(QWidget):
|
||||
self.theParent = docViewer.theParent
|
||||
self.theProject = docViewer.theProject
|
||||
self.theTheme = docViewer.theTheme
|
||||
self.theHandle = None
|
||||
|
||||
# Internal Variables
|
||||
self._docHandle = None
|
||||
|
||||
fPx = int(0.9*self.theTheme.fontPixelSize)
|
||||
hSp = self.mainConf.pxInt(6)
|
||||
@@ -740,7 +756,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 +767,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 +778,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 +789,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
|
||||
@@ -822,7 +838,7 @@ class GuiDocViewHeader(QWidget):
|
||||
"""Sets the document title from the handle, or alternatively,
|
||||
set the whole document path.
|
||||
"""
|
||||
self.theHandle = tHandle
|
||||
self._docHandle = tHandle
|
||||
if tHandle is None:
|
||||
self.theTitle.setText("")
|
||||
self.backButton.setVisible(False)
|
||||
@@ -873,7 +889,7 @@ class GuiDocViewHeader(QWidget):
|
||||
def _refreshDocument(self):
|
||||
"""Reload the content of the document.
|
||||
"""
|
||||
if self.docViewer.theHandle == self.theParent.docEditor.theHandle:
|
||||
if self.docViewer.docHandle() == self.theParent.docEditor.docHandle():
|
||||
self.theParent.saveDocument()
|
||||
self.docViewer.reloadText()
|
||||
return
|
||||
@@ -886,11 +902,12 @@ class GuiDocViewHeader(QWidget):
|
||||
"""Capture a click on the title and ensure that the item is
|
||||
selected in the project tree.
|
||||
"""
|
||||
self.theParent.treeView.setSelectedHandle(self.theHandle, doScroll=True)
|
||||
self.theParent.treeView.setSelectedHandle(self._docHandle, doScroll=True)
|
||||
return
|
||||
|
||||
# END Class GuiDocViewHeader
|
||||
|
||||
|
||||
# =============================================================================================== #
|
||||
# The Embedded Document Footer
|
||||
# Only used by DocViewer, and is at a fixed position in the QTextBrowser's viewport
|
||||
@@ -908,7 +925,9 @@ class GuiDocViewFooter(QWidget):
|
||||
self.theParent = docViewer.theParent
|
||||
self.theTheme = docViewer.theTheme
|
||||
self.viewMeta = docViewer.theParent.viewMeta
|
||||
self.theHandle = None
|
||||
|
||||
# Internal Variables
|
||||
self._docHandle = None
|
||||
|
||||
fPx = int(0.9*self.theTheme.fontPixelSize)
|
||||
bSp = self.mainConf.pxInt(2)
|
||||
@@ -944,7 +963,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 +974,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 +988,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 +1000,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 +1012,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 +1021,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 +1030,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)
|
||||
@@ -1092,10 +1111,10 @@ class GuiDocViewFooter(QWidget):
|
||||
def _doToggleSticky(self, theState):
|
||||
"""Toggle the sticky flag for the reference panel.
|
||||
"""
|
||||
logger.verbose("Reference sticky is %s" % str(theState))
|
||||
logger.verbose("Reference sticky is %s", str(theState))
|
||||
self.docViewer.stickyRef = theState
|
||||
if not theState and self.docViewer.theHandle is not None:
|
||||
self.viewMeta.refreshReferences(self.docViewer.theHandle)
|
||||
if not theState and self.docViewer.docHandle() is not None:
|
||||
self.viewMeta.refreshReferences(self.docViewer.docHandle())
|
||||
return
|
||||
|
||||
def _doToggleComments(self, theState):
|
||||
@@ -1114,6 +1133,7 @@ class GuiDocViewFooter(QWidget):
|
||||
|
||||
# END Class GuiDocViewFooter
|
||||
|
||||
|
||||
# =============================================================================================== #
|
||||
# The Document Back-Reference Panel
|
||||
# Placed in a separate QSplitter position in the main GUI window
|
||||
@@ -1129,7 +1149,6 @@ class GuiDocViewDetails(QScrollArea):
|
||||
self.theParent = theParent
|
||||
self.theProject = theParent.theProject
|
||||
self.theTheme = theParent.theTheme
|
||||
self.currHandle = None
|
||||
|
||||
self.refList = QLabel("")
|
||||
self.refList.setWordWrap(True)
|
||||
@@ -1162,7 +1181,6 @@ class GuiDocViewDetails(QScrollArea):
|
||||
"""Update the current list of document references from the
|
||||
project index.
|
||||
"""
|
||||
self.currHandle = tHandle
|
||||
if self.theParent.docViewer.stickyRef:
|
||||
return
|
||||
|
||||
@@ -1187,7 +1205,7 @@ class GuiDocViewDetails(QScrollArea):
|
||||
"""Capture the link-click and forward it to the document viewer
|
||||
class for handling.
|
||||
"""
|
||||
logger.verbose("Clicked link: '%s'" % theLink)
|
||||
logger.verbose("Clicked link: '%s'", theLink)
|
||||
if len(theLink) == 21:
|
||||
tHandle = theLink[:13]
|
||||
tAnchor = theLink[13:]
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – GUI Item Details Panel
|
||||
====================================
|
||||
@@ -27,16 +26,16 @@ 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, pyqtSlot
|
||||
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__)
|
||||
|
||||
|
||||
class GuiItemDetails(QWidget):
|
||||
|
||||
def __init__(self, theParent):
|
||||
@@ -47,7 +46,9 @@ class GuiItemDetails(QWidget):
|
||||
self.theParent = theParent
|
||||
self.theProject = theParent.theProject
|
||||
self.theTheme = theParent.theTheme
|
||||
self.theHandle = None
|
||||
|
||||
# Internal Variables
|
||||
self._itemHandle = None
|
||||
|
||||
# Sizes
|
||||
hSp = self.mainConf.pxInt(6)
|
||||
@@ -56,92 +57,92 @@ class GuiItemDetails(QWidget):
|
||||
iPx = self.theTheme.baseIconSize
|
||||
fPt = self.theTheme.fontPointSize
|
||||
|
||||
self.expCheck = self.theTheme.getPixmap("check", (iPx, iPx))
|
||||
self.expCross = self.theTheme.getPixmap("cross", (iPx, iPx))
|
||||
self._expCheck = self.theTheme.getPixmap("check", (iPx, iPx))
|
||||
self._expCross = self.theTheme.getPixmap("cross", (iPx, iPx))
|
||||
|
||||
self.fntLabel = QFont()
|
||||
self.fntLabel.setBold(True)
|
||||
self.fntLabel.setPointSizeF(0.9*fPt)
|
||||
fntLabel = QFont()
|
||||
fntLabel.setBold(True)
|
||||
fntLabel.setPointSizeF(0.9*fPt)
|
||||
|
||||
self.fntValue = QFont()
|
||||
self.fntValue.setPointSizeF(0.9*fPt)
|
||||
fntValue = QFont()
|
||||
fntValue.setPointSizeF(0.9*fPt)
|
||||
|
||||
# Label
|
||||
self.labelName = QLabel("Label")
|
||||
self.labelName.setFont(self.fntLabel)
|
||||
self.labelName = QLabel(self.tr("Label"))
|
||||
self.labelName.setFont(fntLabel)
|
||||
self.labelName.setAlignment(Qt.AlignLeft | Qt.AlignBaseline)
|
||||
|
||||
self.labelFlag = QLabel("")
|
||||
self.labelFlag.setAlignment(Qt.AlignRight | Qt.AlignBaseline)
|
||||
|
||||
self.labelData = QLabel("")
|
||||
self.labelData.setFont(self.fntValue)
|
||||
self.labelData.setFont(fntValue)
|
||||
self.labelData.setAlignment(Qt.AlignLeft | Qt.AlignBaseline)
|
||||
self.labelData.setWordWrap(True)
|
||||
|
||||
# Status
|
||||
self.statusName = QLabel("Status")
|
||||
self.statusName.setFont(self.fntLabel)
|
||||
self.statusName = QLabel(self.tr("Status"))
|
||||
self.statusName.setFont(fntLabel)
|
||||
self.statusName.setAlignment(Qt.AlignLeft)
|
||||
|
||||
self.statusFlag = QLabel("")
|
||||
self.statusFlag.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
|
||||
self.statusData = QLabel("")
|
||||
self.statusData.setFont(self.fntValue)
|
||||
self.statusData.setFont(fntValue)
|
||||
self.statusData.setAlignment(Qt.AlignLeft)
|
||||
|
||||
# Class
|
||||
self.className = QLabel("Class")
|
||||
self.className.setFont(self.fntLabel)
|
||||
self.className = QLabel(self.tr("Class"))
|
||||
self.className.setFont(fntLabel)
|
||||
self.className.setAlignment(Qt.AlignLeft)
|
||||
|
||||
self.classFlag = QLabel("")
|
||||
self.classFlag.setFont(self.fntValue)
|
||||
self.classFlag.setFont(fntValue)
|
||||
self.classFlag.setAlignment(Qt.AlignRight)
|
||||
|
||||
self.classData = QLabel("")
|
||||
self.classData.setFont(self.fntValue)
|
||||
self.classData.setFont(fntValue)
|
||||
self.classData.setAlignment(Qt.AlignLeft)
|
||||
|
||||
# Layout
|
||||
self.layoutName = QLabel("Layout")
|
||||
self.layoutName.setFont(self.fntLabel)
|
||||
self.layoutName = QLabel(self.tr("Layout"))
|
||||
self.layoutName.setFont(fntLabel)
|
||||
self.layoutName.setAlignment(Qt.AlignLeft)
|
||||
|
||||
self.layoutFlag = QLabel("")
|
||||
self.layoutFlag.setFont(self.fntValue)
|
||||
self.layoutFlag.setFont(fntValue)
|
||||
self.layoutFlag.setAlignment(Qt.AlignRight)
|
||||
|
||||
self.layoutData = QLabel("")
|
||||
self.layoutData.setFont(self.fntValue)
|
||||
self.layoutData.setFont(fntValue)
|
||||
self.layoutData.setAlignment(Qt.AlignLeft)
|
||||
|
||||
# Character Count
|
||||
self.cCountName = QLabel(" Characters")
|
||||
self.cCountName.setFont(self.fntLabel)
|
||||
self.cCountName = QLabel(" "+self.tr("Characters"))
|
||||
self.cCountName.setFont(fntLabel)
|
||||
self.cCountName.setAlignment(Qt.AlignRight)
|
||||
|
||||
self.cCountData = QLabel("")
|
||||
self.cCountData.setFont(self.fntValue)
|
||||
self.cCountData.setFont(fntValue)
|
||||
self.cCountData.setAlignment(Qt.AlignRight)
|
||||
|
||||
# Word Count
|
||||
self.wCountName = QLabel(" Words")
|
||||
self.wCountName.setFont(self.fntLabel)
|
||||
self.wCountName = QLabel(" "+self.tr("Words"))
|
||||
self.wCountName.setFont(fntLabel)
|
||||
self.wCountName.setAlignment(Qt.AlignRight)
|
||||
|
||||
self.wCountData = QLabel("")
|
||||
self.wCountData.setFont(self.fntValue)
|
||||
self.wCountData.setFont(fntValue)
|
||||
self.wCountData.setAlignment(Qt.AlignRight)
|
||||
|
||||
# Paragraph Count
|
||||
self.pCountName = QLabel(" Paragraphs")
|
||||
self.pCountName.setFont(self.fntLabel)
|
||||
self.pCountName = QLabel(" "+self.tr("Paragraphs"))
|
||||
self.pCountName.setFont(fntLabel)
|
||||
self.pCountName.setAlignment(Qt.AlignRight)
|
||||
|
||||
self.pCountData = QLabel("")
|
||||
self.pCountData.setFont(self.fntValue)
|
||||
self.pCountData.setFont(fntValue)
|
||||
self.pCountData.setAlignment(Qt.AlignRight)
|
||||
|
||||
# Assemble
|
||||
@@ -181,8 +182,8 @@ class GuiItemDetails(QWidget):
|
||||
self.setLayout(self.mainBox)
|
||||
|
||||
# Make sure the columns for flags and counts don't resize too often
|
||||
flagWidth = self.theTheme.getTextWidth("Mm", self.fntValue)
|
||||
countWidth = self.theTheme.getTextWidth("99,999", self.fntValue)
|
||||
flagWidth = self.theTheme.getTextWidth("Mm", fntValue)
|
||||
countWidth = self.theTheme.getTextWidth("99,999", fntValue)
|
||||
self.mainBox.setColumnMinimumWidth(1, flagWidth)
|
||||
self.mainBox.setColumnMinimumWidth(4, countWidth)
|
||||
|
||||
@@ -197,6 +198,8 @@ class GuiItemDetails(QWidget):
|
||||
def clearDetails(self):
|
||||
"""Clear all the data values.
|
||||
"""
|
||||
self._itemHandle = None
|
||||
|
||||
self.labelFlag.setPixmap(QPixmap(1, 1))
|
||||
self.statusFlag.setPixmap(QPixmap(1, 1))
|
||||
self.classFlag.setText("")
|
||||
@@ -213,17 +216,6 @@ class GuiItemDetails(QWidget):
|
||||
|
||||
return
|
||||
|
||||
def updateCounts(self, tHandle, cC, wC, pC):
|
||||
"""Update the counts if the handle is the same as the one we're
|
||||
already showing. Otherwise, do nothing.
|
||||
"""
|
||||
if tHandle == self.theHandle:
|
||||
self.cCountData.setText(f"{cC:n}")
|
||||
self.wCountData.setText(f"{wC:n}")
|
||||
self.pCountData.setText(f"{pC:n}")
|
||||
|
||||
return
|
||||
|
||||
def updateViewBox(self, tHandle):
|
||||
"""Populate the details box from a given handle.
|
||||
"""
|
||||
@@ -236,40 +228,40 @@ class GuiItemDetails(QWidget):
|
||||
self.clearDetails()
|
||||
return
|
||||
|
||||
self.theHandle = tHandle
|
||||
self._itemHandle = tHandle
|
||||
theLabel = nwItem.itemName
|
||||
if len(theLabel) > 100:
|
||||
theLabel = theLabel[:96].rstrip()+" ..."
|
||||
|
||||
itStatus = nwItem.itemStatus
|
||||
if nwItem.itemClass == nwItemClass.NOVEL:
|
||||
itStatus = self.theProject.statusItems.checkEntry(itStatus) # Make sure it's valid
|
||||
itStatus = self.theProject.statusItems.checkEntry(itStatus) # Make sure it's valid
|
||||
flagIcon = self.theParent.statusIcons[itStatus]
|
||||
else:
|
||||
itStatus = self.theProject.importItems.checkEntry(itStatus) # Make sure it's valid
|
||||
itStatus = self.theProject.importItems.checkEntry(itStatus) # Make sure it's valid
|
||||
flagIcon = self.theParent.importIcons[itStatus]
|
||||
|
||||
if nwItem.itemType == nwItemType.FILE:
|
||||
if nwItem.isExported:
|
||||
self.labelFlag.setPixmap(self.expCheck)
|
||||
self.labelFlag.setPixmap(self._expCheck)
|
||||
else:
|
||||
self.labelFlag.setPixmap(self.expCross)
|
||||
self.labelFlag.setPixmap(self._expCross)
|
||||
else:
|
||||
self.labelFlag.setPixmap(QPixmap(1, 1))
|
||||
|
||||
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}")
|
||||
@@ -282,4 +274,20 @@ class GuiItemDetails(QWidget):
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Slots
|
||||
##
|
||||
|
||||
@pyqtSlot(str, int, int, int)
|
||||
def doUpdateCounts(self, tHandle, cC, wC, pC):
|
||||
"""Update the counts if the handle is the same as the one we're
|
||||
already showing. Otherwise, do nothing.
|
||||
"""
|
||||
if tHandle == self._itemHandle:
|
||||
self.cCountData.setText(f"{cC:n}")
|
||||
self.wCountData.setText(f"{wC:n}")
|
||||
self.pCountData.setText(f"{pC:n}")
|
||||
|
||||
return
|
||||
|
||||
# END Class GuiItemDetails
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – GUI Novel Tree
|
||||
============================
|
||||
@@ -32,11 +31,12 @@ 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__)
|
||||
|
||||
|
||||
class GuiNovelTree(QTreeWidget):
|
||||
|
||||
C_TITLE = 0
|
||||
@@ -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)
|
||||
@@ -95,9 +99,6 @@ class GuiNovelTree(QTreeWidget):
|
||||
|
||||
logger.debug("GuiNovelTree initialisation complete")
|
||||
|
||||
# Internal Mapping
|
||||
self.makeAlert = self.theParent.makeAlert
|
||||
|
||||
return
|
||||
|
||||
def initTree(self):
|
||||
@@ -221,7 +222,7 @@ class GuiNovelTree(QTreeWidget):
|
||||
tHandle = theData[0]
|
||||
tLine = checkInt(theData[1], 1)
|
||||
|
||||
logger.verbose("User selected entry with handle %s on line %s" % (tHandle, tLine))
|
||||
logger.verbose("User selected entry with handle '%s' on line %s", tHandle, tLine)
|
||||
self.theParent.openDocument(tHandle, tLine=tLine-1, doScroll=True)
|
||||
|
||||
return
|
||||
@@ -246,9 +247,9 @@ class GuiNovelTree(QTreeWidget):
|
||||
"""
|
||||
self.clearTree()
|
||||
|
||||
currTitle = None
|
||||
currTitle = None
|
||||
currChapter = None
|
||||
currScene = None
|
||||
currScene = None
|
||||
|
||||
for tKey, tHandle, sTitle, novIdx in self.theIndex.novelStructure(skipExcluded=True):
|
||||
|
||||
@@ -258,9 +259,9 @@ class GuiNovelTree(QTreeWidget):
|
||||
tLevel = novIdx["level"]
|
||||
if tLevel == "H1":
|
||||
self.addTopLevelItem(tItem)
|
||||
currTitle = tItem
|
||||
currTitle = tItem
|
||||
currChapter = None
|
||||
currScene = None
|
||||
currScene = None
|
||||
|
||||
elif tLevel == "H2":
|
||||
if currTitle is None:
|
||||
@@ -268,7 +269,7 @@ class GuiNovelTree(QTreeWidget):
|
||||
else:
|
||||
currTitle.addChild(tItem)
|
||||
currChapter = tItem
|
||||
currScene = None
|
||||
currScene = None
|
||||
|
||||
elif tLevel == "H3":
|
||||
if currChapter is None:
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – GUI Project Outline
|
||||
=================================
|
||||
@@ -29,55 +28,57 @@ import logging
|
||||
|
||||
from time import time
|
||||
|
||||
from PyQt5.QtCore import Qt, QSize
|
||||
from PyQt5.QtCore import Qt, QSize, pyqtSlot
|
||||
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__)
|
||||
|
||||
|
||||
class GuiOutline(QTreeWidget):
|
||||
|
||||
DEF_WIDTH = {
|
||||
nwOutline.TITLE : 200,
|
||||
nwOutline.LEVEL : 40,
|
||||
nwOutline.LABEL : 150,
|
||||
nwOutline.LINE : 40,
|
||||
nwOutline.CCOUNT : 50,
|
||||
nwOutline.WCOUNT : 50,
|
||||
nwOutline.PCOUNT : 50,
|
||||
nwOutline.POV : 100,
|
||||
nwOutline.FOCUS : 100,
|
||||
nwOutline.CHAR : 100,
|
||||
nwOutline.PLOT : 100,
|
||||
nwOutline.TIME : 100,
|
||||
nwOutline.WORLD : 100,
|
||||
nwOutline.OBJECT : 100,
|
||||
nwOutline.ENTITY : 100,
|
||||
nwOutline.CUSTOM : 100,
|
||||
nwOutline.SYNOP : 200,
|
||||
nwOutline.TITLE: 200,
|
||||
nwOutline.LEVEL: 40,
|
||||
nwOutline.LABEL: 150,
|
||||
nwOutline.LINE: 40,
|
||||
nwOutline.CCOUNT: 50,
|
||||
nwOutline.WCOUNT: 50,
|
||||
nwOutline.PCOUNT: 50,
|
||||
nwOutline.POV: 100,
|
||||
nwOutline.FOCUS: 100,
|
||||
nwOutline.CHAR: 100,
|
||||
nwOutline.PLOT: 100,
|
||||
nwOutline.TIME: 100,
|
||||
nwOutline.WORLD: 100,
|
||||
nwOutline.OBJECT: 100,
|
||||
nwOutline.ENTITY: 100,
|
||||
nwOutline.CUSTOM: 100,
|
||||
nwOutline.SYNOP: 200,
|
||||
}
|
||||
|
||||
DEF_HIDDEN = {
|
||||
nwOutline.TITLE : False,
|
||||
nwOutline.LEVEL : True,
|
||||
nwOutline.LABEL : False,
|
||||
nwOutline.LINE : True,
|
||||
nwOutline.CCOUNT : True,
|
||||
nwOutline.WCOUNT : False,
|
||||
nwOutline.PCOUNT : False,
|
||||
nwOutline.POV : False,
|
||||
nwOutline.FOCUS : True,
|
||||
nwOutline.CHAR : False,
|
||||
nwOutline.PLOT : False,
|
||||
nwOutline.TIME : True,
|
||||
nwOutline.WORLD : False,
|
||||
nwOutline.OBJECT : True,
|
||||
nwOutline.ENTITY : True,
|
||||
nwOutline.CUSTOM : True,
|
||||
nwOutline.SYNOP : False,
|
||||
nwOutline.TITLE: False,
|
||||
nwOutline.LEVEL: True,
|
||||
nwOutline.LABEL: False,
|
||||
nwOutline.LINE: True,
|
||||
nwOutline.CCOUNT: True,
|
||||
nwOutline.WCOUNT: False,
|
||||
nwOutline.PCOUNT: False,
|
||||
nwOutline.POV: False,
|
||||
nwOutline.FOCUS: True,
|
||||
nwOutline.CHAR: False,
|
||||
nwOutline.PLOT: False,
|
||||
nwOutline.TIME: True,
|
||||
nwOutline.WORLD: False,
|
||||
nwOutline.OBJECT: True,
|
||||
nwOutline.ENTITY: True,
|
||||
nwOutline.CUSTOM: True,
|
||||
nwOutline.SYNOP: False,
|
||||
}
|
||||
|
||||
def __init__(self, theParent):
|
||||
@@ -93,9 +94,6 @@ class GuiOutline(QTreeWidget):
|
||||
self.optState = theParent.theProject.optState
|
||||
self.headerMenu = GuiOutlineHeaderMenu(self)
|
||||
|
||||
self.firstView = True
|
||||
self.lastBuild = 0
|
||||
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
self.setExpandsOnDoubleClick(False)
|
||||
@@ -112,16 +110,18 @@ class GuiOutline(QTreeWidget):
|
||||
self.treeHead.customContextMenuRequested.connect(self._headerRightClick)
|
||||
self.treeHead.sectionMoved.connect(self._columnMoved)
|
||||
|
||||
self.treeMap = {}
|
||||
self.treeOrder = []
|
||||
self.colWidth = {}
|
||||
self.colHidden = {}
|
||||
self.colIndex = {}
|
||||
self.treeNCols = 0
|
||||
# Internals
|
||||
self._treeOrder = []
|
||||
self._colWidth = {}
|
||||
self._colHidden = {}
|
||||
self._colIdx = {}
|
||||
self._treeNCols = 0
|
||||
self._firstView = True
|
||||
self._lastBuild = 0
|
||||
|
||||
self.initOutline()
|
||||
self.clearOutline()
|
||||
self.headerMenu.setHiddenState(self.colHidden)
|
||||
self.headerMenu.setHiddenState(self._colHidden)
|
||||
|
||||
logger.debug("GuiOutline initialisation complete")
|
||||
|
||||
@@ -149,20 +149,20 @@ 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 = {}
|
||||
self.colHidden = {}
|
||||
self.colIndex = {}
|
||||
self.treeNCols = 0
|
||||
self._treeOrder = []
|
||||
self._colWidth = {}
|
||||
self._colHidden = {}
|
||||
self._colIdx = {}
|
||||
self._treeNCols = 0
|
||||
|
||||
for i, hItem in enumerate(nwOutline):
|
||||
self.treeOrder.append(hItem)
|
||||
self.colWidth[hItem] = self.DEF_WIDTH[hItem]
|
||||
self.colHidden[hItem] = self.DEF_HIDDEN[hItem]
|
||||
for hItem in nwOutline:
|
||||
self._treeOrder.append(hItem)
|
||||
self._colWidth[hItem] = self.DEF_WIDTH[hItem]
|
||||
self._colHidden[hItem] = self.DEF_HIDDEN[hItem]
|
||||
|
||||
self.treeNCols = len(self.treeOrder)
|
||||
self._treeNCols = len(self._treeOrder)
|
||||
|
||||
return
|
||||
|
||||
@@ -172,15 +172,15 @@ class GuiOutline(QTreeWidget):
|
||||
tree.
|
||||
"""
|
||||
# If it's the first time, we always build
|
||||
if self.firstView or self.firstView and overRide:
|
||||
if self._firstView or self._firstView and overRide:
|
||||
self._loadHeaderState()
|
||||
self._populateTree()
|
||||
self.firstView = False
|
||||
self._firstView = False
|
||||
return
|
||||
|
||||
# If the novel index or novel tree has changed since the tree
|
||||
# was last built, we rebuild the tree from the updated index.
|
||||
indexChanged = self.theIndex.novelChangedSince(self.lastBuild)
|
||||
indexChanged = self.theIndex.novelChangedSince(self._lastBuild)
|
||||
doBuild = (novelChanged or indexChanged) and self.theProject.autoOutline
|
||||
if doBuild or overRide:
|
||||
logger.debug("Rebuilding Project Outline")
|
||||
@@ -193,53 +193,57 @@ class GuiOutline(QTreeWidget):
|
||||
"""
|
||||
self._saveHeaderState()
|
||||
self.clearOutline()
|
||||
self.firstView = True
|
||||
self._firstView = True
|
||||
return
|
||||
|
||||
##
|
||||
# Slots
|
||||
##
|
||||
|
||||
@pyqtSlot("QTreeWidgetItem*", int)
|
||||
def _treeDoubleClick(self, tItem, tCol):
|
||||
"""Extract the handle and line number of the title double-
|
||||
clicked, and send it to the main gui class for opening in the
|
||||
document editor.
|
||||
"""
|
||||
tHandle = tItem.data(self.colIndex[nwOutline.TITLE], Qt.UserRole)
|
||||
tHandle = tItem.data(self._colIdx[nwOutline.TITLE], Qt.UserRole)
|
||||
try:
|
||||
tLine = int(tItem.text(self.colIndex[nwOutline.LINE]))
|
||||
tLine = int(tItem.text(self._colIdx[nwOutline.LINE]))
|
||||
except Exception:
|
||||
tLine = 1
|
||||
|
||||
logger.verbose("User selected entry with handle %s on line %s" % (tHandle, tLine))
|
||||
logger.verbose("User selected entry with handle '%s' on line %s", tHandle, tLine)
|
||||
self.theParent.openDocument(tHandle, tLine=tLine-1, doScroll=True)
|
||||
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _itemSelected(self):
|
||||
"""Extract the handle and line number of the currently selected
|
||||
title, and send it to the details panel.
|
||||
"""
|
||||
selItems = self.selectedItems()
|
||||
if selItems:
|
||||
tHandle = selItems[0].data(self.colIndex[nwOutline.TITLE], Qt.UserRole)
|
||||
sTitle = selItems[0].data(self.colIndex[nwOutline.LINE], Qt.UserRole)
|
||||
tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole)
|
||||
sTitle = selItems[0].data(self._colIdx[nwOutline.LINE], Qt.UserRole)
|
||||
self.theParent.projMeta.showItem(tHandle, sTitle)
|
||||
self.theParent.treeView.setSelectedHandle(tHandle)
|
||||
|
||||
return
|
||||
|
||||
@pyqtSlot("QPoint")
|
||||
def _headerRightClick(self, clickPos):
|
||||
"""Show the header column menu.
|
||||
"""
|
||||
self.headerMenu.exec_(self.mapToGlobal(clickPos))
|
||||
return
|
||||
|
||||
@pyqtSlot(int, int, int)
|
||||
def _columnMoved(self, logIdx, oldVisualIdx, newVisualIdx):
|
||||
"""Make sure the order array is up to date with the actual order
|
||||
of the columns.
|
||||
"""
|
||||
self.treeOrder.insert(newVisualIdx, self.treeOrder.pop(oldVisualIdx))
|
||||
self._treeOrder.insert(newVisualIdx, self._treeOrder.pop(oldVisualIdx))
|
||||
self._saveHeaderState()
|
||||
return
|
||||
|
||||
@@ -247,9 +251,9 @@ class GuiOutline(QTreeWidget):
|
||||
"""Receive the changes to column visibility forwarded by the
|
||||
header context menu.
|
||||
"""
|
||||
logger.verbose("User toggled Outline column '%s'" % theItem.name)
|
||||
if theItem in self.colIndex:
|
||||
self.setColumnHidden(self.colIndex[theItem], not isChecked)
|
||||
logger.verbose("User toggled Outline column '%s'", theItem.name)
|
||||
if theItem in self._colIdx:
|
||||
self.setColumnHidden(self._colIdx[theItem], not isChecked)
|
||||
self._saveHeaderState()
|
||||
|
||||
return
|
||||
@@ -271,7 +275,7 @@ class GuiOutline(QTreeWidget):
|
||||
try:
|
||||
treeOrder.append(nwOutline[hName])
|
||||
except Exception:
|
||||
logger.warning("Ignored unknown outline column '%s'" % str(hName))
|
||||
logger.warning("Ignored unknown outline column '%s'", str(hName))
|
||||
|
||||
# Add columns that was not in the file to the treeOrder array.
|
||||
for hItem in nwOutline:
|
||||
@@ -280,29 +284,29 @@ class GuiOutline(QTreeWidget):
|
||||
|
||||
# Check that we now have a complete list, and only if so, save
|
||||
# the order loaded from file. Otherwise, we keep the default.
|
||||
if len(treeOrder) == self.treeNCols:
|
||||
self.treeOrder = treeOrder
|
||||
if len(treeOrder) == self._treeNCols:
|
||||
self._treeOrder = treeOrder
|
||||
else:
|
||||
logger.error("Failed to extract outline column order from previous session")
|
||||
logger.error("Column count doesn't match %d != %d" % (len(treeOrder), self.treeNCols))
|
||||
logger.error("Column count doesn't match %d != %d", len(treeOrder), self._treeNCols)
|
||||
|
||||
# We load whatever column widths and hidden states we find in
|
||||
# the file, and leave the rest in their default state.
|
||||
tmpWidth = self.optState.getValue("GuiOutline", "columnWidth", {})
|
||||
for hName in tmpWidth:
|
||||
try:
|
||||
self.colWidth[nwOutline[hName]] = self.mainConf.pxInt(tmpWidth[hName])
|
||||
self._colWidth[nwOutline[hName]] = self.mainConf.pxInt(tmpWidth[hName])
|
||||
except Exception:
|
||||
logger.warning("Ignored unknown outline column '%s'" % str(hName))
|
||||
logger.warning("Ignored unknown outline column '%s'", str(hName))
|
||||
|
||||
tmpHidden = self.optState.getValue("GuiOutline", "columnHidden", {})
|
||||
for hName in tmpHidden:
|
||||
try:
|
||||
self.colHidden[nwOutline[hName]] = tmpHidden[hName]
|
||||
self._colHidden[nwOutline[hName]] = tmpHidden[hName]
|
||||
except Exception:
|
||||
logger.warning("Ignored unknown outline column '%s'" % str(hName))
|
||||
logger.warning("Ignored unknown outline column '%s'", str(hName))
|
||||
|
||||
self.headerMenu.setHiddenState(self.colHidden)
|
||||
self.headerMenu.setHiddenState(self._colHidden)
|
||||
|
||||
return
|
||||
|
||||
@@ -313,19 +317,19 @@ class GuiOutline(QTreeWidget):
|
||||
the last known width in case they're unhidden again.
|
||||
"""
|
||||
# If we haven't built the tree, there is nothing to save.
|
||||
if self.lastBuild == 0:
|
||||
if self._lastBuild == 0:
|
||||
return
|
||||
|
||||
treeOrder = []
|
||||
colWidth = {}
|
||||
colWidth = {}
|
||||
colHidden = {}
|
||||
|
||||
for hItem in nwOutline:
|
||||
colWidth[hItem.name] = self.mainConf.rpxInt(self.colWidth[hItem])
|
||||
colHidden[hItem.name] = self.colHidden[hItem]
|
||||
colWidth[hItem.name] = self.mainConf.rpxInt(self._colWidth[hItem])
|
||||
colHidden[hItem.name] = self._colHidden[hItem]
|
||||
|
||||
for iCol in range(self.columnCount()):
|
||||
hName = self.treeOrder[iCol].name
|
||||
hName = self._treeOrder[iCol].name
|
||||
treeOrder.append(hName)
|
||||
|
||||
iLog = self.treeHead.logicalIndex(iCol)
|
||||
@@ -352,41 +356,40 @@ class GuiOutline(QTreeWidget):
|
||||
"""
|
||||
self.clear()
|
||||
|
||||
if self.firstView:
|
||||
if self._firstView:
|
||||
theLabels = []
|
||||
for i, hItem in enumerate(self.treeOrder):
|
||||
theLabels.append(nwLabels.OUTLINE_COLS[hItem])
|
||||
self.colIndex[hItem] = i
|
||||
for i, hItem in enumerate(self._treeOrder):
|
||||
theLabels.append(trConst(nwLabels.OUTLINE_COLS[hItem]))
|
||||
self._colIdx[hItem] = i
|
||||
|
||||
self.setHeaderLabels(theLabels)
|
||||
for hItem in self.treeOrder:
|
||||
self.setColumnWidth(self.colIndex[hItem], self.colWidth[hItem])
|
||||
self.setColumnHidden(self.colIndex[hItem], self.colHidden[hItem])
|
||||
for hItem in self._treeOrder:
|
||||
self.setColumnWidth(self._colIdx[hItem], self._colWidth[hItem])
|
||||
self.setColumnHidden(self._colIdx[hItem], self._colHidden[hItem])
|
||||
|
||||
# Make sure title column is always visible,
|
||||
# and handle column always hidden
|
||||
self.setColumnHidden(self.colIndex[nwOutline.TITLE], False)
|
||||
self.setColumnHidden(self._colIdx[nwOutline.TITLE], False)
|
||||
|
||||
headItem = self.headerItem()
|
||||
headItem.setTextAlignment(self.colIndex[nwOutline.CCOUNT], Qt.AlignRight)
|
||||
headItem.setTextAlignment(self.colIndex[nwOutline.WCOUNT], Qt.AlignRight)
|
||||
headItem.setTextAlignment(self.colIndex[nwOutline.PCOUNT], Qt.AlignRight)
|
||||
headItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignRight)
|
||||
headItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight)
|
||||
headItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight)
|
||||
|
||||
currTitle = None
|
||||
currTitle = None
|
||||
currChapter = None
|
||||
currScene = None
|
||||
currScene = None
|
||||
|
||||
for tKey, tHandle, sTitle, novIdx in self.theIndex.novelStructure(skipExcluded=True):
|
||||
|
||||
tItem = self._createTreeItem(tHandle, sTitle, novIdx)
|
||||
self.treeMap[tKey] = tItem
|
||||
|
||||
tLevel = novIdx["level"]
|
||||
if tLevel == "H1":
|
||||
self.addTopLevelItem(tItem)
|
||||
currTitle = tItem
|
||||
currTitle = tItem
|
||||
currChapter = None
|
||||
currScene = None
|
||||
currScene = None
|
||||
|
||||
elif tLevel == "H2":
|
||||
if currTitle is None:
|
||||
@@ -394,7 +397,7 @@ class GuiOutline(QTreeWidget):
|
||||
else:
|
||||
currTitle.addChild(tItem)
|
||||
currChapter = tItem
|
||||
currScene = None
|
||||
currScene = None
|
||||
|
||||
elif tLevel == "H3":
|
||||
if currChapter is None:
|
||||
@@ -420,52 +423,53 @@ class GuiOutline(QTreeWidget):
|
||||
|
||||
tItem.setExpanded(True)
|
||||
|
||||
self.lastBuild = time()
|
||||
self._lastBuild = time()
|
||||
|
||||
return
|
||||
|
||||
def _createTreeItem(self, tHandle, sTitle, novIdx):
|
||||
"""Populate a tree item with all the column values.
|
||||
"""
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
newItem = QTreeWidgetItem()
|
||||
hIcon = "doc_%s" % novIdx["level"].lower()
|
||||
hIcon = "doc_%s" % novIdx["level"].lower()
|
||||
|
||||
cC = int(novIdx["cCount"])
|
||||
wC = int(novIdx["wCount"])
|
||||
pC = int(novIdx["pCount"])
|
||||
|
||||
newItem.setText(self.colIndex[nwOutline.TITLE], novIdx["title"])
|
||||
newItem.setData(self.colIndex[nwOutline.TITLE], Qt.UserRole, tHandle)
|
||||
newItem.setIcon(self.colIndex[nwOutline.TITLE], self.theTheme.getIcon(hIcon))
|
||||
newItem.setText(self.colIndex[nwOutline.LEVEL], novIdx["level"])
|
||||
newItem.setText(self.colIndex[nwOutline.LABEL], nwItem.itemName)
|
||||
newItem.setIcon(self.colIndex[nwOutline.LABEL], self.theTheme.getIcon("proj_document"))
|
||||
newItem.setText(self.colIndex[nwOutline.LINE], sTitle[1:].lstrip("0"))
|
||||
newItem.setData(self.colIndex[nwOutline.LINE], Qt.UserRole, sTitle)
|
||||
newItem.setText(self.colIndex[nwOutline.SYNOP], novIdx["synopsis"])
|
||||
newItem.setText(self.colIndex[nwOutline.CCOUNT], f"{cC:n}")
|
||||
newItem.setText(self.colIndex[nwOutline.WCOUNT], f"{wC:n}")
|
||||
newItem.setText(self.colIndex[nwOutline.PCOUNT], f"{pC:n}")
|
||||
newItem.setTextAlignment(self.colIndex[nwOutline.CCOUNT], Qt.AlignRight)
|
||||
newItem.setTextAlignment(self.colIndex[nwOutline.WCOUNT], Qt.AlignRight)
|
||||
newItem.setTextAlignment(self.colIndex[nwOutline.PCOUNT], Qt.AlignRight)
|
||||
newItem.setText(self._colIdx[nwOutline.TITLE], novIdx["title"])
|
||||
newItem.setData(self._colIdx[nwOutline.TITLE], Qt.UserRole, tHandle)
|
||||
newItem.setIcon(self._colIdx[nwOutline.TITLE], self.theTheme.getIcon(hIcon))
|
||||
newItem.setText(self._colIdx[nwOutline.LEVEL], novIdx["level"])
|
||||
newItem.setText(self._colIdx[nwOutline.LABEL], nwItem.itemName)
|
||||
newItem.setIcon(self._colIdx[nwOutline.LABEL], self.theTheme.getIcon("proj_document"))
|
||||
newItem.setText(self._colIdx[nwOutline.LINE], sTitle[1:].lstrip("0"))
|
||||
newItem.setData(self._colIdx[nwOutline.LINE], Qt.UserRole, sTitle)
|
||||
newItem.setText(self._colIdx[nwOutline.SYNOP], novIdx["synopsis"])
|
||||
newItem.setText(self._colIdx[nwOutline.CCOUNT], f"{cC:n}")
|
||||
newItem.setText(self._colIdx[nwOutline.WCOUNT], f"{wC:n}")
|
||||
newItem.setText(self._colIdx[nwOutline.PCOUNT], f"{pC:n}")
|
||||
newItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignRight)
|
||||
newItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight)
|
||||
newItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight)
|
||||
|
||||
theRefs = self.theIndex.getReferences(tHandle, sTitle)
|
||||
newItem.setText(self.colIndex[nwOutline.POV], ", ".join(theRefs[nwKeyWords.POV_KEY]))
|
||||
newItem.setText(self.colIndex[nwOutline.FOCUS], ", ".join(theRefs[nwKeyWords.FOCUS_KEY]))
|
||||
newItem.setText(self.colIndex[nwOutline.CHAR], ", ".join(theRefs[nwKeyWords.CHAR_KEY]))
|
||||
newItem.setText(self.colIndex[nwOutline.PLOT], ", ".join(theRefs[nwKeyWords.PLOT_KEY]))
|
||||
newItem.setText(self.colIndex[nwOutline.TIME], ", ".join(theRefs[nwKeyWords.TIME_KEY]))
|
||||
newItem.setText(self.colIndex[nwOutline.WORLD], ", ".join(theRefs[nwKeyWords.WORLD_KEY]))
|
||||
newItem.setText(self.colIndex[nwOutline.OBJECT], ", ".join(theRefs[nwKeyWords.OBJECT_KEY]))
|
||||
newItem.setText(self.colIndex[nwOutline.ENTITY], ", ".join(theRefs[nwKeyWords.ENTITY_KEY]))
|
||||
newItem.setText(self.colIndex[nwOutline.CUSTOM], ", ".join(theRefs[nwKeyWords.CUSTOM_KEY]))
|
||||
newItem.setText(self._colIdx[nwOutline.POV], ", ".join(theRefs[nwKeyWords.POV_KEY]))
|
||||
newItem.setText(self._colIdx[nwOutline.FOCUS], ", ".join(theRefs[nwKeyWords.FOCUS_KEY]))
|
||||
newItem.setText(self._colIdx[nwOutline.CHAR], ", ".join(theRefs[nwKeyWords.CHAR_KEY]))
|
||||
newItem.setText(self._colIdx[nwOutline.PLOT], ", ".join(theRefs[nwKeyWords.PLOT_KEY]))
|
||||
newItem.setText(self._colIdx[nwOutline.TIME], ", ".join(theRefs[nwKeyWords.TIME_KEY]))
|
||||
newItem.setText(self._colIdx[nwOutline.WORLD], ", ".join(theRefs[nwKeyWords.WORLD_KEY]))
|
||||
newItem.setText(self._colIdx[nwOutline.OBJECT], ", ".join(theRefs[nwKeyWords.OBJECT_KEY]))
|
||||
newItem.setText(self._colIdx[nwOutline.ENTITY], ", ".join(theRefs[nwKeyWords.ENTITY_KEY]))
|
||||
newItem.setText(self._colIdx[nwOutline.CUSTOM], ", ".join(theRefs[nwKeyWords.CUSTOM_KEY]))
|
||||
|
||||
return newItem
|
||||
|
||||
# END Class GuiOutline
|
||||
|
||||
|
||||
class GuiOutlineHeaderMenu(QMenu):
|
||||
|
||||
def __init__(self, theParent):
|
||||
@@ -474,7 +478,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,10 +486,10 @@ 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)
|
||||
lambda isChecked, tItem=hItem: self._columnToggled(isChecked, tItem)
|
||||
)
|
||||
self.addAction(self.actionMap[hItem])
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – GUI Project Outline Details
|
||||
=========================================
|
||||
@@ -27,23 +26,24 @@ 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,8 +190,8 @@ class GuiOutlineDetails(QScrollArea):
|
||||
self.mainForm.setVerticalSpacing(vSpace)
|
||||
|
||||
# Selected Item Tags
|
||||
self.tagsGroup = QGroupBox("Reference Tags", self)
|
||||
self.tagsForm = QGridLayout()
|
||||
self.tagsGroup = QGroupBox(self.tr("Reference Tags"), self)
|
||||
self.tagsForm = QGridLayout()
|
||||
self.tagsGroup.setLayout(self.tagsForm)
|
||||
|
||||
self.tagsForm.addWidget(self.povKeyLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
@@ -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("")
|
||||
@@ -279,16 +279,16 @@ class GuiOutlineDetails(QScrollArea):
|
||||
"""Update the content of the tree with the given handle and line
|
||||
number pointing to a header.
|
||||
"""
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
novIdx = self.theIndex.getNovelData(tHandle, sTitle)
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
novIdx = self.theIndex.getNovelData(tHandle, sTitle)
|
||||
theRefs = self.theIndex.getReferences(tHandle, sTitle)
|
||||
if nwItem is None or novIdx is None:
|
||||
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)
|
||||
@@ -323,7 +323,7 @@ class GuiOutlineDetails(QScrollArea):
|
||||
def _tagClicked(self, theLink):
|
||||
"""Capture the click of a tag in the right-most column.
|
||||
"""
|
||||
logger.verbose("Clicked link: '%s'" % theLink)
|
||||
logger.verbose("Clicked link: '%s'", theLink)
|
||||
if len(theLink) > 0:
|
||||
theBits = theLink.split("=")
|
||||
if len(theBits) == 2:
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – GUI Project Details
|
||||
=================================
|
||||
@@ -25,8 +24,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
import logging
|
||||
import math
|
||||
import logging
|
||||
|
||||
from PyQt5.QtCore import Qt, QSize
|
||||
from PyQt5.QtGui import QFont
|
||||
@@ -35,15 +34,16 @@ 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__)
|
||||
|
||||
|
||||
class GuiProjectDetails(PagedDialog):
|
||||
|
||||
def __init__(self, theParent, theProject):
|
||||
def __init__(self, theParent):
|
||||
PagedDialog.__init__(self, theParent)
|
||||
|
||||
logger.debug("Initialising GuiProjectDetails ...")
|
||||
@@ -51,10 +51,10 @@ class GuiProjectDetails(PagedDialog):
|
||||
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theParent = theParent
|
||||
self.theProject = theProject
|
||||
self.optState = theProject.optState
|
||||
self.theProject = theParent.theProject
|
||||
self.optState = theParent.theProject.optState
|
||||
|
||||
self.setWindowTitle("Project Details")
|
||||
self.setWindowTitle(self.tr("Project Details"))
|
||||
|
||||
wW = self.mainConf.pxInt(600)
|
||||
wH = self.mainConf.pxInt(400)
|
||||
@@ -66,13 +66,14 @@ class GuiProjectDetails(PagedDialog):
|
||||
self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "winHeight", wH))
|
||||
)
|
||||
|
||||
self.tabMain = GuiProjectDetailsMain(self.theParent, self.theProject)
|
||||
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)
|
||||
|
||||
@@ -127,6 +128,7 @@ class GuiProjectDetails(PagedDialog):
|
||||
|
||||
# END Class GuiProjectDetails
|
||||
|
||||
|
||||
class GuiProjectDetailsMain(QWidget):
|
||||
|
||||
def __init__(self, theParent, theProject):
|
||||
@@ -154,7 +156,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 +166,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 +179,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 +212,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)
|
||||
@@ -238,6 +242,7 @@ class GuiProjectDetailsMain(QWidget):
|
||||
|
||||
# END Class GuiProjectDetailsMain
|
||||
|
||||
|
||||
class GuiProjectDetailsContents(QWidget):
|
||||
|
||||
C_TITLE = 0
|
||||
@@ -271,7 +276,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 +316,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 +336,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 +347,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 +370,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 +402,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
|
||||
|
||||
##
|
||||
@@ -401,8 +413,8 @@ class GuiProjectDetailsContents(QWidget):
|
||||
"""Set the content of the chapter/page tree.
|
||||
"""
|
||||
dblPages = self.dblValue.isChecked()
|
||||
wpPage = self.wpValue.value()
|
||||
fstPage = self.poValue.value() - 1
|
||||
wpPage = self.wpValue.value()
|
||||
fstPage = self.poValue.value() - 1
|
||||
|
||||
pTotal = 0
|
||||
tPages = 1
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – GUI Project Tree
|
||||
==============================
|
||||
@@ -30,19 +29,19 @@ import logging
|
||||
|
||||
from time import time
|
||||
|
||||
from PyQt5.QtCore import Qt, QSize, pyqtSignal
|
||||
from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import (
|
||||
QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction
|
||||
)
|
||||
|
||||
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__)
|
||||
|
||||
|
||||
class GuiProjectTree(QTreeWidget):
|
||||
|
||||
C_NAME = 0
|
||||
@@ -52,6 +51,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
novelItemChanged = pyqtSignal()
|
||||
noteItemChanged = pyqtSignal()
|
||||
projectWordCountChanged = pyqtSignal(int, int)
|
||||
|
||||
def __init__(self, theParent):
|
||||
QTreeWidget.__init__(self, theParent)
|
||||
@@ -85,14 +85,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
|
||||
@@ -127,7 +132,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
self.initTree()
|
||||
|
||||
# Internal Function Mapping
|
||||
self.makeAlert = self.theParent.makeAlert
|
||||
self.makeAlert = self.theParent.makeAlert
|
||||
self.askQuestion = self.theParent.askQuestion
|
||||
|
||||
logger.debug("GuiProjectTree initialisation complete")
|
||||
@@ -192,24 +197,23 @@ class GuiProjectTree(QTreeWidget):
|
||||
# If class is still not set, alert the user and exit
|
||||
if itemClass is None:
|
||||
if itemType == nwItemType.FILE:
|
||||
self.makeAlert(
|
||||
"Please select a valid location in the tree to add the document.",
|
||||
nwAlert.ERROR
|
||||
)
|
||||
self.makeAlert(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.",
|
||||
nwAlert.ERROR
|
||||
)
|
||||
self.makeAlert(self.tr(
|
||||
"Please select a valid location in the tree to add the folder."
|
||||
), nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
# Everything is fine, we have what we need, so we proceed
|
||||
logger.verbose("Adding new item of type %s and class %s to handle %s" % (
|
||||
itemType.name, itemClass.name, str(pHandle))
|
||||
logger.verbose(
|
||||
"Adding new item of type '%s' and class '%s' to handle '%s'",
|
||||
itemType.name, itemClass.name, str(pHandle)
|
||||
)
|
||||
|
||||
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
|
||||
@@ -222,9 +226,9 @@ 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.makeAlert(self.tr(
|
||||
"Did not find anywhere to add the file or folder!"
|
||||
), nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
# Now check if the selected item is a file, in which case
|
||||
@@ -236,35 +240,33 @@ 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.makeAlert(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.makeAlert(self.tr(
|
||||
"Cannot add new files or folders to the Trash folder."
|
||||
), nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
parTree = self.theProject.projTree.getItemPath(pHandle)
|
||||
|
||||
# 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((
|
||||
self.makeAlert(self.tr(
|
||||
"Cannot add new folder to this item. "
|
||||
"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")
|
||||
@@ -284,8 +286,11 @@ class GuiProjectTree(QTreeWidget):
|
||||
return True
|
||||
|
||||
# This is a new files, so let's add some content
|
||||
newDoc = NWDoc(self.theProject, self.theParent)
|
||||
curTxt = newDoc.openDocument(tHandle, showStatus=False)
|
||||
newDoc = NWDoc(self.theProject, tHandle)
|
||||
curTxt = newDoc.readDocument()
|
||||
if curTxt is None:
|
||||
curTxt = ""
|
||||
|
||||
if curTxt == "":
|
||||
if nwItem.itemLayout == nwItemLayout.CHAPTER:
|
||||
newText = f"## {nwItem.itemName}\n\n"
|
||||
@@ -297,7 +302,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
newText = f"# {nwItem.itemName}\n\n"
|
||||
|
||||
# Save the text and index it
|
||||
newDoc.saveDocument(newText)
|
||||
newDoc.writeDocument(newText)
|
||||
self.theIndex.scanText(tHandle, newText)
|
||||
|
||||
# Get Word Counts
|
||||
@@ -427,9 +432,9 @@ 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.makeAlert(self.tr(
|
||||
"There is currently no Trash folder in this project."
|
||||
), nwAlert.INFO)
|
||||
return False
|
||||
|
||||
theTrash = self.getTreeFromHandle(trashHandle)
|
||||
@@ -438,16 +443,19 @@ 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
|
||||
|
||||
logger.verbose("Deleting %d file(s) from Trash" % nTrash)
|
||||
logger.verbose("Deleting %d file(s) from Trash", nTrash)
|
||||
for tHandle in self.getTreeFromHandle(trashHandle):
|
||||
if tHandle == trashHandle:
|
||||
continue
|
||||
@@ -489,7 +497,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
wCount = int(trItemS.data(self.C_COUNT, Qt.UserRole))
|
||||
if nwItemS.itemType == nwItemType.FILE:
|
||||
logger.debug("User requested file %s deleted" % tHandle)
|
||||
logger.debug("User requested file '%s' deleted", tHandle)
|
||||
trItemP = trItemS.parent()
|
||||
trItemT = self._addTrashRoot()
|
||||
if trItemP is None or trItemT is None:
|
||||
@@ -503,7 +511,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
|
||||
@@ -511,32 +520,39 @@ class GuiProjectTree(QTreeWidget):
|
||||
doPermanent = True
|
||||
|
||||
if doPermanent:
|
||||
logger.debug("Permanently deleting file with handle %s" % tHandle)
|
||||
logger.debug("Permanently deleting file with handle '%s'", tHandle)
|
||||
|
||||
self.propagateCount(tHandle, 0)
|
||||
tIndex = trItemP.indexOfChild(trItemS)
|
||||
trItemC = trItemP.takeChild(tIndex)
|
||||
|
||||
if self.theParent.docEditor.theHandle == tHandle:
|
||||
if self.theParent.docEditor.docHandle() == tHandle:
|
||||
self.theParent.closeDocument()
|
||||
|
||||
theDoc = NWDoc(self.theProject, self.theParent)
|
||||
theDoc.deleteDocument(tHandle)
|
||||
delDoc = NWDoc(self.theProject, tHandle)
|
||||
if not delDoc.deleteDocument():
|
||||
self.makeAlert([
|
||||
self.tr("Could not delete document file."), delDoc.getError()
|
||||
], nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
self.theIndex.deleteHandle(tHandle)
|
||||
self._deleteTreeItem(tHandle)
|
||||
self._setTreeChanged(True)
|
||||
self.projectWordCount()
|
||||
|
||||
else:
|
||||
# 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:
|
||||
logger.warning("File has no parent item")
|
||||
|
||||
logger.debug("Moving file %s to trash" % tHandle)
|
||||
logger.debug("Moving file '%s' to trash", tHandle)
|
||||
|
||||
self.propagateCount(tHandle, 0)
|
||||
tIndex = trItemP.indexOfChild(trItemS)
|
||||
@@ -550,7 +566,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
self._setTreeChanged(True)
|
||||
|
||||
elif nwItemS.itemType == nwItemType.FOLDER:
|
||||
logger.debug("User requested folder %s deleted" % tHandle)
|
||||
logger.debug("User requested folder '%s' deleted", tHandle)
|
||||
trItemP = trItemS.parent()
|
||||
if trItemP is None:
|
||||
logger.error("Could not delete folder")
|
||||
@@ -561,7 +577,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."
|
||||
@@ -569,7 +585,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
return False
|
||||
|
||||
elif nwItemS.itemType == nwItemType.ROOT:
|
||||
logger.debug("User requested root folder %s deleted" % tHandle)
|
||||
logger.debug("User requested root folder '%s' deleted", tHandle)
|
||||
tIndex = self.indexOfTopLevelItem(trItemS)
|
||||
if trItemS.childCount() == 0:
|
||||
self.takeTopLevelItem(tIndex)
|
||||
@@ -577,7 +593,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."
|
||||
@@ -605,10 +621,10 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
iStatus = nwItem.itemStatus
|
||||
if nwItem.itemClass == nwItemClass.NOVEL:
|
||||
iStatus = self.theProject.statusItems.checkEntry(iStatus) # Make sure it's valid
|
||||
iStatus = self.theProject.statusItems.checkEntry(iStatus) # Make sure it's valid
|
||||
flagIcon = self.theParent.statusIcons[iStatus]
|
||||
else:
|
||||
iStatus = self.theProject.importItems.checkEntry(iStatus) # Make sure it's valid
|
||||
iStatus = self.theProject.importItems.checkEntry(iStatus) # Make sure it's valid
|
||||
flagIcon = self.theParent.importIcons[iStatus]
|
||||
|
||||
trItem.setText(self.C_NAME, nwItem.itemName)
|
||||
@@ -661,7 +677,8 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
self.theProject.setProjectWordCount(nWords)
|
||||
sWords = self.theProject.getSessionWordCount()
|
||||
self.theParent.statusBar.setStats(nWords, sWords)
|
||||
|
||||
self.projectWordCountChanged.emit(nWords, sWords)
|
||||
|
||||
return
|
||||
|
||||
@@ -679,7 +696,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
iCount += 1
|
||||
self._addTreeItem(nwItem)
|
||||
|
||||
logger.debug("%d items added to the project tree" % iCount)
|
||||
logger.debug("%d item(s) added to the project tree", iCount)
|
||||
return True
|
||||
|
||||
def undoLastMove(self):
|
||||
@@ -708,9 +725,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
wCount = int(srcItem.data(self.C_COUNT, Qt.UserRole))
|
||||
sHandle = srcItem.data(self.C_NAME, Qt.UserRole)
|
||||
dHandle = dstItem.data(self.C_NAME, Qt.UserRole)
|
||||
logger.debug("Moving item %s back to %s, index %d" % (
|
||||
sHandle, dHandle, dstIndex
|
||||
))
|
||||
logger.debug("Moving item '%s' back to '%s', index %d", sHandle, dHandle, dstIndex)
|
||||
|
||||
self.propagateCount(sHandle, 0)
|
||||
parItem = srcItem.parent()
|
||||
@@ -766,6 +781,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
# Slots
|
||||
##
|
||||
|
||||
@pyqtSlot("QPoint")
|
||||
def _rightClickMenu(self, clickPos):
|
||||
"""The user right clicked an element in the project tree, so we
|
||||
open a context menu in-place.
|
||||
@@ -773,7 +789,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
selItem = self.itemAt(clickPos)
|
||||
if isinstance(selItem, QTreeWidgetItem):
|
||||
tHandle = selItem.data(self.C_NAME, Qt.UserRole)
|
||||
self.setSelectedHandle(tHandle) # Just to be safe
|
||||
self.setSelectedHandle(tHandle) # Just to be safe
|
||||
tItem = self.theProject.projTree[tHandle]
|
||||
if tItem is not None:
|
||||
if self.ctxMenu.filterActions(tItem):
|
||||
@@ -782,6 +798,14 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
return
|
||||
|
||||
@pyqtSlot(str, int, int, int)
|
||||
def doUpdateCounts(self, tHandle, cCount, wCount, pCount):
|
||||
"""Slot for updating the word count of a specific item.
|
||||
"""
|
||||
self.propagateCount(tHandle, wCount)
|
||||
self.projectWordCount()
|
||||
return
|
||||
|
||||
##
|
||||
# Events
|
||||
##
|
||||
@@ -833,7 +857,9 @@ 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()
|
||||
@@ -855,7 +881,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
allowDrop &= not (self.dropIndicatorPosition() == QAbstractItemView.OnItem and onFile)
|
||||
|
||||
if allowDrop and not isRoot:
|
||||
logger.debug("Drag'n'drop of item %s accepted" % sHandle)
|
||||
logger.debug("Drag'n'drop of item '%s' accepted", sHandle)
|
||||
self.propagateCount(sHandle, 0)
|
||||
QTreeWidget.dropEvent(self, theEvent)
|
||||
self._postItemMove(sHandle, snItem, dnItem, wCount)
|
||||
@@ -863,8 +889,10 @@ 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)
|
||||
logger.debug("Drag'n'drop of item '%s' not accepted", sHandle)
|
||||
self.makeAlert(self.tr(
|
||||
"The item cannot be moved to that location."
|
||||
), nwAlert.ERROR)
|
||||
|
||||
return
|
||||
|
||||
@@ -884,9 +912,10 @@ class GuiProjectTree(QTreeWidget):
|
||||
# If the item does not have the same class as the target,
|
||||
# and the target is not a free root folder, update its class
|
||||
if not (isSame or onFree):
|
||||
logger.debug("Item %s class has been changed from %s to %s" % (
|
||||
logger.debug(
|
||||
"Item '%s' class has been changed from '%s' to '%s'",
|
||||
sHandle, snItem.itemClass.name, dnItem.itemClass.name
|
||||
))
|
||||
)
|
||||
snItem.setClass(dnItem.itemClass)
|
||||
self.setTreeItemValues(sHandle)
|
||||
|
||||
@@ -961,9 +990,9 @@ class GuiProjectTree(QTreeWidget):
|
||||
elif nwItem.itemType == nwItemType.TRASH:
|
||||
self.addTopLevelItem(newItem)
|
||||
else:
|
||||
self.makeAlert(
|
||||
"There is nowhere to add item with name '%s'" % nwItem.itemName, nwAlert.ERROR
|
||||
)
|
||||
self.makeAlert(self.tr(
|
||||
"There is nowhere to add item with name '{0}'."
|
||||
).format(nwItem.itemName), nwAlert.ERROR)
|
||||
del self._treeMap[tHandle]
|
||||
return None
|
||||
|
||||
@@ -973,7 +1002,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
try:
|
||||
byIndex = self._treeMap[pHandle].indexOfChild(self._treeMap[nHandle])
|
||||
except Exception:
|
||||
logger.error("Failed to get index of item with handle %s" % nHandle)
|
||||
logger.error("Failed to get index of item with handle '%s'", nHandle)
|
||||
if byIndex >= 0:
|
||||
self._treeMap[pHandle].insertChild(byIndex+1, newItem)
|
||||
else:
|
||||
@@ -1023,14 +1052,14 @@ class GuiProjectTree(QTreeWidget):
|
||||
nwItemS = self.theProject.projTree[tHandle]
|
||||
trItemP = trItemS.parent()
|
||||
if trItemP is None:
|
||||
logger.error("Failed to find new parent item of %s" % tHandle)
|
||||
logger.error("Failed to find new parent item of '%s'", tHandle)
|
||||
return False
|
||||
|
||||
pHandle = trItemP.data(self.C_NAME, Qt.UserRole)
|
||||
nwItemS.setParent(pHandle)
|
||||
self.setTreeItemValues(tHandle)
|
||||
|
||||
logger.debug("The parent of item %s has been changed to %s" % (tHandle, pHandle))
|
||||
logger.debug("The parent of item '%s' has been changed to '%s'", tHandle, pHandle)
|
||||
|
||||
return True
|
||||
|
||||
@@ -1073,6 +1102,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
# END Class GuiProjectTree
|
||||
|
||||
|
||||
class GuiProjectTreeMenu(QMenu):
|
||||
|
||||
def __init__(self, theTree):
|
||||
@@ -1081,43 +1111,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)
|
||||
|
||||
@@ -1137,7 +1167,7 @@ class GuiProjectTreeMenu(QMenu):
|
||||
|
||||
inTrash = theItem.itemParent == trashHandle and trashHandle is not None
|
||||
isTrash = theItem.itemHandle == trashHandle and trashHandle is not None
|
||||
isFile = theItem.itemType == nwItemType.FILE
|
||||
isFile = theItem.itemType == nwItemType.FILE
|
||||
|
||||
allowNew = not (isTrash or inTrash)
|
||||
|
||||
@@ -1156,6 +1186,7 @@ class GuiProjectTreeMenu(QMenu):
|
||||
# Slots
|
||||
##
|
||||
|
||||
@pyqtSlot()
|
||||
def _doOpenItem(self):
|
||||
"""Forward the open document call to the main GUI window.
|
||||
"""
|
||||
@@ -1163,6 +1194,7 @@ class GuiProjectTreeMenu(QMenu):
|
||||
self.theTree.theParent.openDocument(self.theItem.itemHandle, doScroll=False)
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _doViewItem(self):
|
||||
"""Forward the view document call to the main GUI window.
|
||||
"""
|
||||
@@ -1170,6 +1202,7 @@ class GuiProjectTreeMenu(QMenu):
|
||||
self.theTree.theParent.viewDocument(self.theItem.itemHandle)
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _doEditItem(self):
|
||||
"""Forward the edit item call to the main GUI window.
|
||||
"""
|
||||
@@ -1177,6 +1210,7 @@ class GuiProjectTreeMenu(QMenu):
|
||||
self.theTree.theParent.editItem()
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _doMakeFile(self):
|
||||
"""Forward the new file call to the project tree.
|
||||
"""
|
||||
@@ -1184,6 +1218,7 @@ class GuiProjectTreeMenu(QMenu):
|
||||
self.theTree.newTreeItem(nwItemType.FILE, None)
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _doMakeFolder(self):
|
||||
"""Forward the new folder call to the project tree.
|
||||
"""
|
||||
@@ -1191,6 +1226,7 @@ class GuiProjectTreeMenu(QMenu):
|
||||
self.theTree.newTreeItem(nwItemType.FOLDER, None)
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _doToggleExported(self):
|
||||
"""Flip the isExported flag of the current item.
|
||||
"""
|
||||
@@ -1199,6 +1235,7 @@ class GuiProjectTreeMenu(QMenu):
|
||||
self.theTree.setTreeItemValues(self.theItem.itemHandle)
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _doDeleteItem(self):
|
||||
"""Forward the delete item call to the project tree.
|
||||
"""
|
||||
@@ -1206,18 +1243,21 @@ class GuiProjectTreeMenu(QMenu):
|
||||
self.theTree.deleteItem()
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _doEmptyTrash(self):
|
||||
"""Forward the empty trash call to the project tree.
|
||||
"""
|
||||
self.theTree.emptyTrash()
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _doMoveUp(self):
|
||||
"""Forward the move item call to the project tree.
|
||||
"""
|
||||
self.theTree.moveTreeItem(-1)
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _doMoveDown(self):
|
||||
"""Forward the move item call to the project tree.
|
||||
"""
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user