Compare commits

...

409 Commits
v1.2 ... v1.4

Author SHA1 Message Date
Veronica Berglyd Olsen
83e92994d1 Make a clarification in the changelog 2021-07-27 23:14:50 +02:00
Veronica Berglyd Olsen
33146f0a50 Bump version to 1.4 final, and update changelog and release notes 2021-07-27 23:10:48 +02:00
Veronica Berglyd Olsen
732450e163 Add CREDITS file and update README (#833)
* Add CREDITS file and update README
* Fix typo and add note in Libraries section of CREDITS
2021-07-25 16:50:12 +02:00
Veronica Berglyd Olsen
df8e3dd82b Update Contributing Guide (#832)
* Update Contributing Guide
* Minor improvements to README and CONTRIBUTING
2021-07-17 17:27:38 +02:00
Veronica Berglyd Olsen
75ddafd31d Update French translation (#829)
Co-authored-by: jyhelle <47108022+jyhelle@users.noreply.github.com>
2021-07-13 21:51:30 +02:00
Veronica Berglyd Olsen
4f38a8d16c Update the formatting section of the README (#828)
* Update the formatting section of the readme
* Use sup tags for footnotes in readme
2021-07-13 16:13:03 +02:00
Veronica Berglyd Olsen
35746103c7 i18n: Update en_US and nb_NO (#825)
* Update US English translation file
* Update Norwegian translation file
2021-07-13 15:37:24 +02:00
Veronica Berglyd Olsen
fb5347b8e4 Change the branching model (#827)
* Update workflows and add requirements-dev.txt
* Remove codecov from dev requirements
* Update readme and contributing guide
* Update PR template
2021-07-13 15:34:10 +02:00
Veronica Berglyd Olsen
6890b278ea Release 1.4 RC 1 (#824)
* Bump version to 1.4 RC 1
* Update changelog and release notes
2021-07-12 20:52:35 +02:00
Veronica Berglyd Olsen
9f667a0b35 Update code style to new standard 2021-07-05 16:28:44 +02:00
Veronica Berglyd Olsen
36be7b0b33 Test Coverage of the Editor (#787)
* Drop the _qDocument class variables
* Split up the current doceditor test file
* Add tests for editor init, load and save
* Add tests for editor meta functions and actions
* Add tests for editor insert functions
* Add tests for editor text manipulation and block formatting
* Fix code style
* Add text coverage for alignment and indent
2021-07-05 16:10:52 +02:00
Veronica Berglyd Olsen
1d16cc2a03 Move the coding of angle brackets in HTML to the innermost function in the chain (#821) 2021-07-04 18:05:26 +02:00
Veronica Berglyd Olsen
72fd2990e7 Merge branch 'main' into dev 2021-07-04 17:58:53 +02:00
Veronica Berglyd Olsen
6d7ca2bb86 Cleanup (#820)
* Update instructions and readmes
* Cleanup in base files
* Cleanup in core files
* Cleanup in gui files
* Cleanup in dialog files
* Cleanup in tools files
* Cleanup in test files
2021-07-04 17:57:54 +02:00
Veronica Berglyd Olsen
f10a8f9589 Update PR template 2021-07-04 17:56:40 +02:00
Veronica Berglyd Olsen
1881b3d32c Merge branch 'main' into dev 2021-07-02 10:25:56 +02:00
Veronica Berglyd Olsen
1b89d0a4f0 Logger messages (#818)
* Change the way logger messages are generated by using the printf-like method
* Use single quotes around log values that can be empty strings
* Fix bug in log message
2021-07-02 10:22:00 +02:00
용성지
671888b6ea add language support (Simplified Chinese - initial version) (#817)
* add translator's name in about dialog

* add new CN.ts

* Update nw_zh_CN.ts

* inital version (CN)

* add project_zh_CN.json

* Update project_zh_CN.json

* Revert changes to otherlanguage files

* Change case of first character in 'novelWriter' in Chinese translation

* Update about.py

Change the entry "Simplified Chinese" from English the local name of the language

Co-authored-by: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
2021-07-02 10:17:58 +02:00
Veronica Berglyd Olsen
50498a7462 Merge pull request #819 from vkbo/update_guides
Update guides
2021-07-01 20:28:41 +02:00
Veronica Berglyd Olsen
ecd190987e Add a pull request template 2021-07-01 20:23:21 +02:00
Veronica Berglyd Olsen
2c000f6808 Update contributing guide and i18n readme 2021-07-01 20:23:09 +02:00
Veronica Berglyd Olsen
c472e40b2b Fix workflow 2021-06-25 23:28:38 +02:00
Veronica Berglyd Olsen
0575f89ae9 Remove alignments in arguments for format statements 2021-06-25 23:20:15 +02:00
Veronica Berglyd Olsen
e1dcfaa1e3 Drop dictionary alignment by colon 2021-06-25 23:14:13 +02:00
Veronica Berglyd Olsen
486af84107 Merge pull request #816 from vkbo/style
Style
2021-06-25 21:56:03 +02:00
Veronica Berglyd Olsen
8acbd8b978 Update contributing guide with new restrictions 2021-06-25 21:48:50 +02:00
Veronica Berglyd Olsen
7f9180e356 Update workflow 2021-06-25 21:39:24 +02:00
Veronica Berglyd Olsen
26912f226f Add double space before in-line comments 2021-06-25 21:36:52 +02:00
Veronica Berglyd Olsen
0c286231cc Remove multiple hashes before comments 2021-06-25 21:11:25 +02:00
Veronica Berglyd Olsen
991a61f2f6 Remove encoding line and add spaces between classes and functions, tests 2021-06-25 21:04:48 +02:00
Veronica Berglyd Olsen
edfd7c9770 Remove encoding line and add spaces between classes and functions 2021-06-25 20:53:33 +02:00
Veronica Berglyd Olsen
ad62faaeea Fix broken links in docs 2021-06-13 20:03:53 +02:00
Veronica Berglyd Olsen
468903bbeb Merge pull request #812 from vkbo/release_1.4beta1
Release 1.4 Beta 1
2021-06-13 20:01:03 +02:00
Veronica Berglyd Olsen
0723f44dc3 Update changelog and release notes 2021-06-13 19:56:16 +02:00
Veronica Berglyd Olsen
831bdabfc6 Bumped version to 1.4-beta1 2021-06-13 19:12:10 +02:00
Veronica Berglyd Olsen
93a3eed530 Merge pull request #804 from vkbo/text_align
Text alignment and indent
2021-06-13 19:10:15 +02:00
Veronica Berglyd Olsen
307a52e7f4 Update documentation 2021-06-13 19:07:36 +02:00
Veronica Berglyd Olsen
3246f4bfcc Add tests for the menu functions 2021-06-13 18:46:35 +02:00
Veronica Berglyd Olsen
fa4f4a8db4 Make the comment format action a toggle feature 2021-06-13 18:32:11 +02:00
Veronica Berglyd Olsen
cb797fe903 Add menu entries for align and indent 2021-06-13 18:28:23 +02:00
Veronica Berglyd Olsen
618461b1ff Merge branch 'dev' into text_align 2021-06-13 17:59:47 +02:00
Veronica Berglyd Olsen
e2f08d3c76 Merge branch 'main' into dev 2021-06-13 17:58:49 +02:00
Veronica Berglyd Olsen
f39cda729d Merge pull request #811 from vkbo/release_1.3.3
Release 1.3.3
2021-06-13 17:13:54 +02:00
Veronica Berglyd Olsen
b28d826d76 Updated changelog and release notes 2021-06-13 17:11:19 +02:00
Veronica Berglyd Olsen
57ae559723 Merge pull request #810 from vkbo/wcount_when_delete
Update project word count when a file is permanently deleted
2021-06-13 16:57:31 +02:00
Veronica Berglyd Olsen
1d1be18689 Bumped version to 1.3.3 2021-06-13 16:56:29 +02:00
Veronica Berglyd Olsen
141b5eb776 Update project word count when a file is permanently deleted 2021-06-13 16:53:40 +02:00
Veronica Berglyd Olsen
3f941bf9fd Merge branch 'dev' into text_align 2021-06-11 00:11:40 +02:00
Veronica Berglyd Olsen
a48f302531 Merge branch 'main' into dev 2021-06-11 00:11:19 +02:00
Veronica Berglyd Olsen
12d8872973 Merge pull request #809 from vkbo/item_tags
Make XML parser for items less picky
2021-06-11 00:10:46 +02:00
Veronica Berglyd Olsen
40ff865989 Make XML parser for items less picky 2021-06-11 00:01:58 +02:00
Veronica Berglyd Olsen
32e9c10702 Fix word counter for align and indent tags 2021-06-10 23:38:11 +02:00
Veronica Berglyd Olsen
804dd3bb7c Merge branch 'dev' into text_align 2021-06-10 22:52:47 +02:00
Veronica Berglyd Olsen
9f2e3a66cd Merge branch 'main' into dev 2021-06-10 22:51:45 +02:00
Veronica Berglyd Olsen
960d1967a3 Merge pull request #808 from vkbo/focus_keys
Focus keys
2021-06-10 22:45:18 +02:00
Veronica Berglyd Olsen
ad025c3fe4 Merge branch 'main' into focus_keys 2021-06-10 22:42:40 +02:00
Veronica Berglyd Olsen
81212a108e Merge pull request #807 from vkbo/setup_win_encoding
Make the extractVersion function in setup a bit more robust
2021-06-10 22:42:30 +02:00
Veronica Berglyd Olsen
8637a4eb12 Merge branch 'main' into focus_keys 2021-06-10 22:35:55 +02:00
Veronica Berglyd Olsen
0e4489d627 Merge branch 'main' into setup_win_encoding 2021-06-10 22:33:04 +02:00
Veronica Berglyd Olsen
0bfb9d0700 Update docs 2021-06-10 22:18:51 +02:00
Veronica Berglyd Olsen
efe759b94e Change focus shortcuts on Window as they conflict with alt codes 2021-06-10 22:16:06 +02:00
Veronica Berglyd Olsen
9f89c1f7a4 Merge pull request #806 from vkbo/cleanup_alerts
Fix translations and refactor makeAlert calls
2021-06-10 22:10:32 +02:00
Veronica Berglyd Olsen
530d92cc3f Make the extractVersion function in setup a bit more robust 2021-06-10 22:08:26 +02:00
Veronica Berglyd Olsen
b0225515d2 Remove isinstance check in error reporting 2021-06-10 21:55:40 +02:00
Veronica Berglyd Olsen
d511c69ee7 Rewrap some alert messages 2021-06-10 21:51:52 +02:00
Veronica Berglyd Olsen
6e1e40f13e Fix translations and refactor makeAlert calls 2021-06-10 21:42:40 +02:00
Veronica Berglyd Olsen
70d3ee07bf Update sample project 2021-06-10 21:39:58 +02:00
Veronica Berglyd Olsen
e5477f314e Add support for indented paragraphs in exports 2021-06-10 21:38:58 +02:00
Veronica Berglyd Olsen
80c4826253 Added left/right indentation to Tokenizer class 2021-06-10 00:04:54 +02:00
Veronica Berglyd Olsen
e0de3ff191 Improve test coverage 2021-06-09 23:40:37 +02:00
Veronica Berglyd Olsen
239cff3dee Fix tests 2021-06-09 23:32:22 +02:00
Veronica Berglyd Olsen
6f9e3168b0 Fix default alignment 2021-06-09 23:05:16 +02:00
Veronica Berglyd Olsen
7d067f807d Add text alignment tags 2021-06-09 22:08:31 +02:00
Veronica Berglyd Olsen
a7b43699ed Merge pull request #802 from teymour-aldridge/patch-1
Remove redundant assignment.
2021-06-09 19:46:48 +02:00
Teymour Aldridge
7d11917d1c Remove redundant assignment. 2021-06-09 18:42:16 +01:00
Veronica Berglyd Olsen
904ba9b1ca Merge branch 'main' into dev 2021-05-30 12:27:30 +02:00
Veronica Berglyd Olsen
616fe35ed5 Merge pull request #797 from vkbo/release_1.3.2
Release 1.3.2
2021-05-30 12:17:58 +02:00
Veronica Berglyd Olsen
577a3c6c6a Bump version to 1.3.2 and update changelog 2021-05-30 12:14:41 +02:00
Veronica Berglyd Olsen
e937267eb0 Merge pull request #796 from vkbo/split_tool
Fix split tool dropping last line (#795)
2021-05-30 11:58:28 +02:00
Veronica Berglyd Olsen
2c1e1b94e7 Fix split tool dropping last line (#795) 2021-05-30 11:55:56 +02:00
Veronica Berglyd Olsen
60736ce04c Update readme 2021-05-25 19:23:34 +02:00
Veronica Berglyd Olsen
03daa350f8 Merge branch 'main' into dev 2021-05-23 16:37:24 +02:00
Veronica Berglyd Olsen
095cc38122 Merge pull request #786 from vkbo/allow_line_breaks
Allow line breaks
2021-05-23 16:35:09 +02:00
Veronica Berglyd Olsen
142d659b70 Fix an issue with markdown export 2021-05-23 16:31:06 +02:00
Veronica Berglyd Olsen
0826a2f8a3 Remove trailing space highlight and add highlight of redundant spaces 2021-05-23 16:17:05 +02:00
Veronica Berglyd Olsen
a3819a7785 Update docs 2021-05-23 15:20:26 +02:00
Veronica Berglyd Olsen
2314777b4b Add test coverage 2021-05-23 15:04:26 +02:00
Veronica Berglyd Olsen
729e97a95d Add function to clean up line breaks 2021-05-23 14:39:02 +02:00
Veronica Berglyd Olsen
5ab60ca4a3 Fix test 2021-05-23 13:15:09 +02:00
Veronica Berglyd Olsen
f1adff82a8 Remove hard line break menu entry 2021-05-23 13:13:03 +02:00
Veronica Berglyd Olsen
96b2d139c9 Allow line breaks in paragraphs 2021-05-23 13:08:10 +02:00
Veronica Berglyd Olsen
e1bc1222c4 Merge pull request #784 from vkbo/xdg_setup
Add entry in xdg-install to remove old desktop icon
2021-05-22 10:55:53 +02:00
Veronica Berglyd Olsen
0bff4a3d21 Add entry in xdg-install to remove old desktop icon 2021-05-22 10:50:21 +02:00
Veronica K. B. Olsen
dfc775c925 Fix merge from main 2021-05-17 20:28:36 +02:00
Veronica K. B. Olsen
dea304d1eb Merge branch 'main' into dev 2021-05-17 20:27:08 +02:00
Veronica Berglyd Olsen
cfd80645e2 Merge pull request #782 from vkbo/meta_stats
Clear handle when tree details panel is cleared
2021-05-17 20:25:36 +02:00
Veronica K. B. Olsen
4b7f7887c8 Clear handle when tree details panel is cleared 2021-05-17 20:07:56 +02:00
Veronica Berglyd Olsen
9fc640aa59 Merge pull request #780 from vkbo/class_cleanup
GUI Class Cleanup
2021-05-17 17:02:39 +02:00
Veronica K. B. Olsen
d812aab173 Update gitignore with versions files 2021-05-17 16:58:52 +02:00
Veronica K. B. Olsen
39b58711c7 Make some GuiItemDetails variables private 2021-05-17 16:58:07 +02:00
Veronica K. B. Olsen
c6120fcf7c Make some GuiDocViewer variables private 2021-05-17 16:57:49 +02:00
Veronica Berglyd Olsen
45b9215c87 Merge pull request #779 from vkbo/editor_cleanup
Editor Code Improvements
2021-05-16 19:02:57 +02:00
Veronica K. B. Olsen
394f6adf82 Remove no longer needed forward function 2021-05-16 17:35:33 +02:00
Veronica K. B. Olsen
1a5076438d Add a signal for updated word counts 2021-05-16 17:31:12 +02:00
Veronica K. B. Olsen
872b8e7902 Fix broken tests 2021-05-16 16:57:00 +02:00
Veronica K. B. Olsen
0cad96fdca Use an enum to pass values to the status bar LEDs 2021-05-16 16:43:44 +02:00
Veronica K. B. Olsen
e9ef4fd091 Make sure slots and signals are correct type 2021-05-16 16:26:03 +02:00
Veronica K. B. Olsen
4ce8b38acd Add editorDocumentChanged signal in GuiDocEditor 2021-05-16 00:41:44 +02:00
Veronica K. B. Olsen
24ab6f2d3b Make the editor dictionary internal 2021-05-16 00:19:40 +02:00
Veronica K. B. Olsen
ba5e5f4bb5 Make GuiDocEditor variables private 2021-05-15 23:57:31 +02:00
Veronica K. B. Olsen
6ef8611a0a Merge branch 'main' into dev 2021-05-08 21:41:36 +02:00
Veronica Berglyd Olsen
486676baa5 Merge pull request #777 from vkbo/release_1.3.1
Release 1.3.1
2021-05-06 22:26:25 +02:00
Veronica K. B. Olsen
542cbf13ff Update changelog and release notes 2021-05-06 22:23:22 +02:00
Veronica K. B. Olsen
62630eb519 Merge branch 'main' into release_1.3.1 2021-05-06 22:08:11 +02:00
Veronica Berglyd Olsen
b1d597e63f Merge pull request #776 from vkbo/issue769_handles_on_windows
Issue #769: Generating handles on Windows
2021-05-06 22:07:36 +02:00
Veronica Berglyd Olsen
887eee01fc Merge branch 'main' into issue769_handles_on_windows 2021-05-06 22:05:34 +02:00
Veronica K. B. Olsen
f605f1fc75 Solve an unwante error message when creating a new project 2021-05-06 22:02:59 +02:00
Veronica K. B. Olsen
cc8fc38734 Update the handle generator test 2021-05-06 21:58:35 +02:00
Veronica K. B. Olsen
5797b8655d {#769) Fix problem with handle collisions when generated rapidly in Windows 2021-05-06 21:58:08 +02:00
Veronica K. B. Olsen
fd4d663a1c Bump version to 1.3.1 2021-05-06 21:43:43 +02:00
Veronica Berglyd Olsen
88263d55f2 Merge pull request #774 from vkbo/issue773_qtranslate_filepath
Issue #773: Fix QTranslate() filePath issue
2021-05-06 12:52:58 +02:00
Veronica Berglyd Olsen
5c4398f948 Some more tweaks to Linux workflow 2021-05-06 12:50:00 +02:00
Veronica Berglyd Olsen
9ff397dfbc Build translation on Linux workflow 2021-05-06 12:44:58 +02:00
Veronica Berglyd Olsen
1c9ebfd11c Improve feedback and documentation for i18n 2021-05-06 12:33:15 +02:00
Veronica Berglyd Olsen
22e8d7ee71 (#773) Fix Qt 5.15 only call in Config 2021-05-06 10:35:16 +02:00
Veronica K. B. Olsen
35458a7e37 Fix wrong location of parameter in setup.cfg 2021-05-05 19:50:17 +02:00
Veronica Berglyd Olsen
c1544ab374 Merge pull request #760 from vkbo/doc_class
Non-Reusable Document Class
2021-05-02 19:53:20 +02:00
Veronica K. B. Olsen
d795eb6f5f Delete no longer needed test reference files 2021-05-02 19:47:39 +02:00
Veronica K. B. Olsen
ce8264b8f2 New split tool test 2021-05-02 19:47:04 +02:00
Veronica K. B. Olsen
c6cbc8a8b1 New merge tool test 2021-05-02 18:52:36 +02:00
Veronica K. B. Olsen
6c79a1daba Test terminology update 2021-05-02 17:48:28 +02:00
Veronica K. B. Olsen
7867965989 Alter the init logic of handles in NWDoc 2021-05-02 17:19:50 +02:00
Veronica K. B. Olsen
348513a248 Merge branch 'dev' into doc_class 2021-05-02 17:14:15 +02:00
Veronica K. B. Olsen
eb62253137 Merge branch 'main' into dev 2021-05-02 17:12:41 +02:00
Veronica Berglyd Olsen
c4e87435dd Merge pull request #766 from vkbo/release_1.3
Release 1.3
2021-05-02 16:58:48 +02:00
Veronica K. B. Olsen
9980d225f5 Fix test that failed due to change in version number 2021-05-02 16:54:25 +02:00
Veronica K. B. Olsen
9c0f596c0d Release 1.3 2021-05-02 16:49:08 +02:00
Veronica Berglyd Olsen
ff8cca6d8b Merge pull request #765 from vkbo/minimal_setup
Minimal Setup and i18n
2021-05-02 16:08:09 +02:00
Veronica K. B. Olsen
0e5e263dea Merge branch 'main' into minimal_setup 2021-05-02 15:57:48 +02:00
Veronica K. B. Olsen
166732946e Add a custom long description for PyPi 2021-05-02 15:57:38 +02:00
Veronica K. B. Olsen
cccdab45dc Add novelwriter banner png 2021-05-02 15:50:26 +02:00
Veronica K. B. Olsen
190ca7cf2d Merge branch 'main' into minimal_setup 2021-05-02 15:41:08 +02:00
Veronica K. B. Olsen
009160b6bf Add a paragraph break 2021-05-02 15:40:45 +02:00
Veronica K. B. Olsen
851f67d86f Update badges 2021-05-02 15:39:34 +02:00
Veronica K. B. Olsen
b8be2277c4 Merge linux workflows 2021-05-02 15:34:09 +02:00
Veronica K. B. Olsen
14e266b938 Update i18n README 2021-05-02 15:28:38 +02:00
Veronica K. B. Olsen
94f070f096 Fix setup entries for i18n files for minimal packages 2021-05-02 15:20:32 +02:00
Veronica Berglyd Olsen
0c28664ced Merge pull request #764 from vkbo/i18n_pip
I18n and PyPi
2021-05-02 15:05:21 +02:00
Veronica K. B. Olsen
754863e42c Fix setup message 2021-05-01 17:18:15 +02:00
Veronica K. B. Olsen
fb5e4a5f17 Update translations 2021-05-01 17:14:13 +02:00
Veronica K. B. Olsen
03e9fb93c7 Update readmes 2021-05-01 17:14:04 +02:00
Veronica K. B. Olsen
3e77e24b1f The i18n files are no read from assets, while the ts files remain in the root folder 2021-05-01 17:12:59 +02:00
Veronica Berglyd Olsen
d97809e4b0 Merge pull request #762 from vkbo/issue761_lastpath
Issue #761: lastpath in build + test coverage
2021-04-26 23:57:34 +02:00
Veronica K. B. Olsen
551fd0aaf9 This line should not have been changed 2021-04-26 23:54:32 +02:00
Veronica K. B. Olsen
2fc36f8355 Fix timestamp format in odt generator 2021-04-26 23:47:47 +02:00
Veronica K. B. Olsen
13cc2feb3f Improve build class test 2021-04-26 23:41:40 +02:00
Veronica K. B. Olsen
e2b62fc9f6 Fix alternative save path in build, and also add in project wizard 2021-04-26 18:22:16 +02:00
Veronica K. B. Olsen
6b119cf4b8 Remove the return false on error in doc viewer 2021-04-25 23:58:11 +02:00
Veronica K. B. Olsen
88d198403e Update translations 2021-04-25 23:47:42 +02:00
Veronica K. B. Olsen
5ee39d7ecc Tighten up the use of item and nwdoc objects in editor class 2021-04-25 23:46:09 +02:00
Veronica K. B. Olsen
e7102725bd Update translations 2021-04-25 23:24:21 +02:00
Veronica K. B. Olsen
bd28867804 Update tests 2021-04-25 23:22:05 +02:00
Veronica K. B. Olsen
0eb09a2136 Move error reporting out of the NWDoc class 2021-04-25 23:21:51 +02:00
Veronica K. B. Olsen
470daf0036 Make NWDoc class non-reusable 2021-04-25 23:01:20 +02:00
Veronica K. B. Olsen
fd28cbca4e Merge branch 'main' into dev 2021-04-25 18:25:12 +02:00
Veronica Berglyd Olsen
6ecf0ecd88 Merge pull request #759 from vkbo/index_alert
Remove duplicate error message in index class
2021-04-25 18:24:09 +02:00
Veronica K. B. Olsen
c940d9d8a4 Remove duplicate error message in index class 2021-04-25 18:20:31 +02:00
Veronica K. B. Olsen
efef172658 Another round of edits to the main README 2021-04-25 18:18:36 +02:00
Veronica Berglyd Olsen
e865b2c054 Merge pull request #758 from vkbo/class_cleanup
Class Cleanup
2021-04-25 18:02:25 +02:00
Veronica K. B. Olsen
f8243f6934 Update translation files 2021-04-25 17:52:07 +02:00
Veronica K. B. Olsen
746268c3ef GUI classes should only init with main GUI as parent 2021-04-25 17:50:11 +02:00
Veronica K. B. Olsen
48695651e1 Core classes should only init with project class and if needed access main gui via project 2021-04-25 17:39:52 +02:00
Veronica K. B. Olsen
45f726669e Remove the showStatus flag from document handling 2021-04-25 17:16:57 +02:00
Veronica K. B. Olsen
d473615ccf Rename open and save document to read and write in NWDoc 2021-04-25 17:10:12 +02:00
Veronica K. B. Olsen
70425c77ea Clarigy the second paragraph of the README 2021-04-25 13:21:28 +02:00
Veronica K. B. Olsen
8520409459 Shorten the first paragraph of the README 2021-04-25 13:18:39 +02:00
Veronica K. B. Olsen
1e48c6dcfe Merge branch 'main' into dev 2021-04-25 11:27:13 +02:00
Veronica K. B. Olsen
67a81400ff Clarify a few things about markup in the main readme 2021-04-25 11:25:30 +02:00
Veronica K. B. Olsen
51431169dd Merge branch 'main' into dev 2021-04-23 22:02:59 +02:00
Veronica Berglyd Olsen
2a0183c838 Merge pull request #754 from vkbo/restructure_code
Restructure Code
2021-04-23 22:01:36 +02:00
Veronica K. B. Olsen
e222915a37 Update tests readme 2021-04-23 21:54:03 +02:00
Veronica K. B. Olsen
b698ccbc1d Move qutae symbol dialog source file to the dialogs folder 2021-04-23 21:49:20 +02:00
Veronica K. B. Olsen
03a80beebe Move tools source files to their own folder 2021-04-23 21:34:45 +02:00
Veronica K. B. Olsen
1c072e9f32 Move dialog source files to their own folder 2021-04-23 21:24:09 +02:00
Veronica Berglyd Olsen
01576f941f Merge pull request #753 from vkbo/update_readmes
Cleanup of setup instructions and scripts
2021-04-19 20:20:37 +02:00
Veronica K. B. Olsen
4af80286d5 Minor corrections to the main README 2021-04-19 20:15:36 +02:00
Veronica K. B. Olsen
c9785bc57b Cleanup the setup instructions and rename the windows setup scripts 2021-04-19 20:02:58 +02:00
Veronica K. B. Olsen
81e72826ce Merge branch 'main' into dev 2021-04-18 18:18:05 +02:00
Veronica Berglyd Olsen
71f9f3d230 Merge pull request #751 from vkbo/release_1.3rc1
Release 1.3 RC 1
2021-04-18 17:58:36 +02:00
Veronica K. B. Olsen
500a01b46d Bump version to 1.3-rc1 and update changelog and release notes 2021-04-18 17:55:06 +02:00
Veronica K. B. Olsen
8190c189e5 Merge branch 'testing' into dev 2021-04-18 17:33:54 +02:00
Veronica K. B. Olsen
dea2ac22e1 Merge branch 'main' into testing 2021-04-18 17:33:29 +02:00
Veronica Berglyd Olsen
1cae46a936 Merge pull request #750 from vkbo/release_1.2.3
Release 1.2.3
2021-04-18 16:05:18 +02:00
Veronica K. B. Olsen
ac22838ded Bump version to 1.2.3 and update changelog and release notes 2021-04-17 23:44:49 +02:00
Veronica Berglyd Olsen
93875d667c Merge pull request #749 from vkbo/win_uninstall
Windows Uninstall
2021-04-17 18:02:21 +02:00
Veronica K. B. Olsen
41043e4dc1 Fix wrong progress message in setup script 2021-04-17 17:59:35 +02:00
Veronica K. B. Olsen
2e8425d4b1 Improve the setup/uninstall text a bit for windows 2021-04-17 17:57:13 +02:00
Veronica K. B. Olsen
fcac28c69f Improve the windows uninstall script a little 2021-04-17 17:45:28 +02:00
Veronica K. B. Olsen
a14b319ada Update the docs 2021-04-17 17:37:22 +02:00
Veronica K. B. Olsen
6763ebdd66 Don't use import to read version number during setup 2021-04-17 17:18:47 +02:00
Veronica K. B. Olsen
421f2dd688 Add uninstall script for windows 2021-04-17 16:35:13 +02:00
Veronica K. B. Olsen
19b21a3e8a Add win-uninstall command to the setup script 2021-04-17 16:13:13 +02:00
Veronica K. B. Olsen
06dcaff806 Merge branch 'testing' into dev 2021-04-06 17:49:28 +02:00
Veronica K. B. Olsen
f02bba5630 Merge branch 'main' into testing 2021-04-06 17:49:11 +02:00
Veronica Berglyd Olsen
12fa295506 Merge pull request #742 from vkbo/update_i18n
Update i18n
2021-04-06 17:46:39 +02:00
Veronica K. B. Olsen
1054bbdf8b Refresh translation files 2021-04-06 17:42:26 +02:00
Bruno Meneguello
b47436ec59 Update Portuguese translation 2021-04-06 17:42:05 +02:00
Veronica K. B. Olsen
856534cb81 Fix reference to moved file in translation files 2021-04-06 17:39:54 +02:00
Veronica Berglyd Olsen
3ee07c7da7 Merge pull request #736 from vkbo/xdg_setup
XDG Setup and Uninstall
2021-04-02 16:17:57 +02:00
Veronica K. B. Olsen
96fb892465 Update docs 2021-04-02 16:13:18 +02:00
Veronica K. B. Olsen
3a48a124e2 Linux tests 3.6 and 3.7 should also push to codecov 2021-04-02 16:02:40 +02:00
Veronica K. B. Olsen
b391d47a40 Add xdg-uninstall option to setup 2021-04-02 15:50:20 +02:00
Veronica K. B. Olsen
a3c4d7c7b6 Remove desktop icon for xdg install 2021-04-02 15:37:55 +02:00
Veronica K. B. Olsen
4ee5890d90 Merge branch 'testing' into dev 2021-03-31 19:03:42 +02:00
Veronica K. B. Olsen
2ce1134b8b Merge branch 'main' into testing 2021-03-31 19:03:28 +02:00
Veronica Berglyd Olsen
641ff441b5 Merge pull request #734 from vkbo/issue733_project_wordlist
Fix failed initial project dictionary
2021-03-31 19:02:21 +02:00
Veronica K. B. Olsen
7687259381 Non-existing project dictionary file shouldn't prevent the vraiable from being set 2021-03-31 18:53:54 +02:00
Veronica K. B. Olsen
bd888451ec Merge branch 'testing' into dev 2021-03-29 18:52:36 +02:00
Veronica K. B. Olsen
7484542af2 Merge branch 'main' into testing 2021-03-29 18:52:23 +02:00
Veronica K. B. Olsen
1e64a3f94c Further clarifications on installation 2021-03-29 18:51:43 +02:00
Veronica K. B. Olsen
fd7319b295 Merge branch 'testing' into dev 2021-03-29 18:39:36 +02:00
Veronica K. B. Olsen
923e00ad19 Merge branch 'main' into testing 2021-03-29 18:39:17 +02:00
Veronica K. B. Olsen
518be4e630 Merge REaDME from testing branch and rename some tests in workflows 2021-03-29 18:36:15 +02:00
Veronica K. B. Olsen
06a13557e3 Merge branch 'testing' into dev 2021-03-28 18:15:40 +02:00
Veronica Berglyd Olsen
72aa82c8ed Merge pull request #731 from vkbo/release_1.3b1
Release 1.3 Beta 1
2021-03-28 17:59:34 +02:00
Veronica K. B. Olsen
b8a136e0ad Typo in changelog 2021-03-28 17:58:00 +02:00
Veronica K. B. Olsen
38ba214327 Updated changelog and release notes 2021-03-28 17:54:22 +02:00
Veronica K. B. Olsen
c23ec06ce6 Bumped version to 1.3b1 2021-03-28 17:54:06 +02:00
Veronica Berglyd Olsen
c495098582 Merge pull request #730 from vkbo/cleanup
Source Cleanup
2021-03-28 17:25:30 +02:00
Veronica Berglyd Olsen
499d095f81 Merge branch 'testing' into cleanup 2021-03-28 17:18:30 +02:00
Veronica Berglyd Olsen
2ed9367dc8 Merge pull request #729 from vkbo/i18n_setup
I18n Setup
2021-03-28 17:18:22 +02:00
Veronica K. B. Olsen
820e6fcfa3 Update tests 2021-03-28 17:15:35 +02:00
Veronica K. B. Olsen
f92ef05ee2 Move enum and constants to the root source folder 2021-03-28 17:15:17 +02:00
Veronica K. B. Olsen
4b3795671d Update lanaguage files 2021-03-28 16:37:23 +02:00
Veronica K. B. Olsen
bff5ff79bd Added i18n instructions to the docs 2021-03-28 16:30:08 +02:00
Veronica K. B. Olsen
21056c5cb8 Clean up and update the main README 2021-03-28 16:22:03 +02:00
Veronica K. B. Olsen
a4567939d6 Include i18n files in the minimal zip files 2021-03-28 15:42:58 +02:00
Veronica K. B. Olsen
e92ac2b30d Merge branch 'testing' into dev 2021-03-28 15:23:16 +02:00
Veronica K. B. Olsen
90871d5859 Merge branch 'main' into testing 2021-03-28 15:23:02 +02:00
Veronica Berglyd Olsen
cb299d145b Merge pull request #725 from vkbo/release_1.2.2
Release 1.2.2
2021-03-28 15:08:17 +02:00
Veronica K. B. Olsen
6afc34e47e Update changelog and release notes 2021-03-27 22:31:15 +01:00
Veronica K. B. Olsen
f6bcbbd962 Merge branch 'main' into release_1.2.2 2021-03-27 22:22:02 +01:00
Veronica Berglyd Olsen
55f1129a24 Merge pull request #728 from vkbo/issue727_pyw_file
Fix Win-Install from Source
2021-03-27 22:16:22 +01:00
Veronica K. B. Olsen
a2c48492ee Make sure the novelWriter.pyw file exists on win-install 2021-03-27 22:12:33 +01:00
Veronica K. B. Olsen
cb0591fe5f Bump version to 1.2.2 and update changelog 2021-03-27 15:38:48 +01:00
Veronica Berglyd Olsen
2f269c3e17 Merge pull request #724 from vkbo/setup_shasum
SHA 256 Sum for setup files
2021-03-27 15:37:50 +01:00
Veronica K. B. Olsen
d1a15f07d5 Add sha256sum to setup script 2021-03-23 21:21:29 +01:00
Veronica Berglyd Olsen
3cb0c95278 Merge pull request #717 from vkbo/issue716_focus_shortcuts
Issue #716: Focus Mode Improvement and Bugfix
2021-03-22 19:01:42 +01:00
Veronica K. B. Olsen
74910a6b56 Use an enum for switching focus instead of hard coded numbers 2021-03-22 18:57:57 +01:00
Veronica K. B. Olsen
5ab32326c1 Update tests 2021-03-22 18:39:25 +01:00
Veronica K. B. Olsen
7dc184f05f Rename the main GUi setFocus function so that it doesn't override the internal one 2021-03-22 18:35:02 +01:00
Veronica K. B. Olsen
dbdb3931f4 Fix search shortcuts in Focus Mode and stop hiding buttons 2021-03-22 18:34:18 +01:00
Veronica K. B. Olsen
56a5559024 Merge branch 'testing' into dev 2021-03-21 17:18:43 +01:00
Veronica K. B. Olsen
8798afc41c Merge branch 'main' into testing 2021-03-21 17:18:29 +01:00
Veronica Berglyd Olsen
e67c911c97 Merge pull request #710 from vkbo/release_1.2.1
Release 1.2.1
2021-03-21 17:10:31 +01:00
Veronica K. B. Olsen
88d69f6fd4 Update changelog and release notes 2021-03-21 17:08:51 +01:00
Veronica Berglyd Olsen
99555f7dd0 Merge branch 'main' into release_1.2.1 2021-03-21 17:01:14 +01:00
Veronica Berglyd Olsen
b3ec77c631 Merge pull request #715 from vkbo/issue714_install_docs
Issue #714: Linux Setup
2021-03-21 17:01:02 +01:00
Veronica K. B. Olsen
d4c61e7ec9 Update Linux setup for consistency and add more details for macOS 2021-03-21 16:54:53 +01:00
Veronica K. B. Olsen
48bbe288b7 Clarify the install instructions for Linux 2021-03-21 16:50:10 +01:00
Veronica K. B. Olsen
3664266eed Merge branch 'testing' into dev 2021-03-17 22:58:28 +01:00
Veronica Berglyd Olsen
a1f7ab1562 Merge pull request #713 from vkbo/update_french_again
Update french again
2021-03-17 22:57:48 +01:00
Veronica K. B. Olsen
5f95c745e2 Update positions in translation files 2021-03-17 22:40:07 +01:00
jyhelle
1f735d8739 Remove French translations for Qt base, submitted by email 2021-03-17 22:39:46 +01:00
Veronica K. B. Olsen
551fa28b27 Fix typo in i18n readme 2021-03-17 21:19:48 +01:00
Veronica K. B. Olsen
909114e318 Merge branch 'testing' into dev 2021-03-17 21:13:51 +01:00
Veronica Berglyd Olsen
a4e55a0664 Merge pull request #711 from vkbo/update_french
Update French + Bugfix
2021-03-17 21:12:57 +01:00
Veronica K. B. Olsen
86e196808e Fix bug saving quote settings in Preferences 2021-03-17 21:06:58 +01:00
jyhelle
0a4e15de31 Update French translation submitted by email 2021-03-17 21:06:32 +01:00
Veronica K. B. Olsen
cd1f52c6b5 Update translation files 2021-03-17 21:05:57 +01:00
Veronica K. B. Olsen
b63449bffd Merge branch 'main' into testing 2021-03-17 17:25:00 +01:00
Veronica K. B. Olsen
93965138da Bump version and update changelog for patch 1.2.1 2021-03-17 17:23:20 +01:00
Veronica Berglyd Olsen
fae1bd791f Merge pull request #709 from vkbo/issue708_focus
Issue #708: Search bar focus
2021-03-17 17:12:53 +01:00
Veronica K. B. Olsen
35855d2d71 Add a section about search in the docs 2021-03-17 17:09:24 +01:00
Veronica K. B. Olsen
c374534d9e Fix test 2021-03-17 16:43:51 +01:00
Veronica K. B. Olsen
f6c92dab24 Make the editor search/replace bypass the docAction pipeline and be called directly 2021-03-17 16:43:45 +01:00
Veronica K. B. Olsen
03beae30ca Move dev branch to version 1.4-alpha0 2021-03-17 11:33:37 +01:00
Veronica K. B. Olsen
4ee81501a9 Merge branch 'testing' into dev 2021-03-14 18:28:50 +01:00
Veronica K. B. Olsen
3b92c3d75c Wrong docs link on PyPi 2021-03-14 18:22:22 +01:00
Veronica K. B. Olsen
567d1ae104 Improve the text for new Preferences options 2021-03-14 15:00:31 +01:00
Veronica Berglyd Olsen
7b7f028d74 Merge pull request #704 from vkbo/french_space
French space padding
2021-03-14 14:52:32 +01:00
Veronica K. B. Olsen
0e53524f2e Update translation files 2021-03-14 14:49:56 +01:00
Veronica K. B. Olsen
0f0e0951ec Add padding options to Preferences 2021-03-14 14:43:12 +01:00
Veronica K. B. Olsen
a172c4b725 Move quote symbols to new Preferences tab 2021-03-14 14:32:05 +01:00
Veronica K. B. Olsen
9f979c02f8 Use free string entries for padding instead of many switches 2021-03-14 14:23:44 +01:00
Veronica K. B. Olsen
1d59a67d6e Fix tests 2021-03-13 23:41:39 +01:00
Veronica K. B. Olsen
5837a4602a Add optional padding for large punctuation symbols 2021-03-13 23:38:12 +01:00
Veronica K. B. Olsen
e07a7ac496 Add optional padding to single and double quotes 2021-03-13 23:25:20 +01:00
Veronica K. B. Olsen
de57d0b027 Merge branch 'main' into dev 2021-03-13 17:00:07 +01:00
Veronica Berglyd Olsen
2f790140e0 Merge pull request #698 from vkbo/about_dialog
About dialog updates
2021-03-03 23:04:45 +01:00
Veronica K. B. Olsen
a8bf247d01 Consistency 2021-03-03 22:56:03 +01:00
Veronica K. B. Olsen
e2b3e06077 Merge branch 'dev' into about_dialog 2021-03-03 22:53:45 +01:00
Veronica Berglyd Olsen
43332d8752 Merge pull request #697 from bkmeneguello/pt
Update portuguese translation
2021-03-03 22:52:12 +01:00
Veronica K. B. Olsen
b410ef5d08 Update translations 2021-03-03 22:51:13 +01:00
Veronica K. B. Olsen
1e97a9a872 Use tables instead of lists in About dialog 2021-03-03 22:50:55 +01:00
Veronica K. B. Olsen
26c49e7c22 Add a translations credit section to the About dialog 2021-03-03 21:52:30 +01:00
Bruno Kühnen Meneguello
89269b848d Update portuguese translation 2021-03-02 09:04:21 -03:00
Veronica K. B. Olsen
dd10486f27 Merge branch 'main' into dev 2021-03-02 11:38:36 +01:00
Veronica Berglyd Olsen
81302e3ca5 Merge pull request #695 from vkbo/projsettings_update
Project Settings update
2021-02-27 17:37:01 +01:00
Veronica K. B. Olsen
cf99b4f8b9 Fix flake8 error 2021-02-27 17:19:39 +01:00
Veronica K. B. Olsen
12d39550a8 Use constants for column indices 2021-02-27 17:18:30 +01:00
Veronica K. B. Olsen
9c3e063635 Update en_US and nb_NO translations 2021-02-27 17:08:28 +01:00
Veronica K. B. Olsen
bc0f3e343b Merge branch 'dev' into projsettings_update 2021-02-27 17:02:23 +01:00
Veronica K. B. Olsen
d3eabd8d8f Fix test and revealed bug in main code 2021-02-27 17:00:17 +01:00
Veronica K. B. Olsen
bfa8f97e7e Make the Importance and Status tabs look consistent with Auto-Replace 2021-02-27 16:49:54 +01:00
Veronica K. B. Olsen
4136328a36 Improve the layout of Project Settings > Auto-Replace 2021-02-27 16:21:03 +01:00
Veronica K. B. Olsen
2fd2c18dfc Remove the colour range function as it will not be used 2021-02-27 16:06:24 +01:00
Veronica K. B. Olsen
0a32f9405a Merge branch 'testing' into dev 2021-02-24 22:45:41 +01:00
Veronica Berglyd Olsen
7fe3238339 Merge pull request #693 from vkbo/translations
Translations Rondup
2021-02-24 22:34:04 +01:00
Veronica K. B. Olsen
f491f5bf49 Fix broken test 2021-02-24 22:29:49 +01:00
Veronica K. B. Olsen
17070b88d3 Add American English translation 2021-02-24 22:23:35 +01:00
Veronica K. B. Olsen
36fd23b264 Fix some typos and inconsistencies in GUI texts 2021-02-24 22:19:17 +01:00
Veronica K. B. Olsen
fcd1d400f8 Fix spelling of the word "licence" 2021-02-24 21:39:21 +01:00
Veronica K. B. Olsen
dbbec86a13 Merge branch 'dev' into projsettings_update 2021-02-24 20:57:11 +01:00
Veronica Berglyd Olsen
2f0388d692 Merge pull request #692 from vkbo/french_translation
Add French translation
2021-02-24 20:06:36 +01:00
Jan Lüdke
d7922c6330 Add French translation 2021-02-24 20:00:53 +01:00
Veronica K. B. Olsen
b383142c14 Set maximum height for author box on project settings 2021-02-24 19:47:49 +01:00
Veronica K. B. Olsen
c42b0caf0f Clean up the Project Settings classes 2021-02-23 23:29:18 +01:00
Veronica K. B. Olsen
320643d2da Merge branch 'testing' into dev 2021-02-23 21:14:48 +01:00
Veronica Berglyd Olsen
2560b4d336 Merge pull request #687 from vkbo/gui_tweaks
Make settings layouts more responsive
2021-02-23 17:38:44 +01:00
Veronica K. B. Olsen
66282d4ed0 Add Preferences window size to Config and update tests 2021-02-22 20:23:14 +01:00
Veronica K. B. Olsen
2152f54066 Make use of the improved layout class in Project Settings and Preferences 2021-02-22 20:22:49 +01:00
Veronica K. B. Olsen
c89cb32871 Make the QConfigLayout custom class more responsive to the length of labels due to translations 2021-02-22 20:21:22 +01:00
Veronica K. B. Olsen
59eaf9177e Remove some debug print statements 2021-02-22 19:29:01 +01:00
Veronica K. B. Olsen
0cc1573503 Merge remote-tracking branch 'origin/dev' into dev 2021-02-21 20:37:04 +01:00
Veronica Berglyd Olsen
2ad3f2c1ab Merge pull request #686 from bkmeneguello/pt
Update portuguese translation
2021-02-21 17:49:28 +01:00
Bruno Kühnen Meneguello
6cca26b914 Updates portuguese translation 2021-02-21 13:34:00 -03:00
Veronica K. B. Olsen
7b417929e6 Merge branch 'testing' into dev 2021-02-21 16:24:33 +01:00
Veronica K. B. Olsen
58eaddf753 Merge branch 'main' into dev 2021-02-20 17:40:44 +01:00
Veronica Berglyd Olsen
89ab97076c Merge pull request #684 from vkbo/qt_translations
Qt Widget Translations
2021-02-20 13:48:57 +01:00
Veronica K. B. Olsen
dd444ccc8a Update TS files 2021-02-20 13:44:37 +01:00
Veronica K. B. Olsen
c9cab47ce0 Minor code changes for consistency 2021-02-20 13:40:26 +01:00
Veronica K. B. Olsen
b868477406 Change minimum Qt version to 5.3 2021-02-20 13:39:48 +01:00
Veronica K. B. Olsen
71ad3fc51d Fix flake8 error 2021-02-19 17:21:49 +01:00
Veronica K. B. Olsen
837ad7e39c Fix tests 2021-02-19 17:19:10 +01:00
Veronica Berglyd Olsen
898c589e2b Merge branch 'dev' into qt_translations 2021-02-19 16:46:48 +01:00
Veronica K. B. Olsen
49c90a4455 Add a dummy file for translating Qt widgets used in nW for languages with no qtbase localised file 2021-02-19 16:40:52 +01:00
Veronica K. B. Olsen
d315f37c88 Remove translations of standard buttons and switch to native file dialogs 2021-02-19 16:38:56 +01:00
Veronica Berglyd Olsen
799c1dc804 Merge pull request #683 from Number042/dev
Add German translation.
2021-02-19 16:30:00 +01:00
Marian Lückhof
3d457e757d Add German translation. 2021-02-19 16:06:19 +01:00
Veronica K. B. Olsen
f74d575d6a Update the i18n readme 2021-02-18 21:42:46 +01:00
Veronica Berglyd Olsen
d4e0bacde9 Merge pull request #682 from vkbo/project_language
Project Language
2021-02-18 21:26:43 +01:00
Veronica K. B. Olsen
844c3ab26f Allow Build language to not be set at all 2021-02-18 21:18:14 +01:00
Veronica K. B. Olsen
97c884de42 Fix a potential bug 2021-02-18 20:53:31 +01:00
Veronica K. B. Olsen
6cc974a8e9 Fix tests 2021-02-18 20:47:22 +01:00
Veronica K. B. Olsen
5f3822ac1a The word "Synopsis" should be translated to GUI language when it appears in document viewer 2021-02-18 20:46:27 +01:00
Veronica K. B. Olsen
e9763f4ec6 Add language option to the Build tool 2021-02-18 20:45:18 +01:00
Veronica K. B. Olsen
e4d03f9581 Fix tests for variable change, and change monkeypatches to use the context manager 2021-02-18 19:21:46 +01:00
Veronica K. B. Olsen
8b0ac6e81e Rename projLang to spellLang 2021-02-18 19:19:50 +01:00
Veronica Berglyd Olsen
a73eba8916 Merge pull request #681 from bkmeneguello/pt
Updates portuguese translations
2021-02-18 09:10:32 +01:00
Bruno Kühnen Meneguello
c74d761a4b Updates portuguese translations 2021-02-17 21:41:45 -03:00
Veronica Berglyd Olsen
e60cc7b544 Merge pull request #679 from vkbo/norwegian_locale
Norwegian Localisation
2021-02-17 23:25:19 +01:00
Veronica K. B. Olsen
515e53b220 Fix indentation 2021-02-17 23:20:20 +01:00
Veronica K. B. Olsen
8948c3f435 Final translation pass for Norwegian 2021-02-17 23:16:23 +01:00
Veronica K. B. Olsen
3a1367a1d8 Finish the first iteration of the Norwegian translation 2021-02-17 21:55:23 +01:00
Veronica K. B. Olsen
e114873e4c Remove deprecated meta entry support in project files 2021-02-17 21:54:56 +01:00
Veronica K. B. Olsen
be8f98f6c7 Add more translations 2021-02-17 19:10:50 +01:00
Veronica K. B. Olsen
5a685474ff Move the remanining two functions in the tools.py file to other source files 2021-02-17 16:31:37 +01:00
Veronica K. B. Olsen
185974a3ea Add a basic project_pt.json file 2021-02-17 16:30:29 +01:00
Veronica K. B. Olsen
6c87711595 Add internationalisation README 2021-02-17 12:32:00 +01:00
Veronica K. B. Olsen
35b8e389a3 The project language files should obviously also be in the i18n folder 2021-02-17 12:14:57 +01:00
Veronica K. B. Olsen
51564d1adc Rename nb-NO to nb_NO 2021-02-17 10:08:48 +01:00
Veronica K. B. Olsen
88df06ee47 Merge branch 'dev' into norwegian_locale 2021-02-17 10:06:12 +01:00
Veronica Berglyd Olsen
770c746444 Merge pull request #680 from bkmeneguello/Languages-GUI
Adds language selector to preferences
2021-02-17 10:04:41 +01:00
Veronica K. B. Olsen
47215ddb13 Fix instantiation of DummyApp 2021-02-17 09:59:53 +01:00
Veronica K. B. Olsen
fb09066679 Remove debug print and add test coverage 2021-02-17 09:53:27 +01:00
Veronica K. B. Olsen
7591095f2a Rewrite listLanguages function to add more checks and fix misidentification of default language 2021-02-17 09:28:25 +01:00
Bruno Kühnen Meneguello
48575c09ca Adds languages combo to preferences 2021-02-16 23:07:13 -03:00
Veronica K. B. Olsen
5fc01fe685 Add the updated translation files 2021-02-17 01:43:12 +01:00
Veronica Berglyd Olsen
df94dd2cf5 Merge branch 'dev' into norwegian_locale 2021-02-17 01:39:07 +01:00
Veronica Berglyd Olsen
ae53ce5544 Merge pull request #676 from vkbo/tokenizer_localisation
Tokenizer Localisation
2021-02-17 01:37:58 +01:00
Veronica K. B. Olsen
1f0e6c0426 Fix a bunch of missing translation tags and fix a few default strings 2021-02-17 01:37:28 +01:00
Veronica K. B. Olsen
7f84c3c71c Ignore compiled linguistics files 2021-02-17 01:36:06 +01:00
Veronica K. B. Olsen
8a050810c1 Update translation file after removing no longer needed Tokenizer translations 2021-02-17 00:01:07 +01:00
Veronica K. B. Olsen
4033f7a47d Fix tests 2021-02-16 23:58:25 +01:00
Veronica K. B. Olsen
62cc64d912 Remove the old numberToWord functions and clean up the translation files 2021-02-16 23:47:49 +01:00
Veronica K. B. Olsen
290ca2178d Merge branch 'dev' into tokenizer_localisation 2021-02-16 23:13:36 +01:00
Veronica Berglyd Olsen
b935f8a3f3 Merge pull request #673 from bkmeneguello/i18n
Project basic internationalization support
2021-02-16 23:08:34 +01:00
Bruno Kühnen Meneguello
65e31115a1 Final translations 2021-02-16 18:48:15 -03:00
Veronica K. B. Olsen
19d536ea8b Add norwegian project translation files 2021-02-16 22:45:17 +01:00
Veronica K. B. Olsen
8aa2401134 Make sure the project translation map is loaded correctly 2021-02-16 22:42:18 +01:00
Veronica K. B. Olsen
4699d9404b Add the project localisation to the converter classes 2021-02-16 22:18:14 +01:00
Veronica K. B. Olsen
b50132e255 Add support for project language file 2021-02-16 22:02:59 +01:00
Veronica K. B. Olsen
8f1c4af9fa Fix a bug in Build and Constants, and clean up some more nested translates 2021-02-16 19:42:50 +01:00
Veronica K. B. Olsen
d5bd816676 Last round of changes to localisation 2021-02-16 18:14:04 +01:00
Bruno Kühnen Meneguello
8093528694 Update portuguese translation 2021-02-15 22:22:05 -03:00
Veronica K. B. Olsen
7b7bf5d390 Fix a bug in parsing orphaned file names 2021-02-16 00:38:15 +01:00
Veronica K. B. Olsen
8db140fe89 Make the number to word function ready for multiple langauges, and make some minor changes to tooltips in build and status bar 2021-02-15 23:43:56 +01:00
Veronica K. B. Olsen
2e4148a9eb Restore some of the old spell check information in the status bar 2021-02-15 22:29:05 +01:00
Bruno Kühnen Meneguello
ed356ba85d Merge branch 'i18n' of https://github.com/bkmeneguello/novelWriter into i18n 2021-02-15 15:48:52 -03:00
Bruno Kühnen Meneguello
cf79fef0bc Update portuguese translation 2021-02-15 15:47:14 -03:00
Bruno Kühnen Meneguello
d4b4451a48 Improved list and quote handling, also fix a typo 2021-02-15 15:46:48 -03:00
Veronica K. B. Olsen
2f57a4f86f Resolve comment about numbered format string in dialog 2021-02-15 18:03:36 +01:00
Veronica K. B. Olsen
31b665d344 Last pass of fixes and tweaks 2021-02-15 17:52:04 +01:00
Veronica K. B. Olsen
6d56bc7c0c Fix flake8 complaints 2021-02-15 17:01:38 +01:00
Veronica K. B. Olsen
89cf28fade Make tests work on github actions default language code 2021-02-15 16:57:56 +01:00
Veronica K. B. Olsen
8087e8ad75 Fix tests and a bug in GuiMain 2021-02-15 16:48:12 +01:00
Veronica K. B. Olsen
df19ced50c Rewrap self.tr and fix a few bugs 2021-02-15 16:47:57 +01:00
Veronica K. B. Olsen
dba8865d75 Revert some changes to how languages are loaded and remove iso.py 2021-02-15 16:32:20 +01:00
Bruno Meneguello
d506cf2d2a Fix typo and left back "print" 2021-02-15 09:37:45 -03:00
Veronica K. B. Olsen
5e30e1a2ee Just use Qt's own facility for loading localisation files 2021-02-15 11:35:19 +01:00
Veronica K. B. Olsen
af6d96843d Add docstrings and help info for the i18n tools in setup.py 2021-02-15 00:03:14 +01:00
Veronica K. B. Olsen
c0acc1d087 Move the i18n initialisation to the Config class 2021-02-15 00:02:45 +01:00
Bruno Kühnen Meneguello
98e3343808 novelWriter i18n and portuguese translation 2021-02-14 17:16:43 -03:00
Veronica K. B. Olsen
861a847692 Merge branch 'testing' into dev 2021-02-12 22:07:46 +01:00
Veronica K. B. Olsen
1917c2ce38 Merge branch 'testing' into dev 2021-02-11 23:14:50 +01:00
Veronica K. B. Olsen
f9e97735e6 Merge branch 'testing' into dev 2021-02-11 17:19:53 +01:00
Veronica K. B. Olsen
d0f11db700 Fix main init source file header 2021-02-10 23:08:47 +01:00
Veronica K. B. Olsen
27de8138f5 Bumped version to 1.3 alpha0 for next dev cycle 2021-02-10 23:00:40 +01:00
194 changed files with 37496 additions and 7789 deletions

24
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,24 @@
<!--
Please check the following before you make a pull request:
* The branch you are making the pull request from (in your own fork) has a unique and descriptive
name. Do not make a pull request directly from your copy of `main`.
* If you are submitting translation files, only submit the files that you have added translations
to, not the other .ts files that may have been updated in the process.
* Make sure your contribution follows the style guide and other requirements mentioned in the
CONTRIBUTING.md file in the repository.
* Fill in the Summary section below, and if relevant, mention the issue numbers related to the PR.
-->
**Summary:**
**Related Issue(s):**
**Reviewer's Checklist:**
* [ ] The header of all files contain a reference to the repository license
* [ ] The overall test coverage is increased or remains the same as before
* [ ] All tests are passing
* [ ] All flake8 checks are passing and the style guide is followed
* [ ] Documentation (as docstrings) is complete and understandable
* [ ] Only files that have been actively changed are committed

View File

@@ -2,9 +2,11 @@ name: flake8
on:
push:
branches: [ main, testing, dev ]
branches:
- main
pull_request:
branches: [ main, testing, dev ]
branches:
- main
jobs:
checkSyntax:
@@ -25,5 +27,5 @@ jobs:
flake8 tests --count --select=E9,F63,F7,F82 --show-source --statistics
- name: Coding Style Violations
run: |
flake8 nw --count --max-line-length=99 --ignore E203,E221,E226,E228,E241,E251,E261,E266,E302,E305 --show-source --statistics
flake8 tests --count --max-line-length=99 --ignore E203,E221,E226,E228,E241,E251,E261,E266,E302,E305 --show-source --statistics
flake8 nw --count --max-line-length=99 --ignore E221,E226,E228,E241 --show-source --statistics
flake8 tests --count --max-line-length=99 --ignore E221,E226,E228,E241 --show-source --statistics

View File

@@ -1,34 +1,38 @@
name: Linux (3.8)
name: Linux
on:
push:
branches: [ main, testing, dev ]
branches:
- main
pull_request:
branches: [ main, testing, dev ]
branches:
- main
jobs:
testLinux38:
testLinux:
strategy:
matrix:
python-version: [3.6, 3.7, 3.8, 3.9]
runs-on: ubuntu-latest
steps:
- name: Python Setup
uses: actions/setup-python@v2
with:
python-version: 3.8
python-version: ${{ matrix.python-version }}
architecture: x64
- name: Install Packages
- name: Install Packages (apt)
run: |
sudo apt update
sudo apt install libenchant-dev qt5-default
sudo apt install libenchant-dev qttools5-dev-tools
- name: Checkout Source
uses: actions/checkout@v2
- name: Install Dependencies
- name: Install Dependencies (pip)
run: |
pip install --upgrade pip
pip install -r requirements.txt
pip install pytest-timeout
pip install pytest-cov
pip install pytest-qt
pip install codecov
pip install -r requirements-dev.txt
- name: Run Build Commands
run: python setup.py qtlrelease sample
- name: Run Tests
run: |
export QT_QPA_PLATFORM=offscreen

View File

@@ -1,33 +0,0 @@
name: Linux (3.6)
on:
push:
branches: [ main, testing, dev ]
pull_request:
branches: [ main, testing, dev ]
jobs:
testLinux36:
runs-on: ubuntu-latest
steps:
- name: Python Setup
uses: actions/setup-python@v2
with:
python-version: 3.6
architecture: x64
- name: Install Packages
run: |
sudo apt update
sudo apt install libenchant-dev qt5-default
- name: Checkout Source
uses: actions/checkout@v2
- name: Install Dependencies
run: |
pip install --upgrade pip
pip install -r requirements.txt
pip install pytest-timeout
pip install pytest-qt
- name: Run Tests
run: |
export QT_QPA_PLATFORM=offscreen
pytest -v --timeout=60

View File

@@ -1,33 +0,0 @@
name: Linux (3.7)
on:
push:
branches: [ main, testing, dev ]
pull_request:
branches: [ main, testing, dev ]
jobs:
testLinux37:
runs-on: ubuntu-latest
steps:
- name: Python Setup
uses: actions/setup-python@v2
with:
python-version: 3.7
architecture: x64
- name: Install Packages
run: |
sudo apt update
sudo apt install libenchant-dev qt5-default
- name: Checkout Source
uses: actions/checkout@v2
- name: Install Dependencies
run: |
pip install --upgrade pip
pip install -r requirements.txt
pip install pytest-timeout
pip install pytest-qt
- name: Run Tests
run: |
export QT_QPA_PLATFORM=offscreen
pytest -v --timeout=60

View File

@@ -1,37 +0,0 @@
name: Linux (3.9)
on:
push:
branches: [ main, testing, dev ]
pull_request:
branches: [ main, testing, dev ]
jobs:
testLinux39:
runs-on: ubuntu-latest
steps:
- name: Python Setup
uses: actions/setup-python@v2
with:
python-version: 3.9
architecture: x64
- name: Install Packages
run: |
sudo apt update
sudo apt install libenchant-dev qt5-default
- name: Checkout Source
uses: actions/checkout@v2
- name: Install Dependencies
run: |
pip install --upgrade pip
pip install -r requirements.txt
pip install pytest-timeout
pip install pytest-cov
pip install pytest-qt
pip install codecov
- name: Run Tests
run: |
export QT_QPA_PLATFORM=offscreen
pytest -v --cov=nw --timeout=60
- name: Upload to Codecov
uses: codecov/codecov-action@v1

View File

@@ -1,13 +1,15 @@
name: macOS (3.9)
name: macOS
on:
push:
branches: [ main, testing, dev ]
branches:
- main
pull_request:
branches: [ main, testing, dev ]
branches:
- main
jobs:
testMac38:
testMac:
runs-on: macos-latest
steps:
- name: Python Setup
@@ -24,11 +26,8 @@ jobs:
run: |
pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt
pip install pyobjc
pip install pytest-timeout
pip install pytest-cov
pip install pytest-qt
pip install codecov
- name: Run Tests
run: |
export QT_QPA_PLATFORM=offscreen

View File

@@ -1,13 +1,15 @@
name: Windows (3.9)
name: Windows
on:
push:
branches: [ main, testing, dev ]
branches:
- main
pull_request:
branches: [ main, testing, dev ]
branches:
- main
jobs:
testWin38:
testWin:
runs-on: windows-latest
steps:
- name: Python Setup
@@ -21,10 +23,7 @@ jobs:
run: |
pip install --upgrade pip
pip install -r requirements.txt
pip install pytest-timeout
pip install pytest-cov
pip install pytest-qt
pip install codecov
pip install -r requirements-dev.txt
- name: Run Tests
run: |
pytest -v --cov=nw --timeout=60

7
.gitignore vendored
View File

@@ -7,6 +7,11 @@
*.egg-info
setup.iss
novelwriter.desktop
novelWriter.pyw
# Translations
/nw/assets/i18n/*.qm
.qmake.stash
i18n/*.qm
i18n/*.qph
@@ -23,6 +28,7 @@ __pycache__
/nw/assets/sample.zip
/sample/cache
/sample/meta
/sample/versions
*.bak
*.lock
ToC.txt
@@ -41,3 +47,4 @@ ToC.txt
# Coverage
/.coverage
/coverage.*

View File

@@ -1,5 +1,466 @@
# novelWriter Changelog
## Version 1.4 [2021-07-27]
### Release Notes
This release contains some new features and a lot of code refactoring. Among the main new features
is paragraph alignment and indentation. Regular line breaks within paragraphs are now also
supported. The changes are described in more detail below.
#### Internationalisation
Translation files have been added for Simplified Chinese by Qianzhi Long.
#### Line Breaks
The way line breaks inside paragraphs work has been changed. A single line break is now treated as
a proper line break and will show up in the document viewer and exported documents. A single line
break does not start a new paragraph, but forces a break inside the paragraph like a Shift + Enter
does in most rich text editors. Two line breaks is still needed to start a new paragraph.
The old syntax of adding two spaces at the end of a line to force a line break within a paragraph
will still work as before, so there is no need to change your existing text if you've used this
feature. However, there is a new highlighting feature that will show you where in the text you have
redundant spaces. If you are used to having double spaces between sentences, you may want to switch
off this highlighting feature in Preferences as it will also detect those.
A helper function has been added to the Format menu that can look through a paragraph and remove
line breaks in case you've been using line breaks inside your existing text under the assumption
that the exporter and viewer will ignore them.
I hope this change will not be too inconvenient. I believe the new behaviour will make more sense
for most people. Especially considering some of the feedback I've gotten on how line breaks work.
The original implementation was following the Markdown standard, but since novelWriter is not a
proper Markdown editor and instead just borrows from Markdown, this behaviour always seemed a bit
unnecessary.
#### Text Alignment and Indentation
The default text alignment is left or justified based on your preferences. For documents with the
layout set to Title Page or Partitions, the default is centred. However, sometimes you may want to
override this default. A new set of codes have therefore been added to allow specifying alignment
as well as additional text margins on individual paragraphs.
The logic of the syntax is as follows:
A single angle bracket will push the text away from the edge it points away from. Therefore, a
single `>` before the paragraph, or a single `<` after the paragraph, will add indentation on the
respective side. It's perfectly valid to do this on both sides at the same time.
A double set of angle brackets will push the text all the way towards the opposite side. Therefore,
a double set of `>>` before the paragraph will indicate right alignment, and a double set of `<<`
after the paragraph will force left alignment. Also here both can be used at the same time, which
results in the paragraph being centred.
Format menu entries and keyboard shortcuts have been added so that you don't have to memorise these
codes.
_These Release Notes also include the changes from 1.4 Beta 1 and RC 1._
### Detailed Changelog
**Bugfixes**
* The way margins are determined in the paged dialogs used many places, including Preferences, has
been improved. These margins would sometimes be set to zero when they shouldn't. PR #834.
* Dialogs that are non-modal are no longer duplicated when opened multiple times. Instead, the
existing dialog is moved to the front. PR #834.
**Internationalisation**
* US English and Norwegian translations have been updated by @vkbo. PR #825.
* French translations have been updated by @jyhelle. PR #829.
----
## Version 1.4 RC 1 [2021-07-12]
### Release Notes
This is a preview release of novelWriter 1.4. It contains some new features and a lot of code
refactoring. This release is a testing release, and may contain bugs. Please be careful when using
this version to work on your projects.
### Detailed Changelog
**Internationalisation**
* Translation files have been added for Simplified Chinese. The translation has been provided by
Qianzhi Long (@longqzh). PR #817.
**Code Improvements**
* Improve PEP8 compliance for the source code. PRs #816 and #820.
* The way messages are written to the logging tool has been improved. PR #818.
* Some improvements to parsing and encoding of HTML has been added. PR #821.
* Test coverage of the document editor has been improved. PR #787.
----
## Version 1.4 Beta 1 [2021-06-13]
### Release Notes
This is a preview release of novelWriter 1.4. It contains some new features and a lot of code
refactoring. This release is a testing release, and may contain bugs. Please be careful when using
this version to work on your projects.
### Detailed Changelog
**Bugfixes**
* A number of calls for the pop-up alert box were missing translation wrappers. That means they
could not be translated into other languages. The alerts have been fixed, but the PR does not add
the missing translations. PR #806.
* A duplicate error message from the index class has been removed. PR #758.
**Features**
* Single line breaks are now treated as proper line breaks within paragraphs. Paragraphs are still
separated by two line breaks like before. This means that it is no longer necessary to leave two
spaces at the end of the line to force a line break. This is a rather obscure and little known
feature taken from Markdown, and it isn't very intuitive. Issue #785. PR #786.
* It is now possible to specify text alignment and additional indentation on individual paragraphs
in the text. Both features use a similar syntax that I hope is fairly intuitive. Menu entries and
keyboard shortcuts have also been added to make it easier to use these features. Issues #595 and
#803. PR #804.
**Code Improvements**
* Class initialisation has been made consistent. All GUI classes now inherit the main window as its
parent class, and all other classes inherit the main project class as its parent. Since the
project class and the main window class have pointers to each other, all needed pointers are
available from their respective classes. PR #758.
* The document class has been changed from a reusable class to a class that is intended to wrap a
single document via its handle. The class was originally written for the document editor where
the reusable approach made more sense. But it is much simple to create and destroy them in other
parts of the code when they are not reusable. PRs #758 and #760.
* The document class no longer generates any pop-up alerts. Errors are recorded and retrieved and
displayed by the parent or caller class. PR #758.
* Refactored the code of the editor class to make it more isolated by making most class variables
private. PR #779.
* Made similar changes to the viewer class and item details class. PR #780.
----
## Version 1.3.3 [2021-06-13]
### Release Notes
This patch release fixes a potential file encoding issue when running setup on Windows, and a minor
issue with the project word count not being updated immediately when a file is deleted. In
addition, the keyboard shortcuts to change focus between the project tree, the editor, the viewer,
and the outline panel, have been changed for Windows users. They keyboard shortcuts were
interfering with the Alt codes used for special characters. The shortcuts are unchanged for Linux
and macOS.
### Detailed Changelog
**Bugfixes**
* Fix an issue with file encoding when extracting version information from the source code during
setup on Windows. This seems to be a limited issue, but the changes make the relevant function
more fault tolerant. Issue #805. PR #807.
* The project word count on the status bar was not always updated when a file was permanently
deleted from the project. This has now been resolved. Issue #799. PR #810.
* The keyboard shortcuts to change focus will on Windows interfere with the alt key codes as the
focus shortcuts used `Alt+` to `Alt+4`. On Windows, these are now instead `Ctrl+Alt+1` to
`Ctrl+Alt+4`. Part of issue #740. PR #808.
**Source Code**
* Remove a redundant line in the source code. PR #802.
* Make the XML parse for project items a little less panicky when encountering unexpected XML tags.
Generally, this shouldn't be a problem, but the XML parser should silently ignore unexpected tags
when parsing the project file. This may occur if a project is opened in an earlier version of
novelWriter. If so, a warning is issued anyway, so it is safe to disregard unrecognised tags as
the user has already actively selected to proceed and been sufficiently warned. PR #809.
----
## Version 1.3.2 [2021-05-30]
### Release Notes
This is a patch release that fixes some minor issues. One issue was with the split tool, which
would drop the last line from the source document during a split if it was missing a final line
break. A minor issue with the display of word counts on the details panel under the project tree
has also been fixed. In addition, the setup script commands for Linux have been improved a bit.
### Detailed Changelog
**Bugfixes**
* The details panel under the tree would sometimes show character, word and paragraph counts even
when there was no document selected. This has now been fixed. Issue #781. PR #782.
* The split document tool would drop the last line of the source document. This is generally not a
problem if the last line has a line break after it, but if it doesn't, it is lost in the split.
This was caused by an offset error when calculating the split positions in the file and has been
resolved. Issue #795. PR #796.
**Installation**
* Due to an error in the setup script in the past, a desktop icon was created for novelWriter when
running the `xdg-install` command. This is no longer the case, but the old icon was still left on
some user's desktops. The setup script will now remove that icon if it exists when the
`xdg-install` or `xdg-uninstall` commands are run. PR #784.
----
## Version 1.3.1 [2021-05-06]
### Release Notes
This is a patch release primarily to fix a problem with the Qt translation library used to make
novelWriter available in multiple languages. One of the function calls made to the library was
added recently, in Qt 5.15, which many users on Linux will not have installed on their system. The
issue could be resolved by updating the library, but it's an unnecessary and inconvenient
restriction.
### Detailed Changelog
**Bugfixes**
* The code to load translation files for the main GUI was using a function in the QTranslate class
that was added in version 5.15, which means novelWriter could not start on a lower version of the
Qt library if translation files were present. We don't want to force users to use Qt 5.15, so the
call has been removed. Issue #773. PR #774.
**Other Fixes**
* When generating a new project on Windows, the code that generates the new documents in the
project would create duplicate handles, causing a warning to be printed. The warning is harmless
as the collisions are handled. They are caused by the resolution of the clock available to Python
on Windows being in the tens of millisecond range, much slower than the code generates the new
project files. The solution was to append a counter to the timestamp from the clock, ensuring
that the seed is always unique. Issue #769. PR #776.
----
## Version 1.3 [2021-05-02]
### Release Notes
The main feature of the 1.3 release is internationalisation. The release introduces support for
Portuguese, French, and Norwegian for the main GUI, in addition to the default English. The
settings for choosing the GUI language can be found in the main Preferences.
The implementation is such that the projects themselves can use a different language than the one
selected for the GUI. To allow for better support for multiple language projects, the Build Novel
Project tool has the added option to select the language of the injected text into exported
documents. In particularly for the number words that can be added for chapter headers. Available
languages here are the same as mentioned above for the main GUI, plus German.
Settings have also been added in Preferences to allow for automatic injection of spaces or non-
breaking spaces in front of, or behind, certain characters. These are features generally available
in text editors for French, but the implementation in novelWriter simply makes them a free text box
where you can list all characters or symbols where you want a space added automatically.
The translation files for this release have been provided by Bruno Meneguello for the Portuguese
translation, by Jan Lüdke (jyhelle) for the French translation, by Veronica Berglyd Olsen for the
Norwegian translation as well as the minor modifications for US English, and Marian Lückhof
provided the translation file for chapter numbers in German.
The work to rewrite novelWriter to allow for internationalisation was done by Bruno Meneguello and
Veronica Berglyd Olsen.
_These Release Notes also include the changes from 1.3 Beta 1 and RC 1._
### Detailed Changelog
**Bugfixes**
* Fixed an issue when saving a document from the Build Novel Project tool when the previous path
used for a file dialog no longer existed. The dialog defaults to show the same folder as last
time it was opened, but should default to the user's home folder if that path no longer exists.
There was a bug in this default behaviour that resulted in a critical error. Issue #761. PR #762.
* Remove a duplicate error message triggered by a broken cached index file. The two error messages
reported on the same error. The first of them was also not included in the translation framework.
PR #759.
**Installation**
* The setup script, and file locations for the translation files, have been updated such that the
translation files are available in both the PyPi package and in the Minimal Install packages.
PRs #753, #764 and #765.
**Code Maintenance**
* The source code files for dialogs and tools have been moved out of the main `gui` source folder.
The `gui` folder now only contains sub-elements of the main GUI class. PR #754.
----
## Version 1.3 RC 1 [2021-04-18]
### Release Notes
This is a release candidate of 1.3. The primary feature of release 1.3 is the addition of
internationalisation (i18n) of novelWriter. The release introduces support for Portuguese, French,
and Norwegian in addition to the default English.
There have been no major changes since the release of 1.3 Beta 1, but the bugfixes and improvements
from version 1.2.3 have been included.
### Detailed Changelog
**Internationalisation**
* The Portuguese translation has been updated and should now be complete. Thanks again to Bruno
Meneguello (@bkmeneguello). PR #742.
*The changes since 1.3 Beta 1 also include the changes of release 1.2.3.*
----
## Version 1.3 Beta 1 [2021-03-28]
### Release Notes
This is a pre-release of 1.3. The primary feature of release 1.3 is the addition of
internationalisation (i18n) of novelWriter. The release introduces support for Portuguese, French,
and Norwegian in addition to the default English.
This is a beta release. Use with caution on live project.
### Detailed Changelog
**Internationalisation**
* Added support to the source code for internationalisation of the GUI. Thanks to Bruno Meneguello
(@bkmeneguello) for doing most of the work. Issue #93. PRs #673, #680 and #684.
* Build Novel Project localisation has been added as well. This is separate from the GUI
localisation as the project may not be written in the same language as the GUI is set to. PRs
#676 and #682.
* The text editor's auto-replace features now support inserting spaces automatically when replacing
quotes, as well as in front of major punctuation. These features are common for writing tools
supporting French and Spanish for instance. Issue #703. PR #704.
**User Interface**
* The Preferences dialog has been updated to be more responsive to varying text label lengths due
to different needs for different languages. PRs #687 and #711.
* The Project Settings dialog has been improved to be more consistent and user friendly across the
Status, Importance and Auto-Replace tabs. Issue #691. PR #695.
* The About dialog has been updated to contain more information on contributions. PR #698.
**Translations**
* Portuguese translation added by Bruno Meneguello (@bkmeneguello). PRs #673, #681, #686 and #697.
* Norwegian translation added by Veronica Olsen (@vkbo). PR #679.
* German translation added (Build tool only) by Marian Lückhof (@Number042). PR #683.
* French translation added by Jan Lüdke (@jyhelle). PRs #692, #711 and #713.
* American English added by Veronica Olsen (@vkbo). PR #693.
**Installation**
* Added i18n support to the setup script. PRs #673 and #729.
**Code Maintenance**
* The ISO lookup dictionary for language codes has been removed and replaced with the lookup
features available in Qt5 through QLocal. The remaining two source files in the constants folder
have been moved up a level as well. PRs #673 and #730.
----
## Version 1.2.3 [2021-04-18]
### Release Notes
This patch fixes a bug where the user's word list (personal dictionary) was not saved properly for
new projects. The added words were thus "forgotten" the next time the project was opened.
In addition, uninstall commands have been added to the main setup script to make it easier to clean
up icons (Linux and Windows) and registry keys (Windows) if the user wishes to uninstall
novelWriter.
### Detailed Changelog
**Bugfixes**
* Fixed an issue where the initial file for the user dictionary for a new project would not be
created the first time a word was added to it. This caused the words the user added to the
dictionary to not be loaded the next time the project was opened. Issue #733. PR #734.
* Some of the `setup.py` commands could not run if the PyQt5 packages were missing due to an import
command extracting the version number from the main `nw` package. This was not technically a bug
as the choice to do it this way was made deliberately, but a function has been added to the
`setup.py` script to allow reading the version number without having to import the `nw` package.
PR #749.
**Installation**
* An `xdg-uninstall` option has been added to `setup.py` to uninstall the icons installed into the
system by the `xdg-install` command. PR #736.
* Likewise, a `win-uninstall` option has been added to `setup.py` to uninstall the icons installed
into the system by the `win-install` command. Issue #743 and discussion #739. PR #749.
* Improvements have also been made to the `setup_windows.bat` script, and a complementary
`uninstall_windows.bat` has been added as well. PR #749.
----
## Version 1.2.2 [2021-03-28]
### Release Notes
This patch release is a bug fix release addressing some inconsistencies and issues with the
document header buttons when Focus Mode is active. The keyboard shortcuts for search and replace
should now also work in Focus Mode. In addition, the setup script for novelWriter has been improved
when installing on Windows.
### Detailed Changelog
**Bugfixes**
* The way Focus Mode worked when activated through the menu and through the document header button
were inconsistent. The header button would deactivate the edit, search and close buttons, while
the menu entry would not. These two methods now call the same set of functions to ensure the
behaviour is consistent. PR #717.
* Closing the document while in Focus Mode now ends Focus Mode. Previously, the editor would be
left stuck in Focus Mode with no way to exit. PR #717.
**User Interface**
* The keyboard shortcuts for the search and replace tool now also work in Focus Mode. Previously,
the menu entries and their shortcuts were deactivated in this mode. Issue #716. PR #717.
**Installation**
* The setup script command do build minimal install archive files now also generate SHA 256 sum
files. PR #724.
* The setup script will now copy the `novelWriter.py` file to `novelWriter.pyw` if it doesn't exist
when the `win-install` command is run. Issue #727. PR #728.
----
## Version 1.2.1 [2021-03-21]
### Release Notes
This patch release is a bug fix release addressing issues with the document editor's search and
replace tool. Due to some recently added restrictions on when various tools are active, depending
on which part of the main window has the user's focus, the search tool keyboard shortcuts and
buttons were blocked when they shouldn't. This release resolves these issues.
### Detailed Changelog
**Bugfixes**
* Fixed an issue with the search bar where the button and shortcut actions would be blocked unless
the document itself had focus. This focus check was added for all the text altering functions,
but should not affect search and replace. The search and replace actions now bypass the regular
action pipeline with the focus check. Issue #708. PR #709.
**Documentation**
* Improve the instructions for setup on Linux when manually installing using the `setup.py` script.
Previously, the documentation wasn't very clear on the difference between a user space install
and a system wide install. Neither did it explain how to install `setuptools` if the package is
missing. Issue #714. PR #715.
----
## Version 1.2 [2021-03-14]
### Release Notes

View File

@@ -1,39 +1,48 @@
# Contributing Guide
When contributing to this repository, please first discuss the change you wish to make via the
issue tracker or the discussions page with the owner of this repository before making a change. If
you just want to make a minor correction, like fix a typo or similar, feel free to just make a pull
request directly.
issue tracker or the discussions page with the owner of this repository. Especially if it is
regarding new features. If you just want to make a minor correction, like fix a typo or similar,
feel free to just make a pull request directly.
## Branching Structure
The following contributions are welcome:
There are three protected branches on this repository. They are used in the following way:
* Bugfixes for new or existing bugs. Please also report new bugs in the issue tracker even if you
also provide a fix. It makes it easier to keep track of what has been fixed and when.
* New translations. You can read more about contributing translations
[here](https://github.com/vkbo/novelWriter/blob/main/i18n/README.md).
* Improvements to the documentation. Particularly if the documentation is unclear. Please don't
make any larger changes to the documentation without discussing if with the maintainer first.
* Adaptations, installation or packaging features targeting specific operating systems.
* `main` – This is the Stable branch. It is used for releases and subsequent patches. No
development code should be merged into this branch starting from version 1.0.
* `dev` – This is the Unstable (development) branch. It is where new features are merged, and where
pre-releases are taken from.
* `testing` – This is the Testing branch. It is populated from the `dev` branch for pre-releases
that need a longer testing phase.
Please do not:
Stable releases and patches will be tagged in the `main` branch, pre-releases may be tagged from
either `testing` or `dev` branch.
* Make a pull request that restructures or reformats existing code. If you think some parts of the
code could be improved, please make an issue thread or start a discussion. The same applies to
any text document in the repository.
### What Branch to Use for Contributions
## Pull Requests
* If your contribution is a fix for the latest stable release, branch from the `main` branch.
* If your contribution is a fix for the latest testing release, branch from the `testing` branch.
* If your contribution is a new feature, branch from the `dev` branch.
This project follows the [OneFlow](https://www.endoflineblog.com/oneflow-a-git-branching-model-and-workflow)
model. The `main` branch is the default branch. For general changes, please make a new branch in
your own fork from the current `main` branch. Do not make pull requests from your own `main`
branch.
## Pull Request Process
**Note:** If your contribution is a bugfix for a specific version, please make a bugfix branch from
the tag you want the fix to be applied to, not directly from the `main` branch. This is important.
1. Make sure your code passes all tests and conforms to the style guide. You can check that the
code conforms by running `flake8` from the root of the project folder.
2. Please provide a complete description of the changes in the pull request, and a summary that can
be copied into the [CHANGELOG](CHANGELOG.md). Remember to reference any issue related by
providing the issue number.
3. Do not change the version number. Version numbers are bumped in separate release pull requests
by the maintainer.
Also check the following:
* Make sure your code passes all tests and conforms to the style guide. You can check that the
code generally conforms by running the Python linting tool `flake8` from the root of the project
folder, although it doesn't check everything. The same check is also run on pull requests by the
maintainer.
* Please provide a description of the changes in the pull request under the summary section of the
pull request template, and reference any related issues by providing the issue number.
* Do not change the version number.
* Do not submit files that were not actively changed but have otherwise been modifed. This is
mostly an issue with translation files. The language tool may update all files in the `i18n`
folder.
## Code of Conduct
@@ -52,92 +61,63 @@ Please see the [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) file for the full text.
## Code Style Guide
The source code of novelWriter broadly follows the [PEP8](https://www.python.org/dev/peps/pep-0008)
style guide, but with a few modifications and exceptions listed below.
style guide, but with a few exceptions. Some key points are listed below.
### Line Length
**Line Length:**
For this project, source lines should stay within the 79 and 99 character limits described by PEP8.
79 characters is often too restrictive, so 99 character lines are acceptable when that is more
practical. Readability has priority. Generally, if a code statement requires multiple lines, the
lines should wrap at 79 characters, not 99. If wrapping can be avoided by going to 99, then that is
generally preferrable.
* Source code lines can extend to the upper limit of 99 characters allowed by PEP8. 79 characters
is the recommended line length in PEP8, but this is often too restrictive. 99 characters are
acceptable when that is more practical. Readability has priority. Generally, if a code statement
requires multiple lines, the lines should wrap at 79 characters if possible. If wrapping can be
avoided by going to 99, then that is generally preferrable.
* For text files, the text should be wrapped at 99 character. The exception is markdown image tags
and urls which can run past that limit.
For text files, the text should also be wrapped at 99 character. The exception is markdown image
tags and urls.
**Variable and Function Names**
Please do not submit pull requests that re-wrap existing source or text unless this has been
discussed beforehand.
* PEP8 allows for camelCase for consistency with existing code. The Qt library uses camelCase, so
the Python the source code does too.
* The exception to the above is for constants. They should always be in upper snake case, like PEP8
states.
**Spaces, Indentation and Alignment**
* Only indentation by 4 spaces is allowed.
* No trailing spaces should occur on any line in the source code, including empty lines.
* Ideally, a function should end on the same indention level as it started. Exceptions are allowed
if it makes the code easier to follow.
* Aligning operators and attributes in columns with multiple spaces is not allowed by PEP8. The
rule is relaxed a bit here. Alignment is allowed when populating large dictionaries or setting
many class attributes. It does improve readability in such cases, but should not be overused.
### Linting with `flake8`
An excellent tool for checking Python code for errors and coding style is `flake8`. The
documentation is available [here](https://flake8.pycqa.org/en/latest/).
A good tool for checking Python code for errors and code style is `flake8`. The documentation is
available [here](https://flake8.pycqa.org/en/latest/).
The `setup.cfg` file in the root of this project has the following settings for `flake8` that
matches the coding standard:
matches the above code style:
```conf
[flake8]
ignore = E203,E221,E226,E228,E241,E251,E261,E266,E302,E305
ignore = E221,E226,E228,E241
max-line-length = 99
exclude = docs/*
```
The command line equivalent, with reporting, is:
```bash
flake8 . --count --ignore E203,E221,E226,E228,E241,E251,E261,E266,E302,E305 --max-line-length=99 --show-source --statistics
flake8 . --count --ignore E221,E226,E228,E241 --max-line-length=99 --show-source --statistics
```
Passing this check is required before contributions are merged into the `main` branch. This is
checked automatically when you make a pull request. You can run the `flake8` command locally to
check beforehand. The full command will give you a detailed description of the code lines that do
not conform to the standard.
Passing this check is required before contributions are merged. This is checked automatically when
you make a pull request. You can run the `flake8` command locally to check beforehand. The full
command will give you a detailed description of the code lines that do not conform to the standard.
## Ignored Errors
Two of the ignored errors are due to the relaxed restriction on column alignment, these are the
E221 and E241 error codes.
Some `flake8` error codes are ignored for this project for various reasons. The source also uses
camelCase function and variable names. This is the standard for the Qt libraries novelWriter
integrates with. It also happens to be the author's personal preferences.
The reason behind the other ignored error codes are listed below. Many of them are due to PEP8 not
permitting column alignment as opposed to many other coding styles, like for instance for Go. I
find them useful in regions of bulk value assignments. There's a reason why tables are more
readable than lists. They should be used sparingly though.
The ignored errors are all `pycodestyle` errors, and they are documented
[here](https://pycodestyle.pycqa.org/en/latest/intro.html#error-codes).
**E203:** whitespace before ‘:’
**Reason:** Column alignment.
**E221:** multiple spaces before operator
**Reason:** Column alignment.
**E226:** missing whitespace around arithmetic operator
**Reason:** This doesn't actually follow the PEP8 recommendation of grouping longer equations by
operator precedence like `2*a + 3*b` instead of `a * a + 3 * b`. Generally, don't use spaces around
`*`, `/` and `**`, but _do_ use spaces around `+` and `-`. For appending strings, the spaces can be
dropped. Don't use the `+` operator for appending multiple strings. Use formatting instead.
**E228** missing whitespace around modulo operator
**Reason:** See reason for E226. Formatting `%` like `/` and `*` should be possible.
**E241:** multiple spaces after ‘,’
**Reason:** Column alignment.
**E251:** unexpected spaces around keyword / parameter equals
**Reason:** Column alignment.
**E261:** at least two spaces before inline comment
**Reason:** With syntax highlighting, this one doesn't make much sense.
**E266:** too many leading ‘#’ for block comment
**Reason:** In the source multiple `#`s is sometimes used to indicate importance or heading level,
like markdown headers.
**E302:** expected 2 blank lines, found 0
**Reason:** Applies to classes. Instead, end classes with a comment like `# END Class ClassName` to
make it easier to see which class just ended. The double line break is then redundant.
**E305:** expected 2 blank lines after end of function or class
**Reason:** Instead, _always_ end a function with a `return`, preferably indented at function
level. The end of the function is then clear.
The code E226 is ignored becuse it doesn't actually follow the
[PEP8 recommendation](https://www.python.org/dev/peps/pep-0008/#other-recommendations)
of grouping longer equations by operator precedence like `2*a + 3*b` instead of `a * a + 3 * b`.
Generally, don't use spaces around `*`, `/` and `**`, but _do_ use spaces around `+` and `-`.
The same applies to the modulo operator `%`, hence E228 is also ignored.

42
CREDITS.md Normal file
View File

@@ -0,0 +1,42 @@
# Credits
## Main Developer
* Veronica Berglyd Olsen (@vkbo)
## Main Contributors
* Concept: Marian Lückhof (@Number042)
* Internationalisation: Bruno Meneguello (@bkmeneguello)
## Translations
* French: Jan Lüdke (@jyhelle)
* Norwegian: Veronica Berglyd Olsen (@vkbo)
* Portuguese: Bruno Meneguello (@bkmeneguello)
* Simplified Chinese: Qianzhi Long (@longqzh)
## Libraries
The following libraries are dependencies of novelWriter:
* Qt5 by [Qt Company](https://www.qt.io/)
* PyQt5 by [Riverbank Computing](https://www.riverbankcomputing.com/software/pyqt/)
* lxml by [Martijn Faassen](https://lxml.de/)
* Enchant by [Dom Lachowicz](https://abiword.github.io/enchant/)
* PyEnchant by [Dimitri Merejkowsky](https://pyenchant.github.io/pyenchant/)
## Assets
Some of the assets bundled with novelWriter were adapted from the following sources:
* Typicons icons by Stephen Hutchings (CC BY-SA 4.0)
* Cantarell font by Dave Crossland (OPEN FONT LICENSE Version 1.1)
* Tomorrow syntax themes by Chris Kempson (MIT License)
* Owl syntax themes by Sarah Drasner (MIT License)
* Solarized themes by Ethan Schoonover, added by @nullbasis (MIT License)
## Special Mentions
Additional thanks to @johnblommers who was an early user and who has provided a lot of very useful
feedback over the years.

251
README.md
View File

@@ -1,87 +1,100 @@
# novelWriter
[![Linux (3.6)](https://github.com/vkbo/novelWriter/workflows/Linux%20(3.6)/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions)
[![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.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)
[![Linux](https://github.com/vkbo/novelWriter/actions/workflows/test_linux.yml/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions/workflows/test_linux.yml)
[![Windows](https://github.com/vkbo/novelWriter/actions/workflows/test_win.yml/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions/workflows/test_win.yml)
[![macOS](https://github.com/vkbo/novelWriter/actions/workflows/test_mac.yml/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions/workflows/test_mac.yml)
[![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)
[![docs](https://readthedocs.org/projects/novelwriter/badge/?version=latest)](https://novelwriter.readthedocs.io/en/latest/?badge=latest)
[![release](https://img.shields.io/github/v/release/vkbo/novelwriter)](https://github.com/vkbo/novelWriter/releases)
[![pypi](https://img.shields.io/pypi/v/novelwriter)](https://pypi.org/project/novelWriter)
[![python](https://img.shields.io/pypi/pyversions/novelwriter)](https://pypi.org/project/novelWriter)
<img align="left" style="margin: 0 16px 4px 0;" src="https://raw.githubusercontent.com/vkbo/novelWriter/main/setup/icons/scaled/icon-novelwriter-96.png">
novelWriter is a Markdown-like text editor designed for writing novels assembled from many smaller
text documents. It uses a minimal formatting syntax inspired by Markdown, and adds a meta data
syntax for comments, synopsis, and cross-referencing between files. It's designed to be a simple
text editor that allows for easy organisation of text files and notes, built on plain text files
for robustness.
novelWriter is a plain text editor designed for writing novels assembled from many smaller text
documents. It uses a minimal formatting syntax inspired by Markdown, and adds a meta data syntax
for comments, synopsis, and cross-referencing. It's designed to be a simple text editor that allows
for easy organisation of text and notes, using human readable text files as storage for robustness.
The plain text files are suitable for version control software, and also well suited for file
synchronisation tools. The core project structure is stored in a single project XML file. Other
meta data is primarily saved in JSON files.
The project storage is suitable for version control software, and also well suited for file
synchronisation tools. All text is saved as plain text files with a meta data header. The core
project structure is stored in a single project XML file. Other meta data is primarily saved as
JSON files.
The full documentation is available at
[novelwriter.readthedocs.io](https://novelwriter.readthedocs.io).
The full documentation is available on [readthedocs.io](https://novelwriter.readthedocs.io).
The full credits are listed in [CREDITS.md](https://github.com/vkbo/novelWriter/blob/main/CREDITS.md).
## Implementation
The application is written in Python 3 using Qt5 via PyQt5. It is developed on Linux, but it should
in principle work fine on other operating systems as well, as long as dependencies are met. The
unit tests are run on the latest versions of Ubuntu Linux, Windows Server and macOS.
The application is written in Python 3 (3.6+) using Qt5 and PyQt5 (5.3+). It is developed on Linux,
but should in principle work fine on other operating systems as well as long as dependencies are
met. It is regularly tested on Debian and Ubuntu Linux, Windows, and macOS.
## Project Contributions
Contributions to this project are welcome. However, please read the
[Contributing Guide](https://github.com/vkbo/novelWriter/blob/main/CONTRIBUTING.md) before
submitting larger additions or changes to this project.
Please don't make feature pull requests without first having discussed them with the maintainer.
You can make a feature request in the issue tracker, or if the idea isn't fully formed, start a
discussion on the discussion page. Please also don't make pull requests to reformat or rewrite
existing code unless there is a very good reason for doing so.
# Key Features
Fixes and patches are welcome. Contributions related to packaging and installing novelWriter will
also be appreciated, but please make an issue or a discussion topic first. Before contributing any
code, please read the full [Contributing Guide](https://github.com/vkbo/novelWriter/blob/main/CONTRIBUTING.md).
Some features of novelWriter are listed below. Consult the
New translations are always welcome. Please read the additional
[instructions](https://github.com/vkbo/novelWriter/blob/main/i18n/README.md) for further details.
## Key Features
Some key features of novelWriter are listed below. Consult the
[documentation](https://novelwriter.readthedocs.io) for more information.
### Markdown Flavour
### Formatting Codes
novelWriter is _not_ a full-feature Markdown editor. It is a plain text editor that uses
Markdown-like syntax to allow for a minimal set of formatting that is useful for the specific task
of writing novels. The formatting is currently limited to:
Although novelWriter is a plain text editor, it uses a Markdown-like syntax to allow for a minimal
set of formatting that is useful for the specific task of writing novels.
* Headings levels 1 to 4 using the `#` syntax only.
* Emphasised and strongly emphasised text. These are rendered as italicised and bold text.
* Strikethrough text.
* Hard line breaks using two or more spaces at the end of a line.
| Code | Usage | Description |
|------------|----------|-------------|
| `#` | Prefix | Headings level 1 to 4. |
| `_` | Wrapped | Emphasised (italicised) text. |
| `**` | Wrapped | Strongly emphasised (bold) text. |
| `~~` | Wrapped | Strikethrough text. |
| `%` | Prefix | A comment; does not count towards the word count.<sup>1</sup> |
| `@` | Prefix | The following text is parsed as a keyword/value command for meta data. |
| `>` | Prefix | The paragraph is indented one tab width from the left.<sup>2</sup> |
| `<` | Suffix | The paragraph is indented one tab width from the right.<sup>2</sup> |
| `>>` | Prefix | The paragraph is right-aligned.<sup>2</sup> |
| `<<` | Suffix | The paragraph is left-aligned.<sup>2</sup> |
| `>>`, `<<` | Wrapped | The paragraph is centred.<sup>2</sup> |
That is it. Features not supported in the editor are also not exported when using the export tool.
<sup>1</sup> If the first word of the comment is `synopsis:`, the comment is indexed and treated as the
synopsis for the section of text where it occurs. These synopsis comments can be used to build an
outline and exported to external documents.
In addition, novelWriter adds the following, which is otherwise not supported by Markdown:
<sup>2</sup> The indent and alignment codes are available from version 1.4.
In additions:
* A line starting with `%` is treated as a comment and not rendered on exports unless requested.
Comments do not count towards the word count.
* If the first word of the comment is `synopsis:`, the comment is indexed and treated as the
synopsis for the section of text under the same header. These synopsis comments can be used to
build an outline and exported to external documents.
* A set of meta data keyword/values starting with the character `@`. This is used for tagging
and inter-linking documents, and can also be included when generate a project outline.
* A variety of thin and non-breaking spaces are supported. Some of them depend on the system
running at least Qt 5.9. Earlier versions of Qt will unfortunately strip them out when saving.
* Tabs can be used in the text, and should be properly aligned in both editor and viewer. This can
be used to make simple tables and lists. Full Markdown tables and lists are not supported. Note
that for HTML exports, most browsers will treat a tab as a space, so it may not show up like
expected. Open Document exports should produce the expected result.
be used to make simple tables and lists. Note that for HTML exports, most browsers will treat a
tab as a space, so it may not show up like expected. Open Document exports should produce the
expected result.
### Export Formats
The core export formats of novelWriter are Open Document and HTML5. Open Document is an open
standard for office type documents that is supported by most office applications. See
[Open Document > Application Support](https://en.wikipedia.org/wiki/OpenDocument#Application_support)
for more details.
You can also export the entire project as a single novelWriter Markdown-flavour document. These can
later be imported again into novelWriter. In addition, printing and export to PDF is offered
through the Qt library, although with limitations to formatting.
You can also export the entire project as a single novelWriter-flavour document. These can later be
imported again into novelWriter. In addition, printing and export to PDF is offered through the Qt
library, although with limitations to formatting.
### Colour Themes
@@ -92,146 +105,116 @@ syntax highlighting themes. Optional GUI themes are also available, including da
The structure of the project is shown on the left hand side of the main window. Project files are
organised into root folders, indicating what class of file they are. The most important root folder
is the `Novel` folder, which contains all of the files that make up the finished novel. Each root
is the `Novel` folder, which contains all of the files that make up the novel itself. Each root
folder can have subfolders. Subfolders have no impact on the final project structure, they are
there for you to organise your files in whatever way you want.
The editor supports four levels of headings, which determines what level the following text belongs
The editor supports four levels of headings, which determine what level the following text belongs
to. Headings of level one signify a book or partition title. Headings of level two signify the
start of a new chapter. Headings of level three signify the start of a new scene. Headings of level
four can be used internally in each scene to separate sections.
four can be used internally in each scene to create separate sections.
Each novel file can be assigned a layout format, which shows up as a flag next to the item in the
project tree. These are mostly to help the user track what they contain, but they also have some
impact on the format of the exported document. See the
impact on the layout of the exported document. See the
[documentation](https://novelwriter.readthedocs.io) for further details.
### Project Notes
Supporting note files can be added for the story plot, characters, locations, story timeline, etc.
These have their separate root folders. These are optional files.
Supporting notes can be added for the story plot, characters, locations, story timeline, etc. These
have their separate root folders and are optional to use.
### Visualisation of Story Elements
The different notes can be assigned tags, which other files can refer back to using the `@` meta
keywords. This information can be used to display an outline of the story, showing where each scene
connects to the plot, and which characters, etc. occur in them. In addition, the tags themselves
are clickable in the document view pane, and control-clickable in the editor. They make it possible
to quickly navigate between the documents while editing.
The different notes can be assigned tags, which other files can refer back to using `@`-prefixed
meta keywords. This information can be used to display an outline of the story, showing where each
scene connects to the plot, and which characters, etc. occur in them. In addition, the tags
themselves are clickable in the document view pane, and control-clickable in the editor. They make
it possible to quickly navigate between the documents while writing.
# Installing and Running
## Standard Installation
For install instructions, please check the
[Getting Started](https://novelwriter.readthedocs.io/en/latest/int_started.html) section in
the [documentation](https://novelwriter.readthedocs.io/).
For a regular installation, it is recommended that you download one of the minimal zip files from
the [Releases](https://github.com/vkbo/novelWriter/releases) page or the
[novelwriter.io](https://novelwriter.io/) website.
The [documentation](https://novelwriter.readthedocs.io/) has detailed install instructions for
[Linux](https://novelwriter.readthedocs.io/en/latest/setup_linux.html),
[Windows](https://novelwriter.readthedocs.io/en/latest/setup_windows.html), and
[macOS](https://novelwriter.readthedocs.io/en/latest/setup_mac.html).
They are pretty straightforward.
## TLDR Instructions
## Running from Source
If you want to run novelWriter directly from the source code, you must run the `novelWriter.py`
file from command line. For installations on Linux, macOS or Windows, see below.
file from command line.
**Note:** You may need to replace `python` with `python3` and `pip` with `pip3` in the instructions
below on some systems. You may also want to add the `--user` flag for `pip` to install in your user
space only.
### Install from PyPi
### Dependencies
novelWriter is available on [pypi.org](https://pypi.org/project/novelWriter/), and can be installed
with:
```bash
pip install novelwriter
```
Dependencies should be installed automatically, but can also be installed directly with:
```bash
pip install pyqt5 lxml pyenchant
```
When installing via pip, novelWriter can be launched from command line with:
```bash
novelWriter
```
Make sure the install location for pip is in your PATH variable. This is not always the case by
default.
### Setup on Linux
If you're installing from source, the following commands will set up novelWriter on Linux:
Dependencies can generally be installed from PyPi with:
```bash
pip install -r requirements.txt
python setup.py install
```
### Additional Steps for Linux
On Linux, you can most likely find the dependencies in your distribution's repository. On Ubuntu
and Debian, run:
```bash
sudo apt install python3-pyqt5 python3-lxml python3-enchant
```
If you want to set up a launcher and icons on Linux, you can run:
```bash
python setup.py xdg-install
```
This should make novelWriter available as a regular application on your system, with a launceher
icon, and file association with novelWriter project files.
### Additional Steps for macOS
**Note:** If you don't want to install the dependencies from pip and instead use your distro's own
packages, you must manually install `python3-pyqt5`, `python3-lxml` and `python3-enchant`
(Debian/Ubuntu), or the corresponding packages for your distro.
### Setup on macOS
If you're installing from source, the following commands will set up novelWriter on macOS:
First, make sure you have properly set up Python3 with Homebrew. If not, check their
[documentation](https://docs.brew.sh/Homebrew-and-Python).
In addition, the following steps are necessary to install all dependencies:
```bash
brew install enchant
pip3 install --user -r requirements.txt
pip3 install --user pyobjc
python3 setup.py install
```
At present, novelWriter isn't further integrated into the OS, so you must launch it from command
line with:
```bash
novelWriter
```
### Additional Steps for Windows
If you want to help improve the setup process on macOS, it would be much appreciated.
Windows does not by default come with Python installed. If you haven't installed it already, get it
from [python.org/downloads](https://www.python.org/downloads/). Remember to select "Add Python to
PATH" during the installation.
### Setup on Windows
The script `windows_install.bat` in the `setup` folder can be used to create desktop and start menu
icons for novelWriter. The script will also install dependencies for you from PyPi.
For Windows, first ensure that you have Python installed. If not, get it from
[python.org/downloads](https://www.python.org/downloads/). Remember to select "Add Python to PATH"
during the installation, otherwise novelWriter will not start.
### Internationalisation
Then, download the `novelWriter-x.y.z-minimal-win.zip` file, where `x.y.z` is the version number,
from the [releases](https://github.com/vkbo/novelWriter/releases) page. You can extract it to
wherever you want to keep novelWriter on your PC, and run the `setup_windows.bat` file in it
(double-clicking it should work). This will install the necessary dependencies from
[pypi.org](https://pypi.org/) and create desktop and start menu icons.
If you install from source, you must build the translation files yourself if you want to switch to
a different GUI language than British English. This requires that you have the Qt translation
framework installed. Check the specific instruction in the README in the `i18n` source folder for
how to build the translation files.
## Debugging
If you need to debug novelWriter, you must run it from command line. It takes a few parameters,
If you need to debug novelWriter, you must run it from the command line. It takes a few parameters,
which can be listed with the switch `--help`. The `--info`, `--debug` or `--verbose` flags are
particularly useful for increasing logging output for debugging.
# Licenses
## Licenses
This is Open Source software, and novelWriter is licensed under GPLv3. See the
[GNU General Public License website](https://www.gnu.org/licenses/gpl-3.0.en.html) for more
details, or consult the [LICENSE](https://github.com/vkbo/novelWriter/blob/main/LICENSE.md) file.
Bundled assets have the following licenses:
Bundled assets and their licenses are listed in
[CREDITS](https://github.com/vkbo/novelWriter/blob/main/CREDITS.md).
* The Typicons-based icon themes by Stephen Hutchings are licensed under
[CC BY-SA 4.0](http://creativecommons.org/licenses/by-sa/4.0/). The icons have been altered in
size and colour for use with novelWriter, and some additional icons added. The original icon set
is available at
[stephenhutchings/typicons.font](https://github.com/stephenhutchings/typicons.font).
* The Cantarell font by Dave Crossland is licensed under
[OPEN FONT LICENSE Version 1.1](http://scripts.sil.org/OFL).
It is available at [Google Fonts](https://fonts.google.com/specimen/Cantarell).
* The Tomorrow syntax themes use colour schemes taken from Chris Kempson's collection of code
editor themes, licensed with the
[MIT License](https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md),
and the main repo is available at
[chriskempson/tomorrow-theme](https://github.com/chriskempson/tomorrow-theme).
* Likewise, the Owl syntax themes use colours from Sarah Drasner's code editor themes, licensed
with the [MIT License](https://github.com/sdras/night-owl-vscode-theme/blob/master/LICENSE), and
the main repo is available at
[sdras/night-owl-vscode-theme](https://github.com/sdras/night-owl-vscode-theme).
# Screenshot
## Screenshots
**novelWriter with default system theme:**
![Screenshot 1](https://raw.githubusercontent.com/vkbo/novelWriter/main/docs/source/images/screenshot_default.png)

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Configuration file for the Sphinx documentation builder.
#
@@ -25,9 +24,9 @@ copyright = "2018–2021, Veronica Berglyd Olsen"
author = "Veronica Berglyd Olsen"
# The short X.Y version
version = "1.2"
version = "1.4"
# The full version, including alpha/beta/rc tags
release = "1.2"
release = "1.4"
# -- General configuration ---------------------------------------------------
@@ -47,7 +46,6 @@ language = None
exclude_patterns = []
pygments_style = None
# -- Options for HTML output -------------------------------------------------
html_theme = "sphinx_rtd_theme"
@@ -75,13 +73,11 @@ html_css_files = [
# "searchbox.html"]``.
# html_sidebars = {}
# -- Options for HTMLHelp output ---------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = "novelWriterDoc"
# -- Options for LaTeX output ------------------------------------------------
latex_elements = {
@@ -110,7 +106,6 @@ latex_documents = [(
author, "manual"
)]
# -- Options for manual page output ------------------------------------------
# One entry per manual page. List of tuples
@@ -119,7 +114,6 @@ man_pages = [(
master_doc, "novelwriter", "novelWriter Documentation", [author], 1
)]
# -- Options for Texinfo output ----------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
@@ -130,7 +124,6 @@ texinfo_documents = [(
"novelWriter", "Markdown-like editor for novels.", "Miscellaneous"
)]
# -- Options for Epub output -------------------------------------------------
# Bibliographic Dublin Core info.

View File

@@ -71,6 +71,7 @@ operating system for further details.
:caption: Usage
usage_interface
usage_shortcuts
usage_projects
usage_structure
usage_notes

View File

@@ -38,14 +38,13 @@ at the same time provide a complete set of features needed for writing a novel.
.. tip::
If you do need to align information in rows and columns in your notes, you can achieve this with
tabs and hard line breaks. The tab stop width can be specified in :guilabel:`Preferences` and
hard line breaks can be inserted by adding to spaces at the end of the line.
tabs and line breaks. The tab stop width can be specified in :guilabel:`Preferences`.
The main window does not have a toolbar like many other applications do. This reduces clutter, and
since the documents are formatted with markdown tags, is more or less redundant. However, all
formatting features supported are available through convenient keyboard shortcuts. They are also
available in the main menu so you don't have to look up the syntax every time until you've
memorised it. A full list of shortcuts can be found in the :ref:`a_ui_shortcuts` section.
memorised it. A full list of shortcuts can be found in the :ref:`a_kb` section.
In addition, novelWriter has a :guilabel:`Focus Mode` where all the user interface elements other
than the document editor itself are hidden away.

View File

@@ -99,6 +99,31 @@ Make sure the install location for pip is in your PATH variable. This is not alw
default.
.. _a_started_i18n:
Building the Translation Files
==============================
If you installed novelWriter from a package, the translation files should be pre-built and
included. If you're running novelWriter from the source code, you will need to generate the files
yourself. The files you need will be written to the ``nw/assets/i18n`` folder, and will have the
``.qm`` file extension.
You can build the ``.qm`` files with:
.. code-block:: console
python3 setup.py qtlrelease
This requires that the Qt Linguist tool is installed on your system. On Ubuntu and Debian, the
needed package is called `qttools5-dev-tools`.
.. note::
If you want to improve novelWriter with translation files for another language, or update an
existing translation, instructions for how to contribute can be found in the README file in the
``i18n`` folder of the source code.
.. _a_started_docs:
Building the Documentation
@@ -109,13 +134,14 @@ you're running novelWriter from the source code, a local copy of this documentat
generated. It requires the following Python packages on Debian and Ubuntu.
* ``python3-sphinx``
* ``python3-sphinx-rtd-theme``
* ``python3-sphinxcontrib.qthelp``
Or from PyPi:
.. code-block:: console
pip install sphinx sphinxcontrib-qthelp
pip install sphinx sphinx-rtd-theme sphinxcontrib-qthelp
The documentation can then be built from the ``docs`` folder in the source code by running:

View File

@@ -42,34 +42,69 @@ can install them with:
Step 2: Install Package (Optional)
----------------------------------
You can install novelWriter to the correct location for Python packages with:
You can install novelWriter to the default location for Python packages using ``setuptools``. This
step is optional as you can also just put the novelWriter program folder wherever you like
yourself. For instance in ``/opt/novelWriter``, and then run Step 3 to set up icons and launcher.
To install novelWriter to the default location, it requires that you have ``setuptools`` installed
on your system. If you don't have it installed, it can usually be installed from your distro's
repository. For Debian and Ubuntu this is achieved with:
.. code-block:: console
./setup.py install
sudo apt install python3-setuptools
The package is also available from PyPi:
.. code-block:: console
pip3 install --user setuptools
With ``setuptools`` in place, novelWriter can be installed to the user space with:
.. code-block:: console
./setup.py install --user
This should install novelWriter as ``~/.local/bin/novelWriter``. If you instead want to install for
all users, i.e. as ``/usr/local/bin/novelWriter``, run:
.. code-block:: console
sudo ./setup.py install
This is equivalent to what the ``pip`` installer does. It puts novelWriter in the location on your
system where Python packages are usually kept. This is not really the best suited location for a
GUI application like novelWriter, so you may instead copy the entire source to a suiteable location
yourself.
This should install novelWriter to either ``~/.local/bin/novelWriter`` if installed for local user
only, or to ``/usr/local/bin/novelWriter`` if installed for all users (with the ``sudo`` command).
Step 3: Create Launcher Icons
-----------------------------
Regardless of where you extract or install the source files, you can easily set up a standard icon
and launcher. To set up this desktop launcher, the needed icons, and the project file association,
run:
Regardless of where you extract or install the source files, you can set up a standard icon and a
launcher. To set up this desktop launcher, the needed icons, and the project file association,
run the following from inside the novelWriter folder at the installed or final location:
.. code-block:: console
./setup.py xdg-install
By default, these commands install novelWriter and its icons for the current user only. To install
for all users, run the script with the ``sudo`` command.
By default, this command installs the launcher and icons for the current user only. To install for
all users, run the script with the ``sudo`` command.
.. tip::
All options of the setup script can be listed with: ``./setup.py help``.
Uninstalling Icons
==================
The steps taken by the ``xdg-install`` step can be reversed by running:
.. code-block:: console
./setup.py xdg-uninstall
This will remove the desktop launcher and icons from the system. As above, whether this is done on
the current user, or system wide, depends on whether this command is called with ``sudo`` or not.

View File

@@ -52,12 +52,19 @@ You can install novelWriter to the correct location for Python packages with:
.. code-block:: console
./setup.py install
./setup.py install --user
This is equivalent to what the ``pip`` installer does. It puts novelWriter in the location on your
system where Python packages are usually kept. This is not really the best suited location for a
GUI application like novelWriter, so you may instead copy the entire source to a suiteable location
yourself.
This requires that the package ``setuptools`` is installed on your system. If not, it can be
installed with:
.. code-block:: console
pip3 install --user setuptools
This is method of install is equivalent to what the ``pip`` installer does. It puts novelWriter in
the location on your system where Python packages are usually kept. This is not really the best
suited location for a GUI application like novelWriter, so you may instead copy the entire source
to a suiteable location yourself.
After this, you should be able to launch novelWriter by running ``novelWriter`` in a command line
window.

View File

@@ -75,10 +75,13 @@ Step 2: Dependencies and Icons
**Alternative A: By Script**
Open the folder where you extracted the novelWriter source, and double-click the file named
``setup_windows.bat``. This should open a command line window and run the setup script to install
Open the folder where you extracted novelWriter, and double-click the file named
``windows_install.bat``. This should open a command line window and run the setup script to install
dependencies, and add desktop and start menu icons.
.. note::
If you downloaded the full source package, the file may be in the ``setup`` subfolder.
The script will also check that it can find Python on your system and alert you if it cannot run
it. If you are sure you have installed it, but the script cannot find it, you probably didn't
install it with the "Add Python to PATH" option mentioned in Step 1.
@@ -100,7 +103,7 @@ and run the following commands:
.. code-block:: console
python setup.py pip
pip install --user pywin32 -r requirements.txt
python setup.py win-install
The first command will install the dependencies on your system from the `Python Package Index`_,
@@ -108,3 +111,39 @@ and the second command will create a desktop icon and a start menu icon. That sh
you need.
.. _Python Package Index: https://pypi.org/
Uninstalling
============
**Alternative A: By Script**
Open the folder where you keep the novelWriter files, and double-click the file named
``windows_uninstall.bat``. This should open a command line window and run the setup script to
remove the main dependency packages and remove desktop and start menu icons.
.. note::
If you downloaded the full source package, the file may be in the ``setup`` subfolder.
If you plan to also remove Python from your system, you must run the above script first as it needs
Python in order to run.
.. note::
Due to limitations of the ``pip`` installer, dependencies of the dependencies will not be
removed, only the ones the setup script directly installed.
**Alternative B: Manual Uninstallation**
Like for the install process, the script just runs two commands. You can of course run them
yourself if you wish. They are:
.. code-block:: console
python setup.py win-uninstall
pip uninstall -r requirements.txt
There may be other packages on your system installed by ``pip``. To list all packages, run:
.. code-block:: console
pip freeze --user

View File

@@ -152,6 +152,29 @@ convenient if you want to quickly look through all documents in the list in the
pressing :kbd:`F9`.
.. _a_ui_edit_search:
Search & Replace
----------------
The document editor has a search and replace bar that can be activated with :kbd:`Ctrl`:kbd:`F` for
search mode or :kbd:`Ctrl`:kbd:`H` for search/replace mode.
Pressing :kbd:`Return` while in the search box will search for the next occurrence of the word, and
:kbd:`Shift`:kbd:`Return` for the previous. Pressing :kbd:`Return` in the replace box, will replace
the highlighted text and move to the next word.
There are a number of settings for the search bar available as toggle switches above the search
box. They allows you to search for, in order:,: matched case only, whole word results only, search
using regular expressions, loop search when reaching the end of the document, and move to the next
document when reaching the end. There is also a switch that will try to match the case of the word
when the replacement is made. That is, it will try to keep the word upper, lower, or capitalised to
match the word being replaced.
The regular expression search is somewhat dependant on which version of Qt your system has. If you
have Qt 5.13 or higher, there is better support for unicode symbols in the search.
.. _a_ui_edit_auto:
Auto-Replace as You Type
@@ -163,8 +186,8 @@ you wish.
.. tip::
If you don't like auto-replacement, all symbols inserted by this feature are also available in
the :guilabel:`Insert` menu, and via convenient :ref:`a_ui_shortcuts_ins`. You may also be using
a `Compose Key`_ setup, which means you may not need the auto-replace feature.
the :guilabel:`Insert` menu, and via convenient :ref:`a_kb_ins`. You may also be using a
`Compose Key`_ setup, which means you may not need the auto-replace feature.
.. _Compose Key: https://en.wikipedia.org/wiki/Compose_key
@@ -182,15 +205,16 @@ tricky for languages that use the same symbol for these.
.. _a_ui_md:
Markdown Format
===============
The Markdown-Like Format
========================
The document editor uses a simplified markdown format. That is, it supports basic formatting like
emphasis (italic), strong importance (bold) and strikethrough text, as well as four levels of
headings.
The editor itself is a plaintext editor that uses formatting codes for setting meta data values and
allowing for some text formatting. The syntax is based on Markdown, but novelWriter is *not* a
Markdown editor. It supports basic formatting like emphasis (italic), strong importance (bold)
and strikethrough text, as well as four levels of headings.
Some non-standard markdown features have been added. For instance, novelWriter allows for comments,
a synopsis tag, and a set of keyword and value sets used for tags and references.
In addition to formatting codes, novelWriter allows for comments, a synopsis tag, and a set of
keyword and value sets used for tags and references.
.. _a_ui_md_head:
@@ -230,6 +254,69 @@ level of the novel. See :ref:`a_struct_heads` for more details.
itself. See :ref:`a_struct_heads_unnum` for more details.
.. _a_ui_md_text:
Text Paragraphs
---------------
A text paragraph is indicated by a blank line. That is, you need two line breaks to separate two
fragments of text into two paragraphs. Single line breaks are treated as line breaks within a
paragraph.
In addition, the editor supports a few additional types of whitespaces.
* A non-breaking space can be inserted with :kbd:`Ctrl`:kbd:`K`, :kbd:`Space`.
* Thin spaces are also supported, and can be inserted with :kbd:`Ctrl`:kbd:`K`,
:kbd:`Shift`:kbd:`Space`.
* Non-breaking thin space can be inserted with :kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`Space`.
These are all insert features, and the :guilabel:`Insert` menu has more. They are also listed
in :ref:`a_kb_ins`.
Non-breaking spaces are highlighted by the syntax highlighter with an alternate coloured
background, depending on the selected theme.
.. tip::
Non-breaking spaces are the correct type of space to separate a number from its unit. Generally,
it prevents the line wrapping algorithms from adding line breaks where it shouldn't.
.. _a_ui_md_align:
Paragraphs Alignment and Indentation
------------------------------------
Aside from Title Page and Partition which by default have its text centred, all document layouts
have the text by default aligned to the left ot justified, depending on your Preferences.
You can override the default text alignment on individual paragraphs by specifying alignment tags.
These tags are double angle brackets. Either ``>>`` or ``<<``. You put them either before or after
the paragraph, and they will "push" the text towards the edge the brackets point towards. This
should be fairly intuitive.
Indentation uses a similar syntax. But here you use a single ``>`` or ``<`` to push the text away
from the edge.
Examples:
.. csv-table:: Text Alignment and Indentation
:header: "Syntax", "Description"
:widths: 40, 60
:class: "tight-table"
"``>> Right aligned text``", "The text paragraph is right-aligned."
"``Left aligned text <<``", "The text paragraph is left-aligned."
"``>> Centred text <<``", "The text paragraph is centred."
"``> Indented text``", "The text has an increased left margin."
"``Indented text <``", "The text has an increased right margin."
"``> Indented text <``", "The text has an both margins increased."
.. note::
The text editor will not show the alignment and indentation live. But the viewer will show them
when you open the document in the viewer. It will of course also be reflected in the document
generated from the build tool.
.. _a_ui_md_emph:
Text Emphasis
@@ -310,34 +397,6 @@ The available tag and reference keywords are listed in the :ref:`a_struct_tags`
also be inserted at the cursor position in the editor via the :guilabel:`Insert` menu.
.. _a_ui_md_add:
Additional Markdown and Non-Standard Features
---------------------------------------------
The editor and viewer also support markdown standard hard line breaks, and preserve non-breaking
spaces if running with Qt 5.9 or higher. For older versions, the non-breaking spaces are lost when
the document is saved. This is unfortunately hard-coded into the Qt text editor.
* A hard line break can be achieved by leaving two or more spaces at the end of the line. This is
standard markdown syntax. Alternatively, the user can press :kbd:`Ctrl`:kbd:`K`, :kbd:`Return` to
insert this type of line break.
* A non-breaking space can be inserted with :kbd:`Ctrl`:kbd:`K`, :kbd:`Space`.
* Thin spaces are also supported, and can be inserted with :kbd:`Ctrl`:kbd:`K`,
:kbd:`Shift`:kbd:`Space`.
* Non-breaking thin space can be inserted with :kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`Space`.
These are all insert features, and the :guilabel:`Insert` menu has more. They are also listed
in :ref:`a_ui_shortcuts_ins`.
Both hard line breaks and non-breaking spaces are highlighted by the syntax highlighter as an
alternate coloured background, depending on the selected theme.
.. tip::
Non-breaking spaces are the correct type of space to separate a number from its unit. Generally,
it prevents the line wrapping algorithms from adding line breaks where it shouldn't.
.. _a_ui_outline:
Project Outline View
@@ -376,139 +435,3 @@ very large,
The :guilabel:`Synopsis` column of the outline view takes its information from a specially
formatted comment. See :ref:`a_ui_md_comm`.
.. _a_ui_shortcuts:
Keyboard Shortcuts
==================
Most features are available as keyboard shortcuts. These are as follows:
.. csv-table:: Keyboard Shortcuts
:header: "Shortcut", "Description"
:widths: 30, 70
:class: "tight-table"
":kbd:`Alt`:kbd:`1`", "Switch focus to the project tree."
":kbd:`Alt`:kbd:`2`", "Switch focus to document editor."
":kbd:`Alt`:kbd:`3`", "Switch focus to document viewer."
":kbd:`Alt`:kbd:`4`", "Switch focus to outline view."
":kbd:`Alt`:kbd:`Left`", "Move backward in the view history of the document viewer."
":kbd:`Alt`:kbd:`Right`", "Move forward in the view history of the document viewer."
":kbd:`Ctrl`:kbd:`.`", "Open menu to correct word under cursor."
":kbd:`Ctrl`:kbd:`,`", "Open the :guilabel:`Preferences` dialog."
":kbd:`Ctrl`:kbd:`/`", "Change block format to comment."
":kbd:`Ctrl`:kbd:`0`", "Remove block formatting for block under cursor."
":kbd:`Ctrl`:kbd:`1`", "Change block format to header level 1."
":kbd:`Ctrl`:kbd:`2`", "Change block format to header level 2."
":kbd:`Ctrl`:kbd:`3`", "Change block format to header level 3."
":kbd:`Ctrl`:kbd:`4`", "Change block format to header level 4."
":kbd:`Ctrl`:kbd:`A`", "Select all text in the document."
":kbd:`Ctrl`:kbd:`B`", "Format selected text, or word under cursor, with strong emphasis (bold)."
":kbd:`Ctrl`:kbd:`C`", "Copy selected text to clipboard."
":kbd:`Ctrl`:kbd:`D`", "Strikethrough selected text, or word under cursor."
":kbd:`Ctrl`:kbd:`E`", "If in the project tree, edit a document or folder settings. (Same as :kbd:`F2`.)"
":kbd:`Ctrl`:kbd:`F`", "Open the search bar and search for the selected word, if any is selected."
":kbd:`Ctrl`:kbd:`G`", "Find next occurrence of search word in current document. (Same as :kbd:`F3`.)"
":kbd:`Ctrl`:kbd:`H`", "Open the search and replace bar and search for the selected word, if any is selected. (On Mac, this is :kbd:`Cmd`:kbd:`=`.)"
":kbd:`Ctrl`:kbd:`I`", "Format selected text, or word under cursor, with emphasis (italic)."
":kbd:`Ctrl`:kbd:`K`", "Activate the insert commands. The commands are listed in :ref:`a_ui_shortcuts_ins`."
":kbd:`Ctrl`:kbd:`N`", "Create new document."
":kbd:`Ctrl`:kbd:`O`", "Open selected document."
":kbd:`Ctrl`:kbd:`Q`", "Exit novelWriter."
":kbd:`Ctrl`:kbd:`R`", "If in the project tree, open a document for viewing. If the editor has focus, open current document for viewing."
":kbd:`Ctrl`:kbd:`S`", "Save the current document in the document editor."
":kbd:`Ctrl`:kbd:`V`", "Paste text from clipboard to cursor position."
":kbd:`Ctrl`:kbd:`W`", "Close the current document in the document editor."
":kbd:`Ctrl`:kbd:`X`", "Cut selected text to clipboard."
":kbd:`Ctrl`:kbd:`Y`", "Redo latest undo."
":kbd:`Ctrl`:kbd:`Z`", "Undo latest changes."
":kbd:`Ctrl`:kbd:`F7`", "Toggle spell checking."
":kbd:`Ctrl`:kbd:`F10`", "Toggle automatic updating of project outline."
":kbd:`Ctrl`:kbd:`Up`", "Move item one step up in the project tree."
":kbd:`Ctrl`:kbd:`Down`", "Move item one step down in the project tree."
":kbd:`Ctrl`:kbd:`Del`", "Delete next word in editor."
":kbd:`Ctrl`:kbd:`Backspace`", "Delete previous word in editor."
":kbd:`Ctrl`:kbd:`'`", "Wrap selected text, or word under cursor, in single quotes."
":kbd:`Ctrl`:kbd:`""`", "Wrap selected text, or word under cursor, in double quotes."
":kbd:`Ctrl`:kbd:`Enter`", "Open the tag or reference under the cursor in the Viewer."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`,`", "Open the :guilabel:`Project Settings` dialog."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`/`", "Remove block formatting for block under cursor."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`1`", "Replace occurrence of search word in current document, and search for next occurrence."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`A`", "Select all text in current paragraph."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`G`", "Find previous occurrence of search word in current document. (Same as :kbd:`Shift`:kbd:`F3`.)"
":kbd:`Ctrl`:kbd:`Shift`:kbd:`I`", "Import text to the current document from a text file."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`N`", "Create new folder."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`O`", "Open a project."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`R`", "Close the document viewer."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`S`", "Save the current project."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`W`", "Close the current project."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`Z`", "Undo move of project tree item."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`Del`", "If in the project tree, move a document to trash, or delete a folder."
":kbd:`F1`", "Open the documentation. This will either open the Qt Assistant, if available, or send you to the documentation website."
":kbd:`F2`", "If in the project tree, edit a document or folder settings. (Same as :kbd:`Ctrl`:kbd:`E`)"
":kbd:`F3`", "Find next occurrence of search word in current document. (Same as :kbd:`Ctrl`:kbd:`G`)"
":kbd:`F5`", "Open the :guilabel:`Build Novel Project` dialog."
":kbd:`F6`", "Open the :guilabel:`Writing Statistics` dialog."
":kbd:`F7`", "Re-run spell checker."
":kbd:`F8`", "Activate :guilabel:`Focus Mode`, hiding the project tree and document viewer."
":kbd:`F9`", "Re-build the project index."
":kbd:`F10`", "Re-build the project outline."
":kbd:`F11`", "Activate full screen mode."
":kbd:`Shift`:kbd:`F1`", "Open the online documentation in the system default browser."
":kbd:`Shift`:kbd:`F3`", "Find previous occurrence of search word in current document. (Same as :kbd:`Ctrl`:kbd:`Shift`:kbd:`G`.)"
":kbd:`Shift`:kbd:`F6`", "Open the :guilabel:`Project Details` dialog."
":kbd:`Return`", "If in the project tree, open a document for editing."
.. note::
On macOS, replace :kbd:`Ctrl` with :kbd:`Cmd`.
.. _a_ui_shortcuts_ins:
Insert Shortcuts
----------------
A set of insert features are also available through shortcuts, but they require a double
combination of key sequences. The insert feature is activated with :kbd:`Ctrl`:kbd:`K`, followed by
a key or key combination for the inserted content.
.. csv-table:: Keyboard Shortcuts
:header: "Shortcut", "Description"
:widths: 40, 60
:class: "tight-table"
":kbd:`Ctrl`:kbd:`K`, :kbd:`−`", "Insert a short dash (en dash)."
":kbd:`Ctrl`:kbd:`K`, :kbd:`_`", "Insert a long dash (em dash)."
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`_`", "Insert a horizontal bar (quotation dash)."
":kbd:`Ctrl`:kbd:`K`, :kbd:`~`", "Insert a figure dash (same width as a number)."
":kbd:`Ctrl`:kbd:`K`, :kbd:`1`", "Insert a left single quote."
":kbd:`Ctrl`:kbd:`K`, :kbd:`2`", "Insert a right single quote."
":kbd:`Ctrl`:kbd:`K`, :kbd:`3`", "Insert a left double quote."
":kbd:`Ctrl`:kbd:`K`, :kbd:`4`", "Insert a right double quote."
":kbd:`Ctrl`:kbd:`K`, :kbd:`'`", "Insert a modifier apostrophe."
":kbd:`Ctrl`:kbd:`K`, :kbd:`.`", "Insert an ellipsis."
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`'`", "Insert a prime."
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`""`", "Insert a double prime."
":kbd:`Ctrl`:kbd:`K`, :kbd:`Return`", "Insert a hard line break."
":kbd:`Ctrl`:kbd:`K`, :kbd:`Space`", "Insert a non-breaking space."
":kbd:`Ctrl`:kbd:`K`, :kbd:`Shift`:kbd:`Space`", "Insert a thin space."
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`Space`", "Insert a thin non-breaking space."
":kbd:`Ctrl`:kbd:`K`, :kbd:`*`", "Insert a list bullet."
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`−`", "Insert a hyphen bullet (alternative bullet)."
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`*`", "Insert a flower mark (alternative bullet)."
":kbd:`Ctrl`:kbd:`K`, :kbd:`%`", "Insert a per mille symbol."
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`O`", "Insert a degree symbol."
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`X`", "Insert a times sign."
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`D`", "Insert a division sign."
":kbd:`Ctrl`:kbd:`K`, :kbd:`G`", "Insert a ``@tag`` keyword."
":kbd:`Ctrl`:kbd:`K`, :kbd:`V`", "Insert a ``@pov`` keyword."
":kbd:`Ctrl`:kbd:`K`, :kbd:`F`", "Insert a ``@focus`` keyword."
":kbd:`Ctrl`:kbd:`K`, :kbd:`C`", "Insert a ``@char`` keyword."
":kbd:`Ctrl`:kbd:`K`, :kbd:`P`", "Insert a ``@plot`` keyword."
":kbd:`Ctrl`:kbd:`K`, :kbd:`T`", "Insert a ``@time`` keyword."
":kbd:`Ctrl`:kbd:`K`, :kbd:`L`", "Insert a ``@location`` keyword."
":kbd:`Ctrl`:kbd:`K`, :kbd:`O`", "Insert an ``@object`` keyword."
":kbd:`Ctrl`:kbd:`K`, :kbd:`E`", "Insert an ``@entity`` keyword."
":kbd:`Ctrl`:kbd:`K`, :kbd:`X`", "Insert a ``@custom`` keyword."

View File

@@ -0,0 +1,147 @@
.. _a_kb:
******************
Keyboard Shortcuts
******************
Most features in novelWriter are available as keyboard shortcuts.
.. _a_kb_main:
Main Shortcuts
==============
The main shorcuts are as follows:
.. csv-table:: Keyboard Shortcuts
:header: "Shortcut", "Description"
:widths: 30, 70
:class: "tight-table"
":kbd:`Alt`:kbd:`1`", "Switch focus to the project tree. On Windows, use :kbd:`Ctrl`:kbd:`Alt`:kbd:`1`."
":kbd:`Alt`:kbd:`2`", "Switch focus to document editor. On Windows, use :kbd:`Ctrl`:kbd:`Alt`:kbd:`2`."
":kbd:`Alt`:kbd:`3`", "Switch focus to document viewer. On Windows, use :kbd:`Ctrl`:kbd:`Alt`:kbd:`3`."
":kbd:`Alt`:kbd:`4`", "Switch focus to outline view. On Windows, use :kbd:`Ctrl`:kbd:`Alt`:kbd:`4`."
":kbd:`Alt`:kbd:`Left`", "Move backward in the view history of the document viewer."
":kbd:`Alt`:kbd:`Right`", "Move forward in the view history of the document viewer."
":kbd:`Ctrl`:kbd:`.`", "Open menu to correct word under cursor."
":kbd:`Ctrl`:kbd:`,`", "Open the :guilabel:`Preferences` dialog."
":kbd:`Ctrl`:kbd:`/`", "Toggle block format as comment."
":kbd:`Ctrl`:kbd:`0`", "Remove block formatting for block under cursor."
":kbd:`Ctrl`:kbd:`1`", "Change block format to header level 1."
":kbd:`Ctrl`:kbd:`2`", "Change block format to header level 2."
":kbd:`Ctrl`:kbd:`3`", "Change block format to header level 3."
":kbd:`Ctrl`:kbd:`4`", "Change block format to header level 4."
":kbd:`Ctrl`:kbd:`5`", "Change block alignment to left-aligned."
":kbd:`Ctrl`:kbd:`6`", "Change block alignment to centred."
":kbd:`Ctrl`:kbd:`7`", "Change block alignment to right-aligned."
":kbd:`Ctrl`:kbd:`8`", "Add a left margin to the block."
":kbd:`Ctrl`:kbd:`9`", "Add a right margin to the block."
":kbd:`Ctrl`:kbd:`A`", "Select all text in the document."
":kbd:`Ctrl`:kbd:`B`", "Format selected text, or word under cursor, with strong emphasis (bold)."
":kbd:`Ctrl`:kbd:`C`", "Copy selected text to clipboard."
":kbd:`Ctrl`:kbd:`D`", "Strikethrough selected text, or word under cursor."
":kbd:`Ctrl`:kbd:`E`", "If in the project tree, edit a document or folder settings. (Same as :kbd:`F2`.)"
":kbd:`Ctrl`:kbd:`F`", "Open the search bar and search for the selected word, if any is selected."
":kbd:`Ctrl`:kbd:`G`", "Find next occurrence of search word in current document. (Same as :kbd:`F3`.)"
":kbd:`Ctrl`:kbd:`H`", "Open the search and replace bar and search for the selected word, if any is selected. (On Mac, this is :kbd:`Cmd`:kbd:`=`.)"
":kbd:`Ctrl`:kbd:`I`", "Format selected text, or word under cursor, with emphasis (italic)."
":kbd:`Ctrl`:kbd:`K`", "Activate the insert commands. The commands are listed in :ref:`a_kb_ins`."
":kbd:`Ctrl`:kbd:`N`", "Create new document."
":kbd:`Ctrl`:kbd:`O`", "Open selected document."
":kbd:`Ctrl`:kbd:`Q`", "Exit novelWriter."
":kbd:`Ctrl`:kbd:`R`", "If in the project tree, open a document for viewing. If the editor has focus, open current document for viewing."
":kbd:`Ctrl`:kbd:`S`", "Save the current document in the document editor."
":kbd:`Ctrl`:kbd:`V`", "Paste text from clipboard to cursor position."
":kbd:`Ctrl`:kbd:`W`", "Close the current document in the document editor."
":kbd:`Ctrl`:kbd:`X`", "Cut selected text to clipboard."
":kbd:`Ctrl`:kbd:`Y`", "Redo latest undo."
":kbd:`Ctrl`:kbd:`Z`", "Undo latest changes."
":kbd:`Ctrl`:kbd:`F7`", "Toggle spell checking."
":kbd:`Ctrl`:kbd:`F10`", "Toggle automatic updating of project outline."
":kbd:`Ctrl`:kbd:`Up`", "Move item one step up in the project tree."
":kbd:`Ctrl`:kbd:`Down`", "Move item one step down in the project tree."
":kbd:`Ctrl`:kbd:`Del`", "Delete next word in editor."
":kbd:`Ctrl`:kbd:`Backspace`", "Delete previous word in editor."
":kbd:`Ctrl`:kbd:`'`", "Wrap selected text, or word under cursor, in single quotes."
":kbd:`Ctrl`:kbd:`""`", "Wrap selected text, or word under cursor, in double quotes."
":kbd:`Ctrl`:kbd:`Retrun`", "Open the tag or reference under the cursor in the Viewer."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`,`", "Open the :guilabel:`Project Settings` dialog."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`/`", "Remove block formatting for block under cursor."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`1`", "Replace occurrence of search word in current document, and search for next occurrence."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`A`", "Select all text in current paragraph."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`G`", "Find previous occurrence of search word in current document. (Same as :kbd:`Shift`:kbd:`F3`.)"
":kbd:`Ctrl`:kbd:`Shift`:kbd:`I`", "Import text to the current document from a text file."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`N`", "Create new folder."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`O`", "Open a project."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`R`", "Close the document viewer."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`S`", "Save the current project."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`W`", "Close the current project."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`Z`", "Undo move of project tree item."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`Del`", "If in the project tree, move a document to trash, or delete a folder."
":kbd:`F1`", "Open the documentation. This will either open the Qt Assistant, if available, or send you to the documentation website."
":kbd:`F2`", "If in the project tree, edit a document or folder settings. (Same as :kbd:`Ctrl`:kbd:`E`)"
":kbd:`F3`", "Find next occurrence of search word in current document. (Same as :kbd:`Ctrl`:kbd:`G`)"
":kbd:`F5`", "Open the :guilabel:`Build Novel Project` dialog."
":kbd:`F6`", "Open the :guilabel:`Writing Statistics` dialog."
":kbd:`F7`", "Re-run spell checker."
":kbd:`F8`", "Activate :guilabel:`Focus Mode`, hiding the project tree and document viewer."
":kbd:`F9`", "Re-build the project index."
":kbd:`F10`", "Re-build the project outline."
":kbd:`F11`", "Activate full screen mode."
":kbd:`Shift`:kbd:`F1`", "Open the online documentation in the system default browser."
":kbd:`Shift`:kbd:`F3`", "Find previous occurrence of search word in current document. (Same as :kbd:`Ctrl`:kbd:`Shift`:kbd:`G`.)"
":kbd:`Shift`:kbd:`F6`", "Open the :guilabel:`Project Details` dialog."
":kbd:`Return`", "If in the project tree, open a document for editing."
.. note::
On macOS, replace :kbd:`Ctrl` with :kbd:`Cmd`.
.. _a_kb_ins:
Insert Shortcuts
================
A set of insert features are also available through shortcuts, but they require a double
combination of key sequences. The insert feature is activated with :kbd:`Ctrl`:kbd:`K`, followed by
a key or key combination for the inserted content.
.. csv-table:: Keyboard Shortcuts
:header: "Shortcut", "Description"
:widths: 40, 60
:class: "tight-table"
":kbd:`Ctrl`:kbd:`K`, :kbd:`−`", "Insert a short dash (en dash)."
":kbd:`Ctrl`:kbd:`K`, :kbd:`_`", "Insert a long dash (em dash)."
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`_`", "Insert a horizontal bar (quotation dash)."
":kbd:`Ctrl`:kbd:`K`, :kbd:`~`", "Insert a figure dash (same width as a number)."
":kbd:`Ctrl`:kbd:`K`, :kbd:`1`", "Insert a left single quote."
":kbd:`Ctrl`:kbd:`K`, :kbd:`2`", "Insert a right single quote."
":kbd:`Ctrl`:kbd:`K`, :kbd:`3`", "Insert a left double quote."
":kbd:`Ctrl`:kbd:`K`, :kbd:`4`", "Insert a right double quote."
":kbd:`Ctrl`:kbd:`K`, :kbd:`'`", "Insert a modifier apostrophe."
":kbd:`Ctrl`:kbd:`K`, :kbd:`.`", "Insert an ellipsis."
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`'`", "Insert a prime."
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`""`", "Insert a double prime."
":kbd:`Ctrl`:kbd:`K`, :kbd:`Space`", "Insert a non-breaking space."
":kbd:`Ctrl`:kbd:`K`, :kbd:`Shift`:kbd:`Space`", "Insert a thin space."
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`Space`", "Insert a thin non-breaking space."
":kbd:`Ctrl`:kbd:`K`, :kbd:`*`", "Insert a list bullet."
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`−`", "Insert a hyphen bullet (alternative bullet)."
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`*`", "Insert a flower mark (alternative bullet)."
":kbd:`Ctrl`:kbd:`K`, :kbd:`%`", "Insert a per mille symbol."
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`O`", "Insert a degree symbol."
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`X`", "Insert a times sign."
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`D`", "Insert a division sign."
":kbd:`Ctrl`:kbd:`K`, :kbd:`G`", "Insert a ``@tag`` keyword."
":kbd:`Ctrl`:kbd:`K`, :kbd:`V`", "Insert a ``@pov`` keyword."
":kbd:`Ctrl`:kbd:`K`, :kbd:`F`", "Insert a ``@focus`` keyword."
":kbd:`Ctrl`:kbd:`K`, :kbd:`C`", "Insert a ``@char`` keyword."
":kbd:`Ctrl`:kbd:`K`, :kbd:`P`", "Insert a ``@plot`` keyword."
":kbd:`Ctrl`:kbd:`K`, :kbd:`T`", "Insert a ``@time`` keyword."
":kbd:`Ctrl`:kbd:`K`, :kbd:`L`", "Insert a ``@location`` keyword."
":kbd:`Ctrl`:kbd:`K`, :kbd:`O`", "Insert an ``@object`` keyword."
":kbd:`Ctrl`:kbd:`K`, :kbd:`E`", "Insert an ``@entity`` keyword."
":kbd:`Ctrl`:kbd:`K`, :kbd:`X`", "Insert a ``@custom`` keyword."

View File

@@ -7,7 +7,7 @@ Typographical Notes
novelWriter has some support for typographical symbols that are not usually easily available in
many text editors. This includes for instance the proper unicode quotation marks, dashes, ellipsis,
thin spaces, etc. All these symbols are available from the :guilabel:`Insert` menu, and via
keyboard shortcuts. See :ref:`a_ui_shortcuts_ins`.
keyboard shortcuts. See :ref:`a_kb_ins`.
This chapter provides some additional information on how novelWriter handles these symbols.

85
i18n/README.md Normal file
View File

@@ -0,0 +1,85 @@
# novelWriter Internationalisation
The `i18n` folder contains the translation files for the Qt5 GUI. There are two types of files
involved: the `nw_XX.ts` and the `nw_XX.qm` files. The `nw_XX.ts` files are located in the `i18n`
folder at the root of the repository, and the `nw_XX.qm` together with the `project_XX.json` files
are located in `nw/assets/i18n`. The latter are JSON translation maps for the novelWriter projects,
used by the Build Novel Project tool.
**Note**
When making a new translation, or updating an existing one, only commit the `nw_XX.ts` you have
made changes to. The `qtlupdate` command mentioned below will likely modify all `nw_XX.ts`
slightly, but please _don't_ commit those changes to the pull request.
## Qt GUI Localisation
The `.qm` files are the actual files used by the application to translate the GUI into another
language other than the default British English. The files are not generated by default. They can
be built with:
```bash
python3 setup.py qtlrelease
```
To add a new translation file, first add a new entry in the `novelWriter.pro` file under the
`TRANSLATIONS` variable. Please use the underscore syntax for file names as the importer expects
this format.
After adding the entry in the `.pro` file, run:
```bash
python3 setup.py qtlupdate
```
This will build a new `i18n/nw_XX.ts` file for the language you just added. The file can then be
edited with the Qt 5 Linguist application provided by Qt. This is by far easier than manually
editing the `.ts` file. Please select "English" and "United Kingdom" as the source langauge when
prompted by Qt 5 Linguist.
When you're done editing, you can build the `nw/assets/i18n/nw_XX.qm` file and test it in
novelWriter. The Preferences dialog should list the newly added language, and you can select it and
test it.
Please do not submit the `.qm` files to the repository. Only the `.ts` file you just added in the
`i18n` folder is needed.
**Note**
These commands require that you have the tools Qt 5 Linguist and the PyQt5 tool `pylupdate5`
installed on your system.
For Ubuntu/Debian, run:
```bash
sudo apt install qttools5-dev-tools pyqt5-dev-tools
```
### Missing QtBase Translations
The default Qt dialogs also have translations, for instance for standard buttons like "Yes", "No",
"Ok", "Cancel", etc. Generally, these translations files are installed with the Qt libraries on
your system, and novelWriter will collect those translations from there. However, these
translations are missing for many languages.
As a starting point, there is no need to translate any entries in the `.ts` files that are under
elements starting with the letter "Q", like "QPlatformTheme", "QWizard", etc. If these turn up in
English in novelWriter after activating a translation, it means they are probably missing in the Qt
library, and you may also need to translate these.
These additional translation entries are generated from a file named `i18n/qtbase.py`, which is not
a file that novelWriter uses. It is there only to generate these additional entries for the `.ts`
files.
## Project Localisation
Projects can have a different language setting than the GUI itself. The files with format
`project_XX.json` in `nw/assets/i18n` are simple translation maps for text that goes into the
exported documents generated by the Build Novel Project tool.
The files are loaded based on the language setting in the build tool. At the present time, no other
parts of the application use these files. Since these are used as replacement lookups, they are
maintained as plain JSON files. The main usage is to generate chapter headers as number words.
Adding new translations for these files is easy. Just copy the `project_en.json` file, rename it to
the appropriate language coded filename, and edit it. Please use the underscore as separator if a
language needs to be localised to a specific country under a language. The importer will for
instance look for `en_US` first, then use `en` if it is not found. If there are no matching files,
the project falls back to using English.

5023
i18n/nw_en_US.ts Normal file

File diff suppressed because it is too large Load Diff

5024
i18n/nw_fr.ts Normal file

File diff suppressed because it is too large Load Diff

5023
i18n/nw_nb_NO.ts Normal file

File diff suppressed because it is too large Load Diff

4943
i18n/nw_pt.ts Normal file

File diff suppressed because it is too large Load Diff

5025
i18n/nw_zh_CN.ts Normal file

File diff suppressed because it is too large Load Diff

73
i18n/qtbase.py Normal file
View File

@@ -0,0 +1,73 @@
"""
Qt Base Translation File
========================
This file causes Qt Linguist to generate translation entries for the Qt
elements that need translation in novelWriter for those languages who do
not yet have a qtbase_xx.qm file shipped with Qt.
If a qtbase_xx.qm file already exists, do not add a translation for the
entries generated from this file.
"""
from PyQt5.QtCore import QT_TRANSLATE_NOOP
# QDialogButtonBox
# ================
QT_TRANSLATE_NOOP("QDialogButtonBox", "OK")
# QGnomeTheme
# ===========
QT_TRANSLATE_NOOP("QGnomeTheme", "&OK")
QT_TRANSLATE_NOOP("QGnomeTheme", "&Save")
QT_TRANSLATE_NOOP("QGnomeTheme", "&Cancel")
QT_TRANSLATE_NOOP("QGnomeTheme", "&Close")
QT_TRANSLATE_NOOP("QGnomeTheme", "Close without Saving")
# QGuiApplication
# ===============
QT_TRANSLATE_NOOP("QGuiApplication", (
"Translate this string to the string 'LTR' in left-to-right languages or to "
"'RTL' in right-to-left languages (such as Hebrew and Arabic) to get proper widget layout."
))
# QPlatformTheme
# ==============
QT_TRANSLATE_NOOP("QPlatformTheme", "OK")
QT_TRANSLATE_NOOP("QPlatformTheme", "Save")
QT_TRANSLATE_NOOP("QPlatformTheme", "Save All")
QT_TRANSLATE_NOOP("QPlatformTheme", "Open")
QT_TRANSLATE_NOOP("QPlatformTheme", "&Yes")
QT_TRANSLATE_NOOP("QPlatformTheme", "Yes to &All")
QT_TRANSLATE_NOOP("QPlatformTheme", "&No")
QT_TRANSLATE_NOOP("QPlatformTheme", "N&o to All")
QT_TRANSLATE_NOOP("QPlatformTheme", "Abort")
QT_TRANSLATE_NOOP("QPlatformTheme", "Retry")
QT_TRANSLATE_NOOP("QPlatformTheme", "Ignore")
QT_TRANSLATE_NOOP("QPlatformTheme", "Close")
QT_TRANSLATE_NOOP("QPlatformTheme", "Cancel")
QT_TRANSLATE_NOOP("QPlatformTheme", "Discard")
QT_TRANSLATE_NOOP("QPlatformTheme", "Help")
QT_TRANSLATE_NOOP("QPlatformTheme", "Apply")
QT_TRANSLATE_NOOP("QPlatformTheme", "Reset")
QT_TRANSLATE_NOOP("QPlatformTheme", "Restore Defaults")
# QWizard
# =======
QT_TRANSLATE_NOOP("QWizard", "Go Back")
QT_TRANSLATE_NOOP("QWizard", "< &Back")
QT_TRANSLATE_NOOP("QWizard", "Continue")
QT_TRANSLATE_NOOP("QWizard", "&Next")
QT_TRANSLATE_NOOP("QWizard", "&Next >")
QT_TRANSLATE_NOOP("QWizard", "Commit")
QT_TRANSLATE_NOOP("QWizard", "Done")
QT_TRANSLATE_NOOP("QWizard", "&Finish")
QT_TRANSLATE_NOOP("QWizard", "Cancel")
QT_TRANSLATE_NOOP("QWizard", "Cancel")
QT_TRANSLATE_NOOP("QWizard", "Help")
QT_TRANSLATE_NOOP("QWizard", "&Help")

39
novelWriter.pro Normal file
View File

@@ -0,0 +1,39 @@
SOURCES += \
i18n/qtbase.py \
nw/core/project.py \
nw/core/tokenizer.py \
nw/dialogs/about.py \
nw/dialogs/docmerge.py \
nw/dialogs/docsplit.py \
nw/dialogs/itemeditor.py \
nw/dialogs/preferences.py \
nw/dialogs/projload.py \
nw/dialogs/projsettings.py \
nw/dialogs/wordlist.py \
nw/gui/custom.py \
nw/gui/doceditor.py \
nw/gui/docviewer.py \
nw/gui/itemdetails.py \
nw/gui/mainmenu.py \
nw/gui/noveltree.py \
nw/gui/outline.py \
nw/gui/outlinedetails.py \
nw/gui/projdetails.py \
nw/gui/projtree.py \
nw/gui/statusbar.py \
nw/gui/theme.py \
nw/tools/build.py \
nw/tools/projwizard.py \
nw/tools/writingstats.py \
nw/common.py \
nw/constants.py \
nw/error.py \
nw/guimain.py
TRANSLATIONS += \
i18n/nw_en_US.ts \
i18n/nw_fr.ts \
i18n/nw_nb_NO.ts \
i18n/nw_pt.ts \
i18n/nw_zh_CN.ts

View File

@@ -1,5 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
novelWriter – Start Script
==========================
@@ -8,9 +7,9 @@ import os
import sys
try:
import PyQt5.QtWidgets # noqa: F401
import PyQt5.QtGui # noqa: F401
import PyQt5.QtCore # noqa: F401
import PyQt5.QtWidgets # noqa: F401
import PyQt5.QtGui # noqa: F401
import PyQt5.QtCore # noqa: F401
except Exception:
print("ERROR: Failed to load dependency PyQt5")
sys.exit(1)

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – Init File
=======================
@@ -40,9 +39,9 @@ from nw.config import Config
# Generally follows PEP 440
# Hex Version:
# - Digit 1,2 : Major Version (01-ff)
# = Digit 3,4 : Minor Version (01-ff)
# - Digit 3,4 : Minor Version (01-ff)
# - Digit 5,6 : Patch Version (01-ff)
# = Digit 7 : Release Type (a: aplha, b: beta, c: candidate, f: final)
# - Digit 7 : Release Type (a: aplha, b: beta, c: candidate, f: final)
# - Digit 8 : Release Number (0-f)
#
# Example : Full Short Description
@@ -62,9 +61,9 @@ __license__ = "GPLv3"
__author__ = "Veronica Berglyd Olsen"
__maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net"
__version__ = "1.2"
__hexversion__ = "0x010200f0"
__date__ = "2021-03-14"
__version__ = "1.4"
__hexversion__ = "0x010400f0"
__date__ = "2021-07-27"
__status__ = "Stable"
__domain__ = "novelwriter.io"
__url__ = "https://novelwriter.io"
@@ -73,10 +72,6 @@ __issuesurl__ = "https://github.com/vkbo/novelWriter/issues"
__helpurl__ = "https://github.com/vkbo/novelWriter/discussions"
__releaseurl__ = "https://github.com/vkbo/novelWriter/releases/latest"
__docurl__ = "https://novelwriter.readthedocs.io"
__credits__ = [
"Veronica Berglyd Olsen (developer)",
"Marian Lückhof (contributor, tester)"
]
##
# Logging
@@ -94,15 +89,19 @@ __credits__ = [
# Add verbose logging level
VERBOSE = 5
logging.addLevelName(VERBOSE, "VERBOSE")
def logVerbose(self, message, *args, **kws):
if self.isEnabledFor(VERBOSE):
self._log(VERBOSE, message, args, **kws)
logging.Logger.verbose = logVerbose
# Initiating logging
logger = logging.getLogger(__name__)
##
# Main Program
##
@@ -110,8 +109,9 @@ logger = logging.getLogger(__name__)
# Load the main config as a global object
CONFIG = Config()
def main(sysArgs=None):
"""Parses command line, sets up logging, and launches main GUI.
"""Parse command line, set up logging, and launches main GUI.
"""
if sysArgs is None:
sysArgs = sys.argv[1:]
@@ -137,7 +137,7 @@ def main(sysArgs=None):
"This program is distributed in the hope that it will be useful,\n"
"but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n"
"GNU General Public License for more details.\n"
"GNU General Public Licence for more details.\n"
"\n"
"Usage:\n"
" -h, --help Print this message.\n"
@@ -149,19 +149,19 @@ def main(sysArgs=None):
" --config= Alternative config file.\n"
" --data= Alternative user data path.\n"
).format(
version = __version__,
copyright = __copyright__,
date = __date__,
version=__version__,
copyright=__copyright__,
date=__date__,
)
# Defaults
logLevel = logging.WARN
logLevel = logging.WARN
logFormat = "{levelname:8} {message:}"
confPath = None
dataPath = None
testMode = False
qtStyle = "Fusion"
cmdOpen = None
confPath = None
dataPath = None
testMode = False
qtStyle = "Fusion"
cmdOpen = None
# Parse Options
try:
@@ -209,9 +209,7 @@ def main(sysArgs=None):
pkgLogger.addHandler(cHandle)
pkgLogger.setLevel(logLevel)
logger.info("Starting novelWriter %s (%s) %s" % (
__version__, __hexversion__, __date__
))
logger.info("Starting novelWriter %s (%s) %s", __version__, __hexversion__, __date__)
# Check Packages and Versions
errorData = []
@@ -221,19 +219,19 @@ def main(sysArgs=None):
"At least Python 3.6.0 is required, found %s." % CONFIG.verPyString
)
errorCode |= 4
if CONFIG.verQtValue < 50200:
if CONFIG.verQtValue < 50300:
errorData.append(
"At least Qt5 version 5.2 is required, found %s." % CONFIG.verQtString
"At least Qt5 version 5.3 is required, found %s." % CONFIG.verQtString
)
errorCode |= 8
if CONFIG.verPyQtValue < 50200:
if CONFIG.verPyQtValue < 50300:
errorData.append(
"At least PyQt5 version 5.2 is required, found %s." % CONFIG.verPyQtString
"At least PyQt5 version 5.3 is required, found %s." % CONFIG.verPyQtString
)
errorCode |= 16
try:
import lxml # noqa: F401
import lxml # noqa: F401
except ImportError:
errorData.append("Python module 'lxml' is missing.")
errorCode |= 32
@@ -285,6 +283,7 @@ def main(sysArgs=None):
sys.excepthook = exceptionHandler
# Launch main GUI
CONFIG.initLocalisation(nwApp)
nwGUI = GuiMain()
if not nwGUI.hasProject:
nwGUI.showProjectLoadDialog()

View File

@@ -0,0 +1,105 @@
{
"Synopsis": "Synopsis",
"Comment": "Kommentar",
"Notes": "Notizen",
"0": "Null",
"1": "Eins",
"2": "Zwei",
"3": "Drei",
"4": "Vier",
"5": "Fünf",
"6": "Sechs",
"7": "Sieben",
"8": "Acht",
"9": "Neun",
"10": "Zehn",
"11": "Elf",
"12": "Zwölf",
"13": "Dreizehen",
"14": "Vierzehn",
"15": "Fünfzehn",
"16": "Sechzehn",
"17": "Siebzehn",
"18": "Achtzehn",
"19": "Neunzehn",
"20": "Zwanzig",
"21": "Einundzwanzig",
"22": "Zweiundzwanzig",
"23": "Dreiundzwanzig",
"24": "Vierundzwanzig",
"25": "Fünfundzwanzig",
"26": "Sechsundzwanzig",
"27": "Siebenundzwanzig",
"28": "Achtundzwanzig",
"29": "Neunundzwanzig",
"30": "Dreißig",
"31": "Einunddreißig",
"32": "Zweiunddreißig",
"33": "Dreiunddreißig",
"34": "Vierunddreißig",
"35": "Fünfunddreißig",
"36": "Sechsunddreißig",
"37": "Siebenunddreißig",
"38": "Achtunddreißig",
"39": "Neununddreißig",
"40": "Vierzig",
"41": "Einundvierzig",
"42": "Zweiundvierzig",
"43": "Dreiundvierzig",
"44": "Vierundvierzig",
"45": "Fünfundvierzig",
"46": "Sechsundvierzig",
"47": "Siebenundvierzig",
"48": "Achtundvierzig",
"49": "Neunundvierzig",
"50": "Fünfzig",
"51": "Einundfünfzig",
"52": "Zweiundfünfzig",
"53": "Dreiundfünfzig",
"54": "Vierundfünfzig",
"55": "Fünfundfünfzig",
"56": "Sechsundfünfzig",
"57": "Siebenundfünfzig",
"58": "Achtundfünfzig",
"59": "Neunundfünfzig",
"60": "Sechzig",
"61": "Einundsechzig",
"62": "Zweiundsechzig",
"63": "Dreiundsechzig",
"64": "Vierundsechzig",
"65": "Fünfundsechzig",
"66": "Sechsundsechzig",
"67": "Siebenundsechzig",
"68": "Achtundsechzig",
"69": "Neunundsechzig",
"70": "Siebzig",
"71": "Einundsiebzig",
"72": "Zweiundsiebzig",
"73": "Dreiundsiebzig",
"74": "Vierundsiebzig",
"75": "Fünfundsiebzig",
"76": "Sechsundsiebzig",
"77": "Siebenundsiebzig",
"78": "Achtundsiebzig",
"79": "Neunundsiebzig",
"80": "Achtzig",
"81": "Einundachtzig",
"82": "Zweiundachtzig",
"83": "Dreiundachtzig",
"84": "Vierundachtzig",
"85": "Fünfundachtzig",
"86": "Sechsundachtzig",
"87": "Siebenundachtzig",
"88": "Achtundachtzig",
"89": "Neunundachtzig",
"90": "Neunzig",
"91": "Einundneunzig",
"92": "Zweiundneunzig",
"93": "Dreiundneunzig",
"94": "Vierundneunzig",
"95": "Fünfundneunzig",
"96": "Sechsundneunzig",
"97": "Siebenundneunzig",
"98": "Achtundneunzig",
"99": "Neunundneunzig"
}

View File

@@ -0,0 +1,105 @@
{
"Synopsis": "Synopsis",
"Comment": "Comment",
"Notes": "Notes",
"0": "Zero",
"1": "One",
"2": "Two",
"3": "Three",
"4": "Four",
"5": "Five",
"6": "Six",
"7": "Seven",
"8": "Eight",
"9": "Nine",
"10": "Ten",
"11": "Eleven",
"12": "Twelve",
"13": "Thirteen",
"14": "Fourteen",
"15": "Fifteen",
"16": "Sixteen",
"17": "Seventeen",
"18": "Eighteen",
"19": "Nineteen",
"20": "Twenty",
"21": "Twenty-One",
"22": "Twenty-Two",
"23": "Twenty-Three",
"24": "Twenty-Four",
"25": "Twenty-Five",
"26": "Twenty-Six",
"27": "Twenty-Seven",
"28": "Twenty-Eight",
"29": "Twenty-Nine",
"30": "Thirty",
"31": "Thirty-One",
"32": "Thirty-Two",
"33": "Thirty-Three",
"34": "Thirty-Four",
"35": "Thirty-Five",
"36": "Thirty-Six",
"37": "Thirty-Seven",
"38": "Thirty-Eight",
"39": "Thirty-Nine",
"40": "Forty",
"41": "Forty-One",
"42": "Forty-Two",
"43": "Forty-Three",
"44": "Forty-Four",
"45": "Forty-Five",
"46": "Forty-Six",
"47": "Forty-Seven",
"48": "Forty-Eight",
"49": "Forty-Nine",
"50": "Fifty",
"51": "Fifty-One",
"52": "Fifty-Two",
"53": "Fifty-Three",
"54": "Fifty-Four",
"55": "Fifty-Five",
"56": "Fifty-Six",
"57": "Fifty-Seven",
"58": "Fifty-Eight",
"59": "Fifty-Nine",
"60": "Sixty",
"61": "Sixty-One",
"62": "Sixty-Two",
"63": "Sixty-Three",
"64": "Sixty-Four",
"65": "Sixty-Five",
"66": "Sixty-Six",
"67": "Sixty-Seven",
"68": "Sixty-Eight",
"69": "Sixty-Nine",
"70": "Seventy",
"71": "Seventy-One",
"72": "Seventy-Two",
"73": "Seventy-Three",
"74": "Seventy-Four",
"75": "Seventy-Five",
"76": "Seventy-Six",
"77": "Seventy-Seven",
"78": "Seventy-Eight",
"79": "Seventy-Nine",
"80": "Eighty",
"81": "Eighty-One",
"82": "Eighty-Two",
"83": "Eighty-Three",
"84": "Eighty-Four",
"85": "Eighty-Five",
"86": "Eighty-Six",
"87": "Eighty-Seven",
"88": "Eighty-Eight",
"89": "Eighty-Nine",
"90": "Ninety",
"91": "Ninety-One",
"92": "Ninety-Two",
"93": "Ninety-Three",
"94": "Ninety-Four",
"95": "Ninety-Five",
"96": "Ninety-Six",
"97": "Ninety-Seven",
"98": "Ninety-Eight",
"99": "Ninety-Nine"
}

View File

@@ -0,0 +1,105 @@
{
"Synopsis": "Synopsis",
"Comment": "Commentaire",
"Notes": "Notes",
"0": "Zero",
"1": "Un",
"2": "Deux",
"3": "Trois",
"4": "Quatre",
"5": "Cinq",
"6": "Six",
"7": "Sept",
"8": "Huit",
"9": "Neuf",
"10": "Dix",
"11": "Onze",
"12": "Douze",
"13": "Treize",
"14": "Quatorze",
"15": "Quinze",
"16": "Seize",
"17": "Dix-sept",
"18": "Dix-huit",
"19": "Dix-neuf",
"20": "Vingt",
"21": "Vingt et un",
"22": "Vingt-deux",
"23": "Vingt-trois",
"24": "Vingt-quatre",
"25": "Vingt-cinq",
"26": "Vingt-six",
"27": "Vingt-sept",
"28": "Vingt-huit",
"29": "Vingt-neuf",
"30": "Trente",
"31": "Trente et un",
"32": "Trente-deux",
"33": "Trente-trois",
"34": "Trente-quatre",
"35": "Trente-cinq",
"36": "Trente-six",
"37": "Trente-sept",
"38": "Trente-huit",
"39": "Trente-neuf",
"40": "Quarante",
"41": "Quarante et un",
"42": "Quarante-deux",
"43": "Quarante-trois",
"44": "Quarante-quatre",
"45": "Quarante-cinq",
"46": "Quarante-six",
"47": "Quarante-sept",
"48": "Quarante-huit",
"49": "Quarante-neuf",
"50": "Cinquante",
"51": "Cinquante et un",
"52": "Cinquante-deux",
"53": "Cinquante-trois",
"54": "Cinquante-quatre",
"55": "Cinquante-cinq",
"56": "Cinquante-six",
"57": "Cinquante-sept",
"58": "Cinquante-huit",
"59": "Cinquante-neuf",
"60": "Soixante",
"61": "Soixante et un",
"62": "Soixante-deux",
"63": "Soixante-trois",
"64": "Soixante-quatre",
"65": "Soixante-cinq",
"66": "Soixante-six",
"67": "Soixante-sept",
"68": "Soixante-huit",
"69": "Soixante-neuf",
"70": "Soixante-dix",
"71": "Soixante et onze",
"72": "Soixante-douze",
"73": "Soixante-treize",
"74": "Soixante-quatorze",
"75": "Soixante-quinze",
"76": "Soixante-seize",
"77": "Soixante-dix-sept",
"78": "Soixante-dix-huit",
"79": "Soixante-dix-neuf",
"80": "Quatre-vingts",
"81": "Quatre-vingt-un",
"82": "Quatre-vingt-deux",
"83": "Quatre-vingt-trois",
"84": "Quatre-vingt-quatre",
"85": "Quatre-vingt-cinq",
"86": "Quatre-vingt-six",
"87": "Quatre-vingt-sept",
"88": "Quatre-vingt-huit",
"89": "Quatre-vingt-neuf",
"90": "Quatre-vingt-dix",
"91": "Quatre-vingt-onze",
"92": "Quatre-vingt-douze",
"93": "Quatre-vingt-treize",
"94": "Quatre-vingt-quatorze",
"95": "Quatre-vingt-quinze",
"96": "Quatre-vingt-seize",
"97": "Quatre-vingt-dix-sept",
"98": "Quatre-vingt-dix-huit",
"99": "Quatre-vingt-dix-neuf"
}

View File

@@ -0,0 +1,105 @@
{
"Synopsis": "Sammendrag",
"Comment": "Kommentar",
"Notes": "Notat",
"0": "null",
"1": "én",
"2": "to",
"3": "tre",
"4": "fire",
"5": "fem",
"6": "seks",
"7": "sju",
"8": "åtte",
"9": "ni",
"10": "ti",
"11": "elleve",
"12": "tolv",
"13": "tretten",
"14": "fjorten",
"15": "femten",
"16": "seksten",
"17": "sytten",
"18": "atten",
"19": "nitten",
"20": "tjue",
"21": "tjueén",
"22": "tjueto",
"23": "tjuetre",
"24": "tjuefire",
"25": "tjuefem",
"26": "tjueseks",
"27": "tjuesju",
"28": "tjueåtte",
"29": "tjueni",
"30": "tretti",
"31": "trettién",
"32": "trettito",
"33": "trettitre",
"34": "trettifire",
"35": "trettifem",
"36": "trettiseks",
"37": "trettisju",
"38": "trettiåtte",
"39": "trettini",
"40": "førti",
"41": "førtién",
"42": "førtito",
"43": "førtitre",
"44": "førtifire",
"45": "førtifem",
"46": "førtiseks",
"47": "førtisju",
"48": "førtiåtte",
"49": "førtini",
"50": "femti",
"51": "femtién",
"52": "femtito",
"53": "femtitre",
"54": "femtifire",
"55": "femtifem",
"56": "femtiseks",
"57": "femtisju",
"58": "femtiåtte",
"59": "femtini",
"60": "seksti",
"61": "sekstién",
"62": "sekstito",
"63": "sekstitre",
"64": "sekstifire",
"65": "sekstifem",
"66": "sekstiseks",
"67": "sekstisju",
"68": "sekstiåtte",
"69": "sekstini",
"70": "sytti",
"71": "syttién",
"72": "syttito",
"73": "syttitre",
"74": "syttifire",
"75": "syttifem",
"76": "syttiseks",
"77": "syttisju",
"78": "syttiåtte",
"79": "syttini",
"80": "åtti",
"81": "åttién",
"82": "åttito",
"83": "åttitre",
"84": "åttifire",
"85": "åttifem",
"86": "åttiseks",
"87": "åttisju",
"88": "åttiåtte",
"89": "åttini",
"90": "nitti",
"91": "nittién",
"92": "nittito",
"93": "nittitre",
"94": "nittifire",
"95": "nittifem",
"96": "nittiseks",
"97": "nittisju",
"98": "nittiåtte",
"99": "nittini"
}

View File

@@ -0,0 +1,105 @@
{
"Synopsis": "Samandrag",
"Comment": "Kommentar",
"Notes": "Notat",
"0": "null",
"1": "ein",
"2": "to",
"3": "tre",
"4": "fire",
"5": "fem",
"6": "seks",
"7": "sju",
"8": "åtte",
"9": "ni",
"10": "ti",
"11": "elleve",
"12": "tolv",
"13": "tretten",
"14": "fjorten",
"15": "femten",
"16": "seksten",
"17": "sytten",
"18": "atten",
"19": "nitten",
"20": "tjue",
"21": "tjueein",
"22": "tjueto",
"23": "tjuetre",
"24": "tjuefire",
"25": "tjuefem",
"26": "tjueseks",
"27": "tjuesju",
"28": "tjueåtte",
"29": "tjueni",
"30": "tretti",
"31": "trettiein",
"32": "trettito",
"33": "trettitre",
"34": "trettifire",
"35": "trettifem",
"36": "trettiseks",
"37": "trettisju",
"38": "trettiåtte",
"39": "trettini",
"40": "førti",
"41": "førtiein",
"42": "førtito",
"43": "førtitre",
"44": "førtifire",
"45": "førtifem",
"46": "førtiseks",
"47": "førtisju",
"48": "førtiåtte",
"49": "førtini",
"50": "femti",
"51": "femtiein",
"52": "femtito",
"53": "femtitre",
"54": "femtifire",
"55": "femtifem",
"56": "femtiseks",
"57": "femtisju",
"58": "femtiåtte",
"59": "femtini",
"60": "seksti",
"61": "sekstiein",
"62": "sekstito",
"63": "sekstitre",
"64": "sekstifire",
"65": "sekstifem",
"66": "sekstiseks",
"67": "sekstisju",
"68": "sekstiåtte",
"69": "sekstini",
"70": "sytti",
"71": "syttiein",
"72": "syttito",
"73": "syttitre",
"74": "syttifire",
"75": "syttifem",
"76": "syttiseks",
"77": "syttisju",
"78": "syttiåtte",
"79": "syttini",
"80": "åtti",
"81": "åttiein",
"82": "åttito",
"83": "åttitre",
"84": "åttifire",
"85": "åttifem",
"86": "åttiseks",
"87": "åttisju",
"88": "åttiåtte",
"89": "åttini",
"90": "nitti",
"91": "nittiein",
"92": "nittito",
"93": "nittitre",
"94": "nittifire",
"95": "nittifem",
"96": "nittiseks",
"97": "nittisju",
"98": "nittiåtte",
"99": "nittini"
}

View File

@@ -0,0 +1,105 @@
{
"Synopsis": "Sinopse",
"Comment": "Comentários",
"Notes": "Notas",
"0": "Zero",
"1": "Um",
"2": "Dois",
"3": "Três",
"4": "Quatro",
"5": "Cinco",
"6": "Seis",
"7": "Sete",
"8": "Oito",
"9": "Nove",
"10": "Dez",
"11": "Onze",
"12": "Doze",
"13": "Treze",
"14": "Quatorze",
"15": "Quinze",
"16": "Dezesseis",
"17": "Dezesete",
"18": "Dezoito",
"19": "Dezenove",
"20": "Vinte",
"21": "Vinte e Um",
"22": "Vinte e Dois",
"23": "Vinte e Três",
"24": "Vinte e Quatro",
"25": "Vinte e Cinco",
"26": "Vinte e Seis",
"27": "Vinte e Sete",
"28": "Vinte e Oito",
"29": "Vinte e Nove",
"30": "Trinta",
"31": "Trinta e Um",
"32": "Trinta e Dois",
"33": "Trinta e Três",
"34": "Trinta e Quatro",
"35": "Trinta e Cinco",
"36": "Trinta e Seis",
"37": "Trinta e Sete",
"38": "Trinta e Oito",
"39": "Trinta e Nove",
"40": "Quarenta",
"41": "Quarenta e Um",
"42": "Quarenta e Dois",
"43": "Quarenta e Três",
"44": "Quarenta e Quatro",
"45": "Quarenta e Cinco",
"46": "Quarenta e Seis",
"47": "Quarenta e Sete",
"48": "Quarenta e Oito",
"49": "Quarenta e Nove",
"50": "Cinquenta",
"51": "Cinquenta e Um",
"52": "Cinquenta e Dois",
"53": "Cinquenta e Três",
"54": "Cinquenta e Quatro",
"55": "Cinquenta e Cinco",
"56": "Cinquenta e Seis",
"57": "Cinquenta e Sete",
"58": "Cinquenta e Oito",
"59": "Cinquenta e Nove",
"60": "Sessenta",
"61": "Sessenta e Um",
"62": "Sessenta e Dois",
"63": "Sessenta e Três",
"64": "Sessenta e Quatro",
"65": "Sessenta e Cinco",
"66": "Sessenta e Seis",
"67": "Sessenta e Sete",
"68": "Sessenta e Oito",
"69": "Sessenta e Nove",
"70": "Setenta",
"71": "Setenta e Um",
"72": "Setenta e Dois",
"73": "Setenta e Três",
"74": "Setenta e Quatro",
"75": "Setenta e Cinco",
"76": "Setenta e Seis",
"77": "Setenta e Sete",
"78": "Setenta e Oito",
"79": "Setenta e Nove",
"80": "Oitenta",
"81": "Oitenta e Um",
"82": "Oitenta e Dois",
"83": "Oitenta e Três",
"84": "Oitenta e Quatro",
"85": "Oitenta e Cinco",
"86": "Oitenta e Seis",
"87": "Oitenta e Sete",
"88": "Oitenta e Oito",
"89": "Oitenta e Nove",
"90": "Noventa",
"91": "Noventa e Um",
"92": "Noventa e Dois",
"93": "Noventa e Três",
"94": "Noventa e Quatro",
"95": "Noventa e Cinco",
"96": "Noventa e Seis",
"97": "Noventa e Sete",
"98": "Noventa e Oito",
"99": "Noventa e Nove"
}

View File

@@ -0,0 +1,105 @@
{
"Synopsis": "概要",
"Comment": "注释",
"Notes": "笔记",
"0": "零",
"1": "一",
"2": "二",
"3": "三",
"4": "四",
"5": "五",
"6": "六",
"7": "七",
"8": "八",
"9": "九",
"10": "十",
"11": "十一",
"12": "十二",
"13": "十三",
"14": "十四",
"15": "十五",
"16": "十六",
"17": "十七",
"18": "十八",
"19": "十九",
"20": "二十",
"21": "二十一",
"22": "二十二",
"23": "二十三",
"24": "二十四",
"25": "二十五",
"26": "二十六",
"27": "二十七",
"28": "二十八",
"29": "二十九",
"30": "三十",
"31": "三十一",
"32": "三十二",
"33": "三十三",
"34": "三十四",
"35": "三十五",
"36": "三十六",
"37": "三十七",
"38": "三十八",
"39": "三十九",
"40": "四十",
"41": "四十一",
"42": "四十二",
"43": "四十三",
"44": "四十四",
"45": "四十五",
"46": "四十六",
"47": "四十七",
"48": "四十八",
"49": "四十九",
"50": "五十",
"51": "五十一",
"52": "五十二",
"53": "五十三",
"54": "五十四",
"55": "五十五",
"56": "五十六",
"57": "五十七",
"58": "五十八",
"59": "五十九",
"60": "六十",
"61": "六十一",
"62": "六十二",
"63": "六十三",
"64": "六十四",
"65": "六十五",
"66": "六十六",
"67": "六十七",
"68": "六十八",
"69": "六十九",
"70": "七十",
"71": "七十一",
"72": "七十二",
"73": "七十三",
"74": "七十四",
"75": "七十五",
"76": "七十六",
"77": "七十七",
"78": "七十八",
"79": "七十九",
"80": "八十",
"81": "八十一",
"82": "八十二",
"83": "八十三",
"84": "八十四",
"85": "八十五",
"86": "八十六",
"87": "八十七",
"88": "八十八",
"89": "八十九",
"90": "九十",
"91": "九十一",
"92": "九十二",
"93": "九十三",
"94": "九十四",
"95": "九十五",
"96": "九十六",
"97": "九十七",
"98": "九十八",
"99": "九十九",
}

View File

@@ -2,82 +2,50 @@
<html>
<body>
<h2>Release Notes for 1.2</h2>
<p><i>Released on 14 March 2021</i></p>
<p>This release is mainly focused on the Build Novel Project tool. Completely new export classes
have been written to support Open Document and Markdown exports. In addition, the way document
layouts are handled have been automated a little to assist the user in keeping header levels and
document layout flags in sync. The third new additoion is the ability to record and log idle time
during a writing session to improve the writing statistics information as requested by several
users. Finally, it is now possible to directly edit the project dictionary via a new, simple GUI
dialog.</p>
<h2>Release Notes for 1.4</h2>
<p><i>Released on 27 July 2021</i></p>
<h3>The Build Novel Project Tool</h3>
<p>The main changes for this release are to the Build Novel Project tool. The Open Document export,
as well as the Markdown export, is now handled entirely by code written for novelWriter.
Previously, these export features depended on the underlying Qt library's save routines connected
to the preview document shown in the build dialog. Using this method of export both meant that the
content of the document was dependent on the preview being generated first, and it also meant that
the exported document had limited support for novelWriter-specific features and custom formatting.
The new export class should generate a much better result, especially for the Open Document
formats. The Open Document standard is supported by Open Office, Libre Office, Google Docs,
Microsoft Word, and probably a number of other applications too. The Markdown export hasn't changed
a lot, but should be a slight improvement on the previous export feature.</p>
<p>These changes to the build tool also imply that the saving process is now independent of the
content of the preview window, meaning you don't have to rebuild the preview before saving, which
was previously the case. To make this more consistent, the PDF export option has been moved to the
print button as it is actually a print-to-file feature under the hood, not technically a proper PDF
export format. It is exactly the same as printing to file from the print preview dialog.</p>
<p>In addition to the changes to the export features, the Build Novel Project tool now also has
controls for line height, which applies to all rich text export formats, and the option to replace
unicode characters with appropriate html entities for html export.</p>
<p>This release contains some new features and a lot of code refactoring. Among the main new
features is paragraph alignment and indentation. Regular line breaks within paragraphs are now also
supported. The changes are described in more detail below.</p>
<h3>Document Layout Automation</h3>
<p>Among other changes in this release are a few improvements to the process of creating and
changing documents. When a new document is first created, the header is generated from the assigned
label and layout.</p>
<p>In addition, for some document layouts, when the user changes the header level of the first
header of the document, the document layout setting is updated accordingly. This should reduce the
need for the user to maintain two ways of assigning the role of a given document. This automation
only applies to combinations of header level and current document layouts where there is no
ambiguity. For instance, changing the header level in a "Scene" document from level 3 to 2 changes
the document layout automatically to "Chapter". But changing the first header of a "Book" layout
document from 1 to 2 does not change the document's layout as the "Book" layout is a generic
document layout and it's perfectly reasonable for its first header to be a chapter header.</p>
<p>Keep in mind that novelWriter treats documents with layout "Book", "Chapter" and "Scene" exactly
the same during exports. The distinction is only meant as a way to indicate the purpose of a
document in the project tree. This new automation is meant to assist in keeping this information up
to date. The other layouts do have an effect on formatting during export, and are generally left
alone.</p>
<h3>Internationalisation</h3>
<p>Translation files have been added for Simplified Chinese by Qianzhi Long.</p>
<h3>The Session Timer and Idle Time</h3>
<p>Another change that has been requested by a couple of users is to have the session timer in the
status bar stop counting when the user is inactive (idle). This feature is optional, and can be
controlled from Preferences. The definition of "idle" in this context is either that the user is
active in a different application than novelWriter (loss of focus) or that the user has not made
any changes to the current document for a given amount of time. The time threshold is by default
five minutes, but can be altered in Preferences.</p>
<p>In addition, the idle time is also recorded in the session log, and can be viewed in the Writing
Statistics dialog and exported with the rest of the information. The idle time is recorder in the
logs regardless of whether the status bar clock takes idle time into consideration or not. So even
if you turn off the idle time switch in Preferences, the other idle time setting still affects the
writing stats log entries.</p>
<h3>Line Breaks</h3>
<p>The way line breaks inside paragraphs work has been changed. A single line break is now treated
as a proper line break and will show up in the document viewer and exported documents. A single
line break does not start a new paragraph, but forces a break inside the paragraph like a Shift +
Enter does in most rich text editors. Two line breaks is still needed to start a new paragraph.</p>
<p>The old syntax of adding two spaces at the end of a line to force a line break within a
paragraph will still work as before, so there is no need to change your existing text if you've
used this feature. However, there is a new highlighting feature that will show you where in the
text you have redundant spaces. If you are used to having double spaces between sentences, you may
want to switch off this highlighting feature in Preferences as it will also detect those.</p>
<p>A helper function has been added to the Format menu that can look through a paragraph and remove
line breaks in case you've been using line breaks inside your existing text under the assumption
that the exporter and viewer will ignore them.</p>
<p>I hope this change will not be too inconvenient. I believe the new behaviour will make more
sense for most people. Especially considering some of the feedback I've gotten on how line breaks
work. The original implementation was following the Markdown standard, but since novelWriter is not
a proper Markdown editor and instead just borrows from Markdown, this behaviour always seemed a bit
unnecessary.</p>
<h3>Other Changes</h3>
<p>The user dictionary of the project, where words added to the dictionary from the document editor
go, can now be viewed and edited with a new "Project Word List" tool in the "Tools" menu.</p>
<p>A small additional feature added is also the ability to undo the last move of an item in the
project tree. The keyboard shortcut for this is `Ctrl+Shift+Z`, or it can be accessed from the
menu. The feature can only undo the last move, but it includes both documents moved to trash, moves
by up/down keypress or menu entries, and drag and drop moves.</p>
<p>A new keyword has been added to mark characters in the story. The new keyword is intended to tag
a character as the focus character for a chapter or scene. This is useful for stories where the
point-of-view character and the focus character are different.</p>
<p>Lastly, two bugfixes have been made as well. The Empty Trash feature was no longer working due
to an earlier fix solving another issue. The feature has now been restored. In addition, the
indexer now checks that a keyword (tag or reference) is valid before saving it to the index.
Previously, an invalid keyword could be saved to the index and potentially crash the application.
</p>
<h3>Text Alignment and Indentation</h3>
<p>The default text alignment is left or justified based on your preferences. For documents with
the layout set to Title Page or Partitions, the default is centred. However, sometimes you may
want to override this default. A new set of codes have therefore been added to allow specifying
alignment as well as additional text margins on individual paragraphs.</p>
<p>The logic of the syntax is as follows:</p>
<p>A single angle bracket will push the text away from the edge it points away from. Therefore, a
single '&gt;' before the paragraph, or a single '&lt;' after the paragraph, will add indentation on
the respective side. It's perfectly valid to do this on both sides at the same time.</p>
<p>A double set of angle brackets will push the text all the way towards the opposite side.
Therefore, a double set of '&gt;&gt;' before the paragraph will indicate right alignment, and a
double set of '&lt;&lt;' after the paragraph will force left alignment. Also here both can be used
at the same time, which results in the paragraph being centred.</p>
<p>Format menu entries and keyboard shortcuts have been added so that you don't have to memorise
these codes.</p>
<p><i>See also the <a href="https://github.com/vkbo/novelWriter/releases">Releases</a> page.</i></p>

View File

@@ -0,0 +1,104 @@
<!DOCTYPE html public "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<body>
<h2>Release Notes for 1.2.2</h2>
<p><i>Released on 28 March 2021</i></p>
<p>This patch release is a bug fix release addressing some inconsistencies and issues with the
document header buttons when Focus Mode is active. The keyboard shortcuts for search and replace
should now also work in Focus Mode. In addition, the setup script for novelWriter has been improved
when installing on Windows.</p>
<p><i>See also the <a href="https://github.com/vkbo/novelWriter/releases">Releases</a> page.</i></p>
<hr/>
<h2>Release Notes for 1.2.1</h2>
<p><i>Released on 21 March 2021</i></p>
<p>This patch release is a bug fix release addressing issues with the document editor's search and
replace tool. Due to some recently added restrictions on when various tools are active, depending
on which part of the main window has the user's focus, the search tool keyboard shortcuts and
buttons were blocked when they shouldn't. This release resolves these issues.</p>
<hr/>
<h2>Release Notes for 1.2</h2>
<p><i>Released on 14 March 2021</i></p>
<p>This release is mainly focused on the Build Novel Project tool. Completely new export classes
have been written to support Open Document and Markdown exports. In addition, the way document
layouts are handled have been automated a little to assist the user in keeping header levels and
document layout flags in sync. The third new additoion is the ability to record and log idle time
during a writing session to improve the writing statistics information as requested by several
users. Finally, it is now possible to directly edit the project dictionary via a new, simple GUI
dialog.</p>
<h3>The Build Novel Project Tool</h3>
<p>The main changes for this release are to the Build Novel Project tool. The Open Document export,
as well as the Markdown export, is now handled entirely by code written for novelWriter.
Previously, these export features depended on the underlying Qt library's save routines connected
to the preview document shown in the build dialog. Using this method of export both meant that the
content of the document was dependent on the preview being generated first, and it also meant that
the exported document had limited support for novelWriter-specific features and custom formatting.
The new export class should generate a much better result, especially for the Open Document
formats. The Open Document standard is supported by Open Office, Libre Office, Google Docs,
Microsoft Word, and probably a number of other applications too. The Markdown export hasn't changed
a lot, but should be a slight improvement on the previous export feature.</p>
<p>These changes to the build tool also imply that the saving process is now independent of the
content of the preview window, meaning you don't have to rebuild the preview before saving, which
was previously the case. To make this more consistent, the PDF export option has been moved to the
print button as it is actually a print-to-file feature under the hood, not technically a proper PDF
export format. It is exactly the same as printing to file from the print preview dialog.</p>
<p>In addition to the changes to the export features, the Build Novel Project tool now also has
controls for line height, which applies to all rich text export formats, and the option to replace
unicode characters with appropriate html entities for html export.</p>
<h3>Document Layout Automation</h3>
<p>Among other changes in this release are a few improvements to the process of creating and
changing documents. When a new document is first created, the header is generated from the assigned
label and layout.</p>
<p>In addition, for some document layouts, when the user changes the header level of the first
header of the document, the document layout setting is updated accordingly. This should reduce the
need for the user to maintain two ways of assigning the role of a given document. This automation
only applies to combinations of header level and current document layouts where there is no
ambiguity. For instance, changing the header level in a "Scene" document from level 3 to 2 changes
the document layout automatically to "Chapter". But changing the first header of a "Book" layout
document from 1 to 2 does not change the document's layout as the "Book" layout is a generic
document layout and it's perfectly reasonable for its first header to be a chapter header.</p>
<p>Keep in mind that novelWriter treats documents with layout "Book", "Chapter" and "Scene" exactly
the same during exports. The distinction is only meant as a way to indicate the purpose of a
document in the project tree. This new automation is meant to assist in keeping this information up
to date. The other layouts do have an effect on formatting during export, and are generally left
alone.</p>
<h3>The Session Timer and Idle Time</h3>
<p>Another change that has been requested by a couple of users is to have the session timer in the
status bar stop counting when the user is inactive (idle). This feature is optional, and can be
controlled from Preferences. The definition of "idle" in this context is either that the user is
active in a different application than novelWriter (loss of focus) or that the user has not made
any changes to the current document for a given amount of time. The time threshold is by default
five minutes, but can be altered in Preferences.</p>
<p>In addition, the idle time is also recorded in the session log, and can be viewed in the Writing
Statistics dialog and exported with the rest of the information. The idle time is recorder in the
logs regardless of whether the status bar clock takes idle time into consideration or not. So even
if you turn off the idle time switch in Preferences, the other idle time setting still affects the
writing stats log entries.</p>
<h3>Other Changes</h3>
<p>The user dictionary of the project, where words added to the dictionary from the document editor
go, can now be viewed and edited with a new "Project Word List" tool in the "Tools" menu.</p>
<p>A small additional feature added is also the ability to undo the last move of an item in the
project tree. The keyboard shortcut for this is `Ctrl+Shift+Z`, or it can be accessed from the
menu. The feature can only undo the last move, but it includes both documents moved to trash, moves
by up/down keypress or menu entries, and drag and drop moves.</p>
<p>A new keyword has been added to mark characters in the story. The new keyword is intended to tag
a character as the focus character for a chapter or scene. This is useful for stories where the
point-of-view character and the focus character are different.</p>
<p>Lastly, two bugfixes have been made as well. The Empty Trash feature was no longer working due
to an earlier fix solving another issue. The feature has now been restored. In addition, the
indexer now checks that a keyword (tag or reference) is valid before saving it to the index.
Previously, an invalid keyword could be saved to the index and potentially crash the application.
</p>
</body>
</html>

View File

@@ -0,0 +1,60 @@
<!DOCTYPE html public "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<body>
<h2>Release Notes for 1.3.3</h2>
<p><i>Released on 13 June 2021</i></p>
<p>This patch release fixes a potential file encoding issue when running setup on Windows, and a
minor issue with the project word count not being updated immediately when a file is deleted. In
addition, the keyboard shortcuts to change focus between the project tree, the editor, the viewer,
and the outline panel, have been changed for Windows users. They keyboard shortcuts were
interfering with the Alt codes used for special characters. The shortcuts are unchanged for Linux
and macOS.</p>
<p><i>See also the <a href="https://github.com/vkbo/novelWriter/releases">Releases</a> page.</i></p>
<hr/>
<h2>Release Notes for 1.3.2</h2>
<p><i>Released on 30 May 2021</i></p>
<p>This is a patch release that fixes some minor issues. One issue was with the split tool, which
would drop the last line from the source document during a split if it was missing a final line
break. A minor issue with the display of word counts on the details panel under the project tree
has also been fixed. In addition, the setup script commands for Linux have been improved a bit.</p>
<hr/>
<h2>Release Notes for 1.3.1</h2>
<p><i>Released on 6 May 2021</i></p>
<p>This is a patch release primarily to fix a problem with the Qt translation library used to make
novelWriter available in multiple languages. One of the function calls made to the library was
added recently, in Qt 5.15, which many users on Linux will not have installed on their system. The
issue could be resolved by updating the library, but it's an unnecessary and inconvenient
restriction.</p>
<hr/>
<h2>Release Notes for 1.3</h2>
<p><i>Released on 2 May 2021</i></p>
<p>The main feature of the 1.3 release is internationalisation. The release introduces support for
Portuguese, French, and Norwegian for the main GUI, in addition to the default English. The
settings for choosing the GUI language can be found in the main Preferences.</p>
<p>The implementation is such that the projects themselves can use a different language than the
one selected for the GUI. To allow for better support for multiple language projects, the Build
Novel Project tool has the added option to select the language of the injected text into exported
documents. In particularly for the number words that can be added for chapter headers. Available
languages here are the same as mentioned above for the main GUI, plus German.</p>
<p>Settings have also been added in Preferences to allow for automatic injection of spaces or
non-breaking spaces in front of, or behind, certain characters. These are features generally
available in text editors for French, but the implementation in novelWriter simply makes them a
free text box where you can list all characters or symbols where you want a space added
automatically.</p>
<p>The translation files for this release have been provided by Bruno Meneguello for the Portuguese
translation, by Jan Lüdke (jyhelle) for the French translation, by Veronica Berglyd Olsen for the
Norwegian translation as well as the minor modifications for US English, and Marian Lückhof
provided the translation file for chapter numbers in German.</p>
<p>The work to rewrite novelWriter to allow for internationalisation was done by Bruno Meneguello
and Veronica Berglyd Olsen.</p>
</body>
</html>

View File

@@ -20,6 +20,6 @@ hidden = 150, 150, 150
keyword = 200, 46, 0
value = 184, 200, 0
spellcheckline = 200, 46, 0
tagerror = 46, 200, 0
errorline = 46, 200, 0
replacetag = 0, 184, 46
modifier = 200, 120, 0

View File

@@ -20,6 +20,6 @@ hidden = 100, 100, 100
keyword = 200, 50, 50
value = 50, 150, 50
spellcheckline = 200, 0, 0
tagerror = 0, 150, 0
errorline = 0, 150, 0
replacetag = 0, 150, 0
modifier = 150, 110, 30

View File

@@ -20,6 +20,6 @@ hidden = 150, 150, 150
keyword = 225, 225, 225
value = 200, 200, 200
spellcheckline = 200, 46, 0
tagerror = 46, 200, 0
errorline = 46, 200, 0
replacetag = 225, 225, 225
modifier = 225, 225, 225

View File

@@ -20,6 +20,6 @@ hidden = 100, 100, 100
keyword = 0, 0, 0
value = 20, 20, 20
spellcheckline = 200, 0, 0
tagerror = 0, 150, 0
errorline = 0, 150, 0
replacetag = 0, 0, 0
modifier = 0, 0, 0

View File

@@ -40,6 +40,6 @@ hidden = 152, 159, 177
keyword = 222, 61, 58
value = 150, 74, 193
spellcheckline = 222, 61, 58
tagerror = 8, 145, 106
errorline = 8, 145, 106
replacetag = 42, 162, 152
modifier = 224, 175, 5

View File

@@ -40,6 +40,6 @@ hidden = 99, 119, 119
keyword = 247, 140, 108
value = 199, 146, 234
spellcheckline = 247, 140, 108
tagerror = 173, 219, 103
errorline = 173, 219, 103
replacetag = 127, 219, 202
modifier = 236, 196, 141

View File

@@ -20,6 +20,6 @@ hidden = 147, 161, 161
keyword = 133, 153, 0
value = 203, 75, 22
spellcheckline = 203, 75, 22
tagerror = 220, 50, 47
errorline = 220, 50, 47
replacetag = 133, 153, 0
modifier = 181, 137, 0

View File

@@ -20,6 +20,6 @@ hidden = 88, 110, 117
keyword = 133, 153, 0
value = 203, 75, 22
spellcheckline = 203, 75, 22
tagerror = 220, 50, 47
errorline = 220, 50, 47
replacetag = 133, 153, 0
modifier = 181, 137, 0

View File

@@ -40,6 +40,6 @@ hidden = 142, 144, 140
keyword = 240, 40, 41
value = 137, 89, 168
spellcheckline = 240, 40, 41
tagerror = 113, 140, 0
errorline = 113, 140, 0
replacetag = 62, 153, 159
modifier = 245, 135, 31

View File

@@ -40,6 +40,6 @@ hidden = 150, 152, 150
keyword = 204, 102, 102
value = 178, 148, 187
spellcheckline = 204, 102, 102
tagerror = 181, 189, 104
errorline = 181, 189, 104
replacetag = 138, 190, 183
modifier = 222, 147, 95

View File

@@ -40,6 +40,6 @@ hidden = 114, 133, 183
keyword = 255, 157, 164
value = 235, 187, 255
spellcheckline = 255, 157, 164
tagerror = 209, 241, 169
errorline = 209, 241, 169
replacetag = 153, 255, 255
modifier = 255, 197, 143

View File

@@ -40,6 +40,6 @@ hidden = 150, 152, 150
keyword = 213, 78, 83
value = 195, 151, 216
spellcheckline = 213, 78, 83
tagerror = 185, 202, 74
errorline = 185, 202, 74
replacetag = 112, 192, 177
modifier = 231, 140, 69

View File

@@ -40,6 +40,6 @@ hidden = 153, 153, 153
keyword = 242, 119, 122
value = 204, 153, 204
spellcheckline = 242, 119, 122
tagerror = 153, 204, 153
errorline = 153, 204, 153
replacetag = 102, 204, 204
modifier = 249, 145, 57

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – Common Functions
==============================
@@ -29,13 +28,14 @@ import logging
from datetime import datetime
from PyQt5.QtWidgets import qApp
from PyQt5.QtCore import QCoreApplication
from nw.constants import (
nwConst, nwUnicode, nwItemClass, nwItemType, nwItemLayout
)
from nw.enum import nwItemClass, nwItemType, nwItemLayout
from nw.constants import nwConst, nwUnicode
logger = logging.getLogger(__name__)
def checkString(value, default, allowNone=False):
"""Check if a variable is a string or a none.
"""
@@ -48,6 +48,7 @@ def checkString(value, default, allowNone=False):
return str(value)
return default
def checkInt(value, default, allowNone=False):
"""Check if a variable is an integer or a none.
"""
@@ -61,6 +62,7 @@ def checkInt(value, default, allowNone=False):
except Exception:
return default
def checkBool(value, default, allowNone=False):
"""Check if a variable is a boolean or a none.
"""
@@ -85,6 +87,7 @@ def checkBool(value, default, allowNone=False):
return default
return default
def checkHandle(value, default, allowNone=False):
"""Check if a value is a handle.
"""
@@ -97,6 +100,7 @@ def checkHandle(value, default, allowNone=False):
return str(value)
return default
def isHandle(theString):
"""Check if a string is a valid novelWriter handle.
Note: This is case sensitive. Must be lower case!
@@ -110,6 +114,7 @@ def isHandle(theString):
return False
return True
def isTitleTag(theString):
"""Check if a string is a valid title string.
"""
@@ -124,21 +129,25 @@ def isTitleTag(theString):
return False
return True
def isItemClass(theString):
"""Check if an item is a calid nwItemClass identifier.
"""
return theString in nwItemClass.__members__
def isItemType(theString):
"""Check if an item is a calid nwItemType identifier.
"""
return theString in nwItemType.__members__
def isItemLayout(theString):
"""Check if an item is a calid nwItemLayout identifier.
"""
return theString in nwItemLayout.__members__
def hexToInt(value, default=0):
"""Convert a hex string to an integer.
"""
@@ -149,36 +158,6 @@ def hexToInt(value, default=0):
return default
return default
def colRange(rgbStart, rgbEnd, nStep):
"""Generate a range of colours from one RGB value to another.
"""
if len(rgbStart) != 3 and len(rgbEnd) != 3 and nStep < 1:
logger.error("Cannot create colour range from given parameters")
return None
if nStep == 1:
return rgbStart
elif nStep == 2:
return [rgbStart, rgbEnd]
dC = [0, 0, 0]
for c in range(3):
cA = rgbStart[c]
cB = rgbEnd[c]
dC[c] = (cB-cA)/(nStep-1)
print(dC)
retCol = [rgbStart]
for n in range(nStep):
if n > 0 and n < nStep:
retCol.append([
int(retCol[n-1][0] + dC[0]),
int(retCol[n-1][1] + dC[1]),
int(retCol[n-1][2] + dC[2]),
])
retCol[-1] = rgbEnd
print(retCol)
return retCol
def formatInt(theInt):
"""Formats an integer with k, M, G etc.
@@ -199,6 +178,7 @@ def formatInt(theInt):
return str(theInt)
def formatTimeStamp(theTime, fileSafe=False):
"""Take a number (on the format returned by time.time()) and convert
it to a timestamp string.
@@ -208,6 +188,7 @@ def formatTimeStamp(theTime, fileSafe=False):
else:
return datetime.fromtimestamp(theTime).strftime(nwConst.FMT_TSTAMP)
def formatTime(tS):
"""Format a time in seconds in HH:MM:SS format or d-HH:MM:SS format
if a full day or longer.
@@ -219,6 +200,7 @@ def formatTime(tS):
return f"{tS//3600:02d}:{tS%3600//60:02d}:{tS%60:02d}"
return "ERROR"
def splitVersionNumber(vString):
""" Splits a version string on the form aa.bb.cc into major, minor
and patch, and computes an integer value aabbcc.
@@ -226,7 +208,7 @@ def splitVersionNumber(vString):
vMajor = 0
vMinor = 0
vPatch = 0
vInt = 0
vInt = 0
vBits = vString.split(".")
nBits = len(vBits)
@@ -242,6 +224,7 @@ def splitVersionNumber(vString):
return [vMajor, vMinor, vPatch, vInt]
def transferCase(theSource, theTarget):
"""Transfers the case of the source word to the target word. This
will consider all upper or lower, and first char capitalisation.
@@ -263,37 +246,67 @@ def transferCase(theSource, theTarget):
return theResult
def fuzzyTime(secDiff):
"""Converts a time difference in seconds into a fuzzy time string.
"""
if secDiff < 0:
return "in the future"
return QCoreApplication.translate(
"Common", "in the future"
)
elif secDiff < 30:
return "just now"
return QCoreApplication.translate(
"Common", "just now"
)
elif secDiff < 90:
return "a minute ago"
elif secDiff < 3300: # 55 minutes
return "%d minutes ago" % int(round(secDiff/60))
elif secDiff < 5400: # 90 minutes
return "an hour ago"
elif secDiff < 84600: # 23.5 hours
return "%d hours ago" % int(round(secDiff/3600))
elif secDiff < 129600: # 1.5 days
return "a day ago"
elif secDiff < 561600: # 6.5 days
return "%d days ago" % int(round(secDiff/86400))
elif secDiff < 907200: # 10.5 days
return "a week ago"
elif secDiff < 2419200: # 28 days
return "%d weeks ago" % int(round(secDiff/604800))
elif secDiff < 3888000: # 45 days
return "a month ago"
elif secDiff < 29808000: # 345 days
return "%d months ago" % int(round(secDiff/2592000))
elif secDiff < 47336400: # 1.5 years
return "a year ago"
return QCoreApplication.translate(
"Common", "a minute ago"
)
elif secDiff < 3300: # 55 minutes
return QCoreApplication.translate(
"Common", "{0} minutes ago"
).format(int(round(secDiff/60)))
elif secDiff < 5400: # 90 minutes
return QCoreApplication.translate(
"Common", "an hour ago"
)
elif secDiff < 84600: # 23.5 hours
return QCoreApplication.translate(
"Common", "{0} hours ago"
).format(int(round(secDiff/3600)))
elif secDiff < 129600: # 1.5 days
return QCoreApplication.translate(
"Common", "a day ago"
)
elif secDiff < 561600: # 6.5 days
return QCoreApplication.translate(
"Common", "{0} days ago"
).format(int(round(secDiff/86400)))
elif secDiff < 907200: # 10.5 days
return QCoreApplication.translate(
"Common", "a week ago"
)
elif secDiff < 2419200: # 28 days
return QCoreApplication.translate(
"Common", "{0} weeks ago"
).format(int(round(secDiff/604800)))
elif secDiff < 3888000: # 45 days
return QCoreApplication.translate(
"Common", "a month ago"
)
elif secDiff < 29808000: # 345 days
return QCoreApplication.translate(
"Common", "{0} months ago"
).format(int(round(secDiff/2592000)))
elif secDiff < 47336400: # 1.5 years
return QCoreApplication.translate(
"Common", "a year ago"
)
else:
return "%d years ago" % int(round(secDiff/31557600))
return QCoreApplication.translate(
"Common", "{0} years ago"
).format(int(round(secDiff/31557600)))
def makeFileNameSafe(theText):
"""Returns a filename safe version of the text.
@@ -304,6 +317,7 @@ def makeFileNameSafe(theText):
cleanName += c
return cleanName
def getGuiItem(theName):
"""Returns a QtWidget based on its objectName.
"""
@@ -311,3 +325,27 @@ def getGuiItem(theName):
if qWidget.objectName() == theName:
return qWidget
return None
def numberToRoman(numVal, isLower=False):
"""Convert an integer to a roman number.
"""
if not isinstance(numVal, int):
return "NAN"
if numVal < 1 or numVal > 4999:
return "OOR"
theValues = [
(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"), (100, "C"), (90, "XC"),
(50, "L"), (40, "XL"), (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"),
]
romNum = ""
for theDiv, theSym in theValues:
n = numVal//theDiv
romNum += n*theSym
numVal -= n*theDiv
if numVal <= 0:
break
return romNum.lower() if isLower else romNum

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – Config Class
==========================
@@ -24,24 +23,28 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import os
import sys
import json
import shutil
import logging
import configparser
import shutil
import json
import sys
import os
from time import time
from PyQt5.Qt import PYQT_VERSION_STR
from PyQt5.QtCore import QT_VERSION_STR, QStandardPaths, QSysInfo
from PyQt5.QtCore import (
QT_VERSION_STR, QStandardPaths, QSysInfo, QLocale, QLibraryInfo,
QTranslator
)
from nw.constants import nwConst, nwFiles, nwUnicode
from nw.common import splitVersionNumber, formatTimeStamp
from nw.error import logException
from nw.common import splitVersionNumber, formatTimeStamp
from nw.constants import nwConst, nwFiles, nwUnicode
logger = logging.getLogger(__name__)
class Config:
CNF_STR = 0
@@ -50,6 +53,9 @@ class Config:
CNF_S_LST = 3
CNF_I_LST = 4
LANG_NW = 1
LANG_PROJ = 2
def __init__(self):
# Set Application Variables
@@ -76,22 +82,29 @@ class Config:
self.helpPath = None # The full path to the novelwriter .qhc help file
# Runtime Settings and Variables
self.confChanged = False # True whenever the config has chenged, false after save
self.hasHelp = False # True if the Qt help files are present in the assets folder
self.confChanged = False # True whenever the config has chenged, false after save
self.hasHelp = False # True if the Qt help files are present in the assets folder
## General
# General
self.guiTheme = "default"
self.guiSyntax = "default_light"
self.guiIcons = "typicons_colour_light"
self.guiDark = False # Load icons for dark backgrounds, if available
self.guiLang = "en" # Hardcoded for now since the GUI is only in English
self.guiFont = "" # Defaults to system default font
self.guiFontSize = 11
self.guiScale = 1.0 # Set automatically by Theme class
self.lastNotes = "0x0" # The latest release notes that have been shown
self.guiDark = False # Load icons for dark backgrounds, if available
self.guiFont = "" # Defaults to system default font
self.guiFontSize = 11 # Is overridden if system default is loaded
self.guiScale = 1.0 # Set automatically by Theme class
self.lastNotes = "0x0" # The latest release notes that have been shown
## Sizes
# Localisation
self.qLocal = QLocale.system()
self.guiLang = self.qLocal.name()
self.qtLangPath = QLibraryInfo.location(QLibraryInfo.TranslationsPath)
self.nwLangPath = None
self.qtTrans = {}
# Sizes
self.winGeometry = [1200, 650]
self.prefGeometry = [700, 615]
self.treeColWidth = [200, 50, 30]
self.novelColWidth = [200, 50]
self.projColWidth = [200, 60, 140]
@@ -101,62 +114,66 @@ class Config:
self.outlnPanePos = [500, 150]
self.isFullScreen = False
## Features
self.hideVScroll = False # Hide vertical scroll bars on main widgets
self.hideHScroll = False # Hide horizontal scroll bars on main widgets
# Features
self.hideVScroll = False # Hide vertical scroll bars on main widgets
self.hideHScroll = False # Hide horizontal scroll bars on main widgets
## Project
self.autoSaveProj = 60 # Interval for auto-saving project in seconds
self.autoSaveDoc = 30 # Interval for auto-saving document in seconds
# Project
self.autoSaveProj = 60 # Interval for auto-saving project in seconds
self.autoSaveDoc = 30 # Interval for auto-saving document in seconds
## Text Editor
self.textFont = None # Editor font
self.textSize = 12 # Editor font size
self.textFixedW = True # Keep editor text fixed width
self.textWidth = 600 # Editor text width
self.textMargin = 40 # Editor/viewer text margin
self.tabWidth = 40 # Editor tabulator width
# Text Editor
self.textFont = None # Editor font
self.textSize = 12 # Editor font size
self.textFixedW = True # Keep editor text fixed width
self.textWidth = 600 # Editor text width
self.textMargin = 40 # Editor/viewer text margin
self.tabWidth = 40 # Editor tabulator width
self.focusWidth = 800 # Focus Mode text width
self.hideFocusFooter = False # Hide document footer in Focus Mode
self.showFullPath = True # Show full document path in editor header
self.autoSelect = True # Auto-select word when applying format with no selection
self.focusWidth = 800 # Focus Mode text width
self.hideFocusFooter = False # Hide document footer in Focus Mode
self.showFullPath = True # Show full document path in editor header
self.autoSelect = True # Auto-select word when applying format with no selection
self.doJustify = False # Justify text
self.showTabsNSpaces = False # Show tabs and spaces in edior
self.showLineEndings = False # Show line endings in editor
self.doJustify = False # Justify text
self.showTabsNSpaces = False # Show tabs and spaces in edior
self.showLineEndings = False # Show line endings in editor
self.showMultiSpaces = True # Highlight multiple spaces in the text
self.doReplace = True # Enable auto-replace as you type
self.doReplaceSQuote = True # Smart single quotes
self.doReplaceDQuote = True # Smart double quotes
self.doReplaceDash = True # Replace multiple hyphens with dashes
self.doReplaceDots = True # Replace three dots with ellipsis
self.doReplace = True # Enable auto-replace as you type
self.doReplaceSQuote = True # Smart single quotes
self.doReplaceDQuote = True # Smart double quotes
self.doReplaceDash = True # Replace multiple hyphens with dashes
self.doReplaceDots = True # Replace three dots with ellipsis
self.scrollPastEnd = True # Allow scrolling past end of document
self.autoScroll = False # Typewriter-like scrolling
self.autoScrollPos = 30 # Start point for typewriter-like scrolling
self.scrollPastEnd = True # Allow scrolling past end of document
self.autoScroll = False # Typewriter-like scrolling
self.autoScrollPos = 30 # Start point for typewriter-like scrolling
self.wordCountTimer = 5.0 # Interval for word count update in seconds
self.bigDocLimit = 800 # Size threshold for heavy editor features in kilobytes
self.wordCountTimer = 5.0 # Interval for word count update in seconds
self.bigDocLimit = 800 # Size threshold for heavy editor features in kilobytes
self.highlightQuotes = True # Highlight text in quotes
self.allowOpenSQuote = False # Allow open-ended single quotes
self.allowOpenDQuote = True # Allow open-ended double quotes
self.highlightEmph = True # Add colour to text emphasis
self.highlightQuotes = True # Highlight text in quotes
self.allowOpenSQuote = False # Allow open-ended single quotes
self.allowOpenDQuote = True # Allow open-ended double quotes
self.highlightEmph = True # Add colour to text emphasis
self.stopWhenIdle = True # Stop the status bar clock when the user is idle
self.userIdleTime = 300 # Time of inactivity to consider user idle
self.stopWhenIdle = True # Stop the status bar clock when the user is idle
self.userIdleTime = 300 # Time of inactivity to consider user idle
## User-Selected Symbols
# User-Selected Symbols
self.fmtApostrophe = nwUnicode.U_RSQUO
self.fmtSingleQuotes = [nwUnicode.U_LSQUO, nwUnicode.U_RSQUO]
self.fmtDoubleQuotes = [nwUnicode.U_LDQUO, nwUnicode.U_RDQUO]
self.fmtPadBefore = ""
self.fmtPadAfter = ""
self.fmtPadThin = False
## Spell Checking
# Spell Checking
self.spellTool = None
self.spellLanguage = None
## Search Bar Switches
# Search Bar Switches
self.searchCase = False
self.searchWord = False
self.searchRegEx = False
@@ -164,12 +181,12 @@ class Config:
self.searchNextFile = False
self.searchMatchCap = False
## Backup
# Backup
self.backupPath = ""
self.backupOnClose = False
self.askBeforeBackup = True
## State
# State
self.showRefPanel = True
self.viewComments = True
self.viewSynopsis = True
@@ -218,8 +235,8 @@ class Config:
self.kernelVer = "Unknown"
# Packages
self.hasEnchant = False # The pyenchant package
self.hasAssistant = False # The Qt Assistant executable
self.hasEnchant = False # The pyenchant package
self.hasAssistant = False # The Qt Assistant executable
# Recent Cache
self.recentProj = {}
@@ -255,7 +272,7 @@ class Config:
confRoot = QStandardPaths.writableLocation(QStandardPaths.ConfigLocation)
self.confPath = os.path.join(os.path.abspath(confRoot), self.appHandle)
else:
logger.info("Setting config from alternative path: %s" % confPath)
logger.info("Setting config from alternative path: %s", confPath)
self.confPath = confPath
if dataPath is None:
@@ -265,11 +282,11 @@ class Config:
dataRoot = QStandardPaths.writableLocation(QStandardPaths.DataLocation)
self.dataPath = os.path.join(os.path.abspath(dataRoot), self.appHandle)
else:
logger.info("Setting data path from alternative path: %s" % dataPath)
logger.info("Setting data path from alternative path: %s", dataPath)
self.dataPath = dataPath
logger.verbose("Config path: %s" % self.confPath)
logger.verbose("Data path: %s" % self.dataPath)
logger.verbose("Config path: %s", self.confPath)
logger.verbose("Data path: %s", self.dataPath)
self.confFile = self.appHandle+".conf"
self.lastPath = os.path.expanduser("~")
@@ -290,8 +307,11 @@ class Config:
self.iconPath = os.path.join(self.assetPath, "icons")
self.appIcon = os.path.join(self.iconPath, "novelwriter.svg")
logger.verbose("App path: %s" % self.appPath)
logger.verbose("Last path: %s" % self.lastPath)
# Internationalisation
self.nwLangPath = os.path.join(self.assetPath, "i18n")
logger.verbose("App path: %s", self.appPath)
logger.verbose("Last path: %s", self.lastPath)
# If config folder does not exist, create it.
# This assumes that the os config folder itself exists.
@@ -299,7 +319,7 @@ class Config:
try:
os.mkdir(self.confPath)
except Exception as e:
logger.error("Could not create folder: %s" % self.confPath)
logger.error("Could not create folder: %s", self.confPath)
logException()
self.hasError = True
self.errData.append("Could not create folder: %s" % self.confPath)
@@ -322,7 +342,7 @@ class Config:
try:
os.mkdir(self.dataPath)
except Exception as e:
logger.error("Could not create folder: %s" % self.dataPath)
logger.error("Could not create folder: %s", self.dataPath)
logException()
self.hasError = True
self.errData.append("Could not create folder: %s" % self.dataPath)
@@ -354,6 +374,57 @@ class Config:
return True
def initLocalisation(self, nwApp):
"""Initialise the localisation of the GUI.
"""
self.qLocal = QLocale(self.guiLang)
QLocale.setDefault(self.qLocal)
self.qtTrans = {}
langList = [
(self.qtLangPath, "qtbase"), # Qt 5.x
(self.nwLangPath, "qtbase"), # Alternative Qt 5.x
(self.nwLangPath, "nw"), # novelWriter
]
for lngPath, lngBase in langList:
for lngCode in self.qLocal.uiLanguages():
qTrans = QTranslator()
lngFile = "%s_%s" % (lngBase, lngCode.replace("-", "_"))
if lngFile not in self.qtTrans:
if qTrans.load(lngFile, lngPath):
logger.debug("Loaded: %s", os.path.join(lngPath, lngFile))
nwApp.installTranslator(qTrans)
self.qtTrans[lngFile] = qTrans
return
def listLanguages(self, lngSet):
"""List localisation files in the i18n folder. The default GUI
language 'en_GB' is British English.
"""
if lngSet == self.LANG_NW:
fPre = "nw_"
fExt = ".qm"
langList = {"en_GB": QLocale("en_GB").nativeLanguageName().title()}
elif lngSet == self.LANG_PROJ:
fPre = "project_"
fExt = ".json"
langList = {"en": "English"}
else:
return []
for qmFile in os.listdir(self.nwLangPath):
if not os.path.isfile(os.path.join(self.nwLangPath, qmFile)):
continue
if not qmFile.startswith(fPre) or not qmFile.endswith(fExt):
continue
qmLang = qmFile[len(fPre):-len(fExt)]
qmName = QLocale(qmLang).nativeLanguageName().title()
if qmLang and qmName and qmLang != "en":
langList[qmLang] = qmName
return sorted(langList.items(), key=lambda x: x[0])
def loadConfig(self):
"""Load preferences from file and replace default settings.
"""
@@ -362,9 +433,9 @@ class Config:
return False
cnfParse = configparser.ConfigParser()
cnfPath = os.path.join(self.confPath, self.confFile)
cnfPath = os.path.join(self.confPath, self.confFile)
try:
with open(cnfPath, mode="r", encoding="utf8") as inFile:
with open(cnfPath, mode="r", encoding="utf-8") as inFile:
cnfParse.read_file(inFile)
except Exception as e:
logger.error("Could not load config file")
@@ -374,7 +445,7 @@ class Config:
self.errData.append(str(e))
return False
## Main
# Main
cnfSec = "Main"
self.guiTheme = self._parseLine(
cnfParse, cnfSec, "theme", self.CNF_STR, self.guiTheme
@@ -397,12 +468,18 @@ class Config:
self.lastNotes = self._parseLine(
cnfParse, cnfSec, "lastnotes", self.CNF_STR, self.lastNotes
)
self.guiLang = self._parseLine(
cnfParse, cnfSec, "guilang", self.CNF_STR, self.guiLang
)
## Sizes
# Sizes
cnfSec = "Sizes"
self.winGeometry = self._parseLine(
cnfParse, cnfSec, "geometry", self.CNF_I_LST, self.winGeometry
)
self.prefGeometry = self._parseLine(
cnfParse, cnfSec, "preferences", self.CNF_I_LST, self.prefGeometry
)
self.treeColWidth = self._parseLine(
cnfParse, cnfSec, "treecols", self.CNF_I_LST, self.treeColWidth
)
@@ -434,7 +511,7 @@ class Config:
cnfParse, cnfSec, "hidehscroll", self.CNF_BOOL, self.hideHScroll
)
## Project
# Project
cnfSec = "Project"
self.autoSaveProj = self._parseLine(
cnfParse, cnfSec, "autosaveproject", self.CNF_INT, self.autoSaveProj
@@ -443,7 +520,7 @@ class Config:
cnfParse, cnfSec, "autosavedoc", self.CNF_INT, self.autoSaveDoc
)
## Editor
# Editor
cnfSec = "Editor"
self.textFont = self._parseLine(
cnfParse, cnfSec, "textfont", self.CNF_STR, self.textFont
@@ -505,6 +582,15 @@ class Config:
self.fmtDoubleQuotes = self._parseLine(
cnfParse, cnfSec, "fmtdoublequote", self.CNF_S_LST, self.fmtDoubleQuotes
)
self.fmtPadBefore = self._parseLine(
cnfParse, cnfSec, "fmtpadbefore", self.CNF_STR, self.fmtPadBefore
)
self.fmtPadAfter = self._parseLine(
cnfParse, cnfSec, "fmtpadafter", self.CNF_STR, self.fmtPadAfter
)
self.fmtPadThin = self._parseLine(
cnfParse, cnfSec, "fmtpadthin", self.CNF_BOOL, self.fmtPadThin
)
self.spellTool = self._parseLine(
cnfParse, cnfSec, "spelltool", self.CNF_STR, self.spellTool
)
@@ -517,6 +603,9 @@ class Config:
self.showLineEndings = self._parseLine(
cnfParse, cnfSec, "showlineendings", self.CNF_BOOL, self.showLineEndings
)
self.showMultiSpaces = self._parseLine(
cnfParse, cnfSec, "showmultispaces", self.CNF_BOOL, self.showMultiSpaces
)
self.bigDocLimit = self._parseLine(
cnfParse, cnfSec, "bigdoclimit", self.CNF_INT, self.bigDocLimit
)
@@ -542,7 +631,7 @@ class Config:
cnfParse, cnfSec, "useridletime", self.CNF_INT, self.userIdleTime
)
## Backup
# Backup
cnfSec = "Backup"
self.backupPath = self._parseLine(
cnfParse, cnfSec, "backuppath", self.CNF_STR, self.backupPath
@@ -554,7 +643,7 @@ class Config:
cnfParse, cnfSec, "askbeforebackup", self.CNF_BOOL, self.askBeforeBackup
)
## State
# State
cnfSec = "State"
self.showRefPanel = self._parseLine(
cnfParse, cnfSec, "showrefpanel", self.CNF_BOOL, self.showRefPanel
@@ -584,7 +673,7 @@ class Config:
cnfParse, cnfSec, "searchmatchcap", self.CNF_BOOL, self.searchMatchCap
)
## Path
# Path
cnfSec = "Path"
self.lastPath = self._parseLine(
cnfParse, cnfSec, "lastpath", self.CNF_STR, self.lastPath
@@ -615,7 +704,7 @@ class Config:
# Set options
## Main
# Main
cnfSec = "Main"
cnfParse.add_section(cnfSec)
cnfParse.set(cnfSec, "timestamp", formatTimeStamp(time()))
@@ -626,11 +715,13 @@ class Config:
cnfParse.set(cnfSec, "guifont", str(self.guiFont))
cnfParse.set(cnfSec, "guifontsize", str(self.guiFontSize))
cnfParse.set(cnfSec, "lastnotes", str(self.lastNotes))
cnfParse.set(cnfSec, "guilang", str(self.guiLang))
## Sizes
# Sizes
cnfSec = "Sizes"
cnfParse.add_section(cnfSec)
cnfParse.set(cnfSec, "geometry", self._packList(self.winGeometry))
cnfParse.set(cnfSec, "preferences", self._packList(self.prefGeometry))
cnfParse.set(cnfSec, "treecols", self._packList(self.treeColWidth))
cnfParse.set(cnfSec, "novelcols", self._packList(self.novelColWidth))
cnfParse.set(cnfSec, "projcols", self._packList(self.projColWidth))
@@ -642,13 +733,13 @@ class Config:
cnfParse.set(cnfSec, "hidevscroll", str(self.hideVScroll))
cnfParse.set(cnfSec, "hidehscroll", str(self.hideHScroll))
## Project
# Project
cnfSec = "Project"
cnfParse.add_section(cnfSec)
cnfParse.set(cnfSec, "autosaveproject", str(self.autoSaveProj))
cnfParse.set(cnfSec, "autosavedoc", str(self.autoSaveDoc))
## Editor
# Editor
cnfSec = "Editor"
cnfParse.add_section(cnfSec)
cnfParse.set(cnfSec, "textfont", str(self.textFont))
@@ -671,10 +762,14 @@ class Config:
cnfParse.set(cnfSec, "autoscrollpos", str(self.autoScrollPos))
cnfParse.set(cnfSec, "fmtsinglequote", self._packList(self.fmtSingleQuotes))
cnfParse.set(cnfSec, "fmtdoublequote", self._packList(self.fmtDoubleQuotes))
cnfParse.set(cnfSec, "fmtpadbefore", str(self.fmtPadBefore))
cnfParse.set(cnfSec, "fmtpadafter", str(self.fmtPadAfter))
cnfParse.set(cnfSec, "fmtpadthin", str(self.fmtPadThin))
cnfParse.set(cnfSec, "spelltool", str(self.spellTool))
cnfParse.set(cnfSec, "spellcheck", str(self.spellLanguage))
cnfParse.set(cnfSec, "showtabsnspaces", str(self.showTabsNSpaces))
cnfParse.set(cnfSec, "showlineendings", str(self.showLineEndings))
cnfParse.set(cnfSec, "showmultispaces", str(self.showMultiSpaces))
cnfParse.set(cnfSec, "bigdoclimit", str(self.bigDocLimit))
cnfParse.set(cnfSec, "showfullpath", str(self.showFullPath))
cnfParse.set(cnfSec, "highlightquotes", str(self.highlightQuotes))
@@ -684,14 +779,14 @@ class Config:
cnfParse.set(cnfSec, "stopwhenidle", str(self.stopWhenIdle))
cnfParse.set(cnfSec, "useridletime", str(self.userIdleTime))
## Backup
# Backup
cnfSec = "Backup"
cnfParse.add_section(cnfSec)
cnfParse.set(cnfSec, "backuppath", str(self.backupPath))
cnfParse.set(cnfSec, "backuponclose", str(self.backupOnClose))
cnfParse.set(cnfSec, "askbeforebackup", str(self.askBeforeBackup))
## State
# State
cnfSec = "State"
cnfParse.add_section(cnfSec)
cnfParse.set(cnfSec, "showrefpanel", str(self.showRefPanel))
@@ -704,7 +799,7 @@ class Config:
cnfParse.set(cnfSec, "searchnextfile", str(self.searchNextFile))
cnfParse.set(cnfSec, "searchmatchcap", str(self.searchMatchCap))
## Path
# Path
cnfSec = "Path"
cnfParse.add_section(cnfSec)
cnfParse.set(cnfSec, "lastpath", str(self.lastPath))
@@ -712,7 +807,7 @@ class Config:
# Write config file
cnfPath = os.path.join(self.confPath, self.confFile)
try:
with open(cnfPath, mode="w", encoding="utf8") as outFile:
with open(cnfPath, mode="w", encoding="utf-8") as outFile:
cnfParse.write(outFile)
self.confChanged = False
except Exception as e:
@@ -736,13 +831,13 @@ class Config:
if os.path.isfile(cacheFile):
try:
with open(cacheFile, mode="r", encoding="utf8") as inFile:
with open(cacheFile, mode="r", encoding="utf-8") as inFile:
theData = json.load(inFile)
for projPath in theData.keys():
theEntry = theData[projPath]
theTitle = ""
lastTime = 0
theEntry = theData[projPath]
theTitle = ""
lastTime = 0
wordCount = 0
if "title" in theEntry.keys():
theTitle = theEntry["title"]
@@ -751,9 +846,9 @@ class Config:
if "words" in theEntry.keys():
wordCount = int(theEntry["words"])
self.recentProj[projPath] = {
"title" : theTitle,
"time" : lastTime,
"words" : wordCount,
"title": theTitle,
"time": lastTime,
"words": wordCount,
}
except Exception as e:
@@ -774,7 +869,7 @@ class Config:
cacheTemp = os.path.join(self.dataPath, nwFiles.RECENT_FILE+"~")
try:
with open(cacheTemp, mode="w+", encoding="utf8") as outFile:
with open(cacheTemp, mode="w+", encoding="utf-8") as outFile:
json.dump(self.recentProj, outFile, indent=2)
except Exception as e:
self.hasError = True
@@ -792,9 +887,9 @@ class Config:
"""Add or update recent cache information o9n a given project.
"""
self.recentProj[os.path.abspath(projPath)] = {
"title" : projTitle,
"time" : int(saveTime),
"words" : int(wordCount),
"title": projTitle,
"time": int(saveTime),
"words": int(wordCount),
}
return True
@@ -803,10 +898,10 @@ class Config:
"""
if thePath in self.recentProj:
del self.recentProj[thePath]
logger.verbose("Removed recent: %s" % thePath)
logger.verbose("Removed recent: %s", thePath)
self.saveRecentCache()
else:
logger.error("Unknown recent: %s" % thePath)
logger.error("Unknown recent: %s", thePath)
return False
return True
@@ -851,6 +946,12 @@ class Config:
self.confChanged = True
return True
def setPreferencesSize(self, newWidth, newHeight):
self.prefGeometry[0] = int(newWidth/self.guiScale)
self.prefGeometry[1] = int(newHeight/self.guiScale)
self.confChanged = True
return True
def setTreeColWidths(self, colWidths):
self.treeColWidth = [int(x/self.guiScale) for x in colWidths]
self.confChanged = True
@@ -914,6 +1015,9 @@ class Config:
def getWinSize(self):
return [int(x*self.guiScale) for x in self.winGeometry]
def getPreferencesSize(self):
return [int(x*self.guiScale) for x in self.prefGeometry]
def getTreeColWidths(self):
return [int(x*self.guiScale) for x in self.treeColWidth]
@@ -1015,9 +1119,9 @@ class Config:
"""Cheks if we have the optional packages used by some features.
"""
try:
import enchant # noqa: F401
import enchant # noqa: F401
self.hasEnchant = True
logger.debug("Checking package 'pyenchant': Ok")
logger.debug("Checking package 'pyenchant': OK")
except Exception:
self.hasEnchant = False
logger.debug("Checking package 'pyenchant': Missing")
@@ -1025,7 +1129,7 @@ class Config:
assistPath = shutil.which("assistant")
self.hasAssistant = assistPath is not None
if self.hasAssistant:
logger.debug("Checking executable 'assistant': Ok")
logger.debug("Checking executable 'assistant': OK")
else:
logger.debug("Checking executable 'assistant': Missing")

451
nw/constants.py Normal file
View File

@@ -0,0 +1,451 @@
"""
novelWriter – Constants
=======================
Constants and maps for translating flags and enums to text
File History:
Created: 2019-04-28 [0.0.1]
This file is a part of novelWriter
Copyright 2018–2021, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP
from nw.enum import nwItemClass, nwItemLayout, nwItemType, nwOutline
def trConst(tString):
"""Wrapper function for locally translating constants.
"""
return QCoreApplication.translate("Constant", tString)
class nwConst():
# Date and Time Formats
FMT_TSTAMP = "%Y-%m-%d %H:%M:%S" # Default format
FMT_FSTAMP = "%Y-%m-%d %H.%M.%S" # FileName safe format
FMT_DSTAMP = "%Y-%m-%d" # Date only format
# Various Hard Limits
MAX_DEPTH = 30 # Maximum folder depth of a project
MAX_DOCSIZE = 5000000 # Maxium size of a single document
MAX_BUILDSIZE = 10000000 # Maxium size of a project build
# Spell Check Providers
SP_INTERNAL = "internal"
SP_ENCHANT = "enchant"
# END Class nwConst
class nwLists():
"""Lists used for grouping various other constants.
"""
# Regular user-accessible item types
REG_TYPES = {nwItemType.ROOT, nwItemType.FOLDER, nwItemType.FILE}
# Item classes where the full list of novel layouts are allowed
CLS_NOVEL = {nwItemClass.NOVEL, nwItemClass.ARCHIVE}
# Item classes which do not require items to have same class
FREE_CLASS = {nwItemClass.ARCHIVE, nwItemClass.TRASH}
# END Class nwLists
class nwRegEx():
FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_EB = r"(?<![\w\\])([\*]{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_ST = r"(?<![\w\\])([~]{2})(?![\s~])(.+?)(?<![\s\\])(\1)(?!\w)"
# END Class nwRegEx
class nwFiles():
PROJ_FILE = "nwProject.nwx"
PROJ_DICT = "wordlist.txt"
PROJ_LOCK = "nwProject.lock"
TOC_TXT = "ToC.txt"
SESS_STATS = "sessionStats.log"
INDEX_FILE = "tagsIndex.json"
OPTS_FILE = "guiOptions.json"
RECENT_FILE = "recentProjects.json"
BUILD_CACHE = "prevBuild.json"
# END Class nwFiles
class nwKeyWords:
TAG_KEY = "@tag"
POV_KEY = "@pov"
FOCUS_KEY = "@focus"
CHAR_KEY = "@char"
PLOT_KEY = "@plot"
TIME_KEY = "@time"
WORLD_KEY = "@location"
OBJECT_KEY = "@object"
ENTITY_KEY = "@entity"
CUSTOM_KEY = "@custom"
# Set of Valid Keys
VALID_KEYS = {
TAG_KEY, POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY,
WORLD_KEY, OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY
}
# Map from Keys to Item Class
KEY_CLASS = {
POV_KEY: nwItemClass.CHARACTER,
FOCUS_KEY: nwItemClass.CHARACTER,
CHAR_KEY: nwItemClass.CHARACTER,
PLOT_KEY: nwItemClass.PLOT,
TIME_KEY: nwItemClass.TIMELINE,
WORLD_KEY: nwItemClass.WORLD,
OBJECT_KEY: nwItemClass.OBJECT,
ENTITY_KEY: nwItemClass.ENTITY,
CUSTOM_KEY: nwItemClass.CUSTOM,
}
# END Class nwKeyWords
class nwLabels():
CLASS_NAME = {
nwItemClass.NO_CLASS: QT_TRANSLATE_NOOP("Constant", "None"),
nwItemClass.NOVEL: QT_TRANSLATE_NOOP("Constant", "Novel"),
nwItemClass.PLOT: QT_TRANSLATE_NOOP("Constant", "Plot"),
nwItemClass.CHARACTER: QT_TRANSLATE_NOOP("Constant", "Characters"),
nwItemClass.WORLD: QT_TRANSLATE_NOOP("Constant", "Locations"),
nwItemClass.TIMELINE: QT_TRANSLATE_NOOP("Constant", "Timeline"),
nwItemClass.OBJECT: QT_TRANSLATE_NOOP("Constant", "Objects"),
nwItemClass.ENTITY: QT_TRANSLATE_NOOP("Constant", "Entity"),
nwItemClass.CUSTOM: QT_TRANSLATE_NOOP("Constant", "Custom"),
nwItemClass.ARCHIVE: QT_TRANSLATE_NOOP("Constant", "Outtakes"),
nwItemClass.TRASH: QT_TRANSLATE_NOOP("Constant", "Trash"),
}
CLASS_FLAG = {
nwItemClass.NO_CLASS: "0",
nwItemClass.NOVEL: "N",
nwItemClass.PLOT: "P",
nwItemClass.CHARACTER: "C",
nwItemClass.WORLD: "L",
nwItemClass.TIMELINE: "T",
nwItemClass.OBJECT: "O",
nwItemClass.ENTITY: "E",
nwItemClass.CUSTOM: "X",
nwItemClass.ARCHIVE: "U",
nwItemClass.TRASH: "R",
}
CLASS_ICON = {
nwItemClass.NO_CLASS: "cls_none",
nwItemClass.NOVEL: "cls_novel",
nwItemClass.PLOT: "cls_plot",
nwItemClass.CHARACTER: "cls_character",
nwItemClass.WORLD: "cls_world",
nwItemClass.TIMELINE: "cls_timeline",
nwItemClass.OBJECT: "cls_object",
nwItemClass.ENTITY: "cls_entity",
nwItemClass.CUSTOM: "cls_custom",
nwItemClass.ARCHIVE: "cls_archive",
nwItemClass.TRASH: "cls_trash",
}
LAYOUT_NAME = {
nwItemLayout.NO_LAYOUT: QT_TRANSLATE_NOOP("Constant", "None"),
nwItemLayout.TITLE: QT_TRANSLATE_NOOP("Constant", "Title Page"),
nwItemLayout.BOOK: QT_TRANSLATE_NOOP("Constant", "Book"),
nwItemLayout.PAGE: QT_TRANSLATE_NOOP("Constant", "Plain Page"),
nwItemLayout.PARTITION: QT_TRANSLATE_NOOP("Constant", "Partition"),
nwItemLayout.UNNUMBERED: QT_TRANSLATE_NOOP("Constant", "Unnumbered"),
nwItemLayout.CHAPTER: QT_TRANSLATE_NOOP("Constant", "Chapter"),
nwItemLayout.SCENE: QT_TRANSLATE_NOOP("Constant", "Scene"),
nwItemLayout.NOTE: QT_TRANSLATE_NOOP("Constant", "Note"),
}
LAYOUT_FLAG = {
nwItemLayout.NO_LAYOUT: "Xo",
nwItemLayout.TITLE: "Tt",
nwItemLayout.BOOK: "Bk",
nwItemLayout.PAGE: "Pg",
nwItemLayout.PARTITION: "Pt",
nwItemLayout.UNNUMBERED: "Un",
nwItemLayout.CHAPTER: "Ch",
nwItemLayout.SCENE: "Sc",
nwItemLayout.NOTE: "Nt",
}
KEY_NAME = {
nwKeyWords.TAG_KEY: QT_TRANSLATE_NOOP("Constant", "Tag"),
nwKeyWords.POV_KEY: QT_TRANSLATE_NOOP("Constant", "Point of View"),
nwKeyWords.FOCUS_KEY: QT_TRANSLATE_NOOP("Constant", "Focus"),
nwKeyWords.CHAR_KEY: QT_TRANSLATE_NOOP("Constant", "Characters"),
nwKeyWords.PLOT_KEY: QT_TRANSLATE_NOOP("Constant", "Plot"),
nwKeyWords.TIME_KEY: QT_TRANSLATE_NOOP("Constant", "Timeline"),
nwKeyWords.WORLD_KEY: QT_TRANSLATE_NOOP("Constant", "Locations"),
nwKeyWords.OBJECT_KEY: QT_TRANSLATE_NOOP("Constant", "Objects"),
nwKeyWords.ENTITY_KEY: QT_TRANSLATE_NOOP("Constant", "Entities"),
nwKeyWords.CUSTOM_KEY: QT_TRANSLATE_NOOP("Constant", "Custom"),
}
OUTLINE_COLS = {
nwOutline.TITLE: QT_TRANSLATE_NOOP("Constant", "Title"),
nwOutline.LEVEL: QT_TRANSLATE_NOOP("Constant", "Level"),
nwOutline.LABEL: QT_TRANSLATE_NOOP("Constant", "Document"),
nwOutline.LINE: QT_TRANSLATE_NOOP("Constant", "Line"),
nwOutline.CCOUNT: QT_TRANSLATE_NOOP("Constant", "Chars"),
nwOutline.WCOUNT: QT_TRANSLATE_NOOP("Constant", "Words"),
nwOutline.PCOUNT: QT_TRANSLATE_NOOP("Constant", "Pars"),
nwOutline.POV: QT_TRANSLATE_NOOP("Constant", "POV"),
nwOutline.FOCUS: QT_TRANSLATE_NOOP("Constant", "Focus"),
nwOutline.CHAR: KEY_NAME[nwKeyWords.CHAR_KEY],
nwOutline.PLOT: KEY_NAME[nwKeyWords.PLOT_KEY],
nwOutline.TIME: KEY_NAME[nwKeyWords.TIME_KEY],
nwOutline.WORLD: KEY_NAME[nwKeyWords.WORLD_KEY],
nwOutline.OBJECT: KEY_NAME[nwKeyWords.OBJECT_KEY],
nwOutline.ENTITY: KEY_NAME[nwKeyWords.ENTITY_KEY],
nwOutline.CUSTOM: KEY_NAME[nwKeyWords.CUSTOM_KEY],
nwOutline.SYNOP: QT_TRANSLATE_NOOP("Constant", "Synopsis"),
}
# END Class nwLabels
class nwQuotes():
"""Allowed quotation marks.
Source: https://en.wikipedia.org/wiki/Quotation_mark
"""
SYMBOLS = {
"\u0027": QT_TRANSLATE_NOOP("Constant", "Straight single quotation mark"),
"\u0022": QT_TRANSLATE_NOOP("Constant", "Straight double quotation mark"),
"\u2018": QT_TRANSLATE_NOOP("Constant", "Left single quotation mark"),
"\u2019": QT_TRANSLATE_NOOP("Constant", "Right single quotation mark"),
"\u201a": QT_TRANSLATE_NOOP("Constant", "Single low-9 quotation mark"),
"\u201b": QT_TRANSLATE_NOOP("Constant", "Single high-reversed-9 quotation mark"),
"\u201c": QT_TRANSLATE_NOOP("Constant", "Left double quotation mark"),
"\u201d": QT_TRANSLATE_NOOP("Constant", "Right double quotation mark"),
"\u201e": QT_TRANSLATE_NOOP("Constant", "Double low-9 quotation mark"),
"\u201f": QT_TRANSLATE_NOOP("Constant", "Double high-reversed-9 quotation mark"),
"\u2e42": QT_TRANSLATE_NOOP("Constant", "Double low-reversed-9 quotation mark"),
"\u2039": QT_TRANSLATE_NOOP("Constant", "Single left-pointing angle quotation mark"),
"\u203a": QT_TRANSLATE_NOOP("Constant", "Single right-pointing angle quotation mark"),
"\u00ab": QT_TRANSLATE_NOOP("Constant", "Double left-pointing angle quotation mark"),
"\u00bb": QT_TRANSLATE_NOOP("Constant", "Double right-pointing angle quotation mark"),
"\u300c": QT_TRANSLATE_NOOP("Constant", "Left corner bracket"),
"\u300d": QT_TRANSLATE_NOOP("Constant", "Right corner bracket"),
"\u300e": QT_TRANSLATE_NOOP("Constant", "Left white corner bracket"),
"\u300f": QT_TRANSLATE_NOOP("Constant", "Right white corner bracket"),
}
# END Class nwQuotes
class nwUnicode:
"""Supported unicode character constants and their HTML equivalents.
"""
# Unicode Constants
# =================
# Quotation Marks
U_QUOT = "\u0022" # Quotation mark
U_APOS = "\u0027" # Apostrophe
U_LAQUO = "\u00ab" # Left-pointing double angle quotation mark
U_RAQUO = "\u00bb" # Right-pointing double angle quotation mark
U_LSQUO = "\u2018" # Left single quotation mark
U_RSQUO = "\u2019" # Right single quotation mark
U_SBQUO = "\u201a" # Single low-9 quotation mark
U_SUQUO = "\u201b" # Single high-reversed-9 quotation mark
U_LDQUO = "\u201c" # Left double quotation mark
U_RDQUO = "\u201d" # Right double quotation mark
U_BDQUO = "\u201e" # Double low-9 quotation mark
U_UDQUO = "\u201f" # Double high-reversed-9 quotation mark
U_LSAQUO = "\u2039" # Single left-pointing angle quotation mark
U_RSAQUO = "\u203a" # Single right-pointing angle quotation mark
U_BDRQUO = "\u2e42" # Double low-reversed-9 quotation mark
U_LCQUO = "\u300c" # Left corner bracket
U_RCQUO = "\u300d" # Right corner bracket
U_LWCQUO = "\u300e" # Left white corner bracket
U_RWCQUO = "\u300f" # Right white corner bracket
# Punctuation
U_FGDASH = "\u2012" # Figure dash
U_ENDASH = "\u2013" # Short dash
U_EMDASH = "\u2014" # Long dash
U_HBAR = "\u2015" # Horizontal bar
U_HELLIP = "\u2026" # Ellipsis
U_MAPOSS = "\u02bc" # Modifier letter single apostrophe
U_PRIME = "\u2032" # Prime
U_DPRIME = "\u2033" # Double prime
# Spaces and Lines
U_NBSP = "\u00a0" # Non-breaking space
U_THSP = "\u2009" # Thin space
U_THNBSP = "\u202f" # Thin non-breaking space
U_ENSP = "\u2002" # Short (en) space
U_EMSP = "\u2003" # Long (em) space
U_LSEP = "\u2028" # Line separator
U_PSEP = "\u2029" # Paragraph separator
# Symbols
U_CHECK = "\u2714" # Heavy check mark
U_CROSS = "\u2715" # Heavy cross mark
U_BULL = "\u2022" # List bullet
U_TRBULL = "\u2023" # Triangle bullet
U_HYBULL = "\u2043" # Hyphen bullet
U_FLOWER = "\u2055" # Flower punctuation mark
U_PERMIL = "\u2030" # Per mille sign
U_DEGREE = "\u00b0" # Degree symbol
U_MINUS = "\u2212" # Minus sign
U_TIMES = "\u00d7" # Multiplaction sign
U_DIVIDE = "\u00f7" # Division sign
# Arrows
U_UTRI = "\u25b2" # Up-pointing triangle
U_UTRIS = "\u25b4" # Up-pointing triangle, small
U_RTRI = "\u25b6" # Right-pointing triangle
U_RTRIS = "\u25b8" # Right-pointing triangle, small
U_DTRI = "\u25bc" # Down-pointing triangle
U_DTRIS = "\u25be" # Down-pointing triangle, small
U_LTRI = "\u25c0" # Left-pointing triangle
U_LTRIS = "\u25c2" # Left-pointing triangle, small
# HTML Equivalents
# ================
# Quotes
H_QUOT = "&quot;"
H_APOS = "&#39;"
H_LAQUO = "&laquo;"
H_RAQUO = "&raquo;"
H_LSQUO = "&lsquo;"
H_RSQUO = "&rsquo;"
H_SBQUO = "&sbquo;"
H_SUQUO = "&#8219;"
H_LDQUO = "&ldquo;"
H_RDQUO = "&rdquo;"
H_BDQUO = "&bdquo;"
H_UDQUO = "&#8223;"
H_LSAQUO = "&lsaquo;"
H_RSAQUO = "&rsaquo;"
H_BDRQUO = "&#11842;"
H_LCQUO = "&#12300;"
H_RCQUO = "&#12301;"
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_THSP = "&thinsp;"
H_THNBSP = "&#8239;"
H_ENSP = "&ensp;"
H_EMSP = "&emsp;"
# Symbols
H_CHECK = "&#10004;"
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;"
H_UTRIS = "&#9652;"
H_RTRI = "&#9654;"
H_RTRIS = "&#9656;"
H_DTRI = "&#9660;"
H_DTRIS = "&#9662;"
H_LTRI = "&#9664;"
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,31 +0,0 @@
# -*- coding: utf-8 -*-
from nw.constants.iso import isoLanguage, isoCountry
from nw.constants.constants import (
nwConst, nwLists, nwRegEx, nwFiles, nwKeyWords, nwLabels, nwQuotes,
nwUnicode, nwHtmlUnicode
)
from nw.constants.enum import (
nwAlert, nwDocAction, nwItemClass, nwItemLayout, nwItemType, nwOutline,
nwDocInsert
)
__all__ = [
"isoCountry",
"isoLanguage",
"nwConst",
"nwLists",
"nwRegEx",
"nwFiles",
"nwKeyWords",
"nwLabels",
"nwQuotes",
"nwUnicode",
"nwHtmlUnicode",
"nwAlert",
"nwDocAction",
"nwItemClass",
"nwItemLayout",
"nwItemType",
"nwOutline",
"nwDocInsert",
]

View File

@@ -1,437 +0,0 @@
# -*- coding: utf-8 -*-
"""
novelWriter – Constants
=======================
Constants and maps for translating flags and enums to text
File History:
Created: 2019-04-28 [0.0.1]
This file is a part of novelWriter
Copyright 2018–2021, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from nw.constants.enum import (
nwItemClass, nwItemLayout, nwItemType, nwOutline
)
class nwConst():
# Date and Time Formats
FMT_TSTAMP = "%Y-%m-%d %H:%M:%S" # Default format
FMT_FSTAMP = "%Y-%m-%d %H.%M.%S" # FileName safe format
FMT_DSTAMP = "%Y-%m-%d" # Date only format
# Various Hard Limits
MAX_DEPTH = 30 # Maximum folder depth of a project
MAX_DOCSIZE = 5000000 # Maxium size of a single document
MAX_BUILDSIZE = 10000000 # Maxium size of a project build
# Spell Check Providers
SP_INTERNAL = "internal"
SP_ENCHANT = "enchant"
# END Class nwConst
class nwLists():
"""Lists used for grouping various other constants.
"""
# Regular user-accessible item types
REG_TYPES = {nwItemType.ROOT, nwItemType.FOLDER, nwItemType.FILE}
# Item classes where the full list of novel layouts are allowed
CLS_NOVEL = {nwItemClass.NOVEL, nwItemClass.ARCHIVE}
# Item classes which do not require items to have same class
FREE_CLASS = {nwItemClass.ARCHIVE, nwItemClass.TRASH}
# END Class nwLists
class nwRegEx():
FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_EB = r"(?<![\w\\])([\*]{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_ST = r"(?<![\w\\])([~]{2})(?![\s~])(.+?)(?<![\s\\])(\1)(?!\w)"
# END Class nwRegEx
class nwFiles():
PROJ_FILE = "nwProject.nwx"
PROJ_DICT = "wordlist.txt"
PROJ_LOCK = "nwProject.lock"
TOC_TXT = "ToC.txt"
SESS_STATS = "sessionStats.log"
INDEX_FILE = "tagsIndex.json"
OPTS_FILE = "guiOptions.json"
RECENT_FILE = "recentProjects.json"
BUILD_CACHE = "prevBuild.json"
# END Class nwFiles
class nwKeyWords:
TAG_KEY = "@tag"
POV_KEY = "@pov"
FOCUS_KEY = "@focus"
CHAR_KEY = "@char"
PLOT_KEY = "@plot"
TIME_KEY = "@time"
WORLD_KEY = "@location"
OBJECT_KEY = "@object"
ENTITY_KEY = "@entity"
CUSTOM_KEY = "@custom"
# Set of Valid Keys
VALID_KEYS = {
TAG_KEY, POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY,
WORLD_KEY, OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY
}
# Map from Keys to Item Class
KEY_CLASS = {
POV_KEY : nwItemClass.CHARACTER,
FOCUS_KEY : nwItemClass.CHARACTER,
CHAR_KEY : nwItemClass.CHARACTER,
PLOT_KEY : nwItemClass.PLOT,
TIME_KEY : nwItemClass.TIMELINE,
WORLD_KEY : nwItemClass.WORLD,
OBJECT_KEY : nwItemClass.OBJECT,
ENTITY_KEY : nwItemClass.ENTITY,
CUSTOM_KEY : nwItemClass.CUSTOM,
}
# END Class nwKeyWords
class nwLabels():
CLASS_NAME = {
nwItemClass.NO_CLASS : "None",
nwItemClass.NOVEL : "Novel",
nwItemClass.PLOT : "Plot",
nwItemClass.CHARACTER : "Characters",
nwItemClass.WORLD : "Locations",
nwItemClass.TIMELINE : "Timeline",
nwItemClass.OBJECT : "Objects",
nwItemClass.ENTITY : "Entity",
nwItemClass.CUSTOM : "Custom",
nwItemClass.ARCHIVE : "Outtakes",
nwItemClass.TRASH : "Trash",
}
CLASS_FLAG = {
nwItemClass.NO_CLASS : "0",
nwItemClass.NOVEL : "N",
nwItemClass.PLOT : "P",
nwItemClass.CHARACTER : "C",
nwItemClass.WORLD : "L",
nwItemClass.TIMELINE : "T",
nwItemClass.OBJECT : "O",
nwItemClass.ENTITY : "E",
nwItemClass.CUSTOM : "X",
nwItemClass.ARCHIVE : "U",
nwItemClass.TRASH : "R",
}
CLASS_ICON = {
nwItemClass.NO_CLASS : "cls_none",
nwItemClass.NOVEL : "cls_novel",
nwItemClass.PLOT : "cls_plot",
nwItemClass.CHARACTER : "cls_character",
nwItemClass.WORLD : "cls_world",
nwItemClass.TIMELINE : "cls_timeline",
nwItemClass.OBJECT : "cls_object",
nwItemClass.ENTITY : "cls_entity",
nwItemClass.CUSTOM : "cls_custom",
nwItemClass.ARCHIVE : "cls_archive",
nwItemClass.TRASH : "cls_trash",
}
LAYOUT_NAME = {
nwItemLayout.NO_LAYOUT : "None",
nwItemLayout.TITLE : "Title Page",
nwItemLayout.BOOK : "Book",
nwItemLayout.PAGE : "Plain Page",
nwItemLayout.PARTITION : "Partition",
nwItemLayout.UNNUMBERED : "Unnumbered",
nwItemLayout.CHAPTER : "Chapter",
nwItemLayout.SCENE : "Scene",
nwItemLayout.NOTE : "Note",
}
LAYOUT_FLAG = {
nwItemLayout.NO_LAYOUT : "Xo",
nwItemLayout.TITLE : "Tt",
nwItemLayout.BOOK : "Bk",
nwItemLayout.PAGE : "Pg",
nwItemLayout.PARTITION : "Pt",
nwItemLayout.UNNUMBERED : "Un",
nwItemLayout.CHAPTER : "Ch",
nwItemLayout.SCENE : "Sc",
nwItemLayout.NOTE : "Nt",
}
KEY_NAME = {
nwKeyWords.TAG_KEY : "Tag",
nwKeyWords.POV_KEY : "Point of View",
nwKeyWords.FOCUS_KEY : "Focus",
nwKeyWords.CHAR_KEY : "Characters",
nwKeyWords.PLOT_KEY : "Plot",
nwKeyWords.TIME_KEY : "Timeline",
nwKeyWords.WORLD_KEY : "Locations",
nwKeyWords.OBJECT_KEY : "Objects",
nwKeyWords.ENTITY_KEY : "Entities",
nwKeyWords.CUSTOM_KEY : "Custom",
}
OUTLINE_COLS = {
nwOutline.TITLE : "Title",
nwOutline.LEVEL : "Level",
nwOutline.LABEL : "Document",
nwOutline.LINE : "Line",
nwOutline.CCOUNT : "Chars",
nwOutline.WCOUNT : "Words",
nwOutline.PCOUNT : "Pars",
nwOutline.POV : "POV",
nwOutline.FOCUS : "Focus",
nwOutline.CHAR : KEY_NAME[nwKeyWords.CHAR_KEY],
nwOutline.PLOT : KEY_NAME[nwKeyWords.PLOT_KEY],
nwOutline.TIME : KEY_NAME[nwKeyWords.TIME_KEY],
nwOutline.WORLD : KEY_NAME[nwKeyWords.WORLD_KEY],
nwOutline.OBJECT : KEY_NAME[nwKeyWords.OBJECT_KEY],
nwOutline.ENTITY : KEY_NAME[nwKeyWords.ENTITY_KEY],
nwOutline.CUSTOM : KEY_NAME[nwKeyWords.CUSTOM_KEY],
nwOutline.SYNOP : "Synopsis",
}
# END Class nwLabels
class nwQuotes():
"""Allowed quotation marks.
Source: https://en.wikipedia.org/wiki/Quotation_mark
"""
SYMBOLS = {
"\u0027" : "Straight single quotation mark",
"\u0022" : "Straight double quotation mark",
"\u2018" : "Left single quotation mark",
"\u2019" : "Right single quotation mark",
"\u201a" : "Single low-9 quotation mark",
"\u201b" : "Single high-reversed-9 quotation mark",
"\u201c" : "Left double quotation mark",
"\u201d" : "Right double quotation mark",
"\u201e" : "Double low-9 quotation mark",
"\u201f" : "Double high-reversed-9 quotation mark",
"\u2e42" : "Double low-reversed-9 quotation mark",
"\u2039" : "Single left-pointing angle quotation mark",
"\u203a" : "Single right-pointing angle quotation mark",
"\u00ab" : "Left-pointing double angle quotation mark",
"\u00bb" : "Right-pointing double angle quotation mark",
"\u300c" : "Left corner bracket",
"\u300d" : "Right corner bracket",
"\u300e" : "Left white corner bracket",
"\u300f" : "Right white corner bracket",
}
# END Class nwQuotes
class nwUnicode:
"""Supported unicode character constants and their HTML equivalents.
"""
# Unicode Constants
# =================
## Quotation Marks
U_QUOT = "\u0022" # Quotation mark
U_APOS = "\u0027" # Apostrophe
U_LAQUO = "\u00ab" # Left-pointing double angle quotation mark
U_RAQUO = "\u00bb" # Right-pointing double angle quotation mark
U_LSQUO = "\u2018" # Left single quotation mark
U_RSQUO = "\u2019" # Right single quotation mark
U_SBQUO = "\u201a" # Single low-9 quotation mark
U_SUQUO = "\u201b" # Single high-reversed-9 quotation mark
U_LDQUO = "\u201c" # Left double quotation mark
U_RDQUO = "\u201d" # Right double quotation mark
U_BDQUO = "\u201e" # Double low-9 quotation mark
U_UDQUO = "\u201f" # Double high-reversed-9 quotation mark
U_LSAQUO = "\u2039" # Single left-pointing angle quotation mark
U_RSAQUO = "\u203a" # Single right-pointing angle quotation mark
U_BDRQUO = "\u2e42" # Double low-reversed-9 quotation mark
U_LCQUO = "\u300c" # Left corner bracket
U_RCQUO = "\u300d" # Right corner bracket
U_LWCQUO = "\u300e" # Left white corner bracket
U_RWCQUO = "\u300f" # Right white corner bracket
## Punctuation
U_FGDASH = "\u2012" # Figure dash
U_ENDASH = "\u2013" # Short dash
U_EMDASH = "\u2014" # Long dash
U_HBAR = "\u2015" # Horizontal bar
U_HELLIP = "\u2026" # Ellipsis
U_MAPOSS = "\u02bc" # Modifier letter single apostrophe
U_PRIME = "\u2032" # Prime
U_DPRIME = "\u2033" # Double prime
## Spaces and Lines
U_NBSP = "\u00a0" # Non-breaking space
U_THSP = "\u2009" # Thin space
U_THNBSP = "\u202f" # Thin non-breaking space
U_ENSP = "\u2002" # Short (en) space
U_EMSP = "\u2003" # Long (em) space
U_LSEP = "\u2028" # Line separator
U_PSEP = "\u2029" # Paragraph separator
## Symbols
U_CHECK = "\u2714" # Heavy check mark
U_CROSS = "\u2715" # Heavy cross mark
U_BULL = "\u2022" # List bullet
U_TRBULL = "\u2023" # Triangle bullet
U_HYBULL = "\u2043" # Hyphen bullet
U_FLOWER = "\u2055" # Flower punctuation mark
U_PERMIL = "\u2030" # Per mille sign
U_DEGREE = "\u00b0" # Degree symbol
U_MINUS = "\u2212" # Minus sign
U_TIMES = "\u00d7" # Multiplaction sign
U_DIVIDE = "\u00f7" # Division sign
## Arrows
U_UTRI = "\u25b2" # Up-pointing triangle
U_UTRIS = "\u25b4" # Up-pointing triangle, small
U_RTRI = "\u25b6" # Right-pointing triangle
U_RTRIS = "\u25b8" # Right-pointing triangle, small
U_DTRI = "\u25bc" # Down-pointing triangle
U_DTRIS = "\u25be" # Down-pointing triangle, small
U_LTRI = "\u25c0" # Left-pointing triangle
U_LTRIS = "\u25c2" # Left-pointing triangle, small
# HTML Equivalents
# ================
## Quotes
H_QUOT = "&quot;"
H_APOS = "&#39;"
H_LAQUO = "&laquo;"
H_RAQUO = "&raquo;"
H_LSQUO = "&lsquo;"
H_RSQUO = "&rsquo;"
H_SBQUO = "&sbquo;"
H_SUQUO = "&#8219;"
H_LDQUO = "&ldquo;"
H_RDQUO = "&rdquo;"
H_BDQUO = "&bdquo;"
H_UDQUO = "&#8223;"
H_LSAQUO = "&lsaquo;"
H_RSAQUO = "&rsaquo;"
H_BDRQUO = "&#11842;"
H_LCQUO = "&#12300;"
H_RCQUO = "&#12301;"
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_THSP = "&thinsp;"
H_THNBSP = "&#8239;"
H_ENSP = "&ensp;"
H_EMSP = "&emsp;"
## Symbols
H_CHECK = "&#10004;"
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;"
H_UTRIS = "&#9652;"
H_RTRI = "&#9654;"
H_RTRIS = "&#9656;"
H_DTRI = "&#9660;"
H_DTRIS = "&#9662;"
H_LTRI = "&#9664;"
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,472 +0,0 @@
# -*- coding: utf-8 -*-
"""
novelWriter – ISO Codes
=======================
Handles translating language codes to language names
File History:
Created: 2019-11-05 [0.4.0]
This file is a part of novelWriter
Copyright 2018–2021, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
class isoLanguage():
ISO_639_1 = {
"aa" : "Afar",
"ab" : "Abkhazian",
"ae" : "Avestan",
"af" : "Afrikaans",
"ak" : "Akan",
"am" : "Amharic",
"an" : "Aragonese",
"ar" : "Arabic",
"as" : "Assamese",
"av" : "Avaric",
"ay" : "Aymara",
"az" : "Azerbaijani",
"ba" : "Bashkir",
"be" : "Belarusian",
"bg" : "Bulgarian",
"bh" : "Bihari languages",
"bi" : "Bislama",
"bm" : "Bambara",
"bn" : "Bengali",
"bo" : "Tibetan",
"br" : "Breton",
"bs" : "Bosnian",
"ca" : "Catalan",
"ce" : "Chechen",
"ch" : "Chamorro",
"co" : "Corsican",
"cr" : "Cree",
"cs" : "Czech",
"cu" : "Church Slavic",
"cv" : "Chuvash",
"cy" : "Welsh",
"da" : "Danish",
"de" : "German",
"dv" : "Divehi",
"dz" : "Dzongkha",
"ee" : "Ewe",
"el" : "Modern Greek",
"en" : "English",
"eo" : "Esperanto",
"es" : "Spanish",
"et" : "Estonian",
"eu" : "Basque",
"fa" : "Persian",
"ff" : "Fulah",
"fi" : "Finnish",
"fj" : "Fijian",
"fo" : "Faroese",
"fr" : "French",
"fy" : "Western Frisian",
"ga" : "Irish",
"gd" : "Gaelic",
"gl" : "Galician",
"gn" : "Guarani",
"gu" : "Gujarati",
"gv" : "Manx",
"ha" : "Hausa",
"he" : "Hebrew",
"hi" : "Hindi",
"ho" : "Hiri Motu",
"hr" : "Croatian",
"ht" : "Haitian",
"hu" : "Hungarian",
"hy" : "Armenian",
"hz" : "Herero",
"ia" : "Interlingua",
"id" : "Indonesian",
"ie" : "Interlingue",
"ig" : "Igbo",
"ii" : "Sichuan Yi",
"ik" : "Inupiaq",
"io" : "Ido",
"is" : "Icelandic",
"it" : "Italian",
"iu" : "Inuktitut",
"ja" : "Japanese",
"jv" : "Javanese",
"ka" : "Georgian",
"kg" : "Kongo",
"ki" : "Kikuyu",
"kj" : "Kuanyama",
"kk" : "Kazakh",
"kl" : "Kalaallisut",
"km" : "Central Khmer",
"kn" : "Kannada",
"ko" : "Korean",
"kr" : "Kanuri",
"ks" : "Kashmiri",
"ku" : "Kurdish",
"kv" : "Komi",
"kw" : "Cornish",
"ky" : "Kirghiz",
"la" : "Latin",
"lb" : "Luxembourgish",
"lg" : "Ganda",
"li" : "Limburgan",
"ln" : "Lingala",
"lo" : "Lao",
"lt" : "Lithuanian",
"lu" : "Luba-Katanga",
"lv" : "Latvian",
"mg" : "Malagasy",
"mh" : "Marshallese",
"mi" : "Maori",
"mk" : "Macedonian",
"ml" : "Malayalam",
"mn" : "Mongolian",
"mr" : "Marathi",
"ms" : "Malay",
"mt" : "Maltese",
"my" : "Burmese",
"na" : "Nauru",
"nb" : "Norwegian Bokmål",
"nd" : "North Ndebele",
"ne" : "Nepali",
"ng" : "Ndonga",
"nl" : "Dutch",
"nn" : "Norwegian Nynorsk",
"no" : "Norwegian",
"nr" : "South Ndebele",
"nv" : "Navajo",
"ny" : "Chichewa",
"oc" : "Occitan",
"oj" : "Ojibwa",
"om" : "Oromo",
"or" : "Oriya",
"os" : "Ossetian",
"pa" : "Panjabi",
"pi" : "Pali",
"pl" : "Polish",
"ps" : "Pushto",
"pt" : "Portuguese",
"qu" : "Quechua",
"rm" : "Romansh",
"rn" : "Rundi",
"ro" : "Romanian",
"ru" : "Russian",
"rw" : "Kinyarwanda",
"sa" : "Sanskrit",
"sc" : "Sardinian",
"sd" : "Sindhi",
"se" : "Northern Sami",
"sg" : "Sango",
"si" : "Sinhala",
"sk" : "Slovak",
"sl" : "Slovenian",
"sm" : "Samoan",
"sn" : "Shona",
"so" : "Somali",
"sq" : "Albanian",
"sr" : "Serbian",
"ss" : "Swati",
"st" : "Southern Sotho",
"su" : "Sundanese",
"sv" : "Swedish",
"sw" : "Swahili",
"ta" : "Tamil",
"te" : "Telugu",
"tg" : "Tajik",
"th" : "Thai",
"ti" : "Tigrinya",
"tk" : "Turkmen",
"tl" : "Tagalog",
"tn" : "Tswana",
"to" : "Tonga",
"tr" : "Turkish",
"ts" : "Tsonga",
"tt" : "Tatar",
"tw" : "Twi",
"ty" : "Tahitian",
"ug" : "Uighur",
"uk" : "Ukrainian",
"ur" : "Urdu",
"uz" : "Uzbek",
"ve" : "Venda",
"vi" : "Vietnamese",
"vo" : "Volapük",
"wa" : "Walloon",
"wo" : "Wolof",
"xh" : "Xhosa",
"yi" : "Yiddish",
"yo" : "Yoruba",
"za" : "Zhuang",
"zh" : "Chinese",
"zu" : "Zulu",
}
# END Class isoLanguage
class isoCountry():
ISO_3166_1_alpha_2 = {
"AD" : "Andorra",
"AE" : "United Arab Emirates",
"AF" : "Afghanistan",
"AG" : "Antigua and Barbuda",
"AI" : "Anguilla",
"AL" : "Albania",
"AM" : "Armenia",
"AO" : "Angola",
"AQ" : "Antarctica",
"AR" : "Argentina",
"AS" : "American Samoa",
"AT" : "Austria",
"AU" : "Australia",
"AW" : "Aruba",
"AX" : "Åland Islands",
"AZ" : "Azerbaijan",
"BA" : "Bosnia and Herzegovina",
"BB" : "Barbados",
"BD" : "Bangladesh",
"BE" : "Belgium",
"BF" : "Burkina Faso",
"BG" : "Bulgaria",
"BH" : "Bahrain",
"BI" : "Burundi",
"BJ" : "Benin",
"BL" : "Saint Barthélemy",
"BM" : "Bermuda",
"BN" : "Brunei Darussalam",
"BO" : "Plurinational State of Bolivia",
"BQ" : "Sint Eustatius and Saba Bonaire",
"BR" : "Brazil",
"BS" : "Bahamas",
"BT" : "Bhutan",
"BV" : "Bouvet Island",
"BW" : "Botswana",
"BY" : "Belarus",
"BZ" : "Belize",
"CA" : "Canada",
"CC" : "Cocos (Keeling) Islands",
"CD" : "The Democratic Republic of the Congo",
"CF" : "Central African Republic",
"CG" : "Congo",
"CH" : "Switzerland",
"CI" : "Côte d'Ivoire",
"CK" : "Cook Islands",
"CL" : "Chile",
"CM" : "Cameroon",
"CN" : "China",
"CO" : "Colombia",
"CR" : "Costa Rica",
"CU" : "Cuba",
"CV" : "Cape Verde",
"CW" : "Curaçao",
"CX" : "Christmas Island",
"CY" : "Cyprus",
"CZ" : "Czech Republic",
"DE" : "Germany",
"DJ" : "Djibouti",
"DK" : "Denmark",
"DM" : "Dominica",
"DO" : "Dominican Republic",
"DZ" : "Algeria",
"EC" : "Ecuador",
"EE" : "Estonia",
"EG" : "Egypt",
"EH" : "Western Sahara",
"ER" : "Eritrea",
"ES" : "Spain",
"ET" : "Ethiopia",
"FI" : "Finland",
"FJ" : "Fiji",
"FK" : "Falkland Islands (Malvinas)",
"FM" : "Federated States of Micronesia",
"FO" : "Faroe Islands",
"FR" : "France",
"GA" : "Gabon",
"GB" : "United Kingdom",
"GD" : "Grenada",
"GE" : "Georgia",
"GF" : "French Guiana",
"GG" : "Guernsey",
"GH" : "Ghana",
"GI" : "Gibraltar",
"GL" : "Greenland",
"GM" : "Gambia",
"GN" : "Guinea",
"GP" : "Guadeloupe",
"GQ" : "Equatorial Guinea",
"GR" : "Greece",
"GS" : "South Georgia and the South Sandwich Islands",
"GT" : "Guatemala",
"GU" : "Guam",
"GW" : "Guinea-Bissau",
"GY" : "Guyana",
"HK" : "Hong Kong",
"HM" : "Heard Island and McDonald Islands",
"HN" : "Honduras",
"HR" : "Croatia",
"HT" : "Haiti",
"HU" : "Hungary",
"ID" : "Indonesia",
"IE" : "Ireland",
"IL" : "Israel",
"IM" : "Isle of Man",
"IN" : "India",
"IO" : "British Indian Ocean Territory",
"IQ" : "Iraq",
"IR" : "Islamic Republic of Iran",
"IS" : "Iceland",
"IT" : "Italy",
"JE" : "Jersey",
"JM" : "Jamaica",
"JO" : "Jordan",
"JP" : "Japan",
"KE" : "Kenya",
"KG" : "Kyrgyzstan",
"KH" : "Cambodia",
"KI" : "Kiribati",
"KM" : "Comoros",
"KN" : "Saint Kitts and Nevis",
"KP" : "Democratic People's Republic of Korea",
"KR" : "Republic of Korea",
"KW" : "Kuwait",
"KY" : "Cayman Islands",
"KZ" : "Kazakhstan",
"LA" : "Lao People's Democratic Republic",
"LB" : "Lebanon",
"LC" : "Saint Lucia",
"LI" : "Liechtenstein",
"LK" : "Sri Lanka",
"LR" : "Liberia",
"LS" : "Lesotho",
"LT" : "Lithuania",
"LU" : "Luxembourg",
"LV" : "Latvia",
"LY" : "Libya",
"MA" : "Morocco",
"MC" : "Monaco",
"MD" : "Republic of Moldova",
"ME" : "Montenegro",
"MF" : "Saint Martin (French part)",
"MG" : "Madagascar",
"MH" : "Marshall Islands",
"MK" : "The Former Yugoslav Republic of Macedonia",
"ML" : "Mali",
"MM" : "Myanmar",
"MN" : "Mongolia",
"MO" : "Macao",
"MP" : "Northern Mariana Islands",
"MQ" : "Martinique",
"MR" : "Mauritania",
"MS" : "Montserrat",
"MT" : "Malta",
"MU" : "Mauritius",
"MV" : "Maldives",
"MW" : "Malawi",
"MX" : "Mexico",
"MY" : "Malaysia",
"MZ" : "Mozambique",
"NA" : "Namibia",
"NC" : "New Caledonia",
"NE" : "Niger",
"NF" : "Norfolk Island",
"NG" : "Nigeria",
"NI" : "Nicaragua",
"NL" : "Netherlands",
"NO" : "Norway",
"NP" : "Nepal",
"NR" : "Nauru",
"NU" : "Niue",
"NZ" : "New Zealand",
"OM" : "Oman",
"PA" : "Panama",
"PE" : "Peru",
"PF" : "French Polynesia",
"PG" : "Papua New Guinea",
"PH" : "Philippines",
"PK" : "Pakistan",
"PL" : "Poland",
"PM" : "Saint Pierre and Miquelon",
"PN" : "Pitcairn",
"PR" : "Puerto Rico",
"PS" : "State of Palestine",
"PT" : "Portugal",
"PW" : "Palau",
"PY" : "Paraguay",
"QA" : "Qatar",
"RE" : "Réunion",
"RO" : "Romania",
"RS" : "Serbia",
"RU" : "Russian Federation",
"RW" : "Rwanda",
"SA" : "Saudi Arabia",
"SB" : "Solomon Islands",
"SC" : "Seychelles",
"SD" : "Sudan",
"SE" : "Sweden",
"SG" : "Singapore",
"SH" : "Saint Helena, Ascension and Tristan da Cunha",
"SI" : "Slovenia",
"SJ" : "Svalbard and Jan Mayen",
"SK" : "Slovakia",
"SL" : "Sierra Leone",
"SM" : "San Marino",
"SN" : "Senegal",
"SO" : "Somalia",
"SR" : "Suriname",
"SS" : "South Sudan",
"ST" : "Sao Tome and Principe",
"SV" : "El Salvador",
"SX" : "Sint Maarten",
"SY" : "Syrian Arab Republic",
"SZ" : "Swaziland",
"TC" : "Turks and Caicos Islands",
"TD" : "Chad",
"TF" : "French Southern Territories",
"TG" : "Togo",
"TH" : "Thailand",
"TJ" : "Tajikistan",
"TK" : "Tokelau",
"TL" : "Timor-Leste",
"TM" : "Turkmenistan",
"TN" : "Tunisia",
"TO" : "Tonga",
"TR" : "Turkey",
"TT" : "Trinidad and Tobago",
"TV" : "Tuvalu",
"TW" : "Taiwan, Province of China",
"TZ" : "United Republic of Tanzania",
"UA" : "Ukraine",
"UG" : "Uganda",
"UM" : "United States Minor Outlying Islands",
"US" : "United States",
"UY" : "Uruguay",
"UZ" : "Uzbekistan",
"VA" : "Holy See (Vatican City State)",
"VC" : "Saint Vincent and the Grenadines",
"VE" : "Bolivarian Republic of Venezuela",
"VG" : "British Virgin Islands",
"VI" : "U.S. Virgin Islands",
"VN" : "Viet Nam",
"VU" : "Vanuatu",
"WF" : "Wallis and Futuna",
"WS" : "Samoa",
"YE" : "Yemen",
"YT" : "Mayotte",
"ZA" : "South Africa",
"ZM" : "Zambia",
"ZW" : "Zimbabwe",
}
# END Class isoCountry

View File

@@ -1,18 +1,34 @@
# -*- coding: utf-8 -*-
"""
novelWriter – Core Init
=======================
This file is a part of novelWriter
Copyright 2018–2021, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from nw.core.document import NWDoc
from nw.core.index import NWIndex
from nw.core.index import NWIndex, countWords
from nw.core.project import NWProject
from nw.core.spellcheck import NWSpellCheck, NWSpellEnchant, NWSpellSimple
from nw.core.tohtml import ToHtml
from nw.core.toodt import ToOdt
from nw.core.tomd import ToMarkdown
from nw.core.tools import countWords, numberToRoman, numberToWord
__all__ = [
"countWords",
"numberToRoman",
"numberToWord",
"NWDoc",
"NWIndex",
"NWProject",

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – Project Document
==============================
@@ -27,27 +26,30 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
import os
from nw.constants import nwAlert
from nw.enum import nwItemLayout, nwItemClass
from nw.common import isHandle
from nw.constants import nwItemLayout, nwItemClass
logger = logging.getLogger(__name__)
class NWDoc():
def __init__(self, theProject, theParent):
def __init__(self, theProject, theHandle):
self.theProject = theProject
self.theParent = theParent
# Internal Variables
self._theItem = None # The currently open item
self._docHandle = None # The handle of the currently open item
self._fileLoc = None # The file location of the currently open item
self._docMeta = {} # The meta data of the currently open item
self._theItem = None # The currently open item
self._docHandle = None # The handle of the currently open item
self._fileLoc = None # The file location of the currently open item
self._docMeta = {} # The meta data of the currently open item
self._docError = "" # The latest encountered IO error
# Internal Mapping
self.makeAlert = self.theParent.makeAlert
if isHandle(theHandle):
self._docHandle = theHandle
if self._docHandle is not None:
self._theItem = self.theProject.projTree[theHandle]
return
@@ -55,39 +57,23 @@ class NWDoc():
# Class Methods
##
def clearDocument(self):
"""Clear the document contents.
"""
self._theItem = None
self._docHandle = None
self._fileLoc = None
self._docMeta = {}
return
def openDocument(self, tHandle, showStatus=True, isOrphan=False):
"""Open a document from handle, capturing potential file system
errors and parse meta data. If the document doesn't exist on
disk, return an empty string. If something went wrong, return
def readDocument(self, isOrphan=False):
"""Read a document from set handle, capturing potential file
system errors and parse meta data. If the document doesn't exist
on disk, return an empty string. If something went wrong, return
None.
"""
if not isHandle(tHandle):
self._docError = ""
if self._docHandle is None:
logger.error("No document handle set")
return None
# Always clear first, since the object will often be reused.
self.clearDocument()
self._docHandle = tHandle
if not isOrphan:
self._theItem = self.theProject.projTree[tHandle]
else:
self._theItem = None
if self._theItem is None and not isOrphan:
self.clearDocument()
logger.error("Unknown novelWriter document")
return None
docFile = self._docHandle+".nwd"
logger.debug("Opening document %s" % docFile)
logger.debug("Opening document: %s", docFile)
docPath = os.path.join(self.theProject.projContent, docFile)
self._fileLoc = docPath
@@ -96,7 +82,7 @@ class NWDoc():
self._docMeta = {}
if os.path.isfile(docPath):
try:
with open(docPath, mode="r", encoding="utf8") as inFile:
with open(docPath, mode="r", encoding="utf-8") as inFile:
# Check the first <= 10 lines for metadata
for i in range(10):
@@ -111,34 +97,30 @@ class NWDoc():
theText += inFile.read()
except Exception as e:
self.makeAlert(["Failed to open document file.", str(e)], nwAlert.ERROR)
# Note: Document must be cleared in case of an io error,
# or else the auto-save or save will try to overwrite it
# with an empty file. Return None to alert the caller.
self.clearDocument()
self._docError = str(e)
return None
else:
# The document file does not exist, so we assume it's a new
# document and initialise an empty text string.
logger.debug("The requested document does not exist.")
return ""
if showStatus and not isOrphan:
self.theParent.setStatus("Opened Document: %s" % self._theItem.itemName)
return theText
def saveDocument(self, docText):
"""Save the document. The file is saved via a temp file in case
def writeDocument(self, docText):
"""Write the document. The file is saved via a temp file in case
of save failure. Returns True if successful, False if not.
"""
self._docError = ""
if self._docHandle is None:
logger.error("No document handle set")
return False
self.theProject.ensureFolderStructure()
docFile = self._docHandle+".nwd"
logger.debug("Saving document %s" % docFile)
logger.debug("Saving document: %s", docFile)
docPath = os.path.join(self.theProject.projContent, docFile)
docTemp = os.path.join(self.theProject.projContent, docFile+"~")
@@ -154,11 +136,11 @@ class NWDoc():
)
try:
with open(docTemp, mode="w", encoding="utf8") as outFile:
with open(docTemp, mode="w", encoding="utf-8") as outFile:
outFile.write(docMeta)
outFile.write(docText)
except Exception as e:
self.makeAlert(["Could not save document.", str(e)], nwAlert.ERROR)
self._docError = str(e)
return False
# If we're here, the file was successfully saved, so we can
@@ -167,19 +149,18 @@ class NWDoc():
os.unlink(docPath)
os.rename(docTemp, docPath)
if self._theItem is not None:
self.theParent.setStatus("Saved Document: %s" % self._theItem.itemName)
return True
def deleteDocument(self, tHandle):
def deleteDocument(self):
"""Permanently delete a document source file and related files
from the project data folder.
"""
if not isHandle(tHandle):
self._docError = ""
if self._docHandle is None:
logger.error("No document handle set")
return False
docFile = tHandle+".nwd"
docFile = self._docHandle+".nwd"
chkList = []
chkList.append(os.path.join(self.theProject.projContent, docFile))
@@ -189,9 +170,9 @@ class NWDoc():
if os.path.isfile(chkFile):
try:
os.unlink(chkFile)
logger.debug("Deleted: %s" % chkFile)
logger.debug("Deleted: %s", chkFile)
except Exception as e:
self.makeAlert(["Could not delete document file.", str(e)], nwAlert.ERROR)
self._docError = str(e)
return False
return True
@@ -221,6 +202,11 @@ class NWDoc():
return theName, theParent, theClass, theLayout
def getError(self):
"""Return the last recorded exception.
"""
return self._docError
##
# Internal Functions
##
@@ -251,7 +237,7 @@ class NWDoc():
self._docMeta["layout"] = nwItemLayout[metaBits[1]]
else:
logger.debug("Ignoring meta data: '%s'" % metaLine.strip())
logger.debug("Ignoring meta data: '%s'", metaLine.strip())
return

View File

@@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
"""
novelWriter – Project Index
===========================
Data class for the project index of tags, headers and references
File History:
Created: 2019-05-27 [0.1.4]
Created: 2019-04-22 [0.0.1] countWords
Created: 2019-05-27 [0.1.4] NWIndex
This file is a part of novelWriter
Copyright 2018–2021, Veronica Berglyd Olsen
@@ -31,26 +31,24 @@ import os
from time import time
from nw.constants import (
nwFiles, nwKeyWords, nwItemType, nwItemClass, nwItemLayout, nwAlert
)
from nw.core.document import NWDoc
from nw.core.tools import countWords
from nw.enum import nwItemType, nwItemClass, nwItemLayout
from nw.common import isHandle, isTitleTag, isItemClass, isItemLayout
from nw.constants import nwFiles, nwKeyWords, nwUnicode
from nw.core.document import NWDoc
logger = logging.getLogger(__name__)
class NWIndex():
H_VALID = ("H0", "H1", "H2", "H3", "H4")
H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4}
def __init__(self, theProject, theParent):
def __init__(self, theProject):
# Internal
self.mainConf = nw.CONFIG
self.theProject = theProject
self.theParent = theParent
self.indexBroken = False
# Indices
@@ -87,7 +85,7 @@ class NWIndex():
def deleteHandle(self, tHandle):
"""Delete all entries of a given document handle.
"""
logger.debug("Removing item %s from the index" % tHandle)
logger.debug("Removing item '%s' from the index", tHandle)
delTags = []
for tTag in self._tagIndex:
@@ -109,7 +107,7 @@ class NWIndex():
moved from the archive or trash folders back into the active
project.
"""
logger.debug("Re-indexing item %s" % tHandle)
logger.debug("Re-indexing item '%s'", tHandle)
tItem = self.theProject.projTree[tHandle]
if tItem is None:
@@ -117,8 +115,8 @@ class NWIndex():
if tItem.itemType != nwItemType.FILE:
return False
theDoc = NWDoc(self.theProject, self.theParent)
theText = theDoc.openDocument(tHandle, showStatus=False)
theDoc = NWDoc(self.theProject, tHandle)
theText = theDoc.readDocument()
if theText:
self.scanText(tHandle, theText)
@@ -146,28 +144,24 @@ class NWIndex():
def loadIndex(self):
"""Load index from last session from the project meta folder.
"""
theData = {}
theData = {}
indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
if os.path.isfile(indexFile):
logger.debug("Loading index file")
try:
with open(indexFile, mode="r", encoding="utf8") as inFile:
with open(indexFile, mode="r", encoding="utf-8") as inFile:
theData = json.load(inFile)
except Exception:
logger.error("Failed to load index file")
nw.logException()
self.indexBroken = True
self.theParent.makeAlert(
"Could not load cached index file. Rebuilding index.",
nwAlert.WARN
)
return False
self._tagIndex = theData.get("tagIndex", {})
self._refIndex = theData.get("refIndex", {})
self._tagIndex = theData.get("tagIndex", {})
self._refIndex = theData.get("refIndex", {})
self._novelIndex = theData.get("novelIndex", {})
self._noteIndex = theData.get("noteIndex", {})
self._noteIndex = theData.get("noteIndex", {})
self._textCounts = theData.get("textCounts", {})
nowTime = round(time())
@@ -187,13 +181,13 @@ class NWIndex():
indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
try:
with open(indexFile, mode="w+", encoding="utf8") as outFile:
with open(indexFile, mode="w+", encoding="utf-8") as outFile:
json.dump({
"tagIndex" : self._tagIndex,
"refIndex" : self._refIndex,
"novelIndex" : self._novelIndex,
"noteIndex" : self._noteIndex,
"textCounts" : self._textCounts,
"tagIndex": self._tagIndex,
"refIndex": self._refIndex,
"novelIndex": self._novelIndex,
"noteIndex": self._noteIndex,
"textCounts": self._textCounts,
}, outFile, indent=2)
except Exception:
logger.error("Failed to save index file")
@@ -223,15 +217,11 @@ class NWIndex():
self.indexBroken = True
tEnd = time()
logger.debug("Index check took %.3f ms" % ((tEnd - tStart)*1000))
logger.debug("Index check took %.3f ms", (tEnd - tStart)*1000)
logger.debug("Index check complete")
if self.indexBroken:
self.clearIndex()
self.theParent.makeAlert(
"The project index is outdated or broken. Rebuilding index.",
nwAlert.WARN
)
return
@@ -249,16 +239,16 @@ class NWIndex():
theRoot = self.theProject.projTree.getRootItem(tHandle)
if theItem is None:
logger.info("Not indexing unknown item %s" % tHandle)
logger.info("Not indexing unknown item '%s'", tHandle)
return False
if theItem.itemType != nwItemType.FILE:
logger.info("Not indexing non-file item %s" % tHandle)
logger.info("Not indexing non-file item '%s'", tHandle)
return False
if theItem.itemLayout == nwItemLayout.NO_LAYOUT:
logger.info("Not indexing no-layout item %s" % tHandle)
logger.info("Not indexing no-layout item '%s'", tHandle)
return False
if theItem.itemParent is None:
logger.info("Not indexing orphaned item %s" % tHandle)
logger.info("Not indexing orphaned item '%s'", tHandle)
return False
# Run word counter for the whole text
@@ -267,23 +257,23 @@ class NWIndex():
# If the file is archived or trashed, we don't index the file itself
if self.theProject.projTree.isTrashRoot(theItem.itemParent):
logger.info("Not indexing trash item %s" % tHandle)
logger.info("Not indexing trash item '%s'", tHandle)
return False
if theRoot.itemClass == nwItemClass.ARCHIVE:
logger.info("Not indexing archived item %s" % tHandle)
logger.info("Not indexing archived item '%s'", tHandle)
return False
itemClass = theItem.itemClass
itemLayout = theItem.itemLayout
logger.debug("Indexing item with handle %s" % tHandle)
logger.debug("Indexing item with handle '%s'", tHandle)
# Check file type, and reset its old index
# Also add a dummy entry T000000 in case the file has no title
# Also add a default entry T000000 in case the file has no title
self._refIndex[tHandle] = {}
self._refIndex[tHandle]["T000000"] = {
"tags" : [],
"updated" : round(time()),
"tags": [],
"updated": round(time()),
}
if itemLayout == nwItemLayout.NOTE:
self._novelIndex.pop(tHandle, None)
@@ -302,13 +292,12 @@ class NWIndex():
for aTag in clearTags:
self._tagIndex.pop(aTag)
nLine = 0
nLine = 0
nTitle = 0
theLines = theText.splitlines()
for aLine in theLines:
aLine = aLine
nLine += 1
nChar = len(aLine.strip())
nChar = len(aLine.strip())
if nChar == 0:
continue
@@ -363,33 +352,33 @@ class NWIndex():
"""
if aLine.startswith("# "):
hDepth = "H1"
hText = aLine[2:].strip()
hText = aLine[2:].strip()
elif aLine.startswith("## "):
hDepth = "H2"
hText = aLine[3:].strip()
hText = aLine[3:].strip()
elif aLine.startswith("### "):
hDepth = "H3"
hText = aLine[4:].strip()
hText = aLine[4:].strip()
elif aLine.startswith("#### "):
hDepth = "H4"
hText = aLine[5:].strip()
hText = aLine[5:].strip()
else:
return False
sTitle = "T%06d" % nLine
self._refIndex[tHandle][sTitle] = {
"tags" : [],
"updated" : round(time()),
"tags": [],
"updated": round(time()),
}
theData = {
"level" : hDepth,
"title" : hText,
"layout" : itemLayout.name,
"synopsis" : "",
"cCount" : 0,
"wCount" : 0,
"pCount" : 0,
"updated" : round(time()),
"level": hDepth,
"title": hText,
"layout": itemLayout.name,
"synopsis": "",
"cCount": 0,
"wCount": 0,
"pCount": 0,
"updated": round(time()),
}
if hText != "":
@@ -406,14 +395,14 @@ class NWIndex():
"""Index a page with no title.
"""
theData = {
"level" : "H0",
"title" : "Untitled Page",
"layout" : itemLayout.name,
"synopsis" : "",
"cCount" : 0,
"wCount" : 0,
"pCount" : 0,
"updated" : round(time()),
"level": "H0",
"title": "Untitled Page",
"layout": itemLayout.name,
"synopsis": "",
"cCount": 0,
"wCount": 0,
"pCount": 0,
"updated": round(time()),
}
if isNovel:
@@ -468,11 +457,11 @@ class NWIndex():
"""
isValid, theBits, _ = self.scanThis(aLine)
if not isValid or len(theBits) < 2:
logger.warning("Skipping keyword with %d value(s) in %s" % (len(theBits), tHandle))
logger.warning("Skipping keyword with %d value(s) in '%s'", len(theBits), tHandle)
return
if theBits[0] not in nwKeyWords.VALID_KEYS:
logger.warning("Skipping invalid keyword '%s' in %s" % (theBits[0], tHandle))
logger.warning("Skipping invalid keyword '%s' in '%s'", theBits[0], tHandle)
return
sTitle = "T%06d" % nTitle
@@ -493,10 +482,10 @@ class NWIndex():
"""Scan a line starting with @ to check that it's valid. Then
split it up into its elements and positions as two arrays.
"""
theBits = [] # The elements of the string
thePos = [] # The absolute position of each element
theBits = [] # The elements of the string
thePos = [] # The absolute position of each element
aLine = aLine.rstrip() # Remove all trailing white spaces
aLine = aLine.rstrip() # Remove all trailing white spaces
nChar = len(aLine)
if nChar < 2:
return False, theBits, thePos
@@ -905,3 +894,71 @@ class NWIndex():
return
# END Class NWIndex
# =============================================================================================== #
# Simple Word Counter
# =============================================================================================== #
def countWords(theText):
"""Count words in a piece of text, skipping special syntax and
comments.
"""
charCount = 0
wordCount = 0
paraCount = 0
prevEmpty = True
if not isinstance(theText, str):
return charCount, wordCount, paraCount
# We need to treat dashes as word separators for counting words.
# The check+replace apprach is much faster that direct replace for
# large texts, and a bit slower for small texts, but in the latter
# case it doesn't matter.
if nwUnicode.U_ENDASH in theText:
theText = theText.replace(nwUnicode.U_ENDASH, " ")
if nwUnicode.U_EMDASH in theText:
theText = theText.replace(nwUnicode.U_EMDASH, " ")
for aLine in theText.splitlines():
countPara = True
if not aLine:
prevEmpty = True
continue
if aLine[0] == "@" or aLine[0] == "%":
continue
if aLine[0] == "#":
if aLine[:5] == "#### ":
aLine = aLine[5:]
countPara = False
elif aLine[:4] == "### ":
aLine = aLine[4:]
countPara = False
elif aLine[:3] == "## ":
aLine = aLine[3:]
countPara = False
elif aLine[:2] == "# ":
aLine = aLine[2:]
countPara = False
elif aLine[0] == ">" or aLine[-1] == "<":
if aLine[:2] == ">>":
aLine = aLine[2:].lstrip(" ")
elif aLine[:1] == ">":
aLine = aLine[1:].lstrip(" ")
if aLine[-2:] == "<<":
aLine = aLine[:-2].rstrip(" ")
elif aLine[-1:] == "<":
aLine = aLine[:-1].rstrip(" ")
wordCount += len(aLine.split())
charCount += len(aLine)
if countPara and prevEmpty:
paraCount += 1
prevEmpty = not countPara
return charCount, wordCount, paraCount

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – Project Item Class
================================
@@ -28,13 +27,12 @@ import logging
from lxml import etree
from nw.constants import nwItemType, nwItemClass, nwItemLayout
from nw.common import (
checkInt, isHandle, isItemClass, isItemLayout, isItemType
)
from nw.enum import nwItemType, nwItemClass, nwItemLayout
from nw.common import checkInt, isHandle, isItemClass, isItemLayout, isItemType
logger = logging.getLogger(__name__)
class NWItem():
def __init__(self, theProject):
@@ -53,11 +51,11 @@ class NWItem():
self.isExported = True
# Document Meta Data
self.charCount = 0 # Current character count
self.wordCount = 0 # Current word count
self.paraCount = 0 # Current paragraph count
self.initCount = 0 # Initial word count
self.cursorPos = 0 # Last cursor position
self.charCount = 0 # Current character count
self.wordCount = 0 # Current word count
self.paraCount = 0 # Current paragraph count
self.initCount = 0 # Initial word count
self.cursorPos = 0 # Last cursor position
return
@@ -69,9 +67,9 @@ class NWItem():
"""Packs all the data in the class instance into an XML object.
"""
xPack = etree.SubElement(xParent, "item", attrib={
"handle" : str(self.itemHandle),
"order" : str(self.itemOrder),
"parent" : str(self.itemParent),
"handle": str(self.itemHandle),
"order": str(self.itemOrder),
"parent": str(self.itemParent),
})
self._subPack(xPack, "name", text=str(self.itemName))
self._subPack(xPack, "type", text=str(self.itemType.name))
@@ -108,7 +106,6 @@ class NWItem():
if "order" in xItem.attrib:
self.setOrder(xItem.attrib["order"])
retStatus = True
for xValue in xItem:
if xValue.tag == "name":
self.setName(xValue.text)
@@ -133,10 +130,12 @@ class NWItem():
elif xValue.tag == "cursorPos":
self.setCursorPos(xValue.text)
else:
logger.error("Unknown tag '%s'" % xValue.tag)
retStatus = False
# Sliently skip as we may otherwise cause orphaned
# items if an otherwise valid file is opened by a
# version of novelWriter that doesn't know the tag.
logger.error("Unknown tag '%s'", xValue.tag)
return retStatus
return True
@staticmethod
def _subPack(xParent, name, attrib=None, text=None, none=True):
@@ -206,7 +205,7 @@ class NWItem():
elif isItemType(theType):
self.itemType = nwItemType[theType]
else:
logger.error("Unrecognised item type '%s'" % theType)
logger.error("Unrecognised item type '%s'", theType)
self.itemType = nwItemType.NO_TYPE
return
@@ -219,7 +218,7 @@ class NWItem():
elif isItemClass(theClass):
self.itemClass = nwItemClass[theClass]
else:
logger.error("Unrecognised item class '%s'" % theClass)
logger.error("Unrecognised item class '%s'", theClass)
self.itemClass = nwItemClass.NO_CLASS
return
@@ -232,7 +231,7 @@ class NWItem():
elif isItemLayout(theLayout):
self.itemLayout = nwItemLayout[theLayout]
else:
logger.error("Unrecognised item layout '%s'" % theLayout)
logger.error("Unrecognised item layout '%s'", theLayout)
self.itemLayout = nwItemLayout.NO_LAYOUT
return
@@ -252,7 +251,7 @@ class NWItem():
if isinstance(expState, str):
self.isExpanded = (expState == str(True))
else:
self.isExpanded = (expState == True) # noqa: E712
self.isExpanded = (expState == True) # noqa: E712
return
def setExported(self, expState):
@@ -261,7 +260,7 @@ class NWItem():
if isinstance(expState, str):
self.isExported = (expState == str(True))
else:
self.isExported = (expState == True) # noqa: E712
self.isExported = (expState == True) # noqa: E712
return
##

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – Project Options Cache
===================================
@@ -34,6 +33,7 @@ from nw.constants import nwFiles
logger = logging.getLogger(__name__)
class OptionState():
def __init__(self, theProject):
@@ -91,6 +91,8 @@ class OptionState():
"winWidth",
"winHeight",
"replaceColW",
"statusColW",
"importColW",
},
"GuiProjectDetails": {
"winWidth",
@@ -123,12 +125,12 @@ class OptionState():
return False
stateFile = os.path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
theState = {}
theState = {}
if os.path.isfile(stateFile):
logger.debug("Loading GUI options file")
try:
with open(stateFile, mode="r", encoding="utf8") as inFile:
with open(stateFile, mode="r", encoding="utf-8") as inFile:
theState = json.load(inFile)
except Exception:
logger.error("Failed to load GUI options file")
@@ -155,7 +157,7 @@ class OptionState():
logger.debug("Saving GUI options file")
try:
with open(stateFile, mode="w+", encoding="utf8") as outFile:
with open(stateFile, mode="w+", encoding="utf-8") as outFile:
json.dump(self.theState, outFile, indent=2)
except Exception:
logger.error("Failed to save GUI options file")
@@ -172,11 +174,11 @@ class OptionState():
"""Saves a value, with a given group and name.
"""
if setGroup not in self.validMap:
logger.error("Unknown option group '%s'" % setGroup)
logger.error("Unknown option group '%s'", setGroup)
return False
if setName not in self.validMap[setGroup]:
logger.error("Unknown option name '%s'" % setName)
logger.error("Unknown option name '%s'", setName)
return False
if setGroup not in self.theState:

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – Spell Check Classes
=================================
@@ -29,10 +28,9 @@ import logging
import os
import difflib
from nw.constants import nwConst, isoLanguage
logger = logging.getLogger(__name__)
# =============================================================================================== #
# SpellChecking SuperClass
# =============================================================================================== #
@@ -49,17 +47,17 @@ class NWSpellCheck():
return
def setLanguage(self, theLang, projectDict=None):
"""Dummy function.
"""Default function.
"""
return
def checkWord(self, theWord):
"""Dummy function.
"""Default function.
"""
return True
def suggestWords(self, theWord):
"""Dummy function.
"""Default function.
"""
return []
@@ -73,32 +71,22 @@ class NWSpellCheck():
outFile.write("%s\n" % newWord)
self.projDict.append(newWord)
except Exception:
logger.error("Failed to add word to project word list %s" % str(self.projectDict))
logger.error("Failed to add word to project word list %s", str(self.projectDict))
nw.logException()
return False
return True
return False
def listDictionaries(self):
"""Dummy function.
"""Default function.
"""
return []
def describeDict(self):
"""Dummy function.
"""Default function.
"""
return "", ""
@staticmethod
def expandLanguage(spTag):
"""Translate a language tag to something more user friendly.
"""
spBits = spTag.split("_")
spLang = isoLanguage.ISO_639_1.get(spBits[0], spBits[0])
if len(spBits) > 1:
spLang += " (%s)" % spBits[1]
return spLang
##
# Internal Functions
##
@@ -108,31 +96,33 @@ class NWSpellCheck():
lookup lists.
"""
self.projDict = []
if projectDict is not None:
if not os.path.isfile(projectDict):
self.projectDict = None
return False
else:
self.projectDict = projectDict
self.projectDict = projectDict
try:
logger.debug("Loading project word list")
with open(projectDict, mode="r", encoding="utf-8") as wordsFile:
for theLine in wordsFile:
theLine = theLine.strip()
if len(theLine) > 0 and theLine not in self.projDict:
self.projDict.append(theLine)
logger.debug("Project word list contains %d words" % len(self.projDict))
if projectDict is None:
return False
except Exception:
logger.error("Failed to load project word list")
nw.logException()
return False
if not os.path.isfile(projectDict):
return False
try:
logger.debug("Loading project word list")
with open(projectDict, mode="r", encoding="utf-8") as wordsFile:
for theLine in wordsFile:
theLine = theLine.strip()
if len(theLine) > 0 and theLine not in self.projDict:
self.projDict.append(theLine)
logger.debug("Project word list contains %d words", len(self.projDict))
except Exception:
logger.error("Failed to load project word list")
nw.logException()
return False
return True
# END Class NWSpellCheck
# =============================================================================================== #
# Enchant Based SpellChecking
# =============================================================================================== #
@@ -147,7 +137,7 @@ class NWSpellEnchant(NWSpellCheck):
def setLanguage(self, theLang, projectDict=None):
"""Load a dictionary for the language specified in the config.
If that fails, we load a dummy dictionary so that lookups don't
If that fails, we load a mock dictionary so that lookups don't
crash.
"""
try:
@@ -159,11 +149,11 @@ class NWSpellEnchant(NWSpellCheck):
self.theBroker = enchant.Broker()
self.theDict = self.theBroker.request_dict(theLang)
self.spellLanguage = theLang
logger.debug("Enchant spell checking for language %s loaded" % theLang)
logger.debug("Enchant spell checking for language '%s' loaded", theLang)
except Exception:
logger.error("Failed to load enchant spell checking for language %s" % theLang)
self.theDict = NWSpellEnchantDummy()
logger.error("Failed to load enchant spell checking for language '%s'", theLang)
self.theDict = FakeEnchant()
self.spellLanguage = None
self._readProjectDictionary(projectDict)
@@ -196,10 +186,10 @@ class NWSpellEnchant(NWSpellCheck):
try:
import enchant
for spTag, spProvider in enchant.list_dicts():
spName = "%s [%s]" % (self.expandLanguage(spTag), spProvider.name)
retList.append((spTag, spName))
retList.append((spTag, spProvider.name))
except Exception:
logger.error("Failed to list languages for enchant spell checking")
return retList
def describeDict(self):
@@ -219,7 +209,8 @@ class NWSpellEnchant(NWSpellCheck):
# END Class NWSpellEnchant
class NWSpellEnchantDummy:
class FakeEnchant:
"""Fallback for when Enchant is selected, but not installed.
"""
def __init__(self):
@@ -234,7 +225,8 @@ class NWSpellEnchantDummy:
def add_to_session(self, theWord):
return
# END Class NWSpellEnchantDummy
# END Class FakeEnchant
# =============================================================================================== #
# Fallback SpellChecking Using difflib
@@ -243,8 +235,7 @@ class NWSpellEnchantDummy:
class NWSpellSimple(NWSpellCheck):
"""Internal spell check tool that uses standard Python packages with
no other external dependencies. This is the fallback spell checker
when no other is available. This method is fairly slow compared to
other implementations.
when no other is available. This method is slower than enchant.
"""
theWords = set()
@@ -267,12 +258,12 @@ class NWSpellSimple(NWSpellCheck):
continue
self.theWords.add(theLine.strip().lower())
logger.debug("Spell check dictionary for language %s loaded" % theLang)
logger.debug("Dictionary contains %d words" % len(self.theWords))
logger.debug("Spell check dictionary for language '%s' loaded", theLang)
logger.debug("Dictionary contains %d words", len(self.theWords))
self.spellLanguage = theLang
except Exception:
logger.error("Failed to load spell check word list for language %s" % theLang)
logger.error("Failed to load spell check word list for language '%s'", theLang)
nw.logException()
self.spellLanguage = None
@@ -332,8 +323,7 @@ class NWSpellSimple(NWSpellCheck):
if fExt != ".dict":
continue
spName = "%s [%s]" % (self.expandLanguage(fRoot), nwConst.SP_INTERNAL)
retList.append((fRoot, spName))
retList.append((fRoot, "difflib"))
return retList
@@ -341,6 +331,6 @@ class NWSpellSimple(NWSpellCheck):
"""Return the tag and provider of the currently loaded
dictionary.
"""
return self.theLang, nwConst.SP_INTERNAL
return self.theLang, ""
# END Class NWSpellSimple

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – Project Item Status Class
=======================================
@@ -32,15 +31,18 @@ from nw.common import checkInt
logger = logging.getLogger(__name__)
class NWStatus():
def __init__(self):
self._theLabels = []
self._theLabels = []
self._theColours = []
self._theCounts = []
self._theMap = {}
self._theLength = 0
self._theIndex = 0
self._theCounts = []
self._theMap = {}
self._theLength = 0
self._theIndex = 0
return
def addEntry(self, theLabel, theColours):
@@ -87,12 +89,12 @@ class NWStatus():
replaceMap = {}
if newList is not None:
self._theLabels = []
self._theLabels = []
self._theColours = []
self._theCounts = []
self._theMap = {}
self._theLength = 0
self._theIndex = 0
self._theCounts = []
self._theMap = {}
self._theLength = 0
self._theIndex = 0
for nName, nR, nG, nB, oName in newList:
self.addEntry(nName, (nR, nG, nB))
@@ -122,9 +124,9 @@ class NWStatus():
"""
for n in range(self._theLength):
xSub = etree.SubElement(xParent, "entry", attrib={
"blue" : str(self._theColours[n][2]),
"green" : str(self._theColours[n][1]),
"red" : str(self._theColours[n][0]),
"blue": str(self._theColours[n][2]),
"green": str(self._theColours[n][1]),
"red": str(self._theColours[n][0]),
})
xSub.text = self._theLabels[n]
return True

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – HTML Text Converter
=================================
@@ -27,22 +26,23 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
from nw.core.tokenizer import Tokenizer
from nw.constants import nwLabels, nwKeyWords, nwHtmlUnicode
from nw.constants import nwKeyWords, nwLabels, nwHtmlUnicode
logger = logging.getLogger(__name__)
class ToHtml(Tokenizer):
M_PREVIEW = 0 # Tweak output for the DocViewer
M_EXPORT = 1 # Tweak output for saving to HTML or printing
M_EBOOK = 2 # Tweak output for converting to epub
M_PREVIEW = 0 # Tweak output for the DocViewer
M_EXPORT = 1 # Tweak output for saving to HTML or printing
M_EBOOK = 2 # Tweak output for converting to epub
def __init__(self, theProject, theParent):
Tokenizer.__init__(self, theProject, theParent)
def __init__(self, theProject):
Tokenizer.__init__(self, theProject)
self.genMode = self.M_EXPORT
self.genMode = self.M_EXPORT
self.cssStyles = True
self.fullHTML = []
self.fullHTML = []
# Internals
self._trMap = {}
@@ -59,7 +59,7 @@ class ToHtml(Tokenizer):
need to make a few changes to formatting, which is managed by
these flags.
"""
self.genMode = self.M_PREVIEW
self.genMode = self.M_PREVIEW
self.doKeywords = True
self.doComments = doComments
self.doSynopsis = doSynopsis
@@ -74,16 +74,13 @@ class ToHtml(Tokenizer):
return
def setReplaceUnicode(self, doReplace):
"""Set the translation map to either minimal or full unicode to
"""Set the translation map to either minimal or full unicode for
html entities replacement.
"""
# Control characters must always be replaced
self._trMap = str.maketrans({
"<" : "&lt;",
">" : "&gt;",
"&" : "&amp;",
})
# Angle brackets are replaced later as they are also used in
# formatting codes
self._trMap = str.maketrans({"&": "&amp;"})
if doReplace:
# Extend to all relevant Unicode characters
self._trMap.update(str.maketrans(nwHtmlUnicode.U_TO_H))
@@ -112,22 +109,22 @@ class ToHtml(Tokenizer):
to theResult.
"""
if self.genMode == self.M_PREVIEW:
htmlTags = { # HTML4 + CSS2
self.FMT_B_B : "<b>",
self.FMT_B_E : "</b>",
self.FMT_I_B : "<i>",
self.FMT_I_E : "</i>",
self.FMT_D_B : "<span style='text-decoration: line-through;'>",
self.FMT_D_E : "</span>",
htmlTags = { # HTML4 + CSS2
self.FMT_B_B: "<b>",
self.FMT_B_E: "</b>",
self.FMT_I_B: "<i>",
self.FMT_I_E: "</i>",
self.FMT_D_B: "<span style='text-decoration: line-through;'>",
self.FMT_D_E: "</span>",
}
else:
htmlTags = { # HTML5
self.FMT_B_B : "<strong>",
self.FMT_B_E : "</strong>",
self.FMT_I_B : "<em>",
self.FMT_I_E : "</em>",
self.FMT_D_B : "<del>",
self.FMT_D_E : "</del>",
htmlTags = { # HTML5
self.FMT_B_B: "<strong>",
self.FMT_B_E: "</strong>",
self.FMT_I_B: "<em>",
self.FMT_I_E: "</em>",
self.FMT_D_B: "<del>",
self.FMT_D_E: "</del>",
}
if self.isNovel and self.genMode != self.M_PREVIEW:
@@ -151,10 +148,12 @@ class ToHtml(Tokenizer):
thisPar = []
parStyle = None
tmpResult = []
hasHardBreak = False
for tType, tLine, tText, tFormat, tStyle in self.theTokens:
# Replace < and > before adding html tags
tText = tText.replace("<", "&lt;").replace(">", "&gt;")
# Styles
aStyle = []
if tStyle is not None and self.cssStyles:
@@ -182,6 +181,11 @@ class ToHtml(Tokenizer):
if tStyle & self.A_Z_TOPMRG:
aStyle.append("margin-top: 0;")
if tStyle & self.A_IND_L:
aStyle.append("margin-left: %dpx;" % self.mainConf.tabWidth)
if tStyle & self.A_IND_R:
aStyle.append("margin-right: %dpx;" % self.mainConf.tabWidth)
if len(aStyle) > 0:
hStyle = " style='%s'" % (" ".join(aStyle))
else:
@@ -196,16 +200,15 @@ class ToHtml(Tokenizer):
if tType == self.T_EMPTY:
if parStyle is None:
parStyle = ""
if hasHardBreak and self.cssStyles:
if len(thisPar) > 1 and self.cssStyles:
parClass = " class='break'"
else:
parClass = ""
if len(thisPar) > 0:
tTemp = "".join(thisPar)
tTemp = "<br/>".join(thisPar)
tmpResult.append("<p%s%s>%s</p>\n" % (parStyle, parClass, tTemp.rstrip()))
thisPar = []
parStyle = None
hasHardBreak = False
elif tType == self.T_TITLE:
tHead = tText.replace(r"\\", "<br/>")
@@ -239,11 +242,7 @@ class ToHtml(Tokenizer):
parStyle = hStyle
for xPos, xLen, xFmt in reversed(tFormat):
tTemp = tTemp[:xPos] + htmlTags[xFmt] + tTemp[xPos+xLen:]
if tText.endswith(" "):
thisPar.append(tTemp.rstrip() + "<br/>")
hasHardBreak = True
else:
thisPar.append(tTemp.rstrip() + " ")
thisPar.append(tTemp.rstrip())
elif tType == self.T_SYNOPSIS and self.doSynopsis:
tmpResult.append(self._formatSynopsis(tText))
@@ -266,7 +265,7 @@ class ToHtml(Tokenizer):
def saveHTML5(self, savePath):
"""Save the data to an .html file.
"""
with open(savePath, mode="w", encoding="utf8") as outFile:
with open(savePath, mode="w", encoding="utf-8") as outFile:
theStyle = self.getStyleSheet()
theStyle.append("article {width: 800px; margin: 40px auto;}")
bodyText = "".join(self.fullHTML)
@@ -289,9 +288,9 @@ class ToHtml(Tokenizer):
"</body>\n"
"</html>\n"
).format(
projTitle = self.theProject.projName,
htmlStyle = "\n".join(theStyle),
bodyText = bodyText,
projTitle=self.theProject.projName,
htmlStyle="\n".join(theStyle),
bodyText=bodyText,
)
outFile.write(theHtml)
@@ -406,17 +405,20 @@ class ToHtml(Tokenizer):
"""Apply HTML formatting to synopsis.
"""
if self.genMode == self.M_PREVIEW:
return "<p class='comment'><span class='synopsis'>Synopsis:</span> %s</p>\n" % tText
sSynop = self._trSynopsis
return f"<p class='comment'><span class='synopsis'>{sSynop}:</span> {tText}</p>\n"
else:
return "<p class='synopsis'><strong>Synopsis:</strong> %s</p>\n" % tText
sSynop = self._localLookup("Synopsis")
return f"<p class='synopsis'><strong>{sSynop}:</strong> {tText}</p>\n"
def _formatComments(self, tText):
"""Apply HTML formatting to comments.
"""
if self.genMode == self.M_PREVIEW:
return "<p class='comment'>%s</p>\n" % tText
return f"<p class='comment'>{tText}</p>\n"
else:
return "<p class='comment'><strong>Comment:</strong> %s</p>\n" % tText
sComm = self._localLookup("Comment")
return f"<p class='comment'><strong>{sComm}:</strong> {tText}</p>\n"
def _formatKeywords(self, tText):
"""Apply HTML formatting to keywords.

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – Text Tokenizer
============================
@@ -24,82 +23,91 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
import re
import logging
from operator import itemgetter
from PyQt5.QtCore import QRegularExpression
from functools import partial
from PyQt5.QtCore import QCoreApplication, QRegularExpression
from nw.enum import nwItemLayout, nwItemType
from nw.common import numberToRoman
from nw.constants import nwConst, nwRegEx, nwUnicode
from nw.core.document import NWDoc
from nw.core.tools import numberToWord, numberToRoman
from nw.constants import nwConst, nwUnicode, nwItemLayout, nwItemType, nwRegEx
logger = logging.getLogger(__name__)
class Tokenizer():
# In-Text Format
FMT_B_B = 1 # Begin bold
FMT_B_E = 2 # End bold
FMT_I_B = 3 # Begin italics
FMT_I_E = 4 # End italics
FMT_D_B = 5 # Begin strikeout
FMT_D_E = 6 # End strikeout
FMT_B_B = 1 # Begin bold
FMT_B_E = 2 # End bold
FMT_I_B = 3 # Begin italics
FMT_I_E = 4 # End italics
FMT_D_B = 5 # Begin strikeout
FMT_D_E = 6 # End strikeout
# Block Type
T_EMPTY = 1 # Empty line (new paragraph)
T_SYNOPSIS = 2 # Synopsis comment
T_COMMENT = 3 # Comment line
T_KEYWORD = 4 # Command line
T_TITLE = 5 # Title
T_HEAD1 = 6 # Header 1
T_HEAD2 = 7 # Header 2
T_HEAD3 = 8 # Header 3
T_HEAD4 = 9 # Header 4
T_TEXT = 10 # Text line
T_SEP = 11 # Scene separator
T_SKIP = 12 # Paragraph break
T_EMPTY = 1 # Empty line (new paragraph)
T_SYNOPSIS = 2 # Synopsis comment
T_COMMENT = 3 # Comment line
T_KEYWORD = 4 # Command line
T_TITLE = 5 # Title
T_HEAD1 = 6 # Header 1
T_HEAD2 = 7 # Header 2
T_HEAD3 = 8 # Header 3
T_HEAD4 = 9 # Header 4
T_TEXT = 10 # Text line
T_SEP = 11 # Scene separator
T_SKIP = 12 # Paragraph break
# Block Style
A_NONE = 0x0000 # No special style
A_LEFT = 0x0001 # Left aligned
A_RIGHT = 0x0002 # Right aligned
A_CENTRE = 0x0004 # Centred
A_JUSTIFY = 0x0008 # Justified
A_PBB = 0x0010 # Page break before always
A_PBB_AUT = 0x0020 # Page break before auto
A_PBA = 0x0040 # Page break after always
A_PBA_AUT = 0x0080 # Page break after auto
A_Z_TOPMRG = 0x0100 # Zero top margin
A_Z_BTMMRG = 0x0200 # Zero bottom margin
A_NONE = 0x0000 # No special style
A_LEFT = 0x0001 # Left aligned
A_RIGHT = 0x0002 # Right aligned
A_CENTRE = 0x0004 # Centred
A_JUSTIFY = 0x0008 # Justified
A_PBB = 0x0010 # Page break before always
A_PBB_AUT = 0x0020 # Page break before auto
A_PBA = 0x0040 # Page break after always
A_PBA_AUT = 0x0080 # Page break after auto
A_Z_TOPMRG = 0x0100 # Zero top margin
A_Z_BTMMRG = 0x0200 # Zero bottom margin
A_IND_L = 0x0400 # Left indentation
A_IND_R = 0x0800 # Right indentation
def __init__(self, theProject, theParent):
def __init__(self, theProject):
self.theProject = theProject
self.theParent = theParent
self.theParent = theProject.theParent
self.mainConf = nw.CONFIG
# Data Variables
self.theText = "" # The raw text to be tokenized
self.theHandle = None # The handle associated with the text
self.theItem = None # The NWItem associated with the handle
self.theTokens = [] # The list of the processed tokens
self.theResult = "" # The result of the last document
self.theText = "" # The raw text to be tokenized
self.theHandle = None # The handle associated with the text
self.theItem = None # The NWItem associated with the handle
self.theTokens = [] # The list of the processed tokens
self.theResult = "" # The result of the last document
self.keepMarkdown = False # Whether to keep the markdown text
self.theMarkdown = [] # The result novelWriter markdown of all documents
self.keepMarkdown = False # Whether to keep the markdown text
self.theMarkdown = [] # The result novelWriter markdown of all documents
# User Settings
self.textFont = "Serif" # Output text font
self.textSize = 11 # Output text size
self.textFixed = False # Fixed width text
self.lineHeight = 1.15 # Line height
self.doJustify = False # Justify text
self.doBodyText = True # Include body text
self.doSynopsis = False # Also process synopsis comments
self.doComments = False # Also process comments
self.doKeywords = False # Also process keywords like tags and references
self.textFont = "Serif" # Output text font
self.textSize = 11 # Output text size
self.textFixed = False # Fixed width text
self.lineHeight = 1.15 # Line height in units of em
self.blockIndent = 4.00 # Block indent in units of em
self.doJustify = False # Justify text
self.doBodyText = True # Include body text
self.doSynopsis = False # Also process synopsis comments
self.doComments = False # Also process comments
self.doKeywords = False # Also process keywords like tags and references
## Title Margins
# Margins
self.marginTitle = (1.000, 0.500)
self.marginHead1 = (1.000, 0.500)
self.marginHead2 = (0.834, 0.500)
@@ -108,23 +116,23 @@ class Tokenizer():
self.marginText = (0.000, 0.584)
self.marginMeta = (0.000, 0.584)
## Title Formats
self.fmtTitle = "%title%" # Formatting for titles
self.fmtChapter = "%title%" # Formatting for numbered chapters
self.fmtUnNum = "%title%" # Formatting for unnumbered chapters
self.fmtScene = "%title%" # Formatting for scenes
self.fmtSection = "%title%" # Formatting for sections
# Title Formats
self.fmtTitle = "%title%" # Formatting for titles
self.fmtChapter = "%title%" # Formatting for numbered chapters
self.fmtUnNum = "%title%" # Formatting for unnumbered chapters
self.fmtScene = "%title%" # Formatting for scenes
self.fmtSection = "%title%" # Formatting for sections
self.hideScene = False # Do not include scene headers
self.hideSection = False # Do not include section headers
self.hideScene = False # Do not include scene headers
self.hideSection = False # Do not include section headers
self.linkHeaders = False # Add an anchor before headers
self.linkHeaders = False # Add an anchor before headers
# Instance Variables
self.numChapter = 0 # Counter for chapter numbers
self.numChScene = 0 # Counter for scene number within chapter
self.numAbsScene = 0 # Counter for scene number within novel
self.firstScene = False # Flag to indicate that the first scene of the chapter
self.numChapter = 0 # Counter for chapter numbers
self.numChScene = 0 # Counter for scene number within chapter
self.numAbsScene = 0 # Counter for scene number within novel
self.firstScene = False # Flag to indicate that the first scene of the chapter
# This File
self.isNone = False
@@ -141,6 +149,13 @@ class Tokenizer():
# Error Handling
self.errData = []
# Function Mapping
self._localLookup = self.theProject.localLookup
self.tr = partial(QCoreApplication.translate, "Tokenizer")
# Cached Translations
self._trSynopsis = self.tr("Synopsis")
return
##
@@ -176,7 +191,11 @@ class Tokenizer():
return
def setLineHeight(self, lineHeight):
self.lineHeight = float(lineHeight)
self.lineHeight = min(max(float(lineHeight), 0.5), 5.0)
return
def setBlockIndent(self, blockIndent):
self.blockIndent = min(max(float(blockIndent), 0.0), 10.0)
return
def setJustify(self, doJustify):
@@ -249,7 +268,7 @@ class Tokenizer():
if theItem.itemType != nwItemType.ROOT:
return False
theTitle = "Notes: %s" % theItem.itemName
theTitle = "%s: %s" % (self._localLookup("Notes"), theItem.itemName)
self.theTokens = []
self.theTokens.append((
self.T_TITLE, 0, theTitle, None, self.A_PBB | self.A_CENTRE
@@ -268,20 +287,23 @@ class Tokenizer():
if self.theItem is None:
return False
self.theText = ""
if theText is not None:
# If the text is set, just use that
self.theText = theText
else:
# Otherwise, load it from file
theDocument = NWDoc(self.theProject, self.theParent)
self.theText = theDocument.openDocument(theHandle)
theDoc = NWDoc(self.theProject, theHandle)
theText = theDoc.readDocument()
if theText:
self.theText = theText
docSize = len(self.theText)
if docSize > nwConst.MAX_DOCSIZE:
errVal = "Document '%s' is too big (%.2f MB). Skipping." % (
self.theItem.itemName, docSize/1.0e6
errVal = self.tr("Document '{0}' is too big ({1} MB). Skipping.").format(
self.theItem.itemName, f"{docSize/1.0e6:.2f}"
)
self.theText = "# ERROR\n\n%s\n\n" % errVal
self.theText = "# %s\n\n%s\n\n" % (self.tr("ERROR"), errVal)
self.errData.append(errVal)
self.isNone = self.theItem.itemLayout == nwItemLayout.NO_LAYOUT
@@ -298,7 +320,7 @@ class Tokenizer():
return True
def doPreProcessing(self):
"""Reun trough the various replace doctionaries.
"""Run trough the various replace doctionaries.
"""
# Process the user's auto-replace dictionary
if len(self.theProject.autoReplace) > 0:
@@ -319,9 +341,9 @@ class Tokenizer():
does the standard escaped characters.
"""
escapeDict = {
r"\*" : "*",
r"\~" : "~",
r"\_" : "_",
r"\*": "*",
r"\~": "~",
r"\_": "_",
}
escReplace = re.compile(
"|".join([re.escape(k) for k in escapeDict.keys()]), flags=re.DOTALL
@@ -353,6 +375,12 @@ class Tokenizer():
(QRegularExpression(nwRegEx.FMT_ST), [None, self.FMT_D_B, None, self.FMT_D_E]),
]
# Determine default text alignment
if self.isTitle or self.isPart:
defAlign = self.A_CENTRE
else:
defAlign = self.A_NONE
self.theTokens = []
tmpMarkdown = []
nLine = 0
@@ -362,7 +390,7 @@ class Tokenizer():
# Tag lines starting with specific characters
if len(aLine.strip()) == 0:
self.theTokens.append((
self.T_EMPTY, nLine, "", None, self.A_NONE
self.T_EMPTY, nLine, "", None, defAlign
))
if self.keepMarkdown:
tmpMarkdown.append("\n")
@@ -372,48 +400,48 @@ class Tokenizer():
synTag = cLine[:9].lower()
if synTag == "synopsis:":
self.theTokens.append((
self.T_SYNOPSIS, nLine, cLine[9:].strip(), None, self.A_NONE
self.T_SYNOPSIS, nLine, cLine[9:].strip(), None, defAlign
))
if self.doSynopsis and self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
else:
self.theTokens.append((
self.T_COMMENT, nLine, aLine[1:].strip(), None, self.A_NONE
self.T_COMMENT, nLine, aLine[1:].strip(), None, defAlign
))
if self.doComments and self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[0] == "@":
self.theTokens.append((
self.T_KEYWORD, nLine, aLine[1:].strip(), None, self.A_NONE
self.T_KEYWORD, nLine, aLine[1:].strip(), None, defAlign
))
if self.doKeywords and self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:2] == "# ":
self.theTokens.append((
self.T_HEAD1, nLine, aLine[2:].strip(), None, self.A_NONE
self.T_HEAD1, nLine, aLine[2:].strip(), None, defAlign
))
if self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:3] == "## ":
self.theTokens.append((
self.T_HEAD2, nLine, aLine[3:].strip(), None, self.A_NONE
self.T_HEAD2, nLine, aLine[3:].strip(), None, defAlign
))
if self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:4] == "### ":
self.theTokens.append((
self.T_HEAD3, nLine, aLine[4:].strip(), None, self.A_NONE
self.T_HEAD3, nLine, aLine[4:].strip(), None, defAlign
))
if self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:5] == "#### ":
self.theTokens.append((
self.T_HEAD4, nLine, aLine[5:].strip(), None, self.A_NONE
self.T_HEAD4, nLine, aLine[5:].strip(), None, defAlign
))
if self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
@@ -423,6 +451,38 @@ class Tokenizer():
# Skip all body text
continue
# Check Alignment and Indentation
tagLeft = False
tagRight = False
indLeft = False
indRight = False
if aLine.startswith(">>"):
tagRight = True
aLine = aLine[2:].lstrip(" ")
elif aLine.startswith(">"):
indLeft = True
aLine = aLine[1:].lstrip(" ")
if aLine.endswith("<<"):
tagLeft = True
aLine = aLine[:-2].rstrip(" ")
elif aLine.endswith("<"):
indRight = True
aLine = aLine[:-1].rstrip(" ")
textAlign = defAlign
if tagLeft and tagRight:
textAlign = self.A_CENTRE
elif tagLeft:
textAlign = self.A_LEFT
elif tagRight:
textAlign = self.A_RIGHT
if indLeft:
textAlign |= self.A_IND_L
if indRight:
textAlign |= self.A_IND_R
# Otherwise we use RegEx to find formatting tags within a line of text
fmtPos = []
for theRX, theKeys in rxFormats:
@@ -439,14 +499,14 @@ class Tokenizer():
# sorted by position
fmtPos = sorted(fmtPos, key=itemgetter(0))
self.theTokens.append((
self.T_TEXT, nLine, aLine, fmtPos, self.A_NONE
self.T_TEXT, nLine, aLine, fmtPos, textAlign
))
if self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
# Always add an empty line at the end
self.theTokens.append((
self.T_EMPTY, nLine, "", None, self.A_NONE
self.T_EMPTY, nLine, "", None, defAlign
))
if self.keepMarkdown:
tmpMarkdown.append("\n")
@@ -458,8 +518,8 @@ class Tokenizer():
# ===========
# Some items need a second pass
pToken = (self.T_EMPTY, 0, "", None, self.A_NONE)
nToken = (self.T_EMPTY, 0, "", None, self.A_NONE)
pToken = (self.T_EMPTY, 0, "", None, defAlign)
nToken = (self.T_EMPTY, 0, "", None, defAlign)
tCount = len(self.theTokens)
for n, tToken in enumerate(self.theTokens):
@@ -588,55 +648,48 @@ class Tokenizer():
tToken[0], tToken[1], tTemp, None, self.A_NONE
)
# For title page and partitions, we need to centre all text.
# For partition, we also add a page break before, and for
# both types we always add a page break after the content.
# We also swap header level 1 with a title type instead.
# For title page we use a different title class, and we will set
# an automatic page break before (i.e. added if needed). For
# partitions we always need a page break before.
if self.isTitle or self.isPart:
for n, tToken in enumerate(self.theTokens):
aStyle = tToken[4]
if n == 0:
if self.isTitle:
aStyle |= self.A_PBB_AUT
else:
aStyle |= self.A_PBB
if tToken[0] == self.T_HEAD1:
if self.isTitle:
aStyle = self.A_PBB_AUT | self.A_CENTRE
self.theTokens[n] = (
self.T_TITLE, tToken[1], tToken[2], tToken[3], aStyle
)
else:
aStyle = self.A_PBB | self.A_CENTRE
self.theTokens[n] = (
tToken[0], tToken[1], tToken[2], tToken[3], aStyle
)
else:
self.theTokens[n] = (
tToken[0], tToken[1], tToken[2], tToken[3], self.A_CENTRE
)
# Add a page break after the last entry
n = len(self.theTokens) - 1
if n >= 0:
tToken = self.theTokens[n]
self.theTokens[n] = (
if len(self.theTokens) > 0:
tToken = self.theTokens[-1]
self.theTokens[-1] = (
tToken[0], tToken[1], tToken[2], tToken[3], tToken[4] | self.A_PBA
)
# A single page is always left-aligned and starts on a fresh
# page, unless it's empty.
if self.isPage:
for n, tToken in enumerate(self.theTokens):
if n == 0:
self.theTokens[n] = (
tToken[0], tToken[1], tToken[2], tToken[3], self.A_LEFT | self.A_PBB
)
else:
self.theTokens[n] = (
tToken[0], tToken[1], tToken[2], tToken[3], self.A_LEFT
)
# A single page always starts on a fresh page, unless it's empty.
if self.isPage and len(self.theTokens) > 0:
tToken = self.theTokens[0]
self.theTokens[0] = (
tToken[0], tToken[1], tToken[2], tToken[3], tToken[4] | self.A_PBB
)
return True
def saveRawMarkdown(self, savePath):
"""Save the data to a plain text file.
"""
with open(savePath, mode="w", encoding="utf8") as outFile:
with open(savePath, mode="w", encoding="utf-8") as outFile:
for nwdPage in self.theMarkdown:
outFile.write(nwdPage)
return
@@ -653,11 +706,12 @@ class Tokenizer():
theTitle = theTitle.replace(r"%sc%", str(self.numChScene))
theTitle = theTitle.replace(r"%sca%", str(self.numAbsScene))
if r"%chw%" in theTitle:
theTitle = theTitle.replace(r"%chw%", numberToWord(self.numChapter, "en"))
theTitle = theTitle.replace(r"%chw%", self._localLookup(self.numChapter))
if r"%chi%" in theTitle:
theTitle = theTitle.replace(r"%chi%", numberToRoman(self.numChapter, True))
if r"%chI%" in theTitle:
theTitle = theTitle.replace(r"%chI%", numberToRoman(self.numChapter, False))
return theTitle
return theTitle[:1].upper() + theTitle[1:]
# END Class Tokenizer

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – Markdown Text Converter
=====================================
@@ -26,18 +25,19 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
from nw.core.tokenizer import Tokenizer
from nw.constants import nwLabels
from nw.core.tokenizer import Tokenizer
logger = logging.getLogger(__name__)
class ToMarkdown(Tokenizer):
M_STD = 0 # Standard Markdown
M_GH = 1 # GitHub Markdown
M_STD = 0 # Standard Markdown
M_GH = 1 # GitHub Markdown
def __init__(self, theProject, theParent):
Tokenizer.__init__(self, theProject, theParent)
def __init__(self, theProject):
Tokenizer.__init__(self, theProject)
self.genMode = self.M_STD
self.fullMD = []
@@ -72,16 +72,22 @@ class ToMarkdown(Tokenizer):
if self.genMode == self.M_STD:
# Standard
mdTags = {
self.FMT_B_B : "**", self.FMT_B_E : "**",
self.FMT_I_B : "_", self.FMT_I_E : "_",
self.FMT_D_B : "", self.FMT_D_E : "",
self.FMT_B_B: "**",
self.FMT_B_E: "**",
self.FMT_I_B: "_",
self.FMT_I_E: "_",
self.FMT_D_B: "",
self.FMT_D_E: "",
}
else:
# GitHub and novelWriter
# GitHub
mdTags = {
self.FMT_B_B : "**", self.FMT_B_E : "**",
self.FMT_I_B : "_", self.FMT_I_E : "_",
self.FMT_D_B : "~~", self.FMT_D_E : "~~",
self.FMT_B_B: "**",
self.FMT_B_E: "**",
self.FMT_I_B: "_",
self.FMT_I_E: "_",
self.FMT_D_B: "~~",
self.FMT_D_E: "~~",
}
self.theResult = ""
@@ -89,12 +95,12 @@ class ToMarkdown(Tokenizer):
thisPar = []
tmpResult = []
for tType, tLine, tText, tFormat, tStyle in self.theTokens:
for tType, _, tText, tFormat, tStyle in self.theTokens:
# Process Text Type
if tType == self.T_EMPTY:
if len(thisPar) > 0:
tTemp = "".join(thisPar)
tTemp = " \n".join(thisPar)
tmpResult.append("%s\n\n" % tTemp.rstrip(" "))
thisPar = []
@@ -128,16 +134,13 @@ class ToMarkdown(Tokenizer):
tTemp = tText
for xPos, xLen, xFmt in reversed(tFormat):
tTemp = tTemp[:xPos] + mdTags[xFmt] + tTemp[xPos+xLen:]
if tText.endswith(" "):
thisPar.append(tTemp.rstrip() + " \n")
else:
thisPar.append(tTemp.rstrip() + " ")
thisPar.append(tTemp.rstrip())
elif tType == self.T_SYNOPSIS and self.doSynopsis:
tmpResult.append("**Synopsis:** %s\n\n" % tText)
tmpResult.append("**%s:** %s\n\n" % (self._localLookup("Synopsis"), tText))
elif tType == self.T_COMMENT and self.doComments:
tmpResult.append("**Comment:** %s\n\n" % tText)
tmpResult.append("**%s:** %s\n\n" % (self._localLookup("Comment"), tText))
elif tType == self.T_KEYWORD and self.doKeywords:
tmpResult.append(self._formatKeywords(tText, tStyle))
@@ -152,7 +155,7 @@ class ToMarkdown(Tokenizer):
def saveMarkdown(self, savePath):
"""Save the data to a plain text file.
"""
with open(savePath, mode="w", encoding="utf8") as outFile:
with open(savePath, mode="w", encoding="utf-8") as outFile:
theText = "".join(self.fullMD)
outFile.write(theText)
@@ -176,7 +179,7 @@ class ToMarkdown(Tokenizer):
def _formatKeywords(self, tText, tStyle):
"""Apply Markdown formatting to keywords.
"""
isValid, theBits, thePos = self.theParent.theIndex.scanThis("@"+tText)
isValid, theBits, _ = self.theParent.theIndex.scanThis("@"+tText)
if not isValid or not theBits:
return ""

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – ODT Text Converter
================================
@@ -29,22 +28,22 @@ import logging
from lxml import etree
from hashlib import sha256
from datetime import datetime
from zipfile import ZipFile
from datetime import datetime
from nw.constants import nwKeyWords, nwLabels
from nw.core.tokenizer import Tokenizer
from nw.constants import nwLabels, nwKeyWords
logger = logging.getLogger(__name__)
# Main XML NameSpaces
XML_NS = {
"office" : "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
"style" : "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
"loext" : "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0",
"text" : "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
"meta" : "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
"fo" : "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
"office": "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
"style": "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
"loext": "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0",
"text": "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
"meta": "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
"fo": "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
}
# Mimetype and Version
@@ -57,38 +56,39 @@ TAG_TAB = "{%s}tab" % XML_NS["text"]
TAG_SPAN = "{%s}span" % XML_NS["text"]
TAG_STNM = "{%s}style-name" % XML_NS["text"]
class ToOdt(Tokenizer):
X_BLD = 0x01 # Bold format
X_ITA = 0x02 # Italic format
X_DEL = 0x04 # Strikethrough format
X_BRK = 0x08 # Line break
X_TAB = 0x10 # Tab
X_BLD = 0x01 # Bold format
X_ITA = 0x02 # Italic format
X_DEL = 0x04 # Strikethrough format
X_BRK = 0x08 # Line break
X_TAB = 0x10 # Tab
def __init__(self, theProject, theParent, isFlat):
Tokenizer.__init__(self, theProject, theParent)
def __init__(self, theProject, isFlat):
Tokenizer.__init__(self, theProject)
self.mainConf = nw.CONFIG
self._isFlat = isFlat # Flat: .fodt, otherwise .odt
self._isFlat = isFlat # Flat: .fodt, otherwise .odt
self._dFlat = None # FODT file XML root
self._dCont = None # ODT content.xml root
self._dMeta = None # ODT meta.xml root
self._dStyl = None # ODT styles.xml root
self._dFlat = None # FODT file XML root
self._dCont = None # ODT content.xml root
self._dMeta = None # ODT meta.xml root
self._dStyl = None # ODT styles.xml root
self._xMeta = None # Office meta root
self._xStyl = None # Office styles root
self._xAuto = None # Office auto-styles root
self._xMast = None # Office master-styles root
self._xBody = None # Office body root
self._xText = None # Office text root
self._xMeta = None # Office meta root
self._xStyl = None # Office styles root
self._xAuto = None # Office auto-styles root
self._xMast = None # Office master-styles root
self._xBody = None # Office body root
self._xText = None # Office text root
self._xAut2 = None # Page layout auto-styles for ODT file
self._xAut2 = None # Page layout auto-styles for ODT file
self._mainPara = {} # User-accessible paragraph styles
self._autoPara = {} # Auto-generated paragraph styles
self._autoText = {} # Auto-generated text styles
self._mainPara = {} # User-accessible paragraph styles
self._autoPara = {} # Auto-generated paragraph styles
self._autoText = {} # Auto-generated text styles
# Properties
self.textFont = "Liberation Serif"
@@ -99,21 +99,22 @@ class ToOdt(Tokenizer):
self.headerText = ""
# Internal
self._fontFamily = "&apos;Liberation Sans&apos;"
self._fontPitch = "variable"
self._fSizeTitle = "30pt"
self._fSizeHead1 = "24pt"
self._fSizeHead2 = "20pt"
self._fSizeHead3 = "16pt"
self._fSizeHead4 = "14pt"
self._fSizeHead = "14pt"
self._fSizeText = "12pt"
self._lineHeight = "115%"
self._textAlign = "left"
self._dLanguage = "en"
self._dCountry = "GB"
self._fontFamily = "&apos;Liberation Sans&apos;"
self._fontPitch = "variable"
self._fSizeTitle = "30pt"
self._fSizeHead1 = "24pt"
self._fSizeHead2 = "20pt"
self._fSizeHead3 = "16pt"
self._fSizeHead4 = "14pt"
self._fSizeHead = "14pt"
self._fSizeText = "12pt"
self._lineHeight = "115%"
self._blockIndent = "1.693cm"
self._textAlign = "left"
self._dLanguage = "en"
self._dCountry = "GB"
## Text Margings in Units of em
# Text Margings in Units of em
self._mTopTitle = "0.423cm"
self._mTopHead1 = "0.423cm"
self._mTopHead2 = "0.353cm"
@@ -132,13 +133,13 @@ class ToOdt(Tokenizer):
self._mBotText = "0.247cm"
self._mBotMeta = "0.106cm"
## Document Margins
# Document Margins
self._mDocTop = "2.000cm"
self._mDocBtm = "2.000cm"
self._mDocLeft = "2.000cm"
self._mDocRight = "2.000cm"
## Colour
# Colour
self._colHead12 = None
self._opaHead12 = None
self._colHead34 = None
@@ -222,8 +223,9 @@ class ToOdt(Tokenizer):
self._colMetaTx = "#813709"
self._opaMetaTx = "100%"
self._lineHeight = f"{round(100 * self.lineHeight):d}%"
self._textAlign = "justify" if self.doJustify else "left"
self._lineHeight = f"{round(100 * self.lineHeight):d}%"
self._blockIndent = self._emToCm(self.blockIndent)
self._textAlign = "justify" if self.doJustify else "left"
# Document Header
# ===============
@@ -298,7 +300,7 @@ class ToOdt(Tokenizer):
# Meta Data
xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "creation-date"))
xMeta.text = datetime.now().isoformat()
xMeta.text = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "generator"))
xMeta.text = f"novelWriter/{nw.__version__}"
@@ -313,22 +315,21 @@ class ToOdt(Tokenizer):
def doConvert(self):
"""Convert the list of text tokens into XML elements.
"""
self.theResult = "" # Not used, but cleared just in case
self.theResult = "" # Not used, but cleared just in case
odtTags = {
self.FMT_B_B : "_B", # Bold open format
self.FMT_B_E : "b_", # Bold close format
self.FMT_I_B : "I", # Italic open format
self.FMT_I_E : "i", # Italic close format
self.FMT_D_B : "_S", # Strikethrough open format
self.FMT_D_E : "s_", # Strikethrough close format
self.FMT_B_B: "_B", # Bold open format
self.FMT_B_E: "b_", # Bold close format
self.FMT_I_B: "I", # Italic open format
self.FMT_I_E: "i", # Italic close format
self.FMT_D_B: "_S", # Strikethrough open format
self.FMT_D_E: "s_", # Strikethrough close format
}
thisPar = []
thisFmt = []
parStyle = None
hasHardBreak = False
for tType, tLine, tText, tFormat, tStyle in self.theTokens:
for tType, _, tText, tFormat, tStyle in self.theTokens:
# Styles
oStyle = ODTParagraphStyle()
@@ -357,15 +358,20 @@ class ToOdt(Tokenizer):
if tStyle & self.A_Z_TOPMRG:
oStyle.setMarginTop("0.000cm")
if tStyle & self.A_IND_L:
oStyle.setMarginLeft(self._blockIndent)
if tStyle & self.A_IND_R:
oStyle.setMarginRight(self._blockIndent)
# Process Text Types
if tType == self.T_EMPTY:
if hasHardBreak and parStyle is not None:
if len(thisPar) > 1 and parStyle is not None:
if self.doJustify:
parStyle.setTextAlign("left")
if len(thisPar) > 0:
tTemp = "".join(thisPar)
fTemp = "".join(thisFmt)
tTemp = "\n".join(thisPar)
fTemp = " ".join(thisFmt)
tTxt = tTemp.rstrip()
tFmt = fTemp[:len(tTxt)]
self._addTextPar("Text_Body", parStyle, tTxt, theFmt=tFmt)
@@ -373,7 +379,6 @@ class ToOdt(Tokenizer):
thisPar = []
thisFmt = []
parStyle = None
hasHardBreak = False
elif tType == self.T_TITLE:
tHead = tText.replace(r"\\", "\n")
@@ -402,23 +407,17 @@ class ToOdt(Tokenizer):
self._addTextPar("Text_Body", oStyle, "")
elif tType == self.T_TEXT:
tTemp = tText
if parStyle is None:
parStyle = oStyle
tFmt = " "*len(tTemp)
tFmt = " "*len(tText)
for xPos, xLen, xFmt in tFormat:
tFmt = tFmt[:xPos] + odtTags[xFmt] + tFmt[xPos+xLen:]
tTxt = tTemp.rstrip()
tTxt = tText.rstrip()
tFmt = tFmt[:len(tTxt)]
if tText.endswith(" "):
thisPar.append(tTxt + "\n")
thisFmt.append(tFmt + " ")
hasHardBreak = True
else:
thisPar.append(tTxt + " ")
thisFmt.append(tFmt + " ")
thisPar.append(tTxt)
thisFmt.append(tFmt)
elif tType == self.T_SYNOPSIS and self.doSynopsis:
tTemp, fTemp = self._formatSynopsis(tText)
@@ -451,16 +450,16 @@ class ToOdt(Tokenizer):
with open(savePath, mode="wb") as outFile:
outFile.write(etree.tostring(
self._dFlat,
pretty_print = True,
encoding = "utf-8",
xml_declaration = True
pretty_print=True,
encoding="utf-8",
xml_declaration=True
))
return
def saveOpenDocText(self, savePath):
"""Save the data to an .odt file.
"""
mMap = {"manifest" : "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"}
mMap = {"manifest": "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"}
mMani = "{%s}manifest" % mMap["manifest"]
mVers = "{%s}version" % mMap["manifest"]
mPath = "{%s}full-path" % mMap["manifest"]
@@ -474,7 +473,7 @@ class ToOdt(Tokenizer):
etree.SubElement(xMani, mFile, attrib={mPath: "meta.xml", mType: "text/xml"})
etree.SubElement(xMani, mFile, attrib={mPath: "styles.xml", mType: "text/xml"})
sMap = {"office" : "urn:oasis:names:tc:opendocument:xmlns:office:1.0"}
sMap = {"office": "urn:oasis:names:tc:opendocument:xmlns:office:1.0"}
oRoot = "{%s}document-settings" % sMap["office"]
oSett = "{%s}settings" % sMap["office"]
xSett = etree.Element(oRoot, nsmap=sMap)
@@ -507,21 +506,23 @@ class ToOdt(Tokenizer):
def _formatSynopsis(self, tText):
"""Apply formatting to synopsis lines.
"""
rTxt = "**Synopsis:** %s" % tText
rFmt = "_B b_ %s" % (" "*len(tText))
sSynop = self._localLookup("Synopsis")
rTxt = "**%s:** %s" % (sSynop, tText)
rFmt = "_B%s b_ %s" % (" "*len(sSynop), " "*len(tText))
return rTxt, rFmt
def _formatComments(self, tText):
"""Apply formatting to comments.
"""
rTxt = "**Comment:** %s" % tText
rFmt = "_B b_ %s" % (" "*len(tText))
sComm = self._localLookup("Comment")
rTxt = "**%s:** %s" % (sComm, tText)
rFmt = "_B%s b_ %s" % (" "*len(sComm), " "*len(tText))
return rTxt, rFmt
def _formatKeywords(self, tText):
"""Apply formatting to keywords.
"""
isValid, theBits, thePos = self.theParent.theIndex.scanThis("@"+tText)
isValid, theBits, _ = self.theParent.theIndex.scanThis("@"+tText)
if not isValid or not theBits:
return ""
@@ -530,7 +531,7 @@ class ToOdt(Tokenizer):
if theBits[0] in nwLabels.KEY_NAME:
tText = nwLabels.KEY_NAME[theBits[0]]
rTxt += "**%s:** " % tText
rFmt += "_B%s b_ " % (" "*len(tText))
rFmt += "_B%s b_ " % (" "*len(tText))
if len(theBits) > 1:
if theBits[0] == nwKeyWords.TAG_KEY:
rTxt += "%s" % theBits[1]
@@ -561,7 +562,7 @@ class ToOdt(Tokenizer):
##
if len(theText) != len(theFmt):
# Genrate dummy format if there isn't any
# Generate an empty format if there isn't any or it doesn't match
theFmt = " "*len(theText)
# XML functions
@@ -643,7 +644,7 @@ class ToOdt(Tokenizer):
"""
refStyle = self._mainPara.get(parName, None)
if refStyle is None:
logger.error("Unknown paragraph style '%s'" % parName)
logger.error("Unknown paragraph style '%s'", parName)
return "Standard"
if not refStyle.checkNew(oStyle):
@@ -984,6 +985,7 @@ class ToOdt(Tokenizer):
# END Class ToOdt
# =============================================================================================== #
# Auto-Style Classes
# =============================================================================================== #
@@ -1014,6 +1016,8 @@ class ODTParagraphStyle():
self._pAttr = {
"margin-top": ["fo", None],
"margin-bottom": ["fo", None],
"margin-left": ["fo", None],
"margin-right": ["fo", None],
"line-height": ["fo", None],
"text-align": ["fo", None],
"break-before": ["fo", None],
@@ -1070,6 +1074,14 @@ class ODTParagraphStyle():
self._pAttr["margin-bottom"][1] = str(theValue)
return
def setMarginLeft(self, theValue):
self._pAttr["margin-left"][1] = str(theValue)
return
def setMarginRight(self, theValue):
self._pAttr["margin-right"][1] = str(theValue)
return
def setLineHeight(self, theValue):
self._pAttr["line-height"][1] = str(theValue)
return
@@ -1148,13 +1160,13 @@ class ODTParagraphStyle():
"""Check if there are new settings in refStyle that differ from
those in the current object.
"""
for aName, (aNm, aVal) in refStyle._mAttr.items():
for aName, (_, aVal) in refStyle._mAttr.items():
if aVal is not None and aVal != self._mAttr[aName][1]:
return True
for aName, (aNm, aVal) in refStyle._pAttr.items():
for aName, (_, aVal) in refStyle._pAttr.items():
if aVal is not None and aVal != self._pAttr[aName][1]:
return True
for aName, (aNm, aVal) in refStyle._tAttr.items():
for aName, (_, aVal) in refStyle._tAttr.items():
if aVal is not None and aVal != self._tAttr[aName][1]:
return True
return False
@@ -1201,6 +1213,7 @@ class ODTParagraphStyle():
# END Class ODTParagraphStyle
class ODTTextStyle():
"""Wrapper class for the text style setting used by the exporter.
Only the used settings are exposed here to keep the class minimal
@@ -1271,6 +1284,7 @@ class ODTTextStyle():
# END Class ODTTextStyle
# =============================================================================================== #
# Local Functions
# =============================================================================================== #
@@ -1281,5 +1295,5 @@ def _mkTag(nsName, tagName):
theNS = XML_NS.get(nsName, "")
if theNS:
return "{%s}%s" % (theNS, tagName)
logger.warning("Missing xml namespace '%s'" % nsName)
logger.warning("Missing xml namespace '%s'", nsName)
return tagName

View File

@@ -1,193 +0,0 @@
# -*- coding: utf-8 -*-
"""
novelWriter – Various Tools
===========================
Various core tool functions
File History:
Created: 2019-04-22 [0.0.1] countWords
Created: 2019-10-13 [0.2.3] numberToWord, _numberToWordEN
Merged: 2020-05-08 [0.4.5] All of the above into this file
Created: 2020-07-05 [0.10.0] numberToRoman
This file is a part of novelWriter
Copyright 2018–2021, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
from nw.constants import nwUnicode
logger = logging.getLogger(__name__)
# =============================================================================================== #
# Simple Word Counter
# =============================================================================================== #
def countWords(theText):
"""Count words in a piece of text, skipping special syntax and
comments.
"""
charCount = 0
wordCount = 0
paraCount = 0
prevEmpty = True
# We need to treat dashes as word separators for counting words.
# The check+replace apprach is much faster that direct replace for
# large texts, and a bit slower for small texts, but in the latter
# case it doesn't matter.
if nwUnicode.U_ENDASH in theText:
theText = theText.replace(nwUnicode.U_ENDASH, " ")
if nwUnicode.U_EMDASH in theText:
theText = theText.replace(nwUnicode.U_EMDASH, " ")
for aLine in theText.splitlines():
countPara = True
theLen = len(aLine)
if theLen == 0:
prevEmpty = True
continue
if aLine[0] == "@" or aLine[0] == "%":
continue
if aLine[0:5] == "#### ":
wordCount -= 1
charCount -= 5
countPara = False
elif aLine[0:4] == "### ":
wordCount -= 1
charCount -= 4
countPara = False
elif aLine[0:3] == "## ":
wordCount -= 1
charCount -= 3
countPara = False
elif aLine[0:2] == "# ":
wordCount -= 1
charCount -= 2
countPara = False
wordCount += len(aLine.split())
charCount += theLen
if countPara and prevEmpty:
paraCount += 1
prevEmpty = not countPara
return charCount, wordCount, paraCount
# =============================================================================================== #
# Convert an Integer to a Roman Number
# =============================================================================================== #
def numberToRoman(numVal, isLower=False):
"""Convert an integer to a roman number.
"""
if not isinstance(numVal, int):
return "NAN"
if numVal < 1 or numVal > 4999:
return "OOR"
theValues = [
(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"), (100, "C"), (90, "XC"),
(50, "L"), (40, "XL"), (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"),
]
romNum = ""
for theDiv, theSym in theValues:
n = numVal//theDiv
romNum += n*theSym
numVal -= n*theDiv
if numVal <= 0:
break
return romNum.lower() if isLower else romNum
# =============================================================================================== #
# Convert an Integer to a Word Number
# =============================================================================================== #
def numberToWord(numVal, theLanguage):
"""Wrapper for converting numbers to words for chapter headings.
"""
numWord = ""
if theLanguage == "en":
numWord = _numberToWordEN(numVal)
else:
numWord = _numberToWordEN(numVal)
return numWord
def _numberToWordEN(numVal):
"""Convert numbers to English words.
"""
oneWord = ""
tenWord = ""
hunWord = ""
if not isinstance(numVal, int):
return "[NaN]"
if numVal < 0:
return "[Negative]"
if numVal > 999:
return "[Out of Range]"
if numVal == 0:
return "Zero"
oneVal = numVal % 10
tenVal = (numVal - oneVal) % 100
hunVal = (numVal - tenVal - oneVal) % 1000
theHundreds = {
100: "One Hundred", 200: "Two Hundred", 300: "Three Hundred",
400: "Four Hundred", 500: "Five Hundred", 600: "Six Hundred",
700: "Seven Hundred", 800: "Eight Hundred", 900: "Nine Hundred",
}
theTens = {
20: "Twenty", 30: "Thirty", 40: "Forty", 50: "Fifty",
60: "Sixty", 70: "Seventy", 80: "Eighty", 90: "Ninety",
}
theTeens = {
0: "Ten", 1: "Eleven", 2: "Twelve", 3: "Thirteen", 4: "Fourteen",
5: "Fifteen", 6: "Sixteen", 7: "Seventeen", 8: "Eighteen", 9: "Nineteen",
}
theOnes = {
0: "", 1: "One", 2: "Two", 3: "Three", 4: "Four",
5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine",
}
retVale = ""
hunWord = theHundreds.get(hunVal, "")
if tenVal == 10:
oneWord = theTeens.get(oneVal, "")
retVale = f"{hunWord} {oneWord}".strip()
else:
oneWord = theOnes.get(oneVal, "")
if tenVal == 0:
retVale = f"{hunWord} {oneWord}".strip()
else:
tenWord = theTens.get(tenVal, "")
if oneVal == 0:
retVale = f"{hunWord} {tenWord}".strip()
else:
retVale = f"{hunWord} {tenWord}-{oneWord}".strip()
return retVale

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – Project Tree Class
================================
@@ -25,18 +24,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import nw
import logging
import os
import logging
from time import time
from lxml import etree
from hashlib import sha256
from time import time
from nw.core.item import NWItem
from nw.enum import nwItemType, nwItemClass, nwItemLayout
from nw.common import checkHandle
from nw.constants import (
nwFiles, nwItemType, nwItemClass, nwItemLayout, nwConst, nwLists
)
from nw.constants import nwConst, nwLists, nwFiles
from nw.core.item import NWItem
logger = logging.getLogger(__name__)
@@ -63,21 +61,23 @@ LAYOUT_MAP = {
},
}
class NWTree():
def __init__(self, theProject):
self.theProject = theProject
self._projTree = {} # Holds all the items of the project
self._treeOrder = [] # The order of the tree items on the tree view
self._treeRoots = [] # The root items of the tree
self._trashRoot = None # The handle of the trash root folder
self._archRoot = None # The handle of the archive root folder
self._theIndex = 0 # The current iterator index
self._treeChanged = False # True if tree structure has changed
self._projTree = {} # Holds all the items of the project
self._treeOrder = [] # The order of the tree items on the tree view
self._treeRoots = [] # The root items of the tree
self._trashRoot = None # The handle of the trash root folder
self._archRoot = None # The handle of the archive root folder
self._theIndex = 0 # The current iterator index
self._treeChanged = False # True if tree structure has changed
self._handleSeed = None # Used for generating handles for testing
self._handleSeed = None # Used for generating handles for testing
self._handleCount = 0 # A counter that is added to the handle generator
return
@@ -111,24 +111,24 @@ class NWTree():
tHandle = self._makeHandle()
if tHandle in self._projTree:
logger.warning("Duplicate handle %s detected, skipping" % tHandle)
logger.warning("Duplicate handle '%s' detected, skipping", tHandle)
return False
logger.verbose("Adding item %s with parent %s" % (str(tHandle), str(pHandle)))
logger.verbose("Adding item '%s' with parent '%s'", str(tHandle), str(pHandle))
nwItem.setHandle(tHandle)
nwItem.setParent(pHandle)
if nwItem.itemType == nwItemType.ROOT:
logger.verbose("Item %s is a root item" % str(tHandle))
logger.verbose("Item '%s' is a root item", str(tHandle))
self._treeRoots.append(tHandle)
if nwItem.itemClass == nwItemClass.ARCHIVE:
logger.verbose("Item %s is the archive folder" % str(tHandle))
logger.verbose("Item '%s' is the archive folder", str(tHandle))
self._archRoot = tHandle
if nwItem.itemType == nwItemType.TRASH:
if self._trashRoot is None:
logger.verbose("Item %s is the trash folder" % str(tHandle))
logger.verbose("Item '%s' is the trash folder", str(tHandle))
self._trashRoot = tHandle
else:
logger.error("Only one trash folder allowed")
@@ -193,7 +193,7 @@ class NWTree():
try:
# Dump the text
tocText = os.path.join(self.theProject.projPath, nwFiles.TOC_TXT)
with open(tocText, mode="w", encoding="utf8") as outFile:
with open(tocText, mode="w", encoding="utf-8") as outFile:
outFile.write("\n")
outFile.write("Table of Contents\n")
outFile.write("=================\n")
@@ -245,9 +245,10 @@ class NWTree():
if iLayout in LAYOUT_MAP:
if hLevel in LAYOUT_MAP[iLayout]:
tItem.itemLayout = LAYOUT_MAP[iLayout][hLevel]
logger.debug("Changed layout for %s from %s to %s" % (
logger.debug(
"Changed layout for %s from %s to %s",
tHandle, iLayout.name, tItem.itemLayout.name
))
)
return True
return False
@@ -357,13 +358,13 @@ class NWTree():
if tHandle in self._projTree:
tmpOrder.append(tHandle)
else:
logger.error("Handle %s in new tree order is not in project tree" % tHandle)
logger.error("Handle '%s' in new tree order is not in project tree", tHandle)
# Do a reverse lookup to check for items that will be lost
# This is mainly for debugging purposes
for tHandle in self._treeOrder:
if tHandle not in tmpOrder:
logger.warning("Handle %s in old tree order is not in new tree order" % tHandle)
logger.warning("Handle '%s' in old tree order is not in new tree order", tHandle)
# Save the temp list
self._treeOrder = tmpOrder
@@ -387,7 +388,7 @@ class NWTree():
if tItem is None:
return False
if tItem.itemType != nwItemType.FILE:
logger.error("Item %s is not a file" % tHandle)
logger.error("Item %s is not a file", tHandle)
return False
if not isinstance(itemLayout, nwItemLayout):
return False
@@ -444,7 +445,7 @@ class NWTree():
"""
if tHandle in self._projTree:
return self._projTree[tHandle]
logger.error("No tree item with handle %s" % str(tHandle))
logger.error("No tree item with handle '%s'", str(tHandle))
return None
def __delitem__(self, tHandle):
@@ -454,7 +455,7 @@ class NWTree():
self._treeOrder.remove(tHandle)
del self._projTree[tHandle]
else:
logger.warning("Failed to delete item %s: item not found" % tHandle)
logger.warning("Failed to delete item '%s': item not found", tHandle)
return
if tHandle in self._treeRoots:
@@ -516,13 +517,14 @@ class NWTree():
handle requests come faster than the clock resolution.
"""
if self._handleSeed is None:
newSeed = str(time()) + addSeed
newSeed = "%s_%d_%s" % (str(time()), self._handleCount, addSeed)
self._handleCount += 1
else:
# This is used for debugging
newSeed = str(self._handleSeed)
self._handleSeed += 1
logger.verbose("Generating handle with seed '%s'" % newSeed)
logger.verbose("Generating handle with seed '%s'", newSeed)
itemHandle = sha256(newSeed.encode()).hexdigest()[0:13]
if itemHandle in self._projTree:
logger.warning("Duplicate handle encountered! Retrying ...")

42
nw/dialogs/__init__.py Normal file
View File

@@ -0,0 +1,42 @@
"""
novelWriter – Dialogs Init
==========================
This file is a part of novelWriter
Copyright 2018–2021, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from nw.dialogs.about import GuiAbout
from nw.dialogs.docmerge import GuiDocMerge
from nw.dialogs.docsplit import GuiDocSplit
from nw.dialogs.itemeditor import GuiItemEditor
from nw.dialogs.preferences import GuiPreferences
from nw.dialogs.projload import GuiProjectLoad
from nw.dialogs.projsettings import GuiProjectSettings
from nw.dialogs.quotes import GuiQuoteSelect
from nw.dialogs.wordlist import GuiWordList
__all__ = [
"GuiAbout",
"GuiDocMerge",
"GuiDocSplit",
"GuiItemEditor",
"GuiPreferences",
"GuiProjectLoad",
"GuiProjectSettings",
"GuiQuoteSelect",
"GuiWordList",
]

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – GUI About Box
===========================
@@ -25,13 +24,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import nw
import logging
import os
import logging
from datetime import datetime
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QCursor
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
qApp, QDialog, QHBoxLayout, QVBoxLayout, QDialogButtonBox, QTabWidget,
QTextBrowser, QLabel
@@ -39,6 +38,7 @@ from PyQt5.QtWidgets import (
logger = logging.getLogger(__name__)
class GuiAbout(QDialog):
def __init__(self, theParent):
@@ -55,7 +55,7 @@ class GuiAbout(QDialog):
self.innerBox = QHBoxLayout()
self.innerBox.setSpacing(self.mainConf.pxInt(16))
self.setWindowTitle("About novelWriter")
self.setWindowTitle(self.tr("About novelWriter"))
self.setMinimumWidth(self.mainConf.pxInt(650))
self.setMinimumHeight(self.mainConf.pxInt(600))
@@ -90,9 +90,9 @@ class GuiAbout(QDialog):
# Main Tab Area
self.tabBox = QTabWidget()
self.tabBox.addTab(self.pageAbout, "About")
self.tabBox.addTab(self.pageNotes, "Release")
self.tabBox.addTab(self.pageLicense, "License")
self.tabBox.addTab(self.pageAbout, self.tr("About"))
self.tabBox.addTab(self.pageNotes, self.tr("Release"))
self.tabBox.addTab(self.pageLicense, self.tr("Licence"))
self.innerBox.addWidget(self.tabBox)
# OK Button
@@ -131,81 +131,93 @@ class GuiAbout(QDialog):
def _fillAboutPage(self):
"""Generate the content for the About page.
"""
listPrefix = "&nbsp;&nbsp;&bull;&nbsp;&nbsp;"
aboutMsg = (
"<h2>About novelWriter</h2>"
"<p>{copyright:s}.</p>"
"<p>Website: <a href='{website:s}'>{domain:s}</a></p>"
"<p>novelWriter is a markdown-like text editor designed for "
"organising and writing novels. It is written in Python 3 with a "
"Qt5 GUI, using PyQt5.</p>"
"<p>novelWriter is free software: you can redistribute it and/or "
"modify it under the terms of the GNU General Public License as "
"published by the Free Software Foundation, either version 3 of "
"the License, or (at your option) any later version.</p>"
"<p>novelWriter is distributed in the hope that it will be "
"useful, but WITHOUT ANY WARRANTY; without even the implied "
"warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR "
"PURPOSE.</p>"
"<p>See the License tab for the full license text, or visit the "
"GNU website at "
"<a href='https://www.gnu.org/licenses/gpl-3.0.html'>GPL v3.0</a> "
"for more details.</p>"
"<h3>Credits</h3>"
"<p>{credits:s}</p>"
aboutMsg = (
"<h2>{title1}</h2>"
"<p>{copy}</p>"
"<p>{link}</p>"
"<p>{intro}</p>"
"<p>{license1}</p>"
"<p>{license2}</p>"
"<p>{license3}</p>"
"<h3>{title2}</h3>"
"<p>{credits}</p>"
).format(
copyright = nw.__copyright__,
website = nw.__url__,
domain = nw.__domain__,
credits = "<br/>".join(["%s%s" % (listPrefix, x) for x in nw.__credits__]),
title1=self.tr("About novelWriter"),
copy=nw.__copyright__,
link=self.tr("Website: {0}").format(f"<a href='{nw.__url__}'>{nw.__domain__}</a>"),
title2=self.tr("Credits"),
credits=self._wrapTable([
(self.tr("Developer"), "Veronica Berglyd Olsen"),
(self.tr("Concept"), "Veronica Berglyd Olsen, Marian Lückhof"),
(self.tr("i18n"), "Bruno Meneguello"),
]),
intro=self.tr(
"novelWriter is a markdown-like text editor designed for organising and "
"writing novels. It is written in Python 3 with a Qt5 GUI, using PyQt5."
),
license1=self.tr(
"novelWriter is free software: you can redistribute it and/or modify it "
"under the terms of the GNU General Public License as published by the "
"Free Software Foundation, either version 3 of the License, or (at your "
"option) any later version."
),
license2=self.tr(
"novelWriter is distributed in the hope that it will be useful, but "
"WITHOUT ANY WARRANTY; without even the implied warranty of "
"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE."
),
license3=self.tr(
"See the Licence tab for the full licence text, or visit the "
"GNU website at {0} for more details."
).format(
"<a href='https://www.gnu.org/licenses/gpl-3.0.html'>GPL v3.0</a>"
),
)
aboutMsg += "<h4>%s</h4><p>%s</p>" % (
self.tr("Translations"),
self._wrapTable([
("English", "Veronica Berglyd Olsen"),
("Français", "Jan Lüdke (jyhelle)"),
("Norsk Bokmål", "Veronica Berglyd Olsen"),
("Português", "Bruno Meneguello"),
("简体中文", "Qianzhi Long"),
])
)
theTheme = self.theParent.theTheme
theIcons = self.theParent.theTheme.theIcons
if theTheme.themeName:
aboutMsg += (
"<h4>Theme: {name:s}</h4>"
"<p>"
"<b>Author:</b> {author:s}<br/>"
"<b>Credit:</b> {credit:s}<br/>"
"<b>License:</b> <a href='{lic_url:s}'>{license:s}</a>"
"</p>"
).format(
name = theTheme.themeName,
author = theTheme.themeAuthor,
credit = theTheme.themeCredit,
license = theTheme.themeLicense,
lic_url = theTheme.themeLicenseUrl,
if theTheme.themeName and theTheme.themeAuthor != "N/A":
licURL = f"<a href='{theTheme.themeLicenseUrl}'>{theTheme.themeLicense}</a>"
aboutMsg += "<h4>%s</h4><p>%s</p>" % (
self.tr("Theme: {0}").format(theTheme.themeName),
self._wrapTable([
(self.tr("Author"), theTheme.themeAuthor),
(self.tr("Credit"), theTheme.themeCredit),
(self.tr("Licence"), licURL),
])
)
if theIcons.themeName:
aboutMsg += (
"<h4>Icons: {name:s}</h4>"
"<p>"
"<b>Author:</b> {author:s}<br/>"
"<b>Credit:</b> {credit:s}<br/>"
"<b>License:</b> <a href='{lic_url:s}'>{license:s}</a>"
"</p>"
).format(
name = theIcons.themeName,
author = theIcons.themeAuthor,
credit = theIcons.themeCredit,
license = theIcons.themeLicense,
lic_url = theIcons.themeLicenseUrl,
licURL = f"<a href='{theIcons.themeLicenseUrl}'>{theIcons.themeLicense}</a>"
aboutMsg += "<h4>%s</h4><p>%s</p>" % (
self.tr("Icons: {0}").format(theIcons.themeName),
self._wrapTable([
(self.tr("Author"), theIcons.themeAuthor),
(self.tr("Credit"), theIcons.themeCredit),
(self.tr("Licence"), licURL),
])
)
if theTheme.syntaxName:
aboutMsg += (
"<h4>Syntax: {name:s}</h4>"
"<p>"
"<b>Author:</b> {author:s}<br/>"
"<b>Credit:</b> {credit:s}<br/>"
"<b>License:</b> <a href='{lic_url:s}'>{license:s}</a>"
"</p>"
).format(
name = theTheme.syntaxName,
author = theTheme.syntaxAuthor,
credit = theTheme.syntaxCredit,
license = theTheme.syntaxLicense,
lic_url = theTheme.syntaxLicenseUrl,
licURL = f"<a href='{theTheme.syntaxLicenseUrl}'>{theTheme.syntaxLicense}</a>"
aboutMsg += "<h4>%s</h4><p>%s</p>" % (
self.tr("Syntax: {0}").format(theTheme.syntaxName),
self._wrapTable([
(self.tr("Author"), theTheme.syntaxAuthor),
(self.tr("Credit"), theTheme.syntaxCredit),
(self.tr("Licence"), licURL),
])
)
self.pageAbout.setHtml(aboutMsg)
@@ -217,7 +229,7 @@ class GuiAbout(QDialog):
"""
docPath = os.path.join(self.mainConf.assetPath, "text", "release_notes.htm")
if os.path.isfile(docPath):
with open(docPath, mode="r", encoding="utf8") as inFile:
with open(docPath, mode="r", encoding="utf-8") as inFile:
helpText = inFile.read()
self.pageNotes.setHtml(helpText)
else:
@@ -225,17 +237,27 @@ class GuiAbout(QDialog):
return
def _fillLicensePage(self):
"""Load the content for the License page.
"""Load the content for the Licence page.
"""
docPath = os.path.join(self.mainConf.assetPath, "text", "gplv3_en.htm")
if os.path.isfile(docPath):
with open(docPath, mode="r", encoding="utf8") as inFile:
with open(docPath, mode="r", encoding="utf-8") as inFile:
helpText = inFile.read()
self.pageLicense.setHtml(helpText)
else:
self.pageLicense.setHtml("Error loading license text ...")
self.pageLicense.setHtml("Error loading licence text ...")
return
def _wrapTable(self, theData):
"""Wrap a list of label/value tuples in a html table.
"""
theTable = []
for aLabel, aValue in theData:
theTable.append(
f"<tr><td><b>{aLabel}:</b></td><td>{aValue}</td></tr>"
)
return "<table>%s</table>" % "".join(theTable)
def _setStyleSheet(self):
"""Set stylesheet for all browser tabs
"""
@@ -246,10 +268,13 @@ class GuiAbout(QDialog):
"a {{"
" color: rgb({hColR},{hColG},{hColB});"
"}}\n"
"td {{"
" padding-right: 0.8em;"
"}}\n"
).format(
hColR = self.theParent.theTheme.colHead[0],
hColG = self.theParent.theTheme.colHead[1],
hColB = self.theParent.theTheme.colHead[2],
hColR=self.theParent.theTheme.colHead[0],
hColG=self.theParent.theTheme.colHead[1],
hColB=self.theParent.theTheme.colHead[2],
)
self.pageAbout.document().setDefaultStyleSheet(styleSheet)
self.pageNotes.document().setDefaultStyleSheet(styleSheet)

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – GUI Doc Merge Tool
================================
@@ -33,15 +32,16 @@ from PyQt5.QtWidgets import (
QListWidgetItem, QDialogButtonBox
)
from nw.constants import nwAlert, nwItemType
from nw.gui.custom import QHelpLabel
from nw.core import NWDoc
from nw.enum import nwAlert, nwItemType
from nw.gui.custom import QHelpLabel
logger = logging.getLogger(__name__)
class GuiDocMerge(QDialog):
def __init__(self, theParent, theProject):
def __init__(self, theParent):
QDialog.__init__(self, theParent)
logger.debug("Initialising GuiDocMerge ...")
@@ -49,15 +49,15 @@ class GuiDocMerge(QDialog):
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theProject = theProject
self.theProject = theParent.theProject
self.sourceItem = None
self.outerBox = QVBoxLayout()
self.setWindowTitle("Merge Documents")
self.setWindowTitle(self.tr("Merge Documents"))
self.headLabel = QLabel("<b>Documents to Merge</b>")
self.headLabel = QLabel("<b>%s</b>" % self.tr("Documents to Merge"))
self.helpLabel = QHelpLabel(
"Drag and drop items to change the order.", self.theParent.theTheme.helpText
self.tr("Drag and drop items to change the order."), self.theParent.theTheme.helpText
)
self.listBox = QListWidget()
@@ -102,42 +102,51 @@ class GuiDocMerge(QDialog):
finalOrder.append(self.listBox.item(i).data(Qt.UserRole))
if len(finalOrder) == 0:
self.theParent.makeAlert((
self.theParent.makeAlert(self.tr(
"No source documents found. Nothing to do."
), nwAlert.ERROR)
return
return False
theDoc = NWDoc(self.theProject, self.theParent)
theText = ""
for tHandle in finalOrder:
theText += theDoc.openDocument(tHandle, False).rstrip("\n")
theText += "\n\n"
inDoc = NWDoc(self.theProject, tHandle)
docText = inDoc.readDocument()
docErr = inDoc.getError()
if docText is None and docErr:
self.theParent.makeAlert([
self.tr("Failed to open document file."), docErr
], nwAlert.ERROR)
if docText:
theText += docText.rstrip("\n")+"\n\n"
if self.sourceItem is None:
self.theParent.makeAlert((
"No source document selected. Nothing to do."
self.theParent.makeAlert(self.tr(
"No source folder selected. Nothing to do."
), nwAlert.ERROR)
return
return False
srcItem = self.theProject.projTree[self.sourceItem]
if srcItem is None:
self.theParent.makeAlert((
"Could not parse source document."
), nwAlert.ERROR)
return
self.theParent.makeAlert(self.tr("Internal error."), nwAlert.ERROR)
return False
nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemClass, srcItem.itemParent)
newItem = self.theProject.projTree[nHandle]
newItem.setStatus(srcItem.itemStatus)
theDoc.openDocument(nHandle, False)
theDoc.saveDocument(theText)
outDoc = NWDoc(self.theProject, nHandle)
if not outDoc.writeDocument(theText):
self.theParent.makeAlert([
self.tr("Could not save document."), outDoc.getError()
], nwAlert.ERROR)
return False
self.theParent.treeView.revealNewTreeItem(nHandle)
self.theParent.openDocument(nHandle, doScroll=True)
self._doClose()
return
return True
def _doClose(self):
"""Close the dialog window without doing anything.
@@ -158,16 +167,17 @@ class GuiDocMerge(QDialog):
tHandle = self.theParent.treeView.getSelectedHandle()
self.sourceItem = tHandle
if tHandle is None:
return
return False
nwItem = self.theProject.projTree[tHandle]
if nwItem is None:
return
return False
if nwItem.itemType is not nwItemType.FOLDER:
self.theParent.makeAlert((
self.theParent.makeAlert(self.tr(
"Element selected in the project tree must be a folder."
), nwAlert.ERROR)
return
return False
for sHandle in self.theParent.treeView.getTreeFromHandle(tHandle):
newItem = QListWidgetItem()
@@ -178,6 +188,6 @@ class GuiDocMerge(QDialog):
newItem.setData(Qt.UserRole, sHandle)
self.listBox.addItem(newItem)
return
return True
# END Class GuiDocMerge

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – GUI Doc Split Tool
================================
@@ -34,16 +33,16 @@ from PyQt5.QtWidgets import (
)
from nw.core import NWDoc
from nw.enum import nwAlert, nwItemType, nwItemClass, nwItemLayout
from nw.constants import nwConst
from nw.gui.custom import QHelpLabel
from nw.constants import (
nwAlert, nwItemType, nwItemClass, nwItemLayout, nwConst
)
logger = logging.getLogger(__name__)
class GuiDocSplit(QDialog):
def __init__(self, theParent, theProject):
def __init__(self, theParent):
QDialog.__init__(self, theParent)
logger.debug("Initialising GuiDocSplit ...")
@@ -51,16 +50,19 @@ class GuiDocSplit(QDialog):
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theProject = theProject
self.optState = self.theProject.optState
self.theProject = theParent.theProject
self.optState = theParent.theProject.optState
self.sourceItem = None
self.sourceText = []
self.outerBox = QVBoxLayout()
self.setWindowTitle("Split Document")
self.setWindowTitle(self.tr("Split Document"))
self.headLabel = QLabel("<b>Document Headers</b>")
self.headLabel = QLabel("<b>%s</b>" % self.tr("Document Headers"))
self.helpLabel = QHelpLabel(
"Select the maximum level to split into files.", self.theParent.theTheme.helpText
self.tr("Select the maximum level to split into files."),
self.theParent.theTheme.helpText
)
self.listBox = QListWidget()
@@ -69,10 +71,10 @@ class GuiDocSplit(QDialog):
self.listBox.setMinimumHeight(self.mainConf.pxInt(180))
self.splitLevel = QComboBox(self)
self.splitLevel.addItem("Split on Header Level 1 (Title)", 1)
self.splitLevel.addItem("Split up to Header Level 2 (Chapter)", 2)
self.splitLevel.addItem("Split up to Header Level 3 (Scene)", 3)
self.splitLevel.addItem("Split up to Header Level 4 (Section)", 4)
self.splitLevel.addItem(self.tr("Split on Header Level 1 (Title)"), 1)
self.splitLevel.addItem(self.tr("Split up to Header Level 2 (Chapter)"), 2)
self.splitLevel.addItem(self.tr("Split up to Header Level 3 (Scene)"), 3)
self.splitLevel.addItem(self.tr("Split up to Header Level 4 (Section)"), 4)
spIndex = self.splitLevel.findData(
self.optState.getInt("GuiDocSplit", "spLevel", 3)
)
@@ -115,23 +117,31 @@ class GuiDocSplit(QDialog):
logger.verbose("GuiDocSplit split button clicked")
if self.sourceItem is None:
self.theParent.makeAlert((
self.theParent.makeAlert(self.tr(
"No source document selected. Nothing to do."
), nwAlert.ERROR)
return
return False
srcItem = self.theProject.projTree[self.sourceItem]
if srcItem is None:
self.theParent.makeAlert((
self.theParent.makeAlert(self.tr(
"Could not parse source document."
), nwAlert.ERROR)
return
return False
theDoc = NWDoc(self.theProject, self.theParent)
theText = theDoc.openDocument(self.sourceItem, False)
theLines = theText.splitlines()
nLines = len(theLines)
theLines.insert(0, "%Split Doc")
inDoc = NWDoc(self.theProject, self.sourceItem)
theText = inDoc.readDocument()
docErr = inDoc.getError()
if theText is None and docErr:
self.theParent.makeAlert([
self.tr("Failed to open document file."), docErr
], nwAlert.ERROR)
if theText is None:
theText = ""
nLines = len(self.sourceText)
logger.debug(
"Splitting document %s with %d lines" % (self.sourceItem, nLines)
)
@@ -147,35 +157,41 @@ class GuiDocSplit(QDialog):
nFiles = len(finalOrder)
if nFiles == 0:
self.theParent.makeAlert((
self.theParent.makeAlert(self.tr(
"No headers found. Nothing to do."
), nwAlert.ERROR)
return
return False
# Check that another folder can be created
parTree = self.theProject.projTree.getItemPath(srcItem.itemParent)
if len(parTree) >= nwConst.MAX_DEPTH - 1:
self.theParent.makeAlert((
self.theParent.makeAlert(self.tr(
"Cannot add new folder for the document split. "
"Maximum folder depth has been reached. "
"Please move the file to another level in the project tree."
), nwAlert.ERROR)
return
return False
msgYes = self.theParent.askQuestion("Split Document", (
"The document will be split into %d file(s) in a new folder. "
"The original document will remain intact.<br><br>"
"Continue with the splitting process?"
) % nFiles)
msgYes = self.theParent.askQuestion(
self.tr("Split Document"),
"%s<br><br>%s" % (
self.tr(
"The document will be split into {0} file(s) in a new folder. "
"The original document will remain intact.").format(nFiles),
self.tr(
"Continue with the splitting process?"
)
)
)
if not msgYes:
return
return False
# Create the folder
fHandle = self.theProject.newFolder(
srcItem.itemName, srcItem.itemClass, srcItem.itemParent
)
self.theParent.treeView.revealNewTreeItem(fHandle)
logger.verbose("Creating folder %s" % fHandle)
logger.verbose("Creating folder '%s'", fHandle)
# Loop through, and create the files
for wTitle, iStart, iEnd in finalOrder:
@@ -200,20 +216,25 @@ class GuiDocSplit(QDialog):
newItem.setStatus(srcItem.itemStatus)
logger.verbose(
"Creating new document %s with text from line %d to %d" % (
nHandle, iStart, iEnd-1
nHandle, iStart+1, iEnd
)
)
theText = "\n".join(theLines[iStart:iEnd])
theText = "\n".join(self.sourceText[iStart:iEnd])
theText = theText.rstrip("\n") + "\n\n"
theDoc.openDocument(nHandle, False)
theDoc.saveDocument(theText)
theDoc.clearDocument()
outDoc = NWDoc(self.theProject, nHandle)
if not outDoc.writeDocument(theText):
self.theParent.makeAlert([
self.tr("Could not save document."), outDoc.getError()
], nwAlert.ERROR)
return False
self.theParent.treeView.revealNewTreeItem(nHandle)
self._doClose()
return
return True
def _doClose(self):
"""Close the dialog window without doing anything.
@@ -232,24 +253,28 @@ class GuiDocSplit(QDialog):
are then added to the list view in order. The list itself can be
reordered by the user.
"""
self.listBox.clear()
if self.sourceItem is None:
self.sourceItem = self.theParent.treeView.getSelectedHandle()
if self.sourceItem is None:
return
return False
nwItem = self.theProject.projTree[self.sourceItem]
if nwItem is None:
return
return False
if nwItem.itemType is not nwItemType.FILE:
self.theParent.makeAlert((
self.theParent.makeAlert(self.tr(
"Element selected in the project tree must be a file."
), nwAlert.ERROR)
return
return False
self.listBox.clear()
theDoc = NWDoc(self.theProject, self.theParent)
theText = theDoc.openDocument(self.sourceItem, False)
inDoc = NWDoc(self.theProject, self.sourceItem)
theText = inDoc.readDocument()
if theText is None:
theText = ""
return False
spLevel = self.splitLevel.currentData()
self.optState.setValue("GuiDocSplit", "spLevel", spLevel)
@@ -257,12 +282,10 @@ class GuiDocSplit(QDialog):
"Scanning document %s for headings level <= %d" % (self.sourceItem, spLevel)
)
lineNo = 0
for aLine in theText.splitlines():
lineNo += 1
onLine = 0
self.sourceText = theText.splitlines()
for lineNo, aLine in enumerate(self.sourceText):
onLine = -1
if aLine.startswith("# ") and spLevel >= 1:
onLine = lineNo
elif aLine.startswith("## ") and spLevel >= 2:
@@ -272,12 +295,12 @@ class GuiDocSplit(QDialog):
elif aLine.startswith("#### ") and spLevel >= 4:
onLine = lineNo
if onLine > 0:
if onLine >= 0:
newItem = QListWidgetItem()
newItem.setText(aLine.strip())
newItem.setData(Qt.UserRole, onLine)
self.listBox.addItem(newItem)
return
return True
# END Class GuiDocSplit

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – GUI Item Editor
=============================
@@ -33,22 +32,24 @@ from PyQt5.QtWidgets import (
QDialogButtonBox
)
from nw.enum import nwItemLayout, nwItemType
from nw.constants import trConst, nwLists, nwLabels
from nw.gui.custom import QSwitch
from nw.constants import nwLabels, nwItemLayout, nwItemType, nwLists
logger = logging.getLogger(__name__)
class GuiItemEditor(QDialog):
def __init__(self, theParent, theProject, tHandle):
def __init__(self, theParent, tHandle):
QDialog.__init__(self, theParent)
logger.debug("Initialising GuiItemEditor ...")
self.setObjectName("GuiItemEditor")
self.mainConf = nw.CONFIG
self.theProject = theProject
self.theParent = theParent
self.theProject = theParent.theProject
##
# Build GUI
@@ -58,7 +59,7 @@ class GuiItemEditor(QDialog):
if self.theItem is None:
self._doClose()
self.setWindowTitle("Item Settings")
self.setWindowTitle(self.tr("Item Settings"))
mVd = self.mainConf.pxInt(220)
mSp = self.mainConf.pxInt(16)
@@ -103,10 +104,10 @@ class GuiItemEditor(QDialog):
for itemLayout in nwItemLayout:
if itemLayout in validLayouts:
self.editLayout.addItem(nwLabels.LAYOUT_NAME[itemLayout], itemLayout)
self.editLayout.addItem(trConst(nwLabels.LAYOUT_NAME[itemLayout]), itemLayout)
# Export Switch
self.textExport = QLabel("Include when building project")
self.textExport = QLabel(self.tr("Include when building project"))
self.editExport = QSwitch()
if self.theItem.itemType == nwItemType.FILE:
self.editExport.setEnabled(True)
@@ -136,17 +137,21 @@ class GuiItemEditor(QDialog):
# Assemble
##
nameLabel = QLabel(self.tr("Label"))
statusLabel = QLabel(self.tr("Status"))
layoutLabel = QLabel(self.tr("Layout"))
self.mainForm = QGridLayout()
self.mainForm.setVerticalSpacing(vSp)
self.mainForm.setHorizontalSpacing(mSp)
self.mainForm.addWidget(QLabel("Label"), 0, 0, 1, 1)
self.mainForm.addWidget(self.editName, 0, 1, 1, 2)
self.mainForm.addWidget(QLabel("Status"), 1, 0, 1, 1)
self.mainForm.addWidget(self.editStatus, 1, 1, 1, 2)
self.mainForm.addWidget(QLabel("Layout"), 2, 0, 1, 1)
self.mainForm.addWidget(self.editLayout, 2, 1, 1, 2)
self.mainForm.addWidget(self.textExport, 3, 0, 1, 2)
self.mainForm.addWidget(self.editExport, 3, 2, 1, 1)
self.mainForm.addWidget(nameLabel, 0, 0, 1, 1)
self.mainForm.addWidget(self.editName, 0, 1, 1, 2)
self.mainForm.addWidget(statusLabel, 1, 0, 1, 1)
self.mainForm.addWidget(self.editStatus, 1, 1, 1, 2)
self.mainForm.addWidget(layoutLabel, 2, 0, 1, 1)
self.mainForm.addWidget(self.editLayout, 2, 1, 1, 2)
self.mainForm.addWidget(self.textExport, 3, 0, 1, 2)
self.mainForm.addWidget(self.editExport, 3, 2, 1, 1)
self.mainForm.setColumnStretch(0, 0)
self.mainForm.setColumnStretch(1, 1)
self.mainForm.setColumnStretch(2, 0)
@@ -174,7 +179,7 @@ class GuiItemEditor(QDialog):
"""
logger.verbose("ItemEditor save button clicked")
itemName = self.editName.text()
itemName = self.editName.text()
itemStatus = self.editStatus.currentData()
itemLayout = self.editLayout.currentData()
isExported = self.editExport.isChecked()

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – GUI Open Project
==============================
@@ -25,13 +24,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import nw
import logging
import os
import logging
from datetime import datetime
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtGui import QKeySequence
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QPushButton, QTreeWidget,
QAbstractItemView, QTreeWidgetItem, QDialogButtonBox, QLabel, QShortcut,
@@ -43,6 +42,7 @@ from nw.constants import nwFiles
logger = logging.getLogger(__name__)
class GuiProjectLoad(QDialog):
NONE_STATE = 0
@@ -74,7 +74,7 @@ class GuiProjectLoad(QDialog):
self.outerBox.setSpacing(sPx)
self.innerBox.setSpacing(sPx)
self.setWindowTitle("Open Project")
self.setWindowTitle(self.tr("Open Project"))
self.setMinimumWidth(self.mainConf.pxInt(650))
self.setMinimumHeight(self.mainConf.pxInt(400))
self.setModal(True)
@@ -90,7 +90,11 @@ class GuiProjectLoad(QDialog):
self.listBox.setSelectionMode(QAbstractItemView.SingleSelection)
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
self.listBox.setColumnCount(3)
self.listBox.setHeaderLabels(["Working Title", "Words", "Last Opened"])
self.listBox.setHeaderLabels([
self.tr("Working Title"),
self.tr("Words"),
self.tr("Last Opened"),
])
self.listBox.setRootIsDecorated(False)
self.listBox.itemSelectionChanged.connect(self._doSelectRecent)
self.listBox.itemDoubleClicked.connect(self._doOpenRecent)
@@ -100,9 +104,9 @@ class GuiProjectLoad(QDialog):
treeHead.setTextAlignment(self.C_COUNT, Qt.AlignRight)
treeHead.setTextAlignment(self.C_TIME, Qt.AlignRight)
self.lblRecent = QLabel("<b>Recently Opened Projects</b>")
self.lblPath = QLabel("<b>Path</b>")
self.selPath = QLineEdit("")
self.lblRecent = QLabel("<b>%s</b>" % self.tr("Recently Opened Projects"))
self.lblPath = QLabel("<b>%s</b>" % self.tr("Path"))
self.selPath = QLineEdit("")
self.selPath.setReadOnly(True)
self.browseButton = QPushButton("...")
@@ -126,10 +130,10 @@ class GuiProjectLoad(QDialog):
self.buttonBox.accepted.connect(self._doOpenRecent)
self.buttonBox.rejected.connect(self._doCancel)
self.newButton = self.buttonBox.addButton("New", QDialogButtonBox.ActionRole)
self.newButton = self.buttonBox.addButton(self.tr("New"), QDialogButtonBox.ActionRole)
self.newButton.clicked.connect(self._doNewProject)
self.delButton = self.buttonBox.addButton("Remove", QDialogButtonBox.ActionRole)
self.delButton = self.buttonBox.addButton(self.tr("Remove"), QDialogButtonBox.ActionRole)
self.delButton.clicked.connect(self._doDeleteRecent)
self.outerBox.addLayout(self.innerBox)
@@ -180,12 +184,12 @@ class GuiProjectLoad(QDialog):
"""Browse for a folder path.
"""
logger.verbose("GuiProjectLoad browse button clicked")
dlgOpt = QFileDialog.Options()
dlgOpt |= QFileDialog.DontUseNativeDialog
extFilter = [
self.tr("novelWriter Project File ({0})").format(nwFiles.PROJ_FILE),
self.tr("All files ({0})").format("*"),
]
projFile, _ = QFileDialog.getOpenFileName(
self, "Open novelWriter Project", "",
"novelWriter Project File (%s);;All Files (*)" % nwFiles.PROJ_FILE,
options=dlgOpt
self, self.tr("Open Project"), "", filter=";;".join(extFilter)
)
if projFile:
thePath = os.path.abspath(os.path.dirname(projFile))
@@ -221,10 +225,13 @@ class GuiProjectLoad(QDialog):
selList = self.listBox.selectedItems()
if selList:
projName = selList[0].text(self.C_NAME)
msgYes = self.theParent.askQuestion("Remove Entry", (
"Remove '%s' from the recent projects list? "
"The project files will not be deleted."
) % projName)
msgYes = self.theParent.askQuestion(
self.tr("Remove Entry"),
self.tr(
"Remove '{0}' from the recent projects list? "
"The project files will not be deleted."
).format(projName)
)
if msgYes:
self.mainConf.removeFromRecentCache(
selList[0].data(self.C_NAME, Qt.UserRole)

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – GUI Project Settings
==================================
@@ -27,22 +26,23 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import nw
import logging
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QIcon, QPixmap, QColor, QBrush
from PyQt5.QtCore import Qt, QLocale
from PyQt5.QtWidgets import (
QHBoxLayout, QVBoxLayout, QLineEdit, QPlainTextEdit, QLabel, QWidget,
QDialogButtonBox, QListWidget, QPushButton, QListWidgetItem, QColorDialog,
QAbstractItemView, QTreeWidget, QTreeWidgetItem, QComboBox
QDialogButtonBox, QPushButton, QColorDialog, QTreeWidget, QTreeWidgetItem,
QComboBox
)
from nw.constants import nwAlert
from nw.enum import nwAlert
from nw.gui.custom import QSwitch, PagedDialog, QConfigLayout
logger = logging.getLogger(__name__)
class GuiProjectSettings(PagedDialog):
def __init__(self, theParent, theProject):
def __init__(self, theParent):
PagedDialog.__init__(self, theParent)
logger.debug("Initialising GuiProjectSettings ...")
@@ -50,11 +50,11 @@ class GuiProjectSettings(PagedDialog):
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theProject = theProject
self.optState = theProject.optState
self.theProject = theParent.theProject
self.optState = theParent.theProject.optState
self.theProject.countStatus()
self.setWindowTitle("Project Settings")
self.setWindowTitle(self.tr("Project Settings"))
wW = self.mainConf.pxInt(570)
wH = self.mainConf.pxInt(375)
@@ -71,10 +71,10 @@ class GuiProjectSettings(PagedDialog):
self.tabImport = GuiProjectEditStatus(self.theParent, self.theProject, False)
self.tabReplace = GuiProjectEditReplace(self.theParent, self.theProject)
self.addTab(self.tabMain, "Settings")
self.addTab(self.tabStatus, "Status")
self.addTab(self.tabImport, "Importance")
self.addTab(self.tabReplace, "Auto-Replace")
self.addTab(self.tabMain, self.tr("Settings"))
self.addTab(self.tabStatus, self.tr("Status"))
self.addTab(self.tabImport, self.tr("Importance"))
self.addTab(self.tabReplace, self.tr("Auto-Replace"))
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.buttonBox.accepted.connect(self._doSave)
@@ -143,15 +143,20 @@ class GuiProjectSettings(PagedDialog):
winWidth = self.mainConf.rpxInt(self.width())
winHeight = self.mainConf.rpxInt(self.height())
replaceColW = self.mainConf.rpxInt(self.tabReplace.listBox.columnWidth(0))
statusColW = self.mainConf.rpxInt(self.tabStatus.listBox.columnWidth(0))
importColW = self.mainConf.rpxInt(self.tabImport.listBox.columnWidth(0))
self.optState.setValue("GuiProjectSettings", "winWidth", winWidth)
self.optState.setValue("GuiProjectSettings", "winHeight", winHeight)
self.optState.setValue("GuiProjectSettings", "replaceColW", replaceColW)
self.optState.setValue("GuiProjectSettings", "statusColW", statusColW)
self.optState.setValue("GuiProjectSettings", "importColW", importColW)
return
# END Class GuiProjectSettings
class GuiProjectEditMain(QWidget):
def __init__(self, theParent, theProject):
@@ -166,88 +171,102 @@ class GuiProjectEditMain(QWidget):
self.mainForm.setHelpTextStyle(self.theParent.theTheme.helpText)
self.setLayout(self.mainForm)
self.mainForm.addGroupLabel("Project Settings")
self.mainForm.addGroupLabel(self.tr("Project Settings"))
xW = self.mainConf.pxInt(250)
xH = self.mainConf.pxInt(100)
xH = round(4.8*self.theParent.theTheme.fontPixelSize)
self.editName = QLineEdit()
self.editName.setMaxLength(200)
self.editName.setFixedWidth(xW)
self.editName.setMaximumWidth(xW)
self.editName.setText(self.theProject.projName)
self.mainForm.addRow(
"Working title",
self.tr("Working title"),
self.editName,
"Should be set only once."
self.tr("Should be set only once.")
)
self.editTitle = QLineEdit()
self.editTitle.setMaxLength(200)
self.editTitle.setFixedWidth(xW)
self.editTitle.setMaximumWidth(xW)
self.editTitle.setText(self.theProject.bookTitle)
self.mainForm.addRow(
"Novel title",
self.tr("Novel title"),
self.editTitle,
"Change whenever you want!"
self.tr("Change whenever you want!")
)
self.editAuthors = QPlainTextEdit()
bookAuthors = ""
for bookAuthor in self.theProject.bookAuthors:
bookAuthors += bookAuthor+"\n"
self.editAuthors.setPlainText(bookAuthors)
self.editAuthors.setFixedHeight(xH)
self.editAuthors.setFixedWidth(xW)
self.editAuthors.setMaximumHeight(xH)
self.editAuthors.setMaximumWidth(xW)
self.editAuthors.setPlainText("\n".join(self.theProject.bookAuthors))
self.mainForm.addRow(
"Author(s)",
self.tr("Author(s)"),
self.editAuthors,
"One name per line."
self.tr("One name per line.")
)
self.spellLang = QComboBox(self)
theDict = self.theParent.docEditor.theDict
self.spellLang.addItem("Default", "None")
self.spellLang.setMaximumWidth(xW)
theDict = self.theParent.docEditor.currentDictionary()
self.spellLang.addItem(self.tr("Default"), "None")
if theDict is not None:
for spTag, spName in theDict.listDictionaries():
self.spellLang.addItem(spName, spTag)
for spTag, spProv in theDict.listDictionaries():
qLocal = QLocale(spTag)
spLang = qLocal.nativeLanguageName().title()
self.spellLang.addItem("%s [%s]" % (spLang, spProv), spTag)
self.mainForm.addRow(
"Spell check language",
self.tr("Spell check language"),
self.spellLang,
"Overrides main preferences."
self.tr("Overrides main preferences.")
)
spellIdx = 0
if self.theProject.projLang is not None:
spellIdx = self.spellLang.findData(self.theProject.projLang)
if self.theProject.projSpell is not None:
spellIdx = self.spellLang.findData(self.theProject.projSpell)
if spellIdx != -1:
self.spellLang.setCurrentIndex(spellIdx)
self.doBackup = QSwitch(self)
self.doBackup.setChecked(not self.theProject.doBackup)
self.mainForm.addRow(
"No backup on close",
self.tr("No backup on close"),
self.doBackup,
"Overrides main preferences."
self.tr("Overrides main preferences.")
)
return
# END Class GuiProjectEditMain
class GuiProjectEditStatus(QWidget):
COL_LABEL = 0
COL_USAGE = 1
def __init__(self, theParent, theProject, isStatus):
QWidget.__init__(self, theParent)
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theProject = theProject
self.optState = theProject.optState
self.theTheme = theParent.theTheme
if isStatus:
self.theStatus = self.theProject.statusItems
pageLabel = self.tr("Novel File Status Levels")
colSetting = "statusColW"
else:
self.theStatus = self.theProject.importItems
pageLabel = self.tr("Note File Importance Levels")
colSetting = "importColW"
wCol0 = self.mainConf.pxInt(
self.optState.getInt("GuiProjectSettings", colSetting, 130)
)
self.colData = []
self.colCounts = []
@@ -256,64 +275,89 @@ class GuiProjectEditStatus(QWidget):
self.iPx = self.theTheme.baseIconSize
self.outerBox = QVBoxLayout()
self.mainBox = QHBoxLayout()
self.mainForm = QVBoxLayout()
# The List
# ========
self.listBox = QListWidget()
self.listBox.setDragDropMode(QAbstractItemView.InternalMove)
self.listBox = QTreeWidget()
self.listBox.setHeaderLabels([
self.tr("Label"),
self.tr("Usage"),
])
self.listBox.itemSelectionChanged.connect(self._selectedItem)
self.listBox.model().rowsMoved.connect(self._rowsMoved)
self.listBox.setColumnWidth(self.COL_LABEL, wCol0)
self.listBox.setIndentation(0)
for iName, iCol, nUse in self.theStatus:
self._addItem(iName, iCol, iName, nUse)
# List Controls
# =============
self.addButton = QPushButton(self.theTheme.getIcon("add"), "")
self.addButton.setToolTip(self.tr("Add new entry"))
self.addButton.clicked.connect(self._newItem)
self.delButton = QPushButton(self.theTheme.getIcon("remove"), "")
self.delButton.setToolTip(self.tr("Delete selected entry"))
self.delButton.clicked.connect(self._delItem)
# Edit Form
# =========
self.editName = QLineEdit()
self.editName.setMaxLength(40)
self.editName.setEnabled(False)
self.newButton = QPushButton("New")
self.delButton = QPushButton("Delete")
self.saveButton = QPushButton("Save")
self.colPixmap = QPixmap(self.iPx, self.iPx)
self.colPixmap.fill(QColor(120, 120, 120))
self.colButton = QPushButton(QIcon(self.colPixmap), "Colour")
self.colButton.setIconSize(self.colPixmap.rect().size())
self.editName.setPlaceholderText(self.tr("Select item to edit"))
self.newButton.clicked.connect(self._newItem)
self.delButton.clicked.connect(self._delItem)
self.saveButton.clicked.connect(self._saveItem)
self.colPixmap = QPixmap(self.iPx, self.iPx)
self.colPixmap.fill(QColor(120, 120, 120))
self.colButton = QPushButton(QIcon(self.colPixmap), self.tr("Colour"))
self.colButton.setIconSize(self.colPixmap.rect().size())
self.colButton.clicked.connect(self._selectColour)
self.mainForm.addWidget(self.newButton)
self.mainForm.addWidget(self.delButton)
self.mainForm.addStretch(1)
self.mainForm.addWidget(QLabel("<b>Name</b>"))
self.mainForm.addWidget(self.editName)
self.mainForm.addWidget(self.colButton)
self.mainForm.addStretch(1)
self.mainForm.addWidget(self.saveButton)
self.saveButton = QPushButton(self.tr("Save"))
self.saveButton.clicked.connect(self._saveItem)
# Assemble
# ========
self.listControls = QVBoxLayout()
self.listControls.addWidget(self.addButton)
self.listControls.addWidget(self.delButton)
self.listControls.addStretch(1)
self.editBox = QHBoxLayout()
self.editBox.addWidget(self.editName)
self.editBox.addWidget(self.colButton)
self.editBox.addWidget(self.saveButton)
self.mainBox = QVBoxLayout()
self.mainBox.addWidget(self.listBox)
self.mainBox.addLayout(self.mainForm)
self.mainBox.addLayout(self.editBox)
if isStatus:
self.outerBox.addWidget(QLabel("<b>Novel File Status Levels</b>"))
else:
self.outerBox.addWidget(QLabel("<b>Note File Importance Levels</b>"))
self.outerBox.addLayout(self.mainBox)
self.innerBox = QHBoxLayout()
self.innerBox.addLayout(self.mainBox)
self.innerBox.addLayout(self.listControls)
self.outerBox = QVBoxLayout()
self.outerBox.addWidget(QLabel("<b>%s</b>" % pageLabel))
self.outerBox.addLayout(self.innerBox)
self.setLayout(self.outerBox)
return
def getNewList(self):
"""Return list of entries.
"""
if self.colChanged:
newList = []
for n in range(self.listBox.count()):
nItem = self.listBox.item(n)
nIdx = nItem.data(Qt.UserRole)
for n in range(self.listBox.topLevelItemCount()):
nItem = self.listBox.topLevelItem(n)
nIdx = nItem.data(self.COL_LABEL, Qt.UserRole)
newList.append(self.colData[nIdx])
return newList
return None
##
@@ -325,7 +369,7 @@ class GuiProjectEditStatus(QWidget):
"""
if self.selColour is not None:
newCol = QColorDialog.getColor(
self.selColour, self, "Select Colour", QColorDialog.DontUseNativeDialog
self.selColour, self, self.tr("Select Colour")
)
if newCol.isValid():
self.selColour = newCol
@@ -338,8 +382,9 @@ class GuiProjectEditStatus(QWidget):
def _newItem(self):
"""Create a new status item.
"""
newItem = self._addItem("New Item", (0, 0, 0), None, 0)
newItem.setBackground(QBrush(QColor(0, 255, 0, 80)))
newItem = self._addItem(self.tr("New Item"), (0, 0, 0), None, 0)
newItem.setBackground(self.COL_LABEL, QBrush(QColor(0, 255, 0, 70)))
newItem.setBackground(self.COL_USAGE, QBrush(QColor(0, 255, 0, 70)))
self.colChanged = True
return
@@ -348,15 +393,15 @@ class GuiProjectEditStatus(QWidget):
"""
selItem = self._getSelectedItem()
if selItem is not None:
iRow = self.listBox.row(selItem)
selIdx = selItem.data(Qt.UserRole)
iRow = self.listBox.indexOfTopLevelItem(selItem)
selIdx = selItem.data(self.COL_LABEL, Qt.UserRole)
if self.colCounts[selIdx] == 0:
self.listBox.takeItem(iRow)
self.listBox.takeTopLevelItem(iRow)
self.colChanged = True
else:
self.theParent.makeAlert(
"Cannot delete status item that is in use.", nwAlert.ERROR
)
self.theParent.makeAlert(self.tr(
"Cannot delete a status item that is in use."
), nwAlert.ERROR)
return
def _saveItem(self):
@@ -364,7 +409,7 @@ class GuiProjectEditStatus(QWidget):
"""
selItem = self._getSelectedItem()
if selItem is not None:
selIdx = selItem.data(Qt.UserRole)
selIdx = selItem.data(self.COL_LABEL, Qt.UserRole)
self.colData[selIdx] = (
self.editName.text().strip(),
self.selColour.red(),
@@ -372,10 +417,12 @@ class GuiProjectEditStatus(QWidget):
self.selColour.blue(),
self.colData[selIdx][4]
)
selItem.setText("%s [%d]" % (self.colData[selIdx][0], self.colCounts[selIdx]))
selItem.setIcon(self.colButton.icon())
selItem.setText(self.COL_LABEL, self.colData[selIdx][0])
selItem.setText(self.COL_USAGE, self._usageString(self.colCounts[selIdx]))
selItem.setIcon(self.COL_LABEL, self.colButton.icon())
self.editName.setEnabled(False)
self.colChanged = True
return
def _addItem(self, iName, iCol, oName, nUse):
@@ -383,11 +430,12 @@ class GuiProjectEditStatus(QWidget):
"""
newIcon = QPixmap(self.iPx, self.iPx)
newIcon.fill(QColor(*iCol))
newItem = QListWidgetItem()
newItem.setText("%s [%d]" % (iName, nUse))
newItem.setIcon(QIcon(newIcon))
newItem.setData(Qt.UserRole, len(self.colData))
self.listBox.addItem(newItem)
newItem = QTreeWidgetItem()
newItem.setText(self.COL_LABEL, iName)
newItem.setText(self.COL_USAGE, self._usageString(nUse))
newItem.setIcon(self.COL_LABEL, QIcon(newIcon))
newItem.setData(self.COL_LABEL, Qt.UserRole, len(self.colData))
self.listBox.addTopLevelItem(newItem)
self.colData.append((iName, iCol[0], iCol[1], iCol[2], oName))
self.colCounts.append(nUse)
return newItem
@@ -398,7 +446,7 @@ class GuiProjectEditStatus(QWidget):
"""
selItem = self._getSelectedItem()
if selItem is not None:
selIdx = selItem.data(Qt.UserRole)
selIdx = selItem.data(self.COL_LABEL, Qt.UserRole)
selVal = self.colData[selIdx]
self.selColour = QColor(selVal[1], selVal[2], selVal[3])
newIcon = QPixmap(self.iPx, self.iPx)
@@ -408,6 +456,7 @@ class GuiProjectEditStatus(QWidget):
self.editName.setEnabled(True)
self.editName.selectAll()
self.editName.setFocus()
return
##
@@ -423,16 +472,29 @@ class GuiProjectEditStatus(QWidget):
return None
def _rowsMoved(self):
"""A row has been moved, so sett the changed flag.
"""A row has been moved, so set the changed flag.
"""
logger.verbose("A drag move event occurred")
self.colChanged = True
return
def _usageString(self, nUse):
"""Generate usage string.
"""
if nUse == 0:
return self.tr("Not in use")
elif nUse == 1:
return self.tr("Used once")
else:
return self.tr("Used by {0} items").format(nUse)
# END Class GuiProjectEditStatus
class GuiProjectEditReplace(QWidget):
COL_KEY = 0
COL_REPL = 1
def __init__(self, theParent, theProject):
QWidget.__init__(self, theParent)
@@ -443,52 +505,82 @@ class GuiProjectEditReplace(QWidget):
self.optState = theProject.optState
self.arChanged = False
self.outerBox = QVBoxLayout()
self.bottomBox = QHBoxLayout()
wCol0 = self.mainConf.pxInt(
self.optState.getInt("GuiProjectSettings", "replaceColW", 100)
self.optState.getInt("GuiProjectSettings", "replaceColW", 130)
)
pageLabel = self.tr("Text Replace List for Preview and Export")
# List Box
# ========
self.listBox = QTreeWidget()
self.listBox.setHeaderLabels(["Keyword", "Replace With"])
self.listBox.setHeaderLabels([
self.tr("Keyword"),
self.tr("Replace With"),
])
self.listBox.itemSelectionChanged.connect(self._selectedItem)
self.listBox.setColumnWidth(0, wCol0)
self.listBox.setColumnWidth(self.COL_KEY, wCol0)
self.listBox.setIndentation(0)
for aKey, aVal in self.theProject.autoReplace.items():
newItem = QTreeWidgetItem(["<%s>" % aKey, aVal])
self.listBox.addTopLevelItem(newItem)
self.listBox.sortByColumn(0, Qt.AscendingOrder)
self.listBox.sortByColumn(self.COL_KEY, Qt.AscendingOrder)
self.listBox.setSortingEnabled(True)
self.editKey = QLineEdit()
self.editValue = QLineEdit()
self.saveButton = QPushButton(self.theTheme.getIcon("done"), "")
self.addButton = QPushButton(self.theTheme.getIcon("add"), "")
self.delButton = QPushButton(self.theTheme.getIcon("remove"), "")
self.saveButton.setToolTip("Save entry")
self.addButton.setToolTip("Add new entry")
self.delButton.setToolTip("Delete selected entry")
# List Controls
# =============
self.addButton = QPushButton(self.theTheme.getIcon("add"), "")
self.addButton.setToolTip(self.tr("Add new entry"))
self.addButton.clicked.connect(self._addEntry)
self.delButton = QPushButton(self.theTheme.getIcon("remove"), "")
self.delButton.setToolTip(self.tr("Delete selected entry"))
self.delButton.clicked.connect(self._delEntry)
# Edit Form
# =========
self.editKey = QLineEdit()
self.editKey.setPlaceholderText(self.tr("Select item to edit"))
self.editKey.setEnabled(False)
self.editKey.setMaxLength(40)
self.editValue = QLineEdit()
self.editValue.setEnabled(False)
self.editValue.setMaxLength(80)
self.saveButton = QPushButton("Save")
self.saveButton.setToolTip(self.tr("Save entry"))
self.saveButton.clicked.connect(self._saveEntry)
self.addButton.clicked.connect(self._addEntry)
self.delButton.clicked.connect(self._delEntry)
self.bottomBox.addWidget(self.editKey, 2)
self.bottomBox.addWidget(self.editValue, 3)
self.bottomBox.addWidget(self.saveButton)
self.bottomBox.addWidget(self.addButton)
self.bottomBox.addWidget(self.delButton)
# Assemble
# ========
self.listControls = QVBoxLayout()
self.listControls.addWidget(self.addButton)
self.listControls.addWidget(self.delButton)
self.listControls.addStretch(1)
self.editBox = QHBoxLayout()
self.editBox.addWidget(self.editKey, 4)
self.editBox.addWidget(self.editValue, 5)
self.editBox.addWidget(self.saveButton, 0)
self.mainBox = QVBoxLayout()
self.mainBox.addWidget(self.listBox)
self.mainBox.addLayout(self.editBox)
self.innerBox = QHBoxLayout()
self.innerBox.addLayout(self.mainBox)
self.innerBox.addLayout(self.listControls)
self.outerBox = QVBoxLayout()
self.outerBox.addWidget(QLabel("<b>%s</b>" % pageLabel))
self.outerBox.addLayout(self.innerBox)
self.outerBox.addWidget(QLabel("<b>Text Replace List for Preview and Export</b>"))
self.outerBox.addWidget(self.listBox)
self.outerBox.addLayout(self.bottomBox)
self.setLayout(self.outerBox)
return
@@ -503,6 +595,7 @@ class GuiProjectEditReplace(QWidget):
aVal = tItem.text(1)
if len(aKey) > 0:
newList[aKey] = aVal
return newList
##
@@ -533,13 +626,13 @@ class GuiProjectEditReplace(QWidget):
if selItem is None:
return False
newKey = self.editKey.text()
newVal = self.editValue.text()
newKey = self.editKey.text()
newVal = self.editValue.text()
saveKey = self._stripNotAllowed(newKey)
if len(saveKey) > 0 and len(newVal) > 0:
selItem.setText(0, "<%s>" % saveKey)
selItem.setText(1, newVal)
selItem.setText(self.COL_KEY, "<%s>" % saveKey)
selItem.setText(self.COL_REPL, newVal)
self.editKey.clear()
self.editValue.clear()
self.editKey.setEnabled(False)

133
nw/dialogs/quotes.py Normal file
View File

@@ -0,0 +1,133 @@
"""
novelWriter – GUI Quotes Dialog
===============================
GUI class for quotes dialog
File History:
Created: 2020-06-18 [0.9]
This file is a part of novelWriter
Copyright 2018–2021, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import nw
import logging
from PyQt5.QtGui import QFontMetrics
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtWidgets import (
QLabel, QVBoxLayout, QHBoxLayout, QDialog, QDialogButtonBox,
QListWidget, QListWidgetItem, QFrame
)
from nw.constants import trConst, nwQuotes
logger = logging.getLogger(__name__)
class GuiQuoteSelect(QDialog):
selectedQuote = ""
def __init__(self, theParent=None, currentQuote='"'):
QDialog.__init__(self, parent=theParent)
self.mainConf = nw.CONFIG
self.outerBox = QVBoxLayout()
self.innerBox = QHBoxLayout()
self.labelBox = QVBoxLayout()
self.selectedQuote = currentQuote
qMetrics = QFontMetrics(self.font())
pxW = 7*qMetrics.boundingRectChar("M").width()
pxH = 7*qMetrics.boundingRectChar("M").height()
pxH = 7*qMetrics.boundingRectChar("M").height()
lblFont = self.font()
lblFont.setPointSizeF(4*lblFont.pointSizeF())
# Preview Label
self.previewLabel = QLabel(currentQuote)
self.previewLabel.setFont(lblFont)
self.previewLabel.setFixedSize(QSize(pxW, pxH))
self.previewLabel.setAlignment(Qt.AlignCenter)
self.previewLabel.setFrameStyle(QFrame.Box | QFrame.Plain)
# Quote Symbols
self.listBox = QListWidget()
self.listBox.itemSelectionChanged.connect(self._selectedSymbol)
minSize = 100
for sKey, sLabel in nwQuotes.SYMBOLS.items():
theText = "[ %s ] %s" % (sKey, trConst(sLabel))
minSize = max(minSize, qMetrics.boundingRect(theText).width())
qtItem = QListWidgetItem(theText)
qtItem.setData(Qt.UserRole, sKey)
self.listBox.addItem(qtItem)
if sKey == currentQuote:
self.listBox.setCurrentItem(qtItem)
self.listBox.setMinimumWidth(minSize + self.mainConf.pxInt(40))
self.listBox.setMinimumHeight(self.mainConf.pxInt(150))
# Buttons
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.buttonBox.accepted.connect(self._doAccept)
self.buttonBox.rejected.connect(self._doReject)
# Assemble
self.labelBox.addWidget(self.previewLabel, 0, Qt.AlignTop)
self.labelBox.addStretch(1)
self.innerBox.addLayout(self.labelBox)
self.innerBox.addWidget(self.listBox)
self.outerBox.addLayout(self.innerBox)
self.outerBox.addWidget(self.buttonBox)
self.setLayout(self.outerBox)
return
##
# Slots
##
def _selectedSymbol(self):
"""Update the preview label and the selected quote style.
"""
selItems = self.listBox.selectedItems()
if selItems:
theSymbol = selItems[0].data(Qt.UserRole)
self.previewLabel.setText(theSymbol)
self.selectedQuote = theSymbol
return
def _doAccept(self):
"""Ok button clicked.
"""
self.accept()
return
def _doReject(self):
"""Cancel button clicked.
"""
self.reject()
return
# END Class GuiQuoteSelect

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – GUI User Wordlist
===============================
@@ -25,8 +24,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import nw
import logging
import os
import logging
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
@@ -34,13 +33,15 @@ from PyQt5.QtWidgets import (
QAbstractItemView, QPushButton, QLineEdit, QLabel
)
from nw.constants import nwFiles, nwAlert
from nw.enum import nwAlert
from nw.constants import nwFiles
logger = logging.getLogger(__name__)
class GuiWordList(QDialog):
def __init__(self, theParent, theProject):
def __init__(self, theParent):
QDialog.__init__(self, theParent)
logger.debug("Initialising GuiWordList ...")
@@ -49,10 +50,10 @@ class GuiWordList(QDialog):
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theTheme = theParent.theTheme
self.theProject = theProject
self.optState = theProject.optState
self.theProject = theParent.theProject
self.optState = theParent.theProject.optState
self.setWindowTitle("Project Word List")
self.setWindowTitle(self.tr("Project Word List"))
mS = self.mainConf.pxInt(250)
wW = self.mainConf.pxInt(320)
@@ -68,7 +69,7 @@ class GuiWordList(QDialog):
# Main Widgets
# ============
self.headLabel = QLabel("<b>Project Word List</b>")
self.headLabel = QLabel("<b>%s</b>" % self.tr("Project Word List"))
self.listBox = QListWidget()
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
@@ -77,11 +78,11 @@ class GuiWordList(QDialog):
self.newEntry = QLineEdit()
self.addButton = QPushButton(self.theTheme.getIcon("add"), "")
self.addButton.setToolTip("Add new entry")
self.addButton.setToolTip(self.tr("Add new entry"))
self.addButton.clicked.connect(self._doAdd)
self.delButton = QPushButton(self.theTheme.getIcon("remove"), "")
self.delButton.setToolTip("Delete selected entry")
self.delButton.setToolTip(self.tr("Delete selected entry"))
self.delButton.clicked.connect(self._doDelete)
self.editBox = QHBoxLayout()
@@ -121,13 +122,15 @@ class GuiWordList(QDialog):
"""
newWord = self.newEntry.text().strip()
if newWord == "":
self.theParent.makeAlert("Cannot add a blank word.", nwAlert.ERROR)
self.theParent.makeAlert(self.tr(
"Cannot add a blank word."
), nwAlert.ERROR)
return False
if self.listBox.findItems(newWord, Qt.MatchExactly):
self.theParent.makeAlert(
"The word '%s' is already in the word list." % newWord, nwAlert.ERROR
)
self.theParent.makeAlert(self.tr(
"The word '{0}' is already in the word list."
).format(newWord), nwAlert.ERROR)
return False
self.listBox.addItem(newWord)
@@ -152,7 +155,7 @@ class GuiWordList(QDialog):
tmpFile = dctFile + "~"
try:
with open(tmpFile, mode="w", encoding="utf8") as outFile:
with open(tmpFile, mode="w", encoding="utf-8") as outFile:
for i in range(self.listBox.count()):
outFile.write(self.listBox.item(i).text() + "\n")
@@ -190,7 +193,7 @@ class GuiWordList(QDialog):
logger.debug("No project dictionary file found")
return False
with open(wordList, mode="r", encoding="utf8") as inFile:
with open(wordList, mode="r", encoding="utf-8") as inFile:
for inLine in inFile:
theWord = inLine.strip()
if len(theWord) == 0:

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – Enums
===================
@@ -26,6 +25,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
from enum import Enum
class nwItemType(Enum):
NO_TYPE = 0
@@ -36,6 +36,7 @@ class nwItemType(Enum):
# END Enum nwItemType
class nwItemClass(Enum):
NO_CLASS = 0
@@ -52,6 +53,7 @@ class nwItemClass(Enum):
# END Enum nwItemClass
class nwItemLayout(Enum):
NO_LAYOUT = 0
@@ -66,6 +68,7 @@ class nwItemLayout(Enum):
# END Enum nwItemLayout
class nwDocAction(Enum):
NO_ACTION = 0
@@ -81,33 +84,35 @@ class nwDocAction(Enum):
D_QUOTE = 10
SEL_ALL = 11
SEL_PARA = 12
FIND = 13
REPLACE = 14
GO_NEXT = 15
GO_PREV = 16
REPL_NEXT = 17
BLOCK_H1 = 18
BLOCK_H2 = 19
BLOCK_H3 = 20
BLOCK_H4 = 21
BLOCK_COM = 22
BLOCK_TXT = 23
REPL_SNG = 24
REPL_DBL = 25
BLOCK_H1 = 13
BLOCK_H2 = 14
BLOCK_H3 = 15
BLOCK_H4 = 16
BLOCK_COM = 17
BLOCK_TXT = 18
REPL_SNG = 19
REPL_DBL = 20
RM_BREAKS = 21
ALIGN_L = 22
ALIGN_C = 23
ALIGN_R = 24
INDENT_L = 25
INDENT_R = 26
# END Enum nwDocAction
class nwDocInsert(Enum):
NO_INSERT = 0
HARD_BREAK = 1
QUOTE_LS = 2
QUOTE_RS = 3
QUOTE_LD = 4
QUOTE_RD = 5
NO_INSERT = 0
QUOTE_LS = 1
QUOTE_RS = 2
QUOTE_LD = 3
QUOTE_RD = 4
# END Enum nwDocInsert
class nwAlert(Enum):
INFO = 0
@@ -117,6 +122,26 @@ class nwAlert(Enum):
# END Enum nwAlert
class nwState(Enum):
NONE = 0
BAD = 1
GOOD = 2
# END Enum nwState
class nwWidget(Enum):
TREE = 1
EDITOR = 2
VIEWER = 3
OUTLINE = 4
# END Enum nwWidget
class nwOutline(Enum):
TITLE = 0

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – Exception Handling
================================
@@ -35,6 +34,7 @@ from PyQt5.QtWidgets import (
logger = logging.getLogger(__name__)
# =============================================================================================== #
# Utility Functions
# =============================================================================================== #
@@ -43,7 +43,8 @@ def logException():
"""Log the content of an exception message.
"""
exType, exValue, _ = sys.exc_info()
logger.error("%s: %s" % (exType.__name__, str(exValue).strip("'")))
logger.error("%s: %s", exType.__name__, str(exValue).strip("'"))
# =============================================================================================== #
# Error Handler
@@ -103,7 +104,7 @@ class NWErrorMessage(QDialog):
"message and traceback shown below.</p>"
"<p>URL: <a href='{issueUrl}'>{issueUrl}</a></p>"
).format(
issueUrl = __issuesurl__,
issueUrl=__issuesurl__,
))
try:
@@ -123,16 +124,16 @@ class NWErrorMessage(QDialog):
"\n"
"Traceback:\n{exTrace}\n"
).format(
nwVersion = __version__,
osType = sys.platform,
osKernel = kernelVersion,
pyVersion = sys.version.split()[0],
pyHexVer = sys.hexversion,
qtVers = QT_VERSION_STR,
pyqtVers = PYQT_VERSION_STR,
exType = exType.__name__,
exMessage = str(exValue),
exTrace = "\n".join(format_tb(exTrace)),
nwVersion=__version__,
osType=sys.platform,
osKernel=kernelVersion,
pyVersion=sys.version.split()[0],
pyHexVer=sys.hexversion,
qtVers=QT_VERSION_STR,
pyqtVers=PYQT_VERSION_STR,
exType=exType.__name__,
exMessage=str(exValue),
exTrace="\n".join(format_tb(exTrace)),
))
except Exception:
self.msgBody.setPlainText("Failed to generate error report ...")
@@ -151,13 +152,14 @@ class NWErrorMessage(QDialog):
# END Class NWErrorMessage
def exceptionHandler(exType, exValue, exTrace):
"""Function to catch unhandled global exceptions.
"""
from traceback import print_tb
from PyQt5.QtWidgets import qApp
logger.critical("%s: %s" % (exType.__name__, str(exValue)))
logger.critical("%s: %s", exType.__name__, str(exValue))
print_tb(exTrace)
try:

View File

@@ -1,50 +1,47 @@
# -*- coding: utf-8 -*-
"""
novelWriter – GUI Init
======================
This file is a part of novelWriter
Copyright 2018–2021, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from nw.gui.about import GuiAbout
from nw.gui.build import GuiBuildNovel
from nw.gui.doceditor import GuiDocEditor
from nw.gui.docmerge import GuiDocMerge
from nw.gui.docsplit import GuiDocSplit
from nw.gui.docviewer import GuiDocViewer, GuiDocViewDetails
from nw.gui.itemdetails import GuiItemDetails
from nw.gui.itemeditor import GuiItemEditor
from nw.gui.mainmenu import GuiMainMenu
from nw.gui.noveltree import GuiNovelTree
from nw.gui.outline import GuiOutline
from nw.gui.outlinedetails import GuiOutlineDetails
from nw.gui.preferences import GuiPreferences
from nw.gui.projdetails import GuiProjectDetails
from nw.gui.projload import GuiProjectLoad
from nw.gui.projsettings import GuiProjectSettings
from nw.gui.projtree import GuiProjectTree
from nw.gui.projwizard import GuiProjectWizard
from nw.gui.statusbar import GuiMainStatus
from nw.gui.theme import GuiTheme
from nw.gui.wordlist import GuiWordList
from nw.gui.writingstats import GuiWritingStats
__all__ = [
"GuiAbout",
"GuiBuildNovel",
"GuiDocEditor",
"GuiDocMerge",
"GuiDocSplit",
"GuiDocViewDetails",
"GuiDocViewer",
"GuiItemDetails",
"GuiItemEditor",
"GuiMainMenu",
"GuiNovelTree",
"GuiMainStatus",
"GuiNovelTree",
"GuiOutline",
"GuiOutlineDetails",
"GuiPreferences",
"GuiProjectDetails",
"GuiProjectLoad",
"GuiProjectSettings",
"GuiProjectTree",
"GuiProjectWizard",
"GuiTheme",
"GuiWordList",
"GuiWritingStats",
]

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – Custom Widgets and Layouts
========================================
@@ -29,20 +28,21 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import nw
import logging
from PyQt5.QtGui import QColor, QPalette, QPainter, QFontMetrics
from PyQt5.QtGui import QColor, QPalette, QPainter
from PyQt5.QtCore import (
Qt, QRect, QPoint, QSize, QRectF, QPropertyAnimation, pyqtProperty
Qt, QRect, QPoint, QRectF, QPropertyAnimation, pyqtProperty
)
from PyQt5.QtWidgets import (
QGridLayout, QLabel, QWidget, QVBoxLayout, QHBoxLayout, QSizePolicy,
QAbstractButton, QDialog, QTabWidget, QTabBar, QStyle, QDialogButtonBox,
QStylePainter, QStyleOptionTab, QListWidget, QListWidgetItem, QFrame
QAbstractButton, QDialog, QTabWidget, QTabBar, QStyle, QStylePainter,
QStyleOptionTab, QLineEdit
)
from nw.constants import nwUnicode, nwQuotes
from nw.constants import nwUnicode
logger = logging.getLogger(__name__)
# =============================================================================================== #
# Config Form Layout
# =============================================================================================== #
@@ -61,6 +61,7 @@ class QConfigLayout(QGridLayout):
wSp = nw.CONFIG.pxInt(8)
self.setHorizontalSpacing(wSp)
self.setVerticalSpacing(wSp)
self.setColumnStretch(0, 1)
return
@@ -122,9 +123,9 @@ class QConfigLayout(QGridLayout):
"""Add a label and a widget as a new row of the grid.
"""
thisEntry = {
"label" : None,
"help" : None,
"widget" : None,
"label": None,
"help": None,
"widget": None,
}
if isinstance(theLabel, QLabel):
@@ -151,6 +152,7 @@ class QConfigLayout(QGridLayout):
labelBox.addWidget(qLabel)
labelBox.addWidget(qHelp)
labelBox.setSpacing(0)
labelBox.addStretch(1)
thisEntry["help"] = qHelp
self.addLayout(labelBox, self._nextRow, 0, 1, 1, Qt.AlignLeft | Qt.AlignTop)
@@ -163,15 +165,22 @@ class QConfigLayout(QGridLayout):
controlBox.addWidget(qWidget, 0, Qt.AlignVCenter)
controlBox.addWidget(QLabel(theUnit), 0, Qt.AlignVCenter)
controlBox.setSpacing(wSp)
self.addLayout(controlBox, self._nextRow, 1, 1, 1, Qt.AlignRight | Qt.AlignVCenter)
self.addLayout(controlBox, self._nextRow, 1, 1, 1, Qt.AlignRight | Qt.AlignTop)
elif theButton is not None:
controlBox = QHBoxLayout()
controlBox.addWidget(qWidget, 0, Qt.AlignVCenter)
controlBox.addWidget(theButton, 0, Qt.AlignVCenter)
controlBox.setSpacing(wSp)
self.addLayout(controlBox, self._nextRow, 1, 1, 1, Qt.AlignRight | Qt.AlignVCenter)
self.addLayout(controlBox, self._nextRow, 1, 1, 1, Qt.AlignRight | Qt.AlignTop)
else:
self.addWidget(qWidget, self._nextRow, 1, 1, 1, Qt.AlignRight | Qt.AlignVCenter)
if isinstance(theWidget, QLineEdit):
qLayout = QHBoxLayout()
qLayout.addWidget(theWidget)
self.addLayout(qLayout, self._nextRow, 1, 1, 1, Qt.AlignRight | Qt.AlignTop)
else:
self.addWidget(qWidget, self._nextRow, 1, 1, 1, Qt.AlignRight | Qt.AlignTop)
qLabel.setBuddy(qWidget)
@@ -189,6 +198,7 @@ class QConfigLayout(QGridLayout):
# END Class QConfigLayout
class QHelpLabel(QLabel):
def __init__(self, theText, textCol, fontSize=0.9):
@@ -207,10 +217,14 @@ class QHelpLabel(QLabel):
lblFont.setPointSizeF(fontSize*lblFont.pointSizeF())
self.setFont(lblFont)
self.setWordWrap(True)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
return
# END Class QHelpLabel
# =============================================================================================== #
# Switch Widget
# =============================================================================================== #
@@ -297,13 +311,13 @@ class QSwitch(QAbstractButton):
if self.isChecked():
trackBrush = qPalette.highlight()
thumbBrush = qPalette.highlightedText()
textColor = qPalette.highlight().color()
thumbText = nwUnicode.U_CHECK
textColor = qPalette.highlight().color()
thumbText = nwUnicode.U_CHECK
else:
trackBrush = qPalette.dark()
thumbBrush = qPalette.light()
textColor = qPalette.dark().color()
thumbText = nwUnicode.U_CROSS
textColor = qPalette.dark().color()
thumbText = nwUnicode.U_CROSS
if self.isEnabled():
trackOpacity = 1.0
@@ -311,7 +325,7 @@ class QSwitch(QAbstractButton):
trackOpacity = 0.6
trackBrush = qPalette.shadow()
thumbBrush = qPalette.mid()
textColor = qPalette.shadow().color()
textColor = qPalette.shadow().color()
qPaint.setBrush(trackBrush)
qPaint.setOpacity(trackOpacity)
@@ -355,6 +369,7 @@ class QSwitch(QAbstractButton):
# END Class QSwitch
# =============================================================================================== #
# Paged Dialog w/Custom TabWidget
# =============================================================================================== #
@@ -364,18 +379,18 @@ class PagedDialog(QDialog):
def __init__(self, theParent=None):
QDialog.__init__(self, parent=theParent)
self._outerBox = QVBoxLayout()
self._buttonBox = QHBoxLayout()
self._tabBox = QTabWidget()
self._tabBar = VerticalTabBar(self)
self._tabBox.setTabBar(self._tabBar)
self._tabBox.setTabPosition(QTabWidget.West)
self._tabBar.setExpanding(False)
self._tabBox = QTabWidget()
self._tabBox.setTabBar(self._tabBar)
self._tabBox.setTabPosition(QTabWidget.West)
self._buttonBox = QHBoxLayout()
self._outerBox = QVBoxLayout()
self._outerBox.addWidget(self._tabBox)
self._outerBox.addLayout(self._buttonBox)
self.setLayout(self._outerBox)
# Default Margins
qM = self._outerBox.contentsMargins()
@@ -384,11 +399,14 @@ class PagedDialog(QDialog):
mT = qM.top()
mB = qM.bottom()
# Set Margins
self.setContentsMargins(0, 0, 0, 0)
self._outerBox.setContentsMargins(0, 0, 0, mB)
self._buttonBox.setContentsMargins(mL, 0, mR, 0)
self._outerBox.setSpacing(mT)
self.setLayout(self._outerBox)
return
def addTab(self, tabWidget, tabLabel):
@@ -405,10 +423,12 @@ class PagedDialog(QDialog):
# END Class PagedDialog
class VerticalTabBar(QTabBar):
def __init__(self, theParent=None):
QTabBar.__init__(self, parent=theParent)
self._mW = nw.CONFIG.pxInt(150)
return
def tabSizeHint(self, theIndex):
@@ -416,6 +436,7 @@ class VerticalTabBar(QTabBar):
"""
tSize = QTabBar.tabSizeHint(self, theIndex)
tSize.transpose()
tSize.setWidth(min(tSize.width(), self._mW))
return tSize
def paintEvent(self, theEvent):
@@ -446,101 +467,3 @@ class VerticalTabBar(QTabBar):
return
# END Class VerticalTabBar
# =============================================================================================== #
# Quotes Dialog
# =============================================================================================== #
class QuotesDialog(QDialog):
selectedQuote = ""
def __init__(self, theParent=None, currentQuote="\""):
QDialog.__init__(self, parent=theParent)
self.mainConf = nw.CONFIG
self.outerBox = QVBoxLayout()
self.innerBox = QHBoxLayout()
self.labelBox = QVBoxLayout()
self.selectedQuote = currentQuote
qMetrics = QFontMetrics(self.font())
pxW = 7*qMetrics.boundingRectChar("M").width()
pxH = 7*qMetrics.boundingRectChar("M").height()
pxH = 7*qMetrics.boundingRectChar("M").height()
lblFont = self.font()
lblFont.setPointSizeF(4*lblFont.pointSizeF())
# Preview Label
self.previewLabel = QLabel(currentQuote)
self.previewLabel.setFont(lblFont)
self.previewLabel.setFixedSize(QSize(pxW, pxH))
self.previewLabel.setAlignment(Qt.AlignCenter)
self.previewLabel.setFrameStyle(QFrame.Box | QFrame.Plain)
# Quote Symbols
self.listBox = QListWidget()
self.listBox.itemSelectionChanged.connect(self._selectedSymbol)
minSize = 100
for sKey, sLabel in nwQuotes.SYMBOLS.items():
theText = "[ %s ] %s" % (sKey, sLabel)
minSize = max(minSize, qMetrics.boundingRect(theText).width())
qtItem = QListWidgetItem(theText)
qtItem.setData(Qt.UserRole, sKey)
self.listBox.addItem(qtItem)
if sKey == currentQuote:
self.listBox.setCurrentItem(qtItem)
self.listBox.setMinimumWidth(minSize + self.mainConf.pxInt(40))
self.listBox.setMinimumHeight(self.mainConf.pxInt(150))
# Buttons
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.buttonBox.accepted.connect(self._doAccept)
self.buttonBox.rejected.connect(self._doReject)
# Assemble
self.labelBox.addWidget(self.previewLabel, 0, Qt.AlignTop)
self.labelBox.addStretch(1)
self.innerBox.addLayout(self.labelBox)
self.innerBox.addWidget(self.listBox)
self.outerBox.addLayout(self.innerBox)
self.outerBox.addWidget(self.buttonBox)
self.setLayout(self.outerBox)
return
##
# Slots
##
def _selectedSymbol(self):
"""Update the preview label and the selected quote style.
"""
selItems = self.listBox.selectedItems()
if selItems:
theSymbol = selItems[0].data(Qt.UserRole)
self.previewLabel.setText(theSymbol)
self.selectedQuote = theSymbol
return
def _doAccept(self):
"""Ok button clicked.
"""
self.accept()
return
def _doReject(self):
"""Cancel button clicked.
"""
self.reject()
return
# END Class QuotesDialog

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – GUI Syntax Highlighter
====================================
@@ -34,10 +33,11 @@ from PyQt5.QtGui import (
QColor, QTextCharFormat, QFont, QSyntaxHighlighter, QBrush
)
from nw.constants import nwUnicode, nwRegEx
from nw.constants import nwRegEx, nwUnicode
logger = logging.getLogger(__name__)
class GuiDocHighlighter(QSyntaxHighlighter):
BLOCK_NONE = 0
@@ -71,7 +71,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.colKey = QColor(0, 0, 0)
self.colVal = QColor(0, 0, 0)
self.colSpell = QColor(0, 0, 0)
self.colTagErr = QColor(0, 0, 0)
self.colError = QColor(0, 0, 0)
self.colRepTag = QColor(0, 0, 0)
self.initHighlighter()
@@ -95,53 +95,54 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.colKey = QColor(*self.theTheme.colKey)
self.colVal = QColor(*self.theTheme.colVal)
self.colSpell = QColor(*self.theTheme.colSpell)
self.colTagErr = QColor(*self.theTheme.colTagErr)
self.colError = QColor(*self.theTheme.colError)
self.colRepTag = QColor(*self.theTheme.colRepTag)
self.colMod = QColor(*self.theTheme.colMod)
self.colTrail = QColor(*self.theTheme.colEmph)
self.colTrail.setAlpha(64)
self.colBreak = QColor(*self.theTheme.colEmph)
self.colBreak.setAlpha(64)
self.colEmph = None
if self.mainConf.highlightEmph:
self.colEmph = QColor(*self.theTheme.colEmph)
self.hStyles = {
"header1" : self._makeFormat(self.colHead, "bold", 1.8),
"header2" : self._makeFormat(self.colHead, "bold", 1.6),
"header3" : self._makeFormat(self.colHead, "bold", 1.4),
"header4" : self._makeFormat(self.colHead, "bold", 1.2),
"header1h" : self._makeFormat(self.colHeadH, "bold", 1.8),
"header2h" : self._makeFormat(self.colHeadH, "bold", 1.6),
"header3h" : self._makeFormat(self.colHeadH, "bold", 1.4),
"header4h" : self._makeFormat(self.colHeadH, "bold", 1.2),
"bold" : self._makeFormat(self.colEmph, "bold"),
"italic" : self._makeFormat(self.colEmph, "italic"),
"strike" : self._makeFormat(self.colHidden, "strike"),
"trailing" : self._makeFormat(self.colTrail, "background"),
"nobreak" : self._makeFormat(self.colTrail, "background"),
"dialogue1" : self._makeFormat(self.colDialN),
"dialogue2" : self._makeFormat(self.colDialD),
"dialogue3" : self._makeFormat(self.colDialS),
"replace" : self._makeFormat(self.colRepTag),
"hidden" : self._makeFormat(self.colHidden),
"keyword" : self._makeFormat(self.colKey),
"modifier" : self._makeFormat(self.colMod),
"value" : self._makeFormat(self.colVal, "underline"),
"header1": self._makeFormat(self.colHead, "bold", 1.8),
"header2": self._makeFormat(self.colHead, "bold", 1.6),
"header3": self._makeFormat(self.colHead, "bold", 1.4),
"header4": self._makeFormat(self.colHead, "bold", 1.2),
"header1h": self._makeFormat(self.colHeadH, "bold", 1.8),
"header2h": self._makeFormat(self.colHeadH, "bold", 1.6),
"header3h": self._makeFormat(self.colHeadH, "bold", 1.4),
"header4h": self._makeFormat(self.colHeadH, "bold", 1.2),
"bold": self._makeFormat(self.colEmph, "bold"),
"italic": self._makeFormat(self.colEmph, "italic"),
"strike": self._makeFormat(self.colHidden, "strike"),
"mspaces": self._makeFormat(self.colError, "errline"),
"nobreak": self._makeFormat(self.colBreak, "background"),
"dialogue1": self._makeFormat(self.colDialN),
"dialogue2": self._makeFormat(self.colDialD),
"dialogue3": self._makeFormat(self.colDialS),
"replace": self._makeFormat(self.colRepTag),
"hidden": self._makeFormat(self.colHidden),
"keyword": self._makeFormat(self.colKey),
"modifier": self._makeFormat(self.colMod),
"value": self._makeFormat(self.colVal, "underline"),
}
self.hRules = []
# Trailing Spaces, 2+
self.hRules.append((
r"[ ]{2,}$", {
0 : self.hStyles["trailing"],
}
))
# Multiple or Trailing Spaces
if self.mainConf.showMultiSpaces:
self.hRules.append((
r"[ ]{2,}|[ ]*$", {
0: self.hStyles["mspaces"],
}
))
# Non-Breaking Spaces
self.hRules.append((
"[%s%s]+" % (nwUnicode.U_NBSP, nwUnicode.U_THNBSP), {
0 : self.hStyles["nobreak"],
0: self.hStyles["nobreak"],
}
))
@@ -154,7 +155,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
if fmtDbl != ["\"", "\""]:
self.hRules.append((
"(\\B\")(.*?)(\"\\B)", {
0 : self.hStyles["dialogue1"],
0: self.hStyles["dialogue1"],
}
))
@@ -162,7 +163,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
dblEnd = "|$" if self.mainConf.allowOpenDQuote else ""
self.hRules.append((
f"(\\B{fmtDbl[0]})(.*?)({fmtDbl[1]}\\B{dblEnd})", {
0 : self.hStyles["dialogue2"],
0: self.hStyles["dialogue2"],
}
))
@@ -170,37 +171,44 @@ class GuiDocHighlighter(QSyntaxHighlighter):
sngEnd = "|$" if self.mainConf.allowOpenSQuote else ""
self.hRules.append((
f"(\\B{fmtSng[0]})(.*?)({fmtSng[1]}\\B{sngEnd})", {
0 : self.hStyles["dialogue3"],
0: self.hStyles["dialogue3"],
}
))
# Markdown
# Markdown Syntax
self.hRules.append((
nwRegEx.FMT_EI, {
1 : self.hStyles["hidden"],
2 : self.hStyles["italic"],
3 : self.hStyles["hidden"],
1: self.hStyles["hidden"],
2: self.hStyles["italic"],
3: self.hStyles["hidden"],
}
))
self.hRules.append((
nwRegEx.FMT_EB, {
1 : self.hStyles["hidden"],
2 : self.hStyles["bold"],
3 : self.hStyles["hidden"],
1: self.hStyles["hidden"],
2: self.hStyles["bold"],
3: self.hStyles["hidden"],
}
))
self.hRules.append((
nwRegEx.FMT_ST, {
1 : self.hStyles["hidden"],
2 : self.hStyles["strike"],
3 : self.hStyles["hidden"],
1: self.hStyles["hidden"],
2: self.hStyles["strike"],
3: self.hStyles["hidden"],
}
))
# Alignment Tags
self.hRules.append((
r"(^>{1,2}|<{1,2}$)", {
1: self.hStyles["hidden"],
}
))
# Auto-Replace Tags
self.hRules.append((
r"<(\S+?)>", {
0 : self.hStyles["replace"],
0: self.hStyles["replace"],
}
))
@@ -253,11 +261,11 @@ class GuiDocHighlighter(QSyntaxHighlighter):
"""Loop through all blocks and rehighlight those of a given
content type.
"""
qDocument = self.document()
nBlocks = qDocument.blockCount()
qDoc = self.document()
nBlocks = qDoc.blockCount()
bfTime = time()
for i in range(nBlocks):
theBlock = qDocument.findBlockByNumber(i)
theBlock = qDoc.findBlockByNumber(i)
if theBlock.userState() & theType > 0:
self.rehighlightBlock(theBlock)
afTime = time()
@@ -280,7 +288,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
if self.theHandle is None or not theText:
return
if theText.startswith("@"): # Keywords and commands
if theText.startswith("@"): # Keywords and commands
self.setCurrentBlockState(self.BLOCK_META)
tItem = self.theParent.theProject.projTree[self.theHandle]
isValid, theBits, thePos = self.theIndex.scanThis(theText)
@@ -296,7 +304,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.setFormat(xPos, xLen, self.hStyles["value"])
else:
kwFmt = self.format(xPos)
kwFmt.setUnderlineColor(self.colTagErr)
kwFmt.setUnderlineColor(self.colError)
kwFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
self.setFormat(xPos, xLen, kwFmt)
@@ -304,27 +312,27 @@ class GuiDocHighlighter(QSyntaxHighlighter):
# so we force a return here
return
elif theText.startswith("# "): # Header 1
elif theText.startswith("# "): # Header 1
self.setCurrentBlockState(self.BLOCK_TITLE)
self.setFormat(0, 1, self.hStyles["header1h"])
self.setFormat(1, len(theText), self.hStyles["header1"])
elif theText.startswith("## "): # Header 2
elif theText.startswith("## "): # Header 2
self.setCurrentBlockState(self.BLOCK_TITLE)
self.setFormat(0, 2, self.hStyles["header2h"])
self.setFormat(2, len(theText), self.hStyles["header2"])
elif theText.startswith("### "): # Header 3
elif theText.startswith("### "): # Header 3
self.setCurrentBlockState(self.BLOCK_TITLE)
self.setFormat(0, 3, self.hStyles["header3h"])
self.setFormat(3, len(theText), self.hStyles["header3"])
elif theText.startswith("#### "): # Header 4
elif theText.startswith("#### "): # Header 4
self.setCurrentBlockState(self.BLOCK_TITLE)
self.setFormat(0, 4, self.hStyles["header4h"])
self.setFormat(4, len(theText), self.hStyles["header4"])
elif theText.startswith("%"): # Comments
elif theText.startswith("%"): # Comments
self.setCurrentBlockState(self.BLOCK_TEXT)
toCheck = theText[1:].lstrip()
synTag = toCheck[:9].lower()
@@ -337,7 +345,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
else:
self.setFormat(0, tLen, self.hStyles["hidden"])
else: # Text Paragraph
else: # Text Paragraph
self.setCurrentBlockState(self.BLOCK_TEXT)
for rX, xFmt in self.rxRules:
rxItt = rX.globalMatch(theText, 0)
@@ -391,6 +399,9 @@ class GuiDocHighlighter(QSyntaxHighlighter):
theFormat.setFontItalic(True)
if "strike" in fmtStyle:
theFormat.setFontStrikeOut(True)
if "errline" in fmtStyle:
theFormat.setUnderlineColor(self.colError)
theFormat.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
if "underline" in fmtStyle:
theFormat.setFontUnderline(True)
if "background" in fmtStyle:

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – GUI Document Viewer
=================================
@@ -41,10 +40,12 @@ from PyQt5.QtWidgets import (
)
from nw.core import ToHtml
from nw.constants import nwAlert, nwItemType, nwDocAction, nwUnicode
from nw.enum import nwAlert, nwItemType, nwDocAction
from nw.constants import nwUnicode
logger = logging.getLogger(__name__)
class GuiDocViewer(QTextBrowser):
def __init__(self, theParent):
@@ -57,9 +58,11 @@ class GuiDocViewer(QTextBrowser):
self.theParent = theParent
self.theTheme = theParent.theTheme
self.theProject = theParent.theProject
self.theHandle = None
self.qDocument = self.document()
# Internal Variables
self._docHandle = None
# Settings
self.setMinimumWidth(self.mainConf.pxInt(300))
self.setAutoFillBackground(True)
self.setOpenExternalLinks(False)
@@ -89,8 +92,8 @@ class GuiDocViewer(QTextBrowser):
"""
self.clear()
self.setSearchPaths([""])
self.theHandle = None
self.docHeader.setTitleFromHandle(self.theHandle)
self._docHandle = None
self.docHeader.setTitleFromHandle(self._docHandle)
return True
def initViewer(self):
@@ -102,7 +105,7 @@ class GuiDocViewer(QTextBrowser):
theFont = QFont()
if self.mainConf.textFont is None:
# If none is defined, set the default back to config
self.mainConf.textFont = self.qDocument.defaultFont().family()
self.mainConf.textFont = self.document().defaultFont().family()
theFont.setFamily(self.mainConf.textFont)
theFont.setPointSize(self.mainConf.textSize)
self.setFont(theFont)
@@ -123,11 +126,11 @@ class GuiDocViewer(QTextBrowser):
self.docFooter.matchColours()
# Set default text margins
self.qDocument.setDocumentMargin(0)
self.document().setDocumentMargin(0)
theOpt = QTextOption()
if self.mainConf.doJustify:
theOpt.setAlignment(Qt.AlignJustify)
self.qDocument.setDefaultTextOption(theOpt)
self.document().setDefaultTextOption(theOpt)
# Scroll bars
if self.mainConf.hideVScroll:
@@ -147,7 +150,7 @@ class GuiDocViewer(QTextBrowser):
self.setTabStopWidth(self.mainConf.getTabWidth())
# If we have a document open, we should reload it in case the font changed
if self.theHandle is not None:
if self._docHandle is not None:
self.reloadText()
return True
@@ -163,11 +166,11 @@ class GuiDocViewer(QTextBrowser):
if tItem.itemType != nwItemType.FILE:
return False
logger.debug("Generating preview for item %s" % tHandle)
logger.debug("Generating preview for item '%s'", tHandle)
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
sPos = self.verticalScrollBar().value()
aDoc = ToHtml(self.theProject, self.theParent)
aDoc = ToHtml(self.theProject)
aDoc.setPreview(self.mainConf.viewComments, self.mainConf.viewSynopsis)
aDoc.setLinkHeaders(True)
@@ -181,10 +184,9 @@ class GuiDocViewer(QTextBrowser):
aDoc.doConvert()
aDoc.doPostProcessing()
except Exception:
logger.error("Failed to generate preview for document with handle '%s'" % tHandle)
logger.error("Failed to generate preview for document with handle '%s'", tHandle)
nw.logException()
self.setText("An error occurred while generating the preview.")
return False
self.setText(self.tr("An error occurred while generating the preview."))
# Refresh the tab stops
if self.mainConf.verQtValue >= 51000:
@@ -205,11 +207,11 @@ class GuiDocViewer(QTextBrowser):
theCursor = self.textCursor()
theCursor.insertText("\t")
if self.theHandle == tHandle:
if self._docHandle == tHandle:
self.verticalScrollBar().setValue(sPos)
self.theHandle = tHandle
self._docHandle = tHandle
self.theProject.setLastViewed(tHandle)
self.docHeader.setTitleFromHandle(self.theHandle)
self.docHeader.setTitleFromHandle(self._docHandle)
self.updateDocMargins()
# Make sure the main GUI knows we changed the content
@@ -225,13 +227,13 @@ class GuiDocViewer(QTextBrowser):
def reloadText(self):
"""Reload the text in the current document.
"""
self.loadText(self.theHandle, updateHistory=False)
self.loadText(self._docHandle, updateHistory=False)
return
def redrawText(self):
"""Redraw the text by marking the document content as "dirty".
"""
self.qDocument.markContentsDirty(0, self.qDocument.characterCount())
self.document().markContentsDirty(0, self.document().characterCount())
self.updateDocMargins()
return
@@ -240,20 +242,22 @@ class GuiDocViewer(QTextBrowser):
tag rather than a known handle. This function depends on the
index being up to date.
"""
logger.debug("Loading document from tag '%s'" % theTag)
logger.debug("Loading document from tag '%s'", theTag)
tHandle, _, sTitle = self.theParent.theIndex.getTagSource(theTag)
if tHandle is None:
self.theParent.makeAlert((
"Could not find the reference for tag '%s'. It either doesn't "
self.theParent.makeAlert(self.tr(
"Could not find the reference for tag '{0}'. It either doesn't "
"exist, or the index is out of date. The index can be updated "
"from the Tools menu, or by pressing F9."
) % theTag, nwAlert.ERROR)
"from the Tools menu, or by pressing {1}."
).format(
theTag, "F9"
), nwAlert.ERROR)
return False
else:
# Let the parent handle the opening as it also ensures that
# the doc view panel is visible in case this request comes
# from outside this class.
logger.verbose("Tag points to %s#%s" % (tHandle, sTitle))
logger.verbose("Tag points to '%s#%s'", tHandle, sTitle)
self.theParent.viewDocument(tHandle, "#%s" % sTitle)
return True
@@ -261,8 +265,8 @@ class GuiDocViewer(QTextBrowser):
"""Wrapper function for various document actions on the current
document.
"""
logger.verbose("Requesting action: %s" % theAction.name)
if self.theHandle is None:
logger.verbose("Requesting action: '%s'", theAction.name)
if self._docHandle is None:
logger.error("No document open")
return False
if theAction == nwDocAction.CUT:
@@ -274,7 +278,7 @@ class GuiDocViewer(QTextBrowser):
elif theAction == nwDocAction.SEL_PARA:
self._makeSelection(QTextCursor.BlockUnderCursor)
else:
logger.debug("Unknown or unsupported document action %s" % str(theAction))
logger.debug("Unknown or unsupported document action '%s'", str(theAction))
return False
return True
@@ -284,7 +288,7 @@ class GuiDocViewer(QTextBrowser):
if not isinstance(tAnchor, str):
return False
if tAnchor.startswith("#"):
logger.verbose("Moving to anchor %s" % tAnchor)
logger.verbose("Moving to anchor '%s'", tAnchor)
self.setSource(QUrl(tAnchor))
return True
@@ -307,9 +311,7 @@ class GuiDocViewer(QTextBrowser):
return
def updateDocMargins(self):
"""Automatically adjust the margins so the text is centred if
Config.textFixedW is enabled or we're in Focus Mode. Otherwise,
just ensure the margins are set correctly.
"""Automatically adjust the margins so the text is centred.
"""
vBar = self.verticalScrollBar()
sW = vBar.width() if vBar.isVisible() else 0
@@ -334,11 +336,21 @@ class GuiDocViewer(QTextBrowser):
"""Called when an item label is changed to check if the document
title bar needs updating,
"""
if tHandle == self.theHandle:
self.docHeader.setTitleFromHandle(self.theHandle)
if tHandle == self._docHandle:
self.docHeader.setTitleFromHandle(self._docHandle)
self.updateDocMargins()
return
##
# Properties
##
def docHandle(self):
"""Return the handle of the currently open document. Returns
None if no document is open.
"""
return self._docHandle
##
# Setters
##
@@ -360,10 +372,10 @@ class GuiDocViewer(QTextBrowser):
if not isinstance(theLine, int):
return False
if theLine >= 0:
theBlock = self.qDocument.findBlockByLineNumber(theLine)
theBlock = self.document().findBlockByLineNumber(theLine)
if theBlock:
self.setCursorPosition(theBlock.position())
logger.verbose("Cursor moved to line %d" % theLine)
logger.verbose("Cursor moved to line %d", theLine)
return True
def setScrollPosition(self, thePos):
@@ -395,7 +407,7 @@ class GuiDocViewer(QTextBrowser):
"""Slot for a link in the document being clicked.
"""
theLink = theURL.url()
logger.verbose("Clicked link: '%s'" % theLink)
logger.verbose("Clicked link: '%s'", theLink)
if len(theLink) > 0:
theBits = theLink.split("=")
if len(theBits) == 2:
@@ -415,7 +427,7 @@ class GuiDocViewer(QTextBrowser):
# ===================
if userSelection:
mnuCopy = QAction("Copy", mnuContext)
mnuCopy = QAction(self.tr("Copy"), mnuContext)
mnuCopy.triggered.connect(lambda: self.docAction(nwDocAction.COPY))
mnuContext.addAction(mnuCopy)
@@ -424,17 +436,17 @@ class GuiDocViewer(QTextBrowser):
# Selections
# ==========
mnuSelAll = QAction("Select All", mnuContext)
mnuSelAll = QAction(self.tr("Select All"), mnuContext)
mnuSelAll.triggered.connect(lambda: self.docAction(nwDocAction.SEL_ALL))
mnuContext.addAction(mnuSelAll)
mnuSelWord = QAction("Select Word", mnuContext)
mnuSelWord = QAction(self.tr("Select Word"), mnuContext)
mnuSelWord.triggered.connect(
lambda: self._makePosSelection(QTextCursor.WordUnderCursor, thePos)
)
mnuContext.addAction(mnuSelWord)
mnuSelPara = QAction("Select Paragraph", mnuContext)
mnuSelPara = QAction(self.tr("Select Paragraph"), mnuContext)
mnuSelPara.triggered.connect(
lambda: self._makePosSelection(QTextCursor.BlockUnderCursor, thePos)
)
@@ -528,34 +540,35 @@ class GuiDocViewer(QTextBrowser):
" color: rgb({mColR}, {mColG}, {mColB});"
"}}\n"
).format(
tColR = self.theTheme.colText[0],
tColG = self.theTheme.colText[1],
tColB = self.theTheme.colText[2],
hColR = self.theTheme.colHead[0],
hColG = self.theTheme.colHead[1],
hColB = self.theTheme.colHead[2],
aColR = self.theTheme.colVal[0],
aColG = self.theTheme.colVal[1],
aColB = self.theTheme.colVal[2],
eColR = self.theTheme.colEmph[0],
eColG = self.theTheme.colEmph[1],
eColB = self.theTheme.colEmph[2],
kColR = self.theTheme.colKey[0],
kColG = self.theTheme.colKey[1],
kColB = self.theTheme.colKey[2],
cColR = self.theTheme.colHidden[0],
cColG = self.theTheme.colHidden[1],
cColB = self.theTheme.colHidden[2],
mColR = self.theTheme.colMod[0],
mColG = self.theTheme.colMod[1],
mColB = self.theTheme.colMod[2],
tColR=self.theTheme.colText[0],
tColG=self.theTheme.colText[1],
tColB=self.theTheme.colText[2],
hColR=self.theTheme.colHead[0],
hColG=self.theTheme.colHead[1],
hColB=self.theTheme.colHead[2],
aColR=self.theTheme.colVal[0],
aColG=self.theTheme.colVal[1],
aColB=self.theTheme.colVal[2],
eColR=self.theTheme.colEmph[0],
eColG=self.theTheme.colEmph[1],
eColB=self.theTheme.colEmph[2],
kColR=self.theTheme.colKey[0],
kColG=self.theTheme.colKey[1],
kColB=self.theTheme.colKey[2],
cColR=self.theTheme.colHidden[0],
cColG=self.theTheme.colHidden[1],
cColB=self.theTheme.colHidden[2],
mColR=self.theTheme.colMod[0],
mColG=self.theTheme.colMod[1],
mColB=self.theTheme.colMod[2],
)
self.qDocument.setDefaultStyleSheet(styleSheet)
self.document().setDefaultStyleSheet(styleSheet)
return True
# END Class GuiDocViewer
class GuiDocViewHistory():
def __init__(self, docViewer):
@@ -601,7 +614,7 @@ class GuiDocViewHistory():
self._dumpHistory()
logger.verbose("Added %s to view history" % tHandle)
logger.verbose("Added '%s' to view history", tHandle)
return True
@@ -687,6 +700,7 @@ class GuiDocViewHistory():
# END Class GuiDocViewHistory
# =============================================================================================== #
# The Embedded Document Header
# Only used by DocViewer, and is at a fixed position in the QTextBrowser's viewport
@@ -704,7 +718,9 @@ class GuiDocViewHeader(QWidget):
self.theParent = docViewer.theParent
self.theProject = docViewer.theProject
self.theTheme = docViewer.theTheme
self.theHandle = None
# Internal Variables
self._docHandle = None
fPx = int(0.9*self.theTheme.fontPixelSize)
hSp = self.mainConf.pxInt(6)
@@ -740,7 +756,7 @@ class GuiDocViewHeader(QWidget):
self.backButton.setStyleSheet(buttonStyle)
self.backButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.backButton.setVisible(False)
self.backButton.setToolTip("Go backward")
self.backButton.setToolTip(self.tr("Go backward"))
self.backButton.clicked.connect(self.docViewer.navBackward)
self.forwardButton = QToolButton(self)
@@ -751,7 +767,7 @@ class GuiDocViewHeader(QWidget):
self.forwardButton.setStyleSheet(buttonStyle)
self.forwardButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.forwardButton.setVisible(False)
self.forwardButton.setToolTip("Go forward")
self.forwardButton.setToolTip(self.tr("Go forward"))
self.forwardButton.clicked.connect(self.docViewer.navForward)
self.refreshButton = QToolButton(self)
@@ -762,7 +778,7 @@ class GuiDocViewHeader(QWidget):
self.refreshButton.setStyleSheet(buttonStyle)
self.refreshButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.refreshButton.setVisible(False)
self.refreshButton.setToolTip("Reload the document")
self.refreshButton.setToolTip(self.tr("Reload the document"))
self.refreshButton.clicked.connect(self._refreshDocument)
self.closeButton = QToolButton(self)
@@ -773,7 +789,7 @@ class GuiDocViewHeader(QWidget):
self.closeButton.setStyleSheet(buttonStyle)
self.closeButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.closeButton.setVisible(False)
self.closeButton.setToolTip("Close the document")
self.closeButton.setToolTip(self.tr("Close the document"))
self.closeButton.clicked.connect(self._closeDocument)
# Assemble Layout
@@ -822,7 +838,7 @@ class GuiDocViewHeader(QWidget):
"""Sets the document title from the handle, or alternatively,
set the whole document path.
"""
self.theHandle = tHandle
self._docHandle = tHandle
if tHandle is None:
self.theTitle.setText("")
self.backButton.setVisible(False)
@@ -873,7 +889,7 @@ class GuiDocViewHeader(QWidget):
def _refreshDocument(self):
"""Reload the content of the document.
"""
if self.docViewer.theHandle == self.theParent.docEditor.theHandle:
if self.docViewer.docHandle() == self.theParent.docEditor.docHandle():
self.theParent.saveDocument()
self.docViewer.reloadText()
return
@@ -886,11 +902,12 @@ class GuiDocViewHeader(QWidget):
"""Capture a click on the title and ensure that the item is
selected in the project tree.
"""
self.theParent.treeView.setSelectedHandle(self.theHandle, doScroll=True)
self.theParent.treeView.setSelectedHandle(self._docHandle, doScroll=True)
return
# END Class GuiDocViewHeader
# =============================================================================================== #
# The Embedded Document Footer
# Only used by DocViewer, and is at a fixed position in the QTextBrowser's viewport
@@ -908,7 +925,9 @@ class GuiDocViewFooter(QWidget):
self.theParent = docViewer.theParent
self.theTheme = docViewer.theTheme
self.viewMeta = docViewer.theParent.viewMeta
self.theHandle = None
# Internal Variables
self._docHandle = None
fPx = int(0.9*self.theTheme.fontPixelSize)
bSp = self.mainConf.pxInt(2)
@@ -944,7 +963,7 @@ class GuiDocViewFooter(QWidget):
self.showHide.setIconSize(QSize(fPx, fPx))
self.showHide.setFixedSize(QSize(fPx, fPx))
self.showHide.clicked.connect(self._doShowHide)
self.showHide.setToolTip("Show/hide the references panel")
self.showHide.setToolTip(self.tr("Show/hide the references panel"))
# Sticky Button
self.stickyRefs = QToolButton(self)
@@ -955,9 +974,9 @@ class GuiDocViewFooter(QWidget):
self.stickyRefs.setIconSize(QSize(fPx, fPx))
self.stickyRefs.setFixedSize(QSize(fPx, fPx))
self.stickyRefs.toggled.connect(self._doToggleSticky)
self.stickyRefs.setToolTip(
self.stickyRefs.setToolTip(self.tr(
"Activate to freeze the content of the references panel when changing document"
)
))
# Show Comments
self.showComments = QToolButton(self)
@@ -969,7 +988,7 @@ class GuiDocViewFooter(QWidget):
self.showComments.setIconSize(QSize(fPx, fPx))
self.showComments.setFixedSize(QSize(fPx, fPx))
self.showComments.toggled.connect(self._doToggleComments)
self.showComments.setToolTip("Show comments")
self.showComments.setToolTip(self.tr("Show comments"))
# Show Synopsis
self.showSynopsis = QToolButton(self)
@@ -981,10 +1000,10 @@ class GuiDocViewFooter(QWidget):
self.showSynopsis.setIconSize(QSize(fPx, fPx))
self.showSynopsis.setFixedSize(QSize(fPx, fPx))
self.showSynopsis.toggled.connect(self._doToggleSynopsis)
self.showSynopsis.setToolTip("Show synopsis comments")
self.showSynopsis.setToolTip(self.tr("Show synopsis comments"))
# Labels
self.lblRefs = QLabel("References")
self.lblRefs = QLabel(self.tr("References"))
self.lblRefs.setBuddy(self.showHide)
self.lblRefs.setIndent(0)
self.lblRefs.setMargin(0)
@@ -993,7 +1012,7 @@ class GuiDocViewFooter(QWidget):
self.lblRefs.setFixedHeight(fPx)
self.lblRefs.setAlignment(Qt.AlignLeft | Qt.AlignTop)
self.lblSticky = QLabel("Sticky")
self.lblSticky = QLabel(self.tr("Sticky"))
self.lblSticky.setBuddy(self.stickyRefs)
self.lblSticky.setIndent(0)
self.lblSticky.setMargin(0)
@@ -1002,7 +1021,7 @@ class GuiDocViewFooter(QWidget):
self.lblSticky.setFixedHeight(fPx)
self.lblSticky.setAlignment(Qt.AlignLeft | Qt.AlignTop)
self.lblComments = QLabel("Comments")
self.lblComments = QLabel(self.tr("Comments"))
self.lblComments.setBuddy(self.showComments)
self.lblComments.setIndent(0)
self.lblComments.setMargin(0)
@@ -1011,7 +1030,7 @@ class GuiDocViewFooter(QWidget):
self.lblComments.setFixedHeight(fPx)
self.lblComments.setAlignment(Qt.AlignLeft | Qt.AlignTop)
self.lblSynopsis = QLabel("Synopsis")
self.lblSynopsis = QLabel(self.tr("Synopsis"))
self.lblSynopsis.setBuddy(self.showSynopsis)
self.lblSynopsis.setIndent(0)
self.lblSynopsis.setMargin(0)
@@ -1092,10 +1111,10 @@ class GuiDocViewFooter(QWidget):
def _doToggleSticky(self, theState):
"""Toggle the sticky flag for the reference panel.
"""
logger.verbose("Reference sticky is %s" % str(theState))
logger.verbose("Reference sticky is %s", str(theState))
self.docViewer.stickyRef = theState
if not theState and self.docViewer.theHandle is not None:
self.viewMeta.refreshReferences(self.docViewer.theHandle)
if not theState and self.docViewer.docHandle() is not None:
self.viewMeta.refreshReferences(self.docViewer.docHandle())
return
def _doToggleComments(self, theState):
@@ -1114,6 +1133,7 @@ class GuiDocViewFooter(QWidget):
# END Class GuiDocViewFooter
# =============================================================================================== #
# The Document Back-Reference Panel
# Placed in a separate QSplitter position in the main GUI window
@@ -1129,7 +1149,6 @@ class GuiDocViewDetails(QScrollArea):
self.theParent = theParent
self.theProject = theParent.theProject
self.theTheme = theParent.theTheme
self.currHandle = None
self.refList = QLabel("")
self.refList.setWordWrap(True)
@@ -1162,7 +1181,6 @@ class GuiDocViewDetails(QScrollArea):
"""Update the current list of document references from the
project index.
"""
self.currHandle = tHandle
if self.theParent.docViewer.stickyRef:
return
@@ -1187,7 +1205,7 @@ class GuiDocViewDetails(QScrollArea):
"""Capture the link-click and forward it to the document viewer
class for handling.
"""
logger.verbose("Clicked link: '%s'" % theLink)
logger.verbose("Clicked link: '%s'", theLink)
if len(theLink) == 21:
tHandle = theLink[:13]
tAnchor = theLink[13:]

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – GUI Item Details Panel
====================================
@@ -27,16 +26,16 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import nw
import logging
from PyQt5.QtCore import Qt
from PyQt5.QtCore import Qt, pyqtSlot
from PyQt5.QtGui import QFont, QPixmap
from PyQt5.QtWidgets import QWidget, QGridLayout, QLabel
from nw.constants import (
nwLabels, nwItemClass, nwItemType, nwItemLayout
)
from nw.enum import nwItemClass, nwItemType, nwItemLayout
from nw.constants import trConst, nwLabels
logger = logging.getLogger(__name__)
class GuiItemDetails(QWidget):
def __init__(self, theParent):
@@ -47,7 +46,9 @@ class GuiItemDetails(QWidget):
self.theParent = theParent
self.theProject = theParent.theProject
self.theTheme = theParent.theTheme
self.theHandle = None
# Internal Variables
self._itemHandle = None
# Sizes
hSp = self.mainConf.pxInt(6)
@@ -56,92 +57,92 @@ class GuiItemDetails(QWidget):
iPx = self.theTheme.baseIconSize
fPt = self.theTheme.fontPointSize
self.expCheck = self.theTheme.getPixmap("check", (iPx, iPx))
self.expCross = self.theTheme.getPixmap("cross", (iPx, iPx))
self._expCheck = self.theTheme.getPixmap("check", (iPx, iPx))
self._expCross = self.theTheme.getPixmap("cross", (iPx, iPx))
self.fntLabel = QFont()
self.fntLabel.setBold(True)
self.fntLabel.setPointSizeF(0.9*fPt)
fntLabel = QFont()
fntLabel.setBold(True)
fntLabel.setPointSizeF(0.9*fPt)
self.fntValue = QFont()
self.fntValue.setPointSizeF(0.9*fPt)
fntValue = QFont()
fntValue.setPointSizeF(0.9*fPt)
# Label
self.labelName = QLabel("Label")
self.labelName.setFont(self.fntLabel)
self.labelName = QLabel(self.tr("Label"))
self.labelName.setFont(fntLabel)
self.labelName.setAlignment(Qt.AlignLeft | Qt.AlignBaseline)
self.labelFlag = QLabel("")
self.labelFlag.setAlignment(Qt.AlignRight | Qt.AlignBaseline)
self.labelData = QLabel("")
self.labelData.setFont(self.fntValue)
self.labelData.setFont(fntValue)
self.labelData.setAlignment(Qt.AlignLeft | Qt.AlignBaseline)
self.labelData.setWordWrap(True)
# Status
self.statusName = QLabel("Status")
self.statusName.setFont(self.fntLabel)
self.statusName = QLabel(self.tr("Status"))
self.statusName.setFont(fntLabel)
self.statusName.setAlignment(Qt.AlignLeft)
self.statusFlag = QLabel("")
self.statusFlag.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.statusData = QLabel("")
self.statusData.setFont(self.fntValue)
self.statusData.setFont(fntValue)
self.statusData.setAlignment(Qt.AlignLeft)
# Class
self.className = QLabel("Class")
self.className.setFont(self.fntLabel)
self.className = QLabel(self.tr("Class"))
self.className.setFont(fntLabel)
self.className.setAlignment(Qt.AlignLeft)
self.classFlag = QLabel("")
self.classFlag.setFont(self.fntValue)
self.classFlag.setFont(fntValue)
self.classFlag.setAlignment(Qt.AlignRight)
self.classData = QLabel("")
self.classData.setFont(self.fntValue)
self.classData.setFont(fntValue)
self.classData.setAlignment(Qt.AlignLeft)
# Layout
self.layoutName = QLabel("Layout")
self.layoutName.setFont(self.fntLabel)
self.layoutName = QLabel(self.tr("Layout"))
self.layoutName.setFont(fntLabel)
self.layoutName.setAlignment(Qt.AlignLeft)
self.layoutFlag = QLabel("")
self.layoutFlag.setFont(self.fntValue)
self.layoutFlag.setFont(fntValue)
self.layoutFlag.setAlignment(Qt.AlignRight)
self.layoutData = QLabel("")
self.layoutData.setFont(self.fntValue)
self.layoutData.setFont(fntValue)
self.layoutData.setAlignment(Qt.AlignLeft)
# Character Count
self.cCountName = QLabel(" Characters")
self.cCountName.setFont(self.fntLabel)
self.cCountName = QLabel(" "+self.tr("Characters"))
self.cCountName.setFont(fntLabel)
self.cCountName.setAlignment(Qt.AlignRight)
self.cCountData = QLabel("")
self.cCountData.setFont(self.fntValue)
self.cCountData.setFont(fntValue)
self.cCountData.setAlignment(Qt.AlignRight)
# Word Count
self.wCountName = QLabel(" Words")
self.wCountName.setFont(self.fntLabel)
self.wCountName = QLabel(" "+self.tr("Words"))
self.wCountName.setFont(fntLabel)
self.wCountName.setAlignment(Qt.AlignRight)
self.wCountData = QLabel("")
self.wCountData.setFont(self.fntValue)
self.wCountData.setFont(fntValue)
self.wCountData.setAlignment(Qt.AlignRight)
# Paragraph Count
self.pCountName = QLabel(" Paragraphs")
self.pCountName.setFont(self.fntLabel)
self.pCountName = QLabel(" "+self.tr("Paragraphs"))
self.pCountName.setFont(fntLabel)
self.pCountName.setAlignment(Qt.AlignRight)
self.pCountData = QLabel("")
self.pCountData.setFont(self.fntValue)
self.pCountData.setFont(fntValue)
self.pCountData.setAlignment(Qt.AlignRight)
# Assemble
@@ -181,8 +182,8 @@ class GuiItemDetails(QWidget):
self.setLayout(self.mainBox)
# Make sure the columns for flags and counts don't resize too often
flagWidth = self.theTheme.getTextWidth("Mm", self.fntValue)
countWidth = self.theTheme.getTextWidth("99,999", self.fntValue)
flagWidth = self.theTheme.getTextWidth("Mm", fntValue)
countWidth = self.theTheme.getTextWidth("99,999", fntValue)
self.mainBox.setColumnMinimumWidth(1, flagWidth)
self.mainBox.setColumnMinimumWidth(4, countWidth)
@@ -197,6 +198,8 @@ class GuiItemDetails(QWidget):
def clearDetails(self):
"""Clear all the data values.
"""
self._itemHandle = None
self.labelFlag.setPixmap(QPixmap(1, 1))
self.statusFlag.setPixmap(QPixmap(1, 1))
self.classFlag.setText("")
@@ -213,17 +216,6 @@ class GuiItemDetails(QWidget):
return
def updateCounts(self, tHandle, cC, wC, pC):
"""Update the counts if the handle is the same as the one we're
already showing. Otherwise, do nothing.
"""
if tHandle == self.theHandle:
self.cCountData.setText(f"{cC:n}")
self.wCountData.setText(f"{wC:n}")
self.pCountData.setText(f"{pC:n}")
return
def updateViewBox(self, tHandle):
"""Populate the details box from a given handle.
"""
@@ -236,40 +228,40 @@ class GuiItemDetails(QWidget):
self.clearDetails()
return
self.theHandle = tHandle
self._itemHandle = tHandle
theLabel = nwItem.itemName
if len(theLabel) > 100:
theLabel = theLabel[:96].rstrip()+" ..."
itStatus = nwItem.itemStatus
if nwItem.itemClass == nwItemClass.NOVEL:
itStatus = self.theProject.statusItems.checkEntry(itStatus) # Make sure it's valid
itStatus = self.theProject.statusItems.checkEntry(itStatus) # Make sure it's valid
flagIcon = self.theParent.statusIcons[itStatus]
else:
itStatus = self.theProject.importItems.checkEntry(itStatus) # Make sure it's valid
itStatus = self.theProject.importItems.checkEntry(itStatus) # Make sure it's valid
flagIcon = self.theParent.importIcons[itStatus]
if nwItem.itemType == nwItemType.FILE:
if nwItem.isExported:
self.labelFlag.setPixmap(self.expCheck)
self.labelFlag.setPixmap(self._expCheck)
else:
self.labelFlag.setPixmap(self.expCross)
self.labelFlag.setPixmap(self._expCross)
else:
self.labelFlag.setPixmap(QPixmap(1, 1))
iPx = int(round(0.8*self.theTheme.baseIconSize))
self.statusFlag.setPixmap(flagIcon.pixmap(iPx, iPx))
self.classFlag.setText(nwLabels.CLASS_FLAG[nwItem.itemClass])
self.classFlag.setText(nwLabels.CLASS_FLAG[nwItem.itemClass]) # NO-I18N
if nwItem.itemLayout == nwItemLayout.NO_LAYOUT:
self.layoutFlag.setText("-")
else:
self.layoutFlag.setText(nwLabels.LAYOUT_FLAG[nwItem.itemLayout])
self.layoutFlag.setText(nwLabels.LAYOUT_FLAG[nwItem.itemLayout]) # NO-I18N
self.labelData.setText(theLabel)
self.statusData.setText(nwItem.itemStatus)
self.classData.setText(nwLabels.CLASS_NAME[nwItem.itemClass])
self.layoutData.setText(nwLabels.LAYOUT_NAME[nwItem.itemLayout])
self.classData.setText(trConst(nwLabels.CLASS_NAME[nwItem.itemClass]))
self.layoutData.setText(trConst(nwLabels.LAYOUT_NAME[nwItem.itemLayout]))
if nwItem.itemType == nwItemType.FILE:
self.cCountData.setText(f"{nwItem.charCount:n}")
@@ -282,4 +274,20 @@ class GuiItemDetails(QWidget):
return
##
# Slots
##
@pyqtSlot(str, int, int, int)
def doUpdateCounts(self, tHandle, cC, wC, pC):
"""Update the counts if the handle is the same as the one we're
already showing. Otherwise, do nothing.
"""
if tHandle == self._itemHandle:
self.cCountData.setText(f"{cC:n}")
self.wCountData.setText(f"{wC:n}")
self.pCountData.setText(f"{pC:n}")
return
# END Class GuiItemDetails

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – GUI Novel Tree
============================
@@ -32,11 +31,12 @@ from time import time
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QAbstractItemView
from nw.constants import nwKeyWords
from nw.common import checkInt
from nw.constants import nwKeyWords
logger = logging.getLogger(__name__)
class GuiNovelTree(QTreeWidget):
C_TITLE = 0
@@ -63,7 +63,11 @@ class GuiNovelTree(QTreeWidget):
self.setIconSize(QSize(iPx, iPx))
self.setIndentation(iPx)
self.setColumnCount(3)
self.setHeaderLabels(["Title", "Words", "POV"])
self.setHeaderLabels([
self.tr("Title"),
self.tr("Words"),
self.tr("POV")
])
self.itemDoubleClicked.connect(self._treeDoubleClick)
self.itemSelectionChanged.connect(self._itemSelected)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
@@ -73,9 +77,9 @@ class GuiNovelTree(QTreeWidget):
treeHeadItem = self.headerItem()
treeHeadItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
treeHeadItem.setToolTip(self.C_TITLE, "Section title")
treeHeadItem.setToolTip(self.C_WORDS, "Word count")
treeHeadItem.setToolTip(self.C_POV, "Point-of-view character")
treeHeadItem.setToolTip(self.C_TITLE, self.tr("Section title"))
treeHeadItem.setToolTip(self.C_WORDS, self.tr("Word count"))
treeHeadItem.setToolTip(self.C_POV, self.tr("Point-of-view character"))
treeHeader = self.header()
treeHeader.setStretchLastSection(True)
@@ -95,9 +99,6 @@ class GuiNovelTree(QTreeWidget):
logger.debug("GuiNovelTree initialisation complete")
# Internal Mapping
self.makeAlert = self.theParent.makeAlert
return
def initTree(self):
@@ -221,7 +222,7 @@ class GuiNovelTree(QTreeWidget):
tHandle = theData[0]
tLine = checkInt(theData[1], 1)
logger.verbose("User selected entry with handle %s on line %s" % (tHandle, tLine))
logger.verbose("User selected entry with handle '%s' on line %s", tHandle, tLine)
self.theParent.openDocument(tHandle, tLine=tLine-1, doScroll=True)
return
@@ -246,9 +247,9 @@ class GuiNovelTree(QTreeWidget):
"""
self.clearTree()
currTitle = None
currTitle = None
currChapter = None
currScene = None
currScene = None
for tKey, tHandle, sTitle, novIdx in self.theIndex.novelStructure(skipExcluded=True):
@@ -258,9 +259,9 @@ class GuiNovelTree(QTreeWidget):
tLevel = novIdx["level"]
if tLevel == "H1":
self.addTopLevelItem(tItem)
currTitle = tItem
currTitle = tItem
currChapter = None
currScene = None
currScene = None
elif tLevel == "H2":
if currTitle is None:
@@ -268,7 +269,7 @@ class GuiNovelTree(QTreeWidget):
else:
currTitle.addChild(tItem)
currChapter = tItem
currScene = None
currScene = None
elif tLevel == "H3":
if currChapter is None:

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – GUI Project Outline
=================================
@@ -29,55 +28,57 @@ import logging
from time import time
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtCore import Qt, QSize, pyqtSlot
from PyQt5.QtWidgets import (
QTreeWidget, QTreeWidgetItem, QMenu, QAction, QAbstractItemView
)
from nw.constants import nwKeyWords, nwLabels, nwOutline
from nw.enum import nwOutline
from nw.constants import trConst, nwKeyWords, nwLabels
logger = logging.getLogger(__name__)
class GuiOutline(QTreeWidget):
DEF_WIDTH = {
nwOutline.TITLE : 200,
nwOutline.LEVEL : 40,
nwOutline.LABEL : 150,
nwOutline.LINE : 40,
nwOutline.CCOUNT : 50,
nwOutline.WCOUNT : 50,
nwOutline.PCOUNT : 50,
nwOutline.POV : 100,
nwOutline.FOCUS : 100,
nwOutline.CHAR : 100,
nwOutline.PLOT : 100,
nwOutline.TIME : 100,
nwOutline.WORLD : 100,
nwOutline.OBJECT : 100,
nwOutline.ENTITY : 100,
nwOutline.CUSTOM : 100,
nwOutline.SYNOP : 200,
nwOutline.TITLE: 200,
nwOutline.LEVEL: 40,
nwOutline.LABEL: 150,
nwOutline.LINE: 40,
nwOutline.CCOUNT: 50,
nwOutline.WCOUNT: 50,
nwOutline.PCOUNT: 50,
nwOutline.POV: 100,
nwOutline.FOCUS: 100,
nwOutline.CHAR: 100,
nwOutline.PLOT: 100,
nwOutline.TIME: 100,
nwOutline.WORLD: 100,
nwOutline.OBJECT: 100,
nwOutline.ENTITY: 100,
nwOutline.CUSTOM: 100,
nwOutline.SYNOP: 200,
}
DEF_HIDDEN = {
nwOutline.TITLE : False,
nwOutline.LEVEL : True,
nwOutline.LABEL : False,
nwOutline.LINE : True,
nwOutline.CCOUNT : True,
nwOutline.WCOUNT : False,
nwOutline.PCOUNT : False,
nwOutline.POV : False,
nwOutline.FOCUS : True,
nwOutline.CHAR : False,
nwOutline.PLOT : False,
nwOutline.TIME : True,
nwOutline.WORLD : False,
nwOutline.OBJECT : True,
nwOutline.ENTITY : True,
nwOutline.CUSTOM : True,
nwOutline.SYNOP : False,
nwOutline.TITLE: False,
nwOutline.LEVEL: True,
nwOutline.LABEL: False,
nwOutline.LINE: True,
nwOutline.CCOUNT: True,
nwOutline.WCOUNT: False,
nwOutline.PCOUNT: False,
nwOutline.POV: False,
nwOutline.FOCUS: True,
nwOutline.CHAR: False,
nwOutline.PLOT: False,
nwOutline.TIME: True,
nwOutline.WORLD: False,
nwOutline.OBJECT: True,
nwOutline.ENTITY: True,
nwOutline.CUSTOM: True,
nwOutline.SYNOP: False,
}
def __init__(self, theParent):
@@ -93,9 +94,6 @@ class GuiOutline(QTreeWidget):
self.optState = theParent.theProject.optState
self.headerMenu = GuiOutlineHeaderMenu(self)
self.firstView = True
self.lastBuild = 0
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSelectionMode(QAbstractItemView.SingleSelection)
self.setExpandsOnDoubleClick(False)
@@ -112,16 +110,18 @@ class GuiOutline(QTreeWidget):
self.treeHead.customContextMenuRequested.connect(self._headerRightClick)
self.treeHead.sectionMoved.connect(self._columnMoved)
self.treeMap = {}
self.treeOrder = []
self.colWidth = {}
self.colHidden = {}
self.colIndex = {}
self.treeNCols = 0
# Internals
self._treeOrder = []
self._colWidth = {}
self._colHidden = {}
self._colIdx = {}
self._treeNCols = 0
self._firstView = True
self._lastBuild = 0
self.initOutline()
self.clearOutline()
self.headerMenu.setHiddenState(self.colHidden)
self.headerMenu.setHiddenState(self._colHidden)
logger.debug("GuiOutline initialisation complete")
@@ -149,20 +149,20 @@ class GuiOutline(QTreeWidget):
"""
self.clear()
self.setColumnCount(1)
self.setHeaderLabel(nwLabels.OUTLINE_COLS[nwOutline.TITLE])
self.setHeaderLabel(trConst(nwLabels.OUTLINE_COLS[nwOutline.TITLE]))
self.treeOrder = []
self.colWidth = {}
self.colHidden = {}
self.colIndex = {}
self.treeNCols = 0
self._treeOrder = []
self._colWidth = {}
self._colHidden = {}
self._colIdx = {}
self._treeNCols = 0
for i, hItem in enumerate(nwOutline):
self.treeOrder.append(hItem)
self.colWidth[hItem] = self.DEF_WIDTH[hItem]
self.colHidden[hItem] = self.DEF_HIDDEN[hItem]
for hItem in nwOutline:
self._treeOrder.append(hItem)
self._colWidth[hItem] = self.DEF_WIDTH[hItem]
self._colHidden[hItem] = self.DEF_HIDDEN[hItem]
self.treeNCols = len(self.treeOrder)
self._treeNCols = len(self._treeOrder)
return
@@ -172,15 +172,15 @@ class GuiOutline(QTreeWidget):
tree.
"""
# If it's the first time, we always build
if self.firstView or self.firstView and overRide:
if self._firstView or self._firstView and overRide:
self._loadHeaderState()
self._populateTree()
self.firstView = False
self._firstView = False
return
# If the novel index or novel tree has changed since the tree
# was last built, we rebuild the tree from the updated index.
indexChanged = self.theIndex.novelChangedSince(self.lastBuild)
indexChanged = self.theIndex.novelChangedSince(self._lastBuild)
doBuild = (novelChanged or indexChanged) and self.theProject.autoOutline
if doBuild or overRide:
logger.debug("Rebuilding Project Outline")
@@ -193,53 +193,57 @@ class GuiOutline(QTreeWidget):
"""
self._saveHeaderState()
self.clearOutline()
self.firstView = True
self._firstView = True
return
##
# Slots
##
@pyqtSlot("QTreeWidgetItem*", int)
def _treeDoubleClick(self, tItem, tCol):
"""Extract the handle and line number of the title double-
clicked, and send it to the main gui class for opening in the
document editor.
"""
tHandle = tItem.data(self.colIndex[nwOutline.TITLE], Qt.UserRole)
tHandle = tItem.data(self._colIdx[nwOutline.TITLE], Qt.UserRole)
try:
tLine = int(tItem.text(self.colIndex[nwOutline.LINE]))
tLine = int(tItem.text(self._colIdx[nwOutline.LINE]))
except Exception:
tLine = 1
logger.verbose("User selected entry with handle %s on line %s" % (tHandle, tLine))
logger.verbose("User selected entry with handle '%s' on line %s", tHandle, tLine)
self.theParent.openDocument(tHandle, tLine=tLine-1, doScroll=True)
return
@pyqtSlot()
def _itemSelected(self):
"""Extract the handle and line number of the currently selected
title, and send it to the details panel.
"""
selItems = self.selectedItems()
if selItems:
tHandle = selItems[0].data(self.colIndex[nwOutline.TITLE], Qt.UserRole)
sTitle = selItems[0].data(self.colIndex[nwOutline.LINE], Qt.UserRole)
tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole)
sTitle = selItems[0].data(self._colIdx[nwOutline.LINE], Qt.UserRole)
self.theParent.projMeta.showItem(tHandle, sTitle)
self.theParent.treeView.setSelectedHandle(tHandle)
return
@pyqtSlot("QPoint")
def _headerRightClick(self, clickPos):
"""Show the header column menu.
"""
self.headerMenu.exec_(self.mapToGlobal(clickPos))
return
@pyqtSlot(int, int, int)
def _columnMoved(self, logIdx, oldVisualIdx, newVisualIdx):
"""Make sure the order array is up to date with the actual order
of the columns.
"""
self.treeOrder.insert(newVisualIdx, self.treeOrder.pop(oldVisualIdx))
self._treeOrder.insert(newVisualIdx, self._treeOrder.pop(oldVisualIdx))
self._saveHeaderState()
return
@@ -247,9 +251,9 @@ class GuiOutline(QTreeWidget):
"""Receive the changes to column visibility forwarded by the
header context menu.
"""
logger.verbose("User toggled Outline column '%s'" % theItem.name)
if theItem in self.colIndex:
self.setColumnHidden(self.colIndex[theItem], not isChecked)
logger.verbose("User toggled Outline column '%s'", theItem.name)
if theItem in self._colIdx:
self.setColumnHidden(self._colIdx[theItem], not isChecked)
self._saveHeaderState()
return
@@ -271,7 +275,7 @@ class GuiOutline(QTreeWidget):
try:
treeOrder.append(nwOutline[hName])
except Exception:
logger.warning("Ignored unknown outline column '%s'" % str(hName))
logger.warning("Ignored unknown outline column '%s'", str(hName))
# Add columns that was not in the file to the treeOrder array.
for hItem in nwOutline:
@@ -280,29 +284,29 @@ class GuiOutline(QTreeWidget):
# Check that we now have a complete list, and only if so, save
# the order loaded from file. Otherwise, we keep the default.
if len(treeOrder) == self.treeNCols:
self.treeOrder = treeOrder
if len(treeOrder) == self._treeNCols:
self._treeOrder = treeOrder
else:
logger.error("Failed to extract outline column order from previous session")
logger.error("Column count doesn't match %d != %d" % (len(treeOrder), self.treeNCols))
logger.error("Column count doesn't match %d != %d", len(treeOrder), self._treeNCols)
# We load whatever column widths and hidden states we find in
# the file, and leave the rest in their default state.
tmpWidth = self.optState.getValue("GuiOutline", "columnWidth", {})
for hName in tmpWidth:
try:
self.colWidth[nwOutline[hName]] = self.mainConf.pxInt(tmpWidth[hName])
self._colWidth[nwOutline[hName]] = self.mainConf.pxInt(tmpWidth[hName])
except Exception:
logger.warning("Ignored unknown outline column '%s'" % str(hName))
logger.warning("Ignored unknown outline column '%s'", str(hName))
tmpHidden = self.optState.getValue("GuiOutline", "columnHidden", {})
for hName in tmpHidden:
try:
self.colHidden[nwOutline[hName]] = tmpHidden[hName]
self._colHidden[nwOutline[hName]] = tmpHidden[hName]
except Exception:
logger.warning("Ignored unknown outline column '%s'" % str(hName))
logger.warning("Ignored unknown outline column '%s'", str(hName))
self.headerMenu.setHiddenState(self.colHidden)
self.headerMenu.setHiddenState(self._colHidden)
return
@@ -313,19 +317,19 @@ class GuiOutline(QTreeWidget):
the last known width in case they're unhidden again.
"""
# If we haven't built the tree, there is nothing to save.
if self.lastBuild == 0:
if self._lastBuild == 0:
return
treeOrder = []
colWidth = {}
colWidth = {}
colHidden = {}
for hItem in nwOutline:
colWidth[hItem.name] = self.mainConf.rpxInt(self.colWidth[hItem])
colHidden[hItem.name] = self.colHidden[hItem]
colWidth[hItem.name] = self.mainConf.rpxInt(self._colWidth[hItem])
colHidden[hItem.name] = self._colHidden[hItem]
for iCol in range(self.columnCount()):
hName = self.treeOrder[iCol].name
hName = self._treeOrder[iCol].name
treeOrder.append(hName)
iLog = self.treeHead.logicalIndex(iCol)
@@ -352,41 +356,40 @@ class GuiOutline(QTreeWidget):
"""
self.clear()
if self.firstView:
if self._firstView:
theLabels = []
for i, hItem in enumerate(self.treeOrder):
theLabels.append(nwLabels.OUTLINE_COLS[hItem])
self.colIndex[hItem] = i
for i, hItem in enumerate(self._treeOrder):
theLabels.append(trConst(nwLabels.OUTLINE_COLS[hItem]))
self._colIdx[hItem] = i
self.setHeaderLabels(theLabels)
for hItem in self.treeOrder:
self.setColumnWidth(self.colIndex[hItem], self.colWidth[hItem])
self.setColumnHidden(self.colIndex[hItem], self.colHidden[hItem])
for hItem in self._treeOrder:
self.setColumnWidth(self._colIdx[hItem], self._colWidth[hItem])
self.setColumnHidden(self._colIdx[hItem], self._colHidden[hItem])
# Make sure title column is always visible,
# and handle column always hidden
self.setColumnHidden(self.colIndex[nwOutline.TITLE], False)
self.setColumnHidden(self._colIdx[nwOutline.TITLE], False)
headItem = self.headerItem()
headItem.setTextAlignment(self.colIndex[nwOutline.CCOUNT], Qt.AlignRight)
headItem.setTextAlignment(self.colIndex[nwOutline.WCOUNT], Qt.AlignRight)
headItem.setTextAlignment(self.colIndex[nwOutline.PCOUNT], Qt.AlignRight)
headItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignRight)
headItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight)
headItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight)
currTitle = None
currTitle = None
currChapter = None
currScene = None
currScene = None
for tKey, tHandle, sTitle, novIdx in self.theIndex.novelStructure(skipExcluded=True):
tItem = self._createTreeItem(tHandle, sTitle, novIdx)
self.treeMap[tKey] = tItem
tLevel = novIdx["level"]
if tLevel == "H1":
self.addTopLevelItem(tItem)
currTitle = tItem
currTitle = tItem
currChapter = None
currScene = None
currScene = None
elif tLevel == "H2":
if currTitle is None:
@@ -394,7 +397,7 @@ class GuiOutline(QTreeWidget):
else:
currTitle.addChild(tItem)
currChapter = tItem
currScene = None
currScene = None
elif tLevel == "H3":
if currChapter is None:
@@ -420,52 +423,53 @@ class GuiOutline(QTreeWidget):
tItem.setExpanded(True)
self.lastBuild = time()
self._lastBuild = time()
return
def _createTreeItem(self, tHandle, sTitle, novIdx):
"""Populate a tree item with all the column values.
"""
nwItem = self.theProject.projTree[tHandle]
nwItem = self.theProject.projTree[tHandle]
newItem = QTreeWidgetItem()
hIcon = "doc_%s" % novIdx["level"].lower()
hIcon = "doc_%s" % novIdx["level"].lower()
cC = int(novIdx["cCount"])
wC = int(novIdx["wCount"])
pC = int(novIdx["pCount"])
newItem.setText(self.colIndex[nwOutline.TITLE], novIdx["title"])
newItem.setData(self.colIndex[nwOutline.TITLE], Qt.UserRole, tHandle)
newItem.setIcon(self.colIndex[nwOutline.TITLE], self.theTheme.getIcon(hIcon))
newItem.setText(self.colIndex[nwOutline.LEVEL], novIdx["level"])
newItem.setText(self.colIndex[nwOutline.LABEL], nwItem.itemName)
newItem.setIcon(self.colIndex[nwOutline.LABEL], self.theTheme.getIcon("proj_document"))
newItem.setText(self.colIndex[nwOutline.LINE], sTitle[1:].lstrip("0"))
newItem.setData(self.colIndex[nwOutline.LINE], Qt.UserRole, sTitle)
newItem.setText(self.colIndex[nwOutline.SYNOP], novIdx["synopsis"])
newItem.setText(self.colIndex[nwOutline.CCOUNT], f"{cC:n}")
newItem.setText(self.colIndex[nwOutline.WCOUNT], f"{wC:n}")
newItem.setText(self.colIndex[nwOutline.PCOUNT], f"{pC:n}")
newItem.setTextAlignment(self.colIndex[nwOutline.CCOUNT], Qt.AlignRight)
newItem.setTextAlignment(self.colIndex[nwOutline.WCOUNT], Qt.AlignRight)
newItem.setTextAlignment(self.colIndex[nwOutline.PCOUNT], Qt.AlignRight)
newItem.setText(self._colIdx[nwOutline.TITLE], novIdx["title"])
newItem.setData(self._colIdx[nwOutline.TITLE], Qt.UserRole, tHandle)
newItem.setIcon(self._colIdx[nwOutline.TITLE], self.theTheme.getIcon(hIcon))
newItem.setText(self._colIdx[nwOutline.LEVEL], novIdx["level"])
newItem.setText(self._colIdx[nwOutline.LABEL], nwItem.itemName)
newItem.setIcon(self._colIdx[nwOutline.LABEL], self.theTheme.getIcon("proj_document"))
newItem.setText(self._colIdx[nwOutline.LINE], sTitle[1:].lstrip("0"))
newItem.setData(self._colIdx[nwOutline.LINE], Qt.UserRole, sTitle)
newItem.setText(self._colIdx[nwOutline.SYNOP], novIdx["synopsis"])
newItem.setText(self._colIdx[nwOutline.CCOUNT], f"{cC:n}")
newItem.setText(self._colIdx[nwOutline.WCOUNT], f"{wC:n}")
newItem.setText(self._colIdx[nwOutline.PCOUNT], f"{pC:n}")
newItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignRight)
newItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight)
newItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight)
theRefs = self.theIndex.getReferences(tHandle, sTitle)
newItem.setText(self.colIndex[nwOutline.POV], ", ".join(theRefs[nwKeyWords.POV_KEY]))
newItem.setText(self.colIndex[nwOutline.FOCUS], ", ".join(theRefs[nwKeyWords.FOCUS_KEY]))
newItem.setText(self.colIndex[nwOutline.CHAR], ", ".join(theRefs[nwKeyWords.CHAR_KEY]))
newItem.setText(self.colIndex[nwOutline.PLOT], ", ".join(theRefs[nwKeyWords.PLOT_KEY]))
newItem.setText(self.colIndex[nwOutline.TIME], ", ".join(theRefs[nwKeyWords.TIME_KEY]))
newItem.setText(self.colIndex[nwOutline.WORLD], ", ".join(theRefs[nwKeyWords.WORLD_KEY]))
newItem.setText(self.colIndex[nwOutline.OBJECT], ", ".join(theRefs[nwKeyWords.OBJECT_KEY]))
newItem.setText(self.colIndex[nwOutline.ENTITY], ", ".join(theRefs[nwKeyWords.ENTITY_KEY]))
newItem.setText(self.colIndex[nwOutline.CUSTOM], ", ".join(theRefs[nwKeyWords.CUSTOM_KEY]))
newItem.setText(self._colIdx[nwOutline.POV], ", ".join(theRefs[nwKeyWords.POV_KEY]))
newItem.setText(self._colIdx[nwOutline.FOCUS], ", ".join(theRefs[nwKeyWords.FOCUS_KEY]))
newItem.setText(self._colIdx[nwOutline.CHAR], ", ".join(theRefs[nwKeyWords.CHAR_KEY]))
newItem.setText(self._colIdx[nwOutline.PLOT], ", ".join(theRefs[nwKeyWords.PLOT_KEY]))
newItem.setText(self._colIdx[nwOutline.TIME], ", ".join(theRefs[nwKeyWords.TIME_KEY]))
newItem.setText(self._colIdx[nwOutline.WORLD], ", ".join(theRefs[nwKeyWords.WORLD_KEY]))
newItem.setText(self._colIdx[nwOutline.OBJECT], ", ".join(theRefs[nwKeyWords.OBJECT_KEY]))
newItem.setText(self._colIdx[nwOutline.ENTITY], ", ".join(theRefs[nwKeyWords.ENTITY_KEY]))
newItem.setText(self._colIdx[nwOutline.CUSTOM], ", ".join(theRefs[nwKeyWords.CUSTOM_KEY]))
return newItem
# END Class GuiOutline
class GuiOutlineHeaderMenu(QMenu):
def __init__(self, theParent):
@@ -474,7 +478,7 @@ class GuiOutlineHeaderMenu(QMenu):
self.theParent = theParent
self.acceptToggle = True
mnuHead = QAction("Select Columns", self)
mnuHead = QAction(self.tr("Select Columns"), self)
self.addAction(mnuHead)
self.addSeparator()
@@ -482,10 +486,10 @@ class GuiOutlineHeaderMenu(QMenu):
for hItem in nwOutline:
if hItem == nwOutline.TITLE:
continue
self.actionMap[hItem] = QAction(nwLabels.OUTLINE_COLS[hItem], self)
self.actionMap[hItem] = QAction(trConst(nwLabels.OUTLINE_COLS[hItem]), self)
self.actionMap[hItem].setCheckable(True)
self.actionMap[hItem].toggled.connect(
lambda isChecked, tItem=hItem : self._columnToggled(isChecked, tItem)
lambda isChecked, tItem=hItem: self._columnToggled(isChecked, tItem)
)
self.addAction(self.actionMap[hItem])

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – GUI Project Outline Details
=========================================
@@ -27,23 +26,24 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import nw
import logging
from PyQt5.QtCore import Qt
from PyQt5.QtCore import Qt, QT_TRANSLATE_NOOP
from PyQt5.QtWidgets import (
QScrollArea, QWidget, QGridLayout, QHBoxLayout, QGroupBox, QLabel
)
from nw.constants import nwLabels, nwKeyWords
from nw.common import checkInt
from nw.constants import trConst, nwKeyWords, nwLabels
logger = logging.getLogger(__name__)
class GuiOutlineDetails(QScrollArea):
LVL_MAP = {
"H1" : "Title",
"H2" : "Chapter",
"H3" : "Scene",
"H4" : "Section"
"H1": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Title"),
"H2": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Chapter"),
"H3": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Scene"),
"H4": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Section"),
}
def __init__(self, theParent):
@@ -66,9 +66,9 @@ class GuiOutlineDetails(QScrollArea):
vSpace = int(self.mainConf.pxInt(4))
# Details Area
self.titleLabel = QLabel("<b>Title</b>")
self.fileLabel = QLabel("<b>Document</b>")
self.itemLabel = QLabel("<b>Status</b>")
self.titleLabel = QLabel("<b>%s</b>" % self.tr("Title"))
self.fileLabel = QLabel("<b>%s</b>" % self.tr("Document"))
self.itemLabel = QLabel("<b>%s</b>" % self.tr("Status"))
self.titleValue = QLabel("")
self.fileValue = QLabel("")
self.itemValue = QLabel("")
@@ -81,9 +81,9 @@ class GuiOutlineDetails(QScrollArea):
self.itemValue.setMaximumWidth(maxTitle)
# Stats Area
self.cCLabel = QLabel("<b>Characters</b>")
self.wCLabel = QLabel("<b>Words</b>")
self.pCLabel = QLabel("<b>Paragraphs</b>")
self.cCLabel = QLabel("<b>%s</b>" % self.tr("Characters"))
self.wCLabel = QLabel("<b>%s</b>" % self.tr("Words"))
self.pCLabel = QLabel("<b>%s</b>" % self.tr("Paragraphs"))
self.cCValue = QLabel("")
self.wCValue = QLabel("")
self.pCValue = QLabel("")
@@ -96,7 +96,7 @@ class GuiOutlineDetails(QScrollArea):
self.pCValue.setAlignment(Qt.AlignRight)
# Synopsis
self.synopLabel = QLabel("<b>Synopsis</b>")
self.synopLabel = QLabel("<b>%s</b>" % self.tr("Synopsis"))
self.synopValue = QLabel("")
self.synopLWrap = QHBoxLayout()
self.synopValue.setWordWrap(True)
@@ -104,15 +104,15 @@ class GuiOutlineDetails(QScrollArea):
self.synopLWrap.addWidget(self.synopValue, 1)
# Tags
self.povKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.POV_KEY])
self.focKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY])
self.chrKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.CHAR_KEY])
self.pltKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY])
self.timKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.TIME_KEY])
self.wldKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.WORLD_KEY])
self.objKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.OBJECT_KEY])
self.entKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.ENTITY_KEY])
self.cstKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.CUSTOM_KEY])
self.povKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY]))
self.focKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY]))
self.chrKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.CHAR_KEY]))
self.pltKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY]))
self.timKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.TIME_KEY]))
self.wldKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.WORLD_KEY]))
self.objKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.OBJECT_KEY]))
self.entKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.ENTITY_KEY]))
self.cstKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.CUSTOM_KEY]))
self.povKeyLWrap = QHBoxLayout()
self.focKeyLWrap = QHBoxLayout()
@@ -165,7 +165,7 @@ class GuiOutlineDetails(QScrollArea):
self.cstKeyLWrap.addWidget(self.cstKeyValue, 1)
# Selected Item Details
self.mainGroup = QGroupBox("Title Details", self)
self.mainGroup = QGroupBox(self.tr("Title Details"), self)
self.mainForm = QGridLayout()
self.mainGroup.setLayout(self.mainForm)
@@ -190,8 +190,8 @@ class GuiOutlineDetails(QScrollArea):
self.mainForm.setVerticalSpacing(vSpace)
# Selected Item Tags
self.tagsGroup = QGroupBox("Reference Tags", self)
self.tagsForm = QGridLayout()
self.tagsGroup = QGroupBox(self.tr("Reference Tags"), self)
self.tagsForm = QGridLayout()
self.tagsGroup.setLayout(self.tagsForm)
self.tagsForm.addWidget(self.povKeyLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
@@ -256,7 +256,7 @@ class GuiOutlineDetails(QScrollArea):
def clearDetails(self):
"""Clear all the data labels.
"""
self.titleLabel.setText("<b>Title</b>")
self.titleLabel.setText("<b>%s</b>" % self.tr("Title"))
self.titleValue.setText("")
self.fileValue.setText("")
self.itemValue.setText("")
@@ -279,16 +279,16 @@ class GuiOutlineDetails(QScrollArea):
"""Update the content of the tree with the given handle and line
number pointing to a header.
"""
nwItem = self.theProject.projTree[tHandle]
novIdx = self.theIndex.getNovelData(tHandle, sTitle)
nwItem = self.theProject.projTree[tHandle]
novIdx = self.theIndex.getNovelData(tHandle, sTitle)
theRefs = self.theIndex.getReferences(tHandle, sTitle)
if nwItem is None or novIdx is None:
return False
if novIdx["level"] in self.LVL_MAP:
self.titleLabel.setText("<b>%s</b>" % self.LVL_MAP[novIdx["level"]])
self.titleLabel.setText("<b>%s</b>" % self.tr(self.LVL_MAP[novIdx["level"]]))
else:
self.titleLabel.setText("<b>Title</b>")
self.titleLabel.setText("<b>%s</b>" % self.tr("Title"))
self.titleValue.setText(novIdx["title"])
self.fileValue.setText(nwItem.itemName)
@@ -323,7 +323,7 @@ class GuiOutlineDetails(QScrollArea):
def _tagClicked(self, theLink):
"""Capture the click of a tag in the right-most column.
"""
logger.verbose("Clicked link: '%s'" % theLink)
logger.verbose("Clicked link: '%s'", theLink)
if len(theLink) > 0:
theBits = theLink.split("=")
if len(theBits) == 2:

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – GUI Project Details
=================================
@@ -25,8 +24,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import nw
import logging
import math
import logging
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtGui import QFont
@@ -35,15 +34,16 @@ from PyQt5.QtWidgets import (
QLabel, QSpinBox, QGridLayout, QHBoxLayout, QLineEdit, QAbstractItemView
)
from nw.gui.custom import PagedDialog, QSwitch
from nw.common import numberToRoman
from nw.constants import nwUnicode
from nw.core import numberToRoman
from nw.gui.custom import PagedDialog, QSwitch
logger = logging.getLogger(__name__)
class GuiProjectDetails(PagedDialog):
def __init__(self, theParent, theProject):
def __init__(self, theParent):
PagedDialog.__init__(self, theParent)
logger.debug("Initialising GuiProjectDetails ...")
@@ -51,10 +51,10 @@ class GuiProjectDetails(PagedDialog):
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theProject = theProject
self.optState = theProject.optState
self.theProject = theParent.theProject
self.optState = theParent.theProject.optState
self.setWindowTitle("Project Details")
self.setWindowTitle(self.tr("Project Details"))
wW = self.mainConf.pxInt(600)
wH = self.mainConf.pxInt(400)
@@ -66,13 +66,14 @@ class GuiProjectDetails(PagedDialog):
self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "winHeight", wH))
)
self.tabMain = GuiProjectDetailsMain(self.theParent, self.theProject)
self.tabMain = GuiProjectDetailsMain(self.theParent, self.theProject)
self.tabContents = GuiProjectDetailsContents(self.theParent, self.theProject)
self.addTab(self.tabMain, "Overview")
self.addTab(self.tabContents, "Contents")
self.addTab(self.tabMain, self.tr("Overview"))
self.addTab(self.tabContents, self.tr("Contents"))
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close)
self.buttonBox.button(QDialogButtonBox.Close).setText(self.tr("Close"))
self.buttonBox.rejected.connect(self._doClose)
self.addControls(self.buttonBox)
@@ -127,6 +128,7 @@ class GuiProjectDetails(PagedDialog):
# END Class GuiProjectDetails
class GuiProjectDetailsMain(QWidget):
def __init__(self, theParent, theProject):
@@ -154,7 +156,9 @@ class GuiProjectDetailsMain(QWidget):
self.bookTitle.setAlignment(Qt.AlignHCenter)
self.bookTitle.setWordWrap(True)
self.projName = QLabel("Working Title: %s" % self.theProject.projName)
self.projName = QLabel(
self.tr("Working Title: {0}").format(self.theProject.projName)
)
workFont = self.projName.font()
workFont.setPointSizeF(0.8*fPt)
workFont.setItalic(True)
@@ -162,7 +166,7 @@ class GuiProjectDetailsMain(QWidget):
self.projName.setAlignment(Qt.AlignHCenter)
self.projName.setWordWrap(True)
self.bookAuthors = QLabel("By %s" % self.theProject.getAuthors())
self.bookAuthors = QLabel(self.tr("By {0}").format(self.theProject.getAuthors()))
authFont = self.bookAuthors.font()
authFont.setPointSizeF(1.2*fPt)
self.bookAuthors.setFont(authFont)
@@ -175,20 +179,20 @@ class GuiProjectDetailsMain(QWidget):
hCounts = self.theIndex.getNovelTitleCounts()
nwCount = self.theIndex.getNovelWordCount()
self.wordCountLbl = QLabel("<b>Words:</b>")
self.wordCountLbl = QLabel("<b>%s:</b>" % self.tr("Words"))
self.wordCountVal = QLabel(f"{nwCount:n}")
self.chapCountLbl = QLabel("<b>Chapters:</b>")
self.chapCountLbl = QLabel("<b>%s:</b>" % self.tr("Chapters"))
self.chapCountVal = QLabel(f"{hCounts[2]:n}")
self.sceneCountLbl = QLabel("<b>Scenes:</b>")
self.sceneCountLbl = QLabel("<b>%s:</b>" % self.tr("Scenes"))
self.sceneCountVal = QLabel(f"{hCounts[3]:n}")
self.revCountLbl = QLabel("<b>Revisions:</b>")
self.revCountLbl = QLabel("<b>%s:</b>" % self.tr("Revisions"))
self.revCountVal = QLabel(f"{self.theProject.saveCount:n}")
edTime = self.theProject.getCurrentEditTime()
self.editTimeLbl = QLabel("<b>Editing Time:</b>")
self.editTimeLbl = QLabel("<b>%s:</b>" % self.tr("Editing Time"))
self.editTimeVal = QLabel(f"{edTime//3600:02d}:{edTime%3600//60:02d}")
self.statsGrid = QGridLayout()
@@ -208,7 +212,7 @@ class GuiProjectDetailsMain(QWidget):
# Meta
# ====
self.projPathLbl = QLabel("<b>Path:</b>")
self.projPathLbl = QLabel("<b>%s:</b>" % self.tr("Path"))
self.projPathVal = QLineEdit()
self.projPathVal.setText(self.theProject.projPath)
self.projPathVal.setReadOnly(True)
@@ -238,6 +242,7 @@ class GuiProjectDetailsMain(QWidget):
# END Class GuiProjectDetailsMain
class GuiProjectDetailsContents(QWidget):
C_TITLE = 0
@@ -271,7 +276,14 @@ class GuiProjectDetailsContents(QWidget):
self.tocTree.setIndentation(0)
self.tocTree.setColumnCount(6)
self.tocTree.setSelectionMode(QAbstractItemView.NoSelection)
self.tocTree.setHeaderLabels(["Title", "Words", "Pages", "Page", "Progress", ""])
self.tocTree.setHeaderLabels([
self.tr("Title"),
self.tr("Words"),
self.tr("Pages"),
self.tr("Page"),
self.tr("Progress"),
""
])
treeHeadItem = self.tocTree.headerItem()
treeHeadItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
@@ -304,16 +316,16 @@ class GuiProjectDetailsContents(QWidget):
clearDouble = self.optState.getInt("GuiProjectDetails", "clearDouble", True)
wordsHelp = (
"Typical word count for a 5 by 8 inch book page with 11 pt font is 350."
self.tr("Typical word count for a 5 by 8 inch book page with 11 pt font is 350.")
)
offsetHelp = (
"Start counting page numbers from this page."
self.tr("Start counting page numbers from this page.")
)
dblHelp = (
"Assume a new chapter or partition always start on an odd numbered page."
self.tr("Assume a new chapter or partition always start on an odd numbered page.")
)
self.wpLabel = QLabel("Words per page")
self.wpLabel = QLabel(self.tr("Words per page"))
self.wpLabel.setToolTip(wordsHelp)
self.wpValue = QSpinBox()
@@ -324,7 +336,7 @@ class GuiProjectDetailsContents(QWidget):
self.wpValue.setToolTip(wordsHelp)
self.wpValue.valueChanged.connect(self._populateTree)
self.poLabel = QLabel("Count pages from")
self.poLabel = QLabel(self.tr("Count pages from"))
self.poLabel.setToolTip(offsetHelp)
self.poValue = QSpinBox()
@@ -335,7 +347,7 @@ class GuiProjectDetailsContents(QWidget):
self.poValue.setToolTip(offsetHelp)
self.poValue.valueChanged.connect(self._populateTree)
self.dblLabel = QLabel("Clear double pages")
self.dblLabel = QLabel(self.tr("Clear double pages"))
self.dblLabel.setToolTip(dblHelp)
self.dblValue = QSwitch(self, 2*iPx, iPx)
@@ -358,7 +370,7 @@ class GuiProjectDetailsContents(QWidget):
# ========
self.outerBox = QVBoxLayout()
self.outerBox.addWidget(QLabel("<b>Table of Contents</b>"))
self.outerBox.addWidget(QLabel("<b>%s</b>" % self.tr("Table of Contents")))
self.outerBox.addWidget(self.tocTree)
self.outerBox.addLayout(self.optionsBox)
@@ -390,7 +402,7 @@ class GuiProjectDetailsContents(QWidget):
"""
self._theToC = []
self._theToC = self.theIndex.getTableOfContents(2)
self._theToC.append(("", 0, "END", 0))
self._theToC.append(("", 0, self.tr("END"), 0))
return
##
@@ -401,8 +413,8 @@ class GuiProjectDetailsContents(QWidget):
"""Set the content of the chapter/page tree.
"""
dblPages = self.dblValue.isChecked()
wpPage = self.wpValue.value()
fstPage = self.poValue.value() - 1
wpPage = self.wpValue.value()
fstPage = self.poValue.value() - 1
pTotal = 0
tPages = 1

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
novelWriter – GUI Project Tree
==============================
@@ -30,19 +29,19 @@ import logging
from time import time
from PyQt5.QtCore import Qt, QSize, pyqtSignal
from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import (
QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction
)
from nw.core import NWDoc
from nw.constants import (
nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert, nwConst, nwLists
)
from nw.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert
from nw.constants import nwConst, trConst, nwLists, nwLabels
logger = logging.getLogger(__name__)
class GuiProjectTree(QTreeWidget):
C_NAME = 0
@@ -52,6 +51,7 @@ class GuiProjectTree(QTreeWidget):
novelItemChanged = pyqtSignal()
noteItemChanged = pyqtSignal()
projectWordCountChanged = pyqtSignal(int, int)
def __init__(self, theParent):
QTreeWidget.__init__(self, theParent)
@@ -85,14 +85,19 @@ class GuiProjectTree(QTreeWidget):
self.setExpandsOnDoubleClick(True)
self.setIndentation(iPx)
self.setColumnCount(4)
self.setHeaderLabels(["Label", "Words", "Inc", "Flags"])
self.setHeaderLabels([
self.tr("Label"),
self.tr("Words"),
self.tr("Inc"),
self.tr("Flags")
])
treeHeadItem = self.headerItem()
treeHeadItem.setTextAlignment(self.C_COUNT, Qt.AlignRight)
treeHeadItem.setToolTip(self.C_NAME, "Item label")
treeHeadItem.setToolTip(self.C_COUNT, "Word count")
treeHeadItem.setToolTip(self.C_EXPORT, "Include in build")
treeHeadItem.setToolTip(self.C_FLAGS, "Status, class, and layout flags")
treeHeadItem.setToolTip(self.C_NAME, self.tr("Item label"))
treeHeadItem.setToolTip(self.C_COUNT, self.tr("Word count"))
treeHeadItem.setToolTip(self.C_EXPORT, self.tr("Include in build"))
treeHeadItem.setToolTip(self.C_FLAGS, self.tr("Status, class, and layout flags"))
# Let the last column stretch, and set the minimum size to the
# size of the icon as the default Qt font metrics approach fails
@@ -127,7 +132,7 @@ class GuiProjectTree(QTreeWidget):
self.initTree()
# Internal Function Mapping
self.makeAlert = self.theParent.makeAlert
self.makeAlert = self.theParent.makeAlert
self.askQuestion = self.theParent.askQuestion
logger.debug("GuiProjectTree initialisation complete")
@@ -192,24 +197,23 @@ class GuiProjectTree(QTreeWidget):
# If class is still not set, alert the user and exit
if itemClass is None:
if itemType == nwItemType.FILE:
self.makeAlert(
"Please select a valid location in the tree to add the document.",
nwAlert.ERROR
)
self.makeAlert(self.tr(
"Please select a valid location in the tree to add the document."
), nwAlert.ERROR)
else:
self.makeAlert(
"Please select a valid location in the tree to add the folder.",
nwAlert.ERROR
)
self.makeAlert(self.tr(
"Please select a valid location in the tree to add the folder."
), nwAlert.ERROR)
return False
# Everything is fine, we have what we need, so we proceed
logger.verbose("Adding new item of type %s and class %s to handle %s" % (
itemType.name, itemClass.name, str(pHandle))
logger.verbose(
"Adding new item of type '%s' and class '%s' to handle '%s'",
itemType.name, itemClass.name, str(pHandle)
)
if itemType == nwItemType.ROOT:
tHandle = self.theProject.newRoot(nwLabels.CLASS_NAME[itemClass], itemClass)
tHandle = self.theProject.newRoot(trConst(nwLabels.CLASS_NAME[itemClass]), itemClass)
if tHandle is None:
logger.error("No root item added")
return False
@@ -222,9 +226,9 @@ class GuiProjectTree(QTreeWidget):
# If still nothing, give up
if pHandle is None:
self.makeAlert(
"Did not find anywhere to add the file or folder!", nwAlert.ERROR
)
self.makeAlert(self.tr(
"Did not find anywhere to add the file or folder!"
), nwAlert.ERROR)
return False
# Now check if the selected item is a file, in which case
@@ -236,35 +240,33 @@ class GuiProjectTree(QTreeWidget):
# If we again have no home, give up
if pHandle is None:
self.makeAlert(
"Did not find anywhere to add the file or folder!", nwAlert.ERROR
)
self.makeAlert(self.tr(
"Did not find anywhere to add the file or folder!"
), nwAlert.ERROR)
return False
if self.theProject.projTree.isTrashRoot(pHandle):
self.makeAlert(
"Cannot add new files or folders to the %s folder." % (
nwLabels.CLASS_NAME[nwItemClass.TRASH]
), nwAlert.ERROR
)
self.makeAlert(self.tr(
"Cannot add new files or folders to the Trash folder."
), nwAlert.ERROR)
return False
parTree = self.theProject.projTree.getItemPath(pHandle)
# If we're still here, add the file or folder
if itemType == nwItemType.FILE:
tHandle = self.theProject.newFile("New File", itemClass, pHandle)
tHandle = self.theProject.newFile(self.tr("New File"), itemClass, pHandle)
elif itemType == nwItemType.FOLDER:
if len(parTree) >= nwConst.MAX_DEPTH - 1:
# Folders cannot be deeper than MAX_DEPTH - 1, leaving room
# for one more level of files.
self.makeAlert((
self.makeAlert(self.tr(
"Cannot add new folder to this item. "
"Maximum folder depth has been reached."
), nwAlert.ERROR)
return False
tHandle = self.theProject.newFolder("New Folder", itemClass, pHandle)
tHandle = self.theProject.newFolder(self.tr("New Folder"), itemClass, pHandle)
else:
logger.error("Failed to add new item")
@@ -284,8 +286,11 @@ class GuiProjectTree(QTreeWidget):
return True
# This is a new files, so let's add some content
newDoc = NWDoc(self.theProject, self.theParent)
curTxt = newDoc.openDocument(tHandle, showStatus=False)
newDoc = NWDoc(self.theProject, tHandle)
curTxt = newDoc.readDocument()
if curTxt is None:
curTxt = ""
if curTxt == "":
if nwItem.itemLayout == nwItemLayout.CHAPTER:
newText = f"## {nwItem.itemName}\n\n"
@@ -297,7 +302,7 @@ class GuiProjectTree(QTreeWidget):
newText = f"# {nwItem.itemName}\n\n"
# Save the text and index it
newDoc.saveDocument(newText)
newDoc.writeDocument(newText)
self.theIndex.scanText(tHandle, newText)
# Get Word Counts
@@ -427,9 +432,9 @@ class GuiProjectTree(QTreeWidget):
logger.debug("Emptying Trash folder")
if trashHandle is None:
self.makeAlert(
"There is currently no Trash folder in this project.", nwAlert.INFO
)
self.makeAlert(self.tr(
"There is currently no Trash folder in this project."
), nwAlert.INFO)
return False
theTrash = self.getTreeFromHandle(trashHandle)
@@ -438,16 +443,19 @@ class GuiProjectTree(QTreeWidget):
nTrash = len(theTrash)
if nTrash == 0:
self.makeAlert("The Trash folder is already empty.", nwAlert.INFO)
self.makeAlert(self.tr(
"The Trash folder is already empty."
), nwAlert.INFO)
return False
msgYes = self.askQuestion(
"Empty Trash", "Permanently delete %d file(s) from Trash?" % nTrash
self.tr("Empty Trash"),
self.tr("Permanently delete {0} file(s) from Trash?").format(nTrash)
)
if not msgYes:
return False
logger.verbose("Deleting %d file(s) from Trash" % nTrash)
logger.verbose("Deleting %d file(s) from Trash", nTrash)
for tHandle in self.getTreeFromHandle(trashHandle):
if tHandle == trashHandle:
continue
@@ -489,7 +497,7 @@ class GuiProjectTree(QTreeWidget):
wCount = int(trItemS.data(self.C_COUNT, Qt.UserRole))
if nwItemS.itemType == nwItemType.FILE:
logger.debug("User requested file %s deleted" % tHandle)
logger.debug("User requested file '%s' deleted", tHandle)
trItemP = trItemS.parent()
trItemT = self._addTrashRoot()
if trItemP is None or trItemT is None:
@@ -503,7 +511,8 @@ class GuiProjectTree(QTreeWidget):
doPermanent = False
if not alreadyAsked:
msgYes = self.askQuestion(
"Delete File", "Permanently delete file '%s'?" % nwItemS.itemName
self.tr("Delete File"),
self.tr("Permanently delete file '{0}'?").format(nwItemS.itemName)
)
if msgYes:
doPermanent = True
@@ -511,32 +520,39 @@ class GuiProjectTree(QTreeWidget):
doPermanent = True
if doPermanent:
logger.debug("Permanently deleting file with handle %s" % tHandle)
logger.debug("Permanently deleting file with handle '%s'", tHandle)
self.propagateCount(tHandle, 0)
tIndex = trItemP.indexOfChild(trItemS)
trItemC = trItemP.takeChild(tIndex)
if self.theParent.docEditor.theHandle == tHandle:
if self.theParent.docEditor.docHandle() == tHandle:
self.theParent.closeDocument()
theDoc = NWDoc(self.theProject, self.theParent)
theDoc.deleteDocument(tHandle)
delDoc = NWDoc(self.theProject, tHandle)
if not delDoc.deleteDocument():
self.makeAlert([
self.tr("Could not delete document file."), delDoc.getError()
], nwAlert.ERROR)
return False
self.theIndex.deleteHandle(tHandle)
self._deleteTreeItem(tHandle)
self._setTreeChanged(True)
self.projectWordCount()
else:
# The file is not already in the trash folder, so we
# move it there.
msgYes = self.askQuestion(
"Delete File", "Move file '%s' to Trash?" % nwItemS.itemName
self.tr("Delete File"),
self.tr("Move file '{0}' to Trash?").format(nwItemS.itemName),
)
if msgYes:
if pHandle is None:
logger.warning("File has no parent item")
logger.debug("Moving file %s to trash" % tHandle)
logger.debug("Moving file '%s' to trash", tHandle)
self.propagateCount(tHandle, 0)
tIndex = trItemP.indexOfChild(trItemS)
@@ -550,7 +566,7 @@ class GuiProjectTree(QTreeWidget):
self._setTreeChanged(True)
elif nwItemS.itemType == nwItemType.FOLDER:
logger.debug("User requested folder %s deleted" % tHandle)
logger.debug("User requested folder '%s' deleted", tHandle)
trItemP = trItemS.parent()
if trItemP is None:
logger.error("Could not delete folder")
@@ -561,7 +577,7 @@ class GuiProjectTree(QTreeWidget):
self._deleteTreeItem(tHandle)
self._setTreeChanged(True)
else:
self.makeAlert((
self.makeAlert(self.tr(
"Cannot delete folder. It is not empty. "
"Recursive deletion is not supported. "
"Please delete the content first."
@@ -569,7 +585,7 @@ class GuiProjectTree(QTreeWidget):
return False
elif nwItemS.itemType == nwItemType.ROOT:
logger.debug("User requested root folder %s deleted" % tHandle)
logger.debug("User requested root folder '%s' deleted", tHandle)
tIndex = self.indexOfTopLevelItem(trItemS)
if trItemS.childCount() == 0:
self.takeTopLevelItem(tIndex)
@@ -577,7 +593,7 @@ class GuiProjectTree(QTreeWidget):
self.theParent.mainMenu.setAvailableRoot()
self._setTreeChanged(True)
else:
self.makeAlert((
self.makeAlert(self.tr(
"Cannot delete root folder. It is not empty. "
"Recursive deletion is not supported. "
"Please delete the content first."
@@ -605,10 +621,10 @@ class GuiProjectTree(QTreeWidget):
iStatus = nwItem.itemStatus
if nwItem.itemClass == nwItemClass.NOVEL:
iStatus = self.theProject.statusItems.checkEntry(iStatus) # Make sure it's valid
iStatus = self.theProject.statusItems.checkEntry(iStatus) # Make sure it's valid
flagIcon = self.theParent.statusIcons[iStatus]
else:
iStatus = self.theProject.importItems.checkEntry(iStatus) # Make sure it's valid
iStatus = self.theProject.importItems.checkEntry(iStatus) # Make sure it's valid
flagIcon = self.theParent.importIcons[iStatus]
trItem.setText(self.C_NAME, nwItem.itemName)
@@ -661,7 +677,8 @@ class GuiProjectTree(QTreeWidget):
self.theProject.setProjectWordCount(nWords)
sWords = self.theProject.getSessionWordCount()
self.theParent.statusBar.setStats(nWords, sWords)
self.projectWordCountChanged.emit(nWords, sWords)
return
@@ -679,7 +696,7 @@ class GuiProjectTree(QTreeWidget):
iCount += 1
self._addTreeItem(nwItem)
logger.debug("%d items added to the project tree" % iCount)
logger.debug("%d item(s) added to the project tree", iCount)
return True
def undoLastMove(self):
@@ -708,9 +725,7 @@ class GuiProjectTree(QTreeWidget):
wCount = int(srcItem.data(self.C_COUNT, Qt.UserRole))
sHandle = srcItem.data(self.C_NAME, Qt.UserRole)
dHandle = dstItem.data(self.C_NAME, Qt.UserRole)
logger.debug("Moving item %s back to %s, index %d" % (
sHandle, dHandle, dstIndex
))
logger.debug("Moving item '%s' back to '%s', index %d", sHandle, dHandle, dstIndex)
self.propagateCount(sHandle, 0)
parItem = srcItem.parent()
@@ -766,6 +781,7 @@ class GuiProjectTree(QTreeWidget):
# Slots
##
@pyqtSlot("QPoint")
def _rightClickMenu(self, clickPos):
"""The user right clicked an element in the project tree, so we
open a context menu in-place.
@@ -773,7 +789,7 @@ class GuiProjectTree(QTreeWidget):
selItem = self.itemAt(clickPos)
if isinstance(selItem, QTreeWidgetItem):
tHandle = selItem.data(self.C_NAME, Qt.UserRole)
self.setSelectedHandle(tHandle) # Just to be safe
self.setSelectedHandle(tHandle) # Just to be safe
tItem = self.theProject.projTree[tHandle]
if tItem is not None:
if self.ctxMenu.filterActions(tItem):
@@ -782,6 +798,14 @@ class GuiProjectTree(QTreeWidget):
return
@pyqtSlot(str, int, int, int)
def doUpdateCounts(self, tHandle, cCount, wCount, pCount):
"""Slot for updating the word count of a specific item.
"""
self.propagateCount(tHandle, wCount)
self.projectWordCount()
return
##
# Events
##
@@ -833,7 +857,9 @@ class GuiProjectTree(QTreeWidget):
snItem = self.theProject.projTree[sHandle]
dnItem = self.theProject.projTree[dHandle]
if dnItem is None:
self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR)
self.makeAlert(self.tr(
"The item cannot be moved to that location."
), nwAlert.ERROR)
return
pItem = sItem.parent()
@@ -855,7 +881,7 @@ class GuiProjectTree(QTreeWidget):
allowDrop &= not (self.dropIndicatorPosition() == QAbstractItemView.OnItem and onFile)
if allowDrop and not isRoot:
logger.debug("Drag'n'drop of item %s accepted" % sHandle)
logger.debug("Drag'n'drop of item '%s' accepted", sHandle)
self.propagateCount(sHandle, 0)
QTreeWidget.dropEvent(self, theEvent)
self._postItemMove(sHandle, snItem, dnItem, wCount)
@@ -863,8 +889,10 @@ class GuiProjectTree(QTreeWidget):
else:
theEvent.ignore()
logger.debug("Drag'n'drop of item %s not accepted" % sHandle)
self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR)
logger.debug("Drag'n'drop of item '%s' not accepted", sHandle)
self.makeAlert(self.tr(
"The item cannot be moved to that location."
), nwAlert.ERROR)
return
@@ -884,9 +912,10 @@ class GuiProjectTree(QTreeWidget):
# If the item does not have the same class as the target,
# and the target is not a free root folder, update its class
if not (isSame or onFree):
logger.debug("Item %s class has been changed from %s to %s" % (
logger.debug(
"Item '%s' class has been changed from '%s' to '%s'",
sHandle, snItem.itemClass.name, dnItem.itemClass.name
))
)
snItem.setClass(dnItem.itemClass)
self.setTreeItemValues(sHandle)
@@ -961,9 +990,9 @@ class GuiProjectTree(QTreeWidget):
elif nwItem.itemType == nwItemType.TRASH:
self.addTopLevelItem(newItem)
else:
self.makeAlert(
"There is nowhere to add item with name '%s'" % nwItem.itemName, nwAlert.ERROR
)
self.makeAlert(self.tr(
"There is nowhere to add item with name '{0}'."
).format(nwItem.itemName), nwAlert.ERROR)
del self._treeMap[tHandle]
return None
@@ -973,7 +1002,7 @@ class GuiProjectTree(QTreeWidget):
try:
byIndex = self._treeMap[pHandle].indexOfChild(self._treeMap[nHandle])
except Exception:
logger.error("Failed to get index of item with handle %s" % nHandle)
logger.error("Failed to get index of item with handle '%s'", nHandle)
if byIndex >= 0:
self._treeMap[pHandle].insertChild(byIndex+1, newItem)
else:
@@ -1023,14 +1052,14 @@ class GuiProjectTree(QTreeWidget):
nwItemS = self.theProject.projTree[tHandle]
trItemP = trItemS.parent()
if trItemP is None:
logger.error("Failed to find new parent item of %s" % tHandle)
logger.error("Failed to find new parent item of '%s'", tHandle)
return False
pHandle = trItemP.data(self.C_NAME, Qt.UserRole)
nwItemS.setParent(pHandle)
self.setTreeItemValues(tHandle)
logger.debug("The parent of item %s has been changed to %s" % (tHandle, pHandle))
logger.debug("The parent of item '%s' has been changed to '%s'", tHandle, pHandle)
return True
@@ -1073,6 +1102,7 @@ class GuiProjectTree(QTreeWidget):
# END Class GuiProjectTree
class GuiProjectTreeMenu(QMenu):
def __init__(self, theTree):
@@ -1081,43 +1111,43 @@ class GuiProjectTreeMenu(QMenu):
self.theTree = theTree
self.theItem = None
self.editItem = QAction("Edit Project Item", self)
self.editItem = QAction(self.tr("Edit Project Item"), self)
self.editItem.triggered.connect(self._doEditItem)
self.addAction(self.editItem)
self.openItem = QAction("Open Document", self)
self.openItem = QAction(self.tr("Open Document"), self)
self.openItem.triggered.connect(self._doOpenItem)
self.addAction(self.openItem)
self.viewItem = QAction("View Document", self)
self.viewItem = QAction(self.tr("View Document"), self)
self.viewItem.triggered.connect(self._doViewItem)
self.addAction(self.viewItem)
self.toggleExp = QAction("Toggle Included Flag", self)
self.toggleExp = QAction(self.tr("Toggle Included Flag"), self)
self.toggleExp.triggered.connect(self._doToggleExported)
self.addAction(self.toggleExp)
self.newFile = QAction("New File", self)
self.newFile = QAction(self.tr("New File"), self)
self.newFile.triggered.connect(self._doMakeFile)
self.addAction(self.newFile)
self.newFolder = QAction("New Folder", self)
self.newFolder = QAction(self.tr("New Folder"), self)
self.newFolder.triggered.connect(self._doMakeFolder)
self.addAction(self.newFolder)
self.deleteItem = QAction("Delete Item", self)
self.deleteItem = QAction(self.tr("Delete Item"), self)
self.deleteItem.triggered.connect(self._doDeleteItem)
self.addAction(self.deleteItem)
self.emptyTrash = QAction("Empty Trash", self)
self.emptyTrash = QAction(self.tr("Empty Trash"), self)
self.emptyTrash.triggered.connect(self._doEmptyTrash)
self.addAction(self.emptyTrash)
self.moveUp = QAction("Move Item Up", self)
self.moveUp = QAction(self.tr("Move Item Up"), self)
self.moveUp.triggered.connect(self._doMoveUp)
self.addAction(self.moveUp)
self.moveDown = QAction("Move Item Down", self)
self.moveDown = QAction(self.tr("Move Item Down"), self)
self.moveDown.triggered.connect(self._doMoveDown)
self.addAction(self.moveDown)
@@ -1137,7 +1167,7 @@ class GuiProjectTreeMenu(QMenu):
inTrash = theItem.itemParent == trashHandle and trashHandle is not None
isTrash = theItem.itemHandle == trashHandle and trashHandle is not None
isFile = theItem.itemType == nwItemType.FILE
isFile = theItem.itemType == nwItemType.FILE
allowNew = not (isTrash or inTrash)
@@ -1156,6 +1186,7 @@ class GuiProjectTreeMenu(QMenu):
# Slots
##
@pyqtSlot()
def _doOpenItem(self):
"""Forward the open document call to the main GUI window.
"""
@@ -1163,6 +1194,7 @@ class GuiProjectTreeMenu(QMenu):
self.theTree.theParent.openDocument(self.theItem.itemHandle, doScroll=False)
return
@pyqtSlot()
def _doViewItem(self):
"""Forward the view document call to the main GUI window.
"""
@@ -1170,6 +1202,7 @@ class GuiProjectTreeMenu(QMenu):
self.theTree.theParent.viewDocument(self.theItem.itemHandle)
return
@pyqtSlot()
def _doEditItem(self):
"""Forward the edit item call to the main GUI window.
"""
@@ -1177,6 +1210,7 @@ class GuiProjectTreeMenu(QMenu):
self.theTree.theParent.editItem()
return
@pyqtSlot()
def _doMakeFile(self):
"""Forward the new file call to the project tree.
"""
@@ -1184,6 +1218,7 @@ class GuiProjectTreeMenu(QMenu):
self.theTree.newTreeItem(nwItemType.FILE, None)
return
@pyqtSlot()
def _doMakeFolder(self):
"""Forward the new folder call to the project tree.
"""
@@ -1191,6 +1226,7 @@ class GuiProjectTreeMenu(QMenu):
self.theTree.newTreeItem(nwItemType.FOLDER, None)
return
@pyqtSlot()
def _doToggleExported(self):
"""Flip the isExported flag of the current item.
"""
@@ -1199,6 +1235,7 @@ class GuiProjectTreeMenu(QMenu):
self.theTree.setTreeItemValues(self.theItem.itemHandle)
return
@pyqtSlot()
def _doDeleteItem(self):
"""Forward the delete item call to the project tree.
"""
@@ -1206,18 +1243,21 @@ class GuiProjectTreeMenu(QMenu):
self.theTree.deleteItem()
return
@pyqtSlot()
def _doEmptyTrash(self):
"""Forward the empty trash call to the project tree.
"""
self.theTree.emptyTrash()
return
@pyqtSlot()
def _doMoveUp(self):
"""Forward the move item call to the project tree.
"""
self.theTree.moveTreeItem(-1)
return
@pyqtSlot()
def _doMoveDown(self):
"""Forward the move item call to the project tree.
"""

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