Compare commits
454 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a63ac83d2 | ||
|
|
8dc28ddbee | ||
|
|
15803cf0a3 | ||
|
|
5ac001781c | ||
|
|
66986b88f8 | ||
|
|
f4e12a63a5 | ||
|
|
095c005c3f | ||
|
|
e414b6458d | ||
|
|
ece73784c2 | ||
|
|
8dd9644c13 | ||
|
|
556e8db104 | ||
|
|
d2f422a441 | ||
|
|
e4daff8956 | ||
|
|
2e1b17dcdf | ||
|
|
2ef11f56e5 | ||
|
|
fdaeca78a7 | ||
|
|
93e7343fe9 | ||
|
|
5167608f7a | ||
|
|
d4a0881e77 | ||
|
|
7f42210f2f | ||
|
|
33497bc92d | ||
|
|
06aa2e6c07 | ||
|
|
3bffaa01dc | ||
|
|
66237c35ea | ||
|
|
5936e1a68b | ||
|
|
17b3b2b770 | ||
|
|
826e805b06 | ||
|
|
5419d76606 | ||
|
|
0d84653f7d | ||
|
|
7140788b48 | ||
|
|
a43bd5a50e | ||
|
|
742f84576e | ||
|
|
ba81662d6a | ||
|
|
f669ac1265 | ||
|
|
b8a97b1afc | ||
|
|
4eb0ed386c | ||
|
|
ece225c355 | ||
|
|
69d97a3f3e | ||
|
|
dcecaae3af | ||
|
|
1c261ef371 | ||
|
|
ea89e66a46 | ||
|
|
dd03c19be2 | ||
|
|
08e6d320df | ||
|
|
446163c6de | ||
|
|
abd503b5a2 | ||
|
|
2e1c08cbfa | ||
|
|
7bdadadea1 | ||
|
|
bc33a4d0d4 | ||
|
|
188bc5d16f | ||
|
|
3fd0385160 | ||
|
|
5f3f55133e | ||
|
|
47b8e12bc7 | ||
|
|
cc97129e64 | ||
|
|
e40e282837 | ||
|
|
eba0fa58bf | ||
|
|
1c0fdd6a17 | ||
|
|
43eb62576c | ||
|
|
d43a799389 | ||
|
|
4d716555af | ||
|
|
7c65d22452 | ||
|
|
1b8867537e | ||
|
|
b91a11980b | ||
|
|
e80a9824f6 | ||
|
|
140b22dc63 | ||
|
|
60169b914a | ||
|
|
fbe381638a | ||
|
|
6dcb44c391 | ||
|
|
54c7c5a463 | ||
|
|
d5bcee14f5 | ||
|
|
0064b742d3 | ||
|
|
9f88f3b79e | ||
|
|
b95d030032 | ||
|
|
f4f3f82954 | ||
|
|
5e8892ee7c | ||
|
|
42b1e6a559 | ||
|
|
e1bcfcfc05 | ||
|
|
069e35565f | ||
|
|
880b560a39 | ||
|
|
8becc83c05 | ||
|
|
b5ef62194d | ||
|
|
a87c287fa6 | ||
|
|
18f5deee82 | ||
|
|
53545a0015 | ||
|
|
f662eaeccc | ||
|
|
1e6377abaa | ||
|
|
516db26c05 | ||
|
|
2df55cf6ee | ||
|
|
251475197d | ||
|
|
802069b4e5 | ||
|
|
d07543926f | ||
|
|
5665efd9d7 | ||
|
|
ea7f3fe51c | ||
|
|
c6569b63e5 | ||
|
|
930c60ae4a | ||
|
|
869cdd2fb0 | ||
|
|
f7d0adad1b | ||
|
|
d25e2adfb6 | ||
|
|
cb3a24bcd2 | ||
|
|
6540c56672 | ||
|
|
efc94a7fd5 | ||
|
|
683e81c57b | ||
|
|
f548927b74 | ||
|
|
53522908c8 | ||
|
|
43cdc8420c | ||
|
|
45ad0f8c20 | ||
|
|
59c9783393 | ||
|
|
611a6bb80a | ||
|
|
ee46fdb5b1 | ||
|
|
7c33c949cb | ||
|
|
e9e7fae40e | ||
|
|
ac05b07f10 | ||
|
|
aff027b735 | ||
|
|
14f1697428 | ||
|
|
a3381f1f79 | ||
|
|
1b7d331e41 | ||
|
|
df34293e74 | ||
|
|
fe27acc3ef | ||
|
|
35632abea3 | ||
|
|
ea247c5814 | ||
|
|
928a116666 | ||
|
|
2a99cafbf2 | ||
|
|
2196486454 | ||
|
|
f99bd79ce8 | ||
|
|
ddaced1b78 | ||
|
|
3e8845ab58 | ||
|
|
d5be1e0479 | ||
|
|
babc39c6a2 | ||
|
|
5c35ebeffa | ||
|
|
5fbc608c90 | ||
|
|
0c7808a24b | ||
|
|
4aadbdaed2 | ||
|
|
195a7e354b | ||
|
|
c03e832c7b | ||
|
|
73515c96d8 | ||
|
|
e7c0d59809 | ||
|
|
556caea308 | ||
|
|
6d54902a08 | ||
|
|
fb32cea5b5 | ||
|
|
69cd443999 | ||
|
|
cc61c5321e | ||
|
|
7821f099bf | ||
|
|
4b0ad6ec48 | ||
|
|
416c580a45 | ||
|
|
0b3488c265 | ||
|
|
f0f5ae6dc9 | ||
|
|
f61ce3919b | ||
|
|
67c6497e2a | ||
|
|
9bcb2479ac | ||
|
|
8a9b038d56 | ||
|
|
eed4721ef0 | ||
|
|
b76c866c9d | ||
|
|
255a4a327e | ||
|
|
811db81a86 | ||
|
|
29b61c07b0 | ||
|
|
f8c1ee90ce | ||
|
|
d9c86d56bb | ||
|
|
cddf63f111 | ||
|
|
ca91e6a677 | ||
|
|
a48fde95a3 | ||
|
|
aed4ba4310 | ||
|
|
57ee88059d | ||
|
|
c8dccbe438 | ||
|
|
8a071090e0 | ||
|
|
cbf76e6575 | ||
|
|
83d2cb4f82 | ||
|
|
497d4dd7f9 | ||
|
|
d48f1fa292 | ||
|
|
7744ae5912 | ||
|
|
92b7168363 | ||
|
|
8fc97c263b | ||
|
|
d190315909 | ||
|
|
987740f7e9 | ||
|
|
2e143071d9 | ||
|
|
388141aae6 | ||
|
|
80811ebf44 | ||
|
|
208c4fb8ba | ||
|
|
38a5fddb9d | ||
|
|
f844a5080a | ||
|
|
a11a013d16 | ||
|
|
f34cb567e4 | ||
|
|
6cb330b06c | ||
|
|
04cf375725 | ||
|
|
b66686a869 | ||
|
|
31776a51a7 | ||
|
|
f1b7257e73 | ||
|
|
be7078aa7e | ||
|
|
931aed6d78 | ||
|
|
dc6fe77612 | ||
|
|
b387150d4f | ||
|
|
9b09b13bf9 | ||
|
|
d8693c7afa | ||
|
|
019dd3e478 | ||
|
|
b01a2f5abd | ||
|
|
087a9cdd2e | ||
|
|
b33bf954bb | ||
|
|
f7cdc7da0c | ||
|
|
cf14f84652 | ||
|
|
6289840a74 | ||
|
|
770cccda49 | ||
|
|
aa02c0f15d | ||
|
|
ee137b6ffd | ||
|
|
206ca6040e | ||
|
|
16f5670c83 | ||
|
|
0fc1af7180 | ||
|
|
55714bbcad | ||
|
|
6f144c20cd | ||
|
|
f23bd3818f | ||
|
|
d3ea763d6b | ||
|
|
91222f631f | ||
|
|
aceb344856 | ||
|
|
f11ea8d07e | ||
|
|
a4e3bf848b | ||
|
|
e74c1057a4 | ||
|
|
6cc046c65c | ||
|
|
c97a070e98 | ||
|
|
b4c0bf3322 | ||
|
|
e998cb634e | ||
|
|
c8017a1f42 | ||
|
|
6d84942746 | ||
|
|
818f145b62 | ||
|
|
f92c22ce7e | ||
|
|
69b362b9d2 | ||
|
|
1fe7ca3db8 | ||
|
|
cc32674b0f | ||
|
|
cf16b57f66 | ||
|
|
411163f750 | ||
|
|
7f3da655c0 | ||
|
|
76e786d961 | ||
|
|
7e1f1a725c | ||
|
|
c63a0c2f58 | ||
|
|
04a7e092a9 | ||
|
|
309065bcbf | ||
|
|
20ae3febef | ||
|
|
e95d33031c | ||
|
|
3fd62d0b02 | ||
|
|
dc0ddcfa9f | ||
|
|
7a13b14c15 | ||
|
|
adf0550571 | ||
|
|
ba6259f08a | ||
|
|
d8bb2ad3ae | ||
|
|
afe4a29fd1 | ||
|
|
1203dd639b | ||
|
|
aadad6aa8c | ||
|
|
8ac0440bb3 | ||
|
|
d2fca3b980 | ||
|
|
d6b4019614 | ||
|
|
a6eb951361 | ||
|
|
dce5bdd31d | ||
|
|
0b999bd726 | ||
|
|
774437f7bb | ||
|
|
c0673b8128 | ||
|
|
0bc8a63ccf | ||
|
|
192f4507e3 | ||
|
|
2d22adcd2a | ||
|
|
4c4b92fdd4 | ||
|
|
2a727d1c39 | ||
|
|
35693f5433 | ||
|
|
bf459b4090 | ||
|
|
728f1ce1e3 | ||
|
|
7b8021de02 | ||
|
|
60593cde21 | ||
|
|
f9292d61da | ||
|
|
9388ce6a46 | ||
|
|
cb19475409 | ||
|
|
09b51badd4 | ||
|
|
fddd5fda47 | ||
|
|
5b0a6f05ed | ||
|
|
51f91a27e2 | ||
|
|
09018d1214 | ||
|
|
de12b22c97 | ||
|
|
d1ae7f5fe2 | ||
|
|
201d32f6ae | ||
|
|
6a441b1c6b | ||
|
|
0afa15f2db | ||
|
|
85eb44da02 | ||
|
|
4856c7a61b | ||
|
|
1b4cc933e7 | ||
|
|
d64acb038b | ||
|
|
b5def640ed | ||
|
|
7e35b4c675 | ||
|
|
e5e3066b3d | ||
|
|
e1af21bf88 | ||
|
|
8c9d08e46b | ||
|
|
e4acfb4831 | ||
|
|
a8101c2231 | ||
|
|
a3ce426613 | ||
|
|
161d02cd98 | ||
|
|
101400bea3 | ||
|
|
1ff9fea378 | ||
|
|
3f4538a60e | ||
|
|
142d7c4f3a | ||
|
|
2afe0ebbe9 | ||
|
|
bc71a62199 | ||
|
|
1e85ce4a84 | ||
|
|
39951a5099 | ||
|
|
9cbb6f3125 | ||
|
|
2dbd3e2534 | ||
|
|
f871a51e5b | ||
|
|
6ab3c68ebb | ||
|
|
7243ccd44c | ||
|
|
2a96fa1a2d | ||
|
|
c05620dddb | ||
|
|
917977ae13 | ||
|
|
2b8338f537 | ||
|
|
e75b5e012c | ||
|
|
78e194d8c1 | ||
|
|
c594e07c8c | ||
|
|
a3ed079828 | ||
|
|
976ce16ac2 | ||
|
|
5faeb12a74 | ||
|
|
acc463eb76 | ||
|
|
3b7459ffbc | ||
|
|
cfb19a697a | ||
|
|
ceec43db67 | ||
|
|
229fa39436 | ||
|
|
2496eb22cb | ||
|
|
d0a45bceca | ||
|
|
7180fae453 | ||
|
|
e1a1b5980e | ||
|
|
53d410a6ee | ||
|
|
a56b38694c | ||
|
|
600b60b441 | ||
|
|
aa7208b602 | ||
|
|
0092b3ece6 | ||
|
|
d01c1e867d | ||
|
|
dd3c8dfac8 | ||
|
|
7c9c5a9a76 | ||
|
|
8f9a55c5ca | ||
|
|
ea0b61aa6d | ||
|
|
b94e7159b8 | ||
|
|
2eabca8267 | ||
|
|
afbe434b59 | ||
|
|
2a50799873 | ||
|
|
77727ad500 | ||
|
|
e89f1fe5a8 | ||
|
|
503e7eceb5 | ||
|
|
c15eba8f62 | ||
|
|
e2f83d5106 | ||
|
|
937b5cca12 | ||
|
|
1d438d5d76 | ||
|
|
22a34be21a | ||
|
|
17a492444e | ||
|
|
9a714a903d | ||
|
|
69a99fc97f | ||
|
|
8beb74689c | ||
|
|
6be7da0305 | ||
|
|
8a08fe213c | ||
|
|
205b17aea0 | ||
|
|
864172ea50 | ||
|
|
e01e14075c | ||
|
|
e9596e1bbc | ||
|
|
db9305c230 | ||
|
|
4572d101cf | ||
|
|
41506911c1 | ||
|
|
b722224863 | ||
|
|
b585b8ecd1 | ||
|
|
52e8ae9cb6 | ||
|
|
6d6d0c2d12 | ||
|
|
383640cb7e | ||
|
|
e5ef27dfbf | ||
|
|
96741fc08b | ||
|
|
dc41f495be | ||
|
|
b278d64d0c | ||
|
|
b5e32844f5 | ||
|
|
4179d2e515 | ||
|
|
815ba9d553 | ||
|
|
0d294f49f9 | ||
|
|
a195cfc792 | ||
|
|
d3c9168ebf | ||
|
|
4d153b8d99 | ||
|
|
bf8d224064 | ||
|
|
b7ad2c969a | ||
|
|
d78643160c | ||
|
|
221b38c766 | ||
|
|
e8bd9b21b5 | ||
|
|
c2f0d04a35 | ||
|
|
54ebcd91cb | ||
|
|
07411327b6 | ||
|
|
094ad1a17b | ||
|
|
c362848b86 | ||
|
|
47a8dfbd93 | ||
|
|
73b8443d7b | ||
|
|
48a7929254 | ||
|
|
2c1abf1519 | ||
|
|
e27e066ccc | ||
|
|
621e933f63 | ||
|
|
885974a84e | ||
|
|
b1d1af2153 | ||
|
|
cb94be5b3c | ||
|
|
5a202e96fd | ||
|
|
b80edee225 | ||
|
|
48e653a546 | ||
|
|
a2d8aefb21 | ||
|
|
ae893be79b | ||
|
|
791080107f | ||
|
|
7d22ed1249 | ||
|
|
8150edbc43 | ||
|
|
5acbfeffa5 | ||
|
|
3b763e3460 | ||
|
|
6dd5f4295d | ||
|
|
ac7b0bc9a2 | ||
|
|
1733938eb4 | ||
|
|
e5c9ed4b80 | ||
|
|
f45170850c | ||
|
|
bdce0e12c7 | ||
|
|
57f9a0a844 | ||
|
|
988d969de7 | ||
|
|
17960d286f | ||
|
|
26ac68e98e | ||
|
|
0729dd798b | ||
|
|
c65dee1df9 | ||
|
|
92888338f4 | ||
|
|
44192c88f5 | ||
|
|
8d24243d4f | ||
|
|
7c4d4dec5c | ||
|
|
9823677983 | ||
|
|
ec6a5712e5 | ||
|
|
70033cb0f3 | ||
|
|
da102d3d5a | ||
|
|
5568b43b22 | ||
|
|
5da30e6608 | ||
|
|
0fb0af5b32 | ||
|
|
f5fcc57a06 | ||
|
|
4a1a3013f4 | ||
|
|
cf57a3ead0 | ||
|
|
39b2ff4a72 | ||
|
|
eced026aeb | ||
|
|
730d09a76c | ||
|
|
b521b39498 | ||
|
|
492bc044fc | ||
|
|
84ff2f8371 | ||
|
|
54f26ea06e | ||
|
|
e10f3a19d3 | ||
|
|
21a5dbbb52 | ||
|
|
f67d2c5a4c | ||
|
|
6cf83bb381 | ||
|
|
f4decae77f | ||
|
|
e47f0c5166 | ||
|
|
778b65b48c | ||
|
|
d1dbb0b378 | ||
|
|
adfd003532 | ||
|
|
f17302439e | ||
|
|
63d2f5c57d | ||
|
|
478db63b30 | ||
|
|
127588699e | ||
|
|
29c10542b1 | ||
|
|
5c9fd471a7 | ||
|
|
dd15c93f51 | ||
|
|
4003ececb2 | ||
|
|
ff3cae9db4 | ||
|
|
af44646a77 | ||
|
|
ba42285172 | ||
|
|
dff2d5996b | ||
|
|
08b17daec9 |
5
.flake8
5
.flake8
@@ -1,5 +0,0 @@
|
||||
[flake8]
|
||||
ignore = E133,E221,E226,E228,E241,W503,ANN101,ANN102,ANN401
|
||||
per-file-ignores=tests/*:ANN
|
||||
max-line-length = 99
|
||||
exclude = docs/*
|
||||
99
.github/workflows/build.yml
vendored
99
.github/workflows/build.yml
vendored
@@ -1,99 +0,0 @@
|
||||
name: Build
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
buildAssets:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Python Setup
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
architecture: x64
|
||||
|
||||
- name: Install Packages (apt)
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install qttools5-dev-tools latexmk texlive texlive-latex-extra
|
||||
|
||||
- name: Checkout Source
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Packages (pip)
|
||||
run: pip install -r docs/source/requirements.txt
|
||||
|
||||
- name: Build Assets
|
||||
run: python pkgutils.py qtlrelease sample manual
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nw-assets
|
||||
path: |
|
||||
novelwriter/assets/sample.zip
|
||||
novelwriter/assets/manual.pdf
|
||||
novelwriter/assets/i18n/*.qm
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
buildLinux:
|
||||
needs: buildAssets
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.11"]
|
||||
linux-tag: ["manylinux_2_24_x86_64", "manylinux_2_28_x86_64"]
|
||||
steps:
|
||||
- name: Python Setup
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
architecture: x64
|
||||
|
||||
- name: Install Packages (pip)
|
||||
run: pip install python-appimage
|
||||
|
||||
- name: Checkout Source
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: nw-assets
|
||||
path: novelwriter/assets
|
||||
|
||||
- name: Build AppImage
|
||||
run: python pkgutils.py build-appimage --linux-tag ${{ matrix.linux-tag }} --python-version ${{ matrix.python-version }}
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: AppImage-${{ matrix.python-version }}-${{ matrix.linux-tag }}
|
||||
path: dist_appimage
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
buildMac:
|
||||
needs: buildAssets
|
||||
runs-on: macos-12
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: nw-assets
|
||||
path: novelwriter/assets
|
||||
|
||||
- name: Build App Bundle
|
||||
run: ./setup/macos/build.sh
|
||||
|
||||
- name: Upload DMG
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: MacOS-DMG
|
||||
path: dist_macos
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
39
.github/workflows/build_assets.yml
vendored
Normal file
39
.github/workflows/build_assets.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: BuildAssets
|
||||
|
||||
on: workflow_call
|
||||
|
||||
jobs:
|
||||
buildAssets:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Python Setup
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
architecture: x64
|
||||
|
||||
- name: Install Packages (apt)
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install qttools5-dev-tools latexmk texlive texlive-latex-extra
|
||||
|
||||
- name: Checkout Source
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Packages (pip)
|
||||
run: pip install -r docs/source/requirements.txt
|
||||
|
||||
- name: Build Assets
|
||||
run: |
|
||||
python pkgutils.py build-assets
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nw-assets
|
||||
path: |
|
||||
novelwriter/assets/manual.pdf
|
||||
novelwriter/assets/sample.zip
|
||||
novelwriter/assets/i18n/*.qm
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
43
.github/workflows/build_linux.yml
vendored
Normal file
43
.github/workflows/build_linux.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: BuildLinux
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
buildAssets:
|
||||
uses: ./.github/workflows/build_assets.yml
|
||||
|
||||
buildLinux-AppImage:
|
||||
needs: buildAssets
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PYTHON_VERSION: "3.12"
|
||||
LINUX_TAG: "manylinux_2_28_x86_64"
|
||||
steps:
|
||||
- name: Python Setup
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
architecture: x64
|
||||
|
||||
- name: Install Packages (pip)
|
||||
run: pip install python-appimage
|
||||
|
||||
- name: Checkout Source
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: nw-assets
|
||||
path: novelwriter/assets
|
||||
|
||||
- name: Build AppImage
|
||||
run: python pkgutils.py build-appimage --linux-tag $LINUX_TAG --python-version $PYTHON_VERSION
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Linux-AppImage
|
||||
path: dist_appimage
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
64
.github/workflows/build_mac.yml
vendored
Normal file
64
.github/workflows/build_mac.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
name: BuildMacOS
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
buildAssets:
|
||||
uses: ./.github/workflows/build_assets.yml
|
||||
|
||||
buildMac-AMD64:
|
||||
needs: buildAssets
|
||||
# Stay on macos-12 due to https://github.com/create-dmg/create-dmg/issues/143
|
||||
runs-on: macos-12
|
||||
env:
|
||||
PYTHON_VERSION: "3.12"
|
||||
PACKAGE_ARCH: x86_64
|
||||
MINICONDA_ARCH: x86_64
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: nw-assets
|
||||
path: novelwriter/assets
|
||||
|
||||
- name: Build App Bundle
|
||||
run: ./setup/macos/build.sh $PYTHON_VERSION $PACKAGE_ARCH $MINICONDA_ARCH
|
||||
|
||||
- name: Upload DMG
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: MacOS-AMD64-DMG
|
||||
path: dist_macos
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
buildMac-M1:
|
||||
needs: buildAssets
|
||||
runs-on: macos-14
|
||||
env:
|
||||
PYTHON_VERSION: "3.12"
|
||||
PACKAGE_ARCH: aarch64
|
||||
MINICONDA_ARCH: arm64
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: nw-assets
|
||||
path: novelwriter/assets
|
||||
|
||||
- name: Build App Bundle
|
||||
run: ./setup/macos/build.sh $PYTHON_VERSION $PACKAGE_ARCH $MINICONDA_ARCH
|
||||
|
||||
- name: Upload DMG
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: MacOS-M1-DMG
|
||||
path: dist_macos
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
57
.github/workflows/build_win.yml
vendored
Normal file
57
.github/workflows/build_win.yml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
name: BuildWindows
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
buildAssets:
|
||||
uses: ./.github/workflows/build_assets.yml
|
||||
|
||||
buildWin64:
|
||||
needs: buildAssets
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Python Setup
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
architecture: x64
|
||||
|
||||
- name: Checkout Source
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: nw-assets
|
||||
path: novelwriter/assets
|
||||
|
||||
- name: Build Setup Installer
|
||||
run: python pkgutils.py build-win-exe
|
||||
|
||||
- name: Upload Unsigned Artifacts
|
||||
id: upload-unsigned-artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Win-Setup
|
||||
path: dist/*.exe
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
- name: Submit Signing Request
|
||||
uses: signpath/github-action-submit-signing-request@v1
|
||||
with:
|
||||
api-token: "${{ secrets.SIGNPATH_API_TOKEN }}"
|
||||
organization-id: "0471e52c-66fa-4e9a-bfb9-36167095ca3f"
|
||||
project-slug: "novelWriter"
|
||||
signing-policy-slug: "release-signing"
|
||||
github-artifact-id: "${{ steps.upload-unsigned-artifact.outputs.artifact-id }}"
|
||||
wait-for-completion: true
|
||||
output-artifact-directory: "dist/"
|
||||
|
||||
- name: Upload Signed Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Win-Setup
|
||||
path: dist/*.exe
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
9
.github/workflows/test_linux.yml
vendored
9
.github/workflows/test_linux.yml
vendored
@@ -14,7 +14,8 @@ jobs:
|
||||
testLinux:
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
||||
fail-fast: false
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Python Setup
|
||||
@@ -32,11 +33,13 @@ jobs:
|
||||
run: |
|
||||
pip install -U -r requirements.txt -r tests/requirements.txt
|
||||
- name: Run Build Commands
|
||||
run: python pkgutils.py qtlrelease sample
|
||||
run: |
|
||||
python pkgutils.py qtlrelease
|
||||
python pkgutils.py sample
|
||||
- name: Run Tests
|
||||
run: |
|
||||
export QT_QPA_PLATFORM=offscreen
|
||||
pytest -v --cov=novelwriter --timeout=60
|
||||
python -m pytest -v --cov=novelwriter --timeout=60
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
env:
|
||||
|
||||
4
.github/workflows/test_mac.yml
vendored
4
.github/workflows/test_mac.yml
vendored
@@ -12,7 +12,7 @@ on:
|
||||
|
||||
jobs:
|
||||
testMac:
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-13
|
||||
steps:
|
||||
- name: Python Setup
|
||||
uses: actions/setup-python@v5
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
- name: Run Tests
|
||||
run: |
|
||||
export QT_QPA_PLATFORM=offscreen
|
||||
pytest -v --cov=novelwriter --timeout=60
|
||||
python -m pytest -v --cov=novelwriter --timeout=60
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
env:
|
||||
|
||||
2
.github/workflows/test_win.yml
vendored
2
.github/workflows/test_win.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
pip install -U -r requirements.txt -r tests/requirements.txt
|
||||
- name: Run Tests
|
||||
run: |
|
||||
pytest -v --cov=novelwriter --timeout=60
|
||||
python -m pytest -v --cov=novelwriter --timeout=60
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
env:
|
||||
|
||||
391
CHANGELOG.md
391
CHANGELOG.md
@@ -1,5 +1,396 @@
|
||||
# novelWriter Changelog
|
||||
|
||||
## Version 2.5.2 [2024-09-17]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This is a patch release that fixes a series of issues mostly affecting dialogue highlighting in the
|
||||
document viewer. The way the text is formatted in the viewer changed a lot in 2.5, and there were a
|
||||
few issues with the new implementation. The tab stop distance setting was also ignored, but has now
|
||||
been fixed as well.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* Fixed an issue where the tab stop distance setting was being ignored in the document viewer. It
|
||||
must be set again every time a document is loaded. Issue #1996. PR #2014.
|
||||
* Fixed an issue with overlapping formatting markers when processing text into new formats. This
|
||||
issues was introduced when dialogue highlighting was added to the document viewer and manuscript
|
||||
build. It arises because start and end of dialogue can occur on the same character as other
|
||||
formatting, and then the order these markers are processed matters. This was not properly handled
|
||||
in the code. Issue #2012. PR #2014.
|
||||
* Dialogue highlighting is now only applied to plain text paragraphs, not comments, headings, and
|
||||
other places where there should be no dialogue. Issue #2013. PR #2014.
|
||||
* Note documents no longer get dialogue highlighting. Issue #2011. PR #2014.
|
||||
|
||||
**Documentation**
|
||||
|
||||
* Added a section about spell checking in the documentation. PR #2015.
|
||||
|
||||
----
|
||||
|
||||
## Version 2.5.1 [2024-07-28]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This is a patch release that fixes an issue with the lock file being left in the project when the
|
||||
project open is cancelled, a few issues with custom input boxes in Preferences, and a usability
|
||||
issue when selecting to edit a tag or reference from the Tags and References panel as well as fixed
|
||||
an issue with the Importance labels not being updated in the tabs for tags.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* Fixes an issue where the project lock file was created before certain checks where the user could
|
||||
cancel the project open process, leaving a lock file in the project folder behind. Issue #1977.
|
||||
PR #1978.
|
||||
* Fixes an issue where the user could add spaces to the pad before and after Preference settings,
|
||||
which would pad every space entered into the text. The Preferences settings no longer accept any
|
||||
white spaces in these boxes (they are removed on save) and also strips duplicate character
|
||||
entries. Issue #1985. PR #1986.
|
||||
* The narrator break and dialog symbol Preference text boxes will now be cleared if the settings is
|
||||
a white space. PR #1986.
|
||||
* Fixed an issue where Importance labels were not updated in the viewer panel's tags tabs when they
|
||||
where changed in Project Settings. Issue #1992. PR #1993.
|
||||
|
||||
**Usability**
|
||||
|
||||
* When selecting to edit a tag or reference from the panel below the Document Viewer, the editor
|
||||
now scrolls to the relevant heading as it does in the viewer. Issue #1983. PR #1987.
|
||||
|
||||
**Internationalisation**
|
||||
|
||||
* Latin American Spanish translation updated by Tommy Marplatt (@tmarplatt). PR #1989.
|
||||
|
||||
----
|
||||
|
||||
## Version 2.5 [2024-07-10]
|
||||
|
||||
### Release Notes
|
||||
|
||||
The 2.5 release fixes a number of minor issues from the RC 1 pre-release, and updates most
|
||||
translation files. Polish has also been added.
|
||||
|
||||
See the website for complete [Release Notes](https://novelwriter.io/releases/release_2_5.html).
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* Fix an issue with unresponsiveness when switching from Outline View to Tree View with a keyboard
|
||||
shortcut. Issue #1949. PR #1954.
|
||||
* Fix an issue with switching focus between editor and viewer when the viewer is hidden.
|
||||
Issue #1950. PR #1954.
|
||||
* Fix an issue with first line indentation where it would sometimes indent first paragraph when it
|
||||
should not. Issue #1965. PR #1971.
|
||||
|
||||
**Usability**
|
||||
|
||||
* Remove the "Apply" button from Preferences as changing theme does not updated the Preferences
|
||||
dialog, which is unexpected behaviour. The Preferences dialog is now also consistent with other
|
||||
settings dialogs. Issue #1951. PR #1954.
|
||||
* Improved the error messages when opening a project fails. Issue #1944. PR #1956.
|
||||
* Make sure the Project Name is selected and in focus when switching to the New Project Form on the
|
||||
Welcome dialog. Issue #1967. PR #1968.
|
||||
|
||||
**Improvements**
|
||||
|
||||
* Add first line indentation to Manuscript previews. Issue #1961. PR #1962.
|
||||
|
||||
**Internationalisation**
|
||||
|
||||
* Norwegian and US English translations updated by @vkbo. PR #1955.
|
||||
* German translation updated by Myian (@HeyMyian). PR #1955.
|
||||
* Italian translation updated by Lou Cyper (loucyper1). PR #1955.
|
||||
* Dutch translation updated by Martijn van der Kleijn (mvdkleijn). PR #1955.
|
||||
* Chinese translation updated by @longqzh. PR #1955.
|
||||
* Portuguese translation updated by Oli Maia. PR #1955.
|
||||
* Japanese translation updated by @hebekeg. PR #1955.
|
||||
* French translation updated by Albert Aribaud (@aaribaud). PR #1955.
|
||||
* Polish translation added by Anna Maria Polak (@Nauthiz). PR #1955.
|
||||
|
||||
**Packaging**
|
||||
|
||||
* Rewrote the packaging utilities script. Windows builds are now created in GitHub actions, which
|
||||
is needed for signed packages when this is enabled. PRs #1958 and #1959.
|
||||
|
||||
**Documentation**
|
||||
|
||||
* Updated the documentation for 2.5. PR #1928.
|
||||
|
||||
----
|
||||
|
||||
## Version 2.5 RC 1 [2024-06-22]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This is a release candidate of the next release version, and is intended for testing purposes.
|
||||
Please be careful when using this version on live writing projects, and make sure you take frequent
|
||||
backups.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* The Status bar LEDs are now properly updated when the theme changes. Issue #1893. PR #1906.
|
||||
* All HTML tags are now properly closed at the end of each paragraph in HTML output. After
|
||||
shortcodes were introduced, it was possible to leave formatting tags open. Issue #1919. PR #1926.
|
||||
|
||||
**Improvements**
|
||||
|
||||
* Move the first line indent setting from being an Open Document feature to a general build
|
||||
settings feature that also applies to HTML, and make it visible in the Manuscript build tool
|
||||
preview. Issue #1839 and #1858. PR #1898.
|
||||
* Make sure the document in the editor is saved before the same document is opened in the viewer.
|
||||
Issue #1884. PR #1902.
|
||||
* The project name now appears before "novelWriter" in the main window title, which improves the
|
||||
task bar label on at least Linux Mint Cinnamon, and Windows. Issue #1910. PR #1911.
|
||||
* Dialogue highlighting now only applies to novel documents, not notes. It is also possible to
|
||||
apply it to HTML and ODT manuscripts, and it also shows up in the preview and in the document
|
||||
viewer. Issue #1774. PR #1908.
|
||||
* The Welcome dialog and other tools that use the project name to generate files of folder names
|
||||
are now less restrictive on what characters it allows in the file or folder names. Issue #1917.
|
||||
PR #1922.
|
||||
* Last used folder paths are now remembered individually for each tool or feature that requires
|
||||
path input from the user. Issues #1930 and #1933. PR #1934.
|
||||
* The Manuscript preview will now show line height as set in build settings. Issue #1920. PR #1935.
|
||||
* Global search now refreshes if any of the search option buttons are toggled, and the search
|
||||
result will show the complete word if the search term only matches part of a word. Issue #1830.
|
||||
PR #1936.
|
||||
* When a new note is created from a reference tag in the editor, the syntax highlighting is
|
||||
properly updated to indicate the tag is now valid. Issue #1916. PR #1938.
|
||||
* The `Ctrl+E` shortcut now toggles focus between editor and viewer instead of just going to the
|
||||
editor. The header text colour changes to indicate which panel has focus. This should make it
|
||||
easier to scroll the content of the viewer without having to click it with the mouse first.
|
||||
Issue #1387. PRs #1940 and #1941.
|
||||
|
||||
**Code Improvements**
|
||||
|
||||
* Change how dialogs are handled in memory, and drop the calls to deleteLater for the underlying Qt
|
||||
object as it caused problems in some cases. Instead, the dialog is disconnected from the parent
|
||||
object, which seems to let the Python and Qt garbage collectors to kick in.
|
||||
PRs #1899. #1913 and #1921.
|
||||
* Overload the reject call for dialogs rather to call close, which the default implementation does
|
||||
not. This simplifies the logic when closing dialogs, as reject() is also a slot, which close() is
|
||||
not. Issue #1915. PR #1918.
|
||||
* Processing of dialogue highlighting has been added to the Tokenizer class, and the RegEx handling
|
||||
moved to a separate factory class. PR #1908.
|
||||
* The progress bar widgets have been moved to a single module, and test coverage added. PR #1937.
|
||||
|
||||
**Packaging**
|
||||
|
||||
* The Windows installer is now built with Inno Setup 6.3, and uses zip compression rather than
|
||||
lzma. It also properly sets the undelete icon, and the undelete process is better at cleaning up
|
||||
files. PR #1932.
|
||||
|
||||
----
|
||||
|
||||
## Version 2.5 Beta 1 [2024-05-26]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This is a beta release of the next release version, and is intended for testing purposes. Please be
|
||||
careful when using this version on live writing projects, and make sure you take frequent backups.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**General Changes**
|
||||
|
||||
* novelWriter now requires Python 3.9 and Qt 5.15 as a minimum. That means Ubuntu 20.04 and older
|
||||
are no longer supported for the Debian packages. Users on Ubuntu 20.04 and equivalent Linux
|
||||
distros must use the AppImage. PRs #1826 and #1833.
|
||||
|
||||
**Major Features**
|
||||
|
||||
* Status and Importance icons can now be assigned a shape in addition to a colour. Issue #1786.
|
||||
PRs #1810 and #1819.
|
||||
* Footnotes have been added. They use a modified comment format for specifying the footnote text,
|
||||
and a shortcode for inserting the footnote into the text. They are linked via a key. If the
|
||||
footnote is inserted via the **Insert** menu, a unique key is generated. Issue #342. PR #1832.
|
||||
* Comments in general now support formatting, so format tags work just like in regular text. This
|
||||
was needed for footnotes, but has been expanded to include all comments. PR #1832.
|
||||
* Two new themes have been added. Dracula is available as a set for both the GUI and the editor.
|
||||
Snazzy Light is available for syntax only, but works well with the default light GUI theme.
|
||||
PR #1840.
|
||||
* Character dialogue highlighting has been completely redesigned. The highlighting rule now
|
||||
specifically relates to dialogue, and not just quoted text in general. To that effect, more rules
|
||||
for detecting what is and isn't dialogue has been added. This is designed to support difference
|
||||
between dialogue conventions in different languages. Issues #1770 and #1771. PR #1864.
|
||||
* The font settings in Preferences now considers the full range of font options, in particular font
|
||||
style and weight. The changes include the font size in these settings, so the additional setting
|
||||
for font size has been removed. Use the font dialog to pick size as well. Due to this removal,
|
||||
the font size has been reset to default and must be set again by the user. Issue #1733. PR #1881.
|
||||
* The document viewer and the manuscript preview now uses a completely new class to generate the
|
||||
formatted text. The old method used an outdated HTML format as the go-between, which restricted
|
||||
what formatting could be applied. The main noticeable change to users at the moment should be the
|
||||
size of headings, that now match the size in the editor. Issue #1882. PR #1892.
|
||||
|
||||
**Improvements**
|
||||
|
||||
* The default dark theme has been redesigned to match the GUI theme a little better, and is now
|
||||
also consistent with the default light themes. PRs #1836 and #1840.
|
||||
* Special comments now have a different text colour than plain comments. PR #1836.
|
||||
* The various Tomorrow themes are now a little more consistent in what colours are used for which
|
||||
components. More variation has also been added. PR #1840.
|
||||
* The Tango theme has been updated. PR #1840.
|
||||
* The last cursor position is now always saved when a document is closed in the editor, even if the
|
||||
content has not been changed. PR #1849.
|
||||
* The settings for first line indent have now been extended to allow setting how deep the indents
|
||||
are and whether or not to indent the first paragraph after a heading or break. The settings have
|
||||
also moved from being Open Document specific to the general formatting tab for manuscripts.
|
||||
PR #1857.
|
||||
* Preserving in-paragraph line breaks is now a general Manuscript setting that applies to all
|
||||
output formats. PR #1891.
|
||||
* The Manuscript builds now also uses the full font settings of the selected output font, not just
|
||||
the font family and size option. Since the size option has been merged into the general font
|
||||
choice, the separate font size setting has been dropped and users must set it again. PR #1891.
|
||||
* It is now possible to disable the usage of the font selection dialog from your operating system.
|
||||
At least on Linux Gnome desktops, the system's font dialog lacks a lot of the selection options
|
||||
available in the font dialog provided with Qt. PR #1891.
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* Fixed an issue where Manuscript dialogs would be left open if the project was closed. PR #1844.
|
||||
* First line indents are no longer reset after a comment. Issue #1852. PR #1857.
|
||||
|
||||
**Packaging**
|
||||
|
||||
* Minimal zip file release packages can no longer be built from the package utility script. The zip
|
||||
file needed for building the Windows installer is still available. PR #1826.
|
||||
* Added build packages for AARCH64 on MacOS. It seems some dependencies still prevents it from
|
||||
being installed cleanly without requiring some virtualisation feature, but it works. PR #1829.
|
||||
* AppImages are now released with Python 3.12. PR #1829.
|
||||
|
||||
**Other Changes**
|
||||
|
||||
* A new GUI theme setting for error text has been added. It is used when error messages need to be
|
||||
displayed on the GUI. PR #1880.
|
||||
|
||||
**Code Improvements**
|
||||
|
||||
* The behaviour of dialogs across various platforms has been unified and improved a little to
|
||||
ensure more consistent behaviour. It is mostly related to modality and z-order. There are still
|
||||
limitations due to platform restrictions, like non-modal dialogs being stuck on top on Linux
|
||||
Wayland. PR #1844.
|
||||
* Some GUI components have been improved and refactored to rely more on signals and slots rather
|
||||
than reaching into each other's classes and calling methods. This is a little more robust.
|
||||
PR #1849.
|
||||
* The text tokenizer used to parse the formatting codes in novelWriter has been refactored and
|
||||
improved. In particular, multi-line paragraphs are now processed during the second pass of
|
||||
tokenization rather than when the final format is applied. This allows a much more compact format
|
||||
of the token data, and avoid potential inconsistencies between the various output formats.
|
||||
PR #1885.
|
||||
|
||||
----
|
||||
|
||||
## Version 2.4.4 [2024-06-12]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This is a patch release that fixes a usage issue when setting document label from an unsaved
|
||||
document, fixes a rare chance of a crash when closing the Manuscript tool, and updates the
|
||||
Portuguese translation.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* Backported some memory improvements from the upcoming 2.5 release. This also fixes a potential,
|
||||
but low probability crash in the Manuscript tool. If the tool is closed during the milliseconds
|
||||
long window when the clock on the yellow label is updated, the app can crash with a segmentation
|
||||
fault. PRs #1903 and #1905.
|
||||
* Fixed an issue where changing a document's label to match the document heading would suggest the
|
||||
wrong title if the document was open in the editor, but in an unsaved state. An easy condition to
|
||||
encounter when creating a new document and setting the heading first. Issue #1914. PR #1923.
|
||||
|
||||
**Internationalisation**
|
||||
|
||||
* Portuguese translation updated by Oli Maia. PR #1904.
|
||||
|
||||
----
|
||||
|
||||
## Version 2.4.3 [2024-05-20]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This is a patch release that fixes issues with the document font in the editor, viewer and
|
||||
manuscript preview on some Linux distros, and also fixes a potential crash on Windows when using
|
||||
the spell check dictionary install tool.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* Fixed a crash in the dictionaries install tool on Windows if the config folder reported by the
|
||||
third party Enchant spell checker tool didn't already exist prior to adding new dictionaries.
|
||||
The folder is now created when the tool is opened if it doesn't exist. Issue #1874. PR #1876.
|
||||
* Fixed issues when setting a different text font for the editor and viewer, and related issues
|
||||
with the preview in the Manuscript Build tool, on certain platforms. Changing the font and
|
||||
setting non-standard font sizes produced unexpected results when reloading. The issue seems to be
|
||||
related to Qt 5.15.3, but that is not fully confirmed. However, the only place so far where the
|
||||
issue is observed is on Mint 21.3. Issues #1862 and #1875. PR #1877.
|
||||
|
||||
----
|
||||
|
||||
## Version 2.4.2 [2024-05-18]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This is a patch release that fixes two minor font issues and updates the Dutch and Chinese
|
||||
translations.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* Change the preview widget in the Manuscript Build tool to use the document font only for the
|
||||
document itself, not the yellow heading stating the age of the previewed text. PR #1863.
|
||||
* Fixes the syntax highlighter so that it is re-initialised when the document editor is
|
||||
re-initialised, so that changes to document font size regenerates the heading font sizes as well.
|
||||
Issue #1865. PR #1866.
|
||||
|
||||
**Internationalisation**
|
||||
|
||||
* Dutch translation updated by Martijn van der Kleijn (@mvdkleijn). PR #1872.
|
||||
* Chinese translation updated by @longqzh. PR #1872.
|
||||
|
||||
----
|
||||
|
||||
## Version 2.4.1 [2024-05-06]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This is a patch release that fixes a number of minor issues with the Manuscript Build tool. The
|
||||
only critical fix is related to a potential crash when deleting a build entry when its settings
|
||||
dialog is still open.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* Fixed an issue that could crash novelWriter if a build settings entry was deleted from the
|
||||
Manuscript tool while its settings window was open. PR #1845.
|
||||
|
||||
**Improvements**
|
||||
|
||||
* Changed the font used for the document editor and viewer header and footer from the one used for
|
||||
the document text to the one used for the user interface. Issue #1842. PR #1843.
|
||||
* Added a save document step in the editor before running a manuscript build or a preview job. This
|
||||
ensures that the text in the editor is included in the manuscript. Issue #1835. PR #1846.
|
||||
* Restore preview panel scroll bar position after updating the preview on the Manuscript tool.
|
||||
Issue #1837. PR #1846.
|
||||
* Close all non-modal dialogs when a project is closed. PR #1848.
|
||||
|
||||
**Code Improvements**
|
||||
|
||||
* Fixed an issue with the Build Settings dialog that blocks the garbage collector from deleting the
|
||||
dialog after it's been closed. PR #1843.
|
||||
* Remove some deprecated translation entries from the translation files. PR #1850.
|
||||
|
||||
----
|
||||
|
||||
## Version 2.4 [2024-04-20]
|
||||
|
||||
### Release Notes
|
||||
|
||||
59
CREDITS.md
59
CREDITS.md
@@ -2,69 +2,74 @@
|
||||
|
||||
## Main Developer
|
||||
|
||||
* Veronica Berglyd Olsen (@vkbo)
|
||||
* Veronica Berglyd Olsen
|
||||
|
||||
## Contributors
|
||||
|
||||
* Concept: Marian Lückhof (@Number042)
|
||||
* Internationalisation: Bruno Meneguello (@bkmeneguello)
|
||||
* Setup and Packaging: Rachel Powers (@Ryex)
|
||||
* Early Concept: Marian Lückhof
|
||||
* Internationalisation: Bruno Meneguello
|
||||
* Setup and Packaging: Rachel Powers
|
||||
|
||||
For other contributions, see the project's [Contributors](https://github.com/vkbo/novelWriter/graphs/contributors) page.
|
||||
|
||||
## Artwork
|
||||
|
||||
The artwork on the Welcome dialog was created by [Louis Durrant](https://louisdurrant.art).
|
||||
The artwork on the Welcome dialog was created by Louis Durrant.
|
||||
|
||||
## Translations
|
||||
|
||||
The default language is English (UK) with English (US) as an option. These are the original
|
||||
translators for the languages currently available:
|
||||
|
||||
* Dutch: Martijn van der Kleijn (@mvdkleijn)
|
||||
* French: Jan Lüdke (@jyhelle)
|
||||
* German: Myian (@heymyian)
|
||||
* Italian: Riccardo Mangili
|
||||
* Japanese: hebekeg (@hebekeg)
|
||||
* Latin American Spanish: Tommy Marplatt (@tmarplatt)
|
||||
* Norwegian: Veronica Berglyd Olsen (@vkbo)
|
||||
* Portuguese: Bruno Meneguello (@bkmeneguello)
|
||||
* Simplified Chinese: Qianzhi Long (@longqzh)
|
||||
* **Dutch:** Martijn van der Kleijn (mvdkleijn)
|
||||
* **French:** Jan Lüdke (jyhelle)
|
||||
* **German:** Myian (HeyMyian)
|
||||
* **Italian:** Riccardo Mangili
|
||||
* **Japanese:** hebekeg
|
||||
* **Latin American Spanish:** Tommy Marplatt (tmarplatt)
|
||||
* **Norwegian:** Veronica Berglyd Olsen (vkbo)
|
||||
* **Polish:** Anna Maria Polak (Nauthiz)
|
||||
* **Portuguese:** Bruno Meneguello (bkmeneguello)
|
||||
* **Simplified Chinese:** Qianzhi Long (longqzh)
|
||||
|
||||
Additional larger translation contributions:
|
||||
|
||||
* French: Albert Aribaud (@aaribaud)
|
||||
* **French:** Albert Aribaud (aaribaud)
|
||||
* **Portuguese:** Oli Maia (olimaia)
|
||||
|
||||
Translations are managed on [Crowdin](https://crowdin.com/project/novelwriter), and more
|
||||
contributions are listed on the project's [Members](https://crowdin.com/project/novelwriter/members) page.
|
||||
contributions are listed on the project's Members page.
|
||||
|
||||
## Libraries
|
||||
|
||||
The following libraries are dependencies of novelWriter:
|
||||
|
||||
* [Qt5](https://www.qt.io) by Qt Company
|
||||
* [PyQt5](https://www.riverbankcomputing.com/software/pyqt) by Riverbank Computing
|
||||
* [Enchant](https://abiword.github.io/enchant) by Dom Lachowicz
|
||||
* [PyEnchant](https://pyenchant.github.io/pyenchant) by Dimitri Merejkowsky
|
||||
* **Qt5** by Qt Company
|
||||
* **PyQt5** by Riverbank Computing
|
||||
* **Enchant** by Dom Lachowicz
|
||||
* **PyEnchant** by Dimitri Merejkowsky
|
||||
|
||||
## Assets
|
||||
|
||||
Some of the assets bundled with novelWriter were adapted from the following sources:
|
||||
|
||||
* [Typicons](https://github.com/stephenhutchings/typicons.font) icons by Stephen Hutchings (CC BY-SA 4.0)
|
||||
* [Tomorrow](https://github.com/chriskempson/base16) syntax themes by Chris Kempson (MIT License)
|
||||
* [Owl](https://github.com/sdras/night-owl-vscode-theme) syntax themes by Sarah Drasner (MIT License)
|
||||
* [Solarized](https://github.com/altercation/solarized) themes by Ethan Schoonover (MIT License)
|
||||
* **Typicons** icons by Stephen Hutchings (CC BY-SA 4.0)
|
||||
* **Tomorrow** syntax themes by Chris Kempson (MIT License)
|
||||
* **Owl** syntax themes by Sarah Drasner (MIT License)
|
||||
* **Solarized** themes by Ethan Schoonover (MIT License)
|
||||
* **Cyberpunk Night** theme by Anders Lemvigh (CC BY-SA 4.0)
|
||||
* **Dracula** theme by Zeno Rocha (MIT License)
|
||||
* **Snazzy Light** theme by Florian Reuschel (MIT License)
|
||||
|
||||
## Fonts
|
||||
|
||||
The font used for the main novelWriter logo, mimetype and text banners is Pridi. Other fonts are
|
||||
used on buttons and icons.
|
||||
|
||||
* Pridi by Cadson Demak (Open Font License, Version 1.1)
|
||||
* Source Sans Pro by Paul D. Hunt (SIL Open Font License)
|
||||
* **Pridi** by Cadson Demak (Open Font License, Version 1.1)
|
||||
* **Source Sans Pro** by Paul D. Hunt (SIL Open Font License)
|
||||
|
||||
## Special Mentions
|
||||
|
||||
Additional thanks to @johnblommers who was an early user and who has provided a lot of very useful
|
||||
Additional thanks to John Blommers who was an early user and who has provided a lot of very useful
|
||||
feedback over the years.
|
||||
|
||||
@@ -31,9 +31,9 @@ documentation.
|
||||
|
||||
## Implementation
|
||||
|
||||
novelWriter is written with Python 3 (3.8+) using Qt5 and PyQt5 (5.10+), and is released on Linux,
|
||||
Windows and macOS. It can in principle run on any Operating System that also supports Qt, PyQt and
|
||||
Python.
|
||||
novelWriter is written with Python 3 (3.9+) using Qt5 and PyQt5 (5.15 only), and is released on
|
||||
Linux, Windows and macOS. It can in principle run on any Operating System that also supports Qt,
|
||||
PyQt and Python.
|
||||
|
||||
## Project Contributions
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 185 KiB |
@@ -16,13 +16,21 @@ This is a list of hopefully helpful little tips on how to get the most out of no
|
||||
Managing the Project
|
||||
====================
|
||||
|
||||
.. dropdown:: Create a Project from a Template
|
||||
:animate: fade-in-slide-down
|
||||
|
||||
On the Welcome dialog's **Create New Project** form, you can select to "Prefill Project" from
|
||||
the content of a different project. This feature is most useful if you copy a project you have
|
||||
dedicated to be a template project. If you have a structure and settings you want to use for
|
||||
every new project, this is the best solution.
|
||||
|
||||
.. dropdown:: Merge Multiple Documents Into One
|
||||
:animate: fade-in-slide-down
|
||||
|
||||
If you need to merge a selection of documents in your project into a single document, you can
|
||||
achieve this by first making a new folder for just that purpose, and drag all the files you want
|
||||
merged into this folder. Then you can right click the folder, select :guilabel:`Transform` and
|
||||
:guilabel:`Merge Documents in Folder`.
|
||||
achieve this by first making a new folder for just that purpose, and drag all the documents you
|
||||
want merged into this folder. Then you can right click the folder, select :guilabel:`Transform`
|
||||
and :guilabel:`Merge Documents in Folder`.
|
||||
|
||||
In the dialog that pops up, the documents will be in the same order as in the folder, but you
|
||||
can rearrange them here of you wish. See :ref:`a_ui_tree_split_merge` for more details.
|
||||
@@ -31,6 +39,23 @@ Managing the Project
|
||||
Layout Tricks
|
||||
=============
|
||||
|
||||
.. dropdown:: Align Paragraphs with Line Breaks
|
||||
:animate: fade-in-slide-down
|
||||
|
||||
If you have line breaks in you paragraphs, and also want to apply additional text alignment or
|
||||
indentation, you must apply the alignment tags to the first line.
|
||||
|
||||
For example, this will centre the two lines.
|
||||
|
||||
.. code-block:: md
|
||||
|
||||
>> Line one is centred. <<
|
||||
Line two is also centred.
|
||||
|
||||
This text is not centred, because it is a new paragraph.
|
||||
|
||||
See :ref:`a_fmt_align` for more details.
|
||||
|
||||
.. dropdown:: Create a Simple Table
|
||||
:animate: fade-in-slide-down
|
||||
|
||||
@@ -44,6 +69,15 @@ Layout Tricks
|
||||
This is most suitable for your notes, as the result in exported documents cannot be guaranteed
|
||||
to match. Especially if you don't use the same font in your manuscript as in the editor.
|
||||
|
||||
.. dropdown:: Turn Off First Line Indent for a Paragraph
|
||||
:animate: fade-in-slide-down
|
||||
|
||||
If you have first line indent enabled, but have a specific paragraph that you don't want
|
||||
indented, you can disable the indentation by explicitly add text alignment. Aligned paragraphs
|
||||
are not indented. For instance by adding ``<<`` to the end to left-align it,
|
||||
|
||||
See :ref:`a_fmt_align` for more details.
|
||||
|
||||
|
||||
Organising Your Text
|
||||
====================
|
||||
@@ -58,8 +92,6 @@ Organising Your Text
|
||||
such text. Separating chapter and scene files also allows you to make scene files child
|
||||
documents of the chapter.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
.. dropdown:: Distinguishing Soft and Hard Scene Breaks
|
||||
:animate: fade-in-slide-down
|
||||
|
||||
|
||||
@@ -12,16 +12,17 @@ text without having to deal with formatting until you generate a draft document
|
||||
Instead, you can focus on the writing right from the start.
|
||||
|
||||
Of course, you probably need *some* formatting for your text. At the very least you need emphasis.
|
||||
Most people are familiar with adding emphasis using underscores and asterisks. This formatting
|
||||
standard comes from Markdown_ and is supported by novelWriter. It also uses Markdown formatting for
|
||||
defining document headings. If you need more specialised formatting, additional formatting options
|
||||
are available using a shortcode format. See :ref:`a_fmt_shortcodes` for more details.
|
||||
Most people are familiar with adding emphasis using ``_underscores_`` and ``**asterisks**``. This
|
||||
formatting standard comes from Markdown_ and is supported by novelWriter. It also uses Markdown
|
||||
formatting for defining document headings. If you need more specialised formatting, additional
|
||||
formatting options are available using a shortcode format. See :ref:`a_fmt_shortcodes` for more
|
||||
details.
|
||||
|
||||
.. admonition:: Limitations
|
||||
|
||||
novelWriter is designed for writing fiction, so the formatting features available are limited to
|
||||
those relevant for this purpose. It is **not** suitable for technical writing, and it is **not**
|
||||
a full-featured Markdown editor.
|
||||
those relevant for this purpose. It is *not* suitable for technical writing, and it is *not* a
|
||||
full-featured Markdown editor.
|
||||
|
||||
It is also not intended as a tool for organising research for writing, and therefore lacks
|
||||
formatting features you may need for this purpose. The notes feature in novelWriter is mainly
|
||||
@@ -32,7 +33,7 @@ instead of a single, large document. The idea is to make it easier to reorganise
|
||||
structure without having to cut and paste text between chapters and scenes.
|
||||
|
||||
There are two kinds of documents in your project: :term:`Novel Documents` are documents that are
|
||||
part of your story. The other kind of documents are :term:`Project Notes`. These are intended for
|
||||
part of your story. The other kind of documents are :term:`Project Notes`, which are intended for
|
||||
your notes about your characters, your world building, and so on.
|
||||
|
||||
You can at any point split the individual documents by their headings up into multiple documents,
|
||||
@@ -51,9 +52,12 @@ Below are some key features of novelWriter.
|
||||
**Focus on writing**
|
||||
The aim of the user interface is to let you focus on writing instead of spending time formatting
|
||||
text. Formatting is therefore limited to a small set of formatting tags for simple things like
|
||||
text emphasis and paragraph alignment. When you really want to focus on just writing, you can
|
||||
switch the editor into **Focus Mode** where only the text editor panel itself is visible, and
|
||||
the project structure view is hidden away.
|
||||
text emphasis and paragraph alignment. Additional shortcodes are available for special
|
||||
formatting cases.
|
||||
|
||||
When you really want to focus on just writing, you can switch the editor into **Focus Mode**
|
||||
where only the text editor panel itself is visible, and the project structure view is hidden
|
||||
away.
|
||||
|
||||
**Keep an eye on your notes**
|
||||
The main window can optionally show a document viewer to the right of the editor. The viewer
|
||||
@@ -72,19 +76,22 @@ Below are some key features of novelWriter.
|
||||
**Multi-novel project support**
|
||||
The main parts of your project is split up into top level special folders called "Root" folders.
|
||||
Your main story text lives in the "Novel" root folder. You can have multiple such folders in a
|
||||
project. This allows you to keep a series of individual novels with the same characters and
|
||||
world building in the same project, and create manuscripts for them individually.
|
||||
project, and rename them to whatever you want. This allows you to keep a series of individual
|
||||
novels with the same characters and world building in the same project, and create manuscripts
|
||||
for them individually.
|
||||
|
||||
**Keep track of your story elements**
|
||||
All notes in your project can be assigned a :term:`tag` that you can then :term:`reference` from
|
||||
any other document or note. In fact, you can add a new tag under each heading of a note if you
|
||||
need to be able to reference specific sections of it.
|
||||
need to be able to reference specific sections of it, or you want to keep several topics in the
|
||||
same note.
|
||||
|
||||
**Get an overview of your story**
|
||||
It is not the documents themselves that define the chapters and scenes of your story, but the
|
||||
headings that separate them. In the **Outline View** on the main window you can see an outline
|
||||
of all the chapter and scene headings of your project. If they have any references in them, like
|
||||
which character is in what chapter and scene, these are listed in additional columns.
|
||||
of all the chapter and scene headings of each novel root folder in your project. If they have
|
||||
any references in them, like which character is in what chapter and scene, these are listed in
|
||||
additional columns.
|
||||
|
||||
You can also add a synopsis to each chapter or scene, which can be listed here as well. You have
|
||||
the option to add or remove columns of information from this outline. A subset of the outline
|
||||
@@ -100,9 +107,9 @@ Below are some key features of novelWriter.
|
||||
Whether you want to assemble a manuscript, or export all your notes, or generate an outline of
|
||||
your chapters and scenes with a synopsis included, you can use the **Build Manuscript** tool to
|
||||
do so. The tool lets you select what information you want to include in the generated document,
|
||||
and how it is formatted. You can send the result to a printer, a PDF, or to an Open Document
|
||||
file that can be opened by most office type word processors. You can also generate the result
|
||||
as HTML, or Markdown, both suitable for further conversion to other formats.
|
||||
and how it is formatted. You can send the result to a printer or PDF, or generate an Open
|
||||
Document file that can be opened by most office type word processors. You can also generate the
|
||||
result as HTML, or Markdown, both suitable for further conversion to other formats.
|
||||
|
||||
|
||||
.. _a_intro_screenshots:
|
||||
|
||||
@@ -10,7 +10,7 @@ but you don't need all of them to get started.
|
||||
The chapters below labelled "Essential Information" are the ones you need to know to use the
|
||||
application correctly. By "correctly", it is meant: in a way so novelWriter understands the basic
|
||||
structure of your text. It collects a lot of information from your text and uses it to display the
|
||||
structure of it in various ways to help you get an overview of your project.
|
||||
structure of it in various ways to help you get an overview of your writing.
|
||||
|
||||
The chapters labelled "Recommended Reading" includes additional information on how the different
|
||||
parts if the application work and what the features do.
|
||||
|
||||
@@ -131,9 +131,12 @@ A GUI theme ``.conf`` file consists of the following settings:
|
||||
linkvisited = 100, 100, 100
|
||||
|
||||
[GUI]
|
||||
statusnone = 100, 100, 100
|
||||
statussaved = 100, 100, 100
|
||||
statusunsaved = 100, 100, 100
|
||||
helptext = 0, 0, 0
|
||||
fadedtext = 128, 128, 128
|
||||
errortext = 255, 0, 0
|
||||
statusnone = 120, 120, 120
|
||||
statussaved = 2, 133, 37
|
||||
statusunsaved = 200, 15, 39
|
||||
|
||||
In the Main section you must at least define the ``name`` and ``icontheme`` settings. The
|
||||
``icontheme`` settings should correspond to one of the internal icon themes, either
|
||||
@@ -143,7 +146,11 @@ setting must match the icon theme's folder name.
|
||||
The Palette values correspond to the Qt enum values for ``QPalette::ColorRole``, see the
|
||||
`Qt documentation <https://doc.qt.io/qt-5.15/qpalette.html#ColorRole-enum>`_ for more details. The
|
||||
colour values are RGB numbers on the format ``r, g, b`` where each is an integer from ``0`` to
|
||||
``255``. Omitted values are not loaded and will use default values.
|
||||
``255``. Omitted values are not loaded and will use default values. If the ``helptext`` colour is
|
||||
not defined, it is computed as a colour between the ``window`` and ``windowtext`` colour.
|
||||
|
||||
.. versionadded:: 2.5
|
||||
The ``fadedtext`` and ``errortext`` theme colour entries were added.
|
||||
|
||||
|
||||
Custom Syntax Theme
|
||||
@@ -168,12 +175,13 @@ A syntax theme ``.conf`` file consists of the following settings:
|
||||
headertext = 0, 0, 0
|
||||
headertag = 0, 0, 0
|
||||
emphasis = 0, 0, 0
|
||||
straightquotes = 0, 0, 0
|
||||
doublequotes = 0, 0, 0
|
||||
singlequotes = 0, 0, 0
|
||||
dialog = 0, 0, 0
|
||||
altdialog = 0, 0, 0
|
||||
note = 0, 0, 0
|
||||
hidden = 0, 0, 0
|
||||
shortcode = 0, 0, 0
|
||||
keyword = 0, 0, 0
|
||||
tag = 0, 0, 0
|
||||
value = 0, 0, 0
|
||||
optional = 0, 0, 0
|
||||
spellcheckline = 0, 0, 0
|
||||
@@ -197,3 +205,7 @@ Omitted syntax colours default to black, except ``background`` which defaults to
|
||||
|
||||
.. versionadded:: 2.4
|
||||
The ``texthighlight`` syntax colour entry was added.
|
||||
|
||||
.. versionadded:: 2.5
|
||||
The ``dialog``, ``altdialog``, ``note`` and ``tag`` syntax colour entries were added.
|
||||
``straightquotes``, ``doublequotes`` and ``singlequotes`` were removed.
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
.. _a_proj:
|
||||
|
||||
**************
|
||||
Novel Projects
|
||||
**************
|
||||
*****************
|
||||
Managing Projects
|
||||
*****************
|
||||
|
||||
Your text in novelWriter is organised into projects. Each project is meant to contain one novel
|
||||
and associated notes. If you have multiple novels in a series, with the same characters and shared
|
||||
notes, it is also possible to keep all of them in the same project.
|
||||
|
||||
.. _a_proj_new:
|
||||
|
||||
Creating Project
|
||||
================
|
||||
|
||||
New projects can be created from the **Project** menu by selecting **Create or Open Project**. This
|
||||
will open the **Welcome** dialog, where you can select the :guilabel:`New` button that will assist
|
||||
@@ -27,10 +36,19 @@ and page counts, is available through the **Novel Details** dialog. Statistics a
|
||||
is also available in the **Manuscript Build** tool.
|
||||
|
||||
|
||||
Template Projects
|
||||
-----------------
|
||||
|
||||
From the Welcome dialog you can also create a new from another existing project. If you have a
|
||||
specific structure you want to use for all your new projects, you can create a dedicated project to
|
||||
be used as a template, and select to copy an existing project from the :guilabel:"Prefill Project"
|
||||
option from the **New Project** form.
|
||||
|
||||
|
||||
.. _a_proj_roots:
|
||||
|
||||
Project Roots
|
||||
=============
|
||||
Project Structure
|
||||
=================
|
||||
|
||||
Projects are structured into a set of top level folders called "Root Folders". They are visible in
|
||||
the project tree at the left side of the main window.
|
||||
@@ -288,7 +306,8 @@ Status and Importance Tabs
|
||||
--------------------------
|
||||
|
||||
Each document or folder of type **Novel** can be given a "Status" label accompanied by a coloured
|
||||
icon, and each document or folder of the remaining types can be given an "Importance" label.
|
||||
icon with an optional shape selected from a list of pre-defined shapes. Each document or folder of
|
||||
the remaining types can be given an "Importance" label with the same customisation options.
|
||||
|
||||
These labels are there purely for your convenience, and you are not required to use them for any
|
||||
other features to work. No other part of novelWriter accesses this information. The intention is to
|
||||
|
||||
@@ -39,11 +39,9 @@ The following Python packages are needed to run all features of novelWriter:
|
||||
* ``PyQt5`` – needed for connecting with the Qt5 libraries.
|
||||
* ``PyEnchant`` – needed for spell checking (optional).
|
||||
|
||||
PyQt/Qt should be at least 5.10, but ideally 5.13 or higher for all features to work. For instance,
|
||||
searching using regular expressions with full Unicode support requires 5.13.
|
||||
|
||||
If you want spell checking, you must install the ``PyEnchant`` package. The spell check library
|
||||
must be at least 3.0 to work with Windows. On Linux, 2.0 also works fine.
|
||||
PyQt/Qt must be at least 5.15.0. If you want spell checking, you must install the ``PyEnchant``
|
||||
package. The spell check library must be at least 3.0 to work with Windows. On Linux, 2.0 also
|
||||
works fine.
|
||||
|
||||
If you install from PyPi, these dependencies should be installed automatically. If you install from
|
||||
source, dependencies can still be installed from PyPi with:
|
||||
@@ -55,7 +53,7 @@ source, dependencies can still be installed from PyPi with:
|
||||
.. note::
|
||||
On Linux distros, the Qt library is usually split up into multiple packages. In some cases,
|
||||
secondary dependencies may not be installed automatically. For novelWriter, the library files
|
||||
for renderring the SVG icons may be left out and needs to be installed manually. This is the
|
||||
for rendering the SVG icons may be left out and needs to be installed manually. This is the
|
||||
case on for instance Arch Linux.
|
||||
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@ project-related tools and quick access to settings at the bottom.
|
||||
|
||||
.. versionadded:: 2.2
|
||||
A number of new formatting options were added in 2.2 to allow for some special formatting cases.
|
||||
At the same time, a small formatting toolbar was added in the editor. It is hidden by default,
|
||||
but can be opened by pressing the icon button in the top right corner.
|
||||
At the same time, a small formatting toolbar was added to the editor. It is hidden by default,
|
||||
but can be opened by pressing the button in the top right corner of the editor header.
|
||||
|
||||
|
||||
Project Tree and Editor View
|
||||
@@ -148,6 +148,20 @@ in the document editor that acts on the open document.
|
||||
.. versionadded:: 2.4
|
||||
|
||||
|
||||
Switching Focus
|
||||
---------------
|
||||
|
||||
If the project or novel view does not have focus, pressing :kbd:`Ctrl+T` switches focus to
|
||||
whichever of the two is visible. If one of them already has focus, the key press will switch
|
||||
between them instead.
|
||||
|
||||
Likewise, pressing :kbd:`Ctrl+E` with switch focus to the document editor or viewer, or if any of
|
||||
them already have focus, it will switch focus between them,
|
||||
|
||||
These two shortcuts makes it possible to jump between all these GUI elements without having to
|
||||
reach for the mouse or touchpad.
|
||||
|
||||
|
||||
.. _a_breakdown_project:
|
||||
|
||||
Project Layout
|
||||
|
||||
@@ -125,8 +125,8 @@ background, depending on the selected theme.
|
||||
|
||||
.. _a_fmt_emph:
|
||||
|
||||
Text Emphasis
|
||||
=============
|
||||
Text Emphasis with Markdown
|
||||
===========================
|
||||
|
||||
A minimal set of Markdown text emphasis styles are supported for text paragraphs.
|
||||
|
||||
@@ -156,7 +156,7 @@ In addition, the following rules apply:
|
||||
outside, they violate rule 2.
|
||||
4. Text emphasis does not span past line breaks. If you need to add emphasis to multiple lines or
|
||||
paragraphs, you must apply it to each of them in turn.
|
||||
5. Text emphasis can only be used in plain paragraphs. Comments, titles, and meta data tags don't
|
||||
5. Text emphasis can only be used in comments and paragraphs. Headings and meta data tags don't
|
||||
allow for formatting, and any formatting markup will be rendered as-is.
|
||||
|
||||
.. tip::
|
||||
@@ -168,8 +168,8 @@ In addition, the following rules apply:
|
||||
|
||||
.. _a_fmt_shortcodes:
|
||||
|
||||
Extended Formatting with Shortcodes
|
||||
===================================
|
||||
Formatting with Shortcodes
|
||||
==========================
|
||||
|
||||
For additional formatting options, you can use shortcodes. Shortcodes is a form of in-line codes
|
||||
that can be used to change the format of the text that follows and opening code, and last until
|
||||
@@ -197,6 +197,13 @@ word if you need to. You can also freely combine them to form more complex forma
|
||||
The shortcodes are available from the **Format** menu and in the editor toolbar, which can be
|
||||
activated by clicking the left-most icon button in the editor header.
|
||||
|
||||
.. note::
|
||||
|
||||
Shortcodes are not processed until you generate a preview or generate a manuscript document. So
|
||||
there is no highlighting of the text between the formatting markers. There is also no check that
|
||||
your markers make sense. You must ensure that you have both the opening and closing formatting
|
||||
markers where you want them.
|
||||
|
||||
.. versionadded:: 2.2
|
||||
|
||||
|
||||
@@ -217,27 +224,26 @@ correctly formatted, the syntax highlighter will indicate this by altering the c
|
||||
|
||||
The different styles of comments are as follows:
|
||||
|
||||
``% Comment text ...``
|
||||
``% Your comment text ...``
|
||||
This is a comment. The text is not rendered by default (this can be overridden), seen in the
|
||||
document viewer, or counted towards word counts. It is intended for you to make notes in your
|
||||
text for your own sake, whatever that may be, that isn't part of the story text. This is the
|
||||
general format of a comment.
|
||||
|
||||
``%Synopsis: Comment text ...``
|
||||
``%Synopsis: Your synopsis text ...``
|
||||
This is a synopsis comment. It is generally treated in the same way as a regular comment, except
|
||||
that it is also captured by the indexing algorithm and displayed in the :ref:`a_ui_outline`. It
|
||||
can also be filtered separately when building the project to for instance generate an outline
|
||||
document of the whole project.
|
||||
|
||||
``%Short: Comment text ...``
|
||||
``%Short: Your short description ...``
|
||||
This is a short description comment. It is identical to the synopsis comment (they are
|
||||
interchangeable), but is intended to be used for project notes. The text shows up in the
|
||||
Reference panel below the document viewer in the last column labelled **Short Description**.
|
||||
|
||||
``%~ Comment text ...``
|
||||
This can be used to exclude story text from your manuscript without having to delete it from
|
||||
your text. Comments with the ``~`` will *never* be included in the manuscript, even if you have
|
||||
chosen to include comments in it. That is the main difference between these two formats.
|
||||
``%Footnote.<key>: Your footnote text ...``
|
||||
This is a special comment assigned to a footnote marker. See :ref:`a_fmt_footnote` for how to
|
||||
use them in your text.
|
||||
|
||||
.. note::
|
||||
Only one comment can be flagged as a synopsis or short comment for each heading. If multiple
|
||||
@@ -245,16 +251,74 @@ The different styles of comments are as follows:
|
||||
ignored.
|
||||
|
||||
|
||||
.. _a_fmt_footnote:
|
||||
|
||||
Footnotes
|
||||
=========
|
||||
|
||||
Footnotes are added with a shortcode, paired with a matching comment for the actual footnote text.
|
||||
The matching is done with a key that links the two. If you insert a footnote from the **Insert**
|
||||
menu, a unique key is generated for you.
|
||||
|
||||
The insert feature will add the footnote shortcode marker at the position of your cursor in the
|
||||
text, and create the associated footnote comment right after the paragraph, and move the cursor
|
||||
there so you can immediately start typing the footnote text.
|
||||
|
||||
The footnote comment can be anywhere in the document, so if you wish to move them to, say, the
|
||||
bottom of the text, you are free to do so.
|
||||
|
||||
Footnote keys are only required to be unique within a document, so if you copy, move or merge text,
|
||||
you must make sure the keys are not duplicated. If you use the automatically generated keys from
|
||||
the **Insert** menu, they are unique among all indexed documents. They are not guaranteed to be
|
||||
unique against footnotes in the Archive or Trash folder though, but the chance of accidentally
|
||||
generating the same key twice in a project is relatively small in the first place (1 in 810 000).
|
||||
|
||||
This is what a footnote inserted into a paragraph may look like when completed:
|
||||
|
||||
.. code-block:: md
|
||||
|
||||
This is a text paragraph with a footnote[footnote:fn1] in the middle.
|
||||
|
||||
%Footnote.fn1: This is the text of the footnote.
|
||||
|
||||
.. versionadded:: 2.5
|
||||
|
||||
|
||||
.. _a_fmt_ignore:
|
||||
|
||||
Ignored Text
|
||||
============
|
||||
|
||||
If you want to completely ignore some of the text in your documents, but are not ready to delete
|
||||
it, you can add ``%~`` before the text paragraph or line. This will cause novelWriter to skip the
|
||||
text entirely when generating previews or building manuscripts.
|
||||
|
||||
This is a better way of removing text than converting them to regular comments, as you may want to
|
||||
include regular comments in your previews or draft manuscript.
|
||||
|
||||
You can toggle the ignored text feature on and off for a paragraph by pressing :kbd:`Ctrl+Shift+D`
|
||||
on your keyboard with your cursor somewhere in the paragraph.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: md
|
||||
|
||||
%~ This text is ignored.
|
||||
|
||||
This text is a regular paragraph.
|
||||
|
||||
|
||||
.. _a_fmt_tags:
|
||||
|
||||
Tags and References
|
||||
===================
|
||||
|
||||
The document editor supports a set of keywords used for setting tags, and making references between
|
||||
documents.
|
||||
documents based on those tags.
|
||||
|
||||
Tags use the keyword ``@tag:`` to define a tag. The tag can be set once per section defined by a
|
||||
heading. Setting it multiple times under the same heading will just override the previous setting.
|
||||
You must use the keyword ``@tag:`` to define a tag. The tag can be set once per section defined by
|
||||
a heading. Setting it multiple times under the same heading will just override the previous
|
||||
setting.
|
||||
|
||||
``@tag: value``
|
||||
A tag keyword followed by the tag value, like for instance the name of a character.
|
||||
@@ -306,6 +370,40 @@ Examples:
|
||||
from the manuscript build tool as long as the format supports paragraph alignment.
|
||||
|
||||
|
||||
Alignment with Line Breaks
|
||||
--------------------------
|
||||
|
||||
If you have line breaks in the paragraph, like for instance when you are writing verses, the
|
||||
alignment markers must be applied to the first line. Markers on the other lines are ignored. The
|
||||
markers for the first line are used for all the other lines.
|
||||
|
||||
For the following text, all lines will be centred, not just the first:
|
||||
|
||||
.. code-block:: md
|
||||
|
||||
>> I am the very model of a modern Major-General <<
|
||||
I've information vegetable, animal, and mineral
|
||||
I know the kings of England, and I quote the fights historical
|
||||
From Marathon to Waterloo, in order categorical
|
||||
|
||||
|
||||
Alignment with First Line Indent
|
||||
--------------------------------
|
||||
|
||||
If you have first line indent enabled in your Manuscript build settings, you probably want to
|
||||
disable it for text in verses. Adding any alignment tags will cause the first line indent to be
|
||||
switched off for that paragraph.
|
||||
|
||||
The following text will always be aligned against the left margin:
|
||||
|
||||
.. code-block:: md
|
||||
|
||||
I am the very model of a modern Major-General <<
|
||||
I've information vegetable, animal, and mineral
|
||||
I know the kings of England, and I quote the fights historical
|
||||
From Marathon to Waterloo, in order categorical
|
||||
|
||||
|
||||
.. _a_fmt_break:
|
||||
|
||||
Vertical Space and Page Breaks
|
||||
|
||||
@@ -54,7 +54,8 @@ At the top of the project tree, you will find a set of buttons.
|
||||
* The next two buttons can be used to move items up and down in the project tree. This is the only
|
||||
way to move root folders.
|
||||
* The next button opens a dropdown menu for adding new items to the tree. This includes root
|
||||
folders. You can also activate this dropdown menu by pressing :kbd:`Ctrl+N`.
|
||||
folders and template documents. You can also activate this dropdown menu by pressing
|
||||
:kbd:`Ctrl+N`.
|
||||
* The last button is a menu of further actions you can apply to the project tree.
|
||||
|
||||
Below the project tree you will find a small details panel showing the full information of the
|
||||
|
||||
@@ -26,7 +26,7 @@ Main Window Shortcuts
|
||||
":kbd:`F9`", "Re-build the project's index"
|
||||
":kbd:`F11`", "Toggle full screen mode"
|
||||
":kbd:`Ctrl+,`", "Open the **Preferences** dialog"
|
||||
":kbd:`Ctrl+E`", "Switch focus to the document editor"
|
||||
":kbd:`Ctrl+E`", "Switch or toggle focus for the editor or viewer"
|
||||
":kbd:`Ctrl+T`", "Switch or toggle focus for the project tree or novel view"
|
||||
":kbd:`Ctrl+Q`", "Exit novelWriter"
|
||||
":kbd:`Ctrl+Shift+,`", "Open the **Project Settings** dialog"
|
||||
|
||||
@@ -40,6 +40,24 @@ Any :term:`references<reference>` in the editor can be opened in the viewer by m
|
||||
the label and pressing :kbd:`Ctrl+Return`. You can also control-click them with your mouse.
|
||||
|
||||
|
||||
Spell Checking
|
||||
--------------
|
||||
|
||||
A third party library called Enchant is used for spell checking in the editor. The controls for
|
||||
spell checking can be found in the **Tools** menu. You can also set spell checking language in
|
||||
**Project Settings**.
|
||||
|
||||
This spell checking library comes with support for custom words that you can add by selecting
|
||||
"Add Word to Dictionary" from the context menu when a word is highlighted by the spell checker as
|
||||
misspelled. The custom words are managed on a per-project basis, and can the list of words can be
|
||||
edited from the **Project Word List** tool available from the **Tools** menu.
|
||||
|
||||
.. note::
|
||||
|
||||
Generally, spell checking dictionaries are collected from your operating system, but on Windows
|
||||
they are not. See :ref:`a_custom_dict` for how to add spell checking dictionaries on Windows.
|
||||
|
||||
|
||||
Editor Auto-Completer
|
||||
---------------------
|
||||
|
||||
|
||||
2264
i18n/nw_base.ts
2264
i18n/nw_base.ts
File diff suppressed because it is too large
Load Diff
2272
i18n/nw_de_DE.ts
2272
i18n/nw_de_DE.ts
File diff suppressed because it is too large
Load Diff
2258
i18n/nw_en_US.ts
2258
i18n/nw_en_US.ts
File diff suppressed because it is too large
Load Diff
2258
i18n/nw_es_419.ts
2258
i18n/nw_es_419.ts
File diff suppressed because it is too large
Load Diff
2258
i18n/nw_fr_FR.ts
2258
i18n/nw_fr_FR.ts
File diff suppressed because it is too large
Load Diff
2258
i18n/nw_it_IT.ts
2258
i18n/nw_it_IT.ts
File diff suppressed because it is too large
Load Diff
2258
i18n/nw_ja_JP.ts
2258
i18n/nw_ja_JP.ts
File diff suppressed because it is too large
Load Diff
2258
i18n/nw_nb_NO.ts
2258
i18n/nw_nb_NO.ts
File diff suppressed because it is too large
Load Diff
2284
i18n/nw_nl_NL.ts
2284
i18n/nw_nl_NL.ts
File diff suppressed because it is too large
Load Diff
4991
i18n/nw_pl_PL.ts
Normal file
4991
i18n/nw_pl_PL.ts
Normal file
File diff suppressed because it is too large
Load Diff
3736
i18n/nw_pt_BR.ts
3736
i18n/nw_pt_BR.ts
File diff suppressed because it is too large
Load Diff
2966
i18n/nw_zh_CN.ts
2966
i18n/nw_zh_CN.ts
File diff suppressed because it is too large
Load Diff
@@ -47,19 +47,3 @@ 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")
|
||||
|
||||
@@ -32,7 +32,7 @@ from typing import TYPE_CHECKING
|
||||
from PyQt5.QtWidgets import QApplication, QErrorMessage
|
||||
|
||||
from novelwriter.config import Config
|
||||
from novelwriter.error import exceptionHandler, logException
|
||||
from novelwriter.error import exceptionHandler
|
||||
from novelwriter.shared import SharedData
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
@@ -47,9 +47,9 @@ __license__ = "GPLv3"
|
||||
__author__ = "Veronica Berglyd Olsen"
|
||||
__maintainer__ = "Veronica Berglyd Olsen"
|
||||
__email__ = "code@vkbo.net"
|
||||
__version__ = "2.4"
|
||||
__hexversion__ = "0x020400f0"
|
||||
__date__ = "2024-04-20"
|
||||
__version__ = "2.5.2"
|
||||
__hexversion__ = "0x020502f0"
|
||||
__date__ = "2024-09-17"
|
||||
__status__ = "Stable"
|
||||
__domain__ = "novelwriter.io"
|
||||
|
||||
@@ -162,19 +162,19 @@ def main(sysArgs: list | None = None) -> GuiMain | None:
|
||||
# Check Packages and Versions
|
||||
errorData = []
|
||||
errorCode = 0
|
||||
if sys.hexversion < 0x030800f0:
|
||||
if sys.hexversion < 0x030900f0:
|
||||
errorData.append(
|
||||
"At least Python 3.8 is required, found %s" % CONFIG.verPyString
|
||||
"At least Python 3.9 is required, found %s" % CONFIG.verPyString
|
||||
)
|
||||
errorCode |= 0x04
|
||||
if CONFIG.verQtValue < 0x050a00:
|
||||
if CONFIG.verQtValue < 0x050f00:
|
||||
errorData.append(
|
||||
"At least Qt5 version 5.10 is required, found %s" % CONFIG.verQtString
|
||||
"At least Qt5 version 5.15.0 is required, found %s" % CONFIG.verQtString
|
||||
)
|
||||
errorCode |= 0x08
|
||||
if CONFIG.verPyQtValue < 0x050a00:
|
||||
if CONFIG.verPyQtValue < 0x050f00:
|
||||
errorData.append(
|
||||
"At least PyQt5 version 5.10 is required, found %s" % CONFIG.verPyQtString
|
||||
"At least PyQt5 version 5.15.0 is required, found %s" % CONFIG.verPyQtString
|
||||
)
|
||||
errorCode |= 0x10
|
||||
|
||||
@@ -198,52 +198,46 @@ def main(sysArgs: list | None = None) -> GuiMain | None:
|
||||
# Finish initialising config
|
||||
CONFIG.initConfig(confPath, dataPath)
|
||||
|
||||
if CONFIG.osDarwin:
|
||||
if sys.platform == "darwin":
|
||||
try:
|
||||
from Foundation import NSBundle # type: ignore
|
||||
bundle = NSBundle.mainBundle()
|
||||
info = bundle.localizedInfoDictionary() or bundle.infoDictionary()
|
||||
info["CFBundleName"] = "novelWriter"
|
||||
except Exception:
|
||||
logger.error("Failed to set application name")
|
||||
logException()
|
||||
|
||||
elif CONFIG.osWindows:
|
||||
pass # Quietly ignore error
|
||||
elif sys.platform == "win32":
|
||||
try:
|
||||
import ctypes
|
||||
appID = f"io.novelwriter.{__version__}"
|
||||
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(appID) # type: ignore
|
||||
except Exception:
|
||||
logger.error("Failed to set application name")
|
||||
logException()
|
||||
pass # Quietly ignore error
|
||||
|
||||
# Import GUI (after dependency checks), and launch
|
||||
from novelwriter.guimain import GuiMain
|
||||
|
||||
if testMode:
|
||||
nwGUI = GuiMain()
|
||||
return nwGUI
|
||||
# Only used for testing where the test framework creates the app
|
||||
CONFIG.loadConfig()
|
||||
return GuiMain()
|
||||
|
||||
else:
|
||||
nwApp = QApplication([CONFIG.appName, (f"-style={qtStyle}")])
|
||||
nwApp.setApplicationName(CONFIG.appName)
|
||||
nwApp.setApplicationVersion(__version__)
|
||||
nwApp.setOrganizationDomain(__domain__)
|
||||
nwApp.setOrganizationName(__domain__)
|
||||
nwApp.setDesktopFileName(CONFIG.appName)
|
||||
app = QApplication([CONFIG.appName, (f"-style={qtStyle}")])
|
||||
app.setApplicationName(CONFIG.appName)
|
||||
app.setApplicationVersion(__version__)
|
||||
app.setOrganizationDomain(__domain__)
|
||||
app.setOrganizationName(__domain__)
|
||||
app.setDesktopFileName(CONFIG.appName)
|
||||
|
||||
# Connect the exception handler before making the main GUI
|
||||
sys.excepthook = exceptionHandler
|
||||
# Connect the exception handler before making the main GUI
|
||||
sys.excepthook = exceptionHandler
|
||||
|
||||
# Run Config steps that require the QApplication
|
||||
CONFIG.initLocalisation(nwApp)
|
||||
CONFIG.setTextFont(CONFIG.textFont, CONFIG.textSize) # Makes sure these are valid
|
||||
# Run Config steps that require the QApplication
|
||||
CONFIG.loadConfig()
|
||||
CONFIG.initLocalisation(app)
|
||||
|
||||
# Launch main GUI
|
||||
nwGUI = GuiMain()
|
||||
nwGUI.postLaunchTasks(cmdOpen)
|
||||
# Launch main GUI
|
||||
nwGUI = GuiMain()
|
||||
nwGUI.postLaunchTasks(cmdOpen)
|
||||
|
||||
sys.exit(nwApp.exec())
|
||||
|
||||
return None
|
||||
|
||||
# END Function main
|
||||
sys.exit(app.exec())
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"Synopsis": "Synopsis",
|
||||
"Short Description": "Short Description",
|
||||
"Footnotes": "Footnotes",
|
||||
"Comment": "Comment",
|
||||
"Notes": "Notes",
|
||||
"Tag": "Tag",
|
||||
|
||||
116
novelwriter/assets/i18n/project_pl_PL.json
Normal file
116
novelwriter/assets/i18n/project_pl_PL.json
Normal file
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"Synopsis": "Streszczenie",
|
||||
"Short Description": "Krótki opis",
|
||||
"Comment": "Komentarz",
|
||||
"Notes": "Notatki",
|
||||
"Tag": "Znacznik",
|
||||
"Point of View": "Punkt widzenia",
|
||||
"Focus": "Skupienie",
|
||||
"Characters": "Postacie",
|
||||
"Plot": "Fabuła",
|
||||
"Timeline": "Oś czasu",
|
||||
"Locations": "Miejsca",
|
||||
"Objects": "Obiekty",
|
||||
"Entities": "Podmioty",
|
||||
"Custom": "Różne",
|
||||
"0": "zero",
|
||||
"1": "pierwszy",
|
||||
"2": "drugi",
|
||||
"3": "trzeci",
|
||||
"4": "czwarty",
|
||||
"5": "piąty",
|
||||
"6": "szósty",
|
||||
"7": "siódmy",
|
||||
"8": "ósmy",
|
||||
"9": "dziewiąty",
|
||||
"10": "dziesiąty",
|
||||
"11": "jedenasty",
|
||||
"12": "dwunasty",
|
||||
"13": "trzynasty",
|
||||
"14": "czternasty",
|
||||
"15": "piętnasty",
|
||||
"16": "szesnasty",
|
||||
"17": "siedemnasty",
|
||||
"18": "osiemnasty",
|
||||
"19": "dziewiętnasty",
|
||||
"20": "dwudziesty",
|
||||
"21": "dwudziesty pierwszy",
|
||||
"22": "dwudziesty drugi",
|
||||
"23": "dwudziesty Trzeci",
|
||||
"24": "dwudziesty czwarty",
|
||||
"25": "dwudziesty piąty",
|
||||
"26": "dwudziesty szósty",
|
||||
"27": "dwudziesty siódmy",
|
||||
"28": "dwudziesty ósmy",
|
||||
"29": "dwudziesty dziewiąty",
|
||||
"30": "trzydziesty",
|
||||
"31": "trzydziesty pierwszy",
|
||||
"32": "trzydziesty drugi",
|
||||
"33": "trzydziesty trzeci",
|
||||
"34": "trzydziesty czwarty",
|
||||
"35": "trzydziesty piąty",
|
||||
"36": "trzydziesty szósty",
|
||||
"37": "trzydziesty siódmy",
|
||||
"38": "trzydziesty ósmy",
|
||||
"39": "trzydziesty dziewiaty",
|
||||
"40": "czterdziesty",
|
||||
"41": "czterdziesty pierwszy",
|
||||
"42": "czterdziesty drugi",
|
||||
"43": "czterdziesty Trzeci",
|
||||
"44": "czterdziesty czwarty",
|
||||
"45": "czterdziesty piąty",
|
||||
"46": "czterdziesty szósty",
|
||||
"47": "czterdziesty siódmy",
|
||||
"48": "czterdziesty ósmy",
|
||||
"49": "czterdziesty dziewiąty",
|
||||
"50": "pięćdziesiąty",
|
||||
"51": "pięćdziesiąty pierwszy",
|
||||
"52": "pięćdziesiąty drugi",
|
||||
"53": "pięćdziesiąty trzeci",
|
||||
"54": "pięćdziesiąty czwarty",
|
||||
"55": "pięćdziesiąty piąty",
|
||||
"56": "pięćdziesiąty szósty",
|
||||
"57": "pięćdziesiąty siódmy",
|
||||
"58": "pięćdziesiąty ósmy",
|
||||
"59": "pięćdziesiąty dziewiąty",
|
||||
"60": "sześćdziesiąty",
|
||||
"61": "sześćdziesiąty pierwszy",
|
||||
"62": "sześćdziesiąty drugi",
|
||||
"63": "sześćdziesiąty trzeci",
|
||||
"64": "sześćdziesiąty czwarty",
|
||||
"65": "sześćdziesiąty piąty",
|
||||
"66": "sześćdziesiąty szósty",
|
||||
"67": "sześćdziesiąty siódmy",
|
||||
"68": "sześćdziesiąty ósmy",
|
||||
"69": "sześćdziesiąty dziewiąty",
|
||||
"70": "siedemdziesiąty",
|
||||
"71": "siedemdziesiąty pierwszy",
|
||||
"72": "siedemdziesiąty drugi",
|
||||
"73": "siedemdziesiąty trzeci",
|
||||
"74": "siedemdziesiąty czwarty",
|
||||
"75": "siedemdziesiąty piąty",
|
||||
"76": "siedemdziesiąty szósty",
|
||||
"77": "siedemdziesiąty siódmy",
|
||||
"78": "siedemdziesiąty ósmy",
|
||||
"79": "siedemdziesiąty dziewiąty",
|
||||
"80": "osiemdziesiąty",
|
||||
"81": "osiemdziesiąty pierwszy",
|
||||
"82": "osiemdziesiąty drugi",
|
||||
"83": "osiemdziesiąty trzeci",
|
||||
"84": "osiemdziesiąty czwarty",
|
||||
"85": "osiemdziesiąty piąty",
|
||||
"86": "osiemdziesiąty szósty",
|
||||
"87": "osiemdziesiąty siódmy",
|
||||
"88": "osiemdziesiąty ósmy",
|
||||
"89": "osiemdziesiąty dziewiąty",
|
||||
"90": "dziewięćdziesiąty",
|
||||
"91": "dziewięćdziesiąty pierwszy",
|
||||
"92": "dziewięćdziesiąty drugi",
|
||||
"93": "dziewięćdziesiąty trzeci",
|
||||
"94": "dziewięćdziesiąty czwarty",
|
||||
"95": "dziewięćdziesiąty piąty",
|
||||
"96": "dziewięćdziesiąty szósty",
|
||||
"97": "dziewięćdziesiąty siódmy",
|
||||
"98": "dziewięćdziesiąty ósmy",
|
||||
"99": "dziewięćdziesiąty dziewiąty"
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"Synopsis": "Sinopse",
|
||||
"Short Description": "Descrição breve",
|
||||
"Comment": "Comentários",
|
||||
"Comment": "Comentário",
|
||||
"Notes": "Notas",
|
||||
"Tag": "Etiqueta",
|
||||
"Point of View": "Ponto de vista",
|
||||
@@ -30,87 +30,87 @@
|
||||
"14": "Quatorze",
|
||||
"15": "Quinze",
|
||||
"16": "Dezesseis",
|
||||
"17": "Dezesete",
|
||||
"17": "Dezessete",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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"
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ fmt_strike-md = nw_tb-strike-md.svg
|
||||
fmt_subscript = nw_tb-subscript.svg
|
||||
fmt_superscript = nw_tb-superscript.svg
|
||||
fmt_underline = nw_tb-underline.svg
|
||||
font = nw_font.svg
|
||||
forward = typ_chevron-right.svg
|
||||
import = mixed_import.svg
|
||||
list = typ_th-list.svg
|
||||
@@ -78,6 +79,7 @@ proj_scene = mixed_document-scene.svg
|
||||
proj_section = mixed_document-section.svg
|
||||
proj_stats = typ_chart-bar-grey.svg
|
||||
proj_title = mixed_document-title.svg
|
||||
quote = nw_quote.svg
|
||||
refresh = typ_refresh.svg
|
||||
remove = typ_minus.svg
|
||||
revert = typ_refresh-flipped.svg
|
||||
|
||||
4
novelwriter/assets/icons/typicons_dark/nw_font.svg
Normal file
4
novelwriter/assets/icons/typicons_dark/nw_font.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" version="1.1" viewBox="0 0 6.35 6.35" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m2.5744 5.8208v-4.2853h-1.4527v-1.0064h4.1067v1.0064h-1.4527v4.2853z" fill="#aeaeae" stroke-width="1.1503"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 267 B |
4
novelwriter/assets/icons/typicons_dark/nw_quote.svg
Normal file
4
novelwriter/assets/icons/typicons_dark/nw_quote.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" version="1.1" viewBox="0 0 6.35 6.35" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m1.3715 3.0659q-0.32092-0.25674-0.44928-0.44928-0.12836-0.19255-0.12837-0.44928 0-0.42361 0.28241-0.69319 0.28241-0.28241 0.68035-0.28241 0.25674 0 0.44928 0.10269 0.19255 0.089858 0.3466 0.23106 0.24391 0.4878 0.24391 1.0141 0 0.42361-0.14121 0.82157-0.12837 0.38511-0.37227 0.71886-0.23106 0.33377-0.55199 0.61618-0.30808 0.26958-0.65467 0.46213l-0.25674-0.41078q0.29525-0.21822 0.57766-0.53914 0.28241-0.33375 0.46213-0.73171zm2.7598 0q-0.32092-0.25674-0.44928-0.44928-0.12837-0.19255-0.12837-0.44928 0-0.42361 0.28241-0.69319 0.28241-0.28241 0.68035-0.28241 0.25674 0 0.44928 0.10269 0.19255 0.089858 0.3466 0.23106 0.24391 0.4878 0.24391 1.0141 0 0.42361-0.14121 0.82157-0.12837 0.38511-0.37227 0.71886-0.23106 0.33377-0.55199 0.61618-0.30808 0.26958-0.65467 0.46213l-0.25674-0.41078q0.29525-0.21822 0.57766-0.53914 0.28241-0.33375 0.46213-0.73171z" fill="#aeaeae" fill-opacity=".78039" stroke-width="1.8194"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -59,6 +59,7 @@ fmt_strike-md = nw_tb-strike-md.svg
|
||||
fmt_subscript = nw_tb-subscript.svg
|
||||
fmt_superscript = nw_tb-superscript.svg
|
||||
fmt_underline = nw_tb-underline.svg
|
||||
font = nw_font.svg
|
||||
forward = typ_chevron-right.svg
|
||||
import = mixed_import.svg
|
||||
list = typ_th-list.svg
|
||||
@@ -78,6 +79,7 @@ proj_scene = mixed_document-scene.svg
|
||||
proj_section = mixed_document-section.svg
|
||||
proj_stats = typ_chart-bar-grey.svg
|
||||
proj_title = mixed_document-title.svg
|
||||
quote = nw_quote.svg
|
||||
refresh = typ_refresh.svg
|
||||
remove = typ_minus.svg
|
||||
revert = typ_refresh-flipped.svg
|
||||
|
||||
4
novelwriter/assets/icons/typicons_light/nw_font.svg
Normal file
4
novelwriter/assets/icons/typicons_light/nw_font.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" version="1.1" viewBox="0 0 6.35 6.35" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m2.5744 5.8208v-4.2853h-1.4527v-1.0064h4.1067v1.0064h-1.4527v4.2853z" fill-opacity=".78039" stroke-width="1.1503"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 274 B |
4
novelwriter/assets/icons/typicons_light/nw_quote.svg
Normal file
4
novelwriter/assets/icons/typicons_light/nw_quote.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" version="1.1" viewBox="0 0 6.35 6.35" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m1.3715 3.0659q-0.32092-0.25674-0.44928-0.44928-0.12836-0.19255-0.12837-0.44928 0-0.42361 0.28241-0.69319 0.28241-0.28241 0.68035-0.28241 0.25674 0 0.44928 0.10269 0.19255 0.089858 0.3466 0.23106 0.24391 0.4878 0.24391 1.0141 0 0.42361-0.14121 0.82157-0.12837 0.38511-0.37227 0.71886-0.23106 0.33377-0.55199 0.61618-0.30808 0.26958-0.65467 0.46213l-0.25674-0.41078q0.29525-0.21822 0.57766-0.53914 0.28241-0.33375 0.46213-0.73171zm2.7598 0q-0.32092-0.25674-0.44928-0.44928-0.12837-0.19255-0.12837-0.44928 0-0.42361 0.28241-0.69319 0.28241-0.28241 0.68035-0.28241 0.25674 0 0.44928 0.10269 0.19255 0.089858 0.3466 0.23106 0.24391 0.4878 0.24391 1.0141 0 0.42361-0.14121 0.82157-0.12837 0.38511-0.37227 0.71886-0.23106 0.33377-0.55199 0.61618-0.30808 0.26958-0.65467 0.46213l-0.25674-0.41078q0.29525-0.21822 0.57766-0.53914 0.28241-0.33375 0.46213-0.73171z" fill-opacity=".78039" stroke-width="1.8194"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -12,15 +12,17 @@ link = 77, 077, 255
|
||||
headertext = 255, 255, 255
|
||||
headertag = 50, 0, 180
|
||||
emphasis = 0, 255, 255
|
||||
straightquotes = 7, 27, 219
|
||||
doublequotes = 0, 255, 0
|
||||
singlequotes = 0, 140, 255
|
||||
dialog = 0, 255, 0
|
||||
altdialog = 0, 140, 255
|
||||
note = 150, 150, 150
|
||||
hidden = 77, 77, 100
|
||||
shortcode = 255, 255, 0
|
||||
keyword = 255, 100, 255
|
||||
tag = 255, 150, 10
|
||||
value = 255, 150, 10
|
||||
spellcheckline = 242, 72, 23
|
||||
errorline = 186, 218, 4
|
||||
replacetag = 0, 0, 180
|
||||
modifier = 144, 142, 176
|
||||
optional = 180, 180, 180
|
||||
texthighlight = 255, 255, 132, 96
|
||||
|
||||
@@ -6,23 +6,37 @@ url = https://github.com/vkbo/novelWriter
|
||||
license = CC BY-SA 4.0
|
||||
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
|
||||
|
||||
##
|
||||
# Colours:
|
||||
# Background = 2a2a2a : 42, 42, 42
|
||||
# Foreground = cccccc : 204, 204, 204
|
||||
# Comment = 969696 : 150, 150, 150
|
||||
# Red = e64250 : 230, 66, 80
|
||||
# Orange = f99157 : 249, 145, 87
|
||||
# Yellow = ffff84 : 255, 255, 132
|
||||
# Green = 99cc99 : 153, 204, 153
|
||||
# Blue = 288ed7 : 102, 153, 204
|
||||
# Purple = 8c56d7 : 140, 86, 215
|
||||
##
|
||||
|
||||
[Syntax]
|
||||
background = 54, 54, 54
|
||||
text = 199, 207, 208
|
||||
link = 184, 200, 0
|
||||
headertext = 0, 155, 200
|
||||
headertag = 0, 105, 135
|
||||
emphasis = 200, 120, 0
|
||||
straightquotes = 200, 46, 0
|
||||
doublequotes = 184, 200, 0
|
||||
singlequotes = 136, 200, 0
|
||||
background = 42, 42, 42
|
||||
text = 204, 204, 204
|
||||
link = 102, 153, 204
|
||||
headertext = 153, 204, 153
|
||||
headertag = 153, 204, 153, 160
|
||||
emphasis = 249, 145, 87
|
||||
dialog = 102, 153, 204
|
||||
altdialog = 102, 153, 204
|
||||
note = 255, 255, 216
|
||||
hidden = 150, 150, 150
|
||||
shortcode = 0, 155, 200
|
||||
keyword = 200, 46, 0
|
||||
value = 184, 200, 0
|
||||
optional = 0, 155, 200
|
||||
spellcheckline = 200, 46, 0
|
||||
errorline = 46, 200, 0
|
||||
replacetag = 0, 184, 46
|
||||
modifier = 200, 120, 0
|
||||
texthighlight = 136, 200, 0, 96
|
||||
shortcode = 153, 204, 153
|
||||
keyword = 230, 66, 80
|
||||
tag = 153, 204, 153
|
||||
value = 102, 153, 204
|
||||
optional = 153, 204, 153
|
||||
spellcheckline = 230, 66, 80
|
||||
errorline = 153, 204, 153
|
||||
replacetag = 153, 204, 153
|
||||
modifier = 153, 204, 153
|
||||
texthighlight = 255, 255, 132, 96
|
||||
|
||||
@@ -6,23 +6,37 @@ url = https://github.com/vkbo/novelWriter
|
||||
license = CC BY-SA 4.0
|
||||
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
|
||||
|
||||
##
|
||||
# Colours:
|
||||
# Background = ffffff : 255, 255, 255
|
||||
# Foreground = 000000 : 0, 0, 0
|
||||
# Comment = 646464 : 100, 100, 100
|
||||
# Red = c83232 : 200, 50, 50
|
||||
# Orange = ae6e1e : 174, 110, 30
|
||||
# Yellow = 969600 : 150, 150, 0
|
||||
# Green = 006400 : 0, 100, 0
|
||||
# Blue = 0000c8 : 0, 0, 200
|
||||
# Purple = 6400ae : 100, 0, 174
|
||||
##
|
||||
|
||||
[Syntax]
|
||||
background = 255, 255, 255
|
||||
text = 0, 0, 0
|
||||
link = 0, 0, 200
|
||||
headertext = 0, 100, 0
|
||||
headertag = 50, 100, 50
|
||||
emphasis = 150, 110, 30
|
||||
straightquotes = 200, 0, 0
|
||||
doublequotes = 0, 0, 200
|
||||
singlequotes = 0, 0, 200
|
||||
headertag = 0, 100, 0, 160
|
||||
emphasis = 174, 110, 30
|
||||
dialog = 0, 0, 200
|
||||
altdialog = 0, 0, 200
|
||||
note = 100, 100, 0
|
||||
hidden = 100, 100, 100
|
||||
shortcode = 0, 100, 0
|
||||
keyword = 200, 50, 50
|
||||
value = 50, 150, 50
|
||||
tag = 0, 100, 0
|
||||
value = 0, 0, 200
|
||||
optional = 0, 100, 0
|
||||
spellcheckline = 200, 0, 0
|
||||
errorline = 0, 150, 0
|
||||
replacetag = 0, 150, 0
|
||||
modifier = 150, 110, 30
|
||||
spellcheckline = 200, 50, 50
|
||||
errorline = 0, 100, 0
|
||||
replacetag = 0, 100, 0
|
||||
modifier = 0, 100, 0
|
||||
texthighlight = 150, 150, 0, 96
|
||||
|
||||
44
novelwriter/assets/syntax/dracula.conf
Normal file
44
novelwriter/assets/syntax/dracula.conf
Normal file
@@ -0,0 +1,44 @@
|
||||
[Main]
|
||||
name = Dracula
|
||||
author = Veronica Berglyd Olsen (adaptation)
|
||||
credit = Zeno Rocha
|
||||
url = https://draculatheme.com
|
||||
license = MIT
|
||||
licenseurl = https://github.com/dracula/dracula-theme/blob/main/LICENSE
|
||||
|
||||
##
|
||||
# Colours:
|
||||
# Background = 282a36 : 40, 42, 54
|
||||
# Foreground = f8f8f2 : 248, 248, 242
|
||||
# Comment = 6272a4 : 98, 114, 164
|
||||
# Cyan = 8be9fd : 139, 233, 253
|
||||
# Green = 50fa7b : 80, 250, 123
|
||||
# Orange = ffb86c : 255, 184, 108
|
||||
# Pink = ff79c6 : 255, 121, 198
|
||||
# Purple = bd93f9 : 189, 147, 249
|
||||
# Red = ff5555 : 255, 85, 85
|
||||
# Yellow = f1fa8c : 241, 250, 140
|
||||
##
|
||||
|
||||
[Syntax]
|
||||
# See: https://spec.draculatheme.com/
|
||||
background = 40, 42, 54
|
||||
text = 248, 248, 242
|
||||
link = 255, 121, 198
|
||||
headertext = 189, 147, 249
|
||||
headertag = 189, 147, 249, 160
|
||||
emphasis = 255, 184, 108
|
||||
dialog = 80, 250, 123
|
||||
altdialog = 241, 250, 140
|
||||
note = 255, 204, 233
|
||||
hidden = 98, 114, 164
|
||||
shortcode = 139, 233, 253
|
||||
keyword = 255, 121, 198
|
||||
tag = 255, 184, 108
|
||||
value = 255, 184, 108
|
||||
optional = 80, 250, 123
|
||||
spellcheckline = 255, 85, 85
|
||||
errorline = 80, 250, 123
|
||||
replacetag = 241, 250, 140
|
||||
modifier = 139, 233, 253
|
||||
texthighlight = 241, 250, 140, 96
|
||||
@@ -11,14 +11,15 @@ background = 54, 54, 54
|
||||
text = 200, 200, 200
|
||||
link = 200, 200, 200
|
||||
headertext = 225, 225, 225
|
||||
headertag = 225, 225, 225
|
||||
headertag = 225, 225, 225, 160
|
||||
emphasis = 200, 200, 200
|
||||
straightquotes = 200, 200, 200
|
||||
doublequotes = 200, 200, 200
|
||||
singlequotes = 200, 200, 200
|
||||
dialog = 200, 200, 200
|
||||
altdialog = 200, 200, 200
|
||||
note = 200, 200, 200
|
||||
hidden = 150, 150, 150
|
||||
shortcode = 225, 225, 225
|
||||
keyword = 225, 225, 225
|
||||
tag = 200, 200, 200
|
||||
value = 200, 200, 200
|
||||
optional = 225, 225, 225
|
||||
spellcheckline = 200, 46, 0
|
||||
|
||||
@@ -11,14 +11,15 @@ background = 255, 255, 255
|
||||
text = 20, 20, 20
|
||||
link = 20, 20, 20
|
||||
headertext = 0, 0, 0
|
||||
headertag = 0, 0, 0
|
||||
headertag = 0, 0, 0, 160
|
||||
emphasis = 20, 20, 20
|
||||
straightquotes = 20, 20, 20
|
||||
doublequotes = 20, 20, 20
|
||||
singlequotes = 20, 20, 20
|
||||
dialog = 20, 20, 20
|
||||
altdialog = 20, 20, 20
|
||||
note = 20, 20, 20
|
||||
hidden = 100, 100, 100
|
||||
shortcode = 0, 0, 0
|
||||
keyword = 0, 0, 0
|
||||
tag = 20, 20, 20
|
||||
value = 20, 20, 20
|
||||
optional = 0, 0, 0
|
||||
spellcheckline = 200, 0, 0
|
||||
|
||||
@@ -31,18 +31,19 @@ background = 251, 251, 251
|
||||
text = 64, 63, 82
|
||||
link = 40, 142, 215
|
||||
headertext = 40, 142, 215
|
||||
headertag = 40, 142, 215
|
||||
headertag = 40, 142, 215, 160
|
||||
emphasis = 224, 175, 5
|
||||
straightquotes = 222, 61, 58
|
||||
doublequotes = 8, 145, 106
|
||||
singlequotes = 255, 235, 149
|
||||
dialog = 8, 145, 106
|
||||
altdialog = 218, 170, 1
|
||||
note = 128, 118, 75
|
||||
hidden = 152, 159, 177
|
||||
shortcode = 40, 142, 215
|
||||
keyword = 222, 61, 58
|
||||
value = 150, 74, 193
|
||||
tag = 150, 74, 193
|
||||
value = 224, 175, 5
|
||||
optional = 40, 142, 215
|
||||
spellcheckline = 222, 61, 58
|
||||
errorline = 8, 145, 106
|
||||
replacetag = 42, 162, 152
|
||||
modifier = 224, 175, 5
|
||||
modifier = 8, 145, 106
|
||||
texthighlight = 218, 170, 1, 96
|
||||
|
||||
@@ -31,18 +31,19 @@ background = 1, 22, 39
|
||||
text = 214, 222, 235
|
||||
link = 130, 170, 255
|
||||
headertext = 130, 170, 255
|
||||
headertag = 130, 170, 255
|
||||
headertag = 130, 170, 255, 160
|
||||
emphasis = 236, 196, 141
|
||||
straightquotes = 247, 140, 108
|
||||
doublequotes = 173, 219, 103
|
||||
singlequotes = 255, 235, 149
|
||||
dialog = 173, 219, 103
|
||||
altdialog = 255, 235, 149
|
||||
note = 255, 249, 202
|
||||
hidden = 99, 119, 119
|
||||
shortcode = 130, 170, 255
|
||||
keyword = 247, 140, 108
|
||||
value = 199, 146, 234
|
||||
tag = 199, 146, 234
|
||||
value = 255, 235, 149
|
||||
optional = 130, 170, 255
|
||||
spellcheckline = 247, 140, 108
|
||||
errorline = 173, 219, 103
|
||||
replacetag = 127, 219, 202
|
||||
modifier = 236, 196, 141
|
||||
modifier = 173, 219, 103
|
||||
texthighlight = 255, 235, 149, 96
|
||||
|
||||
42
novelwriter/assets/syntax/snazzy.conf
Normal file
42
novelwriter/assets/syntax/snazzy.conf
Normal file
@@ -0,0 +1,42 @@
|
||||
[Main]
|
||||
name = Snazzy Light
|
||||
author = Veronica Berglyd Olsen (adaptation)
|
||||
credit = Florian Reuschel (color theme)
|
||||
url = https://github.com/loilo/vscode-snazzy-light
|
||||
license = MIT License
|
||||
licenseurl = https://github.com/loilo/vscode-snazzy-light/blob/master/LICENSE
|
||||
|
||||
##
|
||||
# Colours:
|
||||
# Background = fafbfc : 250, 251, 252
|
||||
# Foreground = 565869 : 86, 88, 105
|
||||
# Comment = 9194a2 : 145, 148, 162
|
||||
# Yellow = cf9c00 : 207, 156, 0
|
||||
# Red = ff5c57 : 255, 92, 87
|
||||
# Pink = f767bb : 247, 103, 187
|
||||
# Blue = 09a1ed : 9, 161, 237
|
||||
# Cyan = 13bbb7 : 19, 187, 183
|
||||
# Green = 2dae58 : 45, 174, 88
|
||||
##
|
||||
|
||||
[Syntax]
|
||||
background = 250, 251, 252
|
||||
text = 86, 88, 105
|
||||
link = 9, 161, 237
|
||||
headertext = 45, 174, 88
|
||||
headertag = 45, 174, 88, 160
|
||||
emphasis = 19, 187, 183
|
||||
dialog = 207, 156, 0
|
||||
altdialog = 207, 156, 0
|
||||
note = 120, 187, 185
|
||||
hidden = 145, 148, 162
|
||||
shortcode = 247, 103, 187
|
||||
keyword = 9, 161, 237
|
||||
tag = 45, 174, 88
|
||||
value = 9, 161, 237
|
||||
optional = 207, 156, 0
|
||||
spellcheckline = 255, 92, 87
|
||||
errorline = 45, 174, 88
|
||||
replacetag = 19, 187, 183
|
||||
modifier = 247, 103, 187
|
||||
texthighlight = 207, 156, 0, 96
|
||||
@@ -13,12 +13,13 @@ link = 38, 139, 210
|
||||
headertext = 147, 161, 161
|
||||
headertag = 42, 161, 152
|
||||
emphasis = 38, 139, 210
|
||||
straightquotes = 211, 54, 130
|
||||
doublequotes = 42, 161, 152
|
||||
singlequotes = 42, 161, 152
|
||||
dialog = 42, 161, 152
|
||||
altdialog = 42, 161, 152
|
||||
note = 101, 161, 156
|
||||
hidden = 147, 161, 161
|
||||
shortcode = 147, 161, 161
|
||||
keyword = 133, 153, 0
|
||||
tag = 203, 75, 22
|
||||
value = 203, 75, 22
|
||||
optional = 147, 161, 161
|
||||
spellcheckline = 203, 75, 22
|
||||
|
||||
@@ -13,12 +13,13 @@ link = 38, 139, 210
|
||||
headertext = 88, 110, 117
|
||||
headertag = 42, 161, 152
|
||||
emphasis = 38, 139, 210
|
||||
straightquotes = 211, 54, 130
|
||||
doublequotes = 42, 161, 152
|
||||
singlequotes = 42, 161, 152
|
||||
dialog = 42, 161, 152
|
||||
altdialog = 42, 161, 152
|
||||
note = 27, 102, 96
|
||||
hidden = 88, 110, 117
|
||||
shortcode = 88, 110, 117
|
||||
keyword = 133, 153, 0
|
||||
tag = 203, 75, 22
|
||||
value = 203, 75, 22
|
||||
optional = 88, 110, 117
|
||||
spellcheckline = 203, 75, 22
|
||||
|
||||
@@ -2,22 +2,38 @@
|
||||
name = Tango
|
||||
author = Veronica Berglyd Olsen (adaptation)
|
||||
|
||||
##
|
||||
# Colours:
|
||||
# Background = 30302f : 48, 48, 47
|
||||
# Foreground = eeeeec : 238, 238, 236
|
||||
# Comment = 8f8f8d : 143, 143, 141
|
||||
# Red = ef2929 : 239, 41, 41
|
||||
# Orange = c4a000 : 196, 160, 0
|
||||
# Yellow = fce94f : 252, 233, 79
|
||||
# Green = 8ae234 : 138, 226, 52
|
||||
# Cyan = 34e2e2 : 52, 226, 226
|
||||
# Blue = 3465a4 : 114, 159, 207
|
||||
# Purple = ad7fa8 : 173, 127, 168
|
||||
##
|
||||
|
||||
[Syntax]
|
||||
background = 48, 48, 47
|
||||
text = 238, 238, 236
|
||||
link = 115, 159, 207
|
||||
headertext = 115, 159, 207
|
||||
headertag = 52, 101, 164
|
||||
link = 114, 159, 207
|
||||
headertext = 114, 159, 207
|
||||
headertag = 114, 159, 207, 160
|
||||
emphasis = 196, 160, 0
|
||||
straightquotes = 239, 41, 41
|
||||
doublequotes = 138, 226, 52
|
||||
singlequotes = 252, 233, 79
|
||||
dialog = 138, 226, 52
|
||||
altdialog = 252, 233, 79
|
||||
note = 252, 242, 164
|
||||
hidden = 143, 143, 141
|
||||
shortcode = 115, 159, 207
|
||||
shortcode = 114, 159, 207
|
||||
keyword = 239, 41, 41
|
||||
value = 173, 127, 168
|
||||
optional = 115, 159, 207
|
||||
tag = 173, 127, 168
|
||||
value = 239, 41, 41
|
||||
optional = 114, 159, 207
|
||||
spellcheckline = 239, 41, 41
|
||||
errorline = 138, 226, 52
|
||||
replacetag = 51, 226, 226
|
||||
modifier = 196, 160, 0
|
||||
replacetag = 52, 226, 226
|
||||
modifier = 114, 159, 207
|
||||
texthighlight = 252, 233, 79, 96
|
||||
|
||||
@@ -31,15 +31,16 @@ background = 255, 255, 255
|
||||
text = 77, 77, 76
|
||||
link = 66, 113, 174
|
||||
headertext = 66, 113, 174
|
||||
headertag = 66, 113, 174
|
||||
headertag = 66, 113, 174, 160
|
||||
emphasis = 245, 135, 31
|
||||
straightquotes = 240, 40, 41
|
||||
doublequotes = 113, 140, 0
|
||||
singlequotes = 234, 183, 0
|
||||
dialog = 113, 140, 0
|
||||
altdialog = 234, 183, 0
|
||||
note = 115, 90, 0
|
||||
hidden = 142, 144, 140
|
||||
shortcode = 66, 113, 174
|
||||
keyword = 240, 40, 41
|
||||
value = 137, 89, 168
|
||||
tag = 137, 89, 168
|
||||
value = 234, 183, 0
|
||||
optional = 66, 113, 174
|
||||
spellcheckline = 240, 40, 41
|
||||
errorline = 113, 140, 0
|
||||
|
||||
@@ -29,17 +29,18 @@ licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.
|
||||
[Syntax]
|
||||
background = 29, 31, 33
|
||||
text = 197, 200, 198
|
||||
link = 129, 162, 190
|
||||
link = 197, 200, 198, 160
|
||||
headertext = 129, 162, 190
|
||||
headertag = 129, 162, 190
|
||||
headertag = 94, 119, 139
|
||||
emphasis = 222, 147, 95
|
||||
straightquotes = 204, 102, 102
|
||||
doublequotes = 181, 189, 104
|
||||
singlequotes = 240, 198, 116
|
||||
dialog = 181, 189, 104
|
||||
altdialog = 240, 198, 116
|
||||
note = 240, 219, 178
|
||||
hidden = 150, 152, 150
|
||||
shortcode = 129, 162, 190
|
||||
keyword = 204, 102, 102
|
||||
value = 178, 148, 187
|
||||
tag = 178, 148, 187
|
||||
value = 240, 198, 116
|
||||
optional = 129, 162, 190
|
||||
spellcheckline = 204, 102, 102
|
||||
errorline = 181, 189, 104
|
||||
|
||||
@@ -31,15 +31,16 @@ background = 0, 36, 81
|
||||
text = 255, 255, 255
|
||||
link = 187, 218, 255
|
||||
headertext = 187, 218, 255
|
||||
headertag = 187, 218, 255
|
||||
headertag = 187, 218, 255, 160
|
||||
emphasis = 255, 197, 143
|
||||
straightquotes = 255, 157, 164
|
||||
doublequotes = 209, 241, 169
|
||||
singlequotes = 255, 238, 173
|
||||
dialog = 209, 241, 169
|
||||
altdialog = 255, 238, 173
|
||||
note = 255, 247, 214
|
||||
hidden = 114, 133, 183
|
||||
shortcode = 187, 218, 255
|
||||
keyword = 255, 157, 164
|
||||
value = 235, 187, 255
|
||||
tag = 235, 187, 255
|
||||
value = 255, 238, 173
|
||||
optional = 187, 218, 255
|
||||
spellcheckline = 255, 157, 164
|
||||
errorline = 209, 241, 169
|
||||
|
||||
@@ -31,15 +31,16 @@ background = 0, 0, 0
|
||||
text = 234, 234, 234
|
||||
link = 122, 166, 218
|
||||
headertext = 122, 166, 218
|
||||
headertag = 122, 166, 218
|
||||
headertag = 122, 166, 218, 160
|
||||
emphasis = 231, 140, 69
|
||||
straightquotes = 213, 78, 83
|
||||
doublequotes = 185, 202, 74
|
||||
singlequotes = 231, 197, 71
|
||||
dialog = 185, 202, 74
|
||||
altdialog = 231, 197, 71
|
||||
note = 231, 214, 150
|
||||
hidden = 150, 152, 150
|
||||
shortcode = 122, 166, 218
|
||||
keyword = 213, 78, 83
|
||||
value = 195, 151, 216
|
||||
tag = 195, 151, 216
|
||||
value = 231, 197, 71
|
||||
optional = 122, 166, 218
|
||||
spellcheckline = 213, 78, 83
|
||||
errorline = 185, 202, 74
|
||||
|
||||
@@ -31,15 +31,16 @@ background = 45, 45, 45
|
||||
text = 204, 204, 204
|
||||
link = 102, 153, 204
|
||||
headertext = 102, 153, 204
|
||||
headertag = 102, 153, 204
|
||||
headertag = 102, 153, 204, 160
|
||||
emphasis = 249, 145, 57
|
||||
straightquotes = 242, 119, 122
|
||||
doublequotes = 153, 204, 153
|
||||
singlequotes = 255, 204, 102
|
||||
dialog = 153, 204, 153
|
||||
altdialog = 255, 204, 102
|
||||
note = 255, 230, 179
|
||||
hidden = 153, 153, 153
|
||||
shortcode = 102, 153, 204
|
||||
keyword = 242, 119, 122
|
||||
value = 204, 153, 204
|
||||
tag = 204, 153, 204
|
||||
value = 255, 204, 102
|
||||
optional = 102, 153, 204
|
||||
spellcheckline = 242, 119, 122
|
||||
errorline = 153, 204, 153
|
||||
|
||||
@@ -1,81 +1,92 @@
|
||||
<!DOCTYPE html public "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
|
||||
<html>
|
||||
<body>
|
||||
|
||||
<h3>Main Developer</h3>
|
||||
<p>Veronica Berglyd Olsen (<a href="https://github.com/vkbo">@vkbo</a>)</p>
|
||||
|
||||
<p>Veronica Berglyd Olsen</p>
|
||||
|
||||
<h3>Contributors</h3>
|
||||
|
||||
<p>
|
||||
• <b>Concept:</b> Marian Lückhof (<a href="https://github.com/Number042">@Number042</a>)<br>
|
||||
• <b>Internationalisation:</b> Bruno Meneguello (<a href="https://github.com/bkmeneguello">@bkmeneguello</a>)<br>
|
||||
• <b>Setup and Packaging:</b> Rachel Powers (<a href="https://github.com/Ryex">@Ryex</a>)
|
||||
</p>
|
||||
<ul>
|
||||
<li><b>Early Concept:</b> Marian Lückhof</li>
|
||||
<li><b>Internationalisation:</b> Bruno Meneguello</li>
|
||||
<li><b>Setup and Packaging:</b> Rachel Powers</li>
|
||||
</ul>
|
||||
|
||||
<p>For other contributions, see the project's
|
||||
<a href="https://github.com/vkbo/novelWriter/graphs/contributors">Contributors</a> page.</p>
|
||||
|
||||
<h3>Artwork</h3>
|
||||
|
||||
<p>The artwork on the Welcome dialog was created by <a href="https://louisdurrant.art">Louis Durrant</a>.</p>
|
||||
<p>The artwork on the Welcome dialog was created by Louis Durrant.</p>
|
||||
|
||||
<h3>Translations</h3>
|
||||
|
||||
<p>The default language is English (UK) with English (US) as an option. These are the original
|
||||
translators for the languages currently available:</p>
|
||||
<p>
|
||||
• <b>Dutch:</b> Martijn van der Kleijn (<a href="https://github.com/mvdkleijn">@mvdkleijn</a>)<br>
|
||||
• <b>French:</b> Jan Lüdke (<a href="https://github.com/jyhelle">@jyhelle</a>)<br>
|
||||
• <b>German:</b> Myian (<a href="https://github.com/heymyian">@heymyian</a>)<br>
|
||||
• <b>Italian:</b> Riccardo Mangili<br>
|
||||
• <b>Japanese:</b> hebekeg (<a href="https://github.com/hebekeg">@hebekeg</a>)<br>
|
||||
• <b>Latin American Spanish:</b> Tommy Marplatt (<a href="https://github.com/tmarplatt">@tmarplatt</a>)<br>
|
||||
• <b>Norwegian:</b> Veronica Berglyd Olsen (<a href="https://github.com/vkbo">@vkbo</a>)<br>
|
||||
• <b>Portuguese:</b> Bruno Meneguello (<a href="https://github.com/bkmeneguello">@bkmeneguello</a>)<br>
|
||||
• <b>Simplified Chinese:</b> Qianzhi Long (<a href="https://github.com/longqzh">@longqzh</a>)
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li><b>Dutch:</b> Martijn van der Kleijn (mvdkleijn)</li>
|
||||
<li><b>French:</b> Jan Lüdke (jyhelle)</li>
|
||||
<li><b>German:</b> Myian (HeyMyian)</li>
|
||||
<li><b>Italian:</b> Riccardo Mangili</li>
|
||||
<li><b>Japanese:</b> hebekeg</li>
|
||||
<li><b>Latin American Spanish:</b> Tommy Marplatt (tmarplatt)</li>
|
||||
<li><b>Norwegian:</b> Veronica Berglyd Olsen (vkbo)</li>
|
||||
<li><b>Polish:</b> Anna Maria Polak (Nauthiz)</li>
|
||||
<li><b>Portuguese:</b> Bruno Meneguello (bkmeneguello)</li>
|
||||
<li><b>Simplified Chinese:</b> Qianzhi Long (longqzh)</li>
|
||||
</ul>
|
||||
|
||||
<p>Additional larger translation contributions:</p>
|
||||
<p>
|
||||
• <b>French:</b> Albert Aribaud (<a href="https://github.com/aaribaud">@aaribaud</a>)
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li><b>French:</b> Albert Aribaud (aaribaud)</li>
|
||||
<li><b>Portuguese:</b> Oli Maia (olimaia)</li>
|
||||
</ul>
|
||||
|
||||
<p>Translations are managed on <a href="https://crowdin.com/project/novelwriter">Crowdin</a>, and
|
||||
more contributions are listed on the project's <a href="https://crowdin.com/project/novelwriter/members">Members</a> page.</p>
|
||||
more contributions are listed on the project's Members page.</p>
|
||||
|
||||
<h3>Libraries</h3>
|
||||
|
||||
<p>The following libraries are dependencies of novelWriter:</p>
|
||||
<p>
|
||||
• <a href="https://www.qt.io">Qt5</a> by Qt Company<br>
|
||||
• <a href="https://www.riverbankcomputing.com/software/pyqt">PyQt5</a> by Riverbank Computing<br>
|
||||
• <a href="https://abiword.github.io/enchant">Enchant</a> by Dom Lachowicz<br>
|
||||
• <a href="https://pyenchant.github.io/pyenchant">PyEnchant</a> by Dimitri Merejkowsky
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li><b>Qt5</b> by Qt Company</li>
|
||||
<li><b>PyQt5</b> by Riverbank Computing</li>
|
||||
<li><b>Enchant</b> by Dom Lachowicz</li>
|
||||
<li><b>PyEnchant</b> by Dimitri Merejkowsky</li>
|
||||
</ul>
|
||||
|
||||
<h3>Assets</h3>
|
||||
|
||||
<p>Some of the assets bundled with novelWriter were adapted from the following sources:</p>
|
||||
<p>
|
||||
• <a href="https://github.com/stephenhutchings/typicons.font">Typicons</a> icons by Stephen Hutchings (CC BY-SA 4.0)<br>
|
||||
• <a href="https://github.com/chriskempson/base16">Tomorrow</a> syntax themes by Chris Kempson (MIT License)<br>
|
||||
• <a href="https://github.com/sdras/night-owl-vscode-theme">Owl</a> syntax themes by Sarah Drasner (MIT License)<br>
|
||||
• <a href="https://github.com/altercation/solarized">Solarized</a> themes by Ethan Schoonover (MIT License)
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li><b>Typicons</b> icons by Stephen Hutchings (CC BY-SA 4.0)</li>
|
||||
<li><b>Tomorrow</b> syntax themes by Chris Kempson (MIT License)</li>
|
||||
<li><b>Owl</b> syntax themes by Sarah Drasner (MIT License)</li>
|
||||
<li><b>Solarized</b> themes by Ethan Schoonover (MIT License)</li>
|
||||
<li><b>Cyberpunk Night</b> theme by Anders Lemvigh (CC BY-SA 4.0)</li>
|
||||
<li><b>Dracula</b> theme by Zeno Rocha (MIT License)</li>
|
||||
<li><b>Snazzy Light</b> theme by Florian Reuschel (MIT License)</li>
|
||||
</ul>
|
||||
|
||||
<h3>Fonts</h3>
|
||||
|
||||
<p>The font used for the main novelWriter logo, mimetype and text banners is Pridi. Other fonts are
|
||||
used on buttons and icons.</p>
|
||||
<p>
|
||||
• Pridi by Cadson Demak (Open Font License, Version 1.1)<br>
|
||||
• Source Sans Pro by Paul D. Hunt (SIL Open Font License)
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li><b>Pridi</b> by Cadson Demak (Open Font License, Version 1.1)</li>
|
||||
<li><b>Source Sans Pro</b> by Paul D. Hunt (SIL Open Font License)</li>
|
||||
</ul>
|
||||
|
||||
<h3>Special Mentions</h3>
|
||||
|
||||
<p>Additional thanks to <a href="https://github.com/johnblommers">@johnblommers</a> who was an early user and who has provided a lot
|
||||
of very useful feedback over the years.</p>
|
||||
<p>Additional thanks to John Blommers who was an early user and who has provided a lot of very
|
||||
useful feedback over the years.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -24,6 +24,9 @@ link = 77, 77, 255
|
||||
linkvisited = 50, 0, 80
|
||||
|
||||
[GUI]
|
||||
helptext = 97, 97, 97
|
||||
fadedtext = 97, 97, 97
|
||||
errortext = 255, 77, 77
|
||||
statusnone = 50, 50, 50
|
||||
statussaved = 77, 255, 77
|
||||
statusunsaved = 255, 77, 77
|
||||
|
||||
@@ -26,6 +26,8 @@ linkvisited = 102, 153, 204
|
||||
|
||||
[GUI]
|
||||
helptext = 164, 164, 164
|
||||
fadedtext = 148, 148, 148
|
||||
errortext = 255, 164, 164
|
||||
statusnone = 150, 152, 150
|
||||
statussaved = 39, 135, 78
|
||||
statusunsaved = 138, 32, 32
|
||||
|
||||
@@ -26,6 +26,8 @@ linkvisited = 66, 113, 174
|
||||
|
||||
[GUI]
|
||||
helptext = 92, 92, 92
|
||||
fadedtext = 108, 108, 108
|
||||
errortext = 255, 92, 92
|
||||
statusnone = 120, 120, 120
|
||||
statussaved = 200, 15, 39
|
||||
statusunsaved = 2, 133, 37
|
||||
|
||||
48
novelwriter/assets/themes/dracula.conf
Normal file
48
novelwriter/assets/themes/dracula.conf
Normal file
@@ -0,0 +1,48 @@
|
||||
[Main]
|
||||
name = Dracula
|
||||
description = A dark theme with bright colours
|
||||
author = Veronica Berglyd Olsen (adaptation)
|
||||
credit = Zeno Rocha
|
||||
url = https://draculatheme.com
|
||||
license = MIT
|
||||
licenseurl = https://github.com/dracula/dracula-theme/blob/main/LICENSE
|
||||
icontheme = typicons_dark
|
||||
|
||||
##
|
||||
# Colours:
|
||||
# Background = 282a36 : 40, 42, 54
|
||||
# Foreground = f8f8f2 : 248, 248, 242
|
||||
# Current Line = 44475A : 68, 71, 90
|
||||
# Comment = 6272a4 : 98, 114, 164
|
||||
# Cyan = 8be9fd : 139, 233, 253
|
||||
# Green = 50fa7b : 80, 250, 123
|
||||
# Orange = ffb86c : 255, 184, 108
|
||||
# Pink = ff79c6 : 255, 121, 198
|
||||
# Purple = bd93f9 : 189, 147, 249
|
||||
# Red = ff5555 : 255, 85, 85
|
||||
# Yellow = f1fa8c : 241, 250, 140
|
||||
##
|
||||
|
||||
[Palette]
|
||||
window = 68, 71, 90
|
||||
windowtext = 248, 248, 242
|
||||
base = 40, 42, 54
|
||||
alternatebase = 51, 54, 69
|
||||
text = 248, 248, 242
|
||||
tooltipbase = 241, 250, 140
|
||||
tooltiptext = 40, 42, 54
|
||||
button = 80, 83, 105
|
||||
buttontext = 248, 248, 242
|
||||
brighttext = 68, 71, 90
|
||||
highlight = 166, 129, 218
|
||||
highlightedtext = 248, 248, 242
|
||||
link = 139, 233, 253
|
||||
linkvisited = 139, 233, 253
|
||||
|
||||
[GUI]
|
||||
helptext = 204, 172, 249
|
||||
fadedtext = 98, 114, 164
|
||||
errortext = 255, 85, 85
|
||||
statusnone = 98, 114, 164
|
||||
statussaved = 80, 250, 123
|
||||
statusunsaved = 255, 85, 85
|
||||
@@ -25,6 +25,8 @@ linkvisited = 38, 139, 210
|
||||
|
||||
[GUI]
|
||||
helptext = 166, 161, 149
|
||||
fadedtext = 166, 161, 149
|
||||
errortext = 255, 161, 149
|
||||
statusnone = 88, 110, 117
|
||||
statussaved = 42, 161, 152
|
||||
statusunsaved = 203, 75, 22
|
||||
|
||||
@@ -25,6 +25,8 @@ linkvisited = 38, 139, 210
|
||||
|
||||
[GUI]
|
||||
helptext = 78, 91, 95
|
||||
fadedtext = 78, 91, 95
|
||||
errortext = 255, 91, 95
|
||||
statusnone = 88, 110, 117
|
||||
statussaved = 42, 161, 152
|
||||
statusunsaved = 203, 75, 22
|
||||
|
||||
@@ -24,30 +24,32 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import uuid
|
||||
import logging
|
||||
import unicodedata
|
||||
import uuid
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from configparser import ConfigParser
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Literal, TypeVar
|
||||
from urllib.parse import urljoin
|
||||
from urllib.request import pathname2url
|
||||
|
||||
from PyQt5.QtGui import QColor, QDesktopServices
|
||||
from PyQt5.QtCore import QCoreApplication, QUrl
|
||||
from PyQt5.QtGui import QColor, QDesktopServices, QFont, QFontInfo
|
||||
|
||||
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
|
||||
from novelwriter.error import logException
|
||||
from novelwriter.constants import nwConst, nwLabels, nwUnicode, trConst
|
||||
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
|
||||
from novelwriter.error import logException
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from typing import TypeGuard # Requires Python 3.10
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_Type = TypeVar("_Type")
|
||||
|
||||
|
||||
##
|
||||
# Checker Functions
|
||||
@@ -172,6 +174,11 @@ def isItemLayout(value: Any) -> TypeGuard[str]:
|
||||
return isinstance(value, str) and value in nwItemLayout.__members__
|
||||
|
||||
|
||||
def isListInstance(data: Any, check: type[_Type]) -> TypeGuard[list[_Type]]:
|
||||
"""Check that all items of a list is of a given type."""
|
||||
return isinstance(data, list) and all(isinstance(item, check) for item in data)
|
||||
|
||||
|
||||
def hexToInt(value: Any, default: int = 0) -> int:
|
||||
"""Convert a hex string to an integer."""
|
||||
if isinstance(value, str):
|
||||
@@ -272,6 +279,23 @@ def simplified(text: str) -> str:
|
||||
return " ".join(str(text).strip().split())
|
||||
|
||||
|
||||
def compact(text: str) -> str:
|
||||
"""Compact a string by removing spaces."""
|
||||
return "".join(str(text).split())
|
||||
|
||||
|
||||
def uniqueCompact(text: str) -> str:
|
||||
"""Return a unique, compact and sorted string."""
|
||||
return "".join(sorted(set(compact(text))))
|
||||
|
||||
|
||||
def elide(text: str, length: int) -> str:
|
||||
"""Elide a piece of text to a maximum length."""
|
||||
if len(text) > (cut := max(4, length)):
|
||||
return f"{text[:cut-4].rstrip()} ..."
|
||||
return text
|
||||
|
||||
|
||||
def yesNo(value: int | bool | None) -> Literal["yes", "no"]:
|
||||
"""Convert a boolean evaluated variable to a yes or no."""
|
||||
return "yes" if value else "no"
|
||||
@@ -387,6 +411,16 @@ def cssCol(col: QColor, alpha: int | None = None) -> str:
|
||||
return f"rgba({col.red()}, {col.green()}, {col.blue()}, {alpha or col.alpha()})"
|
||||
|
||||
|
||||
def describeFont(font: QFont) -> str:
|
||||
"""Describe a font in a way that can be displayed on the GUI."""
|
||||
if isinstance(font, QFont):
|
||||
info = QFontInfo(font)
|
||||
family = info.family()
|
||||
styles = [v for v in info.styleName().split() if v not in family]
|
||||
return " ".join([f"{info.pointSize()} pt", family] + styles)
|
||||
return "Error"
|
||||
|
||||
|
||||
##
|
||||
# Encoder Functions
|
||||
##
|
||||
@@ -499,12 +533,11 @@ def readTextFile(path: str | Path) -> str:
|
||||
|
||||
|
||||
def makeFileNameSafe(text: str) -> str:
|
||||
"""Return a filename safe string.
|
||||
"""Return a filename-safe string.
|
||||
See: https://unicode.org/reports/tr15/#Norm_Forms
|
||||
"""
|
||||
text = unicodedata.normalize("NFKC", text).strip()
|
||||
allowed = (" ", ".", "-", "_")
|
||||
return "".join(c for c in text if c.isalnum() or c in allowed)
|
||||
return "".join(c for c in text if c.isprintable() and c not in r'\/:*?"<>|')
|
||||
|
||||
|
||||
def getFileSize(path: Path) -> int:
|
||||
@@ -589,5 +622,3 @@ class NWConfigParser(ConfigParser):
|
||||
for i in range(min(len(data), len(result))):
|
||||
result[i] = checkInt(data[i].strip(), result[i])
|
||||
return result
|
||||
|
||||
# END Class NWConfigParser
|
||||
|
||||
@@ -5,6 +5,7 @@ novelWriter – Config Class
|
||||
File History:
|
||||
Created: 2018-09-22 [0.0.1] Config
|
||||
Created: 2022-11-09 [2.0rc2] RecentProjects
|
||||
Created: 2024-06-16 [2.5rc1] RecentPaths
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2024, Veronica Berglyd Olsen
|
||||
@@ -24,24 +25,24 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from time import time
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from time import time
|
||||
|
||||
from PyQt5.QtGui import QFontDatabase
|
||||
from PyQt5.QtCore import (
|
||||
PYQT_VERSION, PYQT_VERSION_STR, QT_VERSION, QT_VERSION_STR, QLibraryInfo,
|
||||
QLocale, QStandardPaths, QSysInfo, QTranslator
|
||||
)
|
||||
from PyQt5.QtGui import QFont, QFontDatabase
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
from novelwriter.error import formatException, logException
|
||||
from novelwriter.common import NWConfigParser, checkInt, checkPath, formatTimeStamp
|
||||
from novelwriter.common import NWConfigParser, checkInt, checkPath, describeFont, formatTimeStamp
|
||||
from novelwriter.constants import nwFiles, nwUnicode
|
||||
from novelwriter.error import formatException, logException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -103,18 +104,19 @@ class Config:
|
||||
# User Settings
|
||||
# =============
|
||||
|
||||
self._recentObj = RecentProjects(self)
|
||||
self._recentProjects = RecentProjects(self)
|
||||
self._recentPaths = RecentPaths(self)
|
||||
|
||||
# General GUI Settings
|
||||
self.guiLocale = self._qLocale.name()
|
||||
self.guiTheme = "default" # GUI theme
|
||||
self.guiSyntax = "default_light" # Syntax theme
|
||||
self.guiFont = "" # Defaults to system default font in theme class
|
||||
self.guiFontSize = 11 # Is overridden if system default is loaded
|
||||
self.guiScale = 1.0 # Set automatically by Theme class
|
||||
self.hideVScroll = False # Hide vertical scroll bars on main widgets
|
||||
self.hideHScroll = False # Hide horizontal scroll bars on main widgets
|
||||
self.lastNotes = "0x0" # The latest release notes that have been shown
|
||||
self.guiLocale = self._qLocale.name()
|
||||
self.guiTheme = "default" # GUI theme
|
||||
self.guiSyntax = "default_light" # Syntax theme
|
||||
self.guiFont = QFont() # Main GUI font
|
||||
self.guiScale = 1.0 # Set automatically by Theme class
|
||||
self.hideVScroll = False # Hide vertical scroll bars on main widgets
|
||||
self.hideHScroll = False # Hide horizontal scroll bars on main widgets
|
||||
self.lastNotes = "0x0" # The latest release notes that have been shown
|
||||
self.nativeFont = True # Use native font dialog
|
||||
|
||||
# Size Settings
|
||||
self._mainWinSize = [1200, 650] # Last size of the main GUI window
|
||||
@@ -132,40 +134,42 @@ class Config:
|
||||
self.askBeforeBackup = True # Flag for asking before running automatic backup
|
||||
|
||||
# Text Editor Settings
|
||||
self.textFont = "" # Editor font
|
||||
self.textSize = 12 # Editor font size
|
||||
self.textWidth = 700 # Editor text width
|
||||
self.textMargin = 40 # Editor/viewer text margin
|
||||
self.tabWidth = 40 # Editor tabulator width
|
||||
self.textFont = QFont() # Editor font
|
||||
self.textWidth = 700 # 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 editor
|
||||
self.showLineEndings = False # Show line endings in editor
|
||||
self.showMultiSpaces = True # Highlight multiple spaces in the text
|
||||
self.doJustify = False # Justify text
|
||||
self.showTabsNSpaces = False # Show tabs and spaces in editor
|
||||
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.autoScroll = False # Typewriter-like scrolling
|
||||
self.autoScrollPos = 30 # Start point for typewriter-like scrolling
|
||||
self.scrollPastEnd = True # Scroll past end of document, and centre cursor
|
||||
self.autoScroll = False # Typewriter-like scrolling
|
||||
self.autoScrollPos = 30 # Start point for typewriter-like scrolling
|
||||
self.scrollPastEnd = True # Scroll past end of document, and centre cursor
|
||||
|
||||
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.dialogStyle = 2 # Quote type to use for dialogue
|
||||
self.allowOpenDial = True # Allow open-ended dialogue quotes
|
||||
self.narratorBreak = "" # Symbol to use for narrator break
|
||||
self.dialogLine = "" # Symbol to use for dialogue line
|
||||
self.altDialogOpen = "" # Alternative dialog symbol, open
|
||||
self.altDialogClose = "" # Alternative dialog symbol, close
|
||||
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.incNotesWCount = True # The status bar word count includes notes
|
||||
self.stopWhenIdle = True # Stop the status bar clock when the user is idle
|
||||
self.userIdleTime = 300 # Time of inactivity to consider user idle
|
||||
self.incNotesWCount = True # The status bar word count includes notes
|
||||
|
||||
# User-Selected Symbol Settings
|
||||
self.fmtApostrophe = nwUnicode.U_RSQUO
|
||||
@@ -178,7 +182,6 @@ class Config:
|
||||
self.fmtPadThin = False
|
||||
|
||||
# User Paths
|
||||
self._lastPath = self._homePath # The user's last used path
|
||||
self._backupPath = self._backPath # Backup path to use, can be none
|
||||
|
||||
# Spell Checking Settings
|
||||
@@ -251,7 +254,7 @@ class Config:
|
||||
|
||||
@property
|
||||
def recentProjects(self) -> RecentProjects:
|
||||
return self._recentObj
|
||||
return self._recentProjects
|
||||
|
||||
@property
|
||||
def mainWinSize(self) -> list[int]:
|
||||
@@ -341,7 +344,7 @@ class Config:
|
||||
self._outlnPanePos = [int(x/self.guiScale) for x in pos]
|
||||
return
|
||||
|
||||
def setLastPath(self, path: str | Path) -> None:
|
||||
def setLastPath(self, key: str, path: str | Path) -> None:
|
||||
"""Set the last used path. Only the folder is saved, so if the
|
||||
path is not a folder, the parent of the path is used instead.
|
||||
"""
|
||||
@@ -350,8 +353,7 @@ class Config:
|
||||
if not path.is_dir():
|
||||
path = path.parent
|
||||
if path.is_dir():
|
||||
self._lastPath = path
|
||||
logger.debug("Last path updated: %s" % self._lastPath)
|
||||
self._recentPaths.setPath(key, path)
|
||||
return
|
||||
|
||||
def setBackupPath(self, path: Path | str) -> None:
|
||||
@@ -359,23 +361,53 @@ class Config:
|
||||
self._backupPath = checkPath(path, self._backPath)
|
||||
return
|
||||
|
||||
def setTextFont(self, family: str | None, pointSize: int = 12) -> None:
|
||||
def setGuiFont(self, value: QFont | str | None) -> None:
|
||||
"""Update the GUI's font style from settings."""
|
||||
if isinstance(value, QFont):
|
||||
self.guiFont = value
|
||||
elif value and isinstance(value, str):
|
||||
self.guiFont = QFont()
|
||||
self.guiFont.fromString(value)
|
||||
else:
|
||||
font = QFont()
|
||||
fontDB = QFontDatabase()
|
||||
if self.osWindows and "Arial" in fontDB.families():
|
||||
# On Windows we default to Arial if possible
|
||||
font.setFamily("Arial")
|
||||
font.setPointSize(10)
|
||||
else:
|
||||
font = fontDB.systemFont(QFontDatabase.SystemFont.GeneralFont)
|
||||
self.guiFont = font
|
||||
logger.debug("GUI font set to: %s", describeFont(font))
|
||||
|
||||
QApplication.setFont(self.guiFont)
|
||||
|
||||
return
|
||||
|
||||
def setTextFont(self, value: QFont | str | None) -> None:
|
||||
"""Set the text font if it exists. If it doesn't, or is None,
|
||||
set to default font.
|
||||
"""
|
||||
fontDB = QFontDatabase()
|
||||
fontFam = fontDB.families()
|
||||
self.textSize = pointSize
|
||||
if family is None or family not in fontFam:
|
||||
logger.warning("Unknown font '%s'", family)
|
||||
if self.osWindows and "Arial" in fontFam:
|
||||
self.textFont = "Arial"
|
||||
elif self.osDarwin and "Helvetica" in fontFam:
|
||||
self.textFont = "Helvetica"
|
||||
else:
|
||||
self.textFont = fontDB.systemFont(QFontDatabase.SystemFont.GeneralFont).family()
|
||||
if isinstance(value, QFont):
|
||||
self.textFont = value
|
||||
elif value and isinstance(value, str):
|
||||
self.textFont = QFont()
|
||||
self.textFont.fromString(value)
|
||||
else:
|
||||
self.textFont = family
|
||||
fontDB = QFontDatabase()
|
||||
fontFam = fontDB.families()
|
||||
if self.osWindows and "Arial" in fontFam:
|
||||
font = QFont()
|
||||
font.setFamily("Arial")
|
||||
font.setPointSize(12)
|
||||
elif self.osDarwin and "Helvetica" in fontFam:
|
||||
font = QFont()
|
||||
font.setFamily("Helvetica")
|
||||
font.setPointSize(12)
|
||||
else:
|
||||
font = fontDB.systemFont(QFontDatabase.SystemFont.GeneralFont)
|
||||
self.textFont = font
|
||||
logger.debug("Text font set to: %s", describeFont(font))
|
||||
return
|
||||
|
||||
##
|
||||
@@ -406,11 +438,12 @@ class Config:
|
||||
return self._appPath / "assets" / target
|
||||
return self._appPath / "assets"
|
||||
|
||||
def lastPath(self) -> Path:
|
||||
def lastPath(self, key: str) -> Path:
|
||||
"""Return the last path used by the user, if it exists."""
|
||||
if isinstance(self._lastPath, Path):
|
||||
if self._lastPath.is_dir():
|
||||
return self._lastPath
|
||||
if path := self._recentPaths.getPath(key):
|
||||
asPath = Path(path)
|
||||
if asPath.is_dir():
|
||||
return asPath
|
||||
return self._homePath
|
||||
|
||||
def backupPath(self) -> Path:
|
||||
@@ -484,7 +517,6 @@ class Config:
|
||||
logger.debug("Data Path: %s", self._dataPath)
|
||||
logger.debug("App Root: %s", self._appRoot)
|
||||
logger.debug("App Path: %s", self._appPath)
|
||||
logger.debug("Last Path: %s", self._lastPath)
|
||||
logger.debug("PDF Manual: %s", self.pdfDocs)
|
||||
|
||||
# If the config and data folders don't exist, create them
|
||||
@@ -499,13 +531,8 @@ class Config:
|
||||
(self._dataPath / "syntax").mkdir(exist_ok=True)
|
||||
(self._dataPath / "themes").mkdir(exist_ok=True)
|
||||
|
||||
# Check if config file exists, and load it. If not, we save defaults
|
||||
if (self._confPath / nwFiles.CONF_FILE).is_file():
|
||||
self.loadConfig()
|
||||
else:
|
||||
self.saveConfig()
|
||||
|
||||
self._recentObj.loadCache()
|
||||
self._recentPaths.loadCache()
|
||||
self._recentProjects.loadCache()
|
||||
self._checkOptionalPackages()
|
||||
|
||||
logger.debug("Config instance initialised")
|
||||
@@ -545,6 +572,14 @@ class Config:
|
||||
|
||||
conf = NWConfigParser()
|
||||
cnfPath = self._confPath / nwFiles.CONF_FILE
|
||||
|
||||
if not cnfPath.exists():
|
||||
# Initial file, so we just create one from defaults
|
||||
self.setGuiFont(None)
|
||||
self.setTextFont(None)
|
||||
self.saveConfig()
|
||||
return True
|
||||
|
||||
try:
|
||||
with open(cnfPath, mode="r", encoding="utf-8") as inFile:
|
||||
conf.read_file(inFile)
|
||||
@@ -558,15 +593,14 @@ class Config:
|
||||
|
||||
# Main
|
||||
sec = "Main"
|
||||
self.setGuiFont(conf.rdStr(sec, "font", ""))
|
||||
self.guiTheme = conf.rdStr(sec, "theme", self.guiTheme)
|
||||
self.guiSyntax = conf.rdStr(sec, "syntax", self.guiSyntax)
|
||||
self.guiFont = conf.rdStr(sec, "font", self.guiFont)
|
||||
self.guiFontSize = conf.rdInt(sec, "fontsize", self.guiFontSize)
|
||||
self.guiLocale = conf.rdStr(sec, "localisation", self.guiLocale)
|
||||
self.hideVScroll = conf.rdBool(sec, "hidevscroll", self.hideVScroll)
|
||||
self.hideHScroll = conf.rdBool(sec, "hidehscroll", self.hideHScroll)
|
||||
self.lastNotes = conf.rdStr(sec, "lastnotes", self.lastNotes)
|
||||
self._lastPath = conf.rdPath(sec, "lastpath", self._lastPath)
|
||||
self.nativeFont = conf.rdBool(sec, "nativefont", self.nativeFont)
|
||||
|
||||
# Sizes
|
||||
sec = "Sizes"
|
||||
@@ -588,8 +622,7 @@ class Config:
|
||||
|
||||
# Editor
|
||||
sec = "Editor"
|
||||
self.textFont = conf.rdStr(sec, "textfont", self.textFont)
|
||||
self.textSize = conf.rdInt(sec, "textsize", self.textSize)
|
||||
self.setTextFont(conf.rdStr(sec, "textfont", ""))
|
||||
self.textWidth = conf.rdInt(sec, "width", self.textWidth)
|
||||
self.textMargin = conf.rdInt(sec, "margin", self.textMargin)
|
||||
self.tabWidth = conf.rdInt(sec, "tabwidth", self.tabWidth)
|
||||
@@ -618,9 +651,12 @@ class Config:
|
||||
self.showMultiSpaces = conf.rdBool(sec, "showmultispaces", self.showMultiSpaces)
|
||||
self.incNotesWCount = conf.rdBool(sec, "incnoteswcount", self.incNotesWCount)
|
||||
self.showFullPath = conf.rdBool(sec, "showfullpath", self.showFullPath)
|
||||
self.highlightQuotes = conf.rdBool(sec, "highlightquotes", self.highlightQuotes)
|
||||
self.allowOpenSQuote = conf.rdBool(sec, "allowopensquote", self.allowOpenSQuote)
|
||||
self.allowOpenDQuote = conf.rdBool(sec, "allowopendquote", self.allowOpenDQuote)
|
||||
self.dialogStyle = conf.rdInt(sec, "dialogstyle", self.dialogStyle)
|
||||
self.allowOpenDial = conf.rdBool(sec, "allowopendial", self.allowOpenDial)
|
||||
self.narratorBreak = conf.rdStr(sec, "narratorbreak", self.narratorBreak)
|
||||
self.altDialogOpen = conf.rdStr(sec, "altdialogopen", self.altDialogOpen)
|
||||
self.altDialogClose = conf.rdStr(sec, "altdialogclose", self.altDialogClose)
|
||||
self.dialogLine = conf.rdStr(sec, "dialogline", self.dialogLine)
|
||||
self.highlightEmph = conf.rdBool(sec, "highlightemph", self.highlightEmph)
|
||||
self.stopWhenIdle = conf.rdBool(sec, "stopwhenidle", self.stopWhenIdle)
|
||||
self.userIdleTime = conf.rdInt(sec, "useridletime", self.userIdleTime)
|
||||
@@ -666,15 +702,14 @@ class Config:
|
||||
}
|
||||
|
||||
conf["Main"] = {
|
||||
"font": self.guiFont.toString(),
|
||||
"theme": str(self.guiTheme),
|
||||
"syntax": str(self.guiSyntax),
|
||||
"font": str(self.guiFont),
|
||||
"fontsize": str(self.guiFontSize),
|
||||
"localisation": str(self.guiLocale),
|
||||
"hidevscroll": str(self.hideVScroll),
|
||||
"hidehscroll": str(self.hideHScroll),
|
||||
"lastnotes": str(self.lastNotes),
|
||||
"lastpath": str(self._lastPath),
|
||||
"nativefont": str(self.nativeFont),
|
||||
}
|
||||
|
||||
conf["Sizes"] = {
|
||||
@@ -696,8 +731,7 @@ class Config:
|
||||
}
|
||||
|
||||
conf["Editor"] = {
|
||||
"textfont": str(self.textFont),
|
||||
"textsize": str(self.textSize),
|
||||
"textfont": self.textFont.toString(),
|
||||
"width": str(self.textWidth),
|
||||
"margin": str(self.textMargin),
|
||||
"tabwidth": str(self.tabWidth),
|
||||
@@ -726,9 +760,12 @@ class Config:
|
||||
"showmultispaces": str(self.showMultiSpaces),
|
||||
"incnoteswcount": str(self.incNotesWCount),
|
||||
"showfullpath": str(self.showFullPath),
|
||||
"highlightquotes": str(self.highlightQuotes),
|
||||
"allowopensquote": str(self.allowOpenSQuote),
|
||||
"allowopendquote": str(self.allowOpenDQuote),
|
||||
"dialogstyle": str(self.dialogStyle),
|
||||
"allowopendial": str(self.allowOpenDial),
|
||||
"narratorbreak": str(self.narratorBreak),
|
||||
"altdialogopen": str(self.altDialogOpen),
|
||||
"altdialogclose": str(self.altDialogClose),
|
||||
"dialogline": str(self.dialogLine),
|
||||
"highlightemph": str(self.highlightEmph),
|
||||
"stopwhenidle": str(self.stopWhenIdle),
|
||||
"useridletime": str(self.userIdleTime),
|
||||
@@ -773,7 +810,7 @@ class Config:
|
||||
"""Pack a list of items into a comma-separated string for saving
|
||||
to the config file.
|
||||
"""
|
||||
return ", ".join([str(inVal) for inVal in data])
|
||||
return ", ".join(str(inVal) for inVal in data)
|
||||
|
||||
def _checkOptionalPackages(self) -> None:
|
||||
"""Check optional packages used by some features."""
|
||||
@@ -787,8 +824,6 @@ class Config:
|
||||
logger.debug("Checking package 'pyenchant': OK")
|
||||
return
|
||||
|
||||
# END Class Config
|
||||
|
||||
|
||||
class RecentProjects:
|
||||
|
||||
@@ -858,4 +893,55 @@ class RecentProjects:
|
||||
self.saveCache()
|
||||
return
|
||||
|
||||
# END Class RecentProjects
|
||||
|
||||
class RecentPaths:
|
||||
|
||||
KEYS = ["default", "project", "import", "outline", "stats"]
|
||||
|
||||
def __init__(self, config: Config) -> None:
|
||||
self._conf = config
|
||||
self._data = {}
|
||||
return
|
||||
|
||||
def setPath(self, key: str, path: Path | str) -> None:
|
||||
"""Set a path for a given key, and save the cache."""
|
||||
if key in self.KEYS:
|
||||
self._data[key] = str(path)
|
||||
self.saveCache()
|
||||
return
|
||||
|
||||
def getPath(self, key: str) -> str | None:
|
||||
"""Get a path for a given key, or return None."""
|
||||
return self._data.get(key)
|
||||
|
||||
def loadCache(self) -> bool:
|
||||
"""Load the cache file for recent paths."""
|
||||
self._data = {}
|
||||
cacheFile = self._conf.dataPath(nwFiles.RECENT_PATH)
|
||||
if cacheFile.is_file():
|
||||
try:
|
||||
with open(cacheFile, mode="r", encoding="utf-8") as inFile:
|
||||
data = json.load(inFile)
|
||||
if isinstance(data, dict):
|
||||
for key, path in data.items():
|
||||
if key in self.KEYS and isinstance(path, str):
|
||||
self._data[key] = path
|
||||
except Exception:
|
||||
logger.error("Could not load recent paths cache")
|
||||
logException()
|
||||
return False
|
||||
return True
|
||||
|
||||
def saveCache(self) -> bool:
|
||||
"""Save the cache dictionary of recent paths."""
|
||||
cacheFile = self._conf.dataPath(nwFiles.RECENT_PATH)
|
||||
cacheTemp = cacheFile.with_suffix(".tmp")
|
||||
try:
|
||||
with open(cacheTemp, mode="w+", encoding="utf-8") as outFile:
|
||||
json.dump(self._data, outFile, indent=2)
|
||||
cacheTemp.replace(cacheFile)
|
||||
except Exception:
|
||||
logger.error("Could not save recent paths cache")
|
||||
logException()
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -23,9 +23,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP
|
||||
from PyQt5.QtCore import QT_TRANSLATE_NOOP, QCoreApplication
|
||||
|
||||
from novelwriter.enum import nwBuildFmt, nwItemClass, nwItemLayout, nwOutline
|
||||
from novelwriter.enum import (
|
||||
nwBuildFmt, nwComment, nwItemClass, nwItemLayout, nwOutline, nwStatusShape
|
||||
)
|
||||
|
||||
|
||||
def trConst(text: str) -> str:
|
||||
@@ -55,21 +57,14 @@ class nwConst:
|
||||
STATUS_MSG_TIMEOUT = 15000 # milliseconds
|
||||
MAX_SEARCH_RESULT = 1000
|
||||
|
||||
# Dialogs
|
||||
DLG_FINISHED = 2
|
||||
|
||||
# END Class nwConst
|
||||
|
||||
|
||||
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)"
|
||||
FMT_SC = r"(?i)(?<!\\)(\[[\/\!]?(?:i|b|s|u|m|sup|sub)\])"
|
||||
FMT_SV = r"(?<!\\)(\[(?i)(?:fn|footnote):)(.+?)(?<!\\)(\])"
|
||||
|
||||
# END Class nwRegEx
|
||||
FMT_EB = r"(?<![\w\\])(\*{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
|
||||
FMT_ST = r"(?<![\w\\])(~{2})(?![\s~])(.+?)(?<![\s\\])(\1)(?!\w)"
|
||||
FMT_SC = r"(?i)(?<!\\)(\[[\/\!]?(?:b|i|s|u|m|sup|sub)\])"
|
||||
FMT_SV = r"(?i)(?<!\\)(\[(?:footnote):)(.+?)(?<!\\)(\])"
|
||||
|
||||
|
||||
class nwShortcode:
|
||||
@@ -89,15 +84,19 @@ class nwShortcode:
|
||||
SUB_O = "[sub]"
|
||||
SUB_C = "[/sub]"
|
||||
|
||||
# END Class nwShortcode
|
||||
FOOTNOTE_B = "[footnote:"
|
||||
|
||||
COMMENT_STYLES = {
|
||||
nwComment.FOOTNOTE: "[footnote:{0}]",
|
||||
nwComment.COMMENT: "[comment:{0}]",
|
||||
}
|
||||
|
||||
|
||||
class nwHeaders:
|
||||
|
||||
H_VALID = ("H0", "H1", "H2", "H3", "H4")
|
||||
H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4}
|
||||
|
||||
# END Class nwHeaders
|
||||
H_SIZES = {0: 2.50, 1: 2.00, 2: 1.75, 3: 1.50, 4: 1.25}
|
||||
|
||||
|
||||
class nwFiles:
|
||||
@@ -105,6 +104,7 @@ class nwFiles:
|
||||
# Config Files
|
||||
CONF_FILE = "novelwriter.conf"
|
||||
RECENT_FILE = "recentProjects.json"
|
||||
RECENT_PATH = "recentPaths.json"
|
||||
|
||||
# Project Root Files
|
||||
PROJ_FILE = "nwProject.nwx"
|
||||
@@ -118,8 +118,6 @@ class nwFiles:
|
||||
DICT_FILE = "userdict.json"
|
||||
SESS_FILE = "sessions.jsonl"
|
||||
|
||||
# END Class nwFiles
|
||||
|
||||
|
||||
class nwKeyWords:
|
||||
|
||||
@@ -153,8 +151,6 @@ class nwKeyWords:
|
||||
CUSTOM_KEY: nwItemClass.CUSTOM,
|
||||
}
|
||||
|
||||
# END Class nwKeyWords
|
||||
|
||||
|
||||
class nwLists:
|
||||
|
||||
@@ -168,8 +164,6 @@ class nwLists:
|
||||
nwItemClass.CUSTOM,
|
||||
]
|
||||
|
||||
# END Class nwLists
|
||||
|
||||
|
||||
class nwLabels:
|
||||
|
||||
@@ -268,6 +262,34 @@ class nwLabels:
|
||||
nwBuildFmt.J_HTML: ".json",
|
||||
nwBuildFmt.J_NWD: ".json",
|
||||
}
|
||||
SHAPES_PLAIN = {
|
||||
nwStatusShape.SQUARE: QT_TRANSLATE_NOOP("Constant", "Square"),
|
||||
nwStatusShape.TRIANGLE: QT_TRANSLATE_NOOP("Constant", "Triangle"),
|
||||
nwStatusShape.NABLA: QT_TRANSLATE_NOOP("Constant", "Nabla"),
|
||||
nwStatusShape.DIAMOND: QT_TRANSLATE_NOOP("Constant", "Diamond"),
|
||||
nwStatusShape.PENTAGON: QT_TRANSLATE_NOOP("Constant", "Pentagon"),
|
||||
nwStatusShape.HEXAGON: QT_TRANSLATE_NOOP("Constant", "Hexagon"),
|
||||
nwStatusShape.STAR: QT_TRANSLATE_NOOP("Constant", "Star"),
|
||||
nwStatusShape.PACMAN: QT_TRANSLATE_NOOP("Constant", "Pacman"),
|
||||
}
|
||||
SHAPES_CIRCLE = {
|
||||
nwStatusShape.CIRCLE_Q: QT_TRANSLATE_NOOP("Constant", "1/4 Circle"),
|
||||
nwStatusShape.CIRCLE_H: QT_TRANSLATE_NOOP("Constant", "Half Circle"),
|
||||
nwStatusShape.CIRCLE_T: QT_TRANSLATE_NOOP("Constant", "3/4 Circle"),
|
||||
nwStatusShape.CIRCLE: QT_TRANSLATE_NOOP("Constant", "Full Circle"),
|
||||
}
|
||||
SHAPES_BARS = {
|
||||
nwStatusShape.BARS_1: QT_TRANSLATE_NOOP("Constant", "1 Bar"),
|
||||
nwStatusShape.BARS_2: QT_TRANSLATE_NOOP("Constant", "2 Bars"),
|
||||
nwStatusShape.BARS_3: QT_TRANSLATE_NOOP("Constant", "3 Bars"),
|
||||
nwStatusShape.BARS_4: QT_TRANSLATE_NOOP("Constant", "4 Bars"),
|
||||
}
|
||||
SHAPES_BLOCKS = {
|
||||
nwStatusShape.BLOCK_1: QT_TRANSLATE_NOOP("Constant", "1 Block"),
|
||||
nwStatusShape.BLOCK_2: QT_TRANSLATE_NOOP("Constant", "2 Blocks"),
|
||||
nwStatusShape.BLOCK_3: QT_TRANSLATE_NOOP("Constant", "3 Blocks"),
|
||||
nwStatusShape.BLOCK_4: QT_TRANSLATE_NOOP("Constant", "4 Blocks"),
|
||||
}
|
||||
FILE_FILTERS = {
|
||||
"*.txt": QT_TRANSLATE_NOOP("Constant", "Text files"),
|
||||
"*.md": QT_TRANSLATE_NOOP("Constant", "Markdown files"),
|
||||
@@ -302,8 +324,6 @@ class nwLabels:
|
||||
"Custom": (-1.0, -1.0),
|
||||
}
|
||||
|
||||
# END Class nwLabels
|
||||
|
||||
|
||||
class nwHeadFmt:
|
||||
|
||||
@@ -329,8 +349,6 @@ class nwHeadFmt:
|
||||
ODT_PAGE = "{Page}"
|
||||
ODT_AUTO = "{Project} / {Author} / {Page}"
|
||||
|
||||
# END Class nwHeadFmt
|
||||
|
||||
|
||||
class nwQuotes:
|
||||
"""Allowed quotation marks.
|
||||
@@ -361,8 +379,6 @@ class nwQuotes:
|
||||
"\u300f": QT_TRANSLATE_NOOP("Constant", "Right white corner bracket"),
|
||||
}
|
||||
|
||||
# END Class nwQuotes
|
||||
|
||||
|
||||
class nwUnicode:
|
||||
"""Supported unicode character constants and their HTML equivalents."""
|
||||
@@ -396,7 +412,7 @@ class nwUnicode:
|
||||
U_EMDASH = "\u2014" # Long dash
|
||||
U_HBAR = "\u2015" # Horizontal bar
|
||||
U_HELLIP = "\u2026" # Ellipsis
|
||||
U_MAPOSS = "\u02bc" # Modifier letter single apostrophe
|
||||
U_MAPOS = "\u02bc" # Modifier letter single apostrophe
|
||||
U_PRIME = "\u2032" # Prime
|
||||
U_DPRIME = "\u2033" # Double prime
|
||||
|
||||
@@ -463,7 +479,7 @@ class nwUnicode:
|
||||
H_EMDASH = "—"
|
||||
H_HBAR = "―"
|
||||
H_HELLIP = "…"
|
||||
H_MAPOSS = "ʼ"
|
||||
H_MAPOS = "ʼ"
|
||||
H_PRIME = "′"
|
||||
H_DPRIME = "″"
|
||||
|
||||
@@ -497,8 +513,6 @@ class nwUnicode:
|
||||
H_LTRI = "◀"
|
||||
H_LTRIS = "◂"
|
||||
|
||||
# END Class nwUnicode
|
||||
|
||||
|
||||
class nwHtmlUnicode():
|
||||
|
||||
@@ -530,7 +544,7 @@ class nwHtmlUnicode():
|
||||
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_MAPOS: nwUnicode.H_MAPOS,
|
||||
nwUnicode.U_PRIME: nwUnicode.H_PRIME,
|
||||
nwUnicode.U_DPRIME: nwUnicode.H_DPRIME,
|
||||
|
||||
@@ -554,5 +568,3 @@ class nwHtmlUnicode():
|
||||
nwUnicode.U_TIMES: nwUnicode.H_TIMES,
|
||||
nwUnicode.U_DIVIDE: nwUnicode.H_DIVIDE,
|
||||
}
|
||||
|
||||
# END Class nwHtmlUnicode
|
||||
|
||||
@@ -25,21 +25,21 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import uuid
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from collections.abc import Iterable
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from collections.abc import Iterable
|
||||
|
||||
from PyQt5.QtCore import QT_TRANSLATE_NOOP, QCoreApplication
|
||||
|
||||
from novelwriter import CONFIG
|
||||
from novelwriter.enum import nwBuildFmt
|
||||
from novelwriter.error import logException
|
||||
from novelwriter.common import checkUuid, isHandle, jsonEncode
|
||||
from novelwriter.constants import nwFiles, nwHeadFmt
|
||||
from novelwriter.core.project import NWProject
|
||||
from novelwriter.enum import nwBuildFmt
|
||||
from novelwriter.error import logException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -76,12 +76,16 @@ SETTINGS_TEMPLATE = {
|
||||
"text.includeBodyText": (bool, True),
|
||||
"text.ignoredKeywords": (str, ""),
|
||||
"text.addNoteHeadings": (bool, True),
|
||||
"format.textFont": (str, CONFIG.textFont),
|
||||
"format.textSize": (int, 12),
|
||||
"format.textFont": (str, CONFIG.textFont.toString()),
|
||||
"format.lineHeight": (float, 1.15, 0.75, 3.0),
|
||||
"format.justifyText": (bool, False),
|
||||
"format.stripUnicode": (bool, False),
|
||||
"format.replaceTabs": (bool, False),
|
||||
"format.keepBreaks": (bool, True),
|
||||
"format.showDialogue": (bool, False),
|
||||
"format.firstLineIndent": (bool, False),
|
||||
"format.firstIndentWidth": (float, 1.4),
|
||||
"format.indentFirstPar": (bool, False),
|
||||
"format.pageUnit": (str, "cm"),
|
||||
"format.pageSize": (str, "A4"),
|
||||
"format.pageWidth": (float, 21.0),
|
||||
@@ -93,65 +97,66 @@ SETTINGS_TEMPLATE = {
|
||||
"odt.addColours": (bool, True),
|
||||
"odt.pageHeader": (str, nwHeadFmt.ODT_AUTO),
|
||||
"odt.pageCountOffset": (int, 0),
|
||||
"odt.firstLineIndent": (bool, False),
|
||||
"md.preserveBreaks": (bool, True),
|
||||
"html.addStyles": (bool, True),
|
||||
"html.preserveTabs": (bool, False),
|
||||
}
|
||||
|
||||
SETTINGS_LABELS = {
|
||||
"filter": QT_TRANSLATE_NOOP("Builds", "Document Filters"),
|
||||
"filter.includeNovel": QT_TRANSLATE_NOOP("Builds", "Novel Documents"),
|
||||
"filter.includeNotes": QT_TRANSLATE_NOOP("Builds", "Project Notes"),
|
||||
"filter.includeInactive": QT_TRANSLATE_NOOP("Builds", "Inactive Documents"),
|
||||
"filter": QT_TRANSLATE_NOOP("Builds", "Document Filters"),
|
||||
"filter.includeNovel": QT_TRANSLATE_NOOP("Builds", "Novel Documents"),
|
||||
"filter.includeNotes": QT_TRANSLATE_NOOP("Builds", "Project Notes"),
|
||||
"filter.includeInactive": QT_TRANSLATE_NOOP("Builds", "Inactive Documents"),
|
||||
|
||||
"headings": QT_TRANSLATE_NOOP("Builds", "Headings"),
|
||||
"headings.fmtTitle": QT_TRANSLATE_NOOP("Builds", "Partition Format"),
|
||||
"headings.fmtChapter": QT_TRANSLATE_NOOP("Builds", "Chapter Format"),
|
||||
"headings.fmtUnnumbered": QT_TRANSLATE_NOOP("Builds", "Unnumbered Format"),
|
||||
"headings.fmtScene": QT_TRANSLATE_NOOP("Builds", "Scene Format"),
|
||||
"headings.fmtAltScene": QT_TRANSLATE_NOOP("Builds", "Alt. Scene Format"),
|
||||
"headings.fmtSection": QT_TRANSLATE_NOOP("Builds", "Section Format"),
|
||||
"headings": QT_TRANSLATE_NOOP("Builds", "Headings"),
|
||||
"headings.fmtTitle": QT_TRANSLATE_NOOP("Builds", "Partition Format"),
|
||||
"headings.fmtChapter": QT_TRANSLATE_NOOP("Builds", "Chapter Format"),
|
||||
"headings.fmtUnnumbered": QT_TRANSLATE_NOOP("Builds", "Unnumbered Format"),
|
||||
"headings.fmtScene": QT_TRANSLATE_NOOP("Builds", "Scene Format"),
|
||||
"headings.fmtAltScene": QT_TRANSLATE_NOOP("Builds", "Alt. Scene Format"),
|
||||
"headings.fmtSection": QT_TRANSLATE_NOOP("Builds", "Section Format"),
|
||||
|
||||
"text.grpContent": QT_TRANSLATE_NOOP("Builds", "Text Content"),
|
||||
"text.includeSynopsis": QT_TRANSLATE_NOOP("Builds", "Include Synopsis"),
|
||||
"text.includeComments": QT_TRANSLATE_NOOP("Builds", "Include Comments"),
|
||||
"text.includeKeywords": QT_TRANSLATE_NOOP("Builds", "Include Keywords"),
|
||||
"text.includeBodyText": QT_TRANSLATE_NOOP("Builds", "Include Body Text"),
|
||||
"text.ignoredKeywords": QT_TRANSLATE_NOOP("Builds", "Ignore These Keywords"),
|
||||
"text.grpInsert": QT_TRANSLATE_NOOP("Builds", "Insert Content"),
|
||||
"text.addNoteHeadings": QT_TRANSLATE_NOOP("Builds", "Add Titles for Notes"),
|
||||
"text.grpContent": QT_TRANSLATE_NOOP("Builds", "Text Content"),
|
||||
"text.includeSynopsis": QT_TRANSLATE_NOOP("Builds", "Include Synopsis"),
|
||||
"text.includeComments": QT_TRANSLATE_NOOP("Builds", "Include Comments"),
|
||||
"text.includeKeywords": QT_TRANSLATE_NOOP("Builds", "Include Keywords"),
|
||||
"text.includeBodyText": QT_TRANSLATE_NOOP("Builds", "Include Body Text"),
|
||||
"text.ignoredKeywords": QT_TRANSLATE_NOOP("Builds", "Ignore These Keywords"),
|
||||
"text.grpInsert": QT_TRANSLATE_NOOP("Builds", "Insert Content"),
|
||||
"text.addNoteHeadings": QT_TRANSLATE_NOOP("Builds", "Add Titles for Notes"),
|
||||
|
||||
"format.grpFormat": QT_TRANSLATE_NOOP("Builds", "Text Format"),
|
||||
"format.textFont": QT_TRANSLATE_NOOP("Builds", "Font Family"),
|
||||
"format.textSize": QT_TRANSLATE_NOOP("Builds", "Font Size"),
|
||||
"format.lineHeight": QT_TRANSLATE_NOOP("Builds", "Line Height"),
|
||||
"format.grpOptions": QT_TRANSLATE_NOOP("Builds", "Text Options"),
|
||||
"format.justifyText": QT_TRANSLATE_NOOP("Builds", "Justify Text Margins"),
|
||||
"format.stripUnicode": QT_TRANSLATE_NOOP("Builds", "Replace Unicode Characters"),
|
||||
"format.replaceTabs": QT_TRANSLATE_NOOP("Builds", "Replace Tabs with Spaces"),
|
||||
"format.grpPage": QT_TRANSLATE_NOOP("Builds", "Page Layout"),
|
||||
"format.pageUnit": QT_TRANSLATE_NOOP("Builds", "Unit"),
|
||||
"format.pageSize": QT_TRANSLATE_NOOP("Builds", "Page Size"),
|
||||
"format.pageWidth": QT_TRANSLATE_NOOP("Builds", "Page Width"),
|
||||
"format.pageHeight": QT_TRANSLATE_NOOP("Builds", "Page Height"),
|
||||
"format.topMargin": QT_TRANSLATE_NOOP("Builds", "Top Margin"),
|
||||
"format.bottomMargin": QT_TRANSLATE_NOOP("Builds", "Bottom Margin"),
|
||||
"format.leftMargin": QT_TRANSLATE_NOOP("Builds", "Left Margin"),
|
||||
"format.rightMargin": QT_TRANSLATE_NOOP("Builds", "Right Margin"),
|
||||
"format.grpFormat": QT_TRANSLATE_NOOP("Builds", "Text Format"),
|
||||
"format.textFont": QT_TRANSLATE_NOOP("Builds", "Text Font"),
|
||||
"format.lineHeight": QT_TRANSLATE_NOOP("Builds", "Line Height"),
|
||||
"format.grpOptions": QT_TRANSLATE_NOOP("Builds", "Text Options"),
|
||||
"format.justifyText": QT_TRANSLATE_NOOP("Builds", "Justify Text Margins"),
|
||||
"format.stripUnicode": QT_TRANSLATE_NOOP("Builds", "Replace Unicode Characters"),
|
||||
"format.replaceTabs": QT_TRANSLATE_NOOP("Builds", "Replace Tabs with Spaces"),
|
||||
"format.keepBreaks": QT_TRANSLATE_NOOP("Builds", "Preserve Hard Line Breaks"),
|
||||
"format.showDialogue": QT_TRANSLATE_NOOP("Builds", "Apply Dialogue Highlighting"),
|
||||
|
||||
"odt": QT_TRANSLATE_NOOP("Builds", "Open Document (.odt)"),
|
||||
"odt.addColours": QT_TRANSLATE_NOOP("Builds", "Add Highlight Colours"),
|
||||
"odt.pageHeader": QT_TRANSLATE_NOOP("Builds", "Page Header"),
|
||||
"odt.pageCountOffset": QT_TRANSLATE_NOOP("Builds", "Page Counter Offset"),
|
||||
"odt.firstLineIndent": QT_TRANSLATE_NOOP("Builds", "First Line Indent"),
|
||||
"format.grpParIndent": QT_TRANSLATE_NOOP("Builds", "First Line Indent"),
|
||||
"format.firstLineIndent": QT_TRANSLATE_NOOP("Builds", "Enable Indent"),
|
||||
"format.firstIndentWidth": QT_TRANSLATE_NOOP("Builds", "Indent Width"),
|
||||
"format.indentFirstPar": QT_TRANSLATE_NOOP("Builds", "Indent First Paragraph"),
|
||||
|
||||
"md": QT_TRANSLATE_NOOP("Builds", "Markdown (.md)"),
|
||||
"md.preserveBreaks": QT_TRANSLATE_NOOP("Builds", "Preserve Hard Line Breaks"),
|
||||
"format.grpPage": QT_TRANSLATE_NOOP("Builds", "Page Layout"),
|
||||
"format.pageUnit": QT_TRANSLATE_NOOP("Builds", "Unit"),
|
||||
"format.pageSize": QT_TRANSLATE_NOOP("Builds", "Page Size"),
|
||||
"format.pageWidth": QT_TRANSLATE_NOOP("Builds", "Page Width"),
|
||||
"format.pageHeight": QT_TRANSLATE_NOOP("Builds", "Page Height"),
|
||||
"format.topMargin": QT_TRANSLATE_NOOP("Builds", "Top Margin"),
|
||||
"format.bottomMargin": QT_TRANSLATE_NOOP("Builds", "Bottom Margin"),
|
||||
"format.leftMargin": QT_TRANSLATE_NOOP("Builds", "Left Margin"),
|
||||
"format.rightMargin": QT_TRANSLATE_NOOP("Builds", "Right Margin"),
|
||||
|
||||
"html": QT_TRANSLATE_NOOP("Builds", "HTML (.html)"),
|
||||
"html.addStyles": QT_TRANSLATE_NOOP("Builds", "Add CSS Styles"),
|
||||
"html.preserveTabs": QT_TRANSLATE_NOOP("Builds", "Preserve Tab Characters"),
|
||||
"odt": QT_TRANSLATE_NOOP("Builds", "Open Document (.odt)"),
|
||||
"odt.addColours": QT_TRANSLATE_NOOP("Builds", "Add Highlight Colours"),
|
||||
"odt.pageHeader": QT_TRANSLATE_NOOP("Builds", "Page Header"),
|
||||
"odt.pageCountOffset": QT_TRANSLATE_NOOP("Builds", "Page Counter Offset"),
|
||||
|
||||
"html": QT_TRANSLATE_NOOP("Builds", "HTML (.html)"),
|
||||
"html.addStyles": QT_TRANSLATE_NOOP("Builds", "Add CSS Styles"),
|
||||
"html.preserveTabs": QT_TRANSLATE_NOOP("Builds", "Preserve Tab Characters"),
|
||||
}
|
||||
|
||||
|
||||
@@ -165,8 +170,6 @@ class FilterMode(Enum):
|
||||
SKIPPED = 4
|
||||
ROOT = 5
|
||||
|
||||
# END Enum FilterMode
|
||||
|
||||
|
||||
class BuildSettings:
|
||||
"""Core: Build Settings Class
|
||||
@@ -216,7 +219,7 @@ class BuildSettings:
|
||||
return self._order
|
||||
|
||||
@property
|
||||
def lastPath(self) -> Path:
|
||||
def lastBuildPath(self) -> Path:
|
||||
"""The last used build path."""
|
||||
if self._path.is_dir():
|
||||
return self._path
|
||||
@@ -290,7 +293,7 @@ class BuildSettings:
|
||||
self._order = value
|
||||
return
|
||||
|
||||
def setLastPath(self, path: Path | str | None) -> None:
|
||||
def setLastBuildPath(self, path: Path | str | None) -> None:
|
||||
"""Set the last used build path."""
|
||||
if isinstance(path, str):
|
||||
path = Path(path)
|
||||
@@ -458,7 +461,7 @@ class BuildSettings:
|
||||
self.setName(data.get("name", ""))
|
||||
self.setBuildID(data.get("uuid", ""))
|
||||
self.setOrder(data.get("order", 0))
|
||||
self.setLastPath(data.get("path", None))
|
||||
self.setLastBuildPath(data.get("path", None))
|
||||
self.setLastBuildName(data.get("build", ""))
|
||||
|
||||
buildFmt = str(data.get("format", ""))
|
||||
@@ -481,8 +484,6 @@ class BuildSettings:
|
||||
|
||||
return
|
||||
|
||||
# END Class BuildSettings
|
||||
|
||||
|
||||
class BuildCollection:
|
||||
"""Core: Build Collection Class
|
||||
@@ -630,5 +631,3 @@ class BuildCollection:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# END Class BuildCollection
|
||||
|
||||
@@ -104,7 +104,7 @@ class DocMerger:
|
||||
docText = self._project.storage.getDocumentText(srcHandle).rstrip("\n")
|
||||
if addComment:
|
||||
docInfo = srcItem.describeMe()
|
||||
docSt, _ = srcItem.getImportStatus(incIcon=False)
|
||||
docSt, _ = srcItem.getImportStatus()
|
||||
cmtLine = f"% {cmtPrefix} {docInfo}: {srcItem.itemName} [{docSt}]\n\n"
|
||||
docText = cmtLine + docText
|
||||
|
||||
@@ -129,8 +129,6 @@ class DocMerger:
|
||||
|
||||
return status
|
||||
|
||||
# END Class DocMerger
|
||||
|
||||
|
||||
class DocSplitter:
|
||||
"""Document tool for splitting a document into a set of new
|
||||
@@ -257,8 +255,6 @@ class DocSplitter:
|
||||
|
||||
return
|
||||
|
||||
# END Class DocSplitter
|
||||
|
||||
|
||||
class DocDuplicator:
|
||||
"""A class that will duplicate all documents and folders starting
|
||||
@@ -297,8 +293,6 @@ class DocDuplicator:
|
||||
nHandle = None
|
||||
return
|
||||
|
||||
# END Class DocDuplicator
|
||||
|
||||
|
||||
class DocSearch:
|
||||
|
||||
@@ -354,7 +348,9 @@ class DocSearch:
|
||||
rxMatch = rxItt.next()
|
||||
pos = rxMatch.capturedStart()
|
||||
num = rxMatch.capturedLength()
|
||||
context = text[pos:pos+100].partition("\n")[0]
|
||||
lim = text[:pos].rfind("\n") + 1
|
||||
cut = text[lim:pos].rfind(" ") + lim + 1
|
||||
context = text[cut:cut+100].partition("\n")[0]
|
||||
if context:
|
||||
results.append((pos, num, context))
|
||||
count += 1
|
||||
@@ -370,23 +366,11 @@ class DocSearch:
|
||||
def _buildPattern(self, search: str) -> str:
|
||||
"""Build the search pattern string."""
|
||||
if self._escape:
|
||||
if CONFIG.verQtValue >= 0x050f00:
|
||||
search = QRegularExpression.escape(search)
|
||||
else:
|
||||
# For older Qt versions, we escape manually
|
||||
escaped = ""
|
||||
for c in search:
|
||||
if c.isalnum() or c == "_":
|
||||
escaped += c
|
||||
else:
|
||||
escaped += f"\\{c}"
|
||||
search = escaped
|
||||
search = QRegularExpression.escape(search)
|
||||
if self._words:
|
||||
search = f"(?:^|\\b){search}(?:$|\\b)"
|
||||
return search
|
||||
|
||||
# END Class DocSearch
|
||||
|
||||
|
||||
class ProjectBuilder:
|
||||
"""A class to build a new project from a set of user-defined
|
||||
@@ -619,5 +603,3 @@ class ProjectBuilder:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# END Class ProjectBuilder
|
||||
|
||||
@@ -25,22 +25,23 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from pathlib import Path
|
||||
from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5.QtGui import QFont, QFontInfo
|
||||
from PyQt5.QtGui import QFont
|
||||
|
||||
from novelwriter import CONFIG
|
||||
from novelwriter.constants import nwLabels
|
||||
from novelwriter.core.buildsettings import BuildSettings
|
||||
from novelwriter.core.item import NWItem
|
||||
from novelwriter.core.project import NWProject
|
||||
from novelwriter.core.tohtml import ToHtml
|
||||
from novelwriter.core.tokenizer import Tokenizer
|
||||
from novelwriter.core.tomarkdown import ToMarkdown
|
||||
from novelwriter.core.toodt import ToOdt
|
||||
from novelwriter.core.toqdoc import TextDocumentTheme, ToQTextDocument
|
||||
from novelwriter.enum import nwBuildFmt
|
||||
from novelwriter.error import formatException, logException
|
||||
from novelwriter.constants import nwLabels
|
||||
from novelwriter.core.item import NWItem
|
||||
from novelwriter.core.tomd import ToMarkdown
|
||||
from novelwriter.core.toodt import ToOdt
|
||||
from novelwriter.core.tohtml import ToHtml
|
||||
from novelwriter.core.project import NWProject
|
||||
from novelwriter.core.tokenizer import Tokenizer
|
||||
from novelwriter.core.buildsettings import BuildSettings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -54,7 +55,7 @@ class NWBuildDocument:
|
||||
|
||||
__slots__ = (
|
||||
"_project", "_build", "_queue", "_error", "_cache", "_count",
|
||||
"_outline", "_preview"
|
||||
"_outline",
|
||||
)
|
||||
|
||||
def __init__(self, project: NWProject, build: BuildSettings) -> None:
|
||||
@@ -65,7 +66,6 @@ class NWBuildDocument:
|
||||
self._cache = None
|
||||
self._count = False
|
||||
self._outline = False
|
||||
self._preview = False
|
||||
return
|
||||
|
||||
##
|
||||
@@ -99,15 +99,6 @@ class NWBuildDocument:
|
||||
self._outline = state
|
||||
return
|
||||
|
||||
def setPreviewMode(self, state: bool) -> None:
|
||||
"""Set the preview mode of the build. This also enables stats
|
||||
count and outline mode.
|
||||
"""
|
||||
self._preview = state
|
||||
self._outline = state
|
||||
self._count = state
|
||||
return
|
||||
|
||||
##
|
||||
# Special Methods
|
||||
##
|
||||
@@ -134,6 +125,32 @@ class NWBuildDocument:
|
||||
self._queue.append(item.itemHandle)
|
||||
return
|
||||
|
||||
def iterBuildPreview(self, theme: TextDocumentTheme) -> Iterable[tuple[int, bool]]:
|
||||
"""Build a preview QTextDocument."""
|
||||
makeObj = ToQTextDocument(self._project)
|
||||
filtered = self._setupBuild(makeObj)
|
||||
|
||||
self._outline = True
|
||||
self._count = True
|
||||
|
||||
font = QFont()
|
||||
font.fromString(self._build.getStr("format.textFont"))
|
||||
|
||||
makeObj.initDocument(font, theme)
|
||||
for i, tHandle in enumerate(self._queue):
|
||||
self._error = None
|
||||
if filtered.get(tHandle, (False, 0))[0]:
|
||||
yield i, self._doBuild(makeObj, tHandle)
|
||||
else:
|
||||
yield i, False
|
||||
|
||||
makeObj.appendFootnotes()
|
||||
|
||||
self._error = None
|
||||
self._cache = makeObj
|
||||
|
||||
return
|
||||
|
||||
def iterBuild(self, path: Path, bFormat: nwBuildFmt) -> Iterable[tuple[int, bool]]:
|
||||
"""Wrapper for builders based on format."""
|
||||
if bFormat in (nwBuildFmt.ODT, nwBuildFmt.FODT):
|
||||
@@ -182,8 +199,6 @@ class NWBuildDocument:
|
||||
makeObj = ToHtml(self._project)
|
||||
filtered = self._setupBuild(makeObj)
|
||||
|
||||
makeObj.setPreview(self._preview)
|
||||
makeObj.setLinkHeadings(self._preview)
|
||||
for i, tHandle in enumerate(self._queue):
|
||||
self._error = None
|
||||
if filtered.get(tHandle, (False, 0))[0]:
|
||||
@@ -191,7 +206,9 @@ class NWBuildDocument:
|
||||
else:
|
||||
yield i, False
|
||||
|
||||
if not (self._build.getBool("html.preserveTabs") or self._preview):
|
||||
makeObj.appendFootnotes()
|
||||
|
||||
if not self._build.getBool("html.preserveTabs"):
|
||||
makeObj.replaceTabs()
|
||||
|
||||
self._error = None
|
||||
@@ -214,16 +231,10 @@ class NWBuildDocument:
|
||||
makeObj = ToMarkdown(self._project)
|
||||
filtered = self._setupBuild(makeObj)
|
||||
|
||||
if extendedMd:
|
||||
makeObj.setExtendedMarkdown()
|
||||
else:
|
||||
makeObj.setStandardMarkdown()
|
||||
|
||||
makeObj.setExtendedMarkdown(extendedMd)
|
||||
if self._build.getBool("format.replaceTabs"):
|
||||
makeObj.replaceTabs(nSpaces=4, spaceChar=" ")
|
||||
|
||||
makeObj.setPreserveBreaks(self._build.getBool("md.preserveBreaks"))
|
||||
|
||||
for i, tHandle in enumerate(self._queue):
|
||||
self._error = None
|
||||
if filtered.get(tHandle, (False, 0))[0]:
|
||||
@@ -231,6 +242,8 @@ class NWBuildDocument:
|
||||
else:
|
||||
yield i, False
|
||||
|
||||
makeObj.appendFootnotes()
|
||||
|
||||
self._error = None
|
||||
self._cache = makeObj
|
||||
|
||||
@@ -281,13 +294,9 @@ class NWBuildDocument:
|
||||
def _setupBuild(self, bldObj: Tokenizer) -> dict:
|
||||
"""Configure the build object."""
|
||||
# Get Settings
|
||||
textFont = self._build.getStr("format.textFont")
|
||||
textSize = self._build.getInt("format.textSize")
|
||||
|
||||
fontFamily = textFont or CONFIG.textFont
|
||||
bldFont = QFont(fontFamily, textSize)
|
||||
fontInfo = QFontInfo(bldFont)
|
||||
textFixed = fontInfo.fixedPitch()
|
||||
textFont = QFont(CONFIG.textFont)
|
||||
textFont.fromString(self._build.getStr("format.textFont"))
|
||||
bldObj.setFont(textFont)
|
||||
|
||||
bldObj.setTitleFormat(
|
||||
self._build.getStr("headings.fmtTitle"),
|
||||
@@ -326,9 +335,15 @@ class NWBuildDocument:
|
||||
self._build.getBool("headings.breakScene")
|
||||
)
|
||||
|
||||
bldObj.setFont(fontFamily, textSize, textFixed)
|
||||
bldObj.setJustify(self._build.getBool("format.justifyText"))
|
||||
bldObj.setLineHeight(self._build.getFloat("format.lineHeight"))
|
||||
bldObj.setKeepLineBreaks(self._build.getBool("format.keepBreaks"))
|
||||
bldObj.setDialogueHighlight(self._build.getBool("format.showDialogue"))
|
||||
bldObj.setFirstLineIndent(
|
||||
self._build.getBool("format.firstLineIndent"),
|
||||
self._build.getFloat("format.firstIndentWidth"),
|
||||
self._build.getBool("format.indentFirstPar"),
|
||||
)
|
||||
|
||||
bldObj.setBodyText(self._build.getBool("text.includeBodyText"))
|
||||
bldObj.setSynopsis(self._build.getBool("text.includeSynopsis"))
|
||||
@@ -346,7 +361,6 @@ class NWBuildDocument:
|
||||
bldObj.setHeaderFormat(
|
||||
self._build.getStr("odt.pageHeader"), self._build.getInt("odt.pageCountOffset")
|
||||
)
|
||||
bldObj.setFirstLineIndent(self._build.getBool("odt.firstLineIndent"))
|
||||
|
||||
scale = nwLabels.UNIT_SCALE.get(self._build.getStr("format.pageUnit"), 1.0)
|
||||
pW, pH = nwLabels.PAPER_SIZE.get(self._build.getStr("format.pageSize"), (-1.0, -1.0))
|
||||
@@ -397,5 +411,3 @@ class NWBuildDocument:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# END Class NWBuildDocument
|
||||
|
||||
@@ -26,14 +26,14 @@ from __future__ import annotations
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
from pathlib import Path
|
||||
from time import time
|
||||
from typing import TYPE_CHECKING
|
||||
from pathlib import Path
|
||||
|
||||
from novelwriter.enum import nwItemLayout, nwItemClass
|
||||
from novelwriter.error import formatException, logException
|
||||
from novelwriter.common import formatTimeStamp, isHandle
|
||||
from novelwriter.core.item import NWItem
|
||||
from novelwriter.enum import nwItemClass, nwItemLayout
|
||||
from novelwriter.error import formatException, logException
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from novelwriter.core.project import NWProject
|
||||
@@ -359,5 +359,3 @@ class NWDocument:
|
||||
logger.debug("Unknown meta data: '%s'", metaLine.strip())
|
||||
|
||||
return
|
||||
|
||||
# END Class NWDocument
|
||||
|
||||
@@ -29,17 +29,20 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
|
||||
from time import time
|
||||
from typing import TYPE_CHECKING
|
||||
from pathlib import Path
|
||||
from collections.abc import ItemsView, Iterable
|
||||
from pathlib import Path
|
||||
from time import time
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
from novelwriter import SHARED
|
||||
from novelwriter.enum import nwComment, nwItemClass, nwItemType, nwItemLayout
|
||||
from novelwriter.common import (
|
||||
checkInt, isHandle, isItemClass, isListInstance, isTitleTag, jsonEncode
|
||||
)
|
||||
from novelwriter.constants import nwFiles, nwHeaders, nwKeyWords
|
||||
from novelwriter.enum import nwComment, nwItemClass, nwItemLayout, nwItemType
|
||||
from novelwriter.error import logException
|
||||
from novelwriter.common import checkInt, isHandle, isItemClass, isTitleTag, jsonEncode
|
||||
from novelwriter.constants import nwFiles, nwKeyWords, nwHeaders
|
||||
from novelwriter.text.counting import standardCounter
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
@@ -48,7 +51,12 @@ if TYPE_CHECKING: # pragma: no cover
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TT_NONE = "T0000"
|
||||
T_NoteTypes = Literal["footnotes", "comments"]
|
||||
|
||||
TT_NONE = "T0000" # Default title key
|
||||
MAX_RETRY = 1000 # Key generator recursion limit
|
||||
KEY_SOURCE = "0123456789bcdfghjklmnpqrstvwxz"
|
||||
NOTE_TYPES: list[T_NoteTypes] = ["footnotes", "comments"]
|
||||
|
||||
|
||||
class NWIndex:
|
||||
@@ -301,9 +309,9 @@ class NWIndex:
|
||||
|
||||
def _scanActive(self, tHandle: str, nwItem: NWItem, text: str, tags: dict[str, bool]) -> None:
|
||||
"""Scan an active document for meta data."""
|
||||
nTitle = 0 # Line Number of the previous title
|
||||
cTitle = TT_NONE # Tag of the current title
|
||||
pTitle = TT_NONE # Tag of the previous title
|
||||
nTitle = 0 # Line Number of the previous title
|
||||
cTitle = TT_NONE # Tag of the current title
|
||||
pTitle = TT_NONE # Tag of the previous title
|
||||
canSetHead = True # First heading has not yet been set
|
||||
|
||||
lines = text.splitlines()
|
||||
@@ -335,10 +343,11 @@ class NWIndex:
|
||||
self._indexKeyword(tHandle, line, cTitle, nwItem.itemClass, tags)
|
||||
|
||||
elif line.startswith("%"):
|
||||
if cTitle != TT_NONE:
|
||||
cStyle, cText, _ = processComment(line)
|
||||
if cStyle in (nwComment.SYNOPSIS, nwComment.SHORT):
|
||||
self._itemIndex.setHeadingSynopsis(tHandle, cTitle, cText)
|
||||
cStyle, cKey, cText, _, _ = processComment(line)
|
||||
if cStyle in (nwComment.SYNOPSIS, nwComment.SHORT):
|
||||
self._itemIndex.setHeadingSynopsis(tHandle, cTitle, cText)
|
||||
elif cStyle == nwComment.FOOTNOTE:
|
||||
self._itemIndex.addNoteKey(tHandle, "footnotes", cKey)
|
||||
|
||||
# Count words for remaining text after last heading
|
||||
if pTitle != TT_NONE:
|
||||
@@ -506,6 +515,14 @@ class NWIndex:
|
||||
name, _, display = text.partition("|")
|
||||
return name.rstrip(), display.lstrip()
|
||||
|
||||
def newCommentKey(self, tHandle: str, style: nwComment) -> str:
|
||||
"""Generate a new key for a comment style."""
|
||||
if style == nwComment.FOOTNOTE:
|
||||
return self._itemIndex.genNewNoteKey(tHandle, "footnotes")
|
||||
elif style == nwComment.COMMENT:
|
||||
return self._itemIndex.genNewNoteKey(tHandle, "comments")
|
||||
return "err"
|
||||
|
||||
##
|
||||
# Extract Data
|
||||
##
|
||||
@@ -690,12 +707,9 @@ class NWIndex:
|
||||
return tName, tClass, iItem, None if iItem is None else iItem[tHeading]
|
||||
return "", "", None, None
|
||||
|
||||
# END Class NWIndex
|
||||
|
||||
|
||||
# =============================================================================================== #
|
||||
# The Tags Index Object
|
||||
# =============================================================================================== #
|
||||
# The Tags Index Object
|
||||
# =====================
|
||||
|
||||
class TagsIndex:
|
||||
"""Core: Tags Index Wrapper Class
|
||||
@@ -790,7 +804,7 @@ class TagsIndex:
|
||||
|
||||
for key, entry in data.items():
|
||||
if not isinstance(key, str):
|
||||
raise ValueError("tagsIndex keys must be a string")
|
||||
raise ValueError("tagsIndex key must be a string")
|
||||
if not isinstance(entry, dict):
|
||||
raise ValueError("tagsIndex entry is not a dict")
|
||||
|
||||
@@ -815,12 +829,9 @@ class TagsIndex:
|
||||
|
||||
return
|
||||
|
||||
# END Class TagsIndex
|
||||
|
||||
|
||||
# =============================================================================================== #
|
||||
# The Item Index Objects
|
||||
# =============================================================================================== #
|
||||
# The Item Index Objects
|
||||
# ======================
|
||||
|
||||
class ItemIndex:
|
||||
"""Core: Item Index Wrapper Class
|
||||
@@ -950,6 +961,25 @@ class ItemIndex:
|
||||
self._items[tHandle].addHeadingRef(sTitle, tagKeys, refType)
|
||||
return
|
||||
|
||||
def addNoteKey(self, tHandle: str, style: T_NoteTypes, key: str) -> None:
|
||||
"""Set notes key for a given item."""
|
||||
if tHandle in self._items:
|
||||
self._items[tHandle].addNoteKey(style, key)
|
||||
return
|
||||
|
||||
def genNewNoteKey(self, tHandle: str, style: T_NoteTypes) -> str:
|
||||
"""Set notes key for a given item."""
|
||||
if style in NOTE_TYPES and (item := self._items.get(tHandle)):
|
||||
keys = set()
|
||||
for entry in self._items.values():
|
||||
keys.update(entry.noteKeys(style))
|
||||
for _ in range(MAX_RETRY):
|
||||
key = style[:1] + "".join(random.choices(KEY_SOURCE, k=4))
|
||||
if key not in keys:
|
||||
item.addNoteKey(style, key)
|
||||
return key
|
||||
return "err"
|
||||
|
||||
##
|
||||
# Pack/Unpack
|
||||
##
|
||||
@@ -978,8 +1008,6 @@ class ItemIndex:
|
||||
|
||||
return
|
||||
|
||||
# END Class ItemIndex
|
||||
|
||||
|
||||
class IndexItem:
|
||||
"""Core: Single Index Item Class
|
||||
@@ -991,12 +1019,13 @@ class IndexItem:
|
||||
must be reset each time the item is re-indexed.
|
||||
"""
|
||||
|
||||
__slots__ = ("_handle", "_item", "_headings", "_count")
|
||||
__slots__ = ("_handle", "_item", "_headings", "_count", "_notes")
|
||||
|
||||
def __init__(self, tHandle: str, nwItem: NWItem) -> None:
|
||||
self._handle = tHandle
|
||||
self._item = nwItem
|
||||
self._headings: dict[str, IndexHeading] = {TT_NONE: IndexHeading(TT_NONE)}
|
||||
self._notes: dict[str, set[str]] = {}
|
||||
self._count = 0
|
||||
return
|
||||
|
||||
@@ -1064,6 +1093,13 @@ class IndexItem:
|
||||
self._headings[sTitle].addReference(tagKey, refType)
|
||||
return
|
||||
|
||||
def addNoteKey(self, style: T_NoteTypes, key: str) -> None:
|
||||
"""Add a note key to the index."""
|
||||
if style not in self._notes:
|
||||
self._notes[style] = set()
|
||||
self._notes[style].add(key)
|
||||
return
|
||||
|
||||
##
|
||||
# Data Methods
|
||||
##
|
||||
@@ -1085,6 +1121,10 @@ class IndexItem:
|
||||
self._count += 1
|
||||
return f"T{self._count:04d}"
|
||||
|
||||
def noteKeys(self, style: T_NoteTypes) -> set[str]:
|
||||
"""Return a set of all note keys."""
|
||||
return self._notes.get(style, set())
|
||||
|
||||
##
|
||||
# Pack/Unpack
|
||||
##
|
||||
@@ -1103,6 +1143,8 @@ class IndexItem:
|
||||
data["headings"] = heads
|
||||
if refs:
|
||||
data["references"] = refs
|
||||
if self._notes:
|
||||
data["notes"] = {style: list(keys) for style, keys in self._notes.items()}
|
||||
|
||||
return data
|
||||
|
||||
@@ -1116,9 +1158,15 @@ class IndexItem:
|
||||
tHeading.unpackData(hData)
|
||||
tHeading.unpackReferences(references.get(sTitle, {}))
|
||||
self.addHeading(tHeading)
|
||||
return
|
||||
|
||||
# END Class IndexItem
|
||||
for style, keys in data.get("notes", {}).items():
|
||||
if style not in NOTE_TYPES:
|
||||
raise ValueError("The notes style is invalid")
|
||||
if not isListInstance(keys, str):
|
||||
raise ValueError("The notes keys must be a list of strings")
|
||||
self._notes[style] = set(keys)
|
||||
|
||||
return
|
||||
|
||||
|
||||
class IndexHeading:
|
||||
@@ -1295,25 +1343,47 @@ class IndexHeading:
|
||||
raise ValueError("The itemIndex contains an invalid reference type")
|
||||
return
|
||||
|
||||
# END Class IndexHeading
|
||||
|
||||
# Text Processing Functions
|
||||
# =========================
|
||||
|
||||
# =============================================================================================== #
|
||||
# Text Processing Functions
|
||||
# =============================================================================================== #
|
||||
|
||||
CLASSIFIERS = {
|
||||
"short": nwComment.SHORT,
|
||||
MODIFIERS = {
|
||||
"synopsis": nwComment.SYNOPSIS,
|
||||
"short": nwComment.SHORT,
|
||||
"note": nwComment.NOTE,
|
||||
"footnote": nwComment.FOOTNOTE,
|
||||
}
|
||||
KEY_REQ = {
|
||||
"synopsis": 0, # Key not allowed
|
||||
"short": 0, # Key not allowed
|
||||
"note": 1, # Key optional
|
||||
"footnote": 2, # Key required
|
||||
}
|
||||
|
||||
|
||||
def processComment(text: str) -> tuple[nwComment, str, int]:
|
||||
"""Extract comment style and text. Should only be called on text
|
||||
starting with a %.
|
||||
def _checkModKey(modifier: str, key: str) -> bool:
|
||||
"""Check if a modifier and key set are ok."""
|
||||
if modifier in MODIFIERS:
|
||||
if key == "":
|
||||
return KEY_REQ[modifier] < 2
|
||||
elif key.replace("_", "").isalnum():
|
||||
return KEY_REQ[modifier] > 0
|
||||
return False
|
||||
|
||||
|
||||
def processComment(text: str) -> tuple[nwComment, str, str, int, int]:
|
||||
"""Extract comment style, key and text. Should only be called on
|
||||
text starting with a %.
|
||||
"""
|
||||
check = text[1:].lstrip()
|
||||
classifier, _, content = check.partition(":")
|
||||
if content and (clean := classifier.strip().lower()) in CLASSIFIERS:
|
||||
return CLASSIFIERS[clean], content.strip(), text.find(":") + 1
|
||||
return nwComment.PLAIN, check, 0
|
||||
if text[:2] == "%~":
|
||||
return nwComment.IGNORE, "", text[2:].lstrip(), 0, 0
|
||||
|
||||
check = text[1:].strip()
|
||||
start, _, content = check.partition(":")
|
||||
modifier, _, key = start.rstrip().partition(".")
|
||||
if content and (clean := modifier.lower()) and _checkModKey(clean, key):
|
||||
col = text.find(":") + 1
|
||||
dot = text.find(".", 0, col) + 1
|
||||
return MODIFIERS[clean], key, content.lstrip(), dot, col
|
||||
|
||||
return nwComment.PLAIN, "", check, 0, 0
|
||||
|
||||
@@ -25,15 +25,16 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Literal, overload
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from PyQt5.QtGui import QIcon
|
||||
|
||||
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
|
||||
from novelwriter.common import (
|
||||
checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified, yesNo
|
||||
checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified,
|
||||
yesNo
|
||||
)
|
||||
from novelwriter.constants import nwHeaders, nwLabels, trConst
|
||||
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from novelwriter.core.project import NWProject
|
||||
@@ -308,25 +309,15 @@ class NWItem:
|
||||
|
||||
return trConst(nwLabels.ITEM_DESCRIPTION.get(descKey, ""))
|
||||
|
||||
@overload # pragma: no cover
|
||||
def getImportStatus(self, incIcon: Literal[True] = True) -> tuple[str, QIcon]:
|
||||
pass
|
||||
|
||||
@overload # pragma: no cover
|
||||
def getImportStatus(self, incIcon: Literal[False]) -> tuple[str, None]:
|
||||
pass
|
||||
|
||||
def getImportStatus(self, incIcon=True):
|
||||
def getImportStatus(self) -> tuple[str, QIcon]:
|
||||
"""Return the relevant importance or status label and icon for
|
||||
the current item based on its class.
|
||||
"""
|
||||
if self.isNovelLike():
|
||||
stName = self._project.data.itemStatus.name(self._status)
|
||||
stIcon = self._project.data.itemStatus.icon(self._status) if incIcon else None
|
||||
entry = self._project.data.itemStatus[self._status]
|
||||
else:
|
||||
stName = self._project.data.itemImport.name(self._import)
|
||||
stIcon = self._project.data.itemImport.icon(self._import) if incIcon else None
|
||||
return stName, stIcon
|
||||
entry = self._project.data.itemImport[self._import]
|
||||
return entry.name, entry.icon
|
||||
|
||||
##
|
||||
# Checker Methods
|
||||
@@ -567,5 +558,3 @@ class NWItem:
|
||||
"""Save the initial word count."""
|
||||
self._initCount = self._wordCount
|
||||
return
|
||||
|
||||
# END Class NWItem
|
||||
|
||||
@@ -28,12 +28,12 @@ import json
|
||||
import logging
|
||||
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any, TypeVar
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, TypeVar
|
||||
|
||||
from novelwriter.error import logException
|
||||
from novelwriter.common import checkBool, checkFloat, checkInt, checkString, jsonEncode
|
||||
from novelwriter.constants import nwFiles
|
||||
from novelwriter.error import logException
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from novelwriter.core.project import NWProject
|
||||
@@ -218,5 +218,3 @@ class OptionState:
|
||||
if value in lookup.__members__:
|
||||
return lookup[value]
|
||||
return default
|
||||
|
||||
# END Class OptionState
|
||||
|
||||
@@ -26,33 +26,33 @@ from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
|
||||
from collections.abc import Iterable
|
||||
from enum import Enum
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from time import time
|
||||
from typing import TYPE_CHECKING
|
||||
from pathlib import Path
|
||||
from functools import partial
|
||||
from collections.abc import Iterable
|
||||
|
||||
from PyQt5.QtCore import QCoreApplication
|
||||
|
||||
from novelwriter import CONFIG, SHARED, __version__, __hexversion__
|
||||
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
|
||||
from novelwriter.error import logException
|
||||
from novelwriter.constants import trConst, nwLabels
|
||||
from novelwriter.core.tree import NWTree
|
||||
from novelwriter import CONFIG, SHARED, __hexversion__, __version__
|
||||
from novelwriter.common import (
|
||||
checkStringNone, formatInt, formatTimeStamp, getFileSize, hexToInt,
|
||||
makeFileNameSafe, minmax
|
||||
)
|
||||
from novelwriter.constants import nwLabels, trConst
|
||||
from novelwriter.core.index import NWIndex
|
||||
from novelwriter.core.options import OptionState
|
||||
from novelwriter.core.storage import NWStorage, NWStorageOpen
|
||||
from novelwriter.core.sessions import NWSessionLog
|
||||
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState
|
||||
from novelwriter.core.projectdata import NWProjectData
|
||||
from novelwriter.common import (
|
||||
checkStringNone, formatInt, formatTimeStamp, getFileSize, hexToInt, makeFileNameSafe, minmax
|
||||
)
|
||||
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState
|
||||
from novelwriter.core.sessions import NWSessionLog
|
||||
from novelwriter.core.storage import NWStorage, NWStorageOpen
|
||||
from novelwriter.core.tree import NWTree
|
||||
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
|
||||
from novelwriter.error import logException
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from novelwriter.core.item import NWItem
|
||||
from novelwriter.core.status import NWStatus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -64,8 +64,6 @@ class NWProjectState(Enum):
|
||||
RECOVERY = 2
|
||||
READY = 3
|
||||
|
||||
# END Enum NWProjectState
|
||||
|
||||
|
||||
class NWProject:
|
||||
|
||||
@@ -256,13 +254,23 @@ class NWProject:
|
||||
status = self._storage.initProjectStorage(projPath, clearLock)
|
||||
if status != NWStorageOpen.READY:
|
||||
if status == NWStorageOpen.UNKOWN:
|
||||
SHARED.error(self.tr("Not a known project file format."))
|
||||
SHARED.error(
|
||||
self.tr("Not a known project file format."),
|
||||
info=self.tr("Path: {0}").format(str(projPath))
|
||||
)
|
||||
elif status == NWStorageOpen.NOT_FOUND:
|
||||
SHARED.error(self.tr("Project file not found."))
|
||||
SHARED.error(
|
||||
self.tr("Project file not found."),
|
||||
info=self.tr("Path: {0}").format(str(projPath))
|
||||
)
|
||||
elif status == NWStorageOpen.FAILED:
|
||||
SHARED.error(
|
||||
self.tr("Failed to open project."),
|
||||
info=self.tr("Path: {0}").format(str(projPath)),
|
||||
exc=self._storage.exc
|
||||
)
|
||||
elif status == NWStorageOpen.LOCKED:
|
||||
self._state = NWProjectState.LOCKED
|
||||
elif status == NWStorageOpen.FAILED:
|
||||
SHARED.error(self.tr("Failed to open project."), exc=self._storage.exc)
|
||||
return False
|
||||
|
||||
# Read Project XML
|
||||
@@ -348,6 +356,7 @@ class NWProject:
|
||||
self.setProjectChanged(False)
|
||||
self._valid = True
|
||||
self._state = NWProjectState.READY
|
||||
self._storage.lockSession() # Lock only after a successful open. See issue #1977.
|
||||
|
||||
SHARED.newStatusMessage(self.tr("Opened Project: {0}").format(self._data.name))
|
||||
|
||||
@@ -461,14 +470,14 @@ class NWProject:
|
||||
|
||||
def setDefaultStatusImport(self) -> None:
|
||||
"""Set the default status and importance values."""
|
||||
self._data.itemStatus.write(None, self.tr("New"), (100, 100, 100))
|
||||
self._data.itemStatus.write(None, self.tr("Note"), (200, 50, 0))
|
||||
self._data.itemStatus.write(None, self.tr("Draft"), (200, 150, 0))
|
||||
self._data.itemStatus.write(None, self.tr("Finished"), (50, 200, 0))
|
||||
self._data.itemImport.write(None, self.tr("New"), (100, 100, 100))
|
||||
self._data.itemImport.write(None, self.tr("Minor"), (200, 50, 0))
|
||||
self._data.itemImport.write(None, self.tr("Major"), (200, 150, 0))
|
||||
self._data.itemImport.write(None, self.tr("Main"), (50, 200, 0))
|
||||
self._data.itemStatus.add(None, self.tr("New"), (100, 100, 100), "SQUARE", 0)
|
||||
self._data.itemStatus.add(None, self.tr("Note"), (200, 50, 0), "SQUARE", 0)
|
||||
self._data.itemStatus.add(None, self.tr("Draft"), (200, 150, 0), "SQUARE", 0)
|
||||
self._data.itemStatus.add(None, self.tr("Finished"), (50, 200, 0), "SQUARE", 0)
|
||||
self._data.itemImport.add(None, self.tr("New"), (100, 100, 100), "SQUARE", 0)
|
||||
self._data.itemImport.add(None, self.tr("Minor"), (200, 50, 0), "SQUARE", 0)
|
||||
self._data.itemImport.add(None, self.tr("Major"), (200, 150, 0), "SQUARE", 0)
|
||||
self._data.itemImport.add(None, self.tr("Main"), (50, 200, 0), "SQUARE", 0)
|
||||
return
|
||||
|
||||
def setProjectLang(self, language: str | None) -> None:
|
||||
@@ -491,14 +500,6 @@ class NWProject:
|
||||
self.setProjectChanged(True)
|
||||
return
|
||||
|
||||
def setStatusColours(self, new: list[dict], deleted: list[str]) -> bool:
|
||||
"""Update the list of novel file status flags."""
|
||||
return self._setStatusImport(new, deleted, self._data.itemStatus)
|
||||
|
||||
def setImportColours(self, new: list[dict], deleted: list[str]) -> bool:
|
||||
"""Update the list of note file importance flags."""
|
||||
return self._setStatusImport(new, deleted, self._data.itemImport)
|
||||
|
||||
def setProjectChanged(self, status: bool) -> bool:
|
||||
"""Toggle the project changed flag, and propagate the
|
||||
information to the GUI statusbar.
|
||||
@@ -584,28 +585,6 @@ class NWProject:
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _setStatusImport(self, new: list[dict], delete: list[str], target: NWStatus) -> bool:
|
||||
"""Update the list of novel file status or importance flags, and
|
||||
delete those that have been requested deleted.
|
||||
"""
|
||||
if not (new or delete):
|
||||
return False
|
||||
|
||||
order = []
|
||||
for entry in new:
|
||||
key = entry.get("key", None)
|
||||
name = entry.get("name", "")
|
||||
cols = entry.get("cols", (100, 100, 100))
|
||||
if name:
|
||||
order.append(target.write(key, name, cols))
|
||||
|
||||
for key in delete:
|
||||
target.remove(key)
|
||||
|
||||
target.reorder(order)
|
||||
|
||||
return True
|
||||
|
||||
def _loadProjectLocalisation(self) -> bool:
|
||||
"""Load the language data for the current project language."""
|
||||
if self._data.language is None or CONFIG._nwLangPath is None:
|
||||
@@ -626,5 +605,3 @@ class NWProject:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# END Class NWProject
|
||||
|
||||
@@ -23,8 +23,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
@@ -322,5 +322,3 @@ class NWProjectData:
|
||||
self._autoReplace[key] = simplified(entry)
|
||||
self._project.setProjectChanged(True)
|
||||
return
|
||||
|
||||
# END Class NWProjectData
|
||||
|
||||
@@ -29,24 +29,24 @@ import logging
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from time import time
|
||||
from typing import TYPE_CHECKING
|
||||
from pathlib import Path
|
||||
|
||||
from novelwriter import __version__, __hexversion__
|
||||
from novelwriter import __hexversion__, __version__
|
||||
from novelwriter.common import (
|
||||
checkBool, checkInt, checkString, checkStringNone, formatTimeStamp,
|
||||
hexToInt, simplified, xmlIndent, yesNo
|
||||
)
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from novelwriter.core.status import NWStatus
|
||||
from novelwriter.core.projectdata import NWProjectData
|
||||
from novelwriter.core.status import NWStatus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
FILE_VERSION = "1.5" # The current project file format version
|
||||
FILE_REVISION = "3" # The current project file format revision
|
||||
FILE_REVISION = "4" # The current project file format revision
|
||||
HEX_VERSION = 0x0105
|
||||
|
||||
NUM_VERSION = {
|
||||
@@ -70,8 +70,6 @@ class XMLReadState(Enum):
|
||||
PARSED_OK = 5
|
||||
WAS_LEGACY = 6
|
||||
|
||||
# END Class XMLReadState
|
||||
|
||||
|
||||
class ProjectXMLReader:
|
||||
"""Core: Project XML Reader
|
||||
@@ -109,6 +107,8 @@ class ProjectXMLReader:
|
||||
Rev 2: Drops the title node from project and adds the TEMPLATE
|
||||
class for items. 2.3 Beta 1.
|
||||
Rev 3: Added TEMPLATE class. 2.3.
|
||||
Rev 4: Added shape attribute to status and importance entry
|
||||
nodes. 2.5.
|
||||
"""
|
||||
|
||||
def __init__(self, path: str | Path) -> None:
|
||||
@@ -356,8 +356,8 @@ class ProjectXMLReader:
|
||||
logger.debug("Parsing <content> section (legacy format)")
|
||||
|
||||
# Create maps to look up name -> key for status and importance
|
||||
statusMap = {entry.get("name"): key for key, entry in data.itemStatus.items()}
|
||||
importMap = {entry.get("name"): key for key, entry in data.itemImport.items()}
|
||||
sMap: dict[str | None, str] = {e.name: k for k, e in data.itemStatus.iterItems()}
|
||||
iMap: dict[str | None, str] = {e.name: k for k, e in data.itemImport.iterItems()}
|
||||
|
||||
for xItem in xSection:
|
||||
if xItem.tag != "item":
|
||||
@@ -404,9 +404,9 @@ class ProjectXMLReader:
|
||||
|
||||
# Status was split into separate status/import with a key in 1.4
|
||||
if item.get("class", "") in ("NOVEL", "ARCHIVE"):
|
||||
name["status"] = statusMap.get(tmpStatus, None)
|
||||
name["status"] = sMap.get(tmpStatus, None)
|
||||
else:
|
||||
name["import"] = importMap.get(tmpStatus, None)
|
||||
name["import"] = iMap.get(tmpStatus, None)
|
||||
|
||||
# A number of layouts were removed in 1.3
|
||||
if item.get("layout", "") in (
|
||||
@@ -436,7 +436,8 @@ class ProjectXMLReader:
|
||||
green = checkInt(xEntry.attrib.get("green", 0), 0)
|
||||
blue = checkInt(xEntry.attrib.get("blue", 0), 0)
|
||||
count = checkInt(xEntry.attrib.get("count", 0), 0)
|
||||
sObject.write(key, xEntry.text or "", (red, green, blue), count)
|
||||
shape = xEntry.attrib.get("shape", "")
|
||||
sObject.add(key, xEntry.text or "", (red, green, blue), shape, count)
|
||||
return
|
||||
|
||||
def _parseDictKeyText(self, xItem: ET.Element) -> dict:
|
||||
@@ -455,8 +456,6 @@ class ProjectXMLReader:
|
||||
"""
|
||||
return {xNode.tag: checkString(xNode.text, "") for xNode in xItem}
|
||||
|
||||
# END Class ProjectXMLReader
|
||||
|
||||
|
||||
class ProjectXMLWriter:
|
||||
"""Core: Project XML Writer
|
||||
@@ -576,5 +575,3 @@ class ProjectXMLWriter:
|
||||
xEntry = ET.SubElement(xItem, "entry", attrib={"key": key})
|
||||
xEntry.text = str(value) or ""
|
||||
return
|
||||
|
||||
# END Class ProjectXMLWriter
|
||||
|
||||
@@ -26,14 +26,14 @@ from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
|
||||
from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
from time import time
|
||||
from typing import TYPE_CHECKING
|
||||
from pathlib import Path
|
||||
from collections.abc import Iterable
|
||||
|
||||
from novelwriter.error import logException
|
||||
from novelwriter.common import formatTimeStamp
|
||||
from novelwriter.constants import nwFiles
|
||||
from novelwriter.error import logException
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from novelwriter.core.project import NWProject
|
||||
@@ -135,5 +135,3 @@ class NWSessionLog:
|
||||
"novel": novel, "notes": notes, "idle": idle,
|
||||
})
|
||||
return f"{data}\n"
|
||||
|
||||
# END Class NWSessionLog
|
||||
|
||||
@@ -27,14 +27,14 @@ from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from pathlib import Path
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import QLocale
|
||||
|
||||
from novelwriter.error import logException
|
||||
from novelwriter.constants import nwFiles
|
||||
from novelwriter.error import logException
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from novelwriter.core.project import NWProject
|
||||
@@ -164,11 +164,10 @@ class NWSpellEnchant:
|
||||
name = ""
|
||||
return tag, name
|
||||
|
||||
# END Class NWSpellEnchant
|
||||
|
||||
|
||||
class FakeEnchant:
|
||||
"""Fallback for when Enchant is selected, but not installed."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
||||
class FakeProvider:
|
||||
@@ -188,8 +187,6 @@ class FakeEnchant:
|
||||
def add_to_session(self, word: str) -> None:
|
||||
return
|
||||
|
||||
# END Class FakeEnchant
|
||||
|
||||
|
||||
class UserDictionary:
|
||||
|
||||
@@ -240,5 +237,3 @@ class UserDictionary:
|
||||
logger.error("Failed to save user dictionary")
|
||||
logException()
|
||||
return
|
||||
|
||||
# END Class UserDictionary
|
||||
|
||||
@@ -24,17 +24,19 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
import dataclasses
|
||||
import logging
|
||||
import random
|
||||
|
||||
from collections.abc import Iterable
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
from collections.abc import ItemsView, Iterable, Iterator, KeysView, ValuesView
|
||||
|
||||
from PyQt5.QtGui import QIcon, QPainter, QPainterPath, QPixmap, QColor
|
||||
from PyQt5.QtCore import QRectF
|
||||
from PyQt5.QtCore import QPointF, Qt
|
||||
from PyQt5.QtGui import QColor, QIcon, QPainter, QPainterPath, QPixmap, QPolygonF
|
||||
|
||||
from novelwriter import CONFIG
|
||||
from novelwriter.common import minmax, simplified
|
||||
from novelwriter import SHARED
|
||||
from novelwriter.common import simplified
|
||||
from novelwriter.enum import nwStatusShape
|
||||
from novelwriter.types import QtPaintAnitAlias, QtTransparent
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
@@ -43,83 +45,95 @@ if TYPE_CHECKING: # pragma: no cover
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class StatusEntry:
|
||||
|
||||
name: str
|
||||
color: QColor
|
||||
shape: nwStatusShape
|
||||
icon: QIcon
|
||||
count: int = 0
|
||||
|
||||
@classmethod
|
||||
def duplicate(cls, source: StatusEntry) -> StatusEntry:
|
||||
"""Create a deep copy of the source object."""
|
||||
cls = dataclasses.replace(source)
|
||||
cls.color = QColor(source.color)
|
||||
cls.icon = QIcon(source.icon)
|
||||
return cls
|
||||
|
||||
|
||||
NO_ENTRY = StatusEntry("", QColor(0, 0, 0), nwStatusShape.SQUARE, QIcon(), 0)
|
||||
|
||||
|
||||
class NWStatus:
|
||||
|
||||
STATUS = 1
|
||||
IMPORT = 2
|
||||
STATUS = "s"
|
||||
IMPORT = "i"
|
||||
|
||||
def __init__(self, kind: Literal[1, 2]) -> None:
|
||||
__slots__ = ("_store", "_default", "_prefix", "_height")
|
||||
|
||||
self._type = kind
|
||||
self._store = {}
|
||||
def __init__(self, prefix: Literal["s", "i"]) -> None:
|
||||
self._store: dict[str, StatusEntry] = {}
|
||||
self._default = None
|
||||
|
||||
self._iPX = CONFIG.pxInt(24)
|
||||
|
||||
pA = CONFIG.pxInt(2)
|
||||
pB = CONFIG.pxInt(20)
|
||||
pR = float(CONFIG.pxInt(4))
|
||||
self._iconPath = QPainterPath()
|
||||
self._iconPath.addRoundedRect(QRectF(pA, pA, pB, pB), pR, pR)
|
||||
|
||||
self._defaultIcon = self._createIcon(100, 100, 100)
|
||||
|
||||
if self._type == self.STATUS:
|
||||
self._prefix = "s"
|
||||
elif self._type == self.IMPORT:
|
||||
self._prefix = "i"
|
||||
else:
|
||||
raise Exception("This is a bug!")
|
||||
|
||||
self._prefix = prefix[:1]
|
||||
self._height = SHARED.theme.baseIconHeight
|
||||
return
|
||||
|
||||
def write(self, key: str | None, name: str, col: tuple, count: int | None = None) -> str:
|
||||
def __len__(self) -> int:
|
||||
return len(self._store)
|
||||
|
||||
def __getitem__(self, key: str | None) -> StatusEntry:
|
||||
"""Return the entry associated with a given key."""
|
||||
if key and key in self._store:
|
||||
return self._store[key]
|
||||
elif self._default is not None:
|
||||
return self._store[self._default]
|
||||
return NO_ENTRY
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
|
||||
def add(self, key: str | None, name: str, color: tuple[int, int, int],
|
||||
shape: str, count: int) -> str:
|
||||
"""Add or update a status entry. If the key is invalid, a new
|
||||
key is generated.
|
||||
"""
|
||||
if not self._isKey(key):
|
||||
key = self._newKey()
|
||||
if not isinstance(col, tuple):
|
||||
col = (100, 100, 100)
|
||||
if len(col) != 3:
|
||||
col = (100, 100, 100)
|
||||
if isinstance(color, tuple) and len(color) == 3:
|
||||
qColor = QColor(*color)
|
||||
else:
|
||||
qColor = QColor(100, 100, 100)
|
||||
|
||||
cR = minmax(col[0], 0, 255)
|
||||
cG = minmax(col[1], 0, 255)
|
||||
cB = minmax(col[2], 0, 255)
|
||||
try:
|
||||
iShape = nwStatusShape[shape]
|
||||
except KeyError:
|
||||
iShape = nwStatusShape.SQUARE
|
||||
|
||||
key = self._checkKey(key)
|
||||
name = simplified(name)
|
||||
if count is None:
|
||||
count = self._store.get(key, {}).get("count", 0)
|
||||
|
||||
self._store[key] = {
|
||||
"name": name,
|
||||
"icon": self._createIcon(cR, cG, cB),
|
||||
"cols": (cR, cG, cB),
|
||||
"count": count,
|
||||
}
|
||||
icon = self.createIcon(self._height, qColor, iShape)
|
||||
self._store[key] = StatusEntry(name, qColor, iShape, icon, count)
|
||||
|
||||
if self._default is None:
|
||||
self._default = key
|
||||
|
||||
return key
|
||||
|
||||
def remove(self, key: str) -> bool:
|
||||
"""Remove an entry in the list, except if the count > 0."""
|
||||
if key not in self._store:
|
||||
return False
|
||||
if self._store[key]["count"] > 0:
|
||||
return False
|
||||
def update(self, update: list[tuple[str | None, StatusEntry]]) -> None:
|
||||
"""Update the list of statuses."""
|
||||
self._store.clear()
|
||||
for key, entry in update:
|
||||
self._store[self._checkKey(key)] = entry
|
||||
|
||||
del self._store[key]
|
||||
# Check if we need a new default
|
||||
if self._default not in self._store:
|
||||
self._default = next(iter(self._store)) if self._store else None
|
||||
|
||||
keys = list(self._store.keys())
|
||||
if key == self._default:
|
||||
if len(keys) > 0:
|
||||
self._default = keys[0]
|
||||
else:
|
||||
self._default = None
|
||||
# Emit the change signal
|
||||
SHARED.projectSingalProxy({"event": "statusLabels", "kind": self._prefix})
|
||||
|
||||
return True
|
||||
return
|
||||
|
||||
def check(self, value: str) -> str:
|
||||
"""Check the key against the stored status names."""
|
||||
@@ -129,93 +143,51 @@ class NWStatus:
|
||||
return self._default
|
||||
return ""
|
||||
|
||||
def name(self, key: str | None) -> str:
|
||||
"""Return the name associated with a given key."""
|
||||
if key and key in self._store:
|
||||
return self._store[key]["name"]
|
||||
elif self._default is not None:
|
||||
return self._store[self._default]["name"]
|
||||
return ""
|
||||
|
||||
def cols(self, key: str | None) -> tuple[int, int, int]:
|
||||
"""Return the colours associated with a given key."""
|
||||
if key and key in self._store:
|
||||
return self._store[key]["cols"]
|
||||
elif self._default is not None:
|
||||
return self._store[self._default]["cols"]
|
||||
return 100, 100, 100
|
||||
|
||||
def count(self, key: str | None) -> int:
|
||||
"""Return the count associated with a given key."""
|
||||
if key and key in self._store:
|
||||
return self._store[key]["count"]
|
||||
elif self._default is not None:
|
||||
return self._store[self._default]["count"]
|
||||
return 0
|
||||
|
||||
def icon(self, key: str | None) -> QIcon:
|
||||
"""Return the icon associated with a given key."""
|
||||
if key and key in self._store:
|
||||
return self._store[key]["icon"]
|
||||
elif self._default is not None:
|
||||
return self._store[self._default]["icon"]
|
||||
return self._defaultIcon
|
||||
|
||||
def reorder(self, order: list[str]) -> bool:
|
||||
"""Reorder the items according to list."""
|
||||
if len(order) != len(self._store):
|
||||
logger.error("Length mismatch between new and old order")
|
||||
return False
|
||||
|
||||
if order == list(self._store.keys()):
|
||||
return False
|
||||
|
||||
store = {}
|
||||
for key in order:
|
||||
if key in self._store:
|
||||
store[key] = self._store[key]
|
||||
else:
|
||||
logger.error("Unknown key '%s' in order", key)
|
||||
return False
|
||||
|
||||
self._store = store
|
||||
|
||||
return True
|
||||
|
||||
def resetCounts(self) -> None:
|
||||
"""Clear the counts of references to the status entries."""
|
||||
for key in self._store:
|
||||
self._store[key]["count"] = 0
|
||||
for entry in self._store.values():
|
||||
entry.count = 0
|
||||
return
|
||||
|
||||
def increment(self, key: str | None) -> None:
|
||||
"""Increment the counter for a given entry."""
|
||||
if key and key in self._store:
|
||||
self._store[key]["count"] += 1
|
||||
self._store[key].count += 1
|
||||
return
|
||||
|
||||
def pack(self) -> Iterable[tuple[str, dict]]:
|
||||
"""Pack the status entries into a dictionary."""
|
||||
for key, data in self._store.items():
|
||||
yield (data["name"], {
|
||||
for key, entry in self._store.items():
|
||||
yield (entry.name, {
|
||||
"key": key,
|
||||
"count": str(data["count"]),
|
||||
"red": str(data["cols"][0]),
|
||||
"green": str(data["cols"][1]),
|
||||
"blue": str(data["cols"][2]),
|
||||
"count": str(entry.count),
|
||||
"red": str(entry.color.red()),
|
||||
"green": str(entry.color.green()),
|
||||
"blue": str(entry.color.blue()),
|
||||
"shape": entry.shape.name,
|
||||
})
|
||||
return
|
||||
|
||||
def unpack(self, data: dict) -> None:
|
||||
"""Unpack a data dictionary and set the class values."""
|
||||
self._store = {}
|
||||
self._default = None
|
||||
for key, entry in data.items():
|
||||
label = entry.get("label", "")
|
||||
colour = entry.get("colour", (100, 100, 100))
|
||||
count = entry.get("count", 0)
|
||||
self.write(key, label, colour, count)
|
||||
return
|
||||
def iterItems(self) -> Iterable[tuple[str, StatusEntry]]:
|
||||
"""Yield entries from the status icons."""
|
||||
yield from self._store.items()
|
||||
|
||||
@staticmethod
|
||||
def createIcon(height: int, color: QColor, shape: nwStatusShape) -> QIcon:
|
||||
"""Generate an icon for a status label."""
|
||||
pixmap = QPixmap(48, 48)
|
||||
pixmap.fill(QtTransparent)
|
||||
|
||||
painter = QPainter(pixmap)
|
||||
painter.setRenderHint(QtPaintAnitAlias)
|
||||
painter.fillPath(_SHAPES.getShape(shape), color)
|
||||
painter.end()
|
||||
|
||||
return QIcon(pixmap.scaled(
|
||||
height, height,
|
||||
Qt.AspectRatioMode.IgnoreAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation
|
||||
))
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
@@ -246,38 +218,116 @@ class NWStatus:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _createIcon(self, red: int, green: int, blue: int) -> QIcon:
|
||||
"""Generate an icon for a status label."""
|
||||
pixmap = QPixmap(self._iPX, self._iPX)
|
||||
pixmap.fill(QtTransparent)
|
||||
def _checkKey(self, key: str | None) -> str:
|
||||
"""Check key is valid, and if not, generate one."""
|
||||
return key if self._isKey(key) else self._newKey()
|
||||
|
||||
painter = QPainter(pixmap)
|
||||
painter.setRenderHint(QtPaintAnitAlias)
|
||||
painter.fillPath(self._iconPath, QColor(red, green, blue))
|
||||
painter.end()
|
||||
|
||||
return QIcon(pixmap)
|
||||
class _ShapeCache:
|
||||
|
||||
##
|
||||
# Iterator Bits
|
||||
##
|
||||
def __init__(self) -> None:
|
||||
self._cache: dict[nwStatusShape, QPainterPath] = {}
|
||||
return
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._store)
|
||||
def getShape(self, shape: nwStatusShape) -> QPainterPath:
|
||||
"""Return a painter shape for an icon."""
|
||||
if shape in self._cache:
|
||||
return self._cache[shape]
|
||||
|
||||
def __getitem__(self, key: str) -> dict:
|
||||
return self._store[key]
|
||||
path = QPainterPath()
|
||||
if shape == nwStatusShape.SQUARE:
|
||||
path.addRoundedRect(2.0, 2.0, 44.0, 44.0, 4.0, 4.0)
|
||||
elif shape == nwStatusShape.TRIANGLE:
|
||||
path.addPolygon(QPolygonF([
|
||||
QPointF(24.00, 3.00),
|
||||
QPointF(43.92, 37.50),
|
||||
QPointF(4.08, 37.50),
|
||||
]))
|
||||
elif shape == nwStatusShape.NABLA:
|
||||
path.addPolygon(QPolygonF([
|
||||
QPointF(24.00, 48.00),
|
||||
QPointF(4.08, 14.50),
|
||||
QPointF(43.92, 14.50),
|
||||
]))
|
||||
elif shape == nwStatusShape.DIAMOND:
|
||||
path.addPolygon(QPolygonF([
|
||||
QPointF(24.00, 2.00),
|
||||
QPointF(44.00, 24.00),
|
||||
QPointF(24.00, 46.00),
|
||||
QPointF(4.00, 24.00),
|
||||
]))
|
||||
elif shape == nwStatusShape.PENTAGON:
|
||||
path.addPolygon(QPolygonF([
|
||||
QPointF(24.00, 1.50),
|
||||
QPointF(45.87, 17.39),
|
||||
QPointF(37.52, 43.11),
|
||||
QPointF(10.48, 43.11),
|
||||
QPointF(2.13, 17.39),
|
||||
]))
|
||||
elif shape == nwStatusShape.HEXAGON:
|
||||
path.addPolygon(QPolygonF([
|
||||
QPointF(24.00, 1.50),
|
||||
QPointF(43.92, 13.00),
|
||||
QPointF(43.92, 36.00),
|
||||
QPointF(24.00, 47.50),
|
||||
QPointF(4.08, 36.00),
|
||||
QPointF(4.08, 13.00),
|
||||
]))
|
||||
elif shape == nwStatusShape.STAR:
|
||||
path.addPolygon(QPolygonF([
|
||||
QPointF(24.00, 0.50), QPointF(31.05, 14.79),
|
||||
QPointF(46.83, 17.08), QPointF(35.41, 28.21),
|
||||
QPointF(38.11, 43.92), QPointF(24.00, 36.50),
|
||||
QPointF(9.89, 43.92), QPointF(12.59, 28.21),
|
||||
QPointF(1.17, 17.08), QPointF(15.37, 16.16),
|
||||
]))
|
||||
elif shape == nwStatusShape.PACMAN:
|
||||
path.moveTo(24.0, 24.0)
|
||||
path.arcTo(2.0, 2.0, 44.0, 44.0, 40.0, 280.0)
|
||||
elif shape == nwStatusShape.CIRCLE_Q:
|
||||
path.moveTo(24.0, 24.0)
|
||||
path.arcTo(2.0, 2.0, 44.0, 44.0, 0.0, 90.0)
|
||||
elif shape == nwStatusShape.CIRCLE_H:
|
||||
path.moveTo(24.0, 24.0)
|
||||
path.arcTo(2.0, 2.0, 44.0, 44.0, -90.0, 180.0)
|
||||
elif shape == nwStatusShape.CIRCLE_T:
|
||||
path.moveTo(24.0, 24.0)
|
||||
path.arcTo(2.0, 2.0, 44.0, 44.0, -180.0, 270.0)
|
||||
elif shape == nwStatusShape.CIRCLE:
|
||||
path.addEllipse(2.0, 2.0, 44.0, 44.0)
|
||||
elif shape == nwStatusShape.BARS_1:
|
||||
path.addRoundedRect(2.0, 2.0, 8.0, 44.0, 4.0, 4.0)
|
||||
elif shape == nwStatusShape.BARS_2:
|
||||
path.addRoundedRect(2.0, 2.0, 8.0, 44.0, 4.0, 4.0)
|
||||
path.addRoundedRect(14.0, 2.0, 8.0, 44.0, 4.0, 4.0)
|
||||
elif shape == nwStatusShape.BARS_3:
|
||||
path.addRoundedRect(2.0, 2.0, 8.0, 44.0, 4.0, 4.0)
|
||||
path.addRoundedRect(14.0, 2.0, 8.0, 44.0, 4.0, 4.0)
|
||||
path.addRoundedRect(26.0, 2.0, 8.0, 44.0, 4.0, 4.0)
|
||||
elif shape == nwStatusShape.BARS_4:
|
||||
path.addRoundedRect(2.0, 2.0, 8.0, 44.0, 4.0, 4.0)
|
||||
path.addRoundedRect(14.0, 2.0, 8.0, 44.0, 4.0, 4.0)
|
||||
path.addRoundedRect(26.0, 2.0, 8.0, 44.0, 4.0, 4.0)
|
||||
path.addRoundedRect(38.0, 2.0, 8.0, 44.0, 4.0, 4.0)
|
||||
elif shape == nwStatusShape.BLOCK_1:
|
||||
path.addRoundedRect(2.0, 2.0, 20.0, 20.0, 4.0, 4.0)
|
||||
elif shape == nwStatusShape.BLOCK_2:
|
||||
path.addRoundedRect(2.0, 2.0, 20.0, 20.0, 4.0, 4.0)
|
||||
path.addRoundedRect(24.0, 2.0, 20.0, 20.0, 4.0, 4.0)
|
||||
elif shape == nwStatusShape.BLOCK_3:
|
||||
path.addRoundedRect(2.0, 2.0, 20.0, 20.0, 4.0, 4.0)
|
||||
path.addRoundedRect(2.0, 24.0, 20.0, 20.0, 4.0, 4.0)
|
||||
path.addRoundedRect(24.0, 2.0, 20.0, 20.0, 4.0, 4.0)
|
||||
elif shape == nwStatusShape.BLOCK_4:
|
||||
path.addRoundedRect(2.0, 2.0, 20.0, 20.0, 4.0, 4.0)
|
||||
path.addRoundedRect(2.0, 24.0, 20.0, 20.0, 4.0, 4.0)
|
||||
path.addRoundedRect(24.0, 2.0, 20.0, 20.0, 4.0, 4.0)
|
||||
path.addRoundedRect(24.0, 24.0, 20.0, 20.0, 4.0, 4.0)
|
||||
|
||||
def __iter__(self) -> Iterator[dict]:
|
||||
return iter(self._store)
|
||||
self._cache[shape] = path
|
||||
|
||||
def keys(self) -> KeysView[str]:
|
||||
return self._store.keys()
|
||||
return path
|
||||
|
||||
def items(self) -> ItemsView[str, dict]:
|
||||
return self._store.items()
|
||||
|
||||
def values(self) -> ValuesView[dict]:
|
||||
return self._store.values()
|
||||
|
||||
# END Class NWStatus
|
||||
# Create Singleton
|
||||
_SHAPES = _ShapeCache()
|
||||
|
||||
@@ -54,8 +54,6 @@ class NWStorageOpen(Enum):
|
||||
FAILED = 3
|
||||
READY = 4
|
||||
|
||||
# END Enum NWStorageOpen
|
||||
|
||||
|
||||
class NWStorageCreate(Enum):
|
||||
|
||||
@@ -63,8 +61,6 @@ class NWStorageCreate(Enum):
|
||||
OS_ERROR = 1
|
||||
READY = 2
|
||||
|
||||
# END Enum NWStorageCreate
|
||||
|
||||
|
||||
class NWStorage:
|
||||
"""Core: Project Storage Class
|
||||
@@ -239,7 +235,6 @@ class NWStorage:
|
||||
if child.is_dir() and child.name.startswith("data_"):
|
||||
legacy.legacyDataFolder(basePath, child)
|
||||
|
||||
self._writeLockFile()
|
||||
self._ready = True
|
||||
|
||||
return NWStorageOpen.READY
|
||||
@@ -253,6 +248,12 @@ class NWStorage:
|
||||
return True
|
||||
return True
|
||||
|
||||
def lockSession(self) -> None:
|
||||
"""Lock the session when the project is successfully opened."""
|
||||
if self._ready:
|
||||
self._writeLockFile()
|
||||
return
|
||||
|
||||
def closeSession(self) -> None:
|
||||
"""Run tasks related to closing the session."""
|
||||
self._clearLockFile()
|
||||
@@ -391,8 +392,6 @@ class NWStorage:
|
||||
self._lockedBy = None
|
||||
return True
|
||||
|
||||
# END Class NWStorage
|
||||
|
||||
|
||||
class _LegacyStorage:
|
||||
"""Core: Legacy Storage Converter Utils
|
||||
@@ -590,5 +589,3 @@ class _LegacyStorage:
|
||||
logException()
|
||||
|
||||
return
|
||||
|
||||
# END Class _LegacyStorage
|
||||
|
||||
@@ -26,17 +26,46 @@ from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
|
||||
from time import time
|
||||
from pathlib import Path
|
||||
from time import time
|
||||
|
||||
from novelwriter import CONFIG
|
||||
from novelwriter.common import formatTimeStamp
|
||||
from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwHtmlUnicode
|
||||
from novelwriter.constants import nwHeadFmt, nwHtmlUnicode, nwKeyWords, nwLabels
|
||||
from novelwriter.core.project import NWProject
|
||||
from novelwriter.core.tokenizer import Tokenizer, stripEscape
|
||||
from novelwriter.core.tokenizer import T_Formats, Tokenizer, stripEscape
|
||||
from novelwriter.types import FONT_STYLE, FONT_WEIGHTS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Each opener tag, with the id of its corresponding closer and tag format
|
||||
HTML_OPENER: dict[int, tuple[int, str]] = {
|
||||
Tokenizer.FMT_B_B: (Tokenizer.FMT_B_E, "<strong>"),
|
||||
Tokenizer.FMT_I_B: (Tokenizer.FMT_I_E, "<em>"),
|
||||
Tokenizer.FMT_D_B: (Tokenizer.FMT_D_E, "<del>"),
|
||||
Tokenizer.FMT_U_B: (Tokenizer.FMT_U_E, "<span style='text-decoration: underline;'>"),
|
||||
Tokenizer.FMT_M_B: (Tokenizer.FMT_M_E, "<mark>"),
|
||||
Tokenizer.FMT_SUP_B: (Tokenizer.FMT_SUP_E, "<sup>"),
|
||||
Tokenizer.FMT_SUB_B: (Tokenizer.FMT_SUB_E, "<sub>"),
|
||||
Tokenizer.FMT_DL_B: (Tokenizer.FMT_DL_E, "<span class='dialog'>"),
|
||||
Tokenizer.FMT_ADL_B: (Tokenizer.FMT_ADL_E, "<span class='altdialog'>"),
|
||||
}
|
||||
|
||||
# Each closer tag, with the id of its corresponding opener and tag format
|
||||
HTML_CLOSER: dict[int, tuple[int, str]] = {
|
||||
Tokenizer.FMT_B_E: (Tokenizer.FMT_B_B, "</strong>"),
|
||||
Tokenizer.FMT_I_E: (Tokenizer.FMT_I_B, "</em>"),
|
||||
Tokenizer.FMT_D_E: (Tokenizer.FMT_D_B, "</del>"),
|
||||
Tokenizer.FMT_U_E: (Tokenizer.FMT_U_B, "</span>"),
|
||||
Tokenizer.FMT_M_E: (Tokenizer.FMT_M_B, "</mark>"),
|
||||
Tokenizer.FMT_SUP_E: (Tokenizer.FMT_SUP_B, "</sup>"),
|
||||
Tokenizer.FMT_SUB_E: (Tokenizer.FMT_SUB_B, "</sub>"),
|
||||
Tokenizer.FMT_DL_E: (Tokenizer.FMT_DL_B, "</span>"),
|
||||
Tokenizer.FMT_ADL_E: (Tokenizer.FMT_ADL_B, "</span>"),
|
||||
}
|
||||
|
||||
# Empty HTML tag record
|
||||
HTML_NONE = (0, "")
|
||||
|
||||
|
||||
class ToHtml(Tokenizer):
|
||||
"""Core: HTML Document Writer
|
||||
@@ -45,19 +74,15 @@ class ToHtml(Tokenizer):
|
||||
also used by the Document Viewer, and Manuscript Build Preview.
|
||||
"""
|
||||
|
||||
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, project: NWProject) -> None:
|
||||
super().__init__(project)
|
||||
|
||||
self._genMode = self.M_EXPORT
|
||||
self._cssStyles = True
|
||||
self._fullHTML: list[str] = []
|
||||
|
||||
# Internals
|
||||
self._trMap = {}
|
||||
self._usedNotes: dict[str, int] = {}
|
||||
self.setReplaceUnicode(False)
|
||||
|
||||
return
|
||||
@@ -74,11 +99,6 @@ class ToHtml(Tokenizer):
|
||||
# Setters
|
||||
##
|
||||
|
||||
def setPreview(self, state: bool) -> None:
|
||||
"""Set to preview generator mode."""
|
||||
self._genMode = self.M_PREVIEW if state else self.M_EXPORT
|
||||
return
|
||||
|
||||
def setStyles(self, cssStyles: bool) -> None:
|
||||
"""Enable or disable CSS styling. Some elements may still have
|
||||
class tags.
|
||||
@@ -117,39 +137,9 @@ class ToHtml(Tokenizer):
|
||||
|
||||
def doConvert(self) -> None:
|
||||
"""Convert the list of text tokens into an HTML document."""
|
||||
if self._genMode == self.M_PREVIEW:
|
||||
htmlTags = { # HTML4 + CSS2 (for Qt)
|
||||
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>",
|
||||
self.FMT_U_B: "<u>",
|
||||
self.FMT_U_E: "</u>",
|
||||
self.FMT_M_B: "<mark>",
|
||||
self.FMT_M_E: "</mark>",
|
||||
}
|
||||
else:
|
||||
htmlTags = { # HTML5 (for export)
|
||||
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>",
|
||||
self.FMT_U_B: "<span style='text-decoration: underline;'>",
|
||||
self.FMT_U_E: "</span>",
|
||||
self.FMT_M_B: "<mark>",
|
||||
self.FMT_M_E: "</mark>",
|
||||
}
|
||||
self._result = ""
|
||||
|
||||
htmlTags[self.FMT_SUP_B] = "<sup>"
|
||||
htmlTags[self.FMT_SUP_E] = "</sup>"
|
||||
htmlTags[self.FMT_SUB_B] = "<sub>"
|
||||
htmlTags[self.FMT_SUB_E] = "</sub>"
|
||||
|
||||
if self._isNovel and self._genMode != self.M_PREVIEW:
|
||||
if self._isNovel:
|
||||
# For story files, we bump the titles one level up
|
||||
h1Cl = " class='title'"
|
||||
h1 = "h1"
|
||||
@@ -163,12 +153,7 @@ class ToHtml(Tokenizer):
|
||||
h3 = "h3"
|
||||
h4 = "h4"
|
||||
|
||||
self._result = ""
|
||||
|
||||
para = []
|
||||
pStyle = None
|
||||
lines = []
|
||||
|
||||
tHandle = self._handle
|
||||
|
||||
for tType, nHead, tText, tFormat, tStyle in self._tokens:
|
||||
@@ -181,11 +166,11 @@ class ToHtml(Tokenizer):
|
||||
for c in tText:
|
||||
if c == "<":
|
||||
cText.append("<")
|
||||
tFormat = [[p + 3 if p > i else p, f] for p, f in tFormat]
|
||||
tFormat = [(p + 3 if p > i else p, f, k) for p, f, k in tFormat]
|
||||
i += 4
|
||||
elif c == ">":
|
||||
cText.append(">")
|
||||
tFormat = [[p + 3 if p > i else p, f] for p, f in tFormat]
|
||||
tFormat = [(p + 3 if p > i else p, f, k) for p, f, k in tFormat]
|
||||
i += 4
|
||||
else:
|
||||
cText.append(c)
|
||||
@@ -209,7 +194,6 @@ class ToHtml(Tokenizer):
|
||||
|
||||
if tStyle & self.A_PBB:
|
||||
aStyle.append("page-break-before: always;")
|
||||
|
||||
if tStyle & self.A_PBA:
|
||||
aStyle.append("page-break-after: always;")
|
||||
|
||||
@@ -219,11 +203,13 @@ class ToHtml(Tokenizer):
|
||||
aStyle.append("margin-top: 0;")
|
||||
|
||||
if tStyle & self.A_IND_L:
|
||||
aStyle.append(f"margin-left: {CONFIG.tabWidth:d}px;")
|
||||
aStyle.append(f"margin-left: {self._blockIndent:.2f}em;")
|
||||
if tStyle & self.A_IND_R:
|
||||
aStyle.append(f"margin-right: {CONFIG.tabWidth:d}px;")
|
||||
aStyle.append(f"margin-right: {self._blockIndent:.2f}em;")
|
||||
if tStyle & self.A_IND_T:
|
||||
aStyle.append(f"text-indent: {self._firstWidth:.2f}em;")
|
||||
|
||||
if len(aStyle) > 0:
|
||||
if aStyle:
|
||||
stVals = " ".join(aStyle)
|
||||
hStyle = f" style='{stVals}'"
|
||||
else:
|
||||
@@ -235,37 +221,27 @@ class ToHtml(Tokenizer):
|
||||
aNm = ""
|
||||
|
||||
# Process Text Type
|
||||
if tType == self.T_EMPTY:
|
||||
if pStyle is None:
|
||||
pStyle = ""
|
||||
if len(para) > 1 and self._cssStyles:
|
||||
pClass = " class='break'"
|
||||
else:
|
||||
pClass = ""
|
||||
if len(para) > 0:
|
||||
tTemp = "<br/>".join(para)
|
||||
lines.append(f"<p{pClass+pStyle}>{tTemp.rstrip()}</p>\n")
|
||||
para = []
|
||||
pStyle = None
|
||||
if tType == self.T_TEXT:
|
||||
lines.append(f"<p{hStyle}>{self._formatText(tText, tFormat)}</p>\n")
|
||||
|
||||
elif tType == self.T_TITLE:
|
||||
tHead = tText.replace(nwHeadFmt.BR, "<br/>")
|
||||
tHead = tText.replace(nwHeadFmt.BR, "<br>")
|
||||
lines.append(f"<h1 class='title'{hStyle}>{aNm}{tHead}</h1>\n")
|
||||
|
||||
elif tType == self.T_HEAD1:
|
||||
tHead = tText.replace(nwHeadFmt.BR, "<br/>")
|
||||
tHead = tText.replace(nwHeadFmt.BR, "<br>")
|
||||
lines.append(f"<{h1}{h1Cl}{hStyle}>{aNm}{tHead}</{h1}>\n")
|
||||
|
||||
elif tType == self.T_HEAD2:
|
||||
tHead = tText.replace(nwHeadFmt.BR, "<br/>")
|
||||
tHead = tText.replace(nwHeadFmt.BR, "<br>")
|
||||
lines.append(f"<{h2}{hStyle}>{aNm}{tHead}</{h2}>\n")
|
||||
|
||||
elif tType == self.T_HEAD3:
|
||||
tHead = tText.replace(nwHeadFmt.BR, "<br/>")
|
||||
tHead = tText.replace(nwHeadFmt.BR, "<br>")
|
||||
lines.append(f"<{h3}{hStyle}>{aNm}{tHead}</{h3}>\n")
|
||||
|
||||
elif tType == self.T_HEAD4:
|
||||
tHead = tText.replace(nwHeadFmt.BR, "<br/>")
|
||||
tHead = tText.replace(nwHeadFmt.BR, "<br>")
|
||||
lines.append(f"<{h4}{hStyle}>{aNm}{tHead}</{h4}>\n")
|
||||
|
||||
elif tType == self.T_SEP:
|
||||
@@ -274,22 +250,14 @@ class ToHtml(Tokenizer):
|
||||
elif tType == self.T_SKIP:
|
||||
lines.append(f"<p class='skip'{hStyle}> </p>\n")
|
||||
|
||||
elif tType == self.T_TEXT:
|
||||
tTemp = tText
|
||||
if pStyle is None:
|
||||
pStyle = hStyle
|
||||
for pos, fmt in reversed(tFormat):
|
||||
tTemp = f"{tTemp[:pos]}{htmlTags[fmt]}{tTemp[pos:]}"
|
||||
para.append(stripEscape(tTemp.rstrip()))
|
||||
|
||||
elif tType == self.T_SYNOPSIS and self._doSynopsis:
|
||||
lines.append(self._formatSynopsis(tText, True))
|
||||
lines.append(self._formatSynopsis(self._formatText(tText, tFormat), True))
|
||||
|
||||
elif tType == self.T_SHORT and self._doSynopsis:
|
||||
lines.append(self._formatSynopsis(tText, False))
|
||||
lines.append(self._formatSynopsis(self._formatText(tText, tFormat), False))
|
||||
|
||||
elif tType == self.T_COMMENT and self._doComments:
|
||||
lines.append(self._formatComments(tText))
|
||||
lines.append(self._formatComments(self._formatText(tText, tFormat)))
|
||||
|
||||
elif tType == self.T_KEYWORD and self._doKeywords:
|
||||
tag, text = self._formatKeywords(tText)
|
||||
@@ -302,6 +270,26 @@ class ToHtml(Tokenizer):
|
||||
|
||||
return
|
||||
|
||||
def appendFootnotes(self) -> None:
|
||||
"""Append the footnotes in the buffer."""
|
||||
if self._usedNotes:
|
||||
footnotes = self._localLookup("Footnotes")
|
||||
|
||||
lines = []
|
||||
lines.append(f"<h3>{footnotes}</h3>\n")
|
||||
lines.append("<ol>\n")
|
||||
for key, index in self._usedNotes.items():
|
||||
if content := self._footnotes.get(key):
|
||||
text = self._formatText(*content)
|
||||
lines.append(f"<li id='footnote_{index}'><p>{text}</p></li>\n")
|
||||
lines.append("</ol>\n")
|
||||
|
||||
result = "".join(lines)
|
||||
self._result += result
|
||||
self._fullHTML.append(result)
|
||||
|
||||
return
|
||||
|
||||
def saveHtml5(self, path: str | Path) -> None:
|
||||
"""Save the data to an HTML file."""
|
||||
with open(path, mode="w", encoding="utf-8") as fObj:
|
||||
@@ -367,8 +355,16 @@ class ToHtml(Tokenizer):
|
||||
mScale = self._lineHeight/1.15
|
||||
|
||||
styles = []
|
||||
styles.append("body {{font-family: '{0:s}'; font-size: {1:d}pt;}}".format(
|
||||
self._textFont, self._textSize
|
||||
font = self._textFont
|
||||
styles.append((
|
||||
"body {{"
|
||||
"font-family: '{0:s}'; font-size: {1:d}pt; "
|
||||
"font-weight: {2:d}; font-style: {3:s};"
|
||||
"}}"
|
||||
).format(
|
||||
font.family(), font.pointSize(),
|
||||
FONT_WEIGHTS.get(font.weight(), 400),
|
||||
FONT_STYLE.get(font.style(), "normal"),
|
||||
))
|
||||
styles.append((
|
||||
"p {{"
|
||||
@@ -442,10 +438,12 @@ class ToHtml(Tokenizer):
|
||||
|
||||
styles.append("a {color: rgb(66, 113, 174);}")
|
||||
styles.append("mark {background: rgb(255, 255, 166);}")
|
||||
styles.append(".tags {color: rgb(245, 135, 31); font-weight: bold;}")
|
||||
styles.append(".keyword {color: rgb(245, 135, 31); font-weight: bold;}")
|
||||
styles.append(".break {text-align: left;}")
|
||||
styles.append(".synopsis {font-style: italic;}")
|
||||
styles.append(".comment {font-style: italic; color: rgb(100, 100, 100);}")
|
||||
styles.append(".dialog {color: rgb(66, 113, 174);}")
|
||||
styles.append(".altdialog {color: rgb(129, 55, 9);}")
|
||||
|
||||
return styles
|
||||
|
||||
@@ -453,24 +451,63 @@ class ToHtml(Tokenizer):
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _formatText(self, text: str, tFmt: T_Formats) -> str:
|
||||
"""Apply formatting tags to text."""
|
||||
temp = text
|
||||
|
||||
# Build a list of all html tags that need to be inserted in the text.
|
||||
# This is done in the forward direction, and a tag is only opened if it
|
||||
# isn't already open, and only closed if it has previously been opened.
|
||||
tags: list[tuple[int, str]] = []
|
||||
state = dict.fromkeys(HTML_OPENER, False)
|
||||
for pos, fmt, data in tFmt:
|
||||
if m := HTML_OPENER.get(fmt):
|
||||
if not state.get(fmt, True):
|
||||
tags.append((pos, m[1]))
|
||||
state[fmt] = True
|
||||
elif m := HTML_CLOSER.get(fmt):
|
||||
if state.get(m[0], False):
|
||||
tags.append((pos, m[1]))
|
||||
state[m[0]] = False
|
||||
elif fmt == self.FMT_FNOTE:
|
||||
if data in self._footnotes:
|
||||
index = len(self._usedNotes) + 1
|
||||
self._usedNotes[data] = index
|
||||
tags.append((pos, f"<sup><a href='#footnote_{index}'>{index}</a></sup>"))
|
||||
else:
|
||||
tags.append((pos, "<sup>ERR</sup>"))
|
||||
|
||||
# Check all format types and close any tag that is still open. This
|
||||
# ensures that unclosed tags don't spill over to the next paragraph.
|
||||
end = len(text)
|
||||
for opener, active in state.items():
|
||||
if active:
|
||||
closer = HTML_OPENER.get(opener, HTML_NONE)[0]
|
||||
tags.append((end, HTML_CLOSER.get(closer, HTML_NONE)[1]))
|
||||
|
||||
# Insert all tags at their correct position, starting from the back.
|
||||
# The reverse order ensures that the positions are not shifted while we
|
||||
# insert tags.
|
||||
for pos, tag in reversed(tags):
|
||||
temp = f"{temp[:pos]}{tag}{temp[pos:]}"
|
||||
|
||||
# Replace all line breaks with proper HTML break tags
|
||||
temp = temp.replace("\n", "<br>")
|
||||
|
||||
return stripEscape(temp)
|
||||
|
||||
def _formatSynopsis(self, text: str, synopsis: bool) -> str:
|
||||
"""Apply HTML formatting to synopsis."""
|
||||
if synopsis:
|
||||
sSynop = self._localLookup("Synopsis")
|
||||
else:
|
||||
sSynop = self._localLookup("Short Description")
|
||||
if self._genMode == self.M_PREVIEW:
|
||||
return f"<p class='comment'><span class='synopsis'>{sSynop}:</span> {text}</p>\n"
|
||||
else:
|
||||
return f"<p class='synopsis'><strong>{sSynop}:</strong> {text}</p>\n"
|
||||
return f"<p class='synopsis'><strong>{sSynop}:</strong> {text}</p>\n"
|
||||
|
||||
def _formatComments(self, text: str) -> str:
|
||||
"""Apply HTML formatting to comments."""
|
||||
if self._genMode == self.M_PREVIEW:
|
||||
return f"<p class='comment'>{text}</p>\n"
|
||||
else:
|
||||
sComm = self._localLookup("Comment")
|
||||
return f"<p class='comment'><strong>{sComm}:</strong> {text}</p>\n"
|
||||
sComm = self._localLookup("Comment")
|
||||
return f"<p class='comment'><strong>{sComm}:</strong> {text}</p>\n"
|
||||
|
||||
def _formatKeywords(self, text: str) -> tuple[str, str]:
|
||||
"""Apply HTML formatting to keywords."""
|
||||
@@ -478,19 +515,16 @@ class ToHtml(Tokenizer):
|
||||
if not valid or not bits or bits[0] not in nwLabels.KEY_NAME:
|
||||
return "", ""
|
||||
|
||||
result = f"<span class='tags'>{self._localLookup(nwLabels.KEY_NAME[bits[0]])}:</span> "
|
||||
result = f"<span class='keyword'>{self._localLookup(nwLabels.KEY_NAME[bits[0]])}:</span> "
|
||||
if len(bits) > 1:
|
||||
if bits[0] == nwKeyWords.TAG_KEY:
|
||||
one, two = self._project.index.parseValue(bits[1])
|
||||
result += f"<a name='tag_{one}'>{one}</a>"
|
||||
result += f"<a class='tag' name='tag_{one}'>{one}</a>"
|
||||
if two:
|
||||
result += f" | <span class='optional'>{two}</a>"
|
||||
else:
|
||||
if self._genMode == self.M_PREVIEW:
|
||||
result += ", ".join(f"<a href='#{bits[0][1:]}={t}'>{t}</a>" for t in bits[1:])
|
||||
else:
|
||||
result += ", ".join(f"<a href='#tag_{t}'>{t}</a>" for t in bits[1:])
|
||||
result += ", ".join(
|
||||
f"<a class='tag' href='#tag_{t}'>{t}</a>" for t in bits[1:]
|
||||
)
|
||||
|
||||
return bits[0][1:], result
|
||||
|
||||
# END Class ToHtml
|
||||
|
||||
@@ -24,30 +24,35 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from time import time
|
||||
from pathlib import Path
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from time import time
|
||||
|
||||
from PyQt5.QtCore import QCoreApplication, QRegularExpression
|
||||
from PyQt5.QtGui import QFont
|
||||
|
||||
from novelwriter.common import formatTimeStamp, numberToRoman, checkInt
|
||||
from novelwriter.constants import (
|
||||
nwHeadFmt, nwKeyWords, nwLabels, nwRegEx, nwShortcode, nwUnicode, trConst
|
||||
)
|
||||
from novelwriter import CONFIG
|
||||
from novelwriter.common import checkInt, formatTimeStamp, numberToRoman
|
||||
from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwShortcode, nwUnicode, trConst
|
||||
from novelwriter.core.index import processComment
|
||||
from novelwriter.core.project import NWProject
|
||||
from novelwriter.enum import nwComment, nwItemLayout
|
||||
from novelwriter.text.patterns import REGEX_PATTERNS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ESCAPES = {r"\*": "*", r"\~": "~", r"\_": "_", r"\[": "[", r"\]": "]", r"\ ": ""}
|
||||
RX_ESC = re.compile("|".join([re.escape(k) for k in ESCAPES.keys()]), flags=re.DOTALL)
|
||||
|
||||
T_Formats = list[tuple[int, int, str]]
|
||||
T_Comment = tuple[str, T_Formats]
|
||||
T_Token = tuple[int, int, str, T_Formats, int]
|
||||
|
||||
|
||||
def stripEscape(text: str) -> str:
|
||||
"""Strip escaped Markdown characters from paragraph text."""
|
||||
@@ -80,6 +85,12 @@ class Tokenizer(ABC):
|
||||
FMT_SUP_E = 12 # End superscript
|
||||
FMT_SUB_B = 13 # Begin subscript
|
||||
FMT_SUB_E = 14 # End subscript
|
||||
FMT_DL_B = 15 # Begin dialogue
|
||||
FMT_DL_E = 16 # End dialogue
|
||||
FMT_ADL_B = 17 # Begin alt dialogue
|
||||
FMT_ADL_E = 18 # End alt dialogue
|
||||
FMT_FNOTE = 19 # Footnote marker
|
||||
FMT_STRIP = 20 # Strip the format code
|
||||
|
||||
# Block Type
|
||||
T_EMPTY = 1 # Empty line (new paragraph)
|
||||
@@ -108,48 +119,60 @@ class Tokenizer(ABC):
|
||||
A_Z_BTMMRG = 0x0080 # Zero bottom margin
|
||||
A_IND_L = 0x0100 # Left indentation
|
||||
A_IND_R = 0x0200 # Right indentation
|
||||
A_IND_T = 0x0400 # Text indentation
|
||||
|
||||
# Masks
|
||||
M_ALIGNED = A_LEFT | A_RIGHT | A_CENTRE | A_JUSTIFY
|
||||
|
||||
# Lookups
|
||||
L_HEADINGS = [T_TITLE, T_HEAD1, T_HEAD2, T_HEAD3, T_HEAD4]
|
||||
L_SKIP_INDENT = [T_TITLE, T_HEAD1, T_HEAD2, T_HEAD2, T_HEAD3, T_HEAD4, T_SEP, T_SKIP]
|
||||
L_SUMMARY = [T_SYNOPSIS, T_SHORT]
|
||||
|
||||
def __init__(self, project: NWProject) -> None:
|
||||
|
||||
self._project = project
|
||||
|
||||
# Data Variables
|
||||
self._text = "" # The raw text to be tokenized
|
||||
self._handle = None # The item handle currently being processed
|
||||
self._result = "" # The result of the last document
|
||||
self._text = "" # The raw text to be tokenized
|
||||
self._handle = None # The item handle currently being processed
|
||||
self._result = "" # The result of the last document
|
||||
self._keepMD = False # Whether to keep the markdown text
|
||||
|
||||
self._keepMarkdown = False # Whether to keep the markdown text
|
||||
self._allMarkdown = [] # The result novelWriter markdown of all documents
|
||||
# Tokens and Meta Data (Per Document)
|
||||
self._tokens: list[T_Token] = []
|
||||
self._footnotes: dict[str, T_Comment] = {}
|
||||
|
||||
# Processed Tokens and Meta Data
|
||||
self._tokens: list[tuple[int, int, str, list[tuple[int, int]], int]] = []
|
||||
# Tokens and Meta Data (Per Instance)
|
||||
self._counts: dict[str, int] = {}
|
||||
self._outline: dict[str, str] = {}
|
||||
self._markdown: list[str] = []
|
||||
|
||||
# User Settings
|
||||
self._textFont = "Serif" # Output text font
|
||||
self._textSize = 11 # Output text size
|
||||
self._textFixed = False # Fixed width text
|
||||
self._textFont = QFont("Serif", 11) # Output text font
|
||||
self._lineHeight = 1.15 # Line height in units of em
|
||||
self._blockIndent = 4.00 # Block indent in units of em
|
||||
self._firstIndent = False # Enable first line indent
|
||||
self._firstWidth = 1.40 # First line indent in units of em
|
||||
self._indentFirst = False # Indent first paragraph
|
||||
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._skipKeywords = set() # Keywords to ignore
|
||||
self._keepBreaks = True # Keep line breaks in paragraphs
|
||||
|
||||
# Margins
|
||||
self._marginTitle = (1.000, 0.500)
|
||||
self._marginHead1 = (1.000, 0.500)
|
||||
self._marginHead2 = (0.834, 0.500)
|
||||
self._marginHead3 = (0.584, 0.500)
|
||||
self._marginHead4 = (0.584, 0.500)
|
||||
self._marginTitle = (1.417, 0.500)
|
||||
self._marginHead1 = (1.417, 0.500)
|
||||
self._marginHead2 = (1.668, 0.500)
|
||||
self._marginHead3 = (1.168, 0.500)
|
||||
self._marginHead4 = (1.168, 0.500)
|
||||
self._marginText = (0.000, 0.584)
|
||||
self._marginMeta = (0.000, 0.584)
|
||||
self._marginFoot = (1.417, 0.467)
|
||||
self._marginSep = (1.168, 1.168)
|
||||
|
||||
# Title Formats
|
||||
self._fmtTitle = nwHeadFmt.TITLE # Formatting for titles
|
||||
@@ -174,7 +197,9 @@ class Tokenizer(ABC):
|
||||
|
||||
# Instance Variables
|
||||
self._hFormatter = HeadingFormatter(self._project)
|
||||
self._noSep = True # Flag to indicate that we don't want a scene separator
|
||||
self._noSep = True # Flag to indicate that we don't want a scene separator
|
||||
self._noIndent = False # Flag to disable text indent on next paragraph
|
||||
self._showDialog = False # Flag for dialogue highlighting
|
||||
|
||||
# This File
|
||||
self._isNovel = False # Document is a novel document
|
||||
@@ -189,12 +214,12 @@ class Tokenizer(ABC):
|
||||
|
||||
# Format RegEx
|
||||
self._rxMarkdown = [
|
||||
(QRegularExpression(nwRegEx.FMT_EI), [0, self.FMT_I_B, 0, self.FMT_I_E]),
|
||||
(QRegularExpression(nwRegEx.FMT_EB), [0, self.FMT_B_B, 0, self.FMT_B_E]),
|
||||
(QRegularExpression(nwRegEx.FMT_ST), [0, self.FMT_D_B, 0, self.FMT_D_E]),
|
||||
(REGEX_PATTERNS.markdownItalic, [0, self.FMT_I_B, 0, self.FMT_I_E]),
|
||||
(REGEX_PATTERNS.markdownBold, [0, self.FMT_B_B, 0, self.FMT_B_E]),
|
||||
(REGEX_PATTERNS.markdownStrike, [0, self.FMT_D_B, 0, self.FMT_D_E]),
|
||||
]
|
||||
self._rxShortCodes = QRegularExpression(nwRegEx.FMT_SC)
|
||||
self._rxShortCodeVals = QRegularExpression(nwRegEx.FMT_SV)
|
||||
self._rxShortCodes = REGEX_PATTERNS.shortcodePlain
|
||||
self._rxShortCodeVals = REGEX_PATTERNS.shortcodeValue
|
||||
|
||||
self._shortCodeFmt = {
|
||||
nwShortcode.ITALIC_O: self.FMT_I_B, nwShortcode.ITALIC_C: self.FMT_I_E,
|
||||
@@ -205,6 +230,11 @@ class Tokenizer(ABC):
|
||||
nwShortcode.SUP_O: self.FMT_SUP_B, nwShortcode.SUP_C: self.FMT_SUP_E,
|
||||
nwShortcode.SUB_O: self.FMT_SUB_B, nwShortcode.SUB_C: self.FMT_SUB_E,
|
||||
}
|
||||
self._shortCodeVals = {
|
||||
nwShortcode.FOOTNOTE_B: self.FMT_FNOTE,
|
||||
}
|
||||
|
||||
self._rxDialogue: list[tuple[QRegularExpression, int, int]] = []
|
||||
|
||||
return
|
||||
|
||||
@@ -220,7 +250,7 @@ class Tokenizer(ABC):
|
||||
@property
|
||||
def allMarkdown(self) -> list[str]:
|
||||
"""The combined novelWriter Markdown text."""
|
||||
return self._allMarkdown
|
||||
return self._markdown
|
||||
|
||||
@property
|
||||
def textStats(self) -> dict[str, int]:
|
||||
@@ -298,11 +328,9 @@ class Tokenizer(ABC):
|
||||
)
|
||||
return
|
||||
|
||||
def setFont(self, family: str, size: int, isFixed: bool = False) -> None:
|
||||
def setFont(self, font: QFont) -> None:
|
||||
"""Set the build font."""
|
||||
self._textFont = family
|
||||
self._textSize = round(int(size))
|
||||
self._textFixed = isFixed
|
||||
self._textFont = font
|
||||
return
|
||||
|
||||
def setLineHeight(self, height: float) -> None:
|
||||
@@ -315,11 +343,43 @@ class Tokenizer(ABC):
|
||||
self._blockIndent = min(max(float(indent), 0.0), 10.0)
|
||||
return
|
||||
|
||||
def setFirstLineIndent(self, state: bool, indent: float, first: bool) -> None:
|
||||
"""Set first line indent and whether to also indent first
|
||||
paragraph after a heading.
|
||||
"""
|
||||
self._firstIndent = state
|
||||
self._firstWidth = indent
|
||||
self._indentFirst = first
|
||||
return
|
||||
|
||||
def setJustify(self, state: bool) -> None:
|
||||
"""Enable or disable text justification."""
|
||||
self._doJustify = state
|
||||
return
|
||||
|
||||
def setDialogueHighlight(self, state: bool) -> None:
|
||||
"""Enable or disable dialogue highlighting."""
|
||||
self._rxDialogue = []
|
||||
self._showDialog = state
|
||||
if state:
|
||||
if CONFIG.dialogStyle > 0:
|
||||
self._rxDialogue.append((
|
||||
REGEX_PATTERNS.dialogStyle, self.FMT_DL_B, self.FMT_DL_E
|
||||
))
|
||||
if CONFIG.dialogLine:
|
||||
self._rxDialogue.append((
|
||||
REGEX_PATTERNS.dialogLine, self.FMT_DL_B, self.FMT_DL_E
|
||||
))
|
||||
if CONFIG.narratorBreak:
|
||||
self._rxDialogue.append((
|
||||
REGEX_PATTERNS.narratorBreak, self.FMT_DL_E, self.FMT_DL_B
|
||||
))
|
||||
if CONFIG.altDialogOpen and CONFIG.altDialogClose:
|
||||
self._rxDialogue.append((
|
||||
REGEX_PATTERNS.altDialogStyle, self.FMT_ADL_B, self.FMT_ADL_E
|
||||
))
|
||||
return
|
||||
|
||||
def setTitleMargins(self, upper: float, lower: float) -> None:
|
||||
"""Set the upper and lower title margin."""
|
||||
self._marginTitle = (float(upper), float(lower))
|
||||
@@ -355,6 +415,11 @@ class Tokenizer(ABC):
|
||||
self._marginMeta = (float(upper), float(lower))
|
||||
return
|
||||
|
||||
def setSeparatorMargins(self, upper: float, lower: float) -> None:
|
||||
"""Set the upper and lower meta text margin."""
|
||||
self._marginSep = (float(upper), float(lower))
|
||||
return
|
||||
|
||||
def setLinkHeadings(self, state: bool) -> None:
|
||||
"""Enable or disable adding an anchor before headings."""
|
||||
self._linkHeadings = state
|
||||
@@ -385,9 +450,14 @@ class Tokenizer(ABC):
|
||||
self._skipKeywords = set(x.lower().strip() for x in keywords.split(","))
|
||||
return
|
||||
|
||||
def setKeepLineBreaks(self, state: bool) -> None:
|
||||
"""Keep line breaks in paragraphs."""
|
||||
self._keepBreaks = state
|
||||
return
|
||||
|
||||
def setKeepMarkdown(self, state: bool) -> None:
|
||||
"""Keep original markdown during build."""
|
||||
self._keepMarkdown = state
|
||||
self._keepMD = state
|
||||
return
|
||||
|
||||
##
|
||||
@@ -417,8 +487,8 @@ class Tokenizer(ABC):
|
||||
self._tokens.append((
|
||||
self.T_TITLE, 1, title, [], textAlign
|
||||
))
|
||||
if self._keepMarkdown:
|
||||
self._allMarkdown.append(f"#! {title}\n\n")
|
||||
if self._keepMD:
|
||||
self._markdown.append(f"#! {title}\n\n")
|
||||
|
||||
return
|
||||
|
||||
@@ -446,7 +516,7 @@ class Tokenizer(ABC):
|
||||
self._text = xRep.sub(lambda x: repDict[x.group(0)], self._text)
|
||||
|
||||
# Process the character translation map
|
||||
trDict = {nwUnicode.U_MAPOSS: nwUnicode.U_RSQUO}
|
||||
trDict = {nwUnicode.U_MAPOS: nwUnicode.U_RSQUO}
|
||||
self._text = self._text.translate(str.maketrans(trDict))
|
||||
|
||||
return
|
||||
@@ -466,22 +536,23 @@ class Tokenizer(ABC):
|
||||
4: The internal formatting map of the text, self.FMT_*
|
||||
5: The style of the block, self.A_*
|
||||
"""
|
||||
self._tokens = []
|
||||
if self._isNovel:
|
||||
self._hFormatter.setHandle(self._handle)
|
||||
|
||||
nHead = 0
|
||||
breakNext = False
|
||||
tmpMarkdown = []
|
||||
tHandle = self._handle or ""
|
||||
tokens: list[T_Token] = []
|
||||
for aLine in self._text.splitlines():
|
||||
sLine = aLine.strip().lower()
|
||||
|
||||
# Check for blank lines
|
||||
if len(sLine) == 0:
|
||||
self._tokens.append((
|
||||
tokens.append((
|
||||
self.T_EMPTY, nHead, "", [], self.A_NONE
|
||||
))
|
||||
if self._keepMarkdown:
|
||||
if self._keepMD:
|
||||
tmpMarkdown.append("\n")
|
||||
|
||||
continue
|
||||
@@ -507,7 +578,7 @@ class Tokenizer(ABC):
|
||||
continue
|
||||
|
||||
elif sLine == "[vspace]":
|
||||
self._tokens.append(
|
||||
tokens.append(
|
||||
(self.T_SKIP, nHead, "", [], sAlign)
|
||||
)
|
||||
continue
|
||||
@@ -515,11 +586,11 @@ class Tokenizer(ABC):
|
||||
elif sLine.startswith("[vspace:") and sLine.endswith("]"):
|
||||
nSkip = checkInt(sLine[8:-1], 0)
|
||||
if nSkip >= 1:
|
||||
self._tokens.append(
|
||||
tokens.append(
|
||||
(self.T_SKIP, nHead, "", [], sAlign)
|
||||
)
|
||||
if nSkip > 1:
|
||||
self._tokens += (nSkip - 1) * [
|
||||
tokens += (nSkip - 1) * [
|
||||
(self.T_SKIP, nHead, "", [], self.A_NONE)
|
||||
]
|
||||
continue
|
||||
@@ -533,24 +604,32 @@ class Tokenizer(ABC):
|
||||
if aLine.startswith("%~"):
|
||||
continue
|
||||
|
||||
cStyle, cText, _ = processComment(aLine)
|
||||
cStyle, cKey, cText, _, _ = processComment(aLine)
|
||||
if cStyle == nwComment.SYNOPSIS:
|
||||
self._tokens.append((
|
||||
self.T_SYNOPSIS, nHead, cText, [], sAlign
|
||||
tLine, tFmt = self._extractFormats(cText)
|
||||
tokens.append((
|
||||
self.T_SYNOPSIS, nHead, tLine, tFmt, sAlign
|
||||
))
|
||||
if self._doSynopsis and self._keepMarkdown:
|
||||
if self._doSynopsis and self._keepMD:
|
||||
tmpMarkdown.append(f"{aLine}\n")
|
||||
elif cStyle == nwComment.SHORT:
|
||||
self._tokens.append((
|
||||
self.T_SHORT, nHead, cText, [], sAlign
|
||||
tLine, tFmt = self._extractFormats(cText)
|
||||
tokens.append((
|
||||
self.T_SHORT, nHead, tLine, tFmt, sAlign
|
||||
))
|
||||
if self._doSynopsis and self._keepMarkdown:
|
||||
if self._doSynopsis and self._keepMD:
|
||||
tmpMarkdown.append(f"{aLine}\n")
|
||||
elif cStyle == nwComment.FOOTNOTE:
|
||||
tLine, tFmt = self._extractFormats(cText, skip=self.FMT_FNOTE)
|
||||
self._footnotes[f"{tHandle}:{cKey}"] = (tLine, tFmt)
|
||||
if self._keepMD:
|
||||
tmpMarkdown.append(f"{aLine}\n")
|
||||
else:
|
||||
self._tokens.append((
|
||||
self.T_COMMENT, nHead, cText, [], sAlign
|
||||
tLine, tFmt = self._extractFormats(cText)
|
||||
tokens.append((
|
||||
self.T_COMMENT, nHead, tLine, tFmt, sAlign
|
||||
))
|
||||
if self._doComments and self._keepMarkdown:
|
||||
if self._doComments and self._keepMD:
|
||||
tmpMarkdown.append(f"{aLine}\n")
|
||||
|
||||
elif aLine.startswith("@"):
|
||||
@@ -560,11 +639,14 @@ class Tokenizer(ABC):
|
||||
# are automatically skipped.
|
||||
|
||||
valid, bits, _ = self._project.index.scanThis(aLine)
|
||||
if valid and bits and bits[0] not in self._skipKeywords:
|
||||
self._tokens.append((
|
||||
if (
|
||||
valid and bits and bits[0] in nwLabels.KEY_NAME
|
||||
and bits[0] not in self._skipKeywords
|
||||
):
|
||||
tokens.append((
|
||||
self.T_KEYWORD, nHead, aLine[1:].strip(), [], sAlign
|
||||
))
|
||||
if self._doKeywords and self._keepMarkdown:
|
||||
if self._doKeywords and self._keepMD:
|
||||
tmpMarkdown.append(f"{aLine}\n")
|
||||
|
||||
elif aLine.startswith(("# ", "#! ")):
|
||||
@@ -597,10 +679,10 @@ class Tokenizer(ABC):
|
||||
self._hFormatter.resetAll()
|
||||
self._noSep = True
|
||||
|
||||
self._tokens.append((
|
||||
tokens.append((
|
||||
tType, nHead, tText, [], tStyle
|
||||
))
|
||||
if self._keepMarkdown:
|
||||
if self._keepMD:
|
||||
tmpMarkdown.append(f"{aLine}\n")
|
||||
|
||||
elif aLine.startswith(("## ", "##! ")):
|
||||
@@ -632,10 +714,10 @@ class Tokenizer(ABC):
|
||||
self._hFormatter.resetScene()
|
||||
self._noSep = True
|
||||
|
||||
self._tokens.append((
|
||||
tokens.append((
|
||||
tType, nHead, tText, [], tStyle
|
||||
))
|
||||
if self._keepMarkdown:
|
||||
if self._keepMD:
|
||||
tmpMarkdown.append(f"{aLine}\n")
|
||||
|
||||
elif aLine.startswith(("### ", "###! ")):
|
||||
@@ -673,10 +755,10 @@ class Tokenizer(ABC):
|
||||
tStyle = self.A_NONE if self._noSep else self.A_CENTRE
|
||||
self._noSep = False
|
||||
|
||||
self._tokens.append((
|
||||
tokens.append((
|
||||
tType, nHead, tText, [], tStyle
|
||||
))
|
||||
if self._keepMarkdown:
|
||||
if self._keepMD:
|
||||
tmpMarkdown.append(f"{aLine}\n")
|
||||
|
||||
elif aLine.startswith("#### "):
|
||||
@@ -703,10 +785,10 @@ class Tokenizer(ABC):
|
||||
tType = self.T_SEP
|
||||
tStyle = self.A_CENTRE
|
||||
|
||||
self._tokens.append((
|
||||
tokens.append((
|
||||
tType, nHead, tText, [], tStyle
|
||||
))
|
||||
if self._keepMarkdown:
|
||||
if self._keepMD:
|
||||
tmpMarkdown.append(f"{aLine}\n")
|
||||
|
||||
else:
|
||||
@@ -750,54 +832,116 @@ class Tokenizer(ABC):
|
||||
sAlign |= self.A_IND_R
|
||||
|
||||
# Process formats
|
||||
tLine, fmtPos = self._extractFormats(aLine)
|
||||
self._tokens.append((
|
||||
self.T_TEXT, nHead, tLine, fmtPos, sAlign
|
||||
tLine, tFmt = self._extractFormats(aLine, hDialog=self._isNovel)
|
||||
tokens.append((
|
||||
self.T_TEXT, nHead, tLine, tFmt, sAlign
|
||||
))
|
||||
if self._keepMarkdown:
|
||||
if self._keepMD:
|
||||
tmpMarkdown.append(f"{aLine}\n")
|
||||
|
||||
# If we have content, turn off the first page flag
|
||||
if self._isFirst and self._tokens:
|
||||
if self._isFirst and tokens:
|
||||
self._isFirst = False # First document has been processed
|
||||
|
||||
# Make sure the token array doesn't start with a page break
|
||||
# on the very first page, adding a blank first page.
|
||||
if self._tokens[0][4] & self.A_PBB:
|
||||
token = self._tokens[0]
|
||||
self._tokens[0] = (
|
||||
token[0], token[1], token[2], token[3], token[4] & ~self.A_PBB
|
||||
if tokens[0][4] & self.A_PBB:
|
||||
cToken = tokens[0]
|
||||
tokens[0] = (
|
||||
cToken[0], cToken[1], cToken[2], cToken[3], cToken[4] & ~self.A_PBB
|
||||
)
|
||||
|
||||
# Always add an empty line at the end of the file
|
||||
self._tokens.append((
|
||||
tokens.append((
|
||||
self.T_EMPTY, nHead, "", [], self.A_NONE
|
||||
))
|
||||
if self._keepMarkdown:
|
||||
if self._keepMD:
|
||||
tmpMarkdown.append("\n")
|
||||
self._allMarkdown.append("".join(tmpMarkdown))
|
||||
self._markdown.append("".join(tmpMarkdown))
|
||||
|
||||
# Second Pass
|
||||
# ===========
|
||||
# Some items need a second pass
|
||||
# This second pass strips away consecutive blank lines, and
|
||||
# combines consecutive text lines into the same paragraph.
|
||||
# It also ensures that there isn't paragraph spacing between
|
||||
# meta data lines for formats that has spacing.
|
||||
|
||||
pToken = (self.T_EMPTY, 0, "", [], self.A_NONE)
|
||||
nToken = (self.T_EMPTY, 0, "", [], self.A_NONE)
|
||||
tCount = len(self._tokens)
|
||||
for n, token in enumerate(self._tokens):
|
||||
self._tokens = []
|
||||
pToken: T_Token = (self.T_EMPTY, 0, "", [], self.A_NONE)
|
||||
nToken: T_Token = (self.T_EMPTY, 0, "", [], self.A_NONE)
|
||||
|
||||
lineSep = "\n" if self._keepBreaks else " "
|
||||
pLines: list[T_Token] = []
|
||||
|
||||
tCount = len(tokens)
|
||||
for n, cToken in enumerate(tokens):
|
||||
|
||||
if n > 0:
|
||||
pToken = self._tokens[n-1]
|
||||
pToken = tokens[n-1] # Look behind
|
||||
if n < tCount - 1:
|
||||
nToken = self._tokens[n+1]
|
||||
nToken = tokens[n+1] # Look ahead
|
||||
|
||||
if token[0] == self.T_KEYWORD:
|
||||
aStyle = token[4]
|
||||
if cToken[0] in self.L_SKIP_INDENT and not self._indentFirst:
|
||||
# Unless the indentFirst flag is set, we set up the next
|
||||
# paragraph to not be indented if we see a block of a
|
||||
# specific type
|
||||
self._noIndent = True
|
||||
|
||||
if cToken[0] == self.T_EMPTY:
|
||||
# We don't need to keep the empty lines after this pass
|
||||
pass
|
||||
|
||||
elif cToken[0] == self.T_KEYWORD:
|
||||
# Adjust margins for lines in a list of keyword lines
|
||||
aStyle = cToken[4]
|
||||
if pToken[0] == self.T_KEYWORD:
|
||||
aStyle |= self.A_Z_TOPMRG
|
||||
if nToken[0] == self.T_KEYWORD:
|
||||
aStyle |= self.A_Z_BTMMRG
|
||||
self._tokens[n] = (token[0], token[1], token[2], token[3], aStyle)
|
||||
self._tokens.append((
|
||||
cToken[0], cToken[1], cToken[2], cToken[3], aStyle
|
||||
))
|
||||
|
||||
elif cToken[0] == self.T_TEXT:
|
||||
# Combine lines from the same paragraph
|
||||
pLines.append(cToken)
|
||||
|
||||
if nToken[0] != self.T_TEXT:
|
||||
# Next token is not text, so we add the buffer to tokens
|
||||
nLines = len(pLines)
|
||||
cStyle = pLines[0][4]
|
||||
if self._firstIndent and not (self._noIndent or cStyle & self.M_ALIGNED):
|
||||
# If paragraph indentation is enabled, not temporarily
|
||||
# turned off, and the block is not aligned, we add the
|
||||
# text indentation flag
|
||||
cStyle |= self.A_IND_T
|
||||
|
||||
if nLines == 1:
|
||||
# The paragraph contains a single line, so we just
|
||||
# save that directly to the token list
|
||||
self._tokens.append((
|
||||
self.T_TEXT, pLines[0][1], pLines[0][2], pLines[0][3], cStyle
|
||||
))
|
||||
elif nLines > 1:
|
||||
# The paragraph contains multiple lines, so we need to
|
||||
# join them according to the line break policy, and
|
||||
# recompute all the formatting markers
|
||||
tTxt = ""
|
||||
tFmt: T_Formats = []
|
||||
for aToken in pLines:
|
||||
tLen = len(tTxt)
|
||||
tTxt += f"{aToken[2]}{lineSep}"
|
||||
tFmt.extend((p+tLen, fmt, key) for p, fmt, key in aToken[3])
|
||||
self._tokens.append((
|
||||
self.T_TEXT, pLines[0][1], tTxt[:-1], tFmt, cStyle
|
||||
))
|
||||
|
||||
# Reset buffer and make sure text indent is on for next pass
|
||||
pLines = []
|
||||
self._noIndent = False
|
||||
|
||||
else:
|
||||
self._tokens.append(cToken)
|
||||
|
||||
return
|
||||
|
||||
@@ -840,7 +984,6 @@ class Tokenizer(ABC):
|
||||
textWordChars = self._counts.get("textWordChars", 0)
|
||||
titleWordChars = self._counts.get("titleWordChars", 0)
|
||||
|
||||
para = []
|
||||
for tType, _, tText, _, _ in self._tokens:
|
||||
tText = tText.replace(nwUnicode.U_ENDASH, " ")
|
||||
tText = tText.replace(nwUnicode.U_EMDASH, " ")
|
||||
@@ -850,22 +993,19 @@ class Tokenizer(ABC):
|
||||
nChars = len(tText)
|
||||
nWChars = len("".join(tWords))
|
||||
|
||||
if tType == self.T_EMPTY:
|
||||
if len(para) > 0:
|
||||
tTemp = "\n".join(para)
|
||||
tPWords = tTemp.split()
|
||||
nPWords = len(tPWords)
|
||||
nPChars = len(tTemp)
|
||||
nPWChars = len("".join(tPWords))
|
||||
if tType == self.T_TEXT:
|
||||
tPWords = tText.split()
|
||||
nPWords = len(tPWords)
|
||||
nPChars = len(tText)
|
||||
nPWChars = len("".join(tPWords))
|
||||
|
||||
paragraphCount += 1
|
||||
allWords += nPWords
|
||||
textWords += nPWords
|
||||
allChars += nPChars
|
||||
textChars += nPChars
|
||||
allWordChars += nPWChars
|
||||
textWordChars += nPWChars
|
||||
para = []
|
||||
paragraphCount += 1
|
||||
allWords += nPWords
|
||||
textWords += nPWords
|
||||
allChars += nPChars
|
||||
textChars += nPChars
|
||||
allWordChars += nPWChars
|
||||
textWordChars += nPWChars
|
||||
|
||||
elif tType in self.L_HEADINGS:
|
||||
titleCount += 1
|
||||
@@ -881,9 +1021,6 @@ class Tokenizer(ABC):
|
||||
allChars += nChars
|
||||
allWordChars += nWChars
|
||||
|
||||
elif tType == self.T_TEXT:
|
||||
para.append(tText.rstrip())
|
||||
|
||||
elif tType == self.T_SYNOPSIS and self._doSynopsis:
|
||||
text = "{0}: {1}".format(self._localLookup("Synopsis"), tText)
|
||||
words = text.split()
|
||||
@@ -935,7 +1072,7 @@ class Tokenizer(ABC):
|
||||
def saveRawMarkdown(self, path: str | Path) -> None:
|
||||
"""Save the raw text to a plain text file."""
|
||||
with open(path, mode="w", encoding="utf-8") as outFile:
|
||||
for nwdPage in self._allMarkdown:
|
||||
for nwdPage in self._markdown:
|
||||
outFile.write(nwdPage)
|
||||
return
|
||||
|
||||
@@ -950,7 +1087,7 @@ class Tokenizer(ABC):
|
||||
"buildTimeStr": formatTimeStamp(timeStamp),
|
||||
},
|
||||
"text": {
|
||||
"nwd": [page.rstrip("\n").split("\n") for page in self._allMarkdown],
|
||||
"nwd": [page.rstrip("\n").split("\n") for page in self._markdown],
|
||||
}
|
||||
}
|
||||
with open(path, mode="w", encoding="utf-8") as fObj:
|
||||
@@ -961,9 +1098,14 @@ class Tokenizer(ABC):
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _extractFormats(self, text: str) -> tuple[str, list[tuple[int, int]]]:
|
||||
"""Extract format markers from a text paragraph."""
|
||||
temp = []
|
||||
def _extractFormats(
|
||||
self, text: str, skip: int = 0, hDialog: bool = False
|
||||
) -> tuple[str, T_Formats]:
|
||||
"""Extract format markers from a text paragraph. In order to
|
||||
also process dialogue highlighting, the hDialog flag must be set
|
||||
to True. See issues #2011 and #2013.
|
||||
"""
|
||||
temp: list[tuple[int, int, int, str]] = []
|
||||
|
||||
# Match Markdown
|
||||
for regEx, fmts in self._rxMarkdown:
|
||||
@@ -971,7 +1113,7 @@ class Tokenizer(ABC):
|
||||
while rxItt.hasNext():
|
||||
rxMatch = rxItt.next()
|
||||
temp.extend(
|
||||
[rxMatch.capturedStart(n), rxMatch.capturedLength(n), fmt]
|
||||
(rxMatch.capturedStart(n), rxMatch.capturedLength(n), fmt, "")
|
||||
for n, fmt in enumerate(fmts) if fmt > 0
|
||||
)
|
||||
|
||||
@@ -979,25 +1121,47 @@ class Tokenizer(ABC):
|
||||
rxItt = self._rxShortCodes.globalMatch(text, 0)
|
||||
while rxItt.hasNext():
|
||||
rxMatch = rxItt.next()
|
||||
temp.append([
|
||||
temp.append((
|
||||
rxMatch.capturedStart(1),
|
||||
rxMatch.capturedLength(1),
|
||||
self._shortCodeFmt.get(rxMatch.captured(1).lower(), 0)
|
||||
])
|
||||
self._shortCodeFmt.get(rxMatch.captured(1).lower(), 0),
|
||||
"",
|
||||
))
|
||||
|
||||
# Post-process text and format markers
|
||||
# Match Shortcode w/Values
|
||||
rxItt = self._rxShortCodeVals.globalMatch(text, 0)
|
||||
tHandle = self._handle or ""
|
||||
while rxItt.hasNext():
|
||||
rxMatch = rxItt.next()
|
||||
kind = self._shortCodeVals.get(rxMatch.captured(1).lower(), 0)
|
||||
temp.append((
|
||||
rxMatch.capturedStart(0),
|
||||
rxMatch.capturedLength(0),
|
||||
self.FMT_STRIP if kind == skip else kind,
|
||||
f"{tHandle}:{rxMatch.captured(2)}",
|
||||
))
|
||||
|
||||
# Match Dialogue
|
||||
if self._rxDialogue and hDialog:
|
||||
for regEx, fmtB, fmtE in self._rxDialogue:
|
||||
rxItt = regEx.globalMatch(text, 0)
|
||||
while rxItt.hasNext():
|
||||
rxMatch = rxItt.next()
|
||||
temp.append((rxMatch.capturedStart(0), 0, fmtB, ""))
|
||||
temp.append((rxMatch.capturedEnd(0), 0, fmtE, ""))
|
||||
|
||||
# Post-process text and format
|
||||
result = text
|
||||
formats = []
|
||||
for pos, n, fmt in reversed(sorted(temp, key=lambda x: x[0])):
|
||||
for pos, n, fmt, key in reversed(sorted(temp, key=lambda x: x[0])):
|
||||
if fmt > 0:
|
||||
result = result[:pos] + result[pos+n:]
|
||||
formats = [(p-n, f) for p, f in formats]
|
||||
formats.insert(0, (pos, fmt))
|
||||
if n > 0:
|
||||
result = result[:pos] + result[pos+n:]
|
||||
formats = [(p-n if p > pos else p, f, k) for p, f, k in formats]
|
||||
formats.insert(0, (pos, fmt, key))
|
||||
|
||||
return result, formats
|
||||
|
||||
# END Class Tokenizer
|
||||
|
||||
|
||||
class HeadingFormatter:
|
||||
|
||||
@@ -1067,5 +1231,3 @@ class HeadingFormatter:
|
||||
hFormat = hFormat.replace(nwHeadFmt.CHAR_FOCUS, fText)
|
||||
|
||||
return hFormat
|
||||
|
||||
# END Class HeadingFormatter
|
||||
|
||||
@@ -29,11 +29,50 @@ from pathlib import Path
|
||||
|
||||
from novelwriter.constants import nwHeadFmt, nwLabels, nwUnicode
|
||||
from novelwriter.core.project import NWProject
|
||||
from novelwriter.core.tokenizer import Tokenizer
|
||||
from novelwriter.core.tokenizer import T_Formats, Tokenizer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Standard Markdown
|
||||
STD_MD = {
|
||||
Tokenizer.FMT_B_B: "**",
|
||||
Tokenizer.FMT_B_E: "**",
|
||||
Tokenizer.FMT_I_B: "_",
|
||||
Tokenizer.FMT_I_E: "_",
|
||||
Tokenizer.FMT_D_B: "",
|
||||
Tokenizer.FMT_D_E: "",
|
||||
Tokenizer.FMT_U_B: "",
|
||||
Tokenizer.FMT_U_E: "",
|
||||
Tokenizer.FMT_M_B: "",
|
||||
Tokenizer.FMT_M_E: "",
|
||||
Tokenizer.FMT_SUP_B: "",
|
||||
Tokenizer.FMT_SUP_E: "",
|
||||
Tokenizer.FMT_SUB_B: "",
|
||||
Tokenizer.FMT_SUB_E: "",
|
||||
Tokenizer.FMT_STRIP: "",
|
||||
}
|
||||
|
||||
# Extended Markdown
|
||||
EXT_MD = {
|
||||
Tokenizer.FMT_B_B: "**",
|
||||
Tokenizer.FMT_B_E: "**",
|
||||
Tokenizer.FMT_I_B: "_",
|
||||
Tokenizer.FMT_I_E: "_",
|
||||
Tokenizer.FMT_D_B: "~~",
|
||||
Tokenizer.FMT_D_E: "~~",
|
||||
Tokenizer.FMT_U_B: "",
|
||||
Tokenizer.FMT_U_E: "",
|
||||
Tokenizer.FMT_M_B: "==",
|
||||
Tokenizer.FMT_M_E: "==",
|
||||
Tokenizer.FMT_SUP_B: "^",
|
||||
Tokenizer.FMT_SUP_E: "^",
|
||||
Tokenizer.FMT_SUB_B: "~",
|
||||
Tokenizer.FMT_SUB_E: "~",
|
||||
Tokenizer.FMT_STRIP: "",
|
||||
}
|
||||
|
||||
|
||||
class ToMarkdown(Tokenizer):
|
||||
"""Core: Markdown Document Writer
|
||||
|
||||
@@ -42,14 +81,11 @@ class ToMarkdown(Tokenizer):
|
||||
supports concatenating novelWriter markup files.
|
||||
"""
|
||||
|
||||
M_STD = 0 # Standard Markdown
|
||||
M_EXT = 1 # Extended Markdown
|
||||
|
||||
def __init__(self, project: NWProject) -> None:
|
||||
super().__init__(project)
|
||||
self._genMode = self.M_STD
|
||||
self._fullMD: list[str] = []
|
||||
self._preserveBreaks = True
|
||||
self._usedNotes: dict[str, int] = {}
|
||||
self._extended = True
|
||||
return
|
||||
|
||||
##
|
||||
@@ -65,19 +101,9 @@ class ToMarkdown(Tokenizer):
|
||||
# Setters
|
||||
##
|
||||
|
||||
def setStandardMarkdown(self) -> None:
|
||||
"""Set the converter to use standard Markdown formatting."""
|
||||
self._genMode = self.M_STD
|
||||
return
|
||||
|
||||
def setExtendedMarkdown(self) -> None:
|
||||
def setExtendedMarkdown(self, state: bool) -> None:
|
||||
"""Set the converter to use Extended Markdown formatting."""
|
||||
self._genMode = self.M_EXT
|
||||
return
|
||||
|
||||
def setPreserveBreaks(self, state: bool) -> None:
|
||||
"""Preserve line breaks in paragraphs."""
|
||||
self._preserveBreaks = state
|
||||
self._extended = state
|
||||
return
|
||||
|
||||
##
|
||||
@@ -90,58 +116,21 @@ class ToMarkdown(Tokenizer):
|
||||
|
||||
def doConvert(self) -> None:
|
||||
"""Convert the list of text tokens into a Markdown document."""
|
||||
if self._genMode == self.M_STD:
|
||||
# Standard Markdown
|
||||
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_U_B: "",
|
||||
self.FMT_U_E: "",
|
||||
self.FMT_M_B: "",
|
||||
self.FMT_M_E: "",
|
||||
self.FMT_SUP_B: "",
|
||||
self.FMT_SUP_E: "",
|
||||
self.FMT_SUB_B: "",
|
||||
self.FMT_SUB_E: "",
|
||||
}
|
||||
cSkip = ""
|
||||
else:
|
||||
# Extended Markdown
|
||||
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_U_B: "",
|
||||
self.FMT_U_E: "",
|
||||
self.FMT_M_B: "==",
|
||||
self.FMT_M_E: "==",
|
||||
self.FMT_SUP_B: "^",
|
||||
self.FMT_SUP_E: "^",
|
||||
self.FMT_SUB_B: "~",
|
||||
self.FMT_SUB_E: "~",
|
||||
}
|
||||
cSkip = nwUnicode.U_MMSP
|
||||
|
||||
self._result = ""
|
||||
|
||||
para = []
|
||||
lines = []
|
||||
lineSep = " \n" if self._preserveBreaks else " "
|
||||
if self._extended:
|
||||
mTags = EXT_MD
|
||||
cSkip = nwUnicode.U_MMSP
|
||||
else:
|
||||
mTags = STD_MD
|
||||
cSkip = ""
|
||||
|
||||
lines = []
|
||||
for tType, _, tText, tFormat, tStyle in self._tokens:
|
||||
|
||||
if tType == self.T_EMPTY:
|
||||
if para:
|
||||
tTemp = (lineSep.join(para)).rstrip(" ")
|
||||
lines.append(f"{tTemp}\n\n")
|
||||
para = []
|
||||
if tType == self.T_TEXT:
|
||||
tTemp = self._formatText(tText, tFormat, mTags).replace("\n", " \n")
|
||||
lines.append(f"{tTemp}\n\n")
|
||||
|
||||
elif tType == self.T_TITLE:
|
||||
tHead = tText.replace(nwHeadFmt.BR, "\n")
|
||||
@@ -169,23 +158,17 @@ class ToMarkdown(Tokenizer):
|
||||
elif tType == self.T_SKIP:
|
||||
lines.append(f"{cSkip}\n\n")
|
||||
|
||||
elif tType == self.T_TEXT:
|
||||
tTemp = tText
|
||||
for pos, fmt in reversed(tFormat):
|
||||
tTemp = f"{tTemp[:pos]}{mdTags[fmt]}{tTemp[pos:]}"
|
||||
para.append(tTemp.rstrip())
|
||||
|
||||
elif tType == self.T_SYNOPSIS and self._doSynopsis:
|
||||
label = self._localLookup("Synopsis")
|
||||
lines.append(f"**{label}:** {tText}\n\n")
|
||||
lines.append(f"**{label}:** {self._formatText(tText, tFormat, mTags)}\n\n")
|
||||
|
||||
elif tType == self.T_SHORT and self._doSynopsis:
|
||||
label = self._localLookup("Short Description")
|
||||
lines.append(f"**{label}:** {tText}\n\n")
|
||||
lines.append(f"**{label}:** {self._formatText(tText, tFormat, mTags)}\n\n")
|
||||
|
||||
elif tType == self.T_COMMENT and self._doComments:
|
||||
label = self._localLookup("Comment")
|
||||
lines.append(f"**{label}:** {tText}\n\n")
|
||||
lines.append(f"**{label}:** {self._formatText(tText, tFormat, mTags)}\n\n")
|
||||
|
||||
elif tType == self.T_KEYWORD and self._doKeywords:
|
||||
lines.append(self._formatKeywords(tText, tStyle))
|
||||
@@ -195,6 +178,27 @@ class ToMarkdown(Tokenizer):
|
||||
|
||||
return
|
||||
|
||||
def appendFootnotes(self) -> None:
|
||||
"""Append the footnotes in the buffer."""
|
||||
if self._usedNotes:
|
||||
tags = EXT_MD if self._extended else STD_MD
|
||||
footnotes = self._localLookup("Footnotes")
|
||||
|
||||
lines = []
|
||||
lines.append(f"### {footnotes}\n\n")
|
||||
for key, index in self._usedNotes.items():
|
||||
if content := self._footnotes.get(key):
|
||||
marker = f"{index}. "
|
||||
text = self._formatText(*content, tags)
|
||||
lines.append(f"{marker}{text}\n")
|
||||
lines.append("\n")
|
||||
|
||||
result = "".join(lines)
|
||||
self._result += result
|
||||
self._fullMD.append(result)
|
||||
|
||||
return
|
||||
|
||||
def saveMarkdown(self, path: str | Path) -> None:
|
||||
"""Save the data to a plain text file."""
|
||||
with open(path, mode="w", encoding="utf-8") as outFile:
|
||||
@@ -206,14 +210,31 @@ class ToMarkdown(Tokenizer):
|
||||
"""Replace tabs with spaces."""
|
||||
spaces = spaceChar*nSpaces
|
||||
self._fullMD = [p.replace("\t", spaces) for p in self._fullMD]
|
||||
if self._keepMarkdown:
|
||||
self._allMarkdown = [p.replace("\t", spaces) for p in self._allMarkdown]
|
||||
if self._keepMD:
|
||||
self._markdown = [p.replace("\t", spaces) for p in self._markdown]
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _formatText(self, text: str, tFmt: T_Formats, tags: dict[int, str]) -> str:
|
||||
"""Apply formatting tags to text."""
|
||||
temp = text
|
||||
for pos, fmt, data in reversed(tFmt):
|
||||
md = ""
|
||||
if fmt == self.FMT_FNOTE:
|
||||
if data in self._footnotes:
|
||||
index = len(self._usedNotes) + 1
|
||||
self._usedNotes[data] = index
|
||||
md = f"[{index}]"
|
||||
else:
|
||||
md = "[ERR]"
|
||||
else:
|
||||
md = tags.get(fmt, "")
|
||||
temp = f"{temp[:pos]}{md}{temp[pos:]}"
|
||||
return temp
|
||||
|
||||
def _formatKeywords(self, text: str, style: int) -> str:
|
||||
"""Apply Markdown formatting to keywords."""
|
||||
valid, bits, _ = self._project.index.scanThis("@"+text)
|
||||
@@ -229,5 +250,3 @@ class ToMarkdown(Tokenizer):
|
||||
result += " \n" if style & self.A_Z_BTMMRG else "\n\n"
|
||||
|
||||
return result
|
||||
|
||||
# END Class ToMarkdown
|
||||
@@ -29,17 +29,20 @@ from __future__ import annotations
|
||||
import logging
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from collections.abc import Sequence
|
||||
from datetime import datetime
|
||||
from hashlib import sha256
|
||||
from pathlib import Path
|
||||
from zipfile import ZipFile
|
||||
from datetime import datetime
|
||||
from collections.abc import Sequence
|
||||
|
||||
from PyQt5.QtGui import QFont
|
||||
|
||||
from novelwriter import __version__
|
||||
from novelwriter.common import xmlIndent
|
||||
from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels
|
||||
from novelwriter.core.project import NWProject
|
||||
from novelwriter.core.tokenizer import Tokenizer, stripEscape
|
||||
from novelwriter.core.tokenizer import T_Formats, Tokenizer, stripEscape
|
||||
from novelwriter.types import FONT_STYLE, FONT_WEIGHTS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -79,13 +82,15 @@ TAG_SPAN = _mkTag("text", "span")
|
||||
TAG_STNM = _mkTag("text", "style-name")
|
||||
|
||||
# Formatting Codes
|
||||
X_BLD = 0x01 # Bold format
|
||||
X_ITA = 0x02 # Italic format
|
||||
X_DEL = 0x04 # Strikethrough format
|
||||
X_UND = 0x08 # Underline format
|
||||
X_MRK = 0x10 # Marked format
|
||||
X_SUP = 0x20 # Superscript
|
||||
X_SUB = 0x40 # Subscript
|
||||
X_BLD = 0x001 # Bold format
|
||||
X_ITA = 0x002 # Italic format
|
||||
X_DEL = 0x004 # Strikethrough format
|
||||
X_UND = 0x008 # Underline format
|
||||
X_MRK = 0x010 # Marked format
|
||||
X_SUP = 0x020 # Superscript
|
||||
X_SUB = 0x040 # Subscript
|
||||
X_DLG = 0x080 # Dialogue
|
||||
X_DLA = 0x100 # Alt. Dialogue
|
||||
|
||||
# Formatting Masks
|
||||
M_BLD = ~X_BLD
|
||||
@@ -95,6 +100,24 @@ M_UND = ~X_UND
|
||||
M_MRK = ~X_MRK
|
||||
M_SUP = ~X_SUP
|
||||
M_SUB = ~X_SUB
|
||||
M_DLG = ~X_DLG
|
||||
M_DLA = ~X_DLA
|
||||
|
||||
# ODT Styles
|
||||
S_TITLE = "Title"
|
||||
S_HEAD1 = "Heading_20_1"
|
||||
S_HEAD2 = "Heading_20_2"
|
||||
S_HEAD3 = "Heading_20_3"
|
||||
S_HEAD4 = "Heading_20_4"
|
||||
S_SEP = "Separator"
|
||||
S_FIND = "First_20_line_20_indent"
|
||||
S_TEXT = "Text_20_body"
|
||||
S_META = "Text_20_Meta"
|
||||
S_HNF = "Header_20_and_20_Footer"
|
||||
|
||||
# Font Data
|
||||
FONT_WEIGHT_NUM = ["100", "200", "300", "400", "500", "600", "700", "800", "900"]
|
||||
FONT_WEIGHT_MAP = {"400": "normal", "700": "bold"}
|
||||
|
||||
|
||||
class ToOdt(Tokenizer):
|
||||
@@ -130,20 +153,25 @@ class ToOdt(Tokenizer):
|
||||
self._autoPara: dict[str, ODTParagraphStyle] = {} # Auto-generated paragraph styles
|
||||
self._autoText: dict[int, ODTTextStyle] = {} # Auto-generated text styles
|
||||
|
||||
# Footnotes
|
||||
self._nNote = 0
|
||||
self._etNotes: dict[str, ET.Element] = {} # Generated note elements
|
||||
|
||||
self._errData = [] # List of errors encountered
|
||||
|
||||
# Properties
|
||||
self._textFont = "Liberation Serif"
|
||||
self._textSize = 12
|
||||
self._textFixed = False
|
||||
self._textFont = QFont("Liberation Serif", 12)
|
||||
self._colourHead = False
|
||||
self._firstIndent = False
|
||||
self._headerFormat = ""
|
||||
self._pageOffset = 0
|
||||
|
||||
# Internal
|
||||
self._fontFamily = "'Liberation Serif'"
|
||||
self._fontFamily = "Liberation Serif"
|
||||
self._fontSize = 12
|
||||
self._fontWeight = "normal"
|
||||
self._fontStyle = "normal"
|
||||
self._fontPitch = "variable"
|
||||
self._fontBold = "bold"
|
||||
self._fSizeTitle = "30pt"
|
||||
self._fSizeHead1 = "24pt"
|
||||
self._fSizeHead2 = "20pt"
|
||||
@@ -151,6 +179,7 @@ class ToOdt(Tokenizer):
|
||||
self._fSizeHead4 = "14pt"
|
||||
self._fSizeHead = "14pt"
|
||||
self._fSizeText = "12pt"
|
||||
self._fSizeFoot = "10pt"
|
||||
self._fLineHeight = "115%"
|
||||
self._fBlockIndent = "1.693cm"
|
||||
self._fTextIndent = "0.499cm"
|
||||
@@ -167,6 +196,7 @@ class ToOdt(Tokenizer):
|
||||
self._mTopHead = "0.423cm"
|
||||
self._mTopText = "0.000cm"
|
||||
self._mTopMeta = "0.000cm"
|
||||
self._mTopSep = "0.247cm"
|
||||
|
||||
self._mBotTitle = "0.212cm"
|
||||
self._mBotHead1 = "0.212cm"
|
||||
@@ -176,6 +206,10 @@ class ToOdt(Tokenizer):
|
||||
self._mBotHead = "0.212cm"
|
||||
self._mBotText = "0.247cm"
|
||||
self._mBotMeta = "0.106cm"
|
||||
self._mBotSep = "0.247cm"
|
||||
|
||||
self._mBotFoot = "0.106cm"
|
||||
self._mLeftFoot = "0.600cm"
|
||||
|
||||
# Document Size and Margins
|
||||
self._mDocWidth = "21.0cm"
|
||||
@@ -186,13 +220,15 @@ class ToOdt(Tokenizer):
|
||||
self._mDocRight = "2.000cm"
|
||||
|
||||
# Colour
|
||||
self._colHead12 = None
|
||||
self._opaHead12 = None
|
||||
self._colHead34 = None
|
||||
self._opaHead34 = None
|
||||
self._colMetaTx = None
|
||||
self._opaMetaTx = None
|
||||
self._markText = "#ffffa6"
|
||||
self._colHead12 = None
|
||||
self._opaHead12 = None
|
||||
self._colHead34 = None
|
||||
self._opaHead34 = None
|
||||
self._colMetaTx = None
|
||||
self._opaMetaTx = None
|
||||
self._colDialogM = None
|
||||
self._colDialogA = None
|
||||
self._markText = "#ffffa6"
|
||||
|
||||
return
|
||||
|
||||
@@ -232,11 +268,6 @@ class ToOdt(Tokenizer):
|
||||
self._pageOffset = offset
|
||||
return
|
||||
|
||||
def setFirstLineIndent(self, state: bool) -> None:
|
||||
"""Enable or disable first line indent."""
|
||||
self._firstIndent = state
|
||||
return
|
||||
|
||||
##
|
||||
# Class Methods
|
||||
##
|
||||
@@ -246,18 +277,25 @@ class ToOdt(Tokenizer):
|
||||
# Initialise Variables
|
||||
# ====================
|
||||
|
||||
self._fontFamily = self._textFont
|
||||
if len(self._textFont.split()) > 1:
|
||||
self._fontFamily = f"'{self._textFont}'"
|
||||
self._fontPitch = "fixed" if self._textFixed else "variable"
|
||||
intWeight = FONT_WEIGHTS.get(self._textFont.weight(), 400)
|
||||
fontWeight = str(intWeight)
|
||||
fontBold = str(min(intWeight + 300, 900))
|
||||
|
||||
self._fSizeTitle = f"{round(2.50 * self._textSize):d}pt"
|
||||
self._fSizeHead1 = f"{round(2.00 * self._textSize):d}pt"
|
||||
self._fSizeHead2 = f"{round(1.60 * self._textSize):d}pt"
|
||||
self._fSizeHead3 = f"{round(1.30 * self._textSize):d}pt"
|
||||
self._fSizeHead4 = f"{round(1.15 * self._textSize):d}pt"
|
||||
self._fSizeHead = f"{round(1.15 * self._textSize):d}pt"
|
||||
self._fSizeText = f"{self._textSize:d}pt"
|
||||
self._fontFamily = self._textFont.family()
|
||||
self._fontSize = self._textFont.pointSize()
|
||||
self._fontWeight = FONT_WEIGHT_MAP.get(fontWeight, fontWeight)
|
||||
self._fontStyle = FONT_STYLE.get(self._textFont.style(), "normal")
|
||||
self._fontPitch = "fixed" if self._textFont.fixedPitch() else "variable"
|
||||
self._fontBold = FONT_WEIGHT_MAP.get(fontBold, fontBold)
|
||||
|
||||
self._fSizeTitle = f"{round(2.50 * self._fontSize):d}pt"
|
||||
self._fSizeHead1 = f"{round(2.00 * self._fontSize):d}pt"
|
||||
self._fSizeHead2 = f"{round(1.60 * self._fontSize):d}pt"
|
||||
self._fSizeHead3 = f"{round(1.30 * self._fontSize):d}pt"
|
||||
self._fSizeHead4 = f"{round(1.15 * self._fontSize):d}pt"
|
||||
self._fSizeHead = f"{round(1.15 * self._fontSize):d}pt"
|
||||
self._fSizeText = f"{self._fontSize:d}pt"
|
||||
self._fSizeFoot = f"{round(0.8*self._fontSize):d}pt"
|
||||
|
||||
mScale = self._lineHeight/1.15
|
||||
|
||||
@@ -269,6 +307,7 @@ class ToOdt(Tokenizer):
|
||||
self._mTopHead = self._emToCm(mScale * self._marginHead4[0])
|
||||
self._mTopText = self._emToCm(mScale * self._marginText[0])
|
||||
self._mTopMeta = self._emToCm(mScale * self._marginMeta[0])
|
||||
self._mTopSep = self._emToCm(mScale * self._marginSep[0])
|
||||
|
||||
self._mBotTitle = self._emToCm(mScale * self._marginTitle[1])
|
||||
self._mBotHead1 = self._emToCm(mScale * self._marginHead1[1])
|
||||
@@ -278,6 +317,10 @@ class ToOdt(Tokenizer):
|
||||
self._mBotHead = self._emToCm(mScale * self._marginHead4[1])
|
||||
self._mBotText = self._emToCm(mScale * self._marginText[1])
|
||||
self._mBotMeta = self._emToCm(mScale * self._marginMeta[1])
|
||||
self._mBotSep = self._emToCm(mScale * self._marginSep[1])
|
||||
|
||||
self._mLeftFoot = self._emToCm(self._marginFoot[0])
|
||||
self._mBotFoot = self._emToCm(self._marginFoot[1])
|
||||
|
||||
if self._colourHead:
|
||||
self._colHead12 = "#2a6099"
|
||||
@@ -287,8 +330,13 @@ class ToOdt(Tokenizer):
|
||||
self._colMetaTx = "#813709"
|
||||
self._opaMetaTx = "100%"
|
||||
|
||||
if self._showDialog:
|
||||
self._colDialogM = "#2a6099"
|
||||
self._colDialogA = "#813709"
|
||||
|
||||
self._fLineHeight = f"{round(100 * self._lineHeight):d}%"
|
||||
self._fBlockIndent = self._emToCm(self._blockIndent)
|
||||
self._fTextIndent = self._emToCm(self._firstWidth)
|
||||
self._textAlign = "justify" if self._doJustify else "left"
|
||||
|
||||
# Clear Errors
|
||||
@@ -301,7 +349,7 @@ class ToOdt(Tokenizer):
|
||||
tAttr[_mkTag("office", "version")] = X_VERS
|
||||
|
||||
fAttr = {}
|
||||
fAttr[_mkTag("style", "name")] = self._textFont
|
||||
fAttr[_mkTag("style", "name")] = self._fontFamily
|
||||
fAttr[_mkTag("style", "font-pitch")] = self._fontPitch
|
||||
|
||||
if self._isFlat:
|
||||
@@ -399,10 +447,7 @@ class ToOdt(Tokenizer):
|
||||
"""Convert the list of text tokens into XML elements."""
|
||||
self._result = "" # Not used, but cleared just in case
|
||||
|
||||
pFmt = []
|
||||
pText = []
|
||||
pStyle = None
|
||||
pIndent = True
|
||||
xText = self._xText
|
||||
for tType, _, tText, tFormat, tStyle in self._tokens:
|
||||
|
||||
# Styles
|
||||
@@ -419,7 +464,6 @@ class ToOdt(Tokenizer):
|
||||
|
||||
if tStyle & self.A_PBB:
|
||||
oStyle.setBreakBefore("page")
|
||||
|
||||
if tStyle & self.A_PBA:
|
||||
oStyle.setBreakAfter("page")
|
||||
|
||||
@@ -433,82 +477,57 @@ class ToOdt(Tokenizer):
|
||||
if tStyle & self.A_IND_R:
|
||||
oStyle.setMarginRight(self._fBlockIndent)
|
||||
|
||||
if tType not in (self.T_EMPTY, self.T_TEXT):
|
||||
pIndent = False
|
||||
|
||||
# Process Text Types
|
||||
if tType == self.T_EMPTY:
|
||||
if len(pText) > 1 and pStyle is not None:
|
||||
if self._doJustify:
|
||||
pStyle.setTextAlign("left")
|
||||
|
||||
if len(pText) > 0 and pStyle is not None:
|
||||
tTxt = ""
|
||||
tFmt = []
|
||||
for nText, nFmt in zip(pText, pFmt):
|
||||
tLen = len(tTxt)
|
||||
tTxt += f"{nText}\n"
|
||||
tFmt.extend((p+tLen, fmt) for p, fmt in nFmt)
|
||||
|
||||
# Don't indent a paragraph if it has alignment set
|
||||
tIndent = self._firstIndent and pIndent and pStyle.isUnaligned()
|
||||
self._addTextPar(
|
||||
"First_20_line_20_indent" if tIndent else "Text_20_body",
|
||||
pStyle, tTxt.rstrip(), tFmt=tFmt
|
||||
)
|
||||
pIndent = True
|
||||
|
||||
pFmt = []
|
||||
pText = []
|
||||
pStyle = None
|
||||
if tType == self.T_TEXT:
|
||||
# Text indentation is processed here because there is a
|
||||
# dedicated pre-defined style for it
|
||||
if tStyle & self.A_IND_T:
|
||||
self._addTextPar(xText, S_FIND, oStyle, tText, tFmt=tFormat)
|
||||
else:
|
||||
self._addTextPar(xText, S_TEXT, oStyle, tText, tFmt=tFormat)
|
||||
|
||||
elif tType == self.T_TITLE:
|
||||
# Title must be text:p
|
||||
tHead = tText.replace(nwHeadFmt.BR, "\n")
|
||||
self._addTextPar("Title", oStyle, tHead, isHead=False) # Title must be text:p
|
||||
self._addTextPar(xText, S_TITLE, oStyle, tHead, isHead=False)
|
||||
|
||||
elif tType == self.T_HEAD1:
|
||||
tHead = tText.replace(nwHeadFmt.BR, "\n")
|
||||
self._addTextPar("Heading_20_1", oStyle, tHead, isHead=True, oLevel="1")
|
||||
self._addTextPar(xText, S_HEAD1, oStyle, tHead, isHead=True, oLevel="1")
|
||||
|
||||
elif tType == self.T_HEAD2:
|
||||
tHead = tText.replace(nwHeadFmt.BR, "\n")
|
||||
self._addTextPar("Heading_20_2", oStyle, tHead, isHead=True, oLevel="2")
|
||||
self._addTextPar(xText, S_HEAD2, oStyle, tHead, isHead=True, oLevel="2")
|
||||
|
||||
elif tType == self.T_HEAD3:
|
||||
tHead = tText.replace(nwHeadFmt.BR, "\n")
|
||||
self._addTextPar("Heading_20_3", oStyle, tHead, isHead=True, oLevel="3")
|
||||
self._addTextPar(xText, S_HEAD3, oStyle, tHead, isHead=True, oLevel="3")
|
||||
|
||||
elif tType == self.T_HEAD4:
|
||||
tHead = tText.replace(nwHeadFmt.BR, "\n")
|
||||
self._addTextPar("Heading_20_4", oStyle, tHead, isHead=True, oLevel="4")
|
||||
self._addTextPar(xText, S_HEAD4, oStyle, tHead, isHead=True, oLevel="4")
|
||||
|
||||
elif tType == self.T_SEP:
|
||||
self._addTextPar("Separator", oStyle, tText)
|
||||
self._addTextPar(xText, S_SEP, oStyle, tText)
|
||||
|
||||
elif tType == self.T_SKIP:
|
||||
self._addTextPar("Separator", oStyle, "")
|
||||
|
||||
elif tType == self.T_TEXT:
|
||||
if pStyle is None:
|
||||
pStyle = oStyle
|
||||
pText.append(tText)
|
||||
pFmt.append(tFormat)
|
||||
self._addTextPar(xText, S_TEXT, oStyle, "")
|
||||
|
||||
elif tType == self.T_SYNOPSIS and self._doSynopsis:
|
||||
tTemp, fTemp = self._formatSynopsis(tText, True)
|
||||
self._addTextPar("Text_20_Meta", oStyle, tTemp, tFmt=fTemp)
|
||||
tTemp, tFmt = self._formatSynopsis(tText, tFormat, True)
|
||||
self._addTextPar(xText, S_META, oStyle, tTemp, tFmt=tFmt)
|
||||
|
||||
elif tType == self.T_SHORT and self._doSynopsis:
|
||||
tTemp, fTemp = self._formatSynopsis(tText, False)
|
||||
self._addTextPar("Text_20_Meta", oStyle, tTemp, tFmt=fTemp)
|
||||
tTemp, tFmt = self._formatSynopsis(tText, tFormat, False)
|
||||
self._addTextPar(xText, S_META, oStyle, tTemp, tFmt=tFmt)
|
||||
|
||||
elif tType == self.T_COMMENT and self._doComments:
|
||||
tTemp, fTemp = self._formatComments(tText)
|
||||
self._addTextPar("Text_20_Meta", oStyle, tTemp, tFmt=fTemp)
|
||||
tTemp, tFmt = self._formatComments(tText, tFormat)
|
||||
self._addTextPar(xText, S_META, oStyle, tTemp, tFmt=tFmt)
|
||||
|
||||
elif tType == self.T_KEYWORD and self._doKeywords:
|
||||
tTemp, fTemp = self._formatKeywords(tText)
|
||||
self._addTextPar("Text_20_Meta", oStyle, tTemp, tFmt=fTemp)
|
||||
tTemp, tFmt = self._formatKeywords(tText)
|
||||
self._addTextPar(xText, S_META, oStyle, tTemp, tFmt=tFmt)
|
||||
|
||||
return
|
||||
|
||||
@@ -569,28 +588,32 @@ class ToOdt(Tokenizer):
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _formatSynopsis(self, text: str, synopsis: bool) -> tuple[str, list[tuple[int, int]]]:
|
||||
def _formatSynopsis(self, text: str, fmt: T_Formats, synopsis: bool) -> tuple[str, T_Formats]:
|
||||
"""Apply formatting to synopsis lines."""
|
||||
name = self._localLookup("Synopsis" if synopsis else "Short Description")
|
||||
shift = len(name) + 2
|
||||
rTxt = f"{name}: {text}"
|
||||
rFmt = [(0, self.FMT_B_B), (len(name) + 1, self.FMT_B_E)]
|
||||
rFmt: T_Formats = [(0, self.FMT_B_B, ""), (len(name) + 1, self.FMT_B_E, "")]
|
||||
rFmt.extend((p + shift, f, d) for p, f, d in fmt)
|
||||
return rTxt, rFmt
|
||||
|
||||
def _formatComments(self, text: str) -> tuple[str, list[tuple[int, int]]]:
|
||||
def _formatComments(self, text: str, fmt: T_Formats) -> tuple[str, T_Formats]:
|
||||
"""Apply formatting to comments."""
|
||||
name = self._localLookup("Comment")
|
||||
shift = len(name) + 2
|
||||
rTxt = f"{name}: {text}"
|
||||
rFmt = [(0, self.FMT_B_B), (len(name) + 1, self.FMT_B_E)]
|
||||
rFmt: T_Formats = [(0, self.FMT_B_B, ""), (len(name) + 1, self.FMT_B_E, "")]
|
||||
rFmt.extend((p + shift, f, d) for p, f, d in fmt)
|
||||
return rTxt, rFmt
|
||||
|
||||
def _formatKeywords(self, text: str) -> tuple[str, list[tuple[int, int]]]:
|
||||
def _formatKeywords(self, text: str) -> tuple[str, T_Formats]:
|
||||
"""Apply formatting to keywords."""
|
||||
valid, bits, _ = self._project.index.scanThis("@"+text)
|
||||
if not valid or not bits or bits[0] not in nwLabels.KEY_NAME:
|
||||
return "", []
|
||||
|
||||
rTxt = f"{self._localLookup(nwLabels.KEY_NAME[bits[0]])}: "
|
||||
rFmt = [(0, self.FMT_B_B), (len(rTxt) - 1, self.FMT_B_E)]
|
||||
rFmt: T_Formats = [(0, self.FMT_B_B, ""), (len(rTxt) - 1, self.FMT_B_E, "")]
|
||||
if len(bits) > 1:
|
||||
if bits[0] == nwKeyWords.TAG_KEY:
|
||||
rTxt += bits[1]
|
||||
@@ -600,8 +623,8 @@ class ToOdt(Tokenizer):
|
||||
return rTxt, rFmt
|
||||
|
||||
def _addTextPar(
|
||||
self, styleName: str, oStyle: ODTParagraphStyle, tText: str,
|
||||
tFmt: Sequence[tuple[int, int]] = [], isHead: bool = False, oLevel: str | None = None
|
||||
self, xParent: ET.Element, styleName: str, oStyle: ODTParagraphStyle, tText: str,
|
||||
tFmt: Sequence[tuple[int, int, str]] = [], isHead: bool = False, oLevel: str | None = None
|
||||
) -> None:
|
||||
"""Add a text paragraph to the text XML element."""
|
||||
tAttr = {_mkTag("text", "style-name"): self._paraStyle(styleName, oStyle)}
|
||||
@@ -609,7 +632,7 @@ class ToOdt(Tokenizer):
|
||||
tAttr[_mkTag("text", "outline-level")] = oLevel
|
||||
|
||||
pTag = "h" if isHead else "p"
|
||||
xElem = ET.SubElement(self._xText, _mkTag("text", pTag), attrib=tAttr)
|
||||
xElem = ET.SubElement(xParent, _mkTag("text", pTag), attrib=tAttr)
|
||||
|
||||
# It's important to set the initial text field to empty, otherwise
|
||||
# xmlIndent will add a line break if the first subelement is a span.
|
||||
@@ -627,7 +650,13 @@ class ToOdt(Tokenizer):
|
||||
xFmt = 0x00
|
||||
tFrag = ""
|
||||
fLast = 0
|
||||
for fPos, fFmt in tFmt:
|
||||
xNode = None
|
||||
for fPos, fFmt, fData in tFmt:
|
||||
|
||||
# Add any extra nodes
|
||||
if xNode is not None:
|
||||
parProc.appendNode(xNode)
|
||||
xNode = None
|
||||
|
||||
# Add the text up to the current fragment
|
||||
if tFrag := tText[fLast:fPos]:
|
||||
@@ -665,11 +694,26 @@ class ToOdt(Tokenizer):
|
||||
xFmt |= X_SUB
|
||||
elif fFmt == self.FMT_SUB_E:
|
||||
xFmt &= M_SUB
|
||||
elif fFmt == self.FMT_DL_B:
|
||||
xFmt |= X_DLG
|
||||
elif fFmt == self.FMT_DL_E:
|
||||
xFmt &= M_DLG
|
||||
elif fFmt == self.FMT_ADL_B:
|
||||
xFmt |= X_DLA
|
||||
elif fFmt == self.FMT_ADL_E:
|
||||
xFmt &= M_DLA
|
||||
elif fFmt == self.FMT_FNOTE:
|
||||
xNode = self._generateFootnote(fData)
|
||||
elif fFmt == self.FMT_STRIP:
|
||||
pass
|
||||
else:
|
||||
pErr += 1
|
||||
|
||||
fLast = fPos
|
||||
|
||||
if xNode is not None:
|
||||
parProc.appendNode(xNode)
|
||||
|
||||
if tFrag := tText[fLast:]:
|
||||
if xFmt == 0x00:
|
||||
parProc.appendText(tFrag)
|
||||
@@ -715,7 +759,7 @@ class ToOdt(Tokenizer):
|
||||
|
||||
style = ODTTextStyle(f"T{len(self._autoText)+1:d}")
|
||||
if hFmt & X_BLD:
|
||||
style.setFontWeight("bold")
|
||||
style.setFontWeight(self._fontBold)
|
||||
if hFmt & X_ITA:
|
||||
style.setFontStyle("italic")
|
||||
if hFmt & X_DEL:
|
||||
@@ -731,13 +775,33 @@ class ToOdt(Tokenizer):
|
||||
style.setTextPosition("super")
|
||||
if hFmt & X_SUB:
|
||||
style.setTextPosition("sub")
|
||||
if hFmt & X_DLG:
|
||||
style.setColour(self._colDialogM)
|
||||
if hFmt & X_DLA:
|
||||
style.setColour(self._colDialogA)
|
||||
self._autoText[hFmt] = style
|
||||
|
||||
return style.name
|
||||
|
||||
def _generateFootnote(self, key: str) -> ET.Element | None:
|
||||
"""Generate a footnote XML object."""
|
||||
if content := self._footnotes.get(key):
|
||||
self._nNote += 1
|
||||
nStyle = ODTParagraphStyle("New")
|
||||
xNote = ET.Element(_mkTag("text", "note"), attrib={
|
||||
_mkTag("text", "id"): f"ftn{self._nNote}",
|
||||
_mkTag("text", "note-class"): "footnote",
|
||||
})
|
||||
xCite = ET.SubElement(xNote, _mkTag("text", "note-citation"))
|
||||
xCite.text = str(self._nNote)
|
||||
xBody = ET.SubElement(xNote, _mkTag("text", "note-body"))
|
||||
self._addTextPar(xBody, "Footnote", nStyle, content[0], tFmt=content[1])
|
||||
return xNote
|
||||
return None
|
||||
|
||||
def _emToCm(self, value: float) -> str:
|
||||
"""Converts an em value to centimetres."""
|
||||
return f"{value*2.54/72*self._textSize:.3f}cm"
|
||||
return f"{value*2.54/72*self._fontSize:.3f}cm"
|
||||
|
||||
##
|
||||
# Style Elements
|
||||
@@ -757,7 +821,6 @@ class ToOdt(Tokenizer):
|
||||
_mkTag("fo", "margin-bottom"): self._mDocBtm,
|
||||
_mkTag("fo", "margin-left"): self._mDocLeft,
|
||||
_mkTag("fo", "margin-right"): self._mDocRight,
|
||||
_mkTag("fo", "print-orientation"): "portrait",
|
||||
})
|
||||
|
||||
xHead = ET.SubElement(xPage, _mkTag("style", "header-style"))
|
||||
@@ -782,8 +845,10 @@ class ToOdt(Tokenizer):
|
||||
_mkTag("style", "writing-mode"): "page",
|
||||
})
|
||||
ET.SubElement(xStyl, _mkTag("style", "text-properties"), attrib={
|
||||
_mkTag("style", "font-name"): self._textFont,
|
||||
_mkTag("style", "font-name"): self._fontFamily,
|
||||
_mkTag("fo", "font-family"): self._fontFamily,
|
||||
_mkTag("fo", "font-weight"): self._fontWeight,
|
||||
_mkTag("fo", "font-style"): self._fontStyle,
|
||||
_mkTag("fo", "font-size"): self._fSizeText,
|
||||
_mkTag("fo", "language"): self._dLanguage,
|
||||
_mkTag("fo", "country"): self._dCountry,
|
||||
@@ -796,8 +861,10 @@ class ToOdt(Tokenizer):
|
||||
_mkTag("style", "class"): "text",
|
||||
})
|
||||
ET.SubElement(xStyl, _mkTag("style", "text-properties"), attrib={
|
||||
_mkTag("style", "font-name"): self._textFont,
|
||||
_mkTag("style", "font-name"): self._fontFamily,
|
||||
_mkTag("fo", "font-family"): self._fontFamily,
|
||||
_mkTag("fo", "font-weight"): self._fontWeight,
|
||||
_mkTag("fo", "font-style"): self._fontStyle,
|
||||
_mkTag("fo", "font-size"): self._fSizeText,
|
||||
})
|
||||
|
||||
@@ -806,7 +873,7 @@ class ToOdt(Tokenizer):
|
||||
_mkTag("style", "name"): "Heading",
|
||||
_mkTag("style", "family"): "paragraph",
|
||||
_mkTag("style", "parent-style-name"): "Standard",
|
||||
_mkTag("style", "next-style-name"): "Text_20_body",
|
||||
_mkTag("style", "next-style-name"): S_TEXT,
|
||||
_mkTag("style", "class"): "text",
|
||||
})
|
||||
ET.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib={
|
||||
@@ -815,14 +882,16 @@ class ToOdt(Tokenizer):
|
||||
_mkTag("fo", "keep-with-next"): "always",
|
||||
})
|
||||
ET.SubElement(xStyl, _mkTag("style", "text-properties"), attrib={
|
||||
_mkTag("style", "font-name"): self._textFont,
|
||||
_mkTag("style", "font-name"): self._fontFamily,
|
||||
_mkTag("fo", "font-family"): self._fontFamily,
|
||||
_mkTag("fo", "font-weight"): self._fontWeight,
|
||||
_mkTag("fo", "font-style"): self._fontStyle,
|
||||
_mkTag("fo", "font-size"): self._fSizeHead,
|
||||
})
|
||||
|
||||
# Add Header and Footer Styles
|
||||
ET.SubElement(self._xStyl, _mkTag("style", "style"), attrib={
|
||||
_mkTag("style", "name"): "Header_20_and_20_Footer",
|
||||
_mkTag("style", "name"): S_HNF,
|
||||
_mkTag("style", "display-name"): "Header and Footer",
|
||||
_mkTag("style", "family"): "paragraph",
|
||||
_mkTag("style", "parent-style-name"): "Standard",
|
||||
@@ -834,7 +903,7 @@ class ToOdt(Tokenizer):
|
||||
def _useableStyles(self) -> None:
|
||||
"""Set the usable styles."""
|
||||
# Add Text Body Style
|
||||
style = ODTParagraphStyle("Text_20_body")
|
||||
style = ODTParagraphStyle(S_TEXT)
|
||||
style.setDisplayName("Text body")
|
||||
style.setParentStyleName("Standard")
|
||||
style.setClass("text")
|
||||
@@ -842,136 +911,139 @@ class ToOdt(Tokenizer):
|
||||
style.setMarginBottom(self._mBotText)
|
||||
style.setLineHeight(self._fLineHeight)
|
||||
style.setTextAlign(self._textAlign)
|
||||
style.setFontName(self._textFont)
|
||||
style.setFontName(self._fontFamily)
|
||||
style.setFontFamily(self._fontFamily)
|
||||
style.setFontSize(self._fSizeText)
|
||||
style.setFontWeight(self._fontWeight)
|
||||
style.packXML(self._xStyl)
|
||||
self._mainPara[style.name] = style
|
||||
|
||||
# Add First Line Indent Style
|
||||
style = ODTParagraphStyle("First_20_line_20_indent")
|
||||
style = ODTParagraphStyle(S_FIND)
|
||||
style.setDisplayName("First line indent")
|
||||
style.setParentStyleName("Text_20_body")
|
||||
style.setParentStyleName(S_TEXT)
|
||||
style.setClass("text")
|
||||
style.setTextIndent(self._fTextIndent)
|
||||
style.packXML(self._xStyl)
|
||||
self._mainPara[style.name] = style
|
||||
|
||||
# Add Text Meta Style
|
||||
style = ODTParagraphStyle("Text_20_Meta")
|
||||
style = ODTParagraphStyle(S_META)
|
||||
style.setDisplayName("Text Meta")
|
||||
style.setParentStyleName("Standard")
|
||||
style.setClass("text")
|
||||
style.setMarginTop(self._mTopMeta)
|
||||
style.setMarginBottom(self._mBotMeta)
|
||||
style.setLineHeight(self._fLineHeight)
|
||||
style.setFontName(self._textFont)
|
||||
style.setFontName(self._fontFamily)
|
||||
style.setFontFamily(self._fontFamily)
|
||||
style.setFontSize(self._fSizeText)
|
||||
style.setFontWeight(self._fontWeight)
|
||||
style.setColour(self._colMetaTx)
|
||||
style.setOpacity(self._opaMetaTx)
|
||||
style.packXML(self._xStyl)
|
||||
self._mainPara[style.name] = style
|
||||
|
||||
# Add Title Style
|
||||
style = ODTParagraphStyle("Title")
|
||||
style = ODTParagraphStyle(S_TITLE)
|
||||
style.setDisplayName("Title")
|
||||
style.setParentStyleName("Heading")
|
||||
style.setNextStyleName("Text_20_body")
|
||||
style.setNextStyleName(S_TEXT)
|
||||
style.setClass("chapter")
|
||||
style.setMarginTop(self._mTopTitle)
|
||||
style.setMarginBottom(self._mBotTitle)
|
||||
style.setTextAlign("center")
|
||||
style.setFontName(self._textFont)
|
||||
style.setFontName(self._fontFamily)
|
||||
style.setFontFamily(self._fontFamily)
|
||||
style.setFontSize(self._fSizeTitle)
|
||||
style.setFontWeight("bold")
|
||||
style.setFontWeight(self._fontBold)
|
||||
style.packXML(self._xStyl)
|
||||
self._mainPara[style.name] = style
|
||||
|
||||
# Add Separator Style
|
||||
style = ODTParagraphStyle("Separator")
|
||||
style = ODTParagraphStyle(S_SEP)
|
||||
style.setDisplayName("Separator")
|
||||
style.setParentStyleName("Standard")
|
||||
style.setNextStyleName("Text_20_body")
|
||||
style.setNextStyleName(S_TEXT)
|
||||
style.setClass("text")
|
||||
style.setMarginTop(self._mTopText)
|
||||
style.setMarginBottom(self._mBotText)
|
||||
style.setMarginTop(self._mTopSep)
|
||||
style.setMarginBottom(self._mBotSep)
|
||||
style.setLineHeight(self._fLineHeight)
|
||||
style.setTextAlign("center")
|
||||
style.setFontName(self._textFont)
|
||||
style.setFontName(self._fontFamily)
|
||||
style.setFontFamily(self._fontFamily)
|
||||
style.setFontSize(self._fSizeText)
|
||||
style.setFontWeight(self._fontWeight)
|
||||
style.packXML(self._xStyl)
|
||||
self._mainPara[style.name] = style
|
||||
|
||||
# Add Heading 1 Style
|
||||
style = ODTParagraphStyle("Heading_20_1")
|
||||
style = ODTParagraphStyle(S_HEAD1)
|
||||
style.setDisplayName("Heading 1")
|
||||
style.setParentStyleName("Heading")
|
||||
style.setNextStyleName("Text_20_body")
|
||||
style.setNextStyleName(S_TEXT)
|
||||
style.setOutlineLevel("1")
|
||||
style.setClass("text")
|
||||
style.setMarginTop(self._mTopHead1)
|
||||
style.setMarginBottom(self._mBotHead1)
|
||||
style.setFontName(self._textFont)
|
||||
style.setFontName(self._fontFamily)
|
||||
style.setFontFamily(self._fontFamily)
|
||||
style.setFontSize(self._fSizeHead1)
|
||||
style.setFontWeight("bold")
|
||||
style.setFontWeight(self._fontBold)
|
||||
style.setColour(self._colHead12)
|
||||
style.setOpacity(self._opaHead12)
|
||||
style.packXML(self._xStyl)
|
||||
self._mainPara[style.name] = style
|
||||
|
||||
# Add Heading 2 Style
|
||||
style = ODTParagraphStyle("Heading_20_2")
|
||||
style = ODTParagraphStyle(S_HEAD2)
|
||||
style.setDisplayName("Heading 2")
|
||||
style.setParentStyleName("Heading")
|
||||
style.setNextStyleName("Text_20_body")
|
||||
style.setNextStyleName(S_TEXT)
|
||||
style.setOutlineLevel("2")
|
||||
style.setClass("text")
|
||||
style.setMarginTop(self._mTopHead2)
|
||||
style.setMarginBottom(self._mBotHead2)
|
||||
style.setFontName(self._textFont)
|
||||
style.setFontName(self._fontFamily)
|
||||
style.setFontFamily(self._fontFamily)
|
||||
style.setFontSize(self._fSizeHead2)
|
||||
style.setFontWeight("bold")
|
||||
style.setFontWeight(self._fontBold)
|
||||
style.setColour(self._colHead12)
|
||||
style.setOpacity(self._opaHead12)
|
||||
style.packXML(self._xStyl)
|
||||
self._mainPara[style.name] = style
|
||||
|
||||
# Add Heading 3 Style
|
||||
style = ODTParagraphStyle("Heading_20_3")
|
||||
style = ODTParagraphStyle(S_HEAD3)
|
||||
style.setDisplayName("Heading 3")
|
||||
style.setParentStyleName("Heading")
|
||||
style.setNextStyleName("Text_20_body")
|
||||
style.setNextStyleName(S_TEXT)
|
||||
style.setOutlineLevel("3")
|
||||
style.setClass("text")
|
||||
style.setMarginTop(self._mTopHead3)
|
||||
style.setMarginBottom(self._mBotHead3)
|
||||
style.setFontName(self._textFont)
|
||||
style.setFontName(self._fontFamily)
|
||||
style.setFontFamily(self._fontFamily)
|
||||
style.setFontSize(self._fSizeHead3)
|
||||
style.setFontWeight("bold")
|
||||
style.setFontWeight(self._fontBold)
|
||||
style.setColour(self._colHead34)
|
||||
style.setOpacity(self._opaHead34)
|
||||
style.packXML(self._xStyl)
|
||||
self._mainPara[style.name] = style
|
||||
|
||||
# Add Heading 4 Style
|
||||
style = ODTParagraphStyle("Heading_20_4")
|
||||
style = ODTParagraphStyle(S_HEAD4)
|
||||
style.setDisplayName("Heading 4")
|
||||
style.setParentStyleName("Heading")
|
||||
style.setNextStyleName("Text_20_body")
|
||||
style.setNextStyleName(S_TEXT)
|
||||
style.setOutlineLevel("4")
|
||||
style.setClass("text")
|
||||
style.setMarginTop(self._mTopHead4)
|
||||
style.setMarginBottom(self._mBotHead4)
|
||||
style.setFontName(self._textFont)
|
||||
style.setFontName(self._fontFamily)
|
||||
style.setFontFamily(self._fontFamily)
|
||||
style.setFontSize(self._fSizeHead4)
|
||||
style.setFontWeight("bold")
|
||||
style.setFontWeight(self._fontBold)
|
||||
style.setColour(self._colHead34)
|
||||
style.setOpacity(self._opaHead34)
|
||||
style.packXML(self._xStyl)
|
||||
@@ -980,11 +1052,23 @@ class ToOdt(Tokenizer):
|
||||
# Add Header Style
|
||||
style = ODTParagraphStyle("Header")
|
||||
style.setDisplayName("Header")
|
||||
style.setParentStyleName("Header_20_and_20_Footer")
|
||||
style.setParentStyleName(S_HNF)
|
||||
style.setTextAlign("right")
|
||||
style.packXML(self._xStyl)
|
||||
self._mainPara[style.name] = style
|
||||
|
||||
# Add Footnote Style
|
||||
style = ODTParagraphStyle("Footnote")
|
||||
style.setDisplayName("Footnote")
|
||||
style.setParentStyleName("Standard")
|
||||
style.setClass("extra")
|
||||
style.setMarginLeft(self._mLeftFoot)
|
||||
style.setMarginBottom(self._mBotFoot)
|
||||
style.setTextIndent("-"+self._mLeftFoot)
|
||||
style.setFontSize(self._fSizeFoot)
|
||||
style.packXML(self._xStyl)
|
||||
self._mainPara[style.name] = style
|
||||
|
||||
return
|
||||
|
||||
def _writeHeader(self) -> None:
|
||||
@@ -1026,12 +1110,9 @@ class ToOdt(Tokenizer):
|
||||
|
||||
return
|
||||
|
||||
# END Class ToOdt
|
||||
|
||||
|
||||
# =============================================================================================== #
|
||||
# Auto-Style Classes
|
||||
# =============================================================================================== #
|
||||
# Auto-Style Classes
|
||||
# ==================
|
||||
|
||||
class ODTParagraphStyle:
|
||||
"""Wrapper class for the paragraph style setting used by the
|
||||
@@ -1041,8 +1122,8 @@ class ODTParagraphStyle:
|
||||
VALID_ALIGN = ["start", "center", "end", "justify", "inside", "outside", "left", "right"]
|
||||
VALID_BREAK = ["auto", "column", "page", "even-page", "odd-page", "inherit"]
|
||||
VALID_LEVEL = ["1", "2", "3", "4"]
|
||||
VALID_CLASS = ["text", "chapter"]
|
||||
VALID_WEIGHT = ["normal", "inherit", "bold"]
|
||||
VALID_CLASS = ["text", "chapter", "extra"]
|
||||
VALID_WEIGHT = ["normal", "bold"] + FONT_WEIGHT_NUM
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
|
||||
@@ -1279,16 +1360,14 @@ class ODTParagraphStyle:
|
||||
|
||||
return
|
||||
|
||||
# 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
|
||||
and fast.
|
||||
"""
|
||||
VALID_WEIGHT = ["normal", "inherit", "bold"]
|
||||
VALID_STYLE = ["normal", "inherit", "italic"]
|
||||
VALID_WEIGHT = ["normal", "bold"] + FONT_WEIGHT_NUM
|
||||
VALID_STYLE = ["normal", "italic", "oblique"]
|
||||
VALID_POS = ["super", "sub"]
|
||||
VALID_LSTYLE = ["none", "solid"]
|
||||
VALID_LTYPE = ["single", "double"]
|
||||
@@ -1300,6 +1379,7 @@ class ODTTextStyle:
|
||||
self._tAttr = {
|
||||
"font-weight": ["fo", None],
|
||||
"font-style": ["fo", None],
|
||||
"color": ["fo", None],
|
||||
"background-color": ["fo", None],
|
||||
"text-position": ["style", None],
|
||||
"text-line-through-style": ["style", None],
|
||||
@@ -1334,6 +1414,14 @@ class ODTTextStyle:
|
||||
self._tAttr["font-style"][1] = None
|
||||
return
|
||||
|
||||
def setColour(self, value: str | None) -> None:
|
||||
"""Set text colour."""
|
||||
if value and len(value) == 7 and value[0] == "#":
|
||||
self._tAttr["color"][1] = value
|
||||
else:
|
||||
self._tAttr["color"][1] = None
|
||||
return
|
||||
|
||||
def setBackgroundColour(self, value: str | None) -> None:
|
||||
"""Set text background colour."""
|
||||
if value and len(value) == 7 and value[0] == "#":
|
||||
@@ -1404,12 +1492,9 @@ class ODTTextStyle:
|
||||
ET.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=attr)
|
||||
return
|
||||
|
||||
# END Class ODTTextStyle
|
||||
|
||||
|
||||
# =============================================================================================== #
|
||||
# XML Complex Element Helper Class
|
||||
# =============================================================================================== #
|
||||
# XML Complex Element Helper Class
|
||||
# ================================
|
||||
|
||||
X_ROOT_TEXT = 0
|
||||
X_ROOT_TAIL = 1
|
||||
@@ -1464,7 +1549,6 @@ class XMLParagraph:
|
||||
if c == " ":
|
||||
nSpaces += 1
|
||||
continue
|
||||
|
||||
elif nSpaces > 0:
|
||||
self._processSpaces(nSpaces)
|
||||
nSpaces = 0
|
||||
@@ -1475,26 +1559,22 @@ class XMLParagraph:
|
||||
self._xTail.tail = ""
|
||||
self._nState = X_ROOT_TAIL
|
||||
self._chrPos += 1
|
||||
|
||||
elif self._nState in (X_SPAN_TEXT, X_SPAN_SING):
|
||||
self._xSing = ET.SubElement(self._xTail, TAG_BR)
|
||||
self._xSing.tail = ""
|
||||
self._nState = X_SPAN_SING
|
||||
self._chrPos += 1
|
||||
|
||||
elif c == "\t":
|
||||
if self._nState in (X_ROOT_TEXT, X_ROOT_TAIL):
|
||||
self._xTail = ET.SubElement(self._xRoot, TAG_TAB)
|
||||
self._xTail.tail = ""
|
||||
self._nState = X_ROOT_TAIL
|
||||
self._chrPos += 1
|
||||
|
||||
elif self._nState in (X_SPAN_TEXT, X_SPAN_SING):
|
||||
self._xSing = ET.SubElement(self._xTail, TAG_TAB)
|
||||
self._xSing.tail = ""
|
||||
self._chrPos += 1
|
||||
self._nState = X_SPAN_SING
|
||||
|
||||
else:
|
||||
if self._nState == X_ROOT_TEXT:
|
||||
self._xRoot.text = (self._xRoot.text or "") + c
|
||||
@@ -1529,6 +1609,19 @@ class XMLParagraph:
|
||||
self._nState = X_ROOT_TAIL
|
||||
return
|
||||
|
||||
def appendNode(self, xNode: ET.Element | None) -> None:
|
||||
"""Append an XML node to the paragraph. We only check for the
|
||||
X_ROOT_TEXT and X_ROOT_TAIL states. X_SPAN_TEXT is not possible
|
||||
at all, and X_SPAN_SING only happens internally in an appendSpan
|
||||
call, returning us to an X_ROOT_TAIL state.
|
||||
"""
|
||||
if xNode is not None and self._nState in (X_ROOT_TEXT, X_ROOT_TAIL):
|
||||
self._xRoot.append(xNode)
|
||||
self._xTail = xNode
|
||||
self._xTail.tail = ""
|
||||
self._nState = X_ROOT_TAIL
|
||||
return
|
||||
|
||||
def checkError(self) -> tuple[int, str]:
|
||||
"""Check that the number of characters written matches the
|
||||
number of characters received.
|
||||
@@ -1600,5 +1693,3 @@ class XMLParagraph:
|
||||
self._chrPos += nSpaces - 1
|
||||
|
||||
return
|
||||
|
||||
# END Class XMLParagraph
|
||||
|
||||
419
novelwriter/core/toqdoc.py
Normal file
419
novelwriter/core/toqdoc.py
Normal file
@@ -0,0 +1,419 @@
|
||||
"""
|
||||
novelWriter – QTextDocument Converter
|
||||
=====================================
|
||||
|
||||
File History:
|
||||
Created: 2024-05-21 [2.5b1] ToQTextDocument
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2024, Veronica Berglyd Olsen
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from PyQt5.QtGui import (
|
||||
QColor, QFont, QFontMetricsF, QTextBlockFormat, QTextCharFormat,
|
||||
QTextCursor, QTextDocument
|
||||
)
|
||||
|
||||
from novelwriter.constants import nwHeaders, nwHeadFmt, nwKeyWords, nwLabels, nwUnicode
|
||||
from novelwriter.core.project import NWProject
|
||||
from novelwriter.core.tokenizer import T_Formats, Tokenizer
|
||||
from novelwriter.types import (
|
||||
QtAlignAbsolute, QtAlignCenter, QtAlignJustify, QtAlignLeft, QtAlignRight,
|
||||
QtBlack, QtPageBreakAfter, QtPageBreakBefore, QtTransparent,
|
||||
QtVAlignNormal, QtVAlignSub, QtVAlignSuper
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
T_TextStyle = tuple[QTextBlockFormat, QTextCharFormat]
|
||||
|
||||
|
||||
class TextDocumentTheme:
|
||||
text: QColor = QtBlack
|
||||
highlight: QColor = QtTransparent
|
||||
head: QColor = QtBlack
|
||||
comment: QColor = QtBlack
|
||||
note: QColor = QtBlack
|
||||
code: QColor = QtBlack
|
||||
modifier: QColor = QtBlack
|
||||
keyword: QColor = QtBlack
|
||||
tag: QColor = QtBlack
|
||||
optional: QColor = QtBlack
|
||||
dialog: QColor = QtBlack
|
||||
altdialog: QColor = QtBlack
|
||||
|
||||
|
||||
def newBlock(cursor: QTextCursor, bFmt: QTextBlockFormat) -> None:
|
||||
if cursor.position() > 0:
|
||||
cursor.insertBlock(bFmt)
|
||||
else:
|
||||
cursor.setBlockFormat(bFmt)
|
||||
|
||||
|
||||
class ToQTextDocument(Tokenizer):
|
||||
"""Core: QTextDocument Writer
|
||||
|
||||
Extend the Tokenizer class to generate a QTextDocument output. This
|
||||
is intended for usage in the document viewer and build tool preview.
|
||||
"""
|
||||
|
||||
def __init__(self, project: NWProject) -> None:
|
||||
super().__init__(project)
|
||||
self._document = QTextDocument()
|
||||
self._document.setUndoRedoEnabled(False)
|
||||
self._document.setDocumentMargin(0)
|
||||
|
||||
self._theme = TextDocumentTheme()
|
||||
self._styles: dict[int, T_TextStyle] = {}
|
||||
self._usedNotes: dict[str, int] = {}
|
||||
|
||||
self._init = False
|
||||
self._bold = QFont.Weight.Bold
|
||||
self._normal = QFont.Weight.Normal
|
||||
|
||||
return
|
||||
|
||||
def initDocument(self, font: QFont, theme: TextDocumentTheme) -> None:
|
||||
"""Initialise all computed values of the document."""
|
||||
self._textFont = font
|
||||
self._theme = theme
|
||||
|
||||
self._document.setUndoRedoEnabled(False)
|
||||
self._document.blockSignals(True)
|
||||
self._document.clear()
|
||||
self._document.setDefaultFont(self._textFont)
|
||||
|
||||
qMetric = QFontMetricsF(self._textFont)
|
||||
mPx = qMetric.ascent() # 1 em in pixels
|
||||
fPt = self._textFont.pointSizeF()
|
||||
|
||||
# Scaled Sizes
|
||||
# ============
|
||||
|
||||
self._mHead = {
|
||||
self.T_TITLE: (mPx * self._marginTitle[0], mPx * self._marginTitle[1]),
|
||||
self.T_HEAD1: (mPx * self._marginHead1[0], mPx * self._marginHead1[1]),
|
||||
self.T_HEAD2: (mPx * self._marginHead2[0], mPx * self._marginHead2[1]),
|
||||
self.T_HEAD3: (mPx * self._marginHead3[0], mPx * self._marginHead3[1]),
|
||||
self.T_HEAD4: (mPx * self._marginHead4[0], mPx * self._marginHead4[1]),
|
||||
}
|
||||
|
||||
self._sHead = {
|
||||
self.T_TITLE: nwHeaders.H_SIZES.get(0, 1.0) * fPt,
|
||||
self.T_HEAD1: nwHeaders.H_SIZES.get(1, 1.0) * fPt,
|
||||
self.T_HEAD2: nwHeaders.H_SIZES.get(2, 1.0) * fPt,
|
||||
self.T_HEAD3: nwHeaders.H_SIZES.get(3, 1.0) * fPt,
|
||||
self.T_HEAD4: nwHeaders.H_SIZES.get(4, 1.0) * fPt,
|
||||
}
|
||||
|
||||
self._mText = (mPx * self._marginText[0], mPx * self._marginText[1])
|
||||
self._mMeta = (mPx * self._marginMeta[0], mPx * self._marginMeta[1])
|
||||
self._mSep = (mPx * self._marginSep[0], mPx * self._marginSep[1])
|
||||
|
||||
self._mIndent = mPx * 2.0
|
||||
self._tIndent = mPx * self._firstWidth
|
||||
|
||||
# Block Format
|
||||
# ============
|
||||
|
||||
self._blockFmt = QTextBlockFormat()
|
||||
self._blockFmt.setTopMargin(self._mText[0])
|
||||
self._blockFmt.setBottomMargin(self._mText[1])
|
||||
self._blockFmt.setAlignment(QtAlignJustify if self._doJustify else QtAlignAbsolute)
|
||||
self._blockFmt.setLineHeight(
|
||||
100*self._lineHeight, QTextBlockFormat.LineHeightTypes.ProportionalHeight
|
||||
)
|
||||
|
||||
# Character Formats
|
||||
# =================
|
||||
|
||||
self._cText = QTextCharFormat()
|
||||
self._cText.setBackground(QtTransparent)
|
||||
self._cText.setForeground(self._theme.text)
|
||||
|
||||
self._cHead = QTextCharFormat(self._cText)
|
||||
self._cHead.setForeground(self._theme.head)
|
||||
|
||||
self._cComment = QTextCharFormat(self._cText)
|
||||
self._cComment.setForeground(self._theme.comment)
|
||||
|
||||
self._cCommentMod = QTextCharFormat(self._cText)
|
||||
self._cCommentMod.setForeground(self._theme.comment)
|
||||
self._cCommentMod.setFontWeight(self._bold)
|
||||
|
||||
self._cNote = QTextCharFormat(self._cText)
|
||||
self._cNote.setForeground(self._theme.note)
|
||||
|
||||
self._cCode = QTextCharFormat(self._cText)
|
||||
self._cCode.setForeground(self._theme.code)
|
||||
|
||||
self._cModifier = QTextCharFormat(self._cText)
|
||||
self._cModifier.setForeground(self._theme.modifier)
|
||||
self._cModifier.setFontWeight(self._bold)
|
||||
|
||||
self._cKeyword = QTextCharFormat(self._cText)
|
||||
self._cKeyword.setForeground(self._theme.keyword)
|
||||
|
||||
self._cTag = QTextCharFormat(self._cText)
|
||||
self._cTag.setForeground(self._theme.tag)
|
||||
|
||||
self._cOptional = QTextCharFormat(self._cText)
|
||||
self._cOptional.setForeground(self._theme.optional)
|
||||
|
||||
self._init = True
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Properties
|
||||
##
|
||||
|
||||
@property
|
||||
def document(self) -> QTextDocument:
|
||||
"""Return the document."""
|
||||
return self._document
|
||||
|
||||
##
|
||||
# Class Methods
|
||||
##
|
||||
|
||||
def doConvert(self) -> None:
|
||||
"""Write text tokens into the document."""
|
||||
if not self._init:
|
||||
return
|
||||
|
||||
self._document.blockSignals(True)
|
||||
cursor = QTextCursor(self._document)
|
||||
cursor.movePosition(QTextCursor.MoveOperation.End)
|
||||
|
||||
for tType, nHead, tText, tFormat, tStyle in self._tokens:
|
||||
|
||||
# Styles
|
||||
bFmt = QTextBlockFormat(self._blockFmt)
|
||||
if tStyle is not None:
|
||||
if tStyle & self.A_LEFT:
|
||||
bFmt.setAlignment(QtAlignLeft)
|
||||
elif tStyle & self.A_RIGHT:
|
||||
bFmt.setAlignment(QtAlignRight)
|
||||
elif tStyle & self.A_CENTRE:
|
||||
bFmt.setAlignment(QtAlignCenter)
|
||||
elif tStyle & self.A_JUSTIFY:
|
||||
bFmt.setAlignment(QtAlignJustify)
|
||||
|
||||
if tStyle & self.A_PBB:
|
||||
bFmt.setPageBreakPolicy(QtPageBreakBefore)
|
||||
if tStyle & self.A_PBA:
|
||||
bFmt.setPageBreakPolicy(QtPageBreakAfter)
|
||||
|
||||
if tStyle & self.A_Z_BTMMRG:
|
||||
bFmt.setBottomMargin(0.0)
|
||||
if tStyle & self.A_Z_TOPMRG:
|
||||
bFmt.setTopMargin(0.0)
|
||||
|
||||
if tStyle & self.A_IND_L:
|
||||
bFmt.setLeftMargin(self._mIndent)
|
||||
if tStyle & self.A_IND_R:
|
||||
bFmt.setRightMargin(self._mIndent)
|
||||
if tStyle & self.A_IND_T:
|
||||
bFmt.setTextIndent(self._tIndent)
|
||||
|
||||
if tType == self.T_TEXT:
|
||||
newBlock(cursor, bFmt)
|
||||
self._insertFragments(tText, tFormat, cursor, self._cText)
|
||||
|
||||
elif tType in self.L_HEADINGS:
|
||||
bFmt, cFmt = self._genHeadStyle(tType, nHead, bFmt)
|
||||
newBlock(cursor, bFmt)
|
||||
cursor.insertText(tText.replace(nwHeadFmt.BR, "\n"), cFmt)
|
||||
|
||||
elif tType == self.T_SEP:
|
||||
sFmt = QTextBlockFormat(bFmt)
|
||||
sFmt.setTopMargin(self._mSep[0])
|
||||
sFmt.setBottomMargin(self._mSep[1])
|
||||
newBlock(cursor, sFmt)
|
||||
cursor.insertText(tText, self._cText)
|
||||
|
||||
elif tType == self.T_SKIP:
|
||||
newBlock(cursor, bFmt)
|
||||
cursor.insertText(nwUnicode.U_NBSP, self._cText)
|
||||
|
||||
elif tType in self.L_SUMMARY and self._doSynopsis:
|
||||
newBlock(cursor, bFmt)
|
||||
modifier = self._localLookup(
|
||||
"Short Description" if tType == self.T_SHORT else "Synopsis"
|
||||
)
|
||||
cursor.insertText(f"{modifier}: ", self._cModifier)
|
||||
self._insertFragments(tText, tFormat, cursor, self._cNote)
|
||||
|
||||
elif tType == self.T_COMMENT and self._doComments:
|
||||
newBlock(cursor, bFmt)
|
||||
modifier = self._localLookup("Comment")
|
||||
cursor.insertText(f"{modifier}: ", self._cCommentMod)
|
||||
self._insertFragments(tText, tFormat, cursor, self._cComment)
|
||||
|
||||
elif tType == self.T_KEYWORD and self._doKeywords:
|
||||
newBlock(cursor, bFmt)
|
||||
self._insertKeywords(tText, cursor)
|
||||
|
||||
self._document.blockSignals(False)
|
||||
|
||||
return
|
||||
|
||||
def appendFootnotes(self) -> None:
|
||||
"""Append the footnotes in the buffer."""
|
||||
if self._usedNotes:
|
||||
self._document.blockSignals(True)
|
||||
|
||||
cursor = QTextCursor(self._document)
|
||||
cursor.movePosition(QTextCursor.MoveOperation.End)
|
||||
|
||||
bFmt, cFmt = self._genHeadStyle(self.T_HEAD4, -1, self._blockFmt)
|
||||
newBlock(cursor, bFmt)
|
||||
cursor.insertText(self._localLookup("Footnotes"), cFmt)
|
||||
|
||||
for key, index in self._usedNotes.items():
|
||||
if content := self._footnotes.get(key):
|
||||
cFmt = QTextCharFormat(self._cCode)
|
||||
cFmt.setAnchor(True)
|
||||
cFmt.setAnchorNames([f"footnote_{index}"])
|
||||
newBlock(cursor, self._blockFmt)
|
||||
cursor.insertText(f"{index}. ", cFmt)
|
||||
self._insertFragments(*content, cursor, self._cText)
|
||||
|
||||
self._document.blockSignals(False)
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _insertFragments(
|
||||
self, text: str, tFmt: T_Formats, cursor: QTextCursor, dFmt: QTextCharFormat
|
||||
) -> None:
|
||||
"""Apply formatting tags to text."""
|
||||
cFmt = QTextCharFormat(dFmt)
|
||||
start = 0
|
||||
temp = text.replace("\n", nwUnicode.U_LSEP)
|
||||
for pos, fmt, data in tFmt:
|
||||
|
||||
# Insert buffer with previous format
|
||||
cursor.insertText(temp[start:pos], cFmt)
|
||||
|
||||
# Construct next format
|
||||
if fmt == self.FMT_B_B:
|
||||
cFmt.setFontWeight(self._bold)
|
||||
elif fmt == self.FMT_B_E:
|
||||
cFmt.setFontWeight(self._normal)
|
||||
elif fmt == self.FMT_I_B:
|
||||
cFmt.setFontItalic(True)
|
||||
elif fmt == self.FMT_I_E:
|
||||
cFmt.setFontItalic(False)
|
||||
elif fmt == self.FMT_D_B:
|
||||
cFmt.setFontStrikeOut(True)
|
||||
elif fmt == self.FMT_D_E:
|
||||
cFmt.setFontStrikeOut(False)
|
||||
elif fmt == self.FMT_U_B:
|
||||
cFmt.setFontUnderline(True)
|
||||
elif fmt == self.FMT_U_E:
|
||||
cFmt.setFontUnderline(False)
|
||||
elif fmt == self.FMT_M_B:
|
||||
cFmt.setBackground(self._theme.highlight)
|
||||
elif fmt == self.FMT_M_E:
|
||||
cFmt.setBackground(QtTransparent)
|
||||
elif fmt == self.FMT_SUP_B:
|
||||
cFmt.setVerticalAlignment(QtVAlignSuper)
|
||||
elif fmt == self.FMT_SUP_E:
|
||||
cFmt.setVerticalAlignment(QtVAlignNormal)
|
||||
elif fmt == self.FMT_SUB_B:
|
||||
cFmt.setVerticalAlignment(QtVAlignSub)
|
||||
elif fmt == self.FMT_SUB_E:
|
||||
cFmt.setVerticalAlignment(QtVAlignNormal)
|
||||
elif fmt == self.FMT_DL_B:
|
||||
cFmt.setForeground(self._theme.dialog)
|
||||
elif fmt == self.FMT_DL_E:
|
||||
cFmt.setForeground(self._theme.text)
|
||||
elif fmt == self.FMT_ADL_B:
|
||||
cFmt.setForeground(self._theme.altdialog)
|
||||
elif fmt == self.FMT_ADL_E:
|
||||
cFmt.setForeground(self._theme.text)
|
||||
elif fmt == self.FMT_FNOTE:
|
||||
xFmt = QTextCharFormat(self._cCode)
|
||||
xFmt.setVerticalAlignment(QtVAlignSuper)
|
||||
if data in self._footnotes:
|
||||
index = len(self._usedNotes) + 1
|
||||
self._usedNotes[data] = index
|
||||
xFmt.setAnchor(True)
|
||||
xFmt.setAnchorHref(f"#footnote_{index}")
|
||||
xFmt.setFontUnderline(True)
|
||||
cursor.insertText(f"[{index}]", xFmt)
|
||||
else:
|
||||
cursor.insertText("[ERR]", cFmt)
|
||||
|
||||
# Move pos for next pass
|
||||
start = pos
|
||||
|
||||
# Insert whatever is left in the buffer
|
||||
cursor.insertText(temp[start:], cFmt)
|
||||
|
||||
return
|
||||
|
||||
def _insertKeywords(self, text: str, cursor: QTextCursor) -> None:
|
||||
"""Apply Markdown formatting to keywords."""
|
||||
valid, bits, _ = self._project.index.scanThis("@"+text)
|
||||
if valid and bits:
|
||||
key = f"{self._localLookup(nwLabels.KEY_NAME[bits[0]])}: "
|
||||
cursor.insertText(key, self._cKeyword)
|
||||
if (num := len(bits)) > 1:
|
||||
if bits[0] == nwKeyWords.TAG_KEY:
|
||||
one, two = self._project.index.parseValue(bits[1])
|
||||
cFmt = QTextCharFormat(self._cTag)
|
||||
cFmt.setAnchor(True)
|
||||
cFmt.setAnchorNames([f"tag_{one}".lower()])
|
||||
cursor.insertText(one, cFmt)
|
||||
if two:
|
||||
cursor.insertText(" | ", self._cText)
|
||||
cursor.insertText(two, self._cOptional)
|
||||
else:
|
||||
for n, bit in enumerate(bits[1:], 2):
|
||||
cFmt = QTextCharFormat(self._cTag)
|
||||
cFmt.setFontUnderline(True)
|
||||
cFmt.setAnchor(True)
|
||||
cFmt.setAnchorHref(f"#tag_{bit}".lower())
|
||||
cursor.insertText(bit, cFmt)
|
||||
if n < num:
|
||||
cursor.insertText(", ", self._cText)
|
||||
return
|
||||
|
||||
def _genHeadStyle(self, hType: int, nHead: int, rFmt: QTextBlockFormat) -> T_TextStyle:
|
||||
"""Generate a heading style set."""
|
||||
mTop, mBottom = self._mHead.get(hType, (0.0, 0.0))
|
||||
|
||||
bFmt = QTextBlockFormat(rFmt)
|
||||
bFmt.setTopMargin(mTop)
|
||||
bFmt.setBottomMargin(mBottom)
|
||||
|
||||
cFmt = QTextCharFormat(self._cText if hType == self.T_TITLE else self._cHead)
|
||||
cFmt.setFontWeight(self._bold)
|
||||
cFmt.setFontPointSize(self._sHead.get(hType, 1.0))
|
||||
if nHead >= 0:
|
||||
cFmt.setAnchorNames([f"{self._handle}:T{nHead:04d}"])
|
||||
cFmt.setAnchor(True)
|
||||
|
||||
return bFmt, cFmt
|
||||
@@ -23,18 +23,18 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
import logging
|
||||
import random
|
||||
|
||||
from typing import TYPE_CHECKING, Literal, overload
|
||||
from pathlib import Path
|
||||
from collections.abc import Iterable, Iterator
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Literal, overload
|
||||
|
||||
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
|
||||
from novelwriter.error import logException
|
||||
from novelwriter.common import isHandle
|
||||
from novelwriter.constants import nwFiles
|
||||
from novelwriter.core.item import NWItem
|
||||
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
|
||||
from novelwriter.error import logException
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from novelwriter.core.project import NWProject
|
||||
@@ -520,5 +520,3 @@ class NWTree:
|
||||
handle = self._makeHandle()
|
||||
|
||||
return handle
|
||||
|
||||
# END Class NWTree
|
||||
|
||||
@@ -27,20 +27,20 @@ import logging
|
||||
|
||||
from PyQt5.QtGui import QCloseEvent, QColor
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QDialogButtonBox, QHBoxLayout, QLabel, QTextBrowser, QVBoxLayout,
|
||||
QWidget
|
||||
QDialogButtonBox, QHBoxLayout, QLabel, QTextBrowser, QVBoxLayout, QWidget
|
||||
)
|
||||
|
||||
from novelwriter import CONFIG, SHARED
|
||||
from novelwriter.common import cssCol, readTextFile
|
||||
from novelwriter.extensions.configlayout import NColourLabel
|
||||
from novelwriter.extensions.modified import NDialog
|
||||
from novelwriter.extensions.versioninfo import VersionInfoWidget
|
||||
from novelwriter.types import QtAlignRightTop, QtDialogClose
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiAbout(QDialog):
|
||||
class GuiAbout(NDialog):
|
||||
|
||||
def __init__(self, parent: QWidget) -> None:
|
||||
super().__init__(parent=parent)
|
||||
@@ -75,7 +75,7 @@ class GuiAbout(QDialog):
|
||||
|
||||
# Credits
|
||||
self.lblCredits = NColourLabel(
|
||||
self.tr("Credits"), scale=1.6, parent=self, bold=True
|
||||
self.tr("Credits"), self, scale=1.6, bold=True
|
||||
)
|
||||
|
||||
self.txtCredits = QTextBrowser(self)
|
||||
@@ -85,7 +85,7 @@ class GuiAbout(QDialog):
|
||||
|
||||
# Buttons
|
||||
self.btnBox = QDialogButtonBox(QtDialogClose, self)
|
||||
self.btnBox.rejected.connect(self.close)
|
||||
self.btnBox.rejected.connect(self.reject)
|
||||
|
||||
# Assemble
|
||||
self.innerBox = QVBoxLayout()
|
||||
@@ -106,7 +106,9 @@ class GuiAbout(QDialog):
|
||||
|
||||
self.setLayout(self.outerBox)
|
||||
self.setSizeGripEnabled(True)
|
||||
|
||||
self._setStyleSheet()
|
||||
self._fillCreditsPage()
|
||||
|
||||
logger.debug("Ready: GuiAbout")
|
||||
|
||||
@@ -116,11 +118,6 @@ class GuiAbout(QDialog):
|
||||
logger.debug("Delete: GuiAbout")
|
||||
return
|
||||
|
||||
def populateGUI(self) -> None:
|
||||
"""Populate tabs with text."""
|
||||
self._fillCreditsPage()
|
||||
return
|
||||
|
||||
##
|
||||
# Events
|
||||
##
|
||||
@@ -128,7 +125,7 @@ class GuiAbout(QDialog):
|
||||
def closeEvent(self, event: QCloseEvent) -> None:
|
||||
"""Capture the close event and perform cleanup."""
|
||||
event.accept()
|
||||
self.deleteLater()
|
||||
self.softDelete()
|
||||
return
|
||||
|
||||
##
|
||||
@@ -137,20 +134,16 @@ class GuiAbout(QDialog):
|
||||
|
||||
def _fillCreditsPage(self) -> None:
|
||||
"""Load the content for the Credits page."""
|
||||
docPath = CONFIG.assetPath("text") / "credits_en.htm"
|
||||
docText = readTextFile(docPath)
|
||||
if docText:
|
||||
self.txtCredits.setHtml(docText)
|
||||
if html := readTextFile(CONFIG.assetPath("text") / "credits_en.htm"):
|
||||
self.txtCredits.setHtml(html)
|
||||
else:
|
||||
self.txtCredits.setHtml("Error loading credits text ...")
|
||||
return
|
||||
|
||||
def _setStyleSheet(self) -> None:
|
||||
"""Set stylesheet for all browser tabs."""
|
||||
"""Set stylesheet text document."""
|
||||
baseCol = cssCol(self.palette().window().color())
|
||||
self.txtCredits.setStyleSheet(
|
||||
f"QTextBrowser {{border: none; background: {baseCol};}} "
|
||||
)
|
||||
return
|
||||
|
||||
# END Class GuiAbout
|
||||
|
||||
@@ -27,21 +27,21 @@ from __future__ import annotations
|
||||
import logging
|
||||
|
||||
from PyQt5.QtCore import Qt, pyqtSlot
|
||||
from PyQt5.QtGui import QCloseEvent
|
||||
from PyQt5.QtWidgets import (
|
||||
QAbstractItemView, QDialog, QDialogButtonBox, QGridLayout, QLabel,
|
||||
QListWidget, QListWidgetItem, QVBoxLayout, QWidget
|
||||
QAbstractItemView, QDialogButtonBox, QGridLayout, QLabel, QListWidget,
|
||||
QListWidgetItem, QVBoxLayout, QWidget
|
||||
)
|
||||
|
||||
from novelwriter import CONFIG, SHARED
|
||||
from novelwriter.extensions.switch import NSwitch
|
||||
from novelwriter.extensions.configlayout import NColourLabel
|
||||
from novelwriter.types import QtDialogCancel, QtDialogOk, QtDialogReset, QtUserRole
|
||||
from novelwriter.extensions.modified import NDialog
|
||||
from novelwriter.extensions.switch import NSwitch
|
||||
from novelwriter.types import QtAccepted, QtDialogCancel, QtDialogOk, QtDialogReset, QtUserRole
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiDocMerge(QDialog):
|
||||
class GuiDocMerge(NDialog):
|
||||
|
||||
D_HANDLE = QtUserRole
|
||||
|
||||
@@ -58,7 +58,7 @@ class GuiDocMerge(QDialog):
|
||||
self.headLabel.setFont(SHARED.theme.guiFontB)
|
||||
self.helpLabel = NColourLabel(
|
||||
self.tr("Drag and drop items to change the order, or uncheck to exclude."),
|
||||
SHARED.theme.helpText, parent=self, wrap=True
|
||||
self, color=SHARED.theme.helpText, wrap=True
|
||||
)
|
||||
|
||||
iPx = SHARED.theme.baseIconHeight
|
||||
@@ -117,7 +117,7 @@ class GuiDocMerge(QDialog):
|
||||
logger.debug("Delete: GuiDocMerge")
|
||||
return
|
||||
|
||||
def getData(self) -> dict:
|
||||
def data(self) -> dict:
|
||||
"""Return the user's choices."""
|
||||
finalItems = []
|
||||
for i in range(self.listBox.count()):
|
||||
@@ -130,15 +130,15 @@ class GuiDocMerge(QDialog):
|
||||
|
||||
return self._data
|
||||
|
||||
##
|
||||
# Events
|
||||
##
|
||||
|
||||
def closeEvent(self, event: QCloseEvent) -> None:
|
||||
"""Capture the close event and perform cleanup."""
|
||||
event.accept()
|
||||
self.deleteLater()
|
||||
return
|
||||
@classmethod
|
||||
def getData(cls, parent: QWidget, handle: str, items: list[str]) -> tuple[dict, bool]:
|
||||
"""Pop the dialog and return the result."""
|
||||
cls = GuiDocMerge(parent, handle, items)
|
||||
cls.exec()
|
||||
data = cls.data()
|
||||
accepted = cls.result() == QtAccepted
|
||||
cls.softDelete()
|
||||
return data, accepted
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
@@ -182,5 +182,3 @@ class GuiDocMerge(QDialog):
|
||||
self.listBox.addItem(newItem)
|
||||
|
||||
return
|
||||
|
||||
# END Class GuiDocMerge
|
||||
|
||||
@@ -27,21 +27,21 @@ from __future__ import annotations
|
||||
import logging
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot
|
||||
from PyQt5.QtGui import QCloseEvent
|
||||
from PyQt5.QtWidgets import (
|
||||
QAbstractItemView, QComboBox, QDialog, QDialogButtonBox, QGridLayout,
|
||||
QLabel, QListWidget, QListWidgetItem, QVBoxLayout, QWidget
|
||||
QAbstractItemView, QComboBox, QDialogButtonBox, QGridLayout, QLabel,
|
||||
QListWidget, QListWidgetItem, QVBoxLayout, QWidget
|
||||
)
|
||||
|
||||
from novelwriter import CONFIG, SHARED
|
||||
from novelwriter.extensions.switch import NSwitch
|
||||
from novelwriter.extensions.configlayout import NColourLabel
|
||||
from novelwriter.types import QtDialogCancel, QtDialogOk, QtUserRole
|
||||
from novelwriter.extensions.modified import NDialog
|
||||
from novelwriter.extensions.switch import NSwitch
|
||||
from novelwriter.types import QtAccepted, QtDialogCancel, QtDialogOk, QtUserRole
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiDocSplit(QDialog):
|
||||
class GuiDocSplit(NDialog):
|
||||
|
||||
LINE_ROLE = QtUserRole
|
||||
LEVEL_ROLE = QtUserRole + 1
|
||||
@@ -62,7 +62,7 @@ class GuiDocSplit(QDialog):
|
||||
self.headLabel.setFont(SHARED.theme.guiFontB)
|
||||
self.helpLabel = NColourLabel(
|
||||
self.tr("Select the maximum level to split into files."),
|
||||
SHARED.theme.helpText, parent=self, wrap=True
|
||||
self, color=SHARED.theme.helpText, wrap=True
|
||||
)
|
||||
|
||||
# Values
|
||||
@@ -145,7 +145,7 @@ class GuiDocSplit(QDialog):
|
||||
logger.debug("Delete: GuiDocSplit")
|
||||
return
|
||||
|
||||
def getData(self) -> tuple[dict, list]:
|
||||
def data(self) -> tuple[dict, list[str]]:
|
||||
"""Return the user's choices. Also save the users options for
|
||||
the next time the dialog is used.
|
||||
"""
|
||||
@@ -178,15 +178,15 @@ class GuiDocSplit(QDialog):
|
||||
|
||||
return self._data, self._text
|
||||
|
||||
##
|
||||
# Events
|
||||
##
|
||||
|
||||
def closeEvent(self, event: QCloseEvent) -> None:
|
||||
"""Capture the close event and perform cleanup."""
|
||||
event.accept()
|
||||
self.deleteLater()
|
||||
return
|
||||
@classmethod
|
||||
def getData(cls, parent: QWidget, handle: str) -> tuple[dict, list[str], bool]:
|
||||
"""Pop the dialog and return the result."""
|
||||
cls = GuiDocSplit(parent, handle)
|
||||
cls.exec()
|
||||
data, text = cls.data()
|
||||
accepted = cls.result() == QtAccepted
|
||||
cls.softDelete()
|
||||
return data, text, accepted
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
@@ -257,5 +257,3 @@ class GuiDocSplit(QDialog):
|
||||
self.listBox.addItem(newItem)
|
||||
|
||||
return
|
||||
|
||||
# END Class GuiDocSplit
|
||||
|
||||
@@ -25,18 +25,16 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QDialogButtonBox, QHBoxLayout, QLabel, QLineEdit, QVBoxLayout,
|
||||
QWidget
|
||||
)
|
||||
from PyQt5.QtWidgets import QDialogButtonBox, QHBoxLayout, QLabel, QLineEdit, QVBoxLayout, QWidget
|
||||
|
||||
from novelwriter import CONFIG
|
||||
from novelwriter.types import QtDialogCancel, QtDialogOk
|
||||
from novelwriter.extensions.modified import NDialog
|
||||
from novelwriter.types import QtAccepted, QtDialogCancel, QtDialogOk
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiEditLabel(QDialog):
|
||||
class GuiEditLabel(NDialog):
|
||||
|
||||
def __init__(self, parent: QWidget, text: str = "") -> None:
|
||||
super().__init__(parent=parent)
|
||||
@@ -91,8 +89,6 @@ class GuiEditLabel(QDialog):
|
||||
cls = GuiEditLabel(parent, text=text)
|
||||
cls.exec()
|
||||
label = cls.itemLabel
|
||||
accepted = cls.result() == QDialog.DialogCode.Accepted
|
||||
cls.deleteLater()
|
||||
accepted = cls.result() == QtAccepted
|
||||
cls.softDelete()
|
||||
return label, accepted
|
||||
|
||||
# END Class GuiEditLabel
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user