diff --git a/.github/workflows/syntax.yml b/.github/workflows/syntax.yml index 567c40f6..01e4e8ea 100644 --- a/.github/workflows/syntax.yml +++ b/.github/workflows/syntax.yml @@ -1,4 +1,4 @@ -name: flake8 +name: Flake8 on: push: diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ac00b49..39b57f01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,82 @@ # novelWriter Changelog +## Version 2.2 Beta 1 [2023-11-11] + +### 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. + +Please check the changelog for an overview of changes. The full release notes will be added to the +final release. + +### Detailed Changelog + +**Features** + +* novelWriter has a new logo and icon. PR #1593. +* The Document Editor is now a true plain text editor. This has a number of benefits and a couple + of drawbacks. The most important benefits is that the editor responds a lot faster, and can hold + much larger text documents. The big document limit has therefore been removed. It mostly affected + automatic spell checking. The syntax highlighter and spell checker are also more efficient, which + allows for needed improvements to these. The drawbacks are mainly that the editor now scrolls one + line at a time, instead of scrolling pixel by pixel like before. PRs #1521 and #1525. +* Tags and References are now case insensitive. Their display name on the user interface remains + the same as the value set for the `@tag` entry. Issue #1313. PRs #1522 and #1578. +* Keywords for Tags and References, and the References themselves, now have an auto-complete menu + that pops up in the editor on lines starting with the `@` character. It will first suggest what + keyword you want to use, and when it has been added, use that keyword to look up suggestions for + references to add. The suggestions improve as you type by looking for the characters you've + already typed in the tags you've previously defined. Issue #823. PR #1581. +* You can now right-click an undefined tag, and a context menu option to create a Project Note for + that tag will appear in the menu. On selection, it will create a note in the first root folder of + the correct kind, and set the title and tag to match the undefined reference, making it instantly + defined. Issues #1580 and #823. PR #1582. +* Shortcodes have been added to the Document Editor. Shorcodes are HTML-like syntax, but uses + square brackets instead of angular brackets. So `[b]text[/b]` will make the word "text" appear as + bold. Shortcodes currently support bold, italic, striketrough, underline, superscript and + subscript text. The first three are complimentary to the Markdown-like syntax that. The benefit + of the shortcode emphasis syntax, however, is that it does not care about word boundaries, and + can therefore be used any place in the text. Including in the middle of words. Issues #1337 and + #1444. PRs #1540 and #1583. +* A show/hide toolbar has been added to the editor where toolbuttons for formatting options are + available. The toolbar is hidden by default, but can be activated from a three dots icon in the + top left corner of the editor. Issue #1585. PR #1584. +* Build Definitions in the Manuscript Build tool can now be re-ordered, and the order is preserved + when the tool is closed and re-opened. Issue #1542. PR #1591. + +**Usability** + +* The Settings menu in the sidebar now always pops out to the right and upwards from the bottom of + the icon. The previous behaviour was not guaranteed to stay in the visible area of the screen. + PR #1520. +* The right click action on a misspelled word now uses the actual spell checker data for lookup. + Previously, the spell checker would underline a word that was misspelled, but the right click + action actually had no way of reading where the error line was, so it had to guess again what + word the user was clicking. Since these two parts of the code used different logic, they + sometimes produced different results. The spell checker now saves the location of each spell + check error, and the right click action retrieves this data when generating suggestions, which + should eliminate the problem of picking the correct word boundaries. Issue #1532. PR #1525. +* The language of a project is not set in the New Project Wizard and in Project Settings. It is no + longer defined in the Build Settings panel. Issue #1588. PR #1589. +* The way switching focus and view in the main GUI has changed. Pressing `Ctrl+T` will now switch + focus to the Project or Novel Tree if focus is elsewhere, or if either have focus already, it + will switch view to the other tree. Pressing `Ctrl+E` will switch focus and view to the Document + Editor. Pressing `Ctrl+Shift+T` will do the same for the Outline View. The old Alt-based + shortcuts have been removed. Issues #1310 and #1291. PR #1590. + +**User Interface** + +* The labels under the sidebar buttons have been removed. The tool tips have the necessary + information. PR #1520. + +**Other Improvements** + +* Also the Tags and References keywords are now translated into the project language when these are + included in Manuscript builds. As long as the phrases have been translated. PR #1586. + +---- + ## Version 2.1.1 [2023-11-05] ### Release Notes diff --git a/CREDITS.md b/CREDITS.md index d792ce6f..68e673e9 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -52,6 +52,12 @@ Some of the assets bundled with novelWriter were adapted from the following sour * [Owl](https://github.com/sdras/night-owl-vscode-theme) syntax themes by Sarah Drasner (MIT License) * [Solarized](https://github.com/altercation/solarized) themes by Ethan Schoonover (MIT License) +## Fonts + +The font used for the main novelWriter logo, mimetype and text banners is: + +* Pridi by Cadson Demak (Open Font License, Version 1.1) + ## Special Mentions Additional thanks to @johnblommers who was an early user and who has provided a lot of very useful diff --git a/README.md b/README.md index f9ea4bed..6a2022f9 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,12 @@ # novelWriter -[![release](https://img.shields.io/github/v/release/vkbo/novelwriter)](https://github.com/vkbo/novelWriter/releases) [![Linux](https://github.com/vkbo/novelWriter/actions/workflows/test_linux.yml/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions/workflows/test_linux.yml) [![Windows](https://github.com/vkbo/novelWriter/actions/workflows/test_win.yml/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions/workflows/test_win.yml) -[![macOS](https://github.com/vkbo/novelWriter/actions/workflows/test_mac.yml/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions/workflows/test_mac.yml) -[![flake8](https://github.com/vkbo/novelWriter/workflows/flake8/badge.svg)](https://github.com/vkbo/novelWriter/actions) -[![codecov](https://codecov.io/gh/vkbo/novelWriter/branch/main/graph/badge.svg)](https://codecov.io/gh/vkbo/novelWriter) +[![MacOS](https://github.com/vkbo/novelWriter/actions/workflows/test_mac.yml/badge.svg?branch=main)](https://github.com/vkbo/novelWriter/actions/workflows/test_mac.yml) +[![Flake8](https://github.com/vkbo/novelWriter/workflows/flake8/badge.svg)](https://github.com/vkbo/novelWriter/actions) +[![CodeCov](https://codecov.io/gh/vkbo/novelWriter/branch/main/graph/badge.svg)](https://codecov.io/gh/vkbo/novelWriter) - + novelWriter is a plain text editor designed for writing novels assembled from many smaller text documents. It uses a minimal formatting syntax inspired by Markdown, and adds a meta data syntax diff --git a/docs/source/_static/novelwriter-dark.png b/docs/source/_static/novelwriter-dark.png index 0ad6329d..1c7935aa 100644 Binary files a/docs/source/_static/novelwriter-dark.png and b/docs/source/_static/novelwriter-dark.png differ diff --git a/docs/source/_static/novelwriter-light.png b/docs/source/_static/novelwriter-light.png index 02bd281d..7b4723a7 100644 Binary files a/docs/source/_static/novelwriter-light.png and b/docs/source/_static/novelwriter-light.png differ diff --git a/docs/source/_static/novelwriter-pdf.png b/docs/source/_static/novelwriter-pdf.png index 6261b1ee..2b61485e 100644 Binary files a/docs/source/_static/novelwriter-pdf.png and b/docs/source/_static/novelwriter-pdf.png differ diff --git a/docs/source/usage_shortcuts.rst b/docs/source/usage_shortcuts.rst index 4e931351..db9382cb 100644 --- a/docs/source/usage_shortcuts.rst +++ b/docs/source/usage_shortcuts.rst @@ -25,19 +25,19 @@ Main Window Shortcuts ":kbd:`F8`", "Toggle :guilabel:`Focus Mode`" ":kbd:`F9`", "Re-build the project index" ":kbd:`F11`", "Toggle full screen mode" - ":kbd:`Alt+1`", "Switch focus to the project tree (Windows :kbd:`Ctrl+Alt+1`)" - ":kbd:`Alt+2`", "Switch focus to document editor (Windows :kbd:`Ctrl+Alt+2`)" - ":kbd:`Alt+3`", "Switch focus to document viewer (Windows :kbd:`Ctrl+Alt+3`)" - ":kbd:`Alt+4`", "Switch focus to outline view (Windows :kbd:`Ctrl+Alt+4`)" ":kbd:`Ctrl+,`", "Open the :guilabel:`Preferences` dialog" + ":kbd:`Ctrl+E`", "Switch focus to the document editor" + ":kbd:`Ctrl+T`", "Switch focus to the project/novel tree" ":kbd:`Ctrl+Q`", "Exit novelWriter" ":kbd:`Ctrl+Shift+,`", "Open the :guilabel:`Project Settings` dialog" ":kbd:`Ctrl+Shift+O`", "Open a project" ":kbd:`Ctrl+Shift+S`", "Save the current project" + ":kbd:`Ctrl+Shift+T`", "Switch focus to the outline view" ":kbd:`Ctrl+Shift+W`", "Close the current project" ":kbd:`Shift+F1`", "Open the local user manual (PDF) if it is available" ":kbd:`Shift+F6`", "Open the :guilabel:`Project Details` dialog" + .. _a_kb_tree: Project Tree Shortcuts @@ -46,21 +46,21 @@ Project Tree Shortcuts .. csv-table:: :header: "Shortcut", "Description" - ":kbd:`F2`", "Edit the label of the selected item" - ":kbd:`Return`", "Open the selected document in the editor" - ":kbd:`Alt+Up`", "Jump or go to the previous item at same level in the tree" - ":kbd:`Alt+Down`", "Jump or go to the next item at same level in the tree" - ":kbd:`Alt+Left`", "Jump to the parent item in the tree" - ":kbd:`Alt+Right`", "Jump to the first child item in the project tree" - ":kbd:`Ctrl+.`", "Open the context menu on the selected item" - ":kbd:`Ctrl+L`", "Open the :guilabel:`Quick Links` menu" - ":kbd:`Ctrl+N`", "Open the :guilabel:`Create New Item` menu" - ":kbd:`Ctrl+O`", "Open selected document" - ":kbd:`Ctrl+R`", "Open the selected document in the viewer" - ":kbd:`Ctrl+Up`", "Move selected item one step up in the tree" - ":kbd:`Ctrl+Down`", "Move selected item one step down in the tree" - ":kbd:`Ctrl+Shift+Z`", "Undo the last move of a project item, if possible" - ":kbd:`Ctrl+Shift+Del`", "Move the selected item to Trash" + ":kbd:`F2`", "Edit the label of the selected item" + ":kbd:`Return`", "Open the selected document in the editor" + ":kbd:`Alt+Up`", "Jump or go to the previous item at same level in the tree" + ":kbd:`Alt+Down`", "Jump or go to the next item at same level in the tree" + ":kbd:`Alt+Left`", "Jump to the parent item in the tree" + ":kbd:`Alt+Right`", "Jump to the first child item in the project tree" + ":kbd:`Ctrl+.`", "Open the context menu on the selected item" + ":kbd:`Ctrl+L`", "Open the :guilabel:`Quick Links` menu" + ":kbd:`Ctrl+N`", "Open the :guilabel:`Create New Item` menu" + ":kbd:`Ctrl+O`", "Open selected document" + ":kbd:`Ctrl+R`", "Open the selected document in the viewer" + ":kbd:`Ctrl+Up`", "Move selected item one step up in the tree" + ":kbd:`Ctrl+Down`", "Move selected item one step down in the tree" + ":kbd:`Ctrl+Del`", "Move the selected item to Trash" + ":kbd:`Ctrl+Shift+Z`", "Undo the last move of a project item, if possible" .. _a_kb_editor: @@ -134,6 +134,7 @@ Other Editor Shortcuts ":kbd:`Ctrl+Shift+A`", "Select all text in the current paragraph" ":kbd:`Ctrl+Shift+I`", "Import text to the current document from a text file" + .. _a_kb_ins: Insert Shortcuts diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index b3b39028..07eebb28 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -33,26 +33,8 @@ from novelwriter.error import exceptionHandler, logException from novelwriter.config import Config from novelwriter.shared import SharedData -## -# Version Scheme -# ================ -# Generally follows PEP 440 -# Hex Version: -# - Digit 1,2 : Major Version (01-ff) -# - Digit 3,4 : Minor Version (01-ff) -# - Digit 5,6 : Patch Version (01-ff) -# - Digit 7 : Release Type (a: alpha, b: beta, c: candidate, f: final) -# - Digit 8 : Release Number (0-f) -# -# Example : Full Short Description -# -------------------------------------------------------------------- -# 0x010200a0 : 1.2-alpha0 1.2a0 Use while developing next release -# 0x010200a1 : 1.2-alpha1 1.2a1 First alpha release -# 0x010200b1 : 1.2-beta1 1.2b1 First beta release -# 0x010200c1 : 1.2-rc1 1.2rc1 First release candidate -# 0x010200f0 : 1.2.0 1.2.0 Final release -# 0x010201f0 : 1.2.1 1.2.1 Patch release -## +# Package Meta +# ============ __package__ = "novelwriter" __copyright__ = "Copyright 2018–2023, Veronica Berglyd Olsen" @@ -60,9 +42,9 @@ __license__ = "GPLv3" __author__ = "Veronica Berglyd Olsen" __maintainer__ = "Veronica Berglyd Olsen" __email__ = "code@vkbo.net" -__version__ = "2.2-alpha1" -__hexversion__ = "0x020200a1" -__date__ = "2023-09-01" +__version__ = "2.2-beta1" +__hexversion__ = "0x020200b1" +__date__ = "2023-11-12" __status__ = "Stable" __domain__ = "novelwriter.io" diff --git a/novelwriter/assets/i18n/project_nn_NO.json b/novelwriter/assets/i18n/project_nn_NO.json index 09b99a4e..aff847e2 100644 --- a/novelwriter/assets/i18n/project_nn_NO.json +++ b/novelwriter/assets/i18n/project_nn_NO.json @@ -2,6 +2,16 @@ "Synopsis": "Samandrag", "Comment": "Kommentar", "Notes": "Notat", + "Tag": "Knagg", + "Point of View": "Perspektiv", + "Focus": "Fokus", + "Characters": "Karakterar", + "Plot": "Plott", + "Timeline": "Tidslinje", + "Locations": "Lokasjonar", + "Objects": "Objekt", + "Entities": "Eining", + "Custom": "Anna", "0": "null", "1": "ein", "2": "to", diff --git a/novelwriter/assets/icons/novelwriter.ico b/novelwriter/assets/icons/novelwriter.ico index 24de4e24..54301ffe 100644 Binary files a/novelwriter/assets/icons/novelwriter.ico and b/novelwriter/assets/icons/novelwriter.ico differ diff --git a/novelwriter/assets/icons/novelwriter.svg b/novelwriter/assets/icons/novelwriter.svg index 18be89c1..a161cd19 100644 --- a/novelwriter/assets/icons/novelwriter.svg +++ b/novelwriter/assets/icons/novelwriter.svg @@ -1,184 +1,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - + + + + + + + + diff --git a/novelwriter/assets/icons/x-novelwriter-project.ico b/novelwriter/assets/icons/x-novelwriter-project.ico index c4937b3b..ab97bb2b 100644 Binary files a/novelwriter/assets/icons/x-novelwriter-project.ico and b/novelwriter/assets/icons/x-novelwriter-project.ico differ diff --git a/novelwriter/assets/icons/x-novelwriter-project.svg b/novelwriter/assets/icons/x-novelwriter-project.svg index 08546d15..dc577083 100644 --- a/novelwriter/assets/icons/x-novelwriter-project.svg +++ b/novelwriter/assets/icons/x-novelwriter-project.svg @@ -1,207 +1,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - + + + + + + + diff --git a/novelwriter/assets/text/credits_en.htm b/novelwriter/assets/text/credits_en.htm index 525ffca9..98393ef6 100644 --- a/novelwriter/assets/text/credits_en.htm +++ b/novelwriter/assets/text/credits_en.htm @@ -61,6 +61,13 @@ more contributions are listed on the project's Solarized themes by Ethan Schoonover (MIT License)

