Compare commits

...

247 Commits
v1.2 ... v1.3

Author SHA1 Message Date
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 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 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
67a81400ff Clarify a few things about markup in the main readme 2021-04-25 11:25:30 +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 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
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
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
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
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
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 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
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
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 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 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
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
140 changed files with 25423 additions and 3736 deletions

View File

@@ -1,4 +1,4 @@
name: Linux (3.8)
name: Linux
on:
push:
@@ -7,13 +7,16 @@ on:
branches: [ main, testing, dev ]
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
run: |

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,4 +1,4 @@
name: macOS (3.9)
name: macOS
on:
push:
@@ -7,7 +7,7 @@ on:
branches: [ main, testing, dev ]
jobs:
testMac38:
testMac:
runs-on: macos-latest
steps:
- name: Python Setup

View File

@@ -1,4 +1,4 @@
name: Windows (3.9)
name: Windows
on:
push:
@@ -7,7 +7,7 @@ on:
branches: [ main, testing, dev ]
jobs:
testWin38:
testWin:
runs-on: windows-latest
steps:
- name: Python Setup

6
.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
@@ -41,3 +46,4 @@ ToC.txt
# Coverage
/.coverage
/coverage.*

View File

@@ -1,5 +1,228 @@
# novelWriter Changelog
## 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

191
README.md
View File

@@ -1,53 +1,53 @@
# 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, and 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).
## 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.
submitting larger additions or changes.
# Key Features
If you want to help translating novelWriter into another language, please see the
[README](https://github.com/vkbo/novelWriter/blob/main/i18n/README.md) in the `nw/assets/i18n`
folder.
Some features of novelWriter are listed below. Consult the
## Key Features
Some key features of novelWriter are listed below. Consult the
[documentation](https://novelwriter.readthedocs.io) for more information.
### Markdown Flavour
novelWriter is _not_ a full-feature Markdown editor. It is a plain text editor that uses
Note that novelWriter is _not_ a proper 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:
@@ -58,30 +58,30 @@ of writing novels. The formatting is currently limited to:
That is it. Features not supported in the editor are also not exported when using the export tool.
In addition, novelWriter adds the following, which is otherwise not supported by Markdown:
In addition, novelWriter adds the following syntax used for its additional features:
* A line starting with `%` is treated as a comment and not rendered on exports unless requested.
Comments do not count towards the word count.
Comments do not count towards word counts and other statistics.
* 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.
synopsis for the section of text where it occurs. 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 `@`. These are used for tagging
and inter-linking documents, and can also be included when generating 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.
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,14 +92,14 @@ 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
@@ -108,104 +108,91 @@ impact on the format of the exported document. See the
### 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
@@ -231,7 +218,7 @@ Bundled assets have the following licenses:
the main repo is available at
[sdras/night-owl-vscode-theme](https://github.com/sdras/night-owl-vscode-theme).
# Screenshot
## Screenshots
**novelWriter with default system theme:**
![Screenshot 1](https://raw.githubusercontent.com/vkbo/novelWriter/main/docs/source/images/screenshot_default.png)

View File

@@ -25,10 +25,9 @@ copyright = "2018–2021, Veronica Berglyd Olsen"
author = "Veronica Berglyd Olsen"
# The short X.Y version
version = "1.2"
version = "1.3"
# The full version, including alpha/beta/rc tags
release = "1.2"
release = "1.3"
# -- 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

@@ -99,6 +99,30 @@ 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 ``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 Python package ``pylupdate5`` to be installed.
.. 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 +133,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
@@ -432,7 +455,7 @@ Most features are available as keyboard shortcuts. These are as follows:
":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:`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."

72
i18n/README.md Normal file
View File

@@ -0,0 +1,72 @@
# 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 files translation maps for the novelWriter
projects, used by the Build Novel Project tool.
## 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` files in the `i18n` folder
are needed.
**Note:** This requires that you have the tools Qt 5 Linguist and the PyQt5 tool `pylupdate5`
installed on your system.
### Missing QtBase Translations
The default Qt dialogs also have translations, for instance for standard buttons for "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.

4931
i18n/nw_en_US.ts Normal file

File diff suppressed because it is too large Load Diff

4931
i18n/nw_fr.ts Normal file

File diff suppressed because it is too large Load Diff

4931
i18n/nw_nb_NO.ts Normal file

File diff suppressed because it is too large Load Diff

4931
i18n/nw_pt.ts Normal file

File diff suppressed because it is too large Load Diff

74
i18n/qtbase.py Normal file
View File

@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
"""
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")

38
novelWriter.pro Normal file
View File

@@ -0,0 +1,38 @@
SOURCES += \
i18n/qtbase.py \
nw/core/document.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

View File

@@ -62,9 +62,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.3"
__hexversion__ = "0x010300f0"
__date__ = "2021-05-02"
__status__ = "Stable"
__domain__ = "novelwriter.io"
__url__ = "https://novelwriter.io"
@@ -73,10 +73,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
@@ -137,7 +133,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"
@@ -221,14 +217,14 @@ 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
@@ -285,6 +281,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

@@ -2,82 +2,27 @@
<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>
<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>
<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>
<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

@@ -29,10 +29,10 @@ 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__)
@@ -149,37 +149,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.
"""
@@ -267,33 +236,61 @@ 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"
return QCoreApplication.translate(
"Common", "a minute ago"
)
elif secDiff < 3300: # 55 minutes
return "%d minutes ago" % int(round(secDiff/60))
return QCoreApplication.translate(
"Common", "{0} minutes ago"
).format(int(round(secDiff/60)))
elif secDiff < 5400: # 90 minutes
return "an hour ago"
return QCoreApplication.translate(
"Common", "an hour ago"
)
elif secDiff < 84600: # 23.5 hours
return "%d hours ago" % int(round(secDiff/3600))
return QCoreApplication.translate(
"Common", "{0} hours ago"
).format(int(round(secDiff/3600)))
elif secDiff < 129600: # 1.5 days
return "a day ago"
return QCoreApplication.translate(
"Common", "a day ago"
)
elif secDiff < 561600: # 6.5 days
return "%d days ago" % int(round(secDiff/86400))
return QCoreApplication.translate(
"Common", "{0} days ago"
).format(int(round(secDiff/86400)))
elif secDiff < 907200: # 10.5 days
return "a week ago"
return QCoreApplication.translate(
"Common", "a week ago"
)
elif secDiff < 2419200: # 28 days
return "%d weeks ago" % int(round(secDiff/604800))
return QCoreApplication.translate(
"Common", "{0} weeks ago"
).format(int(round(secDiff/604800)))
elif secDiff < 3888000: # 45 days
return "a month ago"
return QCoreApplication.translate(
"Common", "a month ago"
)
elif secDiff < 29808000: # 345 days
return "%d months ago" % int(round(secDiff/2592000))
return QCoreApplication.translate(
"Common", "{0} months ago"
).format(int(round(secDiff/2592000)))
elif secDiff < 47336400: # 1.5 years
return "a year ago"
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.
@@ -311,3 +308,26 @@ 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

@@ -34,11 +34,14 @@ 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__)
@@ -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
@@ -84,14 +90,21 @@ class Config:
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.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
## 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]
@@ -151,6 +164,9 @@ class Config:
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
self.spellTool = None
@@ -290,6 +306,9 @@ class Config:
self.iconPath = os.path.join(self.assetPath, "icons")
self.appIcon = os.path.join(self.iconPath, "novelwriter.svg")
# Internationalisation
self.nwLangPath = os.path.join(self.assetPath, "i18n")
logger.verbose("App path: %s" % self.appPath)
logger.verbose("Last path: %s" % self.lastPath)
@@ -354,6 +373,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" % qTrans.filePath())
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.
"""
@@ -397,12 +467,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
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
)
@@ -505,6 +581,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
)
@@ -626,11 +711,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
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))
@@ -671,6 +758,9 @@ 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))
@@ -851,6 +941,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 +1010,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]
@@ -1017,7 +1116,7 @@ class Config:
try:
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 +1124,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")

View File

@@ -24,9 +24,14 @@ 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
)
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():
@@ -119,17 +124,17 @@ 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",
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",
@@ -158,15 +163,15 @@ class nwLabels():
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",
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",
@@ -180,27 +185,27 @@ class nwLabels():
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",
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 : "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.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],
@@ -208,7 +213,7 @@ class nwLabels():
nwOutline.OBJECT : KEY_NAME[nwKeyWords.OBJECT_KEY],
nwOutline.ENTITY : KEY_NAME[nwKeyWords.ENTITY_KEY],
nwOutline.CUSTOM : KEY_NAME[nwKeyWords.CUSTOM_KEY],
nwOutline.SYNOP : "Synopsis",
nwOutline.SYNOP : QT_TRANSLATE_NOOP("Constant", "Synopsis"),
}
# END Class nwLabels
@@ -218,28 +223,28 @@ class nwQuotes():
Source: https://en.wikipedia.org/wiki/Quotation_mark
"""
SYMBOLS = {
"\u0027" : "Straight single quotation mark",
"\u0022" : "Straight double quotation mark",
"\u0027" : QT_TRANSLATE_NOOP("Constant", "Straight single quotation mark"),
"\u0022" : QT_TRANSLATE_NOOP("Constant", "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",
"\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" : "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",
"\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" : "Left corner bracket",
"\u300d" : "Right corner bracket",
"\u300e" : "Left white corner bracket",
"\u300f" : "Right white corner bracket",
"\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

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,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,15 @@
# -*- coding: utf-8 -*-
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

@@ -27,9 +27,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
import os
from nw.constants import nwAlert
from functools import partial
from PyQt5.QtCore import QCoreApplication
from nw.enum import nwAlert, nwItemLayout, nwItemClass
from nw.common import isHandle
from nw.constants import nwItemLayout, nwItemClass
logger = logging.getLogger(__name__)
@@ -48,6 +51,7 @@ class NWDoc():
# Internal Mapping
self.makeAlert = self.theParent.makeAlert
self.tr = partial(QCoreApplication.translate, "NWDoc")
return
@@ -111,7 +115,7 @@ class NWDoc():
theText += inFile.read()
except Exception as e:
self.makeAlert(["Failed to open document file.", str(e)], nwAlert.ERROR)
self.makeAlert([self.tr("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.
@@ -124,7 +128,9 @@ class NWDoc():
return ""
if showStatus and not isOrphan:
self.theParent.setStatus("Opened Document: %s" % self._theItem.itemName)
self.theParent.setStatus(
self.tr("Opened Document: {0}").format(self._theItem.itemName)
)
return theText
@@ -158,7 +164,7 @@ class NWDoc():
outFile.write(docMeta)
outFile.write(docText)
except Exception as e:
self.makeAlert(["Could not save document.", str(e)], nwAlert.ERROR)
self.makeAlert([self.tr("Could not save document."), str(e)], nwAlert.ERROR)
return False
# If we're here, the file was successfully saved, so we can
@@ -168,7 +174,9 @@ class NWDoc():
os.rename(docTemp, docPath)
if self._theItem is not None:
self.theParent.setStatus("Saved Document: %s" % self._theItem.itemName)
self.theParent.setStatus(
self.tr("Saved Document: {0}").format(self._theItem.itemName)
)
return True
@@ -191,7 +199,9 @@ class NWDoc():
os.unlink(chkFile)
logger.debug("Deleted: %s" % chkFile)
except Exception as e:
self.makeAlert(["Could not delete document file.", str(e)], nwAlert.ERROR)
self.makeAlert(
[self.tr("Could not delete document file."), str(e)], nwAlert.ERROR
)
return False
return True

View File

@@ -5,7 +5,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,12 +32,10 @@ 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__)
@@ -158,10 +157,6 @@ class NWIndex():
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", {})
@@ -228,10 +223,6 @@ class NWIndex():
if self.indexBroken:
self.clearIndex()
self.theParent.makeAlert(
"The project index is outdated or broken. Rebuilding index.",
nwAlert.WARN
)
return
@@ -905,3 +896,62 @@ 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
# 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

View File

@@ -28,10 +28,8 @@ 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__)

View File

@@ -91,6 +91,8 @@ class OptionState():
"winWidth",
"winHeight",
"replaceColW",
"statusColW",
"importColW",
},
"GuiProjectDetails": {
"winWidth",

View File

@@ -27,10 +27,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import nw
import logging
import os
import json
import shutil
from lxml import etree
from time import time
from functools import partial
from PyQt5.QtCore import QCoreApplication
from nw.core.tree import NWTree
from nw.core.item import NWItem
@@ -41,9 +45,8 @@ from nw.common import (
checkString, checkBool, checkInt, isHandle, formatTimeStamp,
makeFileNameSafe, hexToInt
)
from nw.constants import (
nwFiles, nwItemType, nwItemClass, nwItemLayout, nwLabels, nwAlert
)
from nw.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert
from nw.constants import trConst, nwFiles, nwLabels
logger = logging.getLogger(__name__)
@@ -58,6 +61,7 @@ class NWProject():
# Core Elements
self.optState = OptionState(self) # Project-specific GUI options
self.projTree = NWTree(self) # The project tree
self.langData = {} # Localisation data
# Project Status
self.projOpened = 0 # The time stamp of when the project file was opened
@@ -74,7 +78,8 @@ class NWProject():
self.projCache = None # The full path to the project's cache folder
self.projContent = None # The full path to the project's content folder
self.projDict = None # The spell check dictionary
self.projLang = None # The spell check language, if different than default
self.projSpell = None # The spell check language, if different than default
self.projLang = None # The project language, used for builds
self.projFile = None # The file name of the project main XML file
# Project Meta
@@ -97,11 +102,12 @@ class NWProject():
self.notesWCount = 0 # Total number of words in note files
self.doBackup = True # Run project backup on exit
# Set Defaults
self.clearProject()
# Internal Mapping
self.makeAlert = self.theParent.makeAlert
self.tr = partial(QCoreApplication.translate, "NWProject")
# Set Defaults
self.clearProject()
return
@@ -157,7 +163,7 @@ class NWProject():
trashHandle = self.projTree.trashRoot()
if trashHandle is None:
newItem = NWItem(self)
newItem.setName("Trash")
newItem.setName(self.tr("Trash"))
newItem.setType(nwItemType.TRASH)
newItem.setClass(nwItemClass.TRASH)
self.projTree.append(None, None, newItem)
@@ -189,6 +195,7 @@ class NWProject():
self.projCache = None
self.projContent = None
self.projDict = None
self.projSpell = None
self.projLang = None
self.projFile = nwFiles.PROJ_FILE
self.projName = ""
@@ -197,7 +204,7 @@ class NWProject():
self.autoReplace = {}
self.titleFormat = {
"title" : r"%title%",
"chapter" : r"Chapter %ch%: %title%",
"chapter" : self.tr("Chapter")+r" %ch%: %title%",
"unnumbered" : r"%title%",
"scene" : r"* * *",
"section" : r"",
@@ -205,15 +212,15 @@ class NWProject():
self.spellCheck = False
self.autoOutline = True
self.statusItems = NWStatus()
self.statusItems.addEntry("New", (100, 100, 100))
self.statusItems.addEntry("Note", (200, 50, 0))
self.statusItems.addEntry("Draft", (200, 150, 0))
self.statusItems.addEntry("Finished", (50, 200, 0))
self.statusItems.addEntry(self.tr("New"), (100, 100, 100))
self.statusItems.addEntry(self.tr("Note"), (200, 50, 0))
self.statusItems.addEntry(self.tr("Draft"), (200, 150, 0))
self.statusItems.addEntry(self.tr("Finished"), (50, 200, 0))
self.importItems = NWStatus()
self.importItems.addEntry("New", (100, 100, 100))
self.importItems.addEntry("Minor", (200, 50, 0))
self.importItems.addEntry("Major", (200, 150, 0))
self.importItems.addEntry("Main", (50, 200, 0))
self.importItems.addEntry(self.tr("New"), (100, 100, 100))
self.importItems.addEntry(self.tr("Minor"), (200, 50, 0))
self.importItems.addEntry(self.tr("Major"), (200, 150, 0))
self.importItems.addEntry(self.tr("Main"), (50, 200, 0))
self.lastEdited = None
self.lastViewed = None
self.lastWCount = 0
@@ -239,7 +246,7 @@ class NWProject():
# Project Settings
projPath = projData.get("projPath", None)
projName = projData.get("projName", "New Project")
projName = projData.get("projName", self.tr("New Project"))
projTitle = projData.get("projTitle", "")
projAuthors = projData.get("projAuthors", "")
@@ -256,7 +263,7 @@ class NWProject():
titlePage = "# %s\n\n" % (self.bookTitle if self.bookTitle else self.projName)
if self.bookAuthors:
titlePage = "%sBy %s\n" % (titlePage, ", ".join(self.bookAuthors))
titlePage = "%s%s %s\n" % (titlePage, self.tr("By"), self.getAuthors())
# Document object for writing files
aDoc = NWDoc(self, self.theParent)
@@ -265,14 +272,14 @@ class NWProject():
# Creating a minimal project with a few root folders and a
# single chapter folder with a single file.
xHandle = {}
xHandle[1] = self.newRoot("Novel", nwItemClass.NOVEL)
xHandle[2] = self.newRoot("Plot", nwItemClass.PLOT)
xHandle[3] = self.newRoot("Characters", nwItemClass.CHARACTER)
xHandle[4] = self.newRoot("World", nwItemClass.WORLD)
xHandle[5] = self.newFile("Title Page", nwItemClass.NOVEL, xHandle[1])
xHandle[6] = self.newFolder("New Chapter", nwItemClass.NOVEL, xHandle[1])
xHandle[7] = self.newFile("New Chapter", nwItemClass.NOVEL, xHandle[6])
xHandle[8] = self.newFile("New Scene", nwItemClass.NOVEL, xHandle[6])
xHandle[1] = self.newRoot(self.tr("Novel"), nwItemClass.NOVEL)
xHandle[2] = self.newRoot(self.tr("Plot"), nwItemClass.PLOT)
xHandle[3] = self.newRoot(self.tr("Characters"), nwItemClass.CHARACTER)
xHandle[4] = self.newRoot(self.tr("World"), nwItemClass.WORLD)
xHandle[5] = self.newFile(self.tr("Title Page"), nwItemClass.NOVEL, xHandle[1])
xHandle[6] = self.newFolder(self.tr("New Chapter"), nwItemClass.NOVEL, xHandle[1])
xHandle[7] = self.newFile(self.tr("New Chapter"), nwItemClass.NOVEL, xHandle[6])
xHandle[8] = self.newFile(self.tr("New Scene"), nwItemClass.NOVEL, xHandle[6])
self.projTree.setFileItemLayout(xHandle[5], nwItemLayout.TITLE)
self.projTree.setFileItemLayout(xHandle[7], nwItemLayout.CHAPTER)
@@ -282,11 +289,11 @@ class NWProject():
aDoc.clearDocument()
aDoc.openDocument(xHandle[7], showStatus=False)
aDoc.saveDocument("## New Chapter\n\n")
aDoc.saveDocument("## %s\n\n" % self.tr("New Chapter"))
aDoc.clearDocument()
aDoc.openDocument(xHandle[8], showStatus=False)
aDoc.saveDocument("### New Scene\n\n")
aDoc.saveDocument("### %s\n\n" % self.tr("New Scene"))
aDoc.clearDocument()
elif popCustom:
@@ -295,13 +302,13 @@ class NWProject():
# wizard's custom page.
# Create root folders
nHandle = self.newRoot("Novel", nwItemClass.NOVEL)
nHandle = self.newRoot(self.tr("Novel"), nwItemClass.NOVEL)
for newRoot in projData.get("addRoots", []):
if newRoot in nwItemClass:
self.newRoot(nwLabels.CLASS_NAME[newRoot], newRoot)
self.newRoot(trConst(nwLabels.CLASS_NAME[newRoot]), newRoot)
# Create a title page
tHandle = self.newFile("Title Page", nwItemClass.NOVEL, nHandle)
tHandle = self.newFile(self.tr("Title Page"), nwItemClass.NOVEL, nHandle)
self.projTree.setFileItemLayout(tHandle, nwItemLayout.TITLE)
aDoc.openDocument(tHandle, showStatus=False)
@@ -316,7 +323,7 @@ class NWProject():
# Create chapters
if numChapters > 0:
for ch in range(numChapters):
chTitle = "Chapter %d" % (ch+1)
chTitle = self.tr("Chapter {0}").format(f"{ch+1:d}")
pHandle = nHandle
if chFolders:
pHandle = self.newFolder(chTitle, nwItemClass.NOVEL, nHandle)
@@ -331,7 +338,7 @@ class NWProject():
# Create chapter scenes
if numScenes > 0:
for sc in range(numScenes):
scTitle = "Scene %d.%d" % (ch+1, sc+1)
scTitle = self.tr("Scene {0}").format(f"{ch+1:d}.{sc+1:d}")
sHandle = self.newFile(scTitle, nwItemClass.NOVEL, pHandle)
aDoc.openDocument(sHandle, showStatus=False)
@@ -341,7 +348,7 @@ class NWProject():
# Create scenes (no chapters)
elif numScenes > 0:
for sc in range(numScenes):
scTitle = "Scene %d" % (sc+1)
scTitle = self.tr("Scene {0}").format(f"{sc+1:d}")
sHandle = self.newFile(scTitle, nwItemClass.NOVEL, nHandle)
aDoc.openDocument(sHandle, showStatus=False)
@@ -365,7 +372,7 @@ class NWProject():
if not os.path.isfile(fileName):
fileName = os.path.join(fileName, nwFiles.PROJ_FILE)
if not os.path.isfile(fileName):
self.makeAlert("File not found: %s" % fileName, nwAlert.ERROR)
self.makeAlert(self.tr("File not found: {0}").format(fileName), nwAlert.ERROR)
return False
self.clearProject()
@@ -414,16 +421,20 @@ class NWProject():
try:
nwXML = etree.parse(fileName)
except Exception as e:
self.makeAlert(["Failed to parse project xml.", str(e)], nwAlert.ERROR)
self.makeAlert([self.tr("Failed to parse project xml."), str(e)], nwAlert.ERROR)
# Trying to open backup file instead
backFile = fileName[:-3]+"bak"
if os.path.isfile(backFile):
self.makeAlert("Attempting to open backup project file instead.", nwAlert.INFO)
self.makeAlert(
self.tr("Attempting to open backup project file instead."), nwAlert.INFO
)
try:
nwXML = etree.parse(backFile)
except Exception as e:
self.makeAlert(["Failed to parse project xml.", str(e)], nwAlert.ERROR)
self.makeAlert(
[self.tr("Failed to parse project xml."), str(e)], nwAlert.ERROR
)
self.clearProject()
return False
else:
@@ -433,15 +444,9 @@ class NWProject():
xRoot = nwXML.getroot()
nwxRoot = xRoot.tag
appVersion = xRoot.attrib.get("appVersion", "Unknown")
hexVersion = xRoot.attrib.get("hexVersion", "0x0")
fileVersion = xRoot.attrib.get("fileVersion", "Unknown")
# The following are deprecated and will be removed
# The settings have been moved to the <project> tag
self.saveCount = checkInt(xRoot.attrib.get("saveCount", 0), 0, False)
self.autoCount = checkInt(xRoot.attrib.get("autoCount", 0), 0, False)
self.editTime = checkInt(xRoot.attrib.get("editTime", 0), 0, False)
appVersion = xRoot.attrib.get("appVersion", self.tr("Unknown"))
hexVersion = xRoot.attrib.get("hexVersion", "0x0")
fileVersion = xRoot.attrib.get("fileVersion", self.tr("Unknown"))
logger.verbose("XML root is %s" % nwxRoot)
logger.verbose("File version is %s" % fileVersion)
@@ -451,7 +456,7 @@ class NWProject():
if not nwxRoot == "novelWriterXML":
self.makeAlert(
"Project file does not appear to be a novelWriterXML file.",
self.tr("Project file does not appear to be a novelWriterXML file."),
nwAlert.ERROR
)
self.clearProject()
@@ -469,25 +474,13 @@ class NWProject():
# parser will lose the autoReplace settings if allowed to
# read the file. Introduced in version 0.10.
if fileVersion == "1.0":
msgYes = self.theParent.askQuestion("Old Project Version", (
"The project file and data is created by a novelWriter version "
"lower than 0.7. Do you want to upgrade the project to the "
"most recent format?<br><br>Note that after the upgrade, you "
"cannot open the project with an older version of novelWriter "
"any more, so make sure you have a recent backup."
))
if not msgYes:
self.clearProject()
return False
elif fileVersion != "1.1" and fileVersion != "1.2":
if fileVersion not in ("1.0", "1.1", "1.2"):
self.makeAlert((
"Unknown or unsupported novelWriter project file format. "
"The project cannot be opened by this version of novelWriter. "
"The file was saved with novelWriter version {vers:s}."
).format(
vers = appVersion,
self.tr(
"Unknown or unsupported novelWriter project file format. "
"The project cannot be opened by this version of novelWriter. "
"The file was saved with novelWriter version {0}."
).format(appVersion)
), nwAlert.ERROR)
self.clearProject()
return False
@@ -496,14 +489,15 @@ class NWProject():
# =========================
if hexToInt(hexVersion) > hexToInt(nw.__hexversion__):
msgYes = self.theParent.askQuestion("Version Conflict", (
"This project was saved by a newer version of novelWriter, version %s. "
"This is version %s. If you continue to open the project, some attributes "
"and settings may not be preserved, but the overall project should be fine. "
"Continue opening the project?"
) % (
appVersion, nw.__version__
))
msgYes = self.theParent.askQuestion(
self.tr("Version Conflict"),
self.tr(
"This project was saved by a newer version of novelWriter, version "
"{0}. This is version {1}. If you continue to open the project, "
"some attributes and settings may not be preserved, but the "
"overall project should be fine. Continue opening the project?"
).format(appVersion, nw.__version__)
)
if not msgYes:
self.clearProject()
return False
@@ -533,10 +527,6 @@ class NWProject():
elif xItem.tag == "editTime":
self.editTime = checkInt(xItem.text, 0)
# The following is deprecated, and will be removed
elif xItem.tag == "backup":
self.doBackup = checkBool(xItem.text, False)
elif xChild.tag == "settings":
logger.debug("Found project settings")
for xItem in xChild:
@@ -544,10 +534,12 @@ class NWProject():
continue
if xItem.tag == "doBackup":
self.doBackup = checkBool(xItem.text, False)
elif xItem.tag == "language":
self.projLang = checkString(xItem.text, None, True)
elif xItem.tag == "spellCheck":
self.spellCheck = checkBool(xItem.text, False)
elif xItem.tag == "spellLang":
self.projLang = checkString(xItem.text, None, True)
self.projSpell = checkString(xItem.text, None, True)
elif xItem.tag == "autoOutline":
self.autoOutline = checkBool(xItem.text, True)
elif xItem.tag == "lastEdited":
@@ -566,13 +558,8 @@ class NWProject():
self.importItems.unpackXML(xItem)
elif xItem.tag == "autoReplace":
for xEntry in xItem:
if xEntry.tag == "entry":
if "key" in xEntry.attrib:
self.autoReplace[xEntry.attrib["key"]] = checkString(
xEntry.text, None, False
)
else: # Old format
self.autoReplace[xEntry.tag] = checkString(
if xEntry.tag == "entry" and "key" in xEntry.attrib:
self.autoReplace[xEntry.attrib["key"]] = checkString(
xEntry.text, None, False
)
elif xItem.tag == "titleFormat":
@@ -595,16 +582,17 @@ class NWProject():
if errList:
self.makeAlert(errList, nwAlert.ERROR)
# Clean up old files
# Clean up no longer used files
self._deprecatedFiles()
# Update recent projects
self.mainConf.updateRecentCache(self.projPath, self.projName, self.lastWCount, time())
self.mainConf.saveRecentCache()
self.theParent.setStatus("Opened Project: %s" % self.projName)
self.theParent.setStatus(self.tr("Opened Project: {0}").format(self.projName))
self._scanProjectFolder()
self._loadProjectLocalisation()
self.currWCount = self.lastWCount
self.projOpened = time()
@@ -623,7 +611,7 @@ class NWProject():
"""
if self.projPath is None:
self.makeAlert(
"Project path not set, cannot save project.", nwAlert.ERROR
self.tr("Project path not set, cannot save project."), nwAlert.ERROR
)
return False
@@ -665,8 +653,9 @@ class NWProject():
# Save Project Settings
xSettings = etree.SubElement(nwXML, "settings")
self._packProjectValue(xSettings, "doBackup", self.doBackup)
self._packProjectValue(xSettings, "language", self.projLang)
self._packProjectValue(xSettings, "spellCheck", self.spellCheck)
self._packProjectValue(xSettings, "spellLang", self.projLang)
self._packProjectValue(xSettings, "spellLang", self.projSpell)
self._packProjectValue(xSettings, "autoOutline", self.autoOutline)
self._packProjectValue(xSettings, "lastEdited", self.lastEdited)
self._packProjectValue(xSettings, "lastViewed", self.lastViewed)
@@ -702,7 +691,7 @@ class NWProject():
xml_declaration = True
))
except Exception as e:
self.makeAlert(["Failed to save project.", str(e)], nwAlert.ERROR)
self.makeAlert([self.tr("Failed to save project."), str(e)], nwAlert.ERROR)
return False
# If we're here, the file was successfully saved,
@@ -721,7 +710,7 @@ class NWProject():
self.mainConf.saveRecentCache()
self._writeLockFile()
self.theParent.setStatus("Saved Project: %s" % self.projName)
self.theParent.setStatus(self.tr("Saved Project: {0}").format(self.projName))
self.setProjectChanged(False)
return True
@@ -773,27 +762,33 @@ class NWProject():
return False
logger.info("Backing up project")
self.theParent.setStatus("Backing up project ...")
self.theParent.setStatus(self.tr("Backing up project ..."))
if self.mainConf.backupPath is None or self.mainConf.backupPath == "":
self.theParent.makeAlert((
"Cannot backup project because no backup path is set. "
"Please set a valid backup location in Tools > Preferences."
), nwAlert.ERROR)
self.theParent.makeAlert(
self.tr(
"Cannot backup project because no backup path is set. "
"Please set a valid backup location in Tools > Preferences."
), nwAlert.ERROR
)
return False
if self.projName is None or self.projName == "":
self.theParent.makeAlert((
"Cannot backup project because no project name is set. "
"Please set a Working Title in Project > Project Settings."
), nwAlert.ERROR)
self.theParent.makeAlert(
self.tr(
"Cannot backup project because no project name is set. "
"Please set a Working Title in Project > Project Settings."
), nwAlert.ERROR
)
return False
if not os.path.isdir(self.mainConf.backupPath):
self.theParent.makeAlert((
"Cannot backup project because the backup path does not exist. "
"Please set a valid backup location in Tools > Preferences."
), nwAlert.ERROR)
self.theParent.makeAlert(
self.tr(
"Cannot backup project because the backup path does not exist. "
"Please set a valid backup location in Tools > Preferences."
), nwAlert.ERROR
)
return False
cleanName = makeFileNameSafe(self.projName)
@@ -804,20 +799,22 @@ class NWProject():
logger.debug("Created folder %s" % baseDir)
except Exception as e:
self.theParent.makeAlert(
["Could not create backup folder.", str(e)],
[self.tr("Could not create backup folder."), str(e)],
nwAlert.ERROR
)
return False
if os.path.commonpath([self.projPath, baseDir]) == self.projPath:
self.theParent.makeAlert((
"Cannot backup project because the backup path is within the "
"project folder to be backed up. Please choose a different "
"backup path in Tools > Preferences."
), nwAlert.ERROR)
self.theParent.makeAlert(
self.tr(
"Cannot backup project because the backup path is within the "
"project folder to be backed up. Please choose a different "
"backup path in Tools > Preferences."
), nwAlert.ERROR
)
return False
archName = "Backup from %s" % formatTimeStamp(time(), fileSafe=True)
archName = self.tr("Backup from {0}").format(formatTimeStamp(time(), fileSafe=True))
baseName = os.path.join(baseDir, archName)
try:
@@ -827,18 +824,21 @@ class NWProject():
logger.info("Backup written to: %s" % archName)
if doNotify:
self.theParent.makeAlert(
"Backup archive file written to: %s.zip" % os.path.join(cleanName, archName),
nwAlert.INFO
self.tr(
"Backup archive file written to: {0}"
).format(
f"{os.path.join(cleanName, archName)}.zip"
), nwAlert.INFO
)
except Exception as e:
self.theParent.makeAlert(
["Could not write backup archive.", str(e)],
[self.tr("Could not write backup archive."), str(e)],
nwAlert.ERROR
)
return False
self.theParent.setStatus("Project backed up to '%s.zip'" % baseName)
self.theParent.setStatus(self.tr("Project backed up to '{0}'").format(f"{baseName}.zip"))
return True
@@ -865,7 +865,7 @@ class NWProject():
isSuccess = True
except Exception as e:
self.makeAlert(
["Failed to create a new example project.", str(e)], nwAlert.ERROR
[self.tr("Failed to create a new example project."), str(e)], nwAlert.ERROR
)
elif os.path.isdir(srcSample):
@@ -887,14 +887,16 @@ class NWProject():
except Exception as e:
self.makeAlert(
["Failed to create a new example project.", str(e)], nwAlert.ERROR
[self.tr("Failed to create a new example project."), str(e)], nwAlert.ERROR
)
else:
self.makeAlert((
"Failed to create a new example project. Could not find the "
"necessary files. They seem to be missing from this installation."
), nwAlert.ERROR)
self.makeAlert(
self.tr(
"Failed to create a new example project. Could not find the "
"necessary files. They seem to be missing from this installation."
), nwAlert.ERROR
)
if isSuccess:
self.clearProject()
@@ -925,16 +927,18 @@ class NWProject():
logger.debug("Created folder %s" % projPath)
except Exception as e:
self.theParent.makeAlert((
["Could not create new project folder.", str(e)]
[self.tr("Could not create new project folder."), str(e)]
), nwAlert.ERROR)
return False
if os.path.isdir(projPath):
if os.listdir(self.projPath):
self.theParent.makeAlert((
"New project folder is not empty. "
"Each project requires a dedicated project folder."
), nwAlert.ERROR)
self.theParent.makeAlert(
self.tr(
"New project folder is not empty. "
"Each project requires a dedicated project folder."
), nwAlert.ERROR
)
return False
self.ensureFolderStructure()
@@ -981,17 +985,21 @@ class NWProject():
self.doBackup = doBackup
if doBackup:
if not os.path.isdir(self.mainConf.backupPath):
self.theParent.makeAlert((
"You must set a valid backup path in preferences to use "
"the automatic project backup feature."
), nwAlert.WARN)
self.theParent.makeAlert(
self.tr(
"You must set a valid backup path in Preferences to use "
"the automatic project backup feature."
), nwAlert.WARN
)
return False
if self.projName == "":
self.theParent.makeAlert((
"You must set a valid project name in project settings to "
"use the automatic project backup feature."
), nwAlert.WARN)
self.theParent.makeAlert(
self.tr(
"You must set a valid project name in Project Settings to "
"use the automatic project backup feature."
), nwAlert.WARN
)
return False
return True
@@ -1008,8 +1016,18 @@ class NWProject():
"""Set the project-specific spell check language.
"""
theLang = checkString(theLang, None, True)
if self.projSpell != theLang:
self.projSpell = theLang
self.setProjectChanged(True)
return True
def setProjectLang(self, theLang):
"""Set the project-specific language.
"""
theLang = checkString(theLang, None, True)
if self.projLang != theLang:
self.projLang = theLang
self._loadProjectLocalisation()
self.setProjectChanged(True)
return True
@@ -1120,8 +1138,8 @@ class NWProject():
if nAuth == 1:
authString = self.bookAuthors[0]
elif nAuth > 1:
authString = "%s and %s" % (
", ".join(self.bookAuthors[0:-1]), self.bookAuthors[-1]
authString = "%s %s %s" % (
", ".join(self.bookAuthors[0:-1]), self.tr("and"), self.bookAuthors[-1]
)
return authString
@@ -1197,10 +1215,47 @@ class NWProject():
self.importItems.countEntry(nwItem.itemStatus)
return
def localLookup(self, theWord):
"""Look up a word in the translation map for the project and
return it. The variable is cast to a string before lookup. If
the word does not exist, it returns itself.
"""
theValue = str(theWord)
return self.langData.get(theValue, theValue)
##
# Internal Functions
##
def _loadProjectLocalisation(self):
"""Load the language data for the current project language.
"""
if self.projLang is None:
self.langData = {}
return False
lngShort = self.projLang.split("_")[0]
loadFile = os.path.join(self.mainConf.nwLangPath, "project_en.json")
chkFile1 = os.path.join(self.mainConf.nwLangPath, "project_%s.json" % self.projLang)
chkFile2 = os.path.join(self.mainConf.nwLangPath, "project_%s.json" % lngShort)
if os.path.isfile(chkFile1):
loadFile = chkFile1
elif os.path.isfile(chkFile2):
loadFile = chkFile2
try:
with open(loadFile, mode="r", encoding="utf8") as inFile:
self.langData = json.load(inFile)
logger.debug("Loaded project language file: %s" % os.path.basename(loadFile))
except Exception:
logger.error("Failed to project language file")
nw.logException()
return False
return True
def _readLockFile(self):
"""Reads the lock file in the project folder.
"""
@@ -1330,7 +1385,7 @@ class NWProject():
# Report status
if len(orphanFiles) > 0:
self.makeAlert(
"Found %d orphaned file(s) in project folder." % len(orphanFiles),
self.tr("Found {0} orphaned file(s) in project folder.").format(len(orphanFiles)),
nwAlert.WARN
)
else:
@@ -1341,6 +1396,7 @@ class NWProject():
aDoc = NWDoc(self, self.theParent)
nOrph = 0
noWhere = False
oPrefix = self.tr("Recovered")
for oHandle in orphanFiles:
# Look for meta data
@@ -1352,10 +1408,12 @@ class NWProject():
oName, oParent, oClass, oLayout = aDoc.getMeta()
if oName:
oName = "Recovered: %s" % oName.lstrip("Recovered: ")
oName = self.tr("[{0}] {1}").format(
oPrefix, oName.replace("[%s]" % oPrefix, "").strip()
)
else:
nOrph += 1
oName = "Recovered File %d" % nOrph
oName = self.tr("Recovered File {0}").format(nOrph)
# Recover file meta data
if oClass is None:
@@ -1382,10 +1440,12 @@ class NWProject():
self.projTree.append(oHandle, oParent, orphItem)
if noWhere:
self.makeAlert((
"One or more orphaned files could not be added back into the "
"project. Make sure at least a Novel root folder exists."
), nwAlert.WARN)
self.makeAlert(
self.tr(
"One or more orphaned files could not be added back into the "
"project. Make sure at least a Novel root folder exists."
), nwAlert.WARN
)
return True
@@ -1441,7 +1501,7 @@ class NWProject():
"""
theData = os.path.join(self.projPath, theFolder)
if not os.path.isdir(theData):
errList.append("Not a folder: %s" % theData)
errList.append(self.tr("Not a folder: {0}").format(theData))
return errList
logger.info("Old data folder %s found" % theFolder)
@@ -1464,7 +1524,7 @@ class NWProject():
logger.info("Moved file: %s" % theFile)
logger.info("New location: %s" % newPath)
except Exception:
errList.append("Could not move: %s" % theFile)
errList.append(self.tr("Could not move: {0}").format(theFile))
logger.error("Could not move: %s" % theFile)
nw.logException()
@@ -1473,7 +1533,7 @@ class NWProject():
os.unlink(theFile)
logger.info("Deleted file: %s" % theFile)
except Exception:
errList.append("Could not delete: %s" % theFile)
errList.append(self.tr("Could not delete: {0}").format(theFile))
logger.error("Could not delete: %s" % theFile)
nw.logException()
@@ -1486,10 +1546,10 @@ class NWProject():
# ==================
try:
os.rmdir(theData)
logger.info("Removed folder: %s" % theFolder)
logger.info("Deleted folder: %s" % theFolder)
except Exception:
errList.append("Failed to remove: %s" % theFolder)
logger.error("Failed to remove: %s" % theFolder)
errList.append(self.tr("Could not delete: {0}").format(theFolder))
logger.error("Could not delete: %s" % theFolder)
nw.logException()
return errList
@@ -1500,7 +1560,7 @@ class NWProject():
"""
theJunk = os.path.join(self.projPath, "junk")
if not self._checkFolder(theJunk):
return "Could not make folder: %s" % theJunk
return self.tr("Could not make folder: {0}").format(theJunk)
theSrc = os.path.join(theDir, theItem)
theDst = os.path.join(theJunk, theItem)
@@ -1511,7 +1571,7 @@ class NWProject():
except Exception:
logger.error("Could not move item %s to junk." % theSrc)
nw.logException()
return "Could not move item %s to junk." % theSrc
return self.tr("Could not move item {0} to {1}.").format(theSrc, theJunk)
return ""

View File

@@ -29,8 +29,6 @@ import logging
import os
import difflib
from nw.constants import nwConst, isoLanguage
logger = logging.getLogger(__name__)
# =============================================================================================== #
@@ -89,16 +87,6 @@ class NWSpellCheck():
"""
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,26 +96,27 @@ 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
@@ -196,10 +185,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):
@@ -332,8 +321,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 +329,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

@@ -27,7 +27,7 @@ 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__)
@@ -406,17 +406,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

@@ -28,11 +28,14 @@ import logging
import re
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__)
@@ -141,6 +144,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
##
@@ -249,7 +259,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
@@ -278,10 +288,10 @@ class Tokenizer():
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
@@ -653,11 +663,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

@@ -26,8 +26,8 @@ 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__)
@@ -77,7 +77,7 @@ class ToMarkdown(Tokenizer):
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 : "_",
@@ -134,10 +134,10 @@ class ToMarkdown(Tokenizer):
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))

View File

@@ -29,11 +29,11 @@ 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__)
@@ -298,7 +298,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__}"
@@ -507,15 +507,17 @@ 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):
@@ -530,7 +532,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 +563,7 @@ class ToOdt(Tokenizer):
##
if len(theText) != len(theFmt):
# Genrate dummy format if there isn't any
# Generate dummy format if there isn't any or it doesn't match
theFmt = " "*len(theText)
# XML functions

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

