Compare commits

...

467 Commits
v1.0 ... v1.2

Author SHA1 Message Date
Veronica Berglyd Olsen
814876f8df Merge pull request #699 from vkbo/release_1.2
Release 1.2
2021-03-14 18:11:44 +01:00
Veronica K. B. Olsen
32bcb52f14 Properly update release notes 2021-03-13 16:54:02 +01:00
Veronica K. B. Olsen
612ec06262 Updated changelog and release notes 2021-03-13 16:44:59 +01:00
Veronica K. B. Olsen
93dea90970 Merge branch 'main' into release_1.2 2021-03-13 16:35:27 +01:00
Veronica Berglyd Olsen
47ece1f285 Merge pull request #700 from vkbo/manual_updates
Updates for the documentation
2021-03-13 16:33:17 +01:00
Veronica Berglyd Olsen
64378419d0 Merge branch 'main' into manual_updates 2021-03-13 16:27:15 +01:00
Veronica K. B. Olsen
00a201181d Updated documentation for release 1.2 2021-03-13 16:25:59 +01:00
Veronica Berglyd Olsen
b3f9a14c85 Merge pull request #702 from vkbo/empty_trash
Empty Trash Fix
2021-03-09 12:06:51 +01:00
Veronica K. B. Olsen
065e3828b1 Update test and always ask before moving to trash 2021-03-09 11:02:44 +01:00
Veronica K. B. Olsen
63e91f0ee5 Fix broken emptu trash feature (#701) 2021-03-09 10:52:27 +01:00
jyhelle
182ef5c8f7 Corrections received by email 2021-03-05 19:10:59 +01:00
Veronica B. Olsen
b0f000acfd Bumped version for 1.2 2021-03-03 09:21:52 +01:00
Veronica K. B. Olsen
7a55af405b Update outdated info in README 2021-03-02 12:31:22 +01:00
Veronica K. B. Olsen
a3343bc738 Backport .gitignore from dev branch 2021-03-02 11:42:07 +01:00
Veronica Berglyd Olsen
a324ad2dd8 Merge pull request #696 from vkbo/testing
Testing to Main
2021-03-02 11:36:30 +01:00
Veronica Berglyd Olsen
38b4653cb3 Merge pull request #671 from vkbo/release_1.2rc1
Release 1.2 RC 1
2021-03-02 10:29:51 +01:00
Veronica K. B. Olsen
fdd9186ae9 Update release notes and bumped release date 2021-03-02 10:26:55 +01:00
Veronica K. B. Olsen
7125e9ecd5 Updated changelog 2021-02-24 22:44:01 +01:00
Veronica K. B. Olsen
869e4a0b9d Merge branch 'testing' into release_1.2rc1 2021-02-24 22:38:05 +01:00
Veronica K. B. Olsen
dc0e5c218f Merge branch 'main' into testing 2021-02-24 22:37:44 +01:00
Veronica K. B. Olsen
67bd23a027 Workflow updates and file cleanup 2021-02-24 20:47:00 +01:00
Veronica Berglyd Olsen
f22a350938 Create FUNDING.yml 2021-02-24 20:38:51 +01:00
Veronica Berglyd Olsen
6ba286087d Merge pull request #690 from vkbo/issue688_testing
Issue #688: Keyword indexing
2021-02-23 22:35:07 +01:00
Veronica K. B. Olsen
c2e3fef7fc Improve keyword indexer and extend test 2021-02-23 22:28:02 +01:00
Veronica K. B. Olsen
b9d344c7d9 Make sure the test cheks for issue 688 2021-02-23 22:07:14 +01:00
Veronica K. B. Olsen
fe04c1af25 Merge branch 'main' into testing 2021-02-23 21:14:30 +01:00
Veronica Berglyd Olsen
21d3ed61ef Merge pull request #689 from vkbo/issue688_wrong_tag
Issue #688: Indexing of invalid keywords
2021-02-23 21:14:03 +01:00
Veronica K. B. Olsen
b7f27a2af0 Don't save invalid keyword references to the index 2021-02-23 20:51:43 +01:00
Veronica B. Olsen
36aa58b38e Update changelog 2021-02-22 09:36:09 +01:00
Veronica B. Olsen
2f4fccfb2c Merge branch 'testing' into release_1.2rc1 2021-02-22 09:14:46 +01:00
Veronica B. Olsen
d251ebb995 Fix typo in changelog 2021-02-22 09:13:52 +01:00
Veronica K. B. Olsen
0c946516e8 Merge branch 'testing' into release_1.2rc1 2021-02-21 16:27:04 +01:00
Veronica K. B. Olsen
c77ef4e4ce Merge branch 'main' into testing 2021-02-21 16:23:50 +01:00
Veronica Berglyd Olsen
a9d90da5c8 Merge pull request #672 from vkbo/release_1.1.1
Release 1.1.1
2021-02-21 16:09:27 +01:00
Veronica K. B. Olsen
4071c2ca43 Last changes to release notes 2021-02-21 16:01:15 +01:00
Veronica K. B. Olsen
0a571502ce Merge branch 'main' into testing 2021-02-20 17:40:30 +01:00
Veronica K. B. Olsen
a850e5b74e Merge latest changes in main and update changelog and release notes 2021-02-20 17:40:17 +01:00
Veronica K. B. Olsen
71aa1fa113 Merge branch 'main' into release_1.1.1 2021-02-20 17:37:01 +01:00
Veronica Berglyd Olsen
24eef2dc0b Merge pull request #685 from vkbo/session_log_short
Drop short sessions from the log file
2021-02-20 17:35:53 +01:00
Veronica K. B. Olsen
5977c8c8e2 Fix test 2021-02-20 17:31:31 +01:00
Veronica K. B. Olsen
dcd4c19b21 Drop logging quick open and close sessions to the session log 2021-02-20 17:28:05 +01:00
Veronica K. B. Olsen
464c7dae0a Fix an issue in index with open a version 1.2 project 2021-02-20 17:27:32 +01:00
Veronica K. B. Olsen
ad92e2e5eb Update changelog 2021-02-20 17:09:47 +01:00
Veronica K. B. Olsen
254845d71f Update changelog and release notes 2021-02-20 16:41:33 +01:00
Veronica K. B. Olsen
3c72f195ba Bumped version for release 1.1.1 2021-02-13 12:54:16 +01:00
Veronica K. B. Olsen
f3f46858c9 Bumped version for release 1.2 RC 1 2021-02-13 12:48:41 +01:00
Veronica K. B. Olsen
d4631229af Merge branch 'main' into testing 2021-02-12 22:07:32 +01:00
Veronica Berglyd Olsen
a78f7d5789 Merge pull request #669 from vkbo/user_dict
Tool to Edit User Dictionary
2021-02-12 22:05:06 +01:00
Veronica K. B. Olsen
fca5c77089 Add GuiWordList test 2021-02-12 22:00:22 +01:00
Veronica Berglyd Olsen
fe828a1c49 Merge pull request #670 from Symbian9/patch-1
Fix installation guide links order
2021-02-12 21:04:42 +01:00
app4soft
11e3573ebd Fix installation guide links order 2021-02-12 22:00:09 +02:00
Veronica K. B. Olsen
ff1d625dc8 Fix logging of exception 2021-02-12 20:26:37 +01:00
Veronica K. B. Olsen
bd9691532f Merge branch 'testing' into user_dict 2021-02-12 20:25:05 +01:00
Veronica K. B. Olsen
133b597093 Merge branch 'main' into testing 2021-02-12 20:23:50 +01:00
Veronica Berglyd Olsen
d4d540bc33 Merge pull request #668 from vkbo/spelling_speedup
Simple Spell Check Speedup
2021-02-12 20:22:11 +01:00
Veronica K. B. Olsen
d08322c1a3 No point making a list to convert to a set 2021-02-12 20:18:32 +01:00
Veronica K. B. Olsen
ec1c664ed6 Fix test 2021-02-12 20:15:38 +01:00
Veronica K. B. Olsen
a3b46581b0 Make the simple spell checker use a set instead of a list 2021-02-12 20:15:28 +01:00
Veronica K. B. Olsen
b115d4f02d Finish the word list dialog and make it possible to reload the user dictionary with pyenchant 2021-02-12 19:40:10 +01:00
Veronica K. B. Olsen
6e64ede90b Add a starting widget for the word liste ditor and connect it to the GUI 2021-02-12 17:01:46 +01:00
Veronica K. B. Olsen
c06c2422a9 Merge branch 'main' into testing 2021-02-11 23:13:32 +01:00
Veronica K. Berglyd Olsen
14d205f845 Merge pull request #666 from vkbo/ctrl_del
Change Delete Item Shortcut
2021-02-11 22:06:39 +00:00
Veronica K. B. Olsen
a57ce557e8 Clean up lisence header of main init file 2021-02-11 22:59:19 +01:00
Veronica K. B. Olsen
23c200f333 Update docs 2021-02-11 22:53:58 +01:00
Veronica K. B. Olsen
bf6a3adc06 Change the delete item shortcut 2021-02-11 22:53:14 +01:00
Veronica K. Berglyd Olsen
60a8c8212c Merge pull request #664 from vkbo/minor_fixes
Minor Fixes for 1.2 Beta 1
2021-02-11 21:51:15 +00:00
Veronica K. B. Olsen
525c9c8fd8 Revert last commit and instead change delete item shortcut 2021-02-11 19:27:35 +01:00
Veronica K. B. Olsen
08651685d4 Allow Ctrl+Del to be propagated to the document editor 2021-02-11 19:11:41 +01:00
Veronica K. B. Olsen
3a60abaeee F2 should only edit item if project tree has focus, and edit current document meta if editor has focus 2021-02-11 18:57:44 +01:00
Veronica K. B. Olsen
1e5d980b85 Fix the label of the idle time switch on Writing Statistics 2021-02-11 18:42:28 +01:00
Veronica K. Berglyd Olsen
df36aceddb Merge pull request #663 from vkbo/release_1.2beta1
Release 1.2 Beta 1
2021-02-11 16:08:31 +00:00
Veronica K. B. Olsen
600e1cdbfb Added release notes 2021-02-11 17:04:48 +01:00
Veronica K. B. Olsen
bc0cd2332c Changelog clarifications and corrected typos 2021-02-10 22:59:13 +01:00
Veronica K. B. Olsen
c1b5287ba0 Bumped version to 1.2beta1 and updated changelog 2021-02-10 22:49:32 +01:00
Veronica K. Berglyd Olsen
a2a43b3996 Merge pull request #662 from vkbo/issue605_focus_char
Issue #605: Focus Character
2021-02-10 21:07:25 +00:00
Veronica K. B. Olsen
6c07ecb700 Fix a typo 2021-02-10 22:03:25 +01:00
Veronica K. B. Olsen
2d3f848ab3 Update documentation with references to the @focus keyword 2021-02-10 21:58:25 +01:00
Veronica K. B. Olsen
fdd188516b Update sample project and test 2021-02-10 21:47:42 +01:00
Veronica K. B. Olsen
4c95b1d7ad Add Focus reference to the GUI elements 2021-02-10 21:47:23 +01:00
Veronica K. B. Olsen
19b7b51eea Add the @focus keyword to the base constant classes 2021-02-10 21:46:55 +01:00
Veronica K. Berglyd Olsen
3c355e2d75 Merge pull request #660 from vkbo/build_tool
Build Tool Updates
2021-02-10 19:30:40 +00:00
Veronica K. Berglyd Olsen
290c2f7b62 Merge branch 'dev' into build_tool 2021-02-10 18:34:51 +00:00
Veronica K. Berglyd Olsen
be7bcd03e1 Merge pull request #661 from vkbo/fix_idle_stats
Fix idle Time in Writing Stats
2021-02-10 18:34:04 +00:00
Veronica K. B. Olsen
0965f4213e Fix an issue with summing up idle time when grouping writing stats by day 2021-02-10 19:27:32 +01:00
Veronica K. B. Olsen
bb9ca6e40f Reorder HTML cache file 2021-02-10 18:48:13 +01:00
Veronica K. B. Olsen
672ea6d6f2 Fix and update tests 2021-02-10 16:57:32 +01:00
Veronica K. B. Olsen
ec189a4867 Clean up the Build dialog and connect new options 2021-02-10 16:57:15 +01:00
Veronica K. B. Olsen
f67a50c064 Change the way Unicode is handled in HTML, and clean up the class a bit 2021-02-10 16:56:52 +01:00
Veronica K. B. Olsen
a0fba34ff6 Apply line height and margins to HTML output 2021-02-10 16:55:46 +01:00
Veronica K. B. Olsen
7ea45ac3d2 Cleanup Build dialog and add line height to HTML 2021-02-10 10:39:25 +01:00
Veronica K. B. Olsen
78a58617c1 Connect line height to Build dialog 2021-02-10 10:38:22 +01:00
Veronica K. Berglyd Olsen
f098aeb622 Merge pull request #656 from vkbo/session_idle
Session Idle Time
2021-02-09 20:36:10 +00:00
Veronica K. B. Olsen
6904afc5e7 Updated tests 2021-02-09 21:09:03 +01:00
Veronica K. B. Olsen
b079b30694 Merge branch 'dev' into session_idle 2021-02-09 20:55:26 +01:00
Veronica K. B. Olsen
3e8a425684 Merge branch 'main' into dev 2021-02-09 20:54:31 +01:00
Veronica K. Berglyd Olsen
b47c4c8f71 Merge pull request #659 from vkbo/writing_stats_calc
Make writing stats word calculation more intuitive
2021-02-09 19:54:03 +00:00
Veronica K. B. Olsen
6d3097ae76 Update test 2021-02-09 20:48:48 +01:00
Veronica K. B. Olsen
09e521c2c2 Make writing stats word calculation more intuitive 2021-02-09 19:57:14 +01:00
Veronica K. B. Olsen
2439cf4633 Writing Stats tool should show idle time as either percantage or actual time 2021-02-09 19:46:00 +01:00
Veronica K. B. Olsen
f1b31f26d0 Add preferences for controlling session timer behaviour 2021-02-09 18:54:44 +01:00
Veronica K. B. Olsen
b1eca89a05 Switch icon when session timer is paused 2021-02-09 18:28:22 +01:00
Veronica K. B. Olsen
09bf2a0fe0 Forgot to consider GUI scale 2021-02-08 23:51:03 +01:00
Veronica K. B. Olsen
2d754641b5 Add a switch to hide the idle time column 2021-02-08 23:40:21 +01:00
Veronica K. B. Olsen
871a0dfc6c Fix tests 2021-02-08 23:26:40 +01:00
Veronica K. B. Olsen
3521d4a4f7 Add idle time to session log and writing stats dialog 2021-02-08 23:17:33 +01:00
Veronica K. B. Olsen
407f12760c Record idle time 2021-02-08 21:58:38 +01:00
Veronica K. B. Olsen
479975fdff Move clock tick timer to main GUI 2021-02-08 21:33:06 +01:00
Veronica K. B. Olsen
98fae8a6de Merge branch 'main' into dev 2021-02-08 17:35:41 +01:00
Veronica K. Berglyd Olsen
2e94104ff8 Merge pull request #655 from vkbo/improvements
Various Improvements
2021-02-08 16:34:48 +00:00
Veronica K. B. Olsen
039c13fc0a Add some clarifications to the changelog 2021-02-08 17:29:58 +01:00
Veronica K. B. Olsen
de266e8c88 Add branching policy to the contributing guide 2021-02-08 17:08:48 +01:00
Veronica K. B. Olsen
ada99077a4 Fix some issues with resolving paths for a packaged single executable 2021-02-08 09:44:10 +01:00
Veronica K. B. Olsen
f5588f94db Tweak the project config files 2021-02-08 09:19:53 +01:00
Veronica K. B. Olsen
2a325ae4cc GUI font on Windows should default to Arial first, and fall back to Cantarell 2021-02-07 21:43:32 +01:00
Veronica K. B. Olsen
ac9345419e Merge branch 'main' into dev 2021-02-07 21:14:34 +01:00
Veronica K. B. Olsen
b3fb020dfa Drop using the __status__ variable 2021-02-07 21:12:31 +01:00
Veronica K. Berglyd Olsen
4b885a6e30 Merge pull request #648 from vkbo/release_1.1
Release 1.1
2021-02-07 19:09:48 +00:00
Veronica K. B. Olsen
fa814d64de Updated release notes 2021-02-07 19:45:46 +01:00
Veronica K. B. Olsen
8aa376de25 Merge branch 'main' into release_1.1 2021-02-07 19:43:39 +01:00
Veronica K. Berglyd Olsen
14e7f2d986 Merge pull request #652 from vkbo/odt_header
Add Header to Open Document export
2021-02-07 14:14:44 +00:00
Veronica K. B. Olsen
f816f3d29b Turn document margins into settings 2021-02-06 23:28:41 +01:00
Veronica K. B. Olsen
3db9987966 Add a default header to Open Document files 2021-02-06 23:22:23 +01:00
Veronica K. B. Olsen
5e40bd5deb Change text on build tool print button 2021-02-06 18:31:37 +01:00
Veronica K. B. Olsen
74a9ddbe47 Fix wrong version in ToOdt class header 2021-02-06 18:29:59 +01:00
Veronica K. Berglyd Olsen
188c2ae7ea Merge pull request #650 from vkbo/markdown_export
Markdown Export
2021-02-06 17:05:11 +00:00
Veronica K. B. Olsen
e645c19761 Fix flake8 error 2021-02-06 18:01:03 +01:00
Veronica K. B. Olsen
baf9cab66c Fix a few other docs sections about export 2021-02-06 17:55:29 +01:00
Veronica K. B. Olsen
2fd940bcde Update documentation 2021-02-06 17:48:59 +01:00
Veronica K. B. Olsen
f4988c5d86 Clean up build tool and extend export tests 2021-02-06 17:40:39 +01:00
Veronica K. B. Olsen
b59b3a3a74 Use the old converter for nwd file, not the markdown class 2021-02-06 17:19:30 +01:00
Veronica K. B. Olsen
c677db70b4 Fix a few issues with markdown export 2021-02-06 16:38:06 +01:00
Veronica K. B. Olsen
9861f294b7 Connect markdown exporter to build tool 2021-02-06 15:20:03 +01:00
Veronica K. B. Olsen
c7a42a1641 Basic to markdown class added 2021-02-06 15:07:08 +01:00
Veronica K. B. Olsen
e2a721b209 Merge branch 'main' into dev 2021-02-06 14:35:09 +01:00
Veronica K. Berglyd Olsen
66090e3926 Merge pull request #649 from vkbo/branch_and_ci
Branch and Actions
2021-02-06 13:18:31 +00:00
Veronica K. B. Olsen
60395fb16d Change badges in readme 2021-02-06 14:14:47 +01:00
Veronica K. B. Olsen
54881facf8 Change release branch to testing, and bump mac and windows action to Python 3.9 2021-02-06 14:07:08 +01:00
Veronica K. B. Olsen
9f2f131df9 Merge branch 'main' into dev 2021-02-06 14:03:22 +01:00
Veronica K. B. Olsen
70ab4122a3 Updated version to 1.1 and updated changelog 2021-02-06 13:59:54 +01:00
Veronica K. Berglyd Olsen
7302fb951e Merge pull request #639 from vkbo/minor_tweaks
Minor Tweaks
2021-02-06 12:38:48 +00:00
Veronica K. B. Olsen
dee8ccd8b3 Search box should select the text on open #645 2021-02-06 13:35:53 +01:00
Veronica K. B. Olsen
40b2efb339 Remove some redundant line breaks 2021-02-06 13:35:20 +01:00
Veronica K. B. Olsen
190e74b4d2 Clarify and cross-reference the two sections of the docs describing references and tags 2021-02-06 01:41:56 +01:00
Veronica K. B. Olsen
8bec08799e Merge branch 'release' into dev 2021-02-05 23:08:48 +01:00
Veronica K. B. Olsen
003b056301 Merge branch 'release' into minor_tweaks 2021-02-05 23:08:35 +01:00
Veronica K. B. Olsen
97fd58f793 Merge branch 'main' into release 2021-02-05 23:08:05 +01:00
Veronica K. Berglyd Olsen
aa70d410f8 Merge pull request #644 from vkbo/packaging_cleanup
Packaging Cleanup
2021-02-05 22:07:12 +00:00
Veronica K. B. Olsen
ab21a9238d Update Windows Setup documentation 2021-02-05 17:28:16 +01:00
Veronica K. B. Olsen
be080b1463 Remove the pyinstaller setup functions 2021-02-05 17:27:56 +01:00
Veronica K. B. Olsen
f8ef3e6754 Add flag to tailor minimal zip package to different OSes 2021-02-05 16:52:23 +01:00
Veronica K. Berglyd Olsen
e4e8b939e2 Merge pull request #643 from vkbo/minimal_zip
Minimal Zip Package
2021-02-04 22:20:59 +00:00
Veronica K. B. Olsen
020f122647 Add size 192 scaled icons 2021-02-04 23:16:31 +01:00
Veronica K. B. Olsen
ab0626720a Add setup files to the minimal zip packager in setup.py 2021-02-04 23:16:14 +01:00
Veronica K. B. Olsen
637c05466a Compress minimal zip file 2021-02-03 21:45:22 +01:00
Veronica K. B. Olsen
08b98739f7 Fix typo 2021-02-03 21:37:35 +01:00
Veronica K. B. Olsen
920a835344 Merge branch 'release' into dev 2021-02-03 21:33:42 +01:00
Veronica K. B. Olsen
88e27e315f Merge branch 'release' into minor_tweaks 2021-02-03 21:33:25 +01:00
Veronica K. B. Olsen
7d3427474d Merge branch 'main' into release 2021-02-03 21:32:29 +01:00
Veronica K. Berglyd Olsen
d77183fd48 Merge pull request #635 from vkbo/release_1.0.4
Release 1.0.4
2021-02-03 19:55:38 +00:00
Veronica K. B. Olsen
caa601c9aa Update of release notes and changelog before release 1.0.4 2021-02-03 20:52:36 +01:00
Veronica K. B. Olsen
b9a0152000 Merge branch 'main' into release_1.0.4 2021-02-03 20:41:32 +01:00
Veronica K. Berglyd Olsen
5c5e4e3464 Merge pull request #642 from vkbo/minimal_zip
Minimal Zip Package
2021-02-03 19:40:42 +00:00
Veronica K. B. Olsen
044f987ecd Include the CHANGELOG in the minimal zip 2021-02-03 20:37:04 +01:00
Veronica K. B. Olsen
295351d3e9 Fix a few issues with the minimal zip file and update README 2021-02-03 20:33:32 +01:00
Veronica K. B. Olsen
ab7ca411a4 Add setup command to build a minimal zip file 2021-02-03 20:13:15 +01:00
Veronica K. B. Olsen
c0dee497cd Updated readme and changelog 2021-02-03 19:38:34 +01:00
Veronica K. B. Olsen
247e51478b Merge branch 'main' into release_1.0.4 2021-02-03 17:49:35 +01:00
Veronica K. Berglyd Olsen
a0540e0452 Merge pull request #641 from vkbo/win_install_cont
Windows Install Continued
2021-02-03 16:48:41 +00:00
Veronica K. B. Olsen
515d421dc0 Fix a couple of setup issues and add a note to the docs about updating python 2021-02-03 17:36:25 +01:00
Veronica K. B. Olsen
6032f52798 Update documentation 2021-02-03 16:59:07 +01:00
Veronica K. B. Olsen
05989fdc58 Improve setup for Windows 2021-02-03 16:58:42 +01:00
Veronica K. B. Olsen
b62fafe520 Add source headers to setup and start files, and keep windows start file 2021-02-03 15:21:01 +01:00
Veronica K. B. Olsen
4562a0130a Add a button to the Project Open to remove listed projects, and clarify the dialog message 2021-02-02 22:06:02 +01:00
Veronica K. B. Olsen
6949481198 Stop lookup error on none in project tree meta panel 2021-02-02 16:36:45 +01:00
Veronica K. B. Olsen
5654170278 Let the main gui handle the novel tree refresh decision 2021-02-02 16:36:19 +01:00
Veronica K. Berglyd Olsen
735a324f5a Merge pull request #638 from vkbo/fix_duplicate_from_issue636
Fix Duplicate Code from PR #637
2021-02-01 22:25:47 +00:00
Veronica K. B. Olsen
2cb51f5b58 Backport a small fix from the dev branch 2021-02-01 23:24:02 +01:00
Veronica K. B. Olsen
3baeef4f44 Drop the whole firstTitle index again 2021-02-01 23:20:07 +01:00
Veronica K. B. Olsen
cada78ca4d use what was added in #637 to replace first title lookup in editor saveText 2021-02-01 23:19:24 +01:00
Veronica K. B. Olsen
a2929ef441 Merge branch 'release' into dev 2021-02-01 23:02:40 +01:00
Veronica K. Berglyd Olsen
6c7c8b43b9 Merge pull request #637 from vkbo/issue636_noveltab_wcount
Issue #636: Novel Tab Word Count
2021-02-01 21:59:06 +00:00
Veronica K. B. Olsen
d0940708b0 Add a header structure record to the editor, and force an update of te novel tree if header levels change 2021-02-01 22:49:11 +01:00
Veronica K. B. Olsen
af21f3c5d8 Add index functions to extract headers and word counts for a given handle 2021-02-01 22:48:07 +01:00
Veronica K. B. Olsen
dd18040232 Fix some source file headers 2021-02-01 21:52:51 +01:00
Veronica K. B. Olsen
c72e3a1585 Bumped version to 1.0.4 and updated changelog 2021-02-01 21:17:30 +01:00
Veronica K. B. Olsen
888a47e7a9 Merge branch 'release' into dev 2021-02-01 17:53:40 +01:00
Veronica K. B. Olsen
0df51d2d7c Merge branch 'main' into release 2021-02-01 17:53:17 +01:00
Veronica K. Berglyd Olsen
022945b0d1 Merge pull request #634 from vkbo/win_install
Windows Installation Improvements
2021-02-01 16:51:52 +00:00
Veronica K. B. Olsen
0e62284671 Allow the win-install option to keep icons for both stable and testing version 2021-02-01 17:30:21 +01:00
Veronica K. B. Olsen
704103f9f1 Reorganised and update install instruction in the docs 2021-02-01 17:12:59 +01:00
Veronica K. B. Olsen
1f2ffe7e76 Add win-install command to setup script 2021-02-01 14:14:38 +01:00
Veronica K. Berglyd Olsen
b763abdc76 Merge pull request #632 from vkbo/actions_refactoring
Menu Actions Refactoring and Extension
2021-01-31 22:07:09 +00:00
Veronica K. B. Olsen
af2fcbc059 Add a simple test for now 2021-01-31 23:01:36 +01:00
Veronica K. B. Olsen
0c4d278de2 Extract duplicate code into a new function 2021-01-31 22:53:58 +01:00
Veronica K. B. Olsen
9596d24cca Some final tweaks to the drag and drop feature 2021-01-31 22:29:39 +01:00
Veronica K. B. Olsen
a52b71ef43 Remove ToC.txt files from repo 2021-01-31 22:06:13 +01:00
Veronica K. B. Olsen
216289455e Merge branch 'dev' into actions_refactoring 2021-01-31 19:48:43 +01:00
Veronica K. B. Olsen
019d8cf756 Merge branch 'release' into dev 2021-01-31 19:47:22 +01:00
Veronica K. Berglyd Olsen
3a1c09f89c Merge pull request #596 from vkbo/release_1.1rc1
Release 1.1 RC1
2021-01-31 17:17:23 +00:00
Veronica K. B. Olsen
e9ed4a3f93 Updated changelog and added release notes 2021-01-31 18:12:35 +01:00
Veronica K. B. Olsen
164d6b1b6c Merge branch 'release' into release_1.1rc1 2021-01-31 17:46:37 +01:00
Veronica K. B. Olsen
2307dfaaac Add feature to undo last project tree move 2021-01-31 17:44:28 +01:00
Veronica K. B. Olsen
3cecc1229f Fix GUI blocking in tests 2021-01-31 13:44:44 +01:00
Veronica K. B. Olsen
cca895e418 Cleanup in main menu and move the Move Up/Down entries to the project menu 2021-01-31 13:35:40 +01:00
Veronica K. B. Olsen
ddda934eed Fix tests 2021-01-31 12:56:58 +01:00
Veronica K. B. Olsen
1d8c9ecfc4 Merge branch 'dev' into actions_refactoring 2021-01-31 12:53:03 +01:00
Veronica K. B. Olsen
1f20574520 Merge branch 'release' into dev 2021-01-31 12:51:57 +01:00
Veronica K. B. Olsen
da81bc3ba5 Merge branch 'main' into release 2021-01-31 12:51:36 +01:00
Veronica K. B. Olsen
013fb58267 Block document action if editor does not have focus 2021-01-31 12:50:42 +01:00
Veronica K. B. Olsen
f4f2feae3c Merge branch 'main' into actions_refactoring 2021-01-31 12:45:57 +01:00
Veronica K. Berglyd Olsen
ffad6e47ec Merge pull request #631 from vkbo/issue629_ctrl_del_action
Issue #629: ctrl del action
2021-01-31 11:27:40 +00:00
Veronica K. Berglyd Olsen
2b8f884c12 Merge pull request #630 from vkbo/revert-626-doc_meta
Revert "Add timestamp to document meta"
2021-01-31 11:20:21 +00:00
Veronica K. Berglyd Olsen
2df0d81d29 Revert "Add timestamp to document meta" 2021-01-31 11:59:24 +01:00
Veronica K. B. Olsen
b02c4904a9 Fix tests 2021-01-31 11:58:56 +01:00
Veronica K. B. Olsen
f73cdac9e2 Only allow the delete item command in the project tree if it has focus 2021-01-31 11:53:38 +01:00
Veronica K. Berglyd Olsen
de848511af Merge pull request #628 from vkbo/various_improvements
Various Improvements
2021-01-30 22:39:00 +00:00
Veronica K. B. Olsen
9e294529cf When a new file is added, automatically add the title line 2021-01-30 23:31:19 +01:00
Veronica K. B. Olsen
96d6accbde Add layout and class info to editor footer bar 2021-01-30 22:55:38 +01:00
Veronica K. B. Olsen
86e276ea94 Merge branch 'release' into dev 2021-01-30 22:38:55 +01:00
Veronica K. B. Olsen
8566d62aee Merge branch 'main' into release 2021-01-30 22:37:06 +01:00
Veronica K. Berglyd Olsen
0ccc78e90a Merge pull request #626 from vkbo/doc_meta
Add timestamp to document meta
2021-01-30 18:59:55 +00:00
Veronica K. Berglyd Olsen
3c86207d63 Merge pull request #627 from vkbo/load_project_order_bug
Fix "Open Project" list sorting issue
2021-01-30 18:59:24 +00:00
Veronica K. B. Olsen
c2a37c814d Fix Open Project list sorting issue 2021-01-30 19:43:00 +01:00
Veronica K. B. Olsen
4727866b15 Update test projects 2021-01-30 19:00:12 +01:00
Veronica K. B. Olsen
7a3c33fde3 Fix tests 2021-01-30 18:54:30 +01:00
Veronica K. B. Olsen
087735a08f Add timestamp to document meta data 2021-01-30 18:54:13 +01:00
Veronica K. B. Olsen
a3077d91ec Updated changelog 2021-01-30 15:21:16 +01:00
Veronica K. B. Olsen
649763f2b1 Merge branch 'release' into release_1.1rc1 2021-01-30 15:18:10 +01:00
Veronica K. Berglyd Olsen
f9b988a232 Merge pull request #620 from vkbo/item_layouts
Automatic Item Layouts
2021-01-30 14:16:14 +00:00
Veronica K. B. Olsen
ab45bcb121 Merge branch 'dev' into item_layouts 2021-01-30 15:13:13 +01:00
Veronica K. B. Olsen
e9190d01dc Merge branch 'release' into dev 2021-01-30 15:12:32 +01:00
Veronica K. B. Olsen
ba0e98fd4b Merge branch 'main' into release 2021-01-30 15:11:51 +01:00
Veronica K. Berglyd Olsen
40cc5b379b Merge pull request #624 from vkbo/prefs_fixes
Preferences Fixes
2021-01-30 14:06:37 +00:00
Veronica K. Berglyd Olsen
b1cb48aa9b Merge pull request #625 from vkbo/issue623_justify_prefs
Fix issue #623
2021-01-30 14:06:23 +00:00
Veronica K. B. Olsen
82d9165d92 Fix test 2021-01-30 15:00:51 +01:00
Veronica K. B. Olsen
39d4ee9ad8 More Preferences clarifications 2021-01-30 14:59:17 +01:00
Veronica K. B. Olsen
2cdd444e29 Fix issue #623 2021-01-30 14:48:47 +01:00
Veronica K. B. Olsen
d492f0f41f Rename Preferences variables to match those in the Config class 2021-01-30 14:41:04 +01:00
Veronica K. B. Olsen
943e085d37 Fix a bug and some misleading labels in Preferences 2021-01-30 14:24:06 +01:00
Veronica K. B. Olsen
0624f9103f Get test coverage of NWTree back to 100% 2021-01-30 01:28:05 +01:00
Veronica K. B. Olsen
cab1530afc Add debug log output for layout change 2021-01-30 01:03:54 +01:00
Veronica K. B. Olsen
90e63f1898 Merge branch 'dev' into item_layouts 2021-01-30 00:57:42 +01:00
Veronica K. B. Olsen
b2fdd92f9c Merge branch 'release' into dev 2021-01-30 00:57:22 +01:00
Veronica K. Berglyd Olsen
91701b484a Merge pull request #621 from vkbo/issue608_cursor_in_blank_doc
Issue #608: Missing Cursor
2021-01-29 22:08:59 +00:00
Veronica K. B. Olsen
9fdae1ac45 Fix missing cursor when opening empty document 2021-01-29 22:38:07 +01:00
Veronica K. B. Olsen
b7766946d8 Update index check and test 2021-01-29 20:43:04 +01:00
Veronica K. B. Olsen
9486749345 Merge branch 'dev' into item_layouts 2021-01-29 20:20:16 +01:00
Veronica K. Berglyd Olsen
a3bec4d5fc Merge pull request #619 from vkbo/index_check
More thorough index check
2021-01-29 19:18:06 +00:00
Veronica K. B. Olsen
d8cc091f94 Also use the new nwItem checkers in the NWItem class 2021-01-29 18:39:48 +01:00
Veronica K. B. Olsen
89ba4aeeaf Use the logException function in all places where exception messages are written to the error log 2021-01-29 18:29:50 +01:00
Veronica K. B. Olsen
a2f223a992 Perform a detailed check of the loaded index to make sure everything is in order 2021-01-29 18:15:00 +01:00
Veronica K. B. Olsen
286efade59 Add more check functions 2021-01-29 13:22:38 +01:00
Veronica K. B. Olsen
4e7888259f Add logging wrapper for exceptions 2021-01-29 13:17:31 +01:00
Veronica K. B. Olsen
8e3fa892e1 Modify conditions according to #618 2021-01-29 11:55:36 +01:00
Veronica K. B. Olsen
8de0c55b68 Fix broken tests 2021-01-29 01:14:58 +01:00
Veronica K. B. Olsen
4c27d2600a Update document layout during editor save process 2021-01-29 01:09:04 +01:00
Veronica K. B. Olsen
59cfb14a19 Add a first title index that tracks the level and location of the first title of a file 2021-01-29 00:57:34 +01:00
Veronica K. B. Olsen
6f7cf4f186 Bump dev version to 1.2-alpha0 2021-01-28 22:09:28 +01:00
Veronica K. B. Olsen
52454c50b7 Updated changelog 2021-01-28 21:49:29 +01:00
Veronica K. B. Olsen
c56219f5b4 Merge branch 'release' into release_1.1rc1 2021-01-28 21:41:35 +01:00
Veronica K. B. Olsen
99b6e1f93c Merge branch 'release' into dev 2021-01-28 21:35:43 +01:00
Veronica K. Berglyd Olsen
6ff1549d2e Merge pull request #616 from vkbo/issue610_save_doc_on_build
Issue #610: Save document before build
2021-01-28 20:33:20 +00:00
Veronica K. B. Olsen
1b05e8ddc9 Save current document before building project 2021-01-28 21:26:51 +01:00
Veronica K. Berglyd Olsen
4745877a63 Merge pull request #615 from vkbo/issue612_change_focus
Issue #612: Change Focus
2021-01-28 20:17:43 +00:00
Veronica K. B. Olsen
c81c7f5a33 Fix syntax check for release branch 2021-01-28 21:13:19 +01:00
Veronica K. B. Olsen
a23beddc66 Update documentation with new shortcut key 2021-01-28 21:07:58 +01:00
Veronica K. B. Olsen
045929681d Add Alt+4 focus key, and switch main tab on focus change 2021-01-28 21:04:22 +01:00
Veronica K. Berglyd Olsen
cf0cf52298 Merge pull request #607 from vkbo/odt_format
Open Document Export
2021-01-28 19:54:00 +00:00
Veronica K. B. Olsen
1ff7c8bdd5 Update reference files for build test 2021-01-28 20:48:49 +01:00
Veronica K. B. Olsen
7254f766d5 Remove div tags from HTML output and bring Tokenizer and ToHtml tests back to 100% 2021-01-28 20:42:39 +01:00
Veronica K. B. Olsen
83e07ec76c Fix tests 2021-01-28 17:04:33 +01:00
Veronica K. B. Olsen
c3a1f25894 Reshuffle build and preview in Build Novel Project tool 2021-01-28 17:04:14 +01:00
Veronica K. B. Olsen
750d8b0a25 Clean up ODT code and add some comments 2021-01-28 01:23:48 +01:00
Veronica K. B. Olsen
2e4558a5d3 Bypass html cache when not building html doc 2021-01-28 00:40:10 +01:00
Veronica K. B. Olsen
3e5813323b Drop the manifest.rdf file 2021-01-28 00:22:53 +01:00
Veronica K. B. Olsen
ecd858251f Add code for writing ODT files 2021-01-28 00:21:39 +01:00
Veronica K. B. Olsen
ac0847ffe3 Predefine some XML tags 2021-01-27 21:54:14 +01:00
Veronica K. B. Olsen
d1bf49790e Add exports for comments, synopsis and keywords 2021-01-27 21:48:20 +01:00
Veronica K. B. Olsen
815f24fbf5 Add release branch for release candidates 2021-01-27 19:53:53 +01:00
Veronica K. B. Olsen
d83352e16f Fix Build Novel Project test on Ubuntu 2021-01-27 19:49:21 +01:00
Veronica K. B. Olsen
8715ab9322 Add logic to apply formatting to text 2021-01-27 19:36:34 +01:00
Veronica K. B. Olsen
4df294f0a2 Add customised margin sizes 2021-01-27 16:22:59 +01:00
Veronica K. B. Olsen
cbbe1b4f47 Add ODT meta namespace, add font format to HTML, and merge page break settings 2021-01-27 13:36:09 +01:00
Veronica K. B. Olsen
8ccd6a8ed3 Fix setting font setting for ODT export 2021-01-27 12:38:31 +01:00
Veronica K. B. Olsen
cbd222fe30 Add code to save tabs and line breaks to ODT file 2021-01-27 02:35:19 +01:00
Veronica K. B. Olsen
1683567dda Restructure Build Novel Project tool to allow ODT save 2021-01-27 01:12:34 +01:00
Veronica K. B. Olsen
da2e40db4d Add ODT auto-style logic 2021-01-27 01:12:09 +01:00
Veronica K. B. Olsen
e3c37f003b Add main ODT style XML 2021-01-26 20:22:32 +01:00
Veronica K. B. Olsen
c9eebafb2d Add core structure for ODT class 2021-01-26 16:25:14 +01:00
Veronica K. Berglyd Olsen
751da877ee Merge pull request #604 from vkbo/insert_symbols
More Insert Symbols
2021-01-26 15:09:08 +00:00
Veronica K. B. Olsen
14d19b5117 Fix indentation of Project Details source file header 2021-01-26 16:06:36 +01:00
Veronica K. B. Olsen
41d4c883a3 Add horizontal bar 2021-01-26 16:03:30 +01:00
Veronica K. B. Olsen
09504508d9 Add proper minus sign, and change a few shortcuts to more sensible ones 2021-01-26 14:03:19 +01:00
Veronica K. B. Olsen
94c755a0e6 Fix tests and clean up a couple of minor things in HTML generator 2021-01-26 13:12:38 +01:00
Veronica K. B. Olsen
f21cff335b Drop the triangle bullet as it's not always available in fonts 2021-01-26 13:11:26 +01:00
Veronica K. B. Olsen
6947996c9a Correct name of HTML constant for cross symbol 2021-01-25 23:50:47 +01:00
Veronica K. B. Olsen
0756090081 Update documentations on new symbols 2021-01-25 23:45:28 +01:00
Veronica K. B. Olsen
95e03d2684 Add triangle and hyphen bullets 2021-01-25 23:45:11 +01:00
Veronica K. B. Olsen
fab5833cdf Add degree, times and divide symbols to insert menu, and simply the editor insert function 2021-01-25 23:05:41 +01:00
Veronica K. Berglyd Olsen
a011c10d4d Merge pull request #603 from vkbo/various_changes
Various Minor Changes
2021-01-25 16:24:50 +00:00
Veronica K. B. Olsen
14584bef55 Update docs and tests 2021-01-25 17:15:12 +01:00
Veronica K. B. Olsen
907a371bd4 Some minor improvements to the logging system 2021-01-25 17:11:05 +01:00
Veronica K. B. Olsen
a420c1749a Add more insert menu entries 2021-01-25 17:09:39 +01:00
Veronica K. B. Olsen
c29e9bb68d Project Details Content tree should not have selectable rows 2021-01-25 11:28:18 +01:00
Veronica K. B. Olsen
fd965e6022 Updated changelog 2021-01-25 11:24:37 +01:00
Veronica K. B. Olsen
2e8d7a20d0 Merge branch 'dev' into release_1.1rc1 2021-01-25 11:20:02 +01:00
Veronica K. Berglyd Olsen
8084ed191e Merge pull request #598 from vkbo/improve_details
Improve Project Details
2021-01-25 10:18:20 +00:00
Veronica K. B. Olsen
0b51e50c19 Add a title to the Contents tab in project Details 2021-01-24 22:57:32 +01:00
Veronica K. B. Olsen
659e056eeb Fix flake8 complaint 2021-01-24 22:54:40 +01:00
Veronica K. B. Olsen
c70023c066 Fix tests 2021-01-24 22:49:23 +01:00
Veronica K. B. Olsen
ba2bdf8ea4 Add page offset spinbox to Contents tab on Project Details dialog 2021-01-24 22:49:13 +01:00
Veronica K. B. Olsen
57330bb2cd Make sure project tree is updated before Project Details dialog is opened 2021-01-24 22:47:49 +01:00
Veronica K. B. Olsen
16fdef2d55 Merge branch 'dev' into release_1.1rc1 2021-01-24 14:52:59 +01:00
Veronica K. B. Olsen
31928d5ebb Merge branch 'main' into dev 2021-01-24 14:52:35 +01:00
Veronica K. Berglyd Olsen
8370b99302 Merge pull request #594 from vkbo/release_1.0.3
Release 1.0.3
2021-01-24 13:19:53 +00:00
Veronica K. B. Olsen
13f98e1675 Update changelog and release notes 2021-01-24 14:14:43 +01:00
Veronica K. B. Olsen
615804333e Merge branch 'main' into release_1.0.3 2021-01-24 13:56:26 +01:00
Veronica K. Berglyd Olsen
e1b9e113b1 Merge pull request #597 from vkbo/build_tweaks
HTML conversion tweaks
2021-01-24 12:55:56 +00:00
Veronica K. B. Olsen
dfbde3e41c Fix broken tests 2021-01-24 13:41:40 +01:00
Veronica K. B. Olsen
034b4feff4 HTML divs should end in a line break, and use hex values for binary flags 2021-01-24 13:41:26 +01:00
Veronica K. B. Olsen
949439361e Bump version to 1.1 RC1 and update changelog 2021-01-23 16:27:17 +01:00
Veronica K. Berglyd Olsen
163b6b3725 Merge pull request #577 from vkbo/issue546_multipar_quotes
Issue #546: Multi-Paragraph Quotes
2021-01-23 15:05:38 +00:00
Veronica K. B. Olsen
bdc6addf81 Fix a minor issue in the Preferences test 2021-01-23 16:00:06 +01:00
Veronica K. B. Olsen
fd7c1e24a3 Merge branch 'dev' into issue546_multipar_quotes 2021-01-23 15:44:01 +01:00
Veronica K. B. Olsen
a16ecbcebd Merge branch 'main' into dev 2021-01-23 15:43:24 +01:00
Veronica K. B. Olsen
0a1f6be4a5 Bump version and update changelog 2021-01-23 15:39:41 +01:00
Veronica K. Berglyd Olsen
98bf462e02 Merge pull request #593 from vkbo/minor_fixes
Minor Fixes
2021-01-23 14:21:20 +00:00
Veronica K. B. Olsen
86fa59e5e1 Easier to just make the scroll past end feature go to 90% 2021-01-23 15:19:01 +01:00
Veronica K. B. Olsen
1ddadeba5d Make the autoscroll feature also work for a very short document 2021-01-23 15:12:25 +01:00
Veronica K. B. Olsen
2e1d5d4d13 Windows installer for pyz should have a different name than the regular installer 2021-01-23 15:11:52 +01:00
Veronica K. Berglyd Olsen
31b29abf5f Merge pull request #584 from vkbo/simple_pack
Package for Windows without an .exe file
2021-01-23 13:55:22 +00:00
Veronica K. B. Olsen
16c428dbd5 Update docs to reflect that make.py has been merged into setup.py 2021-01-23 14:51:27 +01:00
Veronica K. Berglyd Olsen
54bc6550ee Merge branch 'main' into simple_pack 2021-01-23 13:46:31 +00:00
Veronica K. B. Olsen
a698cb6ef7 Automatically download the correct Python embed version 2021-01-23 14:43:54 +01:00
Veronica K. B. Olsen
5f86ab27bb Complete the zipapp packaging for Windows 2021-01-23 14:25:12 +01:00
Veronica K. Berglyd Olsen
f0b2c08a9e Merge pull request #592 from vkbo/update_texts
Update README.md and CONTRIBUTING.md
2021-01-23 13:22:00 +01:00
Veronica K. B. Olsen
3f8c705989 Fix some typos and reword some bits in README and CONTRIBUTING 2021-01-23 13:14:50 +01:00
Veronica K. B. Olsen
6f84574511 Also updated docs 2021-01-23 12:12:45 +01:00
Veronica K. B. Olsen
6a131977d3 Reorganise and update README 2021-01-23 11:43:22 +01:00
Veronica K. B. Olsen
4df0075b42 Add note on line length to contributing guide 2021-01-23 11:10:59 +01:00
Veronica K. B. Olsen
e3972c6205 Merge branch 'main' into simple_pack 2021-01-22 20:18:52 +01:00
Veronica K. B. Olsen
4e949f542c Re-organised the Preferences dialog as some pages were growing quite long 2021-01-22 20:18:03 +01:00
Veronica K. Berglyd Olsen
af38fd3ba3 Merge pull request #590 from mgrhm/setup-typo-fix
fix: Typos in setup.py file.
2021-01-22 19:53:57 +01:00
Mike Graham
d00ac278c6 Fixed typos in setup.py. 2021-01-22 18:24:19 +00:00
Veronica K. Berglyd Olsen
0e01303921 Merge pull request #587 from vkbo/issue581_os_relpath
Issue #581: os.path.relpath on Windows
2021-01-22 00:41:51 +01:00
Veronica K. B. Olsen
39eeaf06e5 Add explicit reference directory for os relpath calls to avoid issue #581 on Windows 2021-01-22 00:35:43 +01:00
Veronica K. B. Olsen
7c2c4b26b3 Merge make.py into setup.py 2021-01-21 17:24:54 +01:00
Veronica K. B. Olsen
2ea0703701 Preserve both methods for building Inno Setup installers 2021-01-21 16:45:16 +01:00
Veronica K. B. Olsen
d425cdc27d Added alternative packaging method for windows 2021-01-21 00:50:13 +01:00
Veronica K. B. Olsen
799c833a66 Add a simple packaging function 2021-01-20 18:25:51 +01:00
Veronica K. B. Olsen
96bc88d045 Added some smart filters for quote settings 2021-01-20 00:08:49 +01:00
Veronica K. Berglyd Olsen
da7bbc2b20 Merge pull request #578 from nullbasis/main
Added new GUI and editor themes
2021-01-19 23:39:03 +01:00
Null
7b4f6c3222 Add files via upload 2021-01-19 16:34:36 -06:00
Null
e3bc528647 Add files via upload 2021-01-19 16:32:32 -06:00
Null
29665203c0 Add files via upload 2021-01-19 16:19:16 -06:00
Null
7beb9afc36 Added Solarized Dark and Light GUI themes 2021-01-19 15:24:29 -06:00
Null
2fcb3a0131 Added Solarized Dark and Light editor themes 2021-01-19 15:23:44 -06:00
Veronica K. B. Olsen
0a9749726f Merge branch 'dev' into issue546_multipar_quotes 2021-01-19 21:34:23 +01:00
Veronica K. B. Olsen
c1e6d9c633 Merge branch 'main' into dev 2021-01-19 21:33:59 +01:00
Veronica K. Berglyd Olsen
4767d49e4d Fixed duplicate word in README 2021-01-19 21:25:02 +01:00
Veronica K. Berglyd Olsen
2bf7e8506c Merge pull request #564 from vkbo/release_1.0.2
Release 1.0.2
2021-01-19 20:59:38 +01:00
Veronica K. B. Olsen
447966e3b8 Changed release date and updated release notes 2021-01-19 20:56:29 +01:00
Veronica K. B. Olsen
adc17272dc Updated changelog with latest changes 2021-01-19 20:17:15 +01:00
Veronica K. B. Olsen
6e54f34419 Merge branch 'main' into release_1.0.2 2021-01-19 20:02:43 +01:00
Veronica K. Berglyd Olsen
4b4051a823 Merge pull request #576 from vkbo/issue570_install_docs
Issue #570: Clarify install instructions
2021-01-19 20:01:28 +01:00
Veronica K. B. Olsen
5c4e230f69 No need to duplicate instructions, but added a TLDR section 2021-01-19 19:55:25 +01:00
Veronica K. B. Olsen
2c191a9bd3 Minor modification to the macOS workflow 2021-01-19 19:30:35 +01:00
Veronica K. B. Olsen
ed9313759b Clarify install instructions on dependency install 2021-01-19 19:30:14 +01:00
Veronica K. Berglyd Olsen
70a7a72baa Merge pull request #575 from vkbo/issue574_user_dict_no_suggest
Issue #574: Show "Add Word to Dictionary" when there are no spell suggestions
2021-01-19 19:20:54 +01:00
Veronica K. B. Olsen
4d69025250 Fix lambda function syntax 2021-01-19 19:09:37 +01:00
Veronica K. B. Olsen
9f9028b7ce Fix "Add Word to Dictionary" missing when there are no spell suggestions 2021-01-19 19:08:51 +01:00
Veronica K. Berglyd Olsen
e9be3242b2 Merge pull request #573 from stranger-danger-zamu/patch-1
Issue #570
2021-01-19 18:37:57 +01:00
Veronica K. B. Olsen
6868b683d8 Added new quote setting to Preferences and updated tests 2021-01-19 18:37:19 +01:00
Veronica K. B. Olsen
4934ec2ea1 Add syntax highlighting for open-ended quotes 2021-01-19 18:36:47 +01:00
stranger-danger-zamu
211a06e9bc Issue #570 2021-01-19 11:44:44 -05:00
Veronica K. Berglyd Olsen
debca0a82d Merge pull request #565 from vkbo/gui_tweaks
GUI Tweaks
2021-01-18 19:05:55 +01:00
Veronica K. B. Olsen
92da954229 Drop the showGUI debug flag, removing test mode from beyond the main function 2021-01-18 18:55:32 +01:00
Veronica K. B. Olsen
91d96b162e Fixed and imrpoved margin calculations on GUI elements 2021-01-18 17:52:32 +01:00
Veronica K. B. Olsen
0907bf195e Bump version to 1.0.2 and update changelog 2021-01-17 21:11:51 +01:00
Veronica K. B. Olsen
f30b14a3da Merge branch 'main' into dev 2021-01-17 20:58:15 +01:00
Veronica K. B. Olsen
2030793a02 Latest release notes check should use hex version number instead of string version number 2021-01-17 20:52:34 +01:00
Veronica K. Berglyd Olsen
d09f561ef6 Merge pull request #563 from vkbo/source_code_maintenance
Source code maintenance
2021-01-16 18:14:38 +01:00
Veronica K. B. Olsen
2d1bede57e Fix test file header titles 2021-01-16 18:10:47 +01:00
Veronica K. B. Olsen
a947ec10e4 Move test files into subfolders with similar structure as source code 2021-01-16 17:59:29 +01:00
Veronica K. B. Olsen
fd04f94b76 Also update the newest test 2021-01-16 17:55:11 +01:00
Veronica K. B. Olsen
6881446e67 Merge branch 'dev' into source_code_maintenance 2021-01-16 17:53:58 +01:00
Veronica K. B. Olsen
bd0a9952ff Merge branch 'main' into dev 2021-01-16 17:53:13 +01:00
Veronica K. B. Olsen
c35255b82b Merge branch 'file_headers' into source_code_maintenance 2021-01-16 17:51:32 +01:00
Veronica K. B. Olsen
5e71e7d04d Add license header to test files 2021-01-16 17:50:14 +01:00
Veronica K. B. Olsen
cdf3c62d17 Reformat and update the source file header docstrings 2021-01-16 17:41:40 +01:00
Veronica K. Berglyd Olsen
e7e2176e64 Merge pull request #561 from vkbo/issue560_window_title
Issue #560: Window Title
2021-01-15 19:44:45 +01:00
Veronica K. B. Olsen
ecb9e92c48 Clear statusbar on new project, and change colors of saved indicators on light desktop 2021-01-15 19:27:19 +01:00
Veronica K. B. Olsen
053c2eb379 Set window title after new project is created, and also automatically call close if a project is open. 2021-01-15 10:47:50 +01:00
Veronica K. B. Olsen
bde2810c08 Clear window title when closing a project 2021-01-15 10:40:41 +01:00
Veronica K. B. Olsen
692159219e Fix an issue with generated files in test projects in the test suite 2021-01-15 10:40:19 +01:00
Veronica K. Berglyd Olsen
71b194cbcb Merge pull request #555 from vkbo/project_details
Project Details Dialog
2021-01-14 20:14:22 +01:00
Veronica K. B. Olsen
fb81c7e8e0 Minimal docs update, and add test for Project Details 2021-01-14 20:09:18 +01:00
Veronica K. B. Olsen
84878315d5 Minor GUI tweaks to Project Details and related items 2021-01-14 18:58:11 +01:00
Veronica K. B. Olsen
1bd675cce9 Merge branch 'dev' into project_details 2021-01-14 18:47:37 +01:00
Veronica K. B. Olsen
e77a0a9385 Merge branch 'main' into dev 2021-01-14 18:47:14 +01:00
Veronica K. Berglyd Olsen
d63da05e87 Merge pull request #559 from vkbo/fix_clear_treemeta
Backport from 1.1: clear project tree meta panel on close
2021-01-14 18:45:36 +01:00
Veronica K. B. Olsen
dece58faf6 Backport from 1.1: clear project tree meta panel on close 2021-01-14 18:36:39 +01:00
Veronica K. B. Olsen
0bfc166899 Added a few buttons below the project tree now that the tabs make room for it 2021-01-14 18:31:27 +01:00
Veronica K. B. Olsen
3bc8006028 Some minor improvements to the Project Detals dialog 2021-01-13 23:52:48 +01:00
Veronica K. B. Olsen
f6b7a35c81 Add some more unicode constants and rename thin non-breaking space variable 2021-01-13 23:52:09 +01:00
Veronica K. B. Olsen
5f17b28e05 Remove old details panel in Project Settings 2021-01-13 23:51:24 +01:00
Veronica K. B. Olsen
138099f11d Merge branch 'dev' into project_details 2021-01-11 21:21:16 +01:00
Veronica K. B. Olsen
beb425bbd0 Merge branch 'main' into dev 2021-01-11 21:19:56 +01:00
Veronica K. Berglyd Olsen
b5631aa1df Merge pull request #558 from vkbo/index_note_bug
Duplicate index entry issue
2021-01-11 21:16:14 +01:00
Veronica K. B. Olsen
2958881343 Fix issue with changing novel doc to note before indexing which produces duplicate entry 2021-01-11 21:05:55 +01:00
Veronica K. B. Olsen
981e26e1b6 Use actual novel word count in project details, and update index test 2021-01-11 21:02:45 +01:00
Veronica K. B. Olsen
b5e11f2303 Added more info to the forst tab of the Project Details dialog 2021-01-11 19:47:42 +01:00
Veronica K. Berglyd Olsen
6a159aa631 Merge pull request #557 from vkbo/docs_spelling
Spelling, typo, and wording update of the docs
2021-01-10 18:56:53 +01:00
Veronica K. B. Olsen
4b14a4bc50 Extend the texhnical doc to cover project meta data and cache 2021-01-10 18:52:23 +01:00
Veronica K. B. Olsen
2ca4b15154 Fix typos, grammars and improve wording of the docs 2021-01-10 18:30:58 +01:00
Veronica K. Berglyd Olsen
e02d602341 Merge pull request #553 from vkbo/release_1.0.1
Release 1.0.1
2021-01-10 16:04:16 +01:00
Veronica K. B. Olsen
8d218db35f Project Details page Overview populated 2021-01-10 15:54:36 +01:00
Veronica K. B. Olsen
f96d78e9b8 Contents tab now fully working 2021-01-10 15:09:34 +01:00
Veronica K. B. Olsen
28dd7e4ca7 Add Chapters tab to Project Details tool 2021-01-10 00:14:03 +01:00
Veronica K. B. Olsen
14a0e6bd6e Allow indexing of untitled pages, and add a ToC function to the index class 2021-01-10 00:13:26 +01:00
Veronica K. B. Olsen
f2b2952909 Added heading level 0 icon 2021-01-09 23:52:07 +01:00
Veronica K. B. Olsen
adc82bea85 Merge branch 'dev' into project_details 2021-01-09 22:25:57 +01:00
Veronica K. B. Olsen
778adc2f39 Merge branch 'main' into dev 2021-01-09 21:42:18 +01:00
Veronica K. B. Olsen
a92895d09d Update changelog and release notes 2021-01-09 20:40:09 +01:00
Veronica K. B. Olsen
dfb66cc41f Merge branch 'main' into release_1.0.1 2021-01-09 20:30:59 +01:00
Veronica K. Berglyd Olsen
065180e86c Merge pull request #554 from vkbo/doc_modified
Doc and Project Modified Status
2021-01-09 20:29:38 +01:00
Veronica K. B. Olsen
73cd7f7513 Improve doc changed checking and some code cleanup 2021-01-09 20:26:05 +01:00
Veronica K. B. Olsen
e2d5534959 Some minor code cleanup and redundant code removed 2021-01-09 20:21:32 +01:00
Veronica K. B. Olsen
a6cbdfc559 Fix initial setting of project word count 2021-01-09 20:07:09 +01:00
Veronica K. B. Olsen
7c643ea698 Make sure document edit status is not set on non-text changes 2021-01-09 19:53:42 +01:00
Veronica K. B. Olsen
39be3d0958 Merge branch 'main' into dev 2021-01-09 15:33:03 +01:00
Veronica K. B. Olsen
e693141f50 Bumped version to 1.0.1 and updated changelog 2021-01-07 22:31:58 +01:00
Veronica K. Berglyd Olsen
927dad4ff5 Merge pull request #552 from vkbo/edit_item_fix
Item Editor fixes
2021-01-07 21:38:47 +01:00
Veronica K. B. Olsen
885caa52db Code cleanup in the Item Settings constructor 2021-01-07 21:15:03 +01:00
Veronica K. B. Olsen
88776b4e00 Fix max length bug in Item Editor and let input boxes expand with dialog width 2021-01-06 22:09:20 +01:00
Veronica K. B. Olsen
8b963f3fe4 Use new novel class list to select status or importance list in Item Editor 2021-01-06 22:08:34 +01:00
Veronica K. B. Olsen
4a4ac1d6c8 Add a new constants class named nwLists 2021-01-06 21:53:42 +01:00
Veronica K. Berglyd Olsen
b5eef748b4 Merge pull request #551 from vkbo/install_instructions
Install instructions
2021-01-05 00:02:37 +01:00
Veronica K. B. Olsen
6ebe235dc3 Rewritten "Getting Started" section in the documentation 2021-01-04 23:58:57 +01:00
Veronica K. B. Olsen
bbde3159bb Updated install intructions in readme 2021-01-04 23:45:47 +01:00
Veronica K. B. Olsen
7e65aea6d9 Connected new dialog to menu 2021-01-03 21:46:53 +01:00
Veronica K. B. Olsen
e377675897 Added basic project details dialog 2021-01-03 18:14:00 +01:00
Veronica K. Berglyd Olsen
c3d40c9fb9 Merge pull request #538 from vkbo/novel_tab
Novel Tree Tab [v1.1]
2021-01-03 17:23:25 +01:00
Veronica K. B. Olsen
5efc61b155 Updated changelog 2021-01-03 17:21:06 +01:00
Veronica K. B. Olsen
323ab5498b Merge branch 'dev' into novel_tab 2021-01-03 17:17:30 +01:00
Veronica K. B. Olsen
b6e18e70bc Merge branch 'main' into dev 2021-01-03 17:14:16 +01:00
Veronica K. Berglyd Olsen
1496d9d931 Merge pull request #550 from vkbo/update_status
Update status of novelWriter to stable, from beta
2021-01-03 17:00:52 +01:00
Veronica K. B. Olsen
e0e35f5abf Update status of novelWriter to stable, from beta 2021-01-03 16:47:30 +01:00
Veronica K. B. Olsen
0b7e4d594e Fix a minor issue in the outline details class 2021-01-02 22:34:52 +01:00
Veronica K. B. Olsen
1bdf0b5500 Make index dictionaries internal 2021-01-02 22:28:30 +01:00
Veronica K. B. Olsen
acb6f1fc87 Add novel data extraction function to index 2021-01-02 22:22:05 +01:00
Veronica K. B. Olsen
a57e8f3a5d Remove GUI block in test 2021-01-02 22:05:17 +01:00
Veronica K. B. Olsen
4cb86b76e8 Add test of novel tree 2021-01-02 22:04:25 +01:00
Veronica K. B. Olsen
24f9511726 Prevent items from ending up in the wrong order in outline and novel tree 2021-01-02 22:04:08 +01:00
Veronica K. B. Olsen
e4d160ee0b Make novel structure function in index class an iterable function instead 2021-01-02 21:37:46 +01:00
Veronica K. B. Olsen
21d91193a2 Keep project tree and outline tree in sync 2021-01-02 17:33:53 +01:00
Veronica K. B. Olsen
4d7f925c5a Added timestamp to project tree, and made some class variables internal in various classes 2021-01-02 16:57:12 +01:00
Veronica K. B. Olsen
ad5f3f2450 Merge branch 'dev' into novel_tab 2021-01-02 13:18:08 +01:00
Veronica K. B. Olsen
a84fc734ff Merge branch 'main' into dev 2021-01-02 13:17:39 +01:00
Veronica K. B. Olsen
3d5a5e4d86 Selecting outline item should also select it in project tree 2021-01-01 15:08:11 +01:00
Veronica K. B. Olsen
2d14a74ec2 Merge branch 'main' into dev 2020-12-21 21:51:35 +01:00
Veronica K. B. Olsen
942a604b5b Merge branch 'main' into novel_tab 2020-12-21 21:51:17 +01:00
Veronica K. B. Olsen
e5ea8074d2 Copy the mouse press behaviour from the project tree to the novel tree 2020-12-20 17:18:54 +01:00
Veronica K. B. Olsen
2779c8e69f Use build time to update novel tree, and restore last selected item on tab change 2020-12-20 16:50:01 +01:00
Veronica K. B. Olsen
1d12014399 The basic functionality of the novel tree view is now in place 2020-12-20 16:27:42 +01:00
Veronica K. B. Olsen
d4b26025ed Add the basic structure for a novel tree 2020-12-20 15:50:01 +01:00
Veronica K. B. Olsen
597f177d5b Bump version for next stage of dev branch to 1.1 alpha 0 2020-12-20 14:40:25 +01:00
167 changed files with 13957 additions and 5201 deletions

12
.github/FUNDING.yml vendored Normal file
View 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']

View File

@@ -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

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

View File

@@ -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

View File

@@ -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
View File

@@ -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/

View File

@@ -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.

View File

@@ -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

View File

@@ -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
View File

@@ -4,8 +4,8 @@
[![Linux (3.7)](https://github.com/vkbo/novelWriter/workflows/Linux%20(3.7)/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions)
[![Linux (3.8)](https://github.com/vkbo/novelWriter/workflows/Linux%20(3.8)/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions)
[![Linux (3.9)](https://github.com/vkbo/novelWriter/workflows/Linux%20(3.9)/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions)
[![Windows (3.8)](https://github.com/vkbo/novelWriter/workflows/Windows%20(3.8)/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions)
[![macOS (3.8)](https://github.com/vkbo/novelWriter/workflows/macOS%20(3.8)/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions)
[![Windows (3.9)](https://github.com/vkbo/novelWriter/workflows/Windows%20(3.9)/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions)
[![macOS (3.9)](https://github.com/vkbo/novelWriter/workflows/macOS%20(3.9)/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions)
[![flake8](https://github.com/vkbo/novelWriter/workflows/flake8/badge.svg)](https://github.com/vkbo/novelWriter/actions)
[![codecov](https://codecov.io/gh/vkbo/novelWriter/branch/main/graph/badge.svg)](https://codecov.io/gh/vkbo/novelWriter)
[![docs](https://readthedocs.org/projects/novelwriter/badge/?version=latest)](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:**
![Screenshot 1](https://raw.githubusercontent.com/vkbo/novelWriter/main/docs/source/images/screenshot_default.png)

View File

@@ -21,3 +21,7 @@
white-space: normal !important;
vertical-align: text-top;
}
pre span.go {
color: #333333 !important;
}

View File

@@ -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 ---------------------------------------------------

View File

@@ -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::

View File

@@ -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:

View File

@@ -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.

View 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
View 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.

View 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/

View File

@@ -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.

View File

@@ -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.

View File

@@ -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."

View File

@@ -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.

View File

@@ -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`.

View File

@@ -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.

View File

@@ -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
View File

@@ -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

View File

@@ -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:])

View File

@@ -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

View 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

View 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

View 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

View 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

View 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

View File

@@ -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

View 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

View File

@@ -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

View 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

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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>

View 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>

View 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>

View File

@@ -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

View 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

View 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

View 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

View 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

View File

@@ -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)

View File

@@ -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

View File

@@ -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",
]

View File

@@ -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 = "&#12300;"
H_RCQUO = "&#12301;"
H_LWCQUO = "&#12302;"
H_LWCQUO = "&#12302;"
H_RWCQUO = "&#12303;"
## Punctuation
H_FGDASH = "&#8210;"
H_ENDASH = "&ndash;"
H_EMDASH = "&mdash;"
H_HBAR = "&#8213;"
H_HELLIP = "&hellip;"
H_MAPOSS = "&#700;"
H_PRIME = "&prime;"
H_DPRIME = "&#8243;"
## Spaces
H_NBSP = "&nbsp;"
H_THNSP = "&thinsp;"
H_THSP = "&thinsp;"
H_THNBSP = "&#8239;"
H_ENSP = "&ensp;"
H_EMSP = "&emsp;"
## Symbols
H_CHECK = "&#10004;"
H_MULT = "&#10005;"
H_CROSS = "&#10005;"
H_BULL = "&bull;"
H_TRBULL = "&#8227;"
H_HYBULL = "&hybull;"
H_FLOWER = "&#8277;"
H_PERMIL = "&#8240;"
H_DEGREE = "&deg;"
H_MINUS = "&minus;"
H_TIMES = "&times;"
H_DIVIDE = "&divide;"
## Arrows
H_UTRI = "&#9650;"
@@ -338,3 +378,60 @@ class nwUnicode:
H_LTRIS = "&#9666;"
# 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

View File

@@ -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

View File

@@ -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():

View File

@@ -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",
]

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 = {
"<" : "&lt;",
">" : "&gt;",
"&" : "&amp;",
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({
"<" : "&lt;",
">" : "&gt;",
"&" : "&amp;",
})
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", "&#09;").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="&nbsp;"):
"""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

View File

@@ -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
View 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

File diff suppressed because it is too large Load Diff

View 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

View File

@@ -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

View File

@@ -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)

View File

@@ -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",
]

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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

View File

@@ -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]))

View File

@@ -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))

File diff suppressed because it is too large Load Diff

462
nw/gui/projdetails.py Normal file
View 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

View File

@@ -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:

View File

@@ -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)

View File

@@ -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):

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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

View File

@@ -1,3 +1,4 @@
# Minimal PEP 518 pyproject file
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

View File

@@ -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

Some files were not shown because too many files have changed in this diff Show More