Compare commits

..

452 Commits
v2.5 ... v2.6

Author SHA1 Message Date
Veronica Berglyd Olsen
448057edf6 Release 2.6 (#2208) 2025-01-26 20:05:33 +01:00
Veronica Berglyd Olsen
9d993b2992 Fix typo in changelog 2025-01-26 20:05:03 +01:00
Veronica Berglyd Olsen
8ca13f6e08 Update changelog with last changes 2025-01-26 20:01:34 +01:00
Veronica Berglyd Olsen
80fc4c9538 Merge branch 'release' into release_2.6 2025-01-26 19:56:51 +01:00
Veronica Berglyd Olsen
9f8867c1b4 Update i18n for 2.6 final (#2207) 2025-01-26 19:56:32 +01:00
Veronica Berglyd Olsen
481ce883ab Update Italian translation 2025-01-26 18:36:25 +01:00
Veronica Berglyd Olsen
63f184dd60 Update Codecov action to v5 2025-01-26 18:34:44 +01:00
Veronica Berglyd Olsen
3d4d9ed111 Merge branch 'release' into i18n 2025-01-26 18:31:21 +01:00
Veronica Berglyd Olsen
b2ba932735 Update docs for 2.6 (#2210) 2025-01-26 18:31:00 +01:00
Veronica Berglyd Olsen
8c1070b318 Disable flaky test on Windows 2025-01-26 18:26:45 +01:00
Veronica Berglyd Olsen
6c4d8f3ede Update tests with change of default config 2025-01-26 18:19:12 +01:00
Veronica Berglyd Olsen
f2c118776a Update the project sections of the docs to 2.6 2025-01-26 18:15:36 +01:00
Veronica Berglyd Olsen
87890dd249 Update the Introduction section of the docs to 2.6 2025-01-26 16:17:56 +01:00
Veronica Berglyd Olsen
15301c9121 Bump version and update changelog 2025-01-25 22:00:14 +01:00
Veronica Berglyd Olsen
8dc4b661d8 Update final translations for de, en, ja, nb and pl 2025-01-25 21:43:41 +01:00
Veronica Berglyd Olsen
d47ec4157d Update completed translations before #2206 2025-01-25 21:41:44 +01:00
Veronica Berglyd Olsen
9d64a59b70 Update project translations 2025-01-25 21:38:59 +01:00
Veronica Berglyd Olsen
234fc20048 Change chapter heading level to H1 (#2206) 2025-01-25 21:36:13 +01:00
Veronica Berglyd Olsen
8432dfdd1f Extend ODT and DocX tests 2025-01-25 21:18:30 +01:00
Veronica Berglyd Olsen
5fddfd395e Update HTML output 2025-01-25 21:06:20 +01:00
Veronica Berglyd Olsen
11d0e4220f Use alternative level 1 format for titles and partitions for markdown 2025-01-25 20:51:24 +01:00
Veronica Berglyd Olsen
6d4e066810 Update other tests 2025-01-25 16:17:31 +01:00
Veronica Berglyd Olsen
9f1274f953 Update ODT and DocX to work with new chapter headers 2025-01-25 16:17:19 +01:00
Veronica Berglyd Olsen
9ac1692e2a Drop separate types for chapters, scenes and sections as they seem to not be needed 2025-01-25 16:11:57 +01:00
Veronica Berglyd Olsen
6f88357977 Update QTextDocument to work with new chapter headers 2025-01-25 15:35:02 +01:00
Veronica Berglyd Olsen
19bdbf9aca Update build settings margin labels 2025-01-25 15:26:25 +01:00
Veronica Berglyd Olsen
5a25d34ba3 Update HTML to work with new chapter headers 2025-01-25 15:05:11 +01:00
Veronica Berglyd Olsen
13e6199af2 Separate ordinary headers and novel headers in the Tokenizer 2025-01-25 14:58:06 +01:00
Veronica Berglyd Olsen
5be1e2c954 Make some minor code and GUI fixes (#2202) 2025-01-22 01:05:28 +01:00
Veronica Berglyd Olsen
306bd245fc Block tree context menu in blank area 2025-01-22 00:58:55 +01:00
Veronica Berglyd Olsen
7e42d04e36 Drop deleteLater for context menus 2025-01-22 00:53:18 +01:00
Veronica Berglyd Olsen
53dfdb3a2e Fix potential undefined variable in editor 2025-01-22 00:48:59 +01:00
Veronica Berglyd Olsen
eb113a8928 Make sure status label in item details panel can wrap 2025-01-22 00:48:03 +01:00
Veronica Berglyd Olsen
c369ef450f Fix add dicitonary tool (#2189) 2025-01-15 20:56:08 +01:00
Veronica Berglyd Olsen
6674cf99a6 Update spell check dictionary download links 2025-01-15 20:28:53 +01:00
Veronica Berglyd Olsen
70cca405f5 Make sure selection model exists before accessing (#2181) 2025-01-11 18:43:47 +01:00
Veronica Berglyd Olsen
d7d211baf2 Fix empty selection model bug (#2173) 2025-01-11 18:39:23 +01:00
Veronica Berglyd Olsen
d5a35fa86f Fix novel selector bug (#2179) 2025-01-11 18:37:06 +01:00
Veronica Berglyd Olsen
dc265e57f3 Store previous selection before clear in novel selector widget 2025-01-11 18:30:53 +01:00
Veronica Berglyd Olsen
4b79b2bd57 Fix i18n sources (#2169) 2025-01-10 01:06:24 +01:00
Veronica Berglyd Olsen
80c9e54a9a Update translation files 2025-01-10 01:00:16 +01:00
Veronica Berglyd Olsen
eba0b607a4 Fix translation issues 2025-01-10 00:59:53 +01:00
Veronica Berglyd Olsen
04464a2798 Separate some constants due to duplicate words 2025-01-09 23:51:29 +01:00
Veronica Berglyd Olsen
202b5192ed Release 2.6 RC 1 (#2168) 2025-01-09 23:07:24 +01:00
Veronica Berglyd Olsen
62397225f9 Update translations 2025-01-09 22:58:28 +01:00
Veronica Berglyd Olsen
b220fffd6f Bump version to 2.6 RC 1 and update changelog 2025-01-09 22:54:19 +01:00
Veronica Berglyd Olsen
e14f7881a3 Fix search panel theme update bug (#2167) 2025-01-09 22:42:47 +01:00
Veronica Berglyd Olsen
2085fbe107 Drop the wrapper widget for the search tool header (#2162) 2025-01-09 22:36:11 +01:00
Veronica Berglyd Olsen
31a75f044e Fix minor issues with some constants (#2166) 2025-01-09 22:27:08 +01:00
Veronica Berglyd Olsen
5b74125fcd Merge branch 'main' into minor_fixes 2025-01-09 22:22:39 +01:00
Veronica Berglyd Olsen
1cf3cfcf77 Fix typo in constant 2025-01-09 22:15:33 +01:00
Veronica Berglyd Olsen
defaf6c1ee Fix constant in error handler 2025-01-09 22:15:20 +01:00
Veronica Berglyd Olsen
e0f467fa10 Fix minor bug from in editor counter (#2157) 2025-01-04 01:42:21 +01:00
Veronica Berglyd Olsen
362da679b6 Fix minor issue introduced by selection counter update 2025-01-04 01:38:06 +01:00
Veronica Berglyd Olsen
0b35bcfc3b Fix minor issue with editor word counter (#2156) 2025-01-03 23:47:24 +01:00
Veronica Berglyd Olsen
fad162c6c1 Give selection counter priority in editor footer (#2155) 2025-01-03 23:42:41 +01:00
Veronica Berglyd Olsen
97fc64fcae Update copyright (#2154) 2025-01-03 12:02:48 +00:00
Veronica Berglyd Olsen
5224234637 Make sure imported status labels are saved (#2153) 2024-12-31 01:05:38 +01:00
Veronica Berglyd Olsen
b4f1d0ec77 Fix bug in status label import 2024-12-31 00:59:25 +01:00
Veronica Berglyd Olsen
5911c43b5b Add Import/export of status labels (#2152) 2024-12-31 00:40:52 +01:00
Veronica Berglyd Olsen
4f137466c5 Fix test for Windows 2024-12-31 00:38:02 +01:00
Veronica Berglyd Olsen
3836a2a56c Fix column stretch setting 2024-12-31 00:35:52 +01:00
Veronica Berglyd Olsen
49e59d74cc Update tests 2024-12-31 00:28:42 +01:00
Veronica Berglyd Olsen
9461eea03a Add import/export for status labels 2024-12-31 00:14:47 +01:00
Veronica Berglyd Olsen
136939d391 Make some general import/export functionality improvements 2024-12-31 00:14:25 +01:00
Veronica Berglyd Olsen
33aea82e6e Remove "Apply" buttons in Project Settings (#2151) 2024-12-30 17:00:18 +01:00
Veronica Berglyd Olsen
4a6511b39f Add some minor code cleanup 2024-12-30 16:55:54 +01:00
Veronica Berglyd Olsen
6448f05557 Also remove apply button from auto-replace settings 2024-12-30 16:50:34 +01:00
Veronica Berglyd Olsen
c08eca7c3b Update test 2024-12-30 16:39:44 +01:00
Veronica Berglyd Olsen
df6ca2e17b Drop the status/importance Apply button (#2150) 2024-12-30 16:34:54 +01:00
Veronica Berglyd Olsen
c39a5f9708 Hide session timer (#2149) 2024-12-29 18:02:17 +01:00
Veronica Berglyd Olsen
46d69c31d1 Update docs 2024-12-29 17:43:54 +01:00
Veronica Berglyd Olsen
e924d121bf Update tests 2024-12-29 17:38:21 +01:00
Veronica Berglyd Olsen
2fa1dcccac Use correct signal naming 2024-12-29 17:38:03 +01:00
Veronica Berglyd Olsen
dc9413fa7e Show or hide the status bar timer when it is clicked 2024-12-29 17:30:03 +01:00
Veronica Berglyd Olsen
1b34bc30c7 Add a modified label widget that emits a click signal 2024-12-29 17:29:07 +01:00
Veronica Berglyd Olsen
4d33f087df Fix project tree status/importance for folders (#2147) 2024-12-29 16:33:38 +01:00
Veronica Berglyd Olsen
81a4d5fadb Fix wrong check type for single item status label chang (#2145) 2024-12-29 16:15:56 +01:00
Veronica Berglyd Olsen
7682ae0425 Update MacOS DMG build (#2141) 2024-12-23 15:57:46 +01:00
Veronica Berglyd Olsen
677274adb3 Try a different MacOS image 2024-12-23 15:37:28 +01:00
Veronica Berglyd Olsen
83fca9fd94 Release 2.6 Beta 2 (#2139) 2024-12-23 15:06:51 +01:00
Veronica Berglyd Olsen
4150a9740b Bump version and update changelog 2024-12-23 15:00:01 +01:00
Veronica Berglyd Olsen
f7021559d0 Refactor quoted dialogue detection (#2138) 2024-12-23 14:23:53 +01:00
Veronica Berglyd Olsen
00d4c550f1 Update tests 2024-12-23 14:20:02 +01:00
Veronica Berglyd Olsen
97903993c8 Restructure dialogue detection from quotes 2024-12-23 14:19:47 +01:00
Veronica Berglyd Olsen
3d858ed9ab Remove boundary condition in quote dialogue highlighting in most cases (#2131) 2024-12-09 00:23:48 +01:00
Veronica Berglyd Olsen
cc5cd50065 Improve editor auto-replace (#2132) 2024-12-05 23:41:22 +01:00
Veronica Berglyd Olsen
0c0d909dcb Remove debug code 2024-12-05 23:34:12 +01:00
Veronica Berglyd Olsen
8dafaa13c0 Use slots for some of the often called attributes in the editor 2024-12-05 23:01:17 +01:00
Veronica Berglyd Olsen
05385ba73b Improve the text auto-replace class 2024-12-05 22:57:43 +01:00
Veronica Berglyd Olsen
924a2c3381 Move editor auto-replace to a separate class 2024-12-05 22:40:15 +01:00
Veronica Berglyd Olsen
6502c424e2 Align headers to bottom of page margin for ODT and DocX (#2128) 2024-11-29 12:52:50 +01:00
Veronica Berglyd Olsen
243124a4ba Update tests 2024-11-29 00:41:15 +01:00
Veronica Berglyd Olsen
4a2a14bc05 Make header margins similar in ODT and DocX files (#2120) 2024-11-29 00:40:45 +01:00
Veronica Berglyd Olsen
976328c9dc Fix font style and PDF issues (#2122) 2024-11-27 22:59:20 +01:00
Veronica Berglyd Olsen
3b0086e75e Use a frame for page break marker in manuscript preview 2024-11-26 23:56:31 +01:00
Veronica Berglyd Olsen
7c38a748b6 Merge branch 'main' into font_styles 2024-11-26 22:58:32 +01:00
Veronica Berglyd Olsen
4d98689304 Merge release 2.5.3 2024-11-26 22:57:51 +01:00
Veronica Berglyd Olsen
8307822e20 Release 2.5.3 (#2126) 2024-11-26 22:27:30 +01:00
Veronica Berglyd Olsen
f1548dc8ea Fix some issues in the changelog 2024-11-26 22:23:15 +01:00
Veronica Berglyd Olsen
25b33ab4d3 Bump version to 2.5.3 and update changelog 2024-11-26 22:21:30 +01:00
Veronica Berglyd Olsen
4af98dcab5 Add Russian translation 2024-11-26 22:13:55 +01:00
Veronica Berglyd Olsen
3cf916c845 Update translations 2024-11-26 22:10:43 +01:00
Veronica Berglyd Olsen
bcac05ca20 Only use 1200 DPI for scalable fonts 2024-11-26 21:42:54 +01:00
Veronica Berglyd Olsen
07c61450c7 Try forcing resolution for PDF printer 2024-11-26 18:57:07 +01:00
Veronica Berglyd Olsen
43d95462e0 Optimise a few details and add some tests 2024-11-26 17:07:02 +01:00
Veronica Berglyd Olsen
c091e0b9bb Apply the font matcher closer to config read 2024-11-26 17:06:34 +01:00
Veronica Berglyd Olsen
18c70ffe3e Allow setting resolution for ToQTextDocument converter 2024-11-26 02:22:36 +01:00
Veronica Berglyd Olsen
ff470028be Add a font matcher for text fonts (#2118) 2024-11-26 02:03:09 +01:00
Veronica Berglyd Olsen
8463188336 Fix default font in editor 2024-11-26 01:06:47 +01:00
Veronica Berglyd Olsen
2808c36753 Rename tokenizer setFont to setTextFont 2024-11-26 00:40:09 +01:00
Veronica Berglyd Olsen
05b1fdd7f2 Drop relative font weights in ODT output 2024-11-25 23:43:38 +01:00
Veronica Berglyd Olsen
00017e8065 Reset to default font style after emphasis in QTextDocuments (#2121) 2024-11-25 23:36:35 +01:00
Veronica Berglyd Olsen
ff34112532 Rewrite project tree as a model/view set (#2119) 2024-11-25 00:11:53 +01:00
Veronica Berglyd Olsen
4cadd1597e Add coverage to remaining missed lines 2024-11-25 00:07:32 +01:00
Veronica Berglyd Olsen
e30d2cbbf1 Always use forward slash for ToC file 2024-11-24 19:25:46 +01:00
Veronica Berglyd Olsen
0449cf824d Add test coverage of the gui project tree 2024-11-24 18:39:23 +01:00
Veronica Berglyd Olsen
32d899dbfb Fix windows path 2024-11-24 02:53:54 +01:00
Veronica Berglyd Olsen
9ec9c55c3f Add test coverage of the project tree 2024-11-24 02:48:17 +01:00
Veronica Berglyd Olsen
01b2b53fd5 Add test coverage of the item model 2024-11-24 00:43:01 +01:00
Veronica Berglyd Olsen
be6888f8c8 Fix Python 3.9 incompatible type 2024-11-23 20:29:02 +01:00
Veronica Berglyd Olsen
c1ed0647fe Refactor project signal proxies 2024-11-23 19:56:24 +01:00
Veronica Berglyd Olsen
1bf42ceb55 Update tests other than tree-related 2024-11-23 19:30:29 +01:00
Veronica Berglyd Olsen
ca8bd8d271 Disable project tree tests in core and gui 2024-11-22 17:04:53 +01:00
Veronica Berglyd Olsen
bf23a4920f Merge branch 'main' into project_model 2024-11-22 16:46:50 +01:00
Veronica Berglyd Olsen
cfb048ec19 Update log formatting again (#2117) 2024-11-22 16:46:28 +01:00
Veronica Berglyd Olsen
5284bea0f0 Rewrite log colouring 2024-11-22 16:41:11 +01:00
Veronica Berglyd Olsen
1244a4058c Merge branch 'main' into project_model 2024-11-22 02:22:47 +01:00
Veronica Berglyd Olsen
8c51363651 Colourise log output (#2114) 2024-11-22 02:22:10 +01:00
Veronica Berglyd Olsen
893f18c7ad Add test coverage 2024-11-22 02:18:31 +01:00
Veronica Berglyd Olsen
909c97feda Add ANSI colours to log output 2024-11-22 01:47:21 +01:00
Veronica Berglyd Olsen
fea989c1f8 Update comments 2024-11-22 00:56:33 +01:00
Veronica Berglyd Olsen
a64ed405f3 Add some clear data functions 2024-11-22 00:44:14 +01:00
Veronica Berglyd Olsen
ffe6cd0a65 Update item change signals 2024-11-22 00:15:12 +01:00
Veronica Berglyd Olsen
e0b27317f3 Make sure deleted documents are closed if open 2024-11-21 22:24:34 +01:00
Veronica Berglyd Olsen
7ed619cf2e Restore project item emphasis 2024-11-21 01:33:09 +01:00
Veronica Berglyd Olsen
d55f53be0a Clean up item notification and item main icon 2024-11-21 01:18:28 +01:00
Veronica Berglyd Olsen
55eed706fb Re-implement word counters 2024-11-21 00:14:54 +01:00
Veronica Berglyd Olsen
2f71ec68d8 Re-implement row highlight, empty trash, and fix recursive permanent delete 2024-11-20 23:07:40 +01:00
Veronica Berglyd Olsen
a45ab2f017 Remove tooltip for non-checkable items 2024-11-20 22:36:44 +01:00
Veronica Berglyd Olsen
26b4da9eb4 Fix most tests with minor issues 2024-11-20 19:58:34 +01:00
Veronica Berglyd Olsen
4897f6ad69 Update tests 2024-11-20 18:48:54 +01:00
Veronica Berglyd Olsen
bbe8683d59 Re-implement project item duplication 2024-11-20 18:48:14 +01:00
Veronica Berglyd Olsen
072be78991 Re-enable project tree context menu and clean up split and merge 2024-11-20 16:42:08 +01:00
Veronica Berglyd Olsen
3f41e62e21 Move some variables around 2024-11-20 16:41:26 +01:00
Veronica Berglyd Olsen
ec01336c21 Fix active status icon and tooltip 2024-11-20 13:06:49 +01:00
Veronica Berglyd Olsen
5dcfe5c874 Allow drag and drop to open documents 2024-11-20 02:12:28 +01:00
Veronica Berglyd Olsen
72f1bc2d77 Add model support for drag and drop 2024-11-20 01:31:56 +01:00
Veronica Berglyd Olsen
54685c8184 Reimplement item deletion 2024-11-19 23:26:01 +01:00
Veronica Berglyd Olsen
3532306833 Add move to trash functionality 2024-11-19 17:48:01 +01:00
Veronica Berglyd Olsen
442b065834 Handle item consistency in the nodes tree 2024-11-19 17:47:21 +01:00
Veronica Berglyd Olsen
53e70c118c Add back create new project item functionality 2024-11-19 00:22:59 +01:00
Veronica Berglyd Olsen
a5575992d6 Re-enable project tree move item and navigation 2024-11-18 18:04:25 +01:00
Veronica Berglyd Olsen
df0244095f Rewrite how project items are refreshed on the GUI 2024-11-18 17:51:05 +01:00
Veronica Berglyd Olsen
337f05f60e Re-enable item rename 2024-11-18 00:01:40 +01:00
Veronica Berglyd Olsen
4f8e898440 Switch data tree to new model structure 2024-11-17 23:24:49 +01:00
Veronica Berglyd Olsen
7991b9b27c Re-enable item selection 2024-11-17 22:41:45 +01:00
Veronica Berglyd Olsen
ef42e1956c Merge new and old project tree 2024-11-17 22:07:37 +01:00
Veronica Berglyd Olsen
27b3dde6f6 Complete build initial tree and save tree logic 2024-11-17 21:24:55 +01:00
Veronica Berglyd Olsen
cbe557507f Add a basic project tree model 2024-11-17 19:25:55 +01:00
Veronica Berglyd Olsen
7a9da5f219 Merge branch 'release' 2024-11-16 20:58:09 +01:00
Veronica Berglyd Olsen
d944a70063 Fix secondary issues due to Qt tree widget bug (#2109) 2024-11-16 20:35:14 +01:00
Veronica Berglyd Olsen
c20f970405 Block drops on project tree viewport, and allow non-root items on root to be deleted (#2108) 2024-11-16 20:21:57 +01:00
Veronica Berglyd Olsen
49bb7f3a34 Fix default button issue on Build dialog (#2105) 2024-11-15 21:51:25 +01:00
Veronica Berglyd Olsen
021827ff2d Force the Build button to be default on the build dialog (#2101) 2024-11-15 20:49:45 +01:00
Veronica Berglyd Olsen
97312aecf5 Fix PDF formatting (#2104) 2024-11-15 20:25:29 +01:00
Veronica Berglyd Olsen
f3c9a77989 Revert to QPrinter, but update paint device 2024-11-15 19:31:55 +01:00
Veronica Berglyd Olsen
2c031bc093 Skip anchors when generating PDF 2024-11-15 01:34:02 +01:00
Veronica Berglyd Olsen
69147c2480 Correct pixel units 2024-11-15 01:27:49 +01:00
Veronica Berglyd Olsen
22ca0045bc Correct font scaling on PDF output 2024-11-15 00:40:58 +01:00
Veronica Berglyd Olsen
af431a4766 Merge branch 'main' into pdf_format_fixes 2024-11-14 23:20:12 +01:00
Veronica Berglyd Olsen
5a8045131b Update editor toolbar icon (#2103) 2024-11-14 23:18:43 +01:00
Veronica Berglyd Olsen
7c0a6f1d02 Use correct em calculation in qdoc class 2024-11-14 23:18:22 +01:00
Veronica Berglyd Olsen
4fc69ef7c6 Replace printer with Qt's PDF writer 2024-11-14 23:17:54 +01:00
Veronica Berglyd Olsen
91e502deb6 Add a new icon for the editor toolbar 2024-11-14 21:10:33 +01:00
Veronica Berglyd Olsen
1ee191220f Refactor manuscript format text scaling and text margin settings (#2102) 2024-11-13 23:22:28 +01:00
Veronica Berglyd Olsen
a96ccf8422 Add icons for text margins 2024-11-13 23:16:54 +01:00
Veronica Berglyd Olsen
57641ecd7d Make page number work with Office 365 2024-11-13 20:50:47 +01:00
Veronica Berglyd Olsen
ed431aa30f Update tests 2024-11-13 20:40:44 +01:00
Veronica Berglyd Olsen
7a111bb173 Update default margins 2024-11-13 20:24:10 +01:00
Veronica Berglyd Olsen
220be06631 Make HTML margins scale to em units 2024-11-13 20:05:03 +01:00
Veronica Berglyd Olsen
d8c14f6712 Make ODT margins scale to em units 2024-11-13 20:04:52 +01:00
Veronica Berglyd Olsen
e514b1be4f Fix DocX line height setting 2024-11-13 19:51:36 +01:00
Veronica Berglyd Olsen
5e7ef1d25a Minor improvements for 2.6 beta release (#2095) 2024-11-13 18:43:11 +01:00
Veronica Berglyd Olsen
c2b3eb4ac2 Fix issue of alternative unicode symbols being replaced too early 2024-11-13 00:03:38 +01:00
Veronica Berglyd Olsen
1bd8370864 Update dialogue highlighting settings text in Preferences 2024-11-12 23:10:57 +01:00
Veronica Berglyd Olsen
8c952953e3 Release 2.6 Beta 1 (#2092) 2024-11-12 17:42:59 +01:00
Veronica Berglyd Olsen
c065c9b30f Fix a couple of mistakes in the changelog 2024-11-12 17:35:59 +01:00
Veronica Berglyd Olsen
a18eb1158e Bump version to 2.6b1 and update changelog 2024-11-12 00:38:49 +01:00
Veronica Berglyd Olsen
08ef352288 GUI improvements and fixes (#2089) 2024-11-08 22:02:35 +01:00
Veronica Berglyd Olsen
25297b8b0b Add some coverage settings to the main project file 2024-11-08 17:34:06 +01:00
Veronica Berglyd Olsen
ea591fe707 Fix bug in outline view preserving details panel split position 2024-11-08 17:31:23 +01:00
Veronica Berglyd Olsen
b0bcf8605c Fix closing app with no project bug 2024-11-08 17:30:54 +01:00
Veronica Berglyd Olsen
b28921d6c2 Add an edit button to the viewer (#1963) 2024-11-08 17:30:10 +01:00
Veronica Berglyd Olsen
87ae7585e8 Add status column to project outline (#2088) 2024-11-08 00:37:57 +01:00
Veronica Berglyd Olsen
b39b37fb2f Add timing to outline build 2024-11-08 00:34:21 +01:00
Veronica Berglyd Olsen
cd9e0de9dd Update test 2024-11-07 23:59:52 +01:00
Veronica Berglyd Olsen
5107849909 Add status column to outline 2024-11-07 23:55:59 +01:00
Veronica Berglyd Olsen
5c988960f5 Revert a mistake in page break cleanup 2024-11-07 19:05:49 +01:00
Veronica Berglyd Olsen
44792778aa Simplify collection of raw text (#2087) 2024-11-07 19:03:34 +01:00
Veronica Berglyd Olsen
0a7bd68258 Simplify collection of raw text 2024-11-07 18:56:16 +01:00
Veronica Berglyd Olsen
30118eceaa Make page breaks visible in manuscript preview (#2086) 2024-11-07 18:39:10 +01:00
Veronica Berglyd Olsen
87512eb13b Rename variable 2024-11-07 18:33:53 +01:00
Veronica Berglyd Olsen
9919e1a49e Add new features to new project title page 2024-11-07 18:31:08 +01:00
Veronica Berglyd Olsen
d04a21b70f Improve page break handling in the tokenizer 2024-11-07 17:53:19 +01:00
Veronica Berglyd Olsen
fc6e0a577f Add a switch to enable/disable showing page breaks in the manuscript tool 2024-11-07 17:52:56 +01:00
Veronica Berglyd Olsen
0c659422b7 Add page break markers to the QTextDocument format class 2024-11-07 17:52:01 +01:00
Veronica Berglyd Olsen
2018023dc7 Remove redundant if-checks 2024-11-07 14:34:35 +01:00
Veronica Berglyd Olsen
04645cab71 Merge bugfix for #2080 from 2.5 release branch 2024-11-05 18:46:12 +01:00
Veronica Berglyd Olsen
bec082c0ae Fix HTML formatting issue (#2085) 2024-11-05 18:44:05 +01:00
Veronica Berglyd Olsen
83b244a8a3 Fix head tags order in HTML output (#2080) 2024-11-05 18:39:43 +01:00
Veronica Berglyd Olsen
ae261381aa Add build copy feature (#2084) 2024-11-05 18:38:32 +01:00
Veronica Berglyd Olsen
b088b79ee8 Add test coverage 2024-11-05 18:32:02 +01:00
Veronica Berglyd Olsen
0b59ee8634 Add duplicate build feature 2024-11-05 18:17:43 +01:00
Veronica Berglyd Olsen
f62b755903 Add copy button icon 2024-11-05 18:17:21 +01:00
Veronica Berglyd Olsen
04036ef35c Improve dialogue highlighting after feedback (#2082) 2024-11-05 17:34:04 +01:00
Veronica Berglyd Olsen
e4a2ca2d19 Add horizontal bar to em dash conversion in tokenizer 2024-11-04 17:32:22 +01:00
Veronica Berglyd Olsen
89f452f3fc Restructure and improve dialogue highlighting 2024-11-04 17:28:21 +01:00
Veronica Berglyd Olsen
843f8e0d39 Add missing line in dialogue highlighting check 2024-11-04 00:21:25 +01:00
Veronica Berglyd Olsen
8618306230 Update dialogue handling again (#2081) 2024-11-03 22:40:17 +01:00
Veronica Berglyd Olsen
34a3e17326 Fix quotes regex issue and update tests 2024-11-03 22:09:16 +01:00
Veronica Berglyd Olsen
5af49dfc6f Add a separate setting for alternating dialogue/narration 2024-11-03 21:45:00 +01:00
Veronica Berglyd Olsen
837d3060b8 Separate single and double quoted dialogue 2024-11-03 21:14:36 +01:00
Veronica Berglyd Olsen
2754f78083 Merge Spanish and Portuguese narrator break logic 2024-11-03 21:14:11 +01:00
Veronica Berglyd Olsen
1b509ad60b Add a dialogue parser, and horizontal bar auto-complete (#2079) 2024-11-01 22:23:16 +01:00
Veronica Berglyd Olsen
82e923b181 Remove debug code 2024-11-01 22:11:35 +01:00
Veronica Berglyd Olsen
e92b8400c7 Add dialogue parser test coverage 2024-11-01 22:07:47 +01:00
Veronica Berglyd Olsen
1ad6a80682 Update auto-replace in editor, and update tests 2024-11-01 21:17:46 +01:00
Veronica Berglyd Olsen
f355b82bba Add auto-replace for horizontal bar 2024-11-01 20:49:16 +01:00
Veronica Berglyd Olsen
66567b6d6f Allow up to 4 dialogue line symbols 2024-11-01 20:07:35 +01:00
Veronica Berglyd Olsen
d9582436d4 Update tests 2024-11-01 19:55:56 +01:00
Veronica Berglyd Olsen
31c75ea0b1 Add a dialog parser class 2024-11-01 19:55:35 +01:00
Veronica Berglyd Olsen
057fc10565 Refactor and clean up GUI code (#2078) 2024-10-31 18:42:32 +01:00
Veronica Berglyd Olsen
bbb07c9963 Make some minor GUI tweaks 2024-10-31 18:35:28 +01:00
Veronica Berglyd Olsen
dd09d3af2f Refactor outline details panel code 2024-10-31 18:28:58 +01:00
Veronica Berglyd Olsen
ccb5f8418b Remove redundant function in project tree 2024-10-31 18:28:35 +01:00
Veronica Berglyd Olsen
55d2605222 Use the new qtLambda wrapper where appropriate 2024-10-31 17:31:58 +01:00
Veronica Berglyd Olsen
7f49979bc9 Clean up project tree class 2024-10-31 17:30:49 +01:00
Veronica Berglyd Olsen
b529d5b125 Improve document editing and test coverage (#2077) 2024-10-31 00:21:19 +01:00
Veronica Berglyd Olsen
f2e36d9974 Cover missing lines in error module 2024-10-31 00:18:16 +01:00
Veronica Berglyd Olsen
ca2b509d54 Make the test a little safer 2024-10-31 00:11:32 +01:00
Veronica Berglyd Olsen
0ba0674b9e Some code flow improvements and main GUI test coverage 2024-10-31 00:07:48 +01:00
Veronica Berglyd Olsen
a2b09fdfec Update various features in the doc editor using new helper code 2024-10-30 19:13:13 +01:00
Veronica Berglyd Olsen
fa0bef5500 Extend and refactor current open document highlighting 2024-10-30 19:12:09 +01:00
Veronica Berglyd Olsen
92d148b03d Allow inserting stats in documents (#2073) 2024-10-30 00:14:48 +01:00
Veronica Berglyd Olsen
3d935e29eb Update test coverage 2024-10-30 00:08:31 +01:00
Veronica Berglyd Olsen
3749c9f21d Fix variable formatting for DocX and ODT 2024-10-29 17:50:51 +01:00
Veronica Berglyd Olsen
2a3d3e46c2 Pin tests to set locale to en_GB 2024-10-29 17:50:51 +01:00
Veronica Berglyd Olsen
9bc1561e01 Change to use document locale for integers 2024-10-29 17:50:51 +01:00
Veronica Berglyd Olsen
d987246062 Use Qt for int locale 2024-10-29 17:50:50 +01:00
Veronica Berglyd Olsen
c6a22e2d96 Add stats variables to Lipsum test project 2024-10-29 17:50:50 +01:00
Veronica Berglyd Olsen
42f904f1ea Add field value insertion to all formats 2024-10-29 17:50:49 +01:00
Veronica Berglyd Olsen
f92521c93a Process field values in QTextDocuments 2024-10-29 17:50:49 +01:00
Veronica Berglyd Olsen
041183f914 Add processing of fields in the Tokenizer 2024-10-29 17:50:49 +01:00
Veronica Berglyd Olsen
857b6ddc55 Update main menu 2024-10-29 17:50:48 +01:00
Veronica Berglyd Olsen
f2d2b4c4f2 Generalise the manuscript stats count values 2024-10-29 17:47:32 +01:00
Veronica Berglyd Olsen
8725edaf7e Refactor main menu (#2075) 2024-10-29 17:46:16 +01:00
Veronica Berglyd Olsen
4eace705e3 Add a function wrapper that makes the linter happy when used for Qt slots 2024-10-29 17:37:50 +01:00
Veronica Berglyd Olsen
b2d71acd93 Connect menu actions to main GUI in the menu class 2024-10-29 17:36:54 +01:00
Veronica Berglyd Olsen
e424fcfe53 Add support for referencing tags in novel documents (#2072) 2024-10-28 00:18:46 +01:00
Veronica Berglyd Olsen
eefb40cb1f Update tests 2024-10-28 00:12:06 +01:00
Veronica Berglyd Olsen
785cd3aea9 Add support for story reference, and update mention reference in outline details 2024-10-28 00:08:28 +01:00
Veronica Berglyd Olsen
07c700f666 Rewrite narrator break for dialogue highlighting (#2068) 2024-10-27 13:15:57 +01:00
Veronica Berglyd Olsen
8db0928724 Re-order preferences settings and ensure dialog/narrator symbols are single characters 2024-10-27 13:11:50 +01:00
Veronica Berglyd Olsen
b25ba9de27 Add test coverage of the highlighter 2024-10-27 13:05:24 +01:00
Veronica Berglyd Olsen
bf478a6706 Update tests 2024-10-27 02:22:39 +02:00
Veronica Berglyd Olsen
a1a58f51b1 Add new logic for dialogue narrator break to tokenizer 2024-10-27 02:15:06 +02:00
Veronica Berglyd Olsen
8850161641 Add new logic for dialogue narrator break 2024-10-27 01:55:19 +02:00
Veronica Berglyd Olsen
f931148594 Bump version to 2.6 Alpha 3 2024-10-27 00:35:57 +02:00
Veronica Berglyd Olsen
c31b0bcf17 Add Url recognition and formatting (#2067) 2024-10-27 00:17:11 +02:00
Veronica Berglyd Olsen
ba260ac608 Add test coverage of new features, and some other places 2024-10-27 00:13:18 +02:00
Veronica Berglyd Olsen
f2fc19b168 Add test coverage of new features 2024-10-26 23:28:34 +02:00
Veronica Berglyd Olsen
ceabc666c3 Add font table, fix core.xml bug, clean up code, update tests, for DocX 2024-10-26 17:09:50 +02:00
Veronica Berglyd Olsen
6c217c2450 Fix spell checker issue 2024-10-25 23:53:38 +02:00
Veronica Berglyd Olsen
7ff8921420 Add links to DocX 2024-10-25 23:19:23 +02:00
Veronica Berglyd Olsen
fe56c88309 Separate spell checking and link scanning, and add ODT support for links 2024-10-25 23:19:23 +02:00
Veronica Berglyd Olsen
b13b91c8e8 Add url to Lipsum test project 2024-10-25 23:19:22 +02:00
Veronica Berglyd Olsen
d71435df53 Distinguish between anchor ref and href in tokenizer 2024-10-25 23:19:22 +02:00
Veronica Berglyd Olsen
058758122c Add url support in tokenizer and qdoc preview 2024-10-25 23:18:27 +02:00
Veronica Berglyd Olsen
759cfd77e9 Add url handling in editor and viewer 2024-10-25 23:18:27 +02:00
Veronica Berglyd Olsen
37268b873a Add URL regex pattern 2024-10-25 23:18:22 +02:00
Veronica Berglyd Olsen
439a04c603 Add support for mentions references (#2064) 2024-10-25 23:17:36 +02:00
Veronica Berglyd Olsen
9f642d501a Update documentation 2024-10-25 00:20:17 +02:00
Veronica Berglyd Olsen
7f8e2c66a6 Update tests 2024-10-25 00:13:09 +02:00
Veronica Berglyd Olsen
9962fd4a55 Add menu entry and shortcut 2024-10-25 00:13:03 +02:00
Veronica Berglyd Olsen
c27343534f Add mentions to sample project 2024-10-24 23:58:37 +02:00
Veronica Berglyd Olsen
46ecb2cf70 Add mentions to outline 2024-10-24 23:57:39 +02:00
Veronica Berglyd Olsen
88aa3f38c2 Add autocomplete for mentions 2024-10-24 23:57:10 +02:00
Veronica Berglyd Olsen
9214da3756 Add mention keyword 2024-10-24 23:53:48 +02:00
Veronica Berglyd Olsen
41e5152771 Add forced line breaks feature (#2063) 2024-10-24 17:55:56 +02:00
Veronica Berglyd Olsen
6b01bd7ffe Fix title page break issue and add test coverage 2024-10-24 17:47:14 +02:00
Veronica Berglyd Olsen
76a9f8e3df Fix a minor issue, and add test 2024-10-24 14:32:54 +02:00
Veronica Berglyd Olsen
e15941520f Add working forced line break implementation 2024-10-24 01:25:32 +02:00
Veronica Berglyd Olsen
ff7404d01d Add line break shortcode support 2024-10-24 01:07:03 +02:00
Veronica Berglyd Olsen
560540618c Process heading line breaks in the Tokenizer 2024-10-24 00:02:39 +02:00
Veronica Berglyd Olsen
bfa82f6bb3 Add test coverage of title line breaks, and don't break markdown titles 2024-10-24 00:02:39 +02:00
Veronica Berglyd Olsen
f77cd1c752 Fix counting error (#2062) 2024-10-24 00:01:39 +02:00
Veronica Berglyd Olsen
8461b0c15d Fix wrong character count when there is a footnote 2024-10-23 23:57:42 +02:00
Veronica Berglyd Olsen
2938a0f122 Remove version dependency in DocX tests 2024-10-23 17:44:55 +02:00
Veronica Berglyd Olsen
beccd1a7d1 Bump version to 2.6 Alpha 2 2024-10-23 17:41:28 +02:00
Veronica Berglyd Olsen
c6883908f1 Improve text document formatting (#2061) 2024-10-23 17:39:13 +02:00
Veronica Berglyd Olsen
36d8547f78 Correct misspelled variable 2024-10-23 17:36:12 +02:00
Veronica Berglyd Olsen
b01c91c0a3 Remove more redundant code 2024-10-23 17:20:07 +02:00
Veronica Berglyd Olsen
3712b2e93f Combine the different ways of storing generated text data in formats 2024-10-23 17:12:13 +02:00
Veronica Berglyd Olsen
0ddc1131dc Replace heading field with a general meta data field in the text block tuple 2024-10-23 16:20:50 +02:00
Veronica Berglyd Olsen
f4a941cd93 Fix format tag order for HTML validation 2024-10-23 11:01:06 +02:00
Veronica Berglyd Olsen
db72f2a722 Remove redundant code 2024-10-23 10:31:27 +02:00
Veronica Berglyd Olsen
16023676ea Simplify CSS in HTML format 2024-10-23 01:27:36 +02:00
Veronica Berglyd Olsen
52e85835df Flip if statement in Tokenizer 2024-10-23 01:26:48 +02:00
Veronica Berglyd Olsen
fe63770d21 Refactor and standardise text format modules (#2060) 2024-10-22 21:51:19 +02:00
Veronica Berglyd Olsen
bd5442c9e5 Process tags and references in Tokenizer 2024-10-22 21:37:18 +02:00
Veronica Berglyd Olsen
c7554f2b8d Process keyword flag in Tokenizer 2024-10-22 19:33:05 +02:00
Veronica Berglyd Olsen
63f9cd0bac Tokenize dialogue with colour tag 2024-10-22 18:41:34 +02:00
Veronica Berglyd Olsen
c537d0f396 Process comments in the Tokenizer 2024-10-22 17:37:00 +02:00
Veronica Berglyd Olsen
e759aa8d1b Move colour theme to Tokenizer class 2024-10-22 17:07:57 +02:00
Veronica Berglyd Olsen
2d8664b3ce Clean up wrapped code, and rename test variable 2024-10-22 00:40:54 +02:00
Veronica Berglyd Olsen
16076ebfb6 Move shared formats code to separate file, and rename some variables 2024-10-22 00:25:35 +02:00
Veronica Berglyd Olsen
38ca447c8a Convert format codes to enums 2024-10-21 23:37:27 +02:00
Veronica Berglyd Olsen
f91006631d Rename ODT test files 2024-10-21 18:56:37 +02:00
Veronica Berglyd Olsen
3cc5e4cc7a Handle text justify in Tokenizer instead 2024-10-21 18:54:19 +02:00
Veronica Berglyd Olsen
75178b7874 Merge branch 'release' 2024-10-21 17:14:32 +02:00
Veronica Berglyd Olsen
ce3f6dae20 Add DocX support (#2056) 2024-10-21 17:13:42 +02:00
Veronica Berglyd Olsen
bd82392d71 Add Python 3.13 and fix theme issue and broken link (#2059) 2024-10-21 17:12:09 +02:00
Veronica Berglyd Olsen
836a3d353a Update Enchant link (#2042) 2024-10-21 17:08:35 +02:00
Veronica Berglyd Olsen
a4d2a9ea6f Fix swapped colours in default light theme (#2057) 2024-10-21 17:05:19 +02:00
Veronica Berglyd Olsen
b0fc8d63a6 Add Python 3.13 support 2024-10-21 17:03:52 +02:00
Veronica Berglyd Olsen
ec7781fb9f Generalise ODT variables to DOC 2024-10-21 17:00:44 +02:00
Veronica Berglyd Olsen
787e1a46d1 Add DocX test coverage 2024-10-21 16:59:43 +02:00
Veronica Berglyd Olsen
148d46fcbc Add settings to DocX and compression to ODT and DocX 2024-10-20 23:08:14 +02:00
Veronica Berglyd Olsen
7e1a676e0a Add footnote support to DocX 2024-10-20 22:01:13 +02:00
Veronica Berglyd Olsen
e9220c5e77 Add DocX page settings and header support 2024-10-20 21:02:53 +02:00
Veronica Berglyd Olsen
72d41d4883 Refactor XML generation for DocX 2024-10-20 17:07:33 +02:00
Veronica Berglyd Olsen
de6c3e9b45 Fix issue with global justify, and add indentation to DocX 2024-10-19 20:14:01 +02:00
Veronica Berglyd Olsen
a0ab507ed1 Fix DocX tabs in text, and page break before issues 2024-10-19 19:04:16 +02:00
Veronica Berglyd Olsen
3c76d0e7e3 Fix issue with line spacing in DocX 2024-10-19 17:08:24 +02:00
Veronica Berglyd Olsen
34127d78bf Move custom XML sub element function to common 2024-10-19 17:08:08 +02:00
Veronica Berglyd Olsen
ca33f488f1 Get rid of the DocXRun class 2024-10-19 14:43:06 +02:00
Veronica Berglyd Olsen
717aac9dc8 Add remaining code blocks, and add colours 2024-10-18 21:18:59 +02:00
Veronica Berglyd Olsen
4b07dbadb2 Basic DocX text formatting works 2024-10-18 20:31:21 +02:00
Veronica Berglyd Olsen
9bbebd2085 Add DocX text styles 2024-10-18 18:35:56 +02:00
Veronica Berglyd Olsen
d60b29e0a2 make some improvements to the ODT writer 2024-10-18 16:37:42 +02:00
Veronica Berglyd Olsen
80d2dfd9a4 Add structure for DocX documents 2024-10-18 16:37:04 +02:00
Veronica Berglyd Olsen
464454e7a6 Expose text margin settings in Manuscript Build (#2051) 2024-10-17 22:41:42 +02:00
Veronica Berglyd Olsen
d145b65237 Standardise text sizes and margins 2024-10-17 22:34:46 +02:00
Veronica Berglyd Olsen
912198cc40 Add text margin settings 2024-10-17 21:57:04 +02:00
Veronica Berglyd Olsen
bffab79ba7 Merge build settings page layout options 2024-10-17 21:56:35 +02:00
Veronica Berglyd Olsen
c82f7fd9ca Bump version to 2.6 Alpha 1 2024-10-16 17:42:50 +02:00
Veronica Berglyd Olsen
53afe528e8 Add PDF output for manuscripts (#2048) 2024-10-16 17:41:31 +02:00
Veronica Berglyd Olsen
225befe57e Add test coverage 2024-10-16 17:33:32 +02:00
Veronica Berglyd Olsen
40817e73b4 Apply page layout settings to PDF format 2024-10-16 17:26:10 +02:00
Veronica Berglyd Olsen
17c93c2172 Add PDF format 2024-10-16 17:25:46 +02:00
Veronica Berglyd Olsen
c8d7ccadb7 Refactor document builder (#2047) 2024-10-16 00:26:32 +02:00
Veronica Berglyd Olsen
7bb292404a Update Tokenizer tests 2024-10-16 00:23:18 +02:00
Veronica Berglyd Olsen
d2ac0ed53f Pass the Markdown style to the constructor 2024-10-16 00:15:56 +02:00
Veronica Berglyd Olsen
c633a56bfb Simplify JSON and raw text writing 2024-10-15 23:59:25 +02:00
Veronica Berglyd Olsen
34696ba3a8 Merge doc build functions 2024-10-15 23:29:11 +02:00
Veronica Berglyd Olsen
93c105fe86 Extend heading styling options (#2045) 2024-10-15 21:40:21 +02:00
Veronica Berglyd Olsen
29eac0b4e5 Remove type alias for Python 3.9 2024-10-15 21:36:15 +02:00
Veronica Berglyd Olsen
fc27078954 Standardise the save document call for all document classes 2024-10-15 21:25:36 +02:00
Veronica Berglyd Olsen
759a8e84f0 Fix QTextDocument header formatting 2024-10-15 20:59:10 +02:00
Veronica Berglyd Olsen
b2c7da319e Generalise ODT build settings 2024-10-15 20:34:41 +02:00
Veronica Berglyd Olsen
b56095224a Move document format classes to new folder 2024-10-15 19:29:56 +02:00
Veronica Berglyd Olsen
2f12a8cc93 Add settings to style ODT headers 2024-10-15 19:22:08 +02:00
Veronica Berglyd Olsen
140f85f214 Merge branch 'main' into heading_styling 2024-10-14 00:26:55 +02:00
Veronica Berglyd Olsen
7405c7e20f Add Python 3.13 (#2044) 2024-10-14 00:25:07 +02:00
Veronica Berglyd Olsen
f29b13679a List and check support for Python 3.13 (#2040) 2024-10-14 00:21:22 +02:00
Veronica Berglyd Olsen
9b9ed53cf9 Don't use "match" as a variable (#2043) 2024-10-14 00:18:24 +02:00
Veronica Berglyd Olsen
2447ae5cff Replace usage of "match" as a variable name 2024-10-14 00:14:08 +02:00
Veronica Berglyd Olsen
7379b8b521 Remove plain heading style setting again 2024-10-14 00:10:23 +02:00
Veronica Berglyd Olsen
22473823c9 Improve build settings 2024-10-01 14:01:36 +02:00
Veronica Berglyd Olsen
d8bb9950d3 Add plain style setting for headings 2024-10-01 14:00:57 +02:00
Veronica Berglyd Olsen
279fe2ae18 Add title setting 2024-10-01 12:54:38 +02:00
Veronica Berglyd Olsen
1e812f3f06 Update tests 2024-10-01 12:15:42 +02:00
Veronica Berglyd Olsen
0f1401eb71 Rename title format settings to partition 2024-10-01 12:14:39 +02:00
Veronica Berglyd Olsen
57e739c3ef Add word counts to ODT output (#2035) 2024-10-01 12:00:04 +02:00
Veronica Berglyd Olsen
1b457585d9 Update tests 2024-09-30 22:52:39 +02:00
Veronica Berglyd Olsen
c9eab0b01e Add word counts to the ODT file as user fields (#2033) 2024-09-30 22:52:28 +02:00
Veronica Berglyd Olsen
3da5074d14 Refactor build settings (#2031) 2024-09-28 11:58:58 +02:00
Veronica Berglyd Olsen
e97a476c7f Update tests 2024-09-28 11:55:47 +02:00
Veronica Berglyd Olsen
a716f31b17 Fix some minor issues with spell checking 2024-09-28 11:41:26 +02:00
Veronica Berglyd Olsen
b2e760a284 Merge build settings formatting into a single form 2024-09-28 11:36:01 +02:00
Veronica Berglyd Olsen
7cc48aeb7f Improve spell checking (#2030) 2024-09-28 11:31:49 +02:00
Veronica Berglyd Olsen
a6494b4226 Allow words to be ignored by the spell checker (session only) 2024-09-28 11:21:04 +02:00
Veronica Berglyd Olsen
5ed03e29f3 Change regexes to use the pattern object directly 2024-09-28 11:20:09 +02:00
Veronica Berglyd Olsen
5c07148eb2 Replace Qt RegEx with Python RegEx (#2028) 2024-09-22 18:02:17 +02:00
Veronica Berglyd Olsen
17b4a7be4d Use regex end instead of length in tokenizer format processing function 2024-09-22 17:57:13 +02:00
Veronica Berglyd Olsen
9b08ebbbd5 Use Python regex for document search tool 2024-09-22 17:47:49 +02:00
Veronica Berglyd Olsen
37c469dbc7 Move word split to regex pattern class 2024-09-22 17:37:12 +02:00
Veronica Berglyd Olsen
c08033d153 Use stdlib re for dialogue matching 2024-09-22 17:34:04 +02:00
Veronica Berglyd Olsen
9c63f621a8 Use stdlib re for Shortcode matching 2024-09-22 17:06:57 +02:00
Veronica Berglyd Olsen
c3cb12e5e2 Use stdlib re for Markdown matching 2024-09-22 16:56:21 +02:00
Veronica Berglyd Olsen
d3799bc30a Update plain regex patterns to std lib re 2024-09-22 16:37:45 +02:00
Veronica Berglyd Olsen
cb8933b493 Use Python regex for spell checking 2024-09-22 16:23:35 +02:00
Veronica Berglyd Olsen
1aa58c047f Clean up Qt enums (#2025) 2024-09-18 22:18:15 +02:00
Veronica Berglyd Olsen
52b3084bee Fix type annotation 2024-09-18 22:12:39 +02:00
Veronica Berglyd Olsen
70b05c9e07 Use strings for shortcut keys 2024-09-18 22:11:32 +02:00
Veronica Berglyd Olsen
cafa440aee Update menu role enums 2024-09-18 22:06:23 +02:00
Veronica Berglyd Olsen
681cb7226a Fix bug in test 2024-09-18 22:04:06 +02:00
Veronica Berglyd Olsen
b36e49e43f Update scrollbar enums 2024-09-18 22:02:50 +02:00
Veronica Berglyd Olsen
90a6c2d98d Update keyboard and mouse enums 2024-09-18 21:55:59 +02:00
Veronica Berglyd Olsen
21d21846ca Update cursor and messagebox enums 2024-09-18 21:51:23 +02:00
Veronica Berglyd Olsen
cc8d7d9912 Fix ActionEvent enums 2024-09-18 21:46:44 +02:00
Veronica Berglyd Olsen
10527afbda Remove borders 2024-09-18 15:00:55 +02:00
Veronica Berglyd Olsen
d16ece2a81 Use html table instead 2024-09-18 14:59:38 +02:00
Veronica Berglyd Olsen
e7f7dc8d60 Increase sponsor logo size 2024-09-18 14:55:47 +02:00
Veronica Berglyd Olsen
2f2943452f Use px for logo size 2024-09-18 14:55:06 +02:00
Veronica Berglyd Olsen
08ae2b5351 Merge actions updates from 'release' 2024-09-18 14:52:46 +02:00
Veronica Berglyd Olsen
eda1a1c4dd Add sponsor link to readme 2024-09-18 14:52:24 +02:00
Veronica Berglyd Olsen
238abdb567 Update to release certificate for windows build 2024-09-18 14:33:45 +02:00
Veronica Berglyd Olsen
c3f9ceb03a Add version numbers to all build jobs 2024-09-18 14:02:27 +02:00
Veronica Berglyd Olsen
b95e9fcda3 Update for windows runner 2024-09-18 13:39:17 +02:00
Veronica Berglyd Olsen
b3f8a2c18c Add version parameter to windows build 2024-09-18 13:11:08 +02:00
Veronica Berglyd Olsen
18bbf7d86f Switch to test signing for windows setup 2024-09-18 12:14:37 +02:00
Veronica Berglyd Olsen
8e3ad6d874 Merge release 2.5.2 2024-09-17 16:51:06 +02:00
Veronica Berglyd Olsen
7a63ac83d2 Release 2.5.2 (#2021) 2024-09-17 15:49:04 +02:00
Veronica Berglyd Olsen
8dc28ddbee Add signing request to windows build 2024-09-17 15:45:30 +02:00
Veronica Berglyd Olsen
15803cf0a3 Bump version and update changelog 2024-09-17 15:33:37 +02:00
Veronica Berglyd Olsen
5ac001781c Improve documentation (#2015) 2024-09-09 12:03:22 +02:00
Veronica Berglyd Olsen
66986b88f8 Improve the spell checking info a little 2024-09-01 21:41:26 +02:00
Veronica Berglyd Olsen
f4e12a63a5 Merge branch 'release' into improve_docs 2024-09-01 21:37:53 +02:00
Veronica Berglyd Olsen
09b0f34199 Merge branch 'release' 2024-09-01 17:47:36 +02:00
Veronica Berglyd Olsen
095c005c3f Fix bugs in the document viewer (#2014) 2024-09-01 17:45:44 +02:00
Veronica Berglyd Olsen
e414b6458d Refresh tab stop distance after loading document (#1996) 2024-09-01 17:38:04 +02:00
Veronica Berglyd Olsen
ece73784c2 Only highlight dialogue in novel text (#2011 and #2013) 2024-09-01 17:32:37 +02:00
Veronica Berglyd Olsen
8dd9644c13 Fix issue with overlapping formatting (#2012) 2024-09-01 17:22:54 +02:00
Veronica Berglyd Olsen
556e8db104 Add some text about spell checking to editor docs 2024-08-16 09:33:38 +02:00
Veronica Berglyd Olsen
ce979b6c84 Merge branch 'release' 2024-07-28 14:10:36 +02:00
Veronica Berglyd Olsen
d2f422a441 Update Ubuntu releases (#1995) 2024-07-28 14:09:39 +02:00
Veronica Berglyd Olsen
e4daff8956 Drop Ubuntu 23.10 and add 24.10 2024-07-28 14:04:14 +02:00
Veronica Berglyd Olsen
c146c311c1 Merge branch 'release' 2024-07-28 13:46:53 +02:00
Veronica Berglyd Olsen
2e1b17dcdf Release 2.5.1 (#1989) 2024-07-28 13:32:15 +02:00
Veronica Berglyd Olsen
2ef11f56e5 Update changelog 2024-07-28 13:26:34 +02:00
Veronica Berglyd Olsen
fdaeca78a7 Update translations 2024-07-28 13:21:58 +02:00
Veronica Berglyd Olsen
93e7343fe9 Update changelog with latest fix 2024-07-26 00:22:58 +02:00
Veronica Berglyd Olsen
5167608f7a Merge branch 'release' into release_2.5.1 2024-07-26 00:20:11 +02:00
Veronica Berglyd Olsen
7fe3a8520d Merge branch 'release' 2024-07-26 00:19:25 +02:00
Veronica Berglyd Olsen
d4a0881e77 Fix updating of importance labels (#1993) 2024-07-26 00:18:47 +02:00
Veronica Berglyd Olsen
7f42210f2f Add test coverage 2024-07-26 00:15:14 +02:00
Veronica Berglyd Olsen
33497bc92d Simplify how the project tree is populated 2024-07-25 20:47:14 +02:00
Veronica Berglyd Olsen
06aa2e6c07 Only refresh project tree values when status labels change 2024-07-25 20:39:32 +02:00
Veronica Berglyd Olsen
3bffaa01dc Update viewer panel when importance labels change (#1992) 2024-07-25 19:21:52 +02:00
Veronica Berglyd Olsen
66237c35ea Bump version to 2.5.1 and update changelog 2024-07-21 20:05:54 +02:00
Veronica Berglyd Olsen
f532857345 Merge branch 'release' 2024-07-21 19:54:43 +02:00
Veronica Berglyd Olsen
5936e1a68b Improve test coverage 2024-07-21 19:45:40 +02:00
Veronica Berglyd Olsen
17b3b2b770 Navigate to heading when a tag is clicked for editing (#1983) 2024-07-21 19:45:28 +02:00
Veronica Berglyd Olsen
826e805b06 Make sure free form text boxes in Preferences are safe (#1986) 2024-07-21 19:20:51 +02:00
Veronica Berglyd Olsen
5419d76606 Fix test 2024-07-21 19:15:56 +02:00
Veronica Berglyd Olsen
0d84653f7d Process pad before and after settings in Preferences (#1985) 2024-07-21 19:06:45 +02:00
Veronica Berglyd Olsen
b7da56ac42 Merge branch 'release' 2024-07-14 16:43:43 +02:00
Veronica Berglyd Olsen
7140788b48 Lock project only after successful open (#1978) 2024-07-14 16:33:48 +02:00
Veronica Berglyd Olsen
a43bd5a50e Move project lock until all steps in project open are completed (#1977) 2024-07-14 16:28:09 +02:00
Veronica Berglyd Olsen
81dd98e83b Bump main branch to 2.6 alpha 0 2024-07-10 17:30:14 +02:00
274 changed files with 40528 additions and 26801 deletions

View File

@@ -32,12 +32,15 @@ jobs:
path: novelwriter/assets
- name: Build AppImage
run: python pkgutils.py build-appimage --linux-tag $LINUX_TAG --python-version $PYTHON_VERSION
id: build
run: |
echo "BUILD_VERSION=$(python pkgutils.py version)" >> $GITHUB_OUTPUT
python pkgutils.py build-appimage --linux-tag $LINUX_TAG --python-version $PYTHON_VERSION
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: Linux-AppImage
path: dist_appimage
name: novelWriter-${{ steps.build.outputs.BUILD_VERSION }}-Linux-AppImage
path: dist_appimage/*.AppImage*
if-no-files-found: error
retention-days: 14

View File

@@ -8,8 +8,7 @@ jobs:
buildMac-AMD64:
needs: buildAssets
# Stay on macos-12 due to https://github.com/create-dmg/create-dmg/issues/143
runs-on: macos-12
runs-on: macos-latest
env:
PYTHON_VERSION: "3.12"
PACKAGE_ARCH: x86_64
@@ -25,19 +24,22 @@ jobs:
path: novelwriter/assets
- name: Build App Bundle
run: ./setup/macos/build.sh $PYTHON_VERSION $PACKAGE_ARCH $MINICONDA_ARCH
id: build
run: |
echo "BUILD_VERSION=$(python pkgutils.py version)" >> $GITHUB_OUTPUT
./setup/macos/build.sh $PYTHON_VERSION $PACKAGE_ARCH $MINICONDA_ARCH
- name: Upload DMG
uses: actions/upload-artifact@v4
with:
name: MacOS-AMD64-DMG
path: dist_macos
name: novelWriter-${{ steps.build.outputs.BUILD_VERSION }}-MacOS-AMD64-DMG
path: dist_macos/*.dmg*
if-no-files-found: error
retention-days: 14
buildMac-M1:
needs: buildAssets
runs-on: macos-14
runs-on: macos-latest
env:
PYTHON_VERSION: "3.12"
PACKAGE_ARCH: aarch64
@@ -53,12 +55,15 @@ jobs:
path: novelwriter/assets
- name: Build App Bundle
run: ./setup/macos/build.sh $PYTHON_VERSION $PACKAGE_ARCH $MINICONDA_ARCH
id: build
run: |
echo "BUILD_VERSION=$(python pkgutils.py version)" >> $GITHUB_OUTPUT
./setup/macos/build.sh $PYTHON_VERSION $PACKAGE_ARCH $MINICONDA_ARCH
- name: Upload DMG
uses: actions/upload-artifact@v4
with:
name: MacOS-M1-DMG
path: dist_macos
name: novelWriter-${{ steps.build.outputs.BUILD_VERSION }}-MacOS-M1-DMG
path: dist_macos/*.dmg*
if-no-files-found: error
retention-days: 14

View File

@@ -26,12 +26,37 @@ jobs:
path: novelwriter/assets
- name: Build Setup Installer
run: python pkgutils.py build-win-exe
id: build
run: |
echo "BUILD_VERSION=$(python pkgutils.py version)" >> $Env:GITHUB_OUTPUT
python pkgutils.py build-win-exe
- name: Upload Artifacts
- name: Upload Unsigned Artifacts
id: upload-unsigned-artifact
uses: actions/upload-artifact@v4
with:
name: Win-Setup
name: novelWriter-${{ steps.build.outputs.BUILD_VERSION }}-Win-Setup
path: dist/*.exe
if-no-files-found: error
retention-days: 14
- name: Submit Signing Request
uses: signpath/github-action-submit-signing-request@v1
with:
api-token: "${{ secrets.SIGNPATH_API_TOKEN }}"
organization-id: "0471e52c-66fa-4e9a-bfb9-36167095ca3f"
project-slug: "novelWriter"
signing-policy-slug: "release-signing "
github-artifact-id: "${{ steps.upload-unsigned-artifact.outputs.artifact-id }}"
wait-for-completion: true
output-artifact-directory: "dist/"
parameters: |
version: "${{ steps.build.outputs.BUILD_VERSION }}"
- name: Upload Signed Artifacts
uses: actions/upload-artifact@v4
with:
name: novelWriter-${{ steps.build.outputs.BUILD_VERSION }}-Win-Setup-Signed
path: dist/*.exe
if-no-files-found: error
retention-days: 14

View File

@@ -14,7 +14,7 @@ jobs:
testLinux:
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
fail-fast: false
runs-on: ubuntu-latest
steps:
@@ -41,6 +41,6 @@ jobs:
export QT_QPA_PLATFORM=offscreen
python -m pytest -v --cov=novelwriter --timeout=60
- name: Upload to Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -32,6 +32,6 @@ jobs:
export QT_QPA_PLATFORM=offscreen
python -m pytest -v --cov=novelwriter --timeout=60
- name: Upload to Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -28,6 +28,6 @@ jobs:
run: |
python -m pytest -v --cov=novelwriter --timeout=60
- name: Upload to Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -1,5 +1,310 @@
# novelWriter Changelog
## Version 2.6 [2025-01-26]
### Release Notes
The 2.6 release fixes a few minor issues from the RC 1 pre-release, and updates most translations.
See the website for complete [Release Notes](https://novelwriter.io/releases/release_2_6.html).
### Detailed Changelog
**Bugfixes**
* Fixed a bug in the novel selector dropdown used a few places that would clear the selection if
the novel list was refreshed. PR #2179.
* Fixed a bug that would crash the app if the project tree was clicked without any project having
been loaded. The selection model does not exist prior to a project load. Issue #2173. PR #2181.
* Replaced a broken link on the dictionary install tool available on Windows. PR #2189.
* Backported a few code fixes from 2.7 related to freeing memory from closed dialogs and one
instance of a potentially uninitialised variable. PR #2202.
* Fixed word wrapping in the item details panel for status and importance labels with very long
names. PR #2202.
* Fixed the project tree context menu so that it is not opened when right-clicking on an empty area
of the tree widget. PR #2202.
**Improvements**
* Chapter headings are now level 1 headings in all manuscript output formats, not just HTML like
before. Most applications that post-processes output will assume chapters are level 1.
Correspondingly, scenes are now level 2 and sections are level 3. Notes headings remain
unchanged. Issue #2205. PR #2206.
**Internationalisation**
* German, US English, Italian, Japanese, Norwegian and Polish have been updated in full. Brazilian
Portuguese and Chinese have been partially updated. PR #2207.
**Documentation**
* Documentation has been updated with 2.6 features. PR #2210.
----
## Version 2.6 RC 1 [2025-01-09]
### Release Notes
This is a release candidate of the next release version, and is intended for testing purposes.
Please be careful when using this version on live writing projects, and make sure you take frequent
backups.
### Detailed Changelog
**New Features**
* The session timer in the status bar can now be hidden by clicking on it. Issue #1029. PR #2149.
* Status and importance labels can now be exported and imported. Issue #1847. PRs #2152 and #2153.
**Bugfixes**
* Fix a beta release bug where it was not possible to change the status or importance flag on
folders in the project tree. Issue #2145. PR #2147.
* Fix an issue where the word counter would override the selection counter in the editor. This was
caused by the selection counter being on a much shorter timer than the word counter, so in some
cases the latter result came in after the former even if the user actions where the other way
around. The selection counter now takes precedence, always. Issue #2155. PRs #2156 and #2157.
* Fix a minor issue where the search panel header background would not update colour when the theme
was changed. Issue #2162. PR #2167.
**Improvements**
* It is no longer necessary to click the "Apply" button when making changes to status and
importance labels, or auto-replace settings, in Project Settings. The changes are applied
directly to the listed items. They are not applied to the project until the Project Settings are
accepted. This is a more intuitive approach. Issue #2150. PR #2151.
**Packaging and Installation**
* MacOS packages are now built on the latest MacOS image available on GitHub. The x86_64 build was
previously locked to MacOS 12, which is no loner available. PR #2141.
----
## Version 2.6 Beta 2 [2024-12-23]
### Release Notes
This is a beta release of the next release version, and is intended for testing purposes. Please be
careful when using this version on live writing projects, and make sure you take frequent backups.
### Detailed Changelog
**New Features**
* The project tree has been completely rewritten under the hood. The main difference in the GUI is
a lifting of restrictions on drag and drop. You can now select multiple items in multiple places
in the tree, and drag them to a new location. It is still not allowed to drag root level items,
or drop items on the root level. PR #2119.
* You can now drag and drop items from the project tree onto the editor and viewer to load the
documents there. PR #2119.
**Bugfixes**
* Make line height in DocX scale correctly. Issue #2099. PR #2102.
* Make margins scale with font size in ODT and HTML output. PR #2102.
* Fix DocX page count to work with Office 365. PR #2102.
* Fix font scaling issues for PDF output. Issue #2100. PR #2104.
* Fix an issue that cleared the editor when the return key was used on a folder item. PR #2113.
* Fix an issue where the viewer and preview would not reset text emphasis when selecting a default
font style with emphasis features. Issue #2121. PR #2122.
* Change how custom fonts are loaded to ensure that the Qt framework don't take shortcuts by using
a similar font instead of the chosen font. This is a side effect of the Qt font matching feature.
Issue #2118. PR #2122.
**Improvements**
* Move processing of alternative apostrophe and dash to a later point in the tokenization process.
It needs to happen after dialogue is processed to work as intended. PR #2095.
* Use icons for text margin settings in Build Settings. Issue #2098. PR #2102.
* Replace the toolbar icon in the editor. PR #2103.
* Turn off auto-default button on the Build dialog. Issue #2101. PR #2105.
* Add ANSI colours to log output for easier debugging. PRs #2114 and #2117.
* Document header is now aligned to the bottom of the top page margin in the same way for ODT and
DocX output. Issue #2120. PR #2128.
* Dialogue detection for quoted dialogue has been improved to better support Chinese, and other
languages that don't use whitespaces to separate words. Some improvements have also been made to
how single quoted dialogues are processed when apostrophes are present. Issue #2131. PR #2138.
**Code Improvements**
* Completely rewrite the project tree into a model/view set with one single definition if the
project structure. PR #2109.
* The auto-replace feature in the editor has been refactored. It is now better separated from the
editor code, and may have some minor performance improvements. Issue #2129. PR #2132.
----
## Version 2.6 Beta 1 [2024-11-12]
### Release Notes
This is a beta release of the next release version, and is intended for testing purposes. Please be
careful when using this version on live writing projects, and make sure you take frequent backups.
### Detailed Changelog
**New Features**
* Added PDF build format. Issue #2046. PR #2048.
* Added DocX (MS Word) format. Issue #1537. PR #2056.
* The Build settings tool has the following changes: Ability to control page break and centring of
main titles, an option to disable styling for headings altogether, and the ODT section has been
generalised to apply to all document formats. Issue #2023. PR #2045.
* Text and heading margin can now be control in the Manuscript Build tool. Issue #2023. PR #2051.
* Word counts and other such statistics can now be inserted into the manuscript. This is done
through new insert actions in the Insert menu, and processed at build time. Issue #2024.
PR #2073.
* Forced line breaks can now be inserted as a shortcode. This is useful when line breaks in
paragraphs are ignored when building the manuscript. Issue #1991. PR #2063.
* A new `@mentions` keyword has been added that can list any tag in the project as being mentioned
in the current section. These are listed separately from tags present in the story section.
Issue #1822. PR #2064.
* Tags in novel documents can now be referenced with the `@story` keyword. Issue #1990. PR #2072.
* URLs starting with http/https are now recognised and clickable in the editor, viewer, and
manuscript preview. Issue #1426. PR #2067.
* Added dialogue highlighting for Portuguese and Polish dialogue styles. Issues #2066 and #2070.
Related to #1773. PRs #2068, #2079, #2081 and #2082.
* Four dashes in the editor are now replaced by a horizontal bar. This is useful as a replacement
symbol for em dash when using dialogue highlighting to avoid triggering it. The horizontal bar is
replaced with an em dash in the manuscript. Issue #2070. PR #2079.
* Manuscript Build settings can now be duplicated. Issue #1931. PR #2084.
* Page breaks can optionally be made visible in the Manuscript preview. PR #2086.
* The document status label can now be selected in the Outline View. Issue #1909. PR #2088.
* The document viewer now has an edit button to open the document in the editor. Issue #1963.
PR #2089.
**User Interface**
* The Manuscript Build settings have now been moved into a single scrollable page, using the same
layout as in Preferences. The heading and filtering pages are still separate forms, as they are
of a more complex nature. PR #2031.
* The currently open document in the editor is now highlighted in the project tree, just like it is
in the novel view. Issue #1981. PR #2077.
* The details panel in the Outline view is now resizable. PR #2078.
**Improvements**
* Add an option to ignore a word in the spell checker. An ignored word is ignored only for the
current writing session. PR #2030.
* Word counts and other such statistics is now exported as custom variables to ODT files, and can
be inserted in the text in LibreOffice, and similar. Issue #2033. PR #2035.
**Code Improvements**
* Added Python 3.13 coverage to ensure the new Python release is supported. Issue #2040. PR #2044.
* Preparation for Qt6: Clean up Qt enums. PR #2025.
* Preparation for Qt6: Replace Qt regular expression tools with the internal Python re module for
markup processing and syntax highlighting. It is both slightly faster, and there are issues with
text encoding in at least some versions of Qt6 or PyQt6. PRs #2028 and #2043.
* Preparation for Qt6: Added a wrapper function for connecting signals to slots that has a
different function signature. Python lambdas generate warnings inn Qt6. PR #2075.
* Refactored manuscript formats and moved most of the processing to the Tokenizer class to simplify
the format classes and also make them more consistent. PRs #2060, #2061 and #2062.
* The document builder has been refactored to support more generalised format classes. PR #2047.
* Action parent assignment has been rafactored in the main menu. #2075.
* Raw text format output has been refactored and reduced to just appending documents. PR #2087.
----
## Version 2.5.3 [2024-11-26]
### Release Notes
This is a patch release that fixes a few minor bugs in the user interface, and with the HTML
manuscript format. This patch also adds a Russian translation, and updated the German and
Portuguese translations.
### Detailed Changelog
**Bugfixes**
* Fixed the red/green LEDs on the statusbar for the Default Light theme. The colours were swapped.
Issue #2057. PR #2059.
* Moved the styles tag into the head tag for HTML manuscript output, which is the correct placement
for them. Issue #2080. PR #2085.
* Fixed a bug in the project tree where it was possible to drag content to root level on some Qt
versions where the disallow flag is not obeyed. The illegal action is now additionally blocked by
code. Issue #2108. PR #2109.
**Documentation**
* Updated a dead link to the spell check library with the new one. Issue #2042. PR #2059.
**Internationalisation**
* Russian translation added by Konstantin Tan (@k1kimosha). PR #2126
* Updated German and Portuguese translations. PR #2126
----
## Version 2.5.2 [2024-09-17]
### Release Notes
This is a patch release that fixes a series of issues mostly affecting dialogue highlighting in the
document viewer. The way the text is formatted in the viewer changed a lot in 2.5, and there were a
few issues with the new implementation. The tab stop distance setting was also ignored, but has now
been fixed as well.
### Detailed Changelog
**Bugfixes**
* Fixed an issue where the tab stop distance setting was being ignored in the document viewer. It
must be set again every time a document is loaded. Issue #1996. PR #2014.
* Fixed an issue with overlapping formatting markers when processing text into new formats. This
issues was introduced when dialogue highlighting was added to the document viewer and manuscript
build. It arises because start and end of dialogue can occur on the same character as other
formatting, and then the order these markers are processed matters. This was not properly handled
in the code. Issue #2012. PR #2014.
* Dialogue highlighting is now only applied to plain text paragraphs, not comments, headings, and
other places where there should be no dialogue. Issue #2013. PR #2014.
* Note documents no longer get dialogue highlighting. Issue #2011. PR #2014.
**Documentation**
* Added a section about spell checking in the documentation. PR #2015.
----
## Version 2.5.1 [2024-07-28]
### Release Notes
This is a patch release that fixes an issue with the lock file being left in the project when the
project open is cancelled, a few issues with custom input boxes in Preferences, and a usability
issue when selecting to edit a tag or reference from the Tags and References panel as well as fixed
an issue with the Importance labels not being updated in the tabs for tags.
### Detailed Changelog
**Bugfixes**
* Fixes an issue where the project lock file was created before certain checks where the user could
cancel the project open process, leaving a lock file in the project folder behind. Issue #1977.
PR #1978.
* Fixes an issue where the user could add spaces to the pad before and after Preference settings,
which would pad every space entered into the text. The Preferences settings no longer accept any
white spaces in these boxes (they are removed on save) and also strips duplicate character
entries. Issue #1985. PR #1986.
* The narrator break and dialog symbol Preference text boxes will now be cleared if the settings is
a white space. PR #1986.
* Fixed an issue where Importance labels were not updated in the viewer panel's tags tabs when they
where changed in Project Settings. Issue #1992. PR #1993.
**Usability**
* When selecting to edit a tag or reference from the panel below the Document Viewer, the editor
now scrolls to the relevant heading as it does in the viewer. Issue #1983. PR #1987.
**Internationalisation**
* Latin American Spanish translation updated by Tommy Marplatt (@tmarplatt). PR #1989.
----
## Version 2.5 [2024-07-10]
### Release Notes

View File

@@ -30,6 +30,7 @@ translators for the languages currently available:
* **Norwegian:** Veronica Berglyd Olsen (vkbo)
* **Polish:** Anna Maria Polak (Nauthiz)
* **Portuguese:** Bruno Meneguello (bkmeneguello)
* **Russian:** Konstantin Tan (k1kimosha)
* **Simplified Chinese:** Qianzhi Long (longqzh)
Additional larger translation contributions:

View File

@@ -29,6 +29,15 @@ documentation.
* PyPi Project: [pypi.org/project/novelWriter](https://pypi.org/project/novelWriter)
* Social Media: [fosstodon.org/@novelwriter](https://fosstodon.org/@novelwriter)
## Sponsors
<table style="border: none;">
<tr>
<td><img align="left" style="height: 25px;" src="https://raw.githubusercontent.com/vkbo/novelWriter/main/setup/signpath_logo.png"></td>
<td>Free code signing on Windows provided by <a href="https://about.signpath.io/">SignPath.io</a>, certificate by <a href="https://signpath.org/">SignPath Foundation</a>.</td>
</tr>
</table>
## Implementation
novelWriter is written with Python 3 (3.9+) using Qt5 and PyQt5 (5.15 only), and is released on

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 530 KiB

After

Width:  |  Height:  |  Size: 516 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 540 KiB

After

Width:  |  Height:  |  Size: 535 KiB

View File

@@ -13,7 +13,7 @@ Glossary
see :ref:`a_proj_roots`.
Novel Documents
These are documents that are created under a "Novel" :term:`root folder`. They behave
These are documents that are created under a "Novel" :term:`Root Folder`. They behave
differently than :term:`Project Notes`, and have some more restrictions. For instance, they
can not exist in folders intended only for project notes. See the :ref:`a_struct` chapter for
more details.

View File

@@ -16,7 +16,7 @@ This is a list of hopefully helpful little tips on how to get the most out of no
Managing the Project
====================
.. dropdown:: Create a Project from a Template
.. dropdown:: How do I create a project from a template?
:animate: fade-in-slide-down
On the Welcome dialog's **Create New Project** form, you can select to "Prefill Project" from
@@ -24,7 +24,7 @@ Managing the Project
dedicated to be a template project. If you have a structure and settings you want to use for
every new project, this is the best solution.
.. dropdown:: Merge Multiple Documents Into One
.. dropdown:: How do I merge multiple documents into one?
:animate: fade-in-slide-down
If you need to merge a selection of documents in your project into a single document, you can
@@ -35,28 +35,18 @@ Managing the Project
In the dialog that pops up, the documents will be in the same order as in the folder, but you
can rearrange them here of you wish. See :ref:`a_ui_tree_split_merge` for more details.
.. dropdown:: How do I share status or importance labels between projects?
:animate: fade-in-slide-down
The status or importance labels you have defined in a project can be exported from **Project
Settings** from the respective configuration tabs. You can then import these labels in another
project.
Layout Tricks
=============
.. dropdown:: Align Paragraphs with Line Breaks
:animate: fade-in-slide-down
If you have line breaks in you paragraphs, and also want to apply additional text alignment or
indentation, you must apply the alignment tags to the first line.
For example, this will centre the two lines.
.. code-block:: md
>> Line one is centred. <<
Line two is also centred.
This text is not centred, because it is a new paragraph.
See :ref:`a_fmt_align` for more details.
.. dropdown:: Create a Simple Table
.. dropdown:: How do I create a table?
:animate: fade-in-slide-down
The formatting tools available in novelWriter don't allow for complex structures like tables.
@@ -69,12 +59,22 @@ Layout Tricks
This is most suitable for your notes, as the result in exported documents cannot be guaranteed
to match. Especially if you don't use the same font in your manuscript as in the editor.
.. dropdown:: Turn Off First Line Indent for a Paragraph
.. dropdown:: How do I force a line break when line breaks are ignored in my manuscript?
:animate: fade-in-slide-down
In the **Manuscript Build Settings** you can choose to ignore line breaks within paragraphs in
your text. However, some times you still need those breaks. Like for instance on the cover page
where you may need to add your name and address. In such cases, you can add ``[br]`` where you
want line breaks. These breaks cannot be ignored by any settings and will always be respected.
.. versionadded:: 2.6
.. dropdown:: How do I turn off first line indent for a specific paragraph?
:animate: fade-in-slide-down
If you have first line indent enabled, but have a specific paragraph that you don't want
indented, you can disable the indentation by explicitly add text alignment. Aligned paragraphs
are not indented. For instance by adding ``<<`` to the end to left-align it,
indented, you can disable the indentation by explicitly adding text alignment. For instance by
adding ``<<`` to the end to left-align it. Aligned paragraphs are not indented.
See :ref:`a_fmt_align` for more details.
@@ -82,7 +82,7 @@ Layout Tricks
Organising Your Text
====================
.. dropdown:: Add Introductory Text to Chapters
.. dropdown:: How do I add introductory text to chapters?
:animate: fade-in-slide-down
Sometimes chapters have a short preface, like a brief piece of text or a quote to set the stage
@@ -92,7 +92,7 @@ Organising Your Text
such text. Separating chapter and scene files also allows you to make scene files child
documents of the chapter.
.. dropdown:: Distinguishing Soft and Hard Scene Breaks
.. dropdown:: How do I distinguishing between soft and hard scene breaks?
:animate: fade-in-slide-down
Depending on your writing style, you may need to separate between soft and hard scene breaks
@@ -101,8 +101,8 @@ Organising Your Text
In such cases you may want to use different scene headings for hard and soft scene breaks. The
**Build Manuscript** tool will let you define a different format for scenes using the ``###``
and ``###!`` heading codes when you generate your manuscript. You can for instance add the
common "``* * *``" for hard breaks and select to soft scene breaks, which will just insert an
empty paragraph in their place. See :ref:`a_manuscript_settings` for more details.
common "``* * *``" for hard breaks and select to hide soft scene breaks, which will just insert
an empty paragraph in their place. See :ref:`a_manuscript_settings` for more details.
.. versionadded:: 2.4
@@ -110,7 +110,7 @@ Organising Your Text
Other Tools
===========
.. dropdown:: Convert Project to/from yWriter Format
.. dropdown:: How do I convert my project to/from the yWriter format?
:animate: fade-in-slide-down
There is a tool available that lets you convert a `yWriter <http://spacejock.com/yWriter7.html>`_

View File

@@ -39,7 +39,8 @@ your notes about your characters, your world building, and so on.
You can at any point split the individual documents by their headings up into multiple documents,
or merge multiple documents into a single document. This makes it easier to use variations of the
Snowflake_ method for writing. You can start by writing larger structure-focused documents, like
for instance one document per act, and later effortlessly split these up into chapters or scenes.
for instance one document per act, and later effortlessly split these up into individual chapter or
scene documents.
.. _a_intro_features:
@@ -53,7 +54,7 @@ Below are some key features of novelWriter.
The aim of the user interface is to let you focus on writing instead of spending time formatting
text. Formatting is therefore limited to a small set of formatting tags for simple things like
text emphasis and paragraph alignment. Additional shortcodes are available for special
formatting cases.
formatting cases when you need them.
When you really want to focus on just writing, you can switch the editor into **Focus Mode**
where only the text editor panel itself is visible, and the project structure view is hidden
@@ -78,19 +79,19 @@ Below are some key features of novelWriter.
Your main story text lives in the "Novel" root folder. You can have multiple such folders in a
project, and rename them to whatever you want. This allows you to keep a series of individual
novels with the same characters and world building in the same project, and create manuscripts
for them individually.
for them separately.
**Keep track of your story elements**
All notes in your project can be assigned a :term:`tag` that you can then :term:`reference` from
any other document or note. In fact, you can add a new tag under each heading of a note if you
need to be able to reference specific sections of it, or you want to keep several topics in the
same note.
All documents in your project can be assigned a :term:`tag` that you can then :term:`reference`
from any other document or note. In fact, you can add a new tag under each heading of a note if
you need to be able to reference specific sections of it, or you want to keep several topics or
scenes in the same document.
**Get an overview of your story**
It is not the documents themselves that define the chapters and scenes of your story, but the
headings that separate them. In the **Outline View** on the main window you can see an outline
of all the chapter and scene headings of each novel root folder in your project. If they have
any references in them, like which character is in what chapter and scene, these are listed in
headings within them. In the **Outline View** on the main window you can see an outline of all
the chapter and scene headings of each novel root folder in your project. If they have any
references in them, like which character is in what chapter and scene, these are listed in
additional columns.
You can also add a synopsis to each chapter or scene, which can be listed here as well. You have
@@ -107,9 +108,9 @@ Below are some key features of novelWriter.
Whether you want to assemble a manuscript, or export all your notes, or generate an outline of
your chapters and scenes with a synopsis included, you can use the **Build Manuscript** tool to
do so. The tool lets you select what information you want to include in the generated document,
and how it is formatted. You can send the result to a printer or PDF, or generate an Open
Document file that can be opened by most office type word processors. You can also generate the
result as HTML, or Markdown, both suitable for further conversion to other formats.
and how it is formatted. You can send the result to a printer, or generate an Open Document or
Word Document file that can be opened by most office type word processors. You can also generate
the result as HTML, or Markdown, both suitable for further conversion to other formats.
.. _a_intro_screenshots:

View File

@@ -5,10 +5,10 @@ What to Read First
******************
The documentation of novelWriter is quite extensive. There are a lot of features to get used to,
but you don't need all of them to get started.
but you don't need to know all of them to get started.
The chapters below labelled "Essential Information" are the ones you need to know to use the
application correctly. By "correctly", it is meant: in a way so novelWriter understands the basic
application correctly. By "correctly" it is meant: in a way so novelWriter understands the basic
structure of your text. It collects a lot of information from your text and uses it to display the
structure of it in various ways to help you get an overview of your writing.
@@ -23,7 +23,7 @@ Using novelWriter
=================
In order to use novelWriter effectively, you need to know the basics of how it works. The following
chapters will explain the main principles. It starts with the basics, and gets more detailed as you
chapters will explain the main principles. They start with the basics, and get more detailed as you
read on.
:ref:`a_breakdown` – Essential Information

View File

@@ -4,7 +4,7 @@
Setup and Installation
**********************
.. _Enchant: https://abiword.github.io/enchant/
.. _Enchant: http://rrthomas.github.io/enchant/
.. _GitHub: https://github.com/vkbo/novelWriter
.. _Downloads page: https://download.novelwriter.io
.. _PPA: https://launchpad.net/~vkbo/+archive/ubuntu/novelwriter
@@ -15,7 +15,7 @@ Setup and Installation
.. _AppImage: https://appimage.org/
Ready-made packages and installers for novelWriter are available for all major platforms, including
Linux, Windows and MacOS, from the `Downloads page`_. See below for install additional instructions
Linux, Windows and MacOS, from the `Downloads page`_. See below for additional install instructions
for each platform.
You can also install novelWriter from the Python Package Index (PyPi_). See :ref:`a_started_pip`.
@@ -40,12 +40,6 @@ If you have any issues, try uninstalling the previous version and making a fresh
already had a version installed via a different method, you should uninstall that first as having
multiple installations has been known to cause problems.
.. note::
The novelWriter installer is not signed because Microsoft doesn't currently provide a way for
non-profit open source projects to properly sign their installers. The novelWriter project
doesn't have the funding to pay for commercial software signing certificates. You will therefore
see an additional warning about this when you download and run the installer.
.. _a_started_linux:
@@ -93,10 +87,6 @@ Then run the update and install commands as for Ubuntu:
sudo apt update
sudo apt install novelwriter
.. note::
You may need to use the Ubuntu 20.04 (focal) packages for Debian 11 or earlier. The newer Ubuntu
packages use a different compression algorithm that may not be supported.
.. tip::
If you get an error message like ``gpg: failed to create temporary file`` when importing the key
from the Ubuntu keyserver, try creating the folder it fails on, and import the key again:
@@ -142,7 +132,7 @@ done by holding down the option key on your keyboard and clicking on the applica
.. note::
The novelWriter DMG is not signed because Apple doesn't currently provide a way for non-profit
open source projects to properly sign their installers. The novelWriter project doesn't have the
funding to pay for commercial software signing certificates.
funding to pay for a commercial software signing certificate.
.. _a_started_pip:

View File

@@ -4,7 +4,7 @@
Customisations
**************
.. _Enchant: https://abiword.github.io/enchant
.. _Enchant: https://rrthomas.github.io/enchant/
.. _Free Desktop: https://cgit.freedesktop.org/libreoffice/dictionaries/tree/
There are a few ways you can customise novelWriter yourself. Currently, you can add new GUI themes,

View File

@@ -11,6 +11,7 @@ from the text in your project. All of this is handled by the **Manuscript Build*
activate it from the sidebar, the **Tools** menu, or by pressing :kbd:`F5`.
.. versionadded:: 2.1
This tool is new for version 2.1. A simpler tool was used for earlier versions. The simpler tool
only allows you to define a single set of options for the build, but otherwise had much the same
functionality.
@@ -58,6 +59,7 @@ Each build definition can be edited by opening it in the **Manuscript Build Sett
either by double-clicking or by selecting it and pressing the edit button in the toolbar.
.. tip::
You can keep the **Manuscript Build Settings** dialog open while testing the different options,
and just hit the :guilabel:`Apply` button. You can test the result of your settings by pressing
the :guilabel:`Preview` button in the main **Manuscript Build** window. When you're happy with
@@ -177,21 +179,34 @@ Open Document Format
flat version of the document format as a single XML file. Most rich text editors support the
former, and only a few the latter.
Microsoft Word Document Format
The Microsoft Word Document format writes a single ``.docx`` file. It uses a fairly basic format
that should be compatible with most rich text editors.
Portable Document Format (PDF)
The PDF is generated from a copy of the preview document, and should have the same formatting
capabilities as the preview. It's identical to what is produced if you select the print option
and print to PDF.
novelWriter HTML
The HTML format writes a single ``.htm`` file with minimal style formatting. The HTML document
is suitable for further processing by document conversion tools like Pandoc_, for importing in
word processors, or for printing from browser.
Standard/Extended Markdown
The Markdown format comes in both Standard and Extended flavour. The *only* difference in terms
of novelWriter functionality is the support for strike through text, which is not supported by
the Standard flavour.
novelWriter Markup
This is simply a concatenation of the project documents selected by the filters into a ``.txt``
file. The documents are stacked together in the order they appear in the project tree, with
comments, tags, etc. included if they are selected. This is a useful format for exporting the
project for later import back into novelWriter.
Standard/Extended Markdown
The Markdown format comes in both Standard and Extended flavour. The *only* difference in terms
of novelWriter functionality is the support for strike through text, which is not supported by
the Standard flavour.
.. versionadded:: 2.6
Microsoft Word and PDF output options were added.
Additional Formats
@@ -211,13 +226,14 @@ file can be imported directly into a Python dict object or a PHP array, to menti
.. _a_manuscript_print:
Print and PDF
=============
Printing
========
The :guilabel:`Print` button allows you to print the content in the preview window. You can either
print to one of your system's printers, or select PDF as your output format from the printer icon
on the print dialog.
.. note::
The paper format should in all cases default to whatever your system default is. If you want to
change it, you have to select it from the **Print Preview** dialog.

View File

@@ -115,16 +115,17 @@ Root Folder Types
options. Tags in this folder type can be references using the ``@custom`` keyword.
The root folders are closely tied to the tags and reference system. Each folder type corresponds to
the categories of tags that can be used to reference them. For more information about the tags
listed, see :ref:`a_references_references`.
categories of tags that can be used to reference them. For more information about the tags listed,
see :ref:`a_references_references`.
There is also a ``@mention`` keyword that can be used to reference any tag.
See :ref:`a_references_references` for more details.
.. note::
You can rename root folders to whatever you want. However, this doesn't change the reference
keyword or what they do.
.. versionadded:: 2.0
As of version 2.0, you can make multiple root folders of each kind to split up your project.
.. _a_proj_roots_del:
@@ -167,11 +168,10 @@ The folders are there purely as a way for you to organise the documents in meani
to be able to collapse and hide them in the project tree when you're not working on those
documents.
.. versionadded:: 2.0
As of version 2.0 it is possible to add child documents to other documents. This is particularly
useful when you create chapters and scenes. If you add separate scene documents, you should also
add separate chapter documents, even if they only contain a chapter heading. You can then add
scene documents as child items to the chapters.
In addition to using folders. it is also possible to add child documents to other documents. This
is particularly useful when you create chapters and scenes. If you add separate scene documents,
you should also add separate chapter documents, even if they only contain a chapter heading. You
can then add scene documents as child items to the chapters.
.. _a_proj_roots_orphaned:
@@ -182,7 +182,7 @@ Recovered Documents
If novelWriter crashes or otherwise exits without saving the project state, or if you're using a
file synchronisation tool that runs out of sync, there may be files in the project storage folder
that aren't tracked in the core project file. These files, when discovered, are recovered and added
back into the project.
back into the project when a project is opened.
The discovered files are scanned for metadata that give clues as to where the document may
previously have been located in the project. The project loading routine will try to put them back
@@ -202,19 +202,21 @@ be named after its internal key, which is a string of characters and numbers.
Project Lockfile
----------------
To prevent lost documents caused by file conflicts when novelWriter projects are synchronised via
file synchronisation tools, a project lockfile is written to the project storage folder when a
project is open. If you try to open a project which already has such a file present, you will be
presented with a warning, and some information about where else novelWriter thinks the project is
also open. You will be given the option to ignore this warning, and continue opening the project at
your own risk.
To prevent data loss caused by file conflicts when novelWriter projects are synchronised via file
synchronisation tools, a project lockfile is written to the project storage folder when a project
is open. If you try to open a project which already has such a file present, you will be presented
with a warning, and some information about where else novelWriter thinks the project is also open.
You will be given the option to ignore this warning, and continue opening the project at your own
risk.
.. note::
If, for some reason, novelWriter or your computer crashes, the lock file may remain even if
there are no other instances keeping the project open. In such a case it is safe to ignore the
lock file warning when re-opening the project.
.. warning::
If you choose to ignore the warning and continue opening the project, and multiple instances of
the project are in fact open, you are likely to cause inconsistencies and create diverging
project files, potentially resulting in loss of data and orphaned files. You are not likely to
@@ -315,9 +317,14 @@ use these to indicate at what stage of completion each novel document is, or how
content of a note is to the story. You don't have to use them this way, that's just what they were
intended for, but you can make them whatever you want.
Both status and importance labels can be exported and imported so you can share them between
projects, or define a standard set for all your writing projects. When you import labels to a
project, they are always added as *new* labels.
See also :ref:`a_ui_tree_status`.
.. note::
The status or importance level currently in use by one or more documents cannot be deleted, but
they can be edited.
@@ -381,7 +388,22 @@ also export the filtered data to a JSON file or to a CSV file that can be opened
application like for instance Libre Office Calc or Excel.
.. versionadded:: 1.2
As of version 1.2, the log file also stores how much of the session time was spent idle. The
definition of idle here is that the novelWriter main window loses focus, or the user hasn't made
any changes to the currently open document in five minutes. The number of minutes can be altered
in **Preferences**.
Session Timer
-------------
A session timer is by default visible in the status bar. The icon will show you a clock icon when
you are active, and a pause icon when you are considered "idle" per the criteria mentioned above.
If you do not wish to see the timer, you can click on it once to hide it. The icon will still be
visible. Click the icon once more to display the timer again.
.. versionadded:: 2.6
As of version 2.6, clicking the timer text or icon in the status bar will toggle its visibility.

View File

@@ -15,8 +15,9 @@ applications. It is therefore not always obvious to new users how this is suppos
this chapter hopes to explain in more detail how to use this tags and references system.
.. tip::
If you find the Tags and Reference system difficult to follow just from reading this chapter,
you can create a new project in the **Welcome** dialog's New project form and select "Create an
you can create a new project in the **Welcome** dialog's New project form and select "Create an
example project" from the "Pre-fill project" option. The example project contains several
examples of tags and references.
@@ -39,7 +40,8 @@ documents up using the document split feature. See :ref:`a_ui_tree_split_merge`
You can do the same with your notes. You can treat each heading as an independent element of your
notes that can be referenced somewhere else. That way you can collect all your minor or background
characters in a single note file, and still be able to reference them individually by separating
them with headings and assigning each a tag.
them with headings and assigning each a tag. Or, you can lay out your plot in a single document and
have a tag for each plot stage under separate headings.
.. _a_references_tags:
@@ -82,13 +84,20 @@ to the manuscript, or an outline document. If you want to compile a single docum
notes, you can do this from the **Manuscript Build** tool.
.. versionadded:: 2.2
Tags are no longer case sensitive. The tags are by default displayed with the capitalisation you
use when defining the tag, but you don't have to use the same capitalisation when referencing
it later.
.. versionadded:: 2.3
Tags can have an optional display name for manuscript builds.
.. versionadded:: 2.6
You can now add tags also to Novel Documents. These can be used for cross-referencing between
chapters and scenes, and also from notes if desired.
Example of a heading with a tag for a character of the story:
.. code-block:: md
@@ -162,15 +171,27 @@ reference keywords allow multiple values.
Custom references in the current section. The target must be a note tag in a **Custom** type
root folder. The custom folder are for any other category of notes you may want to use.
``@mention``
Anything mentioned, but not present in the current section. It is intended for those cases where
you reveal details about a character or place in a scene without it being otherwise a part of
it. This can be useful when checking for consistency later. Any tag in any root note folder can
be listed under mentions.
``@story``
This is used when referencing a Novel Document, like a scene or chapter, from somewhere else in
your project.
The syntax highlighter will alert the user that the tags and references are used correctly, and
that the tags referenced exist.
.. note::
The highlighter may be mistaken if the index of defined tags is out of date. If so, press
:kbd:`F9` to regenerate it, or select **Rebuild Index** from the **Tools** menu. In general, the
index for a document is regenerated when it is saved, so this shouldn't normally be necessary.
.. tip::
If you add a reference in the editor to a tag that doesn't yet exist, you can right-click it and
select **Create Note for Tag**. This will generate a new project note automatically with the new
tag defined. In order for this to be possible, a root folder for that category of references
@@ -186,6 +207,7 @@ meaningful if you want to be able to click-navigate between them, or of course i
highlight that two notes are related.
.. tip::
If you cross-reference between notes and export your project as an HTML document using the
**Manuscript Build** tool, the cross-references become clickable links in the exported HTML
document as well.

View File

@@ -11,8 +11,8 @@ Active novel documents can only live in a **Novel** type root folder. You can al
**Archive** and **Trash** of course, where they become inactive.
The project tree can distinguish between the different heading levels of the novel documents using
coloured icons, and optionally add emphasis on the label, set in **Preferences** for easier
identification.
coloured icons, and optionally add emphasis on the label. Emphasis can be enabled in
**Preferences**.
.. _a_struct_heads:
@@ -27,6 +27,7 @@ Four levels of headings are supported, signified by the number of hashes (``#``)
title. See also the :ref:`a_fmt` section for more details about the markup syntax.
.. note::
The heading levels are not only important when generating the manuscript, they are also used by
the indexer when building the outline tree in the **Outline View** as well as in the **Novel
Tree**. Each heading also starts a new region where new Tags and References can be defined. See
@@ -60,16 +61,25 @@ The syntax for the four basic heading types, and the three special types, is lis
mid-scene, like if you change the point-of-view character. You are free to use sections as you
wish, and you can filter them out of the final manuscript.
Page breaks can be automatically added before partition, chapter and scene headings from the
**Manuscript Build** tool when you build your project to a format that supports page breaks. If you
want page breaks in other places, you have to specify them manually. See :ref:`a_fmt_break`.
Page breaks can be automatically added before titles, partition, chapter and scene headings from
the **Manuscript Build** tool when you build your project to a format that supports page breaks. If
you want page breaks in other places, you have to specify them manually. See :ref:`a_fmt_break`.
.. tip::
There are multiple options of how to process novel headings when building the manuscript. For
instance, chapter numbers can be applied automatically, and so can scene numbers if you want
them in a draft manuscript. You can also insert point-of-view character names in chapter titles.
See the :ref:`a_manuscript` page for more details.
.. note::
As of 2.6, the heading levels internally in novelWriter do not map directly to heading levels in
manuscript documents. In manuscript documents, chapters are considered the top level heading,
and partitions become plain text paragraphs with a larger font.
.. versionadded:: 2.6
.. _a_struct_heads_title:
@@ -77,15 +87,28 @@ Novel Title and Front Matter
----------------------------
It is recommended that you add a document at the very top of each **Novel** root folder with the
novel title as the first line. You should modify the level 1 heading format code with an ``!`` in
order to render it as a document title that is excluded from any automatic Table of Content in a
manuscript build document, like so:
novel title in it. You should modify the level 1 heading format code with an ``!`` in order to
render it as a document title that is excluded from any automatic Table of Content in a manuscript
build document.
You can also add the author name and address above this if this is required by the manuscript
format you use, and additional space added before the title.
This is the title page novelWriter generates automatically for a new project as of version 2.6:
.. code-block:: md
Jane Doe[br]
Address 1[br]
Address 2 <<
[vspace:5]
#! My Novel
>> _by Jane Doe_ <<
>> **By Jane Doe** <<
>> Word Count: [field:textWords] <<
The title is by default centred on the page. You can add more text to the page as you wish, like
for instance the author's name and details.

View File

@@ -7,6 +7,8 @@ How it Works
.. _Fusion: https://doc.qt.io/qt-6/gallery.html
.. _Pandoc: https://pandoc.org/
.. _Open Document: https://en.wikipedia.org/wiki/OpenDocument
.. _Office Open XML: https://en.wikipedia.org/wiki/Office_Open_XML
.. _version control: https://en.wikipedia.org/wiki/Version_control
The main features of novelWriter are listed in the :ref:`a_intro` chapter. In this chapter, we go
into some more details on how they are implemented. This is intended as an overview. Later on in
@@ -42,9 +44,10 @@ views you can switch between, a quick link to the **Build Manuscript** tool, and
project-related tools and quick access to settings at the bottom.
.. versionadded:: 2.2
A number of new formatting options were added in 2.2 to allow for some special formatting cases.
At the same time, a small formatting toolbar was added to the editor. It is hidden by default,
but can be opened by pressing the button in the top right corner of the editor header.
but can be opened by pressing the button in the top--left corner of the editor header.
Project Tree and Editor View
@@ -54,9 +57,9 @@ Project Tree and Editor View
A screenshot of the Project Tree and Editor View.
When the application is in **Project Tree View** mode, the main work area of the main window is
split in two, or optionally three, panels. The left-most panel contains the project tree and all
the documents in your project. The second panel is the document editor.
When the application is in **Project Tree View** mode, the work area of the main window is split in
two, or optionally three, panels. The left-most panel contains the project tree and all the
documents in your project. The second panel is the document editor.
An optional third panel on the right side contains a document viewer which can view any document in
your project independently of what is open in the document editor. This panel is not intended as a
@@ -68,6 +71,14 @@ The editor also has a **Focus Mode** you can toggle either from the menu, from t
editor's header, or by pressing :kbd:`F8`. When **Focus Mode** is enabled, all the user interface
elements other than the document editor itself are hidden away.
The project tree will highlight with a different background colour the document that is currently
open in the editor.
.. versionadded:: 2.6
You can now drag and drop documents from the project tree onto the editor or viewer panels to
open them.
Novel View and Editor View
--------------------------
@@ -110,21 +121,20 @@ Novel Outline View
A screenshot of the Novel Outline View.
When the application is in **Novel Outline View** mode, the tree, editor and viewer will be
replaced by a large table that shows the entire novel structure with all the tags and references
listed. Pretty much all collected meta data is available here in different columns.
When the application is in **Novel Outline View** mode, the tree, editor and viewer are replaced by
a large table that shows the entire novel structure with all the tags and references listed. Pretty
much all collected meta data is available here in different columns.
You can select which novel root folder to display from the dropdown box, and you can select which
columns to show or hide from the menu button. You can also rearrange the columns by drag and drop.
The app will remember your column order and size between sessions, and for each individual project.
The app remembers your column order and sizes between sessions, and for each individual project.
Colour Themes
-------------
By default, novelWriter will use the colour theme provided by the Qt library, which is determined
by the Fusion_ style setting. You can also choose between a standard dark and light theme that have
neutral colours from **Preferences**.
By default, novelWriter uses a light colour theme. You can also choose between a standard dark
theme that have neutral colours, or a series of other included themes, from **Preferences**.
If you wish, you *can* create your own colour themes, and even have them added to the application.
See :ref:`a_custom_theme` for more details.
@@ -134,6 +144,7 @@ separate colour selectable from the "Document colour theme" setting in **Prefere
separated because there are a lot more options to choose from for the editor and viewer.
.. note::
If you switch between light and dark mode on the GUI, you should also switch editor theme to
match, otherwise icons may be hard to see in the editor and viewer.
@@ -155,11 +166,11 @@ If the project or novel view does not have focus, pressing :kbd:`Ctrl+T` switche
whichever of the two is visible. If one of them already has focus, the key press will switch
between them instead.
Likewise, pressing :kbd:`Ctrl+E` with switch focus to the document editor or viewer, or if any of
Likewise, pressing :kbd:`Ctrl+E` will switch focus to the document editor or viewer, or if any of
them already have focus, it will switch focus between them,
These two shortcuts makes it possible to jump between all these GUI elements without having to
reach for the mouse or touchpad.
These two shortcuts make it possible to jump between all these GUI elements without having to reach
for the mouse or touchpad.
.. _a_breakdown_project:
@@ -167,7 +178,7 @@ reach for the mouse or touchpad.
Project Layout
==============
This is a brief introduction to how you structure your writing projects. All of this will be
This is a brief introduction to how you should structure your writing projects. All of this will be
covered in more detail later.
The main point of novelWriter is that you are free to organise your project documents as you wish
@@ -193,12 +204,19 @@ This heading level structure is only taken into account for :term:`novel documen
:term:`project notes`, the heading levels have no structural meaning, and you are free to use them
however you want. See :ref:`a_struct` and :ref:`a_references` for more details.
.. versionadded:: 2.0
.. tip::
You can add documents as child items of other documents. This is often more useful than adding
folders, since you anyway may want to have the chapter heading in a separate document from your
individual scene documents so that you can rearrange scene documents freely without affecting
chapter placement.
.. versionadded:: 2.6
The heading levels for partitions, chapters and scenes only apply within novelWriter. When you
generate your manuscript, chapters are considered as the topmost heading level, with scenes
below it. Partitions are inserted as text elements in most formats.
.. _a_breakdown_export:
@@ -206,22 +224,24 @@ Building a Manuscript
=====================
The project can at any time be assembled into a range of different formats through the
**Build Manuscript** tool. Natively, novelWriter supports `Open Document`_, HTML5, and
various flavours of Markdown.
**Build Manuscript** tool. Natively, novelWriter supports `Open Document`_, Microsoft Word Document
(known as `Office Open XML`_), HTML5, and various flavours of Markdown. You can also generate a PDF
document.
The HTML5 format is suitable for conversion by a number of other tools like Pandoc_, or for
importing into word processors if the Open Document format isn't suitable. The Open Document format
is supported by most office type applications. In addition, printing is also possible. Print to PDF
is available from the print dialog.
importing into word processors if the Open Document or Word Document format isn't suitable. The
Open Document format is supported by most office type applications, so is the Word Document format.
In addition, printing is also possible.
For advanced processing, you can export the content of the project to a JSON file. This is useful
if you want to write your own custom processing script in for instance Python, as the entire novel
can be read into a Python dictionary with a couple of lines of code. The JSON file can be populated
with either HTML formatted text, or with the raw text as typed into the novel documents.
with either HTML formatted text, or with the raw text as typed it into the novel documents.
See :ref:`a_manuscript` for more details.
.. versionadded:: 2.1
You can now define multiple build definitions in the **Build Manuscript** tool. This allows you
to define specific settings for various types of draft documents, outline documents, and
manuscript formats. See :ref:`a_manuscript` for more details.
@@ -234,8 +254,8 @@ Project Storage
The files of a novelWriter project are stored in a dedicated project folder. The project structure
is kept in a file at the root of this folder called ``nwProject.nwx``. All the document files and
associated meta data is stored in other folders below the project folder. For more technical
details about what all the files mean and how they're organised, see the :ref:`a_storage` section.
associated meta data are stored in other folders below the project folder. For a more technical
description of what all the files mean and how they're organised, see the :ref:`a_storage` section.
This way of storing data was chosen for several reasons.
@@ -247,17 +267,19 @@ crashes.
Secondly, having multiple small files means it is very easy to synchronise them between computers
with standard file synchronisation tools.
Thirdly, if you use version control software to track the changes to your project, the file formats
used for the files are well suited. All the JSON documents have line breaks and indents as well,
which makes it easier to track them with version control software.
Thirdly, if you use `version control`_ software to track the changes to your project, the file
formats used for the files are well suited. All the JSON documents have line breaks and indents as
well, which makes it easier to track them with version control software.
.. note::
Since novelWriter has to keep track of a bunch of files and folders when a project is open, it
may not run well on some virtual file systems. A file or folder must be accessible with exactly
the path it was saved or created with. An example where this is not the case is the way Google
Drive is mapped on Linux Gnome desktops using gvfs/gio.
.. caution::
You should not add additional files to the project folder yourself. Nor should you, as a rule,
manually edit files within it. If you really must manually edit the text files, e.g. with some
automated task you want to perform, you need to rebuild the :term:`Project Index` when you open

View File

@@ -14,6 +14,10 @@ In addition to formatting codes, novelWriter allows for comments, a synopsis tag
keyword and value sets used for :term:`tags<tag>` and :term:`references<reference>`. There are also
some codes that apply to whole paragraphs. See :ref:`a_fmt_text` for more details.
URLs should also be highlighted and become clickable. However, only URLs starting with "http" or
"https" are recognised. In the editor, you must hold down the :kbd:`Ctrl` key when clicking a URL
to follow it.
.. _a_fmt_hlight:
@@ -76,14 +80,13 @@ For headings level one through three, adding a ``!`` modifies the meaning of the
``#! Title Text``
This tells the build tool that the level one heading is intended to be used for the novel or
notes folder's main title, like for instance on the front page. When building the manuscript,
this will use a different styling and will exclude the title from, for instance, a Table of
Contents in Libre Office.
notes folder's main title, like for instance the novel title on the cover page. When building
the manuscript, this will use a different styling.
``##! Title Text``
This tells the build tool to not assign a chapter number to this chapter title if automatic
chapter numbers are being used. Such titles are useful for a prologue for instance. See
:ref:`a_struct_heads_unnum` for more details.
chapter numbers are being used. Such titles are useful for prologues and epilogues for instance.
See :ref:`a_struct_heads_unnum` for more details.
``###! Title Text``
This is an alternative scene heading that can be formatted differently in the **Manuscript
@@ -92,6 +95,7 @@ For headings level one through three, adding a ``!`` modifies the meaning of the
details.
.. note::
The space after the ``#`` or ``!`` character is mandatory. The syntax highlighter will change
colour and font size when the heading is correctly formatted.
@@ -118,10 +122,18 @@ Non-breaking spaces are highlighted by the syntax highlighter with an alternate
background, depending on the selected theme.
.. tip::
Non-breaking spaces are for instance the correct type of space to separate a number from its
unit. Generally, non-breaking spaces are used to prevent line wrapping algorithms from adding
line breaks where they shouldn't.
.. note::
You should not manually apply text indentation in your paragraphs in the editor. These can be
added automatically in the **Manuscript Build** tool if you want to have them in the manuscript.
Each new paragraph should be separated from the previous content by an empty line and with no
indentation.
.. _a_fmt_emph:
@@ -160,6 +172,7 @@ In addition, the following rules apply:
allow for formatting, and any formatting markup will be rendered as-is.
.. tip::
novelWriter supports standard escape syntax for the emphasis markup characters in case the
editor misunderstands your intended usage of them. That is, ``\*``, ``\_`` and ``\~`` will
generate a plain ``*``, ``_`` and ``~``, respectively, without interpreting them as part of the
@@ -216,8 +229,8 @@ In addition to the above formatting features, novelWriter also allows for commen
The text of a comment is always ignored by the word counter. The text can also be filtered out
when building the manuscript or viewing the document.
The first word of a comment, followed by a colon, can be one of a small set of modifiers that
indicates the comment is intended for a specific purpose. For instance, if the comment starts with
The first word of a comment, followed by a colon, can be one of a set of modifiers that indicates
the comment is intended for a specific purpose. For instance, if the comment starts with
``Synopsis:``, the comment is treated in a special manner and will show up in the
:ref:`a_ui_outline` in a dedicated column. The word ``synopsis`` is not case sensitive. If it is
correctly formatted, the syntax highlighter will indicate this by altering the colour of the word.
@@ -246,6 +259,7 @@ The different styles of comments are as follows:
use them in your text.
.. note::
Only one comment can be flagged as a synopsis or short comment for each heading. If multiple
comments are flagged as synopsis or short comments, the last one will be used and the rest
ignored.
@@ -365,6 +379,7 @@ Examples:
"``> Left/right indented text <``", "The text has both margins increased."
.. note::
The text editor will not show the alignment and indentation live. But the viewer will show them
when you open the document there. It will of course also be reflected in the document generated
from the manuscript build tool as long as the format supports paragraph alignment.
@@ -373,18 +388,15 @@ Examples:
Alignment with Line Breaks
--------------------------
If you have line breaks in the paragraph, like for instance when you are writing verses, the
alignment markers must be applied to the first line. Markers on the other lines are ignored. The
markers for the first line are used for all the other lines.
For the following text, all lines will be centred, not just the first:
If you have line breaks in the paragraph, the markers for all the lines are combined and used for
the entire paragraph. For the following text, all lines will be centred:
.. code-block:: md
>> I am the very model of a modern Major-General <<
>> I am the very model of a modern Major-General
I've information vegetable, animal, and mineral
I know the kings of England, and I quote the fights historical
From Marathon to Waterloo, in order categorical
From Marathon to Waterloo, in order categorical <<
Alignment with First Line Indent
@@ -424,6 +436,7 @@ If you need to add a page break somewhere, put the text ``[new page]`` on a line
the text you wish to start on a new page.
.. note::
The page break code is applied to the text that follows it. It adds a "page break before" mark
to the text when exporting to HTML or Open Document. This means that a ``[new page]`` which has
no text following it, it will not result in a page break.
@@ -442,3 +455,16 @@ the text you wish to start on a new page.
[new page]
This text will start on a new page if the build format has pages.
.. _a_fmt_stats:
Inserting Word Counts in the Text
=================================
The cover page of a manuscript normally has the word count stated on it. Any statistics value
collected by novelWriter can be inserted into any document. You can generate the code for this from
the **Insert** menu under **Word/Character Count**.
The value inserted is the actual count for your entire manuscript, so it is not populated until you
run the **Manuscript Build** tool. Until then they will show up as "0" in the viewer panel.

View File

@@ -63,6 +63,7 @@ currently selected item. This panel also includes the latest paragraph and chara
addition to the word count.
.. tip::
If you want to set the label of a document to be the same as a header within it, you can
right-click a header in the document when it is open in the editor and select
:guilabel:`Set as Document Name` from the context menu.
@@ -140,23 +141,19 @@ lives in. If it's in a **Novel** type folder, it uses the "Status" flag, otherwi
Project Tree Drag & Drop
------------------------
The project tree allows drag & drop to a certain extent to allow you to reorder your documents and
folders. Moving a document in the project tree will affect the text's position when you assemble
your manuscript in the **Manuscript Build** tool.
The project tree allows drag & drop to allow you to reorder your documents and folders. Moving a
document in the project tree will affect the text's position when you assemble your manuscript in
the **Manuscript Build** tool.
.. versionadded:: 2.2
You can now select multiple items in the project tree by holding down the :kbd:`Ctrl` or
:kbd:`Shift` key while selecting items.
You can drag and drop documents and regular folders, but not root folders. If you select multiple
items, they can only be dragged and dropped if they are siblings. That is, they have the same
parent item in the project. This limitation is due to the way drag and drop is implemented in the
user interface framework novelWriter is built upon.
Documents and their folders can be rearranged freely within their root folders. If you move a
**Novel Document** out of a **Novel** folder, it will be converted to a **Project Note**. Notes can
be moved freely between all root folders, but keep in mind that if you move a note into a **Novel**
type root folder, its "Importance" setting will be switched with a "Status" setting. See
type root folder, its "Importance" setting will be replaced by a "Status" setting. See
:ref:`a_ui_tree_status`. The old value will not be overwritten though, and should be restored if
you move it back at some point.
@@ -164,6 +161,10 @@ Root folders in the project tree cannot be dragged and dropped at all. If you wa
you can move them up or down with respect to each other from the arrow buttons at the top of the
project tree, or by pressing :kbd:`Ctrl+Up` or :kbd:`Ctrl+Down` when they are selected.
.. tip::
You can drag and drop documents onto the editor or viewer panel to open them.
.. _a_ui_tree_novel:
@@ -180,6 +181,7 @@ the view in the **Outline View**. It is convenient when you want to browse the s
story itself rather than the document files.
.. note::
You cannot reorganise the entries in the novel view, or add any new documents, as that would
imply restructuring the content of the document files themselves. Any such editing must be done
in the project tree. However, you can add new headings to existing documents, or change
@@ -208,6 +210,7 @@ You can select which novel folder to display from the dropdown menu. You can opt
to show a combination of all novel folders.
.. note::
Since the internal structure of the novel does not depend directly on the folder and document
structure of the project tree, this view will not necessarily look the same, depending on how
you choose to organise your documents. See the :ref:`a_struct` page for more details.
@@ -218,6 +221,7 @@ the menu button in the toolbar. The order of the columns can also be rearranged
a different position. You column settings are saved between sessions on a per-project basis.
.. note::
The **Title** column cannot be disabled or moved.
The information viewed in the outline is based on the :term:`project index`. While novelWriter does

View File

@@ -165,6 +165,7 @@ a key or key combination for the inserted content.
":kbd:`Ctrl+K`, :kbd:`G`", "Insert a ``@tag`` keyword"
":kbd:`Ctrl+K`, :kbd:`H`", "Insert a short description comment"
":kbd:`Ctrl+K`, :kbd:`L`", "Insert a ``@location`` keyword"
":kbd:`Ctrl+K`, :kbd:`M`", "Insert a ``@mention`` keyword"
":kbd:`Ctrl+K`, :kbd:`O`", "Insert an ``@object`` keyword"
":kbd:`Ctrl+K`, :kbd:`P`", "Insert a ``@plot`` keyword"
":kbd:`Ctrl+K`, :kbd:`S`", "Insert a synopsis comment"

View File

@@ -18,10 +18,10 @@ Editing a Document
A screenshot of the Document Editor panel.
To edit a document, double-click it in the project tree, or press the :kbd:`Return` key while
having it selected. This will open the document in the document editor. The editor uses a
Markdown-like syntax for some features, and a novelWriter-specific syntax for others. The syntax
format is described in the :ref:`a_fmt` chapter.
To edit a document, double-click it in the project tree, press the :kbd:`Return` key while having
it selected, or drag and drop it onto the editor panel. This will open the document in the document
editor. The editor uses a Markdown-like syntax for some features, and a novelWriter-specific syntax
for others. The syntax format is described in the :ref:`a_fmt` chapter.
The editor has a maximise button, which toggles the **Focus Mode**, and a close button in the
top--right corner. On the top--left side you will find a tools button that opens a toolbar with a
@@ -33,6 +33,7 @@ header at the top of the edit or view panel. Optionally, the full project path t
be shown. This can be set in **Preferences**.
.. tip::
Clicking on the document title bar will select the document in the project tree and thus reveal
its location there, making it easier to find in a large project.
@@ -40,6 +41,24 @@ Any :term:`references<reference>` in the editor can be opened in the viewer by m
the label and pressing :kbd:`Ctrl+Return`. You can also control-click them with your mouse.
Spell Checking
--------------
A third party library called Enchant is used for spell checking in the editor. The controls for
spell checking can be found in the **Tools** menu. You can also set spell checking language in
**Project Settings**.
This spell checking library comes with support for custom words that you can add by selecting
"Add Word to Dictionary" from the context menu when a word is highlighted by the spell checker as
misspelled. The custom words are managed on a per-project basis, and can the list of words can be
edited from the **Project Word List** tool available from the **Tools** menu.
.. note::
Generally, spell checking dictionaries are collected from your operating system, but on Windows
they are not. See :ref:`a_custom_dict` for how to add spell checking dictionaries on Windows.
Editor Auto-Completer
---------------------
@@ -61,14 +80,14 @@ Viewing a Document
A screenshot of the Document Viewer panel.
Any document in the project tree can also be viewed in parallel in a right hand side document
viewer. To view a document, press :kbd:`Ctrl+R`, or select **View Document** in the menu or context
menu. If you have a middle mouse button, middle-clicking on the document will also open it in the
viewer.
viewer. To view a document, press :kbd:`Ctrl+R`, select **View Document** in the menu or context
menu, or drag and drop the document onto the viewer panel. If you have a middle mouse button,
middle-clicking on the document will also open it in the viewer.
The document viewed does not have to be the same document as the one currently being edited.
However, If you *are* viewing the same document, pressing :kbd:`Ctrl+R` again will update the
document with your latest changes. You can also press the reload button in the top--right corner of
the viewer panel, next to the close button, to achieve the same thing.
However, If you *are* viewing the same document, pressing :kbd:`Ctrl+R` from the editor will update
the document with your latest changes. You can also press the reload button in the top--right
corner of the viewer panel, next to the close button, to achieve the same thing.
In the viewer :term:`references<reference>` become clickable links. Clicking them will replace the
content of the viewer with the content of the document the reference points to.
@@ -77,7 +96,7 @@ The document viewer keeps a history of viewed documents, which you can navigate
buttons in the top--left corner of the viewer. If your mouse has backward and forward navigation
buttons, these can be used as well. They work just like the backward and forward features in a
browser. The left-most button is a dropdown menu for quickly navigation between headings in the
document.
document. The edit button on the right will open the viewed document in the editor.
At the bottom of the view panel there is a **References** panel. (If it is hidden, click the button
on the left side of the footer area to reveal it.) This panel contains a References tab with links
@@ -86,12 +105,14 @@ you have created root folders and tags for various story elements like character
these will appear as additional tabs in this panel.
.. note::
The **References** panel relies on an up-to-date :term:`index<project index>` of the project.
The index is maintained automatically. However, if anything is missing, or seems wrong, the
index can always be rebuilt by selecting **Rebuild Index** from the **Tools** menu, or by
pressing :kbd:`F9`.
.. versionadded:: 2.2
The reference panel was redesigned and the additional tabs added.
@@ -118,10 +139,8 @@ document when reaching the end. There is also a switch that will try to match th
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.
.. seealso::
For more information on the capabilities of the Regular Expression option, see the Qt
documentation for the `QRegularExpression <https://doc.qt.io/qt-5/qregularexpression.html>`_
class.
@@ -136,6 +155,7 @@ A few auto-replace features are supported by the editor. You can control every a
auto-replace feature from **Preferences**. You can also disable this feature entirely if you wish.
.. tip::
If you don't like auto-replacement, all symbols inserted by this feature are also available in
the :guilabel:`Insert` menu, and via :ref:`a_kb_ins`. You may also be using a `Compose Key`_
setup, which means you may not need the auto-replace feature at all.
@@ -147,6 +167,22 @@ always accurate. Especially distinguishing between closing single quote and apos
tricky for languages that use the same symbol for these, like English does.
.. tip::
If the auto-replace feature changes a symbol when you did not want it to change, pressing
:kbd:`Ctrl+Z` once after the auto-replacement will undo it without undoing the character
you typed before it.
.. _a_ui_edit_dialogue:
Dialogue Highlighting
=====================
The editor and viewer can optionally highlight character dialogue in your novel documents. By
default, this feature is set to display text in double quotes as dialogue. In **Preferences**, you
can change this to support various other dialogue markers that instead uses dashes to indicate
dialogue, like those used in Spanish, Portuguese, Polish, etc.
Various styles are supported by changing the settings under "Text Highlighting" in **Preferences**.
It is also possible to include the dialogue highlighting in your manuscripts.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

5067
i18n/nw_ru_RU.ts Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,10 +3,10 @@ novelWriter – Init File
=======================
File History:
Created: 2018-09-22 [0.0.1]
Created: 2018-09-22 [0.0.1] main
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
Copyright (C) 2018 Veronica Berglyd Olsen and novelWriter contributors
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
@@ -47,9 +47,9 @@ __license__ = "GPLv3"
__author__ = "Veronica Berglyd Olsen"
__maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net"
__version__ = "2.5"
__hexversion__ = "0x020500f0"
__date__ = "2024-07-10"
__version__ = "2.6"
__hexversion__ = "0x020600f0"
__date__ = "2025-01-26"
__status__ = "Stable"
__domain__ = "novelwriter.io"
@@ -60,10 +60,26 @@ logger = logging.getLogger(__name__)
# Main Program
##
# Global config and data singletons
# Globals Singletons
CONFIG = Config()
SHARED = SharedData()
# ANSI Colours
C_RED = "\033[91m"
C_GREEN = "\033[92m"
C_YELLOW = "\033[93m"
C_BLUE = "\033[94m"
C_WHITE = "\033[97m"
C_END = "\033[0m"
# Log Formats
L_TIME = "[{asctime:}]"
L_FILE = "{filename:>18}"
L_LINE = "{lineno:<4d}"
L_LVLP = "{levelname:8}"
L_LVLC = "{levelname:17}"
L_TEXT = "{message:}"
def main(sysArgs: list | None = None) -> GuiMain | None:
"""Parse command line, set up logging, and launch main GUI."""
@@ -77,6 +93,7 @@ def main(sysArgs: list | None = None) -> GuiMain | None:
"version",
"info",
"debug",
"color",
"style=",
"config=",
"data=",
@@ -98,6 +115,7 @@ def main(sysArgs: list | None = None) -> GuiMain | None:
" -v, --version Print program version and exit.\n"
" --info Print additional runtime information.\n"
" --debug Print debug output. Includes --info.\n"
" --color Add ANSI colors to log output.\n"
" --meminfo Show memory usage information in the status bar.\n"
" --style= Sets Qt5 style flag. Defaults to 'Fusion'.\n"
" --config= Alternative config file.\n"
@@ -106,12 +124,12 @@ def main(sysArgs: list | None = None) -> GuiMain | None:
# Defaults
logLevel = logging.WARN
logFormat = "{levelname:8} {message:}"
fmtFlags = 0b00
confPath = None
dataPath = None
testMode = False
qtStyle = "Fusion"
cmdOpen = None
qtStyle = "Fusion"
cmdOpen = None
# Parse Options
try:
@@ -135,8 +153,10 @@ def main(sysArgs: list | None = None) -> GuiMain | None:
logLevel = logging.INFO
elif inOpt == "--debug":
CONFIG.isDebug = True
fmtFlags = fmtFlags | 0b10
logLevel = logging.DEBUG
logFormat = "[{asctime:}] {filename:>18}:{lineno:<4d} {levelname:8} {message:}"
elif inOpt == "--color":
fmtFlags = fmtFlags | 0b01
elif inOpt == "--style":
qtStyle = inArg
elif inOpt == "--config":
@@ -148,13 +168,32 @@ def main(sysArgs: list | None = None) -> GuiMain | None:
elif inOpt == "--meminfo":
CONFIG.memInfo = True
if fmtFlags & 0b01:
# This will overwrite the default level names, and also ensure that
# they can be converted back to integer levels
logging.addLevelName(logging.DEBUG, f"{C_BLUE}DEBUG{C_END}")
logging.addLevelName(logging.INFO, f"{C_GREEN}INFO{C_END}")
logging.addLevelName(logging.WARNING, f"{C_YELLOW}WARNING{C_END}")
logging.addLevelName(logging.ERROR, f"{C_RED}ERROR{C_END}")
logging.addLevelName(logging.CRITICAL, f"{C_RED}CRITICAL{C_END}")
# Determine Log Format
if fmtFlags == 0b00:
logFmt = f"{L_LVLP} {L_TEXT}"
elif fmtFlags == 0b01:
logFmt = f"{L_LVLC} {L_TEXT}"
elif fmtFlags == 0b10:
logFmt = f"{L_TIME} {L_FILE}:{L_LINE} {L_LVLP} {L_TEXT}"
elif fmtFlags == 0b11:
logFmt = f"{L_TIME} {C_BLUE}{L_FILE}{C_END}:{C_WHITE}{L_LINE}{C_END} {L_LVLC} {L_TEXT}"
# Setup Logging
pkgLogger = logging.getLogger(__package__)
pkgLogger.setLevel(logLevel)
if len(pkgLogger.handlers) == 0:
# Make sure we only create one logger (mostly an issue with tests)
cHandle = logging.StreamHandler()
cHandle.setFormatter(logging.Formatter(fmt=logFormat, style="{"))
cHandle.setFormatter(logging.Formatter(fmt=logFmt, style="{"))
pkgLogger.addHandler(cHandle)
logger.info("Starting novelWriter %s (%s) %s", __version__, __hexversion__, __date__)

View File

@@ -1,18 +1,20 @@
{
"Synopsis": "Synopsis",
"Short Description": "Kurzbeschreibung",
"Footnotes": "Fußnoten",
"Comment": "Kommentar",
"Notes": "Notizen",
"Tag": "Schlagwort",
"Point of View": "Perspektive",
"Focus": "Mittelpunkt",
"Focus": "Fokus",
"Characters": "Charaktere",
"Plot": "Handlungen",
"Plot": "Handlung",
"Timeline": "Zeitleiste",
"Locations": "Schauplätze",
"Objects": "Objekte",
"Entities": "Organisationen",
"Custom": "Benutzerdefiniert",
"New Page": "Neue Seite",
"0": "Null",
"1": "Eins",
"2": "Zwei",

View File

@@ -14,6 +14,7 @@
"Objects": "Objects",
"Entities": "Entities",
"Custom": "Custom",
"New Page": "New Page",
"0": "Zero",
"1": "One",
"2": "Two",

View File

@@ -1,6 +1,7 @@
{
"Synopsis": "Synopsis",
"Short Description": "Short Description",
"Footnotes": "Footnotes",
"Comment": "Comment",
"Notes": "Notes",
"Tag": "Tag",
@@ -13,6 +14,7 @@
"Objects": "Objects",
"Entities": "Entities",
"Custom": "Custom",
"New Page": "New Page",
"0": "Zero",
"1": "One",
"2": "Two",

View File

@@ -1,6 +1,7 @@
{
"Synopsis": "Sinossi",
"Short Description": "Breve descrizione",
"Footnotes": "Note a piè pagina",
"Comment": "Commento",
"Notes": "Note",
"Tag": "Etichetta",
@@ -13,6 +14,7 @@
"Objects": "Oggetti",
"Entities": "Enti",
"Custom": "Personalizzato",
"New Page": "Nuova pagina",
"0": "Zero",
"1": "Uno",
"2": "Due",

View File

@@ -1,6 +1,7 @@
{
"Synopsis": "あらすじ",
"Short Description": "短い説明",
"Footnotes": "脚注",
"Comment": "コメント ",
"Notes": "ノート",
"Tag": "タグ",
@@ -13,6 +14,7 @@
"Objects": "オブジェクト",
"Entities": "エンティティ",
"Custom": "カスタム",
"New Page": "新規ページ",
"0": "零",
"1": "一",
"2": "二",

View File

@@ -1,6 +1,7 @@
{
"Synopsis": "Sammendrag",
"Short Description": "Kort beskrivelse",
"Footnotes": "Fotnoter",
"Comment": "Kommentar",
"Notes": "Notat",
"Tag": "Knagg",
@@ -13,6 +14,7 @@
"Objects": "Objekter",
"Entities": "Enheter",
"Custom": "Annet",
"New Page": "Ny side",
"0": "null",
"1": "én",
"2": "to",

View File

@@ -1,6 +1,7 @@
{
"Synopsis": "Synopsis",
"Short Description": "Korte beschrijving",
"Footnotes": "Voetnoten",
"Comment": "Opmerking",
"Notes": "Notities",
"Tag": "Label",
@@ -13,6 +14,7 @@
"Objects": "Objecten",
"Entities": "Entiteiten",
"Custom": "Aangepast",
"New Page": "Nieuwe pagina",
"0": "nul",
"1": "één",
"2": "twee",

View File

@@ -1,6 +1,7 @@
{
"Synopsis": "Streszczenie",
"Short Description": "Krótki opis",
"Footnotes": "Przypisy",
"Comment": "Komentarz",
"Notes": "Notatki",
"Tag": "Znacznik",
@@ -13,6 +14,7 @@
"Objects": "Obiekty",
"Entities": "Podmioty",
"Custom": "Różne",
"New Page": "Nowa strona",
"0": "zero",
"1": "pierwszy",
"2": "drugi",

View File

@@ -1,6 +1,7 @@
{
"Synopsis": "Sinopse",
"Short Description": "Descrição breve",
"Footnotes": "Notas de rodapé",
"Comment": "Comentário",
"Notes": "Notas",
"Tag": "Etiqueta",
@@ -13,6 +14,7 @@
"Objects": "Objetos",
"Entities": "Entidades",
"Custom": "Outros",
"New Page": "Nova página",
"0": "Zero",
"1": "Um",
"2": "Dois",

View File

@@ -1,7 +1,18 @@
{
"Synopsis": "Сводка",
"Short Description": "Краткое описание",
"Comment": "Комментарий",
"Notes": "Заметки",
"Tag": "Тэг",
"Point of View": "Точка зрения",
"Focus": "Фокус",
"Characters": "Персонажей",
"Plot": "Сюжет",
"Timeline": "Хронология",
"Locations": "Локации",
"Objects": "Объекты",
"Entities": "Сущности",
"Custom": "Другое",
"0": "Ноль",
"1": "Один",
"2": "Два",

View File

@@ -1,6 +1,7 @@
{
"Synopsis": "概要",
"Short Description": "简短描述",
"Footnotes": "注释",
"Comment": "注释",
"Notes": "笔记",
"Tag": "标签",
@@ -13,6 +14,7 @@
"Objects": "物品",
"Entities": "条目",
"Custom": "自定义",
"New Page": "新建页面",
"0": "零",
"1": "一",
"2": "二",

View File

@@ -44,6 +44,7 @@ cls_template = mixed_document-new.svg
cls_timeline = typ_calendar.svg
cls_trash = typ_trash.svg
cls_world = typ_location.svg
copy = mixed_copy.svg
cross = typ_times.svg
document = typ_document.svg
down = typ_chevron-down.svg
@@ -63,6 +64,10 @@ font = nw_font.svg
forward = typ_chevron-right.svg
import = mixed_import.svg
list = typ_th-list.svg
margin_bottom = mixed_margin-bottom.svg
margin_left = mixed_margin-left.svg
margin_right = mixed_margin-right.svg
margin_top = mixed_margin-top.svg
maximise = typ_arrow-maximise.svg
menu = typ_th-dot-menu.svg
minimise = typ_arrow-minimise.svg
@@ -93,6 +98,8 @@ search_regex = nw_search-regex.svg
search_replace = mixed_search-replace.svg
search_word = nw_search-word.svg
settings = typ_cog.svg
size_height = mixed_size-height.svg
size_width = mixed_size-width.svg
star = typ_star.svg
status_idle = typ_media-pause-grey.svg
status_lang = typ_globe-grey.svg
@@ -101,6 +108,7 @@ status_stats = typ_chart-bar-grey.svg
status_time = typ_stopwatch-grey.svg
sticky-off = typ_pin-outline.svg
sticky-on = typ_pin.svg
toolbar = nw_toolbar.svg
unchecked = mixed_input-unchecked.svg
unfold-hide = typ_unfold-hidden.svg
unfold-show = typ_unfold-visible.svg

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m7.8824 2c-1.2988 0-2.3529 1.0541-2.3529 2.3529v2.3529h-1.1765c-1.2988 0-2.3529 1.0541-2.3529 2.3529v10.588c0 1.2988 1.0541 2.3529 2.3529 2.3529h10.588c1.2988 0 2.3529-1.0541 2.3529-2.3529v-1.1765h2.3529c1.2988 0 2.3529-1.0541 2.3529-2.3529v-11.765c0-1.2988-1.0541-2.3529-2.3529-2.3529zm0 2.3529h11.765v11.765h-2.3529v-7.0588c0-1.2988-1.0541-2.3529-2.3529-2.3529h-7.0588zm-3.5294 4.7059h10.588c0.32353 0 0-0.32353 0 0v10.588h-10.588v-9.8943z" fill="#69c" stroke-width="1.1765"/>
</svg>

After

Width:  |  Height:  |  Size: 633 B

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="2" width="20" height="20" rx="1" ry="1" fill="#e6e6e6" fill-opacity=".14902" stroke-linecap="round" stroke-width="2"/>
<rect x="2" y="18" width="20" height="4" rx="1" ry="1" fill="#848484" stroke-linecap="round" stroke-width="2"/>
<path d="m18.414 8.5959c0.78104 0.79361 0.78104 2.0837 0 2.8773l-6.4141 6.5268-6.4141-6.5268c-0.391-0.3968-0.58601-0.91769-0.58601-1.4386 0-0.52092 0.195-1.0418 0.58601-1.4386 0.78104-0.79464 2.047-0.79464 2.8281 0l3.5861 3.6475 3.5861-3.6475c0.78104-0.79464 2.047-0.79464 2.8281 0z" fill="#69c" stroke-width="1.0087"/>
</svg>

After

Width:  |  Height:  |  Size: 713 B

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="2" width="20" height="20" rx="1" ry="1" fill="#e6e6e6" fill-opacity=".14902" stroke-linecap="round" stroke-width="2"/>
<rect x="2" y="2" width="4" height="20" rx="1" ry="1" fill="#848484" stroke-linecap="round" stroke-width="2"/>
<path d="m15.404 18.5c-0.79361 0.78104-2.0837 0.78104-2.8773 0l-6.5268-6.4141 6.5268-6.4141c0.3968-0.391 0.91769-0.58601 1.4386-0.58601 0.52092 0 1.0418 0.195 1.4386 0.58601 0.79464 0.78104 0.79464 2.047 0 2.8281l-3.6475 3.5861 3.6475 3.5861c0.79464 0.78104 0.79464 2.047 0 2.8281z" fill="#69c" stroke-width="1.0087"/>
</svg>

After

Width:  |  Height:  |  Size: 711 B

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="2" width="20" height="20" rx="1" ry="1" fill="#e6e6e6" fill-opacity=".14902" stroke-linecap="round" stroke-width="2"/>
<rect x="18" y="2" width="4" height="20" rx="1" ry="1" fill="#848484" stroke-linecap="round" stroke-width="2"/>
<path d="m8.5959 18.5c0.79361 0.78104 2.0837 0.78104 2.8773 0l6.5268-6.4141-6.5268-6.4141c-0.3968-0.391-0.91769-0.58601-1.4386-0.58601-0.52092 0-1.0418 0.195-1.4386 0.58601-0.79464 0.78104-0.79464 2.047 0 2.8281l3.6475 3.5861-3.6475 3.5861c-0.79464 0.78104-0.79464 2.047 0 2.8281z" fill="#69c" stroke-width="1.0087"/>
</svg>

After

Width:  |  Height:  |  Size: 711 B

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="2" width="20" height="20" rx="1" ry="1" fill="#e6e6e6" fill-opacity=".14902" stroke-linecap="round" stroke-width="2"/>
<rect x="2" y="2" width="20" height="4" rx="1" ry="1" fill="#848484" stroke-linecap="round" stroke-width="2"/>
<path d="m18.414 15.404c0.78104-0.79361 0.78104-2.0837 0-2.8773l-6.4141-6.5268-6.4141 6.5268c-0.391 0.3968-0.58601 0.91769-0.58601 1.4386 0 0.52092 0.195 1.0418 0.58601 1.4386 0.78104 0.79464 2.047 0.79464 2.8281 0l3.5861-3.6475 3.5861 3.6475c0.78104 0.79464 2.047 0.79464 2.8281 0z" fill="#69c" stroke-width="1.0087"/>
</svg>

After

Width:  |  Height:  |  Size: 712 B

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="2" width="20" height="20" rx="1" ry="1" fill="#e6e6e6" fill-opacity=".14902" stroke-linecap="round" stroke-width="2"/>
<path d="m18.414 10.404c0.78104-0.79361 0.78104-2.0837 0-2.8773l-6.4141-6.5268-6.4141 6.5268c-0.391 0.3968-0.58601 0.91769-0.58601 1.4386 0 0.52092 0.195 1.0418 0.58601 1.4386 0.78104 0.79464 2.047 0.79464 2.8281 0l3.5861-3.6475 3.5861 3.6475c0.78104 0.79464 2.047 0.79464 2.8281 0z" fill="#69c" stroke-width="1.0087"/>
<path d="m18.414 13.596c0.78104 0.79361 0.78104 2.0837 0 2.8773l-6.4141 6.5268-6.4141-6.5268c-0.391-0.3968-0.58601-0.91769-0.58601-1.4386 0-0.52092 0.195-1.0418 0.58601-1.4386 0.78104-0.79464 2.047-0.79464 2.8281 0l3.5861 3.6475 3.5861-3.6475c0.78104-0.79464 2.047-0.79464 2.8281 0z" fill="#69c" stroke-width="1.0087"/>
</svg>

After

Width:  |  Height:  |  Size: 921 B

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="2" width="20" height="20" rx="1" ry="1" fill="#e6e6e6" fill-opacity=".15" stroke-linecap="round" stroke-width="2"/>
<path d="m13.596 18.414c0.79361 0.78104 2.0837 0.78104 2.8773 0l6.5268-6.4141-6.5268-6.4141c-0.3968-0.391-0.91769-0.58601-1.4386-0.58601-0.52092 0-1.0418 0.195-1.4386 0.58601-0.79464 0.78104-0.79464 2.047 0 2.8281l3.6475 3.5861-3.6475 3.5861c-0.79464 0.78104-0.79464 2.047 0 2.8281z" fill="#69c" stroke-width="1.0087"/>
<path d="m10.404 18.414c-0.79361 0.78104-2.0837 0.78104-2.8773 0l-6.5268-6.4141 6.5268-6.4141c0.3968-0.391 0.91769-0.58601 1.4386-0.58601 0.52092 0 1.0418 0.195 1.4386 0.58601 0.79464 0.78104 0.79464 2.047 0 2.8281l-3.6475 3.5861 3.6475 3.5861c0.79464 0.78104 0.79464 2.047 0 2.8281z" fill="#69c" stroke-width="1.0087"/>
</svg>

After

Width:  |  Height:  |  Size: 919 B

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<rect x="8" y="2" width="8" height="8" rx="1" ry="1" fill="none" stroke="#69c" stroke-linecap="round" stroke-width="2"/>
<rect x="8" y="14" width="8" height="8" rx="1" ry="1" fill="none" stroke="#69c" stroke-linecap="round" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 389 B

View File

@@ -44,6 +44,7 @@ cls_template = mixed_document-new.svg
cls_timeline = typ_calendar.svg
cls_trash = typ_trash.svg
cls_world = typ_location.svg
copy = mixed_copy.svg
cross = typ_times.svg
document = typ_document.svg
down = typ_chevron-down.svg
@@ -63,6 +64,10 @@ font = nw_font.svg
forward = typ_chevron-right.svg
import = mixed_import.svg
list = typ_th-list.svg
margin_bottom = mixed_margin-bottom.svg
margin_left = mixed_margin-left.svg
margin_right = mixed_margin-right.svg
margin_top = mixed_margin-top.svg
maximise = typ_arrow-maximise.svg
menu = typ_th-dot-menu.svg
minimise = typ_arrow-minimise.svg
@@ -93,6 +98,8 @@ search_regex = nw_search-regex.svg
search_replace = mixed_search-replace.svg
search_word = nw_search-word.svg
settings = typ_cog.svg
size_height = mixed_size-height.svg
size_width = mixed_size-width.svg
star = typ_star.svg
status_idle = typ_media-pause-grey.svg
status_lang = typ_globe-grey.svg
@@ -101,6 +108,7 @@ status_stats = typ_chart-bar-grey.svg
status_time = typ_stopwatch-grey.svg
sticky-off = typ_pin-outline.svg
sticky-on = typ_pin.svg
toolbar = nw_toolbar.svg
unchecked = mixed_input-unchecked.svg
unfold-hide = typ_unfold-hidden.svg
unfold-show = typ_unfold-visible.svg

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m7.8824 2c-1.2988 0-2.3529 1.0541-2.3529 2.3529v2.3529h-1.1765c-1.2988 0-2.3529 1.0541-2.3529 2.3529v10.588c0 1.2988 1.0541 2.3529 2.3529 2.3529h10.588c1.2988 0 2.3529-1.0541 2.3529-2.3529v-1.1765h2.3529c1.2988 0 2.3529-1.0541 2.3529-2.3529v-11.765c0-1.2988-1.0541-2.3529-2.3529-2.3529zm0 2.3529h11.765v11.765h-2.3529v-7.0588c0-1.2988-1.0541-2.3529-2.3529-2.3529h-7.0588zm-3.5294 4.7059h10.588c0.32353 0 0-0.32353 0 0v10.588h-10.588v-9.8943z" fill="#4271ae" stroke-width="1.1765"/>
</svg>

After

Width:  |  Height:  |  Size: 636 B

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="2" width="20" height="20" rx="1" ry="1" fill="#333" fill-opacity=".15" stroke-linecap="round" stroke-width="2"/>
<rect x="2" y="18" width="20" height="4" rx="1" ry="1" fill="#333" stroke-linecap="round" stroke-width="2"/>
<path d="m18.414 8.5959c0.78104 0.79361 0.78104 2.0837 0 2.8773l-6.4141 6.5268-6.4141-6.5268c-0.391-0.3968-0.58601-0.91769-0.58601-1.4386 0-0.52092 0.195-1.0418 0.58601-1.4386 0.78104-0.79464 2.047-0.79464 2.8281 0l3.5861 3.6475 3.5861-3.6475c0.78104-0.79464 2.047-0.79464 2.8281 0z" fill="#4271ae" stroke-width="1.0087"/>
</svg>

After

Width:  |  Height:  |  Size: 707 B

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="2" width="20" height="20" rx="1" ry="1" fill="#333" fill-opacity=".15" stroke-linecap="round" stroke-width="2"/>
<rect x="2" y="2" width="4" height="20" rx="1" ry="1" fill="#333" stroke-linecap="round" stroke-width="2"/>
<path d="m15.404 18.5c-0.79361 0.78104-2.0837 0.78104-2.8773 0l-6.5268-6.4141 6.5268-6.4141c0.3968-0.391 0.91769-0.58601 1.4386-0.58601 0.52092 0 1.0418 0.195 1.4386 0.58601 0.79464 0.78104 0.79464 2.047 0 2.8281l-3.6475 3.5861 3.6475 3.5861c0.79464 0.78104 0.79464 2.047 0 2.8281z" fill="#4271ae" stroke-width="1.0087"/>
</svg>

After

Width:  |  Height:  |  Size: 705 B

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="2" width="20" height="20" rx="1" ry="1" fill="#333" fill-opacity=".15" stroke-linecap="round" stroke-width="2"/>
<rect x="18" y="2" width="4" height="20" rx="1" ry="1" fill="#333" stroke-linecap="round" stroke-width="2"/>
<path d="m8.5959 18.5c0.79361 0.78104 2.0837 0.78104 2.8773 0l6.5268-6.4141-6.5268-6.4141c-0.3968-0.391-0.91769-0.58601-1.4386-0.58601-0.52092 0-1.0418 0.195-1.4386 0.58601-0.79464 0.78104-0.79464 2.047 0 2.8281l3.6475 3.5861-3.6475 3.5861c-0.79464 0.78104-0.79464 2.047 0 2.8281z" fill="#4271ae" stroke-width="1.0087"/>
</svg>

After

Width:  |  Height:  |  Size: 705 B

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="2" width="20" height="20" rx="1" ry="1" fill="#333" fill-opacity=".15" stroke-linecap="round" stroke-width="2"/>
<rect x="2" y="2" width="20" height="4" rx="1" ry="1" fill="#333" stroke-linecap="round" stroke-width="2"/>
<path d="m18.414 15.404c0.78104-0.79361 0.78104-2.0837 0-2.8773l-6.4141-6.5268-6.4141 6.5268c-0.391 0.3968-0.58601 0.91769-0.58601 1.4386 0 0.52092 0.195 1.0418 0.58601 1.4386 0.78104 0.79464 2.047 0.79464 2.8281 0l3.5861-3.6475 3.5861 3.6475c0.78104 0.79464 2.047 0.79464 2.8281 0z" fill="#4271ae" stroke-width="1.0087"/>
</svg>

After

Width:  |  Height:  |  Size: 706 B

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="2" width="20" height="20" rx="1" ry="1" fill="#333" fill-opacity=".15" stroke-linecap="round" stroke-width="2"/>
<path d="m18.414 10.404c0.78104-0.79361 0.78104-2.0837 0-2.8773l-6.4141-6.5268-6.4141 6.5268c-0.391 0.3968-0.58601 0.91769-0.58601 1.4386 0 0.52092 0.195 1.0418 0.58601 1.4386 0.78104 0.79464 2.047 0.79464 2.8281 0l3.5861-3.6475 3.5861 3.6475c0.78104 0.79464 2.047 0.79464 2.8281 0z" fill="#4271ae" stroke-width="1.0087"/>
<path d="m18.414 13.596c0.78104 0.79361 0.78104 2.0837 0 2.8773l-6.4141 6.5268-6.4141-6.5268c-0.391-0.3968-0.58601-0.91769-0.58601-1.4386 0-0.52092 0.195-1.0418 0.58601-1.4386 0.78104-0.79464 2.047-0.79464 2.8281 0l3.5861 3.6475 3.5861-3.6475c0.78104-0.79464 2.047-0.79464 2.8281 0z" fill="#4271ae" stroke-width="1.0087"/>
</svg>

After

Width:  |  Height:  |  Size: 921 B

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="2" width="20" height="20" rx="1" ry="1" fill="#333" fill-opacity=".15" stroke-linecap="round" stroke-width="2"/>
<path d="m13.596 18.414c0.79361 0.78104 2.0837 0.78104 2.8773 0l6.5268-6.4141-6.5268-6.4141c-0.3968-0.391-0.91769-0.58601-1.4386-0.58601-0.52092 0-1.0418 0.195-1.4386 0.58601-0.79464 0.78104-0.79464 2.047 0 2.8281l3.6475 3.5861-3.6475 3.5861c-0.79464 0.78104-0.79464 2.047 0 2.8281z" fill="#4271ae" stroke-width="1.0087"/>
<path d="m10.404 18.414c-0.79361 0.78104-2.0837 0.78104-2.8773 0l-6.5268-6.4141 6.5268-6.4141c0.3968-0.391 0.91769-0.58601 1.4386-0.58601 0.52092 0 1.0418 0.195 1.4386 0.58601 0.79464 0.78104 0.79464 2.047 0 2.8281l-3.6475 3.5861 3.6475 3.5861c0.79464 0.78104 0.79464 2.047 0 2.8281z" fill="#4271ae" stroke-width="1.0087"/>
</svg>

After

Width:  |  Height:  |  Size: 922 B

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<rect x="8" y="2" width="8" height="8" rx="1" ry="1" fill="none" stroke="#4271ae" stroke-linecap="round" stroke-width="2"/>
<rect x="8" y="14" width="8" height="8" rx="1" ry="1" fill="none" stroke="#4271ae" stroke-linecap="round" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 395 B

View File

@@ -35,6 +35,7 @@ translators for the languages currently available:</p>
<li><b>Norwegian:</b> Veronica Berglyd Olsen (vkbo)</li>
<li><b>Polish:</b> Anna Maria Polak (Nauthiz)</li>
<li><b>Portuguese:</b> Bruno Meneguello (bkmeneguello)</li>
<li><b>Russian:</b> Konstantin Tan (k1kimosha)</li>
<li><b>Simplified Chinese:</b> Qianzhi Long (longqzh)</li>
</ul>

View File

@@ -29,5 +29,5 @@ helptext = 92, 92, 92
fadedtext = 108, 108, 108
errortext = 255, 92, 92
statusnone = 120, 120, 120
statussaved = 200, 15, 39
statusunsaved = 2, 133, 37
statussaved = 2, 133, 37
statusunsaved = 200, 15, 39

View File

@@ -6,7 +6,7 @@ File History:
Created: 2019-05-12 [0.1.0]
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
Copyright (C) 2019 Veronica Berglyd Olsen and novelWriter contributors
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
@@ -29,6 +29,7 @@ import unicodedata
import uuid
import xml.etree.ElementTree as ET
from collections.abc import Callable
from configparser import ConfigParser
from datetime import datetime
from pathlib import Path
@@ -36,8 +37,8 @@ from typing import TYPE_CHECKING, Any, Literal, TypeVar
from urllib.parse import urljoin
from urllib.request import pathname2url
from PyQt5.QtCore import QCoreApplication, QUrl
from PyQt5.QtGui import QColor, QDesktopServices, QFont, QFontInfo
from PyQt5.QtCore import QCoreApplication, QMimeData, QUrl
from PyQt5.QtGui import QColor, QDesktopServices, QFont, QFontDatabase, QFontInfo
from novelwriter.constants import nwConst, nwLabels, nwUnicode, trConst
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
@@ -204,6 +205,14 @@ def checkIntTuple(value: int, valid: tuple | list | set, default: int) -> int:
return default
def firstFloat(*args: Any) -> float:
"""Return the first value that is a float."""
for arg in args:
if isinstance(arg, float):
return arg
return 0.0
##
# Formatting Functions
##
@@ -279,6 +288,16 @@ def simplified(text: str) -> str:
return " ".join(str(text).strip().split())
def compact(text: str) -> str:
"""Compact a string by removing spaces."""
return "".join(str(text).split())
def uniqueCompact(text: str) -> str:
"""Return a unique, compact and sorted string."""
return "".join(sorted(set(compact(text))))
def elide(text: str, length: int) -> str:
"""Elide a piece of text to a maximum length."""
if len(text) > (cut := max(4, length)):
@@ -396,6 +415,10 @@ def numberToRoman(value: int, toLower: bool = False) -> str:
return roman.lower() if toLower else roman
##
# Qt Helpers
##
def cssCol(col: QColor, alpha: int | None = None) -> str:
"""Convert a QColor object to an rgba entry to use in CSS."""
return f"rgba({col.red()}, {col.green()}, {col.blue()}, {alpha or col.alpha()})"
@@ -411,6 +434,48 @@ def describeFont(font: QFont) -> str:
return "Error"
def fontMatcher(font: QFont) -> QFont:
"""Make sure the font is the correct family, if possible. This
ensures that Qt doesn't re-use another font under the hood. The
default Qt5 font matching algorithm doesn't handle well changing
application fonts at runtime.
"""
info = QFontInfo(font)
if (famRequest := font.family()) != (famActual := info.family()):
logger.warning("Font mismatch: Requested '%s', but got '%s'", famRequest, famActual)
db = QFontDatabase()
if famRequest in db.families():
styleRequest, sizeRequest = font.styleName(), font.pointSize()
logger.info("Lookup: %s, %s, %d pt", famRequest, styleRequest, sizeRequest)
temp = db.font(famRequest, styleRequest, sizeRequest)
temp.setPointSize(sizeRequest) # Make sure it isn't changed
famFound, styleFound, sizeFound = temp.family(), temp.styleName(), temp.pointSize()
if famFound == famRequest:
logger.info("Found: %s, %s, %d pt", famFound, styleFound, sizeFound)
return temp
logger.warning("Could not find a font match in the font database")
logger.warning("If you just changed font, you may need to restart the application")
return font
def qtLambda(func: Callable, *args: Any, **kwargs: Any) -> Callable:
"""A replacement for Python lambdas that works for Qt slots."""
def wrapper(*a_: Any) -> None:
func(*args, **kwargs)
return wrapper
def encodeMimeHandles(mimeData: QMimeData, handles: list[str]) -> None:
"""Encode handles into a mime data object."""
mimeData.setData(nwConst.MIME_HANDLE, b"|".join(h.encode() for h in handles))
return
def decodeMimeHandles(mimeData: QMimeData) -> list[str]:
"""Decode and split a mime data object with handles."""
return mimeData.data(nwConst.MIME_HANDLE).data().decode().split("|")
##
# Encoder Functions
##
@@ -462,6 +527,10 @@ def jsonEncode(data: dict | list | tuple, n: int = 0, nmax: int = 0) -> str:
return "".join(buffer)
##
# XML Helpers
##
def xmlIndent(tree: ET.Element | ET.ElementTree) -> None:
"""A modified version of the XML indent function in the standard
library. It behaves more closely to how the one from lxml does.
@@ -505,6 +574,45 @@ def xmlIndent(tree: ET.Element | ET.ElementTree) -> None:
return
def xmlElement(
tag: str,
text: str | int | float | bool | None = None,
*,
attrib: dict | None = None,
tail: str | None = None,
) -> ET.Element:
"""A custom implementation of Element with more arguments."""
xSub = ET.Element(tag, attrib=attrib or {})
if text is not None:
if isinstance(text, bool):
xSub.text = str(text).lower()
else:
xSub.text = str(text)
if tail is not None:
xSub.tail = tail
return xSub
def xmlSubElem(
parent: ET.Element,
tag: str,
text: str | int | float | bool | None = None,
*,
attrib: dict | None = None,
tail: str | None = None,
) -> ET.Element:
"""A custom implementation of SubElement with more arguments."""
xSub = ET.SubElement(parent, tag, attrib=attrib or {})
if text is not None:
if isinstance(text, bool):
xSub.text = str(text).lower()
else:
xSub.text = str(text)
if tail is not None:
xSub.tail = tail
return xSub
##
# File and File System Functions
##

View File

@@ -8,7 +8,7 @@ Created: 2022-11-09 [2.0rc2] RecentProjects
Created: 2024-06-16 [2.5rc1] RecentPaths
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
Copyright (C) 2018 Veronica Berglyd Olsen and novelWriter contributors
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
@@ -40,7 +40,10 @@ from PyQt5.QtCore import (
from PyQt5.QtGui import QFont, QFontDatabase
from PyQt5.QtWidgets import QApplication
from novelwriter.common import NWConfigParser, checkInt, checkPath, describeFont, formatTimeStamp
from novelwriter.common import (
NWConfigParser, checkInt, checkPath, describeFont, fontMatcher,
formatTimeStamp
)
from novelwriter.constants import nwFiles, nwUnicode
from novelwriter.error import formatException, logException
@@ -109,7 +112,7 @@ class Config:
# General GUI Settings
self.guiLocale = self._qLocale.name()
self.guiTheme = "default" # GUI theme
self.guiTheme = "default_light" # GUI theme
self.guiSyntax = "default_light" # Syntax theme
self.guiFont = QFont() # Main GUI font
self.guiScale = 1.0 # Set automatically by Theme class
@@ -161,8 +164,9 @@ class Config:
self.dialogStyle = 2 # Quote type to use for dialogue
self.allowOpenDial = True # Allow open-ended dialogue quotes
self.narratorBreak = "" # Symbol to use for narrator break
self.dialogLine = "" # Symbol to use for dialogue line
self.narratorBreak = "" # Symbol to use for narrator break
self.narratorDialog = "" # Symbol for alternating between dialogue and narrator
self.altDialogOpen = "" # Alternative dialog symbol, open
self.altDialogClose = "" # Alternative dialog symbol, close
self.highlightEmph = True # Add colour to text emphasis
@@ -190,6 +194,7 @@ class Config:
# State
self.showViewerPanel = True # The panel for the viewer is visible
self.showEditToolBar = False # The document editor toolbar visibility
self.showSessionTime = True # Show the session time in the status bar
self.viewComments = True # Comments are shown in the viewer
self.viewSynopsis = True # Synopsis is shown in the viewer
@@ -252,6 +257,10 @@ class Config:
def hasError(self) -> bool:
return self._hasError
@property
def locale(self) -> QLocale:
return self._dLocale
@property
def recentProjects(self) -> RecentProjects:
return self._recentProjects
@@ -364,10 +373,11 @@ class Config:
def setGuiFont(self, value: QFont | str | None) -> None:
"""Update the GUI's font style from settings."""
if isinstance(value, QFont):
self.guiFont = value
self.guiFont = fontMatcher(value)
elif value and isinstance(value, str):
self.guiFont = QFont()
self.guiFont.fromString(value)
font = QFont()
font.fromString(value)
self.guiFont = fontMatcher(font)
else:
font = QFont()
fontDB = QFontDatabase()
@@ -377,11 +387,9 @@ class Config:
font.setPointSize(10)
else:
font = fontDB.systemFont(QFontDatabase.SystemFont.GeneralFont)
self.guiFont = font
self.guiFont = fontMatcher(font)
logger.debug("GUI font set to: %s", describeFont(font))
QApplication.setFont(self.guiFont)
return
def setTextFont(self, value: QFont | str | None) -> None:
@@ -389,10 +397,11 @@ class Config:
set to default font.
"""
if isinstance(value, QFont):
self.textFont = value
self.textFont = fontMatcher(value)
elif value and isinstance(value, str):
self.textFont = QFont()
self.textFont.fromString(value)
font = QFont()
font.fromString(value)
self.textFont = fontMatcher(font)
else:
fontDB = QFontDatabase()
fontFam = fontDB.families()
@@ -406,8 +415,8 @@ class Config:
font.setPointSize(12)
else:
font = fontDB.systemFont(QFontDatabase.SystemFont.GeneralFont)
self.textFont = font
logger.debug("Text font set to: %s", describeFont(font))
self.textFont = fontMatcher(font)
logger.debug("Text font set to: %s", describeFont(self.textFont))
return
##
@@ -653,10 +662,11 @@ class Config:
self.showFullPath = conf.rdBool(sec, "showfullpath", self.showFullPath)
self.dialogStyle = conf.rdInt(sec, "dialogstyle", self.dialogStyle)
self.allowOpenDial = conf.rdBool(sec, "allowopendial", self.allowOpenDial)
self.dialogLine = conf.rdStr(sec, "dialogline", self.dialogLine)
self.narratorBreak = conf.rdStr(sec, "narratorbreak", self.narratorBreak)
self.narratorDialog = conf.rdStr(sec, "narratordialog", self.narratorDialog)
self.altDialogOpen = conf.rdStr(sec, "altdialogopen", self.altDialogOpen)
self.altDialogClose = conf.rdStr(sec, "altdialogclose", self.altDialogClose)
self.dialogLine = conf.rdStr(sec, "dialogline", self.dialogLine)
self.highlightEmph = conf.rdBool(sec, "highlightemph", self.highlightEmph)
self.stopWhenIdle = conf.rdBool(sec, "stopwhenidle", self.stopWhenIdle)
self.userIdleTime = conf.rdInt(sec, "useridletime", self.userIdleTime)
@@ -665,6 +675,7 @@ class Config:
sec = "State"
self.showViewerPanel = conf.rdBool(sec, "showviewerpanel", self.showViewerPanel)
self.showEditToolBar = conf.rdBool(sec, "showedittoolbar", self.showEditToolBar)
self.showSessionTime = conf.rdBool(sec, "showsessiontime", self.showSessionTime)
self.viewComments = conf.rdBool(sec, "viewcomments", self.viewComments)
self.viewSynopsis = conf.rdBool(sec, "viewsynopsis", self.viewSynopsis)
self.searchCase = conf.rdBool(sec, "searchcase", self.searchCase)
@@ -762,10 +773,11 @@ class Config:
"showfullpath": str(self.showFullPath),
"dialogstyle": str(self.dialogStyle),
"allowopendial": str(self.allowOpenDial),
"dialogline": str(self.dialogLine),
"narratorbreak": str(self.narratorBreak),
"narratordialog": str(self.narratorDialog),
"altdialogopen": str(self.altDialogOpen),
"altdialogclose": str(self.altDialogClose),
"dialogline": str(self.dialogLine),
"highlightemph": str(self.highlightEmph),
"stopwhenidle": str(self.stopWhenIdle),
"useridletime": str(self.userIdleTime),
@@ -774,6 +786,7 @@ class Config:
conf["State"] = {
"showviewerpanel": str(self.showViewerPanel),
"showedittoolbar": str(self.showEditToolBar),
"showsessiontime": str(self.showSessionTime),
"viewcomments": str(self.viewComments),
"viewsynopsis": str(self.viewSynopsis),
"searchcase": str(self.searchCase),

View File

@@ -6,7 +6,7 @@ File History:
Created: 2019-04-28 [0.0.1]
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
Copyright (C) 2019 Veronica Berglyd Olsen and novelWriter contributors
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
@@ -53,6 +53,9 @@ class nwConst:
# Requests
USER_AGENT = "Mozilla/5.0 (compatible; novelWriter (Python))"
# Mime Types
MIME_HANDLE = "text/vnd.novelwriter.handle"
# Gui Settings
STATUS_MSG_TIMEOUT = 15000 # milliseconds
MAX_SEARCH_RESULT = 1000
@@ -60,11 +63,14 @@ class nwConst:
class nwRegEx:
URL = r"https?://(?:www\.|(?!www))[\w/()@:%_\+-.~#?&=]+"
WORDS = r"\b[^\s\-\+\/–—\[\]:]+\b"
BREAK = r"(?i)(?<!\\)(\[br\]\n?)"
FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_EB = r"(?<![\w\\])(\*{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_ST = r"(?<![\w\\])(~{2})(?![\s~])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_SC = r"(?i)(?<!\\)(\[[\/\!]?(?:b|i|s|u|m|sup|sub)\])"
FMT_SV = r"(?i)(?<!\\)(\[(?:footnote):)(.+?)(?<!\\)(\])"
FMT_SC = r"(?i)(?<!\\)(\[(?:b|/b|i|/i|s|/s|u|/u|m|/m|sup|/sup|sub|/sub|br)\])"
FMT_SV = r"(?i)(?<!\\)(\[(?:footnote|field):)(.+?)(?<!\\)(\])"
class nwShortcode:
@@ -83,21 +89,49 @@ class nwShortcode:
SUP_C = "[/sup]"
SUB_O = "[sub]"
SUB_C = "[/sub]"
BREAK = "[br]"
FOOTNOTE_B = "[footnote:"
FIELD_B = "[field:"
COMMENT_STYLES = {
nwComment.FOOTNOTE: "[footnote:{0}]",
nwComment.COMMENT: "[comment:{0}]",
}
FIELD_VALUE = "[field:{0}]"
class nwHeaders:
class nwStyles:
H_VALID = ("H0", "H1", "H2", "H3", "H4")
H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4}
H_SIZES = {0: 2.50, 1: 2.00, 2: 1.75, 3: 1.50, 4: 1.25}
T_NORMAL = 1.0
T_SMALL = 0.8
T_LABEL = {
"H0": QT_TRANSLATE_NOOP("Constant", "Title"),
"H1": QT_TRANSLATE_NOOP("Constant", "Heading 1 (Partition)"),
"H2": QT_TRANSLATE_NOOP("Constant", "Heading 2 (Chapter)"),
"H3": QT_TRANSLATE_NOOP("Constant", "Heading 3 (Scene)"),
"H4": QT_TRANSLATE_NOOP("Constant", "Heading 4 (Section)"),
"TT": QT_TRANSLATE_NOOP("Constant", "Text Paragraph"),
"SP": QT_TRANSLATE_NOOP("Constant", "Scene Separator"),
}
T_MARGIN = {
"H0": (1.50, 0.60), # Title margins (top, bottom)
"H1": (1.50, 0.60), # Heading 1 margins (top, bottom)
"H2": (1.50, 0.60), # Heading 2 margins (top, bottom)
"H3": (1.20, 0.60), # Heading 3 margins (top, bottom)
"H4": (1.20, 0.60), # Heading 4 margins (top, bottom)
"TT": (0.00, 0.60), # Text margins (top, bottom)
"SP": (1.20, 1.20), # Separator margins (top, bottom)
"MT": (0.00, 0.60), # Meta margins (top, bottom)
"FT": (1.40, 0.40), # Footnote margins (left, bottom)
}
class nwFiles:
@@ -121,34 +155,40 @@ class nwFiles:
class nwKeyWords:
TAG_KEY = "@tag"
POV_KEY = "@pov"
FOCUS_KEY = "@focus"
CHAR_KEY = "@char"
PLOT_KEY = "@plot"
TIME_KEY = "@time"
WORLD_KEY = "@location"
OBJECT_KEY = "@object"
ENTITY_KEY = "@entity"
CUSTOM_KEY = "@custom"
TAG_KEY = "@tag"
POV_KEY = "@pov"
FOCUS_KEY = "@focus"
CHAR_KEY = "@char"
PLOT_KEY = "@plot"
TIME_KEY = "@time"
WORLD_KEY = "@location"
OBJECT_KEY = "@object"
ENTITY_KEY = "@entity"
CUSTOM_KEY = "@custom"
STORY_KEY = "@story"
MENTION_KEY = "@mention"
# Note: The order here affects the order of menu entries
ALL_KEYS = [
TAG_KEY, POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY, WORLD_KEY,
OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY, STORY_KEY, MENTION_KEY,
]
# Set of Valid Keys
VALID_KEYS = {
TAG_KEY, POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY,
WORLD_KEY, OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY
}
VALID_KEYS = set(ALL_KEYS)
# Map from Keys to Item Class
KEY_CLASS = {
POV_KEY: nwItemClass.CHARACTER,
FOCUS_KEY: nwItemClass.CHARACTER,
CHAR_KEY: nwItemClass.CHARACTER,
PLOT_KEY: nwItemClass.PLOT,
TIME_KEY: nwItemClass.TIMELINE,
WORLD_KEY: nwItemClass.WORLD,
OBJECT_KEY: nwItemClass.OBJECT,
ENTITY_KEY: nwItemClass.ENTITY,
CUSTOM_KEY: nwItemClass.CUSTOM,
POV_KEY: nwItemClass.CHARACTER,
FOCUS_KEY: nwItemClass.CHARACTER,
CHAR_KEY: nwItemClass.CHARACTER,
PLOT_KEY: nwItemClass.PLOT,
TIME_KEY: nwItemClass.TIMELINE,
WORLD_KEY: nwItemClass.WORLD,
OBJECT_KEY: nwItemClass.OBJECT,
ENTITY_KEY: nwItemClass.ENTITY,
CUSTOM_KEY: nwItemClass.CUSTOM,
STORY_KEY: nwItemClass.NOVEL,
}
@@ -165,6 +205,29 @@ class nwLists:
]
class nwStats:
CHARS = "allChars"
CHARS_TEXT = "textChars"
CHARS_TITLE = "titleChars"
PARAGRAPHS = "paragraphCount"
TITLES = "titleCount"
WCHARS_ALL = "allWordChars"
WCHARS_TEXT = "textWordChars"
WCHARS_TITLE = "titleWordChars"
WORDS = "allWords"
WORDS_TEXT = "textWords"
WORDS_TITLE = "titleWords"
# Note: The order here affects the order of menu entries
ALL_FIELDS = [
WORDS, WORDS_TEXT, WORDS_TITLE,
CHARS, CHARS_TEXT, CHARS_TITLE,
WCHARS_ALL, WCHARS_TEXT, WCHARS_TITLE,
PARAGRAPHS, TITLES,
]
class nwLabels:
CLASS_NAME = {
@@ -211,84 +274,124 @@ class nwLabels:
"doc_h4": QT_TRANSLATE_NOOP("Constant", "Novel Section"),
"note": QT_TRANSLATE_NOOP("Constant", "Project Note"),
}
ACTIVE_NAME = {
"checked": QT_TRANSLATE_NOOP("Constant", "Active"),
"unchecked": QT_TRANSLATE_NOOP("Constant", "Inactive"),
}
KEY_NAME = {
nwKeyWords.TAG_KEY: QT_TRANSLATE_NOOP("Constant", "Tag"),
nwKeyWords.POV_KEY: QT_TRANSLATE_NOOP("Constant", "Point of View"),
nwKeyWords.FOCUS_KEY: QT_TRANSLATE_NOOP("Constant", "Focus"),
nwKeyWords.CHAR_KEY: QT_TRANSLATE_NOOP("Constant", "Characters"),
nwKeyWords.PLOT_KEY: QT_TRANSLATE_NOOP("Constant", "Plot"),
nwKeyWords.TIME_KEY: QT_TRANSLATE_NOOP("Constant", "Timeline"),
nwKeyWords.WORLD_KEY: QT_TRANSLATE_NOOP("Constant", "Locations"),
nwKeyWords.OBJECT_KEY: QT_TRANSLATE_NOOP("Constant", "Objects"),
nwKeyWords.ENTITY_KEY: QT_TRANSLATE_NOOP("Constant", "Entities"),
nwKeyWords.CUSTOM_KEY: QT_TRANSLATE_NOOP("Constant", "Custom"),
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"),
nwKeyWords.STORY_KEY: QT_TRANSLATE_NOOP("Constant", "Story"),
nwKeyWords.MENTION_KEY: QT_TRANSLATE_NOOP("Constant", "Mentions"),
}
KEY_SHORTCUT = {
nwKeyWords.TAG_KEY: "Ctrl+K, G",
nwKeyWords.POV_KEY: "Ctrl+K, V",
nwKeyWords.FOCUS_KEY: "Ctrl+K, F",
nwKeyWords.CHAR_KEY: "Ctrl+K, C",
nwKeyWords.PLOT_KEY: "Ctrl+K, P",
nwKeyWords.TIME_KEY: "Ctrl+K, T",
nwKeyWords.WORLD_KEY: "Ctrl+K, L",
nwKeyWords.OBJECT_KEY: "Ctrl+K, O",
nwKeyWords.ENTITY_KEY: "Ctrl+K, E",
nwKeyWords.CUSTOM_KEY: "Ctrl+K, X",
nwKeyWords.STORY_KEY: "Ctrl+K, N",
nwKeyWords.MENTION_KEY: "Ctrl+K, M",
}
OUTLINE_COLS = {
nwOutline.TITLE: QT_TRANSLATE_NOOP("Constant", "Title"),
nwOutline.LEVEL: QT_TRANSLATE_NOOP("Constant", "Level"),
nwOutline.LABEL: QT_TRANSLATE_NOOP("Constant", "Document"),
nwOutline.LINE: QT_TRANSLATE_NOOP("Constant", "Line"),
nwOutline.CCOUNT: QT_TRANSLATE_NOOP("Constant", "Chars"),
nwOutline.WCOUNT: QT_TRANSLATE_NOOP("Constant", "Words"),
nwOutline.PCOUNT: QT_TRANSLATE_NOOP("Constant", "Pars"),
nwOutline.POV: QT_TRANSLATE_NOOP("Constant", "POV"),
nwOutline.FOCUS: QT_TRANSLATE_NOOP("Constant", "Focus"),
nwOutline.CHAR: KEY_NAME[nwKeyWords.CHAR_KEY],
nwOutline.PLOT: KEY_NAME[nwKeyWords.PLOT_KEY],
nwOutline.WORLD: KEY_NAME[nwKeyWords.WORLD_KEY],
nwOutline.TIME: KEY_NAME[nwKeyWords.TIME_KEY],
nwOutline.OBJECT: KEY_NAME[nwKeyWords.OBJECT_KEY],
nwOutline.ENTITY: KEY_NAME[nwKeyWords.ENTITY_KEY],
nwOutline.CUSTOM: KEY_NAME[nwKeyWords.CUSTOM_KEY],
nwOutline.SYNOP: QT_TRANSLATE_NOOP("Constant", "Synopsis"),
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.STATUS: QT_TRANSLATE_NOOP("Constant", "Status"),
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.WORLD: KEY_NAME[nwKeyWords.WORLD_KEY],
nwOutline.TIME: KEY_NAME[nwKeyWords.TIME_KEY],
nwOutline.OBJECT: KEY_NAME[nwKeyWords.OBJECT_KEY],
nwOutline.ENTITY: KEY_NAME[nwKeyWords.ENTITY_KEY],
nwOutline.CUSTOM: KEY_NAME[nwKeyWords.CUSTOM_KEY],
nwOutline.STORY: KEY_NAME[nwKeyWords.STORY_KEY],
nwOutline.MENTION: KEY_NAME[nwKeyWords.MENTION_KEY],
nwOutline.SYNOP: QT_TRANSLATE_NOOP("Constant", "Synopsis"),
}
STATS_NAME = {
nwStats.CHARS: QT_TRANSLATE_NOOP("Stats", "Characters"),
nwStats.CHARS_TEXT: QT_TRANSLATE_NOOP("Stats", "Characters in Text"),
nwStats.CHARS_TITLE: QT_TRANSLATE_NOOP("Stats", "Characters in Headings"),
nwStats.PARAGRAPHS: QT_TRANSLATE_NOOP("Stats", "Paragraphs"),
nwStats.TITLES: QT_TRANSLATE_NOOP("Stats", "Headings"),
nwStats.WCHARS_ALL: QT_TRANSLATE_NOOP("Stats", "Characters, No Spaces"),
nwStats.WCHARS_TEXT: QT_TRANSLATE_NOOP("Stats", "Characters in Text, No Spaces"),
nwStats.WCHARS_TITLE: QT_TRANSLATE_NOOP("Stats", "Characters in Headings, No Spaces"),
nwStats.WORDS: QT_TRANSLATE_NOOP("Stats", "Words"),
nwStats.WORDS_TEXT: QT_TRANSLATE_NOOP("Stats", "Words in Text"),
nwStats.WORDS_TITLE: QT_TRANSLATE_NOOP("Stats", "Words in Headings"),
}
BUILD_FMT = {
nwBuildFmt.ODT: QT_TRANSLATE_NOOP("Constant", "Open Document (.odt)"),
nwBuildFmt.FODT: QT_TRANSLATE_NOOP("Constant", "Flat Open Document (.fodt)"),
nwBuildFmt.HTML: QT_TRANSLATE_NOOP("Constant", "novelWriter HTML (.html)"),
nwBuildFmt.DOCX: QT_TRANSLATE_NOOP("Constant", "Microsoft Word Document (.docx)"),
nwBuildFmt.HTML: QT_TRANSLATE_NOOP("Constant", "HTML 5 (.html)"),
nwBuildFmt.NWD: QT_TRANSLATE_NOOP("Constant", "novelWriter Markup (.txt)"),
nwBuildFmt.STD_MD: QT_TRANSLATE_NOOP("Constant", "Standard Markdown (.md)"),
nwBuildFmt.EXT_MD: QT_TRANSLATE_NOOP("Constant", "Extended Markdown (.md)"),
nwBuildFmt.J_HTML: QT_TRANSLATE_NOOP("Constant", "JSON + novelWriter HTML (.json)"),
nwBuildFmt.PDF: QT_TRANSLATE_NOOP("Constant", "Portable Document Format (.pdf)"),
nwBuildFmt.J_HTML: QT_TRANSLATE_NOOP("Constant", "JSON + HTML 5 (.json)"),
nwBuildFmt.J_NWD: QT_TRANSLATE_NOOP("Constant", "JSON + novelWriter Markup (.json)"),
}
BUILD_EXT = {
nwBuildFmt.ODT: ".odt",
nwBuildFmt.FODT: ".fodt",
nwBuildFmt.DOCX: ".docx",
nwBuildFmt.HTML: ".html",
nwBuildFmt.NWD: ".txt",
nwBuildFmt.STD_MD: ".md",
nwBuildFmt.EXT_MD: ".md",
nwBuildFmt.PDF: ".pdf",
nwBuildFmt.J_HTML: ".json",
nwBuildFmt.J_NWD: ".json",
}
SHAPES_PLAIN = {
nwStatusShape.SQUARE: QT_TRANSLATE_NOOP("Constant", "Square"),
nwStatusShape.TRIANGLE: QT_TRANSLATE_NOOP("Constant", "Triangle"),
nwStatusShape.NABLA: QT_TRANSLATE_NOOP("Constant", "Nabla"),
nwStatusShape.DIAMOND: QT_TRANSLATE_NOOP("Constant", "Diamond"),
nwStatusShape.PENTAGON: QT_TRANSLATE_NOOP("Constant", "Pentagon"),
nwStatusShape.HEXAGON: QT_TRANSLATE_NOOP("Constant", "Hexagon"),
nwStatusShape.STAR: QT_TRANSLATE_NOOP("Constant", "Star"),
nwStatusShape.PACMAN: QT_TRANSLATE_NOOP("Constant", "Pacman"),
nwStatusShape.SQUARE: QT_TRANSLATE_NOOP("Shape", "Square"),
nwStatusShape.TRIANGLE: QT_TRANSLATE_NOOP("Shape", "Triangle"),
nwStatusShape.NABLA: QT_TRANSLATE_NOOP("Shape", "Nabla"),
nwStatusShape.DIAMOND: QT_TRANSLATE_NOOP("Shape", "Diamond"),
nwStatusShape.PENTAGON: QT_TRANSLATE_NOOP("Shape", "Pentagon"),
nwStatusShape.HEXAGON: QT_TRANSLATE_NOOP("Shape", "Hexagon"),
nwStatusShape.STAR: QT_TRANSLATE_NOOP("Shape", "Star"),
nwStatusShape.PACMAN: QT_TRANSLATE_NOOP("Shape", "Pacman"),
}
SHAPES_CIRCLE = {
nwStatusShape.CIRCLE_Q: QT_TRANSLATE_NOOP("Constant", "1/4 Circle"),
nwStatusShape.CIRCLE_H: QT_TRANSLATE_NOOP("Constant", "Half Circle"),
nwStatusShape.CIRCLE_T: QT_TRANSLATE_NOOP("Constant", "3/4 Circle"),
nwStatusShape.CIRCLE: QT_TRANSLATE_NOOP("Constant", "Full Circle"),
nwStatusShape.CIRCLE_Q: QT_TRANSLATE_NOOP("Shape", "1/4 Circle"),
nwStatusShape.CIRCLE_H: QT_TRANSLATE_NOOP("Shape", "Half Circle"),
nwStatusShape.CIRCLE_T: QT_TRANSLATE_NOOP("Shape", "3/4 Circle"),
nwStatusShape.CIRCLE: QT_TRANSLATE_NOOP("Shape", "Full Circle"),
}
SHAPES_BARS = {
nwStatusShape.BARS_1: QT_TRANSLATE_NOOP("Constant", "1 Bar"),
nwStatusShape.BARS_2: QT_TRANSLATE_NOOP("Constant", "2 Bars"),
nwStatusShape.BARS_3: QT_TRANSLATE_NOOP("Constant", "3 Bars"),
nwStatusShape.BARS_4: QT_TRANSLATE_NOOP("Constant", "4 Bars"),
nwStatusShape.BARS_1: QT_TRANSLATE_NOOP("Shape", "1 Bar"),
nwStatusShape.BARS_2: QT_TRANSLATE_NOOP("Shape", "2 Bars"),
nwStatusShape.BARS_3: QT_TRANSLATE_NOOP("Shape", "3 Bars"),
nwStatusShape.BARS_4: QT_TRANSLATE_NOOP("Shape", "4 Bars"),
}
SHAPES_BLOCKS = {
nwStatusShape.BLOCK_1: QT_TRANSLATE_NOOP("Constant", "1 Block"),
nwStatusShape.BLOCK_2: QT_TRANSLATE_NOOP("Constant", "2 Blocks"),
nwStatusShape.BLOCK_3: QT_TRANSLATE_NOOP("Constant", "3 Blocks"),
nwStatusShape.BLOCK_4: QT_TRANSLATE_NOOP("Constant", "4 Blocks"),
nwStatusShape.BLOCK_1: QT_TRANSLATE_NOOP("Shape", "1 Block"),
nwStatusShape.BLOCK_2: QT_TRANSLATE_NOOP("Shape", "2 Blocks"),
nwStatusShape.BLOCK_3: QT_TRANSLATE_NOOP("Shape", "3 Blocks"),
nwStatusShape.BLOCK_4: QT_TRANSLATE_NOOP("Shape", "4 Blocks"),
}
FILE_FILTERS = {
"*.txt": QT_TRANSLATE_NOOP("Constant", "Text files"),
@@ -343,11 +446,11 @@ class nwHeadFmt:
CHAR_POV, CHAR_FOCUS
]
# ODT Document Page Header
ODT_PROJECT = "{Project}"
ODT_AUTHOR = "{Author}"
ODT_PAGE = "{Page}"
ODT_AUTO = "{Project} / {Author} / {Page}"
# Document Page Header
DOC_PROJECT = "{Project}"
DOC_AUTHOR = "{Author}"
DOC_PAGE = "{Page}"
DOC_AUTO = "{Project} / {Author} / {Page}"
class nwQuotes:
@@ -449,6 +552,11 @@ class nwUnicode:
U_LTRI = "\u25c0" # Left-pointing triangle
U_LTRIS = "\u25c2" # Left-pointing triangle, small
# Special
U_UNKN = "\ufffd" # Unknown character
U_NAC1 = "\ufffe" # Not a character
U_NAC2 = "\uffff" # Not a character
# HTML Equivalents
# ================

View File

@@ -7,7 +7,7 @@ Created: 2023-02-14 [2.1b1] BuildSettings
Created: 2023-05-22 [2.1b1] BuildCollection
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
Copyright (C) 2023 Veronica Berglyd Olsen and novelWriter contributors
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
@@ -36,7 +36,7 @@ from PyQt5.QtCore import QT_TRANSLATE_NOOP, QCoreApplication
from novelwriter import CONFIG
from novelwriter.common import checkUuid, isHandle, jsonEncode
from novelwriter.constants import nwFiles, nwHeadFmt
from novelwriter.constants import nwFiles, nwHeadFmt, nwStyles
from novelwriter.core.project import NWProject
from novelwriter.enum import nwBuildFmt
from novelwriter.error import logException
@@ -45,29 +45,30 @@ logger = logging.getLogger(__name__)
# The Settings Template
# =====================
# Each entry contains a tuple on the form:
# (type, default, [min value, max value])
# Each entry contains a tuple on the form: (type, default)
SETTINGS_TEMPLATE = {
SETTINGS_TEMPLATE: dict[str, tuple[type, str | int | float | bool]] = {
"filter.includeNovel": (bool, True),
"filter.includeNotes": (bool, False),
"filter.includeInactive": (bool, False),
"headings.fmtTitle": (str, nwHeadFmt.TITLE),
"headings.fmtPart": (str, nwHeadFmt.TITLE),
"headings.fmtChapter": (str, nwHeadFmt.TITLE),
"headings.fmtUnnumbered": (str, nwHeadFmt.TITLE),
"headings.fmtScene": (str, "* * *"),
"headings.fmtAltScene": (str, ""),
"headings.fmtSection": (str, ""),
"headings.hideTitle": (bool, False),
"headings.hidePart": (bool, False),
"headings.hideChapter": (bool, False),
"headings.hideUnnumbered": (bool, False),
"headings.hideScene": (bool, False),
"headings.hideAltScene": (bool, False),
"headings.hideSection": (bool, True),
"headings.centerTitle": (bool, True),
"headings.centerPart": (bool, True),
"headings.centerChapter": (bool, False),
"headings.centerScene": (bool, False),
"headings.breakTitle": (bool, True),
"headings.breakTitle": (bool, False),
"headings.breakPart": (bool, True),
"headings.breakChapter": (bool, True),
"headings.breakScene": (bool, False),
"text.includeSynopsis": (bool, False),
@@ -77,7 +78,7 @@ SETTINGS_TEMPLATE = {
"text.ignoredKeywords": (str, ""),
"text.addNoteHeadings": (bool, True),
"format.textFont": (str, CONFIG.textFont.toString()),
"format.lineHeight": (float, 1.15, 0.75, 3.0),
"format.lineHeight": (float, 1.15),
"format.justifyText": (bool, False),
"format.stripUnicode": (bool, False),
"format.replaceTabs": (bool, False),
@@ -86,6 +87,20 @@ SETTINGS_TEMPLATE = {
"format.firstLineIndent": (bool, False),
"format.firstIndentWidth": (float, 1.4),
"format.indentFirstPar": (bool, False),
"format.titleMarginT": (float, nwStyles.T_MARGIN["H0"][0]),
"format.titleMarginB": (float, nwStyles.T_MARGIN["H0"][1]),
"format.h1MarginT": (float, nwStyles.T_MARGIN["H1"][0]),
"format.h1MarginB": (float, nwStyles.T_MARGIN["H1"][1]),
"format.h2MarginT": (float, nwStyles.T_MARGIN["H2"][0]),
"format.h2MarginB": (float, nwStyles.T_MARGIN["H2"][1]),
"format.h3MarginT": (float, nwStyles.T_MARGIN["H3"][0]),
"format.h3MarginB": (float, nwStyles.T_MARGIN["H3"][1]),
"format.h4MarginT": (float, nwStyles.T_MARGIN["H4"][0]),
"format.h4MarginB": (float, nwStyles.T_MARGIN["H4"][1]),
"format.textMarginT": (float, nwStyles.T_MARGIN["TT"][0]),
"format.textMarginB": (float, nwStyles.T_MARGIN["TT"][1]),
"format.sepMarginT": (float, nwStyles.T_MARGIN["SP"][0]),
"format.sepMarginB": (float, nwStyles.T_MARGIN["SP"][1]),
"format.pageUnit": (str, "cm"),
"format.pageSize": (str, "A4"),
"format.pageWidth": (float, 21.0),
@@ -94,9 +109,11 @@ SETTINGS_TEMPLATE = {
"format.bottomMargin": (float, 2.0),
"format.leftMargin": (float, 2.0),
"format.rightMargin": (float, 2.0),
"odt.addColours": (bool, True),
"odt.pageHeader": (str, nwHeadFmt.ODT_AUTO),
"odt.pageCountOffset": (int, 0),
"doc.pageHeader": (str, nwHeadFmt.DOC_AUTO),
"doc.pageCountOffset": (int, 0),
"doc.colorHeadings": (bool, True),
"doc.scaleHeadings": (bool, True),
"doc.boldHeadings": (bool, True),
"html.addStyles": (bool, True),
"html.preserveTabs": (bool, False),
}
@@ -108,12 +125,16 @@ SETTINGS_LABELS = {
"filter.includeInactive": QT_TRANSLATE_NOOP("Builds", "Inactive Documents"),
"headings": QT_TRANSLATE_NOOP("Builds", "Headings"),
"headings.fmtTitle": QT_TRANSLATE_NOOP("Builds", "Partition Format"),
"headings.fmtPart": QT_TRANSLATE_NOOP("Builds", "Partition Format"),
"headings.fmtChapter": QT_TRANSLATE_NOOP("Builds", "Chapter Format"),
"headings.fmtUnnumbered": QT_TRANSLATE_NOOP("Builds", "Unnumbered Format"),
"headings.fmtScene": QT_TRANSLATE_NOOP("Builds", "Scene Format"),
"headings.fmtAltScene": QT_TRANSLATE_NOOP("Builds", "Alt. Scene Format"),
"headings.fmtSection": QT_TRANSLATE_NOOP("Builds", "Section Format"),
"headings.styleTitle": QT_TRANSLATE_NOOP("Builds", "Title Styling"),
"headings.stylePart": QT_TRANSLATE_NOOP("Builds", "Partition Styling"),
"headings.styleChapter": QT_TRANSLATE_NOOP("Builds", "Chapter Styling"),
"headings.styleScene": QT_TRANSLATE_NOOP("Builds", "Scene Styling"),
"text.grpContent": QT_TRANSLATE_NOOP("Builds", "Text Content"),
"text.includeSynopsis": QT_TRANSLATE_NOOP("Builds", "Include Synopsis"),
@@ -121,13 +142,11 @@ SETTINGS_LABELS = {
"text.includeKeywords": QT_TRANSLATE_NOOP("Builds", "Include Keywords"),
"text.includeBodyText": QT_TRANSLATE_NOOP("Builds", "Include Body Text"),
"text.ignoredKeywords": QT_TRANSLATE_NOOP("Builds", "Ignore These Keywords"),
"text.grpInsert": QT_TRANSLATE_NOOP("Builds", "Insert Content"),
"text.addNoteHeadings": QT_TRANSLATE_NOOP("Builds", "Add Titles for Notes"),
"format.grpFormat": QT_TRANSLATE_NOOP("Builds", "Text Format"),
"format.textFont": QT_TRANSLATE_NOOP("Builds", "Text Font"),
"format.lineHeight": QT_TRANSLATE_NOOP("Builds", "Line Height"),
"format.grpOptions": QT_TRANSLATE_NOOP("Builds", "Text Options"),
"format.justifyText": QT_TRANSLATE_NOOP("Builds", "Justify Text Margins"),
"format.stripUnicode": QT_TRANSLATE_NOOP("Builds", "Replace Unicode Characters"),
"format.replaceTabs": QT_TRANSLATE_NOOP("Builds", "Replace Tabs with Spaces"),
@@ -139,26 +158,38 @@ SETTINGS_LABELS = {
"format.firstIndentWidth": QT_TRANSLATE_NOOP("Builds", "Indent Width"),
"format.indentFirstPar": QT_TRANSLATE_NOOP("Builds", "Indent First Paragraph"),
"format.grpMargins": QT_TRANSLATE_NOOP("Builds", "Text Margins"),
"format.titleMargin": QT_TRANSLATE_NOOP("Builds", "Title and Partition"),
"format.h1Margin": QT_TRANSLATE_NOOP("Builds", "Heading 1 and Chapter"),
"format.h2Margin": QT_TRANSLATE_NOOP("Builds", "Heading 2 and Scene"),
"format.h3Margin": QT_TRANSLATE_NOOP("Builds", "Heading 3 and Section"),
"format.h4Margin": QT_TRANSLATE_NOOP("Builds", "Heading 4"),
"format.textMargin": QT_TRANSLATE_NOOP("Builds", "Text Paragraph"),
"format.sepMargin": QT_TRANSLATE_NOOP("Builds", "Scene Separator"),
"format.grpPage": QT_TRANSLATE_NOOP("Builds", "Page Layout"),
"format.pageUnit": QT_TRANSLATE_NOOP("Builds", "Unit"),
"format.pageSize": QT_TRANSLATE_NOOP("Builds", "Page Size"),
"format.pageWidth": QT_TRANSLATE_NOOP("Builds", "Page Width"),
"format.pageHeight": QT_TRANSLATE_NOOP("Builds", "Page Height"),
"format.topMargin": QT_TRANSLATE_NOOP("Builds", "Top Margin"),
"format.bottomMargin": QT_TRANSLATE_NOOP("Builds", "Bottom Margin"),
"format.leftMargin": QT_TRANSLATE_NOOP("Builds", "Left Margin"),
"format.rightMargin": QT_TRANSLATE_NOOP("Builds", "Right Margin"),
"format.pageMargins": QT_TRANSLATE_NOOP("Builds", "Page Margins"),
"odt": QT_TRANSLATE_NOOP("Builds", "Open Document (.odt)"),
"odt.addColours": QT_TRANSLATE_NOOP("Builds", "Add Highlight Colours"),
"odt.pageHeader": QT_TRANSLATE_NOOP("Builds", "Page Header"),
"odt.pageCountOffset": QT_TRANSLATE_NOOP("Builds", "Page Counter Offset"),
"doc": QT_TRANSLATE_NOOP("Builds", "Document Style"),
"doc.pageHeader": QT_TRANSLATE_NOOP("Builds", "Page Header"),
"doc.pageCountOffset": QT_TRANSLATE_NOOP("Builds", "Page Counter Offset"),
"doc.colorHeadings": QT_TRANSLATE_NOOP("Builds", "Add Colours to Headings"),
"doc.scaleHeadings": QT_TRANSLATE_NOOP("Builds", "Increase Size of Headings"),
"doc.boldHeadings": QT_TRANSLATE_NOOP("Builds", "Bold Headings"),
"html": QT_TRANSLATE_NOOP("Builds", "HTML (.html)"),
"html": QT_TRANSLATE_NOOP("Builds", "HTML Options"),
"html.addStyles": QT_TRANSLATE_NOOP("Builds", "Add CSS Styles"),
"html.preserveTabs": QT_TRANSLATE_NOOP("Builds", "Preserve Tab Characters"),
}
RENAMED = {
"odt.addColours": "doc.addColours",
"odt.pageHeader": "doc.pageHeader",
"odt.pageCountOffset": "doc.pageCountOffset",
}
class FilterMode(Enum):
"""The decision reason for an item in a filtered project."""
@@ -348,18 +379,12 @@ class BuildSettings:
self._changed = True
return
def setValue(self, key: str, value: str | int | bool | float) -> bool:
def setValue(self, key: str, value: str | int | float | bool) -> None:
"""Set a specific value for a build setting."""
if key not in SETTINGS_TEMPLATE:
return False
definition = SETTINGS_TEMPLATE[key]
if not isinstance(value, definition[0]):
return False
if len(definition) == 4 and isinstance(value, (int, float)):
value = min(max(value, definition[2]), definition[3])
self._changed = value != self._settings[key]
self._settings[key] = value
return True
if (d := SETTINGS_TEMPLATE.get(key)) and len(d) == 2 and isinstance(value, d[0]):
self._changed = value != self._settings[key]
self._settings[key] = value
return
##
# Methods
@@ -478,12 +503,21 @@ class BuildSettings:
self._settings = {k: v[1] for k, v in SETTINGS_TEMPLATE.items()}
if isinstance(settings, dict):
for key, value in settings.items():
self.setValue(key, value)
self.setValue(RENAMED.get(key, key), value)
self._changed = False
return
@classmethod
def duplicate(cls, source: BuildSettings) -> BuildSettings:
"""Make a copy of another build."""
cls = BuildSettings()
cls.unpack(source.pack())
cls._uuid = str(uuid.uuid4())
cls._name = f"{source.name} 2"
return cls
class BuildCollection:
"""Core: Build Collection Class

View File

@@ -9,7 +9,7 @@ Created: 2022-11-03 [2.0rc2] ProjectBuilder
Created: 2023-07-20 [2.1b1] DocDuplicator
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
Copyright (C) 2022 Veronica Berglyd Olsen and novelWriter contributors
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
@@ -27,6 +27,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
import logging
import re
import shutil
from collections.abc import Iterable
@@ -34,11 +35,11 @@ from functools import partial
from pathlib import Path
from zipfile import ZipFile, is_zipfile
from PyQt5.QtCore import QCoreApplication, QRegularExpression
from PyQt5.QtCore import QCoreApplication
from novelwriter import CONFIG, SHARED
from novelwriter.common import isHandle, minmax, simplified
from novelwriter.constants import nwConst, nwFiles, nwItemClass
from novelwriter.constants import nwConst, nwFiles, nwItemClass, nwStats
from novelwriter.core.item import NWItem
from novelwriter.core.project import NWProject
from novelwriter.core.storage import NWStorageCreate
@@ -55,10 +56,17 @@ class DocMerger:
def __init__(self, project: NWProject) -> None:
self._project = project
self._error = ""
self._targetDoc = None
self._targetText = []
self._target = None
self._text = []
return
@property
def targetHandle(self) -> str | None:
"""Get the handle of the target document."""
if self._target:
return self._target.itemHandle
return None
##
# Methods
##
@@ -71,63 +79,56 @@ class DocMerger:
"""Set the target document for the merging. Calling this
function resets the class.
"""
self._targetDoc = tHandle
self._targetText = []
self._target = self._project.tree[tHandle]
self._text = []
return
def newTargetDoc(self, srcHandle: str, docLabel: str) -> str | None:
def newTargetDoc(self, sHandle: str, label: str) -> None:
"""Create a brand new target document based on a source handle
and a new doc label. Calling this function resets the class.
"""
srcItem = self._project.tree[srcHandle]
if srcItem is None or srcItem.itemParent is None:
return None
sItem = self._project.tree[sHandle]
if sItem and sItem.itemParent:
tHandle = self._project.newFile(label, sItem.itemParent)
if nwItem := self._project.tree[tHandle]:
nwItem.setLayout(sItem.itemLayout)
nwItem.setStatus(sItem.itemStatus)
nwItem.setImport(sItem.itemImport)
nwItem.notifyToRefresh()
self._target = nwItem
self._text = []
return
newHandle = self._project.newFile(docLabel, srcItem.itemParent)
newItem = self._project.tree[newHandle]
if isinstance(newItem, NWItem):
newItem.setLayout(srcItem.itemLayout)
newItem.setStatus(srcItem.itemStatus)
newItem.setImport(srcItem.itemImport)
self._targetDoc = newHandle
self._targetText = []
return newHandle
def appendText(self, srcHandle: str, addComment: bool, cmtPrefix: str) -> bool:
def appendText(self, sHandle: str, addComment: bool, cmtPrefix: str) -> None:
"""Append text from an existing document to the text buffer."""
srcItem = self._project.tree[srcHandle]
if srcItem is None:
return False
docText = self._project.storage.getDocumentText(srcHandle).rstrip("\n")
if addComment:
docInfo = srcItem.describeMe()
docSt, _ = srcItem.getImportStatus()
cmtLine = f"% {cmtPrefix} {docInfo}: {srcItem.itemName} [{docSt}]\n\n"
docText = cmtLine + docText
self._targetText.append(docText)
return True
if item := self._project.tree[sHandle]:
text = self._project.storage.getDocumentText(sHandle).rstrip("\n")
if addComment:
info = item.describeMe()
status, _ = item.getImportStatus()
text = f"% {cmtPrefix} {info}: {item.itemName} [{status}]\n\n{text}"
self._text.append(text)
return
def writeTargetDoc(self) -> bool:
"""Write the accumulated text into the designated target
document, appending any existing text.
"""
if self._targetDoc is None:
return False
if self._target:
outDoc = self._project.storage.getDocument(self._target.itemHandle)
if text := (outDoc.readDocument() or "").rstrip("\n"):
self._text.insert(0, text)
outDoc = self._project.storage.getDocument(self._targetDoc)
if text := (outDoc.readDocument() or "").rstrip("\n"):
self._targetText.insert(0, text)
status = outDoc.writeDocument("\n\n".join(self._text) + "\n\n")
if not status:
self._error = outDoc.getError()
status = outDoc.writeDocument("\n\n".join(self._targetText) + "\n\n")
if not status:
self._error = outDoc.getError()
self._project.index.reIndexHandle(self._target.itemHandle)
self._target.notifyToRefresh()
return status
return status
return False
class DocSplitter:
@@ -171,23 +172,19 @@ class DocSplitter:
self._inFolder = False
return
def newParentFolder(self, pHandle: str, folderLabel: str) -> str | None:
def newParentFolder(self, pHandle: str, folderLabel: str) -> None:
"""Create a new folder that will be the top level parent item
for the new documents.
"""
if self._srcItem is None:
return None
newHandle = self._project.newFolder(folderLabel, pHandle)
newItem = self._project.tree[newHandle]
if isinstance(newItem, NWItem):
newItem.setStatus(self._srcItem.itemStatus)
newItem.setImport(self._srcItem.itemImport)
self._parHandle = newHandle
self._inFolder = True
return newHandle
if self._srcItem:
nHandle = self._project.newFolder(folderLabel, pHandle)
if nwItem := self._project.tree[nHandle]:
nwItem.setStatus(self._srcItem.itemStatus)
nwItem.setImport(self._srcItem.itemImport)
nwItem.notifyToRefresh()
self._parHandle = nHandle
self._inFolder = True
return
def splitDocument(self, splitData: list, splitText: list[str]) -> None:
"""Loop through the split data record and perform the split job
@@ -201,58 +198,50 @@ class DocSplitter:
self._rawData.insert(0, (chunk, hLevel, hLabel))
return
def writeDocuments(self, docHierarchy: bool) -> Iterable[tuple[bool, str | None, str | None]]:
def writeDocuments(self, docHierarchy: bool) -> Iterable[bool]:
"""An iterator that will write each document in the buffer, and
return its new handle, parent handle, and sibling handle.
"""
if self._srcHandle is None or self._srcItem is None or self._parHandle is None:
return
if self._srcHandle and self._srcItem and self._parHandle:
pHandle = self._parHandle
hHandle = [self._parHandle, None, None, None, None]
pLevel = 0
for docText, hLevel, docLabel in self._rawData:
pHandle = self._parHandle
nHandle = self._parHandle if self._inFolder else self._srcHandle
hHandle = [self._parHandle, None, None, None, None]
hLevel = minmax(hLevel, 1, 4)
if pLevel == 0:
pLevel = hLevel
pLevel = 0
for docText, hLevel, docLabel in self._rawData:
if docHierarchy:
if hLevel == 1:
pHandle = self._parHandle
elif hLevel == 2:
pHandle = hHandle[1] or hHandle[0]
elif hLevel == 3:
pHandle = hHandle[2] or hHandle[1] or hHandle[0]
elif hLevel == 4:
pHandle = hHandle[3] or hHandle[2] or hHandle[1] or hHandle[0]
hLevel = minmax(hLevel, 1, 4)
if pLevel == 0:
pLevel = hLevel
if (
(dHandle := self._project.newFile(docLabel, pHandle))
and (nwItem := self._project.tree[dHandle])
):
hHandle[hLevel] = dHandle
nwItem.setStatus(self._srcItem.itemStatus)
nwItem.setImport(self._srcItem.itemImport)
if docHierarchy:
if hLevel == 1:
pHandle = self._parHandle
elif hLevel == 2:
pHandle = hHandle[1] or hHandle[0]
elif hLevel == 3:
pHandle = hHandle[2] or hHandle[1] or hHandle[0]
elif hLevel == 4:
pHandle = hHandle[3] or hHandle[2] or hHandle[1] or hHandle[0]
outDoc = self._project.storage.getDocument(dHandle)
status = outDoc.writeDocument("\n".join(docText))
if not status:
self._error = outDoc.getError()
if hLevel < pLevel:
nHandle = hHandle[hLevel] or hHandle[0]
elif hLevel > pLevel:
nHandle = pHandle
self._project.index.reIndexHandle(dHandle)
nwItem.notifyToRefresh()
dHandle = self._project.newFile(docLabel, pHandle)
hHandle[hLevel] = dHandle
newItem = self._project.tree[dHandle]
if isinstance(newItem, NWItem):
newItem.setStatus(self._srcItem.itemStatus)
newItem.setImport(self._srcItem.itemImport)
outDoc = self._project.storage.getDocument(dHandle)
status = outDoc.writeDocument("\n".join(docText))
if not status:
self._error = outDoc.getError()
yield status, dHandle, nHandle
hHandle[hLevel] = dHandle
nHandle = dHandle
pLevel = hLevel
yield status
hHandle[hLevel] = dHandle
pLevel = hLevel
return
@@ -269,36 +258,34 @@ class DocDuplicator:
# Methods
##
def duplicate(self, items: list[str]) -> Iterable[tuple[str, str | None]]:
def duplicate(self, items: list[str]) -> list[str]:
"""Run through a list of items, duplicate them, and copy the
text content if they are documents.
"""
result = []
after = True
if items:
nHandle = items[0]
hMap: dict[str, str | None] = {t: None for t in items}
for tHandle in items:
newItem = self._project.tree.duplicate(tHandle)
if newItem is None:
return
hMap[tHandle] = newItem.itemHandle
if newItem.itemParent in hMap:
newItem.setParent(hMap[newItem.itemParent])
self._project.tree.updateItemData(newItem.itemHandle)
if newItem.isFileType():
newDoc = self._project.storage.getDocument(newItem.itemHandle)
if newDoc.fileExists():
return
newDoc.writeDocument(self._project.storage.getDocumentText(tHandle))
yield newItem.itemHandle, nHandle
nHandle = None
return
if oldItem := self._project.tree[tHandle]:
pHandle = hMap.get(oldItem.itemParent or "") or oldItem.itemParent
if newItem := self._project.tree.duplicate(tHandle, pHandle, after):
hMap[tHandle] = newItem.itemHandle
if newItem.isFileType():
self._project.copyFileContent(newItem.itemHandle, tHandle)
newItem.notifyToRefresh()
result.append(newItem.itemHandle)
after = False
else:
break
return result
class DocSearch:
def __init__(self) -> None:
self._regEx = QRegularExpression()
self.setCaseSensitive(False)
self._regEx = re.compile("")
self._opts = re.UNICODE | re.IGNORECASE
self._words = False
self._escape = True
return
@@ -309,10 +296,9 @@ class DocSearch:
def setCaseSensitive(self, state: bool) -> None:
"""Set the case sensitive search flag."""
opts = QRegularExpression.PatternOption.UseUnicodePropertiesOption
self._opts = re.UNICODE
if not state:
opts |= QRegularExpression.PatternOption.CaseInsensitiveOption
self._regEx.setPatternOptions(opts)
self._opts |= re.IGNORECASE
return
def setWholeWords(self, state: bool) -> None:
@@ -329,8 +315,8 @@ class DocSearch:
self, project: NWProject, search: str
) -> Iterable[tuple[NWItem, list[tuple[int, int, str]], bool]]:
"""Iteratively search through documents in a project."""
self._regEx.setPattern(self._buildPattern(search))
logger.debug("Searching with pattern '%s'", self._regEx.pattern())
self._regEx = re.compile(self._buildPattern(search), self._opts)
logger.debug("Searching with pattern '%s'", self._regEx.pattern)
storage = project.storage
for item in project.tree:
if item.isFileType():
@@ -340,14 +326,12 @@ class DocSearch:
def searchText(self, text: str) -> tuple[list[tuple[int, int, str]], bool]:
"""Search a piece of text for RegEx matches."""
rxItt = self._regEx.globalMatch(text)
count = 0
capped = False
results = []
while rxItt.hasNext():
rxMatch = rxItt.next()
pos = rxMatch.capturedStart()
num = rxMatch.capturedLength()
for res in self._regEx.finditer(text):
pos = res.start(0)
num = len(res.group(0))
lim = text[:pos].rfind("\n") + 1
cut = text[lim:pos].rfind(" ") + lim + 1
context = text[cut:cut+100].partition("\n")[0]
@@ -366,7 +350,7 @@ class DocSearch:
def _buildPattern(self, search: str) -> str:
"""Build the search pattern string."""
if self._escape:
search = QRegularExpression.escape(search)
search = re.escape(search)
if self._words:
search = f"(?:^|\\b){search}(?:$|\\b)"
return search
@@ -430,7 +414,6 @@ class ProjectBuilder:
lblNewProject = self.tr("New Project")
lblTitlePage = self.tr("Title Page")
lblByAuthors = self.tr("By")
# Settings
project.data.setUuid(None)
@@ -443,14 +426,29 @@ class ProjectBuilder:
# Add Root Folders
hNovelRoot = project.newRoot(nwItemClass.NOVEL)
hTitlePage = project.newFile(lblTitlePage, hNovelRoot)
novelTitle = project.data.name
titlePage = f"#! {novelTitle}\n\n"
if project.data.author:
titlePage += f">> {lblByAuthors} {project.data.author} <<\n\n"
# Generate Title Page
aDoc = project.storage.getDocument(hTitlePage)
aDoc.writeDocument(titlePage)
aDoc.writeDocument((
"{author}[br]\n"
"{address} 1[br]\n"
"{address} 2 <<\n"
"\n"
"[vspace:5]\n"
"\n"
"#! {title}\n"
"\n"
">> **{by} {author}** <<\n"
"\n"
">> {count}: [field:{field}] <<\n"
).format(
author=project.data.author or "None",
address=self.tr("Address"),
title=project.data.name or "None",
by=self.tr("By"),
count=self.tr("Word Count"),
field=nwStats.WORDS_TEXT,
))
# Create a project structure based on selected root folders
# and a number of chapters and scenes selected in the
@@ -511,7 +509,7 @@ class ProjectBuilder:
# Also add the archive and trash folders
project.newRoot(nwItemClass.ARCHIVE)
project.trashFolder()
project.tree.trash # Triggers the creation of Trash
project.saveProject()
project.closeProject()

View File

@@ -6,7 +6,7 @@ File History:
Created: 2022-12-01 [2.1b1] NWBuildDocument
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
Copyright (C) 2022 Veronica Berglyd Olsen and novelWriter contributors
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
@@ -35,13 +35,15 @@ from novelwriter.constants import nwLabels
from novelwriter.core.buildsettings import BuildSettings
from novelwriter.core.item import NWItem
from novelwriter.core.project import NWProject
from novelwriter.core.tohtml import ToHtml
from novelwriter.core.tokenizer import Tokenizer
from novelwriter.core.tomarkdown import ToMarkdown
from novelwriter.core.toodt import ToOdt
from novelwriter.core.toqdoc import TextDocumentTheme, ToQTextDocument
from novelwriter.enum import nwBuildFmt
from novelwriter.error import formatException, logException
from novelwriter.formats.todocx import ToDocX
from novelwriter.formats.tohtml import ToHtml
from novelwriter.formats.tokenizer import Tokenizer
from novelwriter.formats.tomarkdown import ToMarkdown
from novelwriter.formats.toodt import ToOdt
from novelwriter.formats.toqdoc import ToQTextDocument
from novelwriter.formats.toraw import ToRaw
logger = logging.getLogger(__name__)
@@ -85,20 +87,6 @@ class NWBuildDocument:
"""
return self._cache
##
# Setters
##
def setCountEnabled(self, state: bool) -> None:
"""Turn on/off stats for builds."""
self._count = state
return
def setBuildOutline(self, state: bool) -> None:
"""Turn on/off outline for builds."""
self._outline = state
return
##
# Special Methods
##
@@ -125,182 +113,117 @@ class NWBuildDocument:
self._queue.append(item.itemHandle)
return
def iterBuildPreview(self, theme: TextDocumentTheme) -> Iterable[tuple[int, bool]]:
def iterBuildPreview(self, newPage: bool) -> Iterable[tuple[int, bool]]:
"""Build a preview QTextDocument."""
makeObj = ToQTextDocument(self._project)
filtered = self._setupBuild(makeObj)
self._outline = True
self._count = True
font = QFont()
font.fromString(self._build.getStr("format.textFont"))
makeObj.initDocument(font, theme)
for i, tHandle in enumerate(self._queue):
self._error = None
if filtered.get(tHandle, (False, 0))[0]:
yield i, self._doBuild(makeObj, tHandle)
else:
yield i, False
makeObj.appendFootnotes()
self._error = None
self._cache = makeObj
return
def iterBuild(self, path: Path, bFormat: nwBuildFmt) -> Iterable[tuple[int, bool]]:
"""Wrapper for builders based on format."""
if bFormat in (nwBuildFmt.ODT, nwBuildFmt.FODT):
yield from self.iterBuildOpenDocument(path, bFormat == nwBuildFmt.FODT)
elif bFormat in (nwBuildFmt.HTML, nwBuildFmt.J_HTML):
yield from self.iterBuildHTML(path, asJson=bFormat == nwBuildFmt.J_HTML)
elif bFormat in (nwBuildFmt.STD_MD, nwBuildFmt.EXT_MD):
yield from self.iterBuildMarkdown(path, bFormat == nwBuildFmt.EXT_MD)
elif bFormat in (nwBuildFmt.NWD, nwBuildFmt.J_NWD):
yield from self.iterBuildNWD(path, asJson=bFormat == nwBuildFmt.J_NWD)
return
def iterBuildOpenDocument(self, path: Path, isFlat: bool) -> Iterable[tuple[int, bool]]:
"""Build an Open Document file."""
makeObj = ToOdt(self._project, isFlat=isFlat)
filtered = self._setupBuild(makeObj)
makeObj.initDocument()
for i, tHandle in enumerate(self._queue):
self._error = None
if filtered.get(tHandle, (False, 0))[0]:
yield i, self._doBuild(makeObj, tHandle)
else:
yield i, False
makeObj.setShowNewPage(newPage)
self._outline = True
yield from self._iterBuild(makeObj, filtered)
makeObj.closeDocument()
self._error = None
self._cache = makeObj
return
def iterBuildDocument(self, path: Path, bFormat: nwBuildFmt) -> Iterable[tuple[int, bool]]:
"""Wrapper for builders based on format."""
self._error = None
self._cache = None
if bFormat in (nwBuildFmt.J_HTML, nwBuildFmt.J_NWD):
# Ensure that JSON output has the correct extension
path = path.with_suffix(".json")
if bFormat in (nwBuildFmt.ODT, nwBuildFmt.FODT):
makeObj = ToOdt(self._project, bFormat == nwBuildFmt.FODT)
filtered = self._setupBuild(makeObj)
makeObj.initDocument()
yield from self._iterBuild(makeObj, filtered)
makeObj.closeDocument()
elif bFormat in (nwBuildFmt.HTML, nwBuildFmt.J_HTML):
makeObj = ToHtml(self._project)
filtered = self._setupBuild(makeObj)
makeObj.initDocument()
yield from self._iterBuild(makeObj, filtered)
makeObj.closeDocument()
if not self._build.getBool("html.preserveTabs"):
makeObj.replaceTabs()
elif bFormat in (nwBuildFmt.STD_MD, nwBuildFmt.EXT_MD):
makeObj = ToMarkdown(self._project, bFormat == nwBuildFmt.EXT_MD)
filtered = self._setupBuild(makeObj)
yield from self._iterBuild(makeObj, filtered)
makeObj.closeDocument()
if self._build.getBool("format.replaceTabs"):
makeObj.replaceTabs(nSpaces=4, spaceChar=" ")
elif bFormat in (nwBuildFmt.NWD, nwBuildFmt.J_NWD):
makeObj = ToRaw(self._project)
filtered = self._setupBuild(makeObj)
yield from self._iterBuild(makeObj, filtered)
makeObj.closeDocument()
if self._build.getBool("format.replaceTabs"):
makeObj.replaceTabs(nSpaces=4, spaceChar=" ")
elif bFormat == nwBuildFmt.DOCX:
makeObj = ToDocX(self._project)
filtered = self._setupBuild(makeObj)
makeObj.initDocument()
yield from self._iterBuild(makeObj, filtered)
makeObj.closeDocument()
elif bFormat == nwBuildFmt.PDF:
makeObj = ToQTextDocument(self._project)
makeObj.disableAnchors()
filtered = self._setupBuild(makeObj)
makeObj.initDocument(pdf=True)
yield from self._iterBuild(makeObj, filtered)
makeObj.closeDocument()
else:
logger.error("Unsupported document format")
return
self._error = None
self._cache = makeObj
try:
if isFlat:
makeObj.saveFlatXML(path)
else:
makeObj.saveOpenDocText(path)
makeObj.saveDocument(path)
except Exception as exc:
logException()
self._error = formatException(exc)
return
def iterBuildHTML(self, path: Path | None, asJson: bool = False) -> Iterable[tuple[int, bool]]:
"""Build an HTML file. If path is None, no file is saved. This
is used for generating build previews.
"""
makeObj = ToHtml(self._project)
filtered = self._setupBuild(makeObj)
for i, tHandle in enumerate(self._queue):
self._error = None
if filtered.get(tHandle, (False, 0))[0]:
yield i, self._doBuild(makeObj, tHandle)
else:
yield i, False
makeObj.appendFootnotes()
if not self._build.getBool("html.preserveTabs"):
makeObj.replaceTabs()
self._error = None
self._cache = makeObj
if isinstance(path, Path):
try:
if asJson:
makeObj.saveHtmlJson(path)
else:
makeObj.saveHtml5(path)
except Exception as exc:
logException()
self._error = formatException(exc)
return
def iterBuildMarkdown(self, path: Path, extendedMd: bool) -> Iterable[tuple[int, bool]]:
"""Build a Markdown file."""
makeObj = ToMarkdown(self._project)
filtered = self._setupBuild(makeObj)
makeObj.setExtendedMarkdown(extendedMd)
if self._build.getBool("format.replaceTabs"):
makeObj.replaceTabs(nSpaces=4, spaceChar=" ")
for i, tHandle in enumerate(self._queue):
self._error = None
if filtered.get(tHandle, (False, 0))[0]:
yield i, self._doBuild(makeObj, tHandle)
else:
yield i, False
makeObj.appendFootnotes()
self._error = None
self._cache = makeObj
try:
makeObj.saveMarkdown(path)
except Exception as exc:
logException()
self._error = formatException(exc)
return
def iterBuildNWD(self, path: Path | None, asJson: bool = False) -> Iterable[tuple[int, bool]]:
"""Build a novelWriter Markdown file."""
makeObj = ToMarkdown(self._project)
filtered = self._setupBuild(makeObj)
makeObj.setKeepMarkdown(True)
for i, tHandle in enumerate(self._queue):
self._error = None
if filtered.get(tHandle, (False, 0))[0]:
yield i, self._doBuild(makeObj, tHandle, convert=False)
else:
yield i, False
if self._build.getBool("format.replaceTabs"):
makeObj.replaceTabs(nSpaces=4, spaceChar=" ")
self._error = None
self._cache = makeObj
if isinstance(path, Path):
try:
if asJson:
makeObj.saveRawMarkdownJSON(path)
else:
makeObj.saveRawMarkdown(path)
except Exception as exc:
logException()
self._error = formatException(exc)
return
##
# Internal Functions
##
def _iterBuild(self, makeObj: Tokenizer, filtered: dict) -> Iterable[tuple[int, bool]]:
"""Iterate over buildable documents."""
self._count = True
for i, tHandle in enumerate(self._queue):
self._error = None
if filtered.get(tHandle, (False, 0))[0]:
yield i, self._doBuild(makeObj, tHandle)
else:
yield i, False
return
def _setupBuild(self, bldObj: Tokenizer) -> dict:
"""Configure the build object."""
# Get Settings
textFont = QFont(CONFIG.textFont)
textFont.fromString(self._build.getStr("format.textFont"))
bldObj.setFont(textFont)
bldObj.setTitleFormat(
self._build.getStr("headings.fmtTitle"),
self._build.getBool("headings.hideTitle")
bldObj.setTextFont(textFont)
bldObj.setLanguage(self._project.data.language)
bldObj.setPartitionFormat(
self._build.getStr("headings.fmtPart"),
self._build.getBool("headings.hidePart")
)
bldObj.setChapterFormat(
self._build.getStr("headings.fmtChapter"),
@@ -326,6 +249,10 @@ class NWBuildDocument:
self._build.getBool("headings.centerTitle"),
self._build.getBool("headings.breakTitle")
)
bldObj.setPartitionStyle(
self._build.getBool("headings.centerPart"),
self._build.getBool("headings.breakPart")
)
bldObj.setChapterStyle(
self._build.getBool("headings.centerChapter"),
self._build.getBool("headings.breakChapter")
@@ -338,12 +265,46 @@ class NWBuildDocument:
bldObj.setJustify(self._build.getBool("format.justifyText"))
bldObj.setLineHeight(self._build.getFloat("format.lineHeight"))
bldObj.setKeepLineBreaks(self._build.getBool("format.keepBreaks"))
bldObj.setDialogueHighlight(self._build.getBool("format.showDialogue"))
bldObj.setDialogHighlight(self._build.getBool("format.showDialogue"))
bldObj.setFirstLineIndent(
self._build.getBool("format.firstLineIndent"),
self._build.getFloat("format.firstIndentWidth"),
self._build.getBool("format.indentFirstPar"),
)
bldObj.setHeadingStyles(
self._build.getBool("doc.colorHeadings"),
self._build.getBool("doc.scaleHeadings"),
self._build.getBool("doc.boldHeadings"),
)
bldObj.setTitleMargins(
self._build.getFloat("format.titleMarginT"),
self._build.getFloat("format.titleMarginB"),
)
bldObj.setHead1Margins(
self._build.getFloat("format.h1MarginT"),
self._build.getFloat("format.h1MarginB"),
)
bldObj.setHead2Margins(
self._build.getFloat("format.h2MarginT"),
self._build.getFloat("format.h2MarginB"),
)
bldObj.setHead3Margins(
self._build.getFloat("format.h3MarginT"),
self._build.getFloat("format.h3MarginB"),
)
bldObj.setHead4Margins(
self._build.getFloat("format.h4MarginT"),
self._build.getFloat("format.h4MarginB"),
)
bldObj.setTextMargins(
self._build.getFloat("format.textMarginT"),
self._build.getFloat("format.textMarginB"),
)
bldObj.setSeparatorMargins(
self._build.getFloat("format.sepMarginT"),
self._build.getFloat("format.sepMarginB"),
)
bldObj.setBodyText(self._build.getBool("text.includeBodyText"))
bldObj.setSynopsis(self._build.getBool("text.includeSynopsis"))
@@ -355,13 +316,13 @@ class NWBuildDocument:
bldObj.setStyles(self._build.getBool("html.addStyles"))
bldObj.setReplaceUnicode(self._build.getBool("format.stripUnicode"))
if isinstance(bldObj, ToOdt):
bldObj.setColourHeaders(self._build.getBool("odt.addColours"))
bldObj.setLanguage(self._project.data.language)
if isinstance(bldObj, (ToOdt, ToDocX)):
bldObj.setHeaderFormat(
self._build.getStr("odt.pageHeader"), self._build.getInt("odt.pageCountOffset")
self._build.getStr("doc.pageHeader"),
self._build.getInt("doc.pageCountOffset"),
)
if isinstance(bldObj, (ToOdt, ToDocX, ToQTextDocument)):
scale = nwLabels.UNIT_SCALE.get(self._build.getStr("format.pageUnit"), 1.0)
pW, pH = nwLabels.PAPER_SIZE.get(self._build.getStr("format.pageSize"), (-1.0, -1.0))
bldObj.setPageLayout(
@@ -384,14 +345,17 @@ class NWBuildDocument:
tItem = self._project.tree[tHandle]
if isinstance(tItem, NWItem):
try:
if tItem.isRootType() and not tItem.isNovelLike():
bldObj.addRootHeading(tHandle)
if convert:
bldObj.doConvert()
if self._count:
bldObj.countStats()
if self._outline:
bldObj.buildOutline()
if tItem.isRootType():
if tItem.isNovelLike():
bldObj.setBreakNext()
else:
bldObj.addRootHeading(tHandle)
if convert:
bldObj.doConvert()
if self._count:
bldObj.countStats()
if self._outline:
bldObj.buildOutline()
elif tItem.isFileType():
bldObj.setText(tHandle)
bldObj.doPreProcessing()
@@ -402,8 +366,6 @@ class NWBuildDocument:
bldObj.buildOutline()
if convert:
bldObj.doConvert()
else:
logger.info(f"Build: Skipping '{tHandle}'")
except Exception:
self._error = f"Build: Failed to build '{tHandle}'"

View File

@@ -6,7 +6,7 @@ File History:
Created: 2018-09-29 [0.0.1]
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
Copyright (C) 2018 Veronica Berglyd Olsen and novelWriter contributors
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

View File

@@ -10,7 +10,7 @@ Created: 2022-05-29 [2.0rc1] TagsIndex
Created: 2022-05-29 [2.0rc1] ItemIndex
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
Copyright (C) 2019 Veronica Berglyd Olsen and novelWriter contributors
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
@@ -40,7 +40,7 @@ from novelwriter import SHARED
from novelwriter.common import (
checkInt, isHandle, isItemClass, isListInstance, isTitleTag, jsonEncode
)
from novelwriter.constants import nwFiles, nwHeaders, nwKeyWords
from novelwriter.constants import nwFiles, nwKeyWords, nwStyles
from novelwriter.enum import nwComment, nwItemClass, nwItemLayout, nwItemType
from novelwriter.error import logException
from novelwriter.text.counting import standardCounter
@@ -116,24 +116,24 @@ class NWIndex:
# Public Methods
##
def clearIndex(self) -> None:
def clear(self) -> None:
"""Clear the index dictionaries and time stamps."""
self._tagsIndex.clear()
self._itemIndex.clear()
self._indexChange = 0.0
self._rootChange = {}
SHARED.indexSignalProxy({"event": "clearIndex"})
SHARED.emitIndexCleared(self._project)
return
def rebuildIndex(self) -> None:
def rebuild(self) -> None:
"""Rebuild the entire index from scratch."""
self.clearIndex()
self.clear()
for nwItem in self._project.tree:
if nwItem.isFileType():
text = self._project.storage.getDocumentText(nwItem.itemHandle)
self.scanText(nwItem.itemHandle, text, blockSignal=True)
self._indexBroken = False
SHARED.indexSignalProxy({"event": "buildIndex"})
SHARED.emitIndexAvailable(self._project)
return
def deleteHandle(self, tHandle: str) -> None:
@@ -143,10 +143,7 @@ class NWIndex:
for tTag in delTags:
del self._tagsIndex[tTag]
del self._itemIndex[tHandle]
SHARED.indexSignalProxy({
"event": "updateTags",
"deleted": delTags,
})
SHARED.emitIndexChangedTags(self._project, [], delTags)
return
def reIndexHandle(self, tHandle: str | None) -> None:
@@ -212,7 +209,7 @@ class NWIndex:
self.reIndexHandle(fHandle)
self._indexChange = time()
SHARED.indexSignalProxy({"event": "buildIndex"})
SHARED.emitIndexAvailable(self._project)
logger.debug("Index loaded in %.3f ms", (time() - tStart)*1000)
@@ -296,10 +293,7 @@ class NWIndex:
self._indexChange = nowTime
self._rootChange[tItem.itemRoot] = nowTime
if not blockSignal:
SHARED.indexSignalProxy({
"event": "scanText",
"handle": tHandle,
})
tItem.notifyToRefresh()
return True
@@ -370,11 +364,7 @@ class NWIndex:
del self._tagsIndex[tTag]
deleted.append(tTag)
if updated or deleted:
SHARED.indexSignalProxy({
"event": "updateTags",
"updated": updated,
"deleted": deleted,
})
SHARED.emitIndexChangedTags(self._project, updated, deleted)
return
@@ -487,13 +477,14 @@ class NWIndex:
if nBits == 0:
return []
# Check that the key is valid
isGood[0] = tBits[0] in nwKeyWords.VALID_KEYS
# Check that the keyword is valid
kBit = tBits[0]
isGood[0] = kBit in nwKeyWords.VALID_KEYS
if not isGood[0] or nBits == 1:
return isGood
# For a tag, only the first value is accepted, the rest are ignored
if tBits[0] == nwKeyWords.TAG_KEY and nBits > 1:
if kBit == nwKeyWords.TAG_KEY and nBits > 1:
check, _ = self.parseValue(tBits[1])
if check in self._tagsIndex:
isGood[1] = self._tagsIndex.tagHandle(check) == tHandle
@@ -501,12 +492,16 @@ class NWIndex:
isGood[1] = True
return isGood
if kBit == nwKeyWords.MENTION_KEY and nBits > 1:
isGood[1:nBits] = [aBit in self._tagsIndex for aBit in tBits[1:nBits]]
return isGood
# If we're still here, we check that the references exist
# Class references cannot have the | symbol in them
refKey = nwKeyWords.KEY_CLASS[tBits[0]].name
for n in range(1, nBits):
if (aBit := tBits[n]) in self._tagsIndex:
isGood[n] = self._tagsIndex.tagClass(aBit) == refKey and "|" not in aBit
if rClass := nwKeyWords.KEY_CLASS.get(kBit):
for n in range(1, nBits):
if (aBit := tBits[n]) in self._tagsIndex:
isGood[n] = self._tagsIndex.tagClass(aBit) == rClass.name and "|" not in aBit
return isGood
@@ -569,7 +564,7 @@ class NWIndex:
for _, _, hItem in self._itemIndex.iterNovelStructure(
rHandle=rootHandle, activeOnly=activeOnly
):
iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0)
iLevel = nwStyles.H_LEVEL.get(hItem.level, 0)
hCount[iLevel] += 1
return hCount
@@ -591,7 +586,7 @@ class NWIndex:
rHandle=rHandle, activeOnly=activeOnly
):
tKey = f"{tHandle}:{sTitle}"
iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0)
iLevel = nwStyles.H_LEVEL.get(hItem.level, 0)
if iLevel > maxDepth:
if pKey in tData:
tData[pKey]["words"] += hItem.wordCount
@@ -635,7 +630,7 @@ class NWIndex:
"""Extract all references made in a file, and optionally title
section.
"""
tRefs = {x: [] for x in nwKeyWords.KEY_CLASS}
tRefs = {x: [] for x in nwKeyWords.VALID_KEYS}
for rTitle, hItem in self._itemIndex.iterItemHeaders(tHandle):
if sTitle is None or sTitle == rTitle:
for aTag, refTypes in hItem.references.items():
@@ -681,9 +676,10 @@ class NWIndex:
"""Return all tags used by a specific document."""
return self._itemIndex.allItemTags(tHandle) if tHandle else []
def getClassTags(self, itemClass: nwItemClass) -> list[str]:
def getClassTags(self, itemClass: nwItemClass | None) -> list[str]:
"""Return all tags based on itemClass."""
return self._tagsIndex.filterTagNames(itemClass.name)
name = None if itemClass is None else itemClass.name
return self._tagsIndex.filterTagNames(name)
def getTagsData(
self, activeOnly: bool = True
@@ -780,11 +776,16 @@ class TagsIndex:
"""Get the class of a given tag."""
return self._tags.get(tagKey.lower(), {}).get("class", None)
def filterTagNames(self, className: str) -> list[str]:
def filterTagNames(self, className: str | None) -> list[str]:
"""Get a list of tag names for a given class."""
return [
x.get("name", "") for x in self._tags.values() if x.get("class", "") == className
]
if className is None:
return [
x.get("name", "") for x in self._tags.values()
]
else:
return [
x.get("name", "") for x in self._tags.values() if x.get("class", "") == className
]
##
# Pack/Unpack
@@ -1251,7 +1252,7 @@ class IndexHeading:
def setLevel(self, level: str) -> None:
"""Set the level of the heading if it's a valid value."""
if level in nwHeaders.H_VALID:
if level in nwStyles.H_VALID:
self._level = level
return

View File

@@ -6,7 +6,7 @@ File History:
Created: 2018-10-27 [0.0.1] NWItem
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
Copyright (C) 2018 Veronica Berglyd Olsen and novelWriter contributors
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
@@ -27,13 +27,14 @@ import logging
from typing import TYPE_CHECKING, Any
from PyQt5.QtGui import QIcon
from PyQt5.QtGui import QFont, QIcon
from novelwriter import CONFIG, SHARED
from novelwriter.common import (
checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified,
yesNo
)
from novelwriter.constants import nwHeaders, nwLabels, trConst
from novelwriter.constants import nwLabels, nwStyles, trConst
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
if TYPE_CHECKING: # pragma: no cover
@@ -256,6 +257,8 @@ class NWItem:
self._paraCount = 0
self._cursorPos = 0
self._initCount = self._wordCount
return True
@classmethod
@@ -281,6 +284,15 @@ class NWItem:
cls._initCount = source._initCount
return cls
##
# Action Methods
##
def notifyToRefresh(self) -> None:
"""Notify GUI that item info needs to be refreshed."""
self._project.tree.refreshItems([self._handle])
return
##
# Lookup Methods
##
@@ -309,6 +321,19 @@ class NWItem:
return trConst(nwLabels.ITEM_DESCRIPTION.get(descKey, ""))
def getMainIcon(self) -> QIcon:
"""Get the main item icon."""
return SHARED.theme.getItemIcon(self._type, self._class, self._layout, self._heading)
def getMainFont(self) -> QFont:
"""Get the main item icon."""
if CONFIG.emphLabels and self._layout == nwItemLayout.DOCUMENT:
if self._heading == "H1":
return SHARED.theme.guiFontBU
elif self._heading == "H2":
return SHARED.theme.guiFontB
return SHARED.theme.guiFont
def getImportStatus(self) -> tuple[str, QIcon]:
"""Return the relevant importance or status label and icon for
the current item based on its class.
@@ -319,6 +344,19 @@ class NWItem:
entry = self._project.data.itemImport[self._import]
return entry.name, entry.icon
def getActiveStatus(self) -> tuple[str, QIcon]:
"""Return the relevant active status label and icon for
the current item based on its type.
"""
if self.isFileType():
key = "checked" if self._active else "unchecked"
text = trConst(nwLabels.ACTIVE_NAME[key])
icon = SHARED.theme.getIcon(key)
else:
text = ""
icon = SHARED.theme.getIcon("noncheckable")
return text, icon
##
# Checker Methods
##
@@ -518,7 +556,7 @@ class NWItem:
def setMainHeading(self, value: str) -> None:
"""Set the main heading level."""
if value in nwHeaders.H_LEVEL:
if value in nwStyles.H_LEVEL:
self._heading = value
return
@@ -553,8 +591,3 @@ class NWItem:
else:
self._cursorPos = 0
return
def saveInitialCount(self) -> None:
"""Save the initial word count."""
self._initCount = self._wordCount
return

View File

@@ -0,0 +1,518 @@
"""
novelWriter – Project Item Model
================================
File History:
Created: 2024-11-16 [2.6b2] ProjectNode
Created: 2024-11-16 [2.6b2] ProjectModel
This file is a part of novelWriter
Copyright (C) 2024 Veronica Berglyd Olsen and novelWriter contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from PyQt5.QtCore import QAbstractItemModel, QMimeData, QModelIndex, Qt
from PyQt5.QtGui import QFont, QIcon
from novelwriter.common import decodeMimeHandles, encodeMimeHandles, minmax
from novelwriter.constants import nwConst
from novelwriter.core.item import NWItem
from novelwriter.enum import nwItemClass
from novelwriter.types import QtAlignRight
if TYPE_CHECKING: # pragma: no cover
from novelwriter.core.tree import NWTree
logger = logging.getLogger(__name__)
INV_ROOT = "invisibleRoot"
C_FACTOR = 0x0100
C_LABEL_TEXT = 0x0000 | Qt.ItemDataRole.DisplayRole
C_LABEL_ICON = 0x0000 | Qt.ItemDataRole.DecorationRole
C_LABEL_FONT = 0x0000 | Qt.ItemDataRole.FontRole
C_COUNT_TEXT = 0x0100 | Qt.ItemDataRole.DisplayRole
C_COUNT_ICON = 0x0100 | Qt.ItemDataRole.DecorationRole
C_COUNT_ALIGN = 0x0100 | Qt.ItemDataRole.TextAlignmentRole
C_ACTIVE_ICON = 0x0200 | Qt.ItemDataRole.DecorationRole
C_ACTIVE_TIP = 0x0200 | Qt.ItemDataRole.ToolTipRole
C_STATUS_ICON = 0x0300 | Qt.ItemDataRole.DecorationRole
C_STATUS_TIP = 0x0300 | Qt.ItemDataRole.ToolTipRole
NODE_FLAGS = Qt.ItemFlag.ItemIsEnabled
NODE_FLAGS |= Qt.ItemFlag.ItemIsSelectable
NODE_FLAGS |= Qt.ItemFlag.ItemIsDropEnabled
if TYPE_CHECKING: # pragma: no cover
# Requires Python 3.10
T_NodeData = str | QIcon | QFont | Qt.AlignmentFlag | None
class ProjectNode:
"""Core: Project Model Node Class
The project tree structure is saved as nodes in a tree, starting
from a root node. This class makes up these nodes.
Each node is a wrapper around an NWItem object. The NWItem is the
object representing a single item in the project, and it only
contains a reference to its parent as well as it top level root, but
is itself not structured in a hierarchy in memory.
This class provides the necessary hierarchical structure, as well as
the data entries needed for populating the GUI project tree. It also
handles pushing and pulling information from its NWItem when
necessary.
The data to be displayed could in principle be pulled from the
NWItem whenever it is needed, but for performance reason it is
cached, as the GUI will pull this information often.
"""
C_NAME = 0
C_COUNT = 1
C_ACTIVE = 2
C_STATUS = 3
__slots__ = ("_item", "_children", "_parent", "_row", "_cache", "_flags", "_count")
def __init__(self, item: NWItem) -> None:
self._item = item
self._children: list[ProjectNode] = []
self._parent: ProjectNode | None = None
self._row = 0
self._cache: dict[int, T_NodeData] = {}
self._flags = NODE_FLAGS
self._count = 0
self.refresh()
self.updateCount()
return
def __repr__(self) -> str:
return (
f"<ProjectNode handle={self._item.itemHandle} "
f"parent={self._parent.item.itemHandle if self._parent else None} "
f"row={self._row} "
f"children={len(self._children)}>"
)
def __bool__(self) -> bool:
"""A node should always evaluate to True."""
return True
##
# Properties
##
@property
def item(self) -> NWItem:
"""The project item of the node."""
return self._item
@property
def children(self) -> list[ProjectNode]:
"""All children of the node."""
return self._children
@property
def count(self) -> int:
"""The count of the node."""
return self._count
##
# Data Maintenance
##
def refresh(self) -> None:
"""Refresh data values."""
# Label
self._cache[C_LABEL_ICON] = self._item.getMainIcon()
self._cache[C_LABEL_TEXT] = self._item.itemName
self._cache[C_LABEL_FONT] = self._item.getMainFont()
# Count
self._cache[C_COUNT_ALIGN] = QtAlignRight
# Active
aText, aIcon = self._item.getActiveStatus()
self._cache[C_ACTIVE_TIP] = aText
self._cache[C_ACTIVE_ICON] = aIcon
# Status
sText, sIcon = self._item.getImportStatus()
self._cache[C_STATUS_TIP] = sText
self._cache[C_STATUS_ICON] = sIcon
return
def updateCount(self, propagate: bool = True) -> None:
"""Update counts, and propagate upwards in the tree."""
self._count = self._item.wordCount + sum(c._count for c in self._children)
self._cache[C_COUNT_TEXT] = f"{self._count:n}"
if propagate and (parent := self._parent):
parent.updateCount()
return
##
# Data Access
##
def row(self) -> int:
"""Return the node's row number."""
return self._row
def childCount(self) -> int:
"""Return the number of children of the node."""
return len(self._children)
def data(self, column: int, role: Qt.ItemDataRole) -> T_NodeData:
"""Return cached node data."""
return self._cache.get(C_FACTOR*column | role)
def flags(self) -> Qt.ItemFlag:
"""Return cached node flags."""
return self._flags
def parent(self) -> ProjectNode | None:
"""Return the parent of the node."""
return self._parent
def child(self, row: int) -> ProjectNode | None:
"""Return a child ofg the node."""
if 0 <= row < len(self._children):
return self._children[row]
return None
def allChildren(self) -> list[ProjectNode]:
"""Return a recursive list of all children."""
nodes: list[ProjectNode] = []
self._recursiveAppendChildren(nodes)
return nodes
##
# Data Edit
##
def addChild(self, child: ProjectNode, pos: int = -1) -> None:
"""Add a child item to this item."""
child._parent = self
self._updateRelationships(child)
if 0 <= pos < len(self._children):
self._children.insert(pos, child)
else:
child._row = len(self._children)
self._children.append(child)
self._refreshChildrenPos()
return
def takeChild(self, pos: int) -> ProjectNode | None:
"""Remove a child item and return it."""
if 0 <= pos < len(self._children):
node = self._children.pop(pos)
self._refreshChildrenPos()
self.updateCount()
return node
return None
def moveChild(self, source: int, target: int) -> None:
"""Move a child internally."""
count = len(self._children)
if (source != target) and (0 <= source < count) and (0 <= target <= count):
node = self._children.pop(source)
self._children.insert(target, node)
self._refreshChildrenPos()
return
def setExpanded(self, state: bool) -> None:
"""Set the node's expanded state."""
if state and self._children:
self._item.setExpanded(True)
else:
self._item.setExpanded(False)
return
##
# Internal Functions
##
def _recursiveAppendChildren(self, children: list[ProjectNode]) -> None:
"""Recursively add all nodes to a list."""
for node in self._children:
children.append(node)
node._recursiveAppendChildren(children)
return
def _refreshChildrenPos(self) -> None:
"""Update the row value on all children."""
for n, child in enumerate(self._children):
child._row = n
child.item.setOrder(n)
return
def _updateRelationships(self, child: ProjectNode) -> None:
"""Update a child item's relationships."""
if self._parent:
child.item.setParent(self._item.itemHandle)
child.item.setRoot(self._item.itemRoot)
child.item.setClassDefaults(self._item.itemClass)
child._flags = NODE_FLAGS | Qt.ItemFlag.ItemIsDragEnabled
else:
child.item.setParent(None)
child.item.setRoot(child.item.itemHandle)
child.item.setClassDefaults(child.item.itemClass)
return
class ProjectModel(QAbstractItemModel):
"""Core: Project Model Class
This class provides the interface for the tree widget used on the
GUI. It implements the QModelIndex based interface required, adds
support for drag and drop, and a few other novelWriter-specific
methods needed primarily by the project tree GUI component.
"""
__slots__ = ("_tree", "_root")
def __init__(self, tree: NWTree) -> None:
super().__init__()
self._tree = tree
self._root = ProjectNode(NWItem(tree._project, INV_ROOT))
self._root.item.setName("Invisible Root")
logger.debug("Ready: ProjectModel")
return
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: ProjectModel")
return
##
# Properties
##
@property
def root(self) -> ProjectNode:
"""Return the model root item."""
return self._root
##
# Model Interface
##
def rowCount(self, index: QModelIndex) -> int:
"""Return the number of rows for an entry."""
if index.isValid():
return index.internalPointer().childCount()
return self._root.childCount()
def columnCount(self, index: QModelIndex) -> int:
"""Return the number of columns for an entry."""
return 4
def parent(self, index: QModelIndex) -> QModelIndex:
"""Get the parent model index of another index."""
if index.isValid() and (parent := index.internalPointer().parent()):
return self.createIndex(parent.row(), 0, parent)
return QModelIndex()
def index(self, row: int, column: int, parent: QModelIndex = QModelIndex()) -> QModelIndex:
"""get the index of a child item of a parent."""
if self.hasIndex(row, column, parent):
node: ProjectNode = parent.internalPointer() if parent.isValid() else self._root
if child := node.child(row):
return self.createIndex(row, column, child)
return QModelIndex()
def data(self, index: QModelIndex, role: Qt.ItemDataRole) -> T_NodeData:
"""Return display data for a project node."""
if index.isValid():
return index.internalPointer().data(index.column(), role)
return None
def flags(self, index: QModelIndex) -> Qt.ItemFlag:
"""Return flags for a project node."""
if index.isValid():
return index.internalPointer().flags()
return Qt.ItemFlag.NoItemFlags
##
# Drag and Drop
##
def supportedDropActions(self) -> Qt.DropAction:
"""Return supported drop actions"""
return Qt.DropAction.MoveAction
def mimeTypes(self) -> list[str]:
"""Return the supported mime types of the model."""
return [nwConst.MIME_HANDLE]
def mimeData(self, indices: list[QModelIndex]) -> QMimeData:
"""Encode mime data about a selection."""
handles = [
i.internalPointer().item.itemHandle
for i in indices if i.isValid() and i.column() == 0
]
mime = QMimeData()
encodeMimeHandles(mime, handles)
return mime
def canDropMimeData(
self, data: QMimeData, action: Qt.DropAction,
row: int, column: int, parent: QModelIndex
) -> bool:
"""Check if mime data can be dropped on the current location."""
return data.hasFormat(nwConst.MIME_HANDLE) and action == Qt.DropAction.MoveAction
def dropMimeData(
self, data: QMimeData, action: Qt.DropAction,
row: int, column: int, parent: QModelIndex
) -> bool:
"""Process mime data drop."""
if self.canDropMimeData(data, action, row, column, parent):
items = []
for handle in decodeMimeHandles(data):
if (index := self.indexFromHandle(handle)).isValid():
items.append(index)
self.multiMove(items, parent, row)
return True
return False
##
# Data Access
##
def row(self, index: QModelIndex) -> int:
"""Return the row number of the index."""
if index.isValid():
return index.internalPointer().row()
return -1
def node(self, index: QModelIndex) -> ProjectNode | None:
"""Return the node for a given model index."""
if index.isValid():
return index.internalPointer()
return None
def nodes(self, indices: list[QModelIndex]) -> list[ProjectNode]:
"""Return the nodes for a list of model indices."""
return [i.internalPointer() for i in indices if i.isValid() and i.column() == 0]
def indexFromHandle(self, handle: str | None) -> QModelIndex:
"""Get the index representing a node in the model."""
if handle and (node := self._tree.nodes.get(handle)):
return self.createIndex(node.row(), 0, node)
return QModelIndex()
def indexFromNode(self, node: ProjectNode, column: int = 0) -> QModelIndex:
"""Get the index representing a node in the model."""
return self.createIndex(node.row(), column, node)
##
# Model Edit
##
def insertChild(self, child: ProjectNode, parent: QModelIndex, pos: int) -> None:
"""Insert a node into the model at a given position."""
node: ProjectNode = parent.internalPointer() if parent.isValid() else self._root
count = node.childCount()
row = minmax(pos, 0, count) if pos >= 0 else count
self.beginInsertRows(parent, row, row)
node.addChild(child, row)
self.endInsertRows()
return
def removeChild(self, parent: QModelIndex, pos: int) -> ProjectNode | None:
"""Remove a node from the model and return it."""
node: ProjectNode = parent.internalPointer() if parent.isValid() else self._root
if 0 <= pos < node.childCount():
self.beginRemoveRows(parent, pos, pos)
child = node.takeChild(pos)
self.endRemoveRows()
return child
return None
def internalMove(self, index: QModelIndex, step: int) -> None:
"""Move an item internally among its siblings."""
if index.isValid():
node: ProjectNode = index.internalPointer()
if parent := node.parent():
pos = index.row()
new = minmax(pos + step, 0, parent.childCount() - 1)
if new != pos:
end = new if new < pos else new + 1
self.beginMoveRows(index.parent(), pos, pos, index.parent(), end)
parent.moveChild(pos, new)
self.endMoveRows()
return
def multiMove(self, indices: list[QModelIndex], target: QModelIndex, pos: int = -1) -> None:
"""Move multiple items to a new location."""
if target.isValid():
# This is a two pass process. First we only select unique
# non-root items for move, then we do a second pass and only
# move those items that don't have a parent also scheduled
# for moving or have already been moved. Child items are
# moved with the parent.
pruned = []
handles = set()
for index in indices:
if index.isValid():
node: ProjectNode = index.internalPointer()
handle = node.item.itemHandle
if node.item.isRootType() is False and handle not in handles:
pruned.append(node)
handles.add(handle)
for node in (reversed(pruned) if pos >= 0 else pruned):
if node.item.itemParent not in handles:
index = self.indexFromNode(node)
if temp := self.removeChild(index.parent(), index.row()):
self.insertChild(temp, target, pos)
for child in reversed(node.allChildren()):
node._updateRelationships(child)
child.item.notifyToRefresh()
node.item.notifyToRefresh()
return
##
# Other Methods
##
def clear(self) -> None:
"""Clear the project model."""
self._root._children.clear()
return
def allExpanded(self) -> list[QModelIndex]:
"""Return a list of all expanded items."""
expanded = []
for node in self._root.allChildren():
if node._item.isExpanded:
expanded.append(self.createIndex(node.row(), 0, node))
return expanded
def trashSelection(self, indices: list[QModelIndex]) -> bool:
"""Check if a selection of indices are all in trash or not."""
for index in indices:
if index.isValid():
node: ProjectNode = index.internalPointer()
if node.item.itemClass != nwItemClass.TRASH:
return False
return True

View File

@@ -7,7 +7,7 @@ Created: 2019-10-21 [0.3.1] OptionState
Rewritten: 2020-02-19 [0.4.5] OptionState
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
Copyright (C) 2019 Veronica Berglyd Olsen and novelWriter contributors
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
@@ -60,7 +60,7 @@ VALID_MAP: dict[str, set[str]] = {
},
"GuiManuscript": {
"winWidth", "winHeight", "optsWidth", "viewWidth", "listHeight",
"detailsHeight", "detailsWidth", "detailsExpanded",
"detailsHeight", "detailsWidth", "detailsExpanded", "showNewPage",
},
"GuiManuscriptBuild": {
"winWidth", "winHeight", "fmtWidth", "sumWidth",
@@ -73,6 +73,9 @@ VALID_MAP: dict[str, set[str]] = {
"widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble",
"novelRoot",
},
"GuiOutlineDetails": {
"detailsWidth", "tagsWidth",
}
}

View File

@@ -6,7 +6,7 @@ File History:
Created: 2018-09-29 [0.0.1] NWProject
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
Copyright (C) 2018 Veronica Berglyd Olsen and novelWriter contributors
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
@@ -26,7 +26,6 @@ from __future__ import annotations
import json
import logging
from collections.abc import Iterable
from enum import Enum
from functools import partial
from pathlib import Path
@@ -52,7 +51,8 @@ from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
from novelwriter.error import logException
if TYPE_CHECKING: # pragma: no cover
from novelwriter.core.item import NWItem
# Requires Python 3.10
from novelwriter.core.status import T_StatusKind, T_UpdateEntry
logger = logging.getLogger(__name__)
@@ -67,6 +67,11 @@ class NWProjectState(Enum):
class NWProject:
__slots__ = (
"_options", "_storage", "_data", "_tree", "_index", "_session",
"_langData", "_changed", "_valid", "_state", "tr",
)
def __init__(self) -> None:
# Core Elements
@@ -94,6 +99,12 @@ class NWProject:
logger.debug("Delete: NWProject")
return
def clear(self) -> None:
"""Clear the project."""
self._tree.clear()
self._index.clear()
return
##
# Properties
##
@@ -150,24 +161,46 @@ class NWProject:
"""Return total edit time, including the current session."""
return self._data.editTime + round(time() - self._session.start)
@property
def currentTotalCount(self) -> int:
"""Return the current total word count from the tree."""
return self._tree.model.root.count
##
# Item Methods
##
def newRoot(self, itemClass: nwItemClass, label: str | None = None) -> str:
def newRoot(self, itemClass: nwItemClass, pos: int = -1) -> str:
"""Add a new root folder to the project. If label is not set,
use the class label.
"""
label = label or trConst(nwLabels.CLASS_NAME[itemClass])
return self._tree.create(label, None, nwItemType.ROOT, itemClass)
label = trConst(nwLabels.CLASS_NAME[itemClass])
return self._tree.create(label, None, nwItemType.ROOT, itemClass=itemClass, pos=pos)
def newFolder(self, label: str, parent: str) -> str | None:
def newFolder(self, label: str, parent: str, pos: int = -1) -> str | None:
"""Add a new folder with a given label and parent item."""
return self._tree.create(label, parent, nwItemType.FOLDER)
return self._tree.create(label, parent, nwItemType.FOLDER, pos=pos)
def newFile(self, label: str, parent: str) -> str | None:
def newFile(self, label: str, parent: str, pos: int = -1) -> str | None:
"""Add a new file with a given label and parent item."""
return self._tree.create(label, parent, nwItemType.FILE)
return self._tree.create(label, parent, nwItemType.FILE, pos=pos)
def removeItem(self, tHandle: str) -> bool:
"""Remove an item from the project. This will delete both the
project entry and a document file if it exists.
"""
if self._tree.checkType(tHandle, nwItemType.FILE):
SHARED.closeDocument(tHandle)
doc = self._storage.getDocument(tHandle)
if not doc.deleteDocument():
SHARED.error(
self.tr("Could not delete document file."),
info=doc.getError()
)
return False
self._index.deleteHandle(tHandle)
self._tree.remove(tHandle)
return True
def writeNewFile(self, tHandle: str, hLevel: int, isDocument: bool, text: str = "") -> bool:
"""Write content to a new document after it is created. This
@@ -209,35 +242,21 @@ class NWProject:
text = self._storage.getDocumentText(sHandle)
self._storage.getDocument(tHandle).writeDocument(text)
sItem.setLayout(tItem.itemLayout)
self._index.scanText(tHandle, text)
self._index.reIndexHandle(tHandle)
return True
def removeItem(self, tHandle: str) -> bool:
"""Remove an item from the project. This will delete both the
project entry and a document file if it exists.
def createNewNote(self, tag: str, itemClass: nwItemClass) -> None:
"""Create a new note. This function is used by the document
editor to create note files for unknown tags.
"""
if self._tree.checkType(tHandle, nwItemType.FILE):
delDoc = self._storage.getDocument(tHandle)
if not delDoc.deleteDocument():
SHARED.error(
self.tr("Could not delete document file."),
info=delDoc.getError()
)
return False
self._index.deleteHandle(tHandle)
del self._tree[tHandle]
return True
def trashFolder(self) -> str:
"""Add the special trash root folder to the project."""
trashHandle = self._tree.trashRoot
if trashHandle is None:
label = trConst(nwLabels.CLASS_NAME[nwItemClass.TRASH])
return self._tree.create(label, None, nwItemType.ROOT, nwItemClass.TRASH)
return trashHandle
if itemClass != nwItemClass.NO_CLASS:
if not (rHandle := self._tree.findRoot(itemClass)):
rHandle = self.newRoot(itemClass)
if rHandle and (tHandle := SHARED.project.newFile(tag.title(), rHandle)):
self.writeNewFile(tHandle, 1, False, f"@tag: {tag}\n\n")
self._tree.refreshItems([tHandle])
return
##
# Project Methods
@@ -349,13 +368,14 @@ class NWProject:
self._index.loadIndex()
if xmlReader.state == XMLReadState.WAS_LEGACY:
# Often, the index needs to be rebuilt when updating format
self._index.rebuildIndex()
self._index.rebuild()
self.updateWordCounts()
self._session.startSession()
self.setProjectChanged(False)
self._valid = True
self._state = NWProjectState.READY
self._storage.lockSession() # Lock only after a successful open. See issue #1977.
SHARED.newStatusMessage(self.tr("Opened Project: {0}").format(self._data.name))
@@ -413,12 +433,11 @@ class NWProject:
def closeProject(self, idleTime: float = 0.0) -> None:
"""Close the project."""
logger.info("Closing project")
self._index.clearIndex() # Triggers clear signal, see #1718
self._index.clear() # Triggers clear signal, see #1718
self._options.saveSettings()
self._tree.writeToCFile()
self._session.appendSession(idleTime)
self._storage.closeSession()
self._lockedBy = None
return
def backupProject(self, doNotify: bool) -> bool:
@@ -488,17 +507,6 @@ class NWProject:
self.setProjectChanged(True)
return
def setTreeOrder(self, order: list[str]) -> None:
"""A list representing the linear/flattened order of project
items in the GUI project tree. The user can rearrange the order
by drag-and-drop. Forwarded to the NWTree class.
"""
if len(self._tree) != len(order):
logger.warning("Sizes of new and old tree order do not match")
self._tree.setOrder(order)
self.setProjectChanged(True)
return
def setProjectChanged(self, status: bool) -> bool:
"""Toggle the project changed flag, and propagate the
information to the GUI statusbar.
@@ -512,47 +520,6 @@ class NWProject:
# Class Methods
##
def iterProjectItems(self) -> Iterable[NWItem]:
"""This function ensures that the item tree loaded is sent to
the GUI tree view in such a way that the tree can be built. That
is, the parent item must be sent before its child. In principle,
a proper XML file will already ensure that, but in the event the
order has been altered, or a file is orphaned, this function is
capable of handling it.
"""
sentItems = set()
iterItems = self._tree.handles()
n = 0
nMax = min(len(iterItems), 10000)
while n < nMax:
tHandle = iterItems[n]
tItem = self._tree[tHandle]
n += 1
if tItem is None:
# Technically a bug
continue
elif tItem.itemParent is None:
# Item is a root, or already been identified as orphaned
sentItems.add(tHandle)
yield tItem
elif tItem.itemParent in sentItems:
# Item's parent has been sent, so all is fine
sentItems.add(tHandle)
yield tItem
elif tItem.itemParent in iterItems:
# Item's parent exists, but hasn't been sent yet, so add
# it again to the end, but make sure this doesn't get
# out hand, so we cap at 10000 items
logger.warning("Item '%s' found before its parent", tHandle)
iterItems.append(tHandle)
nMax = min(len(iterItems), 10000)
else:
# Item is orphaned
logger.error("Item '%s' has no parent in current tree", tHandle)
tItem.setParent(None)
yield tItem
return
def updateWordCounts(self) -> None:
"""Update the total word count values."""
novel, notes = self._tree.sumWords()
@@ -573,6 +540,18 @@ class NWProject:
self._data.itemImport.increment(nwItem.itemImport)
return
def updateStatus(self, kind: T_StatusKind, update: T_UpdateEntry) -> None:
"""Update status or import entries."""
if kind == "s":
self._data.itemStatus.update(update)
SHARED.emitStatusLabelsChanged(self, kind)
self._tree.refreshAllItems()
elif kind == "i":
self._data.itemImport.update(update)
SHARED.emitStatusLabelsChanged(self, kind)
self._tree.refreshAllItems()
return
def localLookup(self, word: str | int) -> str:
"""Look up a word or number in the translation map for the
project and return it. The variable is cast to a string before

View File

@@ -6,7 +6,7 @@ File History:
Created: 2022-10-30 [2.0rc2] NWProjectData
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
Copyright (C) 2022 Veronica Berglyd Olsen and novelWriter contributors
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
@@ -29,7 +29,8 @@ import uuid
from typing import TYPE_CHECKING, Any
from novelwriter.common import (
checkBool, checkInt, checkStringNone, checkUuid, isHandle, simplified
checkBool, checkInt, checkStringNone, checkUuid, isHandle,
makeFileNameSafe, simplified
)
from novelwriter.core.status import NWStatus
@@ -101,6 +102,11 @@ class NWProjectData:
"""Return the project name."""
return self._name
@property
def fileSafeName(self) -> str:
"""Return the project name in a file name safe format."""
return makeFileNameSafe(self._name)
@property
def author(self) -> str:
"""Return the project author."""

View File

@@ -8,7 +8,7 @@ Created: 2022-09-28 [2.0rc2] ProjectXMLReader
Created: 2022-10-31 [2.0rc2] ProjectXMLWriter
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
Copyright (C) 2022 Veronica Berglyd Olsen and novelWriter contributors
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

View File

@@ -6,7 +6,7 @@ File History:
Created: 2023-06-11 [2.1b1] NWSessionLog
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
Copyright (C) 2023 Veronica Berglyd Olsen and novelWriter contributors
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

View File

@@ -7,7 +7,7 @@ Created: 2019-06-11 [0.1.5] NWSpellEnchant
Created: 2023-06-13 [2.1b1] UserDictionary
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
Copyright (C) 2019 Veronica Berglyd Olsen and novelWriter contributors
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
@@ -125,21 +125,16 @@ class NWSpellEnchant:
except Exception:
return []
def addWord(self, word: str) -> bool:
def addWord(self, word: str, save: bool = True) -> None:
"""Add a word to the project dictionary."""
word = word.strip()
if not word:
return False
try:
self._enchant.add_to_session(word)
except Exception:
return False
added = self._userDict.add(word)
if added:
self._userDict.save()
return added
if word := word.strip():
try:
self._enchant.add_to_session(word)
except Exception:
return
if save and self._userDict.add(word):
self._userDict.save()
return
def listDictionaries(self) -> list[tuple[str, str]]:
"""List available dictionaries."""

View File

@@ -7,7 +7,7 @@ Created: 2019-05-19 [0.1.3] NWStatus
Rewritten: 2022-04-05 [2.0b1] NWStatus
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
Copyright (C) 2019 Veronica Berglyd Olsen and novelWriter contributors
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
@@ -37,7 +37,7 @@ from PyQt5.QtGui import QColor, QIcon, QPainter, QPainterPath, QPixmap, QPolygon
from novelwriter import SHARED
from novelwriter.common import simplified
from novelwriter.enum import nwStatusShape
from novelwriter.types import QtPaintAnitAlias, QtTransparent
from novelwriter.types import QtPaintAntiAlias, QtTransparent
if TYPE_CHECKING: # pragma: no cover
from typing import TypeGuard # Requires Python 3.10
@@ -65,6 +65,11 @@ class StatusEntry:
NO_ENTRY = StatusEntry("", QColor(0, 0, 0), nwStatusShape.SQUARE, QIcon(), 0)
if TYPE_CHECKING: # pragma: no cover
# Requires Python 3.10
T_UpdateEntry = list[tuple[str | None, StatusEntry]]
T_StatusKind = Literal["s", "i"]
class NWStatus:
@@ -73,7 +78,7 @@ class NWStatus:
__slots__ = ("_store", "_default", "_prefix", "_height")
def __init__(self, prefix: Literal["s", "i"]) -> None:
def __init__(self, prefix: T_StatusKind) -> None:
self._store: dict[str, StatusEntry] = {}
self._default = None
self._prefix = prefix[:1]
@@ -120,8 +125,8 @@ class NWStatus:
return key
def update(self, update: list[tuple[str | None, StatusEntry]]) -> None:
"""Update the list of statuses, and from removed list."""
def update(self, update: T_UpdateEntry) -> None:
"""Update the list of statuses."""
self._store.clear()
for key, entry in update:
self._store[self._checkKey(key)] = entry
@@ -169,6 +174,20 @@ class NWStatus:
"""Yield entries from the status icons."""
yield from self._store.items()
def fromRaw(self, data: list[str]) -> StatusEntry | None:
"""Create a StatusEntry from a list of three strings consisting
of shape, colour, and name. This entry is not automatically
added to the list of entries.
"""
try:
shape = nwStatusShape[str(data[0])]
color = QColor(str(data[1]))
icon = NWStatus.createIcon(self._height, color, shape)
return StatusEntry(simplified(data[2]), color, shape, icon)
except Exception:
logger.error("Could not parse entry %s", str(data))
return None
@staticmethod
def createIcon(height: int, color: QColor, shape: nwStatusShape) -> QIcon:
"""Generate an icon for a status label."""
@@ -176,7 +195,7 @@ class NWStatus:
pixmap.fill(QtTransparent)
painter = QPainter(pixmap)
painter.setRenderHint(QtPaintAnitAlias)
painter.setRenderHint(QtPaintAntiAlias)
painter.fillPath(_SHAPES.getShape(shape), color)
painter.end()

View File

@@ -6,7 +6,7 @@ File History:
Created: 2022-11-01 [2.0rc2] NWStorage
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
Copyright (C) 2022 Veronica Berglyd Olsen and novelWriter contributors
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
@@ -235,7 +235,6 @@ class NWStorage:
if child.is_dir() and child.name.startswith("data_"):
legacy.legacyDataFolder(basePath, child)
self._writeLockFile()
self._ready = True
return NWStorageOpen.READY
@@ -249,6 +248,12 @@ class NWStorage:
return True
return True
def lockSession(self) -> None:
"""Lock the session when the project is successfully opened."""
if self._ready:
self._writeLockFile()
return
def closeSession(self) -> None:
"""Run tasks related to closing the session."""
self._clearLockFile()

View File

@@ -1,530 +0,0 @@
"""
novelWriter – HTML Text Converter
=================================
File History:
Created: 2019-05-07 [0.0.1] ToHtml
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import json
import logging
from pathlib import Path
from time import time
from novelwriter.common import formatTimeStamp
from novelwriter.constants import nwHeadFmt, nwHtmlUnicode, nwKeyWords, nwLabels
from novelwriter.core.project import NWProject
from novelwriter.core.tokenizer import T_Formats, Tokenizer, stripEscape
from novelwriter.types import FONT_STYLE, FONT_WEIGHTS
logger = logging.getLogger(__name__)
# Each opener tag, with the id of its corresponding closer and tag format
HTML_OPENER: dict[int, tuple[int, str]] = {
Tokenizer.FMT_B_B: (Tokenizer.FMT_B_E, "<strong>"),
Tokenizer.FMT_I_B: (Tokenizer.FMT_I_E, "<em>"),
Tokenizer.FMT_D_B: (Tokenizer.FMT_D_E, "<del>"),
Tokenizer.FMT_U_B: (Tokenizer.FMT_U_E, "<span style='text-decoration: underline;'>"),
Tokenizer.FMT_M_B: (Tokenizer.FMT_M_E, "<mark>"),
Tokenizer.FMT_SUP_B: (Tokenizer.FMT_SUP_E, "<sup>"),
Tokenizer.FMT_SUB_B: (Tokenizer.FMT_SUB_E, "<sub>"),
Tokenizer.FMT_DL_B: (Tokenizer.FMT_DL_E, "<span class='dialog'>"),
Tokenizer.FMT_ADL_B: (Tokenizer.FMT_ADL_E, "<span class='altdialog'>"),
}
# Each closer tag, with the id of its corresponding opener and tag format
HTML_CLOSER: dict[int, tuple[int, str]] = {
Tokenizer.FMT_B_E: (Tokenizer.FMT_B_B, "</strong>"),
Tokenizer.FMT_I_E: (Tokenizer.FMT_I_B, "</em>"),
Tokenizer.FMT_D_E: (Tokenizer.FMT_D_B, "</del>"),
Tokenizer.FMT_U_E: (Tokenizer.FMT_U_B, "</span>"),
Tokenizer.FMT_M_E: (Tokenizer.FMT_M_B, "</mark>"),
Tokenizer.FMT_SUP_E: (Tokenizer.FMT_SUP_B, "</sup>"),
Tokenizer.FMT_SUB_E: (Tokenizer.FMT_SUB_B, "</sub>"),
Tokenizer.FMT_DL_E: (Tokenizer.FMT_DL_B, "</span>"),
Tokenizer.FMT_ADL_E: (Tokenizer.FMT_ADL_B, "</span>"),
}
# Empty HTML tag record
HTML_NONE = (0, "")
class ToHtml(Tokenizer):
"""Core: HTML Document Writer
Extend the Tokenizer class to writer HTML output. This class is
also used by the Document Viewer, and Manuscript Build Preview.
"""
def __init__(self, project: NWProject) -> None:
super().__init__(project)
self._cssStyles = True
self._fullHTML: list[str] = []
# Internals
self._trMap = {}
self._usedNotes: dict[str, int] = {}
self.setReplaceUnicode(False)
return
##
# Properties
##
@property
def fullHTML(self) -> list[str]:
return self._fullHTML
##
# Setters
##
def setStyles(self, cssStyles: bool) -> None:
"""Enable or disable CSS styling. Some elements may still have
class tags.
"""
self._cssStyles = cssStyles
return
def setReplaceUnicode(self, doReplace: bool) -> None:
"""Set the translation map to either minimal or full unicode for
html entities replacement.
"""
# Control characters must always be replaced
# Angle brackets are replaced later as they are also used in
# formatting codes
self._trMap = str.maketrans({"&": "&amp;"})
if doReplace:
# Extend to all relevant Unicode characters
self._trMap.update(str.maketrans(nwHtmlUnicode.U_TO_H))
return
##
# Class Methods
##
def getFullResultSize(self) -> int:
"""Return the size of the full HTML result."""
return sum(len(x) for x in self._fullHTML)
def doPreProcessing(self) -> None:
"""Extend the auto-replace to also properly encode some unicode
characters into their respective HTML entities.
"""
super().doPreProcessing()
self._text = self._text.translate(self._trMap)
return
def doConvert(self) -> None:
"""Convert the list of text tokens into an HTML document."""
self._result = ""
if self._isNovel:
# For story files, we bump the titles one level up
h1Cl = " class='title'"
h1 = "h1"
h2 = "h1"
h3 = "h2"
h4 = "h3"
else:
h1Cl = ""
h1 = "h1"
h2 = "h2"
h3 = "h3"
h4 = "h4"
lines = []
tHandle = self._handle
for tType, nHead, tText, tFormat, tStyle in self._tokens:
# Replace < and > with HTML entities
if tFormat:
# If we have formatting, we must recompute the locations
cText = []
i = 0
for c in tText:
if c == "<":
cText.append("&lt;")
tFormat = [(p + 3 if p > i else p, f, k) for p, f, k in tFormat]
i += 4
elif c == ">":
cText.append("&gt;")
tFormat = [(p + 3 if p > i else p, f, k) for p, f, k in tFormat]
i += 4
else:
cText.append(c)
i += 1
tText = "".join(cText)
else:
# If we don't have formatting, we can do a plain replace
tText = tText.replace("<", "&lt;").replace(">", "&gt;")
# Styles
aStyle = []
if tStyle is not None and self._cssStyles:
if tStyle & self.A_LEFT:
aStyle.append("text-align: left;")
elif tStyle & self.A_RIGHT:
aStyle.append("text-align: right;")
elif tStyle & self.A_CENTRE:
aStyle.append("text-align: center;")
elif tStyle & self.A_JUSTIFY:
aStyle.append("text-align: justify;")
if tStyle & self.A_PBB:
aStyle.append("page-break-before: always;")
if tStyle & self.A_PBA:
aStyle.append("page-break-after: always;")
if tStyle & self.A_Z_BTMMRG:
aStyle.append("margin-bottom: 0;")
if tStyle & self.A_Z_TOPMRG:
aStyle.append("margin-top: 0;")
if tStyle & self.A_IND_L:
aStyle.append(f"margin-left: {self._blockIndent:.2f}em;")
if tStyle & self.A_IND_R:
aStyle.append(f"margin-right: {self._blockIndent:.2f}em;")
if tStyle & self.A_IND_T:
aStyle.append(f"text-indent: {self._firstWidth:.2f}em;")
if aStyle:
stVals = " ".join(aStyle)
hStyle = f" style='{stVals}'"
else:
hStyle = ""
if self._linkHeadings and tHandle:
aNm = f"<a name='{tHandle}:T{nHead:04d}'></a>"
else:
aNm = ""
# Process Text Type
if tType == self.T_TEXT:
lines.append(f"<p{hStyle}>{self._formatText(tText, tFormat)}</p>\n")
elif tType == self.T_TITLE:
tHead = tText.replace(nwHeadFmt.BR, "<br>")
lines.append(f"<h1 class='title'{hStyle}>{aNm}{tHead}</h1>\n")
elif tType == self.T_HEAD1:
tHead = tText.replace(nwHeadFmt.BR, "<br>")
lines.append(f"<{h1}{h1Cl}{hStyle}>{aNm}{tHead}</{h1}>\n")
elif tType == self.T_HEAD2:
tHead = tText.replace(nwHeadFmt.BR, "<br>")
lines.append(f"<{h2}{hStyle}>{aNm}{tHead}</{h2}>\n")
elif tType == self.T_HEAD3:
tHead = tText.replace(nwHeadFmt.BR, "<br>")
lines.append(f"<{h3}{hStyle}>{aNm}{tHead}</{h3}>\n")
elif tType == self.T_HEAD4:
tHead = tText.replace(nwHeadFmt.BR, "<br>")
lines.append(f"<{h4}{hStyle}>{aNm}{tHead}</{h4}>\n")
elif tType == self.T_SEP:
lines.append(f"<p class='sep'{hStyle}>{tText}</p>\n")
elif tType == self.T_SKIP:
lines.append(f"<p class='skip'{hStyle}>&nbsp;</p>\n")
elif tType == self.T_SYNOPSIS and self._doSynopsis:
lines.append(self._formatSynopsis(self._formatText(tText, tFormat), True))
elif tType == self.T_SHORT and self._doSynopsis:
lines.append(self._formatSynopsis(self._formatText(tText, tFormat), False))
elif tType == self.T_COMMENT and self._doComments:
lines.append(self._formatComments(self._formatText(tText, tFormat)))
elif tType == self.T_KEYWORD and self._doKeywords:
tag, text = self._formatKeywords(tText)
kClass = f" class='meta meta-{tag}'" if tag else ""
tTemp = f"<p{kClass}{hStyle}>{text}</p>\n"
lines.append(tTemp)
self._result = "".join(lines)
self._fullHTML.append(self._result)
return
def appendFootnotes(self) -> None:
"""Append the footnotes in the buffer."""
if self._usedNotes:
footnotes = self._localLookup("Footnotes")
lines = []
lines.append(f"<h3>{footnotes}</h3>\n")
lines.append("<ol>\n")
for key, index in self._usedNotes.items():
if content := self._footnotes.get(key):
text = self._formatText(*content)
lines.append(f"<li id='footnote_{index}'><p>{text}</p></li>\n")
lines.append("</ol>\n")
result = "".join(lines)
self._result += result
self._fullHTML.append(result)
return
def saveHtml5(self, path: str | Path) -> None:
"""Save the data to an HTML file."""
with open(path, mode="w", encoding="utf-8") as fObj:
fObj.write((
"<!DOCTYPE html>\n"
"<html>\n"
"<head>\n"
"<meta charset='utf-8'>\n"
"<title>{title:s}</title>\n"
"</head>\n"
"<style>\n"
"{style:s}\n"
"</style>\n"
"<body>\n"
"<article>\n"
"{body:s}\n"
"</article>\n"
"</body>\n"
"</html>\n"
).format(
title=self._project.data.name,
style="\n".join(self.getStyleSheet()),
body=("".join(self._fullHTML)).replace("\t", "&#09;").rstrip(),
))
logger.info("Wrote file: %s", path)
return
def saveHtmlJson(self, path: str | Path) -> None:
"""Save the data to a JSON file."""
timeStamp = time()
data = {
"meta": {
"projectName": self._project.data.name,
"novelAuthor": self._project.data.author,
"buildTime": int(timeStamp),
"buildTimeStr": formatTimeStamp(timeStamp),
},
"text": {
"css": self.getStyleSheet(),
"html": [t.replace("\t", "&#09;").rstrip().split("\n") for t in self.fullHTML],
}
}
with open(path, mode="w", encoding="utf-8") as fObj:
json.dump(data, fObj, indent=2)
logger.info("Wrote file: %s", path)
return
def replaceTabs(self, nSpaces: int = 8, spaceChar: str = "&nbsp;") -> None:
"""Replace tabs with spaces in the html."""
htmlText = []
tabSpace = spaceChar*nSpaces
for aLine in self._fullHTML:
htmlText.append(aLine.replace("\t", tabSpace))
self._fullHTML = htmlText
return
def getStyleSheet(self) -> list[str]:
"""Generate a stylesheet for the current settings."""
if not self._cssStyles:
return []
mScale = self._lineHeight/1.15
styles = []
font = self._textFont
styles.append((
"body {{"
"font-family: '{0:s}'; font-size: {1:d}pt; "
"font-weight: {2:d}; font-style: {3:s};"
"}}"
).format(
font.family(), font.pointSize(),
FONT_WEIGHTS.get(font.weight(), 400),
FONT_STYLE.get(font.style(), "normal"),
))
styles.append((
"p {{"
"text-align: {0}; line-height: {1:d}%; "
"margin-top: {2:.2f}em; margin-bottom: {3:.2f}em;"
"}}"
).format(
"justify" if self._doJustify else "left",
round(100 * self._lineHeight),
mScale * self._marginText[0],
mScale * self._marginText[1],
))
styles.append((
"h1 {{"
"color: rgb(66, 113, 174); "
"page-break-after: avoid; "
"margin-top: {0:.2f}em; "
"margin-bottom: {1:.2f}em;"
"}}"
).format(
mScale * self._marginHead1[0], mScale * self._marginHead1[1]
))
styles.append((
"h2 {{"
"color: rgb(66, 113, 174); "
"page-break-after: avoid; "
"margin-top: {0:.2f}em; "
"margin-bottom: {1:.2f}em;"
"}}"
).format(
mScale * self._marginHead2[0], mScale * self._marginHead2[1]
))
styles.append((
"h3 {{"
"color: rgb(50, 50, 50); "
"page-break-after: avoid; "
"margin-top: {0:.2f}em; "
"margin-bottom: {1:.2f}em;"
"}}"
).format(
mScale * self._marginHead3[0], mScale * self._marginHead3[1]
))
styles.append((
"h4 {{"
"color: rgb(50, 50, 50); "
"page-break-after: avoid; "
"margin-top: {0:.2f}em; "
"margin-bottom: {1:.2f}em;"
"}}"
).format(
mScale * self._marginHead4[0], mScale * self._marginHead4[1]
))
styles.append((
".title {{"
"font-size: 2.5em; "
"margin-top: {0:.2f}em; "
"margin-bottom: {1:.2f}em;"
"}}"
).format(
mScale * self._marginTitle[0], mScale * self._marginTitle[1]
))
styles.append((
".sep, .skip {{"
"text-align: center; "
"margin-top: {0:.2f}em; "
"margin-bottom: {1:.2f}em;"
"}}"
).format(
mScale, mScale
))
styles.append("a {color: rgb(66, 113, 174);}")
styles.append("mark {background: rgb(255, 255, 166);}")
styles.append(".keyword {color: rgb(245, 135, 31); font-weight: bold;}")
styles.append(".break {text-align: left;}")
styles.append(".synopsis {font-style: italic;}")
styles.append(".comment {font-style: italic; color: rgb(100, 100, 100);}")
styles.append(".dialog {color: rgb(66, 113, 174);}")
styles.append(".altdialog {color: rgb(129, 55, 9);}")
return styles
##
# Internal Functions
##
def _formatText(self, text: str, tFmt: T_Formats) -> str:
"""Apply formatting tags to text."""
temp = text
# Build a list of all html tags that need to be inserted in the text.
# This is done in the forward direction, and a tag is only opened if it
# isn't already open, and only closed if it has previously been opened.
tags: list[tuple[int, str]] = []
state = dict.fromkeys(HTML_OPENER, False)
for pos, fmt, data in tFmt:
if m := HTML_OPENER.get(fmt):
if not state.get(fmt, True):
tags.append((pos, m[1]))
state[fmt] = True
elif m := HTML_CLOSER.get(fmt):
if state.get(m[0], False):
tags.append((pos, m[1]))
state[m[0]] = False
elif fmt == self.FMT_FNOTE:
if data in self._footnotes:
index = len(self._usedNotes) + 1
self._usedNotes[data] = index
tags.append((pos, f"<sup><a href='#footnote_{index}'>{index}</a></sup>"))
else:
tags.append((pos, "<sup>ERR</sup>"))
# Check all format types and close any tag that is still open. This
# ensures that unclosed tags don't spill over to the next paragraph.
end = len(text)
for opener, active in state.items():
if active:
closer = HTML_OPENER.get(opener, HTML_NONE)[0]
tags.append((end, HTML_CLOSER.get(closer, HTML_NONE)[1]))
# Insert all tags at their correct position, starting from the back.
# The reverse order ensures that the positions are not shifted while we
# insert tags.
for pos, tag in reversed(tags):
temp = f"{temp[:pos]}{tag}{temp[pos:]}"
# Replace all line breaks with proper HTML break tags
temp = temp.replace("\n", "<br>")
return stripEscape(temp)
def _formatSynopsis(self, text: str, synopsis: bool) -> str:
"""Apply HTML formatting to synopsis."""
if synopsis:
sSynop = self._localLookup("Synopsis")
else:
sSynop = self._localLookup("Short Description")
return f"<p class='synopsis'><strong>{sSynop}:</strong> {text}</p>\n"
def _formatComments(self, text: str) -> str:
"""Apply HTML formatting to comments."""
sComm = self._localLookup("Comment")
return f"<p class='comment'><strong>{sComm}:</strong> {text}</p>\n"
def _formatKeywords(self, text: str) -> tuple[str, str]:
"""Apply HTML formatting to keywords."""
valid, bits, _ = self._project.index.scanThis("@"+text)
if not valid or not bits or bits[0] not in nwLabels.KEY_NAME:
return "", ""
result = f"<span class='keyword'>{self._localLookup(nwLabels.KEY_NAME[bits[0]])}:</span> "
if len(bits) > 1:
if bits[0] == nwKeyWords.TAG_KEY:
one, two = self._project.index.parseValue(bits[1])
result += f"<a class='tag' name='tag_{one}'>{one}</a>"
if two:
result += f" | <span class='optional'>{two}</a>"
else:
result += ", ".join(
f"<a class='tag' href='#tag_{t}'>{t}</a>" for t in bits[1:]
)
return bits[0][1:], result

View File

@@ -1,252 +0,0 @@
"""
novelWriter – Markdown Text Converter
=====================================
File History:
Created: 2021-02-06 [1.2b1] ToMarkdown
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import logging
from pathlib import Path
from novelwriter.constants import nwHeadFmt, nwLabels, nwUnicode
from novelwriter.core.project import NWProject
from novelwriter.core.tokenizer import T_Formats, Tokenizer
logger = logging.getLogger(__name__)
# Standard Markdown
STD_MD = {
Tokenizer.FMT_B_B: "**",
Tokenizer.FMT_B_E: "**",
Tokenizer.FMT_I_B: "_",
Tokenizer.FMT_I_E: "_",
Tokenizer.FMT_D_B: "",
Tokenizer.FMT_D_E: "",
Tokenizer.FMT_U_B: "",
Tokenizer.FMT_U_E: "",
Tokenizer.FMT_M_B: "",
Tokenizer.FMT_M_E: "",
Tokenizer.FMT_SUP_B: "",
Tokenizer.FMT_SUP_E: "",
Tokenizer.FMT_SUB_B: "",
Tokenizer.FMT_SUB_E: "",
Tokenizer.FMT_STRIP: "",
}
# Extended Markdown
EXT_MD = {
Tokenizer.FMT_B_B: "**",
Tokenizer.FMT_B_E: "**",
Tokenizer.FMT_I_B: "_",
Tokenizer.FMT_I_E: "_",
Tokenizer.FMT_D_B: "~~",
Tokenizer.FMT_D_E: "~~",
Tokenizer.FMT_U_B: "",
Tokenizer.FMT_U_E: "",
Tokenizer.FMT_M_B: "==",
Tokenizer.FMT_M_E: "==",
Tokenizer.FMT_SUP_B: "^",
Tokenizer.FMT_SUP_E: "^",
Tokenizer.FMT_SUB_B: "~",
Tokenizer.FMT_SUB_E: "~",
Tokenizer.FMT_STRIP: "",
}
class ToMarkdown(Tokenizer):
"""Core: Markdown Document Writer
Extend the Tokenizer class to writer Markdown output. It supports
both Standard Markdown and Extended Markdown. The class also
supports concatenating novelWriter markup files.
"""
def __init__(self, project: NWProject) -> None:
super().__init__(project)
self._fullMD: list[str] = []
self._usedNotes: dict[str, int] = {}
self._extended = True
return
##
# Properties
##
@property
def fullMD(self) -> list[str]:
"""Return the markdown as a list."""
return self._fullMD
##
# Setters
##
def setExtendedMarkdown(self, state: bool) -> None:
"""Set the converter to use Extended Markdown formatting."""
self._extended = state
return
##
# Class Methods
##
def getFullResultSize(self) -> int:
"""Return the size of the full Markdown result."""
return sum(len(x) for x in self._fullMD)
def doConvert(self) -> None:
"""Convert the list of text tokens into a Markdown document."""
self._result = ""
if self._extended:
mTags = EXT_MD
cSkip = nwUnicode.U_MMSP
else:
mTags = STD_MD
cSkip = ""
lines = []
for tType, _, tText, tFormat, tStyle in self._tokens:
if tType == self.T_TEXT:
tTemp = self._formatText(tText, tFormat, mTags).replace("\n", " \n")
lines.append(f"{tTemp}\n\n")
elif tType == self.T_TITLE:
tHead = tText.replace(nwHeadFmt.BR, "\n")
lines.append(f"# {tHead}\n\n")
elif tType == self.T_HEAD1:
tHead = tText.replace(nwHeadFmt.BR, "\n")
lines.append(f"# {tHead}\n\n")
elif tType == self.T_HEAD2:
tHead = tText.replace(nwHeadFmt.BR, "\n")
lines.append(f"## {tHead}\n\n")
elif tType == self.T_HEAD3:
tHead = tText.replace(nwHeadFmt.BR, "\n")
lines.append(f"### {tHead}\n\n")
elif tType == self.T_HEAD4:
tHead = tText.replace(nwHeadFmt.BR, "\n")
lines.append(f"#### {tHead}\n\n")
elif tType == self.T_SEP:
lines.append(f"{tText}\n\n")
elif tType == self.T_SKIP:
lines.append(f"{cSkip}\n\n")
elif tType == self.T_SYNOPSIS and self._doSynopsis:
label = self._localLookup("Synopsis")
lines.append(f"**{label}:** {self._formatText(tText, tFormat, mTags)}\n\n")
elif tType == self.T_SHORT and self._doSynopsis:
label = self._localLookup("Short Description")
lines.append(f"**{label}:** {self._formatText(tText, tFormat, mTags)}\n\n")
elif tType == self.T_COMMENT and self._doComments:
label = self._localLookup("Comment")
lines.append(f"**{label}:** {self._formatText(tText, tFormat, mTags)}\n\n")
elif tType == self.T_KEYWORD and self._doKeywords:
lines.append(self._formatKeywords(tText, tStyle))
self._result = "".join(lines)
self._fullMD.append(self._result)
return
def appendFootnotes(self) -> None:
"""Append the footnotes in the buffer."""
if self._usedNotes:
tags = EXT_MD if self._extended else STD_MD
footnotes = self._localLookup("Footnotes")
lines = []
lines.append(f"### {footnotes}\n\n")
for key, index in self._usedNotes.items():
if content := self._footnotes.get(key):
marker = f"{index}. "
text = self._formatText(*content, tags)
lines.append(f"{marker}{text}\n")
lines.append("\n")
result = "".join(lines)
self._result += result
self._fullMD.append(result)
return
def saveMarkdown(self, path: str | Path) -> None:
"""Save the data to a plain text file."""
with open(path, mode="w", encoding="utf-8") as outFile:
outFile.write("".join(self._fullMD))
logger.info("Wrote file: %s", path)
return
def replaceTabs(self, nSpaces: int = 8, spaceChar: str = " ") -> None:
"""Replace tabs with spaces."""
spaces = spaceChar*nSpaces
self._fullMD = [p.replace("\t", spaces) for p in self._fullMD]
if self._keepMD:
self._markdown = [p.replace("\t", spaces) for p in self._markdown]
return
##
# Internal Functions
##
def _formatText(self, text: str, tFmt: T_Formats, tags: dict[int, str]) -> str:
"""Apply formatting tags to text."""
temp = text
for pos, fmt, data in reversed(tFmt):
md = ""
if fmt == self.FMT_FNOTE:
if data in self._footnotes:
index = len(self._usedNotes) + 1
self._usedNotes[data] = index
md = f"[{index}]"
else:
md = "[ERR]"
else:
md = tags.get(fmt, "")
temp = f"{temp[:pos]}{md}{temp[pos:]}"
return temp
def _formatKeywords(self, text: str, style: int) -> str:
"""Apply Markdown formatting to keywords."""
valid, bits, _ = self._project.index.scanThis("@"+text)
if not valid or not bits:
return ""
result = ""
if bits[0] in nwLabels.KEY_NAME:
result += f"**{self._localLookup(nwLabels.KEY_NAME[bits[0]])}:** "
if len(bits) > 1:
result += ", ".join(bits[1:])
result += " \n" if style & self.A_Z_BTMMRG else "\n\n"
return result

View File

@@ -1,419 +0,0 @@
"""
novelWriter – QTextDocument Converter
=====================================
File History:
Created: 2024-05-21 [2.5b1] ToQTextDocument
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import logging
from PyQt5.QtGui import (
QColor, QFont, QFontMetricsF, QTextBlockFormat, QTextCharFormat,
QTextCursor, QTextDocument
)
from novelwriter.constants import nwHeaders, nwHeadFmt, nwKeyWords, nwLabels, nwUnicode
from novelwriter.core.project import NWProject
from novelwriter.core.tokenizer import T_Formats, Tokenizer
from novelwriter.types import (
QtAlignAbsolute, QtAlignCenter, QtAlignJustify, QtAlignLeft, QtAlignRight,
QtBlack, QtPageBreakAfter, QtPageBreakBefore, QtTransparent,
QtVAlignNormal, QtVAlignSub, QtVAlignSuper
)
logger = logging.getLogger(__name__)
T_TextStyle = tuple[QTextBlockFormat, QTextCharFormat]
class TextDocumentTheme:
text: QColor = QtBlack
highlight: QColor = QtTransparent
head: QColor = QtBlack
comment: QColor = QtBlack
note: QColor = QtBlack
code: QColor = QtBlack
modifier: QColor = QtBlack
keyword: QColor = QtBlack
tag: QColor = QtBlack
optional: QColor = QtBlack
dialog: QColor = QtBlack
altdialog: QColor = QtBlack
def newBlock(cursor: QTextCursor, bFmt: QTextBlockFormat) -> None:
if cursor.position() > 0:
cursor.insertBlock(bFmt)
else:
cursor.setBlockFormat(bFmt)
class ToQTextDocument(Tokenizer):
"""Core: QTextDocument Writer
Extend the Tokenizer class to generate a QTextDocument output. This
is intended for usage in the document viewer and build tool preview.
"""
def __init__(self, project: NWProject) -> None:
super().__init__(project)
self._document = QTextDocument()
self._document.setUndoRedoEnabled(False)
self._document.setDocumentMargin(0)
self._theme = TextDocumentTheme()
self._styles: dict[int, T_TextStyle] = {}
self._usedNotes: dict[str, int] = {}
self._init = False
self._bold = QFont.Weight.Bold
self._normal = QFont.Weight.Normal
return
def initDocument(self, font: QFont, theme: TextDocumentTheme) -> None:
"""Initialise all computed values of the document."""
self._textFont = font
self._theme = theme
self._document.setUndoRedoEnabled(False)
self._document.blockSignals(True)
self._document.clear()
self._document.setDefaultFont(self._textFont)
qMetric = QFontMetricsF(self._textFont)
mPx = qMetric.ascent() # 1 em in pixels
fPt = self._textFont.pointSizeF()
# Scaled Sizes
# ============
self._mHead = {
self.T_TITLE: (mPx * self._marginTitle[0], mPx * self._marginTitle[1]),
self.T_HEAD1: (mPx * self._marginHead1[0], mPx * self._marginHead1[1]),
self.T_HEAD2: (mPx * self._marginHead2[0], mPx * self._marginHead2[1]),
self.T_HEAD3: (mPx * self._marginHead3[0], mPx * self._marginHead3[1]),
self.T_HEAD4: (mPx * self._marginHead4[0], mPx * self._marginHead4[1]),
}
self._sHead = {
self.T_TITLE: nwHeaders.H_SIZES.get(0, 1.0) * fPt,
self.T_HEAD1: nwHeaders.H_SIZES.get(1, 1.0) * fPt,
self.T_HEAD2: nwHeaders.H_SIZES.get(2, 1.0) * fPt,
self.T_HEAD3: nwHeaders.H_SIZES.get(3, 1.0) * fPt,
self.T_HEAD4: nwHeaders.H_SIZES.get(4, 1.0) * fPt,
}
self._mText = (mPx * self._marginText[0], mPx * self._marginText[1])
self._mMeta = (mPx * self._marginMeta[0], mPx * self._marginMeta[1])
self._mSep = (mPx * self._marginSep[0], mPx * self._marginSep[1])
self._mIndent = mPx * 2.0
self._tIndent = mPx * self._firstWidth
# Block Format
# ============
self._blockFmt = QTextBlockFormat()
self._blockFmt.setTopMargin(self._mText[0])
self._blockFmt.setBottomMargin(self._mText[1])
self._blockFmt.setAlignment(QtAlignJustify if self._doJustify else QtAlignAbsolute)
self._blockFmt.setLineHeight(
100*self._lineHeight, QTextBlockFormat.LineHeightTypes.ProportionalHeight
)
# Character Formats
# =================
self._cText = QTextCharFormat()
self._cText.setBackground(QtTransparent)
self._cText.setForeground(self._theme.text)
self._cHead = QTextCharFormat(self._cText)
self._cHead.setForeground(self._theme.head)
self._cComment = QTextCharFormat(self._cText)
self._cComment.setForeground(self._theme.comment)
self._cCommentMod = QTextCharFormat(self._cText)
self._cCommentMod.setForeground(self._theme.comment)
self._cCommentMod.setFontWeight(self._bold)
self._cNote = QTextCharFormat(self._cText)
self._cNote.setForeground(self._theme.note)
self._cCode = QTextCharFormat(self._cText)
self._cCode.setForeground(self._theme.code)
self._cModifier = QTextCharFormat(self._cText)
self._cModifier.setForeground(self._theme.modifier)
self._cModifier.setFontWeight(self._bold)
self._cKeyword = QTextCharFormat(self._cText)
self._cKeyword.setForeground(self._theme.keyword)
self._cTag = QTextCharFormat(self._cText)
self._cTag.setForeground(self._theme.tag)
self._cOptional = QTextCharFormat(self._cText)
self._cOptional.setForeground(self._theme.optional)
self._init = True
return
##
# Properties
##
@property
def document(self) -> QTextDocument:
"""Return the document."""
return self._document
##
# Class Methods
##
def doConvert(self) -> None:
"""Write text tokens into the document."""
if not self._init:
return
self._document.blockSignals(True)
cursor = QTextCursor(self._document)
cursor.movePosition(QTextCursor.MoveOperation.End)
for tType, nHead, tText, tFormat, tStyle in self._tokens:
# Styles
bFmt = QTextBlockFormat(self._blockFmt)
if tStyle is not None:
if tStyle & self.A_LEFT:
bFmt.setAlignment(QtAlignLeft)
elif tStyle & self.A_RIGHT:
bFmt.setAlignment(QtAlignRight)
elif tStyle & self.A_CENTRE:
bFmt.setAlignment(QtAlignCenter)
elif tStyle & self.A_JUSTIFY:
bFmt.setAlignment(QtAlignJustify)
if tStyle & self.A_PBB:
bFmt.setPageBreakPolicy(QtPageBreakBefore)
if tStyle & self.A_PBA:
bFmt.setPageBreakPolicy(QtPageBreakAfter)
if tStyle & self.A_Z_BTMMRG:
bFmt.setBottomMargin(0.0)
if tStyle & self.A_Z_TOPMRG:
bFmt.setTopMargin(0.0)
if tStyle & self.A_IND_L:
bFmt.setLeftMargin(self._mIndent)
if tStyle & self.A_IND_R:
bFmt.setRightMargin(self._mIndent)
if tStyle & self.A_IND_T:
bFmt.setTextIndent(self._tIndent)
if tType == self.T_TEXT:
newBlock(cursor, bFmt)
self._insertFragments(tText, tFormat, cursor, self._cText)
elif tType in self.L_HEADINGS:
bFmt, cFmt = self._genHeadStyle(tType, nHead, bFmt)
newBlock(cursor, bFmt)
cursor.insertText(tText.replace(nwHeadFmt.BR, "\n"), cFmt)
elif tType == self.T_SEP:
sFmt = QTextBlockFormat(bFmt)
sFmt.setTopMargin(self._mSep[0])
sFmt.setBottomMargin(self._mSep[1])
newBlock(cursor, sFmt)
cursor.insertText(tText, self._cText)
elif tType == self.T_SKIP:
newBlock(cursor, bFmt)
cursor.insertText(nwUnicode.U_NBSP, self._cText)
elif tType in self.L_SUMMARY and self._doSynopsis:
newBlock(cursor, bFmt)
modifier = self._localLookup(
"Short Description" if tType == self.T_SHORT else "Synopsis"
)
cursor.insertText(f"{modifier}: ", self._cModifier)
self._insertFragments(tText, tFormat, cursor, self._cNote)
elif tType == self.T_COMMENT and self._doComments:
newBlock(cursor, bFmt)
modifier = self._localLookup("Comment")
cursor.insertText(f"{modifier}: ", self._cCommentMod)
self._insertFragments(tText, tFormat, cursor, self._cComment)
elif tType == self.T_KEYWORD and self._doKeywords:
newBlock(cursor, bFmt)
self._insertKeywords(tText, cursor)
self._document.blockSignals(False)
return
def appendFootnotes(self) -> None:
"""Append the footnotes in the buffer."""
if self._usedNotes:
self._document.blockSignals(True)
cursor = QTextCursor(self._document)
cursor.movePosition(QTextCursor.MoveOperation.End)
bFmt, cFmt = self._genHeadStyle(self.T_HEAD4, -1, self._blockFmt)
newBlock(cursor, bFmt)
cursor.insertText(self._localLookup("Footnotes"), cFmt)
for key, index in self._usedNotes.items():
if content := self._footnotes.get(key):
cFmt = QTextCharFormat(self._cCode)
cFmt.setAnchor(True)
cFmt.setAnchorNames([f"footnote_{index}"])
newBlock(cursor, self._blockFmt)
cursor.insertText(f"{index}. ", cFmt)
self._insertFragments(*content, cursor, self._cText)
self._document.blockSignals(False)
return
##
# Internal Functions
##
def _insertFragments(
self, text: str, tFmt: T_Formats, cursor: QTextCursor, dFmt: QTextCharFormat
) -> None:
"""Apply formatting tags to text."""
cFmt = QTextCharFormat(dFmt)
start = 0
temp = text.replace("\n", nwUnicode.U_LSEP)
for pos, fmt, data in tFmt:
# Insert buffer with previous format
cursor.insertText(temp[start:pos], cFmt)
# Construct next format
if fmt == self.FMT_B_B:
cFmt.setFontWeight(self._bold)
elif fmt == self.FMT_B_E:
cFmt.setFontWeight(self._normal)
elif fmt == self.FMT_I_B:
cFmt.setFontItalic(True)
elif fmt == self.FMT_I_E:
cFmt.setFontItalic(False)
elif fmt == self.FMT_D_B:
cFmt.setFontStrikeOut(True)
elif fmt == self.FMT_D_E:
cFmt.setFontStrikeOut(False)
elif fmt == self.FMT_U_B:
cFmt.setFontUnderline(True)
elif fmt == self.FMT_U_E:
cFmt.setFontUnderline(False)
elif fmt == self.FMT_M_B:
cFmt.setBackground(self._theme.highlight)
elif fmt == self.FMT_M_E:
cFmt.setBackground(QtTransparent)
elif fmt == self.FMT_SUP_B:
cFmt.setVerticalAlignment(QtVAlignSuper)
elif fmt == self.FMT_SUP_E:
cFmt.setVerticalAlignment(QtVAlignNormal)
elif fmt == self.FMT_SUB_B:
cFmt.setVerticalAlignment(QtVAlignSub)
elif fmt == self.FMT_SUB_E:
cFmt.setVerticalAlignment(QtVAlignNormal)
elif fmt == self.FMT_DL_B:
cFmt.setForeground(self._theme.dialog)
elif fmt == self.FMT_DL_E:
cFmt.setForeground(self._theme.text)
elif fmt == self.FMT_ADL_B:
cFmt.setForeground(self._theme.altdialog)
elif fmt == self.FMT_ADL_E:
cFmt.setForeground(self._theme.text)
elif fmt == self.FMT_FNOTE:
xFmt = QTextCharFormat(self._cCode)
xFmt.setVerticalAlignment(QtVAlignSuper)
if data in self._footnotes:
index = len(self._usedNotes) + 1
self._usedNotes[data] = index
xFmt.setAnchor(True)
xFmt.setAnchorHref(f"#footnote_{index}")
xFmt.setFontUnderline(True)
cursor.insertText(f"[{index}]", xFmt)
else:
cursor.insertText("[ERR]", cFmt)
# Move pos for next pass
start = pos
# Insert whatever is left in the buffer
cursor.insertText(temp[start:], cFmt)
return
def _insertKeywords(self, text: str, cursor: QTextCursor) -> None:
"""Apply Markdown formatting to keywords."""
valid, bits, _ = self._project.index.scanThis("@"+text)
if valid and bits:
key = f"{self._localLookup(nwLabels.KEY_NAME[bits[0]])}: "
cursor.insertText(key, self._cKeyword)
if (num := len(bits)) > 1:
if bits[0] == nwKeyWords.TAG_KEY:
one, two = self._project.index.parseValue(bits[1])
cFmt = QTextCharFormat(self._cTag)
cFmt.setAnchor(True)
cFmt.setAnchorNames([f"tag_{one}".lower()])
cursor.insertText(one, cFmt)
if two:
cursor.insertText(" | ", self._cText)
cursor.insertText(two, self._cOptional)
else:
for n, bit in enumerate(bits[1:], 2):
cFmt = QTextCharFormat(self._cTag)
cFmt.setFontUnderline(True)
cFmt.setAnchor(True)
cFmt.setAnchorHref(f"#tag_{bit}".lower())
cursor.insertText(bit, cFmt)
if n < num:
cursor.insertText(", ", self._cText)
return
def _genHeadStyle(self, hType: int, nHead: int, rFmt: QTextBlockFormat) -> T_TextStyle:
"""Generate a heading style set."""
mTop, mBottom = self._mHead.get(hType, (0.0, 0.0))
bFmt = QTextBlockFormat(rFmt)
bFmt.setTopMargin(mTop)
bFmt.setBottomMargin(mBottom)
cFmt = QTextCharFormat(self._cText if hType == self.T_TITLE else self._cHead)
cFmt.setFontWeight(self._bold)
cFmt.setFontPointSize(self._sHead.get(hType, 1.0))
if nHead >= 0:
cFmt.setAnchorNames([f"{self._handle}:T{nHead:04d}"])
cFmt.setAnchor(True)
return bFmt, cFmt

View File

@@ -3,10 +3,11 @@ novelWriter – Project Tree Class
================================
File History:
Created: 2020-05-07 [0.4.5] NWTree
Created: 2020-05-07 [0.4.5] NWTree
Rewritten: 2024-11-16 [2.6b2] NWTree
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
Copyright (C) 2020 Veronica Berglyd Olsen and novelWriter contributors
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
@@ -30,10 +31,13 @@ from collections.abc import Iterable, Iterator
from pathlib import Path
from typing import TYPE_CHECKING, Literal, overload
from novelwriter.common import isHandle
from novelwriter.constants import nwFiles
from PyQt5.QtCore import QModelIndex
from novelwriter import SHARED
from novelwriter.constants import nwFiles, nwLabels, trConst
from novelwriter.core.item import NWItem
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
from novelwriter.core.itemmodel import ProjectModel, ProjectNode
from novelwriter.enum import nwChange, nwItemClass, nwItemLayout, nwItemType
from novelwriter.error import logException
if TYPE_CHECKING: # pragma: no cover
@@ -41,7 +45,7 @@ if TYPE_CHECKING: # pragma: no cover
logger = logging.getLogger(__name__)
MAX_DEPTH = 1000 # Cap of tree traversing for loops (recursion limit)
MAX_DEPTH = 999 # Cap of tree traversing for loops (recursion limit)
class NWTree:
@@ -51,30 +55,51 @@ class NWTree:
This class holds all the project items of the project as instances
of NWItem.
For historical reasons, the order of the items is saved in a
separate list from the items themselves, which are stored in a
dictionary. This is somewhat redundant with the newer versions of
Python, but is still practical as it's easier to update the item
order as a list.
Each item has a handle, which is a random hex string of length 13.
The handle is the name of the item everywhere in novelWriter, and is
also used for file names.
"""
__slots__ = ("_project", "_tree", "_order", "_roots", "_trash", "_changed")
__slots__ = ("_project", "_model", "_items", "_nodes", "_trash")
def __init__(self, project: NWProject) -> None:
self._project = project
self._model = ProjectModel(self)
self._items: dict[str, NWItem] = {}
self._nodes: dict[str, ProjectNode] = {}
self._trash = None
logger.debug("Ready: NWTree")
return
self._tree: dict[str, NWItem] = {} # Holds all the items of the project
self._order: list[str] = [] # The order of the tree items in the tree view
self._roots: dict[str, NWItem] = {} # The root items of the tree
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: NWTree")
return
self._trash = None # The handle of the trash root folder
self._changed = False # True if tree structure has changed
def __len__(self) -> int:
"""The number of items in the project."""
return len(self._items)
def __bool__(self) -> bool:
"""True if there are any items in the project."""
return bool(self._items)
def __getitem__(self, tHandle: str | None) -> NWItem | None:
"""Return a project item based on its handle. Returns None if
the handle doesn't exist in the project.
"""
if tHandle and tHandle in self._items:
return self._items[tHandle]
logger.error("No tree item with handle '%s'", str(tHandle))
return None
def __contains__(self, tHandle: str) -> bool:
"""Checks if a handle exists in the tree."""
return tHandle in self._items
def __iter__(self) -> Iterator[NWItem]:
"""Iterate through project items."""
for node in self._model.root.allChildren():
yield node.item
return
##
@@ -82,9 +107,19 @@ class NWTree:
##
@property
def trashRoot(self) -> str | None:
"""Return the handle of the trash folder, or None."""
return self._trash
def trash(self) -> ProjectNode | None:
"""Return trash node, if it exists."""
if self._trash:
return self._trash
return self._getTrashNode()
@property
def model(self) -> ProjectModel:
return self._model
@property
def nodes(self) -> dict[str, ProjectNode]:
return self._nodes
##
# Class Methods
@@ -92,83 +127,91 @@ class NWTree:
def clear(self) -> None:
"""Clear the item tree entirely."""
self._tree = {}
self._order = []
self._roots = {}
self._trash = None
self._changed = False
oldModel = self._model
oldModel.clear()
self._model = ProjectModel(self)
self._items.clear()
self._nodes.clear()
self._trash = None
oldModel.deleteLater()
del oldModel
return
def handles(self) -> list[str]:
"""Returns a copy of the list of all the active handles."""
return self._order.copy()
def add(self, item: NWItem, pos: int = -1) -> bool:
"""Add a project item into the project tree."""
if pHandle := item.itemParent:
if parent := self._nodes.get(pHandle):
node = ProjectNode(item)
index = self._model.indexFromNode(parent)
self._model.insertChild(node, index, pos)
self._nodes[item.itemHandle] = node
self._items[item.itemHandle] = item
self._itemChange(item, nwChange.CREATE)
else:
logger.error("Could not locate parent of '%s'", item.itemHandle)
return False
elif item.isRootType():
node = ProjectNode(item)
self._model.insertChild(node, QModelIndex(), pos)
self._nodes[item.itemHandle] = node
self._items[item.itemHandle] = item
self._itemChange(item, nwChange.CREATE)
else:
logger.error("Invalid project item '%s'", item.itemHandle)
return False
return True
def remove(self, tHandle: str) -> bool:
"""Remove an item from the project tree."""
if (node := self._nodes.get(tHandle)) and tHandle in self._items:
index = self._model.indexFromNode(node)
if index.isValid() and self._model.removeChild(index.parent(), index.row()):
self._itemChange(node.item, nwChange.DELETE)
del self._nodes[tHandle]
del self._items[tHandle]
return True
return False
@overload # pragma: no cover
def create(self, label: str, parent: None, itemType: Literal[nwItemType.ROOT],
itemClass: nwItemClass) -> str:
def create(
self, label: str, parent: None, itemType: Literal[nwItemType.ROOT],
itemClass: nwItemClass, pos: int = -1
) -> str:
pass
@overload # pragma: no cover
def create(self, label: str, parent: str | None, itemType: nwItemType,
itemClass: nwItemClass = nwItemClass.NO_CLASS) -> str | None:
def create(
self, label: str, parent: str | None, itemType: nwItemType,
itemClass: nwItemClass = nwItemClass.NO_CLASS, pos: int = -1
) -> str | None:
pass
def create(self, label, parent, itemType, itemClass=nwItemClass.NO_CLASS):
def create(
self, label: str, parent: str | None, itemType: nwItemType,
itemClass: nwItemClass = nwItemClass.NO_CLASS, pos: int = -1,
) -> str | None:
"""Create a new item in the project tree, and return its handle.
If the item cannot be added to the project because of an invalid
parent, None is returned. For root elements, this cannot occur.
"""
parent = None if itemType == nwItemType.ROOT else parent
if parent is None or parent in self._order:
if parent is None or parent in self._nodes:
tHandle = self._makeHandle()
newItem = NWItem(self._project, tHandle)
newItem.setName(label)
newItem.setParent(parent)
newItem.setType(itemType)
newItem.setClass(itemClass)
self.append(newItem)
self.updateItemData(tHandle)
return tHandle
nwItem = NWItem(self._project, tHandle)
nwItem.setName(label)
nwItem.setParent(parent)
nwItem.setType(itemType)
nwItem.setClass(itemClass)
if self.add(nwItem, pos):
return tHandle
return None
def append(self, nwItem: NWItem) -> bool:
"""Add a new item to the end of the tree."""
tHandle = nwItem.itemHandle
pHandle = nwItem.itemParent
if not isHandle(tHandle):
logger.warning("Invalid item handle '%s' detected, skipping", tHandle)
return False
if tHandle in self._tree:
logger.warning("Duplicate handle '%s' detected, skipping", tHandle)
return False
logger.debug("Adding item '%s' with parent '%s'", str(tHandle), str(pHandle))
if nwItem.isRootType():
logger.debug("Item '%s' is a root item", str(tHandle))
self._roots[tHandle] = nwItem
if nwItem.itemClass == nwItemClass.TRASH:
if self._trash is None:
logger.debug("Item '%s' is the trash folder", str(tHandle))
self._trash = tHandle
else:
logger.error("Only one trash folder allowed")
return False
self._tree[tHandle] = nwItem
self._order.append(tHandle)
self._setTreeChanged(True)
return True
def duplicate(self, sHandle: str) -> NWItem | None:
def duplicate(self, sHandle: str, pHandle: str | None, putAfter: bool) -> NWItem | None:
"""Duplicate an item and set a new handle."""
sItem = self.__getitem__(sHandle)
if isinstance(sItem, NWItem):
nItem = NWItem.duplicate(sItem, self._makeHandle())
if self.append(nItem):
if sNode := self._nodes.get(sHandle):
nItem = NWItem.duplicate(sNode.item, self._makeHandle())
nItem.setParent(pHandle)
if self.add(nItem, (sNode.row() + 1) if putAfter else -1):
logger.info("Duplicated item '%s' -> '%s'", sHandle, nItem.itemHandle)
return nItem
return None
@@ -177,23 +220,62 @@ class NWTree:
"""Pack the content of the tree into a list of dictionaries of
items. In the order defined by the _treeOrder list.
"""
tree = []
for tHandle in self._order:
tItem = self.__getitem__(tHandle)
if tItem:
tree.append(tItem.pack())
return tree
nodes = self._model.root.allChildren()
if len(nodes) != len(self._nodes):
logger.warning(
"Model tree is inconsitent with nodes map, %d != %d",
len(nodes), len(self._nodes)
)
return [node.item.pack() for node in nodes]
def unpack(self, data: list[dict]) -> None:
"""Iterate through all items of a list and add them to the
project tree.
"""
self.clear()
items: dict[str, NWItem] = self._items.copy()
for item in data:
nwItem = NWItem(self._project, "") # Handle is set by unpack()
nwItem = NWItem(self._project, "")
if nwItem.unpack(item):
self.append(nwItem)
nwItem.saveInitialCount()
items[nwItem.itemHandle] = nwItem
later = items
self._model.beginInsertRows(self._model.index(0, 0), 0, 0)
for _ in range(MAX_DEPTH):
later = self._addItems(later)
if len(later) == 0:
break
else:
logger.error("Not all items could be added to project tree")
self._trash = self._getTrashNode()
self._model.endInsertRows()
self._model.layoutChanged.emit()
return
def refreshItems(self, items: list[str]) -> None:
"""Refresh these items on the GUI. If they are an ordered range,
also set the isRange flag to True.
"""
for tHandle in items:
if node := self._nodes.get(tHandle):
node.refresh()
node.updateCount()
indexS = self._model.indexFromNode(node, 0)
indexE = self._model.indexFromNode(node, 3)
self._model.dataChanged.emit(indexS, indexE)
self._itemChange(node.item, nwChange.UPDATE)
return
def refreshAllItems(self) -> None:
"""Refresh all items in the tree."""
for node in reversed(self._model.root.allChildren()):
node.refresh()
node.updateCount(propagate=False)
self._model.root.refresh()
self._model.root.updateCount(propagate=False)
self._model.layoutChanged.emit()
return
def checkConsistency(self, prefix: str) -> tuple[int, int]:
@@ -205,29 +287,21 @@ class NWTree:
mark recovered files.
"""
storage = self._project.storage
files = set(storage.scanContent())
for tHandle in self._order:
if self.updateItemData(tHandle):
logger.debug("Checking item '%s' ... OK", tHandle)
files.discard(tHandle) # Remove it from the record
else:
logger.error("Checking item '%s' ... ERROR", tHandle)
self.__delitem__(tHandle) # The file will be re-added as orphaned
orphans = len(files)
remains = set(storage.scanContent()).difference(set(self._nodes.keys()))
orphans = len(remains)
if orphans == 0:
logger.info("Checked project files: OK")
return 0, 0
logger.warning("Found %d file(s) not tracked in project", orphans)
recovered = 0
for cHandle in files:
for cHandle in remains:
aDoc = storage.getDocument(cHandle)
aDoc.readDocument(isOrphan=True)
oName, oParent, oClass, oLayout = aDoc.getMeta()
oName = oName or cHandle
oParent = oParent if oParent in self._order else None
oParent = oParent if oParent in self._nodes else None
oClass = oClass or nwItemClass.NOVEL
oLayout = oLayout or nwItemLayout.NOTE
@@ -248,8 +322,7 @@ class NWTree:
newItem.setType(nwItemType.FILE)
newItem.setClass(oClass)
newItem.setLayout(oLayout)
if self.append(newItem):
self.updateItemData(cHandle)
if self.add(newItem):
recovered += 1
return orphans, recovered
@@ -263,38 +336,33 @@ class NWTree:
if not (isinstance(contentPath, Path) and isinstance(runtimePath, Path)):
return False
tocList = []
tocLen = 0
for tHandle in self._order:
tItem = self.__getitem__(tHandle)
if tItem is None:
continue
tFile = tHandle+".nwd"
if (contentPath / tFile).is_file():
entries = []
maxLen = 0
for node in self._model.root.allChildren():
item = node.item
file = f"{item.itemHandle}.nwd"
if (contentPath / file).is_file():
tocLine = "{0:<25s} {1:<9s} {2:<8s} {3:s}".format(
str(Path("content") / tFile),
tItem.itemClass.name,
tItem.itemLayout.name,
tItem.itemName,
f"content/{file}",
item.itemClass.name,
item.itemLayout.name,
item.itemName,
)
tocList.append(tocLine)
tocLen = max(tocLen, len(tocLine))
entries.append(tocLine)
maxLen = max(maxLen, len(tocLine))
try:
# Dump the text
tocText = runtimePath / nwFiles.TOC_TXT
with open(tocText, mode="w", encoding="utf-8") as outFile:
outFile.write("\n")
outFile.write("Table of Contents\n")
outFile.write("=================\n")
outFile.write("\n")
outFile.write("{0:<25s} {1:<9s} {2:<8s} {3:s}\n".format(
with open(runtimePath / nwFiles.TOC_TXT, mode="w", encoding="utf-8") as toc:
toc.write("\n")
toc.write("Table of Contents\n")
toc.write("=================\n")
toc.write("\n")
toc.write("{0:<25s} {1:<9s} {2:<8s} {3:s}\n".format(
"File Name", "Class", "Layout", "Document Label"
))
outFile.write("-"*max(tocLen, 62) + "\n")
outFile.write("\n".join(tocList))
outFile.write("\n")
toc.write("-"*max(maxLen, 62) + "\n")
toc.write("\n".join(entries))
toc.write("\n")
except Exception:
logger.error("Could not write ToC file")
@@ -307,74 +375,46 @@ class NWTree:
"""Loop over all entries and add up the word counts."""
noteWords = 0
novelWords = 0
for tHandle in self._order:
tItem = self.__getitem__(tHandle)
if tItem is None:
continue
if tItem.itemLayout == nwItemLayout.NO_LAYOUT:
pass
elif tItem.itemLayout == nwItemLayout.NOTE:
noteWords += tItem.wordCount
else:
novelWords += tItem.wordCount
for item in self._items.values():
if item.itemLayout == nwItemLayout.NOTE:
noteWords += item.wordCount
elif item.itemLayout == nwItemLayout.DOCUMENT:
novelWords += item.wordCount
return novelWords, noteWords
##
# Tree Item Methods
##
def updateItemData(self, tHandle: str) -> bool:
"""Update the root item handle of a given item. Returns True if
a root was found and data updated, otherwise False.
"""
tItem = self.__getitem__(tHandle)
if tItem is None:
return False
iItem = tItem
for _ in range(MAX_DEPTH):
if iItem.itemParent is None:
tItem.setRoot(iItem.itemHandle)
tItem.setClassDefaults(iItem.itemClass)
return True
else:
iItem = self.__getitem__(iItem.itemParent)
if iItem is None:
return False
else:
raise RecursionError("Critical internal error")
def checkType(self, tHandle: str, itemType: nwItemType) -> bool:
"""Check if item exists and is of the specified item type."""
tItem = self.__getitem__(tHandle)
if not tItem:
return False
return tItem.itemType == itemType
if tItem := self.__getitem__(tHandle):
return tItem.itemType == itemType
return False
def getItemPath(self, tHandle: str, asName: bool = False) -> list[str]:
def itemPath(self, tHandle: str, asName: bool = False) -> list[str]:
"""Iterate upwards in the tree until we find the item with
parent None, the root item, and return the list of handles, or
alternatively item names. We do this with a for loop with a
maximum depth to make infinite loops impossible.
"""
tTree = []
tItem = self.__getitem__(tHandle)
if tItem is not None:
tTree.append(tItem.itemName if asName else tHandle)
path = []
if node := self._nodes.get(tHandle):
for _ in range(MAX_DEPTH):
if tItem.itemParent is None:
return tTree
if parent := node.parent():
path.append(node.item.itemName if asName else tHandle)
node = parent
else:
tHandle = tItem.itemParent
tItem = self.__getitem__(tHandle)
if tItem is None:
return tTree
else:
tTree.append(tItem.itemName if asName else tHandle)
return path
else:
raise RecursionError("Critical internal error")
logger.error("Max project tree depth reached")
return path
return tTree
def subTree(self, tHandle: str) -> list[str]:
"""Get the subtree from a given handle."""
if node := self._nodes.get(tHandle):
return [child.item.itemHandle for child in node.allChildren()]
return []
##
# Tree Root Methods
@@ -383,139 +423,80 @@ class NWTree:
def rootClasses(self) -> set[nwItemClass]:
"""Return a set of all root classes in use by the project."""
rootClasses = set()
for nwItem in self._roots.values():
rootClasses.add(nwItem.itemClass)
for node in self._model.root.children:
rootClasses.add(node.item.itemClass)
return rootClasses
def iterRoots(self, itemClass: nwItemClass | None) -> Iterable[tuple[str, NWItem]]:
"""Iterate over all root items of a given class in order."""
for tHandle in self._order:
nwItem = self.__getitem__(tHandle)
if isinstance(nwItem, NWItem) and nwItem.isRootType():
if itemClass is None or nwItem.itemClass == itemClass:
yield tHandle, nwItem
for node in self._model.root.children:
if node.item.isRootType():
if itemClass is None or node.item.itemClass == itemClass:
yield node.item.itemHandle, node.item
return
def isTrash(self, tHandle: str) -> bool:
"""Check if an item is in or is the trash folder."""
tItem = self.__getitem__(tHandle)
if tItem is None:
return True
if tItem.itemClass == nwItemClass.TRASH:
return True
if self._trash is not None:
if tHandle == self._trash:
return True
elif tItem.itemParent == self._trash:
return True
elif tItem.itemRoot == self._trash:
return True
return False
def findRoot(self, itemClass: nwItemClass | None) -> str | None:
"""Find the first root item for a given class."""
for aRoot in self._roots:
tItem = self.__getitem__(aRoot)
if tItem is None:
continue
if itemClass == tItem.itemClass:
return tItem.itemHandle
for node in self._model.root.children:
if node.item.itemClass == itemClass:
return node.item.itemHandle
return None
##
# Setters
##
def setOrder(self, newOrder: list[str]) -> None:
"""Reorders the tree based on a list of items."""
tmpOrder = [tHandle for tHandle in newOrder if tHandle in self._tree]
if not (len(tmpOrder) == len(newOrder) == len(self._order)):
# Something is wrong, so let's debug it
for tHandle in newOrder:
if tHandle not in self._tree:
logger.error("Handle '%s' in new tree order is not in old order", tHandle)
for tHandle in self._order:
if tHandle not in tmpOrder:
logger.warning("Handle '%s' in old tree order is not in new order", tHandle)
# Save the temp list
self._order = tmpOrder
self._setTreeChanged(True)
logger.debug("Project tree order updated")
return
##
# Special Methods
##
def __len__(self) -> int:
"""The number of items in the project."""
return len(self._order)
def __bool__(self) -> bool:
"""True if there are any items in the project."""
return bool(self._order)
def __getitem__(self, tHandle: str | None) -> NWItem | None:
"""Return a project item based on its handle. Returns None if
the handle doesn't exist in the project.
"""
if tHandle and tHandle in self._tree:
return self._tree[tHandle]
logger.error("No tree item with handle '%s'", str(tHandle))
return None
def __delitem__(self, tHandle: str) -> None:
"""Remove an item from the internal lists and dictionaries."""
if tHandle in self._order and tHandle in self._tree:
self._order.remove(tHandle)
del self._tree[tHandle]
else:
logger.warning("Failed to delete item '%s': item not found", tHandle)
return
if tHandle in self._roots:
del self._roots[tHandle]
if tHandle == self._trash:
self._trash = None
self._setTreeChanged(True)
return
def __contains__(self, tHandle: str) -> bool:
"""Checks if a handle exists in the tree."""
return tHandle in self._order
def __iter__(self) -> Iterator[NWItem]:
"""Iterate through project items."""
for tHandle in self._order:
tItem = self._tree.get(tHandle)
if isinstance(tItem, NWItem):
yield tItem
return
##
# Internal Functions
##
def _setTreeChanged(self, state: bool) -> None:
"""Set the changed flag to state, and if being set to True,
propagate that state change to the parent NWProject class.
"""
self._changed = state
if state:
self._project.setProjectChanged(True)
def _itemChange(self, item: NWItem, change: nwChange) -> None:
"""Signal item change and notify project."""
tHandle = item.itemHandle
logger.debug("Item change: %s -> %s", tHandle, change.name)
self._project.setProjectChanged(True)
SHARED.emitProjectItemChanged(self._project, tHandle, change)
if item.isRootType():
SHARED.emitRootFolderChanged(self._project, tHandle, change)
return
def _getTrashNode(self) -> ProjectNode | None:
"""Get the trash node. If it doesn't exist, create it."""
for node in self._model.root.children:
if node.item.itemClass == nwItemClass.TRASH:
return node
label = trConst(nwLabels.CLASS_NAME[nwItemClass.TRASH])
if handle := self.create(label, None, nwItemType.ROOT, nwItemClass.TRASH):
return self._nodes.get(handle)
return None
def _addItems(self, items: dict[str, NWItem]) -> dict[str, NWItem]:
"""Add a dictionary of items to the project tree. Returns a new
dictionary of items that could not be added yet, but can be.
"""
remains: dict[str, NWItem] = {}
for handle, item in items.items():
if pHandle := item.itemParent:
if parent := self._nodes.get(pHandle):
node = ProjectNode(item)
parent.addChild(node)
parent.updateCount()
self._items[handle] = item
self._nodes[handle] = node
elif pHandle in items:
remains[handle] = item
logger.warning("Item '%s' found before its parent", handle)
elif item.isRootType():
node = ProjectNode(item)
self._model.root.addChild(node)
self._model.root.updateCount()
self._items[handle] = item
self._nodes[handle] = node
return remains
def _makeHandle(self) -> str:
"""Generate a unique item handle. In the event that the key
already exists, generate a new one.
"""
logger.debug("Generating new handle")
handle = f"{random.getrandbits(52):013x}"
if handle in self._tree:
if handle in self._items:
logger.warning("Duplicate handle encountered! Retrying ...")
handle = self._makeHandle()

View File

@@ -6,7 +6,7 @@ File History:
Created: 2020-05-21 [0.5.2] GuiAbout
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
Copyright (C) 2020 Veronica Berglyd Olsen and novelWriter contributors
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

View File

@@ -7,7 +7,7 @@ Created: 2020-01-23 [0.4.3] GuiDocMerge
Rewritten: 2022-10-06 [2.0rc1] GuiDocMerge
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
Copyright (C) 2020 Veronica Berglyd Olsen and novelWriter contributors
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
@@ -162,23 +162,13 @@ class GuiDocMerge(NDialog):
self._data = {}
self._data["sHandle"] = sHandle
self._data["origItems"] = itemList
self.listBox.clear()
for tHandle in itemList:
nwItem = SHARED.project.tree[tHandle]
if nwItem is None or not nwItem.isFileType():
continue
itemIcon = SHARED.theme.getItemIcon(
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, nwItem.mainHeading
)
newItem = QListWidgetItem()
newItem.setIcon(itemIcon)
newItem.setText(nwItem.itemName)
newItem.setData(self.D_HANDLE, tHandle)
newItem.setCheckState(Qt.CheckState.Checked)
self.listBox.addItem(newItem)
if (nwItem := SHARED.project.tree[tHandle]) and nwItem.isFileType():
item = QListWidgetItem()
item.setIcon(nwItem.getMainIcon())
item.setText(nwItem.itemName)
item.setData(self.D_HANDLE, tHandle)
item.setCheckState(Qt.CheckState.Checked)
self.listBox.addItem(item)
return

View File

@@ -7,7 +7,7 @@ Created: 2020-02-01 [0.4.3] GuiDocSplit
Rewritten: 2022-10-12 [2.0rc1] GuiDocSplit
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
Copyright (C) 2020 Veronica Berglyd Olsen and novelWriter contributors
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

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