+

Fonts

+ +

The font used for the main novelWriter logo, mimetype and text banners is:

+

+ Pridi by Cadson Demak (Open Font License, Version 1.1) +

+

Special Mentions

Additional thanks to @johnblommers who was an early user and who has provided a lot diff --git a/novelwriter/assets/text/release_notes.htm b/novelwriter/assets/text/release_notes.htm index c74a1574..69e1eaab 100644 --- a/novelwriter/assets/text/release_notes.htm +++ b/novelwriter/assets/text/release_notes.htm @@ -2,46 +2,16 @@ -

Release Notes for 2.1

-

Released on 17 October 2023

+

Release Notes for 2.2 Beta 1

+

Released on 11 November 2023

-

Scroll down for Patch Notes

- -

The primary focus of this release has been a complete redesign of the Build Tool, that is, the -tool that assembles your project into a manuscript document. The new tool, called the "Manuscript -Build Tool" allows you to define multiple build definitions for your project. The build definitions -are edited in a new Manuscript Build Settings dialog, with a lot more options than the old tool.

-

The reason for this redesign is a long list of feature requests that could not easily be -accommodated in the old, much simpler tool. Far from all the features have been added yet, but now -that the new tool is in place, they will be gradually added in the coming releases.

-

The key feature added in this release is the extended control you now have for selecting exactly -what part of your project is included in a given build definition. You have the same filters for -selecting documents and notes, and turning on or off root folders as before, but you can now easily -override on a per-document basis what is included or excluded in addition to the filter.

-

A second major improvement is a better tool to format your manuscript headings. You no longer -have to look up formatting codes and add them manually. Instead, there is now a heading format -editor in the Build Settings dialog for creating the header format, with syntax highlighting -included.

- -

Other Changes

-

Among other features is a new option to duplicate documents and folders in the project tree. The -duplicate feature is available from the right-click menu. A proper light colour theme has also been -added. In most cases it will be the same as the default theme, depending on your platform.

-

There are other, minor improvements as well, and a lot of code improvements under the hood. For -a full list of changes, see the detailed changelogs.

+

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.

+

Please check the changelog for an overview of changes. The full release notes will be added to +the final release.

See also the Releases page.

-

Patch Notes

- -

Patch 2.2.1 – 5 November 2023

- -

This is a patch release that fixes a layout issue and internationalisation issues with the new -Manuscript Build tool. It also fixes a number of issues related to bugs in the underlying Qt -framework that affects drag and drop functionality in the project tree. These issues were mostly -only affecting Debian Linux package releases.

-

Other, minor issues related to updating the editor on colour theme change and project word list -changes have been fixed as well. See the full changelog for more details.

- diff --git a/novelwriter/config.py b/novelwriter/config.py index ebd0d05f..bc8af368 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -148,6 +148,7 @@ class Config: self.autoScroll = False # Typewriter-like scrolling self.autoScrollPos = 30 # Start point for typewriter-like scrolling + self.scrollPastEnd = True # Scroll past end of document, and centre cursor self.wordCountTimer = 5.0 # Interval for word count update in seconds self.incNotesWCount = True # The status bar word count includes notes @@ -577,6 +578,7 @@ class Config: self.doReplaceDots = conf.rdBool(sec, "repdots", self.doReplaceDots) self.autoScroll = conf.rdBool(sec, "autoscroll", self.autoScroll) self.autoScrollPos = conf.rdInt(sec, "autoscrollpos", self.autoScrollPos) + self.scrollPastEnd = conf.rdBool(sec, "scrollpastend", self.scrollPastEnd) self.fmtSQuoteOpen = conf.rdStr(sec, "fmtsquoteopen", self.fmtSQuoteOpen) self.fmtSQuoteClose = conf.rdStr(sec, "fmtsquoteclose", self.fmtSQuoteClose) self.fmtDQuoteOpen = conf.rdStr(sec, "fmtdquoteopen", self.fmtDQuoteOpen) @@ -701,6 +703,7 @@ class Config: "repdots": str(self.doReplaceDots), "autoscroll": str(self.autoScroll), "autoscrollpos": str(self.autoScrollPos), + "scrollpastend": str(self.scrollPastEnd), "fmtsquoteopen": str(self.fmtSQuoteOpen), "fmtsquoteclose": str(self.fmtSQuoteClose), "fmtdquoteopen": str(self.fmtDQuoteOpen), diff --git a/novelwriter/constants.py b/novelwriter/constants.py index 5579e8a3..514ea9f2 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -327,8 +327,7 @@ class nwQuotes: class nwUnicode: - """Supported unicode character constants and their HTML equivalents. - """ + """Supported unicode character constants and their HTML equivalents.""" # Unicode Constants # ================= diff --git a/novelwriter/core/buildsettings.py b/novelwriter/core/buildsettings.py index 931525ea..84dd7ff2 100644 --- a/novelwriter/core/buildsettings.py +++ b/novelwriter/core/buildsettings.py @@ -64,7 +64,6 @@ SETTINGS_TEMPLATE = { "text.includeKeywords": (bool, False), "text.includeBodyText": (bool, True), "text.addNoteHeadings": (bool, True), - "format.buildLang": (str, "en_GB"), "format.textFont": (str, CONFIG.textFont), "format.textSize": (int, 12), "format.lineHeight": (float, 1.15, 0.75, 3.0), @@ -107,7 +106,6 @@ SETTINGS_LABELS = { "text.addNoteHeadings": QT_TRANSLATE_NOOP("Builds", "Add Titles for Notes"), "format.grpFormat": QT_TRANSLATE_NOOP("Builds", "Text Format"), - "format.buildLang": QT_TRANSLATE_NOOP("Builds", "Language"), "format.textFont": QT_TRANSLATE_NOOP("Builds", "Font Family"), "format.textSize": QT_TRANSLATE_NOOP("Builds", "Font Size"), "format.lineHeight": QT_TRANSLATE_NOOP("Builds", "Line Height"), @@ -158,6 +156,7 @@ class BuildSettings: self._uuid = str(uuid.uuid4()) self._path = Path.home() self._build = "" + self._order = 0 self._format = nwBuildFmt.ODT self._skipRoot = set() self._excluded = set() @@ -166,20 +165,32 @@ class BuildSettings: self._changed = False return + @classmethod + def fromDict(cls, data: dict) -> BuildSettings: + """Create a build settings object from a dict.""" + cls = BuildSettings() + cls.unpack(data) + return cls + ## # Properties ## @property def name(self) -> str: - """The build name.""" + """Return the build name.""" return self._name @property def buildID(self) -> str: - """The build ID as a UUID.""" + """Return the build ID as a UUID.""" return self._uuid + @property + def order(self) -> int: + """Return the build order.""" + return self._order + @property def lastPath(self) -> Path: """The last used build path.""" @@ -253,6 +264,12 @@ class BuildSettings: self._uuid = value return + def setOrder(self, value: int) -> None: + """Set the build order.""" + if isinstance(value, int): + self._order = value + return + def setLastPath(self, path: Path | str | None) -> None: """Set the last used build path.""" if isinstance(path, str): @@ -400,6 +417,7 @@ class BuildSettings: "uuid": self._uuid, "path": str(self._path), "build": self._build, + "order": self._order, "format": self._format.name, "settings": self._settings.copy(), "content": { @@ -411,14 +429,15 @@ class BuildSettings: def unpack(self, data: dict) -> None: """Unpack a dictionary and populate the class.""" + content = data.get("content", {}) settings = data.get("settings", {}) - content = data.get("content", {}) included = content.get("included", []) excluded = content.get("excluded", []) skipRoot = content.get("skipRoot", []) self.setName(data.get("name", "")) self.setBuildID(data.get("uuid", "")) + self.setOrder(data.get("order", 0)) self.setLastPath(data.get("path", None)) self.setLastBuildName(data.get("build", "")) @@ -455,9 +474,9 @@ class BuildCollection: def __init__(self, project: NWProject) -> None: self._project = project - self._builds = {} self._lastBuild = "" self._defaultBuild = "" + self._builds: dict[str, BuildSettings] = {} self._loadCollection() return @@ -485,21 +504,19 @@ class BuildCollection: def getBuild(self, buildID: str) -> BuildSettings | None: """Get a specific build settings object.""" - if buildID not in self._builds: - return None - build = BuildSettings() - build.unpack(self._builds[buildID]) - return build + return self._builds.get(buildID, None) ## # Setters ## - def setLastBuild(self, buildID: str) -> None: + def setBuildsState(self, lastBuild: str, order: list[str]) -> None: """Set the last active build id.""" - if buildID != self._lastBuild: - self._lastBuild = buildID - self._saveCollection() + for i, key in enumerate(order): + if build := self._builds.get(key): + build.setOrder(i) + self._lastBuild = lastBuild + self._saveCollection() return def setDefaultBuild(self, buildID: str) -> None: @@ -512,8 +529,7 @@ class BuildCollection: def setBuild(self, build: BuildSettings) -> None: """Set build settings data in the collection.""" if isinstance(build, BuildSettings): - buildID = build.buildID - self._builds[buildID] = build.pack() + self._builds[build.buildID] = build self._saveCollection() return @@ -522,15 +538,15 @@ class BuildCollection: ## def removeBuild(self, buildID: str) -> None: - """Remove the a build from the collection.""" + """Remove a build from the collection.""" self._builds.pop(buildID, None) self._saveCollection() return def builds(self) -> Iterable[tuple[str, str]]: """Iterate over all available builds.""" - for buildID in self._builds: - yield buildID, self._builds[buildID].get("name", "") + for buildID, build in sorted(self._builds.items(), key=lambda x: x[1].order): + yield buildID, build.name return ## @@ -569,7 +585,7 @@ class BuildCollection: elif key == "defaultBuild": self._defaultBuild = str(entry) elif isinstance(entry, dict): - self._builds[key] = entry + self._builds[key] = BuildSettings.fromDict(entry) return True @@ -581,11 +597,11 @@ class BuildCollection: logger.debug("Saving builds file") try: - data = { + data: dict[str, str | dict] = { "lastBuild": self._lastBuild, "defaultBuild": self._defaultBuild, } - data.update(self._builds) + data.update({k: b.pack() for k, b in self._builds.items()}) with open(buildsFile, mode="w+", encoding="utf-8") as outFile: outFile.write(jsonEncode({"novelWriter.builds": data}, nmax=4)) except Exception: diff --git a/novelwriter/core/coretools.py b/novelwriter/core/coretools.py index 017ea082..81803576 100644 --- a/novelwriter/core/coretools.py +++ b/novelwriter/core/coretools.py @@ -353,11 +353,13 @@ class ProjectBuilder: projName = data.get("projName", lblNewProject) projTitle = data.get("projTitle", lblNewProject) projAuthor = data.get("projAuthor", "") + projLang = data.get("projLang", "en_GB") project.data.setUuid(None) project.data.setName(projName) project.data.setTitle(projTitle) project.data.setAuthor(projAuthor) + project.data.setLanguage(projLang) project.setDefaultStatusImport() project.session.startSession() diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py index 8c4b4ca5..75906d9a 100644 --- a/novelwriter/core/docbuild.py +++ b/novelwriter/core/docbuild.py @@ -248,13 +248,9 @@ class NWBuildDocument: def _setupBuild(self, bldObj: Tokenizer) -> dict: """Configure the build object.""" # Get Settings - buildLang = self._build.getStr("format.buildLang") textFont = self._build.getStr("format.textFont") textSize = self._build.getInt("format.textSize") - # The language lookup dict is reloaded if needed - self._project.setProjectLang(buildLang) - fontFamily = textFont or CONFIG.textFont bldFont = QFont(fontFamily, textSize) fontInfo = QFontInfo(bldFont) @@ -287,7 +283,7 @@ class NWBuildDocument: if isinstance(bldObj, ToOdt): bldObj.setColourHeaders(self._build.getBool("odt.addColours")) - bldObj.setLanguage(buildLang) + bldObj.setLanguage(self._project.data.language) 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)) diff --git a/novelwriter/core/projectdata.py b/novelwriter/core/projectdata.py index 9b0cc36c..d248a7a1 100644 --- a/novelwriter/core/projectdata.py +++ b/novelwriter/core/projectdata.py @@ -152,14 +152,14 @@ class NWProjectData: """Return the initial count of words for novel and note documents. """ - return tuple(self._initCounts) + return self._initCounts[0], self._initCounts[1] @property def currCounts(self) -> tuple[int, int]: """Return the current count of words for novel and note documents. """ - return tuple(self._currCounts) + return self._currCounts[0], self._currCounts[1] @property def lastHandle(self) -> dict[str, str | None]: diff --git a/novelwriter/core/tohtml.py b/novelwriter/core/tohtml.py index 41201412..7443ed34 100644 --- a/novelwriter/core/tohtml.py +++ b/novelwriter/core/tohtml.py @@ -456,11 +456,10 @@ class ToHtml(Tokenizer): def _formatSynopsis(self, text: str) -> str: """Apply HTML formatting to synopsis.""" + sSynop = self._localLookup("Synopsis") if self._genMode == self.M_PREVIEW: - sSynop = self._trSynopsis return f"