@@ -29,14 +29,13 @@ import logging
import os
from lxml import etree
from hashlib import sha256
from time import time
from hashlib import sha256
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__)

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

@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
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

@@ -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,92 @@ 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"),
])
)
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)
@@ -225,7 +236,7 @@ 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):
@@ -233,9 +244,19 @@ class GuiAbout(QDialog):
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,6 +267,9 @@ 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],

View File

@@ -33,9 +33,9 @@ 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__)
@@ -53,11 +53,11 @@ class GuiDocMerge(QDialog):
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,9 +102,9 @@ class GuiDocMerge(QDialog):
finalOrder.append(self.listBox.item(i).data(Qt.UserRole))
if len(finalOrder) == 0:
self.theParent.makeAlert((
"No source documents found. Nothing to do."
), nwAlert.ERROR)
self.theParent.makeAlert(
self.tr("No source documents found. Nothing to do."), nwAlert.ERROR
)
return
theDoc = NWDoc(self.theProject, self.theParent)
@@ -114,16 +114,16 @@ class GuiDocMerge(QDialog):
theText += "\n\n"
if self.sourceItem is None:
self.theParent.makeAlert((
"No source document selected. Nothing to do."
), nwAlert.ERROR)
self.theParent.makeAlert(
self.tr("No source document selected. Nothing to do."), nwAlert.ERROR
)
return
srcItem = self.theProject.projTree[self.sourceItem]
if srcItem is None:
self.theParent.makeAlert((
"Could not parse source document."
), nwAlert.ERROR)
self.theParent.makeAlert(
self.tr("Could not parse source document."), nwAlert.ERROR
)
return
nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemClass, srcItem.itemParent)
@@ -164,9 +164,9 @@ class GuiDocMerge(QDialog):
if nwItem is None:
return
if nwItem.itemType is not nwItemType.FOLDER:
self.theParent.makeAlert((
"Element selected in the project tree must be a folder."
), nwAlert.ERROR)
self.theParent.makeAlert(
self.tr("Element selected in the project tree must be a folder."), nwAlert.ERROR
)
return
for sHandle in self.theParent.treeView.getTreeFromHandle(tHandle):

View File

@@ -34,10 +34,9 @@ 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__)
@@ -56,11 +55,12 @@ class GuiDocSplit(QDialog):
self.sourceItem = None
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 +69,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,16 +115,16 @@ class GuiDocSplit(QDialog):
logger.verbose("GuiDocSplit split button clicked")
if self.sourceItem is None:
self.theParent.makeAlert((
"No source document selected. Nothing to do."
), nwAlert.ERROR)
self.theParent.makeAlert(
self.tr("No source document selected. Nothing to do."), nwAlert.ERROR
)
return
srcItem = self.theProject.projTree[self.sourceItem]
if srcItem is None:
self.theParent.makeAlert((
"Could not parse source document."
), nwAlert.ERROR)
self.theParent.makeAlert(
self.tr("Could not parse source document."), nwAlert.ERROR
)
return
theDoc = NWDoc(self.theProject, self.theParent)
@@ -147,26 +147,34 @@ class GuiDocSplit(QDialog):
nFiles = len(finalOrder)
if nFiles == 0:
self.theParent.makeAlert((
"No headers found. Nothing to do."
), nwAlert.ERROR)
self.theParent.makeAlert(
self.tr("No headers found. Nothing to do."), nwAlert.ERROR
)
return
# Check that another folder can be created
parTree = self.theProject.projTree.getItemPath(srcItem.itemParent)
if len(parTree) >= nwConst.MAX_DEPTH - 1:
self.theParent.makeAlert((
"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)
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
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
@@ -242,9 +250,9 @@ class GuiDocSplit(QDialog):
if nwItem is None:
return
if nwItem.itemType is not nwItemType.FILE:
self.theParent.makeAlert((
"Element selected in the project tree must be a file."
), nwAlert.ERROR)
self.theParent.makeAlert(
self.tr("Element selected in the project tree must be a file."), nwAlert.ERROR
)
return
self.listBox.clear()

View File

@@ -33,8 +33,9 @@ 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__)
@@ -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)

File diff suppressed because it is too large Load Diff

View File

@@ -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,8 +104,8 @@ 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.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)
@@ -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

