Compare commits
467 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
814876f8df | ||
|
|
32bcb52f14 | ||
|
|
612ec06262 | ||
|
|
93dea90970 | ||
|
|
47ece1f285 | ||
|
|
64378419d0 | ||
|
|
00a201181d | ||
|
|
b3f9a14c85 | ||
|
|
065e3828b1 | ||
|
|
63e91f0ee5 | ||
|
|
182ef5c8f7 | ||
|
|
b0f000acfd | ||
|
|
7a55af405b | ||
|
|
a3343bc738 | ||
|
|
a324ad2dd8 | ||
|
|
38b4653cb3 | ||
|
|
fdd9186ae9 | ||
|
|
7125e9ecd5 | ||
|
|
869e4a0b9d | ||
|
|
dc0e5c218f | ||
|
|
67bd23a027 | ||
|
|
f22a350938 | ||
|
|
6ba286087d | ||
|
|
c2e3fef7fc | ||
|
|
b9d344c7d9 | ||
|
|
fe04c1af25 | ||
|
|
21d3ed61ef | ||
|
|
b7f27a2af0 | ||
|
|
36aa58b38e | ||
|
|
2f4fccfb2c | ||
|
|
d251ebb995 | ||
|
|
0c946516e8 | ||
|
|
c77ef4e4ce | ||
|
|
a9d90da5c8 | ||
|
|
4071c2ca43 | ||
|
|
0a571502ce | ||
|
|
a850e5b74e | ||
|
|
71aa1fa113 | ||
|
|
24eef2dc0b | ||
|
|
5977c8c8e2 | ||
|
|
dcd4c19b21 | ||
|
|
464c7dae0a | ||
|
|
ad92e2e5eb | ||
|
|
254845d71f | ||
|
|
3c72f195ba | ||
|
|
f3f46858c9 | ||
|
|
d4631229af | ||
|
|
a78f7d5789 | ||
|
|
fca5c77089 | ||
|
|
fe828a1c49 | ||
|
|
11e3573ebd | ||
|
|
ff1d625dc8 | ||
|
|
bd9691532f | ||
|
|
133b597093 | ||
|
|
d4d540bc33 | ||
|
|
d08322c1a3 | ||
|
|
ec1c664ed6 | ||
|
|
a3b46581b0 | ||
|
|
b115d4f02d | ||
|
|
6e64ede90b | ||
|
|
c06c2422a9 | ||
|
|
14d205f845 | ||
|
|
a57ce557e8 | ||
|
|
23c200f333 | ||
|
|
bf6a3adc06 | ||
|
|
60a8c8212c | ||
|
|
525c9c8fd8 | ||
|
|
08651685d4 | ||
|
|
3a60abaeee | ||
|
|
1e5d980b85 | ||
|
|
df36aceddb | ||
|
|
600e1cdbfb | ||
|
|
bc0cd2332c | ||
|
|
c1b5287ba0 | ||
|
|
a2a43b3996 | ||
|
|
6c07ecb700 | ||
|
|
2d3f848ab3 | ||
|
|
fdd188516b | ||
|
|
4c95b1d7ad | ||
|
|
19b7b51eea | ||
|
|
3c355e2d75 | ||
|
|
290c2f7b62 | ||
|
|
be7bcd03e1 | ||
|
|
0965f4213e | ||
|
|
bb9ca6e40f | ||
|
|
672ea6d6f2 | ||
|
|
ec189a4867 | ||
|
|
f67a50c064 | ||
|
|
a0fba34ff6 | ||
|
|
7ea45ac3d2 | ||
|
|
78a58617c1 | ||
|
|
f098aeb622 | ||
|
|
6904afc5e7 | ||
|
|
b079b30694 | ||
|
|
3e8a425684 | ||
|
|
b47c4c8f71 | ||
|
|
6d3097ae76 | ||
|
|
09e521c2c2 | ||
|
|
2439cf4633 | ||
|
|
f1b31f26d0 | ||
|
|
b1eca89a05 | ||
|
|
09bf2a0fe0 | ||
|
|
2d754641b5 | ||
|
|
871a0dfc6c | ||
|
|
3521d4a4f7 | ||
|
|
407f12760c | ||
|
|
479975fdff | ||
|
|
98fae8a6de | ||
|
|
2e94104ff8 | ||
|
|
039c13fc0a | ||
|
|
de266e8c88 | ||
|
|
ada99077a4 | ||
|
|
f5588f94db | ||
|
|
2a325ae4cc | ||
|
|
ac9345419e | ||
|
|
b3fb020dfa | ||
|
|
4b885a6e30 | ||
|
|
fa814d64de | ||
|
|
8aa376de25 | ||
|
|
14e7f2d986 | ||
|
|
f816f3d29b | ||
|
|
3db9987966 | ||
|
|
5e40bd5deb | ||
|
|
74a9ddbe47 | ||
|
|
188c2ae7ea | ||
|
|
e645c19761 | ||
|
|
baf9cab66c | ||
|
|
2fd940bcde | ||
|
|
f4988c5d86 | ||
|
|
b59b3a3a74 | ||
|
|
c677db70b4 | ||
|
|
9861f294b7 | ||
|
|
c7a42a1641 | ||
|
|
e2a721b209 | ||
|
|
66090e3926 | ||
|
|
60395fb16d | ||
|
|
54881facf8 | ||
|
|
9f2f131df9 | ||
|
|
70ab4122a3 | ||
|
|
7302fb951e | ||
|
|
dee8ccd8b3 | ||
|
|
40b2efb339 | ||
|
|
190e74b4d2 | ||
|
|
8bec08799e | ||
|
|
003b056301 | ||
|
|
97fd58f793 | ||
|
|
aa70d410f8 | ||
|
|
ab21a9238d | ||
|
|
be080b1463 | ||
|
|
f8ef3e6754 | ||
|
|
e4e8b939e2 | ||
|
|
020f122647 | ||
|
|
ab0626720a | ||
|
|
637c05466a | ||
|
|
08b98739f7 | ||
|
|
920a835344 | ||
|
|
88e27e315f | ||
|
|
7d3427474d | ||
|
|
d77183fd48 | ||
|
|
caa601c9aa | ||
|
|
b9a0152000 | ||
|
|
5c5e4e3464 | ||
|
|
044f987ecd | ||
|
|
295351d3e9 | ||
|
|
ab7ca411a4 | ||
|
|
c0dee497cd | ||
|
|
247e51478b | ||
|
|
a0540e0452 | ||
|
|
515d421dc0 | ||
|
|
6032f52798 | ||
|
|
05989fdc58 | ||
|
|
b62fafe520 | ||
|
|
4562a0130a | ||
|
|
6949481198 | ||
|
|
5654170278 | ||
|
|
735a324f5a | ||
|
|
2cb51f5b58 | ||
|
|
3baeef4f44 | ||
|
|
cada78ca4d | ||
|
|
a2929ef441 | ||
|
|
6c7c8b43b9 | ||
|
|
d0940708b0 | ||
|
|
af21f3c5d8 | ||
|
|
dd18040232 | ||
|
|
c72e3a1585 | ||
|
|
888a47e7a9 | ||
|
|
0df51d2d7c | ||
|
|
022945b0d1 | ||
|
|
0e62284671 | ||
|
|
704103f9f1 | ||
|
|
1f2ffe7e76 | ||
|
|
b763abdc76 | ||
|
|
af2fcbc059 | ||
|
|
0c4d278de2 | ||
|
|
9596d24cca | ||
|
|
a52b71ef43 | ||
|
|
216289455e | ||
|
|
019d8cf756 | ||
|
|
3a1c09f89c | ||
|
|
e9ed4a3f93 | ||
|
|
164d6b1b6c | ||
|
|
2307dfaaac | ||
|
|
3cecc1229f | ||
|
|
cca895e418 | ||
|
|
ddda934eed | ||
|
|
1d8c9ecfc4 | ||
|
|
1f20574520 | ||
|
|
da81bc3ba5 | ||
|
|
013fb58267 | ||
|
|
f4f2feae3c | ||
|
|
ffad6e47ec | ||
|
|
2b8f884c12 | ||
|
|
2df0d81d29 | ||
|
|
b02c4904a9 | ||
|
|
f73cdac9e2 | ||
|
|
de848511af | ||
|
|
9e294529cf | ||
|
|
96d6accbde | ||
|
|
86e276ea94 | ||
|
|
8566d62aee | ||
|
|
0ccc78e90a | ||
|
|
3c86207d63 | ||
|
|
c2a37c814d | ||
|
|
4727866b15 | ||
|
|
7a3c33fde3 | ||
|
|
087735a08f | ||
|
|
a3077d91ec | ||
|
|
649763f2b1 | ||
|
|
f9b988a232 | ||
|
|
ab45bcb121 | ||
|
|
e9190d01dc | ||
|
|
ba0e98fd4b | ||
|
|
40cc5b379b | ||
|
|
b1cb48aa9b | ||
|
|
82d9165d92 | ||
|
|
39d4ee9ad8 | ||
|
|
2cdd444e29 | ||
|
|
d492f0f41f | ||
|
|
943e085d37 | ||
|
|
0624f9103f | ||
|
|
cab1530afc | ||
|
|
90e63f1898 | ||
|
|
b2fdd92f9c | ||
|
|
91701b484a | ||
|
|
9fdae1ac45 | ||
|
|
b7766946d8 | ||
|
|
9486749345 | ||
|
|
a3bec4d5fc | ||
|
|
d8cc091f94 | ||
|
|
89ba4aeeaf | ||
|
|
a2f223a992 | ||
|
|
286efade59 | ||
|
|
4e7888259f | ||
|
|
8e3fa892e1 | ||
|
|
8de0c55b68 | ||
|
|
4c27d2600a | ||
|
|
59cfb14a19 | ||
|
|
6f7cf4f186 | ||
|
|
52454c50b7 | ||
|
|
c56219f5b4 | ||
|
|
99b6e1f93c | ||
|
|
6ff1549d2e | ||
|
|
1b05e8ddc9 | ||
|
|
4745877a63 | ||
|
|
c81c7f5a33 | ||
|
|
a23beddc66 | ||
|
|
045929681d | ||
|
|
cf0cf52298 | ||
|
|
1ff7c8bdd5 | ||
|
|
7254f766d5 | ||
|
|
83e07ec76c | ||
|
|
c3a1f25894 | ||
|
|
750d8b0a25 | ||
|
|
2e4558a5d3 | ||
|
|
3e5813323b | ||
|
|
ecd858251f | ||
|
|
ac0847ffe3 | ||
|
|
d1bf49790e | ||
|
|
815f24fbf5 | ||
|
|
d83352e16f | ||
|
|
8715ab9322 | ||
|
|
4df294f0a2 | ||
|
|
cbbe1b4f47 | ||
|
|
8ccd6a8ed3 | ||
|
|
cbd222fe30 | ||
|
|
1683567dda | ||
|
|
da2e40db4d | ||
|
|
e3c37f003b | ||
|
|
c9eebafb2d | ||
|
|
751da877ee | ||
|
|
14d19b5117 | ||
|
|
41d4c883a3 | ||
|
|
09504508d9 | ||
|
|
94c755a0e6 | ||
|
|
f21cff335b | ||
|
|
6947996c9a | ||
|
|
0756090081 | ||
|
|
95e03d2684 | ||
|
|
fab5833cdf | ||
|
|
a011c10d4d | ||
|
|
14584bef55 | ||
|
|
907a371bd4 | ||
|
|
a420c1749a | ||
|
|
c29e9bb68d | ||
|
|
fd965e6022 | ||
|
|
2e8d7a20d0 | ||
|
|
8084ed191e | ||
|
|
0b51e50c19 | ||
|
|
659e056eeb | ||
|
|
c70023c066 | ||
|
|
ba2bdf8ea4 | ||
|
|
57330bb2cd | ||
|
|
16fdef2d55 | ||
|
|
31928d5ebb | ||
|
|
8370b99302 | ||
|
|
13f98e1675 | ||
|
|
615804333e | ||
|
|
e1b9e113b1 | ||
|
|
dfbde3e41c | ||
|
|
034b4feff4 | ||
|
|
949439361e | ||
|
|
163b6b3725 | ||
|
|
bdc6addf81 | ||
|
|
fd7c1e24a3 | ||
|
|
a16ecbcebd | ||
|
|
0a1f6be4a5 | ||
|
|
98bf462e02 | ||
|
|
86fa59e5e1 | ||
|
|
1ddadeba5d | ||
|
|
2e1d5d4d13 | ||
|
|
31b29abf5f | ||
|
|
16c428dbd5 | ||
|
|
54bc6550ee | ||
|
|
a698cb6ef7 | ||
|
|
5f86ab27bb | ||
|
|
f0b2c08a9e | ||
|
|
3f8c705989 | ||
|
|
6f84574511 | ||
|
|
6a131977d3 | ||
|
|
4df0075b42 | ||
|
|
e3972c6205 | ||
|
|
4e949f542c | ||
|
|
af38fd3ba3 | ||
|
|
d00ac278c6 | ||
|
|
0e01303921 | ||
|
|
39eeaf06e5 | ||
|
|
7c2c4b26b3 | ||
|
|
2ea0703701 | ||
|
|
d425cdc27d | ||
|
|
799c833a66 | ||
|
|
96bc88d045 | ||
|
|
da7bbc2b20 | ||
|
|
7b4f6c3222 | ||
|
|
e3bc528647 | ||
|
|
29665203c0 | ||
|
|
7beb9afc36 | ||
|
|
2fcb3a0131 | ||
|
|
0a9749726f | ||
|
|
c1e6d9c633 | ||
|
|
4767d49e4d | ||
|
|
2bf7e8506c | ||
|
|
447966e3b8 | ||
|
|
adc17272dc | ||
|
|
6e54f34419 | ||
|
|
4b4051a823 | ||
|
|
5c4e230f69 | ||
|
|
2c191a9bd3 | ||
|
|
ed9313759b | ||
|
|
70a7a72baa | ||
|
|
4d69025250 | ||
|
|
9f9028b7ce | ||
|
|
e9be3242b2 | ||
|
|
6868b683d8 | ||
|
|
4934ec2ea1 | ||
|
|
211a06e9bc | ||
|
|
debca0a82d | ||
|
|
92da954229 | ||
|
|
91d96b162e | ||
|
|
0907bf195e | ||
|
|
f30b14a3da | ||
|
|
2030793a02 | ||
|
|
d09f561ef6 | ||
|
|
2d1bede57e | ||
|
|
a947ec10e4 | ||
|
|
fd04f94b76 | ||
|
|
6881446e67 | ||
|
|
bd0a9952ff | ||
|
|
c35255b82b | ||
|
|
5e71e7d04d | ||
|
|
cdf3c62d17 | ||
|
|
e7e2176e64 | ||
|
|
ecb9e92c48 | ||
|
|
053c2eb379 | ||
|
|
bde2810c08 | ||
|
|
692159219e | ||
|
|
71b194cbcb | ||
|
|
fb81c7e8e0 | ||
|
|
84878315d5 | ||
|
|
1bd675cce9 | ||
|
|
e77a0a9385 | ||
|
|
d63da05e87 | ||
|
|
dece58faf6 | ||
|
|
0bfc166899 | ||
|
|
3bc8006028 | ||
|
|
f6b7a35c81 | ||
|
|
5f17b28e05 | ||
|
|
138099f11d | ||
|
|
beb425bbd0 | ||
|
|
b5631aa1df | ||
|
|
2958881343 | ||
|
|
981e26e1b6 | ||
|
|
b5e11f2303 | ||
|
|
6a159aa631 | ||
|
|
4b14a4bc50 | ||
|
|
2ca4b15154 | ||
|
|
e02d602341 | ||
|
|
8d218db35f | ||
|
|
f96d78e9b8 | ||
|
|
28dd7e4ca7 | ||
|
|
14a0e6bd6e | ||
|
|
f2b2952909 | ||
|
|
adc82bea85 | ||
|
|
778adc2f39 | ||
|
|
a92895d09d | ||
|
|
dfb66cc41f | ||
|
|
065180e86c | ||
|
|
73cd7f7513 | ||
|
|
e2d5534959 | ||
|
|
a6cbdfc559 | ||
|
|
7c643ea698 | ||
|
|
39be3d0958 | ||
|
|
e693141f50 | ||
|
|
927dad4ff5 | ||
|
|
885caa52db | ||
|
|
88776b4e00 | ||
|
|
8b963f3fe4 | ||
|
|
4a4ac1d6c8 | ||
|
|
b5eef748b4 | ||
|
|
6ebe235dc3 | ||
|
|
bbde3159bb | ||
|
|
7e65aea6d9 | ||
|
|
e377675897 | ||
|
|
c3d40c9fb9 | ||
|
|
5efc61b155 | ||
|
|
323ab5498b | ||
|
|
b6e18e70bc | ||
|
|
1496d9d931 | ||
|
|
e0e35f5abf | ||
|
|
0b7e4d594e | ||
|
|
1bdf0b5500 | ||
|
|
acb6f1fc87 | ||
|
|
a57e8f3a5d | ||
|
|
4cb86b76e8 | ||
|
|
24f9511726 | ||
|
|
e4d160ee0b | ||
|
|
21d91193a2 | ||
|
|
4d7f925c5a | ||
|
|
ad5f3f2450 | ||
|
|
a84fc734ff | ||
|
|
3d5a5e4d86 | ||
|
|
2d14a74ec2 | ||
|
|
942a604b5b | ||
|
|
e5ea8074d2 | ||
|
|
2779c8e69f | ||
|
|
1d12014399 | ||
|
|
d4b26025ed | ||
|
|
597f177d5b |
12
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
# github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
# patreon: # Replace with a single Patreon username
|
||||
# open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: jadzia626
|
||||
# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
# community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
# liberapay: # Replace with a single Liberapay username
|
||||
# issuehunt: # Replace with a single IssueHunt username
|
||||
# otechie: # Replace with a single Otechie username
|
||||
# custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
6
.github/workflows/syntax.yml
vendored
@@ -2,16 +2,16 @@ name: flake8
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, dev ]
|
||||
branches: [ main, testing, dev ]
|
||||
pull_request:
|
||||
branches: [ main, dev ]
|
||||
branches: [ main, testing, dev ]
|
||||
|
||||
jobs:
|
||||
checkSyntax:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Python Setup
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3
|
||||
architecture: x64
|
||||
|
||||
4
.github/workflows/test_linux_3.6.yml
vendored
@@ -2,9 +2,9 @@ name: Linux (3.6)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, dev ]
|
||||
branches: [ main, testing, dev ]
|
||||
pull_request:
|
||||
branches: [ main, dev ]
|
||||
branches: [ main, testing, dev ]
|
||||
|
||||
jobs:
|
||||
testLinux36:
|
||||
|
||||
4
.github/workflows/test_linux_3.7.yml
vendored
@@ -2,9 +2,9 @@ name: Linux (3.7)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, dev ]
|
||||
branches: [ main, testing, dev ]
|
||||
pull_request:
|
||||
branches: [ main, dev ]
|
||||
branches: [ main, testing, dev ]
|
||||
|
||||
jobs:
|
||||
testLinux37:
|
||||
|
||||
4
.github/workflows/test_linux_3.8.yml
vendored
@@ -2,9 +2,9 @@ name: Linux (3.8)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, dev ]
|
||||
branches: [ main, testing, dev ]
|
||||
pull_request:
|
||||
branches: [ main, dev ]
|
||||
branches: [ main, testing, dev ]
|
||||
|
||||
jobs:
|
||||
testLinux38:
|
||||
|
||||
4
.github/workflows/test_linux_3.9.yml
vendored
@@ -2,9 +2,9 @@ name: Linux (3.9)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, dev ]
|
||||
branches: [ main, testing, dev ]
|
||||
pull_request:
|
||||
branches: [ main, dev ]
|
||||
branches: [ main, testing, dev ]
|
||||
|
||||
jobs:
|
||||
testLinux39:
|
||||
|
||||
9
.github/workflows/test_mac.yml
vendored
@@ -1,10 +1,10 @@
|
||||
name: macOS (3.8)
|
||||
name: macOS (3.9)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, dev ]
|
||||
branches: [ main, testing, dev ]
|
||||
pull_request:
|
||||
branches: [ main, dev ]
|
||||
branches: [ main, testing, dev ]
|
||||
|
||||
jobs:
|
||||
testMac38:
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
- name: Python Setup
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: 3.9
|
||||
architecture: x64
|
||||
- name: Install Packages
|
||||
run: |
|
||||
@@ -24,6 +24,7 @@ jobs:
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install pyobjc
|
||||
pip install pytest-timeout
|
||||
pip install pytest-cov
|
||||
pip install pytest-qt
|
||||
|
||||
8
.github/workflows/test_win.yml
vendored
@@ -1,10 +1,10 @@
|
||||
name: Windows (3.8)
|
||||
name: Windows (3.9)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, dev ]
|
||||
branches: [ main, testing, dev ]
|
||||
pull_request:
|
||||
branches: [ main, dev ]
|
||||
branches: [ main, testing, dev ]
|
||||
|
||||
jobs:
|
||||
testWin38:
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
- name: Python Setup
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: 3.9
|
||||
architecture: x64
|
||||
- name: Checkout Source
|
||||
uses: actions/checkout@v2
|
||||
|
||||
3
.gitignore
vendored
@@ -7,6 +7,8 @@
|
||||
*.egg-info
|
||||
setup.iss
|
||||
novelwriter.desktop
|
||||
i18n/*.qm
|
||||
i18n/*.qph
|
||||
|
||||
# Documentation
|
||||
/docs/build/
|
||||
@@ -23,6 +25,7 @@ __pycache__
|
||||
/sample/meta
|
||||
*.bak
|
||||
*.lock
|
||||
ToC.txt
|
||||
|
||||
# PyTest
|
||||
/prof/
|
||||
|
||||
571
CHANGELOG.md
@@ -1,4 +1,571 @@
|
||||
# novelWriter Change Log
|
||||
# novelWriter Changelog
|
||||
|
||||
## Version 1.2 [2021-03-14]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This release is mainly focused on the Build Novel Project tool. Completely new export classes have
|
||||
been written to support Open Document and Markdown exports. In addition, the way document layouts
|
||||
are handled have been automated a little to assist the user in keeping header levels and document
|
||||
layout flags in sync. The third new additoion is the ability to record and log idle time during a
|
||||
writing session to improve the writing statistics information as requested by several users.
|
||||
Finally, it is now possible to directly edit the project dictionary via a new, simple GUI dialog.
|
||||
|
||||
#### The Build Novel Project Tool
|
||||
|
||||
The main changes for this release are to the Build Novel Project tool. The Open Document export, as
|
||||
well as the Markdown export, is now handled entirely by code written for novelWriter. Previously,
|
||||
these export features depended on the underlying Qt library's save routines connected to the
|
||||
preview document shown in the build dialog. Using this method of export both meant that the content
|
||||
of the document was dependent on the preview being generated first, and it also meant that the
|
||||
exported document had limited support for novelWriter-specific features and custom formatting. The
|
||||
new export class should generate a much better result, especially for the Open Document formats.
|
||||
The Open Document standard is supported by Open Office, Libre Office, Google Docs, Microsoft Word,
|
||||
and probably a number of other applications too. The Markdown export hasn't changed a lot, but
|
||||
should be a slight improvement on the previous export feature.
|
||||
|
||||
These changes to the build tool also imply that the saving process is now independent of the
|
||||
content of the preview window, meaning you don't have to rebuild the preview before saving, which
|
||||
was previously the case. To make this more consistent, the PDF export option has been moved to the
|
||||
print button as it is actually a print-to-file feature under the hood, not technically a proper PDF
|
||||
export format. It is exactly the same as printing to file from the print preview dialog.
|
||||
|
||||
In addition to the changes to the export features, the Build Novel Project tool now also has
|
||||
controls for line height, which applies to all rich text export formats, and the option to replace
|
||||
unicode characters with appropriate html entities for html export.
|
||||
|
||||
#### Document Layout Automation
|
||||
|
||||
Among other changes in this release are a few improvements to the process of creating and changing
|
||||
documents. When a new document is first created, the header is generated from the assigned label
|
||||
and layout.
|
||||
|
||||
In addition, for some document layouts, when the user changes the header level of the first header
|
||||
of the document, the document layout setting is updated accordingly. This should reduce the need
|
||||
for the user to maintain two ways of assigning the role of a given document. This automation only
|
||||
applies to combinations of header level and current document layouts where there is no ambiguity.
|
||||
For instance, changing the header level in a "Scene" document from level 3 to 2 changes the
|
||||
document layout automatically to "Chapter". But changing the first header of a "Book" layout
|
||||
document from 1 to 2 does not change the document's layout as the "Book" layout is a generic
|
||||
document layout and it's perfectly reasonable for its first header to be a chapter header.
|
||||
|
||||
Keep in mind that novelWriter treats documents with layout "Book", "Chapter" and "Scene" exactly
|
||||
the same during exports. The distinction is only meant as a way to indicate the purpose of a
|
||||
document in the project tree. This new automation is meant to assist in keeping this information up
|
||||
to date. The other layouts do have an effect on formatting during export, and are generally left
|
||||
alone.
|
||||
|
||||
#### The Session Timer and Idle Time
|
||||
|
||||
Another change that has been requested by a couple of users is to have the session timer in the
|
||||
status bar stop counting when the user is inactive (idle). This feature is optional, and can be
|
||||
controlled from Preferences. The definition of "idle" in this context is either that the user is
|
||||
active in a different application than novelWriter (loss of focus) or that the user has not made
|
||||
any changes to the current document for a given amount of time. The time threshold is by default
|
||||
five minutes, but can be altered in Preferences.
|
||||
|
||||
In addition, the idle time is also recorded in the session log, and can be viewed in the Writing
|
||||
Statistics dialog and exported with the rest of the information. The idle time is recorder in the
|
||||
logs regardless of whether the status bar clock takes idle time into consideration or not. So even
|
||||
if you turn off the idle time switch in Preferences, the other idle time setting still affects the
|
||||
writing stats log entries.
|
||||
|
||||
#### Other Changes
|
||||
|
||||
The user dictionary of the project, where words added to the dictionary from the document editor
|
||||
go, can now be viewed and edited with a new "Project Word List" tool in the "Tools" menu.
|
||||
|
||||
A small additional feature added is also the ability to undo the last move of an item in the
|
||||
project tree. The keyboard shortcut for this is `Ctrl+Shift+Z`, or it can be accessed from the
|
||||
menu. The feature can only undo the last move, but it includes both documents moved to trash, moves
|
||||
by up/down keypress or menu entries, and drag and drop moves.
|
||||
|
||||
A new keyword has been added to mark characters in the story. The new keyword is intended to tag a
|
||||
character as the focus character for a chapter or scene. This is useful for stories where the
|
||||
point-of-view character and the focus character are different.
|
||||
|
||||
Lastly, two bugfixes have been made as well. The Empty Trash feature was no longer working due to
|
||||
an earlier fix solving another issue. The feature has now been restored. In addition, the indexer
|
||||
now checks that a keyword (tag or reference) is valid before saving it to the index. Previously, an
|
||||
invalid keyword could be saved to the index and potentially crash the application.
|
||||
|
||||
_These Release Notes also include the changes from 1.2 Beta 1 and RC 1._
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* Fixed an issue where a typo in a tag or reference using the `@` character would add an invalid
|
||||
entry into the project index. The invalid keyword would be saved to the index cache, invalidating
|
||||
the index on next load. For earlier versions of novelWriter before 1.1.1, it would also cause a
|
||||
crash. Invalid keywords are now rejected during indexing. Issue #688. PR #689.
|
||||
* The "Empty Trash" option was no longer working due to an earlier fix that added a requirement
|
||||
that the project tree has focus to allow the emptying to procede. Since the Empty Trash feature
|
||||
opens a dialog, the tree loses focus, and the deletions are therefore ignored. The focus check is
|
||||
no longer considered when emptying the trash. Issue #701. PR #702.
|
||||
|
||||
**Documentation**
|
||||
|
||||
* The documentation has been updated to reflect the changes in 1.2, and a few corrections pointed
|
||||
out by @jyhelle applied. PR #700.
|
||||
|
||||
----
|
||||
|
||||
## Version 1.2 RC 1 [2021-03-02]
|
||||
|
||||
### Release Notes
|
||||
|
||||
_The Release Notes have been moved and merged into the 1.2 notes._
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* If a tag or reference keyword was mistyped, it would still be indexed and put into the index.
|
||||
This caused the index to be deemed invalid on the next loading of the project, triggering a
|
||||
rebuild. A check has been added to the code parsing the lines starting with `@` to ensure only
|
||||
valid keywords are written into the index. Issue #688. PR #690.
|
||||
|
||||
**New Features**
|
||||
|
||||
* Added a tool to edit the project's user dictionary. This is the dictionary where the "Add Word to
|
||||
Dictionary" actions from the document editor go. The dialog tool allows for listing, removing and
|
||||
adding words to this dictionary. Issue #665. PR #669.
|
||||
|
||||
**User Interface**
|
||||
|
||||
* Pressing the `F2` key when in the document editor will open the Item Editor for the open document
|
||||
instead of the one selected in the project tree. PR #664.
|
||||
* The shortcut for deleting an item in the project tree has been changed from `Ctrl+Del` to
|
||||
`Ctrl+Shift+Del`. This change was also backported to version 1.1.1. Resolves #629. PR #664.
|
||||
|
||||
----
|
||||
|
||||
## Version 1.2 Beta 1 [2021-02-11]
|
||||
|
||||
### Release Notes
|
||||
|
||||
_The Release Notes have been moved and merged into the 1.2 RC 1 notes._
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**New Features**
|
||||
|
||||
* A full Open Document exporter has been written and added for the Build Novel Project tool. This
|
||||
replaces the previously used Qt Save to ODT which just saved the content of the preview window to
|
||||
an ODT file. The Qt pathway was very limited and didn't generate proper formatting classes. The
|
||||
new export class can generate both full `.odt` and flat XML `.fodt` files. The formatting support
|
||||
of this exporter is equivalent or better than the HTML5 exporter, which was previously the best
|
||||
supported option. Solves issue #611. PRs #607, #652, #660, and #654.
|
||||
* A full Markdown exporter has been written and added for the Build Novel Project tool. This too
|
||||
replaces a Qt feature that was used to save the content of the preview window into a markdown
|
||||
file. The exporter allows for both standard markdown and GitHub flavour markdown. The only
|
||||
relevant difference being that the latter allows strikethrough text. Issue #617. PR #650.
|
||||
* The Build Novel Project tool now has a "Line height" property that is applied to the preview, and
|
||||
to the HTML5 and Open Document export formats. Discussion #653. Issue #654. PR #660.
|
||||
* The Build Novel Project tool now has an option to convert Unicode characters to HTML entities on
|
||||
export to HTML5. Previously, some symbols were converted while others were not. This option
|
||||
provides a more consistent "all or nothing" option. PR #660.
|
||||
* The last file or folder move in the project tree can now be undone from the Project menu or by
|
||||
pressing `Ctrl+Shift+Z`. PR #632.
|
||||
* When a document header level is altered in a novel file of layout type Scene, Chapter,
|
||||
Unnumbered, or Partition, and that document is saved, the document's layout setting as seen in
|
||||
the project tree is updated to reflect the new level of that heading. This helps reduce the
|
||||
duplication of effort by the user to keep this information in sync. See discussion #613. This is
|
||||
an acceptable solution to #614. Issue #618. PR #620.
|
||||
* The session timer now records the amount of time the user is idle. Idle is defined to be when the
|
||||
application window does not have focus, and when the user hasn't made any changes to the document
|
||||
open in the editor for a specified amount of time. The default is 5 minutes. The idle time is
|
||||
recorded to the session log and can be shown in Writing Statistics. Optionally, the status bar
|
||||
session timer can also be set to pause when the user is considered idle. Issues #606 and #651.
|
||||
PRs #656 and #661.
|
||||
* It is now possible to tag a character as the focus character for a give section of text. This is
|
||||
useful for cases where the point-of-view character differs from the main character of the story,
|
||||
or the part of the story. Issue #605. PR #662.
|
||||
|
||||
**User Interface**
|
||||
|
||||
* A document's class and layout is now displayed next to its status or importance in the document
|
||||
editor footer bar. PR #628.
|
||||
* When a new document is created, the header of the file is automatically generated based on the
|
||||
document's tree label, and the level determined by the selected layout. Issue #530. PR #628.
|
||||
* On the Build Novel Project GUI, the PDF option has been moved from the "Save As" button to the
|
||||
"Print" button. This more accurately reflects what it actually does: print the content of the
|
||||
preview to a PDF using the printer pathway. This also means that all remaining items on the "Save
|
||||
As" dropdown list now can be executed regardless of the content of the preview window. They all
|
||||
run their own separate build process. This resolves #611. PR #650.
|
||||
* Export to plain text has been dropped. PR #617.
|
||||
|
||||
**Code Improvements**
|
||||
|
||||
* The index class now scans every element of the loaded index cache before accepting it. This means
|
||||
that any unrecognised content will trigger a full re-indexing. This is particularly useful when
|
||||
opening an index that was saved by a later release. Every entry that requires a lookup is checked
|
||||
to avoid potential key errors for instance. All values are also checked for correct data type.
|
||||
The new check is extensive, but still fast enough that it only adds a few milliseconds to the
|
||||
startup time. PR #619.
|
||||
|
||||
**Code Maintenance**
|
||||
|
||||
* Cleaned up some redundant code after PR #637. PR #638.
|
||||
|
||||
----
|
||||
|
||||
## Version 1.1.1 [2021-02-21]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This patch makes a couple of minor improvements to the GUI. The keyboard shortcut for deleting
|
||||
entries in the project tree has been changed from `Ctrl+Del` to `Ctrl+Shift+Del` to free up that
|
||||
shortcut for the document editor. It is now possible to use the shortcut for deleting the word in
|
||||
front of the cursor, a common and useful feature of many text editors.
|
||||
|
||||
The way the negative word count filter option works on the Writing Statistics tool has been changed
|
||||
to be more intuitive. Enabling this filter now appears to remove all negative entries without
|
||||
altering the other entries. Previously, the removed negative counts would be included in the
|
||||
following entries to make it consistent with the total word count. In addition, writing sessions
|
||||
shorter than five minutes, and with no change in the word count, are no longer recorded in the
|
||||
session log file.
|
||||
|
||||
Other changes include improving the speed of the internal spell checker, used when the Enchant
|
||||
spell check library isn't available. The internal spell checker is no longer significantly slower,
|
||||
but is still lacking in functionality compared to Enchant.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**User Interface**
|
||||
|
||||
* The default GUI font on Windows is now Arial. It works better with the Qt framework than the
|
||||
default system font. If Arial is missing, it falls back to the bundled font Cantarell. PR #655.
|
||||
* The way word changes are calculated on the Writing Statistics tool has changed when the option to
|
||||
exclude negative word counts is active. Previously, the entries with negative counts were
|
||||
filtered out, but the change in count was still applied to the next line, altering the value.
|
||||
Now, the GUI will instead just drop the lines that are negative and keep the other lines
|
||||
unchanged. This is more intutitive, but it also means that the total count now longer matches the
|
||||
sum of the lines. PR #659.
|
||||
* The keyboard shortcut for deleting entries in the project tree has been changed from `Ctrl+Del`
|
||||
to `Ctrl+Shift+Del`. The `Ctrl+Del` shortcut is thus free to be used exclusively by the editor to
|
||||
delete the word in front of the cursor. Having the same shortcut do different things depending on
|
||||
which area of the GUI has focus is a bit confusing. Related to #529. PR #666.
|
||||
|
||||
**Other Improvements**
|
||||
|
||||
* The internal spell checker, which is used when the Enchant library isn't available, has been
|
||||
given a significant speed improvement by caching the imported dictionary as a Python `set`
|
||||
instead of a `list`. The `set` has a hashed key lookup algorithm that is significantly faster.
|
||||
PR #668.
|
||||
* Sessions shortar than 5 minutes, and with no word count changes, are no longer recorded in the
|
||||
session stats log file. PR #685.
|
||||
|
||||
**Installation**
|
||||
|
||||
* The PyPi packages now include the `setup.py` file, which makes it possible to install icon
|
||||
launchers for novelWriter on both Linux and Windows after installing with `pip`. PR #655.
|
||||
|
||||
----
|
||||
|
||||
## Version 1.1 [2021-02-07]
|
||||
|
||||
### Release Notes
|
||||
|
||||
The main change in this release is the addition of a new tab to the project tree on the left side
|
||||
of the main window. The regular project tree is now on a tab named "Project", while a new tab named
|
||||
"Novel" displays a simpler version of the information on the main "Outline" page. It lists all the
|
||||
headers of the novel part of the project, as well as the word count and point-of-view character of
|
||||
each section. This is an alternative way to navigate the novel part of the project. The various
|
||||
tree views are now also kept better in sync when the user selects various documents and headers.
|
||||
|
||||
In addition, a new information dialog named "Project Details" has been added. It replaces the
|
||||
"Details" tab in "Project Settings", and adds more information about the novel part of the project.
|
||||
In particular, a "Table of Contents" in the "Contents" tab displays a summary of the main parts
|
||||
and chapters of the project, their total word counts, and an estimated page count. This was made in
|
||||
response to users asking for ways to estimate the total page count of the project. The page count
|
||||
is estimated based on a words per page setting, which can be changed on the dialog window.
|
||||
|
||||
Since the tabs below the project tree now add some extra room on the GUI, some convenient buttons
|
||||
have been added in the same area, with direct access to "Project Details", "Writing Statistics" and
|
||||
"Project Setting".
|
||||
|
||||
A few other minor changes have been made as well. The Preferences dialog has been improved with
|
||||
clearer categories and hopefully better help text. Some new options have been added too. They allow
|
||||
syntax highlighting of multi-paragraph quotes. The highlighter can now optionally accept quotes to
|
||||
be left "hanging", that is, no closing quote in the same paragraph.
|
||||
|
||||
_These Release Notes also include the changes from 1.1 RC 1._
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* A `None` check in the details panel below the project tree was missing, resulting in an
|
||||
occasional error message being printed to the logging output. The error was otherwise handled, so
|
||||
this is mainly a fix to prevent the error message. PR #639.
|
||||
|
||||
**User Interface**
|
||||
|
||||
* The word counts in the Novel tree are now updated each time a file is saved. Issue #636, PR #637.
|
||||
* A "Remove" button has been added to the "Open Project" dialog. Previously, recent project entries
|
||||
could be removed by pressing the `Del` key, but no obvious other methods were present on the GUI.
|
||||
PR #639.
|
||||
* When the search function is activated, the text in the search box is automatically selected.
|
||||
Issue #645, PR #639.
|
||||
|
||||
**Installation**
|
||||
|
||||
* The minimal zip release package tool in `setup.py` has been improved to generate tailored
|
||||
packages for each operating system. The old `pyinstaller` build command has been removed, but the
|
||||
manual build path for a Windows setup.exe file has been kept. PRs #643 and #644.
|
||||
|
||||
----
|
||||
|
||||
## Version 1.0.4 [2021-02-03]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This patch release fixes a couple of minor issues with the Preferences dialog and the behaviour of
|
||||
one of the keyboard shortcuts.
|
||||
|
||||
Aside from these fixes, the main point of this patch is to add new setup features for novelWriter
|
||||
on Windows. A Windows installer will no longer be provided for the foreseeable future, and instead
|
||||
functionality has been added to the main setup script to create desktop and start menu icons.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
* Fixed an issue with the Preferences dialog where the setting for justified text was mixed with
|
||||
the setting for fixed text width. This meant that the justified text setting could potentially
|
||||
get overwritten when the Preferences were changed and saved. Issue #623, PR #625.
|
||||
* Fixed an issue with the Open Project dialog where the list of recent projects would contain
|
||||
duplicate entries if the dialog was opened multiple times. PR #627.
|
||||
|
||||
**User Interface**
|
||||
|
||||
* The `Ctrl+Del` keyboard shortcut is now only active when the project tree has focus. Since this
|
||||
is also a common shortcut in many applications for deleting the next word ahead of the cursor,
|
||||
the activation of the delete file function when the editor has focus is unexpected to some users.
|
||||
Issue #629, PR #631.
|
||||
|
||||
**Installation**
|
||||
|
||||
* A new command has been added to the `setup.py` script. The new command, `win-install`, will
|
||||
create a desktop and start menu icon for novelWriter when run in the source folder. A windows
|
||||
batch file, `setup_windows.bat`, has also been added. Running this file from the source folder,
|
||||
either by command line or by double-click, will install dependencies from PyPi and set up the
|
||||
icons and file association with novelWriter project files. This should make it easier to run
|
||||
novelWriter from the source folder on Windows. PRs #634, #641 and #642.
|
||||
|
||||
**Documentation**
|
||||
|
||||
* The documentation on how to setup and install novelWriter has been extended and reorganised into
|
||||
one file per operating system. Some of the other documentation files have also been moved to a
|
||||
different section. PR #634.
|
||||
|
||||
----
|
||||
|
||||
## Version 1.1 RC 1 [2021-01-31]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This is a preview and test release for version 1.1.
|
||||
|
||||
A few new features have been added. The primary change is that the project tree on the left side of
|
||||
the main window now has two tabs. The regular project tree is now on a tab named "Project", while a
|
||||
new tab "Novel" displays a simpler version of the information on the main "Outline" page. It lists
|
||||
all the headers of the novel part of the project, as well as the word count and point-of-view
|
||||
character of each section. This is an alternative way to navigate the novel part of the project.
|
||||
The various tree views are now also kept better in sync when the user selects various documents and
|
||||
headers.
|
||||
|
||||
In addition, a new information dialog named "Project Details" has been added. It replaces the
|
||||
"Details" tab in "Project Settings", and adds more information about the novel part of the project.
|
||||
In particular, a "Table of Contents" in the "Contents" tab displays a summary of the main parts
|
||||
and chapters of the project, their total word counts, and an estimated page count. This was made in
|
||||
response to users asking for ways to estimate the total page count of the project. The page count
|
||||
is estimated based on the word count, and can be changed on the dialog window.
|
||||
|
||||
Since the tabs below the project tree now adds some extra room on the GUI, some convenient buttons
|
||||
have been added, with direct access to "Project Details", "Writing Statistics" and "Project
|
||||
Setting".
|
||||
|
||||
A few other minor changes have been made as well. The Preferences dialog has been improved with
|
||||
clearer categories and hopefully better help text. Some new options have been added too. They allow
|
||||
syntax highlighting of multi-paragraph quotes. The highlighter can now optionally accept quotes to
|
||||
be left "hanging", that is, no closing quote in the same paragraph.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**User Interface**
|
||||
|
||||
* Added a Novel tab under the project tree where the user can navigate the novel's layout of
|
||||
chapters and scenes, similar to the Outline view, but next to the document editor. The Outline
|
||||
view and Novel/Project trees now also behave more in cooperation. When files on one are selected
|
||||
or moved, the other will follow and update. Issues #541 and #185, PR #538.
|
||||
* Added a Project Details dialog that lists project details (moved from Project Settings' Details
|
||||
Tab) and a Table of Contents tab where details on chapter level is displayed. This table also
|
||||
shows an estimated page count and estimated page location of each chapter. Issue #528, PRs #555,
|
||||
#598 and #603.
|
||||
* Added three buttons below the project tree that connects to Project Details, Writing Statistics,
|
||||
and Project Settings. PR #555.
|
||||
* The settings and tabs in the Preferences dialog have been re-arranged into more tabs with less
|
||||
options on each tab. PRs #577 and #624.
|
||||
* Minor changes to margins and alignments of widgets on the main GUI. PR #565.
|
||||
* Added a keyboard shortcut to change focus to the Outline tab. The focus change now also ensures
|
||||
that the main GUI also switches to the tab where the focus is shifted. Issues #609 and #612, PR
|
||||
#615.
|
||||
* The cursor should now also be visible when opening a blank document and the editor has focus.
|
||||
Issue #608, PR #621.
|
||||
|
||||
**Text Editor**
|
||||
|
||||
* Added support for multi-paragraph quote (dialogue) highlighting. This feature is optional, and
|
||||
can be enabled/disabled in Preferences. Issue #546, PR #577.
|
||||
* Add several new symbols to the Insert menu/ Issue #602, PRs #603 and #604.
|
||||
|
||||
**Other Changes**
|
||||
|
||||
* Trigger a save document call before the Build Novel Project tool starts the build. This ensures
|
||||
that unsaved changes in the editor are included in the build. Issue #610, PR #616.
|
||||
|
||||
**Code Maintenance**
|
||||
|
||||
* Reformatting of source file headers and adding license headers to all test source files. Test
|
||||
source files are now also organised into subfolders. PR #563.
|
||||
|
||||
----
|
||||
|
||||
## Version 1.0.3 [2021-01-24]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This patch release fixes a minor bug sometimes encountered when running novelWriter from command
|
||||
line on Windows. In addition, the Solarized Dark and Solarized Light themes have been added to the
|
||||
selection of GUI and syntax themes by a user contribution.
|
||||
|
||||
The main change in this release is to the install scripts and the documentation related to
|
||||
installing and running novelWriter. The primary change is a different method of packaging the app
|
||||
for Windows. Instead of building an `.exe` file, the new setup instead builds a runnable zip file
|
||||
`.pyz`. The executable would often be mistakenly flagged by virus control software due to the
|
||||
packaging tools. This is a known problem with pyinstaller and similar tools, but such warnings are
|
||||
always concerning even if they are false positives.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
* Fix crash when starting novelWriter from command line on Windows from a different mounted drive
|
||||
than where it is installed. This was caused by a relative path lookup that defaulted to the wrong
|
||||
current directory. This works on Linux/macOS which have a common root path, but not on Windows.
|
||||
Issue #581, PR #587.
|
||||
|
||||
**User Interface**
|
||||
|
||||
* Added Solarized Dark and Solarized Light GUI and syntax themes. PR #578 by @nullbasis.
|
||||
* The Typewriter Scroll Mode now works better in combination with the Scroll Past End feature. The
|
||||
scroll mode still only works when there is actually any document to scroll into, but previously
|
||||
it would also not work until the total length of the document reached 40% of the height of the
|
||||
editor window. This was quite confusing. This limit is now reduced to 10%, which means that as
|
||||
long as the Scroll Past End option is enabled, the Typewriter Scroll will always work according
|
||||
to its settings. Issue #589, PR #593.
|
||||
|
||||
**Installation**
|
||||
|
||||
* Merged the `make.py` script into `setup.py`. PR #584.
|
||||
* Added a second way to build distributable packages of novelWriter for Windows. The new method
|
||||
does not use any of the current package tools that produce a Windows executable of the app. These
|
||||
packages tend to cause false virus warnings. This new method uses the Python tool `zipapp` to
|
||||
bundle novelWriter as an executable `.pyz` file, and adds Python embeddable and library
|
||||
dependencies into the same folder. The folder itself can be distributed as-is, or a Windows
|
||||
installer executable can be generated with `setup.py setup-pyz`. Issue #580, PR #584.
|
||||
|
||||
**Documentation**
|
||||
|
||||
* Updated documentation, main README and Contribution Guide to make them more consistent and to
|
||||
improve installation instructions. Based on issue #586 and input from @mgrhm. PR #592.
|
||||
|
||||
**Other Changes**
|
||||
|
||||
* The HTML generator now adds line breaks after `div` blocks used to wrap tag/reference lines. This
|
||||
makes the output easier to process by scripts, but has no impact on browser rendering and import
|
||||
into other applications. PR #597.
|
||||
|
||||
----
|
||||
|
||||
## Version 1.0.2 [2021-01-19]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This patch release fixes a few minor cosmetic issues, a minor issue with the indexer, and a bug
|
||||
when adding words to the user's own spell check dictionary. Additionally, the documentation has
|
||||
been updated based on user feedback, and some install issues resolved.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Installation**
|
||||
|
||||
* The dependency list was missing in the setup configuration for PyPi due to a bug in the
|
||||
`setup.cfg` file. The dependencies have been moved to a different section where the setup tool
|
||||
now picks them up properly. Issue #570, PR #573 by @stranger-danger-zamu.
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
* Fixed an issue with note files being moved between a non-novel root folder and a novel root
|
||||
folder without clearing its index entry in the former note or novel index. This would cause
|
||||
duplicate entries for such a file. PR #558.
|
||||
* Fixed a cosmetic issue where the meta data panel below the project tree was not cleared when the
|
||||
project was closed. PR #559.
|
||||
* Fixed an issue where the main window title would not be cleared when a project was closed, and
|
||||
the new title not set when a new project was first created. Issue #560, PR #561.
|
||||
* The editor context menu option to "Add Word to Dictionary" should also be visible when there are
|
||||
no spell checker suggestions. The entry was erroneously added under an if-condition that excluded
|
||||
it in those cases. Issue #574, PR #575.
|
||||
|
||||
**Documentation**
|
||||
|
||||
* Fixed some typos and spelling mistakes in the documentation, and reworded parts of the text that
|
||||
were unclear. The technical page has also been extended with more information on project folder
|
||||
structure. PR #557.
|
||||
* Clarify install instructions, and remove the duplicate instructions in the README file and
|
||||
replace them with a brief section. The full instructions are in the documentation. Issues #566
|
||||
and #570, PR #576.
|
||||
|
||||
----
|
||||
|
||||
## Version 1.0.1 [2021-01-10]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This release is mainly to bring the documentation up to date, as I forgot to update the install
|
||||
instructions in the original 1.0 release. I also forgot to change the various settings and help
|
||||
texts that describe novelWriter as under initial development (beta state).
|
||||
|
||||
Some minor improvements have been made to the "Edit Project Item" dialog and some restrictions on
|
||||
the settings available for documents created in the "Outtakes" folder relaxed. A few minor issues
|
||||
with the document and project changed icons on the status bar have also been resolved. The
|
||||
indicators were previously set to changed status even if no actual change had been made to the
|
||||
project.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**User Interface**
|
||||
|
||||
* Added the Outtakes folder to a list of root folders that will allow the setting of file layouts
|
||||
otherwise only permitted under the Novel root folder. It makes sense to permit the files in this
|
||||
folder to have the same extended settings that Novel files have. PR #552.
|
||||
* The text input and dropdown boxes of the Edit Project Item dialog box now extend when the dialog
|
||||
window is resized. Previously, the space between the label and the box would stretch instead,
|
||||
which isn't very useful. PR #552.
|
||||
* The document and project changed status icons on the status bar are now set to unchanged status
|
||||
when the project is opened. In addition, an issue with the status being set to changed on various
|
||||
events that were not actual changes to the document or project has been resolved. For instance,
|
||||
changing the size of the document editor would flag the document itself as changed. PR #554.
|
||||
|
||||
**Documentation**
|
||||
|
||||
* Updated the install instructions of the documentation and the main readme file, as well as the
|
||||
current development status as listed on PyPi. PRs #550 and #551.
|
||||
|
||||
----
|
||||
|
||||
## Version 1.0 [2021-01-03]
|
||||
|
||||
@@ -747,7 +1314,7 @@ planned by GitHub. See their [notes](https://github.com/github/renaming) for mor
|
||||
**Other Changes**
|
||||
|
||||
* The install scripts now try to create folders before copying icons. PR #364.
|
||||
* The manifest file now lists the root assets folder, so that it is included in the pypi build. PR
|
||||
* The manifest file now lists the root assets folder, so that it is included in the PyPi build. PR
|
||||
#364.
|
||||
* The .desktop template file has the correct categories set according to the FreeDesktop standard.
|
||||
PR #364.
|
||||
|
||||
@@ -1,43 +1,80 @@
|
||||
# Contributing
|
||||
# Contributing Guide
|
||||
|
||||
When contributing to this repository, please first discuss the change you wish to make via the
|
||||
issue tracker with the owner of this repository before making a change. If you just want to make a
|
||||
minor correction, like fix a typo or similar, feel free to just make a pull request directly.
|
||||
issue tracker or the discussions page with the owner of this repository before making a change. If
|
||||
you just want to make a minor correction, like fix a typo or similar, feel free to just make a pull
|
||||
request directly.
|
||||
|
||||
There is a code of conduct. Please follow it in all your interactions with the project.
|
||||
## Branching Structure
|
||||
|
||||
There are three protected branches on this repository. They are used in the following way:
|
||||
|
||||
* `main` – This is the Stable branch. It is used for releases and subsequent patches. No
|
||||
development code should be merged into this branch starting from version 1.0.
|
||||
* `dev` – This is the Unstable (development) branch. It is where new features are merged, and where
|
||||
pre-releases are taken from.
|
||||
* `testing` – This is the Testing branch. It is populated from the `dev` branch for pre-releases
|
||||
that need a longer testing phase.
|
||||
|
||||
Stable releases and patches will be tagged in the `main` branch, pre-releases may be tagged from
|
||||
either `testing` or `dev` branch.
|
||||
|
||||
### What Branch to Use for Contributions
|
||||
|
||||
* If your contribution is a fix for the latest stable release, branch from the `main` branch.
|
||||
* If your contribution is a fix for the latest testing release, branch from the `testing` branch.
|
||||
* If your contribution is a new feature, branch from the `dev` branch.
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
1. Make sure your code passes all tests and conforms to the style guide. You can check that the code
|
||||
conforms by running `flake8` from the root of the project folder.
|
||||
1. Make sure your code passes all tests and conforms to the style guide. You can check that the
|
||||
code conforms by running `flake8` from the root of the project folder.
|
||||
2. Please provide a complete description of the changes in the pull request, and a summary that can
|
||||
be copied into the [CHANGELOG](CHANGELOG.md). Remember to reference any issue related by
|
||||
providing the issue number.
|
||||
3. Do not change the version number unless asked to do so. Version numbers are bumped in separate
|
||||
release pull requests by the maintainer.
|
||||
3. Do not change the version number. Version numbers are bumped in separate release pull requests
|
||||
by the maintainer.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
There is a code of conduct. Please follow it in all your interactions with the project.
|
||||
|
||||
### Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers
|
||||
pledge to making participation in our project and our community a harassment-free experience for
|
||||
everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level
|
||||
of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
everyone, regardless of age, body size, disability, ethnicity, gender identity and expression,
|
||||
level of experience, nationality, personal appearance, race, religion, or sexual identity and
|
||||
orientation.
|
||||
|
||||
Please see the [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) file for the full text.
|
||||
|
||||
## Code Style Guide
|
||||
|
||||
The source code of novelWriter broadly follows the [PEP8](https://www.python.org/dev/peps/pep-0008/)
|
||||
style guide , but with a few modifications and exceptions.
|
||||
The source code of novelWriter broadly follows the [PEP8](https://www.python.org/dev/peps/pep-0008)
|
||||
style guide, but with a few modifications and exceptions listed below.
|
||||
|
||||
### Linting with flake8
|
||||
### Line Length
|
||||
|
||||
An excellent tool for checking Python code for errors and coding style is `flake8`.
|
||||
The documentation is available [here](https://flake8.pycqa.org/en/latest/).
|
||||
For this project, source lines should stay within the 79 and 99 character limits described by PEP8.
|
||||
79 characters is often too restrictive, so 99 character lines are acceptable when that is more
|
||||
practical. Readability has priority. Generally, if a code statement requires multiple lines, the
|
||||
lines should wrap at 79 characters, not 99. If wrapping can be avoided by going to 99, then that is
|
||||
generally preferrable.
|
||||
|
||||
The `setup.cfg` file in the root of this project has the following settings:
|
||||
For text files, the text should also be wrapped at 99 character. The exception is markdown image
|
||||
tags and urls.
|
||||
|
||||
Please do not submit pull requests that re-wrap existing source or text unless this has been
|
||||
discussed beforehand.
|
||||
|
||||
### Linting with `flake8`
|
||||
|
||||
An excellent tool for checking Python code for errors and coding style is `flake8`. The
|
||||
documentation is available [here](https://flake8.pycqa.org/en/latest/).
|
||||
|
||||
The `setup.cfg` file in the root of this project has the following settings for `flake8` that
|
||||
matches the coding standard:
|
||||
```conf
|
||||
[flake8]
|
||||
ignore = E203,E221,E226,E228,E241,E251,E261,E266,E302,E305
|
||||
@@ -57,20 +94,20 @@ not conform to the standard.
|
||||
|
||||
## Ignored Errors
|
||||
|
||||
Some errors are ignored in novelWriter, for various reasons. In addition, novelWriter uses camelCase
|
||||
function and variable names due to this being the standard for the Qt libraries, and also because of
|
||||
the author's personal preferences.
|
||||
Some `flake8` error codes are ignored for this project for various reasons. The source also uses
|
||||
camelCase function and variable names. This is the standard for the Qt libraries novelWriter
|
||||
integrates with. It also happens to be the author's personal preferences.
|
||||
|
||||
The reason behind the other ignored error codes are listed below. Many of them are due to PEP8 not
|
||||
permitting column alignment as opposed to many other coding styles. I find them useful in regions of
|
||||
bulk value assignments. There's a reason why tables are more readable than lists. They should be
|
||||
used sparingly though.
|
||||
permitting column alignment as opposed to many other coding styles, like for instance for Go. I
|
||||
find them useful in regions of bulk value assignments. There's a reason why tables are more
|
||||
readable than lists. They should be used sparingly though.
|
||||
|
||||
The ignored errors are all `pycodestyle` errors, and they are documented
|
||||
[here](https://pycodestyle.pycqa.org/en/latest/intro.html#error-codes).
|
||||
|
||||
**E203:** whitespace before ‘:’
|
||||
**Reason:** Column alignment. It is natural to align dictionary columns along the `:` character.
|
||||
**Reason:** Column alignment.
|
||||
|
||||
**E221:** multiple spaces before operator
|
||||
**Reason:** Column alignment.
|
||||
@@ -78,7 +115,7 @@ The ignored errors are all `pycodestyle` errors, and they are documented
|
||||
**E226:** missing whitespace around arithmetic operator
|
||||
**Reason:** This doesn't actually follow the PEP8 recommendation of grouping longer equations by
|
||||
operator precedence like `2*a + 3*b` instead of `a * a + 3 * b`. Generally, don't use spaces around
|
||||
`*`, `/` and `**`, but do use spaces around `+` and `-`. For appending strings, the spaces can be
|
||||
`*`, `/` and `**`, but _do_ use spaces around `+` and `-`. For appending strings, the spaces can be
|
||||
dropped. Don't use the `+` operator for appending multiple strings. Use formatting instead.
|
||||
|
||||
**E228** missing whitespace around modulo operator
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
include setup.py
|
||||
include LICENSE.md
|
||||
include CHANGELOG.md
|
||||
recursive-include setup *
|
||||
recursive-include nw/assets *
|
||||
recursive-include sample *.nwx *.nwd
|
||||
|
||||
334
README.md
@@ -4,8 +4,8 @@
|
||||
[/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions)
|
||||
[/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions)
|
||||
[/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions)
|
||||
[/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions)
|
||||
[/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions)
|
||||
[/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions)
|
||||
[/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions)
|
||||
[](https://github.com/vkbo/novelWriter/actions)
|
||||
[](https://codecov.io/gh/vkbo/novelWriter)
|
||||
[](https://novelwriter.readthedocs.io/en/latest/?badge=latest)
|
||||
@@ -15,178 +15,44 @@
|
||||
|
||||
<img align="left" style="margin: 0 16px 4px 0;" src="https://raw.githubusercontent.com/vkbo/novelWriter/main/setup/icons/scaled/icon-novelwriter-96.png">
|
||||
|
||||
novelWriter is a Markdown-like text editor designed for writing novels and larger projects of many
|
||||
smaller plain text documents. It uses its own flavour of Markdown that supports a meta data syntax
|
||||
for comments, synopsis, and cross-referencing between files. It's designed to be a simple text
|
||||
editor that allows for easy organisation of text files and notes, built on plain text files for
|
||||
robustness.
|
||||
novelWriter is a Markdown-like text editor designed for writing novels assembled from many smaller
|
||||
text documents. It uses a minimal formatting syntax inspired by Markdown, and adds a meta data
|
||||
syntax for comments, synopsis, and cross-referencing between files. It's designed to be a simple
|
||||
text editor that allows for easy organisation of text files and notes, built on plain text files
|
||||
for robustness.
|
||||
|
||||
The plain text storage is suitable for version control software, and also well suited for file
|
||||
synchronisation tools. The core project structure is stored in a project XML file. Other meta data
|
||||
is primarily saved in JSON files.
|
||||
The plain text files are suitable for version control software, and also well suited for file
|
||||
synchronisation tools. The core project structure is stored in a single project XML file. Other
|
||||
meta data is primarily saved in JSON files.
|
||||
|
||||
The full documentation is available at
|
||||
[novelwriter.readthedocs.io](https://novelwriter.readthedocs.io/).
|
||||
|
||||
The contributing guide is available in [CONTRIBUTING](CONTRIBUTING.md).
|
||||
|
||||
|
||||
### Development Status
|
||||
|
||||
The application is still under initial development, but all core features have now been added.
|
||||
The core functionality has been in place for a while, and novelWriter is being used for writing
|
||||
projects by the author and collaborators.
|
||||
|
||||
No new major features will be added at this time, until the application is stable. Until then,
|
||||
novelWriter is in a _beta_ state. Please report any issues you encounter via the repository's issue
|
||||
tracker.
|
||||
|
||||
You should be able to use novelWriter for real projects, but as with all software, please make
|
||||
regular backups of your data. There is a built in backup feature that can pack the entire project
|
||||
into a zip file each time the main window or the project is closed. Please check the documentation
|
||||
for further details.
|
||||
|
||||
[novelwriter.readthedocs.io](https://novelwriter.readthedocs.io).
|
||||
|
||||
## Implementation
|
||||
|
||||
The application is written in Python 3 using Qt5 via PyQt5. It is developed on Linux, but it should
|
||||
in principle work fine on other operating systems as well as long as dependencies are met. The unit
|
||||
tests are run on the latest versions of Ubuntu Linux, Windows Server and macOS.
|
||||
in principle work fine on other operating systems as well, as long as dependencies are met. The
|
||||
unit tests are run on the latest versions of Ubuntu Linux, Windows Server and macOS.
|
||||
|
||||
## Project Contributions
|
||||
|
||||
# Installing and Running
|
||||
|
||||
novelWriter is available on [pypi.org](https://pypi.org/project/novelWriter/), and can be installed
|
||||
with:
|
||||
```bash
|
||||
pip install novelwriter
|
||||
```
|
||||
|
||||
To upgrade an existing installation, use:
|
||||
```bash
|
||||
pip install --upgrade novelwriter
|
||||
```
|
||||
|
||||
Dependencies are installed automatically, but can generally be installed with:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Below are some brief instructions on how to get started on different operating systems.
|
||||
|
||||
|
||||
## Linux
|
||||
|
||||
Either download the source, or install with pip.
|
||||
|
||||
If you run from source, install the dependencies via pip, or directly from the OS repo. There are
|
||||
very few dependencies, and they should be available in the standard repo. The Python packages
|
||||
needed are `pyqt5`, `lxml` and `pyenchant`.
|
||||
|
||||
### Installing from Source
|
||||
|
||||
You can also install novelWriter from source with:
|
||||
```bash
|
||||
python3 setup.py sample
|
||||
sudo python3 setup.py install
|
||||
sudo python3 setup.py launcher
|
||||
```
|
||||
|
||||
The last line will install the application icons and set up a launcher for novelWriter. The method
|
||||
uses hardcoded paths, so it may or may not work for your Linux distro. If you have any issues,
|
||||
please submit a ticket so the script can be tuned.
|
||||
|
||||
The script may prompt you to choose which executable to configure if it finds more than one.
|
||||
|
||||
### Running from Source
|
||||
|
||||
If you want to run directly from the source, the application can be started with:
|
||||
```bash
|
||||
./novelWriter.py
|
||||
```
|
||||
|
||||
You can also create a launcher for running directly from source with:
|
||||
```bash
|
||||
python3 setup.py xdg-install
|
||||
```
|
||||
This will install the launcher and icons for the current user. To install them system-wide, run the
|
||||
above command with `sudo` or as root.
|
||||
|
||||
For more install options, see [Build and Install novelWriter](setup/README.md).
|
||||
|
||||
|
||||
## macOS
|
||||
|
||||
These instructions assume you're using brew, and have Python and pip set up. If not, see the
|
||||
[brew docs](https://docs.brew.sh/Homebrew-and-Python) for help.
|
||||
|
||||
Main requirements are installed via the requirements file. You also need to install the `pyobjc`
|
||||
package on macOS, so you must run:
|
||||
```bash
|
||||
pip3 install --user -r requirements.txt
|
||||
pip3 install --user pyobjc
|
||||
```
|
||||
|
||||
For spell checking you may also need to install the enchant package. It comes with a lot of default
|
||||
dictionaries.
|
||||
```bash
|
||||
brew install enchant
|
||||
```
|
||||
|
||||
## Windows
|
||||
|
||||
On Windows, you may first need to install Python. See the [python.org](https://www.python.org/)
|
||||
website for download packages. It is recommended that you install the latest version of Python 3.8.
|
||||
|
||||
To install dependencies, run:
|
||||
```bash
|
||||
pip install --user -r requirements.txt
|
||||
```
|
||||
|
||||
**Note:** On Windows, make sure Python3 is in your PATH if you want to launch novelWriter from
|
||||
command line. You can also right click the `novelWriter.py` file, create a shortcut, then right
|
||||
click again, select "Properties" and change the target to your python executable and
|
||||
`novelWriter.py`.
|
||||
|
||||
It should look something like this:
|
||||
```
|
||||
C:\...\AppData\Local\Programs\Python\Python38\python.exe novelWriter.py
|
||||
```
|
||||
|
||||
You can also run the `make.py` script to generate a single executable, or an installer.
|
||||
See [Build and Install novelWriter](setup/README.md) for more details.
|
||||
|
||||
|
||||
## Package Versions
|
||||
|
||||
Exporting to Markdown requires PyQt/Qt 5.14. There are no known minimum for `lxml`, but the code
|
||||
was originally written with 4.2. The optional spell check library must be at least 3.0.0 to work
|
||||
with Windows 64 bit systems. On Linux, 2.0.0 also works fine.
|
||||
|
||||
If no external spell checking tool is installed, novelWriter will use a basic spell checker based
|
||||
on standard Python package `difflib`. Currently, only English dictionaries are available for this
|
||||
spell checker, but more can be added to the `nw/assets/dict` folder. See the
|
||||
[README](nw/assets/dict/README.md) file in that folder for how to generate more dictionaries. Note
|
||||
that the difflib-based option is both slow and limited.
|
||||
|
||||
## Debugging
|
||||
|
||||
If you need to debug novelWriter, you must run it from command line. It takes a few parameters,
|
||||
which can be listed with the switch `--help`. The `--info`, `--debug` or `--verbose` flags are
|
||||
particularly useful for increasing logging output for debugging.
|
||||
|
||||
Contributions to this project are welcome. However, please read the
|
||||
[Contributing Guide](https://github.com/vkbo/novelWriter/blob/main/CONTRIBUTING.md) before
|
||||
submitting larger additions or changes to this project.
|
||||
|
||||
# Key Features
|
||||
|
||||
Some features of novelWriter are listed below. Consult the documentation for more information.
|
||||
Some features of novelWriter are listed below. Consult the
|
||||
[documentation](https://novelwriter.readthedocs.io) for more information.
|
||||
|
||||
### Markdown Flavour
|
||||
|
||||
novelWriter is _not_ a full-feature Markdown editor. It allows for a minimal set of formatting
|
||||
needed for writing text documents for novels. These are currently limited to:
|
||||
novelWriter is _not_ a full-feature Markdown editor. It is a plain text editor that uses
|
||||
Markdown-like syntax to allow for a minimal set of formatting that is useful for the specific task
|
||||
of writing novels. The formatting is currently limited to:
|
||||
|
||||
* Headings level 1 to 4 using the `#` syntax only.
|
||||
* Emphasised and strong text. These are rendered as italicised and bold.
|
||||
* Headings levels 1 to 4 using the `#` syntax only.
|
||||
* Emphasised and strongly emphasised text. These are rendered as italicised and bold text.
|
||||
* Strikethrough text.
|
||||
* Hard line breaks using two or more spaces at the end of a line.
|
||||
|
||||
@@ -195,45 +61,40 @@ That is it. Features not supported in the editor are also not exported when usin
|
||||
In addition, novelWriter adds the following, which is otherwise not supported by Markdown:
|
||||
|
||||
* A line starting with `%` is treated as a comment and not rendered on exports unless requested.
|
||||
Comments do not count towards the word count. If the first word of the comment is `synopsis:`,
|
||||
the comment is indexed and treated as the synopsis for the section of text under the same header.
|
||||
These synopsis comments can be used to build an outline and exported to external documents.
|
||||
Comments do not count towards the word count.
|
||||
* If the first word of the comment is `synopsis:`, the comment is indexed and treated as the
|
||||
synopsis for the section of text under the same header. These synopsis comments can be used to
|
||||
build an outline and exported to external documents.
|
||||
* A set of meta data keyword/values starting with the character `@`. This is used for tagging
|
||||
and inter-linking documents, and can be used to generate a project outline.
|
||||
* Non-breaking spaces are supported as long as your system is using at least Qt 5.9. For earlier
|
||||
version, non-breaking spaces are converted to normal spaces when saving the document. This is
|
||||
done by the Qt library.
|
||||
* Thin spaces are also supported, as well as non-breaking thin spaces, with the same library
|
||||
version restriction as above.
|
||||
* Tabs can be used in the text, and should be properly aligned. The width of a tab in pixels can be
|
||||
changed in Preferences. Note that for the HTML format, most browsers will treat a tab as a space,
|
||||
so it may not show up like expected. If you import the HTML file to Libre Office, for instance,
|
||||
they should appear as expected.
|
||||
and inter-linking documents, and can also be included when generate a project outline.
|
||||
* A variety of thin and non-breaking spaces are supported. Some of them depend on the system
|
||||
running at least Qt 5.9. Earlier versions of Qt will unfortunately strip them out when saving.
|
||||
* Tabs can be used in the text, and should be properly aligned in both editor and viewer. This can
|
||||
be used to make simple tables and lists. Full Markdown tables and lists are not supported. Note
|
||||
that for HTML exports, most browsers will treat a tab as a space, so it may not show up like
|
||||
expected. Open Document exports should produce the expected result.
|
||||
|
||||
The core export format of novelWriter is HTML5. You can also export the entire project as a single
|
||||
novelWriter Markdown-flavour document. In addition, other exports to Open Document, PDF, and plain
|
||||
text is offered through the Qt library, although with limitations to formatting.
|
||||
|
||||
The HTML format is well suited for file conversion tools and import into other text editors.
|
||||
The core export formats of novelWriter are Open Document and HTML5. Open Document is an open
|
||||
standard for office type documents that is supported by most office applications. See
|
||||
[Open Document > Application Support](https://en.wikipedia.org/wiki/OpenDocument#Application_support)
|
||||
for more details.
|
||||
|
||||
You can also export the entire project as a single novelWriter Markdown-flavour document. These can
|
||||
later be imported again into novelWriter. In addition, printing and export to PDF is offered
|
||||
through the Qt library, although with limitations to formatting.
|
||||
|
||||
### Colour Themes
|
||||
|
||||
The editor has syntax highlighting for the features it supports, and includes a set of different
|
||||
syntax highlighting themes. The GUI also has an optional dark theme in addition to the default
|
||||
system theme.
|
||||
|
||||
New themes can easily be added to the `nw/assets/themes` folder. Have a look in the existing
|
||||
folders for examples of how to define the colours.
|
||||
|
||||
syntax highlighting themes. Optional GUI themes are also available, including dark themes.
|
||||
|
||||
### Easy Organising of Project Files
|
||||
|
||||
The structure of the project is shown on the left hand side of the main GUI. Project files are
|
||||
The structure of the project is shown on the left hand side of the main window. Project files are
|
||||
organised into root folders, indicating what class of file they are. The most important root folder
|
||||
is the Novel folder, which contains all of the files that makes up the finished novel. Each root
|
||||
folder can have subfolders. Folders have no impact on the final project structure, they are purely
|
||||
tools for organising the files in whatever way the user needs.
|
||||
is the `Novel` folder, which contains all of the files that make up the finished novel. Each root
|
||||
folder can have subfolders. Subfolders have no impact on the final project structure, they are
|
||||
there for you to organise your files in whatever way you want.
|
||||
|
||||
The editor supports four levels of headings, which determines what level the following text belongs
|
||||
to. Headings of level one signify a book or partition title. Headings of level two signify the
|
||||
@@ -242,15 +103,14 @@ four can be used internally in each scene to separate sections.
|
||||
|
||||
Each novel file can be assigned a layout format, which shows up as a flag next to the item in the
|
||||
project tree. These are mostly to help the user track what they contain, but they also have some
|
||||
impact on the format of the exported document. See the documentation for further details.
|
||||
impact on the format of the exported document. See the
|
||||
[documentation](https://novelwriter.readthedocs.io) for further details.
|
||||
|
||||
|
||||
#### Project Notes
|
||||
### Project Notes
|
||||
|
||||
Supporting note files can be added for the story plot, characters, locations, story timeline, etc.
|
||||
These have their separate root folders. These are optional files.
|
||||
|
||||
|
||||
### Visualisation of Story Elements
|
||||
|
||||
The different notes can be assigned tags, which other files can refer back to using the `@` meta
|
||||
@@ -259,16 +119,101 @@ connects to the plot, and which characters, etc. occur in them. In addition, the
|
||||
are clickable in the document view pane, and control-clickable in the editor. They make it possible
|
||||
to quickly navigate between the documents while editing.
|
||||
|
||||
# Installing and Running
|
||||
|
||||
## Licenses
|
||||
For install instructions, please check the
|
||||
[Getting Started](https://novelwriter.readthedocs.io/en/latest/int_started.html) section in
|
||||
the [documentation](https://novelwriter.readthedocs.io/).
|
||||
|
||||
## TLDR Instructions
|
||||
|
||||
If you want to run novelWriter directly from the source code, you must run the `novelWriter.py`
|
||||
file from command line. For installations on Linux, macOS or Windows, see below.
|
||||
|
||||
**Note:** You may need to replace `python` with `python3` and `pip` with `pip3` in the instructions
|
||||
below on some systems. You may also want to add the `--user` flag for `pip` to install in your user
|
||||
space only.
|
||||
|
||||
### Install from PyPi
|
||||
|
||||
novelWriter is available on [pypi.org](https://pypi.org/project/novelWriter/), and can be installed
|
||||
with:
|
||||
```bash
|
||||
pip install novelwriter
|
||||
```
|
||||
Dependencies should be installed automatically, but can also be installed directly with:
|
||||
```bash
|
||||
pip install pyqt5 lxml pyenchant
|
||||
```
|
||||
When installing via pip, novelWriter can be launched from command line with:
|
||||
```bash
|
||||
novelWriter
|
||||
```
|
||||
|
||||
Make sure the install location for pip is in your PATH variable. This is not always the case by
|
||||
default.
|
||||
|
||||
### Setup on Linux
|
||||
|
||||
If you're installing from source, the following commands will set up novelWriter on Linux:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
python setup.py install
|
||||
python setup.py xdg-install
|
||||
```
|
||||
|
||||
This should make novelWriter available as a regular application on your system, with a launceher
|
||||
icon, and file association with novelWriter project files.
|
||||
|
||||
**Note:** If you don't want to install the dependencies from pip and instead use your distro's own
|
||||
packages, you must manually install `python3-pyqt5`, `python3-lxml` and `python3-enchant`
|
||||
(Debian/Ubuntu), or the corresponding packages for your distro.
|
||||
|
||||
### Setup on macOS
|
||||
|
||||
If you're installing from source, the following commands will set up novelWriter on macOS:
|
||||
```bash
|
||||
brew install enchant
|
||||
pip3 install --user -r requirements.txt
|
||||
pip3 install --user pyobjc
|
||||
python3 setup.py install
|
||||
```
|
||||
|
||||
At present, novelWriter isn't further integrated into the OS, so you must launch it from command
|
||||
line with:
|
||||
```bash
|
||||
novelWriter
|
||||
```
|
||||
|
||||
If you want to help improve the setup process on macOS, it would be much appreciated.
|
||||
|
||||
### Setup on Windows
|
||||
|
||||
For Windows, first ensure that you have Python installed. If not, get it from
|
||||
[python.org/downloads](https://www.python.org/downloads/). Remember to select "Add Python to PATH"
|
||||
during the installation, otherwise novelWriter will not start.
|
||||
|
||||
Then, download the `novelWriter-x.y.z-minimal-win.zip` file, where `x.y.z` is the version number,
|
||||
from the [releases](https://github.com/vkbo/novelWriter/releases) page. You can extract it to
|
||||
wherever you want to keep novelWriter on your PC, and run the `setup_windows.bat` file in it
|
||||
(double-clicking it should work). This will install the necessary dependencies from
|
||||
[pypi.org](https://pypi.org/) and create desktop and start menu icons.
|
||||
|
||||
## Debugging
|
||||
|
||||
If you need to debug novelWriter, you must run it from command line. It takes a few parameters,
|
||||
which can be listed with the switch `--help`. The `--info`, `--debug` or `--verbose` flags are
|
||||
particularly useful for increasing logging output for debugging.
|
||||
|
||||
# Licenses
|
||||
|
||||
This is Open Source software, and novelWriter is licensed under GPLv3. See the
|
||||
[GNU General Public License website](https://www.gnu.org/licenses/gpl-3.0.en.html) for more
|
||||
details, or consult the [LICENSE](LICENSE.md) file.
|
||||
details, or consult the [LICENSE](https://github.com/vkbo/novelWriter/blob/main/LICENSE.md) file.
|
||||
|
||||
Bundled assets have the following licenses:
|
||||
|
||||
* The Typicon-based icon themes by Stephen Hutchings are licensed under
|
||||
* The Typicons-based icon themes by Stephen Hutchings are licensed under
|
||||
[CC BY-SA 4.0](http://creativecommons.org/licenses/by-sa/4.0/). The icons have been altered in
|
||||
size and colour for use with novelWriter, and some additional icons added. The original icon set
|
||||
is available at
|
||||
@@ -286,8 +231,7 @@ Bundled assets have the following licenses:
|
||||
the main repo is available at
|
||||
[sdras/night-owl-vscode-theme](https://github.com/sdras/night-owl-vscode-theme).
|
||||
|
||||
|
||||
## Screenshot
|
||||
# Screenshot
|
||||
|
||||
**novelWriter with default system theme:**
|
||||

|
||||
|
||||
@@ -21,3 +21,7 @@
|
||||
white-space: normal !important;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
pre span.go {
|
||||
color: #333333 !important;
|
||||
}
|
||||
|
||||
@@ -25,9 +25,9 @@ copyright = "2018–2021, Veronica Berglyd Olsen"
|
||||
author = "Veronica Berglyd Olsen"
|
||||
|
||||
# The short X.Y version
|
||||
version = "1.0"
|
||||
version = "1.2"
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = "1.0"
|
||||
release = "1.2"
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
@@ -41,8 +41,8 @@ meta data is primarily saved as JSON files.
|
||||
Any operating system that can run Python 3 and has the Qt 5 libraries should be able to run
|
||||
novelWriter. It runs fine on Linux, Windows and macOS, and users have tested it on other platforms
|
||||
too. novelWriter can be run directly from the Python source, or installed from the pip tool or via
|
||||
the provided setup script. A setup.exe is provided for Windows. See :ref:`a_started` for further
|
||||
details.
|
||||
the provided setup script. See :ref:`a_started`, or one or the setup instructions for your
|
||||
operating system for further details.
|
||||
|
||||
**Useful Links**
|
||||
|
||||
@@ -57,22 +57,25 @@ details.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: First Steps
|
||||
:caption: Introduction
|
||||
|
||||
int_introduction
|
||||
int_started
|
||||
int_interface
|
||||
int_typography
|
||||
setup_linux
|
||||
setup_mac
|
||||
setup_windows
|
||||
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Writing Novels
|
||||
:caption: Usage
|
||||
|
||||
write_projects
|
||||
write_structure
|
||||
write_notes
|
||||
write_export
|
||||
usage_interface
|
||||
usage_projects
|
||||
usage_structure
|
||||
usage_notes
|
||||
usage_export
|
||||
usage_typography
|
||||
|
||||
|
||||
.. toctree::
|
||||
|
||||
@@ -7,20 +7,20 @@ Introduction
|
||||
novelWriter is a simple, multi-document plain text editor using a modified markdown syntax to apply
|
||||
simple formatting to the text. It is designed for writing novels, and allows for the component
|
||||
documents to be ordered freely to create the desired structure of the novel. More details about how
|
||||
projects are structured is covered on the :ref:`a_struct` page.
|
||||
projects are structured is covered in :ref:`a_struct`.
|
||||
|
||||
In addition, the project can contain notes on the various plot elements, characters, locations,
|
||||
etc, that make up the story. These notes are organised in a set of category-specific top-level
|
||||
folders (root folders), and each entry can be tagged and cross-referenced from within the novel
|
||||
documents and notes. These tags make it possible to inter-link documents, and generate an overview
|
||||
of the entire novel project and how the various documents and plot elements are interconnected.
|
||||
This is covered on the :ref:`a_proj` and :ref:`a_notes` pages.
|
||||
folders referred to as "root" folders, and each entry can be tagged and cross-referenced from
|
||||
within the novel documents and notes. These tags make it possible to inter-link documents, and
|
||||
generate an overview of the entire novel project and how the various documents and plot elements
|
||||
are interconnected. The tag and reference syntax is covered in :ref:`a_proj` and :ref:`a_notes`.
|
||||
|
||||
These additional features are not standard in markdown, but are available through special meta
|
||||
keywords described in :ref:`a_struct_tags`. Syntax highlighting is provided to make it easier to
|
||||
verify that the markdown tags are used correctly.
|
||||
|
||||
An overview of the supported markdown syntax is covered on the :ref:`a_ui` page.
|
||||
An overview of the supported markdown syntax is covered in :ref:`a_ui`.
|
||||
|
||||
|
||||
.. _a_intro_design:
|
||||
@@ -36,30 +36,37 @@ at the same time provide a complete set of features needed for writing a novel.
|
||||
links, tables, and other complex structures and objects often needed for such documents.
|
||||
Formatting is limited to headers, and bold, italicised and strikethrough text.
|
||||
|
||||
The main window does not have a toolbar like most other applications do. This reduces clutter, and
|
||||
.. tip::
|
||||
If you do need to align information in rows and columns in your notes, you can achieve this with
|
||||
tabs and hard line breaks. The tab stop width can be specified in :guilabel:`Preferences` and
|
||||
hard line breaks can be inserted by adding to spaces at the end of the line.
|
||||
|
||||
The main window does not have a toolbar like many other applications do. This reduces clutter, and
|
||||
since the documents are formatted with markdown tags, is more or less redundant. However, all
|
||||
formatting features supported are available through convenient keyboard shortcuts. They are also
|
||||
available in the main menu. A full list of shortcuts can be found in the :ref:`a_ui_shortcuts`
|
||||
section.
|
||||
available in the main menu so you don't have to look up the syntax every time until you've
|
||||
memorised it. A full list of shortcuts can be found in the :ref:`a_ui_shortcuts` section.
|
||||
|
||||
In addition, novelWriter offers a :guilabel:`Distraction Free Mode` where all the user interface
|
||||
elements other than the document editor itself are hidden away.
|
||||
In addition, novelWriter has a :guilabel:`Focus Mode` where all the user interface elements other
|
||||
than the document editor itself are hidden away.
|
||||
|
||||
The colour scheme of the user interface defaults to that of the host operating system. In addition,
|
||||
a dark theme is provided, and can be enabled in :guilabel:`Preferences` from the :guilabel:`Tools`
|
||||
menu. A number of syntax highlighting themes are also available in :guilabel:`Preferences`. A set
|
||||
of icon themes in colour and greyscale are also offered. The icons are based on the Typicons_ icon
|
||||
set designed by Stephen Hutchings.
|
||||
other light and dark themes are provided, and can be enabled in :guilabel:`Preferences` from the
|
||||
:guilabel:`Tools` menu. A number of syntax highlighting themes are also available in
|
||||
:guilabel:`Preferences`. A set of icon themes in colour and greyscale are also offered. The icons
|
||||
are based on the Typicons_ icon set designed by Stephen Hutchings.
|
||||
|
||||
The main window is split in two, or optionally three, panels. The left-most contains the project
|
||||
tree and all the documents in your project. The second panel is the document editor, and the
|
||||
optional third panel is a document viewer which can view any document in your project.
|
||||
The main window is split in two, or optionally three, panels. The left-most panel contains the
|
||||
project tree and all the documents in your project. The second panel is the document editor, and
|
||||
the optional third panel is a document viewer which can view any document in your project
|
||||
independently of the document editor. It is not intended as a preview window, although you can use
|
||||
it for this, but for viewing your notes next to your editor while you're writing.
|
||||
|
||||
A second tab is also available on the main window. This is the :guilabel:`Outline` tab where the
|
||||
entire novel structure can be displayed, with all the tags and references listed. Depending on how
|
||||
you structure your novel documents, this outline can be quite different from your project tree.
|
||||
Your project tree lists individual documents, your Outline tree lists the structure of the novel
|
||||
itself as it appears in the text of the documents.
|
||||
itself in terms of partitions, chapters and scenes as it appears in the text of the documents.
|
||||
|
||||
.. _Typicons: https://github.com/stephenhutchings/typicons.font
|
||||
|
||||
@@ -69,7 +76,7 @@ itself as it appears in the text of the documents.
|
||||
Project Layout
|
||||
==============
|
||||
|
||||
You are free to structure your project documents as you wish in subfolders, and split the text
|
||||
You are free to organise your project documents as you wish into subfolders, and split the text
|
||||
between documents in whatever way suits you. All that matters to novelWriter is the linear order
|
||||
the documents appear at in the project tree (top to bottom). The chapters, scenes and sections of
|
||||
the novel are determined by the headings within those documents.
|
||||
@@ -83,7 +90,7 @@ The four heading levels (**H1** to **H4**) are treated as follows:
|
||||
|
||||
This header level structure is only taken into account for novel documents. For the project notes,
|
||||
the header levels have no structural meaning, and the user is free to do whatever they want. See
|
||||
the :ref:`a_struct` and :ref:`a_notes` pages for more details.
|
||||
:ref:`a_struct` and :ref:`a_notes` for more details.
|
||||
|
||||
|
||||
.. _a_intro_export:
|
||||
@@ -92,13 +99,12 @@ Project Export
|
||||
==============
|
||||
|
||||
The project can at any time be exported to a range of different formats through the
|
||||
:guilabel:`Build Novel Project` tool. Natively, novelWriter supports export to plain text file,
|
||||
HTML document, novelWriter flavoured markdown, standard markdown (requires Qt 5.14), and to a basic
|
||||
Open Document format.
|
||||
:guilabel:`Build Novel Project` tool. Natively, novelWriter supports export to Open Document,
|
||||
HTML5, and various flavours of Markdown.
|
||||
|
||||
In addition, printing and printing to PDF is also possible. The best supported export format is
|
||||
HTML, which can be imported or converted by a number of other tools like Pandoc, or simply imported
|
||||
into Libre Office Writer and similar word processors.
|
||||
The HTML5 export format is suitable for conversion by a number of other tools like Pandoc, or for
|
||||
importing into word processors if the Open Document format isn't suitable. In addition, printing
|
||||
and printing to PDF is also possible.
|
||||
|
||||
It is also possible to export the content of the project to a JSON file. This is useful if you want
|
||||
to write your own processing script in for instance Python as the entire novel can be read into a
|
||||
@@ -106,8 +112,7 @@ Python dictionary with a couple of lines of code.
|
||||
|
||||
A number of filter options can be applied to the produced document, allowing you to export a draft
|
||||
manuscript, a reference document of notes, an outline based on chapter and scene titles with a
|
||||
synopsis each, and so on. See the :ref:`a_export` page for more details on export features and
|
||||
formats.
|
||||
synopsis each, and so on. See :ref:`a_export` for more details on export features and formats.
|
||||
|
||||
|
||||
.. _a_intro_screenshots:
|
||||
|
||||
@@ -4,69 +4,28 @@
|
||||
Getting Started
|
||||
***************
|
||||
|
||||
This is a brief guide to how you can get novelWriter running on your computer. These are the
|
||||
methods currently supported by the developer. Packages may also be available in other package
|
||||
managers, but those are not managed by the developer. A Windows installer file is also provided on
|
||||
the GitHub page and main website.
|
||||
|
||||
As novelWriter matures, more options for how to install it and get it running will be added. For
|
||||
non-Windows users the install process is at the present time best suited for people used to working
|
||||
with the command line.
|
||||
|
||||
.. note::
|
||||
The text below assumes the command ``python`` corresponds to a Python 3 executable. For
|
||||
operating systems with both Python 2 and 3, the command ``python3`` may be needed instead. On
|
||||
Linux, the scripts can also be made executable and run without the ``python`` command.
|
||||
|
||||
|
||||
.. _a_started_install:
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
You can download the latest version of novelWriter from the source repository on GitHub_. You can
|
||||
also install it directly from PyPi with ``pip install novelwriter``, or download the packages
|
||||
directly from the PyPi_ project page.
|
||||
|
||||
Latest version of novelWriter is |release|.
|
||||
|
||||
.. _main website: https://novelwriter.io
|
||||
.. _GitHub: https://github.com/vkbo/novelWriter/releases
|
||||
.. _PyPi: https://pypi.org/project/novelWriter/
|
||||
|
||||
This section contains brief guides to how you can get novelWriter running on your computer. These
|
||||
are the methods currently supported by the developer. Packages may also be available in other
|
||||
package managers, but those are not managed by the developer. No installers are provided at this
|
||||
time, but it is fairly straightforward to set up novelWriter with the provided install scripts.
|
||||
|
||||
.. _a_started_install_win:
|
||||
As novelWriter matures, more options for how to install it and get it running will be added. For
|
||||
non-Windows users the install process is at the present time best suited for people used to working
|
||||
with the command line. But even if you're not, the install process is fairly straightforward.
|
||||
|
||||
Windows Installer
|
||||
-----------------
|
||||
The next pages have specific install instructions for the various operating systems novelWriter can
|
||||
run on. The instructions below are supplementary information, instructions for alterbative methods,
|
||||
and additional build options.
|
||||
|
||||
You can run novelWriter directly from source on Windows, but a Windows installer is also provided
|
||||
for 64-bit Windows on the `main website`_ and GitHub_ page. This installer bundles all that is
|
||||
needed for novelWriter to run, including Python and the xml and Qt libraries.
|
||||
|
||||
.. _main website: https://novelwriter.io
|
||||
|
||||
|
||||
.. _a_started_install_source:
|
||||
|
||||
Install from Source on Linux
|
||||
----------------------------
|
||||
|
||||
For Linux systems, novelWriter can be installed from source with the provided ``setup.py`` script.
|
||||
To install novelWriter into the system's default Python install locations, run:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
python setup.py install
|
||||
|
||||
To set up the novelWriter desktop launcher, the icons and the project file association, run:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
python setup.py xdg-install
|
||||
|
||||
By default, these commands installs novelWriter and its icons for the current user only. To install
|
||||
for all users, run the script with the ``sudo`` command. Other options are also available. Run
|
||||
``python setup.py help`` for a full list of install options.
|
||||
.. note::
|
||||
The text below assumes the command ``python`` corresponds to a Python 3 executable. Python 2 is
|
||||
now deprecated, but many systems still have both Python 2 and 3. For such systems, the command
|
||||
``python3`` may be needed instead. On Linux, the scripts can also be made executable and run
|
||||
without the ``python`` command. Likewise, ``pip`` may need to be replaced with ``pip3``.
|
||||
|
||||
|
||||
.. _a_started_depend:
|
||||
@@ -74,61 +33,80 @@ for all users, run the script with the ``sudo`` command. Other options are also
|
||||
Dependencies
|
||||
============
|
||||
|
||||
novelWriter has been designed to rely on as few dependencies as possible. Aside from the package(s)
|
||||
novelWriter has been designed to rely on as few dependencies as possible. Aside from the packages
|
||||
needed to communicate with the Qt GUI libraries, only one package is required for handling the XML
|
||||
format of the main project file. Everything else is handled with standard Python libraries.
|
||||
|
||||
The following Python packages are needed to run novelWriter:
|
||||
|
||||
* ``pyqt5`` – needed for connecting with the Qt5 libraries.
|
||||
* ``lxml`` – needed for full XML support.
|
||||
* ``pyenchant`` – needed for efficient spell checking (optional).
|
||||
|
||||
PyQt/Qt should be at least 5.3, but ideally 5.10 or higher for nearly all features to work. For
|
||||
instance, searching using regular expressions with full Unicode support requires 5.13. There is no
|
||||
known minimum version requirement for package ``lxml``, but the code was originally written with
|
||||
4.2, which is therefore set as the minimum. It may work on lower versions. You have to test it.
|
||||
|
||||
Optionally, a package can be installed to interface with the Enchant spell checking libaries, but
|
||||
this isn't strictly required. If no external spell checking library is available, novelWriter falls
|
||||
back to using the internal ``difflib`` of Python to check spelling. This is a much slower approach,
|
||||
and it is less sophisticated than full spell checking libaries, but if you only work with small
|
||||
files, the performance loss is not noticeable.
|
||||
back to using the internal ``difflib`` of Python to check spelling. This is a slower and less
|
||||
sophisticated spell checker than the full spell checking libaries. The spell check library must be
|
||||
at least 3.0 to work with Windows. On Linux, 2.0 also works fine.
|
||||
|
||||
|
||||
.. _a_started_depend_packages:
|
||||
|
||||
Package Installation
|
||||
--------------------
|
||||
|
||||
If you already have Python installed, all you need to do is install the dependencies. To do this,
|
||||
you need to open your command line tool, find the folder where you extracted novelWriter, and run
|
||||
the following command:
|
||||
If you install from PyPi, these dependencies should be installed automatically. If you install from
|
||||
source, dependencies can still be installed from PyPi with:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
This will install all the dependencies and recommended packages.
|
||||
|
||||
The following Python packages are required to run novelWriter:
|
||||
.. _a_started_install:
|
||||
|
||||
* ``pyqt5``, needed for connecting with the Qt5 libraries.
|
||||
* ``lxml``, needed full XML support.
|
||||
Installing via PyPi
|
||||
===================
|
||||
|
||||
You can of course also install these packages from your operating system's package repository.
|
||||
The application is written in Python 3 using Qt5 via PyQt5. It is developed on Linux, but it should
|
||||
in principle work fine on other operating systems as long as dependencies are met.
|
||||
|
||||
PyQt/Qt should be at least 5.2.1, but ideally 5.10 or higher for nearly all features to work.
|
||||
Exporting to standard Markdown, for instance, requires PyQt/Qt 5.14. Searching using regular
|
||||
expressions requires 5.3, and for full Unicode support, 5.13. There is no known minimum version
|
||||
requirement for package ``lxml``, but the code was originally written with 4.2, which is therefore
|
||||
set as the minimum. It may work on lower versions. You have to test it.
|
||||
You can download the latest version of novelWriter from the source repository on GitHub_.
|
||||
novelWriter is also hosted on PyPi_, and can be installed on all operating systems that support Qt5
|
||||
and Python 3. It is regularly tested on Linux, Windows and macOS. The latest version of novelWriter
|
||||
is |release|.
|
||||
|
||||
The spell checking extension is optional, but recommended:
|
||||
To install from PyPi you must first have the ``python`` and ``pip`` commands available on your
|
||||
system. If you don't, see specific instructions for your operating system in this documentation.
|
||||
To install novelWriter from PyPi, use the following command:
|
||||
|
||||
* ``pyenchant``, needed for efficient spell checking.
|
||||
.. code-block:: console
|
||||
|
||||
The optional spell check library must be at least 3.0 to work with Windows. On Linux, 2.0 also
|
||||
works fine.
|
||||
pip install novelwriter
|
||||
|
||||
To upgrade an existing installation, use:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
pip install --upgrade novelwriter
|
||||
|
||||
When installing via pip, novelWriter can be launched from command line with:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
novelWriter
|
||||
|
||||
Make sure the install location for pip is in your PATH variable. This is not always the case by
|
||||
default.
|
||||
|
||||
|
||||
.. _a_started_depend_docs:
|
||||
.. _a_started_docs:
|
||||
|
||||
Building the Documentation
|
||||
--------------------------
|
||||
==========================
|
||||
|
||||
If you installed novelWriter from a package, the documentation should be included. If you're
|
||||
running novelWriter from the source code, a local copy of this documentation can be generated. It
|
||||
requires the following Python packages on Debian and Ubuntu.
|
||||
If you installed novelWriter from a package, the documentation should be bre-built and included. If
|
||||
you're running novelWriter from the source code, a local copy of this documentation can be
|
||||
generated. It requires the following Python packages on Debian and Ubuntu.
|
||||
|
||||
* ``python3-sphinx``
|
||||
* ``python3-sphinxcontrib.qthelp``
|
||||
@@ -145,7 +123,8 @@ The documentation can then be built from the ``docs`` folder in the source code
|
||||
|
||||
make html
|
||||
|
||||
If successful, the documentation should then be available in the ``docs/build/html`` folder.
|
||||
If successful, the documentation should be available in the ``docs/build/html`` folder and you can
|
||||
open the ``index.html`` file in your browser.
|
||||
|
||||
The documentation can also be built for the Qt Assistant. To build the help packages from the
|
||||
documentation source, run the following from the root source folder:
|
||||
@@ -163,54 +142,3 @@ locally first, then send you to the website as a fallback.
|
||||
In order for the local version of help to work, the Qt Assistant must be installed on the local
|
||||
computer. If it isn't available, or novelWriter cannot find it, the help feature will fall back
|
||||
to redirecting you to the documentation website.
|
||||
|
||||
|
||||
.. _a_started_running:
|
||||
|
||||
Running from Source
|
||||
===================
|
||||
|
||||
If all the required dependencies are met, you can run novelWriter from the command line:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
python novelWriter.py
|
||||
|
||||
A few switches are supported from the command line, mostly to assist in debugging if an error is
|
||||
encountered. To list all options, run:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
python novelWriter.py --help
|
||||
|
||||
|
||||
.. _a_started_standalone:
|
||||
|
||||
Building a Standalone Executable
|
||||
================================
|
||||
|
||||
A standalone executable can be built with ``pyinstaller``, using the provided python script
|
||||
``make.py`` in the source folder. This script can install dependencies, build a standalone
|
||||
executable of novelWriter, or build a ``setup.exe`` file with Inno Setup.
|
||||
|
||||
For a full list of the script's options, run ``python make.py help``.
|
||||
|
||||
|
||||
.. _a_started_standalone_win:
|
||||
|
||||
Additional Instructions for Windows
|
||||
-----------------------------------
|
||||
|
||||
If you don't have Python installed, you can download it from the python.org website. The installers
|
||||
for Windows are available at https://www.python.org/downloads/windows/
|
||||
|
||||
novelWriter should work with Python 3.6 or higher, and the executable installer is the easiest to
|
||||
install.
|
||||
|
||||
Also, make sure you select the "Add Python to PATH" option.
|
||||
|
||||
.. image:: images/python_win_install.png
|
||||
:width: 600
|
||||
|
||||
Once Python is set up and running, you can either run novelWriter from the folder where you
|
||||
extracted it, or you can build an executable and run that from a desktop icon instead.
|
||||
|
||||
75
docs/source/setup_linux.rst
Normal file
@@ -0,0 +1,75 @@
|
||||
.. _a_setup_linux:
|
||||
|
||||
**************
|
||||
Setup on Linux
|
||||
**************
|
||||
|
||||
.. _GitHub: https://github.com/vkbo/novelWriter/releases
|
||||
.. _main website: https://novelwriter.io
|
||||
|
||||
This is a brief guide to how you can get novelWriter running on a Linux computer. There are
|
||||
currently no install package of novelWriter for Linux, so it is recommended that you download
|
||||
either the full source or minimal package and extract it to a practical location on your system and
|
||||
run the ``setup.py`` script.
|
||||
|
||||
|
||||
Running from Source
|
||||
===================
|
||||
|
||||
To run novelWriter from source, download the latest source package from the release page on
|
||||
GitHub_ or the `main website`_, or if you have git running on your computer, you can also clone the
|
||||
repository.
|
||||
|
||||
|
||||
Step 1: Installing Dependencies
|
||||
-------------------------------
|
||||
|
||||
The dependencies of novelWriter are generally available from Linux distro repositories. For Debian
|
||||
and Ubuntu, they can be installed with:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
sudo apt install python3-pyqt5 python3-lxml python3-enchant
|
||||
|
||||
If you prefer to install dependencies via PyPi, or the repository dependencies are out of date, you
|
||||
can install them with:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
pip3 install --user -r requirements.txt
|
||||
|
||||
|
||||
Step 2: Install Package (Optional)
|
||||
----------------------------------
|
||||
|
||||
You can install novelWriter to the correct location for Python packages with:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
./setup.py install
|
||||
|
||||
This is equivalent to what the ``pip`` installer does. It puts novelWriter in the location on your
|
||||
system where Python packages are usually kept. This is not really the best suited location for a
|
||||
GUI application like novelWriter, so you may instead copy the entire source to a suiteable location
|
||||
yourself.
|
||||
|
||||
This should install novelWriter to either ``~/.local/bin/novelWriter`` if installed for local user
|
||||
only, or to ``/usr/local/bin/novelWriter`` if installed for all users (with the ``sudo`` command).
|
||||
|
||||
|
||||
Step 3: Create Launcher Icons
|
||||
-----------------------------
|
||||
|
||||
Regardless of where you extract or install the source files, you can easily set up a standard icon
|
||||
and launcher. To set up this desktop launcher, the needed icons, and the project file association,
|
||||
run:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
./setup.py xdg-install
|
||||
|
||||
By default, these commands install novelWriter and its icons for the current user only. To install
|
||||
for all users, run the script with the ``sudo`` command.
|
||||
|
||||
.. tip::
|
||||
All options of the setup script can be listed with: ``./setup.py help``.
|
||||
67
docs/source/setup_mac.rst
Normal file
@@ -0,0 +1,67 @@
|
||||
.. _a_setup_mac:
|
||||
|
||||
**************
|
||||
Setup on macOS
|
||||
**************
|
||||
|
||||
.. _GitHub: https://github.com/vkbo/novelWriter/releases
|
||||
.. _main website: https://novelwriter.io
|
||||
.. _brew docs: https://docs.brew.sh/Homebrew-and-Python
|
||||
|
||||
This is a brief guide to how you can get novelWriter running on macOS. There are currently no
|
||||
install package of novelWriter for macOS, so it is recommended that you download either the full
|
||||
source or minimal package and extract it to a practical location on your system and
|
||||
run it.
|
||||
|
||||
|
||||
Running from Source
|
||||
===================
|
||||
|
||||
To run novelWriter from source, download the latest source package from the release page on
|
||||
GitHub_ or the `main website`_, or if you have git running on your computer, you can also clone the
|
||||
repository.
|
||||
|
||||
|
||||
Step 1: Installing Dependencies
|
||||
-------------------------------
|
||||
|
||||
These instructions assume you're using brew, and have Python and pip set up. If not, see the
|
||||
`brew docs`_ for help. Main requirements are installed via the requirements file. You also need to
|
||||
install the ``pyobjc`` package, so you should run:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
pip3 install --user -r requirements.txt
|
||||
pip3 install --user pyobjc
|
||||
|
||||
For spell checking you may also need to install the enchant package. It comes with a lot of default
|
||||
dictionaries.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
brew install enchant
|
||||
|
||||
With the dependencies in place, you can launch the ``novelWriter.py`` script directly to run
|
||||
novelWriter.
|
||||
|
||||
|
||||
Step 2: Install Package (Optional)
|
||||
----------------------------------
|
||||
|
||||
You can install novelWriter to the correct location for Python packages with:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
./setup.py install
|
||||
|
||||
This is equivalent to what the ``pip`` installer does. It puts novelWriter in the location on your
|
||||
system where Python packages are usually kept. This is not really the best suited location for a
|
||||
GUI application like novelWriter, so you may instead copy the entire source to a suiteable location
|
||||
yourself.
|
||||
|
||||
After this, you should be able to launch novelWriter by running ``novelWriter`` in a command line
|
||||
window.
|
||||
|
||||
.. note::
|
||||
Right now there isn't a better integration with macOS available. Contributions from someone more
|
||||
familiar with macOS would be very much appreciated.
|
||||
110
docs/source/setup_windows.rst
Normal file
@@ -0,0 +1,110 @@
|
||||
.. _a_setup_win:
|
||||
|
||||
****************
|
||||
Setup on Windows
|
||||
****************
|
||||
|
||||
This is a brief guide to how you can get novelWriter running on a Windows computer.
|
||||
|
||||
Unlike other operating systems, Windows does not come prepared with an environment to run Python
|
||||
applications, so you must first install Python. After that, running novelWriter is straightforward.
|
||||
|
||||
.. tip::
|
||||
If you have any problems, you can always open a question on the project's discussions_ page.
|
||||
This requires a GitHub account.
|
||||
|
||||
.. _discussions: https://github.com/vkbo/novelWriter/discussions
|
||||
|
||||
|
||||
.. _a_setup_win_installer:
|
||||
|
||||
Using the Installer
|
||||
===================
|
||||
|
||||
The installer for Windows is no longer provided. See the `installer issue`_ for more info on why.
|
||||
You can still create the installer yourself if you want to. It can be generated with the provided
|
||||
``setup.py`` script. use the script's ``help`` command to get further instructions.
|
||||
|
||||
Please use the "Running from Source" option below instead. It has been improved and a script has
|
||||
been added that does nearly the same thing.
|
||||
|
||||
.. _installer issue: https://github.com/vkbo/novelWriter/issues/640
|
||||
|
||||
|
||||
.. _a_setup_win_source:
|
||||
|
||||
Running from Source
|
||||
===================
|
||||
|
||||
To run novelWriter from source, download the latest source zip file from the release page on
|
||||
GitHub_, or from the `main website`_. You can download the "Minimal Windows" zip file. It is a
|
||||
minimal version of the source that contains only the files needed for running on Windows. You can
|
||||
also download the full source if you wish.
|
||||
|
||||
In order to make novelWriter run on your system, you must first have Python installed (Step 1).
|
||||
Thereafter, a script will do the rest of the job (Step 2). Alternatively, you can run this step
|
||||
manually from the command prompt if you wish to.
|
||||
|
||||
.. tip::
|
||||
If your system already has all the dependencies installed, you can run novelWriter directly from
|
||||
the extracted folder by double-clicking the ``novelWriter.pyw`` file. However, running the
|
||||
script also adds start icons.
|
||||
|
||||
.. _GitHub: https://github.com/vkbo/novelWriter/releases
|
||||
.. _main website: https://novelwriter.io
|
||||
|
||||
|
||||
Step 1: Installing Python
|
||||
-------------------------
|
||||
|
||||
If you already have Python installed, you can skip this step. If you don't have it installed, you
|
||||
can download it from the python.org_ website. novelWriter should work with Python 3.6 or higher,
|
||||
but it is recommended that you install the latest version of Python.
|
||||
|
||||
Make sure you select the "Add Python to PATH" option during installation, otherwise the ``python``
|
||||
command will not work in the command line window.
|
||||
|
||||
.. image:: images/python_win_install.png
|
||||
:width: 600
|
||||
|
||||
.. _python.org: https://www.python.org/downloads/windows
|
||||
|
||||
|
||||
Step 2: Dependencies and Icons
|
||||
------------------------------
|
||||
|
||||
**Alternative A: By Script**
|
||||
|
||||
Open the folder where you extracted the novelWriter source, and double-click the file named
|
||||
``setup_windows.bat``. This should open a command line window and run the setup script to install
|
||||
dependencies, and add desktop and start menu icons.
|
||||
|
||||
The script will also check that it can find Python on your system and alert you if it cannot run
|
||||
it. If you are sure you have installed it, but the script cannot find it, you probably didn't
|
||||
install it with the "Add Python to PATH" option mentioned in Step 1.
|
||||
|
||||
.. note::
|
||||
If you upgrade Python to a newer version and the path to ``pythonw.exe`` has therefore changed,
|
||||
you may need to run this script again. You can also run it to upgrade dependencies to the latest
|
||||
version.
|
||||
|
||||
**Alternative B: Manual Installation**
|
||||
|
||||
The above alternative can also be run manually.
|
||||
|
||||
Open the windows command prompt. It can be launched by pressing the :kbd:`Win` key and typing "cmd".
|
||||
The "Command Prompt" app should then be in the list of applications.
|
||||
|
||||
With the command prompt open, navigate to the folder where you extracted the novelWriter source,
|
||||
and run the following commands:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
python setup.py pip
|
||||
python setup.py win-install
|
||||
|
||||
The first command will install the dependencies on your system from the `Python Package Index`_,
|
||||
and the second command will create a desktop icon and a start menu icon. That should be all that
|
||||
you need.
|
||||
|
||||
.. _Python Package Index: https://pypi.org/
|
||||
@@ -7,8 +7,8 @@ Technical Information
|
||||
This section contains details of how novelWriter stores and handles the project data.
|
||||
|
||||
|
||||
How Data is Stored
|
||||
==================
|
||||
How Project Data is Stored
|
||||
==========================
|
||||
|
||||
All novelWriter files are written with utf-8 encoding. Since Python automatically converts Unix
|
||||
line endings to Windows line endings on Windows systems, novelWriter does not make any adaptations
|
||||
@@ -20,13 +20,14 @@ operating systems.
|
||||
Main Project File
|
||||
-----------------
|
||||
|
||||
The project itself requires a dedicated folder for storing its files, where novelWriter will create
|
||||
The project itself requires a dedicated folder for storing its files where novelWriter will create
|
||||
its own "file system" where the folder and file hierarchy is described in a project XML file. This
|
||||
is the main project file in the project's root folder with the name ``nwProject.nwx``. This file
|
||||
also contains all the meta data required for the project, and a number of related project settings.
|
||||
|
||||
If this file is lost or corrupted, the structure of the project is lost. It is important to keep
|
||||
this file backed up, either through the built-in backup tool, or your own backup solution.
|
||||
If this file is lost or corrupted, the structure of the project is lost, although not the text
|
||||
itself. It is important to keep this file backed up, either through the built-in backup tool, or
|
||||
your own backup solution.
|
||||
|
||||
.. tip::
|
||||
The novelWriter project folder is structured so that it can easily be added to a version control
|
||||
@@ -36,27 +37,26 @@ this file backed up, either through the built-in backup tool, or your own backup
|
||||
|
||||
The project XML file is indent-formatted, suitable for diff tools and version control since most of
|
||||
the file will stay static, although a timesetamp is set in the meta section on line 2 each time the
|
||||
file is saved.
|
||||
file is saved, and various meta data entries are incremented on each save.
|
||||
|
||||
|
||||
Project Documents
|
||||
-----------------
|
||||
|
||||
The project documents are saved in a folder in the main project folder named ``content``. Each
|
||||
All the project documents are saved in a folder in the main project folder named ``content``. Each
|
||||
document has a file handle taken from the first 13 characters of a SHA256 hash of the system time
|
||||
when the document was first created. The documents are saved with a filename assembled from this
|
||||
hash and the file extension ``.nwd``.
|
||||
|
||||
If you wish to find the physical location of a document in the project, you can either look it up
|
||||
in the project XML file, select :guilabel:`Show File Details` from the :guilabel:`Document` menu
|
||||
If you wish to find the file system location of a document in the project, you can either look it
|
||||
up in the project XML file, select :guilabel:`Show File Details` from the :guilabel:`Document` menu
|
||||
when having the document open, or look in the ``ToC.txt`` file in the root of the project folder.
|
||||
The ``ToC.txt`` file has a list of all document files in the project and where they are saved.
|
||||
The ``ToC.txt`` file has a list of all documents in the project, referenced by their label, and
|
||||
where they are saved.
|
||||
|
||||
The reason for this cryptic file naming is to avoid issues with file naming conventions and
|
||||
restrictions on different operating systems, and also to have a file name that does not depend on
|
||||
what the user names the document within the project, or changes it to. The file meta data in the
|
||||
project tree is mainly saved in the project XML file, although some basic meta data is added to the
|
||||
header of each document file.
|
||||
what the user names the document within the project, or changes it to.
|
||||
|
||||
Each document file contains a plain text version of the text from the editor. The file can in
|
||||
principle be edited in any text editor, and is suitable for diffing and version control if so
|
||||
@@ -83,3 +83,63 @@ For the project XML file, a ``.bak`` file is in addition kept, which will always
|
||||
previous version of the file, although when auto-save is enabled, they may have the same content.
|
||||
If the opening of a project file fails, novelWriter will automatically try to open the ``.bak``
|
||||
file instead.
|
||||
|
||||
|
||||
Project Meta Data
|
||||
=================
|
||||
|
||||
The project folder contains a subfolder named ``meta``, containing a number of files. The meta
|
||||
folder contains semi-important files. That is, they can be lost with only minor impact to the
|
||||
project.
|
||||
|
||||
If you use version control software on your project, you can exclude this folder, although you may
|
||||
want to track the session log file. The JSON files within this folder can safely be ignored as they
|
||||
will be automatically regenerated if lost.
|
||||
|
||||
|
||||
The Project Index
|
||||
-----------------
|
||||
|
||||
Between writing sessions, the project index is saved in a JSON file in ``meta/tagsIndex.json``.
|
||||
This file is not critical. If it is lost, it can be rebuilt from within novelWriter from the
|
||||
:guilabel:`Tools` menu.
|
||||
|
||||
The index is maintained and updated whenever a document or note is saved in the editor. It contains
|
||||
all references and tags in documents and notes, as well as the location of all headers in the
|
||||
project, and the word counts within each header section.
|
||||
|
||||
The integrity of the index is checked when the file is loaded. It is possible to corrupt the index
|
||||
if the file is manually edited and manipulated, so the check is important to avoid sudden crashes
|
||||
of novelWriter. if the file contains errors, novelWriter will automatically build it anew. If the
|
||||
check somehow fails and novelWriter keeps crashing, you can delete the file manually and rebuild
|
||||
the index. If this too fails, you have likely encountered a bug.
|
||||
|
||||
|
||||
Cached GUI Options
|
||||
------------------
|
||||
|
||||
A file named ``meta/guiOptions.json`` contains the latest state of various GUI buttons, switches,
|
||||
dialog window sizes, column sizes, etc, from the GUI. These are the GUI settings that are specific
|
||||
to the project. Global GUI settings are stored in the main config file.
|
||||
|
||||
The file is not critical, but if it is lost, all such GUI options will revert back to their default
|
||||
settings.
|
||||
|
||||
|
||||
Session Stats
|
||||
-------------
|
||||
|
||||
The writing progress is saved in the ``meta/sessionStats.log`` file. This file records the length
|
||||
and word counts of each writing session on the given project. The file is used by the
|
||||
:guilabel:`Writing Statistics` tool. If this file is lost, the history it contains is also lost,
|
||||
but it has otherwise no impact on the project.
|
||||
|
||||
|
||||
Project Cache
|
||||
=============
|
||||
|
||||
The project ``cache`` folder contains non-critical files. If these files are lost, there is no
|
||||
impact on the functionality of novelWriter or the history of the project. It contains temporary
|
||||
files, like the preview document in the :guilabel:`Build Novel Project` tool.
|
||||
|
||||
It should be excluded from version control tools if such are used.
|
||||
|
||||
@@ -14,7 +14,7 @@ Header Formatting
|
||||
=================
|
||||
|
||||
The titles for the five types of titles (the chapter headings come in a numbered and unnumbered
|
||||
version) of story structure can be formatted collectively in the export tool. This is done through
|
||||
version) of story structure can be formatted collectively in the build tool. This is done through
|
||||
a series of keyword–replace steps. They are all on the format ``%keyword%``.
|
||||
|
||||
``%title%``
|
||||
@@ -22,20 +22,20 @@ a series of keyword–replace steps. They are all on the format ``%keyword%``.
|
||||
your document.
|
||||
|
||||
``%ch%``
|
||||
This is replaced by a chapter number. The number is incremented by one each time the build tool
|
||||
sees a new heading of level two in a document with layout :guilabel:`Chapter`. If the document
|
||||
has layout :guilabel:`Unnumbered`, the counter is *not* incremented. The latter is useful for
|
||||
for instance Prologue and Epilogue chapters.
|
||||
This will be replaced by a chapter number. The number is incremented by one each time the build
|
||||
tool sees a new heading of level two in a document with layout :guilabel:`Chapter`. If the
|
||||
document has layout :guilabel:`Unnumbered`, the counter is *not* incremented. The latter is
|
||||
useful for for instance Prologue and Epilogue chapters. Adding an asterisk (``*``) in front of
|
||||
the title text of a level two heading will also disable the chapter counter for that heading.
|
||||
|
||||
``%chw%``
|
||||
This is like ``%ch%``, but the number is expressed as a word like for instance "One", "Two",
|
||||
etc.
|
||||
Behaves like ``%ch%``, but the number is represented as a number word.
|
||||
|
||||
``%chi%``
|
||||
This is also like ``%ch%``, but the number is represented as a lower case Roman number.
|
||||
Begaves like ``%ch%``, but the number is represented as a lower case Roman number.
|
||||
|
||||
``%chI%``
|
||||
This is also like ``%ch%``, but the number is represented as an upper case Roman number.
|
||||
Behaves like ``%ch%``, but the number is represented as an upper case Roman number.
|
||||
|
||||
``%sc%``
|
||||
This is the number counter equivalent for scenes. These are incremented each time a heading of
|
||||
@@ -43,8 +43,8 @@ a series of keyword–replace steps. They are all on the format ``%keyword%``.
|
||||
used for counting scenes within a chapter.
|
||||
|
||||
``%sca%``
|
||||
This is like ``%sc%``, but the number is *not* reset to 1 for each chapter. Instead it runs from
|
||||
1 from the beginning of the novel.
|
||||
Behaves like ``%sc%``, but the number is *not* reset to 1 for each chapter. Instead it runs from
|
||||
1 from the beginning of the novel to produce an absolute scene count.
|
||||
|
||||
``\\``
|
||||
This inserts a line break within the title.
|
||||
@@ -67,11 +67,12 @@ Scene Separators
|
||||
================
|
||||
|
||||
If you don't want any titles for your scenes (or for your sections if you have them), you can leave
|
||||
the boxes empty. If so, an empty paragraph will be inserted between the scenes or sections instead.
|
||||
the formatting boxes empty. If so, an empty paragraph will be inserted between the scenes or
|
||||
sections instead.
|
||||
|
||||
Alternatively, if you want a separator between them, like the common ``* * *``, you can also enter
|
||||
that in the box. In fact, if the format is a piece of static text, it will always be treated as a
|
||||
separator.
|
||||
Alternatively, if you want a separator between them, like the common ``* * *``, you can enter the
|
||||
desired separator text in the formatting box. In fact, if the format is a piece of static text, it
|
||||
will always be treated as a separator.
|
||||
|
||||
|
||||
.. _a_export_files:
|
||||
@@ -82,8 +83,8 @@ File Selection
|
||||
Which documents and notes are selected for export can be controlled from the options on the left
|
||||
side of the dialog window. The switch for :guilabel:`Include novel files` will select any document
|
||||
that isn't classified as a note. The switch for :guilabel:`Include note files` will select any
|
||||
document that *is* a note. This is allows for exporting just the novel, just your notes, or both,
|
||||
as you see fit.
|
||||
document that *is* a note. This allows for exporting just the novel, just your notes, or both, as
|
||||
you see fit.
|
||||
|
||||
In addition, you can select to export the synopsis comments, regular comments, keywords, and even
|
||||
exclude the body text itself.
|
||||
@@ -101,6 +102,16 @@ document has a checkmark after the status icon in the :guilabel:`Flags` column.
|
||||
override these settings.
|
||||
|
||||
|
||||
.. _a_export_print:
|
||||
|
||||
Printing
|
||||
========
|
||||
|
||||
The print button allows you to print the content in the preview window. You can either print to one
|
||||
of your system's printers, or print directly to file. You can also print to file from the regular
|
||||
print dialog. The direct to file option is just a shortcut.
|
||||
|
||||
|
||||
.. _a_export_formats:
|
||||
|
||||
Export Formats
|
||||
@@ -108,35 +119,25 @@ Export Formats
|
||||
|
||||
Currently, six formats are supported for exporting.
|
||||
|
||||
OpenDocument Format
|
||||
This produces an open document ``.odt`` file. The document produced has very little formatting,
|
||||
and may require further editing afterwards. For a better formatted office document, you may get
|
||||
a better result with exporting to HTML and then import that HTML document into your office word
|
||||
processor. They are generally very good at importing HTML documents.
|
||||
|
||||
PDF Format
|
||||
The PDF export is just a shortcut for print to file. For a better PDF result, you may instead
|
||||
want to export to HTML, and use a word processor to convert the HTML document to PDF.
|
||||
Open Document Format
|
||||
The Build tool can produce either an ``.odt`` file, or an ``.fodt`` file. The latter is just a
|
||||
flat version of the document format as a single XML file.
|
||||
|
||||
novelWriter HTML
|
||||
The HTML export format writes a single ``.htm`` file with minimal style formatting. The exported
|
||||
HTML document is suitable for further processing by document conversion tools like Pandoc, for
|
||||
importing in word processors, or for printing from browser. It is generally the best formatted
|
||||
export option and supports all features of novelWriter since it is entirely geenrated by the
|
||||
application and doesn't depend on Qt library features.
|
||||
importing in word processors, or for printing from browser.
|
||||
|
||||
novelWriter Markdown
|
||||
This is simply a concatenation of the project documents selected by the filters. The documents
|
||||
are stacked together in the order they appear in the project tree, with comments, tags, etc.
|
||||
are stacked together in the order they appear in the project tree, with comments, tags, etc.
|
||||
included if they are selected. This is a useful format for exporting the project for later
|
||||
import back into novelWriter.
|
||||
|
||||
Standard Markdown
|
||||
If you have Qt 5.14 or higher, the option to export to plain markdown is available. This feature
|
||||
uses Qt's own markdown export feature.
|
||||
|
||||
Plain Text
|
||||
The plain text export format writes a simple ``.txt`` file without any formatting at all.
|
||||
Standard/GitHub Markdown
|
||||
The Markdown export format comes in both Standard and GitHub flavour. The *only* difference in
|
||||
terms of novelWriter functionality is the support of strikethrough text, which is not supported
|
||||
by the Standard flavour, but *is* supported by the GitHub flavour.
|
||||
|
||||
|
||||
.. _a_export_options:
|
||||
@@ -146,11 +147,11 @@ Additional Export Options
|
||||
|
||||
In addition to the above document formats, the novelWriter HTML and Markdown formats can also be
|
||||
wrapped in a JSON file. These files will have a meta data entry and a body entry. For HTML, also
|
||||
accompanying css styles are exported.
|
||||
the accompanying css styles are exported.
|
||||
|
||||
The text body is saved in a two-level list. The outer list contains one entry per exported file, in
|
||||
the order they appear in the project tree. Each file is then split up into a list as well, with one
|
||||
entry per paragraph in the document.
|
||||
The text body is saved in a two-level list. The outer list contains one entry per exported
|
||||
document, in the order they appear in the project tree. Each document is then split up into a list
|
||||
as well, with one entry per paragraph it contains.
|
||||
|
||||
These files are mainly intended for scripted post-processing for those who want that option. A JSON
|
||||
file can be imported directly into a Python dict object or a PHP array, to mentions a few options.
|
||||
@@ -7,6 +7,7 @@ User Interface
|
||||
The user interface is kept as simple as possible to avoid distractions when writing. This page
|
||||
lists all the main GUI elements, and explains what they do.
|
||||
|
||||
|
||||
.. _a_ui_tree:
|
||||
|
||||
The Project Tree
|
||||
@@ -22,7 +23,7 @@ the project. It has four columns:
|
||||
document text itself.
|
||||
|
||||
:guilabel:`Words`
|
||||
The second column shows the word count of the document, or the sum of words in the child items
|
||||
The second column shows the word count of the document, or the sum of words of the child items
|
||||
if it is a folder. If the counts seem incorrect, they can be updated by rebuilding the project
|
||||
index from the :guilabel:`Tools` menu, or by pressing :kbd:`F9`.
|
||||
|
||||
@@ -36,36 +37,70 @@ the project. It has four columns:
|
||||
the importance or status of the document. These are colour coded status levels that you control
|
||||
and define yourself. They can be changed in :guilabel:`Project Settings` from the
|
||||
:guilabel:`Project` menu. The first character after the icon indicates the class of the item,
|
||||
that is ``N`` for **Novel**, ``C`` for **Character**, etc (see :ref:`a_struct_tags`). The second
|
||||
character indicates the document layout type (see :ref:`a_proj_roots`).
|
||||
that is ``N`` for **Novel**, ``C`` for **Character**, etc (see :ref:`a_struct_tags`). The
|
||||
characters after the dot indicate the document layout type (see :ref:`a_proj_roots`).
|
||||
|
||||
Right-clicking an item in the project tree will open a context menu under the cursor, displaying
|
||||
a selection of actions that can be performed on the selected item.
|
||||
|
||||
Below the project tree you will find a small details panel showing the full information of the
|
||||
currently selected item. This panel also includes the latest paragraph and character counts in
|
||||
addition to the word count.
|
||||
|
||||
Right-clicking an item in the project tree will open a context menu under the cursor, displaying
|
||||
a selection of actions that can be performed on the selected item.
|
||||
|
||||
.. _a_ui_tree_novel:
|
||||
|
||||
The Novel Tree
|
||||
--------------
|
||||
|
||||
An alternative way to view the project structure is the novel tree. You can switch to this view by
|
||||
selecting the :guilabel:`Novel` tab under the project tree. This view is a simplified version of
|
||||
the view in the :guilabel:`Outline`. It is convenient when you want to browse the structure of the
|
||||
story itself rather than the document files.
|
||||
|
||||
.. note::
|
||||
You cannot reorganise the entries in the novel tree as that would imply restructuring the
|
||||
content of the document files.
|
||||
|
||||
|
||||
.. _a_ui_tree_status:
|
||||
|
||||
Document Importance and Status
|
||||
------------------------------
|
||||
|
||||
Each document or folder in your project can have either a "Status" or "Importance" flag set. These
|
||||
are flags that you control and define in their respective tabs in :guilabel:`Project Settings`. The
|
||||
"Status" flag is intended to tag a Novel document as for instance a draft or as completed, and the
|
||||
"Importance" flag is intended to tag character notes, or other notes, as for instance a main, major
|
||||
or minor character.
|
||||
|
||||
Whether a document uses a "Status" or "Importance" flag depends on which root folder it lives in.
|
||||
If it's in the :guilabel:`Novel` folder, it uses the "Status" flag, otherwise it uses an
|
||||
"Importance" flag. Some folders, like :guilabel:`Trash` and :guilabel:`Outtakes` allow both.
|
||||
|
||||
|
||||
.. _a_ui_tree_dnd:
|
||||
|
||||
Project Tree Drag and Drop
|
||||
--------------------------
|
||||
Project Tree Drag & Drop
|
||||
------------------------
|
||||
|
||||
The project tree allows drag and drop to a certain extent. This feature is primarily intended for
|
||||
The project tree allows drag & drop to a certain extent. This feature is primarily intended for
|
||||
rearranging the order of your documents within each root folder, and has only limited support for
|
||||
moving documents elsewhere in the project tree. In general, bulk actions are not allowed. This is
|
||||
deliberate to avoid accidentally messing up your project. The project tree has no undo function.
|
||||
deliberate to avoid accidentally messing up your project. If you make a mistake, the last move
|
||||
action can be undone by pressing :kbd:`Ctrl`:kbd:`Shift`:kbd:`Z`.
|
||||
|
||||
Documents and their folders can be rearranged freely within their root folders. Novel documents
|
||||
cannot be moved out of the :guilabel:`Novel` folder, except to :guilabel:`Trash` and the
|
||||
:guilabel:`Outtakes` folder. Notes can be moved freely between root folders.
|
||||
:guilabel:`Outtakes` folders. Notes can be moved freely between all root folders, but keep in mind
|
||||
that if you move a note into a :guilabel:`Novel`, its "Importance" setting will be reset to the
|
||||
default "Status" setting. See :ref:`a_ui_tree_status`.
|
||||
|
||||
Folders cannot be moved at all outside their root tree. Neither can a folder containing documents
|
||||
be deleted. You must first delete the containing documents.
|
||||
|
||||
Root folders in the project tree cannot be dragged and dropped at all. If you want to reorder them,
|
||||
you can move them up or down with respect to eachother from the :guilabel:`Tools` menu or the
|
||||
you can move them up or down with respect to eachother from the :guilabel:`Project` menu, the
|
||||
right-click context menu, or by pressing :kbd:`Ctrl`:kbd:`Shift` and the :kbd:`Up` or :kbd:`Down`
|
||||
key.
|
||||
|
||||
@@ -78,8 +113,8 @@ Editing and Viewing Documents
|
||||
To edit a document, double-click it in the project tree, or press the :kbd:`Return` key while
|
||||
having it selected. This will open the document in the document editor. The editor uses a
|
||||
simplified markdown format. The format is described in the :ref:`a_ui_md` section below. The editor
|
||||
has a maximise button (activates :guilabel:`Distraction Free Mode`) and a close button in the
|
||||
top-right corner.
|
||||
has a maximise button (toggles the :guilabel:`Focus Mode`) and a close button in the top-right
|
||||
corner.
|
||||
|
||||
Any document in the project tree can also be viewed in parallel in a right hand side document
|
||||
viewer. To view a document, press :kbd:`Ctrl`:kbd:`R`, or select :guilabel:`View Document` in the
|
||||
@@ -91,27 +126,30 @@ panel next to the close button to achieve the same thing.
|
||||
|
||||
Both the document editor and viewer will show the label of the document in the header at the top of
|
||||
the edit or view panel. Optionally, the full project path to the document can be shown. This can be
|
||||
set in the :guilabel:`Preferences` dialog from the :guilabel:`Tools` menu. Clicking on the document
|
||||
title bar will select and reveal its location in the project tree, making it easier to find in a
|
||||
large project.
|
||||
set in :guilabel:`Preferences` from the :guilabel:`Tools` menu. Clicking on the document title bar
|
||||
will select and reveal its location in the project tree, making it easier to locate in a large
|
||||
project.
|
||||
|
||||
Any reference to a tag in the editor can be opened in the viewer by moving the cursor to the label
|
||||
and pressing :kbd:`Ctrl`:kbd:`Return`. In the viewer, the references become clickable links.
|
||||
Clicking them will replace the content of the viewer with the content of the document the reference
|
||||
points to. The document viewer keeps a history of viewed documents that you can navigate with the
|
||||
arrow buttons in the top-left corner of the viewer. If your mouse has navigation buttons, these can
|
||||
be used as well.
|
||||
Any tag reference in the editor can be opened in the viewer by moving the cursor to the label and
|
||||
pressing :kbd:`Ctrl`:kbd:`Return`. You can also control-click them with your mouse. In the viewer,
|
||||
the references become clickable links. Clicking them will replace the content of the viewer with
|
||||
the content of the document the reference points to. The document viewer keeps a history of viewed
|
||||
documents, which you can navigate with the arrow buttons in the top-left corner of the viewer. If
|
||||
your mouse has back and forward navigation buttons, these can be used as well. They work just like
|
||||
the backward and forward features in a browser.
|
||||
|
||||
At the bottom of the view panel there is a :guilabel:`References` panel. (If it is hidden, click
|
||||
the icon to reveal it.) This panel will show links to all documents referring back to it, if any
|
||||
has been defined. The :guilabel:`Sticky` button will freeze the content of the panel to the current
|
||||
document, even if you navigate to another document. This is convenient if you want to quickly look
|
||||
through all documents in the list in the :guilabel:`References` panel.
|
||||
the icon to reveal it.) This panel will show links to all documents referring back to the one
|
||||
you're currently viewing, if any has been defined. The :guilabel:`Sticky` button will freeze the
|
||||
content of the panel to the current document, even if you navigate to another document. This is
|
||||
convenient if you want to quickly look through all documents in the list in the
|
||||
:guilabel:`References` panel without losing the list in the process.
|
||||
|
||||
.. note::
|
||||
The :guilabel:`References` panel relies on an up-to-date index of the project. If anything is
|
||||
missing, or seems wrong, the index can always be rebuilt by selecting :guilabel:`Rebuild Index`
|
||||
from the :guilabel:`Tools` menu, or by pressing :kbd:`F9`.
|
||||
The :guilabel:`References` panel relies on an up-to-date index of the project. The index is
|
||||
maintained automatically. However, if anything is missing, or seems wrong, the index can always
|
||||
be rebuilt by selecting :guilabel:`Rebuild Index` from the :guilabel:`Tools` menu, or by
|
||||
pressing :kbd:`F9`.
|
||||
|
||||
|
||||
.. _a_ui_edit_auto:
|
||||
@@ -120,20 +158,26 @@ Auto-Replace as You Type
|
||||
========================
|
||||
|
||||
A few auto-replace features are supported by the editor. You can control every aspect of the
|
||||
auto-replace feature from :guilabel:`Preferences`.
|
||||
auto-replace feature from :guilabel:`Preferences`. You can also disable this feature entirely if
|
||||
you wish.
|
||||
|
||||
.. tip::
|
||||
If you don't like auto-replacement, all symbols inserted by this feature are also available in
|
||||
the :guilabel:`Insert` menu, and via convenient :ref:`a_ui_shortcuts_ins`.
|
||||
the :guilabel:`Insert` menu, and via convenient :ref:`a_ui_shortcuts_ins`. You may also be using
|
||||
a `Compose Key`_ setup, which means you may not need the auto-replace feature.
|
||||
|
||||
.. _Compose Key: https://en.wikipedia.org/wiki/Compose_key
|
||||
|
||||
The editor is able to replace two and three hyphens with short and long dashes, triple points with
|
||||
ellipsis, and replace straight single and double quotes with user-defined quote symbols. It will
|
||||
also try to determine whether to use the opening or closing symbol, but this feature isn't always
|
||||
accurate.
|
||||
also try to determine whether to use the opening or closing symbol, although this feature isn't
|
||||
always accurate. Especially distinguishing between closing single quote and apostrophe can be
|
||||
tricky for languages that use the same symbol for these.
|
||||
|
||||
.. tip::
|
||||
If the editor changes a symbol when you did not want it to change, pressing :kbd:`Ctrl`:kbd:`Z`
|
||||
immediately after the auto-replacement will undo it without undoing the character you typed.
|
||||
If the auto-replace feature changes a symbol when you did not want it to change, pressing
|
||||
:kbd:`Ctrl`:kbd:`Z` immediately after the auto-replacement will undo it without undoing the
|
||||
character you typed.
|
||||
|
||||
|
||||
.. _a_ui_md:
|
||||
@@ -154,30 +198,37 @@ a synopsis tag, and a set of keyword and value sets used for tags and references
|
||||
Headings
|
||||
--------
|
||||
|
||||
Four levels of headings are allowed. For documents of layout "Note", they are free to be used as
|
||||
Four levels of headings are allowed. For documents of layout ``Note``, they are free to be used as
|
||||
you see fit, but for all other layouts used for the novel text itself, they indicate the structural
|
||||
level of the novel. See :ref:`a_struct_heads` for more details.
|
||||
|
||||
``# Title``
|
||||
Heading level one. If the document is a novel file, the header level indicates the start of a
|
||||
new partition. This heading level can also be used for the title page novel title.
|
||||
new partition. This heading level can also be used for the title page's novel title.
|
||||
|
||||
``## Title``
|
||||
Heading level two. If the document is a novel file, the header level indicates the start of a
|
||||
new chapter.
|
||||
new chapter. Chapter numbers can be inserted automatically when exporting the manuscript.
|
||||
|
||||
``### Title``
|
||||
Heading level three. If the document is a novel file, the header level indicates the start of a
|
||||
new scene.
|
||||
new scene. Scene numbers or scene separators can be inserted automatically when exporting the
|
||||
manuscript, so you can use the title field as a working title for your scenes if you wish.
|
||||
|
||||
``#### Title``
|
||||
Heading level four. If the document is a novel file, the header level indicates the start of a
|
||||
new section.
|
||||
new section. Section titles can be replaced by separators or removed when exporting the
|
||||
manuscript, so you can use the title field as a working title for your sections if you wish.
|
||||
|
||||
.. note::
|
||||
The space after the ``#`` characters is mandatory. The syntax highlighter will change colour and
|
||||
font size when the heading is correctly formatted.
|
||||
|
||||
.. tip::
|
||||
If you do use the automatic numbering feature for exports, you can tell the export tool to skip
|
||||
assigning a number to a specific chapter by adding a ``*`` as the first character of the title
|
||||
itself. See :ref:`a_struct_heads_unnum` for more details.
|
||||
|
||||
|
||||
.. _a_ui_md_emph:
|
||||
|
||||
@@ -219,26 +270,28 @@ In addition, the following rules apply:
|
||||
Comments and Synopsis
|
||||
---------------------
|
||||
|
||||
In addition to these standard markdown features, novelWriter also allows for comments in document.
|
||||
The text of the comment is ignored by the word counter and not exported or, optionally, hidden when
|
||||
viewing the document. If the first word of a comment is ``Synopsis:`` (with the colon), the comment
|
||||
is treated specially and will show up in the :ref:`a_ui_outline` in a dedicated column. The word
|
||||
``synopsis`` is not case sensitive. If it is correctly formatted, the syntax highlighter will
|
||||
indicate this by altering the colour of the word.
|
||||
In addition to these standard markdown features, novelWriter also allows for comments in documents.
|
||||
The text of a comment is ignored by the word counter. The text can also be filtered out when
|
||||
exporting or viewing the document.
|
||||
|
||||
If the first word of a comment is ``Synopsis:`` (with the colon included), the comment is treated
|
||||
specially and will show up in the :ref:`a_ui_outline` in a dedicated column. The word ``synopsis``
|
||||
is not case sensitive. If it is correctly formatted, the syntax highlighter will indicate this by
|
||||
altering the colour of the word.
|
||||
|
||||
``% text...``
|
||||
A comment. The text is not exported by default (this can be overridden), seen in the Viewer, or
|
||||
counted towards word counts.
|
||||
This is a comment. The text is not exported by default (this can be overridden), seen in the
|
||||
document viewer, or counted towards word counts.
|
||||
|
||||
``% Synopsis: text...``
|
||||
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 exporting the project to for instance generate an outline document
|
||||
of the whole project.
|
||||
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 exporting the project to for instance generate an outline
|
||||
document of the whole project.
|
||||
|
||||
.. note::
|
||||
Only one comment can be flagged as a synopsis comment for each heading. If multiple comments are
|
||||
flagged as synopsis comments, the last one will be used.
|
||||
flagged as synopsis comments, the last one will be used and the rest ignored.
|
||||
|
||||
|
||||
.. _a_ui_md_tags:
|
||||
@@ -262,13 +315,14 @@ also be inserted at the cursor position in the editor via the :guilabel:`Insert`
|
||||
Additional Markdown and Non-Standard Features
|
||||
---------------------------------------------
|
||||
|
||||
The editor and viewer also supports markdown standard hard line breaks, and preserves non-breaking
|
||||
The editor and viewer also support markdown standard hard line breaks, and preserve non-breaking
|
||||
spaces if running with Qt 5.9 or higher. For older versions, the non-breaking spaces are lost when
|
||||
the document is saved. This is unfortunately hard-coded in the Qt text editor.
|
||||
the document is saved. This is unfortunately hard-coded into the Qt text editor.
|
||||
|
||||
* A hard line break is achieved by leaving two or more spaces at the end of the line.
|
||||
Alternatively, the user can press :kbd:`Ctrl`:kbd:`K`, :kbd:`Return` to insert this.
|
||||
* A non-breaking space is inserted with :kbd:`Ctrl`:kbd:`K`, :kbd:`Space`.
|
||||
* A hard line break can be achieved by leaving two or more spaces at the end of the line. This is
|
||||
standard markdown syntax. Alternatively, the user can press :kbd:`Ctrl`:kbd:`K`, :kbd:`Return` to
|
||||
insert this type of line break.
|
||||
* A non-breaking space can be inserted with :kbd:`Ctrl`:kbd:`K`, :kbd:`Space`.
|
||||
* Thin spaces are also supported, and can be inserted with :kbd:`Ctrl`:kbd:`K`,
|
||||
:kbd:`Shift`:kbd:`Space`.
|
||||
* Non-breaking thin space can be inserted with :kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`Space`.
|
||||
@@ -279,6 +333,10 @@ in :ref:`a_ui_shortcuts_ins`.
|
||||
Both hard line breaks and non-breaking spaces are highlighted by the syntax highlighter as an
|
||||
alternate coloured background, depending on the selected theme.
|
||||
|
||||
.. tip::
|
||||
Non-breaking spaces are the correct type of space to separate a number from its unit. Generally,
|
||||
it prevents the line wrapping algorithms from adding line breaks where it shouldn't.
|
||||
|
||||
|
||||
.. _a_ui_outline:
|
||||
|
||||
@@ -287,7 +345,8 @@ Project Outline View
|
||||
|
||||
The project's Outline view is available as the second tab on the right hand side of the main window
|
||||
labelled :guilabel:`Outline`. The outline provides an overview of the novel structure, displaying a
|
||||
tree hierarchy of the elements of the novel, that is, the level 1 to 4 headings.
|
||||
tree hierarchy of the elements of the novel, that is, the level 1 to 4 headings representing
|
||||
partitions, chapters, scenes and sections.
|
||||
|
||||
The document containing the heading can also be displayed as a separate column, as well as the line
|
||||
number where it occurs. Double-clicking an entry will open the corresponding document in the
|
||||
@@ -295,7 +354,7 @@ editor.
|
||||
|
||||
.. note::
|
||||
Since the internal structure of the novel does not depend directly on the folder and document
|
||||
structure of the project tree, these will not necessarily look the same, depending how you
|
||||
structure of the project tree, these will not necessarily look the same, depending on how you
|
||||
choose to organise your documents. See the :ref:`a_struct` page for more details.
|
||||
|
||||
Various meta data and information extracted from tags can be displayed in columns in the outline.
|
||||
@@ -307,7 +366,7 @@ rearranged by dragging them to a different position.
|
||||
The :guilabel:`Title` column cannot be disabled or moved.
|
||||
|
||||
The information viewed in the outline is based on the project's main index. While novelWriter does
|
||||
its best to keep the index up to date when content changes, you can always rebuild it manually by
|
||||
its best to keep the index up to date when contents change, you can always rebuild it manually by
|
||||
pressing :kbd:`F9` if something isn't right.
|
||||
|
||||
The outline view itself can be regenerated by pressing :kbd:`F10`. You can also enable automatic
|
||||
@@ -334,12 +393,12 @@ Most features are available as keyboard shortcuts. These are as follows:
|
||||
":kbd:`Alt`:kbd:`1`", "Switch focus to the project tree."
|
||||
":kbd:`Alt`:kbd:`2`", "Switch focus to document editor."
|
||||
":kbd:`Alt`:kbd:`3`", "Switch focus to document viewer."
|
||||
":kbd:`Alt`:kbd:`4`", "Switch focus to outline view."
|
||||
":kbd:`Alt`:kbd:`Left`", "Move backward in the view history of the document viewer."
|
||||
":kbd:`Alt`:kbd:`Right`", "Move forward in the view history of the document viewer."
|
||||
":kbd:`Ctrl`:kbd:`.`", "Open menu to correct word under cursor."
|
||||
":kbd:`Ctrl`:kbd:`,`", "Open the :guilabel:`Preferences` dialog."
|
||||
":kbd:`Ctrl`:kbd:`/`", "Change block format to comment."
|
||||
":kbd:`Ctrl`:kbd:`-`", "Strikethrough selected text, or word under cursor."
|
||||
":kbd:`Ctrl`:kbd:`0`", "Remove block formatting for block under cursor."
|
||||
":kbd:`Ctrl`:kbd:`1`", "Change block format to header level 1."
|
||||
":kbd:`Ctrl`:kbd:`2`", "Change block format to header level 2."
|
||||
@@ -348,7 +407,7 @@ Most features are available as keyboard shortcuts. These are as follows:
|
||||
":kbd:`Ctrl`:kbd:`A`", "Select all text in the document."
|
||||
":kbd:`Ctrl`:kbd:`B`", "Format selected text, or word under cursor, with strong emphasis (bold)."
|
||||
":kbd:`Ctrl`:kbd:`C`", "Copy selected text to clipboard."
|
||||
":kbd:`Ctrl`:kbd:`D`", "Wrap selected text, or word under cursor, in double quotes."
|
||||
":kbd:`Ctrl`:kbd:`D`", "Strikethrough selected text, or word under cursor."
|
||||
":kbd:`Ctrl`:kbd:`E`", "If in the project tree, edit a document or folder settings. (Same as :kbd:`F2`.)"
|
||||
":kbd:`Ctrl`:kbd:`F`", "Open the search bar and search for the selected word, if any is selected."
|
||||
":kbd:`Ctrl`:kbd:`G`", "Find next occurrence of search word in current document. (Same as :kbd:`F3`.)"
|
||||
@@ -367,13 +426,17 @@ Most features are available as keyboard shortcuts. These are as follows:
|
||||
":kbd:`Ctrl`:kbd:`Z`", "Undo latest changes."
|
||||
":kbd:`Ctrl`:kbd:`F7`", "Toggle spell checking."
|
||||
":kbd:`Ctrl`:kbd:`F10`", "Toggle automatic updating of project outline."
|
||||
":kbd:`Ctrl`:kbd:`Del`", "If in the project tree, move a document to trash, or delete a folder."
|
||||
":kbd:`Ctrl`:kbd:`Up`", "Move item one step up in the project tree."
|
||||
":kbd:`Ctrl`:kbd:`Down`", "Move item one step down in the project tree."
|
||||
":kbd:`Ctrl`:kbd:`Del`", "Delete next word in editor."
|
||||
":kbd:`Ctrl`:kbd:`Backspace`", "Delete previous word in editor."
|
||||
":kbd:`Ctrl`:kbd:`'`", "Wrap selected text, or word under cursor, in single quotes."
|
||||
":kbd:`Ctrl`:kbd:`""`", "Wrap selected text, or word under cursor, in double quotes."
|
||||
":kbd:`Ctrl`:kbd:`Enter`", "Open the tag or reference under the cursor in the Viewer."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`,`", "Open the :guilabel:`Project Settings` dialog."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`/`", "Remove block formatting for block under cursor."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`1`", "Replace occurrence of search word in current document, and search for next occurrence."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`A`", "Select all text in current paragraph."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`D`", "Wrap selected text, or word under cursor, in single quotes."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`G`", "Find previous occurrence of search word in current document. (Same as :kbd:`Shift`:kbd:`F3`.)"
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`I`", "Import text to the current document from a text file."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`N`", "Create new folder."
|
||||
@@ -381,9 +444,8 @@ Most features are available as keyboard shortcuts. These are as follows:
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`R`", "Close the document viewer."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`S`", "Save the current project."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`W`", "Close the current project."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`Z`", "Alternative sequence for redo last undo."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`Up`", "Move item one step up in the project tree."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`Down`", "Move item one step down in the project tree."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`Z`", "Undo move of project tree item."
|
||||
":kbd:`Ctrl`:kbd:`Shift`:kbd:`Del`", "If in the project tree, move a document to trash, or delete a folder."
|
||||
":kbd:`F1`", "Open the documentation. This will either open the Qt Assistant, if available, or send you to the documentation website."
|
||||
":kbd:`F2`", "If in the project tree, edit a document or folder settings. (Same as :kbd:`Ctrl`:kbd:`E`)"
|
||||
":kbd:`F3`", "Find next occurrence of search word in current document. (Same as :kbd:`Ctrl`:kbd:`G`)"
|
||||
@@ -396,6 +458,7 @@ Most features are available as keyboard shortcuts. These are as follows:
|
||||
":kbd:`F11`", "Activate full screen mode."
|
||||
":kbd:`Shift`:kbd:`F1`", "Open the online documentation in the system default browser."
|
||||
":kbd:`Shift`:kbd:`F3`", "Find previous occurrence of search word in current document. (Same as :kbd:`Ctrl`:kbd:`Shift`:kbd:`G`.)"
|
||||
":kbd:`Shift`:kbd:`F6`", "Open the :guilabel:`Project Details` dialog."
|
||||
":kbd:`Return`", "If in the project tree, open a document for editing."
|
||||
|
||||
.. note::
|
||||
@@ -409,31 +472,43 @@ Insert Shortcuts
|
||||
|
||||
A set of insert features are also available through shortcuts, but they require a double
|
||||
combination of key sequences. The insert feature is activated with :kbd:`Ctrl`:kbd:`K`, followed by
|
||||
a key or combination for the inserted character or punctuation.
|
||||
a key or key combination for the inserted content.
|
||||
|
||||
.. csv-table:: Keyboard Shortcuts
|
||||
:header: "Shortcut", "Description"
|
||||
:widths: 40, 60
|
||||
:class: "tight-table"
|
||||
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`-`", "Insert a short dash (en dash)."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`−`", "Insert a short dash (en dash)."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`_`", "Insert a long dash (em dash)."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`.`", "Insert an ellipsis."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`_`", "Insert a horizontal bar (quotation dash)."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`~`", "Insert a figure dash (same width as a number)."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`1`", "Insert a left single quote."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`2`", "Insert a right single quote."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`3`", "Insert a left double quote."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`4`", "Insert a right double quote."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`'`", "Insert a modifier apostrophe."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`.`", "Insert an ellipsis."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`'`", "Insert a prime."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`""`", "Insert a double prime."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Return`", "Insert a hard line break."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Space`", "Insert a non-breaking space."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Shift`:kbd:`Space`", "Insert a thin space."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`Space`", "Insert a thin non-breaking space."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`G`", "Insert a @tag keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`V`", "Insert a @pov keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`C`", "Insert a @char keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`P`", "Insert a @plot keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`T`", "Insert a @time keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`L`", "Insert a @location keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`O`", "Insert a @object keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`E`", "Insert a @entity keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`X`", "Insert a @custom keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`*`", "Insert a list bullet."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`−`", "Insert a hyphen bullet (alternative bullet)."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`*`", "Insert a flower mark (alternative bullet)."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`%`", "Insert a per mille symbol."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`O`", "Insert a degree symbol."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`X`", "Insert a times sign."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`D`", "Insert a division sign."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`G`", "Insert a ``@tag`` keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`V`", "Insert a ``@pov`` keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`F`", "Insert a ``@focus`` keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`C`", "Insert a ``@char`` keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`P`", "Insert a ``@plot`` keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`T`", "Insert a ``@time`` keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`L`", "Insert a ``@location`` keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`O`", "Insert an ``@object`` keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`E`", "Insert an ``@entity`` keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`X`", "Insert a ``@custom`` keyword."
|
||||
@@ -4,16 +4,18 @@
|
||||
Project Notes
|
||||
*************
|
||||
|
||||
novelWriter doesn't have a database and complicated forms to fill in all details about plot
|
||||
novelWriter doesn't have a database and complicated forms for filling in details about plot
|
||||
elements, characters, and all sorts of additional information that isn't a part of the novel text
|
||||
itself. Instead, all such information is saved in notes. The relation between all these additional
|
||||
elements is extracted from these documents by the project indexer based on the tags and references
|
||||
you set.
|
||||
itself. Instead, all such information is saved in notes that are written and maintained just like
|
||||
all other text in your project.
|
||||
|
||||
These notes are not required, but making at least minimal files for each such plot element, and
|
||||
adding a tag to them, makes it possible to use the :guilabel:`Outline` feature to see how each
|
||||
element intersects with each section of the novel itself, and adds clickable cross-references
|
||||
between documents in the editor and viewer.
|
||||
The relation between all these additional elements is extracted from the documents and notes by the
|
||||
project indexer, based on the tags and references you set within them.
|
||||
|
||||
Using notes is not required, but making at least minimal notes for each plot element, and adding a
|
||||
tag to them, makes it possible to use the :guilabel:`Outline` feature to see how each element
|
||||
intersects with each section of the novel itself, and adds clickable cross-references between
|
||||
documents in the editor and viewer.
|
||||
|
||||
|
||||
.. _a_notes_tags:
|
||||
@@ -24,7 +26,8 @@ Tags in Notes
|
||||
Each new heading in a note can have a tag associated with it. The format of a tag is
|
||||
``@tag: tagname``, where tagname is a unique identifier. Tags can then be referenced in the novel
|
||||
documents, or cross-referenced in other notes, and will show up in the outline view and in the
|
||||
back-reference panel when a document is being viewed.
|
||||
back-reference panel when a document is being viewed. See :ref:`a_struct_tags` for how to reference
|
||||
notes.
|
||||
|
||||
The syntax highlighter will alert the user that the keyword is correctly used and that the tag is
|
||||
allowed, that is, the tag is unique. Duplicate tags should be detected as long as the index is up
|
||||
@@ -43,5 +46,5 @@ meaningful if you want to be able to click-navigate between them.
|
||||
|
||||
.. tip::
|
||||
If you cross-reference between notes and export your project as an HTML document using the
|
||||
:guilabel:`Build Novel Project` tool, the cross-references become clickable in the exported
|
||||
HTML document as well.
|
||||
:guilabel:`Build Novel Project` tool, the cross-references become clickable links in the
|
||||
exported HTML document.
|
||||
@@ -4,18 +4,20 @@
|
||||
Novel Projects
|
||||
**************
|
||||
|
||||
A novelWriter project requires a dedicated folder for storing its files on the local file system.
|
||||
See the :ref:`a_tech` page for further details on how files are organised.
|
||||
|
||||
A new project can be created from the :guilabel:`Project` menu by selecting
|
||||
:guilabel:`New Project`. This will open the :guilabel:`New Project Wizard` that will assist you in
|
||||
creating a barebone project suited to your needs.
|
||||
creating a barebone project suited to your needs. A novelWriter project requires a dedicated folder
|
||||
for storing its files on the local file system. See :ref:`a_tech` for further details on how files
|
||||
are organised.
|
||||
|
||||
A list of recently opened projects is maintained, and displayed in the :guilabel:`Open Project`
|
||||
dialog. A project can be removed from this list by selecting it and pressing the :kbd:`Del` key.
|
||||
dialog. A project can be removed from this list by selecting it and pressing the :kbd:`Del` key or
|
||||
by clicking the :guilabel:`Remove` button.
|
||||
|
||||
The project specific settings are available in :guilabel:`Project Settings` in the
|
||||
:guilabel:`Project` menu. See further details below in the :ref:`a_proj_settings` section.
|
||||
Project-specific settings are available in :guilabel:`Project Settings` in the :guilabel:`Project`
|
||||
menu. See further details below in the :ref:`a_proj_settings` section. Details about the project,
|
||||
including word counts, and a table of contents with word and page counts, is available through the
|
||||
:guilabel:`Project Details` dialog.
|
||||
|
||||
|
||||
.. _a_proj_roots:
|
||||
@@ -28,7 +30,7 @@ the project tree at the left side of the main window.
|
||||
|
||||
The core novel documents go into a root folder of type :guilabel:`Novel`. Other supporting
|
||||
documents go into the other root folders. These other root folder types are intended for your notes
|
||||
on the various elements of your story. Using these is of course entirely optional.
|
||||
on the various elements of your story. Using them is of course entirely optional.
|
||||
|
||||
A new project may not have all of the root folders present, but you can add the ones you want from
|
||||
:guilabel:`Create Root Folder` in the :guilabel:`Project` menu.
|
||||
@@ -38,8 +40,8 @@ no restrictions are enforced by the application. You can use them however you wa
|
||||
|
||||
:guilabel:`Novel`
|
||||
This is the root folder of all text that goes into the final novel. This class of documents have
|
||||
other rules and features than other documents in the project. See the :ref:`a_struct` page for
|
||||
more details.
|
||||
other rules and features than other documents in the project. See :ref:`a_struct` for more
|
||||
details.
|
||||
|
||||
:guilabel:`Plot`
|
||||
This is the root folder where main plots can be outlined. It is optional, but adding at least
|
||||
@@ -49,8 +51,9 @@ no restrictions are enforced by the application. You can use them however you wa
|
||||
:guilabel:`Characters`
|
||||
Character notes go in this root folder. These are especially important if one wants to use the
|
||||
Outline view to see which character appears where, and which part of the story is told from a
|
||||
specific character's point-of-view. Tags in this folder can be references using the ``@pov``
|
||||
keyword for point-of-view characters, or the ``@char`` keyword for other characters.
|
||||
specific character's point-of-view or focusing on a particular character's storyline. Tags in
|
||||
this folder can be referenced using the ``@pov`` keyword for point-of-view characters,
|
||||
``@focus`` for a focus character, or the ``@char`` keyword for any other characters.
|
||||
|
||||
:guilabel:`Locations`
|
||||
The locations folder is for various scene locations that you want to track. Tags in this folder
|
||||
@@ -79,8 +82,8 @@ information about the tags listed, see :ref:`a_struct_tags`.
|
||||
|
||||
.. tip::
|
||||
You can rename root folders to whatever you want. The first character in the :guilabel:`Flags`
|
||||
column will still indicate what type they are, and so will the icon if you are using one of the
|
||||
Typicons icon sets.
|
||||
column in the project tree will still indicate what type they are, and so will the icon if you
|
||||
are using one of the optional icon sets.
|
||||
|
||||
|
||||
.. _a_proj_roots_del:
|
||||
@@ -95,7 +98,7 @@ referenced.
|
||||
|
||||
Folders and root folders can only be deleted when they are empty. Recursive deletion is not
|
||||
supported. A document or a folder can be deleted from the :guilabel:`Project` menu, or by pressing
|
||||
:kbd:`Ctrl`:kbd:`Del`.
|
||||
:kbd:`Ctrl`:kbd:`Shift`:kbd:`Del`.
|
||||
|
||||
|
||||
.. _a_proj_roots_out:
|
||||
@@ -105,9 +108,9 @@ Archived Documents (Outtakes)
|
||||
|
||||
If you don't want to delete a document, or put it in the :guilabel:`Trash` folder where it may be
|
||||
deleted, but still want it out of your main project tree, you can create an :guilabel:`Outtakes`
|
||||
root folder from the :guilabel:`Project` menu. You are not allowed to move folders to this root
|
||||
folder, only documents. If you need folders in it to organise your documents, you can of course
|
||||
create new ones there.
|
||||
root folder from the :guilabel:`Project` menu. You are not allowed to move entire folders to this
|
||||
root folder, only documents. If you need folders in it to organise your documents, you can of
|
||||
course create new ones there.
|
||||
|
||||
You can drag any document to this folder and preserve its settings. The document will always be
|
||||
excluded from the :guilabel:`Build Novel Project` builds. It is also removed from the project
|
||||
@@ -157,7 +160,8 @@ this warning, and continue opening the project at your own risk.
|
||||
If you choose to ignore the warning and continue opening the project, and multiple instances of
|
||||
the project are in fact open, you are likely to cause inconsistencies and create diverging
|
||||
project files, potentially resulting in loss of data and orphaned files. You are not likely to
|
||||
lose any actual text unless both instances have the same document open in the editor,
|
||||
lose any actual text unless both instances have the same document open in the editor, and
|
||||
novelWriter will try to resolve inconsistencies the next time you open the project.
|
||||
|
||||
|
||||
.. _a_proj_roots_dirs:
|
||||
@@ -174,9 +178,9 @@ and to be able to collapse and hide them in the project tree when you're not wor
|
||||
documents.
|
||||
|
||||
.. tip::
|
||||
You can use folders to sort your scene documents into chapters. You will then need to add a
|
||||
chapter documents as the first item of your folder, and the scene documents as the following
|
||||
items.
|
||||
You can use folders to sort your scene documents into chapters. You will still need to add a
|
||||
chapter document as the first item of your chapter folder, and the scene documents as the
|
||||
following items. Another way to use folders is to make a folder for each act.
|
||||
|
||||
|
||||
.. _a_proj_files:
|
||||
@@ -208,7 +212,7 @@ Word Counts
|
||||
A character, word and paragraph count is maintained for each document, as well as for each section
|
||||
of a document following a header. The word count, and change of words in the current session, is
|
||||
displayed in the footer of any document open in the editor, and all stats are shown in the details
|
||||
panel below the project tree for any document selected in the project tree.
|
||||
panel below the project tree for any document selected in the project or novel tree.
|
||||
|
||||
The word counts are not updated in real time, but run in the background every five seconds for as
|
||||
long as the document is being actively edited.
|
||||
@@ -243,13 +247,9 @@ The :guilabel:`Book Title` and :guilabel:`Book Authors` settings are currently n
|
||||
anything, so setting then is just for the benefit of the author. Future features may be using them,
|
||||
and they are exported on some export formats in the :guilabel:`Build Novel Project` tool.
|
||||
|
||||
|
||||
Details Tab
|
||||
-----------
|
||||
|
||||
This tab presents an overview of meta data for the project. It states where on your file system the
|
||||
project is saved, how may times it has been saved, how many folders and documents it contains, and
|
||||
how many words exist in the entire project.
|
||||
If your project is in a different language than your main spell checking is set to, you can
|
||||
override the default spell checking language here. You can also override the automatic backup
|
||||
setting.
|
||||
|
||||
|
||||
Status and Importance Tabs
|
||||
@@ -265,6 +265,8 @@ use these to indicate at what stage of completion each novel document is, or how
|
||||
content of a note is to the plot. You don't have to use them this way, that's just what they were
|
||||
intended for, but you can make them whatever you want.
|
||||
|
||||
See also :ref:`a_ui_tree_status`.
|
||||
|
||||
.. note::
|
||||
The status or importance level currently in use by one or more documents cannot be deleted, but
|
||||
they can be edited.
|
||||
@@ -277,13 +279,14 @@ A set of automatically replaced keywords can be added in this tab. The keywords
|
||||
will be replaced by the text in the right column when documents are opened in the viewer. They will
|
||||
also be applied to exports.
|
||||
|
||||
The auto-replace feature will replace text in angle brackets that are also in this list. The syntax
|
||||
highlighter will add an alternate colour to text marching the syntax.
|
||||
The auto-replace feature will replace text in angle brackets that are in this list. The syntax
|
||||
highlighter will add an alternate colour to text marching the syntax, but it doesn't check if the
|
||||
text is in this list.
|
||||
|
||||
.. note::
|
||||
A keyword cannot contain any spaces. The angle brackets are added by default, and when used in
|
||||
the text are a part of the keyword to be replaced. This is to ensure that parts of the text
|
||||
aren't unintentionally replaced by the content of the list.
|
||||
A keyword cannot contain spaces. The angle brackets are added by default, and when used in the
|
||||
text are a part of the keyword to be replaced. This is to ensure that parts of the text aren't
|
||||
unintentionally replaced by the content of the list.
|
||||
|
||||
|
||||
.. _a_proj_backup:
|
||||
@@ -295,9 +298,9 @@ An automatic backup system is built into novelWriter. In order to use it, a back
|
||||
the backup files are to be stored must be provided in :guilabel:`Preferences`.
|
||||
|
||||
Backups can be run automatically when a project is closed, which also implies it is run when the
|
||||
application is closed. Backups are date stamped zip files of the entire project folder, and are
|
||||
stored in a subfolder of the backup path with the same name as the project :guilabel:`Working
|
||||
Title` set in :ref:`a_proj_settings`.
|
||||
application itself is closed. Backups are date stamped zip files of the entire project folder, and
|
||||
are stored in a subfolder of the backup path. The subfolder will have the same name as the project
|
||||
:guilabel:`Working Title` set in :ref:`a_proj_settings`.
|
||||
|
||||
The backup feature, when configured, can also be run manually from the :guilabel:`Tools` menu.
|
||||
It is also possible to disable automated backup for a given project in :guilabel:`Project
|
||||
@@ -306,7 +309,7 @@ Settings`.
|
||||
.. note::
|
||||
For the backup to be able to run, the :guilabel:`Working Title` must be set in
|
||||
:guilabel:`Project Settings`. This value is used to generate the folder name for the zip files.
|
||||
Without it, the backup will not run at all, but produce a warning message.
|
||||
Without it, the backup will not run at all, but it will produce a warning message.
|
||||
|
||||
|
||||
.. _a_proj_stats:
|
||||
@@ -325,3 +328,8 @@ A tool to view the content of this file is available in the :guilabel:`Tools` me
|
||||
The tool will show a list of all your sessions, and a set of filters to apply to it. You can also
|
||||
export the filtered data to a JSON file or to a CSV file that can be opened by a spreadsheet
|
||||
application like for instance Libre Office Calc.
|
||||
|
||||
As of version 1.2, the log file also stores how much of the session time was spent idle. The
|
||||
definition of idle here is that the novelWriter main window loses focus, and the user hasn't made
|
||||
any changes to the currently open document in five minutes. The number of minutes can be altered in
|
||||
:guilabel:`Preferences`.
|
||||
@@ -18,20 +18,22 @@ Importance of Headings
|
||||
Subfolders under root folders have no impact on the structure of the novel itself. The structure is
|
||||
instead dictated by the heading level of the headers within the documents.
|
||||
|
||||
Four levels of headings are supported, signified by the number of hashes preceding the title. See
|
||||
also the :ref:`a_ui_md` section for more details about the markdown syntax.
|
||||
Four levels of headings are supported, signified by the number of hashes (``#``) preceding the
|
||||
title. See also the :ref:`a_ui_md` section for more details about the markdown syntax.
|
||||
|
||||
.. note::
|
||||
The header levels are not only important when generating the exported novel file, they are also
|
||||
used by the indexer when building the outline tree in the :guilabel:`Outline` tab. Each heading
|
||||
also starts a new region where new references to tags can be set.
|
||||
used by the indexer when building the outline tree in the :guilabel:`Outline` tab as well as the
|
||||
:guilabel:`Novel` tab of the project tree. Each heading also starts a new region where new
|
||||
references and tags can be defined.
|
||||
|
||||
The different header levels are interpreted as specific section types of the novel in the following
|
||||
way:
|
||||
|
||||
``# Header1``
|
||||
Header level one signifies that the text refers to either the novel title or the name of a top
|
||||
level partition useful when you want to split the manuscript up into books, parts, or acts.
|
||||
level partition. The latter is useful when you want to split the manuscript up into books,
|
||||
parts, or acts.
|
||||
|
||||
``## Header2``
|
||||
Header level two signifies a chapter level partition. Each time you want to start a new chapter,
|
||||
@@ -47,10 +49,10 @@ way:
|
||||
manuscript.
|
||||
|
||||
``#### Header4``
|
||||
Header level four signifies a sub-scene level partition, usually called just a section in the
|
||||
documentation and user interface. These can be useful if you want to change tag references
|
||||
Header level four signifies a sub-scene level partition, usually called a "section" in the
|
||||
documentation and the user interface. These can be useful if you want to change tag references
|
||||
mid-scene, like if you change the point-of-view character. You are free to use sections as you
|
||||
wish, and can filter the titles out of the final manuscript just like with scene titles.
|
||||
wish, and can filter them out of the final manuscript just like with scene titles.
|
||||
|
||||
.. tip::
|
||||
There are multiple options of how to process novel titles when exporting the manuscript. For
|
||||
@@ -67,13 +69,13 @@ If you use layout types for your documents, the automatic numbering feature for
|
||||
controlled by whether you use the :guilabel:`Chapter` or :guilabel:`Unnumbered` layout type for
|
||||
your document. However, if you have a different document layout where this isn't practical, you can
|
||||
also switch off chapter numbering for a chapter by making the first character of the chapter title
|
||||
an ``*``. Like so:
|
||||
an asterisk (``*``). Like so:
|
||||
|
||||
``## *Unnumbered Chapter Title``
|
||||
|
||||
The leading asterisk is only considered by the :guilabel:`Build Novel Project` tool, and will be
|
||||
removed before inserted at the location of the ``%title%`` label. See the :ref:`a_export` page for
|
||||
more details.
|
||||
removed before the title is inserted at the location of the ``%title%`` label. See the
|
||||
:ref:`a_export` page for more details.
|
||||
|
||||
.. note::
|
||||
If you need the first character of the title to be an actual asterisk, you must escape it:
|
||||
@@ -85,10 +87,11 @@ more details.
|
||||
Tag References
|
||||
==============
|
||||
|
||||
Each text partition indicated by a heading of any level, can contain references to tags set in the
|
||||
supporting notes of the project. The references are gathered by the indexer and used to generate
|
||||
the outline view on the :guilabel:`Outline` tab of how the different parts of the novel are
|
||||
connected.
|
||||
Each text partition, indicated by a heading of any level, can contain references to tags set in the
|
||||
supporting notes of the project. The references are gathered by the indexer and used to generate an
|
||||
outline view on the :guilabel:`Outline` tab of how the different parts of the novel are connected.
|
||||
This section covers how to set references to tags. See :ref:`a_notes_tags` for how to define tags
|
||||
the references can point to.
|
||||
|
||||
References and tags are also clickable in the document editor and viewer, making it easy to
|
||||
navigate between reference notes while writing. Clicked links are always opened in the view panel.
|
||||
@@ -101,28 +104,33 @@ allow multiple values.
|
||||
The point-of-view character for the current section. The target must be a note tag in the
|
||||
:guilabel:`Character` type root folder.
|
||||
|
||||
``@focus``
|
||||
The character that has the focus for the current section. This can be used in cases where the
|
||||
focus is not a point-of-view character. The target must be a note tag in the
|
||||
:guilabel:`Character` type root folder.
|
||||
|
||||
``@char``
|
||||
Other characters in the current section. The target must be a note tag in a
|
||||
:guilabel:`Character` type root folder. This should not include the point-of-view character(s).
|
||||
Other characters in the current section. The target must be a note tag in the
|
||||
:guilabel:`Character` type root folder. This should not include the point-of-view character.
|
||||
|
||||
``@plot``
|
||||
The plot or subplot advanced in the current section. The target must be a note tag in a
|
||||
The plot or subplot advanced in the current section. The target must be a note tag in the
|
||||
:guilabel:`Plot` type root folder.
|
||||
|
||||
``@time``
|
||||
The timelines touched by the current section. The target must be a note tag in a
|
||||
The timelines touched by the current section. The target must be a note tag in the
|
||||
:guilabel:`Timeline` type root folder.
|
||||
|
||||
``@location``
|
||||
The location the current section takes place in. The target must be a note tag in a
|
||||
The location the current section takes place in. The target must be a note tag in the
|
||||
:guilabel:`Locations` type root folder.
|
||||
|
||||
``@object``
|
||||
Objects present in the current section. The target must be a note tag in an :guilabel:`Object`
|
||||
Objects present in the current section. The target must be a note tag in the :guilabel:`Object`
|
||||
type root folder.
|
||||
|
||||
``@entity``
|
||||
Entities present in the current section. The target must be a note tag in an
|
||||
Entities present in the current section. The target must be a note tag in the
|
||||
:guilabel:`Entities` type root folder.
|
||||
|
||||
``@custom``
|
||||
@@ -142,15 +150,15 @@ the index for a document is regenerated when it is saved, so this shouldn't norm
|
||||
Novel Document Layout
|
||||
=====================
|
||||
|
||||
All documents in a novelWriter project can have a layout format set. These layouts are important
|
||||
when the project is exported as they indicate how to treat the content in terms of formatting,
|
||||
All documents in the project can have a layout format set. These layouts are important when the
|
||||
project is exported as they indicate how to treat the content in terms of text formatting,
|
||||
headings, and page breaks. The layout for each document is indicated as the last set of characters
|
||||
in the :guilabel:`Flags` column of the project tree.
|
||||
|
||||
Not all layout types are actually treated differently, they also help to indicate what each
|
||||
document is for in your project. The :guilabel:`Book` layout is a generic novel document layout
|
||||
that is formatted identically to :guilabel:`Chapter` and :guilabel:`Scene` layout documents, but
|
||||
may help to indicate what each document does in your project.
|
||||
document is intended for in your project. The :guilabel:`Book` layout is a generic novel document
|
||||
layout that is formatted identically to :guilabel:`Chapter` and :guilabel:`Scene` layout documents,
|
||||
but may help to indicate what each document does in your project.
|
||||
|
||||
You can for instance lay out your project using :guilabel:`Book` documents for each act, and then
|
||||
later split those into chapter or scene documents by using the :guilabel:`Split Document` tool.
|
||||
@@ -163,15 +171,15 @@ Some layouts *do* have implications on how the project is exported. Documents wi
|
||||
contained within it. The latter is convenient for Prologue and Epilogue type chapters.
|
||||
|
||||
The above layout formats are only usable in the Novel root folder. Documents that are not a part of
|
||||
the novel itself should have the Note layout. These documents are not getting any special
|
||||
formatting, and it is possible to collectively filter them out during export. Notes can be used
|
||||
anywhere in the project, also in the Novel root folder.
|
||||
the novel itself should have the :guilabel:`Note` layout. These documents are not getting any
|
||||
special formatting, and it is possible to collectively filter them out during export. Notes can be
|
||||
used anywhere in the project, also in the :guilabel:`Novel` root folder.
|
||||
|
||||
Below is an overview of all available layout formats.
|
||||
|
||||
:guilabel:`Title Page`
|
||||
The title page layout. The title should be formatted as a heading level one. All text is
|
||||
automatically centred on exports.
|
||||
The title page layout. The title should be formatted as a heading level one. All text is centred
|
||||
on export.
|
||||
|
||||
:guilabel:`Plain Page`
|
||||
A plain page layout useful for instance for front matter pages. Heading levels are ignored for
|
||||
@@ -184,7 +192,7 @@ Below is an overview of all available layout formats.
|
||||
produce the same result as a collection of :guilabel:`Partition`, :guilabel:`Chapter` and
|
||||
:guilabel:`Scene` layout documents. However, it does not provide the functionality of the
|
||||
:guilabel:`Unnumbered` layout format by default, but this can still be achieved by prefixing the
|
||||
chapter title with a ``*``.
|
||||
chapter title with an asterisk (``*``). See :ref:`a_struct_heads_unnum` for more details.
|
||||
|
||||
:guilabel:`Partition`
|
||||
A partition can be used to split the novel into parts. Partition titles are indicated with a
|
||||
@@ -200,8 +208,8 @@ Below is an overview of all available layout formats.
|
||||
text.
|
||||
|
||||
:guilabel:`Unnumbered`
|
||||
Same as :guilabel:`Chapter`, but when exporting the documents and automatic chapter numbering is
|
||||
enabled, documents with this layout will not increment the chapter number. It also has a
|
||||
Same as :guilabel:`Chapter`, but when exporting the project, and automatic chapter numbering is
|
||||
enabled, documents with this layout will not increment and apply a chapter number. It also has a
|
||||
separate title formatting setting. This makes the layout suitable for Prologue and Epilogue type
|
||||
chapters.
|
||||
|
||||
@@ -210,18 +218,33 @@ Below is an overview of all available layout formats.
|
||||
headers of level four, but there are no layout specifically for sections.
|
||||
|
||||
:guilabel:`Note`
|
||||
A generic document that is optionally ignored when the novel is exported. Use this layout for
|
||||
descriptions of content in the supporting root folders. Notes can also be added to the
|
||||
:guilabel:`Novel` root folder if you need to insert notes there. Note headers receive no special
|
||||
formatting when building the project. They are always exported as-is.
|
||||
A generic document that is optionally ignored when the novel project is exported. Use this
|
||||
layout for descriptions of content in the supporting root folders. Notes can also be added to
|
||||
the :guilabel:`Novel` root folder if you need to insert notes there. Note headers receive no
|
||||
special formatting when building the project. They are always exported as-is.
|
||||
|
||||
.. note::
|
||||
The layout granularity is entirely optional. In principle, you can write the entire novel in a
|
||||
single document with layout :guilabel:`Book`. You can also have a single document per chapter if
|
||||
that suits you better. The :guilabel:`Outline` will show your structure of chapters and scenes
|
||||
regardless of how your documents are organised.
|
||||
regardless of how your documents are organised. So will the novel tree.
|
||||
|
||||
.. tip::
|
||||
You can always start writing with a coarse layout with one or a few documents, and then later
|
||||
use the split tool to automatically split the documents into separate chapter and scene
|
||||
documents.
|
||||
documents. The split tool will try to select the correct layout for each of the new documents.
|
||||
|
||||
|
||||
.. _a_struct_layout_auto:
|
||||
|
||||
Novel Layout Automation
|
||||
-----------------------
|
||||
|
||||
When a document is saved from the document editor, novelWriter will check if the level of the first
|
||||
header of the document matches the selected document layout. If the header level is inconsistent
|
||||
with the layout, the layout will automatically be corrected if it can be unambiguously determined.
|
||||
|
||||
This means that for instance saving a document with header level three, when the layout is set to
|
||||
:guilabel:`Chapter`, the layout will automatically be changed to :guilabel:`Scene`. If, however,
|
||||
the layout was set to :guilabel:`Book`, no change will be made because the :guilabel:`Book` layout
|
||||
is not associated with a specific header level.
|
||||
@@ -17,7 +17,7 @@ This chapter provides some additional information on how novelWriter handles the
|
||||
Special Notes on Symbols
|
||||
========================
|
||||
|
||||
Some additional notes on these symbols.
|
||||
Some additional notes on the available symbols.
|
||||
|
||||
|
||||
Dashes and Ellipsis
|
||||
@@ -28,41 +28,58 @@ automatically to short and long dashes, and three dots to ellipsis. The last aut
|
||||
always be reverted with the undo command :kbd:`Ctrl`:kbd:`Z`, reverting the text to what you typed
|
||||
before the automatic replacement occurred.
|
||||
|
||||
In addition, "Figure Dash" is available. The Figure Dash is a dash that has the same width as the
|
||||
numbers of the same font, for most fonts. It helps to align numbers align nicely in columns when
|
||||
you need to use a dash in them.
|
||||
|
||||
|
||||
Single and Double Quotes
|
||||
------------------------
|
||||
|
||||
All the different quotation marks listed on the `Quotation Mark`_ Wikipedia page are available, and
|
||||
can be selected as auto-replaced symbols for straight single and double quote key strokes. The
|
||||
settings can be found in the :guilabel:`Preferences`.
|
||||
settings can be found in :guilabel:`Preferences`.
|
||||
|
||||
Ordinarily, text wrapped in quotes are highlighted by the editor. This is meant as a convenience
|
||||
for highlighting dialogue between characters. This feature can be disabled in the
|
||||
for highlighting dialogue between characters. This feature can be disabled in
|
||||
:guilabel:`Preferences` if this feature isn't wanted.
|
||||
|
||||
The editor distinguishes between text wrapped in straight quotes and with the user-selected double
|
||||
quote symbols. This is to help the writer recognise which parts of the text are not using the
|
||||
chosen quote symbols. Two convenience functions in the :guilabel:`Format` menu can be used to
|
||||
The editor distinguishes between text wrapped in regular straight quotes and with the user-selected
|
||||
double quote symbols. This is to help the writer recognise which parts of the text are not using
|
||||
the chosen quote symbols. Two convenience functions in the :guilabel:`Format` menu can be used to
|
||||
re-format a selected section of text with the correct quote symbols.
|
||||
|
||||
.. _Quotation Mark: https://en.wikipedia.org/wiki/Quotation_mark
|
||||
|
||||
|
||||
Modifier Letter Apostrophes
|
||||
---------------------------
|
||||
Single and Double Prime
|
||||
------------------------
|
||||
|
||||
Both single and double prime symbols are available in the :guilabel:`Insert` menu. These symbols
|
||||
are the correct symbols to use for unit symbols for feet, inches, minutes and seconds. The usage of
|
||||
these is described in more detail on the Wikipedia Prime_ page. They look very similar to single
|
||||
and double straight quotes, and may be renderred similarly by the font, but they have different
|
||||
codes. Using these correctly will also prevent the auto-replace and dialogue highlighting features
|
||||
misunderstanding their meaning in the text.
|
||||
|
||||
.. _Prime: https://en.wikipedia.org/wiki/Prime_(symbol)
|
||||
|
||||
|
||||
Modifier Letter Apostrophe
|
||||
--------------------------
|
||||
|
||||
The auto-replace feature will consider any right-facing single straight quote as a quote symbol,
|
||||
even if it's intended as an apostrophe. This also includes the syntax highlighter, which may assume
|
||||
the first following apostrophe is the closing symbol of a single quoted region of text.
|
||||
even if it is intended as an apostrophe. This also includes the syntax highlighter, which may
|
||||
assume the first following apostrophe is the closing symbol of a single quoted region of text.
|
||||
|
||||
To get around this, an alternative apostrophe is available. It is a special Unicode character that
|
||||
is not categorised as punctuation, but as a modifier. It is usually renderred the same way as right
|
||||
single quotation marks, but not always, depending on the font. There is a Wikipedia article for the
|
||||
is not categorised as punctuation, but as a modifier. It is usually renderred the same way as the
|
||||
right single quotation marks, depending on the font. There is a Wikipedia article for the
|
||||
`Modifier letter apostrophe`_ with more details.
|
||||
|
||||
.. note::
|
||||
On export with the :guilabel:`Build Novel Project` tool, these apostrophes will be replaced
|
||||
automatically with the corresponding right hand quote symbols as is generally recommended.
|
||||
automatically with the corresponding right hand single quote symbol as is generally recommended.
|
||||
Therefore it doesn't really matter if you only use them to correct highlighting.
|
||||
|
||||
.. _Modifier letter apostrophe: https://en.wikipedia.org/wiki/Modifier_letter_apostrophe
|
||||
@@ -82,6 +99,6 @@ All non-breaking spaces are highlighted with a differently coloured background t
|
||||
spot them in the text. The colour will depend on the selected colour theme.
|
||||
|
||||
The thin and non-breaking spaces are converted to their corresponding HTML codes on export to HTML
|
||||
format. For plain text, they are exported as regular spaces.
|
||||
format.
|
||||
|
||||
.. _thin space: https://en.wikipedia.org/wiki/Thin_space
|
||||
333
make.py
@@ -1,333 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This make script is intended for building distributable packages of
|
||||
novelWriter. These are either:
|
||||
|
||||
* A single file executable named dist/novelWriter(.exe). This is a
|
||||
quite slow option, and the file is fairly big.
|
||||
* A single directory named dist/novelWriter with a novelWriter(.exe),
|
||||
and all dependecies included.
|
||||
* The latter can be combined with a build stage of a setup.exe file if
|
||||
on Windows. This requires Inno Setup to be installed and in path.
|
||||
|
||||
In addition, providing the pip otion will cause the script to try to
|
||||
install all dependencies needed for runing the build, and for running
|
||||
novelWriter itself.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
OS_NONE = 0
|
||||
OS_LINUX = 1
|
||||
OS_WIN = 2
|
||||
OS_DARWIN = 3
|
||||
|
||||
# =============================================================================================== #
|
||||
# Package Installer
|
||||
# =============================================================================================== #
|
||||
|
||||
def installPackages(hostOS):
|
||||
"""Install package dependencies both for this script and for running
|
||||
novelWriter itself.
|
||||
"""
|
||||
print("")
|
||||
print("Installing Dependencies")
|
||||
print("#######################")
|
||||
print("")
|
||||
|
||||
installQueue = ["pip", "pyinstaller", "-r requirements.txt"]
|
||||
if hostOS == OS_DARWIN:
|
||||
installQueue.append("pyobjc")
|
||||
|
||||
pyCmd = [sys.executable, "-m"]
|
||||
pipCmd = ["pip", "install", "--user", "--upgrade"]
|
||||
for stepCmd in installQueue:
|
||||
pkgCmd = stepCmd.split(" ")
|
||||
try:
|
||||
subprocess.call(pyCmd + pipCmd + pkgCmd)
|
||||
except Exception as e:
|
||||
print("Failed with error:")
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
|
||||
return
|
||||
|
||||
# =============================================================================================== #
|
||||
# Run PyInstaller on Package
|
||||
# =============================================================================================== #
|
||||
|
||||
def freezePackage(buildWindowed, oneFile, makeSetup, hostOS):
|
||||
"""Run PyInstaller to freeze the packages. This assumes all
|
||||
dependencies are already in place.
|
||||
"""
|
||||
import PyInstaller.__main__ # noqa: E402
|
||||
|
||||
print("")
|
||||
print("Running PyInstaller")
|
||||
print("###################")
|
||||
print("")
|
||||
|
||||
if hostOS == OS_WIN:
|
||||
dotDot = ";"
|
||||
else:
|
||||
dotDot = ":"
|
||||
|
||||
sys.modules["FixTk"] = None
|
||||
instOpt = [
|
||||
"--name=novelWriter",
|
||||
"--clean",
|
||||
"--add-data=%s%s%s" % (os.path.join("nw", "assets"), dotDot, "assets"),
|
||||
"--icon=%s" % os.path.join("nw", "assets", "icons", "novelwriter.ico"),
|
||||
"--exclude-module=PyQt5.QtQml",
|
||||
"--exclude-module=PyQt5.QtBluetooth",
|
||||
"--exclude-module=PyQt5.QtDBus",
|
||||
"--exclude-module=PyQt5.QtMultimedia",
|
||||
"--exclude-module=PyQt5.QtMultimediaWidgets",
|
||||
"--exclude-module=PyQt5.QtNetwork",
|
||||
"--exclude-module=PyQt5.QtNetworkAuth",
|
||||
"--exclude-module=PyQt5.QtNfc",
|
||||
"--exclude-module=PyQt5.QtQuick",
|
||||
"--exclude-module=PyQt5.QtQuickWidgets",
|
||||
"--exclude-module=PyQt5.QtRemoteObjects",
|
||||
"--exclude-module=PyQt5.QtSensors",
|
||||
"--exclude-module=PyQt5.QtSerialPort",
|
||||
"--exclude-module=PyQt5.QtSql",
|
||||
"--exclude-module=FixTk",
|
||||
"--exclude-module=tcl",
|
||||
"--exclude-module=tk",
|
||||
"--exclude-module=_tkinter",
|
||||
"--exclude-module=tkinter",
|
||||
"--exclude-module=Tkinter",
|
||||
]
|
||||
|
||||
if buildWindowed:
|
||||
instOpt.append("--windowed")
|
||||
|
||||
if oneFile and not makeSetup:
|
||||
instOpt.append("--onefile")
|
||||
else:
|
||||
instOpt.append("--onedir")
|
||||
|
||||
instOpt.append("novelWriter.py")
|
||||
|
||||
# Make sample.zip first
|
||||
try:
|
||||
subprocess.call([sys.executable, "setup.py", "sample"])
|
||||
except Exception as e:
|
||||
print("Failed with error:")
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
|
||||
PyInstaller.__main__.run(instOpt)
|
||||
|
||||
if not oneFile:
|
||||
# These files are not needed, and take up a fair bit of space.
|
||||
delFiles = []
|
||||
if hostOS == OS_WIN:
|
||||
delFiles = [
|
||||
"Qt5DBus.dll",
|
||||
"Qt5Network.dll",
|
||||
"Qt5Qml.dll",
|
||||
"Qt5QmlModels.dll",
|
||||
"Qt5Quick.dll",
|
||||
"Qt5Quick3D.dll",
|
||||
"Qt5Quick3DAssetImport.dll",
|
||||
"Qt5Quick3DRender.dll",
|
||||
"Qt5Quick3DRuntimeRender.dll",
|
||||
"Qt5Quick3DUtils.dll",
|
||||
"Qt5Sql.dll"
|
||||
]
|
||||
elif hostOS == OS_LINUX:
|
||||
delFiles = [
|
||||
"libQt5DBus.so.5",
|
||||
"libQt5Network.so.5",
|
||||
"libQt5Qml.so.5",
|
||||
"libQt5QmlModels.so.5",
|
||||
"libQt5Quick.so.5",
|
||||
"libQt5Quick3D.so.5",
|
||||
"libQt5Quick3DAssetImport.so.5",
|
||||
"libQt5Quick3DRender.so.5",
|
||||
"libQt5Quick3DRuntimeRender.so.5",
|
||||
"libQt5Quick3DUtils.so.5",
|
||||
"libQt5Sql.so.5"
|
||||
]
|
||||
distDir = os.path.join(os.getcwd(), "dist", "novelWriter")
|
||||
for delFile in delFiles:
|
||||
delPath = os.path.join(distDir, delFile)
|
||||
if os.path.isfile(delPath):
|
||||
print("Deleting file: %s" % delPath)
|
||||
os.unlink(delPath)
|
||||
|
||||
print("")
|
||||
print("Build Finished")
|
||||
print("")
|
||||
print("The novelWriter executable should be in the folder named 'dist'")
|
||||
print("")
|
||||
|
||||
return
|
||||
|
||||
# =============================================================================================== #
|
||||
# Inno Setup Builder
|
||||
# =============================================================================================== #
|
||||
|
||||
def innoSetup():
|
||||
"""Run the Inno Setup tool to build a setup.exe file for Windows.
|
||||
"""
|
||||
print("")
|
||||
print("Running Inno Setup")
|
||||
print("##################")
|
||||
print("")
|
||||
|
||||
# Read the iss template
|
||||
issData = ""
|
||||
with open(os.path.join("setup", "win_setup.iss"), mode="r") as inFile:
|
||||
issData = inFile.read()
|
||||
|
||||
import nw # noqa: E402
|
||||
issData = issData.replace(r"%%version%%", nw.__version__)
|
||||
issData = issData.replace(r"%%dir%%", os.getcwd())
|
||||
|
||||
with open("setup.iss", mode="w+") as outFile:
|
||||
outFile.write(issData)
|
||||
|
||||
try:
|
||||
subprocess.call(["iscc", "setup.iss"])
|
||||
except Exception as e:
|
||||
print("Inno Setup failed with error:")
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
|
||||
return
|
||||
|
||||
# =============================================================================================== #
|
||||
# Clean Build and Dist Folders
|
||||
# =============================================================================================== #
|
||||
|
||||
def cleanInstall():
|
||||
"""Recursively delete the 'build' and 'dist' folders.
|
||||
"""
|
||||
print("")
|
||||
print("Cleaning up build environment ...")
|
||||
|
||||
buildDir = os.path.join(os.getcwd(), "build")
|
||||
if os.path.isdir(buildDir):
|
||||
try:
|
||||
shutil.rmtree(buildDir)
|
||||
print("Deleted folder 'build'")
|
||||
except Exception as e:
|
||||
print("Error: Cannot delete 'build' folder.")
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("Folder 'build' not found")
|
||||
|
||||
distDir = os.path.join(os.getcwd(), "dist")
|
||||
if os.path.isdir(distDir):
|
||||
try:
|
||||
shutil.rmtree(distDir)
|
||||
print("Deleted folder 'dist'")
|
||||
except Exception as e:
|
||||
print("Error: Cannot delete 'dist' folder.")
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("Folder 'dist' not found")
|
||||
|
||||
print("")
|
||||
|
||||
return
|
||||
|
||||
# =============================================================================================== #
|
||||
# Process Build Steps
|
||||
# =============================================================================================== #
|
||||
|
||||
if __name__ == "__main__":
|
||||
"""Parse command line options and run the commands.
|
||||
"""
|
||||
# Detect OS
|
||||
if sys.platform.startswith("linux"):
|
||||
hostOS = OS_LINUX
|
||||
elif sys.platform.startswith("darwin"):
|
||||
hostOS = OS_DARWIN
|
||||
elif sys.platform.startswith("win32"):
|
||||
hostOS = OS_WIN
|
||||
elif sys.platform.startswith("cygwin"):
|
||||
hostOS = OS_WIN
|
||||
else:
|
||||
hostOS = OS_NONE
|
||||
|
||||
# Flags and Variables
|
||||
buildWindowed = True
|
||||
oneFile = False
|
||||
makeSetup = False
|
||||
doFreeze = False
|
||||
|
||||
helpMsg = (
|
||||
"\n"
|
||||
"novelWriter Make Tool\n"
|
||||
"=====================\n"
|
||||
"This tool provides build commands for distibuting novelWriter as a\n"
|
||||
"package. The available options are as follows:\n"
|
||||
"\n"
|
||||
"help Print the help message.\n"
|
||||
"freeze Freeze the package and produces a folder of all\n"
|
||||
" dependencies using pyinstaller.\n"
|
||||
"onefile Build a standalone executable with all dependencies\n"
|
||||
" bundled. Implies 'freeze', cannot be used with 'setup'.\n"
|
||||
"pip Run pip to install all package dependencies for\n"
|
||||
" novelWriter and this build tool.\n"
|
||||
"setup Build a setup.exe installer for Windows. This option\n"
|
||||
" automaticall disables the 'onefile' option.\n"
|
||||
"clean This will attempt to delete the 'build' and 'dist'\n"
|
||||
" folders in the current folder.\n"
|
||||
)
|
||||
|
||||
if "help" in sys.argv or len(sys.argv) <= 1:
|
||||
print(helpMsg)
|
||||
sys.exit(0)
|
||||
|
||||
if not os.path.isfile(os.path.join(os.getcwd(), "novelWriter.py")):
|
||||
print("Error: This script must be run in the root folder of novelWriter.")
|
||||
sys.exit(1)
|
||||
|
||||
if not os.path.isdir(os.path.join(os.getcwd(), "nw")):
|
||||
print("Error: This script must be run in the root folder of novelWriter.")
|
||||
sys.exit(1)
|
||||
|
||||
if "clean" in sys.argv:
|
||||
sys.argv.remove("clean")
|
||||
cleanInstall()
|
||||
|
||||
if "pip" in sys.argv:
|
||||
sys.argv.remove("pip")
|
||||
installPackages(hostOS)
|
||||
|
||||
if "freeze" in sys.argv:
|
||||
sys.argv.remove("freeze")
|
||||
doFreeze = True
|
||||
|
||||
if "onefile" in sys.argv:
|
||||
sys.argv.remove("onefile")
|
||||
doFreeze = True
|
||||
oneFile = True
|
||||
|
||||
if "setup" in sys.argv:
|
||||
sys.argv.remove("setup")
|
||||
if hostOS == OS_WIN:
|
||||
oneFile = False
|
||||
makeSetup = True
|
||||
else:
|
||||
print("Error: Argument 'setup' for Inno Setup is Windows only.")
|
||||
sys.exit(1)
|
||||
|
||||
if doFreeze:
|
||||
freezePackage(buildWindowed, oneFile, makeSetup, hostOS)
|
||||
|
||||
if makeSetup:
|
||||
innoSetup()
|
||||
|
||||
# END Main
|
||||
@@ -1,6 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
novelWriter – Start Script
|
||||
==========================
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
try:
|
||||
@@ -8,9 +12,11 @@ try:
|
||||
import PyQt5.QtGui # noqa: F401
|
||||
import PyQt5.QtCore # noqa: F401
|
||||
except Exception:
|
||||
print("ERROR: Failed to load dependency python3-pyqt5")
|
||||
print("ERROR: Failed to load dependency PyQt5")
|
||||
sys.exit(1)
|
||||
|
||||
os.curdir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
if __name__ == "__main__":
|
||||
import nw
|
||||
nw.main(sys.argv[1:])
|
||||
|
||||
133
nw/__init__.py
@@ -1,28 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Init
|
||||
"""
|
||||
novelWriter – Init File
|
||||
=======================
|
||||
Application initialisation
|
||||
|
||||
novelWriter – Init File
|
||||
=========================
|
||||
Application initialisation
|
||||
File History:
|
||||
Created: 2018-09-22 [0.0.1]
|
||||
|
||||
File History:
|
||||
Created: 2018-09-22 [0.0.1]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import sys
|
||||
@@ -32,19 +31,19 @@ import logging
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QApplication, QErrorMessage
|
||||
|
||||
from nw.error import exceptionHandler
|
||||
from nw.error import exceptionHandler, logException
|
||||
from nw.config import Config
|
||||
|
||||
#
|
||||
##
|
||||
# Version Scheme
|
||||
# ================
|
||||
# Generally follows PEP 440
|
||||
# Hex Version:
|
||||
# - Digit 1,2 : Major Version (01, 02, 03)
|
||||
# = Digit 3,4 : Minor Version (01, 09, 10, 99)
|
||||
# - Digit 5,6 : Patch Version (01, 09, 10, 99)
|
||||
# - Digit 1,2 : Major Version (01-ff)
|
||||
# = Digit 3,4 : Minor Version (01-ff)
|
||||
# - Digit 5,6 : Patch Version (01-ff)
|
||||
# = Digit 7 : Release Type (a: aplha, b: beta, c: candidate, f: final)
|
||||
# - Digit 8 : Release Number (0-9)
|
||||
# - Digit 8 : Release Number (0-f)
|
||||
#
|
||||
# Example : Full Short Description
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -55,7 +54,7 @@ from nw.config import Config
|
||||
# 0x010200f0 : 1.2 1.2 Final release
|
||||
# 0x010200f1 : 1.2-post1 1.2.post1 Post release, but not a code patch!
|
||||
# 0x010201f0 : 1.2.1 1.2.1 Patch release
|
||||
#
|
||||
##
|
||||
|
||||
__package__ = "nw"
|
||||
__copyright__ = "Copyright 2018–2021, Veronica Berglyd Olsen"
|
||||
@@ -63,9 +62,9 @@ __license__ = "GPLv3"
|
||||
__author__ = "Veronica Berglyd Olsen"
|
||||
__maintainer__ = "Veronica Berglyd Olsen"
|
||||
__email__ = "code@vkbo.net"
|
||||
__version__ = "1.0"
|
||||
__hexversion__ = "0x010000f0"
|
||||
__date__ = "2021-01-03"
|
||||
__version__ = "1.2"
|
||||
__hexversion__ = "0x010200f0"
|
||||
__date__ = "2021-03-14"
|
||||
__status__ = "Stable"
|
||||
__domain__ = "novelwriter.io"
|
||||
__url__ = "https://novelwriter.io"
|
||||
@@ -79,7 +78,7 @@ __credits__ = [
|
||||
"Marian Lückhof (contributor, tester)"
|
||||
]
|
||||
|
||||
#
|
||||
##
|
||||
# Logging
|
||||
# =========
|
||||
# Standard used for logging levels in novelWriter:
|
||||
@@ -90,7 +89,7 @@ __credits__ = [
|
||||
# ----------- SPAM Threshold : Output above should be minimal -----------------
|
||||
# DEBUG Use for descriptions of main program flow
|
||||
# VERBOSE Use for outputting values and program flow details
|
||||
#
|
||||
##
|
||||
|
||||
# Add verbose logging level
|
||||
VERBOSE = 5
|
||||
@@ -104,10 +103,9 @@ logging.Logger.verbose = logVerbose
|
||||
# Initiating logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
#
|
||||
##
|
||||
# Main Program
|
||||
# ==============
|
||||
#
|
||||
##
|
||||
|
||||
# Load the main config as a global object
|
||||
CONFIG = Config()
|
||||
@@ -133,7 +131,7 @@ def main(sysArgs=None):
|
||||
]
|
||||
|
||||
helpMsg = (
|
||||
"novelWriter {version} ({status} {date})\n"
|
||||
"novelWriter {version} ({date})\n"
|
||||
"{copyright}\n"
|
||||
"\n"
|
||||
"This program is distributed in the hope that it will be useful,\n"
|
||||
@@ -142,30 +140,28 @@ def main(sysArgs=None):
|
||||
"GNU General Public License for more details.\n"
|
||||
"\n"
|
||||
"Usage:\n"
|
||||
" -h, --help Print this message.\n"
|
||||
" -v, --version Print program version and exit.\n"
|
||||
" --info Print additional runtime information.\n"
|
||||
" --debug Print debug output. Includes --info.\n"
|
||||
" --verbose Increase verbosity of debug output. Includes --debug.\n"
|
||||
" --style= Sets Qt5 style flag. Defaults to 'Fusion'.\n"
|
||||
" --config= Alternative config file.\n"
|
||||
" --data= Alternative user data path.\n"
|
||||
" --testmode Do not display GUI. Used by the test suite.\n"
|
||||
" -h, --help Print this message.\n"
|
||||
" -v, --version Print program version and exit.\n"
|
||||
" --info Print additional runtime information.\n"
|
||||
" --debug Print debug output. Includes --info.\n"
|
||||
" --verbose Increase verbosity of debug output. Includes --debug.\n"
|
||||
" --style= Sets Qt5 style flag. Defaults to 'Fusion'.\n"
|
||||
" --config= Alternative config file.\n"
|
||||
" --data= Alternative user data path.\n"
|
||||
).format(
|
||||
version = __version__,
|
||||
status = __status__,
|
||||
copyright = __copyright__,
|
||||
date = __date__,
|
||||
)
|
||||
|
||||
# Defaults
|
||||
debugLevel = logging.WARN
|
||||
logFormat = "{levelname:8} {message:}"
|
||||
confPath = None
|
||||
dataPath = None
|
||||
testMode = False
|
||||
qtStyle = "Fusion"
|
||||
cmdOpen = None
|
||||
logLevel = logging.WARN
|
||||
logFormat = "{levelname:8} {message:}"
|
||||
confPath = None
|
||||
dataPath = None
|
||||
testMode = False
|
||||
qtStyle = "Fusion"
|
||||
cmdOpen = None
|
||||
|
||||
# Parse Options
|
||||
try:
|
||||
@@ -183,17 +179,15 @@ def main(sysArgs=None):
|
||||
print(helpMsg)
|
||||
sys.exit(0)
|
||||
elif inOpt in ("-v", "--version"):
|
||||
print("novelWriter %s Version %s [%s]" % (
|
||||
__status__, __version__, __date__)
|
||||
)
|
||||
print("novelWriter Version %s [%s]" % (__version__, __date__))
|
||||
sys.exit(0)
|
||||
elif inOpt == "--info":
|
||||
debugLevel = logging.INFO
|
||||
logLevel = logging.INFO
|
||||
elif inOpt == "--debug":
|
||||
debugLevel = logging.DEBUG
|
||||
logLevel = logging.DEBUG
|
||||
logFormat = "[{asctime:}] {name:>22}:{lineno:<4d} {levelname:8} {message:}"
|
||||
elif inOpt == "--verbose":
|
||||
debugLevel = VERBOSE
|
||||
logLevel = VERBOSE
|
||||
logFormat = "[{asctime:}] {name:>22}:{lineno:<4d} {levelname:8} {message:}"
|
||||
elif inOpt == "--style":
|
||||
qtStyle = inArg
|
||||
@@ -205,18 +199,16 @@ def main(sysArgs=None):
|
||||
testMode = True
|
||||
|
||||
# Set Config Options
|
||||
CONFIG.showGUI = not testMode
|
||||
CONFIG.debugInfo = debugLevel < logging.INFO
|
||||
CONFIG.cmdOpen = cmdOpen
|
||||
CONFIG.cmdOpen = cmdOpen
|
||||
|
||||
# Set Logging
|
||||
logFmt = logging.Formatter(fmt=logFormat, style="{")
|
||||
cHandle = logging.StreamHandler()
|
||||
cHandle.setLevel(debugLevel)
|
||||
cHandle.setFormatter(logFmt)
|
||||
logger.addHandler(cHandle)
|
||||
cHandle.setFormatter(logging.Formatter(fmt=logFormat, style="{"))
|
||||
|
||||
pkgLogger = logging.getLogger(__package__)
|
||||
pkgLogger.addHandler(cHandle)
|
||||
pkgLogger.setLevel(logLevel)
|
||||
|
||||
logger.setLevel(debugLevel)
|
||||
logger.info("Starting novelWriter %s (%s) %s" % (
|
||||
__version__, __hexversion__, __date__
|
||||
))
|
||||
@@ -272,9 +264,9 @@ def main(sysArgs=None):
|
||||
bundle = NSBundle.mainBundle()
|
||||
info = bundle.localizedInfoDictionary() or bundle.infoDictionary()
|
||||
info["CFBundleName"] = "novelWriter"
|
||||
except ImportError as e:
|
||||
except ImportError:
|
||||
logger.error("Failed to set application name")
|
||||
logger.error(str(e))
|
||||
logException()
|
||||
|
||||
# Import GUI (after dependency checks), and launch
|
||||
from nw.guimain import GuiMain
|
||||
@@ -292,7 +284,12 @@ def main(sysArgs=None):
|
||||
# Connect the exception handler before making the main GUI
|
||||
sys.excepthook = exceptionHandler
|
||||
|
||||
# Launch main GUI
|
||||
nwGUI = GuiMain()
|
||||
if not nwGUI.hasProject:
|
||||
nwGUI.showProjectLoadDialog()
|
||||
nwGUI.releaseNotes()
|
||||
|
||||
sys.exit(nwApp.exec_())
|
||||
|
||||
# END Function main
|
||||
|
||||
31
nw/assets/icons/fallback/settings-dark.svg
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.2"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg1989">
|
||||
<metadata
|
||||
id="metadata1995">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs1993" />
|
||||
<path
|
||||
d="M 8.3268645,20.0768 8.9719703,22.448 C 9.2032354,23.3008 10.175241,24 11.131598,24 h 1.738831 c 0.956356,0 1.928363,-0.6992 2.159627,-1.552 l 0.645108,-2.3712 c 0.231264,-0.8528 1.173709,-1.3536 2.091812,-1.1104 l 2.550866,0.672 c 0.91984,0.2416 2.06573,-0.1824 2.542169,-0.9456 l 0.869415,-1.3872 c 0.476441,-0.7632 0.307773,-1.8864 -0.380803,-2.4992 l -1.909236,-1.6976 c -0.688578,-0.6128 -0.688578,-1.6128 0.0017,-2.224 L 23.346891,9.1872 C 24.035507,8.5759996 24.205912,7.4527996 23.729472,6.6895996 L 22.858272,5.3024 C 22.380094,4.5392 21.235943,4.1152 20.317842,4.3568 l -2.550866,0.672 C 16.847134,5.2704 15.906428,4.7712 15.673424,3.9184 L 15.030056,1.5504 C 14.798792,0.69919996 13.826785,0 12.870429,0 H 11.131598 C 10.175241,0 9.2032354,0.69919996 8.9719703,1.552 L 8.3286034,3.92 C 8.0955994,4.7728 7.1548921,5.2736 6.2350507,5.032 L 3.684186,4.36 C 2.7643447,4.1168 1.6184552,4.5424 1.1420155,5.304 L 0.27260019,6.6912 c -0.47643954,0.7632 -0.307773,1.8864 0.38254277,2.4991996 L 2.5609015,10.8848 c 0.6868381,0.6128 0.6868381,1.6128 0,2.2256 L 0.65166536,14.808 c -0.68683817,0.6128 -0.85898242,1.736 -0.38080407,2.4992 l 0.87115421,1.3872 c 0.4764397,0.7632 1.6223292,1.1872 2.5421705,0.9456 l 2.5508647,-0.672 c 0.9181025,-0.2448 1.8605487,0.256 2.0918138,1.1088 z M 12.001014,8.8000004 c 1.919668,0 3.477662,1.4319996 3.477662,3.1999996 0,1.7664 -1.557994,3.2 -3.477662,3.2 -1.91967,0 -3.4776615,-1.4336 -3.4776615,-3.2 0,-1.768 1.5579915,-3.1999996 3.4776615,-3.1999996 z"
|
||||
id="path1987"
|
||||
style="fill:#aeaeae;fill-opacity:1;stroke-width:1.66797" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
31
nw/assets/icons/fallback/settings.svg
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.2"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg1989">
|
||||
<metadata
|
||||
id="metadata1995">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs1993" />
|
||||
<path
|
||||
d="M 8.3268645,20.0768 8.9719703,22.448 C 9.2032354,23.3008 10.175241,24 11.131598,24 h 1.738831 c 0.956356,0 1.928363,-0.6992 2.159627,-1.552 l 0.645108,-2.3712 c 0.231264,-0.8528 1.173709,-1.3536 2.091812,-1.1104 l 2.550866,0.672 c 0.91984,0.2416 2.06573,-0.1824 2.542169,-0.9456 l 0.869415,-1.3872 c 0.476441,-0.7632 0.307773,-1.8864 -0.380803,-2.4992 l -1.909236,-1.6976 c -0.688578,-0.6128 -0.688578,-1.6128 0.0017,-2.224 L 23.346891,9.1872 C 24.035507,8.5759996 24.205912,7.4527996 23.729472,6.6895996 L 22.858272,5.3024 C 22.380094,4.5392 21.235943,4.1152 20.317842,4.3568 l -2.550866,0.672 C 16.847134,5.2704 15.906428,4.7712 15.673424,3.9184 L 15.030056,1.5504 C 14.798792,0.69919996 13.826785,0 12.870429,0 H 11.131598 C 10.175241,0 9.2032354,0.69919996 8.9719703,1.552 L 8.3286034,3.92 C 8.0955994,4.7728 7.1548921,5.2736 6.2350507,5.032 L 3.684186,4.36 C 2.7643447,4.1168 1.6184552,4.5424 1.1420155,5.304 L 0.27260019,6.6912 c -0.47643954,0.7632 -0.307773,1.8864 0.38254277,2.4991996 L 2.5609015,10.8848 c 0.6868381,0.6128 0.6868381,1.6128 0,2.2256 L 0.65166536,14.808 c -0.68683817,0.6128 -0.85898242,1.736 -0.38080407,2.4992 l 0.87115421,1.3872 c 0.4764397,0.7632 1.6223292,1.1872 2.5421705,0.9456 l 2.5508647,-0.672 c 0.9181025,-0.2448 1.8605487,0.256 2.0918138,1.1088 z M 12.001014,8.8000004 c 1.919668,0 3.477662,1.4319996 3.477662,3.1999996 0,1.7664 -1.557994,3.2 -3.477662,3.2 -1.91967,0 -3.4776615,-1.4336 -3.4776615,-3.2 0,-1.768 1.5579915,-3.1999996 3.4776615,-3.1999996 z"
|
||||
id="path1987"
|
||||
style="fill:#000000;fill-opacity:0.721569;stroke-width:1.66797" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
31
nw/assets/icons/fallback/status_idle-dark.svg
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.2"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg5582">
|
||||
<metadata
|
||||
id="metadata5588">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs5586" />
|
||||
<path
|
||||
d="m 6.7499997,3 c -1.656,0 -3,1.3440001 -3,3.0000001 V 17.999999 c 0,1.656 1.344,3.000001 3,3.000001 1.6560002,0 2.9999998,-1.344001 2.9999998,-3.000001 V 6.0000001 C 9.7499995,4.3440001 8.4059999,3 6.7499997,3 Z M 17.249999,3 c -1.656,0 -3.000001,1.3440001 -3.000001,3.0000001 V 17.999999 c 0,1.656 1.344001,3.000001 3.000001,3.000001 1.656,0 3.000001,-1.344001 3.000001,-3.000001 V 6.0000001 C 20.25,4.3440001 18.905999,3 17.249999,3 Z"
|
||||
id="path5580"
|
||||
style="fill:#aeaeae;fill-opacity:1;stroke-width:1.49999" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
31
nw/assets/icons/fallback/status_idle.svg
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.2"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg5582">
|
||||
<metadata
|
||||
id="metadata5588">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs5586" />
|
||||
<path
|
||||
d="m 6.7499997,3 c -1.656,0 -3,1.3440001 -3,3.0000001 V 17.999999 c 0,1.656 1.344,3.000001 3,3.000001 1.6560002,0 2.9999998,-1.344001 2.9999998,-3.000001 V 6.0000001 C 9.7499995,4.3440001 8.4059999,3 6.7499997,3 Z M 17.249999,3 c -1.656,0 -3.000001,1.3440001 -3.000001,3.0000001 V 17.999999 c 0,1.656 1.344001,3.000001 3.000001,3.000001 1.656,0 3.000001,-1.344001 3.000001,-3.000001 V 6.0000001 C 20.25,4.3440001 18.905999,3 17.249999,3 Z"
|
||||
id="path5580"
|
||||
style="fill:#000000;fill-opacity:0.780392;stroke-width:1.49999" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
31
nw/assets/icons/typicons_colour_dark/heading0.svg
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg1897"
|
||||
viewBox="0 0 24 24"
|
||||
height="24"
|
||||
width="24"
|
||||
version="1.2">
|
||||
<metadata
|
||||
id="metadata1903">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs1901" />
|
||||
<path
|
||||
style="fill:#6699cc;fill-opacity:1;stroke-width:1.42859697"
|
||||
id="path1895"
|
||||
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -29,6 +29,7 @@ cls_archive = delete.svg
|
||||
cls_trash = trash.svg
|
||||
proj_document = document-text.svg
|
||||
proj_folder = folder.svg
|
||||
doc_h0 = heading0.svg
|
||||
doc_h1 = heading1.svg
|
||||
doc_h2 = heading2.svg
|
||||
doc_h3 = heading3.svg
|
||||
|
||||
31
nw/assets/icons/typicons_colour_light/heading0.svg
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg1897"
|
||||
viewBox="0 0 24 24"
|
||||
height="24"
|
||||
width="24"
|
||||
version="1.2">
|
||||
<metadata
|
||||
id="metadata1903">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs1901" />
|
||||
<path
|
||||
style="fill:#4271ae;fill-opacity:1;stroke-width:1.42859697"
|
||||
id="path1895"
|
||||
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -29,6 +29,7 @@ cls_archive = delete.svg
|
||||
cls_trash = trash.svg
|
||||
proj_document = document-text.svg
|
||||
proj_folder = folder.svg
|
||||
doc_h0 = heading0.svg
|
||||
doc_h1 = heading1.svg
|
||||
doc_h2 = heading2.svg
|
||||
doc_h3 = heading3.svg
|
||||
|
||||
31
nw/assets/icons/typicons_grey_dark/heading0.svg
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg1897"
|
||||
viewBox="0 0 24 24"
|
||||
height="24"
|
||||
width="24"
|
||||
version="1.2">
|
||||
<metadata
|
||||
id="metadata1903">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs1901" />
|
||||
<path
|
||||
style="fill:#aeaeae;fill-opacity:1;stroke-width:1.42859697"
|
||||
id="path1895"
|
||||
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -29,6 +29,7 @@ cls_archive = delete.svg
|
||||
cls_trash = trash.svg
|
||||
proj_document = document-text.svg
|
||||
proj_folder = folder.svg
|
||||
doc_h0 = heading0.svg
|
||||
doc_h1 = heading1.svg
|
||||
doc_h2 = heading2.svg
|
||||
doc_h3 = heading3.svg
|
||||
|
||||
31
nw/assets/icons/typicons_grey_light/heading0.svg
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg1897"
|
||||
viewBox="0 0 24 24"
|
||||
height="24"
|
||||
width="24"
|
||||
version="1.2">
|
||||
<metadata
|
||||
id="metadata1903">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs1901" />
|
||||
<path
|
||||
style="fill:#000000;fill-opacity:0.78039217;stroke-width:1.42859697"
|
||||
id="path1895"
|
||||
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -29,6 +29,7 @@ cls_archive = delete.svg
|
||||
cls_trash = trash.svg
|
||||
proj_document = document-text.svg
|
||||
proj_folder = folder.svg
|
||||
doc_h0 = heading0.svg
|
||||
doc_h1 = heading1.svg
|
||||
doc_h2 = heading2.svg
|
||||
doc_h3 = heading3.svg
|
||||
|
||||
@@ -2,90 +2,84 @@
|
||||
<html>
|
||||
<body>
|
||||
|
||||
<h2>Release Notes for 1.0</h2>
|
||||
<p><i>Released on 3 January 2021</i></p>
|
||||
<p>Based on my own testing and usage, and no serious bugs discovered in quite some time (aside from
|
||||
a few corner case issues), it appears that novelWriter is stable enough for a 1.0 release. Thanks
|
||||
to all the new users who keep providing feedback on bugs, cosmetic issues, or suggesting
|
||||
improvements and new features. I'm glad to hear that others find my application useful, and I will
|
||||
keep making improvements as I get new suggestions and have new ideas myself. At the same time, I
|
||||
will continue to keep novelWriter simple and clean and avoid feature-bloat.</p>
|
||||
<p>This release mainly fixes cosmetic and other minor issues to the user interface and makes a few
|
||||
minor improvements to some less used features. Aside from this, nothing major has changed since the
|
||||
last release candidate.</p>
|
||||
<p>This release concludes over two years of tinkering with this project. The project grew out of
|
||||
numerous lunch and coffee discussions with my colleague Marian Lückhof at my former job. We were
|
||||
both looking for a tool for writing novels on Linux that suited our needs. We started assembling a
|
||||
wish list of features that has become novelWriter. In addition, users on GitHub have continued to
|
||||
test new features, provide very helpful feedback, and make new suggestions for improvements.
|
||||
Especially the feedback from @johnblommers has been helpful during much of the initial development
|
||||
time. Over the last months more users have started posting ideas and feedback. Thanks to all of you
|
||||
for your contributions.</p>
|
||||
<p>The 1.0 release is intended as a first release of the core features of novelWriter. That does
|
||||
not mean that all planned features have been fully implemented. There is a long list of ideas and
|
||||
suggestions to consider and implement. New ideas and suggestions are welcome. Either as feature
|
||||
requests in the issue tracker, or if not fully formed, can be discussed on the
|
||||
<a href="https://github.com/vkbo/novelWriter/discussions">discussions page</a>.
|
||||
<h2>Release Notes for 1.2</h2>
|
||||
<p><i>Released on 14 March 2021</i></p>
|
||||
<p>This release is mainly focused on the Build Novel Project tool. Completely new export classes
|
||||
have been written to support Open Document and Markdown exports. In addition, the way document
|
||||
layouts are handled have been automated a little to assist the user in keeping header levels and
|
||||
document layout flags in sync. The third new additoion is the ability to record and log idle time
|
||||
during a writing session to improve the writing statistics information as requested by several
|
||||
users. Finally, it is now possible to directly edit the project dictionary via a new, simple GUI
|
||||
dialog.</p>
|
||||
|
||||
<p><i>The full changelog is available
|
||||
<a href="https://github.com/vkbo/novelWriter/blob/main/CHANGELOG.md">here</a>.</i></p>
|
||||
<h3>The Build Novel Project Tool</h3>
|
||||
<p>The main changes for this release are to the Build Novel Project tool. The Open Document export,
|
||||
as well as the Markdown export, is now handled entirely by code written for novelWriter.
|
||||
Previously, these export features depended on the underlying Qt library's save routines connected
|
||||
to the preview document shown in the build dialog. Using this method of export both meant that the
|
||||
content of the document was dependent on the preview being generated first, and it also meant that
|
||||
the exported document had limited support for novelWriter-specific features and custom formatting.
|
||||
The new export class should generate a much better result, especially for the Open Document
|
||||
formats. The Open Document standard is supported by Open Office, Libre Office, Google Docs,
|
||||
Microsoft Word, and probably a number of other applications too. The Markdown export hasn't changed
|
||||
a lot, but should be a slight improvement on the previous export feature.</p>
|
||||
<p>These changes to the build tool also imply that the saving process is now independent of the
|
||||
content of the preview window, meaning you don't have to rebuild the preview before saving, which
|
||||
was previously the case. To make this more consistent, the PDF export option has been moved to the
|
||||
print button as it is actually a print-to-file feature under the hood, not technically a proper PDF
|
||||
export format. It is exactly the same as printing to file from the print preview dialog.</p>
|
||||
<p>In addition to the changes to the export features, the Build Novel Project tool now also has
|
||||
controls for line height, which applies to all rich text export formats, and the option to replace
|
||||
unicode characters with appropriate html entities for html export.</p>
|
||||
|
||||
<hr/>
|
||||
<h3>Document Layout Automation</h3>
|
||||
<p>Among other changes in this release are a few improvements to the process of creating and
|
||||
changing documents. When a new document is first created, the header is generated from the assigned
|
||||
label and layout.</p>
|
||||
<p>In addition, for some document layouts, when the user changes the header level of the first
|
||||
header of the document, the document layout setting is updated accordingly. This should reduce the
|
||||
need for the user to maintain two ways of assigning the role of a given document. This automation
|
||||
only applies to combinations of header level and current document layouts where there is no
|
||||
ambiguity. For instance, changing the header level in a "Scene" document from level 3 to 2 changes
|
||||
the document layout automatically to "Chapter". But changing the first header of a "Book" layout
|
||||
document from 1 to 2 does not change the document's layout as the "Book" layout is a generic
|
||||
document layout and it's perfectly reasonable for its first header to be a chapter header.</p>
|
||||
<p>Keep in mind that novelWriter treats documents with layout "Book", "Chapter" and "Scene" exactly
|
||||
the same during exports. The distinction is only meant as a way to indicate the purpose of a
|
||||
document in the project tree. This new automation is meant to assist in keeping this information up
|
||||
to date. The other layouts do have an effect on formatting during export, and are generally left
|
||||
alone.</p>
|
||||
|
||||
<h2>Release Notes for 1.0 RC2</h2>
|
||||
<p><i>Released on 13 December 2020</i></p>
|
||||
<p>This second release candidate for 1.0 comes with only minor changes and improvements, and a
|
||||
handful of minor bugfixes.</p>
|
||||
<p>Among the improvements is the addition of all the possible @-keywords for tags and references to
|
||||
the "Insert" menu under the sub-menu "Tags and References". The "Help" menu has also received a few
|
||||
improvements and additional links to useful webpages. This release also adds a "Release" notes tab
|
||||
to the "About novelWriter" dialog. The release notes are displayed automatically the first time you
|
||||
launch novelWriter after updating to a new version.</p>
|
||||
<p>Among the fixes is better support for high resolution screens. A few elements on the GUI did not
|
||||
scale properly, among them the document editor and viewer header and footer. These were clipped on
|
||||
high res screens due to an underlying issue with the Qt widget underestimating how much space it
|
||||
required to accommodate the text. Unfortunately, dragging the novelWriter app between screens of
|
||||
different scaling factors is not currently supported. However, the GUI should scale properly to the
|
||||
scaling factor on the screen it is opened on.</p>
|
||||
<p>The work leading up to this release has mostly been focused on improving the test coverage of
|
||||
the source code of novelWriter. This helps to ensure that the code does what it is intended to do,
|
||||
and is able to handle corner cases and unexpected external errors and user actions that may occur.
|
||||
While writing these tests, a number of minor potential issues have been uncovered and handled. Most
|
||||
of these are corner cases that may not even be reachable by unexpected user actions.</p>
|
||||
<p>Hopefully, these changes have resulted in an even more stable version of novelWriter. If no more
|
||||
issues are discovered, the next release will be the final version 1.0 release.</p>
|
||||
<h3>The Session Timer and Idle Time</h3>
|
||||
<p>Another change that has been requested by a couple of users is to have the session timer in the
|
||||
status bar stop counting when the user is inactive (idle). This feature is optional, and can be
|
||||
controlled from Preferences. The definition of "idle" in this context is either that the user is
|
||||
active in a different application than novelWriter (loss of focus) or that the user has not made
|
||||
any changes to the current document for a given amount of time. The time threshold is by default
|
||||
five minutes, but can be altered in Preferences.</p>
|
||||
<p>In addition, the idle time is also recorded in the session log, and can be viewed in the Writing
|
||||
Statistics dialog and exported with the rest of the information. The idle time is recorder in the
|
||||
logs regardless of whether the status bar clock takes idle time into consideration or not. So even
|
||||
if you turn off the idle time switch in Preferences, the other idle time setting still affects the
|
||||
writing stats log entries.</p>
|
||||
|
||||
<hr/>
|
||||
<h3>Other Changes</h3>
|
||||
<p>The user dictionary of the project, where words added to the dictionary from the document editor
|
||||
go, can now be viewed and edited with a new "Project Word List" tool in the "Tools" menu.</p>
|
||||
<p>A small additional feature added is also the ability to undo the last move of an item in the
|
||||
project tree. The keyboard shortcut for this is `Ctrl+Shift+Z`, or it can be accessed from the
|
||||
menu. The feature can only undo the last move, but it includes both documents moved to trash, moves
|
||||
by up/down keypress or menu entries, and drag and drop moves.</p>
|
||||
<p>A new keyword has been added to mark characters in the story. The new keyword is intended to tag
|
||||
a character as the focus character for a chapter or scene. This is useful for stories where the
|
||||
point-of-view character and the focus character are different.</p>
|
||||
<p>Lastly, two bugfixes have been made as well. The Empty Trash feature was no longer working due
|
||||
to an earlier fix solving another issue. The feature has now been restored. In addition, the
|
||||
indexer now checks that a keyword (tag or reference) is valid before saving it to the index.
|
||||
Previously, an invalid keyword could be saved to the index and potentially crash the application.
|
||||
</p>
|
||||
|
||||
<h2>Release Notes for 1.0 RC1</h2>
|
||||
<p><i>Released on 16 November 2020</i></p>
|
||||
<p>This is the first release candidate for the upcoming release of novelWriter 1.0.</p>
|
||||
<p>Since the fifth beta release about four weeks ago, not much has been changed in novelWriter. A
|
||||
few minor tweaks have been made to the GUI.</p>
|
||||
<p>A number of features and tools are now automatically switched off when there is no project or
|
||||
document open for those features to act upon. Previously, this was a bit inconsistent, although no
|
||||
serious bugs have been reported or encountered.</p>
|
||||
<p>Most of the minor changes in this release should not be noticeable to most users. However, there
|
||||
are a couple of noticeable changes.</p>
|
||||
<h3>Typewriter Mode</h3>
|
||||
<p>The "Typewriter Mode" of the editor has been improved. Essentially, this feature is a sort of
|
||||
smart scroll. It tries to keep the cursor stationary in the vertical direction, and will try to
|
||||
scroll the document up when the cursor skips to a new line while typing (or down in case of
|
||||
backspaces). This is similar to the way a typewriter scrolls the paper when hitting the return key.
|
||||
It improves the writing experience as the current active line will stay at the same eye height
|
||||
level on the screen.</p>
|
||||
<p>Previously, the feature would lock the cursor to a given vertical position defined by the user.
|
||||
Now, instead, the cursor will remain stationary in the vertical direction at any position the user
|
||||
sets it to by mouse click or keyboard navigation. The user can define a minimum distance from the
|
||||
top where this feature is activated. These changes makes it more flexible in terms of where the
|
||||
focus is in the editor. The feature can be controlled from the main Preferences.</p>
|
||||
<h3>Switching Syntax Theme</h3>
|
||||
<p>It is now possible to switch syntax highlighting theme without restarting novelWriter.
|
||||
Previously, changing the theme would only half-way update the document, header and footer
|
||||
background and text colours. The new settings would not be fully applied until the application was
|
||||
shut down and started again, thus making it a bit tedious to look through syntax themes to find the
|
||||
one you want.</p>
|
||||
<p>Switching main GUI theme still requires a restart.</p>
|
||||
<p><i>See also the <a href="https://github.com/vkbo/novelWriter/releases">Releases</a> page.</i></p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
137
nw/assets/text/release_notes_1.0.htm
Normal file
@@ -0,0 +1,137 @@
|
||||
<!DOCTYPE html public "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
|
||||
<html>
|
||||
<body>
|
||||
|
||||
<h2>Release Notes for 1.0.4</h2>
|
||||
<p><i>Released on 3 February 2021</i></p>
|
||||
<p>This patch release fixes a couple of minor issues with the Preferences dialog and the behaviour
|
||||
of one of the keyboard shortcuts.</p>
|
||||
<p>Aside from these fixes, the main point of this patch is to add new setup features for
|
||||
novelWriter on Windows. A Windows installer will no longer be provided for the foreseeable future,
|
||||
and instead functionality has been added to the main setup script to create desktop and start menu
|
||||
icons.</p>
|
||||
|
||||
<p><i>The full changelog is available
|
||||
<a href="https://github.com/vkbo/novelWriter/blob/main/CHANGELOG.md">here</a>.</i></p>
|
||||
|
||||
<hr/>
|
||||
|
||||
<h2>Release Notes for 1.0.3</h2>
|
||||
<p><i>Released on 24 January 2021</i></p>
|
||||
<p>This patch release fixes a minor bug sometimes encountered when running novelWriter from command
|
||||
line on Windows. In addition, the Solarized Dark and Solarized Light themes have been added to the
|
||||
selection of GUI and syntax themes by a user contribution.</p>
|
||||
<p>The main change in this release is to the install scripts and the documentation related to
|
||||
installing and running novelWriter. The primary change is a different method of packaging the app
|
||||
for Windows. Instead of building an <i>.exe</i> file, the new setup instead builds a runnable zip
|
||||
file <i>.pyz</i>. The executable would often be mistakenly flagged by virus control software due to
|
||||
the packaging tools. This is a known problem with pyinstaller and similar tools, but such warnings
|
||||
are always concerning even if they are false positives.</p>
|
||||
|
||||
<hr/>
|
||||
|
||||
<h2>Release Notes for 1.0.2</h2>
|
||||
<p><i>Released on 19 January 2021</i></p>
|
||||
<p>This patch release fixes a few minor cosmetic issues, a minor issue with the indexer, and a bug
|
||||
when adding words to the user's own spell check dictionary. Additionally, the documentation has
|
||||
been updated based on user feedback, and some install issues resolved.</p>
|
||||
|
||||
<hr/>
|
||||
|
||||
<h2>Release Notes for 1.0.1</h2>
|
||||
<p><i>Released on 10 January 2021</i></p>
|
||||
<p>This release is mainly to bring the documentation up to date, as I forgot to update the install
|
||||
instructions in the original 1.0 release. I also forgot to change the various settings and help
|
||||
texts that describe novelWriter as under initial development (beta state).</p>
|
||||
<p>Some minor improvements have been made to the "Edit Project Item" dialog and some restrictions
|
||||
on the settings available for documents created in the "Outtakes" folder relaxed. A few minor
|
||||
issues with the document and project changed icons on the status bar have also been resolved. The
|
||||
indicators were previously set to changed status even if no actual change had been made to the
|
||||
project.</p>
|
||||
|
||||
<hr/>
|
||||
|
||||
<h2>Release Notes for 1.0</h2>
|
||||
<p><i>Released on 3 January 2021</i></p>
|
||||
<p>Based on my own testing and usage, and no serious bugs discovered in quite some time (aside from
|
||||
a few corner case issues), it appears that novelWriter is stable enough for a 1.0 release. Thanks
|
||||
to all the new users who keep providing feedback on bugs, cosmetic issues, or suggesting
|
||||
improvements and new features. I'm glad to hear that others find my application useful, and I will
|
||||
keep making improvements as I get new suggestions and have new ideas myself. At the same time, I
|
||||
will continue to keep novelWriter simple and clean and avoid feature-bloat.</p>
|
||||
<p>This release mainly fixes cosmetic and other minor issues to the user interface and makes a few
|
||||
minor improvements to some less used features. Aside from this, nothing major has changed since the
|
||||
last release candidate.</p>
|
||||
<p>This release concludes over two years of tinkering with this project. The project grew out of
|
||||
numerous lunch and coffee discussions with my colleague Marian Lückhof at my former job. We were
|
||||
both looking for a tool for writing novels on Linux that suited our needs. We started assembling a
|
||||
wish list of features that has become novelWriter. In addition, users on GitHub have continued to
|
||||
test new features, provide very helpful feedback, and make new suggestions for improvements.
|
||||
Especially the feedback from @johnblommers has been helpful during much of the initial development
|
||||
time. Over the last months more users have started posting ideas and feedback. Thanks to all of you
|
||||
for your contributions.</p>
|
||||
<p>The 1.0 release is intended as a first release of the core features of novelWriter. That does
|
||||
not mean that all planned features have been fully implemented. There is a long list of ideas and
|
||||
suggestions to consider and implement. New ideas and suggestions are welcome. Either as feature
|
||||
requests in the issue tracker, or if not fully formed, can be discussed on the
|
||||
<a href="https://github.com/vkbo/novelWriter/discussions">discussions page</a>.
|
||||
|
||||
<hr/>
|
||||
|
||||
<h2>Release Notes for 1.0 RC2</h2>
|
||||
<p><i>Released on 13 December 2020</i></p>
|
||||
<p>This second release candidate for 1.0 comes with only minor changes and improvements, and a
|
||||
handful of minor bugfixes.</p>
|
||||
<p>Among the improvements is the addition of all the possible @-keywords for tags and references to
|
||||
the "Insert" menu under the sub-menu "Tags and References". The "Help" menu has also received a few
|
||||
improvements and additional links to useful webpages. This release also adds a "Release" notes tab
|
||||
to the "About novelWriter" dialog. The release notes are displayed automatically the first time you
|
||||
launch novelWriter after updating to a new version.</p>
|
||||
<p>Among the fixes is better support for high resolution screens. A few elements on the GUI did not
|
||||
scale properly, among them the document editor and viewer header and footer. These were clipped on
|
||||
high res screens due to an underlying issue with the Qt widget underestimating how much space it
|
||||
required to accommodate the text. Unfortunately, dragging the novelWriter app between screens of
|
||||
different scaling factors is not currently supported. However, the GUI should scale properly to the
|
||||
scaling factor on the screen it is opened on.</p>
|
||||
<p>The work leading up to this release has mostly been focused on improving the test coverage of
|
||||
the source code of novelWriter. This helps to ensure that the code does what it is intended to do,
|
||||
and is able to handle corner cases and unexpected external errors and user actions that may occur.
|
||||
While writing these tests, a number of minor potential issues have been uncovered and handled. Most
|
||||
of these are corner cases that may not even be reachable by unexpected user actions.</p>
|
||||
<p>Hopefully, these changes have resulted in an even more stable version of novelWriter. If no more
|
||||
issues are discovered, the next release will be the final version 1.0 release.</p>
|
||||
|
||||
<hr/>
|
||||
|
||||
<h2>Release Notes for 1.0 RC1</h2>
|
||||
<p><i>Released on 16 November 2020</i></p>
|
||||
<p>This is the first release candidate for the upcoming release of novelWriter 1.0.</p>
|
||||
<p>Since the fifth beta release about four weeks ago, not much has been changed in novelWriter. A
|
||||
few minor tweaks have been made to the GUI.</p>
|
||||
<p>A number of features and tools are now automatically switched off when there is no project or
|
||||
document open for those features to act upon. Previously, this was a bit inconsistent, although no
|
||||
serious bugs have been reported or encountered.</p>
|
||||
<p>Most of the minor changes in this release should not be noticeable to most users. However, there
|
||||
are a couple of noticeable changes.</p>
|
||||
<h3>Typewriter Mode</h3>
|
||||
<p>The "Typewriter Mode" of the editor has been improved. Essentially, this feature is a sort of
|
||||
smart scroll. It tries to keep the cursor stationary in the vertical direction, and will try to
|
||||
scroll the document up when the cursor skips to a new line while typing (or down in case of
|
||||
backspaces). This is similar to the way a typewriter scrolls the paper when hitting the return key.
|
||||
It improves the writing experience as the current active line will stay at the same eye height
|
||||
level on the screen.</p>
|
||||
<p>Previously, the feature would lock the cursor to a given vertical position defined by the user.
|
||||
Now, instead, the cursor will remain stationary in the vertical direction at any position the user
|
||||
sets it to by mouse click or keyboard navigation. The user can define a minimum distance from the
|
||||
top where this feature is activated. These changes makes it more flexible in terms of where the
|
||||
focus is in the editor. The feature can be controlled from the main Preferences.</p>
|
||||
<h3>Switching Syntax Theme</h3>
|
||||
<p>It is now possible to switch syntax highlighting theme without restarting novelWriter.
|
||||
Previously, changing the theme would only half-way update the document, header and footer
|
||||
background and text colours. The new settings would not be fully applied until the application was
|
||||
shut down and started again, thus making it a bit tedious to look through syntax themes to find the
|
||||
one you want.</p>
|
||||
<p>Switching main GUI theme still requires a restart.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
49
nw/assets/text/release_notes_1.1.htm
Normal file
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html public "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
|
||||
<html>
|
||||
<body>
|
||||
|
||||
<h2>Release Notes for 1.1.1</h2>
|
||||
<p><i>Released on 21 February 2021</i></p>
|
||||
<p>This patch makes a couple of minor improvements to the GUI. The keyboard shortcut for deleting
|
||||
entries in the project tree has been changed from "Ctrl+Del" to "Ctrl+Shift+Del" to free up that
|
||||
shortcut for the document editor. It is now possible to use the shortcut for deleting the word in
|
||||
front of the cursor, a common and useful feature of many text editors.</p>
|
||||
<p>The way the negative word count filter option works on the Writing Statistics tool has been
|
||||
changed to be more intuitive. Enabling this filter now appears to remove all negative entries
|
||||
without altering the other entries. Previously, the removed negative counts would be included in
|
||||
the following entries to make it consistent with the total word count. In addition, writing
|
||||
sessions shorter than five minutes, and with no change in the word count, are no longer recorded in
|
||||
the session log file.</p>
|
||||
<p>Other changes include improving the speed of the internal spell checker, used when the Enchant
|
||||
spell check library isn't available. The internal spell checker is no longer significantly slower,
|
||||
but is still lacking in functionality compared to Enchant.</p>
|
||||
|
||||
<p><i>See also the <a href="https://github.com/vkbo/novelWriter/releases">Releases</a> page.</i></p>
|
||||
|
||||
<hr/>
|
||||
|
||||
<h2>Release Notes for 1.1</h2>
|
||||
<p><i>Released on 7 February 2021</i></p>
|
||||
<p>The main change in this release is the addition of a new tab to the project tree on the left
|
||||
side of the main window. The regular project tree is now on a tab named "Project", while a new tab
|
||||
named "Novel" displays a simpler version of the information on the main "Outline" page. It lists
|
||||
all the headers of the novel part of the project, as well as the word count and point-of-view
|
||||
character of each section. This is an alternative way to navigate the novel part of the project.
|
||||
The various tree views are now also kept better in sync when the user selects various documents and
|
||||
headers.</p>
|
||||
<p>In addition, a new information dialog named "Project Details" has been added. It replaces the
|
||||
"Details" tab in "Project Settings", and adds more information about the novel part of the project.
|
||||
In particular, a "Table of Contents" in the "Contents" tab displays a summary of the main parts
|
||||
and chapters of the project, their total word counts, and an estimated page count. This was made in
|
||||
response to users asking for ways to estimate the total page count of the project. The page count
|
||||
is estimated based on a words per page setting, which can be changed on the dialog window.</p>
|
||||
<p>Since the tabs below the project tree now add some extra room on the GUI, some convenient
|
||||
buttons have been added in the same area, with direct access to "Project Details", "Writing
|
||||
Statistics" and "Project Setting".</p>
|
||||
<p>A few other minor changes have been made as well. The Preferences dialog has been improved with
|
||||
clearer categories and hopefully better help text. Some new options have been added too. They allow
|
||||
syntax highlighting of multi-paragraph quotes. The highlighter can now optionally accept quotes to
|
||||
be left "hanging", that is, no closing quote in the same paragraph.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -23,7 +23,6 @@ link = 44, 152, 247
|
||||
linkvisited = 44, 152, 247
|
||||
|
||||
[GUI]
|
||||
treewordcount = 197, 200, 198
|
||||
statusnone = 150, 152, 150
|
||||
statussaved = 39, 135, 78
|
||||
statusunsaved = 138, 32, 32
|
||||
|
||||
28
nw/assets/themes/gui/solarized_dark/theme.conf
Normal file
@@ -0,0 +1,28 @@
|
||||
[Main]
|
||||
name = Solarized Dark
|
||||
author = nullbasis
|
||||
credit = Ethan Schoonover
|
||||
url = https://ethanschoonover.com/solarized/
|
||||
license = MIT
|
||||
licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE
|
||||
|
||||
[Palette]
|
||||
window = 0, 43, 54
|
||||
windowtext = 253, 246, 227
|
||||
base = 7, 54, 66
|
||||
alternatebase = 67, 67, 67
|
||||
text = 253, 246, 227
|
||||
tooltipbase = 133, 153, 0
|
||||
tooltiptext = 0, 43, 54
|
||||
button = 7, 54, 66
|
||||
buttontext = 253, 246, 227
|
||||
brighttext = 253, 246, 227
|
||||
highlight = 42, 161, 152
|
||||
highlightedtext = 0, 43, 54
|
||||
link = 38, 139, 210
|
||||
linkvisited = 38, 139, 210
|
||||
|
||||
[GUI]
|
||||
statusnone = 88, 110, 117
|
||||
statussaved = 42, 161, 152
|
||||
statusunsaved = 203, 75, 22
|
||||
28
nw/assets/themes/gui/solarized_light/theme.conf
Normal file
@@ -0,0 +1,28 @@
|
||||
[Main]
|
||||
name = Solarized Light
|
||||
author = nullbasis
|
||||
credit = Ethan Schoonover
|
||||
url = https://ethanschoonover.com/solarized/
|
||||
license = MIT
|
||||
licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE
|
||||
|
||||
[Palette]
|
||||
window = 238, 232, 213
|
||||
windowtext = 0, 43, 54
|
||||
base = 253, 246, 227
|
||||
alternatebase = 238, 232, 213
|
||||
text = 0, 43, 54
|
||||
tooltipbase = 133, 153, 0
|
||||
tooltiptext = 0, 43, 54
|
||||
button = 238, 232, 213
|
||||
buttontext = 0, 43, 54
|
||||
brighttext = 0, 43, 54
|
||||
highlight = 42, 161, 152
|
||||
highlightedtext = 253, 246, 227
|
||||
link = 38, 139, 210
|
||||
linkvisited = 38, 139, 210
|
||||
|
||||
[GUI]
|
||||
statusnone = 88, 110, 117
|
||||
statussaved = 42, 161, 152
|
||||
statusunsaved = 203, 75, 22
|
||||
25
nw/assets/themes/syntax/solarized_dark.conf
Normal file
@@ -0,0 +1,25 @@
|
||||
[Main]
|
||||
name = Solarized Dark
|
||||
author = nullbasis
|
||||
credit = Ethan Schoonover
|
||||
url = https://ethanschoonover.com/solarized/
|
||||
license = MIT
|
||||
licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE
|
||||
|
||||
[Syntax]
|
||||
background = 7, 54, 66
|
||||
text = 253, 246, 227
|
||||
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
|
||||
hidden = 147, 161, 161
|
||||
keyword = 133, 153, 0
|
||||
value = 203, 75, 22
|
||||
spellcheckline = 203, 75, 22
|
||||
tagerror = 220, 50, 47
|
||||
replacetag = 133, 153, 0
|
||||
modifier = 181, 137, 0
|
||||
25
nw/assets/themes/syntax/solarized_light.conf
Normal file
@@ -0,0 +1,25 @@
|
||||
[Main]
|
||||
name = Solarized Light
|
||||
author = nullbasis
|
||||
credit = Ethan Schoonover
|
||||
url = https://ethanschoonover.com/solarized/
|
||||
license = MIT
|
||||
licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE
|
||||
|
||||
[Syntax]
|
||||
background = 253, 246, 227
|
||||
text = 0, 43, 54
|
||||
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
|
||||
hidden = 88, 110, 117
|
||||
keyword = 133, 153, 0
|
||||
value = 203, 75, 22
|
||||
spellcheckline = 203, 75, 22
|
||||
tagerror = 220, 50, 47
|
||||
replacetag = 133, 153, 0
|
||||
modifier = 181, 137, 0
|
||||
149
nw/common.py
@@ -1,28 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Common Functions
|
||||
"""
|
||||
novelWriter – Common Functions
|
||||
==============================
|
||||
Various common functions
|
||||
|
||||
novelWriter – Common Functions
|
||||
================================
|
||||
Various functions used multiple places
|
||||
File History:
|
||||
Created: 2019-05-12 [0.1.0]
|
||||
|
||||
File History:
|
||||
Created: 2019-05-12 [0.1.0]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -31,70 +30,72 @@ from datetime import datetime
|
||||
|
||||
from PyQt5.QtWidgets import qApp
|
||||
|
||||
from nw.constants import nwConst, nwUnicode
|
||||
from nw.constants import (
|
||||
nwConst, nwUnicode, nwItemClass, nwItemType, nwItemLayout
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def checkString(checkValue, defaultValue, allowNone=False):
|
||||
def checkString(value, default, allowNone=False):
|
||||
"""Check if a variable is a string or a none.
|
||||
"""
|
||||
if allowNone:
|
||||
if checkValue is None:
|
||||
if value is None:
|
||||
return None
|
||||
if checkValue == "None":
|
||||
if value == "None":
|
||||
return None
|
||||
if isinstance(checkValue, str):
|
||||
return str(checkValue)
|
||||
return defaultValue
|
||||
if isinstance(value, str):
|
||||
return str(value)
|
||||
return default
|
||||
|
||||
def checkInt(checkValue, defaultValue, allowNone=False):
|
||||
def checkInt(value, default, allowNone=False):
|
||||
"""Check if a variable is an integer or a none.
|
||||
"""
|
||||
if allowNone:
|
||||
if checkValue is None:
|
||||
if value is None:
|
||||
return None
|
||||
if checkValue == "None":
|
||||
if value == "None":
|
||||
return None
|
||||
try:
|
||||
return int(checkValue)
|
||||
return int(value)
|
||||
except Exception:
|
||||
return defaultValue
|
||||
return default
|
||||
|
||||
def checkBool(checkValue, defaultValue, allowNone=False):
|
||||
def checkBool(value, default, allowNone=False):
|
||||
"""Check if a variable is a boolean or a none.
|
||||
"""
|
||||
if allowNone:
|
||||
if checkValue is None:
|
||||
if value is None:
|
||||
return None
|
||||
if checkValue == "None":
|
||||
if value == "None":
|
||||
return None
|
||||
if isinstance(checkValue, str):
|
||||
if checkValue == "True":
|
||||
if isinstance(value, str):
|
||||
if value == "True":
|
||||
return True
|
||||
elif checkValue == "False":
|
||||
elif value == "False":
|
||||
return False
|
||||
else:
|
||||
return defaultValue
|
||||
elif isinstance(checkValue, int):
|
||||
if checkValue == 1:
|
||||
return default
|
||||
elif isinstance(value, int):
|
||||
if value == 1:
|
||||
return True
|
||||
elif checkValue == 0:
|
||||
elif value == 0:
|
||||
return False
|
||||
else:
|
||||
return defaultValue
|
||||
return defaultValue
|
||||
return default
|
||||
return default
|
||||
|
||||
def checkHandle(checkValue, defaultValue, allowNone=False):
|
||||
def checkHandle(value, default, allowNone=False):
|
||||
"""Check if a value is a handle.
|
||||
"""
|
||||
if allowNone:
|
||||
if checkValue is None:
|
||||
if value is None:
|
||||
return None
|
||||
if checkValue == "None":
|
||||
if value == "None":
|
||||
return None
|
||||
if isHandle(checkValue):
|
||||
return str(checkValue)
|
||||
return defaultValue
|
||||
if isHandle(value):
|
||||
return str(value)
|
||||
return default
|
||||
|
||||
def isHandle(theString):
|
||||
"""Check if a string is a valid novelWriter handle.
|
||||
@@ -104,11 +105,49 @@ def isHandle(theString):
|
||||
return False
|
||||
if len(theString) != 13:
|
||||
return False
|
||||
invalidChar = False
|
||||
for c in theString:
|
||||
if c not in "0123456789abcdef":
|
||||
invalidChar = True
|
||||
return not invalidChar
|
||||
return False
|
||||
return True
|
||||
|
||||
def isTitleTag(theString):
|
||||
"""Check if a string is a valid title string.
|
||||
"""
|
||||
if not isinstance(theString, str):
|
||||
return False
|
||||
if len(theString) != 7:
|
||||
return False
|
||||
if not theString.startswith("T"):
|
||||
return False
|
||||
for c in theString[1:]:
|
||||
if c not in "0123456789":
|
||||
return False
|
||||
return True
|
||||
|
||||
def isItemClass(theString):
|
||||
"""Check if an item is a calid nwItemClass identifier.
|
||||
"""
|
||||
return theString in nwItemClass.__members__
|
||||
|
||||
def isItemType(theString):
|
||||
"""Check if an item is a calid nwItemType identifier.
|
||||
"""
|
||||
return theString in nwItemType.__members__
|
||||
|
||||
def isItemLayout(theString):
|
||||
"""Check if an item is a calid nwItemLayout identifier.
|
||||
"""
|
||||
return theString in nwItemLayout.__members__
|
||||
|
||||
def hexToInt(value, default=0):
|
||||
"""Convert a hex string to an integer.
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return int(value, 16)
|
||||
except Exception:
|
||||
return default
|
||||
return default
|
||||
|
||||
def colRange(rgbStart, rgbEnd, nStep):
|
||||
"""Generate a range of colours from one RGB value to another.
|
||||
@@ -152,11 +191,11 @@ def formatInt(theInt):
|
||||
theVal /= 1000.0
|
||||
if theVal < 1000.0:
|
||||
if theVal < 10.0:
|
||||
return f"{theVal:4.2f}{nwUnicode.U_THNSP}{pF}"
|
||||
return f"{theVal:4.2f}{nwUnicode.U_THSP}{pF}"
|
||||
elif theVal < 100.0:
|
||||
return f"{theVal:4.1f}{nwUnicode.U_THNSP}{pF}"
|
||||
return f"{theVal:4.1f}{nwUnicode.U_THSP}{pF}"
|
||||
else:
|
||||
return f"{theVal:3.0f}{nwUnicode.U_THNSP}{pF}"
|
||||
return f"{theVal:3.0f}{nwUnicode.U_THSP}{pF}"
|
||||
|
||||
return str(theInt)
|
||||
|
||||
|
||||
132
nw/config.py
@@ -1,28 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Config Class
|
||||
"""
|
||||
novelWriter – Config Class
|
||||
==========================
|
||||
Class holding the user preferences and handling the config file
|
||||
|
||||
novelWriter – Config Class
|
||||
============================
|
||||
Class reading and holding the preferences of the application
|
||||
File History:
|
||||
Created: 2018-09-22 [0.0.1]
|
||||
|
||||
File History:
|
||||
Created: 2018-09-22 [0.0.1]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -39,6 +38,7 @@ from PyQt5.QtCore import QT_VERSION_STR, QStandardPaths, QSysInfo
|
||||
|
||||
from nw.constants import nwConst, nwFiles, nwUnicode
|
||||
from nw.common import splitVersionNumber, formatTimeStamp
|
||||
from nw.error import logException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -56,10 +56,6 @@ class Config:
|
||||
self.appName = "novelWriter"
|
||||
self.appHandle = self.appName.lower()
|
||||
|
||||
# Debug Settings
|
||||
self.showGUI = True # Allow blocking the GUI (disabled for testing)
|
||||
self.debugInfo = False # True if log level is DEBUG or VERBOSE
|
||||
|
||||
# Config Error Handling
|
||||
self.hasError = False # True if the config class encountered an error
|
||||
self.errData = [] # List of error messages
|
||||
@@ -92,17 +88,18 @@ class Config:
|
||||
self.guiFont = "" # Defaults to system default font
|
||||
self.guiFontSize = 11
|
||||
self.guiScale = 1.0 # Set automatically by Theme class
|
||||
self.lastNotes = "" # The latest release notes that have been shown
|
||||
self.lastNotes = "0x0" # The latest release notes that have been shown
|
||||
|
||||
## Sizes
|
||||
self.winGeometry = [1200, 650]
|
||||
self.treeColWidth = [200, 50, 30]
|
||||
self.projColWidth = [200, 60, 140]
|
||||
self.mainPanePos = [300, 800]
|
||||
self.docPanePos = [400, 400]
|
||||
self.viewPanePos = [500, 150]
|
||||
self.outlnPanePos = [500, 150]
|
||||
self.isFullScreen = False
|
||||
self.winGeometry = [1200, 650]
|
||||
self.treeColWidth = [200, 50, 30]
|
||||
self.novelColWidth = [200, 50]
|
||||
self.projColWidth = [200, 60, 140]
|
||||
self.mainPanePos = [300, 800]
|
||||
self.docPanePos = [400, 400]
|
||||
self.viewPanePos = [500, 150]
|
||||
self.outlnPanePos = [500, 150]
|
||||
self.isFullScreen = False
|
||||
|
||||
## Features
|
||||
self.hideVScroll = False # Hide vertical scroll bars on main widgets
|
||||
@@ -143,8 +140,13 @@ class Config:
|
||||
self.bigDocLimit = 800 # Size threshold for heavy editor features in kilobytes
|
||||
|
||||
self.highlightQuotes = True # Highlight text in quotes
|
||||
self.allowOpenSQuote = False # Allow open-ended single quotes
|
||||
self.allowOpenDQuote = True # Allow open-ended double quotes
|
||||
self.highlightEmph = True # Add colour to text emphasis
|
||||
|
||||
self.stopWhenIdle = True # Stop the status bar clock when the user is idle
|
||||
self.userIdleTime = 300 # Time of inactivity to consider user idle
|
||||
|
||||
## User-Selected Symbols
|
||||
self.fmtApostrophe = nwUnicode.U_RSQUO
|
||||
self.fmtSingleQuotes = [nwUnicode.U_LSQUO, nwUnicode.U_RSQUO]
|
||||
@@ -269,10 +271,19 @@ class Config:
|
||||
logger.verbose("Config path: %s" % self.confPath)
|
||||
logger.verbose("Data path: %s" % self.dataPath)
|
||||
|
||||
self.confFile = self.appHandle+".conf"
|
||||
self.lastPath = os.path.expanduser("~")
|
||||
self.appPath = getattr(sys, "_MEIPASS", os.path.abspath(os.path.dirname(__file__)))
|
||||
self.appRoot = os.path.join(self.appPath, os.path.pardir)
|
||||
self.confFile = self.appHandle+".conf"
|
||||
self.lastPath = os.path.expanduser("~")
|
||||
self.appPath = getattr(sys, "_MEIPASS", os.path.abspath(os.path.dirname(__file__)))
|
||||
self.appRoot = os.path.abspath(os.path.join(self.appPath, os.path.pardir))
|
||||
|
||||
if os.path.isfile(self.appRoot):
|
||||
# novelWriter is packaged as a single file, so the app and
|
||||
# root paths are the same, and equal to the folder that
|
||||
# contains the single executable.
|
||||
self.appRoot = os.path.dirname(self.appRoot)
|
||||
self.appPath = self.appRoot
|
||||
|
||||
# Assets
|
||||
self.assetPath = os.path.join(self.appPath, "assets")
|
||||
self.themeRoot = os.path.join(self.assetPath, "themes")
|
||||
self.dictPath = os.path.join(self.assetPath, "dict")
|
||||
@@ -289,7 +300,7 @@ class Config:
|
||||
os.mkdir(self.confPath)
|
||||
except Exception as e:
|
||||
logger.error("Could not create folder: %s" % self.confPath)
|
||||
logger.error(str(e))
|
||||
logException()
|
||||
self.hasError = True
|
||||
self.errData.append("Could not create folder: %s" % self.confPath)
|
||||
self.errData.append(str(e))
|
||||
@@ -312,7 +323,7 @@ class Config:
|
||||
os.mkdir(self.dataPath)
|
||||
except Exception as e:
|
||||
logger.error("Could not create folder: %s" % self.dataPath)
|
||||
logger.error(str(e))
|
||||
logException()
|
||||
self.hasError = True
|
||||
self.errData.append("Could not create folder: %s" % self.dataPath)
|
||||
self.errData.append(str(e))
|
||||
@@ -357,7 +368,7 @@ class Config:
|
||||
cnfParse.read_file(inFile)
|
||||
except Exception as e:
|
||||
logger.error("Could not load config file")
|
||||
logger.error(str(e))
|
||||
logException()
|
||||
self.hasError = True
|
||||
self.errData.append("Could not load config file")
|
||||
self.errData.append(str(e))
|
||||
@@ -395,6 +406,9 @@ class Config:
|
||||
self.treeColWidth = self._parseLine(
|
||||
cnfParse, cnfSec, "treecols", self.CNF_I_LST, self.treeColWidth
|
||||
)
|
||||
self.novelColWidth = self._parseLine(
|
||||
cnfParse, cnfSec, "novelcols", self.CNF_I_LST, self.novelColWidth
|
||||
)
|
||||
self.projColWidth = self._parseLine(
|
||||
cnfParse, cnfSec, "projcols", self.CNF_I_LST, self.projColWidth
|
||||
)
|
||||
@@ -512,9 +526,21 @@ class Config:
|
||||
self.highlightQuotes = self._parseLine(
|
||||
cnfParse, cnfSec, "highlightquotes", self.CNF_BOOL, self.highlightQuotes
|
||||
)
|
||||
self.allowOpenSQuote = self._parseLine(
|
||||
cnfParse, cnfSec, "allowopensquote", self.CNF_BOOL, self.allowOpenSQuote
|
||||
)
|
||||
self.allowOpenDQuote = self._parseLine(
|
||||
cnfParse, cnfSec, "allowopendquote", self.CNF_BOOL, self.allowOpenDQuote
|
||||
)
|
||||
self.highlightEmph = self._parseLine(
|
||||
cnfParse, cnfSec, "highlightemph", self.CNF_BOOL, self.highlightEmph
|
||||
)
|
||||
self.stopWhenIdle = self._parseLine(
|
||||
cnfParse, cnfSec, "stopwhenidle", self.CNF_BOOL, self.stopWhenIdle
|
||||
)
|
||||
self.userIdleTime = self._parseLine(
|
||||
cnfParse, cnfSec, "useridletime", self.CNF_INT, self.userIdleTime
|
||||
)
|
||||
|
||||
## Backup
|
||||
cnfSec = "Backup"
|
||||
@@ -567,6 +593,15 @@ class Config:
|
||||
# Check Certain Values for None
|
||||
self.spellLanguage = self._checkNone(self.spellLanguage)
|
||||
|
||||
# If we're using straight quotes, disable auto-replace
|
||||
if self.fmtSingleQuotes == ["'", "'"] and self.doReplaceSQuote:
|
||||
logger.info("Using straight single quotes, so disabling auto-replace")
|
||||
self.doReplaceSQuote = False
|
||||
|
||||
if self.fmtDoubleQuotes == ["\"", "\""] and self.doReplaceDQuote:
|
||||
logger.info("Using straight double quotes, so disabling auto-replace")
|
||||
self.doReplaceDQuote = False
|
||||
|
||||
return True
|
||||
|
||||
def saveConfig(self):
|
||||
@@ -597,6 +632,7 @@ class Config:
|
||||
cnfParse.add_section(cnfSec)
|
||||
cnfParse.set(cnfSec, "geometry", self._packList(self.winGeometry))
|
||||
cnfParse.set(cnfSec, "treecols", self._packList(self.treeColWidth))
|
||||
cnfParse.set(cnfSec, "novelcols", self._packList(self.novelColWidth))
|
||||
cnfParse.set(cnfSec, "projcols", self._packList(self.projColWidth))
|
||||
cnfParse.set(cnfSec, "mainpane", self._packList(self.mainPanePos))
|
||||
cnfParse.set(cnfSec, "docpane", self._packList(self.docPanePos))
|
||||
@@ -642,7 +678,11 @@ class Config:
|
||||
cnfParse.set(cnfSec, "bigdoclimit", str(self.bigDocLimit))
|
||||
cnfParse.set(cnfSec, "showfullpath", str(self.showFullPath))
|
||||
cnfParse.set(cnfSec, "highlightquotes", str(self.highlightQuotes))
|
||||
cnfParse.set(cnfSec, "allowopensquote", str(self.allowOpenSQuote))
|
||||
cnfParse.set(cnfSec, "allowopendquote", str(self.allowOpenDQuote))
|
||||
cnfParse.set(cnfSec, "highlightemph", str(self.highlightEmph))
|
||||
cnfParse.set(cnfSec, "stopwhenidle", str(self.stopWhenIdle))
|
||||
cnfParse.set(cnfSec, "useridletime", str(self.userIdleTime))
|
||||
|
||||
## Backup
|
||||
cnfSec = "Backup"
|
||||
@@ -677,7 +717,7 @@ class Config:
|
||||
self.confChanged = False
|
||||
except Exception as e:
|
||||
logger.error("Could not save config file")
|
||||
logger.error(str(e))
|
||||
logException()
|
||||
self.hasError = True
|
||||
self.errData.append("Could not save config file")
|
||||
self.errData.append(str(e))
|
||||
@@ -816,6 +856,11 @@ class Config:
|
||||
self.confChanged = True
|
||||
return True
|
||||
|
||||
def setNovelColWidths(self, colWidths):
|
||||
self.novelColWidth = [int(x/self.guiScale) for x in colWidths]
|
||||
self.confChanged = True
|
||||
return True
|
||||
|
||||
def setProjColWidths(self, colWidths):
|
||||
self.projColWidth = [int(x/self.guiScale) for x in colWidths]
|
||||
self.confChanged = True
|
||||
@@ -872,6 +917,9 @@ class Config:
|
||||
def getTreeColWidths(self):
|
||||
return [int(x*self.guiScale) for x in self.treeColWidth]
|
||||
|
||||
def getNovelColWidths(self):
|
||||
return [int(x*self.guiScale) for x in self.novelColWidth]
|
||||
|
||||
def getProjColWidths(self):
|
||||
return [int(x*self.guiScale) for x in self.projColWidth]
|
||||
|
||||
@@ -945,9 +993,9 @@ class Config:
|
||||
return self._unpackList(
|
||||
cnfParse.get(cnfSec, cnfName), cnfDefault, self.CNF_S_LST
|
||||
)
|
||||
except ValueError as e:
|
||||
except ValueError:
|
||||
logger.error("Failed to load value from config file.")
|
||||
logger.error(str(e))
|
||||
logException()
|
||||
return cnfDefault
|
||||
|
||||
return cnfDefault
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from nw.constants.iso import isoLanguage, isoCountry
|
||||
from nw.constants.constants import (
|
||||
nwConst, nwRegEx, nwFiles, nwKeyWords, nwLabels, nwQuotes, nwUnicode
|
||||
nwConst, nwLists, nwRegEx, nwFiles, nwKeyWords, nwLabels, nwQuotes,
|
||||
nwUnicode, nwHtmlUnicode
|
||||
)
|
||||
from nw.constants.enum import (
|
||||
nwAlert, nwDocAction, nwItemClass, nwItemLayout, nwItemType, nwOutline,
|
||||
@@ -11,18 +12,20 @@ from nw.constants.enum import (
|
||||
__all__ = [
|
||||
"isoCountry",
|
||||
"isoLanguage",
|
||||
"nwAlert",
|
||||
"nwConst",
|
||||
"nwDocAction",
|
||||
"nwDocInsert",
|
||||
"nwLists",
|
||||
"nwRegEx",
|
||||
"nwFiles",
|
||||
"nwKeyWords",
|
||||
"nwLabels",
|
||||
"nwQuotes",
|
||||
"nwUnicode",
|
||||
"nwHtmlUnicode",
|
||||
"nwAlert",
|
||||
"nwDocAction",
|
||||
"nwItemClass",
|
||||
"nwItemLayout",
|
||||
"nwItemType",
|
||||
"nwKeyWords",
|
||||
"nwLabels",
|
||||
"nwOutline",
|
||||
"nwQuotes",
|
||||
"nwRegEx",
|
||||
"nwUnicode",
|
||||
"nwDocInsert",
|
||||
]
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Constants
|
||||
"""
|
||||
novelWriter – Constants
|
||||
=======================
|
||||
Constants and maps for translating flags and enums to text
|
||||
|
||||
novelWriter – Constants
|
||||
=========================
|
||||
Constants for translating flags and enums to text
|
||||
File History:
|
||||
Created: 2019-04-28 [0.0.1]
|
||||
|
||||
File History:
|
||||
Created: 2019-04-28 [0.0.1]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
from nw.constants.enum import (
|
||||
@@ -45,10 +44,21 @@ class nwConst():
|
||||
SP_INTERNAL = "internal"
|
||||
SP_ENCHANT = "enchant"
|
||||
|
||||
# Check Lists
|
||||
# END Class nwConst
|
||||
|
||||
class nwLists():
|
||||
"""Lists used for grouping various other constants.
|
||||
"""
|
||||
# Regular user-accessible item types
|
||||
REG_TYPES = {nwItemType.ROOT, nwItemType.FOLDER, nwItemType.FILE}
|
||||
|
||||
# END Class nwConst
|
||||
# Item classes where the full list of novel layouts are allowed
|
||||
CLS_NOVEL = {nwItemClass.NOVEL, nwItemClass.ARCHIVE}
|
||||
|
||||
# Item classes which do not require items to have same class
|
||||
FREE_CLASS = {nwItemClass.ARCHIVE, nwItemClass.TRASH}
|
||||
|
||||
# END Class nwLists
|
||||
|
||||
class nwRegEx():
|
||||
|
||||
@@ -76,6 +86,7 @@ class nwKeyWords:
|
||||
|
||||
TAG_KEY = "@tag"
|
||||
POV_KEY = "@pov"
|
||||
FOCUS_KEY = "@focus"
|
||||
CHAR_KEY = "@char"
|
||||
PLOT_KEY = "@plot"
|
||||
TIME_KEY = "@time"
|
||||
@@ -86,14 +97,15 @@ class nwKeyWords:
|
||||
|
||||
# Set of Valid Keys
|
||||
VALID_KEYS = {
|
||||
TAG_KEY, POV_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY,
|
||||
TAG_KEY, POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY,
|
||||
WORLD_KEY, OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY
|
||||
}
|
||||
|
||||
# Map from Keys to Item Class
|
||||
KEY_CLASS = {
|
||||
CHAR_KEY : nwItemClass.CHARACTER,
|
||||
POV_KEY : nwItemClass.CHARACTER,
|
||||
FOCUS_KEY : nwItemClass.CHARACTER,
|
||||
CHAR_KEY : nwItemClass.CHARACTER,
|
||||
PLOT_KEY : nwItemClass.PLOT,
|
||||
TIME_KEY : nwItemClass.TIMELINE,
|
||||
WORLD_KEY : nwItemClass.WORLD,
|
||||
@@ -170,6 +182,7 @@ class nwLabels():
|
||||
KEY_NAME = {
|
||||
nwKeyWords.TAG_KEY : "Tag",
|
||||
nwKeyWords.POV_KEY : "Point of View",
|
||||
nwKeyWords.FOCUS_KEY : "Focus",
|
||||
nwKeyWords.CHAR_KEY : "Characters",
|
||||
nwKeyWords.PLOT_KEY : "Plot",
|
||||
nwKeyWords.TIME_KEY : "Timeline",
|
||||
@@ -187,6 +200,7 @@ class nwLabels():
|
||||
nwOutline.WCOUNT : "Words",
|
||||
nwOutline.PCOUNT : "Pars",
|
||||
nwOutline.POV : "POV",
|
||||
nwOutline.FOCUS : "Focus",
|
||||
nwOutline.CHAR : KEY_NAME[nwKeyWords.CHAR_KEY],
|
||||
nwOutline.PLOT : KEY_NAME[nwKeyWords.PLOT_KEY],
|
||||
nwOutline.TIME : KEY_NAME[nwKeyWords.TIME_KEY],
|
||||
@@ -203,7 +217,6 @@ class nwQuotes():
|
||||
"""Allowed quotation marks.
|
||||
Source: https://en.wikipedia.org/wiki/Quotation_mark
|
||||
"""
|
||||
|
||||
SYMBOLS = {
|
||||
"\u0027" : "Straight single quotation mark",
|
||||
"\u0022" : "Straight double quotation mark",
|
||||
@@ -232,9 +245,8 @@ class nwQuotes():
|
||||
# END Class nwQuotes
|
||||
|
||||
class nwUnicode:
|
||||
"""Supported unicode character constants and translation maps for HTML.
|
||||
"""Supported unicode character constants and their HTML equivalents.
|
||||
"""
|
||||
|
||||
# Unicode Constants
|
||||
# =================
|
||||
|
||||
@@ -257,25 +269,39 @@ class nwUnicode:
|
||||
U_LCQUO = "\u300c" # Left corner bracket
|
||||
U_RCQUO = "\u300d" # Right corner bracket
|
||||
U_LWCQUO = "\u300e" # Left white corner bracket
|
||||
U_RECQUO = "\u300f" # Right white corner bracket
|
||||
U_RWCQUO = "\u300f" # Right white corner bracket
|
||||
|
||||
## Punctuation
|
||||
U_FGDASH = "\u2012" # Figure dash
|
||||
U_ENDASH = "\u2013" # Short dash
|
||||
U_EMDASH = "\u2014" # Long dash
|
||||
U_HBAR = "\u2015" # Horizontal bar
|
||||
U_HELLIP = "\u2026" # Ellipsis
|
||||
U_MAPOSS = "\u02bc" # Modifier letter single apostrophe
|
||||
U_PRIME = "\u2032" # Prime
|
||||
U_DPRIME = "\u2033" # Double prime
|
||||
|
||||
## Spaces and Lines
|
||||
U_NBSP = "\u00a0" # Non-breaking space
|
||||
U_THNSP = "\u2009" # Thin space
|
||||
U_THSP = "\u2009" # Thin space
|
||||
U_THNBSP = "\u202f" # Thin non-breaking space
|
||||
U_ENSP = "\u2002" # Short (en) space
|
||||
U_EMSP = "\u2003" # Long (em) space
|
||||
U_LSEP = "\u2028" # Line separator
|
||||
U_PSEP = "\u2029" # Paragraph separator
|
||||
|
||||
## Symbols
|
||||
U_CHECK = "\u2714" # Heavy check mark
|
||||
U_MULT = "\u2715" # Multiplication x
|
||||
U_CROSS = "\u2715" # Heavy cross mark
|
||||
U_BULL = "\u2022" # List bullet
|
||||
U_TRBULL = "\u2023" # Triangle bullet
|
||||
U_HYBULL = "\u2043" # Hyphen bullet
|
||||
U_FLOWER = "\u2055" # Flower punctuation mark
|
||||
U_PERMIL = "\u2030" # Per mille sign
|
||||
U_DEGREE = "\u00b0" # Degree symbol
|
||||
U_MINUS = "\u2212" # Minus sign
|
||||
U_TIMES = "\u00d7" # Multiplaction sign
|
||||
U_DIVIDE = "\u00f7" # Division sign
|
||||
|
||||
## Arrows
|
||||
U_UTRI = "\u25b2" # Up-pointing triangle
|
||||
@@ -309,23 +335,37 @@ class nwUnicode:
|
||||
H_LCQUO = "「"
|
||||
H_RCQUO = "」"
|
||||
H_LWCQUO = "『"
|
||||
H_LWCQUO = "『"
|
||||
H_RWCQUO = "』"
|
||||
|
||||
## Punctuation
|
||||
H_FGDASH = "‒"
|
||||
H_ENDASH = "–"
|
||||
H_EMDASH = "—"
|
||||
H_HBAR = "―"
|
||||
H_HELLIP = "…"
|
||||
H_MAPOSS = "ʼ"
|
||||
H_PRIME = "′"
|
||||
H_DPRIME = "″"
|
||||
|
||||
## Spaces
|
||||
H_NBSP = " "
|
||||
H_THNSP = " "
|
||||
H_THSP = " "
|
||||
H_THNBSP = " "
|
||||
H_ENSP = " "
|
||||
H_EMSP = " "
|
||||
|
||||
## Symbols
|
||||
H_CHECK = "✔"
|
||||
H_MULT = "✕"
|
||||
H_CROSS = "✕"
|
||||
H_BULL = "•"
|
||||
H_TRBULL = "‣"
|
||||
H_HYBULL = "⁃"
|
||||
H_FLOWER = "⁕"
|
||||
H_PERMIL = "‰"
|
||||
H_DEGREE = "°"
|
||||
H_MINUS = "−"
|
||||
H_TIMES = "×"
|
||||
H_DIVIDE = "÷"
|
||||
|
||||
## Arrows
|
||||
H_UTRI = "▲"
|
||||
@@ -338,3 +378,60 @@ class nwUnicode:
|
||||
H_LTRIS = "◂"
|
||||
|
||||
# END Class nwUnicode
|
||||
|
||||
class nwHtmlUnicode():
|
||||
|
||||
U_TO_H = {
|
||||
## Quotes
|
||||
nwUnicode.U_QUOT : nwUnicode.H_QUOT,
|
||||
nwUnicode.U_APOS : nwUnicode.H_APOS,
|
||||
nwUnicode.U_LAQUO : nwUnicode.H_LAQUO,
|
||||
nwUnicode.U_RAQUO : nwUnicode.H_RAQUO,
|
||||
nwUnicode.U_LSQUO : nwUnicode.H_LSQUO,
|
||||
nwUnicode.U_RSQUO : nwUnicode.H_RSQUO,
|
||||
nwUnicode.U_SBQUO : nwUnicode.H_SBQUO,
|
||||
nwUnicode.U_SUQUO : nwUnicode.H_SUQUO,
|
||||
nwUnicode.U_LDQUO : nwUnicode.H_LDQUO,
|
||||
nwUnicode.U_RDQUO : nwUnicode.H_RDQUO,
|
||||
nwUnicode.U_BDQUO : nwUnicode.H_BDQUO,
|
||||
nwUnicode.U_UDQUO : nwUnicode.H_UDQUO,
|
||||
nwUnicode.U_LSAQUO : nwUnicode.H_LSAQUO,
|
||||
nwUnicode.U_RSAQUO : nwUnicode.H_RSAQUO,
|
||||
nwUnicode.U_BDRQUO : nwUnicode.H_BDRQUO,
|
||||
nwUnicode.U_LCQUO : nwUnicode.H_LCQUO,
|
||||
nwUnicode.U_RCQUO : nwUnicode.H_RCQUO,
|
||||
nwUnicode.U_LWCQUO : nwUnicode.H_LWCQUO,
|
||||
nwUnicode.U_RWCQUO : nwUnicode.H_RWCQUO,
|
||||
|
||||
## Punctuation
|
||||
nwUnicode.U_FGDASH : nwUnicode.H_FGDASH,
|
||||
nwUnicode.U_ENDASH : nwUnicode.H_ENDASH,
|
||||
nwUnicode.U_EMDASH : nwUnicode.H_EMDASH,
|
||||
nwUnicode.U_HBAR : nwUnicode.H_HBAR,
|
||||
nwUnicode.U_HELLIP : nwUnicode.H_HELLIP,
|
||||
nwUnicode.U_MAPOSS : nwUnicode.H_MAPOSS,
|
||||
nwUnicode.U_PRIME : nwUnicode.H_PRIME,
|
||||
nwUnicode.U_DPRIME : nwUnicode.H_DPRIME,
|
||||
|
||||
## Spaces
|
||||
nwUnicode.U_NBSP : nwUnicode.H_NBSP,
|
||||
nwUnicode.U_THSP : nwUnicode.H_THSP,
|
||||
nwUnicode.U_THNBSP : nwUnicode.H_THNBSP,
|
||||
nwUnicode.U_ENSP : nwUnicode.H_ENSP,
|
||||
nwUnicode.U_EMSP : nwUnicode.H_EMSP,
|
||||
|
||||
## Symbols
|
||||
nwUnicode.U_CHECK : nwUnicode.H_CHECK,
|
||||
nwUnicode.U_CROSS : nwUnicode.H_CROSS,
|
||||
nwUnicode.U_BULL : nwUnicode.H_BULL,
|
||||
nwUnicode.U_TRBULL : nwUnicode.H_TRBULL,
|
||||
nwUnicode.U_HYBULL : nwUnicode.H_HYBULL,
|
||||
nwUnicode.U_FLOWER : nwUnicode.H_FLOWER,
|
||||
nwUnicode.U_PERMIL : nwUnicode.H_PERMIL,
|
||||
nwUnicode.U_DEGREE : nwUnicode.H_DEGREE,
|
||||
nwUnicode.U_MINUS : nwUnicode.H_MINUS,
|
||||
nwUnicode.U_TIMES : nwUnicode.H_TIMES,
|
||||
nwUnicode.U_DIVIDE : nwUnicode.H_DIVIDE,
|
||||
}
|
||||
|
||||
# END Class nwHtmlUnicode
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Enums
|
||||
"""
|
||||
novelWriter – Enums
|
||||
===================
|
||||
Global enum values
|
||||
|
||||
novelWriter – Enums
|
||||
=====================
|
||||
All enum values
|
||||
File History:
|
||||
Created: 2018-11-02 [0.0.1]
|
||||
|
||||
File History:
|
||||
Created: 2018-11-02 [0.0.1]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
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 enum import Enum
|
||||
@@ -100,19 +99,12 @@ class nwDocAction(Enum):
|
||||
|
||||
class nwDocInsert(Enum):
|
||||
|
||||
NO_INSERT = 0
|
||||
HARD_BREAK = 1
|
||||
NB_SPACE = 2
|
||||
THIN_SPACE = 3
|
||||
THIN_NB_SPACE = 4
|
||||
SHORT_DASH = 5
|
||||
LONG_DASH = 6
|
||||
ELLIPSIS = 7
|
||||
QUOTE_LS = 8
|
||||
QUOTE_RS = 9
|
||||
QUOTE_LD = 10
|
||||
QUOTE_RD = 11
|
||||
MODAPOS_S = 12
|
||||
NO_INSERT = 0
|
||||
HARD_BREAK = 1
|
||||
QUOTE_LS = 2
|
||||
QUOTE_RS = 3
|
||||
QUOTE_LD = 4
|
||||
QUOTE_RD = 5
|
||||
|
||||
# END Enum nwDocInsert
|
||||
|
||||
@@ -135,13 +127,14 @@ class nwOutline(Enum):
|
||||
WCOUNT = 5
|
||||
PCOUNT = 6
|
||||
POV = 7
|
||||
CHAR = 8
|
||||
PLOT = 9
|
||||
TIME = 10
|
||||
WORLD = 11
|
||||
OBJECT = 12
|
||||
ENTITY = 13
|
||||
CUSTOM = 14
|
||||
SYNOP = 15
|
||||
FOCUS = 8
|
||||
CHAR = 9
|
||||
PLOT = 10
|
||||
TIME = 11
|
||||
WORLD = 12
|
||||
OBJECT = 13
|
||||
ENTITY = 14
|
||||
CUSTOM = 15
|
||||
SYNOP = 16
|
||||
|
||||
# END Enum nwOutline
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter ISO Codes
|
||||
"""
|
||||
novelWriter – ISO Codes
|
||||
=======================
|
||||
Handles translating language codes to language names
|
||||
|
||||
novelWriter – ISO Codes
|
||||
=========================
|
||||
Handles translating language codes to language names
|
||||
File History:
|
||||
Created: 2019-11-05 [0.4.0]
|
||||
|
||||
File History:
|
||||
Created: 2019-11-05 [0.4.0]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
class isoLanguage():
|
||||
|
||||
@@ -5,6 +5,8 @@ from nw.core.index import NWIndex
|
||||
from nw.core.project import NWProject
|
||||
from nw.core.spellcheck import NWSpellCheck, NWSpellEnchant, NWSpellSimple
|
||||
from nw.core.tohtml import ToHtml
|
||||
from nw.core.toodt import ToOdt
|
||||
from nw.core.tomd import ToMarkdown
|
||||
from nw.core.tools import countWords, numberToRoman, numberToWord
|
||||
|
||||
__all__ = [
|
||||
@@ -18,4 +20,6 @@ __all__ = [
|
||||
"NWSpellEnchant",
|
||||
"NWSpellSimple",
|
||||
"ToHtml",
|
||||
"ToOdt",
|
||||
"ToMarkdown",
|
||||
]
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Project Document
|
||||
"""
|
||||
novelWriter – Project Document
|
||||
==============================
|
||||
Data class for a single novelWriter document
|
||||
|
||||
novelWriter – Project Document
|
||||
================================
|
||||
Class holding a single novelWriter document
|
||||
File History:
|
||||
Created: 2018-09-29 [0.0.1]
|
||||
|
||||
File History:
|
||||
Created: 2018-09-29 [0.0.1]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
656
nw/core/index.py
@@ -1,28 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Project Index
|
||||
"""
|
||||
novelWriter – Project Index
|
||||
===========================
|
||||
Data class for the project index of tags, headers and references
|
||||
|
||||
novelWriter – Project Index
|
||||
=============================
|
||||
Class holding the project index of tags, headers and references
|
||||
File History:
|
||||
Created: 2019-05-27 [0.1.4]
|
||||
|
||||
File History:
|
||||
Created: 2019-05-27 [0.1.4]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
@@ -37,11 +36,15 @@ from nw.constants import (
|
||||
)
|
||||
from nw.core.document import NWDoc
|
||||
from nw.core.tools import countWords
|
||||
from nw.common import isHandle, isTitleTag, isItemClass, isItemLayout
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class NWIndex():
|
||||
|
||||
H_VALID = ("H0", "H1", "H2", "H3", "H4")
|
||||
H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4}
|
||||
|
||||
def __init__(self, theProject, theParent):
|
||||
|
||||
# Internal
|
||||
@@ -51,18 +54,16 @@ class NWIndex():
|
||||
self.indexBroken = False
|
||||
|
||||
# Indices
|
||||
self.tagIndex = None
|
||||
self.refIndex = None
|
||||
self.novelIndex = None
|
||||
self.noteIndex = None
|
||||
self.textCounts = None
|
||||
self._tagIndex = {}
|
||||
self._refIndex = {}
|
||||
self._novelIndex = {}
|
||||
self._noteIndex = {}
|
||||
self._textCounts = {}
|
||||
|
||||
# TimeStamps
|
||||
self.timeNovel = 0
|
||||
self.timeNote = 0
|
||||
self.timeIndex = 0
|
||||
|
||||
self.clearIndex()
|
||||
self._timeNovel = 0
|
||||
self._timeNotes = 0
|
||||
self._timeIndex = 0
|
||||
|
||||
return
|
||||
|
||||
@@ -73,14 +74,14 @@ class NWIndex():
|
||||
def clearIndex(self):
|
||||
"""Clear the index dictionaries and time stamps.
|
||||
"""
|
||||
self.tagIndex = {}
|
||||
self.refIndex = {}
|
||||
self.novelIndex = {}
|
||||
self.noteIndex = {}
|
||||
self.textCounts = {}
|
||||
self.timeNovel = 0
|
||||
self.timeNote = 0
|
||||
self.timeIndex = 0
|
||||
self._tagIndex = {}
|
||||
self._refIndex = {}
|
||||
self._novelIndex = {}
|
||||
self._noteIndex = {}
|
||||
self._textCounts = {}
|
||||
self._timeNovel = 0
|
||||
self._timeNotes = 0
|
||||
self._timeIndex = 0
|
||||
return
|
||||
|
||||
def deleteHandle(self, tHandle):
|
||||
@@ -89,17 +90,17 @@ class NWIndex():
|
||||
logger.debug("Removing item %s from the index" % tHandle)
|
||||
|
||||
delTags = []
|
||||
for tTag in self.tagIndex:
|
||||
if self.tagIndex[tTag][1] == tHandle:
|
||||
for tTag in self._tagIndex:
|
||||
if self._tagIndex[tTag][1] == tHandle:
|
||||
delTags.append(tTag)
|
||||
|
||||
for tTag in delTags:
|
||||
self.tagIndex.pop(tTag, None)
|
||||
self._tagIndex.pop(tTag, None)
|
||||
|
||||
self.refIndex.pop(tHandle, None)
|
||||
self.novelIndex.pop(tHandle, None)
|
||||
self.noteIndex.pop(tHandle, None)
|
||||
self.textCounts.pop(tHandle, None)
|
||||
self._refIndex.pop(tHandle, None)
|
||||
self._novelIndex.pop(tHandle, None)
|
||||
self._noteIndex.pop(tHandle, None)
|
||||
self._textCounts.pop(tHandle, None)
|
||||
|
||||
return
|
||||
|
||||
@@ -123,6 +124,21 @@ class NWIndex():
|
||||
|
||||
return True
|
||||
|
||||
def novelChangedSince(self, checkTime):
|
||||
"""Check if the novel index has changed since a given time.
|
||||
"""
|
||||
return self._timeNovel > checkTime
|
||||
|
||||
def notesChangedSince(self, checkTime):
|
||||
"""Check if the notes index has changed since a given time.
|
||||
"""
|
||||
return self._timeNotes > checkTime
|
||||
|
||||
def indexChangedSince(self, checkTime):
|
||||
"""Check if the index has changed since a given time.
|
||||
"""
|
||||
return self._timeIndex > checkTime
|
||||
|
||||
##
|
||||
# Load and Save Index to/from File
|
||||
##
|
||||
@@ -138,21 +154,26 @@ class NWIndex():
|
||||
try:
|
||||
with open(indexFile, mode="r", encoding="utf8") as inFile:
|
||||
theData = json.load(inFile)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.error("Failed to load index file")
|
||||
logger.error(str(e))
|
||||
nw.logException()
|
||||
self.indexBroken = True
|
||||
self.theParent.makeAlert(
|
||||
"Could not load cached index file. Rebuilding index.",
|
||||
nwAlert.WARN
|
||||
)
|
||||
return False
|
||||
|
||||
self.tagIndex = theData.get("tagIndex", {})
|
||||
self.refIndex = theData.get("refIndex", {})
|
||||
self.novelIndex = theData.get("novelIndex", {})
|
||||
self.noteIndex = theData.get("noteIndex", {})
|
||||
self.textCounts = theData.get("textCounts", {})
|
||||
self._tagIndex = theData.get("tagIndex", {})
|
||||
self._refIndex = theData.get("refIndex", {})
|
||||
self._novelIndex = theData.get("novelIndex", {})
|
||||
self._noteIndex = theData.get("noteIndex", {})
|
||||
self._textCounts = theData.get("textCounts", {})
|
||||
|
||||
nowTime = round(time())
|
||||
self.timeNovel = nowTime
|
||||
self.timeNote = nowTime
|
||||
self.timeIndex = nowTime
|
||||
self._timeNovel = nowTime
|
||||
self._timeNotes = nowTime
|
||||
self._timeIndex = nowTime
|
||||
|
||||
self.checkIndex()
|
||||
|
||||
@@ -168,15 +189,15 @@ class NWIndex():
|
||||
try:
|
||||
with open(indexFile, mode="w+", encoding="utf8") as outFile:
|
||||
json.dump({
|
||||
"tagIndex" : self.tagIndex,
|
||||
"refIndex" : self.refIndex,
|
||||
"novelIndex" : self.novelIndex,
|
||||
"noteIndex" : self.noteIndex,
|
||||
"textCounts" : self.textCounts,
|
||||
"tagIndex" : self._tagIndex,
|
||||
"refIndex" : self._refIndex,
|
||||
"novelIndex" : self._novelIndex,
|
||||
"noteIndex" : self._noteIndex,
|
||||
"textCounts" : self._textCounts,
|
||||
}, outFile, indent=2)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.error("Failed to save index file")
|
||||
logger.error(str(e))
|
||||
nw.logException()
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -186,37 +207,25 @@ class NWIndex():
|
||||
elements it should.
|
||||
"""
|
||||
logger.debug("Checking index")
|
||||
self.indexBroken = False
|
||||
tStart = time()
|
||||
|
||||
try:
|
||||
for tTag in self.tagIndex:
|
||||
if len(self.tagIndex[tTag]) != 4:
|
||||
self.indexBroken = True
|
||||
|
||||
for tHandle in self.refIndex:
|
||||
for sTitle in self.refIndex[tHandle]:
|
||||
for tEntry in self.refIndex[tHandle][sTitle]["tags"]:
|
||||
if len(tEntry) != 3:
|
||||
self.indexBroken = True
|
||||
|
||||
for tHandle in self.novelIndex:
|
||||
for sLine in self.novelIndex[tHandle]:
|
||||
if len(self.novelIndex[tHandle][sLine].keys()) != 8:
|
||||
self.indexBroken = True
|
||||
|
||||
for tHandle in self.noteIndex:
|
||||
for sLine in self.noteIndex[tHandle]:
|
||||
if len(self.noteIndex[tHandle][sLine].keys()) != 8:
|
||||
self.indexBroken = True
|
||||
|
||||
for tHandle in self.textCounts:
|
||||
if len(self.textCounts[tHandle]) != 3:
|
||||
self.indexBroken = True
|
||||
self._checkTagIndex()
|
||||
self._checkRefIndex()
|
||||
self._checkNovelNoteIndex("novelIndex")
|
||||
self._checkNovelNoteIndex("noteIndex")
|
||||
self._checkTextCounts()
|
||||
self.indexBroken = False
|
||||
|
||||
except Exception:
|
||||
logger.error("Error while checking index")
|
||||
nw.logException()
|
||||
self.indexBroken = True
|
||||
|
||||
tEnd = time()
|
||||
logger.debug("Index check took %.3f ms" % ((tEnd - tStart)*1000))
|
||||
logger.debug("Index check complete")
|
||||
|
||||
if self.indexBroken:
|
||||
self.clearIndex()
|
||||
self.theParent.makeAlert(
|
||||
@@ -254,7 +263,7 @@ class NWIndex():
|
||||
|
||||
# Run word counter for the whole text
|
||||
cC, wC, pC = countWords(theText)
|
||||
self.textCounts[tHandle] = [cC, wC, pC]
|
||||
self._textCounts[tHandle] = [cC, wC, pC]
|
||||
|
||||
# If the file is archived or trashed, we don't index the file itself
|
||||
if self.theProject.projTree.isTrashRoot(theItem.itemParent):
|
||||
@@ -271,25 +280,27 @@ class NWIndex():
|
||||
|
||||
# Check file type, and reset its old index
|
||||
# Also add a dummy entry T000000 in case the file has no title
|
||||
self.refIndex[tHandle] = {}
|
||||
self.refIndex[tHandle]["T000000"] = {
|
||||
self._refIndex[tHandle] = {}
|
||||
self._refIndex[tHandle]["T000000"] = {
|
||||
"tags" : [],
|
||||
"updated" : round(time()),
|
||||
}
|
||||
if itemLayout == nwItemLayout.NOTE:
|
||||
self.noteIndex[tHandle] = {}
|
||||
self._novelIndex.pop(tHandle, None)
|
||||
self._noteIndex[tHandle] = {}
|
||||
isNovel = False
|
||||
else:
|
||||
self.novelIndex[tHandle] = {}
|
||||
self._novelIndex[tHandle] = {}
|
||||
self._noteIndex.pop(tHandle, None)
|
||||
isNovel = True
|
||||
|
||||
# Also clear references to file in tag index
|
||||
clearTags = []
|
||||
for aTag in self.tagIndex:
|
||||
if self.tagIndex[aTag][1] == tHandle:
|
||||
for aTag in self._tagIndex:
|
||||
if self._tagIndex[aTag][1] == tHandle:
|
||||
clearTags.append(aTag)
|
||||
for aTag in clearTags:
|
||||
self.tagIndex.pop(aTag)
|
||||
self._tagIndex.pop(aTag)
|
||||
|
||||
nLine = 0
|
||||
nTitle = 0
|
||||
@@ -301,7 +312,7 @@ class NWIndex():
|
||||
if nChar == 0:
|
||||
continue
|
||||
|
||||
if aLine.startswith(r"#"):
|
||||
if aLine.startswith("#"):
|
||||
isTitle = self._indexTitle(tHandle, isNovel, aLine, nLine, itemLayout)
|
||||
if isTitle and nLine > 0:
|
||||
if nTitle > 0:
|
||||
@@ -309,11 +320,10 @@ class NWIndex():
|
||||
self._indexWordCounts(tHandle, isNovel, lastText, nTitle)
|
||||
nTitle = nLine
|
||||
|
||||
elif aLine.startswith(r"@"):
|
||||
self._indexNoteRef(tHandle, aLine, nLine, nTitle)
|
||||
self._indexTag(tHandle, aLine, nLine, nTitle, itemClass)
|
||||
elif aLine.startswith("@"):
|
||||
self._indexKeyword(tHandle, aLine, nLine, nTitle, itemClass)
|
||||
|
||||
elif aLine.startswith(r"%"):
|
||||
elif aLine.startswith("%"):
|
||||
if nTitle > 0:
|
||||
toCheck = aLine[1:].lstrip()
|
||||
synTag = toCheck[:9].lower()
|
||||
@@ -328,13 +338,18 @@ class NWIndex():
|
||||
lastText = "\n".join(theLines[nTitle-1:])
|
||||
self._indexWordCounts(tHandle, isNovel, lastText, nTitle)
|
||||
|
||||
# Index page with no titles and references
|
||||
if nTitle == 0:
|
||||
self._indexPage(tHandle, isNovel, itemLayout)
|
||||
self._indexWordCounts(tHandle, isNovel, theText, nTitle)
|
||||
|
||||
# Update timestamps for index changes
|
||||
nowTime = round(time())
|
||||
self.timeIndex = nowTime
|
||||
self._timeIndex = nowTime
|
||||
if isNovel:
|
||||
self.timeNovel = nowTime
|
||||
self._timeNovel = nowTime
|
||||
else:
|
||||
self.timeNote = nowTime
|
||||
self._timeNotes = nowTime
|
||||
|
||||
return True
|
||||
|
||||
@@ -362,7 +377,7 @@ class NWIndex():
|
||||
return False
|
||||
|
||||
sTitle = "T%06d" % nLine
|
||||
self.refIndex[tHandle][sTitle] = {
|
||||
self._refIndex[tHandle][sTitle] = {
|
||||
"tags" : [],
|
||||
"updated" : round(time()),
|
||||
}
|
||||
@@ -379,33 +394,56 @@ class NWIndex():
|
||||
|
||||
if hText != "":
|
||||
if isNovel:
|
||||
if tHandle in self.novelIndex:
|
||||
self.novelIndex[tHandle][sTitle] = theData
|
||||
if tHandle in self._novelIndex:
|
||||
self._novelIndex[tHandle][sTitle] = theData
|
||||
else:
|
||||
if tHandle in self.noteIndex:
|
||||
self.noteIndex[tHandle][sTitle] = theData
|
||||
if tHandle in self._noteIndex:
|
||||
self._noteIndex[tHandle][sTitle] = theData
|
||||
|
||||
return True
|
||||
|
||||
def _indexPage(self, tHandle, isNovel, itemLayout):
|
||||
"""Index a page with no title.
|
||||
"""
|
||||
theData = {
|
||||
"level" : "H0",
|
||||
"title" : "Untitled Page",
|
||||
"layout" : itemLayout.name,
|
||||
"synopsis" : "",
|
||||
"cCount" : 0,
|
||||
"wCount" : 0,
|
||||
"pCount" : 0,
|
||||
"updated" : round(time()),
|
||||
}
|
||||
|
||||
if isNovel:
|
||||
if tHandle in self._novelIndex:
|
||||
self._novelIndex[tHandle]["T000000"] = theData
|
||||
else:
|
||||
if tHandle in self._noteIndex:
|
||||
self._noteIndex[tHandle]["T000000"] = theData
|
||||
|
||||
return
|
||||
|
||||
def _indexWordCounts(self, tHandle, isNovel, theText, nTitle):
|
||||
"""Count text stats and save the counts to the index.
|
||||
"""
|
||||
cC, wC, pC = countWords(theText)
|
||||
sTitle = "T%06d" % nTitle
|
||||
if isNovel:
|
||||
if tHandle in self.novelIndex:
|
||||
if sTitle in self.novelIndex[tHandle]:
|
||||
self.novelIndex[tHandle][sTitle]["cCount"] = cC
|
||||
self.novelIndex[tHandle][sTitle]["wCount"] = wC
|
||||
self.novelIndex[tHandle][sTitle]["pCount"] = pC
|
||||
self.novelIndex[tHandle][sTitle]["updated"] = round(time())
|
||||
if tHandle in self._novelIndex:
|
||||
if sTitle in self._novelIndex[tHandle]:
|
||||
self._novelIndex[tHandle][sTitle]["cCount"] = cC
|
||||
self._novelIndex[tHandle][sTitle]["wCount"] = wC
|
||||
self._novelIndex[tHandle][sTitle]["pCount"] = pC
|
||||
self._novelIndex[tHandle][sTitle]["updated"] = round(time())
|
||||
else:
|
||||
if tHandle in self.noteIndex:
|
||||
if sTitle in self.noteIndex[tHandle]:
|
||||
self.noteIndex[tHandle][sTitle]["cCount"] = cC
|
||||
self.noteIndex[tHandle][sTitle]["wCount"] = wC
|
||||
self.noteIndex[tHandle][sTitle]["pCount"] = pC
|
||||
self.noteIndex[tHandle][sTitle]["updated"] = round(time())
|
||||
if tHandle in self._noteIndex:
|
||||
if sTitle in self._noteIndex[tHandle]:
|
||||
self._noteIndex[tHandle][sTitle]["cCount"] = cC
|
||||
self._noteIndex[tHandle][sTitle]["wCount"] = wC
|
||||
self._noteIndex[tHandle][sTitle]["pCount"] = pC
|
||||
self._noteIndex[tHandle][sTitle]["updated"] = round(time())
|
||||
return
|
||||
|
||||
def _indexSynopsis(self, tHandle, isNovel, theText, nTitle):
|
||||
@@ -413,44 +451,39 @@ class NWIndex():
|
||||
"""
|
||||
sTitle = "T%06d" % nTitle
|
||||
if isNovel:
|
||||
if tHandle in self.novelIndex:
|
||||
if sTitle in self.novelIndex[tHandle]:
|
||||
self.novelIndex[tHandle][sTitle]["synopsis"] = theText
|
||||
self.novelIndex[tHandle][sTitle]["updated"] = round(time())
|
||||
if tHandle in self._novelIndex:
|
||||
if sTitle in self._novelIndex[tHandle]:
|
||||
self._novelIndex[tHandle][sTitle]["synopsis"] = theText
|
||||
self._novelIndex[tHandle][sTitle]["updated"] = round(time())
|
||||
else:
|
||||
if tHandle in self.noteIndex:
|
||||
if sTitle in self.noteIndex[tHandle]:
|
||||
self.noteIndex[tHandle][sTitle]["synopsis"] = theText
|
||||
self.noteIndex[tHandle][sTitle]["updated"] = round(time())
|
||||
if tHandle in self._noteIndex:
|
||||
if sTitle in self._noteIndex[tHandle]:
|
||||
self._noteIndex[tHandle][sTitle]["synopsis"] = theText
|
||||
self._noteIndex[tHandle][sTitle]["updated"] = round(time())
|
||||
return
|
||||
|
||||
def _indexNoteRef(self, tHandle, aLine, nLine, nTitle):
|
||||
def _indexKeyword(self, tHandle, aLine, nLine, nTitle, itemClass):
|
||||
"""Validate and save the information about a reference to a tag
|
||||
in another file.
|
||||
"""
|
||||
isValid, theBits, _ = self.scanThis(aLine)
|
||||
if not isValid or len(theBits) == 0:
|
||||
return False
|
||||
if not isValid or len(theBits) < 2:
|
||||
logger.warning("Skipping keyword with %d value(s) in %s" % (len(theBits), tHandle))
|
||||
return
|
||||
|
||||
if theBits[0] not in nwKeyWords.VALID_KEYS:
|
||||
logger.warning("Skipping invalid keyword '%s' in %s" % (theBits[0], tHandle))
|
||||
return
|
||||
|
||||
sTitle = "T%06d" % nTitle
|
||||
if sTitle in self.refIndex[tHandle] and theBits[0] != nwKeyWords.TAG_KEY:
|
||||
for aVal in theBits[1:]:
|
||||
self.refIndex[tHandle][sTitle]["tags"].append([nLine, theBits[0], aVal])
|
||||
|
||||
return True
|
||||
|
||||
def _indexTag(self, tHandle, aLine, nLine, nTitle, itemClass):
|
||||
"""Validate and save the information from a tag.
|
||||
"""
|
||||
isValid, theBits, thePos = self.scanThis(aLine)
|
||||
if not isValid or len(theBits) != 2:
|
||||
return False
|
||||
|
||||
if theBits[0] == nwKeyWords.TAG_KEY:
|
||||
sTitle = "T%06d" % nTitle
|
||||
self.tagIndex[theBits[1]] = [nLine, tHandle, itemClass.name, sTitle]
|
||||
self._tagIndex[theBits[1]] = [nLine, tHandle, itemClass.name, sTitle]
|
||||
|
||||
return True
|
||||
elif sTitle in self._refIndex[tHandle]:
|
||||
for aVal in theBits[1:]:
|
||||
self._refIndex[tHandle][sTitle]["tags"].append([nLine, theBits[0], aVal])
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Check @ Lines
|
||||
@@ -512,8 +545,8 @@ class NWIndex():
|
||||
# is ignored
|
||||
if theBits[0] == nwKeyWords.TAG_KEY and nBits > 1:
|
||||
isGood[0] = True
|
||||
if theBits[1] in self.tagIndex:
|
||||
if self.tagIndex[theBits[1]][1] == tItem.itemHandle:
|
||||
if theBits[1] in self._tagIndex:
|
||||
if self._tagIndex[theBits[1]][1] == tItem.itemHandle:
|
||||
isGood[1] = True
|
||||
else:
|
||||
isGood[1] = False
|
||||
@@ -523,8 +556,8 @@ class NWIndex():
|
||||
|
||||
# If we're still here, we better check that the references exist
|
||||
for n in range(1, nBits):
|
||||
if theBits[n] in self.tagIndex:
|
||||
isGood[n] = nwKeyWords.KEY_CLASS[theBits[0]].name == self.tagIndex[theBits[n]][2]
|
||||
if theBits[n] in self._tagIndex:
|
||||
isGood[n] = nwKeyWords.KEY_CLASS[theBits[0]].name == self._tagIndex[theBits[n]][2]
|
||||
|
||||
return isGood
|
||||
|
||||
@@ -532,23 +565,102 @@ class NWIndex():
|
||||
# Extract Data
|
||||
##
|
||||
|
||||
def getNovelStructure(self, skipExcluded=True):
|
||||
"""Builds a list of all titles in the novel, in the correct
|
||||
order as they appear in the tree view and in the respective
|
||||
document files, but skipping all note files.
|
||||
def novelStructure(self, skipExcluded=True):
|
||||
"""Iterate over all titles in the novel, in the correct order as
|
||||
they appear in the tree view and in the respective document
|
||||
files, but skipping all note files.
|
||||
"""
|
||||
theStructure = []
|
||||
for tItem in self.theProject.projTree:
|
||||
if tItem is not None:
|
||||
if not tItem.isExported and skipExcluded:
|
||||
continue
|
||||
tHandle = tItem.itemHandle
|
||||
if tHandle not in self.novelIndex:
|
||||
continue
|
||||
for sTitle in sorted(self.novelIndex[tHandle].keys()):
|
||||
theStructure.append("%s:%s" % (tHandle, sTitle))
|
||||
for tHandle in self._listNovelHandles(skipExcluded):
|
||||
for sTitle in sorted(self._novelIndex[tHandle]):
|
||||
tKey = "%s:%s" % (tHandle, sTitle)
|
||||
yield tKey, tHandle, sTitle, self._novelIndex[tHandle][sTitle]
|
||||
|
||||
return theStructure
|
||||
def getNovelWordCount(self, skipExcluded=True):
|
||||
"""Count the number of words in the novel project.
|
||||
"""
|
||||
wCount = 0
|
||||
for tHandle in self._listNovelHandles(skipExcluded):
|
||||
for sTitle in self._novelIndex[tHandle]:
|
||||
wCount += self._novelIndex[tHandle][sTitle]["wCount"]
|
||||
|
||||
return wCount
|
||||
|
||||
def getNovelTitleCounts(self, skipExcluded=True):
|
||||
"""Count the number of titles in the novel project.
|
||||
"""
|
||||
hCount = [0, 0, 0, 0, 0]
|
||||
for tHandle in self._listNovelHandles(skipExcluded):
|
||||
for sTitle in self._novelIndex[tHandle]:
|
||||
theData = self._novelIndex[tHandle][sTitle]
|
||||
iLevel = self.H_LEVEL.get(theData["level"], 0)
|
||||
hCount[iLevel] += 1
|
||||
|
||||
return hCount
|
||||
|
||||
def getHandleWordCounts(self, tHandle):
|
||||
"""Get all header word counts for a specific handle.
|
||||
"""
|
||||
theCounts = []
|
||||
hRecord = self._novelIndex.get(tHandle, None)
|
||||
if hRecord is None:
|
||||
hRecord = self._noteIndex.get(tHandle, None)
|
||||
if hRecord is None:
|
||||
return theCounts
|
||||
|
||||
for sTitle, sData in hRecord.items():
|
||||
theCounts.append(("%s:%s" % (tHandle, sTitle), sData["wCount"]))
|
||||
|
||||
return theCounts
|
||||
|
||||
def getHandleHeaders(self, tHandle):
|
||||
"""Get all headers for a specific handle.
|
||||
"""
|
||||
theHeaders = []
|
||||
hRecord = self._novelIndex.get(tHandle, None)
|
||||
if hRecord is None:
|
||||
hRecord = self._noteIndex.get(tHandle, None)
|
||||
if hRecord is None:
|
||||
return theHeaders
|
||||
|
||||
for sTitle, sData in hRecord.items():
|
||||
theHeaders.append((sTitle, sData["level"], sData["title"]))
|
||||
|
||||
return theHeaders
|
||||
|
||||
def getTableOfContents(self, maxDepth, skipExcluded=True):
|
||||
"""Generate a table of contents up to a maxiumum depth.
|
||||
"""
|
||||
tOrder = []
|
||||
tData = {}
|
||||
pKey = None
|
||||
for tHandle in self._listNovelHandles(skipExcluded):
|
||||
for sTitle in sorted(self._novelIndex[tHandle]):
|
||||
tKey = "%s:%s" % (tHandle, sTitle)
|
||||
theData = self._novelIndex[tHandle][sTitle]
|
||||
iLevel = self.H_LEVEL.get(theData["level"], 0)
|
||||
if iLevel > maxDepth:
|
||||
if pKey in tData:
|
||||
theData["wCount"]
|
||||
tData[pKey]["words"] += theData["wCount"]
|
||||
else:
|
||||
pKey = tKey
|
||||
tOrder.append(tKey)
|
||||
tData[tKey] = {
|
||||
"level": iLevel,
|
||||
"title": theData["title"],
|
||||
"words": theData["wCount"],
|
||||
}
|
||||
|
||||
theToC = []
|
||||
for tKey in tOrder:
|
||||
theToC.append((
|
||||
tKey,
|
||||
tData[tKey]["level"],
|
||||
tData[tKey]["title"],
|
||||
tData[tKey]["words"],
|
||||
))
|
||||
|
||||
return theToC
|
||||
|
||||
def getCounts(self, tHandle, sTitle=None):
|
||||
"""Returns the counts for a file, or a section of a file
|
||||
@@ -559,21 +671,21 @@ class NWIndex():
|
||||
pC = 0
|
||||
|
||||
if sTitle is None:
|
||||
if tHandle in self.textCounts:
|
||||
cC = self.textCounts[tHandle][0]
|
||||
wC = self.textCounts[tHandle][1]
|
||||
pC = self.textCounts[tHandle][2]
|
||||
if tHandle in self._textCounts:
|
||||
cC = self._textCounts[tHandle][0]
|
||||
wC = self._textCounts[tHandle][1]
|
||||
pC = self._textCounts[tHandle][2]
|
||||
else:
|
||||
if tHandle in self.novelIndex:
|
||||
if sTitle in self.novelIndex[tHandle]:
|
||||
cC = self.novelIndex[tHandle][sTitle]["cCount"]
|
||||
wC = self.novelIndex[tHandle][sTitle]["wCount"]
|
||||
pC = self.novelIndex[tHandle][sTitle]["pCount"]
|
||||
elif tHandle in self.noteIndex:
|
||||
if sTitle in self.noteIndex[tHandle]:
|
||||
cC = self.noteIndex[tHandle][sTitle]["cCount"]
|
||||
wC = self.noteIndex[tHandle][sTitle]["wCount"]
|
||||
pC = self.noteIndex[tHandle][sTitle]["pCount"]
|
||||
if tHandle in self._novelIndex:
|
||||
if sTitle in self._novelIndex[tHandle]:
|
||||
cC = self._novelIndex[tHandle][sTitle]["cCount"]
|
||||
wC = self._novelIndex[tHandle][sTitle]["wCount"]
|
||||
pC = self._novelIndex[tHandle][sTitle]["pCount"]
|
||||
elif tHandle in self._noteIndex:
|
||||
if sTitle in self._noteIndex[tHandle]:
|
||||
cC = self._noteIndex[tHandle][sTitle]["cCount"]
|
||||
wC = self._noteIndex[tHandle][sTitle]["wCount"]
|
||||
pC = self._noteIndex[tHandle][sTitle]["pCount"]
|
||||
|
||||
return cC, wC, pC
|
||||
|
||||
@@ -585,16 +697,25 @@ class NWIndex():
|
||||
for tKey in nwKeyWords.KEY_CLASS:
|
||||
theRefs[tKey] = []
|
||||
|
||||
if tHandle not in self.refIndex:
|
||||
if tHandle not in self._refIndex:
|
||||
return theRefs
|
||||
|
||||
for refTitle in self.refIndex[tHandle]:
|
||||
for aTag in self.refIndex[tHandle][refTitle].get("tags", []):
|
||||
for refTitle in self._refIndex[tHandle]:
|
||||
for aTag in self._refIndex[tHandle][refTitle].get("tags", []):
|
||||
if len(aTag) == 3 and (sTitle is None or sTitle == refTitle):
|
||||
theRefs[aTag[1]].append(aTag[2])
|
||||
if aTag[1] in theRefs:
|
||||
theRefs[aTag[1]].append(aTag[2])
|
||||
|
||||
return theRefs
|
||||
|
||||
def getNovelData(self, tHandle, sTitle):
|
||||
"""Return the novel data of a given handle and title.
|
||||
"""
|
||||
if tHandle in self._novelIndex:
|
||||
if sTitle in self._novelIndex[tHandle]:
|
||||
return self._novelIndex[tHandle][sTitle]
|
||||
return None
|
||||
|
||||
def getBackReferenceList(self, tHandle):
|
||||
"""Build a list of files referring back to our file, specified
|
||||
by tHandle.
|
||||
@@ -604,14 +725,14 @@ class NWIndex():
|
||||
return theRefs
|
||||
|
||||
theTags = set()
|
||||
for tTag in self.tagIndex:
|
||||
if tHandle == self.tagIndex[tTag][1]:
|
||||
for tTag in self._tagIndex:
|
||||
if tHandle == self._tagIndex[tTag][1]:
|
||||
theTags.add(tTag)
|
||||
|
||||
if theTags:
|
||||
for tHandle in self.refIndex:
|
||||
for sTitle in self.refIndex[tHandle]:
|
||||
for _, _, tTag in self.refIndex[tHandle][sTitle]["tags"]:
|
||||
for tHandle in self._refIndex:
|
||||
for sTitle in self._refIndex[tHandle]:
|
||||
for _, _, tTag in self._refIndex[tHandle][sTitle]["tags"]:
|
||||
if tTag in theTags and tHandle not in theRefs:
|
||||
theRefs[tHandle] = sTitle
|
||||
|
||||
@@ -620,10 +741,167 @@ class NWIndex():
|
||||
def getTagSource(self, theTag):
|
||||
"""Return the source location of a given tag.
|
||||
"""
|
||||
if theTag in self.tagIndex:
|
||||
theRef = self.tagIndex[theTag]
|
||||
if theTag in self._tagIndex:
|
||||
theRef = self._tagIndex[theTag]
|
||||
if len(theRef) == 4:
|
||||
return theRef[1], theRef[0], theRef[3]
|
||||
return None, 0, "T000000"
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _listNovelHandles(self, skipExcluded):
|
||||
"""Return a list of all handles that exist in the novel index.
|
||||
"""
|
||||
theHandles = []
|
||||
for tItem in self.theProject.projTree:
|
||||
if tItem is None:
|
||||
continue
|
||||
if not tItem.isExported and skipExcluded:
|
||||
continue
|
||||
if tItem.itemHandle in self._novelIndex:
|
||||
theHandles.append(tItem.itemHandle)
|
||||
|
||||
return theHandles
|
||||
|
||||
##
|
||||
# Index Checkers
|
||||
##
|
||||
|
||||
def _checkTagIndex(self):
|
||||
"""Scan the tag index for errors.
|
||||
Waring: This function raises exceptions.
|
||||
"""
|
||||
for tTag in self._tagIndex:
|
||||
if not isinstance(tTag, str):
|
||||
raise KeyError("tagIndex key is not a string")
|
||||
|
||||
tEntry = self._tagIndex[tTag]
|
||||
if len(tEntry) != 4:
|
||||
raise IndexError("tagIndex[a] expected 4 values")
|
||||
if not isinstance(tEntry[0], int):
|
||||
raise ValueError("tagIndex[a][0] is not an integer")
|
||||
if not isHandle(tEntry[1]):
|
||||
raise ValueError("tagIndex[a][1] is not a handle")
|
||||
if not isItemClass(tEntry[2]):
|
||||
raise ValueError("tagIndex[a][2] is not an nwItemClass")
|
||||
if not isTitleTag(tEntry[3]):
|
||||
raise ValueError("tagIndex[a][3] is not a title tag")
|
||||
|
||||
return
|
||||
|
||||
def _checkRefIndex(self):
|
||||
"""Scan the reference index for errors.
|
||||
Waring: This function raises exceptions.
|
||||
"""
|
||||
for tHandle in self._refIndex:
|
||||
if not isHandle(tHandle):
|
||||
raise KeyError("refIndex key is not a handle")
|
||||
|
||||
hEntry = self._refIndex[tHandle]
|
||||
for sTitle in hEntry:
|
||||
if not isTitleTag(sTitle):
|
||||
raise KeyError("refIndex[a] key is not a title tag")
|
||||
|
||||
sEntry = hEntry[sTitle]
|
||||
if "tags" not in sEntry:
|
||||
raise KeyError("refIndex[a][b] has no 'tag' key")
|
||||
for tEntry in sEntry["tags"]:
|
||||
if len(tEntry) != 3:
|
||||
raise IndexError("refIndex[a][b][tags][i] expected 3 values")
|
||||
if not isinstance(tEntry[0], int):
|
||||
raise ValueError("refIndex[a][b][tags][i][0] is not an integer")
|
||||
if not tEntry[1] in nwKeyWords.VALID_KEYS:
|
||||
raise ValueError("refIndex[a][b][tags][i][1] is not a keyword")
|
||||
if not isinstance(tEntry[2], str):
|
||||
raise ValueError("refIndex[a][b][tags][i][2] is not a string")
|
||||
|
||||
if "updated" not in sEntry:
|
||||
raise KeyError("refIndex[a][b] has no 'updated' key")
|
||||
if not isinstance(sEntry["updated"], int):
|
||||
raise ValueError("%refIndex[a][b][updated] is not an integer")
|
||||
|
||||
return
|
||||
|
||||
def _checkNovelNoteIndex(self, idxName):
|
||||
"""Scan the novel or note index for errors.
|
||||
Waring: This function raises exceptions.
|
||||
"""
|
||||
if idxName == "novelIndex":
|
||||
theIndex = self._novelIndex
|
||||
elif idxName == "noteIndex":
|
||||
theIndex = self._noteIndex
|
||||
else:
|
||||
raise IndexError("Unknown index %s" % idxName)
|
||||
|
||||
for tHandle in theIndex:
|
||||
if not isHandle(tHandle):
|
||||
raise KeyError("%s key is not a handle" % idxName)
|
||||
|
||||
hEntry = theIndex[tHandle]
|
||||
for sTitle in theIndex[tHandle]:
|
||||
if not isTitleTag(sTitle):
|
||||
raise KeyError("%s[a] key is not a title tag" % idxName)
|
||||
|
||||
sEntry = hEntry[sTitle]
|
||||
if len(sEntry) != 8:
|
||||
raise IndexError("%s[a][b] expected 8 values" % idxName)
|
||||
|
||||
if "level" not in sEntry:
|
||||
raise KeyError("%s[a][b] has no 'level' key" % idxName)
|
||||
if "title" not in sEntry:
|
||||
raise KeyError("%s[a][b] has no 'title' key" % idxName)
|
||||
if "layout" not in sEntry:
|
||||
raise KeyError("%s[a][b] has no 'layout' key" % idxName)
|
||||
if "synopsis" not in sEntry:
|
||||
raise KeyError("%s[a][b] has no 'synopsis' key" % idxName)
|
||||
if "cCount" not in sEntry:
|
||||
raise KeyError("%s[a][b] has no 'cCount' key" % idxName)
|
||||
if "wCount" not in sEntry:
|
||||
raise KeyError("%s[a][b] has no 'wCount' key" % idxName)
|
||||
if "pCount" not in sEntry:
|
||||
raise KeyError("%s[a][b] has no 'pCount' key" % idxName)
|
||||
if "updated" not in sEntry:
|
||||
raise KeyError("%s[a][b] has no 'updated' key" % idxName)
|
||||
|
||||
if not sEntry["level"] in self.H_VALID:
|
||||
raise ValueError("%s[a][b][level] is not a header level" % idxName)
|
||||
if not isinstance(sEntry["title"], str):
|
||||
raise ValueError("%s[a][b][title] is not a string" % idxName)
|
||||
if not isItemLayout(sEntry["layout"]):
|
||||
raise ValueError("%s[a][b][layout] is not an nwItemLayout" % idxName)
|
||||
if not isinstance(sEntry["synopsis"], str):
|
||||
raise ValueError("%s[a][b][synopsis] is not a string" % idxName)
|
||||
if not isinstance(sEntry["cCount"], int):
|
||||
raise ValueError("%s[a][b][cCount] is not an integer" % idxName)
|
||||
if not isinstance(sEntry["wCount"], int):
|
||||
raise ValueError("%s[a][b][wCount] is not an integer" % idxName)
|
||||
if not isinstance(sEntry["pCount"], int):
|
||||
raise ValueError("%s[a][b][pCount] is not an integer" % idxName)
|
||||
if not isinstance(sEntry["updated"], int):
|
||||
raise ValueError("%s[a][b][updated] is not an integer" % idxName)
|
||||
|
||||
return
|
||||
|
||||
def _checkTextCounts(self):
|
||||
"""Scan the text counts index for errors.
|
||||
Waring: This function raises exceptions.
|
||||
"""
|
||||
for tHandle in self._textCounts:
|
||||
if not isHandle(tHandle):
|
||||
raise KeyError("textCounts key is not a handle")
|
||||
|
||||
tEntry = self._textCounts[tHandle]
|
||||
if len(tEntry) != 3:
|
||||
raise IndexError("textCounts[a] expected 3 values")
|
||||
if not isinstance(tEntry[0], int):
|
||||
raise ValueError("textCounts[a][0] is not an integer")
|
||||
if not isinstance(tEntry[1], int):
|
||||
raise ValueError("textCounts[a][1] is not an integer")
|
||||
if not isinstance(tEntry[2], int):
|
||||
raise ValueError("textCounts[a][2] is not an integer")
|
||||
|
||||
return
|
||||
|
||||
# END Class NWIndex
|
||||
|
||||
@@ -1,36 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Project Item Class
|
||||
"""
|
||||
novelWriter – Project Item Class
|
||||
================================
|
||||
Data class for a project tree item
|
||||
|
||||
novelWriter – Project Item Class
|
||||
==================================
|
||||
Class holding the data of a project tree item
|
||||
File History:
|
||||
Created: 2018-10-27 [0.0.1]
|
||||
|
||||
File History:
|
||||
Created: 2018-10-27 [0.0.1]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from nw.common import checkInt, isHandle
|
||||
from nw.constants import nwItemType, nwItemClass, nwItemLayout
|
||||
from nw.common import (
|
||||
checkInt, isHandle, isItemClass, isItemLayout, isItemType
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -202,7 +203,7 @@ class NWItem():
|
||||
"""
|
||||
if isinstance(theType, nwItemType):
|
||||
self.itemType = theType
|
||||
elif theType in nwItemType.__members__:
|
||||
elif isItemType(theType):
|
||||
self.itemType = nwItemType[theType]
|
||||
else:
|
||||
logger.error("Unrecognised item type '%s'" % theType)
|
||||
@@ -215,7 +216,7 @@ class NWItem():
|
||||
"""
|
||||
if isinstance(theClass, nwItemClass):
|
||||
self.itemClass = theClass
|
||||
elif theClass in nwItemClass.__members__:
|
||||
elif isItemClass(theClass):
|
||||
self.itemClass = nwItemClass[theClass]
|
||||
else:
|
||||
logger.error("Unrecognised item class '%s'" % theClass)
|
||||
@@ -228,7 +229,7 @@ class NWItem():
|
||||
"""
|
||||
if isinstance(theLayout, nwItemLayout):
|
||||
self.itemLayout = theLayout
|
||||
elif theLayout in nwItemLayout.__members__:
|
||||
elif isItemLayout(theLayout):
|
||||
self.itemLayout = nwItemLayout[theLayout]
|
||||
else:
|
||||
logger.error("Unrecognised item layout '%s'" % theLayout)
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Project Options Cache
|
||||
"""
|
||||
novelWriter – Project Options Cache
|
||||
===================================
|
||||
Data class for user-defined GUI project options
|
||||
|
||||
novelWriter – Project Options Cache
|
||||
=====================================
|
||||
Class wrapping the project options state
|
||||
File History:
|
||||
Created: 2019-10-21 [0.3.1]
|
||||
Rewritten: 2020-02-19 [0.4.5]
|
||||
|
||||
File History:
|
||||
Created: 2019-10-21 [0.3.1]
|
||||
Rewritten: 2020-02-19 [0.4.5]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
import logging
|
||||
import json
|
||||
import os
|
||||
@@ -47,6 +47,7 @@ class OptionState():
|
||||
"widthCol0",
|
||||
"widthCol1",
|
||||
"widthCol2",
|
||||
"widthCol3",
|
||||
"sortCol",
|
||||
"sortOrder",
|
||||
"incNovel",
|
||||
@@ -54,6 +55,7 @@ class OptionState():
|
||||
"hideZeros",
|
||||
"hideNegative",
|
||||
"groupByDay",
|
||||
"showIdleTime",
|
||||
"histMax",
|
||||
},
|
||||
"GuiDocSplit": {
|
||||
@@ -71,12 +73,14 @@ class OptionState():
|
||||
"excludeBody",
|
||||
"textFont",
|
||||
"textSize",
|
||||
"lineHeight",
|
||||
"noStyling",
|
||||
"incSynopsis",
|
||||
"incComments",
|
||||
"incKeywords",
|
||||
"incBodyText",
|
||||
"replaceTabs",
|
||||
"replaceUCode",
|
||||
},
|
||||
"GuiOutline": {
|
||||
"headerOrder",
|
||||
@@ -87,6 +91,22 @@ class OptionState():
|
||||
"winWidth",
|
||||
"winHeight",
|
||||
"replaceColW",
|
||||
},
|
||||
"GuiProjectDetails": {
|
||||
"winWidth",
|
||||
"winHeight",
|
||||
"widthCol0",
|
||||
"widthCol1",
|
||||
"widthCol2",
|
||||
"widthCol3",
|
||||
"widthCol4",
|
||||
"wordsPerPage",
|
||||
"countFrom",
|
||||
"clearDouble",
|
||||
},
|
||||
"GuiWordList": {
|
||||
"winWidth",
|
||||
"winHeight",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,9 +130,9 @@ class OptionState():
|
||||
try:
|
||||
with open(stateFile, mode="r", encoding="utf8") as inFile:
|
||||
theState = json.load(inFile)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.error("Failed to load GUI options file")
|
||||
logger.error(str(e))
|
||||
nw.logException()
|
||||
return False
|
||||
|
||||
# Filter out unused variables
|
||||
@@ -137,9 +157,9 @@ class OptionState():
|
||||
try:
|
||||
with open(stateFile, mode="w+", encoding="utf8") as outFile:
|
||||
json.dump(self.theState, outFile, indent=2)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.error("Failed to save GUI options file")
|
||||
logger.error(str(e))
|
||||
nw.logException()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Project Wrapper
|
||||
"""
|
||||
novelWriter – Project Wrapper
|
||||
=============================
|
||||
Data class for novelWriter projects
|
||||
|
||||
novelWriter – Project Wrapper
|
||||
===============================
|
||||
Class wrapping the data of a novelWriter project
|
||||
File History:
|
||||
Created: 2018-09-29 [0.0.1]
|
||||
|
||||
File History:
|
||||
Created: 2018-09-29 [0.0.1]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
@@ -40,7 +39,7 @@ from nw.core.status import NWStatus
|
||||
from nw.core.options import OptionState
|
||||
from nw.common import (
|
||||
checkString, checkBool, checkInt, isHandle, formatTimeStamp,
|
||||
makeFileNameSafe
|
||||
makeFileNameSafe, hexToInt
|
||||
)
|
||||
from nw.constants import (
|
||||
nwFiles, nwItemType, nwItemClass, nwItemLayout, nwLabels, nwAlert
|
||||
@@ -496,7 +495,7 @@ class NWProject():
|
||||
# Check novelWriter Version
|
||||
# =========================
|
||||
|
||||
if int(hexVersion, 16) > int(nw.__hexversion__, 16):
|
||||
if hexToInt(hexVersion) > hexToInt(nw.__hexversion__):
|
||||
msgYes = self.theParent.askQuestion("Version Conflict", (
|
||||
"This project was saved by a newer version of novelWriter, version %s. "
|
||||
"This is version %s. If you continue to open the project, some attributes "
|
||||
@@ -606,10 +605,13 @@ class NWProject():
|
||||
self.theParent.setStatus("Opened Project: %s" % self.projName)
|
||||
|
||||
self._scanProjectFolder()
|
||||
self.setProjectChanged(False)
|
||||
|
||||
self.currWCount = self.lastWCount
|
||||
self.projOpened = time()
|
||||
self.projAltered = False
|
||||
|
||||
self._writeLockFile()
|
||||
self.setProjectChanged(False)
|
||||
|
||||
return True
|
||||
|
||||
@@ -724,12 +726,12 @@ class NWProject():
|
||||
|
||||
return True
|
||||
|
||||
def closeProject(self):
|
||||
def closeProject(self, idleTime=0):
|
||||
"""Close the current project and clear all meta data.
|
||||
"""
|
||||
self.optState.saveSettings()
|
||||
self.projTree.writeToCFile()
|
||||
self._appendSessionStats()
|
||||
self._appendSessionStats(idleTime)
|
||||
self._clearLockFile()
|
||||
self.clearProject()
|
||||
self.lockedBy = None
|
||||
@@ -1025,7 +1027,7 @@ class NWProject():
|
||||
by drag-and-drop. Forwarded to the NWTree class.
|
||||
"""
|
||||
if len(self.projTree) != len(newOrder):
|
||||
logger.warning("Size of new and old tree order do not match")
|
||||
logger.warning("Sizes of new and old tree order do not match")
|
||||
self.projTree.setOrder(newOrder)
|
||||
self.setProjectChanged(True)
|
||||
return True
|
||||
@@ -1109,11 +1111,32 @@ class NWProject():
|
||||
# Getters
|
||||
##
|
||||
|
||||
def getAuthors(self):
|
||||
"""Returns a formatted string of authors.
|
||||
"""
|
||||
nAuth = len(self.bookAuthors)
|
||||
authString = ""
|
||||
|
||||
if nAuth == 1:
|
||||
authString = self.bookAuthors[0]
|
||||
elif nAuth > 1:
|
||||
authString = "%s and %s" % (
|
||||
", ".join(self.bookAuthors[0:-1]), self.bookAuthors[-1]
|
||||
)
|
||||
|
||||
return authString
|
||||
|
||||
def getSessionWordCount(self):
|
||||
"""Returns the number of words added or removed this session.
|
||||
"""
|
||||
return self.currWCount - self.lastWCount
|
||||
|
||||
def getCurrentEditTime(self):
|
||||
"""Get the total project edit time, including the time spent in
|
||||
the current session.
|
||||
"""
|
||||
return round(self.editTime + time() - self.projOpened)
|
||||
|
||||
def getProjectItems(self):
|
||||
"""This function ensures that the item tree loaded is sent to
|
||||
the GUI tree view in such a way that the tree can be built. That
|
||||
@@ -1196,9 +1219,9 @@ class NWProject():
|
||||
if len(theLines) != 4:
|
||||
return ["ERROR"]
|
||||
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.error("Failed to read project lockfile")
|
||||
logger.error(str(e))
|
||||
nw.logException()
|
||||
return ["ERROR"]
|
||||
|
||||
return theLines
|
||||
@@ -1217,9 +1240,9 @@ class NWProject():
|
||||
outFile.write("%s\n" % self.mainConf.kernelVer)
|
||||
outFile.write("%d\n" % time())
|
||||
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.error("Failed to write project lockfile")
|
||||
logger.error(str(e))
|
||||
nw.logException()
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -1234,9 +1257,9 @@ class NWProject():
|
||||
if os.path.isfile(lockFile):
|
||||
try:
|
||||
os.unlink(lockFile)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.error("Failed to remove project lockfile")
|
||||
logger.error(str(e))
|
||||
nw.logException()
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -1341,7 +1364,7 @@ class NWProject():
|
||||
if oLayout is None:
|
||||
oLayout = nwItemLayout.NOTE
|
||||
|
||||
if oParent is None or not self.projTree.handleExists(oParent):
|
||||
if oParent is None or oParent not in self.projTree:
|
||||
oParent = self.projTree.findRoot(oClass)
|
||||
if oParent is None:
|
||||
oParent = self.projTree.findRoot(nwItemClass.NOVEL)
|
||||
@@ -1366,7 +1389,7 @@ class NWProject():
|
||||
|
||||
return True
|
||||
|
||||
def _appendSessionStats(self):
|
||||
def _appendSessionStats(self, idleTime):
|
||||
"""Append session statistics to the sessions log file.
|
||||
"""
|
||||
if not self.ensureFolderStructure():
|
||||
@@ -1375,26 +1398,36 @@ class NWProject():
|
||||
sessionFile = os.path.join(self.projMeta, nwFiles.SESS_STATS)
|
||||
isFile = os.path.isfile(sessionFile)
|
||||
|
||||
nowTime = time()
|
||||
sessDiff = self.getSessionWordCount()
|
||||
sessTime = nowTime - self.projOpened
|
||||
|
||||
logger.info("The session lasted %d sec and added %d words" % (int(sessTime), sessDiff))
|
||||
if sessTime < 300 and sessDiff == 0:
|
||||
logger.info("Session too short, skipping log entry")
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(sessionFile, mode="a+", encoding="utf8") as outFile:
|
||||
if not isFile:
|
||||
# It's a new file, so add a header
|
||||
if self.lastWCount > 0:
|
||||
outFile.write("# Offset %d\n" % self.lastWCount)
|
||||
outFile.write("# %-17s %-19s %8s %8s\n" % (
|
||||
"Start Time", "End Time", "Novel", "Notes"
|
||||
outFile.write("# %-17s %-19s %8s %8s %8s\n" % (
|
||||
"Start Time", "End Time", "Novel", "Notes", "Idle"
|
||||
))
|
||||
|
||||
outFile.write("%-19s %-19s %8d %8d\n" % (
|
||||
outFile.write("%-19s %-19s %8d %8d %8d\n" % (
|
||||
formatTimeStamp(self.projOpened),
|
||||
formatTimeStamp(time()),
|
||||
formatTimeStamp(nowTime),
|
||||
self.novelWCount,
|
||||
self.notesWCount,
|
||||
int(idleTime),
|
||||
))
|
||||
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.error("Failed to write session stats file")
|
||||
logger.error(str(e))
|
||||
nw.logException()
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -1430,17 +1463,19 @@ class NWProject():
|
||||
os.rename(theFile, newPath)
|
||||
logger.info("Moved file: %s" % theFile)
|
||||
logger.info("New location: %s" % newPath)
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
except Exception:
|
||||
errList.append("Could not move: %s" % theFile)
|
||||
logger.error("Could not move: %s" % theFile)
|
||||
nw.logException()
|
||||
|
||||
elif len(dataItem) == 21 and dataItem.endswith("_main.bak"):
|
||||
try:
|
||||
os.unlink(theFile)
|
||||
logger.info("Deleted file: %s" % theFile)
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
except Exception:
|
||||
errList.append("Could not delete: %s" % theFile)
|
||||
logger.error("Could not delete: %s" % theFile)
|
||||
nw.logException()
|
||||
|
||||
else:
|
||||
theErr = self._moveUnknownItem(theData, dataItem)
|
||||
@@ -1452,9 +1487,10 @@ class NWProject():
|
||||
try:
|
||||
os.rmdir(theData)
|
||||
logger.info("Removed folder: %s" % theFolder)
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
except Exception:
|
||||
errList.append("Failed to remove: %s" % theFolder)
|
||||
logger.error("Failed to remove: %s" % theFolder)
|
||||
nw.logException()
|
||||
|
||||
return errList
|
||||
|
||||
@@ -1472,8 +1508,9 @@ class NWProject():
|
||||
try:
|
||||
os.rename(theSrc, theDst)
|
||||
logger.info("Moved to junk: %s" % theSrc)
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
except Exception:
|
||||
logger.error("Could not move item %s to junk." % theSrc)
|
||||
nw.logException()
|
||||
return "Could not move item %s to junk." % theSrc
|
||||
|
||||
return ""
|
||||
@@ -1506,8 +1543,9 @@ class NWProject():
|
||||
logger.info("Deleting: %s" % rmFile)
|
||||
try:
|
||||
os.unlink(rmFile)
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
except Exception:
|
||||
logger.error("Could not delete: %s" % rmFile)
|
||||
nw.logException()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Spell Check Classes
|
||||
"""
|
||||
novelWriter – Spell Check Classes
|
||||
=================================
|
||||
Wrapper classes for spell checking tools
|
||||
|
||||
novelWriter – Spell Check Classes
|
||||
===================================
|
||||
Wrapper class for spell checking tools
|
||||
File History:
|
||||
Created: 2019-06-11 [0.1.5]
|
||||
|
||||
File History:
|
||||
Created: 2019-06-11 [0.1.5]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
@@ -73,9 +72,9 @@ class NWSpellCheck():
|
||||
with open(self.projectDict, mode="a+", encoding="utf-8") as outFile:
|
||||
outFile.write("%s\n" % newWord)
|
||||
self.projDict.append(newWord)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.error("Failed to add word to project word list %s" % str(self.projectDict))
|
||||
logger.error(str(e))
|
||||
nw.logException()
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
@@ -124,9 +123,10 @@ class NWSpellCheck():
|
||||
if len(theLine) > 0 and theLine not in self.projDict:
|
||||
self.projDict.append(theLine)
|
||||
logger.debug("Project word list contains %d words" % len(self.projDict))
|
||||
except Exception as e:
|
||||
|
||||
except Exception:
|
||||
logger.error("Failed to load project word list")
|
||||
logger.error(str(e))
|
||||
nw.logException()
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -142,6 +142,7 @@ class NWSpellEnchant(NWSpellCheck):
|
||||
def __init__(self):
|
||||
NWSpellCheck.__init__(self)
|
||||
logger.debug("Enchant spell checking activated")
|
||||
self.theBroker = None
|
||||
return
|
||||
|
||||
def setLanguage(self, theLang, projectDict=None):
|
||||
@@ -151,9 +152,15 @@ class NWSpellEnchant(NWSpellCheck):
|
||||
"""
|
||||
try:
|
||||
import enchant
|
||||
self.theDict = enchant.Dict(theLang)
|
||||
if self.theBroker is not None:
|
||||
logger.verbose("Deleting old pyenchant broker")
|
||||
del self.theBroker
|
||||
|
||||
self.theBroker = enchant.Broker()
|
||||
self.theDict = self.theBroker.request_dict(theLang)
|
||||
self.spellLanguage = theLang
|
||||
logger.debug("Enchant spell checking for language %s loaded" % theLang)
|
||||
|
||||
except Exception:
|
||||
logger.error("Failed to load enchant spell checking for language %s" % theLang)
|
||||
self.theDict = NWSpellEnchantDummy()
|
||||
@@ -202,9 +209,9 @@ class NWSpellEnchant(NWSpellCheck):
|
||||
try:
|
||||
spTag = self.theDict.tag
|
||||
spName = self.theDict.provider.name
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.error("Failed to extract information about the dictionary")
|
||||
logger.error(str(e))
|
||||
nw.logException()
|
||||
spTag = ""
|
||||
spName = ""
|
||||
|
||||
@@ -239,7 +246,7 @@ class NWSpellSimple(NWSpellCheck):
|
||||
when no other is available. This method is fairly slow compared to
|
||||
other implementations.
|
||||
"""
|
||||
WORDS = []
|
||||
theWords = set()
|
||||
|
||||
def __init__(self):
|
||||
NWSpellCheck.__init__(self)
|
||||
@@ -251,26 +258,27 @@ class NWSpellSimple(NWSpellCheck):
|
||||
"""Load a dictionary as a list from the app assets folder.
|
||||
"""
|
||||
self.theLang = theLang
|
||||
self.WORDS = []
|
||||
self.theWords = set()
|
||||
dictFile = os.path.join(self.mainConf.dictPath, theLang+".dict")
|
||||
try:
|
||||
with open(dictFile, mode="r", encoding="utf-8") as wordsFile:
|
||||
for theLine in wordsFile:
|
||||
if len(theLine) == 0 or theLine.startswith("#"):
|
||||
continue
|
||||
self.WORDS.append(theLine.strip().lower())
|
||||
logger.debug("Spell check word list for language %s loaded" % theLang)
|
||||
logger.debug("Word list contains %d words" % len(self.WORDS))
|
||||
self.theWords.add(theLine.strip().lower())
|
||||
|
||||
logger.debug("Spell check dictionary for language %s loaded" % theLang)
|
||||
logger.debug("Dictionary contains %d words" % len(self.theWords))
|
||||
self.spellLanguage = theLang
|
||||
except Exception as e:
|
||||
|
||||
except Exception:
|
||||
logger.error("Failed to load spell check word list for language %s" % theLang)
|
||||
logger.error(str(e))
|
||||
nw.logException()
|
||||
self.spellLanguage = None
|
||||
|
||||
self._readProjectDictionary(projectDict)
|
||||
for pWord in self.projDict:
|
||||
if pWord not in self.WORDS:
|
||||
self.WORDS.append(pWord)
|
||||
self.theWords.add(pWord)
|
||||
|
||||
return
|
||||
|
||||
@@ -280,7 +288,7 @@ class NWSpellSimple(NWSpellCheck):
|
||||
word by the syntax highlighter.
|
||||
"""
|
||||
theWord = theWord.replace(self.mainConf.fmtApostrophe, "'").lower()
|
||||
return theWord in self.WORDS
|
||||
return theWord in self.theWords
|
||||
|
||||
def suggestWords(self, theWord):
|
||||
"""Get suggestions for correct word from difflib, and make sure
|
||||
@@ -293,7 +301,7 @@ class NWSpellSimple(NWSpellCheck):
|
||||
if len(theWord) == 0:
|
||||
return []
|
||||
|
||||
theMatches = difflib.get_close_matches(theWord.lower(), self.WORDS, n=10, cutoff=0.75)
|
||||
theMatches = difflib.get_close_matches(theWord.lower(), self.theWords, n=10, cutoff=0.75)
|
||||
theOptions = []
|
||||
for aWord in theMatches:
|
||||
if len(aWord) == 0:
|
||||
@@ -309,8 +317,8 @@ class NWSpellSimple(NWSpellCheck):
|
||||
"""Wrapper for the internal project dictionary feature.
|
||||
"""
|
||||
newWord = newWord.strip().lower()
|
||||
if newWord not in self.WORDS:
|
||||
self.WORDS.append(newWord)
|
||||
if newWord not in self.theWords:
|
||||
self.theWords.add(newWord)
|
||||
NWSpellCheck.addWord(self, newWord)
|
||||
return
|
||||
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Project Item Status Class
|
||||
"""
|
||||
novelWriter – Project Item Status Class
|
||||
=======================================
|
||||
Data class for the status/importance settings of a project item
|
||||
|
||||
novelWriter – Project Item Status Class
|
||||
=========================================
|
||||
Class holding the status/importance elements of a project item
|
||||
File History:
|
||||
Created: 2019-05-19 [0.1.3]
|
||||
|
||||
File History:
|
||||
Created: 2019-05-19 [0.1.3]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
@@ -1,35 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter HTML Text Converter
|
||||
"""
|
||||
novelWriter – HTML Text Converter
|
||||
=================================
|
||||
Extends the Tokenizer class to generate HTML output
|
||||
|
||||
novelWriter – HTML Text Converter
|
||||
===================================
|
||||
Extends the Tokenizer class to write HTML
|
||||
File History:
|
||||
Created: 2019-05-07 [0.0.1]
|
||||
|
||||
File History:
|
||||
Created: 2019-05-07 [0.0.1]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
from nw.core.tokenizer import Tokenizer
|
||||
from nw.constants import nwUnicode, nwLabels, nwKeyWords
|
||||
from nw.constants import nwLabels, nwKeyWords, nwHtmlUnicode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -42,25 +40,13 @@ class ToHtml(Tokenizer):
|
||||
def __init__(self, theProject, theParent):
|
||||
Tokenizer.__init__(self, theProject, theParent)
|
||||
|
||||
self.genMode = self.M_EXPORT
|
||||
self.genMode = self.M_EXPORT
|
||||
self.cssStyles = True
|
||||
self.fullHTML = []
|
||||
|
||||
self.repDict = {
|
||||
"<" : "<",
|
||||
">" : ">",
|
||||
"&" : "&",
|
||||
nwUnicode.U_ENDASH : nwUnicode.H_ENDASH,
|
||||
nwUnicode.U_EMDASH : nwUnicode.H_EMDASH,
|
||||
nwUnicode.U_HELLIP : nwUnicode.H_HELLIP,
|
||||
nwUnicode.U_NBSP : nwUnicode.H_NBSP,
|
||||
nwUnicode.U_THNSP : nwUnicode.H_THNSP,
|
||||
nwUnicode.U_THNBSP : nwUnicode.H_THNBSP,
|
||||
nwUnicode.U_MAPOSS : nwUnicode.H_RSQUO,
|
||||
}
|
||||
self.revDict = {}
|
||||
self.reReplace = []
|
||||
self.reReverse = []
|
||||
self._buildRegEx()
|
||||
# Internals
|
||||
self._trMap = {}
|
||||
self.setReplaceUnicode(False)
|
||||
|
||||
return
|
||||
|
||||
@@ -87,31 +73,38 @@ class ToHtml(Tokenizer):
|
||||
self.cssStyles = cssStyles
|
||||
return
|
||||
|
||||
def setReplaceUnicode(self, doReplace):
|
||||
"""Set the translation map to either minimal or full unicode to
|
||||
html entities replacement.
|
||||
"""
|
||||
# Control characters must always be replaced
|
||||
self._trMap = str.maketrans({
|
||||
"<" : "<",
|
||||
">" : ">",
|
||||
"&" : "&",
|
||||
})
|
||||
|
||||
if doReplace:
|
||||
# Extend to all relevant Unicode characters
|
||||
self._trMap.update(str.maketrans(nwHtmlUnicode.U_TO_H))
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Class Methods
|
||||
##
|
||||
|
||||
def doAutoReplace(self):
|
||||
def getFullResultSize(self):
|
||||
"""Return the size of the full HTML result.
|
||||
"""
|
||||
return sum([len(x) for x in self.fullHTML])
|
||||
|
||||
def doPreProcessing(self):
|
||||
"""Extend the auto-replace to also properly encode some unicode
|
||||
characters into their respective HTML entities.
|
||||
"""
|
||||
Tokenizer.doAutoReplace(self)
|
||||
self.theText = self.reReplace.sub(
|
||||
lambda x: self.repDict[x.group(0)], self.theText
|
||||
)
|
||||
return
|
||||
|
||||
def doPostProcessing(self):
|
||||
"""Reverse the html entities replacement on the markdown text.
|
||||
Otherwise, all the &something; bits will also be in there.
|
||||
"""
|
||||
Tokenizer.doPostProcessing(self)
|
||||
if self.genMode == self.M_PREVIEW:
|
||||
# Doesn't matter for preview as we don't use the markdown
|
||||
return
|
||||
self.theMarkdown = self.reReverse.sub(
|
||||
lambda x: self.revDict[x.group(0)], self.theMarkdown
|
||||
)
|
||||
Tokenizer.doPreProcessing(self)
|
||||
self.theText = self.theText.translate(self._trMap)
|
||||
return
|
||||
|
||||
def doConvert(self):
|
||||
@@ -159,6 +152,7 @@ class ToHtml(Tokenizer):
|
||||
parStyle = None
|
||||
tmpResult = []
|
||||
hasHardBreak = False
|
||||
|
||||
for tType, tLine, tText, tFormat, tStyle in self.theTokens:
|
||||
|
||||
# Styles
|
||||
@@ -166,24 +160,27 @@ class ToHtml(Tokenizer):
|
||||
if tStyle is not None and self.cssStyles:
|
||||
if tStyle & self.A_LEFT:
|
||||
aStyle.append("text-align: left;")
|
||||
if tStyle & self.A_RIGHT:
|
||||
elif tStyle & self.A_RIGHT:
|
||||
aStyle.append("text-align: right;")
|
||||
if tStyle & self.A_CENTRE:
|
||||
elif tStyle & self.A_CENTRE:
|
||||
aStyle.append("text-align: center;")
|
||||
if tStyle & self.A_JUSTIFY:
|
||||
elif tStyle & self.A_JUSTIFY:
|
||||
aStyle.append("text-align: justify;")
|
||||
|
||||
if tStyle & self.A_PBB:
|
||||
aStyle.append("page-break-before: always;")
|
||||
if tStyle & self.A_PBB_AV:
|
||||
aStyle.append("page-break-before: avoid;")
|
||||
if tStyle & self.A_PBB_NO:
|
||||
aStyle.append("page-break-before: never;")
|
||||
elif tStyle & self.A_PBB_AUT:
|
||||
aStyle.append("page-break-before: auto;")
|
||||
|
||||
if tStyle & self.A_PBA:
|
||||
aStyle.append("page-break-after: always;")
|
||||
if tStyle & self.A_PBA_AV:
|
||||
aStyle.append("page-break-after: avoid;")
|
||||
if tStyle & self.A_PBA_NO:
|
||||
aStyle.append("page-break-after: never;")
|
||||
elif tStyle & self.A_PBA_AUT:
|
||||
aStyle.append("page-break-after: auto;")
|
||||
|
||||
if tStyle & self.A_Z_BTMMRG:
|
||||
aStyle.append("margin-bottom: 0;")
|
||||
if tStyle & self.A_Z_TOPMRG:
|
||||
aStyle.append("margin-top: 0;")
|
||||
|
||||
if len(aStyle) > 0:
|
||||
hStyle = " style='%s'" % (" ".join(aStyle))
|
||||
@@ -241,12 +238,12 @@ class ToHtml(Tokenizer):
|
||||
if parStyle is None:
|
||||
parStyle = hStyle
|
||||
for xPos, xLen, xFmt in reversed(tFormat):
|
||||
tTemp = tTemp[:xPos]+htmlTags[xFmt]+tTemp[xPos+xLen:]
|
||||
tTemp = tTemp[:xPos] + htmlTags[xFmt] + tTemp[xPos+xLen:]
|
||||
if tText.endswith(" "):
|
||||
thisPar.append(tTemp.rstrip()+"<br/>")
|
||||
thisPar.append(tTemp.rstrip() + "<br/>")
|
||||
hasHardBreak = True
|
||||
else:
|
||||
thisPar.append(tTemp.rstrip()+" ")
|
||||
thisPar.append(tTemp.rstrip() + " ")
|
||||
|
||||
elif tType == self.T_SYNOPSIS and self.doSynopsis:
|
||||
tmpResult.append(self._formatSynopsis(tText))
|
||||
@@ -255,11 +252,60 @@ class ToHtml(Tokenizer):
|
||||
tmpResult.append(self._formatComments(tText))
|
||||
|
||||
elif tType == self.T_KEYWORD and self.doKeywords:
|
||||
tmpResult.append(self._formatKeywords(tText))
|
||||
tTemp = "<p%s>%s</p>\n" % (hStyle, self._formatKeywords(tText))
|
||||
tmpResult.append(tTemp)
|
||||
|
||||
self.theResult = "".join(tmpResult)
|
||||
tmpResult = []
|
||||
|
||||
if self.genMode != self.M_PREVIEW:
|
||||
self.fullHTML.append(self.theResult)
|
||||
|
||||
return
|
||||
|
||||
def saveHTML5(self, savePath):
|
||||
"""Save the data to an .html file.
|
||||
"""
|
||||
with open(savePath, mode="w", encoding="utf8") as outFile:
|
||||
theStyle = self.getStyleSheet()
|
||||
theStyle.append("article {width: 800px; margin: 40px auto;}")
|
||||
bodyText = "".join(self.fullHTML)
|
||||
bodyText = bodyText.replace("\t", "	").rstrip()
|
||||
|
||||
theHtml = (
|
||||
"<!DOCTYPE html>\n"
|
||||
"<html>\n"
|
||||
"<head>\n"
|
||||
"<meta charset='utf-8'>\n"
|
||||
"<title>{projTitle:s}</title>\n"
|
||||
"</head>\n"
|
||||
"<style>\n"
|
||||
"{htmlStyle:s}\n"
|
||||
"</style>\n"
|
||||
"<body>\n"
|
||||
"<article>\n"
|
||||
"{bodyText:s}\n"
|
||||
"</article>\n"
|
||||
"</body>\n"
|
||||
"</html>\n"
|
||||
).format(
|
||||
projTitle = self.theProject.projName,
|
||||
htmlStyle = "\n".join(theStyle),
|
||||
bodyText = bodyText,
|
||||
)
|
||||
outFile.write(theHtml)
|
||||
|
||||
return
|
||||
|
||||
def replaceTabs(self, nSpaces=8, spaceChar=" "):
|
||||
"""Replace tabs with spaces in the html.
|
||||
"""
|
||||
htmlText = []
|
||||
eightSpace = spaceChar*nSpaces
|
||||
for aLine in self.fullHTML:
|
||||
htmlText.append(aLine.replace("\t", eightSpace))
|
||||
|
||||
self.fullHTML = htmlText
|
||||
return
|
||||
|
||||
def getStyleSheet(self):
|
||||
@@ -269,22 +315,86 @@ class ToHtml(Tokenizer):
|
||||
if not self.cssStyles:
|
||||
return theStyles
|
||||
|
||||
if self.doJustify:
|
||||
theStyles.append(r"p {text-align: justify;}")
|
||||
else:
|
||||
theStyles.append(r"p {text-align: left;}")
|
||||
mScale = self.lineHeight/1.15
|
||||
textAlign = "justify" if self.doJustify else "left"
|
||||
|
||||
theStyles.append(r"h1, h2 {color: rgb(66, 113, 174);}")
|
||||
theStyles.append(r"h3, h4 {color: rgb(50, 50, 50);}")
|
||||
theStyles.append(r"h1, h2, h3, h4 {page-break-after: avoid;}")
|
||||
theStyles.append(r"a {color: rgb(66, 113, 174);}")
|
||||
theStyles.append(r".title {font-size: 2.5em;}")
|
||||
theStyles.append(r".tags {color: rgb(245, 135, 31); font-weight: bold;}")
|
||||
theStyles.append(r".break {text-align: left;}")
|
||||
theStyles.append(r".sep {text-align: center; margin-top: 1em; margin-bottom: 1em;}")
|
||||
theStyles.append(r".skip {margin-top: 1em; margin-bottom: 1em;}")
|
||||
theStyles.append(r".synopsis {font-style: italic;}")
|
||||
theStyles.append(r".comment {font-style: italic; color: rgb(100, 100, 100);}")
|
||||
theStyles.append("body {font-family: '%s'; font-size: %dpt;}" % (
|
||||
self.textFont, self.textSize
|
||||
))
|
||||
theStyles.append((
|
||||
"p {"
|
||||
"text-align: %s; line-height: %d%%; "
|
||||
"margin-top: %.2fem; margin-bottom: %.2fem;"
|
||||
"}"
|
||||
) % (
|
||||
textAlign,
|
||||
round(100 * self.lineHeight),
|
||||
mScale * self.marginText[0],
|
||||
mScale * self.marginText[1],
|
||||
))
|
||||
theStyles.append((
|
||||
"h1 {"
|
||||
"color: rgb(66, 113, 174); "
|
||||
"page-break-after: avoid; "
|
||||
"margin-top: %.2fem; "
|
||||
"margin-bottom: %.2fem;"
|
||||
"}"
|
||||
) % (
|
||||
mScale * self.marginHead1[0], mScale * self.marginHead1[1]
|
||||
))
|
||||
theStyles.append((
|
||||
"h2 {"
|
||||
"color: rgb(66, 113, 174); "
|
||||
"page-break-after: avoid; "
|
||||
"margin-top: %.2fem; "
|
||||
"margin-bottom: %.2fem;"
|
||||
"}"
|
||||
) % (
|
||||
mScale * self.marginHead2[0], mScale * self.marginHead2[1]
|
||||
))
|
||||
theStyles.append((
|
||||
"h3 {"
|
||||
"color: rgb(50, 50, 50); "
|
||||
"page-break-after: avoid; "
|
||||
"margin-top: %.2fem; "
|
||||
"margin-bottom: %.2fem;"
|
||||
"}"
|
||||
) % (
|
||||
mScale * self.marginHead3[0], mScale * self.marginHead3[1]
|
||||
))
|
||||
theStyles.append((
|
||||
"h4 {"
|
||||
"color: rgb(50, 50, 50); "
|
||||
"page-break-after: avoid; "
|
||||
"margin-top: %.2fem; "
|
||||
"margin-bottom: %.2fem;"
|
||||
"}"
|
||||
) % (
|
||||
mScale * self.marginHead4[0], mScale * self.marginHead4[1]
|
||||
))
|
||||
theStyles.append((
|
||||
".title {"
|
||||
"font-size: 2.5em; "
|
||||
"margin-top: %.2fem; "
|
||||
"margin-bottom: %.2fem;"
|
||||
"}"
|
||||
) % (
|
||||
mScale * self.marginTitle[0], mScale * self.marginTitle[1]
|
||||
))
|
||||
theStyles.append((
|
||||
".sep, .skip {"
|
||||
"text-align: center; "
|
||||
"margin-top: %.2fem; "
|
||||
"margin-bottom: %.2fem;}"
|
||||
) % (
|
||||
mScale, mScale
|
||||
))
|
||||
|
||||
theStyles.append("a {color: rgb(66, 113, 174);}")
|
||||
theStyles.append(".tags {color: rgb(245, 135, 31); font-weight: bold;}")
|
||||
theStyles.append(".break {text-align: left;}")
|
||||
theStyles.append(".synopsis {font-style: italic;}")
|
||||
theStyles.append(".comment {font-style: italic; color: rgb(100, 100, 100);}")
|
||||
|
||||
return theStyles
|
||||
|
||||
@@ -296,9 +406,9 @@ class ToHtml(Tokenizer):
|
||||
"""Apply HTML formatting to synopsis.
|
||||
"""
|
||||
if self.genMode == self.M_PREVIEW:
|
||||
return "<p class='comment'><span class='synopsis'>Synopsis: </span>%s</p>\n" % tText
|
||||
return "<p class='comment'><span class='synopsis'>Synopsis:</span> %s</p>\n" % tText
|
||||
else:
|
||||
return "<p class='synopsis'><strong>Synopsis: </strong>%s</p>\n" % tText
|
||||
return "<p class='synopsis'><strong>Synopsis:</strong> %s</p>\n" % tText
|
||||
|
||||
def _formatComments(self, tText):
|
||||
"""Apply HTML formatting to comments.
|
||||
@@ -306,7 +416,7 @@ class ToHtml(Tokenizer):
|
||||
if self.genMode == self.M_PREVIEW:
|
||||
return "<p class='comment'>%s</p>\n" % tText
|
||||
else:
|
||||
return "<p class='comment'><strong>Comment: </strong>%s</p>\n" % tText
|
||||
return "<p class='comment'><strong>Comment:</strong> %s</p>\n" % tText
|
||||
|
||||
def _formatKeywords(self, tText):
|
||||
"""Apply HTML formatting to keywords.
|
||||
@@ -338,18 +448,6 @@ class ToHtml(Tokenizer):
|
||||
))
|
||||
retText += ", ".join(refTags)
|
||||
|
||||
return "<div>%s</div>" % retText
|
||||
|
||||
def _buildRegEx(self):
|
||||
"""Build the regular expressions
|
||||
"""
|
||||
self.revDict = dict(map(reversed, self.repDict.items()))
|
||||
self.reReplace = re.compile(
|
||||
"|".join([re.escape(k) for k in self.repDict.keys()]), flags=re.DOTALL
|
||||
)
|
||||
self.reReverse = re.compile(
|
||||
"|".join([re.escape(k) for k in self.revDict.keys()]), flags=re.DOTALL
|
||||
)
|
||||
return
|
||||
return retText
|
||||
|
||||
# END Class ToHtml
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Text Tokenizer
|
||||
"""
|
||||
novelWriter – Text Tokenizer
|
||||
============================
|
||||
Splits a piece of novelWriter markdown text into its elements
|
||||
|
||||
novelWriter – Text Tokenizer
|
||||
==============================
|
||||
Splits a piece of novelWriter markdown text into its elements
|
||||
File History:
|
||||
Created: 2019-05-05 [0.0.1]
|
||||
|
||||
File History:
|
||||
Created: 2019-05-05 [0.0.1]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -33,19 +32,21 @@ from PyQt5.QtCore import QRegularExpression
|
||||
|
||||
from nw.core.document import NWDoc
|
||||
from nw.core.tools import numberToWord, numberToRoman
|
||||
from nw.constants import nwConst, nwItemLayout, nwItemType, nwRegEx
|
||||
from nw.constants import nwConst, nwUnicode, nwItemLayout, nwItemType, nwRegEx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Tokenizer():
|
||||
|
||||
FMT_B_B = 1 # Begin bold
|
||||
FMT_B_E = 2 # End bold
|
||||
FMT_I_B = 3 # Begin italics
|
||||
FMT_I_E = 4 # End italics
|
||||
FMT_D_B = 5 # Begin strikeout
|
||||
FMT_D_E = 6 # End strikeout
|
||||
# In-Text Format
|
||||
FMT_B_B = 1 # Begin bold
|
||||
FMT_B_E = 2 # End bold
|
||||
FMT_I_B = 3 # Begin italics
|
||||
FMT_I_E = 4 # End italics
|
||||
FMT_D_B = 5 # Begin strikeout
|
||||
FMT_D_E = 6 # End strikeout
|
||||
|
||||
# Block Type
|
||||
T_EMPTY = 1 # Empty line (new paragraph)
|
||||
T_SYNOPSIS = 2 # Synopsis comment
|
||||
T_COMMENT = 3 # Comment line
|
||||
@@ -59,17 +60,18 @@ class Tokenizer():
|
||||
T_SEP = 11 # Scene separator
|
||||
T_SKIP = 12 # Paragraph break
|
||||
|
||||
A_NONE = 0 # No special style
|
||||
A_LEFT = 1 # Left aligned
|
||||
A_RIGHT = 2 # Right aligned
|
||||
A_CENTRE = 4 # Centred
|
||||
A_JUSTIFY = 8 # Justified
|
||||
A_PBB = 16 # Page break before always
|
||||
A_PBB_AV = 32 # Page break before avoid
|
||||
A_PBB_NO = 64 # Page break before never
|
||||
A_PBA = 128 # Page break after always
|
||||
A_PBA_AV = 256 # Page break after avoid
|
||||
A_PBA_NO = 512 # Page break after avoid
|
||||
# Block Style
|
||||
A_NONE = 0x0000 # No special style
|
||||
A_LEFT = 0x0001 # Left aligned
|
||||
A_RIGHT = 0x0002 # Right aligned
|
||||
A_CENTRE = 0x0004 # Centred
|
||||
A_JUSTIFY = 0x0008 # Justified
|
||||
A_PBB = 0x0010 # Page break before always
|
||||
A_PBB_AUT = 0x0020 # Page break before auto
|
||||
A_PBA = 0x0040 # Page break after always
|
||||
A_PBA_AUT = 0x0080 # Page break after auto
|
||||
A_Z_TOPMRG = 0x0100 # Zero top margin
|
||||
A_Z_BTMMRG = 0x0200 # Zero bottom margin
|
||||
|
||||
def __init__(self, theProject, theParent):
|
||||
|
||||
@@ -77,20 +79,36 @@ class Tokenizer():
|
||||
self.theParent = theParent
|
||||
|
||||
# Data Variables
|
||||
self.theText = None # The raw text to be tokenized
|
||||
self.theText = "" # The raw text to be tokenized
|
||||
self.theHandle = None # The handle associated with the text
|
||||
self.theItem = None # The NWItem associated with the handle
|
||||
self.theTokens = None # The list of the processed tokens
|
||||
self.theResult = None # The result text after conversion
|
||||
self.theMarkdown = None # The result text in novelWriter markdown
|
||||
self.theTokens = [] # The list of the processed tokens
|
||||
self.theResult = "" # The result of the last document
|
||||
|
||||
self.keepMarkdown = False # Whether to keep the markdown text
|
||||
self.theMarkdown = [] # The result novelWriter markdown of all documents
|
||||
|
||||
# User Settings
|
||||
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.doJustify = False # Justify text
|
||||
self.textFont = "Serif" # Output text font
|
||||
self.textSize = 11 # Output text size
|
||||
self.textFixed = False # Fixed width text
|
||||
self.lineHeight = 1.15 # Line height
|
||||
self.doJustify = False # Justify text
|
||||
self.doBodyText = True # Include body text
|
||||
self.doSynopsis = False # Also process synopsis comments
|
||||
self.doComments = False # Also process comments
|
||||
self.doKeywords = False # Also process keywords like tags and references
|
||||
|
||||
## Title 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.marginText = (0.000, 0.584)
|
||||
self.marginMeta = (0.000, 0.584)
|
||||
|
||||
## Title Formats
|
||||
self.fmtTitle = "%title%" # Formatting for titles
|
||||
self.fmtChapter = "%title%" # Formatting for numbered chapters
|
||||
self.fmtUnNum = "%title%" # Formatting for unnumbered chapters
|
||||
@@ -151,6 +169,48 @@ class Tokenizer():
|
||||
self.hideSection = hideSection
|
||||
return
|
||||
|
||||
def setFont(self, textFont, textSize, textFixed=False):
|
||||
self.textFont = textFont
|
||||
self.textSize = round(int(textSize))
|
||||
self.textFixed = textFixed
|
||||
return
|
||||
|
||||
def setLineHeight(self, lineHeight):
|
||||
self.lineHeight = float(lineHeight)
|
||||
return
|
||||
|
||||
def setJustify(self, doJustify):
|
||||
self.doJustify = doJustify
|
||||
return
|
||||
|
||||
def setTitleMargins(self, mUpper, mLower):
|
||||
self.marginTitle = (float(mUpper), float(mLower))
|
||||
return
|
||||
|
||||
def setHead1Margins(self, mUpper, mLower):
|
||||
self.marginHead1 = (float(mUpper), float(mLower))
|
||||
return
|
||||
|
||||
def setHead2Margins(self, mUpper, mLower):
|
||||
self.marginHead2 = (float(mUpper), float(mLower))
|
||||
return
|
||||
|
||||
def setHead3Margins(self, mUpper, mLower):
|
||||
self.marginHead3 = (float(mUpper), float(mLower))
|
||||
return
|
||||
|
||||
def setHead4Margins(self, mUpper, mLower):
|
||||
self.marginHead4 = (float(mUpper), float(mLower))
|
||||
return
|
||||
|
||||
def setTextMargins(self, mUpper, mLower):
|
||||
self.marginText = (float(mUpper), float(mLower))
|
||||
return
|
||||
|
||||
def setMetaMargins(self, mUpper, mLower):
|
||||
self.marginMeta = (float(mUpper), float(mLower))
|
||||
return
|
||||
|
||||
def setLinkHeaders(self, linkHeaders):
|
||||
self.linkHeaders = linkHeaders
|
||||
return
|
||||
@@ -171,8 +231,8 @@ class Tokenizer():
|
||||
self.doKeywords = doKeywords
|
||||
return
|
||||
|
||||
def setJustify(self, doJustify):
|
||||
self.doJustify = doJustify
|
||||
def setKeepMarkdown(self, keepMarkdown):
|
||||
self.keepMarkdown = keepMarkdown
|
||||
return
|
||||
|
||||
##
|
||||
@@ -194,7 +254,8 @@ class Tokenizer():
|
||||
self.theTokens.append((
|
||||
self.T_TITLE, 0, theTitle, None, self.A_PBB | self.A_CENTRE
|
||||
))
|
||||
self.theMarkdown = "# %s\n\n" % theTitle
|
||||
if self.keepMarkdown:
|
||||
self.theMarkdown.append("# %s\n\n" % theTitle)
|
||||
|
||||
return True
|
||||
|
||||
@@ -236,26 +297,10 @@ class Tokenizer():
|
||||
|
||||
return True
|
||||
|
||||
def getResult(self):
|
||||
"""Return the result from the conversion.
|
||||
"""
|
||||
return self.theResult
|
||||
|
||||
def getResultSize(self):
|
||||
"""Return the size of the result from the conversion.
|
||||
"""
|
||||
if self.theResult is None:
|
||||
return 0
|
||||
return len(self.theResult)
|
||||
|
||||
def getFilteredMarkdown(self):
|
||||
"""Return the novelWriter markdown after the filters have been applied.
|
||||
"""
|
||||
return self.theMarkdown
|
||||
|
||||
def doAutoReplace(self):
|
||||
"""Run through the user's auto-replace dictionary.
|
||||
def doPreProcessing(self):
|
||||
"""Reun trough the various replace doctionaries.
|
||||
"""
|
||||
# Process the user's auto-replace dictionary
|
||||
if len(self.theProject.autoReplace) > 0:
|
||||
repDict = {}
|
||||
for aKey, aVal in self.theProject.autoReplace.items():
|
||||
@@ -263,6 +308,10 @@ class Tokenizer():
|
||||
xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL)
|
||||
self.theText = xRep.sub(lambda x: repDict[x.group(0)], self.theText)
|
||||
|
||||
# Process the character translation map
|
||||
trDict = {nwUnicode.U_MAPOSS: nwUnicode.U_RSQUO}
|
||||
self.theText = self.theText.translate(str.maketrans(trDict))
|
||||
|
||||
return
|
||||
|
||||
def doPostProcessing(self):
|
||||
@@ -305,7 +354,6 @@ class Tokenizer():
|
||||
]
|
||||
|
||||
self.theTokens = []
|
||||
self.theMarkdown = ""
|
||||
tmpMarkdown = []
|
||||
nLine = 0
|
||||
for aLine in self.theText.splitlines():
|
||||
@@ -314,88 +362,61 @@ class Tokenizer():
|
||||
# Tag lines starting with specific characters
|
||||
if len(aLine.strip()) == 0:
|
||||
self.theTokens.append((
|
||||
self.T_EMPTY,
|
||||
nLine,
|
||||
"",
|
||||
None,
|
||||
self.A_NONE
|
||||
self.T_EMPTY, nLine, "", None, self.A_NONE
|
||||
))
|
||||
tmpMarkdown.append("\n")
|
||||
if self.keepMarkdown:
|
||||
tmpMarkdown.append("\n")
|
||||
|
||||
elif aLine[0] == "%":
|
||||
cLine = aLine[1:].lstrip()
|
||||
synTag = cLine[:9].lower()
|
||||
if synTag == "synopsis:":
|
||||
self.theTokens.append((
|
||||
self.T_SYNOPSIS,
|
||||
nLine,
|
||||
cLine[9:].strip(),
|
||||
None,
|
||||
self.A_NONE
|
||||
self.T_SYNOPSIS, nLine, cLine[9:].strip(), None, self.A_NONE
|
||||
))
|
||||
if self.doSynopsis:
|
||||
if self.doSynopsis and self.keepMarkdown:
|
||||
tmpMarkdown.append("%s\n" % aLine)
|
||||
else:
|
||||
self.theTokens.append((
|
||||
self.T_COMMENT,
|
||||
nLine,
|
||||
aLine[1:].strip(),
|
||||
None,
|
||||
self.A_NONE
|
||||
self.T_COMMENT, nLine, aLine[1:].strip(), None, self.A_NONE
|
||||
))
|
||||
if self.doComments:
|
||||
if self.doComments and self.keepMarkdown:
|
||||
tmpMarkdown.append("%s\n" % aLine)
|
||||
|
||||
elif aLine[0] == "@":
|
||||
self.theTokens.append((
|
||||
self.T_KEYWORD,
|
||||
nLine,
|
||||
aLine[1:].strip(),
|
||||
None,
|
||||
self.A_NONE
|
||||
self.T_KEYWORD, nLine, aLine[1:].strip(), None, self.A_NONE
|
||||
))
|
||||
if self.doKeywords:
|
||||
if self.doKeywords and self.keepMarkdown:
|
||||
tmpMarkdown.append("%s\n" % aLine)
|
||||
|
||||
elif aLine[:2] == "# ":
|
||||
self.theTokens.append((
|
||||
self.T_HEAD1,
|
||||
nLine,
|
||||
aLine[2:].strip(),
|
||||
None,
|
||||
self.A_NONE
|
||||
self.T_HEAD1, nLine, aLine[2:].strip(), None, self.A_NONE
|
||||
))
|
||||
tmpMarkdown.append("%s\n" % aLine)
|
||||
if self.keepMarkdown:
|
||||
tmpMarkdown.append("%s\n" % aLine)
|
||||
|
||||
elif aLine[:3] == "## ":
|
||||
self.theTokens.append((
|
||||
self.T_HEAD2,
|
||||
nLine,
|
||||
aLine[3:].strip(),
|
||||
None,
|
||||
self.A_NONE
|
||||
self.T_HEAD2, nLine, aLine[3:].strip(), None, self.A_NONE
|
||||
))
|
||||
tmpMarkdown.append("%s\n" % aLine)
|
||||
if self.keepMarkdown:
|
||||
tmpMarkdown.append("%s\n" % aLine)
|
||||
|
||||
elif aLine[:4] == "### ":
|
||||
self.theTokens.append((
|
||||
self.T_HEAD3,
|
||||
nLine,
|
||||
aLine[4:].strip(),
|
||||
None,
|
||||
self.A_NONE
|
||||
self.T_HEAD3, nLine, aLine[4:].strip(), None, self.A_NONE
|
||||
))
|
||||
tmpMarkdown.append("%s\n" % aLine)
|
||||
if self.keepMarkdown:
|
||||
tmpMarkdown.append("%s\n" % aLine)
|
||||
|
||||
elif aLine[:5] == "#### ":
|
||||
self.theTokens.append((
|
||||
self.T_HEAD4,
|
||||
nLine,
|
||||
aLine[5:].strip(),
|
||||
None,
|
||||
self.A_NONE
|
||||
self.T_HEAD4, nLine, aLine[5:].strip(), None, self.A_NONE
|
||||
))
|
||||
tmpMarkdown.append("%s\n" % aLine)
|
||||
if self.keepMarkdown:
|
||||
tmpMarkdown.append("%s\n" % aLine)
|
||||
|
||||
else:
|
||||
if not self.doBodyText:
|
||||
@@ -418,26 +439,44 @@ class Tokenizer():
|
||||
# sorted by position
|
||||
fmtPos = sorted(fmtPos, key=itemgetter(0))
|
||||
self.theTokens.append((
|
||||
self.T_TEXT,
|
||||
nLine,
|
||||
aLine,
|
||||
fmtPos,
|
||||
self.A_NONE
|
||||
self.T_TEXT, nLine, aLine, fmtPos, self.A_NONE
|
||||
))
|
||||
tmpMarkdown.append("%s\n" % aLine)
|
||||
if self.keepMarkdown:
|
||||
tmpMarkdown.append("%s\n" % aLine)
|
||||
|
||||
# Always add an empty line at the end
|
||||
self.theTokens.append((
|
||||
self.T_EMPTY,
|
||||
nLine,
|
||||
"",
|
||||
None,
|
||||
self.A_NONE
|
||||
self.T_EMPTY, nLine, "", None, self.A_NONE
|
||||
))
|
||||
tmpMarkdown.append("\n")
|
||||
if self.keepMarkdown:
|
||||
tmpMarkdown.append("\n")
|
||||
|
||||
self.theMarkdown = "".join(tmpMarkdown)
|
||||
tmpMarkdown = []
|
||||
if self.keepMarkdown:
|
||||
self.theMarkdown.append("".join(tmpMarkdown))
|
||||
|
||||
# Second Pass
|
||||
# ===========
|
||||
# Some items need a second pass
|
||||
|
||||
pToken = (self.T_EMPTY, 0, "", None, self.A_NONE)
|
||||
nToken = (self.T_EMPTY, 0, "", None, self.A_NONE)
|
||||
tCount = len(self.theTokens)
|
||||
for n, tToken in enumerate(self.theTokens):
|
||||
|
||||
if n > 0:
|
||||
pToken = self.theTokens[n-1]
|
||||
if n < tCount - 1:
|
||||
nToken = self.theTokens[n+1]
|
||||
|
||||
if tToken[0] == self.T_KEYWORD:
|
||||
aStyle = tToken[4]
|
||||
if pToken[0] == self.T_KEYWORD:
|
||||
aStyle |= self.A_Z_TOPMRG
|
||||
if nToken[0] == self.T_KEYWORD:
|
||||
aStyle |= self.A_Z_BTMMRG
|
||||
self.theTokens[n] = (
|
||||
tToken[0], tToken[1], tToken[2], tToken[3], aStyle
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
@@ -452,9 +491,7 @@ class Tokenizer():
|
||||
# For novel files, we need to handle chapter numbering, scene
|
||||
# numbering, and scene breaks
|
||||
if self.isNovel:
|
||||
for n in range(len(self.theTokens)):
|
||||
|
||||
tToken = self.theTokens[n]
|
||||
for n, tToken in enumerate(self.theTokens):
|
||||
|
||||
# In case we see text before a scene, we reset the flag
|
||||
if tToken[0] == self.T_TEXT:
|
||||
@@ -466,11 +503,7 @@ class Tokenizer():
|
||||
|
||||
tTemp = self._formatHeading(self.fmtTitle, tToken[2])
|
||||
self.theTokens[n] = (
|
||||
tToken[0],
|
||||
tToken[1],
|
||||
tTemp,
|
||||
None,
|
||||
self.A_NONE
|
||||
tToken[0], tToken[1], tTemp, None, self.A_NONE
|
||||
)
|
||||
|
||||
elif tToken[0] == self.T_HEAD2:
|
||||
@@ -488,11 +521,7 @@ class Tokenizer():
|
||||
|
||||
# Format the chapter header
|
||||
self.theTokens[n] = (
|
||||
tToken[0],
|
||||
tToken[1],
|
||||
tTemp,
|
||||
None,
|
||||
self.A_PBB
|
||||
tToken[0], tToken[1], tTemp, None, self.A_PBB
|
||||
)
|
||||
|
||||
# Set scene variables
|
||||
@@ -509,53 +538,29 @@ class Tokenizer():
|
||||
tTemp = self._formatHeading(self.fmtScene, tToken[2])
|
||||
if tTemp == "" and self.hideScene:
|
||||
self.theTokens[n] = (
|
||||
self.T_EMPTY,
|
||||
tToken[1],
|
||||
"",
|
||||
None,
|
||||
self.A_NONE
|
||||
self.T_EMPTY, tToken[1], "", None, self.A_NONE
|
||||
)
|
||||
elif tTemp == "" and not self.hideScene:
|
||||
if self.firstScene:
|
||||
self.theTokens[n] = (
|
||||
self.T_EMPTY,
|
||||
tToken[1],
|
||||
"",
|
||||
None,
|
||||
self.A_NONE
|
||||
self.T_EMPTY, tToken[1], "", None, self.A_NONE
|
||||
)
|
||||
else:
|
||||
self.theTokens[n] = (
|
||||
self.T_SKIP,
|
||||
tToken[1],
|
||||
"",
|
||||
None,
|
||||
self.A_NONE
|
||||
self.T_SKIP, tToken[1], "", None, self.A_NONE
|
||||
)
|
||||
elif tTemp == self.fmtScene:
|
||||
if self.firstScene:
|
||||
self.theTokens[n] = (
|
||||
self.T_EMPTY,
|
||||
tToken[1],
|
||||
"",
|
||||
None,
|
||||
self.A_NONE
|
||||
self.T_EMPTY, tToken[1], "", None, self.A_NONE
|
||||
)
|
||||
else:
|
||||
self.theTokens[n] = (
|
||||
self.T_SEP,
|
||||
tToken[1],
|
||||
tTemp,
|
||||
None,
|
||||
self.A_CENTRE
|
||||
self.T_SEP, tToken[1], tTemp, None, self.A_CENTRE
|
||||
)
|
||||
else:
|
||||
self.theTokens[n] = (
|
||||
tToken[0],
|
||||
tToken[1],
|
||||
tTemp,
|
||||
None,
|
||||
self.A_NONE
|
||||
tToken[0], tToken[1], tTemp, None, self.A_NONE
|
||||
)
|
||||
|
||||
# Definitely no longer the first scene
|
||||
@@ -568,35 +573,19 @@ class Tokenizer():
|
||||
tTemp = self._formatHeading(self.fmtSection, tToken[2])
|
||||
if tTemp == "" and self.hideSection:
|
||||
self.theTokens[n] = (
|
||||
self.T_EMPTY,
|
||||
tToken[1],
|
||||
"",
|
||||
None,
|
||||
self.A_NONE
|
||||
self.T_EMPTY, tToken[1], "", None, self.A_NONE
|
||||
)
|
||||
elif tTemp == "" and not self.hideSection:
|
||||
self.theTokens[n] = (
|
||||
self.T_SKIP,
|
||||
tToken[1],
|
||||
"",
|
||||
None,
|
||||
self.A_NONE
|
||||
self.T_SKIP, tToken[1], "", None, self.A_NONE
|
||||
)
|
||||
elif tTemp == self.fmtSection:
|
||||
self.theTokens[n] = (
|
||||
self.T_SEP,
|
||||
tToken[1],
|
||||
tTemp,
|
||||
None,
|
||||
self.A_CENTRE
|
||||
self.T_SEP, tToken[1], tTemp, None, self.A_CENTRE
|
||||
)
|
||||
else:
|
||||
self.theTokens[n] = (
|
||||
tToken[0],
|
||||
tToken[1],
|
||||
tTemp,
|
||||
None,
|
||||
self.A_NONE
|
||||
tToken[0], tToken[1], tTemp, None, self.A_NONE
|
||||
)
|
||||
|
||||
# For title page and partitions, we need to centre all text.
|
||||
@@ -607,28 +596,18 @@ class Tokenizer():
|
||||
for n, tToken in enumerate(self.theTokens):
|
||||
if tToken[0] == self.T_HEAD1:
|
||||
if self.isTitle:
|
||||
aStyle = self.A_PBB_AUT | self.A_CENTRE
|
||||
self.theTokens[n] = (
|
||||
self.T_TITLE,
|
||||
tToken[1],
|
||||
tToken[2],
|
||||
tToken[3],
|
||||
self.A_PBB_NO | self.A_CENTRE
|
||||
self.T_TITLE, tToken[1], tToken[2], tToken[3], aStyle
|
||||
)
|
||||
else:
|
||||
aStyle = self.A_PBB | self.A_CENTRE
|
||||
self.theTokens[n] = (
|
||||
tToken[0],
|
||||
tToken[1],
|
||||
tToken[2],
|
||||
tToken[3],
|
||||
self.A_PBB | self.A_CENTRE
|
||||
tToken[0], tToken[1], tToken[2], tToken[3], aStyle
|
||||
)
|
||||
else:
|
||||
self.theTokens[n] = (
|
||||
tToken[0],
|
||||
tToken[1],
|
||||
tToken[2],
|
||||
tToken[3],
|
||||
self.A_CENTRE
|
||||
tToken[0], tToken[1], tToken[2], tToken[3], self.A_CENTRE
|
||||
)
|
||||
|
||||
# Add a page break after the last entry
|
||||
@@ -636,11 +615,7 @@ class Tokenizer():
|
||||
if n >= 0:
|
||||
tToken = self.theTokens[n]
|
||||
self.theTokens[n] = (
|
||||
tToken[0],
|
||||
tToken[1],
|
||||
tToken[2],
|
||||
tToken[3],
|
||||
tToken[4] | self.A_PBA
|
||||
tToken[0], tToken[1], tToken[2], tToken[3], tToken[4] | self.A_PBA
|
||||
)
|
||||
|
||||
# A single page is always left-aligned and starts on a fresh
|
||||
@@ -649,23 +624,23 @@ class Tokenizer():
|
||||
for n, tToken in enumerate(self.theTokens):
|
||||
if n == 0:
|
||||
self.theTokens[n] = (
|
||||
tToken[0],
|
||||
tToken[1],
|
||||
tToken[2],
|
||||
tToken[3],
|
||||
self.A_LEFT | self.A_PBB
|
||||
tToken[0], tToken[1], tToken[2], tToken[3], self.A_LEFT | self.A_PBB
|
||||
)
|
||||
else:
|
||||
self.theTokens[n] = (
|
||||
tToken[0],
|
||||
tToken[1],
|
||||
tToken[2],
|
||||
tToken[3],
|
||||
self.A_LEFT
|
||||
tToken[0], tToken[1], tToken[2], tToken[3], self.A_LEFT
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def saveRawMarkdown(self, savePath):
|
||||
"""Save the data to a plain text file.
|
||||
"""
|
||||
with open(savePath, mode="w", encoding="utf8") as outFile:
|
||||
for nwdPage in self.theMarkdown:
|
||||
outFile.write(nwdPage)
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
197
nw/core/tomd.py
Normal file
@@ -0,0 +1,197 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – Markdown Text Converter
|
||||
=====================================
|
||||
Extends the Tokenizer class to generate Makrdown output
|
||||
|
||||
File History:
|
||||
Created: 2021-02-06 [1.2a0]
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from nw.core.tokenizer import Tokenizer
|
||||
from nw.constants import nwLabels
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ToMarkdown(Tokenizer):
|
||||
|
||||
M_STD = 0 # Standard Markdown
|
||||
M_GH = 1 # GitHub Markdown
|
||||
|
||||
def __init__(self, theProject, theParent):
|
||||
Tokenizer.__init__(self, theProject, theParent)
|
||||
|
||||
self.genMode = self.M_STD
|
||||
self.fullMD = []
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Setters
|
||||
##
|
||||
|
||||
def setStandardMarkdown(self):
|
||||
self.genMode = self.M_STD
|
||||
return
|
||||
|
||||
def setGitHubMarkdown(self):
|
||||
self.genMode = self.M_GH
|
||||
return
|
||||
|
||||
##
|
||||
# Class Methods
|
||||
##
|
||||
|
||||
def getFullResultSize(self):
|
||||
"""Return the size of the full Markdown result.
|
||||
"""
|
||||
return sum([len(x) for x in self.fullMD])
|
||||
|
||||
def doConvert(self):
|
||||
"""Convert the list of text tokens into a HTML document saved
|
||||
to theResult.
|
||||
"""
|
||||
if self.genMode == self.M_STD:
|
||||
# Standard
|
||||
mdTags = {
|
||||
self.FMT_B_B : "**", self.FMT_B_E : "**",
|
||||
self.FMT_I_B : "_", self.FMT_I_E : "_",
|
||||
self.FMT_D_B : "", self.FMT_D_E : "",
|
||||
}
|
||||
else:
|
||||
# GitHub and novelWriter
|
||||
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.theResult = ""
|
||||
|
||||
thisPar = []
|
||||
tmpResult = []
|
||||
|
||||
for tType, tLine, tText, tFormat, tStyle in self.theTokens:
|
||||
|
||||
# Process Text Type
|
||||
if tType == self.T_EMPTY:
|
||||
if len(thisPar) > 0:
|
||||
tTemp = "".join(thisPar)
|
||||
tmpResult.append("%s\n\n" % tTemp.rstrip(" "))
|
||||
thisPar = []
|
||||
|
||||
elif tType == self.T_TITLE:
|
||||
tHead = tText.replace(r"\\", "\n")
|
||||
tmpResult.append("# %s\n\n" % tHead)
|
||||
|
||||
elif tType == self.T_HEAD1:
|
||||
tHead = tText.replace(r"\\", "\n")
|
||||
tmpResult.append("# %s\n\n" % tHead)
|
||||
|
||||
elif tType == self.T_HEAD2:
|
||||
tHead = tText.replace(r"\\", "\n")
|
||||
tmpResult.append("## %s\n\n" % tHead)
|
||||
|
||||
elif tType == self.T_HEAD3:
|
||||
tHead = tText.replace(r"\\", "\n")
|
||||
tmpResult.append("### %s\n\n" % tHead)
|
||||
|
||||
elif tType == self.T_HEAD4:
|
||||
tHead = tText.replace(r"\\", "\n")
|
||||
tmpResult.append("#### %s\n\n" % tHead)
|
||||
|
||||
elif tType == self.T_SEP:
|
||||
tmpResult.append("%s\n\n" % tText)
|
||||
|
||||
elif tType == self.T_SKIP:
|
||||
tmpResult.append("\n\n\n")
|
||||
|
||||
elif tType == self.T_TEXT:
|
||||
tTemp = tText
|
||||
for xPos, xLen, xFmt in reversed(tFormat):
|
||||
tTemp = tTemp[:xPos] + mdTags[xFmt] + tTemp[xPos+xLen:]
|
||||
if tText.endswith(" "):
|
||||
thisPar.append(tTemp.rstrip() + " \n")
|
||||
else:
|
||||
thisPar.append(tTemp.rstrip() + " ")
|
||||
|
||||
elif tType == self.T_SYNOPSIS and self.doSynopsis:
|
||||
tmpResult.append("**Synopsis:** %s\n\n" % tText)
|
||||
|
||||
elif tType == self.T_COMMENT and self.doComments:
|
||||
tmpResult.append("**Comment:** %s\n\n" % tText)
|
||||
|
||||
elif tType == self.T_KEYWORD and self.doKeywords:
|
||||
tmpResult.append(self._formatKeywords(tText, tStyle))
|
||||
|
||||
self.theResult = "".join(tmpResult)
|
||||
tmpResult = []
|
||||
|
||||
self.fullMD.append(self.theResult)
|
||||
|
||||
return
|
||||
|
||||
def saveMarkdown(self, savePath):
|
||||
"""Save the data to a plain text file.
|
||||
"""
|
||||
with open(savePath, mode="w", encoding="utf8") as outFile:
|
||||
theText = "".join(self.fullMD)
|
||||
outFile.write(theText)
|
||||
|
||||
return
|
||||
|
||||
def replaceTabs(self, nSpaces=8, spaceChar=" "):
|
||||
"""Replace tabs with spaces.
|
||||
"""
|
||||
fullMD = []
|
||||
eightSpace = spaceChar*nSpaces
|
||||
for aPage in self.fullMD:
|
||||
fullMD.append(aPage.replace("\t", eightSpace))
|
||||
|
||||
self.fullMD = fullMD
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _formatKeywords(self, tText, tStyle):
|
||||
"""Apply Markdown formatting to keywords.
|
||||
"""
|
||||
isValid, theBits, thePos = self.theParent.theIndex.scanThis("@"+tText)
|
||||
if not isValid or not theBits:
|
||||
return ""
|
||||
|
||||
retText = ""
|
||||
if theBits[0] in nwLabels.KEY_NAME:
|
||||
retText += "**%s:** " % nwLabels.KEY_NAME[theBits[0]]
|
||||
|
||||
if len(theBits) > 1:
|
||||
retText += ", ".join(theBits[1:])
|
||||
|
||||
if tStyle & self.A_Z_BTMMRG:
|
||||
retText += " \n"
|
||||
else:
|
||||
retText += "\n\n"
|
||||
|
||||
return retText
|
||||
|
||||
# END Class ToMarkdown
|
||||
1285
nw/core/toodt.py
Normal file
@@ -1,31 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Various Tools
|
||||
"""
|
||||
novelWriter – Various Tools
|
||||
===========================
|
||||
Various core tool functions
|
||||
|
||||
novelWriter – Various Tools
|
||||
=============================
|
||||
Various core tool functions
|
||||
File History:
|
||||
Created: 2019-04-22 [0.0.1] countWords
|
||||
Created: 2019-10-13 [0.2.3] numberToWord, _numberToWordEN
|
||||
Merged: 2020-05-08 [0.4.5] All of the above into this file
|
||||
Created: 2020-07-05 [0.10.0] numberToRoman
|
||||
|
||||
File History:
|
||||
Created: 2019-04-22 [0.0.1] countWords
|
||||
Created: 2019-10-13 [0.2.3] numberToWord, _numberToWordEN
|
||||
Merged: 2020-05-08 [0.4.5] All of the above into this file
|
||||
Created: 2020-07-05 [0.10.0] numberToRoman
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
109
nw/core/tree.py
@@ -1,30 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Project Tree Class
|
||||
"""
|
||||
novelWriter – Project Tree Class
|
||||
================================
|
||||
Data class for the project's tree of project items
|
||||
|
||||
novelWriter – Project Tree Class
|
||||
==================================
|
||||
Class holding the project's tree of project items
|
||||
File History:
|
||||
Created: 2020-05-07 [0.4.5]
|
||||
|
||||
File History:
|
||||
Created: 2020-05-07 [0.4.5]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
import logging
|
||||
import os
|
||||
|
||||
@@ -34,10 +34,35 @@ from time import time
|
||||
|
||||
from nw.core.item import NWItem
|
||||
from nw.common import checkHandle
|
||||
from nw.constants import nwFiles, nwItemType, nwItemClass, nwItemLayout, nwConst
|
||||
from nw.constants import (
|
||||
nwFiles, nwItemType, nwItemClass, nwItemLayout, nwConst, nwLists
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Layout Translation Map
|
||||
LAYOUT_MAP = {
|
||||
nwItemLayout.SCENE: {
|
||||
"H1": nwItemLayout.BOOK,
|
||||
"H2": nwItemLayout.CHAPTER,
|
||||
},
|
||||
nwItemLayout.CHAPTER: {
|
||||
"H1": nwItemLayout.BOOK,
|
||||
"H3": nwItemLayout.SCENE,
|
||||
"H4": nwItemLayout.SCENE,
|
||||
},
|
||||
nwItemLayout.UNNUMBERED: {
|
||||
"H1": nwItemLayout.BOOK,
|
||||
"H3": nwItemLayout.SCENE,
|
||||
"H4": nwItemLayout.SCENE,
|
||||
},
|
||||
nwItemLayout.PARTITION: {
|
||||
"H2": nwItemLayout.CHAPTER,
|
||||
"H3": nwItemLayout.SCENE,
|
||||
"H4": nwItemLayout.SCENE,
|
||||
},
|
||||
}
|
||||
|
||||
class NWTree():
|
||||
|
||||
def __init__(self, theProject):
|
||||
@@ -180,8 +205,9 @@ class NWTree():
|
||||
outFile.write("\n".join(tocList))
|
||||
outFile.write("\n")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
except Exception:
|
||||
logger.error("Could not write ToC file")
|
||||
nw.logException()
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -203,6 +229,29 @@ class NWTree():
|
||||
novelWords += tItem.wordCount
|
||||
return novelWords, noteWords
|
||||
|
||||
def updateItemLayout(self, tHandle, hLevel):
|
||||
"""Check if the item layout needs updating based on the header
|
||||
given level.
|
||||
"""
|
||||
tItem = self.__getitem__(tHandle)
|
||||
if tItem is None:
|
||||
return False
|
||||
if tItem.itemClass not in nwLists.CLS_NOVEL:
|
||||
return False
|
||||
if hLevel not in ("H1", "H2", "H3", "H4"):
|
||||
return False
|
||||
|
||||
iLayout = tItem.itemLayout
|
||||
if iLayout in LAYOUT_MAP:
|
||||
if hLevel in LAYOUT_MAP[iLayout]:
|
||||
tItem.itemLayout = LAYOUT_MAP[iLayout][hLevel]
|
||||
logger.debug("Changed layout for %s from %s to %s" % (
|
||||
tHandle, iLayout.name, tItem.itemLayout.name
|
||||
))
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
##
|
||||
# Tree Structure Methods
|
||||
##
|
||||
@@ -294,11 +343,6 @@ class NWTree():
|
||||
tTree.append(tHandle)
|
||||
return tTree
|
||||
|
||||
def handleExists(self, tHandle):
|
||||
"""Check if a handle exists in the project.
|
||||
"""
|
||||
return tHandle in self._treeOrder
|
||||
|
||||
##
|
||||
# Setters
|
||||
##
|
||||
@@ -463,8 +507,13 @@ class NWTree():
|
||||
return
|
||||
|
||||
def _makeHandle(self, addSeed=""):
|
||||
"""Generate a unique item handle. In the unlikely event that the
|
||||
key already exists, salt the seed and generate a new handle.
|
||||
"""Generate a unique item handle. In the event that the key
|
||||
already exists, salt the seed and generate a new handle.
|
||||
A key collision is very unlikely to be caused by the truncation
|
||||
of the sha256 hash to 13 characters. Assuming it is near-random,
|
||||
it will on average happen every 4.5^15 times. However, the clock
|
||||
seed is likely to occasionally generate a collision if the
|
||||
handle requests come faster than the clock resolution.
|
||||
"""
|
||||
if self._handleSeed is None:
|
||||
newSeed = str(time()) + addSeed
|
||||
@@ -472,11 +521,13 @@ class NWTree():
|
||||
# This is used for debugging
|
||||
newSeed = str(self._handleSeed)
|
||||
self._handleSeed += 1
|
||||
|
||||
logger.verbose("Generating handle with seed '%s'" % newSeed)
|
||||
itemHandle = sha256(newSeed.encode()).hexdigest()[0:13]
|
||||
if itemHandle in self._projTree:
|
||||
logger.warning("Duplicate handle encountered! Retrying ...")
|
||||
itemHandle = self._makeHandle(addSeed+"!")
|
||||
|
||||
return itemHandle
|
||||
|
||||
# END Class NWTree
|
||||
|
||||
70
nw/error.py
@@ -1,29 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Exception Handling
|
||||
|
||||
novelWriter – Exception Handling
|
||||
==================================
|
||||
Error handling functions
|
||||
|
||||
File History:
|
||||
Created: 2020-08-02 [0.10.2]
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
novelWriter – Exception Handling
|
||||
================================
|
||||
Error handling function and error dialog
|
||||
|
||||
File History:
|
||||
Created: 2020-08-02 [0.10.2]
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import logging
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import (
|
||||
@@ -31,6 +33,22 @@ from PyQt5.QtWidgets import (
|
||||
QDialogButtonBox
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# =============================================================================================== #
|
||||
# Utility Functions
|
||||
# =============================================================================================== #
|
||||
|
||||
def logException():
|
||||
"""Log the content of an exception message.
|
||||
"""
|
||||
exType, exValue, _ = sys.exc_info()
|
||||
logger.error("%s: %s" % (exType.__name__, str(exValue).strip("'")))
|
||||
|
||||
# =============================================================================================== #
|
||||
# Error Handler
|
||||
# =============================================================================================== #
|
||||
|
||||
class NWErrorMessage(QDialog):
|
||||
|
||||
def __init__(self, parent):
|
||||
@@ -73,7 +91,6 @@ class NWErrorMessage(QDialog):
|
||||
"""Generate a message and append session data, error info and
|
||||
error traceback.
|
||||
"""
|
||||
import sys
|
||||
from traceback import format_tb
|
||||
from nw import __issuesurl__, __version__
|
||||
from PyQt5.Qt import PYQT_VERSION_STR
|
||||
@@ -134,15 +151,12 @@ class NWErrorMessage(QDialog):
|
||||
|
||||
# END Class NWErrorMessage
|
||||
|
||||
|
||||
def exceptionHandler(exType, exValue, exTrace):
|
||||
"""Function to catch unhandled global exceptions.
|
||||
"""
|
||||
import logging
|
||||
from traceback import print_tb
|
||||
from PyQt5.QtWidgets import qApp
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.critical("%s: %s" % (exType.__name__, str(exValue)))
|
||||
print_tb(exTrace)
|
||||
|
||||
|
||||
@@ -9,15 +9,18 @@ from nw.gui.docviewer import GuiDocViewer, GuiDocViewDetails
|
||||
from nw.gui.itemdetails import GuiItemDetails
|
||||
from nw.gui.itemeditor import GuiItemEditor
|
||||
from nw.gui.mainmenu import GuiMainMenu
|
||||
from nw.gui.noveltree import GuiNovelTree
|
||||
from nw.gui.outline import GuiOutline
|
||||
from nw.gui.outlinedetails import GuiOutlineDetails
|
||||
from nw.gui.preferences import GuiPreferences
|
||||
from nw.gui.projdetails import GuiProjectDetails
|
||||
from nw.gui.projload import GuiProjectLoad
|
||||
from nw.gui.projsettings import GuiProjectSettings
|
||||
from nw.gui.projtree import GuiProjectTree
|
||||
from nw.gui.projwizard import GuiProjectWizard
|
||||
from nw.gui.statusbar import GuiMainStatus
|
||||
from nw.gui.theme import GuiTheme
|
||||
from nw.gui.wordlist import GuiWordList
|
||||
from nw.gui.writingstats import GuiWritingStats
|
||||
|
||||
__all__ = [
|
||||
@@ -31,14 +34,17 @@ __all__ = [
|
||||
"GuiItemDetails",
|
||||
"GuiItemEditor",
|
||||
"GuiMainMenu",
|
||||
"GuiNovelTree",
|
||||
"GuiMainStatus",
|
||||
"GuiOutline",
|
||||
"GuiOutlineDetails",
|
||||
"GuiPreferences",
|
||||
"GuiProjectDetails",
|
||||
"GuiProjectLoad",
|
||||
"GuiProjectSettings",
|
||||
"GuiProjectTree",
|
||||
"GuiProjectWizard",
|
||||
"GuiTheme",
|
||||
"GuiWordList",
|
||||
"GuiWritingStats",
|
||||
]
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI About Box
|
||||
"""
|
||||
novelWriter – GUI About Box
|
||||
===========================
|
||||
The about novelWriter dialog box
|
||||
|
||||
novelWriter – GUI About Box
|
||||
=============================
|
||||
The about novelWriter dialog box
|
||||
File History:
|
||||
Created: 2020-05-21 [0.5.2]
|
||||
|
||||
File History:
|
||||
Created: 2020-05-21 [0.5.2]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
|
||||
883
nw/gui/build.py
@@ -1,30 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Custom Widgets and Layouts
|
||||
"""
|
||||
novelWriter – Custom Widgets and Layouts
|
||||
========================================
|
||||
Various custom widget and layout classes
|
||||
|
||||
novelWriter – Custom Widgets and Layouts
|
||||
==========================================
|
||||
Various custom widget and layout classes
|
||||
File History:
|
||||
Created: 2020-05-03 [0.4.5] QConfigLayout
|
||||
Created: 2020-05-03 [0.4.5] QSwitch
|
||||
Created: 2020-05-17 [0.5.1] PagedDialog
|
||||
|
||||
File History:
|
||||
Created: 2020-05-03 [0.4.5] QConfigLayout
|
||||
Created: 2020-05-03 [0.4.5] QSwitch
|
||||
Created: 2020-05-17 [0.5.1] PagedDialog
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
@@ -304,7 +303,7 @@ class QSwitch(QAbstractButton):
|
||||
trackBrush = qPalette.dark()
|
||||
thumbBrush = qPalette.light()
|
||||
textColor = qPalette.dark().color()
|
||||
thumbText = nwUnicode.U_MULT
|
||||
thumbText = nwUnicode.U_CROSS
|
||||
|
||||
if self.isEnabled():
|
||||
trackOpacity = 1.0
|
||||
|
||||
@@ -1,34 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI Document Editor
|
||||
"""
|
||||
novelWriter – GUI Document Editor
|
||||
=================================
|
||||
GUI classes for the main document editor
|
||||
|
||||
novelWriter – GUI Document Editor
|
||||
===================================
|
||||
Class holding the main document editor
|
||||
File History:
|
||||
Created: 2018-09-29 [0.0.1] GuiDocEditor
|
||||
Created: 2019-04-22 [0.0.1] BackgroundWordCounter
|
||||
Created: 2019-09-29 [0.2.1] GuiDocEditSearch
|
||||
Created: 2020-04-25 [0.4.5] GuiDocEditHeader
|
||||
Rewritten: 2020-06-15 [0.9.0] GuiDocEditSearch
|
||||
Created: 2020-06-27 [0.10.0] GuiDocEditFooter
|
||||
Rewritten: 2020-10-07 [1.0b3] BackgroundWordCounter
|
||||
|
||||
File History:
|
||||
Created: 2018-09-29 [0.0.1] GuiDocEditor
|
||||
Created: 2019-04-22 [0.0.1] BackgroundWordCounter
|
||||
Created: 2019-09-29 [0.2.1] GuiDocEditSearch
|
||||
Created: 2020-04-25 [0.4.5] GuiDocEditHeader
|
||||
Rewritten: 2020-06-15 [0.9.0] GuiDocEditSearch
|
||||
Created: 2020-06-27 [0.10.0] GuiDocEditFooter
|
||||
Rewritten: 2020-10-07 [1.0b3] BackgroundWordCounter
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
@@ -55,7 +54,7 @@ from nw.gui.dochighlight import GuiDocHighlighter
|
||||
from nw.common import transferCase
|
||||
from nw.constants import (
|
||||
nwConst, nwAlert, nwUnicode, nwDocAction, nwDocInsert, nwItemClass,
|
||||
nwKeyWords
|
||||
nwKeyWords, nwLabels
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -76,12 +75,14 @@ class GuiDocEditor(QTextEdit):
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theParent = theParent
|
||||
self.theTheme = theParent.theTheme
|
||||
self.theIndex = theParent.theIndex
|
||||
self.theProject = theParent.theProject
|
||||
self.nwDocument = NWDoc(self.theProject, self.theParent)
|
||||
|
||||
self.docChanged = False # Flag for changed status of document
|
||||
self.spellCheck = False # Flag for spell checking enabled
|
||||
self.theHandle = None # The handle of the open file
|
||||
self.theHeaders = [] # Record of headers in the file
|
||||
self.theDict = None # The current spell check dictionary
|
||||
self.nonWord = "\"'" # Characters to not include in spell checking
|
||||
|
||||
@@ -90,6 +91,7 @@ class GuiDocEditor(QTextEdit):
|
||||
self.wordCount = 0 # Word count
|
||||
self.paraCount = 0 # Paragraph count
|
||||
self.lastEdit = 0 # Time stamp of last edit
|
||||
self.lastActive = 0 # Time stamp of last activity
|
||||
self.lastFind = None # Position of the last found search word
|
||||
self.bigDoc = False # Flag for very large document size
|
||||
self.doReplace = False # Switch to temporarily disable auto-replace
|
||||
@@ -144,15 +146,15 @@ class GuiDocEditor(QTextEdit):
|
||||
)
|
||||
|
||||
# Set Up Word Counter
|
||||
self.wcInterval = self.mainConf.wordCountTimer
|
||||
self.wcTimer = QTimer()
|
||||
self.wcTimer.setInterval(int(self.wcInterval*1000))
|
||||
self.wcTimer.timeout.connect(self._runCounter)
|
||||
|
||||
self.wCounter = BackgroundWordCounter(self)
|
||||
self.wCounter.setAutoDelete(False)
|
||||
self.wCounter.signals.countsReady.connect(self._updateCounts)
|
||||
|
||||
self.wcInterval = self.mainConf.wordCountTimer
|
||||
|
||||
self.initEditor()
|
||||
|
||||
logger.debug("GuiDocEditor initialisation complete")
|
||||
@@ -168,15 +170,16 @@ class GuiDocEditor(QTextEdit):
|
||||
self.clear()
|
||||
self.wcTimer.stop()
|
||||
|
||||
self.theHandle = None
|
||||
self.charCount = 0
|
||||
self.wordCount = 0
|
||||
self.paraCount = 0
|
||||
self.lastEdit = 0
|
||||
self.lastFind = None
|
||||
self.bigDoc = False
|
||||
self.doReplace = False
|
||||
self.queuePos = None
|
||||
self.theHandle = None
|
||||
self.charCount = 0
|
||||
self.wordCount = 0
|
||||
self.paraCount = 0
|
||||
self.lastEdit = 0
|
||||
self.lastActive = 0
|
||||
self.lastFind = None
|
||||
self.bigDoc = False
|
||||
self.doReplace = False
|
||||
self.queuePos = None
|
||||
|
||||
self.setDocumentChanged(False)
|
||||
self.docHeader.setTitleFromHandle(self.theHandle)
|
||||
@@ -259,6 +262,10 @@ class GuiDocEditor(QTextEdit):
|
||||
# Initialise the syntax highlighter
|
||||
self.hLight.initHighlighter()
|
||||
|
||||
# Configure word count timer
|
||||
self.wcInterval = self.mainConf.wordCountTimer
|
||||
self.wcTimer.setInterval(int(self.wcInterval*1000))
|
||||
|
||||
# If we have a document open, we should reload it in case the
|
||||
# font changed, otherwise we just clear the editor entirely,
|
||||
# which makes it read only.
|
||||
@@ -314,9 +321,9 @@ class GuiDocEditor(QTextEdit):
|
||||
logger.debug("Document highlighted in %.3f ms" % (1000*(afTime-bfTime)))
|
||||
|
||||
self.lastEdit = time()
|
||||
self.lastActive = time()
|
||||
self._runCounter()
|
||||
self.wcTimer.start()
|
||||
self.setDocumentChanged(False)
|
||||
self.theHandle = tHandle
|
||||
|
||||
self.setReadOnly(False)
|
||||
@@ -340,9 +347,18 @@ class GuiDocEditor(QTextEdit):
|
||||
|
||||
self.docFooter.updateLineCount()
|
||||
self.lengthLast = self.qDocument.characterCount()
|
||||
self.theHeaders = self.theIndex.getHandleHeaders(self.theHandle)
|
||||
|
||||
qApp.processEvents()
|
||||
self.setDocumentChanged(False)
|
||||
qApp.restoreOverrideCursor()
|
||||
|
||||
# This is a hack to fix invisble cursor on an empty document
|
||||
if self.qDocument.characterCount() <= 1:
|
||||
self.setPlainText("\n")
|
||||
self.setPlainText("")
|
||||
self.setCursorPosition(0)
|
||||
|
||||
return True
|
||||
|
||||
def updateTagHighLighting(self, forceBigDoc=False):
|
||||
@@ -373,8 +389,8 @@ class GuiDocEditor(QTextEdit):
|
||||
|
||||
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
|
||||
self.setPlainText(theText)
|
||||
self.setDocumentChanged(True)
|
||||
self.updateDocMargins()
|
||||
self.setDocumentChanged(True)
|
||||
qApp.restoreOverrideCursor()
|
||||
|
||||
return True
|
||||
@@ -388,6 +404,7 @@ class GuiDocEditor(QTextEdit):
|
||||
return False
|
||||
|
||||
docText = self.getText()
|
||||
tHandle = theItem.itemHandle
|
||||
|
||||
cC, wC, pC = countWords(docText)
|
||||
self._updateCounts(cC, wC, pC)
|
||||
@@ -400,7 +417,21 @@ class GuiDocEditor(QTextEdit):
|
||||
self.nwDocument.saveDocument(docText)
|
||||
self.setDocumentChanged(False)
|
||||
|
||||
self.theParent.theIndex.scanText(theItem.itemHandle, docText)
|
||||
self.theIndex.scanText(tHandle, docText)
|
||||
|
||||
if self._updateHeaders(checkLevel=True):
|
||||
self.theParent.requestNovelTreeRefresh()
|
||||
else:
|
||||
self.theParent.novelView.updateWordCounts(tHandle)
|
||||
|
||||
hLevel = "H0"
|
||||
if self.theHeaders:
|
||||
hLevel = self.theHeaders[0][1]
|
||||
|
||||
if self.theProject.projTree.updateItemLayout(tHandle, hLevel):
|
||||
self.theParent.treeView.setTreeItemValues(tHandle)
|
||||
self.nwDocument.saveDocument(docText)
|
||||
self.docFooter.updateInfo()
|
||||
|
||||
return True
|
||||
|
||||
@@ -450,11 +481,17 @@ class GuiDocEditor(QTextEdit):
|
||||
lM = max(cM, fH)
|
||||
self.setViewportMargins(tM, uM, tM, lM)
|
||||
|
||||
docChanged = self.docChanged
|
||||
if self.mainConf.scrollPastEnd:
|
||||
docFrame = self.qDocument.rootFrame().frameFormat()
|
||||
docFrame.setBottomMargin(max(0, 0.6*(wH - uM - lM - 4*tB)))
|
||||
docFrame.setBottomMargin(max(0, 0.9*(wH - uM - lM - 4*tB)))
|
||||
self.qDocument.rootFrame().setFrameFormat(docFrame)
|
||||
|
||||
# This is needed as the setFrameFormat function itself will
|
||||
# trigger the contetsChanged signal which sets docChanged, so we
|
||||
# set it back to whatever it was before.
|
||||
self.setDocumentChanged(docChanged)
|
||||
|
||||
return
|
||||
|
||||
def updateDocInfo(self, tHandle):
|
||||
@@ -502,9 +539,10 @@ class GuiDocEditor(QTextEdit):
|
||||
if not isinstance(thePosition, int):
|
||||
return False
|
||||
|
||||
if thePosition >= 0:
|
||||
nChars = self.qDocument.characterCount()
|
||||
if nChars > 1:
|
||||
theCursor = self.textCursor()
|
||||
theCursor.setPosition(thePosition)
|
||||
theCursor.setPosition(min(max(thePosition, 0), nChars-1))
|
||||
self.setTextCursor(theCursor)
|
||||
self.docFooter.updateLineCount()
|
||||
|
||||
@@ -622,6 +660,10 @@ class GuiDocEditor(QTextEdit):
|
||||
this class when calling these actions from other classes.
|
||||
"""
|
||||
logger.verbose("Requesting action: %s" % theAction.name)
|
||||
if not self.hasFocus():
|
||||
logger.verbose("Editor does not have focus")
|
||||
return False
|
||||
|
||||
if self.theHandle is None:
|
||||
logger.error("No document open")
|
||||
return False
|
||||
@@ -683,6 +725,7 @@ class GuiDocEditor(QTextEdit):
|
||||
return False
|
||||
|
||||
self._allowAutoReplace(True)
|
||||
self.lastActive = time()
|
||||
|
||||
return True
|
||||
|
||||
@@ -723,20 +766,6 @@ class GuiDocEditor(QTextEdit):
|
||||
elif isinstance(theInsert, nwDocInsert):
|
||||
if theInsert == nwDocInsert.HARD_BREAK:
|
||||
theText = " \n"
|
||||
elif theInsert == nwDocInsert.NB_SPACE:
|
||||
theText = nwUnicode.U_NBSP
|
||||
elif theInsert == nwDocInsert.THIN_SPACE:
|
||||
theText = nwUnicode.U_THNSP
|
||||
elif theInsert == nwDocInsert.THIN_NB_SPACE:
|
||||
theText = nwUnicode.U_THNBSP
|
||||
elif theInsert == nwDocInsert.SHORT_DASH:
|
||||
theText = nwUnicode.U_ENDASH
|
||||
elif theInsert == nwDocInsert.LONG_DASH:
|
||||
theText = nwUnicode.U_EMDASH
|
||||
elif theInsert == nwDocInsert.ELLIPSIS:
|
||||
theText = nwUnicode.U_HELLIP
|
||||
elif theInsert == nwDocInsert.MODAPOS_S:
|
||||
theText = nwUnicode.U_MAPOSS
|
||||
elif theInsert == nwDocInsert.QUOTE_LS:
|
||||
theText = self.typSQOpen
|
||||
elif theInsert == nwDocInsert.QUOTE_RS:
|
||||
@@ -814,6 +843,7 @@ class GuiDocEditor(QTextEdit):
|
||||
* The undo/redo/select all sequences bypasses the docAction
|
||||
pathway from the menu, so we redirect them back from here.
|
||||
"""
|
||||
self.lastActive = time()
|
||||
isReturn = keyEvent.key() == Qt.Key_Return
|
||||
isReturn |= keyEvent.key() == Qt.Key_Enter
|
||||
if isReturn and self.docSearch.anyFocus():
|
||||
@@ -839,7 +869,7 @@ class GuiDocEditor(QTextEdit):
|
||||
if okMod and okKey:
|
||||
cNew = self.cursorRect().center().y()
|
||||
cMov = cNew - cOld
|
||||
mPos = self.mainConf.autoScrollPos * self.viewport().height() * 0.01
|
||||
mPos = self.mainConf.autoScrollPos*0.01 * self.viewport().height()
|
||||
if abs(cMov) > 0 and cOld > mPos:
|
||||
# Move the scroll bar
|
||||
vBar = self.verticalScrollBar()
|
||||
@@ -895,12 +925,13 @@ class GuiDocEditor(QTextEdit):
|
||||
##
|
||||
|
||||
@pyqtSlot(int, int, int)
|
||||
def _docChange(self, thePos, charsRemoved, charsAdded):
|
||||
def _docChange(self, thePos, chrRem, chrAdd):
|
||||
"""Triggered by QTextDocument->contentsChanged. This also
|
||||
triggers the syntax highlighter.
|
||||
"""
|
||||
self.lastEdit = time()
|
||||
self.lastFind = None
|
||||
|
||||
if self.qDocument.characterCount() > nwConst.MAX_DOCSIZE:
|
||||
self.theParent.makeAlert((
|
||||
"The document has grown too big and you cannot add more text to it. "
|
||||
@@ -908,12 +939,16 @@ class GuiDocEditor(QTextEdit):
|
||||
) % (nwConst.MAX_DOCSIZE/1.0e6), nwAlert.ERROR)
|
||||
self.undo()
|
||||
return
|
||||
|
||||
if not self.docChanged:
|
||||
self.setDocumentChanged(True)
|
||||
self.setDocumentChanged(chrRem != 0 or chrAdd != 0)
|
||||
|
||||
if not self.wcTimer.isActive():
|
||||
self.wcTimer.start()
|
||||
if self.doReplace and charsAdded == 1:
|
||||
|
||||
if self.doReplace and chrAdd == 1:
|
||||
self._docAutoReplace(self.qDocument.findBlock(thePos))
|
||||
|
||||
return
|
||||
|
||||
@pyqtSlot("QPoint")
|
||||
@@ -998,18 +1033,18 @@ class GuiDocEditor(QTextEdit):
|
||||
for aWord in theSuggest:
|
||||
mnuWord = QAction("%s %s" % (nwUnicode.U_ENDASH, aWord), mnuContext)
|
||||
mnuWord.triggered.connect(
|
||||
lambda thePos, aWord=aWord : self._correctWord(posCursor, aWord)
|
||||
lambda thePos, aWord=aWord: self._correctWord(posCursor, aWord)
|
||||
)
|
||||
mnuContext.addAction(mnuWord)
|
||||
mnuContext.addSeparator()
|
||||
mnuAdd = QAction("Add Word to Dictionary", mnuContext)
|
||||
mnuAdd.triggered.connect(lambda thePos : self._addWord(posCursor))
|
||||
mnuContext.addAction(mnuAdd)
|
||||
|
||||
else:
|
||||
mnuHead = QAction("No Suggestions", mnuContext)
|
||||
mnuHead = QAction("%s No Suggestions" % nwUnicode.U_ENDASH, mnuContext)
|
||||
mnuContext.addAction(mnuHead)
|
||||
|
||||
mnuContext.addSeparator()
|
||||
mnuAdd = QAction("Add Word to Dictionary", mnuContext)
|
||||
mnuAdd.triggered.connect(lambda thePos: self._addWord(posCursor))
|
||||
mnuContext.addAction(mnuAdd)
|
||||
|
||||
# Open the context menu
|
||||
mnuContext.exec_(self.viewport().mapToGlobal(thePos))
|
||||
|
||||
@@ -1053,7 +1088,7 @@ class GuiDocEditor(QTextEdit):
|
||||
logger.verbose("Word counter is busy")
|
||||
return
|
||||
|
||||
if time() - self.lastEdit < 5*self.wcInterval:
|
||||
if time() - self.lastEdit < 5 * self.wcInterval:
|
||||
logger.verbose("Running word counter")
|
||||
self.theParent.threadPool.start(self.wCounter)
|
||||
|
||||
@@ -1211,6 +1246,30 @@ class GuiDocEditor(QTextEdit):
|
||||
|
||||
return
|
||||
|
||||
def _updateHeaders(self, checkPos=False, checkLevel=False):
|
||||
"""Update the headers record and return True if anything
|
||||
changed, if a check flag was provided.
|
||||
"""
|
||||
if self.theHandle is None:
|
||||
return False
|
||||
|
||||
newHeaders = self.theIndex.getHandleHeaders(self.theHandle)
|
||||
if checkPos:
|
||||
newPos = [x[0] for x in newHeaders]
|
||||
oldPos = [x[0] for x in self.theHeaders]
|
||||
if checkLevel:
|
||||
newLev = [x[1] for x in newHeaders]
|
||||
oldLev = [x[1] for x in self.theHeaders]
|
||||
|
||||
self.theHeaders = newHeaders
|
||||
|
||||
if checkPos:
|
||||
return newPos != oldPos
|
||||
if checkLevel:
|
||||
return newLev != oldLev
|
||||
|
||||
return False
|
||||
|
||||
def _replaceQuotes(self, sQuote, oQuote, cQuote):
|
||||
"""Replace all straight quotes in the selected text.
|
||||
"""
|
||||
@@ -1709,7 +1768,9 @@ class BackgroundWordCounter(QRunnable):
|
||||
## END Class BackgroundWordCounter
|
||||
|
||||
class BackgroundWordCounterSignals(QObject):
|
||||
|
||||
"""The QRunnable cannot emit a signal, so we need a simple QObject
|
||||
to hold the word counter signal.
|
||||
"""
|
||||
countsReady = pyqtSignal(int, int, int)
|
||||
|
||||
# END Class BackgroundWordCounterSignals
|
||||
@@ -1946,6 +2007,7 @@ class GuiDocEditSearch(QFrame):
|
||||
if theText is not None:
|
||||
self.searchBox.setText(theText)
|
||||
self.searchBox.setFocus()
|
||||
self.searchBox.selectAll()
|
||||
if self.isRegEx:
|
||||
self._alertSearchValid(True)
|
||||
logger.verbose("Setting search text to '%s'" % theText)
|
||||
@@ -2468,8 +2530,11 @@ class GuiDocEditFooter(QWidget):
|
||||
else:
|
||||
iStatus = self.theProject.importItems.checkEntry(iStatus)
|
||||
theIcon = self.theParent.importIcons[iStatus]
|
||||
|
||||
sIcon = theIcon.pixmap(self.sPx, self.sPx)
|
||||
sText = self.theItem.itemStatus
|
||||
sClass = nwLabels.CLASS_NAME[self.theItem.itemClass]
|
||||
sLayout = nwLabels.LAYOUT_NAME[self.theItem.itemLayout]
|
||||
sText = f"{self.theItem.itemStatus} / {sClass} / {sLayout}"
|
||||
|
||||
self.statusIcon.setPixmap(sIcon)
|
||||
self.statusText.setText(sText)
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI Document Highlighter
|
||||
"""
|
||||
novelWriter – GUI Syntax Highlighter
|
||||
====================================
|
||||
Class for the main document editor syntax highlighter
|
||||
|
||||
novelWriter – GUI Document Highlighter
|
||||
========================================
|
||||
Subclass for the main editor syntax highlighting
|
||||
File History:
|
||||
Created: 2019-04-06 [0.0.1]
|
||||
|
||||
File History:
|
||||
Created: 2019-04-06 [0.0.1]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
@@ -148,22 +147,29 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
|
||||
# Quoted Strings
|
||||
if self.mainConf.highlightQuotes:
|
||||
fmtDO = self.mainConf.fmtDoubleQuotes[0]
|
||||
fmtDC = self.mainConf.fmtDoubleQuotes[1]
|
||||
fmtSO = self.mainConf.fmtSingleQuotes[0]
|
||||
fmtSC = self.mainConf.fmtSingleQuotes[1]
|
||||
fmtDbl = self.mainConf.fmtDoubleQuotes
|
||||
fmtSng = self.mainConf.fmtSingleQuotes
|
||||
|
||||
# Straight Quotes
|
||||
if fmtDbl != ["\"", "\""]:
|
||||
self.hRules.append((
|
||||
"(\\B\")(.*?)(\"\\B)", {
|
||||
0 : self.hStyles["dialogue1"],
|
||||
}
|
||||
))
|
||||
|
||||
# Double Quotes
|
||||
dblEnd = "|$" if self.mainConf.allowOpenDQuote else ""
|
||||
self.hRules.append((
|
||||
"\\B\"(.*?)\"\\B", {
|
||||
0 : self.hStyles["dialogue1"],
|
||||
}
|
||||
))
|
||||
self.hRules.append((
|
||||
f"\\B{fmtDO:s}(.*?){fmtDC:s}\\B", {
|
||||
f"(\\B{fmtDbl[0]})(.*?)({fmtDbl[1]}\\B{dblEnd})", {
|
||||
0 : self.hStyles["dialogue2"],
|
||||
}
|
||||
))
|
||||
|
||||
# Single Quotes
|
||||
sngEnd = "|$" if self.mainConf.allowOpenSQuote else ""
|
||||
self.hRules.append((
|
||||
f"\\B{fmtSO:s}(.*?){fmtSC:s}\\B", {
|
||||
f"(\\B{fmtSng[0]})(.*?)({fmtSng[1]}\\B{sngEnd})", {
|
||||
0 : self.hStyles["dialogue3"],
|
||||
}
|
||||
))
|
||||
@@ -391,7 +397,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
theFormat.setBackground(QBrush(fmtCol, Qt.SolidPattern))
|
||||
|
||||
if fmtSize is not None:
|
||||
theFormat.setFontPointSize(round(fmtSize*self.mainConf.textSize))
|
||||
theFormat.setFontPointSize(int(round(fmtSize*self.mainConf.textSize)))
|
||||
|
||||
return theFormat
|
||||
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI Doc Merge
|
||||
"""
|
||||
novelWriter – GUI Doc Merge Tool
|
||||
================================
|
||||
GUI class for merging multiple documents to one document
|
||||
|
||||
novelWriter – GUI Doc Merge
|
||||
=============================
|
||||
Tool for merging multiple documents to one document
|
||||
File History:
|
||||
Created: 2020-01-23 [0.4.3]
|
||||
|
||||
File History:
|
||||
Created: 2020-01-23 [0.4.3]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI Doc Split
|
||||
"""
|
||||
novelWriter – GUI Doc Split Tool
|
||||
================================
|
||||
GUI class for splitting a single document into multiple documents
|
||||
|
||||
novelWriter – GUI Doc Split
|
||||
=============================
|
||||
Tool for splitting a single document into multiple documents
|
||||
File History:
|
||||
Created: 2020-02-01 [0.4.3]
|
||||
|
||||
File History:
|
||||
Created: 2020-02-01 [0.4.3]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
|
||||
@@ -1,32 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI Document Viewer
|
||||
"""
|
||||
novelWriter – GUI Document Viewer
|
||||
=================================
|
||||
GUI classes for the main document viewer
|
||||
|
||||
novelWriter – GUI Document Viewer
|
||||
===================================
|
||||
Class holding the main document viewer
|
||||
File History:
|
||||
Created: 2019-05-10 [0.0.1] GuiDocViewer
|
||||
Created: 2019-10-31 [0.3.2] GuiDocViewDetails
|
||||
Created: 2020-04-25 [0.4.5] GuiDocViewHeader
|
||||
Created: 2020-06-09 [0.8.0] GuiDocViewFooter
|
||||
Created: 2020-09-08 [1.0b1] GuiDocViewHistory
|
||||
|
||||
File History:
|
||||
Created: 2019-05-10 [0.0.1] GuiDocViewer
|
||||
Created: 2019-10-31 [0.3.2] GuiDocViewDetails
|
||||
Created: 2020-04-25 [0.4.5] GuiDocViewHeader
|
||||
Created: 2020-06-09 [0.8.0] GuiDocViewFooter
|
||||
Created: 2020-09-08 [1.0b1] GuiDocViewHistory
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
@@ -177,13 +176,13 @@ class GuiDocViewer(QTextBrowser):
|
||||
# See issue #298
|
||||
try:
|
||||
aDoc.setText(tHandle)
|
||||
aDoc.doAutoReplace()
|
||||
aDoc.doPreProcessing()
|
||||
aDoc.tokenizeText()
|
||||
aDoc.doConvert()
|
||||
aDoc.doPostProcessing()
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.error("Failed to generate preview for document with handle '%s'" % tHandle)
|
||||
logger.error(str(e))
|
||||
nw.logException()
|
||||
self.setText("An error occurred while generating the preview.")
|
||||
return False
|
||||
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI Document Details
|
||||
|
||||
novelWriter – GUI Document Details
|
||||
"""
|
||||
novelWriter – GUI Item Details Panel
|
||||
====================================
|
||||
Class holding the project tree item details panel
|
||||
GUI class for the project tree item details panel
|
||||
|
||||
File History:
|
||||
Created: 2019-04-24 [0.0.1]
|
||||
File History:
|
||||
Created: 2019-04-24 [0.0.1]
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
This program is 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.
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
@@ -50,24 +49,22 @@ class GuiItemDetails(QWidget):
|
||||
self.theTheme = theParent.theTheme
|
||||
self.theHandle = None
|
||||
|
||||
self.mainBox = QGridLayout(self)
|
||||
self.mainBox.setVerticalSpacing(1)
|
||||
self.mainBox.setHorizontalSpacing(6)
|
||||
self.setLayout(self.mainBox)
|
||||
# Sizes
|
||||
hSp = self.mainConf.pxInt(6)
|
||||
vSp = self.mainConf.pxInt(1)
|
||||
mPx = self.mainConf.pxInt(6)
|
||||
iPx = self.theTheme.baseIconSize
|
||||
fPt = self.theTheme.fontPointSize
|
||||
|
||||
self.pS = 0.9*self.theTheme.fontPointSize
|
||||
self.iPx = self.theTheme.baseIconSize
|
||||
self.sPx = int(round(0.8*self.theTheme.baseIconSize))
|
||||
|
||||
self.expCheck = self.theTheme.getPixmap("check", (self.iPx, self.iPx))
|
||||
self.expCross = self.theTheme.getPixmap("cross", (self.iPx, self.iPx))
|
||||
self.expCheck = self.theTheme.getPixmap("check", (iPx, iPx))
|
||||
self.expCross = self.theTheme.getPixmap("cross", (iPx, iPx))
|
||||
|
||||
self.fntLabel = QFont()
|
||||
self.fntLabel.setBold(True)
|
||||
self.fntLabel.setPointSizeF(self.pS)
|
||||
self.fntLabel.setPointSizeF(0.9*fPt)
|
||||
|
||||
self.fntValue = QFont()
|
||||
self.fntValue.setPointSizeF(self.pS)
|
||||
self.fntValue.setPointSizeF(0.9*fPt)
|
||||
|
||||
# Label
|
||||
self.labelName = QLabel("Label")
|
||||
@@ -148,6 +145,7 @@ class GuiItemDetails(QWidget):
|
||||
self.pCountData.setAlignment(Qt.AlignRight)
|
||||
|
||||
# Assemble
|
||||
self.mainBox = QGridLayout(self)
|
||||
self.mainBox.addWidget(self.labelName, 0, 0, 1, 1)
|
||||
self.mainBox.addWidget(self.labelFlag, 0, 1, 1, 1)
|
||||
self.mainBox.addWidget(self.labelData, 0, 2, 1, 3)
|
||||
@@ -176,6 +174,12 @@ class GuiItemDetails(QWidget):
|
||||
self.mainBox.setColumnStretch(3, 0)
|
||||
self.mainBox.setColumnStretch(4, 0)
|
||||
|
||||
self.mainBox.setHorizontalSpacing(hSp)
|
||||
self.mainBox.setVerticalSpacing(vSp)
|
||||
self.mainBox.setContentsMargins(mPx, mPx, mPx, mPx)
|
||||
|
||||
self.setLayout(self.mainBox)
|
||||
|
||||
# Make sure the columns for flags and counts don't resize too often
|
||||
flagWidth = self.theTheme.getTextWidth("Mm", self.fntValue)
|
||||
countWidth = self.theTheme.getTextWidth("99,999", self.fntValue)
|
||||
@@ -190,76 +194,92 @@ class GuiItemDetails(QWidget):
|
||||
# Class Methods
|
||||
##
|
||||
|
||||
def clearDetails(self):
|
||||
"""Clear all the data values.
|
||||
"""
|
||||
self.labelFlag.setPixmap(QPixmap(1, 1))
|
||||
self.statusFlag.setPixmap(QPixmap(1, 1))
|
||||
self.classFlag.setText("")
|
||||
self.layoutFlag.setText("")
|
||||
|
||||
self.labelData.setText("–")
|
||||
self.statusData.setText("–")
|
||||
self.classData.setText("–")
|
||||
self.layoutData.setText("–")
|
||||
|
||||
self.cCountData.setText("–")
|
||||
self.wCountData.setText("–")
|
||||
self.pCountData.setText("–")
|
||||
|
||||
return
|
||||
|
||||
def updateCounts(self, tHandle, cC, wC, pC):
|
||||
"""Just update the counts if the handle is the same as the one
|
||||
we're already showing.
|
||||
"""Update the counts if the handle is the same as the one we're
|
||||
already showing. Otherwise, do nothing.
|
||||
"""
|
||||
if tHandle == self.theHandle:
|
||||
self.cCountData.setText(f"{cC:n}")
|
||||
self.wCountData.setText(f"{wC:n}")
|
||||
self.pCountData.setText(f"{pC:n}")
|
||||
|
||||
return
|
||||
|
||||
def updateViewBox(self, tHandle):
|
||||
"""Populate the details box from a given handle.
|
||||
"""
|
||||
self.theHandle = tHandle
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
if tHandle is None:
|
||||
self.clearDetails()
|
||||
return
|
||||
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
if nwItem is None:
|
||||
self.labelFlag.setText("")
|
||||
self.labelData.setText("")
|
||||
self.statusFlag.setText("")
|
||||
self.statusData.setText("")
|
||||
self.classFlag.setText("")
|
||||
self.classData.setText("")
|
||||
self.layoutFlag.setText("")
|
||||
self.layoutData.setText("")
|
||||
self.clearDetails()
|
||||
return
|
||||
|
||||
self.theHandle = tHandle
|
||||
theLabel = nwItem.itemName
|
||||
if len(theLabel) > 100:
|
||||
theLabel = theLabel[:96].rstrip()+" ..."
|
||||
|
||||
itStatus = nwItem.itemStatus
|
||||
if nwItem.itemClass == nwItemClass.NOVEL:
|
||||
itStatus = self.theProject.statusItems.checkEntry(itStatus) # Make sure it's valid
|
||||
flagIcon = self.theParent.statusIcons[itStatus]
|
||||
else:
|
||||
itStatus = self.theProject.importItems.checkEntry(itStatus) # Make sure it's valid
|
||||
flagIcon = self.theParent.importIcons[itStatus]
|
||||
|
||||
if nwItem.itemType == nwItemType.FILE:
|
||||
if nwItem.isExported:
|
||||
self.labelFlag.setPixmap(self.expCheck)
|
||||
else:
|
||||
self.labelFlag.setPixmap(self.expCross)
|
||||
else:
|
||||
self.labelFlag.setPixmap(QPixmap(1, 1))
|
||||
|
||||
iPx = int(round(0.8*self.theTheme.baseIconSize))
|
||||
self.statusFlag.setPixmap(flagIcon.pixmap(iPx, iPx))
|
||||
self.classFlag.setText(nwLabels.CLASS_FLAG[nwItem.itemClass])
|
||||
|
||||
if nwItem.itemLayout == nwItemLayout.NO_LAYOUT:
|
||||
self.layoutFlag.setText("-")
|
||||
else:
|
||||
self.layoutFlag.setText(nwLabels.LAYOUT_FLAG[nwItem.itemLayout])
|
||||
|
||||
self.labelData.setText(theLabel)
|
||||
self.statusData.setText(nwItem.itemStatus)
|
||||
self.classData.setText(nwLabels.CLASS_NAME[nwItem.itemClass])
|
||||
self.layoutData.setText(nwLabels.LAYOUT_NAME[nwItem.itemLayout])
|
||||
|
||||
if nwItem.itemType == nwItemType.FILE:
|
||||
self.cCountData.setText(f"{nwItem.charCount:n}")
|
||||
self.wCountData.setText(f"{nwItem.wordCount:n}")
|
||||
self.pCountData.setText(f"{nwItem.paraCount:n}")
|
||||
else:
|
||||
self.cCountData.setText("–")
|
||||
self.wCountData.setText("–")
|
||||
self.pCountData.setText("–")
|
||||
|
||||
else:
|
||||
theLabel = nwItem.itemName
|
||||
if len(theLabel) > 100:
|
||||
theLabel = theLabel[:96].rstrip()+" ..."
|
||||
|
||||
iStatus = nwItem.itemStatus
|
||||
if nwItem.itemClass == nwItemClass.NOVEL:
|
||||
iStatus = self.theProject.statusItems.checkEntry(iStatus) # Make sure it's valid
|
||||
flagIcon = self.theParent.statusIcons[iStatus]
|
||||
else:
|
||||
iStatus = self.theProject.importItems.checkEntry(iStatus) # Make sure it's valid
|
||||
flagIcon = self.theParent.importIcons[iStatus]
|
||||
|
||||
if nwItem.itemType == nwItemType.FILE:
|
||||
if nwItem.isExported:
|
||||
self.labelFlag.setPixmap(self.expCheck)
|
||||
else:
|
||||
self.labelFlag.setPixmap(self.expCross)
|
||||
else:
|
||||
self.labelFlag.setPixmap(QPixmap(1, 1))
|
||||
self.statusFlag.setPixmap(flagIcon.pixmap(self.sPx, self.sPx))
|
||||
self.classFlag.setText(nwLabels.CLASS_FLAG[nwItem.itemClass])
|
||||
if nwItem.itemLayout == nwItemLayout.NO_LAYOUT:
|
||||
self.layoutFlag.setText("-")
|
||||
else:
|
||||
self.layoutFlag.setText(nwLabels.LAYOUT_FLAG[nwItem.itemLayout])
|
||||
|
||||
self.labelData.setText(theLabel)
|
||||
self.statusData.setText(nwItem.itemStatus)
|
||||
self.classData.setText(nwLabels.CLASS_NAME[nwItem.itemClass])
|
||||
self.layoutData.setText(nwLabels.LAYOUT_NAME[nwItem.itemLayout])
|
||||
|
||||
if nwItem.itemType == nwItemType.FILE:
|
||||
self.cCountData.setText(f"{nwItem.charCount:n}")
|
||||
self.wCountData.setText(f"{nwItem.wordCount:n}")
|
||||
self.pCountData.setText(f"{nwItem.paraCount:n}")
|
||||
else:
|
||||
self.cCountData.setText("–")
|
||||
self.wCountData.setText("–")
|
||||
self.pCountData.setText("–")
|
||||
|
||||
return
|
||||
|
||||
# END Class GuiItemDetails
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI Item Editor
|
||||
"""
|
||||
novelWriter – GUI Item Editor
|
||||
=============================
|
||||
GUI class for the item editor dialog
|
||||
|
||||
novelWriter – GUI Item Editor
|
||||
===============================
|
||||
Class holding the item editor dialog
|
||||
File History:
|
||||
Created: 2019-04-27 [0.0.1]
|
||||
|
||||
File History:
|
||||
Created: 2019-04-27 [0.0.1]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
import logging
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QGridLayout, QLineEdit, QComboBox, QLabel,
|
||||
QDialogButtonBox
|
||||
)
|
||||
|
||||
from nw.gui.custom import QSwitch
|
||||
from nw.constants import nwLabels, nwItemLayout, nwItemClass, nwItemType
|
||||
from nw.constants import nwLabels, nwItemLayout, nwItemType, nwLists
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -50,23 +50,29 @@ class GuiItemEditor(QDialog):
|
||||
self.theProject = theProject
|
||||
self.theParent = theParent
|
||||
|
||||
self.outerBox = QVBoxLayout()
|
||||
##
|
||||
# Build GUI
|
||||
##
|
||||
|
||||
self.theItem = self.theProject.projTree[tHandle]
|
||||
if self.theItem is None:
|
||||
self._doClose()
|
||||
|
||||
self.setWindowTitle("Item Settings")
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
mVd = self.mainConf.pxInt(220)
|
||||
mSp = self.mainConf.pxInt(16)
|
||||
vSp = self.mainConf.pxInt(4)
|
||||
|
||||
# Item Label
|
||||
self.editName = QLineEdit()
|
||||
self.editName.setMinimumWidth(self.mainConf.pxInt(220))
|
||||
self.editName.setMaxLength(self.mainConf.pxInt(200))
|
||||
self.editName.setMinimumWidth(mVd)
|
||||
self.editName.setMaxLength(200)
|
||||
|
||||
# Item Status
|
||||
self.editStatus = QComboBox()
|
||||
if self.theItem.itemClass == nwItemClass.NOVEL:
|
||||
self.editStatus.setMinimumWidth(mVd)
|
||||
if self.theItem.itemClass in nwLists.CLS_NOVEL:
|
||||
for sLabel, _, _ in self.theProject.statusItems:
|
||||
self.editStatus.addItem(
|
||||
self.theParent.statusIcons[sLabel], sLabel, sLabel
|
||||
@@ -79,24 +85,24 @@ class GuiItemEditor(QDialog):
|
||||
|
||||
# Item Layout
|
||||
self.editLayout = QComboBox()
|
||||
self.validLayouts = []
|
||||
self.editLayout.setMinimumWidth(mVd)
|
||||
validLayouts = []
|
||||
if self.theItem.itemType == nwItemType.FILE:
|
||||
if self.theItem.itemClass == nwItemClass.NOVEL:
|
||||
self.validLayouts.append(nwItemLayout.TITLE)
|
||||
self.validLayouts.append(nwItemLayout.BOOK)
|
||||
self.validLayouts.append(nwItemLayout.PAGE)
|
||||
self.validLayouts.append(nwItemLayout.PARTITION)
|
||||
self.validLayouts.append(nwItemLayout.UNNUMBERED)
|
||||
self.validLayouts.append(nwItemLayout.CHAPTER)
|
||||
self.validLayouts.append(nwItemLayout.SCENE)
|
||||
self.validLayouts.append(nwItemLayout.NOTE)
|
||||
else:
|
||||
self.validLayouts.append(nwItemLayout.NOTE)
|
||||
if self.theItem.itemClass in nwLists.CLS_NOVEL:
|
||||
validLayouts.append(nwItemLayout.TITLE)
|
||||
validLayouts.append(nwItemLayout.BOOK)
|
||||
validLayouts.append(nwItemLayout.PAGE)
|
||||
validLayouts.append(nwItemLayout.PARTITION)
|
||||
validLayouts.append(nwItemLayout.UNNUMBERED)
|
||||
validLayouts.append(nwItemLayout.CHAPTER)
|
||||
validLayouts.append(nwItemLayout.SCENE)
|
||||
validLayouts.append(nwItemLayout.NOTE)
|
||||
else:
|
||||
self.validLayouts.append(nwItemLayout.NO_LAYOUT)
|
||||
validLayouts.append(nwItemLayout.NO_LAYOUT)
|
||||
self.editLayout.setEnabled(False)
|
||||
|
||||
for itemLayout in nwItemLayout:
|
||||
if itemLayout in self.validLayouts:
|
||||
if itemLayout in validLayouts:
|
||||
self.editLayout.addItem(nwLabels.LAYOUT_NAME[itemLayout], itemLayout)
|
||||
|
||||
# Export Switch
|
||||
@@ -109,23 +115,30 @@ class GuiItemEditor(QDialog):
|
||||
self.editExport.setEnabled(False)
|
||||
self.editExport.setChecked(False)
|
||||
|
||||
self.editName.setText(self.theItem.itemName)
|
||||
statusIdx = self.editStatus.findData(self.theItem.itemStatus)
|
||||
if statusIdx != -1:
|
||||
self.editStatus.setCurrentIndex(statusIdx)
|
||||
layoutIdx = self.editLayout.findData(self.theItem.itemLayout)
|
||||
if layoutIdx != -1:
|
||||
self.editLayout.setCurrentIndex(layoutIdx)
|
||||
|
||||
# Buttons
|
||||
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
self.buttonBox.accepted.connect(self._doSave)
|
||||
self.buttonBox.rejected.connect(self._doClose)
|
||||
|
||||
# Assemble
|
||||
# Set Current Values
|
||||
self.editName.setText(self.theItem.itemName)
|
||||
self.editName.selectAll()
|
||||
|
||||
statusIdx = self.editStatus.findData(self.theItem.itemStatus)
|
||||
if statusIdx != -1:
|
||||
self.editStatus.setCurrentIndex(statusIdx)
|
||||
|
||||
layoutIdx = self.editLayout.findData(self.theItem.itemLayout)
|
||||
if layoutIdx != -1:
|
||||
self.editLayout.setCurrentIndex(layoutIdx)
|
||||
|
||||
##
|
||||
# Assemble
|
||||
##
|
||||
|
||||
self.mainForm = QGridLayout()
|
||||
self.mainForm.setVerticalSpacing(self.mainConf.pxInt(4))
|
||||
self.mainForm.setHorizontalSpacing(self.mainConf.pxInt(16))
|
||||
self.mainForm.setVerticalSpacing(vSp)
|
||||
self.mainForm.setHorizontalSpacing(mSp)
|
||||
self.mainForm.addWidget(QLabel("Label"), 0, 0, 1, 1)
|
||||
self.mainForm.addWidget(self.editName, 0, 1, 1, 2)
|
||||
self.mainForm.addWidget(QLabel("Status"), 1, 0, 1, 1)
|
||||
@@ -134,20 +147,28 @@ class GuiItemEditor(QDialog):
|
||||
self.mainForm.addWidget(self.editLayout, 2, 1, 1, 2)
|
||||
self.mainForm.addWidget(self.textExport, 3, 0, 1, 2)
|
||||
self.mainForm.addWidget(self.editExport, 3, 2, 1, 1)
|
||||
self.mainForm.setColumnStretch(0, 0)
|
||||
self.mainForm.setColumnStretch(1, 1)
|
||||
self.mainForm.setColumnStretch(2, 0)
|
||||
|
||||
self.outerBox.setSpacing(self.mainConf.pxInt(16))
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.outerBox.setSpacing(mSp)
|
||||
self.outerBox.addLayout(self.mainForm)
|
||||
self.outerBox.addStretch(1)
|
||||
self.outerBox.addWidget(self.buttonBox)
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
self.rejected.connect(self._doClose)
|
||||
self.editName.selectAll()
|
||||
|
||||
logger.debug("GuiItemEditor initialisation complete")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Slots
|
||||
##
|
||||
|
||||
@pyqtSlot()
|
||||
def _doSave(self):
|
||||
"""Save the setting to the item.
|
||||
"""
|
||||
@@ -170,10 +191,11 @@ class GuiItemEditor(QDialog):
|
||||
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _doClose(self):
|
||||
"""Close the dialog without saving the settings.
|
||||
"""
|
||||
logger.verbose("ItemEditor close button clicked")
|
||||
logger.verbose("ItemEditor cancel button clicked")
|
||||
self.close()
|
||||
return
|
||||
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI Main Menu
|
||||
"""
|
||||
novelWriter – GUI Main Menu
|
||||
===========================
|
||||
GUI class for the main window menu
|
||||
|
||||
novelWriter – GUI Main Menu
|
||||
=============================
|
||||
Class holding the main window menu
|
||||
File History:
|
||||
Created: 2019-04-27 [0.0.1]
|
||||
|
||||
File History:
|
||||
Created: 2019-04-27 [0.0.1]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
@@ -33,7 +32,8 @@ from PyQt5.QtGui import QDesktopServices
|
||||
from PyQt5.QtWidgets import QMenuBar, QAction
|
||||
|
||||
from nw.constants import (
|
||||
nwItemType, nwItemClass, nwDocAction, nwDocInsert, nwKeyWords, nwLabels
|
||||
nwItemType, nwItemClass, nwDocAction, nwDocInsert, nwKeyWords, nwLabels,
|
||||
nwUnicode
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -115,22 +115,25 @@ class GuiMainMenu(QMenuBar):
|
||||
##
|
||||
|
||||
def setSpellCheck(self, theMode):
|
||||
"""Set the spell check check box to theMode. This is controlled
|
||||
by the document editor class, which holds the master spell check
|
||||
flag.
|
||||
"""Forward spell check check state to its action.
|
||||
"""
|
||||
self.aSpellCheck.setChecked(theMode)
|
||||
return
|
||||
|
||||
def setAutoOutline(self, theMode):
|
||||
"""Set the auto outline check box to theMode. Used during
|
||||
initialisation.
|
||||
"""Forward auto outline check state to its action.
|
||||
"""
|
||||
self.aAutoOutline.setChecked(theMode)
|
||||
return
|
||||
|
||||
def setFocusMode(self, theMode):
|
||||
"""Forward focus mode check state to its action.
|
||||
"""
|
||||
self.aFocusMode.setChecked(theMode)
|
||||
return
|
||||
|
||||
##
|
||||
# Menu Action
|
||||
# Slots
|
||||
##
|
||||
|
||||
def _toggleSpellCheck(self, isChecked=False):
|
||||
@@ -164,17 +167,11 @@ class GuiMainMenu(QMenuBar):
|
||||
return True
|
||||
|
||||
def _openWebsite(self, theUrl):
|
||||
"""Open an URL in the system's default browser.
|
||||
"""Open a URL in the system's default browser.
|
||||
"""
|
||||
QDesktopServices.openUrl(QUrl(theUrl))
|
||||
return True
|
||||
|
||||
def _openIssue(self):
|
||||
"""Open the issue tracker URL in the system's default browser.
|
||||
"""
|
||||
QDesktopServices.openUrl(QUrl(nw.__issuesurl__))
|
||||
return True
|
||||
|
||||
##
|
||||
# Menu Builders
|
||||
##
|
||||
@@ -212,6 +209,9 @@ class GuiMainMenu(QMenuBar):
|
||||
self.aCloseProject.triggered.connect(lambda: self.theParent.closeProject(False))
|
||||
self.projMenu.addAction(self.aCloseProject)
|
||||
|
||||
# Project > Separator
|
||||
self.projMenu.addSeparator()
|
||||
|
||||
# Project > Project Settings
|
||||
self.aProjectSettings = QAction("Project Settings", self)
|
||||
self.aProjectSettings.setStatusTip("Project settings")
|
||||
@@ -219,6 +219,13 @@ class GuiMainMenu(QMenuBar):
|
||||
self.aProjectSettings.triggered.connect(lambda: self.theParent.showProjectSettingsDialog())
|
||||
self.projMenu.addAction(self.aProjectSettings)
|
||||
|
||||
# Project > Project Details
|
||||
self.aProjectDetails = QAction("Project Details", self)
|
||||
self.aProjectDetails.setStatusTip("Project details")
|
||||
self.aProjectDetails.setShortcut("Shift+F6")
|
||||
self.aProjectDetails.triggered.connect(lambda: self.theParent.showProjectDetailsDialog())
|
||||
self.projMenu.addAction(self.aProjectDetails)
|
||||
|
||||
# Project > Separator
|
||||
self.projMenu.addSeparator()
|
||||
|
||||
@@ -253,19 +260,40 @@ class GuiMainMenu(QMenuBar):
|
||||
self.projMenu.addSeparator()
|
||||
|
||||
# Project > Edit
|
||||
self.aEditItem = QAction("Edit Project Item", self)
|
||||
self.aEditItem.setStatusTip("Change item settings")
|
||||
self.aEditItem = QAction("Edit Item", self)
|
||||
self.aEditItem.setStatusTip("Change project item settings")
|
||||
self.aEditItem.setShortcuts(["Ctrl+E", "F2"])
|
||||
self.aEditItem.triggered.connect(lambda: self.theParent.editItem(None))
|
||||
self.projMenu.addAction(self.aEditItem)
|
||||
|
||||
# Project > Delete
|
||||
self.aDeleteItem = QAction("Delete Project Item", self)
|
||||
self.aDeleteItem.setStatusTip("Delete selected item")
|
||||
self.aDeleteItem.setShortcut("Ctrl+Del")
|
||||
self.aDeleteItem = QAction("Delete Item", self)
|
||||
self.aDeleteItem.setStatusTip("Delete selected project item")
|
||||
self.aDeleteItem.setShortcut("Ctrl+Shift+Del")
|
||||
self.aDeleteItem.triggered.connect(lambda: self.theParent.treeView.deleteItem(None))
|
||||
self.projMenu.addAction(self.aDeleteItem)
|
||||
|
||||
# Project > Move Up
|
||||
self.aMoveUp = QAction("Move Item Up", self)
|
||||
self.aMoveUp.setStatusTip("Move project item up")
|
||||
self.aMoveUp.setShortcut("Ctrl+Up")
|
||||
self.aMoveUp.triggered.connect(lambda: self._moveTreeItem(-1))
|
||||
self.projMenu.addAction(self.aMoveUp)
|
||||
|
||||
# Project > Move Down
|
||||
self.aMoveDown = QAction("Move Item Down", self)
|
||||
self.aMoveDown.setStatusTip("Move project item down")
|
||||
self.aMoveDown.setShortcut("Ctrl+Down")
|
||||
self.aMoveDown.triggered.connect(lambda: self._moveTreeItem(1))
|
||||
self.projMenu.addAction(self.aMoveDown)
|
||||
|
||||
# Project > Undo Last Action
|
||||
self.aMoveUndo = QAction("Undo Last Move", self)
|
||||
self.aMoveUndo.setStatusTip("Undo last item move")
|
||||
self.aMoveUndo.setShortcut("Ctrl+Shift+Z")
|
||||
self.aMoveUndo.triggered.connect(lambda: self.theParent.treeView.undoLastMove())
|
||||
self.projMenu.addAction(self.aMoveUndo)
|
||||
|
||||
# Project > Empty Trash
|
||||
self.aEmptyTrash = QAction("Empty Trash", self)
|
||||
self.aEmptyTrash.setStatusTip("Permanently delete all files in the Trash folder")
|
||||
@@ -458,6 +486,13 @@ class GuiMainMenu(QMenuBar):
|
||||
self.aFocusView.triggered.connect(lambda: self.theParent.setFocus(3))
|
||||
self.viewMenu.addAction(self.aFocusView)
|
||||
|
||||
# View > Outline
|
||||
self.aFocusOutline = QAction("Focus Outline", self)
|
||||
self.aFocusOutline.setStatusTip("Move focus to outline")
|
||||
self.aFocusOutline.setShortcut("Alt+4")
|
||||
self.aFocusOutline.triggered.connect(lambda: self.theParent.setFocus(4))
|
||||
self.viewMenu.addAction(self.aFocusOutline)
|
||||
|
||||
# View > Separator
|
||||
self.viewMenu.addSeparator()
|
||||
|
||||
@@ -478,9 +513,9 @@ class GuiMainMenu(QMenuBar):
|
||||
# View > Separator
|
||||
self.viewMenu.addSeparator()
|
||||
|
||||
# View > Toggle Distraction Free Mode
|
||||
self.aFocusMode = QAction("Distraction Free Mode", self)
|
||||
self.aFocusMode.setStatusTip("Toggles distraction free mode, only showing text editor")
|
||||
# View > Focus Mode
|
||||
self.aFocusMode = QAction("Focus Mode", self)
|
||||
self.aFocusMode.setStatusTip("Toggles a distraction free mode, only showing text editor")
|
||||
self.aFocusMode.setShortcut("F8")
|
||||
self.aFocusMode.setCheckable(True)
|
||||
self.aFocusMode.setChecked(self.theParent.isFocusMode)
|
||||
@@ -502,29 +537,36 @@ class GuiMainMenu(QMenuBar):
|
||||
# Insert
|
||||
self.insertMenu = self.addMenu("&Insert")
|
||||
|
||||
# Insert > Dashes and Dots
|
||||
self.mInsDashes = self.insertMenu.addMenu("Dashes and Dots")
|
||||
# Insert > Dashes
|
||||
self.mInsDashes = self.insertMenu.addMenu("Dashes")
|
||||
|
||||
# Insert > Short Dash
|
||||
self.aInsENDash = QAction("Short Dash", self)
|
||||
self.aInsENDash.setStatusTip("Insert short dash")
|
||||
self.aInsENDash.setStatusTip("Insert short dash (en dash)")
|
||||
self.aInsENDash.setShortcut("Ctrl+K, -")
|
||||
self.aInsENDash.triggered.connect(lambda: self._docInsert(nwDocInsert.SHORT_DASH))
|
||||
self.aInsENDash.triggered.connect(lambda: self._docInsert(nwUnicode.U_ENDASH))
|
||||
self.mInsDashes.addAction(self.aInsENDash)
|
||||
|
||||
# Insert > Long Dash
|
||||
self.aInsEMDash = QAction("Long Dash", self)
|
||||
self.aInsEMDash.setStatusTip("Insert long dash")
|
||||
self.aInsEMDash.setStatusTip("Insert long dash (em dash)")
|
||||
self.aInsEMDash.setShortcut("Ctrl+K, _")
|
||||
self.aInsEMDash.triggered.connect(lambda: self._docInsert(nwDocInsert.LONG_DASH))
|
||||
self.aInsEMDash.triggered.connect(lambda: self._docInsert(nwUnicode.U_EMDASH))
|
||||
self.mInsDashes.addAction(self.aInsEMDash)
|
||||
|
||||
# Insert > Ellipsis
|
||||
self.aInsEllipsis = QAction("Ellipsis", self)
|
||||
self.aInsEllipsis.setStatusTip("Insert ellipsis")
|
||||
self.aInsEllipsis.setShortcut("Ctrl+K, .")
|
||||
self.aInsEllipsis.triggered.connect(lambda: self._docInsert(nwDocInsert.ELLIPSIS))
|
||||
self.mInsDashes.addAction(self.aInsEllipsis)
|
||||
# Insert > Long Dash
|
||||
self.aInsHorBar = QAction("Horizontal Bar", self)
|
||||
self.aInsHorBar.setStatusTip("Insert a horizontal bar (quotation dash)")
|
||||
self.aInsHorBar.setShortcut("Ctrl+K, Ctrl+_")
|
||||
self.aInsHorBar.triggered.connect(lambda: self._docInsert(nwUnicode.U_HBAR))
|
||||
self.mInsDashes.addAction(self.aInsHorBar)
|
||||
|
||||
# Insert > Figure Dash
|
||||
self.aInsFigDash = QAction("Figure Dash", self)
|
||||
self.aInsFigDash.setStatusTip("Insert figure dash (same width as a number character)")
|
||||
self.aInsFigDash.setShortcut("Ctrl+K, ~")
|
||||
self.aInsFigDash.triggered.connect(lambda: self._docInsert(nwUnicode.U_FGDASH))
|
||||
self.mInsDashes.addAction(self.aInsFigDash)
|
||||
|
||||
# Insert > Quote Marks
|
||||
self.mInsQuotes = self.insertMenu.addMenu("Quote Marks")
|
||||
@@ -559,11 +601,35 @@ class GuiMainMenu(QMenuBar):
|
||||
|
||||
# Insert > Alternative Apostrophe
|
||||
self.aInsMSApos = QAction("Alternative Apostrophe", self)
|
||||
self.aInsMSApos.setStatusTip("Insert unicode modifier letter single apostrophe")
|
||||
self.aInsMSApos.setStatusTip("Insert modifier letter single apostrophe")
|
||||
self.aInsMSApos.setShortcut("Ctrl+K, '")
|
||||
self.aInsMSApos.triggered.connect(lambda: self._docInsert(nwDocInsert.MODAPOS_S))
|
||||
self.aInsMSApos.triggered.connect(lambda: self._docInsert(nwUnicode.U_MAPOSS))
|
||||
self.mInsQuotes.addAction(self.aInsMSApos)
|
||||
|
||||
# Insert > Symbols
|
||||
self.mInsPunct = self.insertMenu.addMenu("General Punctuation")
|
||||
|
||||
# Insert > Ellipsis
|
||||
self.aInsEllipsis = QAction("Ellipsis", self)
|
||||
self.aInsEllipsis.setStatusTip("Insert ellipsis")
|
||||
self.aInsEllipsis.setShortcut("Ctrl+K, .")
|
||||
self.aInsEllipsis.triggered.connect(lambda: self._docInsert(nwUnicode.U_HELLIP))
|
||||
self.mInsPunct.addAction(self.aInsEllipsis)
|
||||
|
||||
# Insert > Prime
|
||||
self.aInsPrime = QAction("Prime", self)
|
||||
self.aInsPrime.setStatusTip("Insert a prime symbol")
|
||||
self.aInsPrime.setShortcut("Ctrl+K, Ctrl+'")
|
||||
self.aInsPrime.triggered.connect(lambda: self._docInsert(nwUnicode.U_PRIME))
|
||||
self.mInsPunct.addAction(self.aInsPrime)
|
||||
|
||||
# Insert > Double Prime
|
||||
self.aInsDPrime = QAction("Double Prime", self)
|
||||
self.aInsDPrime.setStatusTip("Insert a double prime symbol")
|
||||
self.aInsDPrime.setShortcut("Ctrl+K, Ctrl+\"")
|
||||
self.aInsDPrime.triggered.connect(lambda: self._docInsert(nwUnicode.U_DPRIME))
|
||||
self.mInsPunct.addAction(self.aInsDPrime)
|
||||
|
||||
# Insert > Breaks and Spaces
|
||||
self.mInsBreaks = self.insertMenu.addMenu("Breaks and Spaces")
|
||||
|
||||
@@ -578,23 +644,82 @@ class GuiMainMenu(QMenuBar):
|
||||
self.aInsNBSpace = QAction("Non-Breaking Space", self)
|
||||
self.aInsNBSpace.setStatusTip("Insert a non-breaking space")
|
||||
self.aInsNBSpace.setShortcut("Ctrl+K, Space")
|
||||
self.aInsNBSpace.triggered.connect(lambda: self._docInsert(nwDocInsert.NB_SPACE))
|
||||
self.aInsNBSpace.triggered.connect(lambda: self._docInsert(nwUnicode.U_NBSP))
|
||||
self.mInsBreaks.addAction(self.aInsNBSpace)
|
||||
|
||||
# Insert > Thin Space
|
||||
self.aInsThinSpace = QAction("Thin Space", self)
|
||||
self.aInsThinSpace.setStatusTip("Insert a thin space")
|
||||
self.aInsThinSpace.setShortcut("Ctrl+K, Shift+Space")
|
||||
self.aInsThinSpace.triggered.connect(lambda: self._docInsert(nwDocInsert.THIN_SPACE))
|
||||
self.aInsThinSpace.triggered.connect(lambda: self._docInsert(nwUnicode.U_THSP))
|
||||
self.mInsBreaks.addAction(self.aInsThinSpace)
|
||||
|
||||
# Insert > Thin Non-Breaking Space
|
||||
self.aInsThinNBSpace = QAction("Thin Non-Breaking Space", self)
|
||||
self.aInsThinNBSpace.setStatusTip("Insert a thin non-breaking space")
|
||||
self.aInsThinNBSpace.setShortcut("Ctrl+K, Ctrl+Space")
|
||||
self.aInsThinNBSpace.triggered.connect(lambda: self._docInsert(nwDocInsert.THIN_NB_SPACE))
|
||||
self.aInsThinNBSpace.triggered.connect(lambda: self._docInsert(nwUnicode.U_THNBSP))
|
||||
self.mInsBreaks.addAction(self.aInsThinNBSpace)
|
||||
|
||||
# Insert > Symbols
|
||||
self.mInsSymbol = self.insertMenu.addMenu("Other Symbols")
|
||||
|
||||
# Insert > List Bullet
|
||||
self.aInsBullet = QAction("List Bullet", self)
|
||||
self.aInsBullet.setStatusTip("Insert a list bullet")
|
||||
self.aInsBullet.setShortcut("Ctrl+K, *")
|
||||
self.aInsBullet.triggered.connect(lambda: self._docInsert(nwUnicode.U_BULL))
|
||||
self.mInsSymbol.addAction(self.aInsBullet)
|
||||
|
||||
# Insert > Hyphen Bullet
|
||||
self.aInsHyBull = QAction("Hyphen Bullet", self)
|
||||
self.aInsHyBull.setStatusTip("Insert a hyphen bullet (alternative bullet)")
|
||||
self.aInsHyBull.setShortcut("Ctrl+K, Ctrl+-")
|
||||
self.aInsHyBull.triggered.connect(lambda: self._docInsert(nwUnicode.U_HYBULL))
|
||||
self.mInsSymbol.addAction(self.aInsHyBull)
|
||||
|
||||
# Insert > Flower Mark
|
||||
self.aInsFlower = QAction("Flower Mark", self)
|
||||
self.aInsFlower.setStatusTip("Insert a flower mark (alternative bullet)")
|
||||
self.aInsFlower.setShortcut("Ctrl+K, Ctrl+*")
|
||||
self.aInsFlower.triggered.connect(lambda: self._docInsert(nwUnicode.U_FLOWER))
|
||||
self.mInsSymbol.addAction(self.aInsFlower)
|
||||
|
||||
# Insert > Per Mille
|
||||
self.aInsPerMille = QAction("Per Mille", self)
|
||||
self.aInsPerMille.setStatusTip("Insert a per mille symbol")
|
||||
self.aInsPerMille.setShortcut("Ctrl+K, %")
|
||||
self.aInsPerMille.triggered.connect(lambda: self._docInsert(nwUnicode.U_PERMIL))
|
||||
self.mInsSymbol.addAction(self.aInsPerMille)
|
||||
|
||||
# Insert > Degree Symbol
|
||||
self.aInsDegree = QAction("Degree Symbol", self)
|
||||
self.aInsDegree.setStatusTip("Insert a degree symbol")
|
||||
self.aInsDegree.setShortcut("Ctrl+K, Ctrl+O")
|
||||
self.aInsDegree.triggered.connect(lambda: self._docInsert(nwUnicode.U_DEGREE))
|
||||
self.mInsSymbol.addAction(self.aInsDegree)
|
||||
|
||||
# Insert > Minus Sign
|
||||
self.aInsMinus = QAction("Minus Sign", self)
|
||||
self.aInsMinus.setStatusTip("Insert a minus sign (not a hypen or dash)")
|
||||
self.aInsMinus.setShortcut("Ctrl+K, Ctrl+M")
|
||||
self.aInsMinus.triggered.connect(lambda: self._docInsert(nwUnicode.U_MINUS))
|
||||
self.mInsSymbol.addAction(self.aInsMinus)
|
||||
|
||||
# Insert > Times Sign
|
||||
self.aInsTimes = QAction("Times Sign", self)
|
||||
self.aInsTimes.setStatusTip("Insert a times sign (multiplication cross)")
|
||||
self.aInsTimes.setShortcut("Ctrl+K, Ctrl+X")
|
||||
self.aInsTimes.triggered.connect(lambda: self._docInsert(nwUnicode.U_TIMES))
|
||||
self.mInsSymbol.addAction(self.aInsTimes)
|
||||
|
||||
# Insert > Division
|
||||
self.aInsDivide = QAction("Division Sign", self)
|
||||
self.aInsDivide.setStatusTip("Insert a division sign")
|
||||
self.aInsDivide.setShortcut("Ctrl+K, Ctrl+D")
|
||||
self.aInsDivide.triggered.connect(lambda: self._docInsert(nwUnicode.U_DIVIDE))
|
||||
self.mInsSymbol.addAction(self.aInsDivide)
|
||||
|
||||
# Insert > Separator
|
||||
self.insertMenu.addSeparator()
|
||||
|
||||
@@ -603,6 +728,7 @@ class GuiMainMenu(QMenuBar):
|
||||
self.mInsKWItems = {}
|
||||
self.mInsKWItems[nwKeyWords.TAG_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, G")
|
||||
self.mInsKWItems[nwKeyWords.POV_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, V")
|
||||
self.mInsKWItems[nwKeyWords.FOCUS_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, F")
|
||||
self.mInsKWItems[nwKeyWords.CHAR_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, C")
|
||||
self.mInsKWItems[nwKeyWords.PLOT_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, P")
|
||||
self.mInsKWItems[nwKeyWords.TIME_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, T")
|
||||
@@ -695,7 +821,7 @@ class GuiMainMenu(QMenuBar):
|
||||
# Format > Strikethrough
|
||||
self.aFmtStrike = QAction("Strikethrough", self)
|
||||
self.aFmtStrike.setStatusTip("Add strikethrough to selected text")
|
||||
self.aFmtStrike.setShortcut("Ctrl+-")
|
||||
self.aFmtStrike.setShortcut("Ctrl+D")
|
||||
self.aFmtStrike.triggered.connect(lambda: self._docAction(nwDocAction.STRIKE))
|
||||
self.fmtMenu.addAction(self.aFmtStrike)
|
||||
|
||||
@@ -705,14 +831,14 @@ class GuiMainMenu(QMenuBar):
|
||||
# Format > Double Quotes
|
||||
self.aFmtDQuote = QAction("Wrap Double Quotes", self)
|
||||
self.aFmtDQuote.setStatusTip("Wrap selected text in double quotes")
|
||||
self.aFmtDQuote.setShortcut("Ctrl+D")
|
||||
self.aFmtDQuote.setShortcut("Ctrl+\"")
|
||||
self.aFmtDQuote.triggered.connect(lambda: self._docAction(nwDocAction.D_QUOTE))
|
||||
self.fmtMenu.addAction(self.aFmtDQuote)
|
||||
|
||||
# Format > Single Quotes
|
||||
self.aFmtSQuote = QAction("Wrap Single Quotes", self)
|
||||
self.aFmtSQuote.setStatusTip("Wrap selected text in single quotes")
|
||||
self.aFmtSQuote.setShortcut("Ctrl+Shift+D")
|
||||
self.aFmtSQuote.setShortcut("Ctrl+'")
|
||||
self.aFmtSQuote.triggered.connect(lambda: self._docAction(nwDocAction.S_QUOTE))
|
||||
self.fmtMenu.addAction(self.aFmtSQuote)
|
||||
|
||||
@@ -784,40 +910,28 @@ class GuiMainMenu(QMenuBar):
|
||||
# Tools
|
||||
self.toolsMenu = self.addMenu("&Tools")
|
||||
|
||||
# Tools > Move Up
|
||||
self.aMoveUp = QAction("Move Tree Item Up", self)
|
||||
self.aMoveUp.setStatusTip("Move item up")
|
||||
self.aMoveUp.setShortcut("Ctrl+Shift+Up")
|
||||
self.aMoveUp.triggered.connect(lambda: self._moveTreeItem(-1))
|
||||
self.toolsMenu.addAction(self.aMoveUp)
|
||||
|
||||
# Tools > Move Down
|
||||
self.aMoveDown = QAction("Move Tree Item Down", self)
|
||||
self.aMoveDown.setStatusTip("Move item down")
|
||||
self.aMoveDown.setShortcut("Ctrl+Shift+Down")
|
||||
self.aMoveDown.triggered.connect(lambda: self._moveTreeItem(1))
|
||||
self.toolsMenu.addAction(self.aMoveDown)
|
||||
|
||||
# Tools > Separator
|
||||
self.toolsMenu.addSeparator()
|
||||
|
||||
# Tools > Toggle Spell Check
|
||||
# Tools > Check Spelling
|
||||
self.aSpellCheck = QAction("Check Spelling", self)
|
||||
self.aSpellCheck.setStatusTip("Toggle check spelling")
|
||||
self.aSpellCheck.setCheckable(True)
|
||||
self.aSpellCheck.setChecked(self.theProject.spellCheck)
|
||||
# Here we must used triggered, not toggled, to avoid recursion
|
||||
self.aSpellCheck.triggered.connect(self._toggleSpellCheck)
|
||||
self.aSpellCheck.triggered.connect(self._toggleSpellCheck) # triggered, not toggled!
|
||||
self.aSpellCheck.setShortcut("Ctrl+F7")
|
||||
self.toolsMenu.addAction(self.aSpellCheck)
|
||||
|
||||
# Tools > Update Spell Check
|
||||
# Tools > Re-Run Spell Check
|
||||
self.aReRunSpell = QAction("Re-Run Spell Check", self)
|
||||
self.aReRunSpell.setStatusTip("Run the spell checker on current document")
|
||||
self.aReRunSpell.setShortcut("F7")
|
||||
self.aReRunSpell.triggered.connect(lambda: self.theParent.docEditor.spellCheckDocument())
|
||||
self.toolsMenu.addAction(self.aReRunSpell)
|
||||
|
||||
# Tools > Project Word List
|
||||
self.aEditWordList = QAction("Project Word List", self)
|
||||
self.aEditWordList.setStatusTip("Edit the project's word list")
|
||||
self.aEditWordList.triggered.connect(lambda: self.theParent.showProjectWordListDialog())
|
||||
self.toolsMenu.addAction(self.aEditWordList)
|
||||
|
||||
# Tools > Separator
|
||||
self.toolsMenu.addSeparator()
|
||||
|
||||
|
||||
321
nw/gui/noveltree.py
Normal file
@@ -0,0 +1,321 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – GUI Novel Tree
|
||||
============================
|
||||
GUI classe for the main window novel tree
|
||||
|
||||
File History:
|
||||
Created: 2020-12-20 [1.1a0]
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2020, Veronica Berglyd Olsen
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
import logging
|
||||
|
||||
from time import time
|
||||
|
||||
from PyQt5.QtCore import Qt, QSize
|
||||
from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QAbstractItemView
|
||||
|
||||
from nw.constants import nwKeyWords
|
||||
from nw.common import checkInt
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class GuiNovelTree(QTreeWidget):
|
||||
|
||||
C_TITLE = 0
|
||||
C_WORDS = 1
|
||||
C_POV = 2
|
||||
|
||||
def __init__(self, theParent):
|
||||
QTreeWidget.__init__(self, theParent)
|
||||
|
||||
logger.debug("Initialising GuiNovelTree ...")
|
||||
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theParent = theParent
|
||||
self.theTheme = theParent.theTheme
|
||||
self.theProject = theParent.theProject
|
||||
self.theIndex = theParent.theIndex
|
||||
|
||||
# Internal Variables
|
||||
self._treeMap = {}
|
||||
self._lastBuild = 0
|
||||
|
||||
# Build GUI
|
||||
iPx = self.theTheme.baseIconSize
|
||||
self.setIconSize(QSize(iPx, iPx))
|
||||
self.setIndentation(iPx)
|
||||
self.setColumnCount(3)
|
||||
self.setHeaderLabels(["Title", "Words", "POV"])
|
||||
self.itemDoubleClicked.connect(self._treeDoubleClick)
|
||||
self.itemSelectionChanged.connect(self._itemSelected)
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
self.setExpandsOnDoubleClick(False)
|
||||
self.setDragEnabled(False)
|
||||
|
||||
treeHeadItem = self.headerItem()
|
||||
treeHeadItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
|
||||
treeHeadItem.setToolTip(self.C_TITLE, "Section title")
|
||||
treeHeadItem.setToolTip(self.C_WORDS, "Word count")
|
||||
treeHeadItem.setToolTip(self.C_POV, "Point-of-view character")
|
||||
|
||||
treeHeader = self.header()
|
||||
treeHeader.setStretchLastSection(True)
|
||||
treeHeader.setMinimumSectionSize(iPx + 6)
|
||||
|
||||
# Get user's column width preferences for NAME and COUNT
|
||||
treeColWidth = self.mainConf.getNovelColWidths()
|
||||
if len(treeColWidth) <= 3:
|
||||
for colN, colW in enumerate(treeColWidth):
|
||||
self.setColumnWidth(colN, colW)
|
||||
|
||||
# The last column should just auto-scale
|
||||
self.resizeColumnToContents(self.C_POV)
|
||||
|
||||
# Set custom settings
|
||||
self.initTree()
|
||||
|
||||
logger.debug("GuiNovelTree initialisation complete")
|
||||
|
||||
# Internal Mapping
|
||||
self.makeAlert = self.theParent.makeAlert
|
||||
|
||||
return
|
||||
|
||||
def initTree(self):
|
||||
"""Set or update tree widget settings.
|
||||
"""
|
||||
# Scroll bars
|
||||
if self.mainConf.hideVScroll:
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
else:
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||
|
||||
if self.mainConf.hideHScroll:
|
||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
else:
|
||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Class Methods
|
||||
##
|
||||
|
||||
def clearTree(self):
|
||||
"""Clear the GUI content and the related maps.
|
||||
"""
|
||||
self.clear()
|
||||
self._treeMap = {}
|
||||
self._lastBuild = 0
|
||||
return
|
||||
|
||||
def refreshTree(self, overRide=False):
|
||||
"""Called whenever the Novel tab is activated.
|
||||
"""
|
||||
logger.verbose("Requesting refresh of the novel tree")
|
||||
treeChanged = self.theParent.treeView.changedSince(self._lastBuild)
|
||||
indexChanged = self.theIndex.novelChangedSince(self._lastBuild)
|
||||
if not (treeChanged or indexChanged):
|
||||
logger.verbose("No changes have been made to the novel index")
|
||||
return
|
||||
|
||||
selItem = self.selectedItems()
|
||||
titleKey = None
|
||||
if selItem:
|
||||
titleKey = selItem[0].data(self.C_TITLE, Qt.UserRole)[2]
|
||||
|
||||
self.theParent.treeView.flushTreeOrder()
|
||||
self._populateTree()
|
||||
|
||||
if titleKey is not None and titleKey in self._treeMap:
|
||||
self._treeMap[titleKey].setSelected(True)
|
||||
|
||||
return
|
||||
|
||||
def updateWordCounts(self, tHandle):
|
||||
"""Update the word count for a given handle.
|
||||
"""
|
||||
tHeaders = self.theIndex.getHandleWordCounts(tHandle)
|
||||
for titleKey, wCount in tHeaders:
|
||||
if titleKey in self._treeMap:
|
||||
self._treeMap[titleKey].setText(self.C_WORDS, f"{wCount:n}")
|
||||
return
|
||||
|
||||
def getColumnSizes(self):
|
||||
"""Return the column widths for the tree columns.
|
||||
"""
|
||||
retVals = [
|
||||
self.columnWidth(0),
|
||||
self.columnWidth(1),
|
||||
]
|
||||
return retVals
|
||||
|
||||
def getSelectedHandle(self):
|
||||
"""Get the currently selected handle. If multiple items are
|
||||
selected, return the first.
|
||||
"""
|
||||
selItem = self.selectedItems()
|
||||
if selItem:
|
||||
return selItem[0].data(self.C_TITLE, Qt.UserRole)[0]
|
||||
|
||||
return None
|
||||
|
||||
##
|
||||
# Events
|
||||
##
|
||||
|
||||
def mousePressEvent(self, theEvent):
|
||||
"""Overload mousePressEvent to clear selection if clicking the
|
||||
mouse in a blank area of the tree view, and to load a document
|
||||
for viewing if the user middle-clicked.
|
||||
"""
|
||||
QTreeWidget.mousePressEvent(self, theEvent)
|
||||
|
||||
if theEvent.button() == Qt.LeftButton:
|
||||
selItem = self.indexAt(theEvent.pos())
|
||||
if not selItem.isValid():
|
||||
self.clearSelection()
|
||||
|
||||
elif theEvent.button() == Qt.MiddleButton:
|
||||
selItem = self.itemAt(theEvent.pos())
|
||||
if not isinstance(selItem, QTreeWidgetItem):
|
||||
return
|
||||
|
||||
tHandle = self.getSelectedHandle()
|
||||
if tHandle is None:
|
||||
return
|
||||
|
||||
self.theParent.viewDocument(tHandle)
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Slots
|
||||
##
|
||||
|
||||
def _treeDoubleClick(self, tItem, tCol):
|
||||
"""Extract the handle and line number of the title double-
|
||||
clicked, and send it to the main gui class for opening in the
|
||||
document editor.
|
||||
"""
|
||||
theData = tItem.data(self.C_TITLE, Qt.UserRole)
|
||||
tHandle = theData[0]
|
||||
tLine = checkInt(theData[1], 1)
|
||||
|
||||
logger.verbose("User selected entry with handle %s on line %s" % (tHandle, tLine))
|
||||
self.theParent.openDocument(tHandle, tLine=tLine-1, doScroll=True)
|
||||
|
||||
return
|
||||
|
||||
def _itemSelected(self):
|
||||
"""Extract the handle and line number of the currently selected
|
||||
title, and send it to the tree meta panel.
|
||||
"""
|
||||
selItems = self.selectedItems()
|
||||
if selItems:
|
||||
tHandle = selItems[0].data(self.C_TITLE, Qt.UserRole)[0]
|
||||
self.theParent.treeMeta.updateViewBox(tHandle)
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _populateTree(self):
|
||||
"""Build the tree based on the project index.
|
||||
"""
|
||||
self.clearTree()
|
||||
|
||||
currTitle = None
|
||||
currChapter = None
|
||||
currScene = None
|
||||
|
||||
for tKey, tHandle, sTitle, novIdx in self.theIndex.novelStructure(skipExcluded=True):
|
||||
|
||||
tItem = self._createTreeItem(tHandle, sTitle, tKey, novIdx)
|
||||
self._treeMap[tKey] = tItem
|
||||
|
||||
tLevel = novIdx["level"]
|
||||
if tLevel == "H1":
|
||||
self.addTopLevelItem(tItem)
|
||||
currTitle = tItem
|
||||
currChapter = None
|
||||
currScene = None
|
||||
|
||||
elif tLevel == "H2":
|
||||
if currTitle is None:
|
||||
self.addTopLevelItem(tItem)
|
||||
else:
|
||||
currTitle.addChild(tItem)
|
||||
currChapter = tItem
|
||||
currScene = None
|
||||
|
||||
elif tLevel == "H3":
|
||||
if currChapter is None:
|
||||
if currTitle is None:
|
||||
self.addTopLevelItem(tItem)
|
||||
else:
|
||||
currTitle.addChild(tItem)
|
||||
else:
|
||||
currChapter.addChild(tItem)
|
||||
currScene = tItem
|
||||
|
||||
elif tLevel == "H4":
|
||||
if currScene is None:
|
||||
if currChapter is None:
|
||||
if currTitle is None:
|
||||
self.addTopLevelItem(tItem)
|
||||
else:
|
||||
currTitle.addChild(tItem)
|
||||
else:
|
||||
currChapter.addChild(tItem)
|
||||
else:
|
||||
currScene.addChild(tItem)
|
||||
|
||||
tItem.setExpanded(True)
|
||||
|
||||
self._lastBuild = time()
|
||||
|
||||
return
|
||||
|
||||
def _createTreeItem(self, tHandle, sTitle, titleKey, novIdx):
|
||||
"""Populate a tree item with all the column values.
|
||||
"""
|
||||
newItem = QTreeWidgetItem()
|
||||
hIcon = "doc_%s" % novIdx["level"].lower()
|
||||
theData = (tHandle, sTitle[1:].lstrip("0"), titleKey)
|
||||
|
||||
wC = int(novIdx["wCount"])
|
||||
|
||||
newItem.setText(self.C_TITLE, novIdx["title"])
|
||||
newItem.setData(self.C_TITLE, Qt.UserRole, theData)
|
||||
newItem.setIcon(self.C_TITLE, self.theTheme.getIcon(hIcon))
|
||||
newItem.setText(self.C_WORDS, f"{wC:n}")
|
||||
newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
|
||||
|
||||
theRefs = self.theIndex.getReferences(tHandle, sTitle)
|
||||
newItem.setText(self.C_POV, ", ".join(theRefs[nwKeyWords.POV_KEY]))
|
||||
|
||||
return newItem
|
||||
|
||||
# END Class GuiNovelTree
|
||||
@@ -1,28 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI Project Outline
|
||||
"""
|
||||
novelWriter – GUI Project Outline
|
||||
=================================
|
||||
GUI class for the project outline view
|
||||
|
||||
novelWriter – GUI Project Outline
|
||||
===================================
|
||||
Class holding the project outline view
|
||||
File History:
|
||||
Created: 2019-11-16 [0.4.1]
|
||||
|
||||
File History:
|
||||
Created: 2019-11-16 [0.4.1]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
@@ -50,6 +49,7 @@ class GuiOutline(QTreeWidget):
|
||||
nwOutline.WCOUNT : 50,
|
||||
nwOutline.PCOUNT : 50,
|
||||
nwOutline.POV : 100,
|
||||
nwOutline.FOCUS : 100,
|
||||
nwOutline.CHAR : 100,
|
||||
nwOutline.PLOT : 100,
|
||||
nwOutline.TIME : 100,
|
||||
@@ -69,6 +69,7 @@ class GuiOutline(QTreeWidget):
|
||||
nwOutline.WCOUNT : False,
|
||||
nwOutline.PCOUNT : False,
|
||||
nwOutline.POV : False,
|
||||
nwOutline.FOCUS : True,
|
||||
nwOutline.CHAR : False,
|
||||
nwOutline.PLOT : False,
|
||||
nwOutline.TIME : True,
|
||||
@@ -165,7 +166,7 @@ class GuiOutline(QTreeWidget):
|
||||
|
||||
return
|
||||
|
||||
def refreshTree(self, overRide=False):
|
||||
def refreshTree(self, overRide=False, novelChanged=False):
|
||||
"""Called whenever the Outline tab is activated and controls
|
||||
what data to load, and if necessary, force a rebuild of the
|
||||
tree.
|
||||
@@ -177,13 +178,10 @@ class GuiOutline(QTreeWidget):
|
||||
self.firstView = False
|
||||
return
|
||||
|
||||
# If the novel index has changed since the tree was last built,
|
||||
# we rebuild the tree from the updated index.
|
||||
lastChange = self.theParent.theIndex.timeNovel
|
||||
logger.verbose("Last outline build: %.3f" % self.lastBuild)
|
||||
logger.verbose("Novel index change: %.3f" % lastChange)
|
||||
|
||||
doBuild = lastChange > self.lastBuild and self.theProject.autoOutline
|
||||
# If the novel index or novel tree has changed since the tree
|
||||
# was last built, we rebuild the tree from the updated index.
|
||||
indexChanged = self.theIndex.novelChangedSince(self.lastBuild)
|
||||
doBuild = (novelChanged or indexChanged) and self.theProject.autoOutline
|
||||
if doBuild or overRide:
|
||||
logger.debug("Rebuilding Project Outline")
|
||||
self._populateTree()
|
||||
@@ -227,6 +225,7 @@ class GuiOutline(QTreeWidget):
|
||||
tHandle = selItems[0].data(self.colIndex[nwOutline.TITLE], Qt.UserRole)
|
||||
sTitle = selItems[0].data(self.colIndex[nwOutline.LINE], Qt.UserRole)
|
||||
self.theParent.projMeta.showItem(tHandle, sTitle)
|
||||
self.theParent.treeView.setSelectedHandle(tHandle)
|
||||
|
||||
return
|
||||
|
||||
@@ -377,23 +376,12 @@ class GuiOutline(QTreeWidget):
|
||||
currChapter = None
|
||||
currScene = None
|
||||
|
||||
for titleKey in self.theIndex.getNovelStructure(skipExcluded=True):
|
||||
for tKey, tHandle, sTitle, novIdx in self.theIndex.novelStructure(skipExcluded=True):
|
||||
|
||||
if len(titleKey) < 16:
|
||||
continue
|
||||
|
||||
tHandle = titleKey[:13]
|
||||
sTitle = titleKey[14:]
|
||||
|
||||
if tHandle not in self.theIndex.novelIndex:
|
||||
continue
|
||||
if sTitle not in self.theIndex.novelIndex[tHandle]:
|
||||
continue
|
||||
|
||||
tLevel = self.theIndex.novelIndex[tHandle][sTitle]["level"]
|
||||
tItem = self._createTreeItem(tHandle, sTitle, tLevel)
|
||||
self.treeMap[titleKey] = tItem
|
||||
tItem = self._createTreeItem(tHandle, sTitle, novIdx)
|
||||
self.treeMap[tKey] = tItem
|
||||
|
||||
tLevel = novIdx["level"]
|
||||
if tLevel == "H1":
|
||||
self.addTopLevelItem(tItem)
|
||||
currTitle = tItem
|
||||
@@ -436,14 +424,12 @@ class GuiOutline(QTreeWidget):
|
||||
|
||||
return
|
||||
|
||||
def _createTreeItem(self, tHandle, sTitle, tLevel):
|
||||
def _createTreeItem(self, tHandle, sTitle, novIdx):
|
||||
"""Populate a tree item with all the column values.
|
||||
"""
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
novIdx = self.theIndex.novelIndex[tHandle][sTitle]
|
||||
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
newItem = QTreeWidgetItem()
|
||||
hIcon = "doc_%s" % tLevel.lower()
|
||||
hIcon = "doc_%s" % novIdx["level"].lower()
|
||||
|
||||
cC = int(novIdx["cCount"])
|
||||
wC = int(novIdx["wCount"])
|
||||
@@ -467,6 +453,7 @@ class GuiOutline(QTreeWidget):
|
||||
|
||||
theRefs = self.theIndex.getReferences(tHandle, sTitle)
|
||||
newItem.setText(self.colIndex[nwOutline.POV], ", ".join(theRefs[nwKeyWords.POV_KEY]))
|
||||
newItem.setText(self.colIndex[nwOutline.FOCUS], ", ".join(theRefs[nwKeyWords.FOCUS_KEY]))
|
||||
newItem.setText(self.colIndex[nwOutline.CHAR], ", ".join(theRefs[nwKeyWords.CHAR_KEY]))
|
||||
newItem.setText(self.colIndex[nwOutline.PLOT], ", ".join(theRefs[nwKeyWords.PLOT_KEY]))
|
||||
newItem.setText(self.colIndex[nwOutline.TIME], ", ".join(theRefs[nwKeyWords.TIME_KEY]))
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI Project Outline Details
|
||||
"""
|
||||
novelWriter – GUI Project Outline Details
|
||||
=========================================
|
||||
GUI class for the project outline details panel
|
||||
|
||||
novelWriter – GUI Project Outline Details
|
||||
===========================================
|
||||
Class holding the project outline details panel
|
||||
File History:
|
||||
Created: 2020-06-02 [0.7.0]
|
||||
|
||||
File History:
|
||||
Created: 2020-06-02 [0.7.0]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
@@ -106,6 +105,7 @@ class GuiOutlineDetails(QScrollArea):
|
||||
|
||||
# Tags
|
||||
self.povKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.POV_KEY])
|
||||
self.focKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY])
|
||||
self.chrKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.CHAR_KEY])
|
||||
self.pltKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY])
|
||||
self.timKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.TIME_KEY])
|
||||
@@ -115,6 +115,7 @@ class GuiOutlineDetails(QScrollArea):
|
||||
self.cstKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.CUSTOM_KEY])
|
||||
|
||||
self.povKeyLWrap = QHBoxLayout()
|
||||
self.focKeyLWrap = QHBoxLayout()
|
||||
self.chrKeyLWrap = QHBoxLayout()
|
||||
self.pltKeyLWrap = QHBoxLayout()
|
||||
self.timKeyLWrap = QHBoxLayout()
|
||||
@@ -124,6 +125,7 @@ class GuiOutlineDetails(QScrollArea):
|
||||
self.cstKeyLWrap = QHBoxLayout()
|
||||
|
||||
self.povKeyValue = QLabel("")
|
||||
self.focKeyValue = QLabel("")
|
||||
self.chrKeyValue = QLabel("")
|
||||
self.pltKeyValue = QLabel("")
|
||||
self.timKeyValue = QLabel("")
|
||||
@@ -133,6 +135,7 @@ class GuiOutlineDetails(QScrollArea):
|
||||
self.cstKeyValue = QLabel("")
|
||||
|
||||
self.povKeyValue.setWordWrap(True)
|
||||
self.focKeyValue.setWordWrap(True)
|
||||
self.chrKeyValue.setWordWrap(True)
|
||||
self.pltKeyValue.setWordWrap(True)
|
||||
self.timKeyValue.setWordWrap(True)
|
||||
@@ -142,6 +145,7 @@ class GuiOutlineDetails(QScrollArea):
|
||||
self.cstKeyValue.setWordWrap(True)
|
||||
|
||||
self.povKeyValue.linkActivated.connect(self._tagClicked)
|
||||
self.focKeyValue.linkActivated.connect(self._tagClicked)
|
||||
self.chrKeyValue.linkActivated.connect(self._tagClicked)
|
||||
self.pltKeyValue.linkActivated.connect(self._tagClicked)
|
||||
self.timKeyValue.linkActivated.connect(self._tagClicked)
|
||||
@@ -151,6 +155,7 @@ class GuiOutlineDetails(QScrollArea):
|
||||
self.cstKeyValue.linkActivated.connect(self._tagClicked)
|
||||
|
||||
self.povKeyLWrap.addWidget(self.povKeyValue, 1)
|
||||
self.focKeyLWrap.addWidget(self.focKeyValue, 1)
|
||||
self.chrKeyLWrap.addWidget(self.chrKeyValue, 1)
|
||||
self.pltKeyLWrap.addWidget(self.pltKeyValue, 1)
|
||||
self.timKeyLWrap.addWidget(self.timKeyValue, 1)
|
||||
@@ -191,23 +196,25 @@ class GuiOutlineDetails(QScrollArea):
|
||||
|
||||
self.tagsForm.addWidget(self.povKeyLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addLayout(self.povKeyLWrap, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addWidget(self.chrKeyLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addLayout(self.chrKeyLWrap, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addWidget(self.pltKeyLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addLayout(self.pltKeyLWrap, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addWidget(self.timKeyLabel, 3, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addLayout(self.timKeyLWrap, 3, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addWidget(self.wldKeyLabel, 4, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addLayout(self.wldKeyLWrap, 4, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addWidget(self.objKeyLabel, 5, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addLayout(self.objKeyLWrap, 5, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addWidget(self.entKeyLabel, 6, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addLayout(self.entKeyLWrap, 6, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addWidget(self.cstKeyLabel, 7, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addLayout(self.cstKeyLWrap, 7, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addWidget(self.focKeyLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addLayout(self.focKeyLWrap, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addWidget(self.chrKeyLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addLayout(self.chrKeyLWrap, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addWidget(self.pltKeyLabel, 3, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addLayout(self.pltKeyLWrap, 3, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addWidget(self.timKeyLabel, 4, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addLayout(self.timKeyLWrap, 4, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addWidget(self.wldKeyLabel, 5, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addLayout(self.wldKeyLWrap, 5, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addWidget(self.objKeyLabel, 6, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addLayout(self.objKeyLWrap, 6, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addWidget(self.entKeyLabel, 7, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addLayout(self.entKeyLWrap, 7, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addWidget(self.cstKeyLabel, 8, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
self.tagsForm.addLayout(self.cstKeyLWrap, 8, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
|
||||
|
||||
self.tagsForm.setColumnStretch(1, 1)
|
||||
self.tagsForm.setRowStretch(7, 1)
|
||||
self.tagsForm.setRowStretch(8, 1)
|
||||
self.tagsForm.setHorizontalSpacing(hSpace)
|
||||
self.tagsForm.setVerticalSpacing(vSpace)
|
||||
|
||||
@@ -258,6 +265,7 @@ class GuiOutlineDetails(QScrollArea):
|
||||
self.pCValue.setText("")
|
||||
self.synopValue.setText("")
|
||||
self.povKeyValue.setText("")
|
||||
self.focKeyValue.setText("")
|
||||
self.chrKeyValue.setText("")
|
||||
self.pltKeyValue.setText("")
|
||||
self.timKeyValue.setText("")
|
||||
@@ -271,11 +279,10 @@ class GuiOutlineDetails(QScrollArea):
|
||||
"""Update the content of the tree with the given handle and line
|
||||
number pointing to a header.
|
||||
"""
|
||||
try:
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
novIdx = self.theIndex.novelIndex[tHandle][sTitle]
|
||||
theRefs = self.theIndex.getReferences(tHandle, sTitle)
|
||||
except Exception:
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
novIdx = self.theIndex.getNovelData(tHandle, sTitle)
|
||||
theRefs = self.theIndex.getReferences(tHandle, sTitle)
|
||||
if nwItem is None or novIdx is None:
|
||||
return False
|
||||
|
||||
if novIdx["level"] in self.LVL_MAP:
|
||||
@@ -298,6 +305,7 @@ class GuiOutlineDetails(QScrollArea):
|
||||
self.synopValue.setText(novIdx["synopsis"])
|
||||
|
||||
self.povKeyValue.setText(self._formatTags(theRefs, nwKeyWords.POV_KEY))
|
||||
self.focKeyValue.setText(self._formatTags(theRefs, nwKeyWords.FOCUS_KEY))
|
||||
self.chrKeyValue.setText(self._formatTags(theRefs, nwKeyWords.CHAR_KEY))
|
||||
self.pltKeyValue.setText(self._formatTags(theRefs, nwKeyWords.PLOT_KEY))
|
||||
self.timKeyValue.setText(self._formatTags(theRefs, nwKeyWords.TIME_KEY))
|
||||
|
||||
462
nw/gui/projdetails.py
Normal file
@@ -0,0 +1,462 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – GUI Project Details
|
||||
=================================
|
||||
Class holding the project details dialog
|
||||
|
||||
File History:
|
||||
Created: 2021-01-03 [1.1a0]
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
import logging
|
||||
import math
|
||||
|
||||
from PyQt5.QtCore import Qt, QSize
|
||||
from PyQt5.QtGui import QFont
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget, QDialogButtonBox, QVBoxLayout, QTreeWidget, QTreeWidgetItem,
|
||||
QLabel, QSpinBox, QGridLayout, QHBoxLayout, QLineEdit, QAbstractItemView
|
||||
)
|
||||
|
||||
from nw.gui.custom import PagedDialog, QSwitch
|
||||
from nw.constants import nwUnicode
|
||||
from nw.core import numberToRoman
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class GuiProjectDetails(PagedDialog):
|
||||
|
||||
def __init__(self, theParent, theProject):
|
||||
PagedDialog.__init__(self, theParent)
|
||||
|
||||
logger.debug("Initialising GuiProjectDetails ...")
|
||||
self.setObjectName("GuiProjectDetails")
|
||||
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theParent = theParent
|
||||
self.theProject = theProject
|
||||
self.optState = theProject.optState
|
||||
|
||||
self.setWindowTitle("Project Details")
|
||||
|
||||
wW = self.mainConf.pxInt(600)
|
||||
wH = self.mainConf.pxInt(400)
|
||||
|
||||
self.setMinimumWidth(wW)
|
||||
self.setMinimumHeight(wH)
|
||||
self.resize(
|
||||
self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "winWidth", wW)),
|
||||
self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "winHeight", wH))
|
||||
)
|
||||
|
||||
self.tabMain = GuiProjectDetailsMain(self.theParent, self.theProject)
|
||||
self.tabContents = GuiProjectDetailsContents(self.theParent, self.theProject)
|
||||
|
||||
self.addTab(self.tabMain, "Overview")
|
||||
self.addTab(self.tabContents, "Contents")
|
||||
|
||||
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close)
|
||||
self.buttonBox.rejected.connect(self._doClose)
|
||||
self.addControls(self.buttonBox)
|
||||
|
||||
logger.debug("GuiProjectDetails initialisation complete")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Slots
|
||||
##
|
||||
|
||||
def _doClose(self):
|
||||
"""Save settings and close the dialog.
|
||||
"""
|
||||
self._saveGuiSettings()
|
||||
self.close()
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _saveGuiSettings(self):
|
||||
"""Save GUI settings.
|
||||
"""
|
||||
winWidth = self.mainConf.rpxInt(self.width())
|
||||
winHeight = self.mainConf.rpxInt(self.height())
|
||||
|
||||
cColWidth = self.tabContents.getColumnSizes()
|
||||
widthCol0 = self.mainConf.rpxInt(cColWidth[0])
|
||||
widthCol1 = self.mainConf.rpxInt(cColWidth[1])
|
||||
widthCol2 = self.mainConf.rpxInt(cColWidth[2])
|
||||
widthCol3 = self.mainConf.rpxInt(cColWidth[3])
|
||||
widthCol4 = self.mainConf.rpxInt(cColWidth[4])
|
||||
|
||||
wordsPerPage = self.tabContents.wpValue.value()
|
||||
countFrom = self.tabContents.poValue.value()
|
||||
clearDouble = self.tabContents.dblValue.isChecked()
|
||||
|
||||
self.optState.setValue("GuiProjectDetails", "winWidth", winWidth)
|
||||
self.optState.setValue("GuiProjectDetails", "winHeight", winHeight)
|
||||
self.optState.setValue("GuiProjectDetails", "widthCol0", widthCol0)
|
||||
self.optState.setValue("GuiProjectDetails", "widthCol1", widthCol1)
|
||||
self.optState.setValue("GuiProjectDetails", "widthCol2", widthCol2)
|
||||
self.optState.setValue("GuiProjectDetails", "widthCol3", widthCol3)
|
||||
self.optState.setValue("GuiProjectDetails", "widthCol4", widthCol4)
|
||||
self.optState.setValue("GuiProjectDetails", "wordsPerPage", wordsPerPage)
|
||||
self.optState.setValue("GuiProjectDetails", "countFrom", countFrom)
|
||||
self.optState.setValue("GuiProjectDetails", "clearDouble", clearDouble)
|
||||
|
||||
return
|
||||
|
||||
# END Class GuiProjectDetails
|
||||
|
||||
class GuiProjectDetailsMain(QWidget):
|
||||
|
||||
def __init__(self, theParent, theProject):
|
||||
QWidget.__init__(self, theParent)
|
||||
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theParent = theParent
|
||||
self.theProject = theProject
|
||||
self.theTheme = theParent.theTheme
|
||||
self.theIndex = theParent.theIndex
|
||||
|
||||
fPx = self.theTheme.fontPixelSize
|
||||
fPt = self.theTheme.fontPointSize
|
||||
vPx = self.mainConf.pxInt(4)
|
||||
hPx = self.mainConf.pxInt(12)
|
||||
|
||||
# Header
|
||||
# ======
|
||||
|
||||
self.bookTitle = QLabel(self.theProject.bookTitle)
|
||||
bookFont = self.bookTitle.font()
|
||||
bookFont.setPointSizeF(2.2*fPt)
|
||||
bookFont.setWeight(QFont.Bold)
|
||||
self.bookTitle.setFont(bookFont)
|
||||
self.bookTitle.setAlignment(Qt.AlignHCenter)
|
||||
self.bookTitle.setWordWrap(True)
|
||||
|
||||
self.projName = QLabel("Working Title: %s" % self.theProject.projName)
|
||||
workFont = self.projName.font()
|
||||
workFont.setPointSizeF(0.8*fPt)
|
||||
workFont.setItalic(True)
|
||||
self.projName.setFont(workFont)
|
||||
self.projName.setAlignment(Qt.AlignHCenter)
|
||||
self.projName.setWordWrap(True)
|
||||
|
||||
self.bookAuthors = QLabel("By %s" % self.theProject.getAuthors())
|
||||
authFont = self.bookAuthors.font()
|
||||
authFont.setPointSizeF(1.2*fPt)
|
||||
self.bookAuthors.setFont(authFont)
|
||||
self.bookAuthors.setAlignment(Qt.AlignHCenter)
|
||||
self.bookAuthors.setWordWrap(True)
|
||||
|
||||
# Stats
|
||||
# =====
|
||||
|
||||
hCounts = self.theIndex.getNovelTitleCounts()
|
||||
nwCount = self.theIndex.getNovelWordCount()
|
||||
|
||||
self.wordCountLbl = QLabel("<b>Words:</b>")
|
||||
self.wordCountVal = QLabel(f"{nwCount:n}")
|
||||
|
||||
self.chapCountLbl = QLabel("<b>Chapters:</b>")
|
||||
self.chapCountVal = QLabel(f"{hCounts[2]:n}")
|
||||
|
||||
self.sceneCountLbl = QLabel("<b>Scenes:</b>")
|
||||
self.sceneCountVal = QLabel(f"{hCounts[3]:n}")
|
||||
|
||||
self.revCountLbl = QLabel("<b>Revisions:</b>")
|
||||
self.revCountVal = QLabel(f"{self.theProject.saveCount:n}")
|
||||
|
||||
edTime = self.theProject.getCurrentEditTime()
|
||||
self.editTimeLbl = QLabel("<b>Editing Time:</b>")
|
||||
self.editTimeVal = QLabel(f"{edTime//3600:02d}:{edTime%3600//60:02d}")
|
||||
|
||||
self.statsGrid = QGridLayout()
|
||||
self.statsGrid.addWidget(self.wordCountLbl, 0, 0, 1, 1, Qt.AlignRight)
|
||||
self.statsGrid.addWidget(self.wordCountVal, 0, 1, 1, 1, Qt.AlignLeft)
|
||||
self.statsGrid.addWidget(self.chapCountLbl, 1, 0, 1, 1, Qt.AlignRight)
|
||||
self.statsGrid.addWidget(self.chapCountVal, 1, 1, 1, 1, Qt.AlignLeft)
|
||||
self.statsGrid.addWidget(self.sceneCountLbl, 2, 0, 1, 1, Qt.AlignRight)
|
||||
self.statsGrid.addWidget(self.sceneCountVal, 2, 1, 1, 1, Qt.AlignLeft)
|
||||
self.statsGrid.addWidget(self.revCountLbl, 3, 0, 1, 1, Qt.AlignRight)
|
||||
self.statsGrid.addWidget(self.revCountVal, 3, 1, 1, 1, Qt.AlignLeft)
|
||||
self.statsGrid.addWidget(self.editTimeLbl, 4, 0, 1, 1, Qt.AlignRight)
|
||||
self.statsGrid.addWidget(self.editTimeVal, 4, 1, 1, 1, Qt.AlignLeft)
|
||||
self.statsGrid.setHorizontalSpacing(hPx)
|
||||
self.statsGrid.setVerticalSpacing(vPx)
|
||||
|
||||
# Meta
|
||||
# ====
|
||||
|
||||
self.projPathLbl = QLabel("<b>Path:</b>")
|
||||
self.projPathVal = QLineEdit()
|
||||
self.projPathVal.setText(self.theProject.projPath)
|
||||
self.projPathVal.setReadOnly(True)
|
||||
|
||||
self.projPathBox = QHBoxLayout()
|
||||
self.projPathBox.addWidget(self.projPathLbl)
|
||||
self.projPathBox.addWidget(self.projPathVal)
|
||||
self.projPathBox.setSpacing(hPx)
|
||||
|
||||
# Assemble
|
||||
# ========
|
||||
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.outerBox.addSpacing(fPx)
|
||||
self.outerBox.addWidget(self.bookTitle)
|
||||
self.outerBox.addWidget(self.projName)
|
||||
self.outerBox.addWidget(self.bookAuthors)
|
||||
self.outerBox.addSpacing(2*fPx)
|
||||
self.outerBox.addLayout(self.statsGrid)
|
||||
self.outerBox.addSpacing(fPx)
|
||||
self.outerBox.addStretch(1)
|
||||
self.outerBox.addLayout(self.projPathBox)
|
||||
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
return
|
||||
|
||||
# END Class GuiProjectDetailsMain
|
||||
|
||||
class GuiProjectDetailsContents(QWidget):
|
||||
|
||||
C_TITLE = 0
|
||||
C_WORDS = 1
|
||||
C_PAGES = 2
|
||||
C_PAGE = 3
|
||||
C_PROG = 4
|
||||
|
||||
def __init__(self, theParent, theProject):
|
||||
QWidget.__init__(self, theParent)
|
||||
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theParent = theParent
|
||||
self.theProject = theProject
|
||||
self.theTheme = theParent.theTheme
|
||||
self.theIndex = theParent.theIndex
|
||||
self.optState = theProject.optState
|
||||
|
||||
# Internal
|
||||
self._theToC = []
|
||||
|
||||
iPx = self.theTheme.baseIconSize
|
||||
hPx = self.mainConf.pxInt(12)
|
||||
vPx = self.mainConf.pxInt(4)
|
||||
|
||||
# Contents Tree
|
||||
# =============
|
||||
|
||||
self.tocTree = QTreeWidget()
|
||||
self.tocTree.setIconSize(QSize(iPx, iPx))
|
||||
self.tocTree.setIndentation(0)
|
||||
self.tocTree.setColumnCount(6)
|
||||
self.tocTree.setSelectionMode(QAbstractItemView.NoSelection)
|
||||
self.tocTree.setHeaderLabels(["Title", "Words", "Pages", "Page", "Progress", ""])
|
||||
|
||||
treeHeadItem = self.tocTree.headerItem()
|
||||
treeHeadItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
|
||||
treeHeadItem.setTextAlignment(self.C_PAGES, Qt.AlignRight)
|
||||
treeHeadItem.setTextAlignment(self.C_PAGE, Qt.AlignRight)
|
||||
treeHeadItem.setTextAlignment(self.C_PROG, Qt.AlignRight)
|
||||
|
||||
treeHeader = self.tocTree.header()
|
||||
treeHeader.setStretchLastSection(True)
|
||||
treeHeader.setMinimumSectionSize(hPx)
|
||||
|
||||
wCol0 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol0", 200))
|
||||
wCol1 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol1", 60))
|
||||
wCol2 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol2", 60))
|
||||
wCol3 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol3", 60))
|
||||
wCol4 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol4", 90))
|
||||
|
||||
self.tocTree.setColumnWidth(0, wCol0)
|
||||
self.tocTree.setColumnWidth(1, wCol1)
|
||||
self.tocTree.setColumnWidth(2, wCol2)
|
||||
self.tocTree.setColumnWidth(3, wCol3)
|
||||
self.tocTree.setColumnWidth(4, wCol4)
|
||||
self.tocTree.setColumnWidth(5, hPx)
|
||||
|
||||
# Options
|
||||
# =======
|
||||
|
||||
wordsPerPage = self.optState.getInt("GuiProjectDetails", "wordsPerPage", 350)
|
||||
countFrom = self.optState.getInt("GuiProjectDetails", "countFrom", 1)
|
||||
clearDouble = self.optState.getInt("GuiProjectDetails", "clearDouble", True)
|
||||
|
||||
wordsHelp = (
|
||||
"Typical word count for a 5 by 8 inch book page with 11 pt font is 350."
|
||||
)
|
||||
offsetHelp = (
|
||||
"Start counting page numbers from this page."
|
||||
)
|
||||
dblHelp = (
|
||||
"Assume a new chapter or partition always start on an odd numbered page."
|
||||
)
|
||||
|
||||
self.wpLabel = QLabel("Words per page")
|
||||
self.wpLabel.setToolTip(wordsHelp)
|
||||
|
||||
self.wpValue = QSpinBox()
|
||||
self.wpValue.setMinimum(10)
|
||||
self.wpValue.setMaximum(1000)
|
||||
self.wpValue.setSingleStep(10)
|
||||
self.wpValue.setValue(wordsPerPage)
|
||||
self.wpValue.setToolTip(wordsHelp)
|
||||
self.wpValue.valueChanged.connect(self._populateTree)
|
||||
|
||||
self.poLabel = QLabel("Count pages from")
|
||||
self.poLabel.setToolTip(offsetHelp)
|
||||
|
||||
self.poValue = QSpinBox()
|
||||
self.poValue.setMinimum(1)
|
||||
self.poValue.setMaximum(9999)
|
||||
self.poValue.setSingleStep(1)
|
||||
self.poValue.setValue(countFrom)
|
||||
self.poValue.setToolTip(offsetHelp)
|
||||
self.poValue.valueChanged.connect(self._populateTree)
|
||||
|
||||
self.dblLabel = QLabel("Clear double pages")
|
||||
self.dblLabel.setToolTip(dblHelp)
|
||||
|
||||
self.dblValue = QSwitch(self, 2*iPx, iPx)
|
||||
self.dblValue.setChecked(clearDouble)
|
||||
self.dblValue.setToolTip(dblHelp)
|
||||
self.dblValue.clicked.connect(self._populateTree)
|
||||
|
||||
self.optionsBox = QGridLayout()
|
||||
self.optionsBox.addWidget(self.wpLabel, 0, 0)
|
||||
self.optionsBox.addWidget(self.wpValue, 0, 1)
|
||||
self.optionsBox.addWidget(self.dblLabel, 0, 3)
|
||||
self.optionsBox.addWidget(self.dblValue, 0, 4)
|
||||
self.optionsBox.addWidget(self.poLabel, 1, 0)
|
||||
self.optionsBox.addWidget(self.poValue, 1, 1)
|
||||
self.optionsBox.setHorizontalSpacing(hPx)
|
||||
self.optionsBox.setVerticalSpacing(vPx)
|
||||
self.optionsBox.setColumnStretch(2, 1)
|
||||
|
||||
# Assemble
|
||||
# ========
|
||||
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.outerBox.addWidget(QLabel("<b>Table of Contents</b>"))
|
||||
self.outerBox.addWidget(self.tocTree)
|
||||
self.outerBox.addLayout(self.optionsBox)
|
||||
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
self._prepareData()
|
||||
self._populateTree()
|
||||
|
||||
return
|
||||
|
||||
def getColumnSizes(self):
|
||||
"""Return the column widths for the tree columns.
|
||||
"""
|
||||
retVals = [
|
||||
self.tocTree.columnWidth(0),
|
||||
self.tocTree.columnWidth(1),
|
||||
self.tocTree.columnWidth(2),
|
||||
self.tocTree.columnWidth(3),
|
||||
self.tocTree.columnWidth(4),
|
||||
]
|
||||
return retVals
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _prepareData(self):
|
||||
"""Extract the data for the tree.
|
||||
"""
|
||||
self._theToC = []
|
||||
self._theToC = self.theIndex.getTableOfContents(2)
|
||||
self._theToC.append(("", 0, "END", 0))
|
||||
return
|
||||
|
||||
##
|
||||
# Slots
|
||||
##
|
||||
|
||||
def _populateTree(self):
|
||||
"""Set the content of the chapter/page tree.
|
||||
"""
|
||||
dblPages = self.dblValue.isChecked()
|
||||
wpPage = self.wpValue.value()
|
||||
fstPage = self.poValue.value() - 1
|
||||
|
||||
pTotal = 0
|
||||
tPages = 1
|
||||
|
||||
theList = []
|
||||
for _, tLevel, tTitle, wCount in self._theToC:
|
||||
pCount = math.ceil(wCount/wpPage)
|
||||
if dblPages:
|
||||
pCount += pCount%2
|
||||
|
||||
pTotal += pCount
|
||||
theList.append((tLevel, tTitle, wCount, pCount))
|
||||
|
||||
pMax = pTotal - fstPage
|
||||
|
||||
self.tocTree.clear()
|
||||
for tLevel, tTitle, wCount, pCount in theList:
|
||||
newItem = QTreeWidgetItem()
|
||||
|
||||
if tPages <= fstPage:
|
||||
progPage = numberToRoman(tPages, True)
|
||||
progText = ""
|
||||
else:
|
||||
cPage = tPages - fstPage
|
||||
pgProg = 100.0*(cPage - 1)/pMax if pMax > 0 else 0.0
|
||||
progPage = f"{cPage:n}"
|
||||
progText = f"{pgProg:.1f}{nwUnicode.U_THSP}%"
|
||||
|
||||
newItem.setIcon(self.C_TITLE, self.theTheme.getIcon("doc_h%d" % tLevel))
|
||||
newItem.setText(self.C_TITLE, tTitle)
|
||||
newItem.setText(self.C_WORDS, f"{wCount:n}")
|
||||
newItem.setText(self.C_PAGES, f"{pCount:n}")
|
||||
newItem.setText(self.C_PAGE, progPage)
|
||||
newItem.setText(self.C_PROG, progText)
|
||||
|
||||
newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
|
||||
newItem.setTextAlignment(self.C_PAGES, Qt.AlignRight)
|
||||
newItem.setTextAlignment(self.C_PAGE, Qt.AlignRight)
|
||||
newItem.setTextAlignment(self.C_PROG, Qt.AlignRight)
|
||||
|
||||
# Make pages and titles/partitions stand out
|
||||
if tLevel < 2:
|
||||
bFont = newItem.font(self.C_TITLE)
|
||||
if tLevel == 0:
|
||||
bFont.setItalic(True)
|
||||
else:
|
||||
bFont.setBold(True)
|
||||
bFont.setUnderline(True)
|
||||
newItem.setFont(self.C_TITLE, bFont)
|
||||
|
||||
tPages += pCount
|
||||
|
||||
self.tocTree.addTopLevelItem(newItem)
|
||||
|
||||
return
|
||||
|
||||
# END Class GuiProjectDetailsContents
|
||||
@@ -1,28 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI Open Project
|
||||
"""
|
||||
novelWriter – GUI Open Project
|
||||
==============================
|
||||
GUI class for the load/browse/new project dialog
|
||||
|
||||
novelWriter – GUI Open Project
|
||||
================================
|
||||
Class holding the load/browse/new project dialog
|
||||
File History:
|
||||
Created: 2020-02-26 [0.4.5]
|
||||
|
||||
File History:
|
||||
Created: 2020-02-26 [0.4.5]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
@@ -130,6 +129,9 @@ class GuiProjectLoad(QDialog):
|
||||
self.newButton = self.buttonBox.addButton("New", QDialogButtonBox.ActionRole)
|
||||
self.newButton.clicked.connect(self._doNewProject)
|
||||
|
||||
self.delButton = self.buttonBox.addButton("Remove", QDialogButtonBox.ActionRole)
|
||||
self.delButton.clicked.connect(self._doDeleteRecent)
|
||||
|
||||
self.outerBox.addLayout(self.innerBox)
|
||||
self.outerBox.addWidget(self.buttonBox)
|
||||
self.setLayout(self.outerBox)
|
||||
@@ -139,7 +141,7 @@ class GuiProjectLoad(QDialog):
|
||||
|
||||
keyDelete = QShortcut(self.listBox)
|
||||
keyDelete.setKey(QKeySequence(Qt.Key_Delete))
|
||||
keyDelete.activated.connect(self._keyPressDelete)
|
||||
keyDelete.activated.connect(self._doDeleteRecent)
|
||||
|
||||
logger.debug("GuiProjectLoad initialisation complete")
|
||||
|
||||
@@ -213,15 +215,16 @@ class GuiProjectLoad(QDialog):
|
||||
self.accept()
|
||||
return
|
||||
|
||||
def _keyPressDelete(self):
|
||||
def _doDeleteRecent(self):
|
||||
"""Remove an entry from the recent projects list.
|
||||
"""
|
||||
selList = self.listBox.selectedItems()
|
||||
if selList:
|
||||
msgYes = self.theParent.askQuestion(
|
||||
"Remove Entry",
|
||||
"Remove the selected entry from the recent projects list?"
|
||||
)
|
||||
projName = selList[0].text(self.C_NAME)
|
||||
msgYes = self.theParent.askQuestion("Remove Entry", (
|
||||
"Remove '%s' from the recent projects list? "
|
||||
"The project files will not be deleted."
|
||||
) % projName)
|
||||
if msgYes:
|
||||
self.mainConf.removeFromRecentCache(
|
||||
selList[0].data(self.C_NAME, Qt.UserRole)
|
||||
@@ -258,40 +261,31 @@ class GuiProjectLoad(QDialog):
|
||||
def _populateList(self):
|
||||
"""Populate the list box with recent project data.
|
||||
"""
|
||||
listOrder = []
|
||||
listData = {}
|
||||
for projPath in self.mainConf.recentProj.keys():
|
||||
dataList = []
|
||||
for projPath in self.mainConf.recentProj:
|
||||
theEntry = self.mainConf.recentProj[projPath]
|
||||
theTitle = ""
|
||||
theTime = 0
|
||||
theWords = 0
|
||||
if "title" in theEntry.keys():
|
||||
theTitle = theEntry["title"]
|
||||
if "time" in theEntry.keys():
|
||||
theTime = theEntry["time"]
|
||||
if "words" in theEntry.keys():
|
||||
theWords = theEntry["words"]
|
||||
if theTime > 0:
|
||||
listOrder.append(theTime)
|
||||
listData[theTime] = [theTitle, theWords, projPath]
|
||||
theTitle = theEntry.get("title", "")
|
||||
theTime = theEntry.get("time", 0)
|
||||
theWords = theEntry.get("words", 0)
|
||||
dataList.append([theTitle, theTime, theWords, projPath])
|
||||
|
||||
self.listBox.clear()
|
||||
hasSelection = False
|
||||
for timeStamp in sorted(listOrder, reverse=True):
|
||||
sortList = sorted(dataList, key=lambda x: x[1], reverse=True)
|
||||
for theTitle, theTime, theWords, projPath in sortList:
|
||||
newItem = QTreeWidgetItem([""]*4)
|
||||
newItem.setIcon(self.C_NAME, self.theParent.theTheme.getIcon("proj_nwx"))
|
||||
newItem.setText(self.C_NAME, listData[timeStamp][0])
|
||||
newItem.setData(self.C_NAME, Qt.UserRole, listData[timeStamp][2])
|
||||
newItem.setText(self.C_COUNT, formatInt(listData[timeStamp][1]))
|
||||
newItem.setText(self.C_TIME, datetime.fromtimestamp(timeStamp).strftime("%x %X"))
|
||||
newItem.setText(self.C_NAME, theTitle)
|
||||
newItem.setData(self.C_NAME, Qt.UserRole, projPath)
|
||||
newItem.setText(self.C_COUNT, formatInt(theWords))
|
||||
newItem.setText(self.C_TIME, datetime.fromtimestamp(theTime).strftime("%x %X"))
|
||||
newItem.setTextAlignment(self.C_NAME, Qt.AlignLeft | Qt.AlignVCenter)
|
||||
newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight | Qt.AlignVCenter)
|
||||
newItem.setTextAlignment(self.C_TIME, Qt.AlignRight | Qt.AlignVCenter)
|
||||
newItem.setFont(self.C_TIME, self.theTheme.guiFontFixed)
|
||||
self.listBox.addTopLevelItem(newItem)
|
||||
if not hasSelection:
|
||||
newItem.setSelected(True)
|
||||
hasSelection = True
|
||||
|
||||
if self.listBox.topLevelItemCount() > 0:
|
||||
self.listBox.topLevelItem(0).setSelected(True)
|
||||
|
||||
projColWidth = self.mainConf.getProjColWidths()
|
||||
if len(projColWidth) == 3:
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI Project Settings
|
||||
"""
|
||||
novelWriter – GUI Project Settings
|
||||
==================================
|
||||
GUI classes for the project settings dialog
|
||||
|
||||
novelWriter – GUI Project Settings
|
||||
====================================
|
||||
Class holding the project settings dialog
|
||||
File History:
|
||||
Created: 2018-09-29 [0.0.1]
|
||||
|
||||
File History:
|
||||
Created: 2018-09-29 [0.0.1]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
@@ -31,9 +30,9 @@ import logging
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QIcon, QPixmap, QColor, QBrush
|
||||
from PyQt5.QtWidgets import (
|
||||
QHBoxLayout, QVBoxLayout, QGridLayout, QLineEdit, QPlainTextEdit, QLabel,
|
||||
QWidget, QDialogButtonBox, QListWidget, QPushButton, QListWidgetItem,
|
||||
QColorDialog, QAbstractItemView, QTreeWidget, QTreeWidgetItem, QComboBox
|
||||
QHBoxLayout, QVBoxLayout, QLineEdit, QPlainTextEdit, QLabel, QWidget,
|
||||
QDialogButtonBox, QListWidget, QPushButton, QListWidgetItem, QColorDialog,
|
||||
QAbstractItemView, QTreeWidget, QTreeWidgetItem, QComboBox
|
||||
)
|
||||
|
||||
from nw.constants import nwAlert
|
||||
@@ -68,13 +67,11 @@ class GuiProjectSettings(PagedDialog):
|
||||
)
|
||||
|
||||
self.tabMain = GuiProjectEditMain(self.theParent, self.theProject)
|
||||
self.tabMeta = GuiProjectEditMeta(self.theParent, self.theProject)
|
||||
self.tabStatus = GuiProjectEditStatus(self.theParent, self.theProject, True)
|
||||
self.tabImport = GuiProjectEditStatus(self.theParent, self.theProject, False)
|
||||
self.tabReplace = GuiProjectEditReplace(self.theParent, self.theProject)
|
||||
|
||||
self.addTab(self.tabMain, "Settings")
|
||||
self.addTab(self.tabMeta, "Details")
|
||||
self.addTab(self.tabStatus, "Status")
|
||||
self.addTab(self.tabImport, "Importance")
|
||||
self.addTab(self.tabReplace, "Auto-Replace")
|
||||
@@ -118,7 +115,7 @@ class GuiProjectSettings(PagedDialog):
|
||||
self.theProject.setImportColours(importCol)
|
||||
|
||||
if self.tabStatus.colChanged or self.tabImport.colChanged:
|
||||
self.theParent.rebuildTree()
|
||||
self.theParent.rebuildTrees()
|
||||
|
||||
if self.tabReplace.arChanged:
|
||||
newList = self.tabReplace.getNewList()
|
||||
@@ -238,94 +235,6 @@ class GuiProjectEditMain(QWidget):
|
||||
|
||||
# END Class GuiProjectEditMain
|
||||
|
||||
class GuiProjectEditMeta(QWidget):
|
||||
|
||||
def __init__(self, theParent, theProject):
|
||||
QWidget.__init__(self, theParent)
|
||||
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theParent = theParent
|
||||
self.theProject = theProject
|
||||
|
||||
xInd = self.mainConf.pxInt(8)
|
||||
|
||||
# The Form
|
||||
self.mainForm = QGridLayout()
|
||||
self.setLayout(self.mainForm)
|
||||
|
||||
self.headLabel = QLabel("<b>Project Details</b>")
|
||||
|
||||
self.nameLabel = QLabel("Working title:")
|
||||
self.nameLabel.setIndent(xInd)
|
||||
self.nameValue = QLabel(self.theProject.projName)
|
||||
self.nameValue.setWordWrap(True)
|
||||
|
||||
self.pathLabel = QLabel("Project path:")
|
||||
self.pathLabel.setIndent(xInd)
|
||||
self.pathValue = QLabel(self.theProject.projPath)
|
||||
self.pathValue.setWordWrap(True)
|
||||
self.pathValue.setTextInteractionFlags(Qt.TextSelectableByMouse)
|
||||
self.pathValue.setCursor(Qt.IBeamCursor)
|
||||
|
||||
self.revLabel = QLabel("Revision count:")
|
||||
self.revLabel.setIndent(xInd)
|
||||
self.revValue = QLabel(f"{self.theProject.saveCount:n}")
|
||||
|
||||
editHours = self.theProject.editTime/3600
|
||||
self.editLabel = QLabel("Edit time:")
|
||||
self.editLabel.setIndent(xInd)
|
||||
self.editValue = QLabel(f"{editHours:.2f} hours")
|
||||
|
||||
self.statsLabel = QLabel("<b>Project Stats</b>")
|
||||
|
||||
nR, nD, nF = self.theProject.projTree.countTypes()
|
||||
|
||||
self.nRootLabel = QLabel("Root folders:")
|
||||
self.nRootLabel.setIndent(xInd)
|
||||
self.nRootValue = QLabel(f"{nR:n}")
|
||||
|
||||
self.nDirLabel = QLabel("Folders:")
|
||||
self.nDirLabel.setIndent(xInd)
|
||||
self.nDirValue = QLabel(f"{nD:n}")
|
||||
|
||||
self.nFileLabel = QLabel("Documents:")
|
||||
self.nFileLabel.setIndent(xInd)
|
||||
self.nFileValue = QLabel(f"{nF:n}")
|
||||
|
||||
self.wordsLabel = QLabel("Word count:")
|
||||
self.wordsLabel.setIndent(xInd)
|
||||
self.wordsValue = QLabel(f"{self.theProject.currWCount:n}")
|
||||
|
||||
self.mainForm.addWidget(self.headLabel, 0, 0, 1, 2, Qt.AlignTop)
|
||||
self.mainForm.addWidget(self.nameLabel, 1, 0, 1, 1, Qt.AlignTop)
|
||||
self.mainForm.addWidget(self.nameValue, 1, 1, 1, 1, Qt.AlignTop)
|
||||
self.mainForm.addWidget(self.pathLabel, 2, 0, 1, 1, Qt.AlignTop)
|
||||
self.mainForm.addWidget(self.pathValue, 2, 1, 1, 1, Qt.AlignTop)
|
||||
self.mainForm.addWidget(self.revLabel, 3, 0, 1, 1, Qt.AlignTop)
|
||||
self.mainForm.addWidget(self.revValue, 3, 1, 1, 1, Qt.AlignTop)
|
||||
self.mainForm.addWidget(self.editLabel, 4, 0, 1, 1, Qt.AlignTop)
|
||||
self.mainForm.addWidget(self.editValue, 4, 1, 1, 1, Qt.AlignTop)
|
||||
|
||||
self.mainForm.addWidget(self.statsLabel, 5, 0, 1, 2, Qt.AlignTop)
|
||||
self.mainForm.addWidget(self.nRootLabel, 6, 0, 1, 1, Qt.AlignTop)
|
||||
self.mainForm.addWidget(self.nRootValue, 6, 1, 1, 1, Qt.AlignTop)
|
||||
self.mainForm.addWidget(self.nDirLabel, 7, 0, 1, 1, Qt.AlignTop)
|
||||
self.mainForm.addWidget(self.nDirValue, 7, 1, 1, 1, Qt.AlignTop)
|
||||
self.mainForm.addWidget(self.nFileLabel, 8, 0, 1, 1, Qt.AlignTop)
|
||||
self.mainForm.addWidget(self.nFileValue, 8, 1, 1, 1, Qt.AlignTop)
|
||||
self.mainForm.addWidget(self.wordsLabel, 9, 0, 1, 1, Qt.AlignTop)
|
||||
self.mainForm.addWidget(self.wordsValue, 9, 1, 1, 1, Qt.AlignTop)
|
||||
|
||||
self.mainForm.setVerticalSpacing(self.mainConf.pxInt(6))
|
||||
self.mainForm.setHorizontalSpacing(self.mainConf.pxInt(12))
|
||||
self.mainForm.setColumnStretch(0, 0)
|
||||
self.mainForm.setColumnStretch(1, 1)
|
||||
self.mainForm.setRowStretch(10, 1)
|
||||
|
||||
return
|
||||
|
||||
# END Class GuiProjectEditMeta
|
||||
|
||||
class GuiProjectEditStatus(QWidget):
|
||||
|
||||
def __init__(self, theParent, theProject, isStatus):
|
||||
@@ -414,7 +323,6 @@ class GuiProjectEditStatus(QWidget):
|
||||
def _selectColour(self):
|
||||
"""Open a dialog to select the status icon colour.
|
||||
"""
|
||||
logger.verbose("Item colour button clicked")
|
||||
if self.selColour is not None:
|
||||
newCol = QColorDialog.getColor(
|
||||
self.selColour, self, "Select Colour", QColorDialog.DontUseNativeDialog
|
||||
@@ -430,7 +338,6 @@ class GuiProjectEditStatus(QWidget):
|
||||
def _newItem(self):
|
||||
"""Create a new status item.
|
||||
"""
|
||||
logger.verbose("New item button clicked")
|
||||
newItem = self._addItem("New Item", (0, 0, 0), None, 0)
|
||||
newItem.setBackground(QBrush(QColor(0, 255, 0, 80)))
|
||||
self.colChanged = True
|
||||
@@ -439,7 +346,6 @@ class GuiProjectEditStatus(QWidget):
|
||||
def _delItem(self):
|
||||
"""Delete a status item.
|
||||
"""
|
||||
logger.verbose("Delete item button clicked")
|
||||
selItem = self._getSelectedItem()
|
||||
if selItem is not None:
|
||||
iRow = self.listBox.row(selItem)
|
||||
@@ -456,7 +362,6 @@ class GuiProjectEditStatus(QWidget):
|
||||
def _saveItem(self):
|
||||
"""Save changes made to a status item.
|
||||
"""
|
||||
logger.verbose("Save item button clicked")
|
||||
selItem = self._getSelectedItem()
|
||||
if selItem is not None:
|
||||
selIdx = selItem.data(Qt.UserRole)
|
||||
@@ -491,7 +396,6 @@ class GuiProjectEditStatus(QWidget):
|
||||
"""Extract the info of a selected item and populate the settings
|
||||
boxes and button.
|
||||
"""
|
||||
logger.verbose("Item selected")
|
||||
selItem = self._getSelectedItem()
|
||||
if selItem is not None:
|
||||
selIdx = selItem.data(Qt.UserRole)
|
||||
|
||||
@@ -1,43 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI Project Tree
|
||||
"""
|
||||
novelWriter – GUI Project Tree
|
||||
==============================
|
||||
GUI classes for the main window project tree
|
||||
|
||||
novelWriter – GUI project Tree
|
||||
================================
|
||||
Class holding the project tree view
|
||||
File History:
|
||||
Created: 2018-09-29 [0.0.1] GuiProjectTree
|
||||
Created: 2020-06-04 [0.7.0] GuiProjectTreeMenu
|
||||
|
||||
File History:
|
||||
Created: 2018-09-29 [0.0.1] GuiProjectTree
|
||||
Created: 2020-06-04 [0.7.0] GuiProjectTreeMenu
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
import logging
|
||||
|
||||
from PyQt5.QtCore import Qt, QSize
|
||||
from time import time
|
||||
|
||||
from PyQt5.QtCore import Qt, QSize, pyqtSignal
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import (
|
||||
qApp, QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction
|
||||
QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction
|
||||
)
|
||||
|
||||
from nw.core import NWDoc
|
||||
from nw.constants import (
|
||||
nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert, nwConst
|
||||
nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert, nwConst, nwLists
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -49,6 +50,9 @@ class GuiProjectTree(QTreeWidget):
|
||||
C_EXPORT = 2
|
||||
C_FLAGS = 3
|
||||
|
||||
novelItemChanged = pyqtSignal()
|
||||
noteItemChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, theParent):
|
||||
QTreeWidget.__init__(self, theParent)
|
||||
|
||||
@@ -60,22 +64,28 @@ class GuiProjectTree(QTreeWidget):
|
||||
self.theProject = theParent.theProject
|
||||
self.theIndex = theParent.theIndex
|
||||
|
||||
# Tree Settings
|
||||
self.theMap = {}
|
||||
self.treeChanged = False
|
||||
# Internal Variables
|
||||
self._treeMap = {}
|
||||
self._treeChanged = False
|
||||
self._timeChanged = 0
|
||||
self._lastMove = {}
|
||||
|
||||
##
|
||||
# Build GUI
|
||||
##
|
||||
|
||||
# Context Menu
|
||||
self.ctxMenu = GuiProjectTreeMenu(self)
|
||||
self.clearTree()
|
||||
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self._rightClickMenu)
|
||||
|
||||
# Build GUI
|
||||
# Tree Settings
|
||||
iPx = self.theTheme.baseIconSize
|
||||
self.setIconSize(QSize(iPx, iPx))
|
||||
self.setExpandsOnDoubleClick(True)
|
||||
self.setIndentation(iPx)
|
||||
self.setColumnCount(4)
|
||||
self.setHeaderLabels(["Label", "Words", "Inc", "Flags"])
|
||||
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self._rightClickMenu)
|
||||
|
||||
treeHeadItem = self.headerItem()
|
||||
treeHeadItem.setTextAlignment(self.C_COUNT, Qt.AlignRight)
|
||||
@@ -102,7 +112,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
# Set Multiple Selection by CTRL
|
||||
# Disabled for now, until the merge files option has been added
|
||||
# self.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
||||
# self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
|
||||
# Get user's column width preferences for NAME and COUNT
|
||||
treeColWidth = self.mainConf.getTreeColWidths()
|
||||
@@ -116,10 +126,11 @@ class GuiProjectTree(QTreeWidget):
|
||||
# Set custom settings
|
||||
self.initTree()
|
||||
|
||||
logger.debug("GuiProjectTree initialisation complete")
|
||||
# Internal Function Mapping
|
||||
self.makeAlert = self.theParent.makeAlert
|
||||
self.askQuestion = self.theParent.askQuestion
|
||||
|
||||
# Internal Mapping
|
||||
self.makeAlert = self.theParent.makeAlert
|
||||
logger.debug("GuiProjectTree initialisation complete")
|
||||
|
||||
return
|
||||
|
||||
@@ -147,8 +158,9 @@ class GuiProjectTree(QTreeWidget):
|
||||
"""Clear the GUI content and the related map.
|
||||
"""
|
||||
self.clear()
|
||||
self.theMap = {}
|
||||
self.treeChanged = False
|
||||
self._treeMap = {}
|
||||
self._treeChanged = False
|
||||
self._timeChanged = 0
|
||||
return
|
||||
|
||||
def newTreeItem(self, itemType, itemClass):
|
||||
@@ -258,10 +270,43 @@ class GuiProjectTree(QTreeWidget):
|
||||
logger.error("Failed to add new item")
|
||||
return False
|
||||
|
||||
# If there is no handle set, return here
|
||||
if tHandle is None:
|
||||
return True
|
||||
|
||||
# Add the new item to the tree
|
||||
if tHandle is not None:
|
||||
self.revealNewTreeItem(tHandle, nHandle)
|
||||
self.theParent.editItem(tHandle)
|
||||
self.revealNewTreeItem(tHandle, nHandle)
|
||||
self.theParent.editItem(tHandle)
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
|
||||
# If this is a folder, return here
|
||||
if nwItem.itemType != nwItemType.FILE:
|
||||
return True
|
||||
|
||||
# This is a new files, so let's add some content
|
||||
newDoc = NWDoc(self.theProject, self.theParent)
|
||||
curTxt = newDoc.openDocument(tHandle, showStatus=False)
|
||||
if curTxt == "":
|
||||
if nwItem.itemLayout == nwItemLayout.CHAPTER:
|
||||
newText = f"## {nwItem.itemName}\n\n"
|
||||
elif nwItem.itemLayout == nwItemLayout.UNNUMBERED:
|
||||
newText = f"## {nwItem.itemName}\n\n"
|
||||
elif nwItem.itemLayout == nwItemLayout.SCENE:
|
||||
newText = f"### {nwItem.itemName}\n\n"
|
||||
else:
|
||||
newText = f"# {nwItem.itemName}\n\n"
|
||||
|
||||
# Save the text and index it
|
||||
newDoc.saveDocument(newText)
|
||||
self.theIndex.scanText(tHandle, newText)
|
||||
|
||||
# Get Word Counts
|
||||
cC, wC, pC = self.theIndex.getCounts(tHandle)
|
||||
nwItem.setCharCount(cC)
|
||||
nwItem.setWordCount(wC)
|
||||
nwItem.setParaCount(pC)
|
||||
self.propagateCount(tHandle, wC)
|
||||
self.projectWordCount()
|
||||
|
||||
return True
|
||||
|
||||
@@ -274,10 +319,13 @@ class GuiProjectTree(QTreeWidget):
|
||||
return False
|
||||
|
||||
pHandle = nwItem.itemParent
|
||||
if pHandle is not None and pHandle in self.theMap:
|
||||
self.theMap[pHandle].setExpanded(True)
|
||||
if pHandle is not None and pHandle in self._treeMap:
|
||||
self._treeMap[pHandle].setExpanded(True)
|
||||
|
||||
self._emitItemChange(tHandle)
|
||||
self.clearSelection()
|
||||
trItem.setSelected(True)
|
||||
|
||||
return True
|
||||
|
||||
def moveTreeItem(self, nStep):
|
||||
@@ -288,7 +336,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
logger.error("No project open")
|
||||
return False
|
||||
|
||||
if qApp.focusWidget() != self:
|
||||
if not self.hasFocus():
|
||||
return False
|
||||
|
||||
tHandle = self.getSelectedHandle()
|
||||
@@ -314,10 +362,12 @@ class GuiProjectTree(QTreeWidget):
|
||||
return False
|
||||
cItem = pItem.takeChild(tIndex)
|
||||
pItem.insertChild(nIndex, cItem)
|
||||
self._recordLastMove(cItem, pItem, tIndex)
|
||||
|
||||
self.clearSelection()
|
||||
cItem.setSelected(True)
|
||||
self._setTreeChanged(True)
|
||||
self._emitItemChange(tHandle)
|
||||
|
||||
return True
|
||||
|
||||
@@ -338,7 +388,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
"""Calls saveTreeOrder if there are unsaved changes, otherwise
|
||||
does nothing.
|
||||
"""
|
||||
if self.treeChanged:
|
||||
if self._treeChanged:
|
||||
logger.verbose("Flushing project tree to project class")
|
||||
self.saveTreeOrder()
|
||||
self._setTreeChanged(False)
|
||||
@@ -391,7 +441,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
self.makeAlert("The Trash folder is already empty.", nwAlert.INFO)
|
||||
return False
|
||||
|
||||
msgYes = self.theParent.askQuestion(
|
||||
msgYes = self.askQuestion(
|
||||
"Empty Trash", "Permanently delete %d file(s) from Trash?" % nTrash
|
||||
)
|
||||
if not msgYes:
|
||||
@@ -401,14 +451,14 @@ class GuiProjectTree(QTreeWidget):
|
||||
for tHandle in self.getTreeFromHandle(trashHandle):
|
||||
if tHandle == trashHandle:
|
||||
continue
|
||||
self.deleteItem(tHandle, True)
|
||||
self.deleteItem(tHandle, alreadyAsked=True, bulkAction=True)
|
||||
|
||||
if nTrash > 0:
|
||||
self._setTreeChanged(True)
|
||||
|
||||
return True
|
||||
|
||||
def deleteItem(self, tHandle=None, alreadyAsked=False, askForTrash=False):
|
||||
def deleteItem(self, tHandle=None, alreadyAsked=False, bulkAction=False):
|
||||
"""Delete an item from the project tree. As a first step, files are
|
||||
moved to the Trash folder. Permanent deletion is a second step. This
|
||||
second step also deletes the item from the project object as well as
|
||||
@@ -419,21 +469,27 @@ class GuiProjectTree(QTreeWidget):
|
||||
logger.error("No project open")
|
||||
return False
|
||||
|
||||
if not self.hasFocus() and not bulkAction:
|
||||
logger.info("Delete action blocked due to no widget focus")
|
||||
return False
|
||||
|
||||
if tHandle is None:
|
||||
tHandle = self.getSelectedHandle()
|
||||
|
||||
if tHandle is None:
|
||||
logger.error("There is no item to delete")
|
||||
return False
|
||||
|
||||
trItemS = self._getTreeItem(tHandle)
|
||||
nwItemS = self.theProject.projTree[tHandle]
|
||||
|
||||
if trItemS is None or nwItemS is None:
|
||||
logger.error("Could not find tree item for deletion")
|
||||
return False
|
||||
|
||||
wCount = int(trItemS.data(self.C_COUNT, Qt.UserRole))
|
||||
if nwItemS.itemType == nwItemType.FILE:
|
||||
logger.debug("User requested file %s moved to trash" % tHandle)
|
||||
logger.debug("User requested file %s deleted" % tHandle)
|
||||
trItemP = trItemS.parent()
|
||||
trItemT = self._addTrashRoot()
|
||||
if trItemP is None or trItemT is None:
|
||||
@@ -446,7 +502,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
# user if they want to permanently delete the file.
|
||||
doPermanent = False
|
||||
if not alreadyAsked:
|
||||
msgYes = self.theParent.askQuestion(
|
||||
msgYes = self.askQuestion(
|
||||
"Delete File", "Permanently delete file '%s'?" % nwItemS.itemName
|
||||
)
|
||||
if msgYes:
|
||||
@@ -466,35 +522,32 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
theDoc = NWDoc(self.theProject, self.theParent)
|
||||
theDoc.deleteDocument(tHandle)
|
||||
del self.theProject.projTree[tHandle]
|
||||
self.theIndex.deleteHandle(tHandle)
|
||||
self._deleteTreeItem(tHandle)
|
||||
self._setTreeChanged(True)
|
||||
|
||||
else:
|
||||
# The file is not already in the trash folder, so we
|
||||
# move it there.
|
||||
doTrash = False
|
||||
if askForTrash:
|
||||
msgYes = self.theParent.askQuestion(
|
||||
"Delete File", "Move file '%s' to Trash?" % nwItemS.itemName
|
||||
)
|
||||
if msgYes:
|
||||
doTrash = True
|
||||
else:
|
||||
doTrash = True
|
||||
|
||||
if doTrash:
|
||||
msgYes = self.askQuestion(
|
||||
"Delete File", "Move file '%s' to Trash?" % nwItemS.itemName
|
||||
)
|
||||
if msgYes:
|
||||
if pHandle is None:
|
||||
logger.warning("File has no parent item")
|
||||
|
||||
logger.debug("Moving file %s to trash" % tHandle)
|
||||
|
||||
self.propagateCount(tHandle, 0)
|
||||
tIndex = trItemP.indexOfChild(trItemS)
|
||||
trItemC = trItemP.takeChild(tIndex)
|
||||
trItemT.addChild(trItemC)
|
||||
nwItemS.setParent(self.theProject.projTree.trashRoot())
|
||||
self._updateItemParent(tHandle)
|
||||
self.propagateCount(tHandle, wCount)
|
||||
|
||||
self._setTreeChanged(True)
|
||||
self.theIndex.deleteHandle(tHandle)
|
||||
self._recordLastMove(trItemS, trItemP, tIndex)
|
||||
self._setTreeChanged(True)
|
||||
|
||||
elif nwItemS.itemType == nwItemType.FOLDER:
|
||||
logger.debug("User requested folder %s deleted" % tHandle)
|
||||
@@ -505,7 +558,8 @@ class GuiProjectTree(QTreeWidget):
|
||||
tIndex = trItemP.indexOfChild(trItemS)
|
||||
if trItemS.childCount() == 0:
|
||||
trItemP.takeChild(tIndex)
|
||||
del self.theProject.projTree[tHandle]
|
||||
self._deleteTreeItem(tHandle)
|
||||
self._setTreeChanged(True)
|
||||
else:
|
||||
self.makeAlert((
|
||||
"Cannot delete folder. It is not empty. "
|
||||
@@ -519,7 +573,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
tIndex = self.indexOfTopLevelItem(trItemS)
|
||||
if trItemS.childCount() == 0:
|
||||
self.takeTopLevelItem(tIndex)
|
||||
del self.theProject.projTree[tHandle]
|
||||
self._deleteTreeItem(tHandle)
|
||||
self.theParent.mainMenu.setAvailableRoot()
|
||||
self._setTreeChanged(True)
|
||||
else:
|
||||
@@ -533,38 +587,34 @@ class GuiProjectTree(QTreeWidget):
|
||||
return True
|
||||
|
||||
def setTreeItemValues(self, tHandle):
|
||||
"""Set the name and flag values for a tree item.
|
||||
"""Set the name and flag values for a tree item from a handle in
|
||||
the project tree. Does not trigger a tree change as the data is
|
||||
already coming from the project tree.
|
||||
"""
|
||||
trItem = self._getTreeItem(tHandle)
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
tName = nwItem.itemName
|
||||
tClass = nwItem.itemClass
|
||||
tHandle = nwItem.itemHandle
|
||||
trItem = self._getTreeItem(tHandle)
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
|
||||
expIcon = QIcon()
|
||||
|
||||
stClass = nwLabels.CLASS_FLAG[nwItem.itemClass]
|
||||
stLayout = ""
|
||||
stFlags = nwLabels.CLASS_FLAG[nwItem.itemClass]
|
||||
if nwItem.itemType == nwItemType.FILE:
|
||||
stLayout = "."+nwLabels.LAYOUT_FLAG[nwItem.itemLayout]
|
||||
stFlags += "."+nwLabels.LAYOUT_FLAG[nwItem.itemLayout]
|
||||
if nwItem.isExported:
|
||||
expIcon = self.theTheme.getIcon("check")
|
||||
else:
|
||||
expIcon = self.theTheme.getIcon("cross")
|
||||
tStatus = stClass+stLayout
|
||||
|
||||
iStatus = nwItem.itemStatus
|
||||
if tClass == nwItemClass.NOVEL:
|
||||
if nwItem.itemClass == nwItemClass.NOVEL:
|
||||
iStatus = self.theProject.statusItems.checkEntry(iStatus) # Make sure it's valid
|
||||
flagIcon = self.theParent.statusIcons[iStatus]
|
||||
else:
|
||||
iStatus = self.theProject.importItems.checkEntry(iStatus) # Make sure it's valid
|
||||
flagIcon = self.theParent.importIcons[iStatus]
|
||||
|
||||
trItem.setText(self.C_NAME, tName)
|
||||
trItem.setText(self.C_NAME, nwItem.itemName)
|
||||
trItem.setIcon(self.C_EXPORT, expIcon)
|
||||
trItem.setText(self.C_FLAGS, tStatus)
|
||||
trItem.setIcon(self.C_FLAGS, flagIcon)
|
||||
trItem.setText(self.C_FLAGS, stFlags)
|
||||
|
||||
return
|
||||
|
||||
@@ -622,9 +672,9 @@ class GuiProjectTree(QTreeWidget):
|
||||
sent first.
|
||||
"""
|
||||
logger.debug("Building the project tree ...")
|
||||
self.clear()
|
||||
iCount = 0
|
||||
self.clearTree()
|
||||
|
||||
iCount = 0
|
||||
for nwItem in self.theProject.getProjectItems():
|
||||
iCount += 1
|
||||
self._addTreeItem(nwItem)
|
||||
@@ -632,6 +682,52 @@ class GuiProjectTree(QTreeWidget):
|
||||
logger.debug("%d items added to the project tree" % iCount)
|
||||
return True
|
||||
|
||||
def undoLastMove(self):
|
||||
"""Attempt to undo the last action.
|
||||
"""
|
||||
srcItem = self._lastMove.get("item", None)
|
||||
dstItem = self._lastMove.get("parent", None)
|
||||
dstIndex = self._lastMove.get("index", None)
|
||||
|
||||
if not self.hasFocus():
|
||||
return False
|
||||
|
||||
if srcItem is None or dstItem is None or dstIndex is None:
|
||||
logger.verbose("No tree move to undo")
|
||||
return False
|
||||
|
||||
if srcItem not in self._treeMap.values():
|
||||
logger.warning("Source item no longer exists")
|
||||
return False
|
||||
|
||||
if dstItem not in self._treeMap.values():
|
||||
logger.warning("Previous parent item no longer exists")
|
||||
return False
|
||||
|
||||
dstIndex = min(max(0, dstIndex), dstItem.childCount())
|
||||
wCount = int(srcItem.data(self.C_COUNT, Qt.UserRole))
|
||||
sHandle = srcItem.data(self.C_NAME, Qt.UserRole)
|
||||
dHandle = dstItem.data(self.C_NAME, Qt.UserRole)
|
||||
logger.debug("Moving item %s back to %s, index %d" % (
|
||||
sHandle, dHandle, dstIndex
|
||||
))
|
||||
|
||||
self.propagateCount(sHandle, 0)
|
||||
parItem = srcItem.parent()
|
||||
srcIndex = parItem.indexOfChild(srcItem)
|
||||
movItem = parItem.takeChild(srcIndex)
|
||||
dstItem.insertChild(dstIndex, movItem)
|
||||
|
||||
snItem = self.theProject.projTree[sHandle]
|
||||
dnItem = self.theProject.projTree[dHandle]
|
||||
self._postItemMove(sHandle, snItem, dnItem, wCount)
|
||||
|
||||
self.clearSelection()
|
||||
movItem.setSelected(True)
|
||||
self._lastMove = {}
|
||||
|
||||
return True
|
||||
|
||||
def getSelectedHandle(self):
|
||||
"""Get the currently selected handle. If multiple items are
|
||||
selected, return the first.
|
||||
@@ -642,21 +738,10 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
return None
|
||||
|
||||
def getSelectedHandles(self):
|
||||
"""Return a list of all currently selected item handles.
|
||||
"""
|
||||
selItems = self.selectedItems()
|
||||
selHandles = []
|
||||
for n in range(len(selItems)):
|
||||
if isinstance(selItems[n], QTreeWidgetItem):
|
||||
selHandles.append(selItems[n].data(self.C_NAME, Qt.UserRole))
|
||||
|
||||
return selHandles
|
||||
|
||||
def setSelectedHandle(self, tHandle, doScroll=False):
|
||||
"""Set a specific handle as the selected item.
|
||||
"""
|
||||
if tHandle not in self.theMap:
|
||||
if tHandle not in self._treeMap:
|
||||
return False
|
||||
|
||||
tItem = self._getTreeItem(tHandle)
|
||||
@@ -664,7 +749,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
return False
|
||||
|
||||
self.clearSelection()
|
||||
self.theMap[tHandle].setSelected(True)
|
||||
self._treeMap[tHandle].setSelected(True)
|
||||
|
||||
selItems = self.selectedIndexes()
|
||||
if selItems and doScroll:
|
||||
@@ -672,6 +757,11 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
return True
|
||||
|
||||
def changedSince(self, checkTime):
|
||||
"""Check if the tree has changed since a given time.
|
||||
"""
|
||||
return self._timeChanged > checkTime
|
||||
|
||||
##
|
||||
# Slots
|
||||
##
|
||||
@@ -699,7 +789,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
def mousePressEvent(self, theEvent):
|
||||
"""Overload mousePressEvent to clear selection if clicking the
|
||||
mouse in a blank area of the tree view, and to load a document
|
||||
for viewing if the suer middle clicked.
|
||||
for viewing if the user middle-clicked.
|
||||
"""
|
||||
QTreeWidget.mousePressEvent(self, theEvent)
|
||||
|
||||
@@ -746,42 +836,30 @@ class GuiProjectTree(QTreeWidget):
|
||||
self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR)
|
||||
return
|
||||
|
||||
pItem = sItem.parent()
|
||||
pIndex = 0
|
||||
if pItem is not None:
|
||||
pIndex = pItem.indexOfChild(sItem)
|
||||
|
||||
wCount = int(sItem.data(self.C_COUNT, Qt.UserRole))
|
||||
isFile = snItem.itemType == nwItemType.FILE
|
||||
isRoot = snItem.itemType == nwItemType.ROOT
|
||||
onFile = dnItem.itemType == nwItemType.FILE
|
||||
|
||||
isSame = snItem.itemClass == dnItem.itemClass
|
||||
isNone = snItem.itemClass == nwItemClass.NO_CLASS
|
||||
isNote = snItem.itemLayout == nwItemLayout.NOTE
|
||||
onFile = dnItem.itemType == nwItemType.FILE
|
||||
isRoot = snItem.itemType == nwItemType.ROOT
|
||||
onFree = dnItem.itemClass == nwItemClass.ARCHIVE
|
||||
onFree |= dnItem.itemClass == nwItemClass.TRASH
|
||||
onFree &= snItem.itemType == nwItemType.FILE
|
||||
isOnTop = self.dropIndicatorPosition() == QAbstractItemView.OnItem
|
||||
if (isSame or isNone or isNote or onFree) and not (onFile and isOnTop) and not isRoot:
|
||||
onFree = dnItem.itemClass in nwLists.FREE_CLASS and isFile
|
||||
|
||||
allowDrop = isSame or isNone or isNote or onFree
|
||||
allowDrop &= not (self.dropIndicatorPosition() == QAbstractItemView.OnItem and onFile)
|
||||
|
||||
if allowDrop and not isRoot:
|
||||
logger.debug("Drag'n'drop of item %s accepted" % sHandle)
|
||||
self.propagateCount(sHandle, 0)
|
||||
QTreeWidget.dropEvent(self, theEvent)
|
||||
self._updateItemParent(sHandle)
|
||||
|
||||
# If the item does not have the same class as the target,
|
||||
# and the target is not a free root folder, update its class
|
||||
if not (isSame or onFree):
|
||||
logger.debug("Item %s class has been changed from %s to %s" % (
|
||||
sHandle,
|
||||
snItem.itemClass.name,
|
||||
dnItem.itemClass.name
|
||||
))
|
||||
snItem.setClass(dnItem.itemClass)
|
||||
self.setTreeItemValues(sHandle)
|
||||
|
||||
self.propagateCount(sHandle, wCount)
|
||||
|
||||
# The items dropped into archive or trash should be removed
|
||||
# from the project index, for all other items, we rescan the
|
||||
# file to ensure the index is up to date.
|
||||
if onFree:
|
||||
self.theIndex.deleteHandle(sHandle)
|
||||
else:
|
||||
self.theIndex.reIndexHandle(sHandle)
|
||||
self._postItemMove(sHandle, snItem, dnItem, wCount)
|
||||
self._recordLastMove(sItem, pItem, pIndex)
|
||||
|
||||
else:
|
||||
theEvent.ignore()
|
||||
@@ -794,22 +872,63 @@ class GuiProjectTree(QTreeWidget):
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _postItemMove(self, sHandle, snItem, dnItem, wCount):
|
||||
"""Run various maintenance tasks for a moved item.
|
||||
"""
|
||||
isFile = snItem.itemType == nwItemType.FILE
|
||||
isSame = snItem.itemClass == dnItem.itemClass
|
||||
onFree = dnItem.itemClass in nwLists.FREE_CLASS and isFile
|
||||
|
||||
self._updateItemParent(sHandle)
|
||||
|
||||
# If the item does not have the same class as the target,
|
||||
# and the target is not a free root folder, update its class
|
||||
if not (isSame or onFree):
|
||||
logger.debug("Item %s class has been changed from %s to %s" % (
|
||||
sHandle, snItem.itemClass.name, dnItem.itemClass.name
|
||||
))
|
||||
snItem.setClass(dnItem.itemClass)
|
||||
self.setTreeItemValues(sHandle)
|
||||
|
||||
self.propagateCount(sHandle, wCount)
|
||||
|
||||
# The items dropped into archive or trash should be removed
|
||||
# from the project index, for all other items, we rescan the
|
||||
# file to ensure the index is up to date.
|
||||
if onFree:
|
||||
self.theIndex.deleteHandle(sHandle)
|
||||
else:
|
||||
self.theIndex.reIndexHandle(sHandle)
|
||||
|
||||
# Trigger dependent updates
|
||||
self._setTreeChanged(True)
|
||||
self._emitItemChange(sHandle)
|
||||
|
||||
return
|
||||
|
||||
def _getTreeItem(self, tHandle):
|
||||
"""Returns the QTreeWidgetItem of a given item handle.
|
||||
"""
|
||||
return self.theMap.get(tHandle, None)
|
||||
return self._treeMap.get(tHandle, None)
|
||||
|
||||
def _scanChildren(self, theList, theItem, theIndex):
|
||||
def _deleteTreeItem(self, tHandle):
|
||||
"""Delete a tree item from the project and the map.
|
||||
"""
|
||||
del self.theProject.projTree[tHandle]
|
||||
self._treeMap.pop(tHandle, None)
|
||||
return
|
||||
|
||||
def _scanChildren(self, theList, tItem, tIndex):
|
||||
"""This is a recursive function returning all items in a tree
|
||||
starting at a given QTreeWidgetItem.
|
||||
"""
|
||||
tHandle = theItem.data(self.C_NAME, Qt.UserRole)
|
||||
tHandle = tItem.data(self.C_NAME, Qt.UserRole)
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
nwItem.setExpanded(theItem.isExpanded())
|
||||
nwItem.setOrder(theIndex)
|
||||
nwItem.setExpanded(tItem.isExpanded())
|
||||
nwItem.setOrder(tIndex)
|
||||
theList.append(tHandle)
|
||||
for i in range(theItem.childCount()):
|
||||
self._scanChildren(theList, theItem.child(i), i)
|
||||
for i in range(tItem.childCount()):
|
||||
self._scanChildren(theList, tItem.child(i), i)
|
||||
return theList
|
||||
|
||||
def _addTreeItem(self, nwItem, nHandle=None):
|
||||
@@ -834,7 +953,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
newItem.setData(self.C_NAME, Qt.UserRole, tHandle)
|
||||
newItem.setData(self.C_COUNT, Qt.UserRole, 0)
|
||||
|
||||
self.theMap[tHandle] = newItem
|
||||
self._treeMap[tHandle] = newItem
|
||||
if pHandle is None:
|
||||
if nwItem.itemType == nwItemType.ROOT:
|
||||
self.addTopLevelItem(newItem)
|
||||
@@ -845,20 +964,20 @@ class GuiProjectTree(QTreeWidget):
|
||||
self.makeAlert(
|
||||
"There is nowhere to add item with name '%s'" % nwItem.itemName, nwAlert.ERROR
|
||||
)
|
||||
del self.theMap[tHandle]
|
||||
del self._treeMap[tHandle]
|
||||
return None
|
||||
|
||||
else:
|
||||
byIndex = -1
|
||||
if nHandle is not None and nHandle in self.theMap:
|
||||
if nHandle is not None and nHandle in self._treeMap:
|
||||
try:
|
||||
byIndex = self.theMap[pHandle].indexOfChild(self.theMap[nHandle])
|
||||
byIndex = self._treeMap[pHandle].indexOfChild(self._treeMap[nHandle])
|
||||
except Exception:
|
||||
logger.error("Failed to get index of item with handle %s" % nHandle)
|
||||
if byIndex >= 0:
|
||||
self.theMap[pHandle].insertChild(byIndex+1, newItem)
|
||||
self._treeMap[pHandle].insertChild(byIndex+1, newItem)
|
||||
else:
|
||||
self.theMap[pHandle].addChild(newItem)
|
||||
self._treeMap[pHandle].addChild(newItem)
|
||||
self.propagateCount(tHandle, nwItem.wordCount)
|
||||
|
||||
self.setTreeItemValues(tHandle)
|
||||
@@ -910,7 +1029,6 @@ class GuiProjectTree(QTreeWidget):
|
||||
pHandle = trItemP.data(self.C_NAME, Qt.UserRole)
|
||||
nwItemS.setParent(pHandle)
|
||||
self.setTreeItemValues(tHandle)
|
||||
self._setTreeChanged(True)
|
||||
|
||||
logger.debug("The parent of item %s has been changed to %s" % (tHandle, pHandle))
|
||||
|
||||
@@ -919,11 +1037,40 @@ class GuiProjectTree(QTreeWidget):
|
||||
def _setTreeChanged(self, theState):
|
||||
"""Set the tree change flag, and propagate to the project.
|
||||
"""
|
||||
self.treeChanged = theState
|
||||
self._treeChanged = theState
|
||||
if theState:
|
||||
self._timeChanged = time()
|
||||
self.theProject.setProjectChanged(True)
|
||||
return
|
||||
|
||||
def _emitItemChange(self, tHandle):
|
||||
"""Emit an item change signal for a given handle.
|
||||
"""
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
if nwItem is None:
|
||||
return
|
||||
|
||||
if nwItem.itemType == nwItemType.FILE:
|
||||
if nwItem.itemClass == nwItemClass.NOVEL:
|
||||
self.novelItemChanged.emit()
|
||||
else:
|
||||
self.noteItemChanged.emit()
|
||||
|
||||
return
|
||||
|
||||
def _recordLastMove(self, srcItem, parItem, parIndex):
|
||||
"""Record the last action so that it can be undone.
|
||||
"""
|
||||
prevItem = self._lastMove.get("item", None)
|
||||
if prevItem is None or srcItem != prevItem:
|
||||
self._lastMove = {
|
||||
"item": srcItem,
|
||||
"parent": parItem,
|
||||
"index": parIndex,
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
# END Class GuiProjectTree
|
||||
|
||||
class GuiProjectTreeMenu(QMenu):
|
||||
@@ -1056,7 +1203,7 @@ class GuiProjectTreeMenu(QMenu):
|
||||
"""Forward the delete item call to the project tree.
|
||||
"""
|
||||
if self.theItem is not None:
|
||||
self.theTree.deleteItem(askForTrash=True)
|
||||
self.theTree.deleteItem()
|
||||
return
|
||||
|
||||
def _doEmptyTrash(self):
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI New Project Wizard
|
||||
"""
|
||||
novelWriter – GUI New Project Wizard
|
||||
====================================
|
||||
GUI classes for the new project wizard dialog
|
||||
|
||||
novelWriter – GUI New project Wizard
|
||||
======================================
|
||||
Class holding the new project wizard dialog
|
||||
File History:
|
||||
Created: 2020-07-11 [0.10.1]
|
||||
|
||||
File History:
|
||||
Created: 2020-07-11 [0.10.1]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI Main Window StatusBar
|
||||
"""
|
||||
novelWriter – GUI Main Window Status Bar
|
||||
========================================
|
||||
GUI class for the main window status bar
|
||||
|
||||
novelWriter – GUI Main Window StatusBar
|
||||
=========================================
|
||||
Class holding the main window status bar
|
||||
File History:
|
||||
Created: 2019-04-20 [0.0.1]
|
||||
|
||||
File History:
|
||||
Created: 2019-04-20 [0.0.1]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
@@ -30,7 +29,6 @@ import logging
|
||||
|
||||
from time import time
|
||||
|
||||
from PyQt5.QtCore import QTimer
|
||||
from PyQt5.QtGui import QColor, QPainter
|
||||
from PyQt5.QtWidgets import qApp, QStatusBar, QLabel, QAbstractButton
|
||||
|
||||
@@ -50,9 +48,7 @@ class GuiMainStatus(QStatusBar):
|
||||
self.theParent = theParent
|
||||
self.theTheme = theParent.theTheme
|
||||
self.refTime = None
|
||||
|
||||
self.projWords = 0
|
||||
self.sessWords = 0
|
||||
self.userIdle = False
|
||||
|
||||
colNone = QColor(*self.theTheme.statNone)
|
||||
colTrue = QColor(*self.theTheme.statUnsaved)
|
||||
@@ -101,9 +97,12 @@ class GuiMainStatus(QStatusBar):
|
||||
|
||||
## The Session Clock
|
||||
### Set the mimimum width so the label doesn't rescale every second
|
||||
self.timePixmap = self.theTheme.getPixmap("status_time", (iPx, iPx))
|
||||
self.idlePixmap = self.theTheme.getPixmap("status_idle", (iPx, iPx))
|
||||
|
||||
self.timeIcon = QLabel()
|
||||
self.timeText = QLabel("")
|
||||
self.timeIcon.setPixmap(self.theTheme.getPixmap("status_time", (iPx, iPx)))
|
||||
self.timeIcon.setPixmap(self.timePixmap)
|
||||
self.timeText.setToolTip("Session Time")
|
||||
self.timeText.setMinimumWidth(self.theTheme.getTextWidth("00:00:00:"))
|
||||
self.timeIcon.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -114,12 +113,6 @@ class GuiMainStatus(QStatusBar):
|
||||
# Other Settings
|
||||
self.setSizeGripEnabled(True)
|
||||
|
||||
# Start the Clock
|
||||
self.sessionTimer = QTimer()
|
||||
self.sessionTimer.setInterval(1000)
|
||||
self.sessionTimer.timeout.connect(self._updateTime)
|
||||
self.sessionTimer.start()
|
||||
|
||||
logger.debug("GuiMainStatus initialisation complete")
|
||||
|
||||
self.clearStatus()
|
||||
@@ -134,7 +127,7 @@ class GuiMainStatus(QStatusBar):
|
||||
self.setStats(0, 0)
|
||||
self.setProjectStatus(None)
|
||||
self.setDocumentStatus(None)
|
||||
self._updateTime()
|
||||
self.updateTime()
|
||||
return True
|
||||
|
||||
##
|
||||
@@ -182,33 +175,37 @@ class GuiMainStatus(QStatusBar):
|
||||
def setStats(self, pWC, sWC):
|
||||
"""Set the current project statistics.
|
||||
"""
|
||||
self.projWords = pWC
|
||||
self.sessWords = sWC
|
||||
self._updateStats()
|
||||
self.statsText.setText(f"Words: {pWC:n} ({sWC:+n})")
|
||||
self.statsText.setToolTip("Project word count (session change)")
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _updateStats(self):
|
||||
"""Update statistics.
|
||||
def setUserIdle(self, userIdle):
|
||||
"""Change the idle status icon.
|
||||
"""
|
||||
self.statsText.setToolTip(
|
||||
"Project word count (session change)"
|
||||
)
|
||||
self.statsText.setText(
|
||||
f"Words: {self.projWords:n} ({self.sessWords:+n})"
|
||||
)
|
||||
if not self.mainConf.stopWhenIdle:
|
||||
userIdle = False
|
||||
|
||||
if self.userIdle != userIdle:
|
||||
if userIdle:
|
||||
self.timeIcon.setPixmap(self.idlePixmap)
|
||||
else:
|
||||
self.timeIcon.setPixmap(self.timePixmap)
|
||||
|
||||
self.userIdle = userIdle
|
||||
|
||||
return
|
||||
|
||||
def _updateTime(self):
|
||||
def updateTime(self, idleTime=0.0):
|
||||
"""Update the session clock.
|
||||
"""
|
||||
if self.refTime is None:
|
||||
self.timeText.setText("00:00:00")
|
||||
else:
|
||||
self.timeText.setText(formatTime(round(time() - self.refTime)))
|
||||
if self.mainConf.stopWhenIdle:
|
||||
sessTime = round(time() - self.refTime - idleTime)
|
||||
else:
|
||||
sessTime = round(time() - self.refTime)
|
||||
self.timeText.setText(formatTime(sessTime))
|
||||
return
|
||||
|
||||
# END Class GuiMainStatus
|
||||
|
||||
114
nw/gui/theme.py
@@ -1,29 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Theme and Icons Classes
|
||||
"""
|
||||
novelWriter – Theme and Icons Classes
|
||||
=====================================
|
||||
Classes managing and caching themes and icons
|
||||
|
||||
novelWriter – Theme and Icons Classs
|
||||
======================================
|
||||
Class managing and caching themes and icons
|
||||
File History:
|
||||
Created: 2019-05-18 [0.1.3] GuiTheme
|
||||
Created: 2019-11-08 [0.4.0] GuiIcons
|
||||
|
||||
File History:
|
||||
Created: 2019-05-18 [0.1.3] GuiTheme
|
||||
Created: 2019-11-08 [0.4.0] GuiIcons
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
@@ -76,11 +75,10 @@ class GuiTheme:
|
||||
self.themeLicenseUrl = ""
|
||||
|
||||
## GUI
|
||||
self.treeWCount = [0, 0, 0]
|
||||
self.statNone = [120, 120, 120]
|
||||
self.statUnsaved = [120, 120, 40]
|
||||
self.statSaved = [40, 120, 0]
|
||||
self.helpText = [0, 0, 0]
|
||||
self.statUnsaved = [200, 15, 39]
|
||||
self.statSaved = [2, 133, 37]
|
||||
self.helpText = [0, 0, 0]
|
||||
|
||||
# Loaded Syntax Settings
|
||||
|
||||
@@ -181,11 +179,14 @@ class GuiTheme:
|
||||
def loadFonts(self):
|
||||
"""Add the fonts in the assets fonts folder to the app.
|
||||
"""
|
||||
logger.debug("Loading additional fonts")
|
||||
|
||||
ttfList = []
|
||||
fontAssets = os.path.join(self.mainConf.assetPath, self.fontPath)
|
||||
for fontFam in os.listdir(fontAssets):
|
||||
fontDir = os.path.join(fontAssets, fontFam)
|
||||
if os.path.isdir(fontDir):
|
||||
logger.verbose("Found font: %s" % fontFam)
|
||||
if fontFam not in self.guiFontDB.families():
|
||||
for fontFile in os.listdir(fontDir):
|
||||
ttfFile = os.path.join(fontDir, fontFile)
|
||||
@@ -193,22 +194,26 @@ class GuiTheme:
|
||||
ttfList.append(ttfFile)
|
||||
|
||||
for ttfFile in ttfList:
|
||||
logger.verbose("Font asset: %s" % os.path.relpath(ttfFile))
|
||||
relPath = os.path.relpath(ttfFile, fontAssets)
|
||||
logger.verbose("Adding font: %s" % relPath)
|
||||
fontID = self.guiFontDB.addApplicationFont(ttfFile)
|
||||
if fontID < 0:
|
||||
logger.error("Failed to add font: %s" % os.path.relpath(ttfFile))
|
||||
logger.error("Failed to add font: %s" % relPath)
|
||||
|
||||
return
|
||||
|
||||
def updateFont(self):
|
||||
"""Updated the GUI's font style from settings,
|
||||
"""Update the GUI's font style from settings.
|
||||
"""
|
||||
theFont = QFont()
|
||||
if self.mainConf.guiFont not in self.guiFontDB.families():
|
||||
if self.mainConf.osWindows:
|
||||
# On Windows, default to Cantarell provided by novelWriter
|
||||
theFont.setFamily("Cantarell")
|
||||
theFont.setPointSize(11)
|
||||
if "Arial" in self.guiFontDB.families():
|
||||
theFont.setFamily("Arial")
|
||||
else:
|
||||
# On Windows, fall back to Cantarell provided by novelWriter
|
||||
theFont.setFamily("Cantarell")
|
||||
theFont.setPointSize(10)
|
||||
else:
|
||||
theFont = self.guiFontDB.systemFont(QFontDatabase.GeneralFont)
|
||||
self.mainConf.guiFont = theFont.family()
|
||||
@@ -252,7 +257,8 @@ class GuiTheme:
|
||||
return True
|
||||
|
||||
def loadTheme(self):
|
||||
|
||||
"""Load the currently specified GUI theme.
|
||||
"""
|
||||
logger.debug("Loading theme files")
|
||||
logger.debug("System icon theme is '%s'" % str(QIcon.themeName()))
|
||||
|
||||
@@ -262,9 +268,9 @@ class GuiTheme:
|
||||
if os.path.isfile(self.cssFile):
|
||||
with open(self.cssFile, mode="r", encoding="utf8") as inFile:
|
||||
cssData = inFile.read()
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.error("Could not load theme css file")
|
||||
logger.error(str(e))
|
||||
nw.logException()
|
||||
return False
|
||||
|
||||
# Config File
|
||||
@@ -272,9 +278,9 @@ class GuiTheme:
|
||||
try:
|
||||
with open(self.confFile, mode="r", encoding="utf8") as inFile:
|
||||
confParser.read_file(inFile)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.error("Could not load theme settings from: %s" % self.confFile)
|
||||
logger.error(str(e))
|
||||
nw.logException()
|
||||
return False
|
||||
|
||||
## Main
|
||||
@@ -309,7 +315,6 @@ class GuiTheme:
|
||||
## GUI
|
||||
cnfSec = "GUI"
|
||||
if confParser.has_section(cnfSec):
|
||||
self.treeWCount = self._loadColour(confParser, cnfSec, "treewordcount")
|
||||
self.statNone = self._loadColour(confParser, cnfSec, "statusnone")
|
||||
self.statUnsaved = self._loadColour(confParser, cnfSec, "statusunsaved")
|
||||
self.statSaved = self._loadColour(confParser, cnfSec, "statussaved")
|
||||
@@ -323,14 +328,17 @@ class GuiTheme:
|
||||
return True
|
||||
|
||||
def loadSyntax(self):
|
||||
"""Load the currently specified syntax highlighter theme.
|
||||
"""
|
||||
logger.debug("Loading syntax theme files")
|
||||
|
||||
confParser = configparser.ConfigParser()
|
||||
try:
|
||||
with open(self.syntaxFile, mode="r", encoding="utf8") as inFile:
|
||||
confParser.read_file(inFile)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.error("Could not load syntax colours from: %s" % self.syntaxFile)
|
||||
logger.error(str(e))
|
||||
nw.logException()
|
||||
return False
|
||||
|
||||
## Main
|
||||
@@ -369,7 +377,7 @@ class GuiTheme:
|
||||
return True
|
||||
|
||||
def listThemes(self):
|
||||
"""Scan the gui themes folder and list all themes.
|
||||
"""Scan the GUI themes folder and list all themes.
|
||||
"""
|
||||
if self.themeList:
|
||||
return self.themeList
|
||||
@@ -473,6 +481,9 @@ class GuiTheme:
|
||||
return
|
||||
|
||||
def _parseLine(self, confParser, cnfSec, cnfName, cnfDefault):
|
||||
"""Simple wrapper for the config parser to check that the entry
|
||||
exists before attempting to load it.
|
||||
"""
|
||||
if confParser.has_section(cnfSec):
|
||||
if confParser.has_option(cnfSec, cnfName):
|
||||
return confParser.get(cnfSec, cnfName)
|
||||
@@ -500,12 +511,13 @@ class GuiIcons:
|
||||
the ICON_MAP data tuple[0]. This will let Qt pull the closest
|
||||
system icon.
|
||||
* Third action is to look up the freedesktop icon theme name using
|
||||
the fromTheme Qt call. This generally produces the same results
|
||||
the fromTheme Qt call. This generally produces the same result
|
||||
as the step above, but has more icons available in other cases.
|
||||
* Fourth, and finally, the icon is looked up in the fallback
|
||||
folder. Files in this folder must have the same file name as the
|
||||
novelWriter internal icon key, with '-dark' appended to it for
|
||||
the dark background version of the icon.
|
||||
novelWriter internal icon key, with '-dark' appended to them for
|
||||
the dark background version of the icon. If no dark icon exists,
|
||||
the non-dark version will be returned.
|
||||
"""
|
||||
|
||||
ICON_MAP = {
|
||||
@@ -527,8 +539,10 @@ class GuiIcons:
|
||||
"proj_nwx" : (None, None),
|
||||
"status_lang" : (None, None),
|
||||
"status_time" : (None, None),
|
||||
"status_idle" : (None, None),
|
||||
"status_stats" : (None, None),
|
||||
"status_lines" : (None, None),
|
||||
"doc_h0" : (QStyle.SP_FileIcon, "x-office-document"),
|
||||
"doc_h1" : (QStyle.SP_FileIcon, "x-office-document"),
|
||||
"doc_h2" : (QStyle.SP_FileIcon, "x-office-document"),
|
||||
"doc_h3" : (QStyle.SP_FileIcon, "x-office-document"),
|
||||
@@ -562,6 +576,7 @@ class GuiIcons:
|
||||
"reference" : (None, None),
|
||||
"backward" : (None, None),
|
||||
"forward" : (None, None),
|
||||
"settings" : (None, None),
|
||||
|
||||
## Switches
|
||||
"sticky-on" : (None, None),
|
||||
@@ -610,7 +625,6 @@ class GuiIcons:
|
||||
the GUI icons cannot really be replaced without writing specific
|
||||
update functions for the classes where they're used.
|
||||
"""
|
||||
|
||||
logger.debug("Loading icon theme files")
|
||||
|
||||
self.themeMap = {}
|
||||
@@ -627,9 +641,9 @@ class GuiIcons:
|
||||
try:
|
||||
with open(self.confFile, mode="r", encoding="utf8") as inFile:
|
||||
confParser.read_file(inFile)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.error("Could not load icon theme settings from: %s" % self.confFile)
|
||||
logger.error(str(e))
|
||||
nw.logException()
|
||||
return False
|
||||
|
||||
## Main
|
||||
@@ -747,7 +761,7 @@ class GuiIcons:
|
||||
##
|
||||
|
||||
def _loadIcon(self, iconKey):
|
||||
"""Load an icon from the assets or theme folder, with a
|
||||
"""Load an icon from the assets or themes folder, with a
|
||||
preference for dark/light icons depending on theme type, if such
|
||||
an icon exists. Prefer svg files over png files. Always returns
|
||||
a QIcon.
|
||||
@@ -763,7 +777,8 @@ class GuiIcons:
|
||||
# Otherwise, we start looking for it
|
||||
# First in the theme folder
|
||||
if iconKey in self.themeMap:
|
||||
logger.verbose("Loading: %s" % os.path.relpath(self.themeMap[iconKey]))
|
||||
relPath = os.path.relpath(self.themeMap[iconKey], self.mainConf.iconPath)
|
||||
logger.verbose("Loading: %s" % relPath)
|
||||
return QIcon(self.themeMap[iconKey])
|
||||
|
||||
# Next, we try to load the Qt style icons
|
||||
@@ -785,6 +800,7 @@ class GuiIcons:
|
||||
if os.path.isfile(fbackIcon):
|
||||
logger.verbose("Loading icon '%s' from fallback theme (dark mode)" % iconKey)
|
||||
return QIcon(fbackIcon)
|
||||
|
||||
fbackIcon = os.path.join(self.mainConf.iconPath, self.fbackName, "%s.svg" % iconKey)
|
||||
if os.path.isfile(fbackIcon):
|
||||
logger.verbose("Loading icon '%s' from fallback theme (light mode)" % iconKey)
|
||||
@@ -796,8 +812,8 @@ class GuiIcons:
|
||||
return QIcon()
|
||||
|
||||
def _parseLine(self, confParser, cnfSec, cnfName, cnfDefault):
|
||||
"""Simple wrapper for the config parser check for entry existing
|
||||
before arrempting to load.
|
||||
"""Simple wrapper for the config parser to check that the entry
|
||||
exists before attempting to load it.
|
||||
"""
|
||||
if confParser.has_section(cnfSec):
|
||||
if confParser.has_option(cnfSec, cnfName):
|
||||
|
||||
213
nw/gui/wordlist.py
Normal file
@@ -0,0 +1,213 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
novelWriter – GUI User Wordlist
|
||||
===============================
|
||||
Class holding the user's wordlist dialog
|
||||
|
||||
File History:
|
||||
Created: 2021-02-12 [1.2b1]
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but
|
||||
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
import logging
|
||||
import os
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, QListWidget,
|
||||
QAbstractItemView, QPushButton, QLineEdit, QLabel
|
||||
)
|
||||
|
||||
from nw.constants import nwFiles, nwAlert
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class GuiWordList(QDialog):
|
||||
|
||||
def __init__(self, theParent, theProject):
|
||||
QDialog.__init__(self, theParent)
|
||||
|
||||
logger.debug("Initialising GuiWordList ...")
|
||||
self.setObjectName("GuiWordList")
|
||||
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theParent = theParent
|
||||
self.theTheme = theParent.theTheme
|
||||
self.theProject = theProject
|
||||
self.optState = theProject.optState
|
||||
|
||||
self.setWindowTitle("Project Word List")
|
||||
|
||||
mS = self.mainConf.pxInt(250)
|
||||
wW = self.mainConf.pxInt(320)
|
||||
wH = self.mainConf.pxInt(340)
|
||||
|
||||
self.setMinimumWidth(mS)
|
||||
self.setMinimumHeight(mS)
|
||||
self.resize(
|
||||
self.mainConf.pxInt(self.optState.getInt("GuiWordList", "winWidth", wW)),
|
||||
self.mainConf.pxInt(self.optState.getInt("GuiWordList", "winHeight", wH))
|
||||
)
|
||||
|
||||
# Main Widgets
|
||||
# ============
|
||||
|
||||
self.headLabel = QLabel("<b>Project Word List</b>")
|
||||
|
||||
self.listBox = QListWidget()
|
||||
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
|
||||
self.listBox.setSortingEnabled(True)
|
||||
|
||||
self.newEntry = QLineEdit()
|
||||
|
||||
self.addButton = QPushButton(self.theTheme.getIcon("add"), "")
|
||||
self.addButton.setToolTip("Add new entry")
|
||||
self.addButton.clicked.connect(self._doAdd)
|
||||
|
||||
self.delButton = QPushButton(self.theTheme.getIcon("remove"), "")
|
||||
self.delButton.setToolTip("Delete selected entry")
|
||||
self.delButton.clicked.connect(self._doDelete)
|
||||
|
||||
self.editBox = QHBoxLayout()
|
||||
self.editBox.addWidget(self.newEntry, 1)
|
||||
self.editBox.addWidget(self.addButton, 0)
|
||||
self.editBox.addWidget(self.delButton, 0)
|
||||
|
||||
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Close)
|
||||
self.buttonBox.accepted.connect(self._doSave)
|
||||
self.buttonBox.rejected.connect(self._doClose)
|
||||
|
||||
# Assemble
|
||||
# ========
|
||||
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.outerBox.addWidget(self.headLabel)
|
||||
self.outerBox.addSpacing(self.mainConf.pxInt(8))
|
||||
self.outerBox.addWidget(self.listBox, 1)
|
||||
self.outerBox.addLayout(self.editBox, 0)
|
||||
self.outerBox.addSpacing(self.mainConf.pxInt(12))
|
||||
self.outerBox.addWidget(self.buttonBox, 0)
|
||||
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
self._loadWordList()
|
||||
|
||||
logger.debug("GuiWordList initialisation complete")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Slots
|
||||
##
|
||||
|
||||
def _doAdd(self):
|
||||
"""Add a new word to the word list.
|
||||
"""
|
||||
newWord = self.newEntry.text().strip()
|
||||
if newWord == "":
|
||||
self.theParent.makeAlert("Cannot add a blank word.", nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
if self.listBox.findItems(newWord, Qt.MatchExactly):
|
||||
self.theParent.makeAlert(
|
||||
"The word '%s' is already in the word list." % newWord, nwAlert.ERROR
|
||||
)
|
||||
return False
|
||||
|
||||
self.listBox.addItem(newWord)
|
||||
self.newEntry.setText("")
|
||||
|
||||
return True
|
||||
|
||||
def _doDelete(self):
|
||||
"""Delete the selected item.
|
||||
"""
|
||||
selItem = self.listBox.selectedItems()
|
||||
if selItem:
|
||||
self.listBox.takeItem(self.listBox.row(selItem[0]))
|
||||
return
|
||||
|
||||
def _doSave(self):
|
||||
"""Save the new word list and close.
|
||||
"""
|
||||
self._saveGuiSettings()
|
||||
|
||||
dctFile = os.path.join(self.theProject.projMeta, nwFiles.PROJ_DICT)
|
||||
tmpFile = dctFile + "~"
|
||||
|
||||
try:
|
||||
with open(tmpFile, mode="w", encoding="utf8") as outFile:
|
||||
for i in range(self.listBox.count()):
|
||||
outFile.write(self.listBox.item(i).text() + "\n")
|
||||
|
||||
except Exception:
|
||||
logger.error("Could not save new word list")
|
||||
nw.logException()
|
||||
self.reject()
|
||||
return False
|
||||
|
||||
if os.path.isfile(dctFile):
|
||||
os.unlink(dctFile)
|
||||
os.rename(tmpFile, dctFile)
|
||||
self.accept()
|
||||
|
||||
return True
|
||||
|
||||
def _doClose(self):
|
||||
"""Close without saving the word list.
|
||||
"""
|
||||
self._saveGuiSettings()
|
||||
self.reject()
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _loadWordList(self):
|
||||
"""Load the project's word list, if it exists.
|
||||
"""
|
||||
self.listBox.clear()
|
||||
|
||||
wordList = os.path.join(self.theProject.projMeta, nwFiles.PROJ_DICT)
|
||||
if not os.path.isfile(wordList):
|
||||
logger.debug("No project dictionary file found")
|
||||
return False
|
||||
|
||||
with open(wordList, mode="r", encoding="utf8") as inFile:
|
||||
for inLine in inFile:
|
||||
theWord = inLine.strip()
|
||||
if len(theWord) == 0:
|
||||
continue
|
||||
self.listBox.addItem(theWord)
|
||||
|
||||
return True
|
||||
|
||||
def _saveGuiSettings(self):
|
||||
"""Save GUI settings.
|
||||
"""
|
||||
winWidth = self.mainConf.rpxInt(self.width())
|
||||
winHeight = self.mainConf.rpxInt(self.height())
|
||||
|
||||
self.optState.setValue("GuiWordList", "winWidth", winWidth)
|
||||
self.optState.setValue("GuiWordList", "winHeight", winHeight)
|
||||
|
||||
return
|
||||
|
||||
# END Class GuiWordList
|
||||
@@ -1,28 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI Writing Statistics
|
||||
"""
|
||||
novelWriter – GUI Writing Statistics
|
||||
====================================
|
||||
GUI class for the session statistics dialog
|
||||
|
||||
novelWriter – GUI Writing Statistics
|
||||
======================================
|
||||
Class holding the word count and session statistics dialog
|
||||
File History:
|
||||
Created: 2019-10-20 [0.3]
|
||||
|
||||
File History:
|
||||
Created: 2019-10-20 [0.3]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
@@ -49,8 +48,9 @@ class GuiWritingStats(QDialog):
|
||||
|
||||
C_TIME = 0
|
||||
C_LENGTH = 1
|
||||
C_COUNT = 2
|
||||
C_BAR = 3
|
||||
C_IDLE = 2
|
||||
C_COUNT = 3
|
||||
C_BAR = 4
|
||||
|
||||
FMT_JSON = 0
|
||||
FMT_CSV = 1
|
||||
@@ -90,16 +90,21 @@ class GuiWritingStats(QDialog):
|
||||
wCol2 = self.mainConf.pxInt(
|
||||
self.optState.getInt("GuiWritingStats", "widthCol2", 80)
|
||||
)
|
||||
wCol3 = self.mainConf.pxInt(
|
||||
self.optState.getInt("GuiWritingStats", "widthCol3", 80)
|
||||
)
|
||||
|
||||
self.listBox = QTreeWidget()
|
||||
self.listBox.setHeaderLabels(["Session Start", "Length", "Words", "Histogram"])
|
||||
self.listBox.setHeaderLabels(["Session Start", "Length", "Idle", "Words", "Histogram"])
|
||||
self.listBox.setIndentation(0)
|
||||
self.listBox.setColumnWidth(self.C_TIME, wCol0)
|
||||
self.listBox.setColumnWidth(self.C_LENGTH, wCol1)
|
||||
self.listBox.setColumnWidth(self.C_COUNT, wCol2)
|
||||
self.listBox.setColumnWidth(self.C_IDLE, wCol2)
|
||||
self.listBox.setColumnWidth(self.C_COUNT, wCol3)
|
||||
|
||||
hHeader = self.listBox.headerItem()
|
||||
hHeader.setTextAlignment(self.C_LENGTH, Qt.AlignRight)
|
||||
hHeader.setTextAlignment(self.C_IDLE, Qt.AlignRight)
|
||||
hHeader.setTextAlignment(self.C_COUNT, Qt.AlignRight)
|
||||
|
||||
sortValid = (Qt.AscendingOrder, Qt.DescendingOrder)
|
||||
@@ -128,6 +133,10 @@ class GuiWritingStats(QDialog):
|
||||
self.labelTotal.setFont(self.theTheme.guiFontFixed)
|
||||
self.labelTotal.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
|
||||
|
||||
self.labelIdleT = QLabel(formatTime(0))
|
||||
self.labelIdleT.setFont(self.theTheme.guiFontFixed)
|
||||
self.labelIdleT.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
|
||||
|
||||
self.labelFilter = QLabel(formatTime(0))
|
||||
self.labelFilter.setFont(self.theTheme.guiFontFixed)
|
||||
self.labelFilter.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
|
||||
@@ -145,16 +154,18 @@ class GuiWritingStats(QDialog):
|
||||
self.totalWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
|
||||
|
||||
self.infoForm.addWidget(QLabel("Total Time:"), 0, 0)
|
||||
self.infoForm.addWidget(QLabel("Filtered Time:"), 1, 0)
|
||||
self.infoForm.addWidget(QLabel("Novel Word Count:"), 2, 0)
|
||||
self.infoForm.addWidget(QLabel("Notes Word Count:"), 3, 0)
|
||||
self.infoForm.addWidget(QLabel("Total Word Count:"), 4, 0)
|
||||
self.infoForm.addWidget(QLabel("Idle Time:"), 1, 0)
|
||||
self.infoForm.addWidget(QLabel("Filtered Time:"), 2, 0)
|
||||
self.infoForm.addWidget(QLabel("Novel Word Count:"), 3, 0)
|
||||
self.infoForm.addWidget(QLabel("Notes Word Count:"), 4, 0)
|
||||
self.infoForm.addWidget(QLabel("Total Word Count:"), 5, 0)
|
||||
self.infoForm.addWidget(self.labelTotal, 0, 1)
|
||||
self.infoForm.addWidget(self.labelFilter, 1, 1)
|
||||
self.infoForm.addWidget(self.novelWords, 2, 1)
|
||||
self.infoForm.addWidget(self.notesWords, 3, 1)
|
||||
self.infoForm.addWidget(self.totalWords, 4, 1)
|
||||
self.infoForm.setRowStretch(5, 1)
|
||||
self.infoForm.addWidget(self.labelIdleT, 1, 1)
|
||||
self.infoForm.addWidget(self.labelFilter, 2, 1)
|
||||
self.infoForm.addWidget(self.novelWords, 3, 1)
|
||||
self.infoForm.addWidget(self.notesWords, 4, 1)
|
||||
self.infoForm.addWidget(self.totalWords, 5, 1)
|
||||
self.infoForm.setRowStretch(6, 1)
|
||||
|
||||
# Filter Options
|
||||
sPx = self.theTheme.baseIconSize
|
||||
@@ -193,17 +204,25 @@ class GuiWritingStats(QDialog):
|
||||
)
|
||||
self.groupByDay.clicked.connect(self._updateListBox)
|
||||
|
||||
self.showIdleTime = QSwitch(width=2*sPx, height=sPx)
|
||||
self.showIdleTime.setChecked(
|
||||
self.optState.getBool("GuiWritingStats", "showIdleTime", False)
|
||||
)
|
||||
self.showIdleTime.clicked.connect(self._updateListBox)
|
||||
|
||||
self.filterForm.addWidget(QLabel("Count novel files"), 0, 0)
|
||||
self.filterForm.addWidget(QLabel("Count note files"), 1, 0)
|
||||
self.filterForm.addWidget(QLabel("Hide zero word count"), 2, 0)
|
||||
self.filterForm.addWidget(QLabel("Hide negative word count"), 3, 0)
|
||||
self.filterForm.addWidget(QLabel("Group entries by day"), 4, 0)
|
||||
self.filterForm.addWidget(QLabel("Show idle time"), 5, 0)
|
||||
self.filterForm.addWidget(self.incNovel, 0, 1)
|
||||
self.filterForm.addWidget(self.incNotes, 1, 1)
|
||||
self.filterForm.addWidget(self.hideZeros, 2, 1)
|
||||
self.filterForm.addWidget(self.hideNegative, 3, 1)
|
||||
self.filterForm.addWidget(self.groupByDay, 4, 1)
|
||||
self.filterForm.setRowStretch(5, 1)
|
||||
self.filterForm.addWidget(self.showIdleTime, 5, 1)
|
||||
self.filterForm.setRowStretch(6, 1)
|
||||
|
||||
# Settings
|
||||
self.histMax = QSpinBox(self)
|
||||
@@ -279,6 +298,7 @@ class GuiWritingStats(QDialog):
|
||||
widthCol0 = self.mainConf.rpxInt(self.listBox.columnWidth(0))
|
||||
widthCol1 = self.mainConf.rpxInt(self.listBox.columnWidth(1))
|
||||
widthCol2 = self.mainConf.rpxInt(self.listBox.columnWidth(2))
|
||||
widthCol3 = self.mainConf.rpxInt(self.listBox.columnWidth(3))
|
||||
sortCol = self.listBox.sortColumn()
|
||||
sortOrder = self.listBox.header().sortIndicatorOrder()
|
||||
incNovel = self.incNovel.isChecked()
|
||||
@@ -286,6 +306,7 @@ class GuiWritingStats(QDialog):
|
||||
hideZeros = self.hideZeros.isChecked()
|
||||
hideNegative = self.hideNegative.isChecked()
|
||||
groupByDay = self.groupByDay.isChecked()
|
||||
showIdleTime = self.showIdleTime.isChecked()
|
||||
histMax = self.histMax.value()
|
||||
|
||||
self.optState.setValue("GuiWritingStats", "winWidth", winWidth)
|
||||
@@ -293,6 +314,7 @@ class GuiWritingStats(QDialog):
|
||||
self.optState.setValue("GuiWritingStats", "widthCol0", widthCol0)
|
||||
self.optState.setValue("GuiWritingStats", "widthCol1", widthCol1)
|
||||
self.optState.setValue("GuiWritingStats", "widthCol2", widthCol2)
|
||||
self.optState.setValue("GuiWritingStats", "widthCol3", widthCol3)
|
||||
self.optState.setValue("GuiWritingStats", "sortCol", sortCol)
|
||||
self.optState.setValue("GuiWritingStats", "sortOrder", sortOrder)
|
||||
self.optState.setValue("GuiWritingStats", "incNovel", incNovel)
|
||||
@@ -300,6 +322,7 @@ class GuiWritingStats(QDialog):
|
||||
self.optState.setValue("GuiWritingStats", "hideZeros", hideZeros)
|
||||
self.optState.setValue("GuiWritingStats", "hideNegative", hideNegative)
|
||||
self.optState.setValue("GuiWritingStats", "groupByDay", groupByDay)
|
||||
self.optState.setValue("GuiWritingStats", "showIdleTime", showIdleTime)
|
||||
self.optState.setValue("GuiWritingStats", "histMax", histMax)
|
||||
|
||||
self.optState.saveSettings()
|
||||
@@ -348,23 +371,25 @@ class GuiWritingStats(QDialog):
|
||||
with open(savePath, mode="w", encoding="utf8") as outFile:
|
||||
if dataFmt == self.FMT_JSON:
|
||||
jsonData = []
|
||||
for _, sD, tT, wD, wA, wB in self.filterData:
|
||||
for _, sD, tT, wD, wA, wB, tI in self.filterData:
|
||||
jsonData.append({
|
||||
"date": sD,
|
||||
"length": tT,
|
||||
"newWords": wD,
|
||||
"novelWords": wA,
|
||||
"noteWords": wB,
|
||||
"idleTime": tI,
|
||||
})
|
||||
json.dump(jsonData, outFile, indent=2)
|
||||
wSuccess = True
|
||||
|
||||
if dataFmt == self.FMT_CSV:
|
||||
outFile.write(
|
||||
'"Date","Length (sec)","Words Changed","Novel Words","Note Words"\n'
|
||||
'"Date","Length (sec)","Words Changed",'
|
||||
'"Novel Words","Note Words","Idle Time (sec)"\n'
|
||||
)
|
||||
for _, sD, tT, wD, wA, wB in self.filterData:
|
||||
outFile.write(f'"{sD}",{tT:.0f},{wD},{wA},{wB}\n')
|
||||
for _, sD, tT, wD, wA, wB, tI in self.filterData:
|
||||
outFile.write(f'"{sD}",{tT:.0f},{wD},{wA},{wB},{tI}\n')
|
||||
wSuccess = True
|
||||
|
||||
except Exception as e:
|
||||
@@ -402,6 +427,7 @@ class GuiWritingStats(QDialog):
|
||||
ttNovel = 0
|
||||
ttNotes = 0
|
||||
ttTime = 0
|
||||
ttIdle = 0
|
||||
|
||||
logFile = os.path.join(self.theProject.projMeta, nwFiles.SESS_STATS)
|
||||
if not os.path.isfile(logFile):
|
||||
@@ -420,7 +446,7 @@ class GuiWritingStats(QDialog):
|
||||
continue
|
||||
|
||||
inData = inLine.split()
|
||||
if len(inData) != 6:
|
||||
if len(inData) < 6:
|
||||
continue
|
||||
|
||||
dStart = datetime.strptime(
|
||||
@@ -430,16 +456,21 @@ class GuiWritingStats(QDialog):
|
||||
"%s %s" % (inData[2], inData[3]), nwConst.FMT_TSTAMP
|
||||
)
|
||||
|
||||
sIdle = 0
|
||||
if len(inData) > 6:
|
||||
sIdle = checkInt(inData[6], 0)
|
||||
|
||||
tDiff = dEnd - dStart
|
||||
sDiff = tDiff.total_seconds()
|
||||
ttTime += sDiff
|
||||
ttIdle += sIdle
|
||||
|
||||
wcNovel = int(inData[4])
|
||||
wcNotes = int(inData[5])
|
||||
ttNovel = wcNovel
|
||||
ttNotes = wcNotes
|
||||
|
||||
self.logData.append((dStart, sDiff, wcNovel, wcNotes))
|
||||
self.logData.append((dStart, sDiff, wcNovel, wcNotes, sIdle))
|
||||
|
||||
except Exception as e:
|
||||
self.theParent.makeAlert(
|
||||
@@ -449,12 +480,17 @@ class GuiWritingStats(QDialog):
|
||||
|
||||
ttWords = ttNovel + ttNotes
|
||||
self.labelTotal.setText(formatTime(round(ttTime)))
|
||||
self.labelIdleT.setText(formatTime(round(ttIdle)))
|
||||
self.novelWords.setText(f"{ttNovel:n}")
|
||||
self.notesWords.setText(f"{ttNotes:n}")
|
||||
self.totalWords.setText(f"{ttWords:n}")
|
||||
|
||||
return True
|
||||
|
||||
##
|
||||
# Slots
|
||||
##
|
||||
|
||||
def _updateListBox(self, dummyVar=None):
|
||||
"""Load/reload the content of the list box. The dummyVar
|
||||
variable captures the variable sent from the widgets connecting
|
||||
@@ -475,25 +511,28 @@ class GuiWritingStats(QDialog):
|
||||
tempData = []
|
||||
sessDate = None
|
||||
sessTime = 0
|
||||
sessIdle = 0
|
||||
lstNovel = 0
|
||||
lstNotes = 0
|
||||
|
||||
for n, (dStart, sDiff, wcNovel, wcNotes) in enumerate(self.logData):
|
||||
for n, (dStart, sDiff, wcNovel, wcNotes, sIdle) in enumerate(self.logData):
|
||||
if n == 0:
|
||||
sessDate = dStart.date()
|
||||
if sessDate != dStart.date():
|
||||
tempData.append((sessDate, sessTime, lstNovel, lstNotes))
|
||||
tempData.append((sessDate, sessTime, lstNovel, lstNotes, sessIdle))
|
||||
sessDate = dStart.date()
|
||||
sessTime = sDiff
|
||||
sessIdle = sIdle
|
||||
lstNovel = wcNovel
|
||||
lstNotes = wcNotes
|
||||
else:
|
||||
sessTime += sDiff
|
||||
sessIdle += sIdle
|
||||
lstNovel = wcNovel
|
||||
lstNotes = wcNotes
|
||||
|
||||
if sessDate is not None:
|
||||
tempData.append((sessDate, sessTime, lstNovel, lstNotes))
|
||||
tempData.append((sessDate, sessTime, lstNovel, lstNotes, sessIdle))
|
||||
|
||||
else:
|
||||
tempData = self.logData
|
||||
@@ -503,7 +542,7 @@ class GuiWritingStats(QDialog):
|
||||
pcTotal = 0
|
||||
listMax = 0
|
||||
isFirst = True
|
||||
for dStart, sDiff, wcNovel, wcNotes in tempData:
|
||||
for dStart, sDiff, wcNovel, wcNotes, sIdle in tempData:
|
||||
|
||||
wcTotal = 0
|
||||
if incNovel:
|
||||
@@ -515,6 +554,7 @@ class GuiWritingStats(QDialog):
|
||||
if hideZeros and dwTotal == 0:
|
||||
continue
|
||||
if hideNegative and dwTotal < 0:
|
||||
pcTotal = wcTotal
|
||||
continue
|
||||
|
||||
if isFirst:
|
||||
@@ -528,16 +568,24 @@ class GuiWritingStats(QDialog):
|
||||
else:
|
||||
sStart = dStart.strftime(nwConst.FMT_TSTAMP)
|
||||
|
||||
self.filterData.append((dStart, sStart, sDiff, dwTotal, wcNovel, wcNotes))
|
||||
self.filterData.append((dStart, sStart, sDiff, dwTotal, wcNovel, wcNotes, sIdle))
|
||||
listMax = min(max(listMax, dwTotal), histMax)
|
||||
pcTotal = wcTotal
|
||||
|
||||
# Populate the list
|
||||
for _, sStart, sDiff, nWords, _, _ in self.filterData:
|
||||
showIdleTime = self.showIdleTime.isChecked()
|
||||
for _, sStart, sDiff, nWords, _, _, sIdle in self.filterData:
|
||||
|
||||
if showIdleTime:
|
||||
idleEntry = formatTime(sIdle)
|
||||
else:
|
||||
sRatio = sIdle/sDiff if sDiff > 0.0 else 0.0
|
||||
idleEntry = "%d %%" % round(100.0 * sRatio)
|
||||
|
||||
newItem = QTreeWidgetItem()
|
||||
newItem.setText(self.C_TIME, sStart)
|
||||
newItem.setText(self.C_LENGTH, formatTime(round(sDiff)))
|
||||
newItem.setText(self.C_IDLE, idleEntry)
|
||||
newItem.setText(self.C_COUNT, f"{nWords:n}")
|
||||
|
||||
if nWords > 0 and listMax > 0:
|
||||
@@ -550,12 +598,17 @@ class GuiWritingStats(QDialog):
|
||||
newItem.setData(self.C_BAR, Qt.DecorationRole, theBar)
|
||||
|
||||
newItem.setTextAlignment(self.C_LENGTH, Qt.AlignRight)
|
||||
newItem.setTextAlignment(self.C_IDLE, Qt.AlignRight)
|
||||
newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight)
|
||||
newItem.setTextAlignment(self.C_BAR, Qt.AlignLeft | Qt.AlignVCenter)
|
||||
|
||||
newItem.setFont(self.C_TIME, self.theTheme.guiFontFixed)
|
||||
newItem.setFont(self.C_LENGTH, self.theTheme.guiFontFixed)
|
||||
newItem.setFont(self.C_COUNT, self.theTheme.guiFontFixed)
|
||||
if showIdleTime:
|
||||
newItem.setFont(self.C_IDLE, self.theTheme.guiFontFixed)
|
||||
else:
|
||||
newItem.setFont(self.C_IDLE, self.theTheme.guiFont)
|
||||
|
||||
self.listBox.addTopLevelItem(newItem)
|
||||
self.timeFilter += sDiff
|
||||
|
||||
382
nw/guimain.py
@@ -1,28 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI Main Window
|
||||
"""
|
||||
novelWriter – GUI Main Window
|
||||
=============================
|
||||
The main application window
|
||||
|
||||
novelWriter – GUI Main Window
|
||||
===============================
|
||||
Class holding the main application window
|
||||
File History:
|
||||
Created: 2018-09-22 [0.0.1]
|
||||
|
||||
File History:
|
||||
Created: 2018-09-22 [0.0.1]
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2021, Veronica Berglyd Olsen
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is 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.
|
||||
|
||||
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/>.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import nw
|
||||
@@ -32,23 +31,23 @@ import os
|
||||
from datetime import datetime
|
||||
from time import time
|
||||
|
||||
from PyQt5.QtCore import Qt, QTimer, QThreadPool
|
||||
from PyQt5.QtCore import Qt, QTimer, QSize, QThreadPool, pyqtSlot
|
||||
from PyQt5.QtGui import QIcon, QPixmap, QColor, QKeySequence, QCursor
|
||||
from PyQt5.QtWidgets import (
|
||||
qApp, QMainWindow, QVBoxLayout, QWidget, QSplitter, QFileDialog, QShortcut,
|
||||
QMessageBox, QDialog, QTabWidget
|
||||
QMessageBox, QDialog, QTabWidget, QToolBar, QAction
|
||||
)
|
||||
|
||||
from nw.gui import (
|
||||
GuiAbout, GuiBuildNovel, GuiDocEditor, GuiDocMerge, GuiDocSplit,
|
||||
GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiItemEditor,
|
||||
GuiMainMenu, GuiMainStatus, GuiOutline, GuiOutlineDetails, GuiPreferences,
|
||||
GuiProjectLoad, GuiProjectSettings, GuiProjectTree, GuiProjectWizard,
|
||||
GuiTheme, GuiWritingStats
|
||||
GuiMainMenu, GuiMainStatus, GuiNovelTree, GuiOutline, GuiOutlineDetails,
|
||||
GuiPreferences, GuiProjectDetails, GuiProjectLoad, GuiProjectSettings,
|
||||
GuiProjectTree, GuiProjectWizard, GuiTheme, GuiWordList, GuiWritingStats
|
||||
)
|
||||
from nw.core import NWProject, NWDoc, NWIndex
|
||||
from nw.constants import nwItemType, nwItemClass, nwAlert, nwConst
|
||||
from nw.common import getGuiItem
|
||||
from nw.constants import nwItemType, nwItemClass, nwAlert, nwLists
|
||||
from nw.common import getGuiItem, hexToInt
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -87,18 +86,26 @@ class GuiMain(QMainWindow):
|
||||
self.theIndex = NWIndex(self.theProject, self)
|
||||
self.hasProject = False
|
||||
self.isFocusMode = False
|
||||
self.idleRefTime = time()
|
||||
self.idleTime = 0.0
|
||||
|
||||
# Prepare Main Window
|
||||
self.resize(*self.mainConf.getWinSize())
|
||||
self._setWindowTitle()
|
||||
self._updateWindowTitle()
|
||||
self.setWindowIcon(QIcon(self.mainConf.appIcon))
|
||||
|
||||
# Build the GUI
|
||||
# =============
|
||||
|
||||
# Sizes
|
||||
mPx = self.mainConf.pxInt(4)
|
||||
fPx = self.theTheme.fontPixelSize
|
||||
fPt = self.theTheme.fontPointSize
|
||||
|
||||
# Main GUI Elements
|
||||
self.statusBar = GuiMainStatus(self)
|
||||
self.treeView = GuiProjectTree(self)
|
||||
self.novelView = GuiNovelTree(self)
|
||||
self.docEditor = GuiDocEditor(self)
|
||||
self.viewMeta = GuiDocViewDetails(self)
|
||||
self.docViewer = GuiDocViewer(self)
|
||||
@@ -107,15 +114,52 @@ class GuiMain(QMainWindow):
|
||||
self.projMeta = GuiOutlineDetails(self)
|
||||
self.mainMenu = GuiMainMenu(self)
|
||||
|
||||
# Minor Gui Elements
|
||||
# Minor GUI Elements
|
||||
self.statusIcons = []
|
||||
self.importIcons = []
|
||||
|
||||
# Project Tree Tabs
|
||||
self.projTabs = QTabWidget()
|
||||
self.projTabs.setTabPosition(QTabWidget.South)
|
||||
self.projTabs.setStyleSheet(r"QTabWidget::pane {border: 0;};")
|
||||
self.projTabs.addTab(self.treeView, "Project")
|
||||
self.projTabs.addTab(self.novelView, "Novel")
|
||||
self.projTabs.currentChanged.connect(self._projTabsChanged)
|
||||
|
||||
tabFont = self.projTabs.tabBar().font()
|
||||
tabFont.setPointSizeF(0.9*fPt)
|
||||
self.projTabs.tabBar().setFont(tabFont)
|
||||
|
||||
# Project Tree Action Buttons
|
||||
btnSize = int(round(0.7*fPx))
|
||||
self.treeButtons = QToolBar()
|
||||
self.treeButtons.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.treeButtons.setIconSize(QSize(btnSize, btnSize))
|
||||
self.treeButtons.setContentsMargins(0, 0, 0, 0)
|
||||
self.treeButtons.setStyleSheet(r"QToolBar {padding: 0;}")
|
||||
self.projTabs.setCornerWidget(self.treeButtons, Qt.BottomRightCorner)
|
||||
|
||||
self.projDetailsBtn = QAction("Project Details")
|
||||
self.projDetailsBtn.setIcon(self.theTheme.getIcon("status_lines"))
|
||||
self.projDetailsBtn.triggered.connect(lambda: self.showProjectDetailsDialog())
|
||||
self.treeButtons.addAction(self.projDetailsBtn)
|
||||
|
||||
self.projStatsBtn = QAction("Writing Statistics")
|
||||
self.projStatsBtn.setIcon(self.theTheme.getIcon("status_stats"))
|
||||
self.projStatsBtn.triggered.connect(lambda: self.showWritingStatsDialog())
|
||||
self.treeButtons.addAction(self.projStatsBtn)
|
||||
|
||||
self.projSettingsBtn = QAction("Project Settings")
|
||||
self.projSettingsBtn.setIcon(self.theTheme.getIcon("settings"))
|
||||
self.projSettingsBtn.triggered.connect(lambda: self.showProjectSettingsDialog())
|
||||
self.treeButtons.addAction(self.projSettingsBtn)
|
||||
|
||||
# Project Tree View
|
||||
self.treePane = QWidget()
|
||||
self.treeBox = QVBoxLayout()
|
||||
self.treeBox.setContentsMargins(0, 0, 0, 0)
|
||||
self.treeBox.addWidget(self.treeView)
|
||||
self.treeBox.setSpacing(mPx)
|
||||
self.treeBox.addWidget(self.projTabs)
|
||||
self.treeBox.addWidget(self.treeMeta)
|
||||
self.treePane.setLayout(self.treeBox)
|
||||
|
||||
@@ -136,31 +180,34 @@ class GuiMain(QMainWindow):
|
||||
self.splitOutline.addWidget(self.projMeta)
|
||||
self.splitOutline.setSizes(self.mainConf.getOutlinePanePos())
|
||||
|
||||
# Main Tabs : Edirot / Outline
|
||||
self.tabWidget = QTabWidget()
|
||||
self.tabWidget.setTabPosition(QTabWidget.East)
|
||||
self.tabWidget.setStyleSheet("QTabWidget::pane {border: 0;}")
|
||||
self.tabWidget.addTab(self.splitDocs, "Editor")
|
||||
self.tabWidget.addTab(self.splitOutline, "Outline")
|
||||
self.tabWidget.currentChanged.connect(self._mainTabChanged)
|
||||
# Main Tabs : Editor / Outline
|
||||
self.mainTabs = QTabWidget()
|
||||
self.mainTabs.setTabPosition(QTabWidget.East)
|
||||
self.mainTabs.setStyleSheet(r"QTabWidget::pane {border: 0;}")
|
||||
self.mainTabs.addTab(self.splitDocs, "Editor")
|
||||
self.mainTabs.addTab(self.splitOutline, "Outline")
|
||||
self.mainTabs.currentChanged.connect(self._mainTabChanged)
|
||||
|
||||
# Splitter : Project Tree / Main Tabs
|
||||
xCM = self.mainConf.pxInt(4)
|
||||
self.splitMain = QSplitter(Qt.Horizontal)
|
||||
self.splitMain.setContentsMargins(xCM, xCM, xCM, xCM)
|
||||
self.splitMain.setContentsMargins(mPx, mPx, mPx, mPx)
|
||||
self.splitMain.addWidget(self.treePane)
|
||||
self.splitMain.addWidget(self.tabWidget)
|
||||
self.splitMain.addWidget(self.mainTabs)
|
||||
self.splitMain.setSizes(self.mainConf.getMainPanePos())
|
||||
|
||||
# Indices of All Splitter Widgets
|
||||
# Indices of Splitter Widgets
|
||||
self.idxTree = self.splitMain.indexOf(self.treePane)
|
||||
self.idxMain = self.splitMain.indexOf(self.tabWidget)
|
||||
self.idxMain = self.splitMain.indexOf(self.mainTabs)
|
||||
self.idxEditor = self.splitDocs.indexOf(self.docEditor)
|
||||
self.idxViewer = self.splitDocs.indexOf(self.splitView)
|
||||
self.idxViewDoc = self.splitView.indexOf(self.docViewer)
|
||||
self.idxViewMeta = self.splitView.indexOf(self.viewMeta)
|
||||
self.idxTabEdit = self.tabWidget.indexOf(self.splitDocs)
|
||||
self.idxTabProj = self.tabWidget.indexOf(self.splitOutline)
|
||||
|
||||
# Indices of Tab Widgets
|
||||
self.idxTabEdit = self.mainTabs.indexOf(self.splitDocs)
|
||||
self.idxTabProj = self.mainTabs.indexOf(self.splitOutline)
|
||||
self.idxTreeView = self.projTabs.indexOf(self.treeView)
|
||||
self.idxNovelView = self.projTabs.indexOf(self.novelView)
|
||||
|
||||
# Splitter Behaviour
|
||||
self.splitMain.setCollapsible(self.idxTree, False)
|
||||
@@ -177,7 +224,8 @@ class GuiMain(QMainWindow):
|
||||
# Initialise the Project Tree
|
||||
self.treeView.itemSelectionChanged.connect(self._treeSingleClick)
|
||||
self.treeView.itemDoubleClicked.connect(self._treeDoubleClick)
|
||||
self.rebuildTree()
|
||||
self.treeView.novelItemChanged.connect(self._treeNovelItemChanged)
|
||||
self.rebuildTrees()
|
||||
|
||||
# Set Main Window Elements
|
||||
self.setMenuBar(self.mainMenu)
|
||||
@@ -195,6 +243,12 @@ class GuiMain(QMainWindow):
|
||||
self.asDocTimer = QTimer()
|
||||
self.asDocTimer.timeout.connect(self._autoSaveDocument)
|
||||
|
||||
# Main Clock
|
||||
self.mainTimer = QTimer()
|
||||
self.mainTimer.setInterval(1000)
|
||||
self.mainTimer.timeout.connect(self._timeTick)
|
||||
self.mainTimer.start()
|
||||
|
||||
# Shortcuts and Actions
|
||||
self._connectMenuActions()
|
||||
|
||||
@@ -229,20 +283,10 @@ class GuiMain(QMainWindow):
|
||||
|
||||
logger.debug("GUI initialisation complete")
|
||||
|
||||
# Check if a project path was provided at command line, and if
|
||||
# not, open the project manager instead.
|
||||
# If a project path was provided at command line, open it
|
||||
if self.mainConf.cmdOpen is not None:
|
||||
logger.debug("Opening project from additional command line option")
|
||||
self.openProject(self.mainConf.cmdOpen)
|
||||
else:
|
||||
if self.mainConf.showGUI:
|
||||
self.showProjectLoadDialog()
|
||||
|
||||
# Show the latest release notes, if they haven't been shown before
|
||||
if self.mainConf.lastNotes != nw.__version__:
|
||||
if self.mainConf.showGUI:
|
||||
self.showAboutNWDialog(showNotes=True)
|
||||
self.mainConf.lastNotes = nw.__version__
|
||||
|
||||
logger.debug("novelWriter is ready ...")
|
||||
self.setStatus("novelWriter is ready ...")
|
||||
@@ -252,10 +296,21 @@ class GuiMain(QMainWindow):
|
||||
def clearGUI(self):
|
||||
"""Wrapper function to clear all sub-elements of the main GUI.
|
||||
"""
|
||||
# Project Area
|
||||
self.treeView.clearTree()
|
||||
self.novelView.clearTree()
|
||||
self.treeMeta.clearDetails()
|
||||
|
||||
# Work Area
|
||||
self.docEditor.clearEditor()
|
||||
self.docEditor.setDictionaries()
|
||||
self.closeDocViewer()
|
||||
self.projMeta.clearDetails()
|
||||
|
||||
# General
|
||||
self.statusBar.clearStatus()
|
||||
self._updateWindowTitle()
|
||||
|
||||
return True
|
||||
|
||||
def initMain(self):
|
||||
@@ -265,6 +320,15 @@ class GuiMain(QMainWindow):
|
||||
self.asDocTimer.setInterval(int(self.mainConf.autoSaveDoc*1000))
|
||||
return True
|
||||
|
||||
def releaseNotes(self):
|
||||
"""Determine whether release notes need to be shown, and show
|
||||
them by calling the About dialog.
|
||||
"""
|
||||
if hexToInt(self.mainConf.lastNotes) < hexToInt(nw.__hexversion__):
|
||||
self.mainConf.lastNotes = nw.__hexversion__
|
||||
self.showAboutNWDialog(showNotes=True)
|
||||
return
|
||||
|
||||
##
|
||||
# Project Actions
|
||||
##
|
||||
@@ -273,11 +337,12 @@ class GuiMain(QMainWindow):
|
||||
"""Create new project via the new project wizard.
|
||||
"""
|
||||
if self.hasProject:
|
||||
self.makeAlert(
|
||||
"Please close the current project before making a new one.",
|
||||
nwAlert.ERROR
|
||||
)
|
||||
return False
|
||||
if not self.closeProject():
|
||||
self.makeAlert(
|
||||
"Cannot create new project when another project is open.",
|
||||
nwAlert.ERROR
|
||||
)
|
||||
return False
|
||||
|
||||
if projData is None:
|
||||
projData = self.showNewProjectDialog()
|
||||
@@ -299,11 +364,16 @@ class GuiMain(QMainWindow):
|
||||
|
||||
logger.info("Creating new project")
|
||||
if self.theProject.newProject(projData):
|
||||
self.rebuildTree()
|
||||
self.rebuildTrees()
|
||||
self.saveProject()
|
||||
self.hasProject = True
|
||||
self.statusBar.setRefTime(self.theProject.projOpened)
|
||||
self.docEditor.setDictionaries()
|
||||
self.rebuildIndex(beQuiet=True)
|
||||
self.statusBar.setRefTime(self.theProject.projOpened)
|
||||
self.statusBar.setProjectStatus(True)
|
||||
self.statusBar.setDocumentStatus(None)
|
||||
self.statusBar.setStatus("New project created ...")
|
||||
self._updateWindowTitle(self.theProject.projName)
|
||||
else:
|
||||
self.theProject.clearProject()
|
||||
return False
|
||||
@@ -350,12 +420,15 @@ class GuiMain(QMainWindow):
|
||||
self.closeDocument()
|
||||
self.docViewer.clearNavHistory()
|
||||
self.projView.closeOutline()
|
||||
self.projMeta.clearDetails()
|
||||
self.theProject.closeProject()
|
||||
|
||||
self.theProject.closeProject(self.idleTime)
|
||||
self.idleRefTime = time()
|
||||
self.idleTime = 0.0
|
||||
|
||||
self.theIndex.clearIndex()
|
||||
self.clearGUI()
|
||||
self.hasProject = False
|
||||
self.tabWidget.setCurrentWidget(self.splitDocs)
|
||||
self.mainTabs.setCurrentWidget(self.splitDocs)
|
||||
|
||||
return saveOK
|
||||
|
||||
@@ -371,7 +444,7 @@ class GuiMain(QMainWindow):
|
||||
return False
|
||||
|
||||
# Switch main tab to editor view
|
||||
self.tabWidget.setCurrentWidget(self.splitDocs)
|
||||
self.mainTabs.setCurrentWidget(self.splitDocs)
|
||||
|
||||
# Try to open the project
|
||||
if not self.theProject.openProject(projFile):
|
||||
@@ -416,22 +489,26 @@ class GuiMain(QMainWindow):
|
||||
return False
|
||||
|
||||
# Project is loaded
|
||||
self.hasProject = True
|
||||
self.hasProject = True
|
||||
self.idleRefTime = time()
|
||||
self.idleTime = 0.0
|
||||
|
||||
# Load the tag index
|
||||
self.theIndex.loadIndex()
|
||||
|
||||
# Update GUI
|
||||
self._setWindowTitle(self.theProject.projName)
|
||||
self.rebuildTree()
|
||||
self._updateWindowTitle(self.theProject.projName)
|
||||
self.rebuildTrees()
|
||||
self.docEditor.setDictionaries()
|
||||
self.docEditor.setSpellCheck(self.theProject.spellCheck)
|
||||
self.mainMenu.setAutoOutline(self.theProject.autoOutline)
|
||||
self.statusBar.setRefTime(self.theProject.projOpened)
|
||||
self.statusBar.setStats(self.theProject.currWCount, 0)
|
||||
|
||||
# Restore previously open documents, if any
|
||||
if self.theProject.lastEdited is not None:
|
||||
self.openDocument(self.theProject.lastEdited, doScroll=True)
|
||||
|
||||
if self.theProject.lastViewed is not None:
|
||||
self.viewDocument(self.theProject.lastViewed)
|
||||
|
||||
@@ -439,6 +516,12 @@ class GuiMain(QMainWindow):
|
||||
if self.theIndex.indexBroken:
|
||||
self.rebuildIndex()
|
||||
|
||||
# Make sure the changed status is set to false on all that was
|
||||
# just opened
|
||||
qApp.processEvents()
|
||||
self.docEditor.setDocumentChanged(False)
|
||||
self.theProject.setProjectChanged(False)
|
||||
|
||||
logger.debug("Project load complete")
|
||||
|
||||
return True
|
||||
@@ -459,7 +542,7 @@ class GuiMain(QMainWindow):
|
||||
return False
|
||||
|
||||
self.treeView.saveTreeOrder()
|
||||
self.theProject.saveProject(autoSave)
|
||||
self.theProject.saveProject(autoSave=autoSave)
|
||||
self.theIndex.saveIndex()
|
||||
|
||||
return True
|
||||
@@ -490,7 +573,7 @@ class GuiMain(QMainWindow):
|
||||
return False
|
||||
|
||||
self.closeDocument()
|
||||
self.tabWidget.setCurrentWidget(self.splitDocs)
|
||||
self.mainTabs.setCurrentWidget(self.splitDocs)
|
||||
if self.docEditor.loadText(tHandle, tLine):
|
||||
if changeFocus:
|
||||
self.docEditor.setFocus()
|
||||
@@ -575,7 +658,7 @@ class GuiMain(QMainWindow):
|
||||
return False
|
||||
|
||||
# Make sure main tab is in Editor view
|
||||
self.tabWidget.setCurrentWidget(self.splitDocs)
|
||||
self.mainTabs.setCurrentWidget(self.splitDocs)
|
||||
|
||||
logger.debug("Viewing document with handle %s" % tHandle)
|
||||
if self.docViewer.loadText(tHandle):
|
||||
@@ -717,7 +800,11 @@ class GuiMain(QMainWindow):
|
||||
return False
|
||||
|
||||
if tHandle is None:
|
||||
tHandle = self.treeView.getSelectedHandle()
|
||||
if self.treeView.hasFocus():
|
||||
tHandle = self.treeView.getSelectedHandle()
|
||||
elif self.docEditor.hasFocus():
|
||||
tHandle = self.docEditor.theHandle
|
||||
|
||||
if tHandle is None:
|
||||
logger.warning("No item selected")
|
||||
return
|
||||
@@ -725,7 +812,7 @@ class GuiMain(QMainWindow):
|
||||
tItem = self.theProject.projTree[tHandle]
|
||||
if tItem is None:
|
||||
return
|
||||
if tItem.itemType not in nwConst.REG_TYPES:
|
||||
if tItem.itemType not in nwLists.REG_TYPES:
|
||||
return
|
||||
|
||||
logger.verbose("Requesting change to item %s" % tHandle)
|
||||
@@ -739,15 +826,23 @@ class GuiMain(QMainWindow):
|
||||
|
||||
return
|
||||
|
||||
def rebuildTree(self):
|
||||
def rebuildTrees(self):
|
||||
"""Rebuild the project tree.
|
||||
"""
|
||||
self._makeStatusIcons()
|
||||
self._makeImportIcons()
|
||||
self.treeView.clearTree()
|
||||
self.treeView.buildTree()
|
||||
self.novelView.refreshTree()
|
||||
return
|
||||
|
||||
def requestNovelTreeRefresh(self):
|
||||
"""Update the novel tree, but only if it is visible.
|
||||
"""
|
||||
if self.projTabs.currentIndex() == self.idxNovelView and self.hasProject:
|
||||
self.novelView.refreshTree()
|
||||
return True
|
||||
return False
|
||||
|
||||
def rebuildIndex(self, beQuiet=False):
|
||||
"""Rebuild the entire index.
|
||||
"""
|
||||
@@ -803,7 +898,7 @@ class GuiMain(QMainWindow):
|
||||
return False
|
||||
|
||||
logger.verbose("Forcing a rebuild of the Project Outline")
|
||||
self.tabWidget.setCurrentWidget(self.splitOutline)
|
||||
self.mainTabs.setCurrentWidget(self.splitOutline)
|
||||
self.projView.refreshTree(overRide=True)
|
||||
|
||||
return True
|
||||
@@ -866,6 +961,7 @@ class GuiMain(QMainWindow):
|
||||
self.docEditor.initEditor()
|
||||
self.docViewer.initViewer()
|
||||
self.treeView.initTree()
|
||||
self.novelView.initTree()
|
||||
self.projView.initOutline()
|
||||
self.projMeta.initDetails()
|
||||
|
||||
@@ -884,7 +980,22 @@ class GuiMain(QMainWindow):
|
||||
if dlgProj.result() == QDialog.Accepted:
|
||||
logger.debug("Applying new project settings")
|
||||
self.docEditor.setDictionaries()
|
||||
self._setWindowTitle(self.theProject.projName)
|
||||
self._updateWindowTitle(self.theProject.projName)
|
||||
|
||||
return
|
||||
|
||||
def showProjectDetailsDialog(self):
|
||||
"""Open the project details dialog.
|
||||
"""
|
||||
if not self.hasProject:
|
||||
logger.error("No project open")
|
||||
return
|
||||
|
||||
self.treeView.flushTreeOrder()
|
||||
|
||||
dlgDetails = GuiProjectDetails(self, self.theProject)
|
||||
dlgDetails.setModal(False)
|
||||
dlgDetails.show()
|
||||
|
||||
return
|
||||
|
||||
@@ -906,6 +1017,22 @@ class GuiMain(QMainWindow):
|
||||
|
||||
return
|
||||
|
||||
def showProjectWordListDialog(self):
|
||||
"""Open the project word list dialog.
|
||||
"""
|
||||
if not self.hasProject:
|
||||
logger.error("No project open")
|
||||
return
|
||||
|
||||
dlgWords = GuiWordList(self, self.theProject)
|
||||
dlgWords.exec_()
|
||||
|
||||
if dlgWords.result() == QDialog.Accepted:
|
||||
logger.debug("Reloading word list")
|
||||
self.docEditor.setDictionaries()
|
||||
|
||||
return
|
||||
|
||||
def showWritingStatsDialog(self):
|
||||
"""Open the session log dialog.
|
||||
"""
|
||||
@@ -1027,6 +1154,7 @@ class GuiMain(QMainWindow):
|
||||
|
||||
self.mainConf.setShowRefPanel(self.viewMeta.isVisible())
|
||||
self.mainConf.setTreeColWidths(self.treeView.getColumnSizes())
|
||||
self.mainConf.setNovelColWidths(self.novelView.getColumnSizes())
|
||||
if not self.mainConf.isFullScreen:
|
||||
self.mainConf.setWinSize(self.width(), self.height())
|
||||
|
||||
@@ -1042,14 +1170,19 @@ class GuiMain(QMainWindow):
|
||||
return True
|
||||
|
||||
def setFocus(self, paneNo):
|
||||
"""Switch focus to one of the three main GUI panes.
|
||||
"""Switch focus between main GUI views.
|
||||
"""
|
||||
if paneNo == 1:
|
||||
self.treeView.setFocus()
|
||||
elif paneNo == 2:
|
||||
self.mainTabs.setCurrentWidget(self.splitDocs)
|
||||
self.docEditor.setFocus()
|
||||
elif paneNo == 3:
|
||||
self.mainTabs.setCurrentWidget(self.splitDocs)
|
||||
self.docViewer.setFocus()
|
||||
elif paneNo == 4:
|
||||
self.mainTabs.setCurrentWidget(self.splitOutline)
|
||||
self.projView.setFocus()
|
||||
return
|
||||
|
||||
def closeDocEditor(self):
|
||||
@@ -1076,14 +1209,14 @@ class GuiMain(QMainWindow):
|
||||
"""
|
||||
if self.docEditor.theHandle is None:
|
||||
logger.error("No document open, so not activating Focus Mode")
|
||||
self.mainMenu.aFocusMode.setChecked(self.isFocusMode)
|
||||
self.mainMenu.setFocusMode(self.isFocusMode)
|
||||
return False
|
||||
|
||||
self.isFocusMode = not self.isFocusMode
|
||||
self.mainMenu.aFocusMode.setChecked(self.isFocusMode)
|
||||
self.mainMenu.setFocusMode(self.isFocusMode)
|
||||
if self.isFocusMode:
|
||||
logger.debug("Activating Focus Mode")
|
||||
self.tabWidget.setCurrentWidget(self.splitDocs)
|
||||
self.mainTabs.setCurrentWidget(self.splitDocs)
|
||||
else:
|
||||
logger.debug("Deactivating Focus Mode")
|
||||
|
||||
@@ -1091,7 +1224,7 @@ class GuiMain(QMainWindow):
|
||||
self.treePane.setVisible(isVisible)
|
||||
self.statusBar.setVisible(isVisible)
|
||||
self.mainMenu.setVisible(isVisible)
|
||||
self.tabWidget.tabBar().setVisible(isVisible)
|
||||
self.mainTabs.tabBar().setVisible(isVisible)
|
||||
|
||||
hideDocFooter = self.isFocusMode and self.mainConf.hideFocusFooter
|
||||
self.docEditor.docFooter.setVisible(not hideDocFooter)
|
||||
@@ -1154,16 +1287,28 @@ class GuiMain(QMainWindow):
|
||||
# Insert
|
||||
self.addAction(self.mainMenu.aInsENDash)
|
||||
self.addAction(self.mainMenu.aInsEMDash)
|
||||
self.addAction(self.mainMenu.aInsEllipsis)
|
||||
self.addAction(self.mainMenu.aInsHorBar)
|
||||
self.addAction(self.mainMenu.aInsFigDash)
|
||||
self.addAction(self.mainMenu.aInsQuoteLS)
|
||||
self.addAction(self.mainMenu.aInsQuoteRS)
|
||||
self.addAction(self.mainMenu.aInsQuoteLD)
|
||||
self.addAction(self.mainMenu.aInsQuoteRD)
|
||||
self.addAction(self.mainMenu.aInsMSApos)
|
||||
self.addAction(self.mainMenu.aInsEllipsis)
|
||||
self.addAction(self.mainMenu.aInsPrime)
|
||||
self.addAction(self.mainMenu.aInsDPrime)
|
||||
self.addAction(self.mainMenu.aInsHardBreak)
|
||||
self.addAction(self.mainMenu.aInsNBSpace)
|
||||
self.addAction(self.mainMenu.aInsThinSpace)
|
||||
self.addAction(self.mainMenu.aInsThinNBSpace)
|
||||
self.addAction(self.mainMenu.aInsBullet)
|
||||
self.addAction(self.mainMenu.aInsHyBull)
|
||||
self.addAction(self.mainMenu.aInsFlower)
|
||||
self.addAction(self.mainMenu.aInsPerMille)
|
||||
self.addAction(self.mainMenu.aInsDegree)
|
||||
self.addAction(self.mainMenu.aInsMinus)
|
||||
self.addAction(self.mainMenu.aInsTimes)
|
||||
self.addAction(self.mainMenu.aInsDivide)
|
||||
|
||||
for mAction, _ in self.mainMenu.mInsKWItems.values():
|
||||
self.addAction(mAction)
|
||||
@@ -1193,7 +1338,7 @@ class GuiMain(QMainWindow):
|
||||
|
||||
return True
|
||||
|
||||
def _setWindowTitle(self, projName=None):
|
||||
def _updateWindowTitle(self, projName=None):
|
||||
"""Set the window title and add the project's working title.
|
||||
"""
|
||||
winTitle = self.mainConf.appName
|
||||
@@ -1300,9 +1445,32 @@ class GuiMain(QMainWindow):
|
||||
return
|
||||
|
||||
##
|
||||
# Signal Handlers
|
||||
# Slots
|
||||
##
|
||||
|
||||
@pyqtSlot()
|
||||
def _timeTick(self):
|
||||
"""Triggered on every tick of the timer.
|
||||
"""
|
||||
if not self.hasProject:
|
||||
return
|
||||
|
||||
currTime = time()
|
||||
editIdle = currTime - self.docEditor.lastActive > self.mainConf.userIdleTime
|
||||
userIdle = qApp.applicationState() != Qt.ApplicationActive
|
||||
|
||||
if editIdle or userIdle:
|
||||
self.idleTime += currTime - self.idleRefTime
|
||||
self.statusBar.setUserIdle(True)
|
||||
else:
|
||||
self.statusBar.setUserIdle(False)
|
||||
|
||||
self.idleRefTime = currTime
|
||||
self.statusBar.updateTime(idleTime=self.idleTime)
|
||||
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _treeSingleClick(self):
|
||||
"""Single click on a project tree item just updates the details
|
||||
panel below the tree.
|
||||
@@ -1312,12 +1480,14 @@ class GuiMain(QMainWindow):
|
||||
self.treeMeta.updateViewBox(sHandle)
|
||||
return
|
||||
|
||||
@pyqtSlot("QTreeWidgetItem*", int)
|
||||
def _treeDoubleClick(self, tItem, colNo):
|
||||
"""The user double-clicked an item in the tree. If it is a file,
|
||||
we open it. Otherwise, we do nothing.
|
||||
"""
|
||||
tHandle = tItem.data(self.treeView.C_NAME, Qt.UserRole)
|
||||
logger.verbose("User double clicked tree item with handle %s" % tHandle)
|
||||
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
if nwItem is not None:
|
||||
if nwItem.itemType == nwItemType.FILE:
|
||||
@@ -1328,6 +1498,20 @@ class GuiMain(QMainWindow):
|
||||
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _treeNovelItemChanged(self):
|
||||
"""Triggered when there is a change to a novel item in the
|
||||
project tree.
|
||||
"""
|
||||
if self.mainTabs.currentIndex() == self.idxTabProj:
|
||||
logger.verbose("Novel tree changed while Outline tab active")
|
||||
if self.hasProject:
|
||||
self.treeView.flushTreeOrder()
|
||||
self.projView.refreshTree(novelChanged=True)
|
||||
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _treeKeyPressReturn(self):
|
||||
"""The user pressed return on an item in the tree. If it is a
|
||||
file, we open it. Otherwise, we do nothing. Pressing return does
|
||||
@@ -1335,6 +1519,7 @@ class GuiMain(QMainWindow):
|
||||
"""
|
||||
tHandle = self.treeView.getSelectedHandle()
|
||||
logger.verbose("User pressed return on tree item with handle %s" % tHandle)
|
||||
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
if nwItem is not None:
|
||||
if nwItem.itemType == nwItemType.FILE:
|
||||
@@ -1342,8 +1527,10 @@ class GuiMain(QMainWindow):
|
||||
self.openDocument(tHandle, changeFocus=False, doScroll=False)
|
||||
else:
|
||||
logger.verbose("Requested item %s is a folder" % tHandle)
|
||||
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _keyPressEscape(self):
|
||||
"""When the escape key is pressed somewhere in the main window,
|
||||
do the following, in order:
|
||||
@@ -1352,8 +1539,10 @@ class GuiMain(QMainWindow):
|
||||
self.docEditor.closeSearch()
|
||||
elif self.isFocusMode:
|
||||
self.toggleFocusMode()
|
||||
|
||||
return
|
||||
|
||||
@pyqtSlot(int)
|
||||
def _mainTabChanged(self, tabIndex):
|
||||
"""Activated when the main window tab is changed.
|
||||
"""
|
||||
@@ -1363,6 +1552,27 @@ class GuiMain(QMainWindow):
|
||||
logger.verbose("Project outline tab activated")
|
||||
if self.hasProject:
|
||||
self.projView.refreshTree()
|
||||
|
||||
return
|
||||
|
||||
@pyqtSlot(int)
|
||||
def _projTabsChanged(self, tabIndex):
|
||||
"""Activated when the project view tab is changed.
|
||||
"""
|
||||
sHandle = None
|
||||
|
||||
if tabIndex == self.idxTreeView:
|
||||
logger.verbose("Project tree tab activated")
|
||||
sHandle = self.treeView.getSelectedHandle()
|
||||
|
||||
elif tabIndex == self.idxNovelView:
|
||||
logger.verbose("Novel tree tab activated")
|
||||
if self.hasProject:
|
||||
self.novelView.refreshTree()
|
||||
sHandle = self.novelView.getSelectedHandle()
|
||||
|
||||
self.treeMeta.updateViewBox(sHandle)
|
||||
|
||||
return
|
||||
|
||||
# END Class GuiMain
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Minimal PEP 518 pyproject file
|
||||
[build-system]
|
||||
requires = ["setuptools", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[pytest]
|
||||
markers =
|
||||
base: Base functionality tests
|
||||
core: Core functionality tests
|
||||
base: Base classes tests
|
||||
core: Core classes tests
|
||||
gui: Qt5 GUI tests
|
||||
serial
|
||||
qt_api = pyqt5
|
||||
|
||||