{sSynop}: {text}

\n" else: - sSynop = self._localLookup("Synopsis") return f"

{sSynop}: {text}

\n" def _formatComments(self, text: str) -> str: diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py index 63518ca7..4f4a8378 100644 --- a/novelwriter/core/tokenizer.py +++ b/novelwriter/core/tokenizer.py @@ -167,9 +167,6 @@ class Tokenizer(ABC): self._localLookup = self._project.localLookup self.tr = partial(QCoreApplication.translate, "Tokenizer") - # Cached Translations - self._trSynopsis = self.tr("Synopsis") - # Format RegEx self._rxMarkdown = [ (QRegularExpression(nwRegEx.FMT_EI), [0, self.FMT_I_B, 0, self.FMT_I_E]), diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index 54cad2fc..53940447 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -26,7 +26,7 @@ from __future__ import annotations import logging from PyQt5.QtGui import QFont -from PyQt5.QtCore import Qt +from PyQt5.QtCore import Qt, pyqtSlot from PyQt5.QtWidgets import ( QDialog, QWidget, QComboBox, QSpinBox, QPushButton, QDialogButtonBox, QLineEdit, QFileDialog, QFontDialog, QDoubleSpinBox @@ -43,12 +43,11 @@ logger = logging.getLogger(__name__) class GuiPreferences(NPagedDialog): - def __init__(self, mainGui): - super().__init__(parent=mainGui) + def __init__(self, parent: QWidget) -> None: + super().__init__(parent=parent) logger.debug("Create: GuiPreferences") self.setObjectName("GuiPreferences") - self.setWindowTitle(self.tr("Preferences")) self.tabGeneral = GuiPreferencesGeneral(self) @@ -93,26 +92,27 @@ class GuiPreferences(NPagedDialog): ## @property - def updateTheme(self): + def updateTheme(self) -> bool: return self._updateTheme @property - def updateSyntax(self): + def updateSyntax(self) -> bool: return self._updateSyntax @property - def needsRestart(self): + def needsRestart(self) -> bool: return self._needsRestart @property - def refreshTree(self): + def refreshTree(self) -> bool: return self._refreshTree ## - # Slots + # Private Slots ## - def _doSave(self): + @pyqtSlot() + def _doSave(self) -> None: """Trigger all the save functions in the tabs, and collect the status of the saves. """ @@ -132,9 +132,9 @@ class GuiPreferences(NPagedDialog): return - def _doClose(self): - """Close the preferences without saving the changes. - """ + @pyqtSlot() + def _doClose(self) -> None: + """Close the preferences without saving the changes.""" self._saveWindowSize() self.reject() return @@ -143,9 +143,8 @@ class GuiPreferences(NPagedDialog): # Internal Functions ## - def _saveWindowSize(self): - """Save the dialog window size. - """ + def _saveWindowSize(self) -> None: + """Save the dialog window size.""" CONFIG.setPreferencesWinSize(self.width(), self.height()) return @@ -154,7 +153,7 @@ class GuiPreferences(NPagedDialog): class GuiPreferencesGeneral(QWidget): - def __init__(self, prefsGui): + def __init__(self, prefsGui: GuiPreferences) -> None: super().__init__(parent=prefsGui) self.prefsGui = prefsGui @@ -285,9 +284,8 @@ class GuiPreferencesGeneral(QWidget): return - def saveValues(self): - """Save the values set for this tab. - """ + def saveValues(self) -> None: + """Save the values set for this tab.""" guiLocale = self.guiLocale.currentData() guiTheme = self.guiTheme.currentData() guiSyntax = self.guiSyntax.currentData() @@ -316,12 +314,12 @@ class GuiPreferencesGeneral(QWidget): return ## - # Slots + # Private Slots ## - def _selectFont(self): - """Open the QFontDialog and set a font for the font style. - """ + @pyqtSlot() + def _selectFont(self) -> None: + """Open the QFontDialog and set a font for the font style.""" currFont = QFont() currFont.setFamily(CONFIG.guiFont) currFont.setPointSize(CONFIG.guiFontSize) @@ -336,7 +334,7 @@ class GuiPreferencesGeneral(QWidget): class GuiPreferencesProjects(QWidget): - def __init__(self, prefsGui): + def __init__(self, prefsGui: GuiPreferences) -> None: super().__init__(parent=prefsGui) # The Form @@ -438,9 +436,8 @@ class GuiPreferencesProjects(QWidget): return - def saveValues(self): - """Save the values set for this tab. - """ + def saveValues(self) -> None: + """Save the values set for this tab.""" # Automatic Save CONFIG.autoSaveDoc = self.autoSaveDoc.value() CONFIG.autoSaveProj = self.autoSaveProj.value() @@ -457,12 +454,12 @@ class GuiPreferencesProjects(QWidget): return ## - # Slots + # Private Slots ## - def _backupFolder(self): - """Open a dialog to select the backup folder. - """ + @pyqtSlot() + def _backupFolder(self) -> None: + """Open a dialog to select the backup folder.""" currDir = self.backupPath or "" newDir = QFileDialog.getExistingDirectory( self, self.tr("Backup Directory"), str(currDir), options=QFileDialog.ShowDirsOnly @@ -472,15 +469,13 @@ class GuiPreferencesProjects(QWidget): self.mainForm.setHelpText( self.backupPathRow, self.tr("Path: {0}").format(self.backupPath) ) - return True + return + return - return False - - def _toggledBackupOnClose(self, theState): - """Enable or disable switch that depends on the backup on close - switch. - """ - self.askBeforeBackup.setEnabled(theState) + @pyqtSlot(bool) + def _toggledBackupOnClose(self, state: bool) -> None: + """Toggle switch that depends on the backup on close switch.""" + self.askBeforeBackup.setEnabled(state) return # END Class GuiPreferencesProjects @@ -488,7 +483,7 @@ class GuiPreferencesProjects(QWidget): class GuiPreferencesDocuments(QWidget): - def __init__(self, prefsGui): + def __init__(self, prefsGui: GuiPreferences) -> None: super().__init__(parent=prefsGui) # The Form @@ -604,9 +599,8 @@ class GuiPreferencesDocuments(QWidget): return - def saveValues(self): - """Save the values set for this tab. - """ + def saveValues(self) -> None: + """Save the values set for this tab.""" # Text Style CONFIG.setTextFont(self.textFont.text(), self.textSize.value()) @@ -621,12 +615,12 @@ class GuiPreferencesDocuments(QWidget): return ## - # Slots + # Private Slots ## + @pyqtSlot() def _selectFont(self): - """Open the QFontDialog and set a font for the font style. - """ + """Open the QFontDialog and set a font for the font style.""" currFont = QFont() currFont.setFamily(CONFIG.textFont) currFont.setPointSize(CONFIG.textSize) @@ -642,7 +636,7 @@ class GuiPreferencesDocuments(QWidget): class GuiPreferencesEditor(QWidget): - def __init__(self, prefsGui): + def __init__(self, prefsGui: GuiPreferences) -> None: super().__init__(parent=prefsGui) # The Form @@ -726,6 +720,15 @@ class GuiPreferencesEditor(QWidget): # ================ self.mainForm.addGroupLabel(self.tr("Scroll Behaviour")) + # Scroll Past End + self.scrollPastEnd = NSwitch() + self.scrollPastEnd.setChecked(CONFIG.scrollPastEnd) + self.mainForm.addRow( + self.tr("Scroll past end of the document"), + self.scrollPastEnd, + self.tr("Also centres the cursor when scrolling.") + ) + # Typewriter Scrolling self.autoScroll = NSwitch() self.autoScroll.setChecked(CONFIG.autoScroll) @@ -750,7 +753,7 @@ class GuiPreferencesEditor(QWidget): return - def saveValues(self): + def saveValues(self) -> None: """Save the values set for this tab.""" # Spell Checking CONFIG.spellLanguage = self.spellLanguage.currentData() @@ -766,6 +769,7 @@ class GuiPreferencesEditor(QWidget): # Scroll Behaviour CONFIG.autoScroll = self.autoScroll.isChecked() CONFIG.autoScrollPos = self.autoScrollPos.value() + CONFIG.scrollPastEnd = self.scrollPastEnd.isChecked() return @@ -774,7 +778,7 @@ class GuiPreferencesEditor(QWidget): class GuiPreferencesSyntax(QWidget): - def __init__(self, prefsGui): + def __init__(self, prefsGui: GuiPreferences) -> None: super().__init__(parent=prefsGui) self.prefsGui = prefsGui @@ -840,9 +844,8 @@ class GuiPreferencesSyntax(QWidget): return - def saveValues(self): - """Save the values set for this tab. - """ + def saveValues(self) -> None: + """Save the values set for this tab.""" highlightQuotes = self.highlightQuotes.isChecked() allowOpenSQuote = self.allowOpenSQuote.isChecked() allowOpenDQuote = self.allowOpenDQuote.isChecked() @@ -862,15 +865,14 @@ class GuiPreferencesSyntax(QWidget): return ## - # Slots + # Private Slots ## - def _toggleHighlightQuotes(self, theState): - """Enables or disables switches controlled by the highlight - quotes switch. - """ - self.allowOpenSQuote.setEnabled(theState) - self.allowOpenDQuote.setEnabled(theState) + @pyqtSlot(bool) + def _toggleHighlightQuotes(self, state: bool) -> None: + """Toggle switches controlled by the highlight quotes switch.""" + self.allowOpenSQuote.setEnabled(state) + self.allowOpenDQuote.setEnabled(state) return # END Class GuiPreferencesSyntax @@ -878,7 +880,7 @@ class GuiPreferencesSyntax(QWidget): class GuiPreferencesAutomation(QWidget): - def __init__(self, prefsGui): + def __init__(self, prefsGui: GuiPreferences) -> None: super().__init__(parent=prefsGui) # The Form @@ -989,9 +991,8 @@ class GuiPreferencesAutomation(QWidget): return - def saveValues(self): - """Save the values set for this tab. - """ + def saveValues(self) -> None: + """Save the values set for this tab.""" # Automatic Features CONFIG.autoSelect = self.autoSelect.isChecked() CONFIG.doReplace = self.doReplace.isChecked() @@ -1010,18 +1011,17 @@ class GuiPreferencesAutomation(QWidget): return ## - # Slots + # Private Slots ## - def _toggleAutoReplaceMain(self, theState): - """Enables or disables switches controlled by the main auto - replace switch. - """ - self.doReplaceSQuote.setEnabled(theState) - self.doReplaceDQuote.setEnabled(theState) - self.doReplaceDash.setEnabled(theState) - self.doReplaceDots.setEnabled(theState) - self.fmtPadThin.setEnabled(theState) + @pyqtSlot(bool) + def _toggleAutoReplaceMain(self, state: bool) -> None: + """Toggle switches controlled by the auto replace switch.""" + self.doReplaceSQuote.setEnabled(state) + self.doReplaceDQuote.setEnabled(state) + self.doReplaceDash.setEnabled(state) + self.doReplaceDots.setEnabled(state) + self.fmtPadThin.setEnabled(state) return # END Class GuiPreferencesAutomation @@ -1029,7 +1029,7 @@ class GuiPreferencesAutomation(QWidget): class GuiPreferencesQuotes(QWidget): - def __init__(self, prefsGui): + def __init__(self, prefsGui: GuiPreferences) -> None: super().__init__(parent=prefsGui) # The Form @@ -1113,9 +1113,8 @@ class GuiPreferencesQuotes(QWidget): return - def saveValues(self): - """Save the values set for this tab. - """ + def saveValues(self) -> None: + """Save the values set for this tab.""" # Quotation Style CONFIG.fmtSQuoteOpen = self.quoteSym["SO"].text() CONFIG.fmtSQuoteClose = self.quoteSym["SC"].text() @@ -1124,12 +1123,11 @@ class GuiPreferencesQuotes(QWidget): return ## - # Slots + # Internal Functions ## - def _getQuote(self, qType): - """Dialog for single quote open. - """ + def _getQuote(self, qType: str) -> None: + """Dialog for single quote open.""" qtBox = GuiQuoteSelect(self, currentQuote=self.quoteSym[qType].text()) if qtBox.exec_() == QDialog.Accepted: self.quoteSym[qType].setText(qtBox.selectedQuote) diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index ac79a986..20b11c53 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -96,21 +96,22 @@ class GuiProjectSettings(NPagedDialog): return - def __del__(self): # pragma: no cover + def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiProjectSettings") return ## - # Slots + # Private Slots ## - def _doSave(self): - """Save settings and close dialog. - """ - project = SHARED.project + @pyqtSlot() + def _doSave(self) -> None: + """Save settings and close dialog.""" + project = SHARED.project projName = self.tabMain.editName.text() bookTitle = self.tabMain.editTitle.text() bookAuthor = self.tabMain.editAuthor.text() + projLang = self.tabMain.projLang.currentData() spellLang = self.tabMain.spellLang.currentData() doBackup = not self.tabMain.doBackup.isChecked() @@ -119,6 +120,7 @@ class GuiProjectSettings(NPagedDialog): project.data.setAuthor(bookAuthor) project.data.setDoBackup(doBackup) project.data.setSpellLang(spellLang) + project.setProjectLang(projLang) if self.tabStatus.colChanged: newList, delList = self.tabStatus.getNewList() @@ -140,9 +142,9 @@ class GuiProjectSettings(NPagedDialog): return - def _doClose(self): - """Save settings and close the dialog. - """ + @pyqtSlot() + def _doClose(self) -> None: + """Save settings and close the dialog.""" self._saveGuiSettings() self.reject() return @@ -151,9 +153,8 @@ class GuiProjectSettings(NPagedDialog): # Internal Functions ## - def _focusTab(self, tab): - """Change which is the focused tab. - """ + def _focusTab(self, tab: int) -> None: + """Change which is the focused tab.""" if tab == self.TAB_MAIN: self.setCurrentWidget(self.tabMain) elif tab == self.TAB_STATUS: @@ -164,9 +165,8 @@ class GuiProjectSettings(NPagedDialog): self.setCurrentWidget(self.tabReplace) return - def _saveGuiSettings(self): - """Save GUI settings. - """ + def _saveGuiSettings(self) -> None: + """Save GUI settings.""" winWidth = CONFIG.rpxInt(self.width()) winHeight = CONFIG.rpxInt(self.height()) replaceColW = CONFIG.rpxInt(self.tabReplace.listBox.columnWidth(0)) @@ -187,8 +187,8 @@ class GuiProjectSettings(NPagedDialog): class GuiProjectEditMain(QWidget): - def __init__(self, projGui): - super().__init__(parent=projGui) + def __init__(self, parent: QWidget) -> None: + super().__init__(parent=parent) # The Form self.mainForm = NConfigLayout() @@ -230,24 +230,41 @@ class GuiProjectEditMain(QWidget): self.tr("Change whenever you want!") ) + self.projLang = QComboBox(self) + self.projLang.setMaximumWidth(xW) + for tag, language in CONFIG.listLanguages(CONFIG.LANG_PROJ): + self.projLang.addItem(language, tag) + self.mainForm.addRow( + self.tr("Project language"), + self.projLang, + self.tr("Used when building the manuscript.") + ) + + langIdx = 0 + if pData.language is not None: + langIdx = self.projLang.findData(pData.language) + if langIdx == -1: + langIdx = self.projLang.findData("en_GB") + if langIdx != -1: + self.projLang.setCurrentIndex(langIdx) + self.spellLang = QComboBox(self) self.spellLang.setMaximumWidth(xW) self.spellLang.addItem(self.tr("Default"), "None") if CONFIG.hasEnchant: for tag, language in SHARED.spelling.listDictionaries(): self.spellLang.addItem(language, tag) - self.mainForm.addRow( self.tr("Spell check language"), self.spellLang, self.tr("Overrides main preferences.") ) - spellIdx = 0 + langIdx = 0 if pData.spellLang is not None: - spellIdx = self.spellLang.findData(pData.spellLang) - if spellIdx != -1: - self.spellLang.setCurrentIndex(spellIdx) + langIdx = self.spellLang.findData(pData.spellLang) + if langIdx != -1: + self.spellLang.setCurrentIndex(langIdx) self.doBackup = NSwitch(self) self.doBackup.setChecked(not pData.doBackup) @@ -271,8 +288,8 @@ class GuiProjectEditStatus(QWidget): COL_ROLE = Qt.ItemDataRole.UserRole + 1 NUM_ROLE = Qt.ItemDataRole.UserRole + 2 - def __init__(self, projGui, isStatus): - super().__init__(parent=projGui) + def __init__(self, parent: QWidget, isStatus: bool) -> None: + super().__init__(parent=parent) if isStatus: self.theStatus = SHARED.project.data.itemStatus @@ -372,9 +389,8 @@ class GuiProjectEditStatus(QWidget): return - def getNewList(self): - """Return list of entries. - """ + def getNewList(self) -> tuple[list, list]: + """Return list of entries.""" if self.colChanged: newList = [] for n in range(self.listBox.topLevelItemCount()): @@ -394,9 +410,8 @@ class GuiProjectEditStatus(QWidget): ## @pyqtSlot() - def _selectColour(self): - """Open a dialog to select the status icon colour. - """ + def _selectColour(self) -> None: + """Open a dialog to select the status icon colour.""" if self.selColour is not None: newCol = QColorDialog.getColor( self.selColour, self, self.tr("Select Colour") @@ -410,17 +425,15 @@ class GuiProjectEditStatus(QWidget): return @pyqtSlot() - def _newItem(self): - """Create a new status item. - """ + def _newItem(self) -> None: + """Create a new status item.""" self._addItem(None, self.tr("New Item"), (100, 100, 100), 0) self.colChanged = True return @pyqtSlot() - def _delItem(self): - """Delete a status item. - """ + def _delItem(self) -> None: + """Delete a status item.""" selItem = self._getSelectedItem() if isinstance(selItem, QTreeWidgetItem): iRow = self.listBox.indexOfTopLevelItem(selItem) @@ -433,9 +446,8 @@ class GuiProjectEditStatus(QWidget): return @pyqtSlot() - def _saveItem(self): - """Save changes made to a status item. - """ + def _saveItem(self) -> None: + """Save changes made to a status item.""" selItem = self._getSelectedItem() if isinstance(selItem, QTreeWidgetItem): selItem.setText(self.COL_LABEL, simplified(self.editName.text())) @@ -444,11 +456,10 @@ class GuiProjectEditStatus(QWidget): self.selColour.red(), self.selColour.green(), self.selColour.blue() )) self.colChanged = True - return @pyqtSlot() - def _selectedItem(self): + def _selectedItem(self) -> None: """Extract the info of a selected item and populate the settings boxes and button. If no item is selected, clear the form. """ @@ -456,7 +467,6 @@ class GuiProjectEditStatus(QWidget): if isinstance(selItem, QTreeWidgetItem): cols = selItem.data(self.COL_LABEL, self.COL_ROLE) name = selItem.text(self.COL_LABEL) - pixmap = QPixmap(self.iPx, self.iPx) pixmap.fill(QColor(*cols)) self.selColour = QColor(*cols) @@ -464,31 +474,27 @@ class GuiProjectEditStatus(QWidget): self.colButton.setIcon(QIcon(pixmap)) self.editName.selectAll() self.editName.setFocus() - self.editName.setEnabled(True) self.colButton.setEnabled(True) self.saveButton.setEnabled(True) - else: pixmap = QPixmap(self.iPx, self.iPx) pixmap.fill(QColor(100, 100, 100)) self.selColour = QColor(100, 100, 100) self.editName.setText("") self.colButton.setIcon(QIcon(pixmap)) - self.editName.setEnabled(False) self.colButton.setEnabled(False) self.saveButton.setEnabled(False) - return ## # Internal Functions ## - def _addItem(self, key, name, cols, count): - """Add a status item to the list. - """ + def _addItem(self, key: str | None, name: str, + cols: tuple[int, int, int], count: int) -> None: + """Add a status item to the list.""" pixmap = QPixmap(self.iPx, self.iPx) pixmap.fill(QColor(*cols)) @@ -504,9 +510,8 @@ class GuiProjectEditStatus(QWidget): return - def _moveItem(self, step): - """Move and item up or down step. - """ + def _moveItem(self, step: int) -> None: + """Move and item up or down step.""" selItem = self._getSelectedItem() if selItem is None: return @@ -527,17 +532,15 @@ class GuiProjectEditStatus(QWidget): return - def _getSelectedItem(self): - """Get the currently selected item. - """ + def _getSelectedItem(self) -> QTreeWidgetItem | None: + """Get the currently selected item.""" selItem = self.listBox.selectedItems() if len(selItem) > 0: return selItem[0] return None - def _usageString(self, nUse): - """Generate usage string. - """ + def _usageString(self, nUse: int) -> str: + """Generate usage string.""" if nUse == 0: return self.tr("Not in use") elif nUse == 1: @@ -553,8 +556,8 @@ class GuiProjectEditReplace(QWidget): COL_KEY = 0 COL_REPL = 1 - def __init__(self, projGui): - super().__init__(parent=projGui) + def __init__(self, parent: QWidget) -> None: + super().__init__(parent=parent) self.arChanged = False @@ -635,25 +638,23 @@ class GuiProjectEditReplace(QWidget): return - def getNewList(self): - """Extract the list from the widget. - """ - newList = {} + def getNewList(self) -> dict: + """Extract the list from the widget.""" + new = {} for n in range(self.listBox.topLevelItemCount()): tItem = self.listBox.topLevelItem(n) if tItem is not None: aKey = self._stripNotAllowed(tItem.text(0)) aVal = tItem.text(1) if len(aKey) > 0: - newList[aKey] = aVal - - return newList + new[aKey] = aVal + return new ## # Internal Functions ## - def _selectedItem(self): + def _selectedItem(self) -> bool: """Extract the details from the selected item and populate the edit form. """ @@ -670,63 +671,53 @@ class GuiProjectEditReplace(QWidget): self.editKey.setFocus() return True - def _saveEntry(self): - """Save the form data into the list widget. - """ + def _saveEntry(self) -> None: + """Save the form data into the list widget.""" selItem = self._getSelectedItem() - if selItem is None: - return False - - newKey = self.editKey.text() - newVal = self.editValue.text() - saveKey = self._stripNotAllowed(newKey) - - if len(saveKey) > 0 and len(newVal) > 0: - selItem.setText(self.COL_KEY, "<%s>" % saveKey) - selItem.setText(self.COL_REPL, newVal) - self.editKey.clear() - self.editValue.clear() - self.editKey.setEnabled(False) - self.editValue.setEnabled(False) - self.listBox.clearSelection() - self.arChanged = True - + if selItem: + newKey = self.editKey.text() + newVal = self.editValue.text() + saveKey = self._stripNotAllowed(newKey) + if len(saveKey) > 0 and len(newVal) > 0: + selItem.setText(self.COL_KEY, "<%s>" % saveKey) + selItem.setText(self.COL_REPL, newVal) + self.editKey.clear() + self.editValue.clear() + self.editKey.setEnabled(False) + self.editValue.setEnabled(False) + self.listBox.clearSelection() + self.arChanged = True return - def _addEntry(self): - """Add a new list entry. - """ + def _addEntry(self) -> None: + """Add a new list entry.""" saveKey = "" % (self.listBox.topLevelItemCount() + 1) newVal = "" newItem = QTreeWidgetItem([saveKey, newVal]) self.listBox.addTopLevelItem(newItem) - return True + return - def _delEntry(self): - """Delete the selected entry. - """ + def _delEntry(self) -> None: + """Delete the selected entry.""" selItem = self._getSelectedItem() - if selItem is None: - return False - self.listBox.takeTopLevelItem(self.listBox.indexOfTopLevelItem(selItem)) - self.arChanged = True - return True + if selItem: + self.listBox.takeTopLevelItem(self.listBox.indexOfTopLevelItem(selItem)) + self.arChanged = True + return - def _getSelectedItem(self): - """Extract the currently selected item. - """ + def _getSelectedItem(self) -> QTreeWidgetItem | None: + """Extract the currently selected item.""" selItem = self.listBox.selectedItems() if len(selItem) == 0: return None return selItem[0] - def _stripNotAllowed(self, theKey): - """Clean up the replace key string. - """ - retKey = "" - for c in theKey: + def _stripNotAllowed(self, key: str) -> str: + """Clean up the replace key string.""" + result = "" + for c in key: if c.isalnum(): - retKey += c - return retKey + result += c + return result # END Class GuiProjectEditReplace diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index d03302ab..d4743c9b 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -10,6 +10,8 @@ Created: 2020-04-25 [0.4.5] GuiDocEditHeader Rewritten: 2020-06-15 [0.9] GuiDocEditSearch Created: 2020-06-27 [0.10] GuiDocEditFooter Rewritten: 2020-10-07 [1.0b3] BackgroundWordCounter +Created: 2023-11-06 [2.2b1] MetaCompleter +Created: 2023-11-07 [2.2b1] GuiDocToolBar This file is a part of novelWriter Copyright 2018–2023, Veronica Berglyd Olsen @@ -67,6 +69,16 @@ if TYPE_CHECKING: # pragma: no cover logger = logging.getLogger(__name__) +class _SelectAction(Enum): + + NO_DECISION = 0 + KEEP_SELECTION = 1 + KEEP_POSITION = 2 + MOVE_AFTER = 3 + +# END Class _SelectAction + + class GuiDocEditor(QPlainTextEdit): """Gui Widget: Main Document Editor""" @@ -152,7 +164,6 @@ class GuiDocEditor(QPlainTextEdit): self.setMinimumWidth(CONFIG.pxInt(300)) self.setAutoFillBackground(True) self.setFrameStyle(QFrame.NoFrame) - self.setCenterOnScroll(True) # Custom Shortcuts self.keyContext = QShortcut(self) @@ -330,7 +341,8 @@ class GuiDocEditor(QPlainTextEdit): self._qDocument.setDefaultTextOption(options) - # Scroll bars + # Scrolling + self.setCenterOnScroll(CONFIG.scrollPastEnd) if CONFIG.hideVScroll: self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) else: @@ -996,7 +1008,9 @@ class GuiDocEditor(QPlainTextEdit): return text = block.text() - if text.startswith("@"): + if text.startswith("@") and added + removed == 1: + # Only run on single keypresses, otherwise it will trigger + # at unwanted times when other changes are made to the document cursor = self.textCursor() bPos = cursor.positionInBlock() if bPos > 0: @@ -1004,8 +1018,10 @@ class GuiDocEditor(QPlainTextEdit): point = self.cursorRect().bottomRight() self._completer.move(self.viewport().mapToGlobal(point)) self._completer.setVisible(show) + else: + self._completer.setVisible(False) - elif self._doReplace and added == 1: + if self._doReplace and added == 1: self._docAutoReplace(text) return @@ -1410,17 +1426,28 @@ class GuiDocEditor(QPlainTextEdit): If more than one block is selected, the formatting is applied to the first block. """ - cursor = self._autoSelect() - if not cursor.hasSelection(): - logger.warning("No selection made, nothing to do") - return False + cursor = self.textCursor() + posO = cursor.position() + if cursor.hasSelection(): + select = _SelectAction.KEEP_SELECTION + else: + cursor = self._autoSelect() + if cursor.hasSelection() and posO == cursor.selectionEnd(): + select = _SelectAction.MOVE_AFTER + else: + select = _SelectAction.KEEP_POSITION posS = cursor.selectionStart() posE = cursor.selectionEnd() + if self._qDocument.characterAt(posO - 1) == fChar: + logger.warning("Format repetition, cancelling action") + cursor.clearSelection() + cursor.setPosition(posO) + self.setTextCursor(cursor) + return False blockS = self._qDocument.findBlock(posS) blockE = self._qDocument.findBlock(posE) - if blockS != blockE: posE = blockS.position() + blockS.length() - 1 cursor.clearSelection() @@ -1443,34 +1470,26 @@ class GuiDocEditor(QPlainTextEdit): break if fLen == min(numA, numB): - self._clearSurrounding(cursor, fLen) + cursor.clearSelection() + cursor.beginEditBlock() + cursor.setPosition(posS) + for i in range(fLen): + cursor.deletePreviousChar() + cursor.setPosition(posE) + for i in range(fLen): + cursor.deletePreviousChar() + cursor.endEditBlock() + cursor.clearSelection() + cursor.setPosition(posO - fLen) + self.setTextCursor(cursor) + else: - self._wrapSelection(fChar*fLen) + self._wrapSelection(fChar*fLen, pos=posO, select=select) return True - def _clearSurrounding(self, cursor: QTextCursor, nChars: int) -> bool: - """Clear n characters before and after the cursor.""" - if not cursor.hasSelection(): - logger.warning("No selection made, nothing to do") - return False - - posS = cursor.selectionStart() - posE = cursor.selectionEnd() - cursor.clearSelection() - cursor.beginEditBlock() - cursor.setPosition(posS) - for i in range(nChars): - cursor.deletePreviousChar() - cursor.setPosition(posE) - for i in range(nChars): - cursor.deletePreviousChar() - cursor.endEditBlock() - cursor.clearSelection() - - return True - - def _wrapSelection(self, before: str, after: str | None = None) -> bool: + def _wrapSelection(self, before: str, after: str | None = None, pos: int | None = None, + select: _SelectAction = _SelectAction.NO_DECISION) -> bool: """Wrap the selected text in whatever is in tBefore and tAfter. If there is no selection, the autoSelect setting decides the action. AutoSelect will select the word under the cursor before @@ -1479,10 +1498,17 @@ class GuiDocEditor(QPlainTextEdit): if after is None: after = before - cursor = self._autoSelect() - if not cursor.hasSelection(): - logger.warning("No selection made, nothing to do") - return False + cursor = self.textCursor() + posO = pos if isinstance(pos, int) else cursor.position() + if select == _SelectAction.NO_DECISION: + if cursor.hasSelection(): + select = _SelectAction.KEEP_SELECTION + else: + cursor = self._autoSelect() + if cursor.hasSelection() and posO == cursor.selectionEnd(): + select = _SelectAction.MOVE_AFTER + else: + select = _SelectAction.KEEP_POSITION posS = cursor.selectionStart() posE = cursor.selectionEnd() @@ -1500,8 +1526,14 @@ class GuiDocEditor(QPlainTextEdit): cursor.insertText(before) cursor.endEditBlock() - cursor.setPosition(posE + len(before), QTextCursor.MoveAnchor) - cursor.setPosition(posS + len(before), QTextCursor.KeepAnchor) + if select == _SelectAction.MOVE_AFTER: + cursor.setPosition(posE + len(before + after)) + elif select == _SelectAction.KEEP_SELECTION: + cursor.setPosition(posE + len(before), QTextCursor.MoveMode.MoveAnchor) + cursor.setPosition(posS + len(before), QTextCursor.MoveMode.KeepAnchor) + elif select == _SelectAction.KEEP_POSITION: + cursor.setPosition(posO + len(before)) + self.setTextCursor(cursor) return True @@ -1908,27 +1940,38 @@ class GuiDocEditor(QPlainTextEdit): def _autoSelect(self) -> QTextCursor: """Return a cursor which may or may not have a selection based - on user settings and document action. + on user settings and document action. The selection will be the + word closest to the cursor consisting of alphanumerical unicode + characters. """ cursor = self.textCursor() if CONFIG.autoSelect and not cursor.hasSelection(): - cursor.select(QTextCursor.WordUnderCursor) - posS = cursor.selectionStart() - posE = cursor.selectionEnd() + cPos = cursor.position() + bPos = cursor.block().position() + bLen = cursor.block().length() - # Underscore counts as a part of the word, so check that the - # selection isn't wrapped in italics markers. - reSelect = False - if self._qDocument.characterAt(posS) == "_": - posS += 1 - reSelect = True - if self._qDocument.characterAt(posE) == "_": - posE -= 1 - reSelect = True - if reSelect: - cursor.clearSelection() - cursor.setPosition(posS, QTextCursor.MoveAnchor) - cursor.setPosition(posE-1, QTextCursor.KeepAnchor) + # Scan backwards + sPos = cPos + for i in range(cPos - bPos): + sPos = cPos - i - 1 + if not self._qDocument.characterAt(sPos).isalnum(): + sPos += 1 + break + + # Scan forwards + ePos = cPos + for i in range(bPos + bLen - cPos): + ePos = cPos + i + if not self._qDocument.characterAt(ePos).isalnum(): + break + + if ePos - sPos <= 0: + # No selection possible + return cursor + + cursor.clearSelection() + cursor.setPosition(sPos, QTextCursor.MoveAnchor) + cursor.setPosition(ePos, QTextCursor.KeepAnchor) self.setTextCursor(cursor) diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index e6453a82..a0420144 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -54,6 +54,7 @@ class GuiMainMenu(QMenuBar): requestDocInsert = pyqtSignal(nwDocInsert) requestDocInsertText = pyqtSignal(str) requestDocKeyWordInsert = pyqtSignal(str) + requestFocusChange = pyqtSignal(nwWidget) def __init__(self, mainGui: GuiMain) -> None: super().__init__(parent=mainGui) @@ -173,7 +174,7 @@ class GuiMainMenu(QMenuBar): # Project > Delete self.aDeleteItem = self.projMenu.addAction(self.tr("Delete Item")) - self.aDeleteItem.setShortcut("Ctrl+Shift+Del") + self.aDeleteItem.setShortcuts(["Ctrl+Del", "Ctrl+Shift+Del"]) # Latter is deprecated self.aDeleteItem.triggered.connect(lambda: self.mainGui.projView.requestDeleteItem(None)) # Project > Empty Trash @@ -246,12 +247,16 @@ class GuiMainMenu(QMenuBar): # Edit > Undo self.aEditUndo = self.editMenu.addAction(self.tr("Undo")) self.aEditUndo.setShortcut("Ctrl+Z") - self.aEditUndo.triggered.connect(lambda: self.requestDocAction.emit(nwDocAction.UNDO)) + self.aEditUndo.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.UNDO) + ) # Edit > Redo self.aEditRedo = self.editMenu.addAction(self.tr("Redo")) self.aEditRedo.setShortcut("Ctrl+Y") - self.aEditRedo.triggered.connect(lambda: self.requestDocAction.emit(nwDocAction.REDO)) + self.aEditRedo.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.REDO) + ) # Edit > Separator self.editMenu.addSeparator() @@ -259,17 +264,23 @@ class GuiMainMenu(QMenuBar): # Edit > Cut self.aEditCut = self.editMenu.addAction(self.tr("Cut")) self.aEditCut.setShortcut("Ctrl+X") - self.aEditCut.triggered.connect(lambda: self.requestDocAction.emit(nwDocAction.CUT)) + self.aEditCut.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.CUT) + ) # Edit > Copy self.aEditCopy = self.editMenu.addAction(self.tr("Copy")) self.aEditCopy.setShortcut("Ctrl+C") - self.aEditCopy.triggered.connect(lambda: self.requestDocAction.emit(nwDocAction.COPY)) + self.aEditCopy.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.COPY) + ) # Edit > Paste self.aEditPaste = self.editMenu.addAction(self.tr("Paste")) self.aEditPaste.setShortcut("Ctrl+V") - self.aEditPaste.triggered.connect(lambda: self.requestDocAction.emit(nwDocAction.PASTE)) + self.aEditPaste.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.PASTE) + ) # Edit > Separator self.editMenu.addSeparator() @@ -277,12 +288,16 @@ class GuiMainMenu(QMenuBar): # Edit > Select All self.aSelectAll = self.editMenu.addAction(self.tr("Select All")) self.aSelectAll.setShortcut("Ctrl+A") - self.aSelectAll.triggered.connect(lambda: self.requestDocAction.emit(nwDocAction.SEL_ALL)) + self.aSelectAll.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.SEL_ALL) + ) # Edit > Select Paragraph self.aSelectPar = self.editMenu.addAction(self.tr("Select Paragraph")) self.aSelectPar.setShortcut("Ctrl+Shift+A") - self.aSelectPar.triggered.connect(lambda: self.requestDocAction.emit(nwDocAction.SEL_PARA)) + self.aSelectPar.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.SEL_PARA) + ) return @@ -293,23 +308,24 @@ class GuiMainMenu(QMenuBar): # View > TreeView self.aFocusTree = self.viewMenu.addAction(self.tr("Go to Project Tree")) - self.aFocusTree.setShortcut("Ctrl+Alt+1" if CONFIG.osWindows else "Alt+1") - self.aFocusTree.triggered.connect(lambda: self.mainGui.switchFocus(nwWidget.TREE)) + self.aFocusTree.setShortcut("Ctrl+T") + self.aFocusTree.triggered.connect( + lambda: self.requestFocusChange.emit(nwWidget.TREE) + ) - # View > Document Pane 1 + # View > Document Editor self.aFocusEditor = self.viewMenu.addAction(self.tr("Go to Document Editor")) - self.aFocusEditor.setShortcut("Ctrl+Alt+2" if CONFIG.osWindows else "Alt+2") - self.aFocusEditor.triggered.connect(lambda: self.mainGui.switchFocus(nwWidget.EDITOR)) - - # View > Document Pane 2 - self.aFocusView = self.viewMenu.addAction(self.tr("Go to Document Viewer")) - self.aFocusView.setShortcut("Ctrl+Alt+3" if CONFIG.osWindows else "Alt+3") - self.aFocusView.triggered.connect(lambda: self.mainGui.switchFocus(nwWidget.VIEWER)) + self.aFocusEditor.setShortcut("Ctrl+E") + self.aFocusEditor.triggered.connect( + lambda: self.requestFocusChange.emit(nwWidget.EDITOR) + ) # View > Outline self.aFocusOutline = self.viewMenu.addAction(self.tr("Go to Outline")) - self.aFocusOutline.setShortcut("Ctrl+Alt+4" if CONFIG.osWindows else "Alt+4") - self.aFocusOutline.triggered.connect(lambda: self.mainGui.switchFocus(nwWidget.OUTLINE)) + self.aFocusOutline.setShortcut("Ctrl+Shift+T") + self.aFocusOutline.triggered.connect( + lambda: self.requestFocusChange.emit(nwWidget.OUTLINE) + ) # View > Separator self.viewMenu.addSeparator() diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index d1c2f903..b52680d7 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -189,7 +189,7 @@ class GuiProjectView(QWidget): self.projTree.buildTree() return - def setFocus(self) -> None: + def setTreeFocus(self) -> None: """Forward the set focus call to the tree widget.""" self.projTree.setFocus() return diff --git a/novelwriter/gui/sidebar.py b/novelwriter/gui/sidebar.py index be719a42..9cfbf118 100644 --- a/novelwriter/gui/sidebar.py +++ b/novelwriter/gui/sidebar.py @@ -57,32 +57,32 @@ class GuiSideBar(QWidget): # Buttons self.tbProject = QToolButton(self) - self.tbProject.setToolTip(self.tr("Project Tree View")) + self.tbProject.setToolTip("{0} [Ctrl+T]".format(self.tr("Project Tree View"))) self.tbProject.setIconSize(iconSize) self.tbProject.clicked.connect(lambda: self.viewChangeRequested.emit(nwView.PROJECT)) self.tbNovel = QToolButton(self) - self.tbNovel.setToolTip(self.tr("Novel Tree View")) + self.tbNovel.setToolTip("{0} [Ctrl+T]".format(self.tr("Novel Tree View"))) self.tbNovel.setIconSize(iconSize) self.tbNovel.clicked.connect(lambda: self.viewChangeRequested.emit(nwView.NOVEL)) self.tbOutline = QToolButton(self) - self.tbOutline.setToolTip(self.tr("Novel Outline View")) + self.tbOutline.setToolTip("{0} [Ctrl+Shift+T]".format(self.tr("Novel Outline View"))) self.tbOutline.setIconSize(iconSize) self.tbOutline.clicked.connect(lambda: self.viewChangeRequested.emit(nwView.OUTLINE)) self.tbBuild = QToolButton(self) - self.tbBuild.setToolTip(self.tr("Build Manuscript")) + self.tbBuild.setToolTip("{0} [F5]".format(self.tr("Build Manuscript"))) self.tbBuild.setIconSize(iconSize) self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog) self.tbDetails = QToolButton(self) - self.tbDetails.setToolTip(self.tr("Project Details")) + self.tbDetails.setToolTip("{0} [Shift+F6]".format(self.tr("Project Details"))) self.tbDetails.setIconSize(iconSize) self.tbDetails.clicked.connect(self.mainGui.showProjectDetailsDialog) self.tbStats = QToolButton(self) - self.tbStats.setToolTip(self.tr("Writing Statistics")) + self.tbStats.setToolTip("{0} [F6]".format(self.tr("Writing Statistics"))) self.tbStats.setIconSize(iconSize) self.tbStats.clicked.connect(self.mainGui.showWritingStatsDialog) @@ -111,7 +111,7 @@ class GuiSideBar(QWidget): self.outerBox.addWidget(self.tbDetails) self.outerBox.addWidget(self.tbStats) self.outerBox.addWidget(self.tbSettings) - self.outerBox.setContentsMargins(0, 0, CONFIG.pxInt(2), 0) + self.outerBox.setContentsMargins(0, 0, 0, 0) self.outerBox.setSpacing(CONFIG.pxInt(4)) self.setLayout(self.outerBox) @@ -131,7 +131,7 @@ class GuiSideBar(QWidget): buttonStyle = ( "QToolButton {{padding: {0}px; border: none; background: transparent;}} " "QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}" - ).format(CONFIG.pxInt(4), fadeCol.red(), fadeCol.green(), fadeCol.blue()) + ).format(CONFIG.pxInt(6), fadeCol.red(), fadeCol.green(), fadeCol.blue()) buttonStyleMenu = f"{buttonStyle} QToolButton::menu-indicator {{image: none;}}" self.tbProject.setIcon(SHARED.theme.getIcon("view_editor")) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 41301476..c672a1f2 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -31,7 +31,7 @@ from pathlib import Path from datetime import datetime from PyQt5.QtCore import Qt, QTimer, pyqtSlot -from PyQt5.QtGui import QCloseEvent, QCursor, QIcon, QKeySequence +from PyQt5.QtGui import QCloseEvent, QCursor, QIcon from PyQt5.QtWidgets import ( QDialog, QFileDialog, QHBoxLayout, QMainWindow, QMessageBox, QShortcut, QSplitter, QStackedWidget, QVBoxLayout, QWidget, qApp @@ -247,6 +247,7 @@ class GuiMain(QMainWindow): self.mainMenu.requestDocInsert.connect(self._passDocumentInsert) self.mainMenu.requestDocInsertText.connect(self._passDocumentInsert) self.mainMenu.requestDocKeyWordInsert.connect(self.docEditor.insertKeyWord) + self.mainMenu.requestFocusChange.connect(self.switchFocus) self.sideBar.viewChangeRequested.connect(self._changeView) @@ -300,17 +301,17 @@ class GuiMain(QMainWindow): # Shortcuts and Actions self._connectMenuActions() - keyReturn = QShortcut(self) - keyReturn.setKey(QKeySequence(Qt.Key_Return)) - keyReturn.activated.connect(self._keyPressReturn) + self.keyReturn = QShortcut(self) + self.keyReturn.setKey(Qt.Key.Key_Return) + self.keyReturn.activated.connect(self._keyPressReturn) - keyEnter = QShortcut(self) - keyEnter.setKey(QKeySequence(Qt.Key_Enter)) - keyEnter.activated.connect(self._keyPressReturn) + self.keyEnter = QShortcut(self) + self.keyEnter.setKey(Qt.Key.Key_Enter) + self.keyEnter.activated.connect(self._keyPressReturn) - keyEscape = QShortcut(self) - keyEscape.setKey(QKeySequence(Qt.Key_Escape)) - keyEscape.activated.connect(self._keyPressEscape) + self.keyEscape = QShortcut(self) + self.keyEscape.setKey(Qt.Key.Key_Escape) + self.keyEscape.activated.connect(self._keyPressEscape) # Check that config loaded fine self.reportConfErr() @@ -1093,25 +1094,6 @@ class GuiMain(QMainWindow): return True - def switchFocus(self, paneNo: nwWidget) -> None: - """Switch focus between main GUI views.""" - if paneNo == nwWidget.TREE: - tabIdx = self.projStack.currentIndex() - if tabIdx == self.idxProjView: - self.projView.setFocus() - elif tabIdx == self.idxNovelView: - self.novelView.setTreeFocus() - elif paneNo == nwWidget.EDITOR: - self._changeView(nwView.EDITOR) - self.docEditor.setFocus() - elif paneNo == nwWidget.VIEWER: - self._changeView(nwView.EDITOR) - self.docViewer.setFocus() - elif paneNo == nwWidget.OUTLINE: - self._changeView(nwView.OUTLINE) - self.outlineView.setTreeFocus() - return - def closeDocViewer(self, byUser: bool = True) -> bool: """Close the document view panel.""" self.docViewer.clearViewer() @@ -1189,6 +1171,33 @@ class GuiMain(QMainWindow): return + @pyqtSlot(nwWidget) + def switchFocus(self, paneNo: nwWidget) -> None: + """Switch focus between main GUI views.""" + if paneNo == nwWidget.TREE: + if self.projStack.currentWidget() is self.projView: + if self.projView.treeHasFocus(): + self._changeView(nwView.NOVEL) + self.novelView.setTreeFocus() + else: + self.projView.setTreeFocus() + else: + if self.novelView.treeHasFocus(): + self._changeView(nwView.PROJECT) + self.projView.setTreeFocus() + else: + self.novelView.setTreeFocus() + elif paneNo == nwWidget.EDITOR: + self._changeView(nwView.EDITOR) + self.docEditor.setFocus() + elif paneNo == nwWidget.VIEWER: + self._changeView(nwView.EDITOR) + self.docViewer.setFocus() + elif paneNo == nwWidget.OUTLINE: + self._changeView(nwView.OUTLINE) + self.outlineView.setTreeFocus() + return + ## # Private Slots ## @@ -1224,18 +1233,14 @@ class GuiMain(QMainWindow): if view == nwView.EDITOR: # Only change the main stack, but not the project stack self.mainStack.setCurrentWidget(self.splitMain) - elif view == nwView.PROJECT: self.mainStack.setCurrentWidget(self.splitMain) self.projStack.setCurrentWidget(self.projView) - elif view == nwView.NOVEL: self.mainStack.setCurrentWidget(self.splitMain) self.projStack.setCurrentWidget(self.novelView) - elif view == nwView.OUTLINE: self.mainStack.setCurrentWidget(self.outlineView) - return @pyqtSlot(nwDocAction) @@ -1482,6 +1487,12 @@ class GuiMain(QMainWindow): projData["numChapters"] = newProj.field("numChapters") projData["numScenes"] = newProj.field("numScenes") + try: + langIdx = newProj.field("projLang") + projData["projLang"] = CONFIG.listLanguages(CONFIG.LANG_PROJ)[langIdx][0] + except Exception: + projData["projLang"] = "en_GB" + return projData def _getTagSource(self, tag: str) -> tuple[str | None, str | None]: diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py index 3a8e2f91..34e7f2cf 100644 --- a/novelwriter/tools/manuscript.py +++ b/novelwriter/tools/manuscript.py @@ -144,6 +144,8 @@ class GuiManuscript(QDialog): self.buildList.setIconSize(QSize(iPx, iPx)) self.buildList.doubleClicked.connect(self._editSelectedBuild) self.buildList.currentItemChanged.connect(self._updateBuildDetails) + self.buildList.setSelectionMode(QAbstractItemView.SingleSelection) + self.buildList.setDragDropMode(QAbstractItemView.InternalMove) self.buildDetails = _DetailsWidget(self) self.buildDetails.setColumnWidth( @@ -417,9 +419,15 @@ class GuiManuscript(QDialog): """Save the user GUI settings.""" logger.debug("Saving GuiManuscript settings") + buildOrder = [] + for i in range(self.buildList.count()): + if item := self.buildList.item(i): + buildOrder.append(item.data(self.D_KEY)) + current = self.buildList.currentItem() - if isinstance(current, QListWidgetItem): - self._builds.setLastBuild(current.data(self.D_KEY)) + lastBuild = current.data(self.D_KEY) if isinstance(current, QListWidgetItem) else "" + + self._builds.setBuildsState(lastBuild, buildOrder) winWidth = CONFIG.rpxInt(self.width()) winHeight = CONFIG.rpxInt(self.height()) diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index cfa1b038..95235424 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -974,15 +974,6 @@ class _FormatTab(QWidget): self.formFormat = NConfigLayout() self.formFormat.addGroupLabel(self._build.getLabel("format.grpFormat")) - # Build Language - self.buildLang = QComboBox() - langauges = CONFIG.listLanguages(CONFIG.LANG_PROJ) - self.buildLang.addItem("[%s]" % self.tr("Not Set"), "None") - for langID, langName in langauges: - self.buildLang.addItem(langName, langID) - - self.formFormat.addRow(self._build.getLabel("format.buildLang"), self.buildLang) - # Font Family self.textFont = QLineEdit() self.textFont.setReadOnly(True) @@ -1103,10 +1094,6 @@ class _FormatTab(QWidget): def loadContent(self) -> None: """Populate the widgets.""" - langIdx = self.buildLang.findData(self._build.getStr("format.buildLang")) - if langIdx != -1: - self.buildLang.setCurrentIndex(langIdx) - textFont = self._build.getStr("format.textFont") if not textFont: textFont = str(CONFIG.textFont) @@ -1146,7 +1133,6 @@ class _FormatTab(QWidget): def saveContent(self) -> None: """Save choices back into build object.""" - self._build.setValue("format.buildLang", str(self.buildLang.currentData())) self._build.setValue("format.textFont", self.textFont.text()) self._build.setValue("format.textSize", self.textSize.value()) self._build.setValue("format.lineHeight", self.lineHeight.value()) diff --git a/novelwriter/tools/projwizard.py b/novelwriter/tools/projwizard.py index ef715847..b47da1cf 100644 --- a/novelwriter/tools/projwizard.py +++ b/novelwriter/tools/projwizard.py @@ -28,8 +28,9 @@ import logging from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( - QFileDialog, QFormLayout, QGridLayout, QHBoxLayout, QLabel, QLineEdit, - QPushButton, QRadioButton, QSpinBox, QVBoxLayout, QWizard, QWizardPage + QComboBox, QFileDialog, QFormLayout, QGridLayout, QHBoxLayout, QLabel, + QLineEdit, QPushButton, QRadioButton, QSpinBox, QVBoxLayout, QWizard, + QWizardPage ) from novelwriter import CONFIG, SHARED @@ -127,15 +128,28 @@ class ProjWizardIntroPage(QWizardPage): self.projAuthor.setFixedWidth(xW) self.projAuthor.setPlaceholderText(self.tr("Optional")) + self.projLang = QComboBox(self) + self.projLang.setMaximumWidth(xW) + for tag, language in CONFIG.listLanguages(CONFIG.LANG_PROJ): + self.projLang.addItem(language, tag) + + langIdx = self.projLang.findData(CONFIG.guiLocale) + if langIdx == -1: + langIdx = self.projLang.findData("en_GB") + if langIdx != -1: + self.projLang.setCurrentIndex(langIdx) + self.mainForm = QFormLayout() self.mainForm.addRow(self.tr("Project Name"), self.projName) self.mainForm.addRow(self.tr("Novel Title"), self.projTitle) self.mainForm.addRow(self.tr("Author(s)"), self.projAuthor) + self.mainForm.addRow(self.tr("Language"), self.projLang) self.mainForm.setVerticalSpacing(fS) self.registerField("projName*", self.projName) self.registerField("projTitle", self.projTitle) self.registerField("projAuthor", self.projAuthor) + self.registerField("projLang", self.projLang) # Assemble self.outerBox = QVBoxLayout() diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 67b86c1f..f2f09e9c 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,6 +1,6 @@ - - + + Sample Project Sample Project Jane Smith @@ -36,7 +36,7 @@ Main - + Novel @@ -46,7 +46,7 @@ Title Page - + Page @@ -58,7 +58,7 @@ Chapter One - + Making a Scene diff --git a/setup/data/hicolor/128x128/apps/novelwriter.png b/setup/data/hicolor/128x128/apps/novelwriter.png index 3bef1318..a4bd8da4 100644 Binary files a/setup/data/hicolor/128x128/apps/novelwriter.png and b/setup/data/hicolor/128x128/apps/novelwriter.png differ diff --git a/setup/data/hicolor/128x128/mimetypes/application-x-novelwriter-project.png b/setup/data/hicolor/128x128/mimetypes/application-x-novelwriter-project.png index dbaac947..60d25560 100644 Binary files a/setup/data/hicolor/128x128/mimetypes/application-x-novelwriter-project.png and b/setup/data/hicolor/128x128/mimetypes/application-x-novelwriter-project.png differ diff --git a/setup/data/hicolor/16x16/apps/novelwriter.png b/setup/data/hicolor/16x16/apps/novelwriter.png index 20a90acf..4eb4c801 100644 Binary files a/setup/data/hicolor/16x16/apps/novelwriter.png and b/setup/data/hicolor/16x16/apps/novelwriter.png differ diff --git a/setup/data/hicolor/16x16/mimetypes/application-x-novelwriter-project.png b/setup/data/hicolor/16x16/mimetypes/application-x-novelwriter-project.png index 132b2f18..983235cd 100644 Binary files a/setup/data/hicolor/16x16/mimetypes/application-x-novelwriter-project.png and b/setup/data/hicolor/16x16/mimetypes/application-x-novelwriter-project.png differ diff --git a/setup/data/hicolor/24x24/apps/novelwriter.png b/setup/data/hicolor/24x24/apps/novelwriter.png index 0b89ac89..4f096a3c 100644 Binary files a/setup/data/hicolor/24x24/apps/novelwriter.png and b/setup/data/hicolor/24x24/apps/novelwriter.png differ diff --git a/setup/data/hicolor/24x24/mimetypes/application-x-novelwriter-project.png b/setup/data/hicolor/24x24/mimetypes/application-x-novelwriter-project.png index be91bcd5..3531fd06 100644 Binary files a/setup/data/hicolor/24x24/mimetypes/application-x-novelwriter-project.png and b/setup/data/hicolor/24x24/mimetypes/application-x-novelwriter-project.png differ diff --git a/setup/data/hicolor/256x256/apps/novelwriter.png b/setup/data/hicolor/256x256/apps/novelwriter.png index 143b1ef8..a9ec557a 100644 Binary files a/setup/data/hicolor/256x256/apps/novelwriter.png and b/setup/data/hicolor/256x256/apps/novelwriter.png differ diff --git a/setup/data/hicolor/256x256/mimetypes/application-x-novelwriter-project.png b/setup/data/hicolor/256x256/mimetypes/application-x-novelwriter-project.png index ce82f04b..9186479b 100644 Binary files a/setup/data/hicolor/256x256/mimetypes/application-x-novelwriter-project.png and b/setup/data/hicolor/256x256/mimetypes/application-x-novelwriter-project.png differ diff --git a/setup/data/hicolor/32x32/apps/novelwriter.png b/setup/data/hicolor/32x32/apps/novelwriter.png index d0046519..66c55c1f 100644 Binary files a/setup/data/hicolor/32x32/apps/novelwriter.png and b/setup/data/hicolor/32x32/apps/novelwriter.png differ diff --git a/setup/data/hicolor/32x32/mimetypes/application-x-novelwriter-project.png b/setup/data/hicolor/32x32/mimetypes/application-x-novelwriter-project.png index 32fc47cc..491b1bc7 100644 Binary files a/setup/data/hicolor/32x32/mimetypes/application-x-novelwriter-project.png and b/setup/data/hicolor/32x32/mimetypes/application-x-novelwriter-project.png differ diff --git a/setup/data/hicolor/48x48/apps/novelwriter.png b/setup/data/hicolor/48x48/apps/novelwriter.png index 3d3fce1d..669a1624 100644 Binary files a/setup/data/hicolor/48x48/apps/novelwriter.png and b/setup/data/hicolor/48x48/apps/novelwriter.png differ diff --git a/setup/data/hicolor/48x48/mimetypes/application-x-novelwriter-project.png b/setup/data/hicolor/48x48/mimetypes/application-x-novelwriter-project.png index 5fd7c22a..dddd247a 100644 Binary files a/setup/data/hicolor/48x48/mimetypes/application-x-novelwriter-project.png and b/setup/data/hicolor/48x48/mimetypes/application-x-novelwriter-project.png differ diff --git a/setup/data/hicolor/512x512/apps/novelwriter.png b/setup/data/hicolor/512x512/apps/novelwriter.png new file mode 100644 index 00000000..f86f49bb Binary files /dev/null and b/setup/data/hicolor/512x512/apps/novelwriter.png differ diff --git a/setup/data/hicolor/512x512/mimetypes/application-x-novelwriter-project.png b/setup/data/hicolor/512x512/mimetypes/application-x-novelwriter-project.png new file mode 100644 index 00000000..55c25651 Binary files /dev/null and b/setup/data/hicolor/512x512/mimetypes/application-x-novelwriter-project.png differ diff --git a/setup/data/hicolor/64x64/apps/novelwriter.png b/setup/data/hicolor/64x64/apps/novelwriter.png index 57f5e307..a60b8288 100644 Binary files a/setup/data/hicolor/64x64/apps/novelwriter.png and b/setup/data/hicolor/64x64/apps/novelwriter.png differ diff --git a/setup/data/hicolor/64x64/mimetypes/application-x-novelwriter-project.png b/setup/data/hicolor/64x64/mimetypes/application-x-novelwriter-project.png index d1382130..7e1a3635 100644 Binary files a/setup/data/hicolor/64x64/mimetypes/application-x-novelwriter-project.png and b/setup/data/hicolor/64x64/mimetypes/application-x-novelwriter-project.png differ diff --git a/setup/data/hicolor/scalable/apps/novelwriter.svg b/setup/data/hicolor/scalable/apps/novelwriter.svg index 18be89c1..a161cd19 100644 --- a/setup/data/hicolor/scalable/apps/novelwriter.svg +++ b/setup/data/hicolor/scalable/apps/novelwriter.svg @@ -1,184 +1,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - + + + + + + + + diff --git a/setup/data/hicolor/scalable/mimetypes/application-x-novelwriter-project.svg b/setup/data/hicolor/scalable/mimetypes/application-x-novelwriter-project.svg index 08546d15..dc577083 100644 --- a/setup/data/hicolor/scalable/mimetypes/application-x-novelwriter-project.svg +++ b/setup/data/hicolor/scalable/mimetypes/application-x-novelwriter-project.svg @@ -1,207 +1,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - + + + + + + + diff --git a/setup/data/novelwriter.png b/setup/data/novelwriter.png index 3d3fce1d..669a1624 100644 Binary files a/setup/data/novelwriter.png and b/setup/data/novelwriter.png differ diff --git a/setup/description_pypi.md b/setup/description_pypi.md index 9ee924c8..1bd8cfef 100644 --- a/setup/description_pypi.md +++ b/setup/description_pypi.md @@ -10,9 +10,9 @@ synchronisation tools. All text is saved as plain text files with a meta data he project structure is stored in a single project XML file, and other meta data is primarily saved as JSON files. -The application is written with Python 3 (3.8+) using Qt5 and PyQt5 (5.3+). It is developed on +The application is written with Python 3 (3.8+) using Qt5 and PyQt5 (5.10+). It is developed on Linux, but should in principle work fine on other operating systems as well as long as dependencies -are met. It is regularly tested on Debian and Ubuntu Linux, Windows, and macOS. +are met. It is regularly tested on Debian and Ubuntu Linux, Windows, and MacOS. novelWriter is developed and maintained by [Veronica Berglyd Olsen](https://github.com/vkbo). diff --git a/setup/icons/novelwriter.ico b/setup/icons/novelwriter.ico index 24de4e24..54301ffe 100644 Binary files a/setup/icons/novelwriter.ico and b/setup/icons/novelwriter.ico differ diff --git a/setup/icons/novelwriter.svg b/setup/icons/novelwriter.svg index 18be89c1..a161cd19 100644 --- a/setup/icons/novelwriter.svg +++ b/setup/icons/novelwriter.svg @@ -1,184 +1,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - + + + + + + + + diff --git a/setup/icons/x-novelwriter-project.ico b/setup/icons/x-novelwriter-project.ico index c4937b3b..ab97bb2b 100644 Binary files a/setup/icons/x-novelwriter-project.ico and b/setup/icons/x-novelwriter-project.ico differ diff --git a/setup/icons/x-novelwriter-project.svg b/setup/icons/x-novelwriter-project.svg index 08546d15..dc577083 100644 --- a/setup/icons/x-novelwriter-project.svg +++ b/setup/icons/x-novelwriter-project.svg @@ -1,207 +1,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - + + + + + + + diff --git a/setup/macos/build.sh b/setup/macos/build.sh index 9a933972..dbc9834e 100755 --- a/setup/macos/build.sh +++ b/setup/macos/build.sh @@ -62,6 +62,14 @@ fi echo "Content of current dir:" ls -lah . +# Create icon +echo "Creating icon ..." +pushd "$SRC_DIR/setup/macos" || exit 1 +iconutil -c icns $SRC_DIR/setup/macos/novelwriter.iconset +echo "Content of current dir:" +ls -lah . + +popd || exit 1 popd || exit 1 pushd "$BUILD_DIR"/ || exit 1 diff --git a/setup/macos/generate_icns.sh b/setup/macos/generate_icns.sh deleted file mode 100755 index 0ca1d9c0..00000000 --- a/setup/macos/generate_icns.sh +++ /dev/null @@ -1,70 +0,0 @@ -#! /usr/bin/env bash - -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) - -function generate_linux () { - echo "Generating on Linux ..." - if ! command -v png2icns &> /dev/null - then - echo "png2icns cound not be found, it is required. Please install a package like icnsutils or libicns." - exit - fi - - if ! command -v convert $> /dev/null - then - echo "convert could not be found, it is required. please install a imagemagick package." - exit - fi - - mkdir -p $SCRIPT_DIR/icons - - sizes=( 16 32 64 128 256 ) - for base in "${sizes[@]}"; do - let basex2=${base}*2 - size="${base}x${base}" - echo "Copying files for $size" - double=${basex2}x${basex2} - cp $SCRIPT_DIR/../data/hicolor/${size}/apps/novelwriter.png $SCRIPT_DIR/icons/icon_${size}.png - echo "Resizing ${size}@2x to $double from $size" - convert $SCRIPT_DIR/icons/icon_${size}.png -resize $double $SCRIPT_DIR/icons/icon_${size}@2x.png - done - - png2icns $SCRIPT_DIR/novelwriter.icns $SCRIPT_DIR/icons/icon_*.png - - rm -r $SCRIPT_DIR/icons - - echo "Done" -} - -function generate_macos () { - echo "Generating on MacOs ..." - mkdir -p $SCRIPT_DIR/icons - - echo "Building Iconset ..." - sizes=( 16 32 64 128 256 ) - for base in "${sizes[@]}"; do - let basex2=${base}*2 - size="${base}x${base}" - echo "Copying files for $size" - double=${basex2}x${basex2} - cp $SCRIPT_DIR/../data/hicolor/${size}/apps/novelwriter.png $SCRIPT_DIR/icons/icon_${size}.png - cp $SCRIPT_DIR/../data/hicolor/${size}/apps/novelwriter.png $SCRIPT_DIR/icons/icon_${size}@2x.png - echo "Resizing ${size}@2x to $double from $size" - sips -Z $basex2 $SCRIPT_DIR/icons/icon_${size}@2x.png - done - - rm -rf $SCRIPT_DIR/novelwriter.iconset - mv $SCRIPT_DIR/icons $SCRIPT_DIR/novelwriter.iconset - - echo "Generating icns ..." - iconutil -c icns $SCRIPT_DIR/novelwriter.iconset - - echo "Done" -} - -unameOut="$(uname -s)" -case "$unameOut" in - Linux*) generate_linux;; - Darwin*) generate_macos;; - *) echo "Unsupported OS" -esac diff --git a/setup/macos/novelwriter.icns b/setup/macos/novelwriter.icns deleted file mode 100644 index 884347bf..00000000 Binary files a/setup/macos/novelwriter.icns and /dev/null differ diff --git a/setup/macos/novelwriter.iconset/icon_128x128.png b/setup/macos/novelwriter.iconset/icon_128x128.png new file mode 100644 index 00000000..37018293 Binary files /dev/null and b/setup/macos/novelwriter.iconset/icon_128x128.png differ diff --git a/setup/macos/novelwriter.iconset/icon_128x128@2x.png b/setup/macos/novelwriter.iconset/icon_128x128@2x.png new file mode 100644 index 00000000..317ac312 Binary files /dev/null and b/setup/macos/novelwriter.iconset/icon_128x128@2x.png differ diff --git a/setup/macos/novelwriter.iconset/icon_16x16.png b/setup/macos/novelwriter.iconset/icon_16x16.png new file mode 100644 index 00000000..c9776183 Binary files /dev/null and b/setup/macos/novelwriter.iconset/icon_16x16.png differ diff --git a/setup/macos/novelwriter.iconset/icon_16x16@2x.png b/setup/macos/novelwriter.iconset/icon_16x16@2x.png new file mode 100644 index 00000000..aeeda556 Binary files /dev/null and b/setup/macos/novelwriter.iconset/icon_16x16@2x.png differ diff --git a/setup/macos/novelwriter.iconset/icon_256x256.png b/setup/macos/novelwriter.iconset/icon_256x256.png new file mode 100644 index 00000000..b9444f9d Binary files /dev/null and b/setup/macos/novelwriter.iconset/icon_256x256.png differ diff --git a/setup/macos/novelwriter.iconset/icon_256x256@2x.png b/setup/macos/novelwriter.iconset/icon_256x256@2x.png new file mode 100644 index 00000000..f93ad86e Binary files /dev/null and b/setup/macos/novelwriter.iconset/icon_256x256@2x.png differ diff --git a/setup/macos/novelwriter.iconset/icon_32x32.png b/setup/macos/novelwriter.iconset/icon_32x32.png new file mode 100644 index 00000000..f5f1b41f Binary files /dev/null and b/setup/macos/novelwriter.iconset/icon_32x32.png differ diff --git a/setup/macos/novelwriter.iconset/icon_32x32@2x.png b/setup/macos/novelwriter.iconset/icon_32x32@2x.png new file mode 100644 index 00000000..640771f3 Binary files /dev/null and b/setup/macos/novelwriter.iconset/icon_32x32@2x.png differ diff --git a/setup/macos/novelwriter.iconset/icon_512x512.png b/setup/macos/novelwriter.iconset/icon_512x512.png new file mode 100644 index 00000000..ab819662 Binary files /dev/null and b/setup/macos/novelwriter.iconset/icon_512x512.png differ diff --git a/setup/macos/novelwriter.iconset/icon_512x512@2x.png b/setup/macos/novelwriter.iconset/icon_512x512@2x.png new file mode 100644 index 00000000..b8bc6c13 Binary files /dev/null and b/setup/macos/novelwriter.iconset/icon_512x512@2x.png differ diff --git a/setup/make_release.sh b/setup/make_release.sh index 4ee78940..ae465909 100755 --- a/setup/make_release.sh +++ b/setup/make_release.sh @@ -26,8 +26,8 @@ echo " Building Minimal Packages" echo "================================================================================" echo "" python3 pkgutils.py minimal-zip --target-win -python3 pkgutils.py minimal-zip --target-linux -python3 pkgutils.py minimal-zip --target-darwin +# python3 pkgutils.py minimal-zip --target-linux +# python3 pkgutils.py minimal-zip --target-darwin echo "" echo " Building Linux Packages" diff --git a/setup/novelwriter.png b/setup/novelwriter.png deleted file mode 100644 index a9f2b5bc..00000000 Binary files a/setup/novelwriter.png and /dev/null differ diff --git a/setup/novelwriter_readme.png b/setup/novelwriter_readme.png new file mode 100644 index 00000000..a4bd8da4 Binary files /dev/null and b/setup/novelwriter_readme.png differ diff --git a/setup/novelwriter_text.png b/setup/novelwriter_text.png index e7f169cd..084ffbf2 100644 Binary files a/setup/novelwriter_text.png and b/setup/novelwriter_text.png differ diff --git a/tests/reference/baseConfig_novelwriter.conf b/tests/reference/baseConfig_novelwriter.conf index 95df9072..739a6378 100644 --- a/tests/reference/baseConfig_novelwriter.conf +++ b/tests/reference/baseConfig_novelwriter.conf @@ -45,6 +45,7 @@ repdash = True repdots = True autoscroll = False autoscrollpos = 30 +scrollpastend = True fmtsquoteopen = ‘ fmtsquoteclose = ’ fmtdquoteopen = “ diff --git a/tests/reference/coreTools_NewMinimal_nwProject.nwx b/tests/reference/coreTools_NewMinimal_nwProject.nwx index 23a377f5..a38b56b0 100644 --- a/tests/reference/coreTools_NewMinimal_nwProject.nwx +++ b/tests/reference/coreTools_NewMinimal_nwProject.nwx @@ -7,7 +7,7 @@ yes - None + en_GB None None diff --git a/tests/reference/guiPreferences_novelwriter.conf b/tests/reference/guiPreferences_novelwriter.conf index 6ea3a0d5..002dc463 100644 --- a/tests/reference/guiPreferences_novelwriter.conf +++ b/tests/reference/guiPreferences_novelwriter.conf @@ -45,6 +45,7 @@ repdash = True repdots = True autoscroll = True autoscrollpos = 30 +scrollpastend = True fmtsquoteopen = ‘ fmtsquoteclose = ’ fmtdquoteopen = “ diff --git a/tests/test_core/test_core_buildsettings.py b/tests/test_core/test_core_buildsettings.py index ca75cc40..eb4affdf 100644 --- a/tests/test_core/test_core_buildsettings.py +++ b/tests/test_core/test_core_buildsettings.py @@ -435,7 +435,7 @@ def testCoreBuildSettings_Collection(monkeypatch, mockGUI, fncPath: Path, mockRn (buildIDTwo, "Build Two"), (buildIDOne, "Build One"), ] - builds.setLastBuild(buildIDOne) + builds.setBuildsState(buildIDOne, [buildIDTwo, buildIDOne]) builds.setDefaultBuild(buildIDTwo) # Check errors: No valid path diff --git a/tests/test_core/test_core_coretools.py b/tests/test_core/test_core_coretools.py index a3f4e11c..0f127e05 100644 --- a/tests/test_core/test_core_coretools.py +++ b/tests/test_core/test_core_coretools.py @@ -446,6 +446,7 @@ def testCoreTools_NewCustomA(monkeypatch, fncPath, tstPaths, mockRnd): "projName": "Test Custom", "projTitle": "Test Novel", "projAuthor": "Jane Doe", + "projLang": -1, "projPath": fncPath, "popSample": False, "popMinimal": False, @@ -484,6 +485,7 @@ def testCoreTools_NewCustomB(monkeypatch, fncPath, tstPaths, mockRnd): "projName": "Test Custom", "projTitle": "Test Novel", "projAuthor": "Jane Doe", + "projLang": -1, "projPath": fncPath, "popSample": False, "popMinimal": False, diff --git a/tests/test_dialogs/test_dlg_projsettings.py b/tests/test_dialogs/test_dlg_projsettings.py index f56f01bf..824d0403 100644 --- a/tests/test_dialogs/test_dlg_projsettings.py +++ b/tests/test_dialogs/test_dlg_projsettings.py @@ -155,9 +155,9 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat # Set some values theProject = SHARED.project - theProject.tree[C.hTitlePage].setStatus(C.sFinished) - theProject.tree[C.hChapterDoc].setStatus(C.sDraft) - theProject.tree[C.hSceneDoc].setStatus(C.sDraft) + theProject.tree[C.hTitlePage].setStatus(C.sFinished) # type: ignore + theProject.tree[C.hChapterDoc].setStatus(C.sDraft) # type: ignore + theProject.tree[C.hSceneDoc].setStatus(C.sDraft) # type: ignore nwGUI.projView.projTree.setSelectedHandle(C.hPlotRoot) nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) @@ -170,9 +170,9 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat hCharNote = "0000000000011" hWorldNote = "0000000000012" - theProject.tree[hPlotNote].setImport(C.iMajor) - theProject.tree[hCharNote].setImport(C.iMajor) - theProject.tree[hWorldNote].setImport(C.iMain) + theProject.tree[hPlotNote].setImport(C.iMajor) # type: ignore + theProject.tree[hCharNote].setImport(C.iMajor) # type: ignore + theProject.tree[hWorldNote].setImport(C.iMain) # type: ignore # Create Dialog projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.TAB_STATUS) @@ -367,23 +367,23 @@ def testDlgProjSettings_Replace(qtbot, monkeypatch, nwGUI, fncPath, projPath, mo tabReplace = projSettings.tabReplace - assert tabReplace.listBox.topLevelItem(0).text(0) == "" - assert tabReplace.listBox.topLevelItem(0).text(1) == "B" - assert tabReplace.listBox.topLevelItem(1).text(0) == "" - assert tabReplace.listBox.topLevelItem(1).text(1) == "D" + assert tabReplace.listBox.topLevelItem(0).text(0) == "" # type: ignore + assert tabReplace.listBox.topLevelItem(0).text(1) == "B" # type: ignore + assert tabReplace.listBox.topLevelItem(1).text(0) == "" # type: ignore + assert tabReplace.listBox.topLevelItem(1).text(1) == "D" # type: ignore assert tabReplace.listBox.topLevelItemCount() == 2 # Nothing to save or delete tabReplace.listBox.clearSelection() - assert tabReplace._saveEntry() is False - assert tabReplace._delEntry() is False + tabReplace._saveEntry() + tabReplace._delEntry() assert tabReplace.listBox.topLevelItemCount() == 2 # Create a new entry qtbot.mouseClick(tabReplace.addButton, Qt.LeftButton) assert tabReplace.listBox.topLevelItemCount() == 3 - assert tabReplace.listBox.topLevelItem(2).text(0) == "" - assert tabReplace.listBox.topLevelItem(2).text(1) == "" + assert tabReplace.listBox.topLevelItem(2).text(0) == "" # type: ignore + assert tabReplace.listBox.topLevelItem(2).text(1) == "" # type: ignore # Edit the entry tabReplace.listBox.setCurrentItem(tabReplace.listBox.topLevelItem(2)) @@ -394,8 +394,8 @@ def testDlgProjSettings_Replace(qtbot, monkeypatch, nwGUI, fncPath, projPath, mo for c in "With This Stuff ": qtbot.keyClick(tabReplace.editValue, c, delay=KEY_DELAY) qtbot.mouseClick(tabReplace.saveButton, Qt.LeftButton) - assert tabReplace.listBox.topLevelItem(2).text(0) == "" - assert tabReplace.listBox.topLevelItem(2).text(1) == "With This Stuff " + assert tabReplace.listBox.topLevelItem(2).text(0) == "" # type: ignore + assert tabReplace.listBox.topLevelItem(2).text(1) == "With This Stuff " # type: ignore # Create a new entry again tabReplace.listBox.clearSelection() @@ -405,7 +405,7 @@ def testDlgProjSettings_Replace(qtbot, monkeypatch, nwGUI, fncPath, projPath, mo # The list is sorted, so we must find it newIdx = -1 for i in range(tabReplace.listBox.topLevelItemCount()): - if tabReplace.listBox.topLevelItem(i).text(0) == "": + if tabReplace.listBox.topLevelItem(i).text(0) == "": # type: ignore newIdx = i break assert newIdx >= 0 diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 064fcf17..008ad053 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -672,7 +672,7 @@ def testGuiEditor_Insert(qtbot, monkeypatch, nwGUI, projPath, ipsumText, mockRnd @pytest.mark.gui -def testGuiEditor_TextManipulation(qtbot, monkeypatch, nwGUI, projPath, ipsumText, mockRnd): +def testGuiEditor_TextManipulation(qtbot, nwGUI, projPath, ipsumText, mockRnd): """Test the text manipulation functions.""" buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) is True @@ -680,37 +680,6 @@ def testGuiEditor_TextManipulation(qtbot, monkeypatch, nwGUI, projPath, ipsumTex text = "### A Scene\n\n%s" % "\n\n".join(ipsumText) nwGUI.docEditor.replaceText(text) - # Clear Surrounding - # ================= - - # No Selection - text = "### A Scene\n\n%s" % ipsumText[0] - nwGUI.docEditor.replaceText(text) - nwGUI.docEditor.setCursorPosition(45) - - cursor = nwGUI.docEditor.textCursor() - assert nwGUI.docEditor._clearSurrounding(cursor, 1) is False - - # Clear Characters, 1 Layer - repText = text.replace("consectetur", "=consectetur=") - nwGUI.docEditor.replaceText(repText) - nwGUI.docEditor.setCursorPosition(45) - - cursor = nwGUI.docEditor.textCursor() - cursor.select(QTextCursor.WordUnderCursor) - assert nwGUI.docEditor._clearSurrounding(cursor, 1) is True - assert nwGUI.docEditor.getText() == text - - # Clear Characters, 2 Layers - repText = text.replace("consectetur", "==consectetur==") - nwGUI.docEditor.replaceText(repText) - nwGUI.docEditor.setCursorPosition(45) - - cursor = nwGUI.docEditor.textCursor() - cursor.select(QTextCursor.WordUnderCursor) - assert nwGUI.docEditor._clearSurrounding(cursor, 2) is True - assert nwGUI.docEditor.getText() == text - # Wrap Selection # ============== @@ -718,11 +687,6 @@ def testGuiEditor_TextManipulation(qtbot, monkeypatch, nwGUI, projPath, ipsumTex nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(45) - # No Selection - with monkeypatch.context() as mp: - mp.setattr(nwGUI.docEditor, "_autoSelect", lambda: QTextCursor()) - assert nwGUI.docEditor._wrapSelection("=", "=") is False - # Wrap Equal nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(45) @@ -752,13 +716,13 @@ def testGuiEditor_TextManipulation(qtbot, monkeypatch, nwGUI, projPath, ipsumTex # ============= text = "### A Scene\n\n%s" % "\n\n".join(ipsumText[0:2]) - nwGUI.docEditor.replaceText(text) - nwGUI.docEditor.setCursorPosition(45) - # No Selection - with monkeypatch.context() as mp: - mp.setattr(nwGUI.docEditor, "_autoSelect", lambda: QTextCursor()) - assert nwGUI.docEditor._toggleFormat(2, "=") is False + # Block format repetition + nwGUI.docEditor.replaceText(text) + nwGUI.docEditor.setCursorPosition(39) + assert nwGUI.docEditor._toggleFormat(1, "=") is True + assert nwGUI.docEditor.getText() == text.replace("amet", "=amet=", 1) + assert nwGUI.docEditor._toggleFormat(1, "=") is False # Wrap Single Equal nwGUI.docEditor.replaceText(text) diff --git a/tests/test_tools/test_tools_manussettings.py b/tests/test_tools/test_tools_manussettings.py index 1371caf0..0a5f9c5a 100644 --- a/tests/test_tools/test_tools_manussettings.py +++ b/tests/test_tools/test_tools_manussettings.py @@ -562,7 +562,6 @@ def testBuildSettings_Format(monkeypatch, qtbot: QtBot, nwGUI: GuiMain): assert bSettings.toolStack.currentWidget() is fmtTab # Check initial values - assert fmtTab.buildLang.currentData() == "en_US" assert fmtTab.textFont.text() == textFont assert fmtTab.textSize.value() == 12 assert fmtTab.lineHeight.value() == 1.2 @@ -581,7 +580,6 @@ def testBuildSettings_Format(monkeypatch, qtbot: QtBot, nwGUI: GuiMain): assert fmtTab.rightMargin.value() == 15.0 # Change values - fmtTab.buildLang.setCurrentIndex(fmtTab.buildLang.findData("en_GB")) fmtTab.textFont.setText("Arial") fmtTab.textSize.setValue(11) fmtTab.lineHeight.setValue(1.15) @@ -596,7 +594,6 @@ def testBuildSettings_Format(monkeypatch, qtbot: QtBot, nwGUI: GuiMain): # Save values fmtTab.saveContent() - assert build.getStr("format.buildLang") == "en_GB" assert build.getStr("format.textFont") == "Arial" assert build.getInt("format.textSize") == 11 assert build.getFloat("format.lineHeight") == 1.15