@@ -27,15 +27,15 @@ 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, QLocale
from PyQt5.QtGui import QIcon, QPixmap, QColor, QBrush
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__)
@@ -54,7 +54,7 @@ class GuiProjectSettings(PagedDialog):
self.optState = 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,10 +143,14 @@ 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
@@ -166,69 +170,69 @@ 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)
self.spellLang.setMaximumWidth(xW)
theDict = self.theParent.docEditor.theDict
self.spellLang.addItem("Default", "None")
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
@@ -237,17 +241,30 @@ class GuiProjectEditMain(QWidget):
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 +273,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 +367,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 +380,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,14 +391,14 @@ 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.tr("Cannot delete a status item that is in use."), nwAlert.ERROR
)
return
@@ -364,7 +407,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 +415,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 +428,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 +444,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 +454,7 @@ class GuiProjectEditStatus(QWidget):
self.editName.setEnabled(True)
self.editName.selectAll()
self.editName.setFocus()
return
##
@@ -423,16 +470,28 @@ 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 +502,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 +592,7 @@ class GuiProjectEditReplace(QWidget):
aVal = tItem.text(1)
if len(aKey) > 0:
newList[aKey] = aVal
return newList
##
@@ -538,8 +628,8 @@ class GuiProjectEditReplace(QWidget):
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 @@
# -*- coding: utf-8 -*-
"""
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

@@ -34,7 +34,8 @@ 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__)
@@ -52,7 +53,7 @@ class GuiWordList(QDialog):
self.theProject = theProject
self.optState = 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,12 +122,13 @@ 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.tr("The word '{0}' is already in the word list.").format(newWord),
nwAlert.ERROR
)
return False

View File

@@ -81,19 +81,14 @@ 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
# END Enum nwDocAction
@@ -117,6 +112,15 @@ class nwAlert(Enum):
# END Enum nwAlert
class nwWidget(Enum):
TREE = 1
EDITOR = 2
VIEWER = 3
OUTLINE = 4
# END Enum nwWidget
class nwOutline(Enum):
TITLE = 0

View File

@@ -1,50 +1,28 @@
# -*- coding: utf-8 -*-
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

@@ -29,17 +29,17 @@ 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__)
@@ -61,6 +61,7 @@ class QConfigLayout(QGridLayout):
wSp = nw.CONFIG.pxInt(8)
self.setHorizontalSpacing(wSp)
self.setVerticalSpacing(wSp)
self.setColumnStretch(0, 1)
return
@@ -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)
@@ -207,6 +216,9 @@ class QHelpLabel(QLabel):
lblFont.setPointSizeF(fontSize*lblFont.pointSizeF())
self.setFont(lblFont)
self.setWordWrap(True)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
return
# END Class QHelpLabel
@@ -409,6 +421,7 @@ 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 +429,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 +460,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

View File

@@ -50,12 +50,10 @@ from PyQt5.QtWidgets import (
)
from nw.core import NWDoc, NWSpellSimple, countWords
from nw.gui.dochighlight import GuiDocHighlighter
from nw.enum import nwAlert, nwDocAction, nwDocInsert, nwItemClass
from nw.common import transferCase
from nw.constants import (
nwConst, nwAlert, nwUnicode, nwDocAction, nwDocInsert, nwItemClass,
nwKeyWords, nwLabels
)
from nw.constants import nwConst, trConst, nwKeyWords, nwLabels, nwUnicode
from nw.gui.dochighlight import GuiDocHighlighter
logger = logging.getLogger(__name__)
@@ -98,10 +96,12 @@ class GuiDocEditor(QTextEdit):
self.queuePos = None # Used for delayed change of cursor position
# Typography
self.typDQOpen = self.mainConf.fmtDoubleQuotes[0]
self.typDQClose = self.mainConf.fmtDoubleQuotes[1]
self.typSQOpen = self.mainConf.fmtSingleQuotes[0]
self.typSQClose = self.mainConf.fmtSingleQuotes[1]
self.typDQOpen = '"'
self.typDQClose = '"'
self.typSQOpen = "'"
self.typSQClose = "'"
self.typPadChar = " "
self.addPadding = False
# Core Elements and Signals
self.qDocument = self.document()
@@ -197,6 +197,17 @@ class GuiDocEditor(QTextEdit):
self.nonWord += "".join(self.mainConf.fmtDoubleQuotes)
self.nonWord += "".join(self.mainConf.fmtSingleQuotes)
# Typography
if self.mainConf.fmtPadThin:
self.typPadChar = nwUnicode.U_THNBSP
else:
self.typPadChar = nwUnicode.U_NBSP
self.typSQOpen = self.mainConf.fmtSingleQuotes[0]
self.typSQClose = self.mainConf.fmtSingleQuotes[1]
self.typDQOpen = self.mainConf.fmtDoubleQuotes[0]
self.typDQClose = self.mainConf.fmtDoubleQuotes[1]
# Reload spell check and dictionaries
self._setupSpellChecking()
self.setDictionaries()
@@ -206,6 +217,7 @@ class GuiDocEditor(QTextEdit):
if self.mainConf.textFont is None:
# If none is defined, set the default back to config
self.mainConf.textFont = self.qDocument.defaultFont().family()
theFont.setFamily(self.mainConf.textFont)
theFont.setPointSize(self.mainConf.textSize)
self.setFont(theFont)
@@ -293,11 +305,17 @@ class GuiDocEditor(QTextEdit):
docSize = len(theDoc)
if docSize > nwConst.MAX_DOCSIZE:
self.theParent.makeAlert((
"The document you are trying to open is too big. "
"The document size is %.2f\u202fMB. "
"The maximum size allowed is %.2f\u202fMB."
) % (docSize/1.0e6, nwConst.MAX_DOCSIZE/1.0e6), nwAlert.ERROR)
self.theParent.makeAlert(
self.tr(
"The document you are trying to open is too big. "
"The document size is {0} MB. "
"The maximum size allowed is {1} MB."
).format(
f"{docSize/1.0e6:.2f}",
f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}"
),
nwAlert.ERROR
)
self.clearEditor()
return False
@@ -380,11 +398,17 @@ class GuiDocEditor(QTextEdit):
"""
docSize = len(theText)
if docSize > nwConst.MAX_DOCSIZE:
self.theParent.makeAlert((
"The text you are trying to add is too big. "
"The text size is %.2f\u202fMB. "
"The maximum size allowed is %.2f\u202fMB."
) % (docSize/1.0e6, nwConst.MAX_DOCSIZE/1.0e6), nwAlert.ERROR)
self.theParent.makeAlert(
self.tr(
"The text you are trying to add is too big. "
"The text size is {0} MB. "
"The maximum size allowed is {1} MB."
).format(
f"{docSize/1.0e6:.2f}",
f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}"
),
nwAlert.ERROR
)
return False
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
@@ -568,12 +592,14 @@ class GuiDocEditor(QTextEdit):
"""
if not isinstance(theLine, int):
return False
if theLine >= 0:
theBlock = self.qDocument.findBlockByLineNumber(theLine)
if theBlock:
self.setCursorPosition(theBlock.position())
self.docFooter.updateLineCount()
logger.verbose("Cursor moved to line %d" % theLine)
return True
##
@@ -585,17 +611,15 @@ class GuiDocEditor(QTextEdit):
status bar to show the one actually loaded by the spell checker
class.
"""
if self.theProject.projLang is None:
if self.theProject.projSpell is None:
theLang = self.mainConf.spellLanguage
else:
theLang = self.theProject.projLang
theLang = self.theProject.projSpell
self.theDict.setLanguage(theLang, self.theProject.projDict)
theTag, theProvider = self.theDict.describeDict()
aLang, aName = self.theDict.describeDict()
self.theParent.statusBar.setLanguage(
aLang, "%s [%s]" % (self.mainConf.spellTool.title(), aName.title())
)
self.theParent.statusBar.setLanguage(theLang, theProvider)
if not self.bigDoc:
self.spellCheckDocument()
@@ -641,10 +665,8 @@ class GuiDocEditor(QTextEdit):
self.hLight.rehighlight()
qApp.restoreOverrideCursor()
afTime = time()
logger.debug(
"Document highlighted in %.3f ms" % (1000*(afTime-bfTime))
)
self.theParent.statusBar.showMessage("Spell check complete")
logger.debug("Document highlighted in %.3f ms" % (1000*(afTime-bfTime)))
self.theParent.statusBar.showMessage(self.tr("Spell check complete"))
return True
@@ -660,10 +682,6 @@ class GuiDocEditor(QTextEdit):
this class when calling these actions from other classes.
"""
logger.verbose("Requesting action: %s" % theAction.name)
if not self.hasFocus():
logger.verbose("Editor does not have focus")
return False
if self.theHandle is None:
logger.error("No document open")
return False
@@ -693,16 +711,6 @@ class GuiDocEditor(QTextEdit):
self._makeSelection(QTextCursor.Document)
elif theAction == nwDocAction.SEL_PARA:
self._makeSelection(QTextCursor.BlockUnderCursor)
elif theAction == nwDocAction.FIND:
self._beginSearch()
elif theAction == nwDocAction.REPLACE:
self._beginReplace()
elif theAction == nwDocAction.GO_NEXT:
self._findNext()
elif theAction == nwDocAction.GO_PREV:
self._findNext(isBackward=True)
elif theAction == nwDocAction.REPL_NEXT:
self._replaceNext()
elif theAction == nwDocAction.BLOCK_H1:
self._formatBlock(nwDocAction.BLOCK_H1)
elif theAction == nwDocAction.BLOCK_H2:
@@ -734,6 +742,15 @@ class GuiDocEditor(QTextEdit):
"""
return self.qDocument.isEmpty()
def anyFocus(self):
"""Check if any widget or child widget has focus.
"""
if self.hasFocus():
return True
if self.isAncestorOf(qApp.focusWidget()):
return True
return False
def revealLocation(self):
"""Tell the user where on the file system the file in the editor
is saved.
@@ -743,14 +760,14 @@ class GuiDocEditor(QTextEdit):
return False
msgBox = QMessageBox()
msgBox.information(self, "File Location", (
"File details for the currently open file<br>"
"Handle: {handle:s}<br>"
"Location: {fileLoc:s}"
).format(
handle = self.theHandle,
fileLoc = str(self.nwDocument.getFileLocation())
))
msgBox.information(
self,
self.tr("File Location"),
"%s<br>%s" % (
self.tr("The currently open file is saved in:"),
self.nwDocument.getFileLocation()
),
)
return
@@ -799,7 +816,7 @@ class GuiDocEditor(QTextEdit):
theCursor = self.textCursor()
theBlock = theCursor.block()
if not theBlock.isValid():
logger.error("Filed to insert keyword '%s'" % keyWord)
logger.error("Failed to insert keyword '%s'" % keyWord)
return False
theCursor.beginEditBlock()
@@ -827,7 +844,7 @@ class GuiDocEditor(QTextEdit):
if self.docSearch.isVisible():
self.docSearch.closeSearch()
else:
self._beginSearch()
self.beginSearch()
return
##
@@ -933,10 +950,15 @@ class GuiDocEditor(QTextEdit):
self.lastFind = None
if self.qDocument.characterCount() > nwConst.MAX_DOCSIZE:
self.theParent.makeAlert((
"The document has grown too big and you cannot add more text to it. "
"The maximum size of a single novelWriter document is %.2f\u202fMB."
) % (nwConst.MAX_DOCSIZE/1.0e6), nwAlert.ERROR)
self.theParent.makeAlert(
self.tr(
"The document has grown too big and you cannot add more text to it. "
"The maximum size of a single novelWriter document is {0} MB."
).format(
f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}"
),
nwAlert.ERROR
)
self.undo()
return
@@ -966,21 +988,21 @@ class GuiDocEditor(QTextEdit):
# ===========================
if self._followTag(theCursor=posCursor, loadTag=False):
mnuTag = QAction("Follow Tag", mnuContext)
mnuTag = QAction(self.tr("Follow Tag"), mnuContext)
mnuTag.triggered.connect(lambda: self._followTag(theCursor=posCursor))
mnuContext.addAction(mnuTag)
mnuContext.addSeparator()
if userSelection:
mnuCut = QAction("Cut", mnuContext)
mnuCut = QAction(self.tr("Cut"), mnuContext)
mnuCut.triggered.connect(lambda: self.docAction(nwDocAction.CUT))
mnuContext.addAction(mnuCut)
mnuCopy = QAction("Copy", mnuContext)
mnuCopy = QAction(self.tr("Copy"), mnuContext)
mnuCopy.triggered.connect(lambda: self.docAction(nwDocAction.COPY))
mnuContext.addAction(mnuCopy)
mnuPaste = QAction("Paste", mnuContext)
mnuPaste = QAction(self.tr("Paste"), mnuContext)
mnuPaste.triggered.connect(lambda: self.docAction(nwDocAction.PASTE))
mnuContext.addAction(mnuPaste)
@@ -989,17 +1011,17 @@ class GuiDocEditor(QTextEdit):
# 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)
)
@@ -1025,7 +1047,7 @@ class GuiDocEditor(QTextEdit):
if spellCheck:
mnuContext.addSeparator()
mnuHead = QAction("Spelling Suggestion(s)", mnuContext)
mnuHead = QAction(self.tr("Spelling Suggestion(s)"), mnuContext)
mnuContext.addAction(mnuHead)
theSuggest = self.theDict.suggestWords(theWord)[:15]
@@ -1037,11 +1059,13 @@ class GuiDocEditor(QTextEdit):
)
mnuContext.addAction(mnuWord)
else:
mnuHead = QAction("%s No Suggestions" % nwUnicode.U_ENDASH, mnuContext)
mnuHead = QAction(
"%s %s" % (nwUnicode.U_ENDASH, self.tr("No Suggestions")), mnuContext
)
mnuContext.addAction(mnuHead)
mnuContext.addSeparator()
mnuAdd = QAction("Add Word to Dictionary", mnuContext)
mnuAdd = QAction(self.tr("Add Word to Dictionary"), mnuContext)
mnuAdd.triggered.connect(lambda thePos: self._addWord(posCursor))
mnuContext.addAction(mnuAdd)
@@ -1131,15 +1155,149 @@ class GuiDocEditor(QTextEdit):
QPointF(theSize.width(), theSize.height()), Qt.FuzzyHit
)
if self.queuePos <= thePos:
logger.verbose(
"Allowed cursor move to %d <= %d" % (self.queuePos, thePos)
)
logger.verbose("Allowed cursor move to %d <= %d" % (self.queuePos, thePos))
self.setCursorPosition(self.queuePos)
self.queuePos = None
else:
logger.verbose(
"Denied cursor move to %d > %d" % (self.queuePos, thePos)
logger.verbose("Denied cursor move to %d > %d" % (self.queuePos, thePos))
return
##
# Search & Replace
##
def beginSearch(self):
"""Sets the selected text as the search text for the search bar.
"""
theCursor = self.textCursor()
if theCursor.hasSelection():
self.docSearch.setSearchText(theCursor.selectedText())
else:
self.docSearch.setSearchText(None)
self.updateDocMargins()
return
def beginReplace(self):
"""Opens the replace line of the search bar and sets the find
text if a selection has been made, and resets the replace text.
"""
theCursor = self.textCursor()
if theCursor.hasSelection():
self.docSearch.setSearchText(theCursor.selectedText())
else:
self.docSearch.setSearchText(None)
self.docSearch.setReplaceText("")
self.updateDocMargins()
return
def findNext(self, goBack=False):
"""Searches for the next or previous occurrence of the search
bar text in the document. Wraps around if not found and loop is
enabled, or continues to next file if next file is enabled.
"""
if not self.anyFocus():
logger.debug("Editor does not have focus")
return False
if not self.docSearch.isVisible():
self.beginSearch()
return
findOpt = QTextDocument.FindFlag(0)
if goBack:
findOpt |= QTextDocument.FindBackward
if self.docSearch.isCaseSense:
findOpt |= QTextDocument.FindCaseSensitively
if self.docSearch.isWholeWord:
findOpt |= QTextDocument.FindWholeWords
searchFor = self.docSearch.getSearchObject()
wasFound = self.find(searchFor, findOpt)
if not wasFound:
if self.docSearch.doNextFile and not goBack:
self.theParent.openNextDocument(
self.theHandle, wrapAround=self.docSearch.doLoop
)
elif self.docSearch.doLoop:
theCursor = self.textCursor()
theCursor.movePosition(
QTextCursor.End if goBack else QTextCursor.Start
)
self.setTextCursor(theCursor)
wasFound = self.find(searchFor, findOpt)
if wasFound:
theCursor = self.textCursor()
self.lastFind = (theCursor.selectionStart(), theCursor.selectionEnd())
return
def replaceNext(self):
"""Searches for the next occurrence of the search bar text in
the document and replaces it with the replace text. Calls search
next automatically when done.
"""
if not self.anyFocus():
logger.debug("Editor does not have focus")
return False
if not self.docSearch.isVisible():
# The search tool is not active, so we activate it.
self.beginSearch()
return
theCursor = self.textCursor()
if not theCursor.hasSelection():
# We have no text selected at all, so just make this a
# regular find next call.
self.findNext()
return
if self.lastFind is None and theCursor.hasSelection():
# If we have a selection but no search, it may have been the
# text we triggered the search with, in which case we search
# again from the beginning of that selection to make sure we
# have a valid result.
sPos = theCursor.selectionStart()
theCursor.clearSelection()
theCursor.setPosition(sPos)
self.setTextCursor(theCursor)
self.findNext()
theCursor = self.textCursor()
if self.lastFind is None:
# In case the above didn't find a result, we give up here.
return
searchFor = self.docSearch.getSearchText()
replWith = self.docSearch.getReplaceText()
if self.docSearch.doMatchCap:
replWith = transferCase(theCursor.selectedText(), replWith)
# Make sure the selected text was selected by an actual find
# call, and not the user.
try:
isFind = self.lastFind[0] == theCursor.selectionStart()
isFind &= self.lastFind[1] == theCursor.selectionEnd()
except Exception:
isFind = False
if isFind:
theCursor.beginEditBlock()
theCursor.removeSelectedText()
theCursor.insertText(replWith)
theCursor.endEditBlock()
theCursor.setPosition(theCursor.selectionEnd())
self.setTextCursor(theCursor)
logger.verbose("Replaced occurrence of '%s' with '%s' on line %d" % (
searchFor, replWith, theCursor.blockNumber()
))
else:
logger.error("The selected text is not a search result, skipping replace")
self.findNext()
return
##
@@ -1189,7 +1347,7 @@ class GuiDocEditor(QTextEdit):
return
def _docAutoReplace(self, theBlock):
"""Autoreplace text elements based on main configuration.
"""Auto-replace text elements based on main configuration.
"""
if not theBlock.isValid():
return
@@ -1206,43 +1364,62 @@ class GuiDocEditor(QTextEdit):
theTwo = theText[thePos-2:thePos]
theThree = theText[thePos-3:thePos]
if self.mainConf.doReplaceDQuote and theTwo == " \"":
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1)
theCursor.insertText(self.typDQOpen)
if not theOne: # Makes Neo sad
return
elif self.mainConf.doReplaceDQuote and theOne == "\"":
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1)
nDelete = 0
tInsert = theOne
if self.mainConf.doReplaceDQuote and theTwo == ' "':
nDelete = 1
tInsert = self.typDQOpen
elif self.mainConf.doReplaceDQuote and theOne == '"':
nDelete = 1
if thePos == 1:
theCursor.insertText(self.typDQOpen)
tInsert = self.typDQOpen
else:
theCursor.insertText(self.typDQClose)
tInsert = self.typDQClose
elif self.mainConf.doReplaceSQuote and theTwo == " '":
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1)
theCursor.insertText(self.typSQOpen)
nDelete = 1
tInsert = self.typSQOpen
elif self.mainConf.doReplaceSQuote and theOne == "'":
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1)
nDelete = 1
if thePos == 1:
theCursor.insertText(self.typSQOpen)
tInsert = self.typSQOpen
else:
theCursor.insertText(self.typSQClose)
tInsert = self.typSQClose
elif self.mainConf.doReplaceDash and theThree == "---":
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 3)
theCursor.insertText(nwUnicode.U_EMDASH)
nDelete = 3
tInsert = nwUnicode.U_EMDASH
elif self.mainConf.doReplaceDash and theTwo == "--":
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 2)
theCursor.insertText(nwUnicode.U_ENDASH)
nDelete = 2
tInsert = nwUnicode.U_ENDASH
elif self.mainConf.doReplaceDash and theTwo == nwUnicode.U_ENDASH+"-":
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 2)
theCursor.insertText(nwUnicode.U_EMDASH)
elif self.mainConf.doReplaceDash and theTwo == nwUnicode.U_ENDASH + "-":
nDelete = 2
tInsert = nwUnicode.U_EMDASH
elif self.mainConf.doReplaceDots and theThree == "...":
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 3)
theCursor.insertText(nwUnicode.U_HELLIP)
nDelete = 3
tInsert = nwUnicode.U_HELLIP
tCheck = tInsert
if tCheck in self.mainConf.fmtPadBefore:
nDelete = max(nDelete, 1)
tInsert = self.typPadChar + tInsert
if tCheck in self.mainConf.fmtPadAfter:
nDelete = max(nDelete, 1)
tInsert = tInsert + self.typPadChar
if nDelete > 0:
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, nDelete)
theCursor.insertText(tInsert)
return
@@ -1317,7 +1494,8 @@ class GuiDocEditor(QTextEdit):
else:
self.theParent.makeAlert(
"Please selection some text before calling replace quotes.", nwAlert.ERROR
self.tr("Please select some text before calling replace quotes."),
nwAlert.ERROR
)
return
@@ -1530,9 +1708,7 @@ class GuiDocEditor(QTextEdit):
theText = newText
cOffset -= 0
else:
logger.error(
"Unknown or unsupported block format requested: %s" % str(docAction)
)
logger.error("Unknown or unsupported block format requested: %s" % str(docAction))
return False
# Replace the block text
@@ -1585,132 +1761,6 @@ class GuiDocEditor(QTextEdit):
self._makeSelection(selMode)
return
def _beginSearch(self):
"""Sets the selected text as the search text for the search bar.
"""
theCursor = self.textCursor()
if theCursor.hasSelection():
self.docSearch.setSearchText(theCursor.selectedText())
else:
self.docSearch.setSearchText(None)
self.updateDocMargins()
return
def _beginReplace(self):
"""Opens the replace line of the search bar and sets the find
text if a selection has been made, and resets the replace text.
"""
theCursor = self.textCursor()
if theCursor.hasSelection():
self.docSearch.setSearchText(theCursor.selectedText())
else:
self.docSearch.setSearchText(None)
self.docSearch.setReplaceText("")
self.updateDocMargins()
return
def _findNext(self, isBackward=False):
"""Searches for the next or previous occurrence of the search
bar text in the document. Wraps around if not found and loop is
enabled, or continues to next file if next file is enabled.
"""
if not self.docSearch.isVisible():
self._beginSearch()
return
findOpt = QTextDocument.FindFlag(0)
if isBackward:
findOpt |= QTextDocument.FindBackward
if self.docSearch.isCaseSense:
findOpt |= QTextDocument.FindCaseSensitively
if self.docSearch.isWholeWord:
findOpt |= QTextDocument.FindWholeWords
searchFor = self.docSearch.getSearchObject()
wasFound = self.find(searchFor, findOpt)
if not wasFound:
if self.docSearch.doNextFile and not isBackward:
self.theParent.openNextDocument(
self.theHandle, wrapAround=self.docSearch.doLoop
)
elif self.docSearch.doLoop:
theCursor = self.textCursor()
theCursor.movePosition(
QTextCursor.End if isBackward else QTextCursor.Start
)
self.setTextCursor(theCursor)
wasFound = self.find(searchFor, findOpt)
if wasFound:
theCursor = self.textCursor()
self.lastFind = (theCursor.selectionStart(), theCursor.selectionEnd())
return
def _replaceNext(self):
"""Searches for the next occurrence of the search bar text in
the document and replaces it with the replace text. Calls search
next automatically when done.
"""
if not self.docSearch.isVisible():
# The search tool is not active, so we activate it.
self._beginSearch()
return
theCursor = self.textCursor()
if not theCursor.hasSelection():
# We have no text selected at all, so just make this a
# regular find next call.
self._findNext()
return
if self.lastFind is None and theCursor.hasSelection():
# If we have a selection but no search, it may have been the
# text we triggered the search with, in which case we search
# again from the beginning of that selection to make sure we
# have a valid result.
sPos = theCursor.selectionStart()
theCursor.clearSelection()
theCursor.setPosition(sPos)
self.setTextCursor(theCursor)
self._findNext()
theCursor = self.textCursor()
if self.lastFind is None:
# In case the above didn't find a result, we give up here.
return
searchFor = self.docSearch.getSearchText()
replWith = self.docSearch.getReplaceText()
if self.docSearch.doMatchCap:
replWith = transferCase(theCursor.selectedText(), replWith)
# Make sure the selected text was selected by an actual find
# call, and not the user.
try:
isFind = self.lastFind[0] == theCursor.selectionStart()
isFind &= self.lastFind[1] == theCursor.selectionEnd()
except Exception:
isFind = False
if isFind:
theCursor.beginEditBlock()
theCursor.removeSelectedText()
theCursor.insertText(replWith)
theCursor.endEditBlock()
theCursor.setPosition(theCursor.selectionEnd())
self.setTextCursor(theCursor)
logger.verbose("Replaced occurrence of '%s' with '%s' on line %d" % (
searchFor, replWith, theCursor.blockNumber()
))
else:
logger.error("The selected text is not a search result, skipping replace")
self._findNext()
return
def _setupSpellChecking(self):
"""Create the spell checking object based on the spellTool
setting in config.
@@ -1817,12 +1867,12 @@ class GuiDocEditSearch(QFrame):
# ==========
self.searchBox = QLineEdit(self)
self.searchBox.setFont(boxFont)
self.searchBox.setPlaceholderText("Search")
self.searchBox.setPlaceholderText(self.tr("Search"))
self.searchBox.returnPressed.connect(self._doSearch)
self.replaceBox = QLineEdit(self)
self.replaceBox.setFont(boxFont)
self.replaceBox.setPlaceholderText("Replace")
self.replaceBox.setPlaceholderText(self.tr("Replace"))
self.replaceBox.returnPressed.connect(self._doReplace)
self.searchOpt = QToolBar(self)
@@ -1831,44 +1881,44 @@ class GuiDocEditSearch(QFrame):
self.searchOpt.setContentsMargins(0, 0, 0, 0)
self.searchOpt.setStyleSheet(r"QToolBar {padding: 0;}")
self.searchLabel = QLabel("Search")
self.searchLabel = QLabel(self.tr("Search"))
self.searchLabel.setFont(boxFont)
self.searchLabel.setIndent(self.mainConf.pxInt(6))
self.toggleCase = QAction("Case Sensitive", self)
self.toggleCase.setToolTip("Match case")
self.toggleCase = QAction(self.tr("Case Sensitive"), self)
self.toggleCase.setToolTip(self.tr("Match case"))
self.toggleCase.setIcon(self.theTheme.getIcon("search_case"))
self.toggleCase.setCheckable(True)
self.toggleCase.setChecked(self.isCaseSense)
self.toggleCase.toggled.connect(self._doToggleCase)
self.searchOpt.addAction(self.toggleCase)
self.toggleWord = QAction("Whole Words Only", self)
self.toggleWord.setToolTip("Match whole words")
self.toggleWord = QAction(self.tr("Whole Words Only"), self)
self.toggleWord.setToolTip(self.tr("Match whole words"))
self.toggleWord.setIcon(self.theTheme.getIcon("search_word"))
self.toggleWord.setCheckable(True)
self.toggleWord.setChecked(self.isWholeWord)
self.toggleWord.toggled.connect(self._doToggleWord)
self.searchOpt.addAction(self.toggleWord)
self.toggleRegEx = QAction("RegEx Mode", self)
self.toggleRegEx.setToolTip("Use regular expressions (requires Qt 5.3)")
self.toggleRegEx = QAction(self.tr("RegEx Mode"), self)
self.toggleRegEx.setToolTip(self.tr("Search using regular expressions"))
self.toggleRegEx.setIcon(self.theTheme.getIcon("search_regex"))
self.toggleRegEx.setCheckable(True)
self.toggleRegEx.setChecked(self.isRegEx)
self.toggleRegEx.toggled.connect(self._doToggleRegEx)
self.searchOpt.addAction(self.toggleRegEx)
self.toggleLoop = QAction("Loop Search", self)
self.toggleLoop.setToolTip("Loop the search when reaching the end")
self.toggleLoop = QAction(self.tr("Loop Search"), self)
self.toggleLoop.setToolTip(self.tr("Loop the search when reaching the end"))
self.toggleLoop.setIcon(self.theTheme.getIcon("search_loop"))
self.toggleLoop.setCheckable(True)
self.toggleLoop.setChecked(self.doLoop)
self.toggleLoop.toggled.connect(self._doToggleLoop)
self.searchOpt.addAction(self.toggleLoop)
self.toggleProject = QAction("Search Next File", self)
self.toggleProject.setToolTip("Continue searching in the next file")
self.toggleProject = QAction(self.tr("Search Next File"), self)
self.toggleProject.setToolTip(self.tr("Continue searching in the next file"))
self.toggleProject.setIcon(self.theTheme.getIcon("search_project"))
self.toggleProject.setCheckable(True)
self.toggleProject.setChecked(self.doNextFile)
@@ -1877,8 +1927,8 @@ class GuiDocEditSearch(QFrame):
self.searchOpt.addSeparator()
self.toggleMatchCap = QAction("Preserve Case", self)
self.toggleMatchCap.setToolTip("Preserve case on replace")
self.toggleMatchCap = QAction(self.tr("Preserve Case"), self)
self.toggleMatchCap.setToolTip(self.tr("Preserve case on replace"))
self.toggleMatchCap.setIcon(self.theTheme.getIcon("search_preserve"))
self.toggleMatchCap.setCheckable(True)
self.toggleMatchCap.setChecked(self.doMatchCap)
@@ -1887,8 +1937,8 @@ class GuiDocEditSearch(QFrame):
self.searchOpt.addSeparator()
self.cancelSearch = QAction("Close Search", self)
self.cancelSearch.setToolTip("Close the search box [Esc]")
self.cancelSearch = QAction(self.tr("Close Search"), self)
self.cancelSearch.setToolTip(self.tr("Close the search box [{0}]").format("Esc"))
self.cancelSearch.setIcon(self.theTheme.getIcon("search_cancel"))
self.cancelSearch.triggered.connect(self._doClose)
self.searchOpt.addAction(self.cancelSearch)
@@ -1900,18 +1950,18 @@ class GuiDocEditSearch(QFrame):
self.showReplace = QToolButton(self)
self.showReplace.setArrowType(Qt.RightArrow)
self.showReplace.setCheckable(True)
self.showReplace.setToolTip("Show/hide the replace text box")
self.showReplace.setToolTip(self.tr("Show/hide the replace text box"))
self.showReplace.setStyleSheet(r"QToolButton {border: none; background: transparent;}")
self.showReplace.toggled.connect(self._doToggleReplace)
self.searchButton = QPushButton(self.theTheme.getIcon("search"), "")
self.searchButton.setFixedSize(QSize(bPx, bPx))
self.searchButton.setToolTip("Find in current document")
self.searchButton.setToolTip(self.tr("Find in current document"))
self.searchButton.clicked.connect(self._doSearch)
self.replaceButton = QPushButton(self.theTheme.getIcon("search-replace"), "")
self.replaceButton.setFixedSize(QSize(bPx, bPx))
self.replaceButton.setToolTip("Find and replace in current document")
self.replaceButton.setToolTip(self.tr("Find and replace in current document"))
self.replaceButton.clicked.connect(self._doReplace)
self.mainBox.addWidget(self.searchLabel, 0, 0, 1, 2, Qt.AlignLeft)
@@ -2038,7 +2088,7 @@ class GuiDocEditSearch(QFrame):
self._alertSearchValid(theRegEx.isValid())
return theRegEx
elif self.mainConf.verQtValue >= 50300:
else: # >= 50300 to < 51300
if self.isCaseSense:
rxOpt = Qt.CaseSensitive
else:
@@ -2074,15 +2124,15 @@ class GuiDocEditSearch(QFrame):
"""
modKey = qApp.keyboardModifiers()
if modKey == Qt.ShiftModifier:
self.docEditor.docAction(nwDocAction.GO_PREV)
self.docEditor.findNext(goBack=True)
else:
self.docEditor.docAction(nwDocAction.GO_NEXT)
self.docEditor.findNext()
return
def _doReplace(self):
"""Call the replace action function for the document editor.
"""
self.docEditor.docAction(nwDocAction.REPL_NEXT)
self.docEditor.replaceNext()
return
def _doToggleReplace(self, theState):
@@ -2171,7 +2221,6 @@ class GuiDocEditHeader(QWidget):
fPx = int(0.9*self.theTheme.fontPixelSize)
hSp = self.mainConf.pxInt(6)
self.buttonSize = fPx + hSp
# Main Widget Settings
self.setAutoFillBackground(True)
@@ -2204,7 +2253,7 @@ class GuiDocEditHeader(QWidget):
self.editButton.setStyleSheet(buttonStyle)
self.editButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.editButton.setVisible(False)
self.editButton.setToolTip("Edit document meta")
self.editButton.setToolTip(self.tr("Edit document meta"))
self.editButton.clicked.connect(self._editDocument)
self.searchButton = QToolButton(self)
@@ -2215,7 +2264,7 @@ class GuiDocEditHeader(QWidget):
self.searchButton.setStyleSheet(buttonStyle)
self.searchButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.searchButton.setVisible(False)
self.searchButton.setToolTip("Search document")
self.searchButton.setToolTip(self.tr("Search document"))
self.searchButton.clicked.connect(self._searchDocument)
self.minmaxButton = QToolButton(self)
@@ -2226,7 +2275,7 @@ class GuiDocEditHeader(QWidget):
self.minmaxButton.setStyleSheet(buttonStyle)
self.minmaxButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.minmaxButton.setVisible(False)
self.minmaxButton.setToolTip("Toggle Focus Mode")
self.minmaxButton.setToolTip(self.tr("Toggle Focus Mode"))
self.minmaxButton.clicked.connect(self._minmaxDocument)
self.closeButton = QToolButton(self)
@@ -2237,7 +2286,7 @@ class GuiDocEditHeader(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
@@ -2317,6 +2366,17 @@ class GuiDocEditHeader(QWidget):
return True
def updateFocusMode(self):
"""Update the minimise/maximise icon of the Focus Mode button.
This function is called by the GuiMain class via the
toggleFocusMode function and should not be activated directly.
"""
if self.theParent.isFocusMode:
self.minmaxButton.setIcon(self.theTheme.getIcon("minimise"))
else:
self.minmaxButton.setIcon(self.theTheme.getIcon("maximise"))
return
##
# Slots
##
@@ -2347,18 +2407,6 @@ class GuiDocEditHeader(QWidget):
"""Switch on or off Focus Mode.
"""
self.theParent.toggleFocusMode()
if self.theParent.isFocusMode:
self.minmaxButton.setIcon(self.theTheme.getIcon("minimise"))
self.setContentsMargins(self.buttonSize, 0, 0, 0)
self.editButton.setVisible(False)
self.searchButton.setVisible(False)
self.closeButton.setVisible(False)
else:
self.minmaxButton.setIcon(self.theTheme.getIcon("maximise"))
self.setContentsMargins(0, 0, 0, 0)
self.editButton.setVisible(True)
self.searchButton.setVisible(True)
self.closeButton.setVisible(True)
return
##
@@ -2413,7 +2461,7 @@ class GuiDocEditFooter(QWidget):
self.statusIcon.setFixedHeight(self.sPx)
self.statusIcon.setAlignment(Qt.AlignLeft | Qt.AlignTop)
self.statusText = QLabel("Status")
self.statusText = QLabel(self.tr("Status"))
self.statusText.setIndent(0)
self.statusText.setMargin(0)
self.statusText.setContentsMargins(0, 0, 0, 0)
@@ -2429,7 +2477,7 @@ class GuiDocEditFooter(QWidget):
self.linesIcon.setFixedHeight(self.sPx)
self.linesIcon.setAlignment(Qt.AlignLeft | Qt.AlignTop)
self.linesText = QLabel("Line: 0")
self.linesText = QLabel("")
self.linesText.setIndent(0)
self.linesText.setMargin(0)
self.linesText.setContentsMargins(0, 0, 0, 0)
@@ -2445,7 +2493,7 @@ class GuiDocEditFooter(QWidget):
self.wordsIcon.setFixedHeight(self.sPx)
self.wordsIcon.setAlignment(Qt.AlignLeft | Qt.AlignTop)
self.wordsText = QLabel("Words: 0")
self.wordsText = QLabel("")
self.wordsText.setIndent(0)
self.wordsText.setMargin(0)
self.wordsText.setContentsMargins(0, 0, 0, 0)
@@ -2476,6 +2524,8 @@ class GuiDocEditFooter(QWidget):
# Fix the Colours
self.matchColours()
self.updateLineCount()
self.updateCounts()
logger.debug("GuiDocEditFooter initialisation complete")
@@ -2532,8 +2582,8 @@ class GuiDocEditFooter(QWidget):
theIcon = self.theParent.importIcons[iStatus]
sIcon = theIcon.pixmap(self.sPx, self.sPx)
sClass = nwLabels.CLASS_NAME[self.theItem.itemClass]
sLayout = nwLabels.LAYOUT_NAME[self.theItem.itemLayout]
sClass = trConst(nwLabels.CLASS_NAME[self.theItem.itemClass])
sLayout = trConst(nwLabels.LAYOUT_NAME[self.theItem.itemLayout])
sText = f"{self.theItem.itemStatus} / {sClass} / {sLayout}"
self.statusIcon.setPixmap(sIcon)
@@ -2552,7 +2602,9 @@ class GuiDocEditFooter(QWidget):
iLine = theCursor.blockNumber() + 1
iDist = 100*iLine/self.docEditor.qDocument.blockCount()
self.linesText.setText(f"Line: {iLine:n} ({iDist:.0f}\u202f%)")
self.linesText.setText(
self.tr("Line: {0} ({1})").format(f"{iLine:n}", f"{iDist:.0f} %")
)
return
@@ -2566,10 +2618,14 @@ class GuiDocEditFooter(QWidget):
wCount = self.theItem.wordCount
wDiff = wCount - self.theItem.initCount
self.wordsText.setText(f"Words: {wCount:n} ({wDiff:+n})")
self.wordsText.setText(
self.tr("Words: {0} ({1})").format(f"{wCount:n}", f"{wDiff:+n}")
)
byteSize = self.docEditor.qDocument.characterCount()
self.wordsText.setToolTip(f"Document size is {byteSize:n} bytes")
self.wordsText.setToolTip(
self.tr("Document size is {0} bytes").format(f"{byteSize:n}")
)
return

View File

@@ -34,7 +34,7 @@ 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__)

View File

@@ -41,7 +41,8 @@ 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__)
@@ -183,7 +184,7 @@ class GuiDocViewer(QTextBrowser):
except Exception:
logger.error("Failed to generate preview for document with handle '%s'" % tHandle)
nw.logException()
self.setText("An error occurred while generating the preview.")
self.setText(self.tr("An error occurred while generating the preview."))
return False
# Refresh the tab stops
@@ -243,11 +244,14 @@ class GuiDocViewer(QTextBrowser):
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 "
"exist, or the index is out of date. The index can be updated "
"from the Tools menu, or by pressing F9."
) % theTag, nwAlert.ERROR)
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 {1}."
).format(theTag, "F9"),
nwAlert.ERROR
)
return False
else:
# Let the parent handle the opening as it also ensures that
@@ -415,7 +419,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 +428,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)
)
@@ -740,7 +744,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 +755,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 +766,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 +777,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
@@ -944,7 +948,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 +959,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 +973,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 +985,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 +997,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 +1006,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 +1015,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)

View File

@@ -31,9 +31,8 @@ from PyQt5.QtCore import Qt
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__)
@@ -67,7 +66,7 @@ class GuiItemDetails(QWidget):
self.fntValue.setPointSizeF(0.9*fPt)
# Label
self.labelName = QLabel("Label")
self.labelName = QLabel(self.tr("Label"))
self.labelName.setFont(self.fntLabel)
self.labelName.setAlignment(Qt.AlignLeft | Qt.AlignBaseline)
@@ -80,7 +79,7 @@ class GuiItemDetails(QWidget):
self.labelData.setWordWrap(True)
# Status
self.statusName = QLabel("Status")
self.statusName = QLabel(self.tr("Status"))
self.statusName.setFont(self.fntLabel)
self.statusName.setAlignment(Qt.AlignLeft)
@@ -92,7 +91,7 @@ class GuiItemDetails(QWidget):
self.statusData.setAlignment(Qt.AlignLeft)
# Class
self.className = QLabel("Class")
self.className = QLabel(self.tr("Class"))
self.className.setFont(self.fntLabel)
self.className.setAlignment(Qt.AlignLeft)
@@ -105,7 +104,7 @@ class GuiItemDetails(QWidget):
self.classData.setAlignment(Qt.AlignLeft)
# Layout
self.layoutName = QLabel("Layout")
self.layoutName = QLabel(self.tr("Layout"))
self.layoutName.setFont(self.fntLabel)
self.layoutName.setAlignment(Qt.AlignLeft)
@@ -118,7 +117,7 @@ class GuiItemDetails(QWidget):
self.layoutData.setAlignment(Qt.AlignLeft)
# Character Count
self.cCountName = QLabel(" Characters")
self.cCountName = QLabel(" "+self.tr("Characters"))
self.cCountName.setFont(self.fntLabel)
self.cCountName.setAlignment(Qt.AlignRight)
@@ -127,7 +126,7 @@ class GuiItemDetails(QWidget):
self.cCountData.setAlignment(Qt.AlignRight)
# Word Count
self.wCountName = QLabel(" Words")
self.wCountName = QLabel(" "+self.tr("Words"))
self.wCountName.setFont(self.fntLabel)
self.wCountName.setAlignment(Qt.AlignRight)
@@ -136,7 +135,7 @@ class GuiItemDetails(QWidget):
self.wCountData.setAlignment(Qt.AlignRight)
# Paragraph Count
self.pCountName = QLabel(" Paragraphs")
self.pCountName = QLabel(" "+self.tr("Paragraphs"))
self.pCountName.setFont(self.fntLabel)
self.pCountName.setAlignment(Qt.AlignRight)
@@ -259,17 +258,17 @@ class GuiItemDetails(QWidget):
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}")

File diff suppressed because it is too large Load Diff

View File

@@ -32,8 +32,8 @@ 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__)
@@ -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)

View File

@@ -34,7 +34,8 @@ 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__)
@@ -149,7 +150,7 @@ 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 = {}
@@ -355,7 +356,7 @@ class GuiOutline(QTreeWidget):
if self.firstView:
theLabels = []
for i, hItem in enumerate(self.treeOrder):
theLabels.append(nwLabels.OUTLINE_COLS[hItem])
theLabels.append(trConst(nwLabels.OUTLINE_COLS[hItem]))
self.colIndex[hItem] = i
self.setHeaderLabels(theLabels)
@@ -474,7 +475,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,7 +483,7 @@ 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)

View File

@@ -27,23 +27,23 @@ 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,7 +190,7 @@ class GuiOutlineDetails(QScrollArea):
self.mainForm.setVerticalSpacing(vSpace)
# Selected Item Tags
self.tagsGroup = QGroupBox("Reference Tags", self)
self.tagsGroup = QGroupBox(self.tr("Reference Tags"), self)
self.tagsForm = QGridLayout()
self.tagsGroup.setLayout(self.tagsForm)
@@ -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("")
@@ -286,9 +286,9 @@ class GuiOutlineDetails(QScrollArea):
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)

View File

@@ -35,9 +35,9 @@ 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__)
@@ -54,7 +54,7 @@ class GuiProjectDetails(PagedDialog):
self.theProject = theProject
self.optState = theProject.optState
self.setWindowTitle("Project Details")
self.setWindowTitle(self.tr("Project Details"))
wW = self.mainConf.pxInt(600)
wH = self.mainConf.pxInt(400)
@@ -69,10 +69,11 @@ class GuiProjectDetails(PagedDialog):
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)
@@ -154,7 +155,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 +165,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 +178,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 +211,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)
@@ -271,7 +274,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 +314,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 +334,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 +345,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 +368,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 +400,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
##

View File

@@ -37,9 +37,8 @@ from PyQt5.QtWidgets import (
)
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__)
@@ -85,14 +84,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
@@ -193,12 +197,12 @@ class GuiProjectTree(QTreeWidget):
if itemClass is None:
if itemType == nwItemType.FILE:
self.makeAlert(
"Please select a valid location in the tree to add the document.",
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.",
self.tr("Please select a valid location in the tree to add the folder."),
nwAlert.ERROR
)
return False
@@ -209,7 +213,7 @@ class GuiProjectTree(QTreeWidget):
)
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
@@ -223,7 +227,7 @@ 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.tr("Did not find anywhere to add the file or folder!"), nwAlert.ERROR
)
return False
@@ -237,15 +241,13 @@ 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.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.tr("Cannot add new files or folders to the Trash folder."), nwAlert.ERROR
)
return False
@@ -253,18 +255,18 @@ class GuiProjectTree(QTreeWidget):
# 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((
"Cannot add new folder to this item. "
"Maximum folder depth has been reached."
self.tr("Cannot add new folder to this item."),
self.tr("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")
@@ -428,7 +430,7 @@ 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.tr("There is currently no Trash folder in this project."), nwAlert.INFO
)
return False
@@ -438,11 +440,12 @@ 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
@@ -503,7 +506,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
@@ -530,7 +534,8 @@ class GuiProjectTree(QTreeWidget):
# 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:
@@ -561,7 +566,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."
@@ -577,7 +582,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."
@@ -833,7 +838,7 @@ 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()
@@ -864,7 +869,7 @@ 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)
self.makeAlert(self.tr("The item cannot be moved to that location."), nwAlert.ERROR)
return
@@ -962,7 +967,9 @@ class GuiProjectTree(QTreeWidget):
self.addTopLevelItem(newItem)
else:
self.makeAlert(
"There is nowhere to add item with name '%s'" % nwItem.itemName, nwAlert.ERROR
self.tr(
"There is nowhere to add item with name '{0}'."
).format(nwItem.itemName), nwAlert.ERROR
)
del self._treeMap[tHandle]
return None
@@ -1081,43 +1088,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)

View File

@@ -29,10 +29,10 @@ import logging
from time import time
from PyQt5.QtCore import QLocale
from PyQt5.QtGui import QColor, QPainter
from PyQt5.QtWidgets import qApp, QStatusBar, QLabel, QAbstractButton
from nw.core import NWSpellCheck
from nw.common import formatTime
logger = logging.getLogger(__name__)
@@ -63,7 +63,7 @@ class GuiMainStatus(QStatusBar):
## The Spell Checker Language
self.langIcon = QLabel("")
self.langText = QLabel("None")
self.langText = QLabel(self.tr("None"))
self.langIcon.setPixmap(self.theTheme.getPixmap("status_lang", (iPx, iPx)))
self.langIcon.setContentsMargins(0, 0, 0, 0)
self.langText.setContentsMargins(0, 0, xM, 0)
@@ -72,7 +72,7 @@ class GuiMainStatus(QStatusBar):
## The Editor Status
self.docIcon = StatusLED(colNone, colTrue, colFalse, iPx, iPx, self)
self.docText = QLabel("Editor")
self.docText = QLabel(self.tr("Editor"))
self.docIcon.setContentsMargins(0, 0, 0, 0)
self.docText.setContentsMargins(0, 0, xM, 0)
self.addPermanentWidget(self.docIcon)
@@ -80,7 +80,7 @@ class GuiMainStatus(QStatusBar):
## The Project Status
self.projIcon = StatusLED(colNone, colTrue, colFalse, iPx, iPx, self)
self.projText = QLabel("Project")
self.projText = QLabel(self.tr("Project"))
self.projIcon.setContentsMargins(0, 0, 0, 0)
self.projText.setContentsMargins(0, 0, xM, 0)
self.addPermanentWidget(self.projIcon)
@@ -103,7 +103,7 @@ class GuiMainStatus(QStatusBar):
self.timeIcon = QLabel()
self.timeText = QLabel("")
self.timeIcon.setPixmap(self.timePixmap)
self.timeText.setToolTip("Session Time")
self.timeText.setToolTip(self.tr("Session Time"))
self.timeText.setMinimumWidth(self.theTheme.getTextWidth("00:00:00:"))
self.timeIcon.setContentsMargins(0, 0, 0, 0)
self.timeText.setContentsMargins(0, 0, 0, 0)
@@ -151,13 +151,17 @@ class GuiMainStatus(QStatusBar):
"""Set the language code for the spell checker.
"""
if theLanguage is None:
self.langText.setText("None")
self.langText.setText(self.tr("None"))
self.langText.setToolTip("")
else:
self.langText.setText(NWSpellCheck.expandLanguage(theLanguage))
self.langText.setToolTip(
"Provider: %s" % (theProvider if theProvider else "unknown")
)
qLocal = QLocale(theLanguage)
spLang = qLocal.nativeLanguageName().title()
self.langText.setText(spLang)
if theProvider:
self.langText.setToolTip("%s (%s)" % (theLanguage, theProvider))
else:
self.langText.setToolTip(theLanguage)
return
def setProjectStatus(self, isChanged):
@@ -175,8 +179,8 @@ class GuiMainStatus(QStatusBar):
def setStats(self, pWC, sWC):
"""Set the current project statistics.
"""
self.statsText.setText(f"Words: {pWC:n} ({sWC:+n})")
self.statsText.setToolTip("Project word count (session change)")
self.statsText.setText(self.tr("Words: {0} ({1})").format(f"{pWC:n}", f"{sWC:+n}"))
self.statsText.setToolTip(self.tr("Project word count (session change)"))
return
def setUserIdle(self, userIdle):

View File

@@ -31,14 +31,15 @@ import configparser
import os
from math import ceil
from functools import partial
from PyQt5.QtCore import Qt
from PyQt5.QtCore import QCoreApplication, Qt
from PyQt5.QtWidgets import QStyle, qApp
from PyQt5.QtGui import (
QPalette, QColor, QIcon, QFont, QFontMetrics, QFontDatabase, QPixmap
)
from nw.constants import nwAlert
from nw.enum import nwAlert
logger = logging.getLogger(__name__)
@@ -157,6 +158,10 @@ class GuiTheme:
logger.verbose("Text 'N' Height: %d" % self.textNHeight)
logger.verbose("Text 'N' Width: %d" % self.textNWidth)
# Internal Mapping
self.makeAlert = self.theParent.makeAlert
self.tr = partial(QCoreApplication.translate, "GuiTheme")
return
##
@@ -392,8 +397,8 @@ class GuiTheme:
with open(themeConf, mode="r", encoding="utf8") as inFile:
confParser.read_file(inFile)
except Exception as e:
self.theParent.makeAlert(
["Could not load theme config file.", str(e)], nwAlert.ERROR
self.makeAlert(
[self.tr("Could not load theme config file."), str(e)], nwAlert.ERROR
)
continue
themeName = ""
@@ -425,8 +430,8 @@ class GuiTheme:
with open(syntaxPath, mode="r", encoding="utf8") as inFile:
confParser.read_file(inFile)
except Exception as e:
self.theParent.makeAlert(
["Could not load syntax file.", str(e)], nwAlert.ERROR
self.makeAlert(
[self.tr("Could not load syntax file."), str(e)], nwAlert.ERROR
)
return []
syntaxName = ""
@@ -740,8 +745,8 @@ class GuiIcons:
with open(themeConf, mode="r", encoding="utf8") as inFile:
confParser.read_file(inFile)
except Exception as e:
self.theParent.makeAlert(
["Could not load theme config file.", str(e)], nwAlert.ERROR
self.makeAlert(
[self.tr("Could not load theme config file."), str(e)], nwAlert.ERROR
)
continue
themeName = ""

View File

@@ -39,15 +39,19 @@ from PyQt5.QtWidgets import (
)
from nw.gui import (
GuiAbout, GuiBuildNovel, GuiDocEditor, GuiDocMerge, GuiDocSplit,
GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiItemEditor,
GuiMainMenu, GuiMainStatus, GuiNovelTree, GuiOutline, GuiOutlineDetails,
GuiPreferences, GuiProjectDetails, GuiProjectLoad, GuiProjectSettings,
GuiProjectTree, GuiProjectWizard, GuiTheme, GuiWordList, GuiWritingStats
GuiDocEditor, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiMainMenu,
GuiMainStatus, GuiNovelTree, GuiOutline, GuiOutlineDetails,
GuiProjectDetails, GuiProjectTree, GuiTheme
)
from nw.dialogs import (
GuiAbout, GuiDocMerge, GuiDocSplit, GuiItemEditor, GuiPreferences,
GuiProjectLoad, GuiProjectSettings, GuiWordList
)
from nw.tools import GuiBuildNovel, GuiProjectWizard, GuiWritingStats
from nw.core import NWProject, NWDoc, NWIndex
from nw.constants import nwItemType, nwItemClass, nwAlert, nwLists
from nw.enum import nwItemType, nwItemClass, nwAlert, nwWidget
from nw.common import getGuiItem, hexToInt
from nw.constants import nwLists
logger = logging.getLogger(__name__)
@@ -76,6 +80,7 @@ class GuiMain(QMainWindow):
logger.info("Python Version: %s (0x%x)" % (
self.mainConf.verPyString, self.mainConf.verPyHexVal)
)
logger.info("GUI Language: %s" % self.mainConf.guiLang)
# Core Classes
# ============
@@ -122,8 +127,8 @@ class GuiMain(QMainWindow):
self.projTabs = QTabWidget()
self.projTabs.setTabPosition(QTabWidget.South)
self.projTabs.setStyleSheet(r"QTabWidget::pane {border: 0;};")
self.projTabs.addTab(self.treeView, "Project")
self.projTabs.addTab(self.novelView, "Novel")
self.projTabs.addTab(self.treeView, self.tr("Project"))
self.projTabs.addTab(self.novelView, self.tr("Novel"))
self.projTabs.currentChanged.connect(self._projTabsChanged)
tabFont = self.projTabs.tabBar().font()
@@ -139,17 +144,17 @@ class GuiMain(QMainWindow):
self.treeButtons.setStyleSheet(r"QToolBar {padding: 0;}")
self.projTabs.setCornerWidget(self.treeButtons, Qt.BottomRightCorner)
self.projDetailsBtn = QAction("Project Details")
self.projDetailsBtn = QAction(self.tr("Project Details"))
self.projDetailsBtn.setIcon(self.theTheme.getIcon("status_lines"))
self.projDetailsBtn.triggered.connect(lambda: self.showProjectDetailsDialog())
self.treeButtons.addAction(self.projDetailsBtn)
self.projStatsBtn = QAction("Writing Statistics")
self.projStatsBtn = QAction(self.tr("Writing Statistics"))
self.projStatsBtn.setIcon(self.theTheme.getIcon("status_stats"))
self.projStatsBtn.triggered.connect(lambda: self.showWritingStatsDialog())
self.treeButtons.addAction(self.projStatsBtn)
self.projSettingsBtn = QAction("Project Settings")
self.projSettingsBtn = QAction(self.tr("Project Settings"))
self.projSettingsBtn.setIcon(self.theTheme.getIcon("settings"))
self.projSettingsBtn.triggered.connect(lambda: self.showProjectSettingsDialog())
self.treeButtons.addAction(self.projSettingsBtn)
@@ -184,8 +189,8 @@ class GuiMain(QMainWindow):
self.mainTabs = QTabWidget()
self.mainTabs.setTabPosition(QTabWidget.East)
self.mainTabs.setStyleSheet(r"QTabWidget::pane {border: 0;}")
self.mainTabs.addTab(self.splitDocs, "Editor")
self.mainTabs.addTab(self.splitOutline, "Outline")
self.mainTabs.addTab(self.splitDocs, self.tr("Editor"))
self.mainTabs.addTab(self.splitOutline, self.tr("Outline"))
self.mainTabs.currentChanged.connect(self._mainTabChanged)
# Splitter : Project Tree / Main Tabs
@@ -289,7 +294,7 @@ class GuiMain(QMainWindow):
self.openProject(self.mainConf.cmdOpen)
logger.debug("novelWriter is ready ...")
self.setStatus("novelWriter is ready ...")
self.setStatus(self.tr("novelWriter is ready ..."))
return
@@ -339,7 +344,7 @@ class GuiMain(QMainWindow):
if self.hasProject:
if not self.closeProject():
self.makeAlert(
"Cannot create new project when another project is open.",
self.tr("Cannot create new project when another project is open."),
nwAlert.ERROR
)
return False
@@ -357,8 +362,10 @@ class GuiMain(QMainWindow):
if os.path.isfile(os.path.join(projPath, self.theProject.projFile)):
self.makeAlert(
"A project already exists in that location. Please choose another folder.",
nwAlert.ERROR
self.tr(
"A project already exists in that location. "
"Please choose another folder."
), nwAlert.ERROR
)
return False
@@ -372,7 +379,7 @@ class GuiMain(QMainWindow):
self.statusBar.setRefTime(self.theProject.projOpened)
self.statusBar.setProjectStatus(True)
self.statusBar.setDocumentStatus(None)
self.statusBar.setStatus("New project created ...")
self.statusBar.setStatus(self.tr("New project created ..."))
self._updateWindowTitle(self.theProject.projName)
else:
self.theProject.clearProject()
@@ -391,8 +398,11 @@ class GuiMain(QMainWindow):
if not isYes:
msgYes = self.askQuestion(
"Close Project",
"Close the current project?<br>Changes are saved automatically."
self.tr("Close Project"),
"%s<br>%s" % (
self.tr("Close the current project?"),
self.tr("Changes are saved automatically.")
)
)
if not msgYes:
return False
@@ -407,7 +417,8 @@ class GuiMain(QMainWindow):
doBackup = True
if self.mainConf.askBeforeBackup:
msgYes = self.askQuestion(
"Backup Project", "Backup the current project?"
self.tr("Backup Project"),
self.tr("Backup the current project?")
)
if not msgYes:
doBackup = False
@@ -457,29 +468,35 @@ class GuiMain(QMainWindow):
try:
lockDetails = (
"<br><br>The project was locked by the computer "
"'%s' (%s %s), last active on %s"
) % (
"<br>%s" % self.tr(
"The project was locked by the computer "
"'{0}' ({1} {2}), last active on {3}."
)
).format(
self.theProject.lockedBy[0],
self.theProject.lockedBy[1],
self.theProject.lockedBy[2],
datetime.fromtimestamp(
int(self.theProject.lockedBy[3])
).strftime("%x %X")
datetime.fromtimestamp(int(self.theProject.lockedBy[3])).strftime("%x %X")
)
except Exception:
lockDetails = ""
msgBox = QMessageBox()
msgRes = msgBox.warning(
self, "Project Locked", (
"The project is already open by another instance of novelWriter, and "
"is therefore locked. Override lock and continue anyway?<br><br>"
"Note: If the program or the computer previously crashed, the lock "
"can safely be overridden. If, however, another instance of "
"novelWriter has the project open, overriding the lock may corrupt "
"the project, and is not recommended.%s"
) % lockDetails,
self, self.tr("Project Locked"),
"%s<br><br>%s<br>%s" % (
self.tr(
"The project is already open by another instance of novelWriter, and "
"is therefore locked. Override lock and continue anyway?"
),
self.tr(
"Note: If the program or the computer previously crashed, the lock "
"can safely be overridden. If, however, another instance of "
"novelWriter has the project open, overriding the lock may corrupt "
"the project, and is not recommended."
),
lockDetails
),
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
)
if msgRes == QMessageBox.Yes:
@@ -514,6 +531,10 @@ class GuiMain(QMainWindow):
# Check if we need to rebuild the index
if self.theIndex.indexBroken:
self.makeAlert(
self.tr("The project index is outdated or broken. Rebuilding index."),
nwAlert.WARN
)
self.rebuildIndex()
# Make sure the changed status is set to false on all that was
@@ -533,17 +554,9 @@ class GuiMain(QMainWindow):
logger.error("No project open")
return False
# If the project is new, it may not have a path, so we need one
if self.theProject.projPath is None:
projPath = self.selectProjectPath()
self.theProject.setProjectPath(projPath)
if self.theProject.projPath is None:
return False
self.treeView.saveTreeOrder()
self.theProject.saveProject(autoSave=autoSave)
self.theIndex.saveIndex()
if self.theProject.saveProject(autoSave=autoSave):
self.theIndex.saveIndex()
return True
@@ -558,6 +571,10 @@ class GuiMain(QMainWindow):
logger.error("No project open")
return False
# Disable focus mode if it is active
if self.isFocusMode:
self.toggleFocusMode()
self.docEditor.saveCursorPosition()
if self.docEditor.docChanged:
self.saveDocument()
@@ -685,15 +702,13 @@ class GuiMain(QMainWindow):
lastPath = self.mainConf.lastPath
extFilter = [
"Text files (*.txt)",
"Markdown files (*.md)",
"novelWriter files (*.nwd)",
"All files (*.*)",
self.tr("Text files ({0})").format("*.txt"),
self.tr("Markdown files ({0})").format("*.md"),
self.tr("novelWriter files ({0})").format("*.nwd"),
self.tr("All files ({0})").format("*"),
]
dlgOpt = QFileDialog.Options()
dlgOpt |= QFileDialog.DontUseNativeDialog
loadFile, _ = QFileDialog.getOpenFileName(
self, "Import File", lastPath, options=dlgOpt, filter=";;".join(extFilter)
self, self.tr("Import File"), lastPath, filter=";;".join(extFilter)
)
if not loadFile:
return False
@@ -707,24 +722,26 @@ class GuiMain(QMainWindow):
theText = inFile.read()
self.mainConf.setLastPath(loadFile)
except Exception as e:
self.makeAlert(
["Could not read file. The file must be an existing text file.", str(e)],
nwAlert.ERROR
)
self.makeAlert([
self.tr("Could not read file. The file must be an existing text file."), str(e)
], nwAlert.ERROR)
return False
if self.docEditor.theHandle is None:
self.makeAlert(
"Please open a document to import the text file into.",
self.tr("Please open a document to import the text file into."),
nwAlert.ERROR
)
return False
if not self.docEditor.isEmpty():
msgYes = self.askQuestion("Import Document", (
"Importing the file will overwrite the current content of the document. "
"Do you want to proceed?"
))
msgYes = self.askQuestion(
self.tr("Import Document"),
self.tr(
"Importing the file will overwrite the current content of the document. "
"Do you want to proceed?"
)
)
if not msgYes:
return False
@@ -757,14 +774,18 @@ class GuiMain(QMainWindow):
return True
def passDocumentAction(self, theAction):
"""Pass on document action theAction to the document viewer if
it has focus, otherwise pass it to the document editor.
"""Pass on document action to the document viewer if it has
focus, or pass it to the document editor if it or any of
its clid widgets have focus. If neither has focus, ignore the
action.
"""
if self.docViewer.hasFocus():
self.docViewer.docAction(theAction)
else:
elif self.docEditor.hasFocus():
self.docEditor.docAction(theAction)
return True
else:
logger.debug("Action cancelled as neither editor nor viewer has focus")
return
##
# Tree Item Actions
@@ -800,10 +821,10 @@ class GuiMain(QMainWindow):
return False
if tHandle is None:
if self.treeView.hasFocus():
tHandle = self.treeView.getSelectedHandle()
elif self.docEditor.hasFocus():
if self.docEditor.anyFocus() or self.isFocusMode:
tHandle = self.docEditor.theHandle
else:
tHandle = self.treeView.getSelectedHandle()
if tHandle is None:
logger.warning("No item selected")
@@ -861,9 +882,9 @@ class GuiMain(QMainWindow):
for nDone, tItem in enumerate(self.theProject.projTree):
if tItem is not None:
self.setStatus("Indexing: '%s'" % tItem.itemName)
self.setStatus(self.tr("Indexing: '{0}'").format(tItem.itemName))
else:
self.setStatus("Indexing: Unknown item")
self.setStatus(self.tr("Indexing: '{0}'").format(self.tr("Unknown item")))
if tItem is not None and tItem.itemType == nwItemType.FILE:
logger.verbose("Scanning: %s" % tItem.itemName)
@@ -881,12 +902,16 @@ class GuiMain(QMainWindow):
self.treeView.projectWordCount()
tEnd = time()
self.setStatus("Indexing completed in %.1f ms" % ((tEnd - tStart)*1000.0))
self.setStatus(
self.tr("Indexing completed in {0} ms").format(f"{(tEnd - tStart)*1000.0:.1f}")
)
self.docEditor.updateTagHighLighting()
qApp.restoreOverrideCursor()
if not beQuiet:
self.makeAlert("The project index has been successfully rebuilt.", nwAlert.INFO)
self.makeAlert(
self.tr("The project index has been successfully rebuilt."), nwAlert.INFO
)
return True
@@ -907,19 +932,6 @@ class GuiMain(QMainWindow):
# Main Dialogs
##
def selectProjectPath(self):
"""Select where to save project.
"""
dlgOpt = QFileDialog.Options()
dlgOpt |= QFileDialog.ShowDirsOnly
dlgOpt |= QFileDialog.DontUseNativeDialog
projPath = QFileDialog.getExistingDirectory(
self, "Save novelWriter Project", "", options=dlgOpt
)
if projPath:
return projPath
return None
def showProjectLoadDialog(self):
"""Opens the projects dialog for selecting either existing
projects from a cache of recently opened projects, or provide a
@@ -1100,14 +1112,14 @@ class GuiMain(QMainWindow):
# Popup
msgBox = QMessageBox()
if theLevel == nwAlert.INFO:
msgBox.information(self, "Information", popMsg)
msgBox.information(self, self.tr("Information"), popMsg)
elif theLevel == nwAlert.WARN:
msgBox.warning(self, "Warning", popMsg)
msgBox.warning(self, self.tr("Warning"), popMsg)
elif theLevel == nwAlert.ERROR:
msgBox.critical(self, "Error", popMsg)
msgBox.critical(self, self.tr("Error"), popMsg)
elif theLevel == nwAlert.BUG:
popMsg += "<br>This is a bug!"
msgBox.critical(self, "Internal Error", popMsg)
popMsg += "<br>%s" % self.tr("This is a bug!")
msgBox.critical(self, self.tr("Internal Error"), popMsg)
return
@@ -1115,7 +1127,7 @@ class GuiMain(QMainWindow):
"""Ask the user a Yes/No question.
"""
msgBox = QMessageBox()
msgRes = msgBox.question(self, theTitle, theQuestion)
msgRes = msgBox.question(self, theTitle, theQuestion, QMessageBox.Yes | QMessageBox.No)
return msgRes == QMessageBox.Yes
def reportConfErr(self):
@@ -1137,8 +1149,11 @@ class GuiMain(QMainWindow):
"""
if self.hasProject:
msgYes = self.askQuestion(
"Exit",
"Do you want to exit novelWriter?<br>Changes are saved automatically."
self.tr("Exit"),
"%s<br>%s" % (
self.tr("Do you want to exit novelWriter?"),
self.tr("Changes are saved automatically.")
)
)
if not msgYes:
return False
@@ -1169,18 +1184,18 @@ class GuiMain(QMainWindow):
return True
def setFocus(self, paneNo):
def switchFocus(self, paneNo):
"""Switch focus between main GUI views.
"""
if paneNo == 1:
if paneNo == nwWidget.TREE:
self.treeView.setFocus()
elif paneNo == 2:
elif paneNo == nwWidget.EDITOR:
self.mainTabs.setCurrentWidget(self.splitDocs)
self.docEditor.setFocus()
elif paneNo == 3:
elif paneNo == nwWidget.VIEWER:
self.mainTabs.setCurrentWidget(self.splitDocs)
self.docViewer.setFocus()
elif paneNo == 4:
elif paneNo == nwWidget.OUTLINE:
self.mainTabs.setCurrentWidget(self.splitOutline)
self.projView.setFocus()
return
@@ -1217,6 +1232,7 @@ class GuiMain(QMainWindow):
if self.isFocusMode:
logger.debug("Activating Focus Mode")
self.mainTabs.setCurrentWidget(self.splitDocs)
self.switchFocus(nwWidget.EDITOR)
else:
logger.debug("Deactivating Focus Mode")
@@ -1228,6 +1244,7 @@ class GuiMain(QMainWindow):
hideDocFooter = self.isFocusMode and self.mainConf.hideFocusFooter
self.docEditor.docFooter.setVisible(not hideDocFooter)
self.docEditor.docHeader.updateFocusMode()
if self.splitView.isVisible():
self.splitView.setVisible(False)
@@ -1265,11 +1282,12 @@ class GuiMain(QMainWindow):
"""
# Project
self.addAction(self.mainMenu.aSaveProject)
self.addAction(self.mainMenu.aEditItem)
self.addAction(self.mainMenu.aExitNW)
# Document
self.addAction(self.mainMenu.aSaveDoc)
self.addAction(self.mainMenu.aFileDetails)
self.addAction(self.mainMenu.aCloseDoc)
# Edit
self.addAction(self.mainMenu.aEditUndo)
@@ -1313,6 +1331,13 @@ class GuiMain(QMainWindow):
for mAction, _ in self.mainMenu.mInsKWItems.values():
self.addAction(mAction)
# Search
self.addAction(self.mainMenu.aFind)
self.addAction(self.mainMenu.aReplace)
self.addAction(self.mainMenu.aFindNext)
self.addAction(self.mainMenu.aFindPrev)
self.addAction(self.mainMenu.aReplaceNext)
# Format
self.addAction(self.mainMenu.aFmtEmph)
self.addAction(self.mainMenu.aFmtStrong)

11
nw/tools/__init__.py Normal file
View File

@@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
from nw.tools.build import GuiBuildNovel
from nw.tools.projwizard import GuiProjectWizard
from nw.tools.writingstats import GuiWritingStats
__all__ = [
"GuiBuildNovel",
"GuiProjectWizard",
"GuiWritingStats",
]

View File

@@ -41,15 +41,14 @@ from PyQt5.QtWidgets import (
qApp, QDialog, QVBoxLayout, QHBoxLayout, QTextBrowser, QPushButton, QLabel,
QLineEdit, QGroupBox, QGridLayout, QProgressBar, QMenu, QAction,
QFileDialog, QFontDialog, QSpinBox, QScrollArea, QSplitter, QWidget,
QSizePolicy, QDoubleSpinBox
QSizePolicy, QDoubleSpinBox, QComboBox
)
from nw.common import fuzzyTime, makeFileNameSafe
from nw.gui.custom import QSwitch
from nw.core import ToHtml, ToOdt, ToMarkdown
from nw.constants import (
nwConst, nwAlert, nwFiles, nwItemType, nwItemLayout, nwItemClass
)
from nw.enum import nwAlert, nwItemType, nwItemLayout, nwItemClass
from nw.common import fuzzyTime, makeFileNameSafe
from nw.constants import nwConst, nwFiles
from nw.gui.custom import QSwitch
logger = logging.getLogger(__name__)
@@ -84,7 +83,7 @@ class GuiBuildNovel(QDialog):
self.htmlSize = 0 # Size of the html document
self.buildTime = 0 # The timestamp of the last build
self.setWindowTitle("Build Novel Project")
self.setWindowTitle(self.tr("Build Novel Project"))
self.setMinimumWidth(self.mainConf.pxInt(700))
self.setMinimumHeight(self.mainConf.pxInt(600))
@@ -101,27 +100,26 @@ class GuiBuildNovel(QDialog):
# Title Formats
# =============
self.titleGroup = QGroupBox("Title Formats for Novel Files", self)
self.titleGroup = QGroupBox(self.tr("Title Formats for Novel Files"), self)
self.titleForm = QGridLayout(self)
self.titleGroup.setLayout(self.titleForm)
fmtHelp = (
r"<b>Formatting Codes:</b><br>"
r"%title% for the title as set in the document<br>"
r"%ch% for chapter number (1, 2, 3)<br>"
r"%chw% for chapter number as a word (one, two)<br>"
r"%chI% for chapter number in upper case Roman<br>"
r"%chi% for chapter number in lower case Roman<br>"
r"%sc% for scene number within chapter<br>"
r"%sca% for scene number within novel"
)
fmtScHelp = (
r"<br><br>"
r"Leave blank to skip this heading, or set to a static text, like "
r"for instance '* * *', to make a separator. The separator will "
r"be centred automatically and only appear between sections of "
r"the same type."
)
fmtHelp = "<br>".join([
"<b>%s</b>" % self.tr("Formatting Codes:"),
self.tr("{0} for the title as set in the document").format(r"%title%"),
self.tr("{0} for chapter number (1, 2, 3)").format(r"%ch%"),
self.tr("{0} for chapter number as a word (one, two)").format(r"%chw%"),
self.tr("{0} for chapter number in upper case Roman").format(r"%chI%"),
self.tr("{0} for chapter number in lower case Roman").format(r"%chi%"),
self.tr("{0} for scene number within chapter").format(r"%sc%"),
self.tr("{0} for scene number within novel").format(r"%sca%"),
])
fmtScHelp = "<br><br>%s" % self.tr(
"Leave blank to skip this heading, or set to a static text, like "
"for instance '{0}', to make a separator. The separator will "
"be centred automatically and only appear between sections of "
"the same type."
).format("* * *")
xFmt = self.mainConf.pxInt(100)
self.fmtTitle = QLineEdit()
@@ -164,6 +162,17 @@ class GuiBuildNovel(QDialog):
self._reFmtCodes(self.theProject.titleFormat["section"])
)
self.buildLang = QComboBox()
self.buildLang.setMinimumWidth(xFmt)
theLangs = self.mainConf.listLanguages(self.mainConf.LANG_PROJ)
self.buildLang.addItem("[%s]" % self.tr("Not Set"), "None")
for langID, langName in theLangs:
self.buildLang.addItem(langName, langID)
langIdx = self.buildLang.findData(self.theProject.projLang)
if langIdx != -1:
self.buildLang.setCurrentIndex(langIdx)
# Dummy boxes due to QGridView and QLineEdit expand bug
self.boxTitle = QHBoxLayout()
self.boxTitle.addWidget(self.fmtTitle)
@@ -176,11 +185,12 @@ class GuiBuildNovel(QDialog):
self.boxSection = QHBoxLayout()
self.boxSection.addWidget(self.fmtSection)
titleLabel = QLabel("Title")
chapterLabel = QLabel("Chapter")
unnumbLabel = QLabel("Unnumbered")
sceneLabel = QLabel("Scene")
sectionLabel = QLabel("Section")
titleLabel = QLabel(self.tr("Title"))
chapterLabel = QLabel(self.tr("Chapter"))
unnumbLabel = QLabel(self.tr("Unnumbered"))
sceneLabel = QLabel(self.tr("Scene"))
sectionLabel = QLabel(self.tr("Section"))
langLabel = QLabel(self.tr("Language"))
self.titleForm.addWidget(titleLabel, 0, 0, 1, 1, Qt.AlignLeft)
self.titleForm.addLayout(self.boxTitle, 0, 1, 1, 1, Qt.AlignRight)
@@ -192,6 +202,8 @@ class GuiBuildNovel(QDialog):
self.titleForm.addLayout(self.boxScene, 3, 1, 1, 1, Qt.AlignRight)
self.titleForm.addWidget(sectionLabel, 4, 0, 1, 1, Qt.AlignLeft)
self.titleForm.addLayout(self.boxSection, 4, 1, 1, 1, Qt.AlignRight)
self.titleForm.addWidget(langLabel, 5, 0, 1, 1, Qt.AlignLeft)
self.titleForm.addWidget(self.buildLang, 5, 1, 1, 1, Qt.AlignRight)
self.titleForm.setColumnStretch(0, 0)
self.titleForm.setColumnStretch(1, 1)
@@ -199,7 +211,7 @@ class GuiBuildNovel(QDialog):
# Font Options
# ============
self.fontGroup = QGroupBox("Font Options", self)
self.fontGroup = QGroupBox(self.tr("Font Options"), self)
self.fontForm = QGridLayout(self)
self.fontGroup.setLayout(self.fontForm)
@@ -237,11 +249,11 @@ class GuiBuildNovel(QDialog):
self.boxFont = QHBoxLayout()
self.boxFont.addWidget(self.textFont)
fontFamilyLabel = QLabel("Font family")
fontSizeLabel = QLabel("Font size")
lineHeightLabel = QLabel("Line height")
justifyLabel = QLabel("Justify text")
stylingLabel = QLabel("Disable styling")
fontFamilyLabel = QLabel(self.tr("Font family"))
fontSizeLabel = QLabel(self.tr("Font size"))
lineHeightLabel = QLabel(self.tr("Line height"))
justifyLabel = QLabel(self.tr("Justify text"))
stylingLabel = QLabel(self.tr("Disable styling"))
self.fontForm.addWidget(fontFamilyLabel, 0, 0, 1, 1, Qt.AlignLeft)
self.fontForm.addLayout(self.boxFont, 0, 1, 1, 1, Qt.AlignRight)
@@ -258,7 +270,7 @@ class GuiBuildNovel(QDialog):
# Styling Options
# ===============
self.styleGroup = QGroupBox("Styling Options", self)
self.styleGroup = QGroupBox(self.tr("Styling Options"), self)
self.styleForm = QGridLayout(self)
self.styleGroup.setLayout(self.styleForm)
@@ -283,7 +295,7 @@ class GuiBuildNovel(QDialog):
# Include Options
# ===============
self.textGroup = QGroupBox("Include Options", self)
self.textGroup = QGroupBox(self.tr("Include Options"), self)
self.textForm = QGridLayout(self)
self.textGroup.setLayout(self.textForm)
@@ -307,10 +319,10 @@ class GuiBuildNovel(QDialog):
self.optState.getBool("GuiBuildNovel", "incBodyText", True)
)
synopsisLabel = QLabel("Include synopsis")
commentsLabel = QLabel("Include comments")
keywordsLabel = QLabel("Include keywords")
bodyLabel = QLabel("Include body text")
synopsisLabel = QLabel(self.tr("Include synopsis"))
commentsLabel = QLabel(self.tr("Include comments"))
keywordsLabel = QLabel(self.tr("Include keywords"))
bodyLabel = QLabel(self.tr("Include body text"))
self.textForm.addWidget(synopsisLabel, 0, 0, 1, 1, Qt.AlignLeft)
self.textForm.addWidget(self.includeSynopsis, 0, 1, 1, 1, Qt.AlignRight)
@@ -327,37 +339,34 @@ class GuiBuildNovel(QDialog):
# File Filter Options
# ===================
self.fileGroup = QGroupBox("File Filter Options", self)
self.fileGroup = QGroupBox(self.tr("File Filter Options"), self)
self.fileForm = QGridLayout(self)
self.fileGroup.setLayout(self.fileForm)
self.novelFiles = QSwitch(width=wS, height=hS)
self.novelFiles.setToolTip(
"Include files with layouts 'Book', 'Page', 'Partition', "
"'Chapter', 'Unnumbered', and 'Scene'."
)
self.novelFiles.setToolTip(self.tr("Include files with layouts other than 'Note'."))
self.novelFiles.setChecked(
self.optState.getBool("GuiBuildNovel", "addNovel", True)
)
self.noteFiles = QSwitch(width=wS, height=hS)
self.noteFiles.setToolTip("Include files with layout 'Note'.")
self.noteFiles.setToolTip(self.tr("Include files with layout 'Note'."))
self.noteFiles.setChecked(
self.optState.getBool("GuiBuildNovel", "addNotes", False)
)
self.ignoreFlag = QSwitch(width=wS, height=hS)
self.ignoreFlag.setToolTip(
self.ignoreFlag.setToolTip(self.tr(
"Ignore the 'Include when building project' setting and include "
"all files in the output."
)
))
self.ignoreFlag.setChecked(
self.optState.getBool("GuiBuildNovel", "ignoreFlag", False)
)
novelLabel = QLabel("Include novel files")
notesLabel = QLabel("Include note files")
exportLabel = QLabel("Ignore export flag")
novelLabel = QLabel(self.tr("Include novel files"))
notesLabel = QLabel(self.tr("Include note files"))
exportLabel = QLabel(self.tr("Ignore export flag"))
self.fileForm.addWidget(novelLabel, 0, 0, 1, 1, Qt.AlignLeft)
self.fileForm.addWidget(self.novelFiles, 0, 1, 1, 1, Qt.AlignRight)
@@ -372,7 +381,7 @@ class GuiBuildNovel(QDialog):
# Export Options
# ==============
self.exportGroup = QGroupBox("Export Options", self)
self.exportGroup = QGroupBox(self.tr("Export Options"), self)
self.exportForm = QGridLayout(self)
self.exportGroup.setLayout(self.exportForm)
@@ -386,8 +395,8 @@ class GuiBuildNovel(QDialog):
self.optState.getBool("GuiBuildNovel", "replaceUCode", False)
)
tabsLabel = QLabel("Replace tabs with spaces")
uCodeLabel = QLabel("Replace Unicode in HTML")
tabsLabel = QLabel(self.tr("Replace tabs with spaces"))
uCodeLabel = QLabel(self.tr("Replace Unicode in HTML"))
self.exportForm.addWidget(tabsLabel, 0, 0, 1, 1, Qt.AlignLeft)
self.exportForm.addWidget(self.replaceTabs, 0, 1, 1, 1, Qt.AlignRight)
@@ -402,7 +411,7 @@ class GuiBuildNovel(QDialog):
self.buildProgress = QProgressBar()
self.buildNovel = QPushButton("Build Preview")
self.buildNovel = QPushButton(self.tr("Build Preview"))
self.buildNovel.clicked.connect(self._buildPreview)
# Action Buttons
@@ -413,56 +422,56 @@ class GuiBuildNovel(QDialog):
# Printing
self.printMenu = QMenu(self)
self.btnPrint = QPushButton("Print")
self.btnPrint = QPushButton(self.tr("Print"))
self.btnPrint.setMenu(self.printMenu)
self.printSend = QAction("Print Preview", self)
self.printSend = QAction(self.tr("Print Preview"), self)
self.printSend.triggered.connect(self._printDocument)
self.printMenu.addAction(self.printSend)
self.printFile = QAction("Print to PDF", self)
self.printFile = QAction(self.tr("Print to PDF"), self)
self.printFile.triggered.connect(lambda: self._saveDocument(self.FMT_PDF))
self.printMenu.addAction(self.printFile)
# Saving to File
self.saveMenu = QMenu(self)
self.btnSave = QPushButton("Save As")
self.btnSave = QPushButton(self.tr("Save As"))
self.btnSave.setMenu(self.saveMenu)
self.saveODT = QAction("Open Document (.odt)", self)
self.saveODT = QAction(self.tr("Open Document (.odt)"), self)
self.saveODT.triggered.connect(lambda: self._saveDocument(self.FMT_ODT))
self.saveMenu.addAction(self.saveODT)
self.saveFODT = QAction("Flat Open Document (.fodt)", self)
self.saveFODT = QAction(self.tr("Flat Open Document (.fodt)"), self)
self.saveFODT.triggered.connect(lambda: self._saveDocument(self.FMT_FODT))
self.saveMenu.addAction(self.saveFODT)
self.saveHTM = QAction("novelWriter HTML (.htm)", self)
self.saveHTM = QAction(self.tr("novelWriter HTML (.htm)"), self)
self.saveHTM.triggered.connect(lambda: self._saveDocument(self.FMT_HTM))
self.saveMenu.addAction(self.saveHTM)
self.saveNWD = QAction("novelWriter Markdown (.nwd)", self)
self.saveNWD = QAction(self.tr("novelWriter Markdown (.nwd)"), self)
self.saveNWD.triggered.connect(lambda: self._saveDocument(self.FMT_NWD))
self.saveMenu.addAction(self.saveNWD)
self.saveMD = QAction("Standard Markdown (.md)", self)
self.saveMD = QAction(self.tr("Standard Markdown (.md)"), self)
self.saveMD.triggered.connect(lambda: self._saveDocument(self.FMT_MD))
self.saveMenu.addAction(self.saveMD)
self.saveGH = QAction("GitHub Markdown (.md)", self)
self.saveGH = QAction(self.tr("GitHub Markdown (.md)"), self)
self.saveGH.triggered.connect(lambda: self._saveDocument(self.FMT_GH))
self.saveMenu.addAction(self.saveGH)
self.saveJsonH = QAction("JSON + novelWriter HTML (.json)", self)
self.saveJsonH = QAction(self.tr("JSON + novelWriter HTML (.json)"), self)
self.saveJsonH.triggered.connect(lambda: self._saveDocument(self.FMT_JSON_H))
self.saveMenu.addAction(self.saveJsonH)
self.saveJsonM = QAction("JSON + novelWriters Markdown (.json)", self)
self.saveJsonM = QAction(self.tr("JSON + novelWriter Markdown (.json)"), self)
self.saveJsonM.triggered.connect(lambda: self._saveDocument(self.FMT_JSON_M))
self.saveMenu.addAction(self.saveJsonM)
self.btnClose = QPushButton("Close")
self.btnClose = QPushButton(self.tr("Close"))
self.btnClose.clicked.connect(self._doClose)
self.buttonBox.addWidget(self.btnSave)
@@ -567,7 +576,7 @@ class GuiBuildNovel(QDialog):
self.docView.setContent(self.htmlText, self.buildTime)
else:
self.docView.setText(
"Failed to generate preview. The result is too big."
self.tr("Failed to generate preview. The result is too big.")
)
else:
@@ -655,6 +664,9 @@ class GuiBuildNovel(QDialog):
includeBody = self.includeBody.isChecked()
replaceUCode = self.replaceUCode.isChecked()
# The language lookup dict is reloaded if needed
self.theProject.setProjectLang(self.buildLang.currentData())
# Get font information
fontInfo = QFontInfo(QFont(textFont, textSize))
textFixed = fontInfo.fixedPitch()
@@ -736,10 +748,10 @@ class GuiBuildNovel(QDialog):
logger.debug("Built project in %.3f ms" % (1000*(tEnd - tStart)))
if bldObj.errData:
self.theParent.makeAlert((
"There were problems when building the project:"
"<br>-&nbsp;%s"
) % "<br>-&nbsp;".join(bldObj.errData), nwAlert.ERROR)
self.theParent.makeAlert("%s:<br>-&nbsp;%s" % (
self.tr("There were problems when building the project"),
"<br>-&nbsp;".join(bldObj.errData)), nwAlert.ERROR
)
return
@@ -796,39 +808,39 @@ class GuiBuildNovel(QDialog):
if theFmt == self.FMT_ODT:
fileExt = "odt"
textFmt = "Open Document"
textFmt = self.tr("Open Document")
elif theFmt == self.FMT_FODT:
fileExt = "fodt"
textFmt = "Flat Open Document"
textFmt = self.tr("Flat Open Document")
elif theFmt == self.FMT_HTM:
fileExt = "htm"
textFmt = "Plain HTML"
textFmt = self.tr("Plain HTML")
elif theFmt == self.FMT_NWD:
fileExt = "nwd"
textFmt = "novelWriter Markdown"
textFmt = self.tr("novelWriter Markdown")
elif theFmt == self.FMT_MD:
fileExt = "md"
textFmt = "Standard Markdown"
textFmt = self.tr("Standard Markdown")
elif theFmt == self.FMT_GH:
fileExt = "md"
textFmt = "GitHub Markdown"
textFmt = self.tr("GitHub Markdown")
elif theFmt == self.FMT_JSON_H:
fileExt = "json"
textFmt = "JSON + novelWriter HTML"
textFmt = self.tr("JSON + novelWriter HTML")
elif theFmt == self.FMT_JSON_M:
fileExt = "json"
textFmt = "JSON + novelWriter Markdown"
textFmt = self.tr("JSON + novelWriter Markdown")
elif theFmt == self.FMT_PDF:
fileExt = "pdf"
textFmt = "PDF"
textFmt = self.tr("PDF")
else:
return False
@@ -836,28 +848,21 @@ class GuiBuildNovel(QDialog):
# Generate File Name
# ==================
if fileExt:
cleanName = makeFileNameSafe(self.theProject.projName)
fileName = "%s.%s" % (cleanName, fileExt)
saveDir = self.mainConf.lastPath
if not os.path.isdir(saveDir):
saveDir = os.path.expanduser("~")
cleanName = makeFileNameSafe(self.theProject.projName)
fileName = "%s.%s" % (cleanName, fileExt)
saveDir = self.mainConf.lastPath
savePath = os.path.join(saveDir, fileName)
if not os.path.isdir(saveDir):
saveDir = self.mainConf.homePath
dlgOpt = QFileDialog.Options()
dlgOpt |= QFileDialog.DontUseNativeDialog
savePath, _ = QFileDialog.getSaveFileName(
self, "Save Document As", savePath, options=dlgOpt
)
if not savePath:
return False
self.mainConf.setLastPath(savePath)
else:
savePath = os.path.join(saveDir, fileName)
savePath, _ = QFileDialog.getSaveFileName(
self, self.tr("Save Document As"), savePath
)
if not savePath:
return False
self.mainConf.setLastPath(savePath)
# Build and Write
# ===============
@@ -985,20 +990,24 @@ class GuiBuildNovel(QDialog):
errMsg - str(e)
else:
errMsg = "Unknown format"
# If the if statements above and here match, it should not
# be possible to reach this else statement.
return False # pragma: no cover
# Report to User
# ==============
# Report to user
if wSuccess:
self.theParent.makeAlert(
"%s file successfully written to:<br> %s" % (
textFmt, savePath
), nwAlert.INFO
"%s<br>%s" % (
self.tr("{0} file successfully written to:").format(textFmt), savePath
),
nwAlert.INFO
)
else:
self.theParent.makeAlert(
"Failed to write %s file. %s" % (
textFmt, errMsg
), nwAlert.ERROR
self.tr("Failed to write {0} file. {1}").format(textFmt, errMsg),
nwAlert.ERROR
)
return wSuccess
@@ -1119,6 +1128,7 @@ class GuiBuildNovel(QDialog):
"section" : self.fmtSection.text().strip(),
})
buildLang = self.buildLang.currentData()
winWidth = self.mainConf.rpxInt(self.width())
winHeight = self.mainConf.rpxInt(self.height())
justifyText = self.justifyText.isChecked()
@@ -1140,6 +1150,8 @@ class GuiBuildNovel(QDialog):
boxWidth = self.mainConf.rpxInt(mainSplit[0])
docWidth = self.mainConf.rpxInt(mainSplit[1])
self.theProject.setProjectLang(buildLang)
# GUI Settings
self.optState.setValue("GuiBuildNovel", "winWidth", winWidth)
self.optState.setValue("GuiBuildNovel", "winHeight", winHeight)
@@ -1193,11 +1205,11 @@ class GuiBuildNovelDocView(QTextBrowser):
self.qDocument = self.document()
self.qDocument.setDocumentMargin(self.mainConf.getTextMargin())
self.setPlaceholderText(
self.setPlaceholderText(self.tr(
"This area will show the content of the document to be "
"exported or printed. Press the \"Build Preview\" button "
"to generate content."
)
))
theFont = QFont()
if self.mainConf.textFont is None:
@@ -1227,7 +1239,7 @@ class GuiBuildNovelDocView(QTextBrowser):
fPx = int(1.1*self.theTheme.fontPixelSize)
self.theTitle = QLabel("<b>Build Time:</b> Unknown", self)
self.theTitle = QLabel("", self)
self.theTitle.setIndent(0)
self.theTitle.setAutoFillBackground(True)
self.theTitle.setAlignment(Qt.AlignCenter)
@@ -1236,6 +1248,7 @@ class GuiBuildNovelDocView(QTextBrowser):
self.theTitle.setFont(lblFont)
self._updateDocMargins()
self._updateBuildAge()
self.setStyleSheet()
# Age Timer
@@ -1341,8 +1354,11 @@ class GuiBuildNovelDocView(QTextBrowser):
fuzzyTime(time() - self.buildTime)
)
else:
strBuildTime = "Unknown"
self.theTitle.setText("<b>Build Time:</b> %s" % strBuildTime)
strBuildTime = self.tr("Unknown")
self.theTitle.setText(self.tr("<b>Build Time:</b> {0}").format(strBuildTime))
return
def _updateDocMargins(self):
"""Automatically adjust the header to fill the top of the

View File

@@ -35,8 +35,9 @@ from PyQt5.QtWidgets import (
QGroupBox, QGridLayout, QSpinBox
)
from nw.enum import nwItemClass
from nw.common import makeFileNameSafe
from nw.constants import nwLabels, nwItemClass
from nw.constants import trConst, nwLabels
from nw.gui.custom import QSwitch
logger = logging.getLogger(__name__)
@@ -94,16 +95,18 @@ class ProjWizardIntroPage(QWizardPage):
self.theWizard = theWizard
self.theTheme = theWizard.theTheme
self.setTitle("Create New Project")
self.theText = QLabel(
self.setTitle(self.tr("Create New Project"))
self.theText = QLabel(self.tr(
"Provide at least a working title. The working title should not "
"be change beyond this point as it is used by the application for "
"generating file names for for instance backups. The other fields "
"are optional and can be changed at any time in Project Settings."
)
))
self.theText.setWordWrap(True)
self.imgCredit = QLabel("Side image by Peter Mitterhofer, CC BY-SA 4.0")
self.imgCredit = QLabel(self.tr("Side image by {0}, {1}").format(
"Peter Mitterhofer", "CC BY-SA 4.0"
))
lblFont = self.imgCredit.font()
lblFont.setPointSizeF(0.6*self.theTheme.fontPointSize)
self.imgCredit.setFont(lblFont)
@@ -117,22 +120,22 @@ class ProjWizardIntroPage(QWizardPage):
self.projName = QLineEdit()
self.projName.setMaxLength(200)
self.projName.setFixedWidth(xW)
self.projName.setPlaceholderText("Required")
self.projName.setPlaceholderText(self.tr("Required"))
self.projTitle = QLineEdit()
self.projTitle.setMaxLength(200)
self.projTitle.setFixedWidth(xW)
self.projTitle.setPlaceholderText("Optional")
self.projTitle.setPlaceholderText(self.tr("Optional"))
self.projAuthors = QPlainTextEdit()
self.projAuthors.setFixedHeight(xH)
self.projAuthors.setFixedWidth(xW)
self.projAuthors.setPlaceholderText("Optional. One name per line.")
self.projAuthors.setPlaceholderText(self.tr("Optional. One name per line."))
self.mainForm = QFormLayout()
self.mainForm.addRow("Working Title", self.projName)
self.mainForm.addRow("Novel Title", self.projTitle)
self.mainForm.addRow("Author(s)", self.projAuthors)
self.mainForm.addRow(self.tr("Working Title"), self.projName)
self.mainForm.addRow(self.tr("Novel Title"), self.projTitle)
self.mainForm.addRow(self.tr("Author(s)"), self.projAuthors)
self.mainForm.setVerticalSpacing(fS)
self.registerField("projName*", self.projName)
@@ -161,11 +164,11 @@ class ProjWizardFolderPage(QWizardPage):
self.theWizard = theWizard
self.theTheme = theWizard.theTheme
self.setTitle("Select Project Folder")
self.theText = QLabel(
self.setTitle(self.tr("Select Project Folder"))
self.theText = QLabel(self.tr(
"Select a location to store the project. A new project folder "
"will be created in the selected location."
)
))
self.theText.setWordWrap(True)
xW = self.mainConf.pxInt(300)
@@ -174,14 +177,14 @@ class ProjWizardFolderPage(QWizardPage):
self.projPath = QLineEdit("")
self.projPath.setFixedWidth(xW)
self.projPath.setPlaceholderText("Required")
self.projPath.setPlaceholderText(self.tr("Required"))
self.browseButton = QPushButton("...")
self.browseButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("...")))
self.browseButton.clicked.connect(self._doBrowse)
self.mainForm = QHBoxLayout()
self.mainForm.addWidget(QLabel("Project Path"), 0)
self.mainForm.addWidget(QLabel(self.tr("Project Path")), 0)
self.mainForm.addWidget(self.projPath, 1)
self.mainForm.addWidget(self.browseButton, 0)
self.mainForm.setSpacing(fS)
@@ -209,11 +212,8 @@ class ProjWizardFolderPage(QWizardPage):
if not os.path.isdir(lastPath):
lastPath = ""
dlgOpt = QFileDialog.Options()
dlgOpt |= QFileDialog.ShowDirsOnly
dlgOpt |= QFileDialog.DontUseNativeDialog
projDir = QFileDialog.getExistingDirectory(
self, "Select Project Folder", lastPath, options=dlgOpt
self, self.tr("Select Project Folder"), lastPath, options=QFileDialog.ShowDirsOnly
)
if projDir:
projName = self.field("projName")
@@ -235,20 +235,20 @@ class ProjWizardPopulatePage(QWizardPage):
self.mainConf = nw.CONFIG
self.theWizard = theWizard
self.setTitle("Populate Project")
self.theText = QLabel(
self.setTitle(self.tr("Populate Project"))
self.theText = QLabel(self.tr(
"Choose how to pre-fill the project. Either with a minimal set of "
"starter items, an example project explaining and showing many of "
"the features, or show further custom options on the next page."
)
))
self.theText.setWordWrap(True)
vS = self.mainConf.pxInt(12)
fS = self.mainConf.pxInt(4)
self.popMinimal = QRadioButton("Fill the project with a minimal set of items")
self.popSample = QRadioButton("Fill the project with example files")
self.popCustom = QRadioButton("Show detailed options for filling the project")
self.popMinimal = QRadioButton(self.tr("Fill the project with a minimal set of items"))
self.popSample = QRadioButton(self.tr("Fill the project with example files"))
self.popCustom = QRadioButton(self.tr("Show detailed options for filling the project"))
self.popMinimal.setChecked(True)
self.popBox = QVBoxLayout()
@@ -290,27 +290,39 @@ class ProjWizardCustomPage(QWizardPage):
self.mainConf = nw.CONFIG
self.theWizard = theWizard
self.setTitle("Custom Project Options")
self.theText = QLabel(
self.setTitle(self.tr("Custom Project Options"))
self.theText = QLabel(self.tr(
"Select which additional root folders to make, and how to populate "
"the Novel folder. If you don't want to add chapters or scenes, set "
"the values to 0. You can add scenes without chapters."
)
))
self.theText.setWordWrap(True)
vS = self.mainConf.pxInt(12)
# Root Folders
self.rootGroup = QGroupBox("Additional Root Folders")
self.rootGroup = QGroupBox(self.tr("Additional Root Folders"))
self.rootForm = QGridLayout()
self.rootGroup.setLayout(self.rootForm)
self.lblPlot = QLabel("%s folder" % nwLabels.CLASS_NAME[nwItemClass.PLOT])
self.lblChar = QLabel("%s folder" % nwLabels.CLASS_NAME[nwItemClass.CHARACTER])
self.lblWorld = QLabel("%s folder" % nwLabels.CLASS_NAME[nwItemClass.WORLD])
self.lblTime = QLabel("%s folder" % nwLabels.CLASS_NAME[nwItemClass.TIMELINE])
self.lblObject = QLabel("%s folder" % nwLabels.CLASS_NAME[nwItemClass.OBJECT])
self.lblEntity = QLabel("%s folder" % nwLabels.CLASS_NAME[nwItemClass.ENTITY])
self.lblPlot = QLabel(self.tr("{0} folder").format(
trConst(nwLabels.CLASS_NAME[nwItemClass.PLOT]))
)
self.lblChar = QLabel(self.tr("{0} folder").format(
trConst(nwLabels.CLASS_NAME[nwItemClass.CHARACTER]))
)
self.lblWorld = QLabel(self.tr("{0} folder").format(
trConst(nwLabels.CLASS_NAME[nwItemClass.WORLD]))
)
self.lblTime = QLabel(self.tr("{0} folder").format(
trConst(nwLabels.CLASS_NAME[nwItemClass.TIMELINE]))
)
self.lblObject = QLabel(self.tr("{0} folder").format(
trConst(nwLabels.CLASS_NAME[nwItemClass.OBJECT]))
)
self.lblEntity = QLabel(self.tr("{0} folder").format(
trConst(nwLabels.CLASS_NAME[nwItemClass.ENTITY]))
)
self.addPlot = QSwitch()
self.addChar = QSwitch()
@@ -338,7 +350,7 @@ class ProjWizardCustomPage(QWizardPage):
self.rootForm.setRowStretch(6, 1)
# Novel Options
self.novelGroup = QGroupBox("Populate Novel Folder")
self.novelGroup = QGroupBox(self.tr("Populate Novel Folder"))
self.novelForm = QGridLayout()
self.novelGroup.setLayout(self.novelForm)
@@ -353,9 +365,9 @@ class ProjWizardCustomPage(QWizardPage):
self.chFolders = QSwitch()
self.chFolders.setChecked(True)
self.novelForm.addWidget(QLabel("Add chapters"), 0, 0)
self.novelForm.addWidget(QLabel("Scenes (per chapter)"), 1, 0)
self.novelForm.addWidget(QLabel("Add chapter folders"), 2, 0)
self.novelForm.addWidget(QLabel(self.tr("Add chapters")), 0, 0)
self.novelForm.addWidget(QLabel(self.tr("Scenes (per chapter)")), 1, 0)
self.novelForm.addWidget(QLabel(self.tr("Add chapter folders")), 2, 0)
self.novelForm.addWidget(self.numChapters, 0, 1, 1, 1, Qt.AlignRight)
self.novelForm.addWidget(self.numScenes, 1, 1, 1, 1, Qt.AlignRight)
self.novelForm.addWidget(self.chFolders, 2, 1, 1, 1, Qt.AlignRight)
@@ -396,13 +408,15 @@ class ProjWizardFinalPage(QWizardPage):
self.mainConf = nw.CONFIG
self.theWizard = theWizard
self.setTitle("Finished")
self.theText = QLabel((
"<p>All done.</p>"
"<p>Press '{finish}' to create the new project.</p>"
).format(
finish = "Done" if self.mainConf.osDarwin else "Finish"
))
self.setTitle(self.tr("Finished"))
self.theText = QLabel(
"<p>%s</p><p>%s</p>" % (
self.tr("All done."),
self.tr("Press '{0}' to create the new project.").format(
self.tr("Done") if self.mainConf.osDarwin else self.tr("Finish")
)
)
)
self.theText.setWordWrap(True)
# Assemble

View File

@@ -38,8 +38,9 @@ from PyQt5.QtWidgets import (
QLabel, QGroupBox, QMenu, QAction, QFileDialog, QSpinBox, QHBoxLayout
)
from nw.enum import nwAlert
from nw.common import formatTime, checkInt
from nw.constants import nwConst, nwFiles, nwAlert
from nw.constants import nwConst, nwFiles
from nw.gui.custom import QSwitch
logger = logging.getLogger(__name__)
@@ -72,7 +73,7 @@ class GuiWritingStats(QDialog):
self.timeFilter = 0.0
self.wordOffset = 0
self.setWindowTitle("Writing Statistics")
self.setWindowTitle(self.tr("Writing Statistics"))
self.setMinimumWidth(self.mainConf.pxInt(420))
self.setMinimumHeight(self.mainConf.pxInt(400))
self.resize(
@@ -95,7 +96,13 @@ class GuiWritingStats(QDialog):
)
self.listBox = QTreeWidget()
self.listBox.setHeaderLabels(["Session Start", "Length", "Idle", "Words", "Histogram"])
self.listBox.setHeaderLabels([
self.tr("Session Start"),
self.tr("Length"),
self.tr("Idle"),
self.tr("Words"),
self.tr("Histogram"),
])
self.listBox.setIndentation(0)
self.listBox.setColumnWidth(self.C_TIME, wCol0)
self.listBox.setColumnWidth(self.C_LENGTH, wCol1)
@@ -125,7 +132,7 @@ class GuiWritingStats(QDialog):
self.barImage.fill(self.palette().highlight().color())
# Session Info
self.infoBox = QGroupBox("Sum Totals", self)
self.infoBox = QGroupBox(self.tr("Sum Totals"), self)
self.infoForm = QGridLayout(self)
self.infoBox.setLayout(self.infoForm)
@@ -153,24 +160,33 @@ class GuiWritingStats(QDialog):
self.totalWords.setFont(self.theTheme.guiFontFixed)
self.totalWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
self.infoForm.addWidget(QLabel("Total Time:"), 0, 0)
self.infoForm.addWidget(QLabel("Idle Time:"), 1, 0)
self.infoForm.addWidget(QLabel("Filtered Time:"), 2, 0)
self.infoForm.addWidget(QLabel("Novel Word Count:"), 3, 0)
self.infoForm.addWidget(QLabel("Notes Word Count:"), 4, 0)
self.infoForm.addWidget(QLabel("Total Word Count:"), 5, 0)
lblTTime = QLabel(self.tr("Total Time:"))
lblITime = QLabel(self.tr("Idle Time:"))
lblFTime = QLabel(self.tr("Filtered Time:"))
lblNvCount = QLabel(self.tr("Novel Word Count:"))
lblNtCount = QLabel(self.tr("Notes Word Count:"))
lblTtCount = QLabel(self.tr("Total Word Count:"))
self.infoForm.addWidget(lblTTime, 0, 0)
self.infoForm.addWidget(lblITime, 1, 0)
self.infoForm.addWidget(lblFTime, 2, 0)
self.infoForm.addWidget(lblNvCount, 3, 0)
self.infoForm.addWidget(lblNtCount, 4, 0)
self.infoForm.addWidget(lblTtCount, 5, 0)
self.infoForm.addWidget(self.labelTotal, 0, 1)
self.infoForm.addWidget(self.labelIdleT, 1, 1)
self.infoForm.addWidget(self.labelFilter, 2, 1)
self.infoForm.addWidget(self.novelWords, 3, 1)
self.infoForm.addWidget(self.notesWords, 4, 1)
self.infoForm.addWidget(self.totalWords, 5, 1)
self.infoForm.setRowStretch(6, 1)
# Filter Options
sPx = self.theTheme.baseIconSize
self.filterBox = QGroupBox("Filters", self)
self.filterBox = QGroupBox(self.tr("Filters"), self)
self.filterForm = QGridLayout(self)
self.filterBox.setLayout(self.filterForm)
@@ -210,12 +226,12 @@ class GuiWritingStats(QDialog):
)
self.showIdleTime.clicked.connect(self._updateListBox)
self.filterForm.addWidget(QLabel("Count novel files"), 0, 0)
self.filterForm.addWidget(QLabel("Count note files"), 1, 0)
self.filterForm.addWidget(QLabel("Hide zero word count"), 2, 0)
self.filterForm.addWidget(QLabel("Hide negative word count"), 3, 0)
self.filterForm.addWidget(QLabel("Group entries by day"), 4, 0)
self.filterForm.addWidget(QLabel("Show idle time"), 5, 0)
self.filterForm.addWidget(QLabel(self.tr("Count novel files")), 0, 0)
self.filterForm.addWidget(QLabel(self.tr("Count note files")), 1, 0)
self.filterForm.addWidget(QLabel(self.tr("Hide zero word count")), 2, 0)
self.filterForm.addWidget(QLabel(self.tr("Hide negative word count")), 3, 0)
self.filterForm.addWidget(QLabel(self.tr("Group entries by day")), 4, 0)
self.filterForm.addWidget(QLabel(self.tr("Show idle time")), 5, 0)
self.filterForm.addWidget(self.incNovel, 0, 1)
self.filterForm.addWidget(self.incNotes, 1, 1)
self.filterForm.addWidget(self.hideZeros, 2, 1)
@@ -236,7 +252,7 @@ class GuiWritingStats(QDialog):
self.optsBox = QHBoxLayout()
self.optsBox.addStretch(1)
self.optsBox.addWidget(QLabel("Word count cap for the histogram"), 0)
self.optsBox.addWidget(QLabel(self.tr("Word count cap for the histogram")), 0)
self.optsBox.addWidget(self.histMax, 0)
# Buttons
@@ -246,17 +262,17 @@ class GuiWritingStats(QDialog):
self.btnClose = self.buttonBox.addButton(QDialogButtonBox.Close)
self.btnClose.setAutoDefault(False)
self.btnSave = self.buttonBox.addButton("Save As", QDialogButtonBox.ActionRole)
self.btnSave = self.buttonBox.addButton(self.tr("Save As"), QDialogButtonBox.ActionRole)
self.btnSave.setAutoDefault(False)
self.saveMenu = QMenu(self)
self.btnSave.setMenu(self.saveMenu)
self.saveJSON = QAction("JSON Data File (.json)", self)
self.saveJSON = QAction(self.tr("JSON Data File (.json)"), self)
self.saveJSON.triggered.connect(lambda: self._saveData(self.FMT_JSON))
self.saveMenu.addAction(self.saveJSON)
self.saveCSV = QAction("CSV Data File (.csv)", self)
self.saveCSV = QAction(self.tr("CSV Data File (.csv)"), self)
self.saveCSV.triggered.connect(lambda: self._saveData(self.FMT_CSV))
self.saveMenu.addAction(self.saveCSV)
@@ -338,10 +354,10 @@ class GuiWritingStats(QDialog):
if dataFmt == self.FMT_JSON:
fileExt = "json"
textFmt = "JSON Data File"
textFmt = self.tr("JSON Data File")
elif dataFmt == self.FMT_CSV:
fileExt = "csv"
textFmt = "CSV Data File"
textFmt = self.tr("CSV Data File")
else:
return False
@@ -353,10 +369,8 @@ class GuiWritingStats(QDialog):
fileName = "sessionStats.%s" % fileExt
savePath = os.path.join(saveDir, fileName)
dlgOpt = QFileDialog.Options()
dlgOpt |= QFileDialog.DontUseNativeDialog
savePath, _ = QFileDialog.getSaveFileName(
self, "Save Document As", savePath, options=dlgOpt
self, self.tr("Save Data As"), savePath, "%s (*.%s)" % (textFmt, fileExt)
)
if not savePath:
return False
@@ -474,7 +488,7 @@ class GuiWritingStats(QDialog):
except Exception as e:
self.theParent.makeAlert(
["Failed to read session log file.", str(e)], nwAlert.ERROR
[self.tr("Failed to read session log file."), str(e)], nwAlert.ERROR
)
return False

View File

@@ -1,16 +1,17 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.2rc1" hexVersion="0x010200c1" fileVersion="1.2" timeStamp="2021-02-20 17:08:19">
<novelWriterXML appVersion="1.3" hexVersion="0x010300f0" fileVersion="1.2" timeStamp="2021-05-02 16:48:34">
<project>
<name>Sample Project</name>
<title>Sample Project</title>
<author>Jane Smith</author>
<author>Jay Doh</author>
<saveCount>937</saveCount>
<autoCount>161</autoCount>
<editTime>46797</editTime>
<saveCount>1060</saveCount>
<autoCount>166</autoCount>
<editTime>49679</editTime>
</project>
<settings>
<doBackup>False</doBackup>
<language>en</language>
<spellCheck>True</spellCheck>
<spellLang>None</spellLang>
<autoOutline>True</autoOutline>
@@ -26,7 +27,7 @@
</autoReplace>
<titleFormat>
<title>%title%</title>
<chapter>Chapter %ch%: %title%</chapter>
<chapter>Chapter %chw%: %title%</chapter>
<unnumbered>%title%</unnumbered>
<scene>Scene %ch%.%sc%: %title%</scene>
<section></section>
@@ -120,7 +121,7 @@
<charCount>1810</charCount>
<wordCount>318</wordCount>
<paraCount>8</paraCount>
<cursorPos>3</cursorPos>
<cursorPos>1505</cursorPos>
</item>
<item handle="bc0cbd2a407f3" order="2" parent="e7ded148d6e4a">
<name>Another Scene</name>

View File

@@ -5,7 +5,7 @@ author = Veronica Berglyd Olsen
author_email = code@vkbo.net
description = A markdown-like document editor for writing novels
url = https://novelwriter.io
long_description = file: README.md
long_description = file: setup/long_description.md
long_description_content_type = text/markdown
license_file = LICENSE.md
license = GNU General Public License v3
@@ -25,7 +25,7 @@ classifiers =
python_requires = >=3.6
project_urls =
Bug Tracker = https://github.com/vkbo/novelWriter/issues
Documentation = https://github.com/vkbo/novelWriter/issues
Documentation = https://novelwriter.readthedocs.io/en/stable
Source Code = https://github.com/vkbo/novelWriter
[options]

430
setup.py
View File

@@ -35,6 +35,30 @@ OS_LINUX = 1
OS_WIN = 2
OS_DARWIN = 3
# =============================================================================================== #
# Utilities
# =============================================================================================== #
def extractVersion():
"""Extract the novelWriter version number without having to import
anything else from the main package.
"""
def getValue(theString):
theBits = theString.partition("=")
return theBits[2].strip().strip('"')
numVers = "Unknown"
hexVers = "Unknown"
initFile = os.path.join("nw", "__init__.py")
with open(initFile, mode="r") as inFile:
for aLine in inFile:
if aLine.startswith("__version__"):
numVers = getValue((aLine))
if aLine.startswith("__hexversion__"):
hexVers = getValue((aLine))
return numVers, hexVers
# =============================================================================================== #
# General
# =============================================================================================== #
@@ -186,6 +210,65 @@ def buildQtDocs():
return
##
# Qt Linguist QM Builder (qtlrelease)
##
def buildQtI18n():
"""Build the lang.qm files for Qt Linguist.
"""
print("")
print("Building Qt Localisation Files")
print("==============================")
print("")
try:
subprocess.call(["lrelease", "-verbose", "novelWriter.pro"])
except Exception as e:
print("QtI18n Release Error:")
print(str(e))
print("")
print("Moving QM Files to Assets")
print("")
langDir = os.path.join("nw", "assets", "i18n")
for langFile in os.listdir("i18n"):
langPath = os.path.join("i18n", langFile)
if not os.path.isfile(langPath):
continue
if langFile.endswith(".qm"):
destPath = os.path.join(langDir, langFile)
os.rename(langPath, destPath)
print("Moved: %s -> %s" % (langPath, destPath))
print("")
return
##
# Qt Linguist TS Builder (qtlupdate)
##
def buildQtI18nTS():
"""Build the lang.ts files for Qt Linguist.
"""
print("")
print("Building Qt Translation Files")
print("=============================")
print("")
try:
subprocess.call(["pylupdate5", "-verbose", "-noobsolete", "novelWriter.pro"])
except Exception as e:
print("QtI18n Release Error:")
print(str(e))
print("")
return
##
# Sample Project ZIP File Builder (sample)
##
@@ -237,9 +320,11 @@ def buildSampleZip():
def makeMinimalPackage(targetOS):
"""Pack the core source file in a single zip file.
"""
from nw import __version__
from zipfile import ZipFile, ZIP_DEFLATED
# Get the version
numVers, _ = extractVersion()
# Make sample.zip first
try:
buildSampleZip()
@@ -248,24 +333,36 @@ def makeMinimalPackage(targetOS):
print(str(e))
sys.exit(1)
# Make translation files
try:
buildQtI18n()
except Exception as e:
print("Failed with error:")
print(str(e))
sys.exit(1)
print("")
print("Building Minimal ZIP File")
print("=========================")
print("")
if not os.path.isdir("dist"):
os.mkdir("dist")
if targetOS == OS_LINUX:
targName = "-linux"
print("Target OS: Linux")
elif targetOS == OS_DARWIN:
targName = "-darwin"
print("Target OS: Darwin")
elif targetOS == OS_WIN:
targName = "-win"
print("Target OS: Windows")
else:
targName = ""
print("")
outFile = os.path.join("dist", f"novelWriter-{__version__}-minimal{targName}.zip")
zipFile = f"novelWriter-{numVers}-minimal{targName}.zip"
outFile = os.path.join("dist", zipFile)
if os.path.isfile(outFile):
os.unlink(outFile)
@@ -274,6 +371,7 @@ def makeMinimalPackage(targetOS):
with ZipFile(outFile, "w", compression=ZIP_DEFLATED, compresslevel=9) as zipObj:
if targetOS != OS_WIN:
# Not needed for Windows as the icons are in nw/assets/icons
for nRoot, _, nFiles in os.walk("setup"):
print("Adding Folder: %s [%d files]" % (nRoot, len(nFiles)))
for aFile in nFiles:
@@ -294,19 +392,30 @@ def makeMinimalPackage(targetOS):
if targetOS == OS_WIN:
zipObj.write("novelWriter.py", "novelWriter.pyw")
print("Adding File: novelWriter.pyw")
zipObj.write("setup_windows.bat")
print("Adding File: setup_windows.bat")
zipObj.write(os.path.join("setup", "windows_install.bat"), "windows_install.bat")
print("Adding File: windows_install.bat")
zipObj.write(os.path.join("setup", "windows_uninstall.bat"), "windows_uninstall.bat")
print("Adding File: windows_uninstall.bat")
else:
zipObj.write("novelWriter.py")
print("Adding File: novelWriter.py")
for aFile in rootFiles:
assert os.path.isfile(aFile)
print("Adding File: %s" % aFile)
zipObj.write(aFile)
print("")
print("Built file: %s" % outFile)
print("Created File: %s" % outFile)
try:
shaFile = open(outFile+".sha256", mode="w")
subprocess.call(["sha256sum", zipFile], stdout=shaFile, cwd="dist")
shaFile.close()
print("SHA256 Sum: %s" % (outFile+".sha256"))
except Exception as e:
print("Could not generate sha256 file")
print(str(e))
print("")
return
@@ -566,14 +675,6 @@ def xdgInstall():
else:
print(f"Error {exCode}: Could not install menu desktop file")
exCode = subprocess.call(
["xdg-desktop-icon", "install", "--novendor", "./novelwriter.desktop"]
)
if exCode == 0:
print("Installed icon desktop file")
else:
print(f"Error {exCode}: Could not install icon desktop file")
print("")
# Install MimeType
@@ -632,6 +733,10 @@ def xdgInstall():
else:
print(f"Error {exCode}: Could not update icon cache")
# Clean up
if os.path.isfile("./novelwriter.desktop"):
os.unlink("./novelwriter.desktop")
print("")
print("Done!")
print("")
@@ -639,14 +744,71 @@ def xdgInstall():
return
##
# WIN Installation (win-install, launcher)
# XDG Uninstallation (xdg-uninstall)
##
def xdgUninstall():
"""Will attempt to uninstall icons and make a launcher.
"""
print("")
print("XDG Uninstall")
print("=============")
print("")
exCode = subprocess.call(
["xdg-desktop-menu", "uninstall", "novelwriter.desktop"]
)
if exCode == 0:
print("Uninstalled menu desktop file")
else:
print(f"Error {exCode}: Could not uninstall menu desktop file")
sizeArr = ["16", "22", "24", "32", "48", "64", "96", "128", "256", "512"]
# App Icon
for aSize in sizeArr:
exCode = subprocess.call([
"xdg-icon-resource", "uninstall", "--noupdate",
"--context", "apps", "--size", aSize, "novelwriter"
])
if exCode == 0:
print(f"Uninstalled app icon size {aSize}")
else:
print(f"Error {exCode}: Could not uninstall app icon size {aSize}")
# Mimetype
for aSize in sizeArr:
exCode = subprocess.call([
"xdg-icon-resource", "uninstall", "--noupdate",
"--context", "mimetypes", "--size", aSize,
"application-x-novelwriter-project"
])
if exCode == 0:
print(f"Uninstalled mime icon size {aSize}")
else:
print(f"Error {exCode}: Could not uninstall mime icon size {aSize}")
# Update Cache
exCode = subprocess.call(["xdg-icon-resource", "forceupdate"])
if exCode == 0:
print("Updated icon cache")
else:
print(f"Error {exCode}: Could not update icon cache")
print("")
print("Done!")
print("")
return
##
# WIN Installation (win-install)
##
def winInstall():
"""Will attempt to install icons and make a launcher for Windows.
"""
import winreg
from nw import __version__, __hexversion__
try:
import win32com.client
except ImportError:
@@ -662,13 +824,14 @@ def winInstall():
print("===============")
print("")
nwTesting = not __hexversion__[-2] == "f"
numVers, hexVers = extractVersion()
nwTesting = not hexVers[-2] == "f"
wShell = win32com.client.Dispatch("WScript.Shell")
if nwTesting:
linkName = "novelWriter Testing %s.lnk" % __version__
linkName = "novelWriter Testing %s.lnk" % numVers
else:
linkName = "novelWriter %s.lnk" % __version__
linkName = "novelWriter %s.lnk" % numVers
desktopDir = wShell.SpecialFolders("Desktop")
desktopIcon = os.path.join(desktopDir, linkName)
@@ -684,6 +847,9 @@ def winInstall():
targetPy = os.path.join(targetDir, "novelWriter.pyw")
targetIcon = os.path.join(targetDir, "nw", "assets", "icons", "novelwriter.ico")
if not os.path.isfile(targetPy):
shutil.copy2(os.path.join(targetDir, "novelWriter.py"), targetPy)
print("Collecting Info ...")
print("Desktop Folder: %s" % desktopDir)
print("Start Menu Folder: %s" % startMenuDir)
@@ -759,6 +925,92 @@ def winInstall():
return
##
# WIN Uninstallation (win-uninstall)
##
def winUninstall():
"""Will attempt to uninstall icons previously installed.
"""
import winreg
try:
import win32com.client
except ImportError:
print(
"ERROR: Package 'pywin32' is missing on this system.\n"
" Please run 'pip install --user pywin32' to install it."
)
sys.exit(1)
print("")
print("Windows Uninstall")
print("=================")
print("")
numVers, hexVers = extractVersion()
nwTesting = not hexVers[-2] == "f"
wShell = win32com.client.Dispatch("WScript.Shell")
if nwTesting:
linkName = "novelWriter Testing %s.lnk" % numVers
else:
linkName = "novelWriter %s.lnk" % numVers
desktopDir = wShell.SpecialFolders("Desktop")
desktopIcon = os.path.join(desktopDir, linkName)
startMenuDir = wShell.SpecialFolders("StartMenu")
startMenuProg = os.path.join(startMenuDir, "Programs", "novelWriter")
startMenuIcon = os.path.join(startMenuProg, linkName)
print("Deleting Links ...")
if os.path.isfile(desktopIcon):
os.unlink(desktopIcon)
print("Deleted: %s" % desktopIcon)
else:
print("Not Found: %s" % desktopIcon)
if os.path.isfile(startMenuIcon):
os.unlink(startMenuIcon)
print("Deleted: %s" % startMenuIcon)
else:
print("Not Found: %s" % startMenuIcon)
if os.path.isdir(startMenuProg):
if not os.listdir(startMenuProg):
os.rmdir(startMenuProg)
print("Deleted: %s" % startMenuProg)
else:
print("Not Empty: %s" % startMenuProg)
print("")
print("Removing registry keys ...")
theKeys = [
r"Software\Classes\novelWriterProject.nwx\shell\open\command",
r"Software\Classes\novelWriterProject.nwx\shell\open",
r"Software\Classes\novelWriterProject.nwx\shell",
r"Software\Classes\novelWriterProject.nwx\DefaultIcon",
r"Software\Classes\novelWriterProject.nwx",
r"Software\Classes\.nwx\OpenWithProgids",
r"Software\Classes\.nwx",
r"Software\Classes\Applications\novelWriter.pyw\SupportedTypes",
r"Software\Classes\Applications\novelWriter.pyw",
]
for aKey in theKeys:
try:
winreg.DeleteKey(winreg.HKEY_CURRENT_USER, aKey)
print("Deleted: HKEY_CURRENT_USER\\%s" % aKey)
except WindowsError:
print("Not Found: HKEY_CURRENT_USER\\%s" % aKey)
print("")
print("Done!")
print("")
return
# =============================================================================================== #
# Windows Installers
# =============================================================================================== #
@@ -780,8 +1032,8 @@ def innoSetup():
with open(os.path.join("setup", "win_setup_pyz.iss"), mode="r") as inFile:
issData = inFile.read()
import nw # noqa: E402
issData = issData.replace(r"%%version%%", nw.__version__)
numVers, _ = extractVersion()
issData = issData.replace(r"%%version%%", numVers)
issData = issData.replace(r"%%dir%%", os.getcwd())
with open("setup.iss", mode="w+") as outFile:
@@ -828,47 +1080,61 @@ if __name__ == "__main__":
else:
targetOS = hostOS
helpMsg = (
"\n"
"novelWriter Setup Tool\n"
"======================\n"
"\n"
"This tool provides setup and build commands for installing or distibuting novelWriter\n"
"as a package on Linux, Mac and Windows. The available options are as follows:\n"
"\n"
"Some of the commands can be targeted towards a different OS than the host OS. To target\n"
"the command, add one of '--target-linux', '--target-darwin' or '--target-win'.\n"
"\n"
"General:\n"
"\n"
" help Print the help message.\n"
" pip Install all package dependencies for novelWriter using pip.\n"
" clean Will attempt to delete the 'build' and 'dist' folders.\n"
"\n"
"Additional Builds:\n"
"\n"
" qthelp Build the help documentation for use with the Qt Assistant. Run before\n"
" install to have local help enable in the the installed version.\n"
" sample Build the sample project as a zip file. Run before install to enable\n"
" creating sample projects in the in-app New Project Wizard.\n"
"\n"
"Python Packaging:\n"
"\n"
" minimal-zip Creates a minimal zip file of the core application without all the\n"
" other source files. Accepts a target OS flag.\n"
" pack-pyz Creates a pyz package in a folder with all dependencies using the\n"
" zipapp tool. On Windows, python embeddable is added to the folder.\n"
" setup-pyz Build a Windows installer from a zipapp package using Inno Setup.\n"
"\n"
"System Install:\n"
"\n"
" install Installs novelWriter to the system's Python install location. Run as \n"
" root or with sudo for system-wide install, or as user for single user \n"
" install.\n"
" xdg-install Install launcher and icons for freedesktop systems. Run as root or \n"
" with sudo for system-wide install, or as user for single user install.\n"
" win-install Install desktop and start menu icons for Windows systems.\n"
)
helpMsg = [
"",
"novelWriter Setup Tool",
"======================",
"",
"This tool provides setup and build commands for installing or distibuting",
"novelWriter as a package on Linux, Mac and Windows. The available options",
"are as follows:",
"",
"Some of the commands can be targeted towards a different OS than the host OS.",
"To target the command, add one of '--target-linux', '--target-darwin' or",
"'--target-win'.",
"",
"General:",
"",
" help Print the help message.",
" pip Install all package dependencies for novelWriter using pip.",
" clean Will attempt to delete the 'build' and 'dist' folders.",
"",
"Additional Builds:",
"",
" qthelp Build the help documentation for use with the Qt Assistant.",
" qtlupdate Update the translation files for internationalisation.",
" qtlrelease Build the language files for internationalisation.",
" sample Build the sample project zip file and add it to assets.",
"",
"Python Packaging:",
"",
" minimal-zip Creates a minimal zip file of the core application without",
" all the other source files. Defaults to tailor the zip file",
" for the current OS, but accepts a target OS flag to build",
" for another OS.",
" pack-pyz Creates a .pyz package in a folder with all dependencies",
" using the zipapp tool. On Windows, python embeddable is",
" added to the folder.",
" setup-pyz Build a Windows executable installer from a zipapp package",
" using Inno Setup. Must run 'pack-pyz' first.",
"",
"System Install:",
"",
" install Installs novelWriter to the system's Python install",
" location. Run as root or with sudo for system-wide install,",
" or as user for single user install.",
" xdg-install Install launcher and icons for freedesktop systems. Run as",
" root or with sudo for system-wide install, or as user for",
" single user install.",
" xdg-uninstall Remove the launcher and icons for the current system as",
" installed by the 'xdg-install' command.",
" win-install Install desktop icon, start menu icon, and registry entries",
" for file association with .nwx files for Windows systems.",
" win-uninstall Remove desktop icon, start menu icon, and registry keys,",
" for the current system. Note that it only removes icons for",
" the version number of the package the command is run from.",
"",
]
# Flags and Variables
makeSetupPyz = False
@@ -880,7 +1146,14 @@ if __name__ == "__main__":
if "help" in sys.argv:
sys.argv.remove("help")
print(helpMsg)
print("\n".join(helpMsg))
sys.exit(0)
if "version" in sys.argv:
sys.argv.remove("version")
numVers, hexVers = extractVersion()
print("Semantic Version: %s" % numVers)
print("Hexadecimal Version: %s" % hexVers)
sys.exit(0)
if "pip" in sys.argv:
@@ -898,6 +1171,14 @@ if __name__ == "__main__":
sys.argv.remove("qthelp")
buildQtDocs()
if "qtlrelease" in sys.argv:
sys.argv.remove("qtlrelease")
buildQtI18n()
if "qtlupdate" in sys.argv:
sys.argv.remove("qtlupdate")
buildQtI18nTS()
if "sample" in sys.argv:
sys.argv.remove("sample")
buildSampleZip()
@@ -918,11 +1199,6 @@ if __name__ == "__main__":
# General Installers
# ==================
if "launcher" in sys.argv:
sys.argv.remove("launcher")
print("The 'launcher' command has been replaced by 'xdg-install'.")
sys.exit(1)
if "xdg-install" in sys.argv:
sys.argv.remove("xdg-install")
if hostOS == OS_WIN:
@@ -931,6 +1207,14 @@ if __name__ == "__main__":
else:
xdgInstall()
if "xdg-uninstall" in sys.argv:
sys.argv.remove("xdg-uninstall")
if hostOS == OS_WIN:
print("ERROR: Command 'xdg-uninstall' cannot be used on Windows")
sys.exit(1)
else:
xdgUninstall()
if "win-install" in sys.argv:
sys.argv.remove("win-install")
if hostOS == OS_WIN:
@@ -939,6 +1223,14 @@ if __name__ == "__main__":
print("ERROR: Command 'win-install' can only be used on Windows")
sys.exit(1)
if "win-uninstall" in sys.argv:
sys.argv.remove("win-uninstall")
if hostOS == OS_WIN:
winUninstall()
else:
print("ERROR: Command 'win-uninstall' can only be used on Windows")
sys.exit(1)
# Windows Setup Installer
# =======================

View File

@@ -1,50 +1,28 @@
# Build and Install novelWriter
The root folder of the repository contains two scripts for setup and install:
The root folder of the repository contains a standard `setup.py` script. The `setup` folder
contains necessary files for the setup process, like icons and other files, and it also contains
additional scripts that are included in the minimal zip packages.
## Setup Script
## Script `setup.py`
The `setup.py` script in the root folder has all the necessary commands for generating packages,
installing icons, launcher, and file associations on Linux and Windows (no macOS support yet).
The `setup.py` is a standard Python setup script with a couple of additional
options:
In addition, it is a wrapper for `setuptools` and accept all commands associated with this tool.
Some of the commands can be targeted towards a different OS than the host OS.
To target the command, add one of `--target-linux`, `--target-darwin` or
`--target-win`.
To list all custom package and install options in this script, run:
```
./setup.py help
```
## Windows Scripts
### General
The `windows_install.bat` and `windows_uninstall.bat` are provided as convenience scripts for
Windows users who aren't used to running commands from command line. They are both located in the
`setup` folder in the source package, but in the minimal zip package for Windows, they can be
found in the root folder.
`help` – Print the help message.
`pip` – Install all package dependencies for novelWriter using pip.
`clean` – Will attempt to delete the `build` and `dist` folders.
### Additional Builds
`qthelp` – Build the help documentation for use with the Qt Assistant. Run
before install to have local help enable in the the installed version
`sample` – Build the sample project as a zip file. Run before install to enable
creating sample projects in the in-app New Project Wizard.
### Python Packaging
`minimal-zip` – Creates a minimal zip file of the core application without all
the other source files.
`pack-pyz` – Creates a pyz package in a folder with all dependencies using the
zipapp tool. On Windows, python embeddable is added to the folder.
`setup-pyz` – Build a Windows installer from a zipapp package using Inno Setup.
### System Install
`install` – Installs novelWriter to the system's Python install location. Run
as root or with sudo for system-wide install, or as user for single user
install.
`xdg-install` – Install launcher and icons for freedesktop systems. Run as root
or with sudo for system-wide install, or as user for single user install.
`win-install` – Install desktop and start menu icons for Windows systems.
These scripts are designed to be double-clicked by the user, but can also be run from the command
line tool. In either case, they will check if Python is properly installed, and then run the
commands to install or uninstall the core dependencies of novelWriter, and then setup or remove the
desktop icon, start menu icon and registry entries.

21
setup/long_description.md Normal file
View File

@@ -0,0 +1,21 @@
<img src="https://raw.githubusercontent.com/vkbo/novelWriter/main/setup/novelwriter.png">
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 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, and other meta data is primarily saved as
JSON files.
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.
novelWriter is developed and maintained by [Veronica Berglyd Olsen](https://github.com/vkbo).
**Website:** https://novelwriter.io
**Documentation:** https://novelwriter.readthedocs.io
**Source Code:** https://github.com/vkbo/novelWriter

BIN
setup/novelwriter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -6,10 +6,21 @@ echo Looking for Python
python --version 2>NUL
if errorlevel 1 goto errorNoPython
echo Python found. OK.
if exist setup.py (
echo setup.py found. OK.
) else (
cd ..
if exist setup.py (
echo setup.py found. OK.
) else (
goto errorNoSetup
)
)
echo.
:: Install the PyQt5, lxml and pyenchant dependencies
python setup.py pip
pip install --user pywin32 -r requirements.txt
:: Create the desktop and start menu icons
python setup.py win-install
@@ -24,5 +35,13 @@ echo.
echo Please download and install it from https://www.python.org/downloads/
echo Also make sure the "Add Python to PATH" option is selected during installation.
echo.
goto:eof
:errorNoSetup
echo.
echo ERROR^: Could not find the setup.py script.
echo.
echo Make sure you run setup.py from the novelWriter root folder.
echo.
pause

View File

@@ -0,0 +1,57 @@
@echo off
:: Check for Python Installation
echo.
echo Looking for Python
python --version 2>NUL
if errorlevel 1 goto errorNoPython
echo Python found. OK.
if exist setup.py (
echo setup.py found. OK.
) else (
cd ..
if exist setup.py (
echo setup.py found. OK.
) else (
goto errorNoSetup
)
)
echo.
echo Ready to remove the novelWriter icons, registry keys, and package dependencies.
set /P c=Are you sure you want to continue [y/n]?
if /I "%c%" EQU "y" goto doUninst
goto afterUninst
:doUninst
:: Remove the desktop and start menu icons
python setup.py win-uninstall
:: Remove the PyQt5, lxml and pyenchant dependencies
pip uninstall --yes -r requirements.txt
:afterUninst
echo.
pause
goto:eof
:errorNoPython
echo.
echo ERROR^: Python is not installed on your system, or cannot be found.
echo.
echo Please download and install it from https://www.python.org/downloads/
echo Also make sure the "Add Python to PATH" option is selected during installation.
echo.
goto:eof
:errorNoSetup
echo.
echo ERROR^: Could not find the setup.py script.
echo.
echo Make sure you run setup.py from the novelWriter root folder.
echo.
pause

View File

@@ -52,45 +52,10 @@ pytest-3 -v -m core
Available markers are:
* `base` for unit tests covering the non-gui classes of the `bw` folder..
* `base` for unit tests covering the non-gui classes of the `nw` folder..
* `core` for unit tests covering the classes in the `nw/core` folder.
* `gui` for unit and integrations tests covering the classes in the `nw/gui` folder.
## Tests
To filter specific groups of tests, use the `-k` switch.
The commands for the respective test categories are listed below.
| Type | Test Target | Source File(s) | Marker | Filter |
| :---------- | :----------------------- | :--------------------- | :-------- | :----------------------- |
| Unit | Main function | nw/\_\_init\_\_.py | `-m base` | `-k testBaseInit` |
| Unit | Common functions | nw/common.py | `-m base` | `-k testBaseCommon` |
| Unit | Config class | nw/config.py | `-m base` | `-k testBaseConfig` |
| Unit | Error handlers | nw/error.py | `-m base` | `-k testBaseError` |
| Unit | Core functions | nw/core/tools.py | `-m core` | `-k testCoreTools` |
| Unit | NWDoc class | nw/core/document.py | `-m core` | `-k testCoreDocument` |
| Unit | NWIndex class | nw/core/index.py | `-m core` | `-k testCoreIndex` |
| Unit | NWItem class | nw/core/item.py | `-m core` | `-k testCoreItem` |
| Unit | NWProject class | nw/core/project.py | `-m core` | `-k testCoreProject` |
| Unit | NWSpell* classes | nw/core/spellcheck.py | `-m core` | `-k testCoreSpell` |
| Unit | NWStatus class | nw/core/status.py | `-m core` | `-k testCoreStatus` |
| Unit | NWTree class | nw/core/tree.py | `-m core` | `-k testCoreTree` |
| Unit | OptionsState class | nw/core/options.py | `-m core` | `-k testCoreOptions` |
| Unit | ToHtml class | nw/core/tohtml.py | `-m core` | `-k testCoreToHtml` |
| Unit | Tokenizer class | nw/core/tokenizer.py | `-m core` | `-k testCoreToken` |
| Integration | About Dialogs | nw/gui/about.py | `-m gui` | `-k testGuiAbout` |
| Integration | Build Novel Project Tool | nw/gui/build.py | `-m gui` | `-k testGuiBuild` |
| Integration | Document Editor Widget | nw/gui/doceditor.py | `-m gui` | `-k testGuiEditor` |
| Integration | Document Viewer Widget | nw/gui/docviewer.py | `-m gui` | `-k testGuiViewer` |
| Integration | Item Editor Dialog | nw/gui/itemeditor.py | `-m gui` | `-k testGuiItemEditor` |
| Integration | Menu Widgets | nw/gui/mainmenu.py | `-m gui` | `-k testGuiMenu` |
| Integration | Merge Tool | nw/gui/docmerge.py | `-m gui` | `-k testGuiMergeSplit` |
| Integration | Outline Widget | nw/gui/outline.py | `-m gui` | `-k testGuiOutline` |
| Integration | Preferences Dialog | nw/gui/preferences.py | `-m gui` | `-k testGuiPreferences` |
| Integration | Project Load Dialog | nw/gui/projload.py | `-m gui` | `-k testGuiProjLoad` |
| Integration | Project Settings Dialog | nw/gui/projsettings.py | `-m gui` | `-k testGuiProjSettings` |
| Integration | Project Tree Widget | nw/gui/projtree.py | `-m gui` | `-k testGuiProjTree` |
| Integration | Theme/Icon Classes | nw/gui/theme.py | `-m gui` | `-k testGuiTheme` |
| Integration | Split Tool | nw/gui/docsplit.py | `-m gui` | `-k testGuiMergeSplit` |
| Integration | Writing Stats Dialog | nw/gui/writingstats.py | `-m gui` | `-k testGuiWritingStats` |
| Integration | Various Dialogs | N/A | `-m gui` | `-k testGuiDialogs` |
You can filter tests further with the `-k` switch, all the way down to a single test. You can for
instance run only dialog tests with `-k testDlg` or tools with `-k testTool`.

View File

@@ -61,6 +61,14 @@ def refDir():
theDir = os.path.join(testDir, "reference")
return theDir
@pytest.fixture(scope="session")
def filesDir():
"""The folder where additional test files are stored.
"""
testDir = os.path.dirname(__file__)
theDir = os.path.join(testDir, "files")
return theDir
@pytest.fixture(scope="session")
def outDir(tmpDir):
"""An output folder for test results
@@ -110,20 +118,36 @@ def tmpConf(tmpDir):
theConf = Config()
theConf.initConfig(tmpDir, tmpDir)
theConf.setLastPath("")
theConf.guiLang = "en_GB"
return theConf
@pytest.fixture(scope="function")
def dummyGUI(tmpConf):
def fncConf(fncDir):
"""Create a temporary novelWriter configuration object.
"""
confFile = os.path.join(fncDir, "novelwriter.conf")
if os.path.isfile(confFile):
os.unlink(confFile)
theConf = Config()
theConf.initConfig(fncDir, fncDir)
theConf.setLastPath("")
theConf.guiLang = "en_GB"
return theConf
@pytest.fixture(scope="function")
def dummyGUI(monkeypatch, tmpConf):
"""Create a dummy instance of novelWriter's main GUI class.
"""
monkeypatch.setattr("nw.CONFIG", tmpConf)
theDummy = DummyMain()
theDummy.mainConf = tmpConf
return theDummy
@pytest.fixture(scope="function")
def nwGUI(qtbot, fncDir):
def nwGUI(qtbot, monkeypatch, fncDir, fncConf):
"""Create an instance of the novelWriter GUI.
"""
monkeypatch.setattr("nw.CONFIG", fncConf)
nwGUI = nw.main(["--testmode", "--config=%s" % fncDir, "--data=%s" % fncDir])
qtbot.addWidget(nwGUI)
nwGUI.show()

View File

@@ -91,6 +91,16 @@ class DummyStatusBar():
# END Class DummyStatusBar
class DummyApp:
def __init__(self):
return
def installTranslator(self, theLang):
return
# END Class DummyApp
# =========================================================================== #
# Error Functions
# Dummy functions that will raise errors instead.

1
tests/files/nw_en_GB.qm Normal file
View File

@@ -0,0 +1 @@
<<3C>d<18><EFBFBD>!<1C>`<60><><EFBFBD>

View File

@@ -10,6 +10,7 @@
</project>
<settings>
<doBackup>False</doBackup>
<language>en</language>
<spellCheck>False</spellCheck>
<spellLang>None</spellLang>
<autoOutline>True</autoOutline>

View File

@@ -11,6 +11,7 @@
</project>
<settings>
<doBackup>True</doBackup>
<language>en</language>
<spellCheck>False</spellCheck>
<spellLang>None</spellLang>
<autoOutline>True</autoOutline>

View File

@@ -1,15 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pstats
import os
profDir = os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir, "prof"))
print("")
print("Profiles directory: %s" % profDir)
print("")
profMainWindows = pstats.Stats(os.path.join(profDir, "testMainWindows.prof"))
profMainWindows.sort_stats("cumtime")
profMainWindows.print_stats("nw/")

View File

@@ -1,5 +1,5 @@
[Main]
timestamp = 2021-02-09 21:07:03
timestamp = 2021-02-15 16:31:24
theme = default
syntax = default_light
icons = typicons_colour_light
@@ -7,9 +7,11 @@ guidark = False
guifont =
guifontsize = 11
lastnotes = 0x0
guilang = en-GB
[Sizes]
geometry = 1200, 650
preferences = 700, 615
treecols = 200, 50, 30
novelcols = 200, 50
projcols = 200, 60, 140
@@ -46,6 +48,9 @@ autoscroll = False
autoscrollpos = 30
fmtsinglequote = ‘, ’
fmtdoublequote = “, ”
fmtpadbefore =
fmtpadafter =
fmtpadthin = False
spelltool = internal
spellcheck = en
showtabsnspaces = False

View File

@@ -11,6 +11,7 @@
</project>
<settings>
<doBackup>True</doBackup>
<language>None</language>
<spellCheck>False</spellCheck>
<spellLang>None</spellLang>
<autoOutline>True</autoOutline>

View File

@@ -11,6 +11,7 @@
</project>
<settings>
<doBackup>True</doBackup>
<language>None</language>
<spellCheck>False</spellCheck>
<spellLang>None</spellLang>
<autoOutline>True</autoOutline>

View File

@@ -9,6 +9,7 @@
</project>
<settings>
<doBackup>True</doBackup>
<language>None</language>
<spellCheck>False</spellCheck>
<spellLang>None</spellLang>
<autoOutline>True</autoOutline>

View File

@@ -9,6 +9,7 @@
</project>
<settings>
<doBackup>True</doBackup>
<language>None</language>
<spellCheck>False</spellCheck>
<spellLang>None</spellLang>
<autoOutline>True</autoOutline>

View File

@@ -9,6 +9,7 @@
</project>
<settings>
<doBackup>True</doBackup>
<language>None</language>
<spellCheck>False</spellCheck>
<spellLang>None</spellLang>
<autoOutline>True</autoOutline>

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