Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da3383a67c | ||
|
|
4dd2d63d9b | ||
|
|
b16783143f | ||
|
|
590c66d1eb | ||
|
|
b4db960e68 | ||
|
|
aaf4ede04d | ||
|
|
5ae385ec02 | ||
|
|
ad2288e2fc | ||
|
|
4b5f3c8af8 | ||
|
|
c1dd2ebb92 | ||
|
|
a210a665fb | ||
|
|
979a55d24a | ||
|
|
3c474987b2 | ||
|
|
0bbe29bc2e | ||
|
|
e690243fc3 | ||
|
|
74862969e1 | ||
|
|
92ef020342 | ||
|
|
2176dd2020 | ||
|
|
2ec943d118 | ||
|
|
01733bdbaa | ||
|
|
5bc39d29eb | ||
|
|
8c38bece3a | ||
|
|
d61329a4f3 | ||
|
|
635493f1c1 | ||
|
|
364b7bcf99 | ||
|
|
7d53c4a06b | ||
|
|
e3981aad60 | ||
|
|
f08078bd08 | ||
|
|
5d1e781d20 | ||
|
|
d1d5116536 | ||
|
|
6379f031c2 | ||
|
|
d153d80595 | ||
|
|
28c45719e2 | ||
|
|
e6befda146 | ||
|
|
910723bbcd | ||
|
|
a534e30467 | ||
|
|
dd7c8162ac | ||
|
|
2bffa83d62 | ||
|
|
01514a94e1 | ||
|
|
53355dbcc6 | ||
|
|
bf1067a346 | ||
|
|
53784b310e | ||
|
|
f6214ae36d | ||
|
|
7aea5eb5af |
19
.github/workflows/build_linux.yml
vendored
19
.github/workflows/build_linux.yml
vendored
@@ -8,10 +8,13 @@ jobs:
|
||||
|
||||
buildLinux-AppImage:
|
||||
needs: buildAssets
|
||||
runs-on: ubuntu-latest
|
||||
# Needs to stay on 22.04 as long as we're using manylinux_2_28
|
||||
# as libxcb-cursor0 in 22.04 supports glibc >= 2.17
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
PYTHON_VERSION: "3.13"
|
||||
LINUX_TAG: "manylinux_2_28_x86_64"
|
||||
LINUX_TAG: "manylinux_2_28"
|
||||
LINUX_ARCH: "x86_64"
|
||||
steps:
|
||||
- name: Python Setup
|
||||
uses: actions/setup-python@v5
|
||||
@@ -19,8 +22,13 @@ jobs:
|
||||
python-version: "3.13"
|
||||
architecture: x64
|
||||
|
||||
- name: Install Packages (apt)
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install libxcb-cursor0
|
||||
|
||||
- name: Install Packages (pip)
|
||||
run: pip install python-appimage
|
||||
run: pip install python-appimage setuptools
|
||||
|
||||
- name: Checkout Source
|
||||
uses: actions/checkout@v4
|
||||
@@ -34,8 +42,11 @@ jobs:
|
||||
- name: Build AppImage
|
||||
id: build
|
||||
run: |
|
||||
wget https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-$LINUX_ARCH.AppImage
|
||||
chmod +x appimagetool-$LINUX_ARCH.AppImage
|
||||
export APPIMAGE_TOOL_EXEC="$(pwd)/appimagetool-$LINUX_ARCH.AppImage"
|
||||
echo "BUILD_VERSION=$(python pkgutils.py version)" >> $GITHUB_OUTPUT
|
||||
python pkgutils.py build-appimage --linux-tag $LINUX_TAG --python-version $PYTHON_VERSION
|
||||
python pkgutils.py build-appimage $LINUX_TAG $LINUX_ARCH $PYTHON_VERSION
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
73
CHANGELOG.md
73
CHANGELOG.md
@@ -1,5 +1,78 @@
|
||||
# novelWriter Changelog
|
||||
|
||||
## Version 2.7.2 [2025-06-24]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This is a patch release that fixes several issues related to DocX and PDF manuscript documents,
|
||||
improves how line breaks, alignment and indentation is processed, and fixes some issues with
|
||||
displaying tags for novel documents on the user interface.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* Fixed an issue where escaped markup characters were not replaced properly in DocX and PDF
|
||||
documents, and in previews. Issue #2410. PR #2411.
|
||||
* Fixed an issue where titles with line breaks in them would have page breaks applied to both lines
|
||||
for preview and PDF documents. Issue #2415. PR #2416.
|
||||
* Fixed an issue where the justified text setting would not be properly applied to a paragraph in a
|
||||
manuscript document if there was a line break in the paragraph, but single line breaks were set
|
||||
to be ignored. Issue #2426. PR #2427.
|
||||
* Fixed an issue where the "Tag" field of the Outline View details panel remained blank even if a
|
||||
tag was set for the novel document. Issue #2428. PR #2429.
|
||||
|
||||
**Improvements**
|
||||
|
||||
* When comments are enabled in the viewer, story comments should also be visible. A separate filter
|
||||
button will be added for this in 2.8. PR #2420.
|
||||
* When a paragraph has line breaks in it, the alignment tag will now override first line
|
||||
indentation even if the alignment tag is not on the first line. This is more consistent with the
|
||||
alignment behaviour for multi-line paragraphs in general. Issue #2425. PR #2427.
|
||||
* Tags will now be shown in the Novel View tooltip pop-out under the triangle button, together with
|
||||
all the other meta data collected about a document or heading. PR #2429.
|
||||
|
||||
**Documentation**
|
||||
|
||||
* The documentation on how alignment and first line indentation works in conjunction with
|
||||
in-paragraph line breaks, the setting to keep or ignore such line breaks, and the forced line
|
||||
break shortcode, has been improved. Issue #2425. PR #2427.
|
||||
|
||||
----
|
||||
|
||||
## Version 2.7.1 [2025-06-09]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This is a patch release that fixes some issues with tags and references, an issue with the
|
||||
auto-complete menu on Windows, and some issues with the AppImage on Linux. The Czech translation
|
||||
for 2.7 has also been completed.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* Fix a bug where tags would be listed in the auto-complete menu for mentions even if the file
|
||||
defining it had been moved to Archive or Trash. Issue #2387. PR #2389.
|
||||
* Fixed a bug that seems to only have occurred on Windows where the auto-complete menu would hold
|
||||
on to the return and arrow keys even when it was hidden. Issue #2386. PR #2389.
|
||||
|
||||
**Improvements**
|
||||
|
||||
* The auto-complete menu in the editor no longer shows any suggestions when defining the tag.
|
||||
This never made sense anyway. PR #2389.
|
||||
|
||||
**Internationalisation**
|
||||
|
||||
* Updated the Czech. PR #2393.
|
||||
|
||||
**Packaging**
|
||||
|
||||
* Fixed an issue with the AppImage release where on some platforms `libxcb-cursor0` was missing.
|
||||
The library is now included in the AppImage. Issue #2374. PR #2392.
|
||||
|
||||
----
|
||||
|
||||
## Version 2.7 [2025-06-01]
|
||||
|
||||
### Release Notes
|
||||
|
||||
@@ -56,6 +56,20 @@ activated by clicking the left-most icon button in the editor header.
|
||||
.. versionadded:: 2.2
|
||||
|
||||
|
||||
.. _docs_usage_formatting_shortcodes_break:
|
||||
|
||||
Forced Line Break
|
||||
-----------------
|
||||
|
||||
Inserting ``[br]`` in the text will ensure a line break is always inserted in that place, even if
|
||||
you turn off **Preserve Hard Line Breaks** in your manuscript build settings.
|
||||
|
||||
You can add a manual line break after it too, for a better visual representation in the editor, but
|
||||
keep in mind that this line break is removed before the text is processed, so the text on either
|
||||
side of the ``[br]`` shortcode will be considered as belonging to the same line. This can affect
|
||||
how alignment is treated. See :ref:`docs_usage_align_indent_forced` for more details.
|
||||
|
||||
|
||||
.. _docs_usage_formatting_breaks:
|
||||
|
||||
Vertical Space and Page Breaks
|
||||
|
||||
@@ -53,18 +53,33 @@ the entire paragraph. For the following text, all lines will be centred:
|
||||
|
||||
.. code-block:: md
|
||||
|
||||
>> I am the very model of a modern Major-General
|
||||
>> I am the very model of a modern Major-General <<
|
||||
I've information vegetable, animal, and mineral
|
||||
I know the kings of England, and I quote the fights historical
|
||||
From Marathon to Waterloo, in order categorical <<
|
||||
From Marathon to Waterloo, in order categorical
|
||||
|
||||
If you have multiple conflicting alignments on a paragraph, only one is applied. The order of
|
||||
precedence is:
|
||||
|
||||
#. Left alignment
|
||||
#. Right alignment
|
||||
#. Centred text
|
||||
#. Justified text
|
||||
|
||||
.. note::
|
||||
|
||||
It is strongly recommended that you keep the **Preserve Hard Line Breaks** setting enabled in
|
||||
your manuscript build settings. This setting assumes all single line breaks in your text are
|
||||
intended. Turning this off makes adding line breaks more complicated, but it is still possible.
|
||||
See :ref:`docs_usage_align_indent_forced`.
|
||||
|
||||
|
||||
Alignment with First Line Indent
|
||||
================================
|
||||
|
||||
If you have first line indent enabled in your manuscript build settings, you probably want to
|
||||
disable it for text in verses. Adding any alignment tags will cause the first line indent to be
|
||||
switched off for that paragraph.
|
||||
disable it for text in verses. Adding any alignment tags on a paragraph will cause the first
|
||||
line indent to be switched off for that paragraph.
|
||||
|
||||
:bdg-info:`Example`
|
||||
|
||||
@@ -76,3 +91,35 @@ The following text will always be aligned against the left margin:
|
||||
I've information vegetable, animal, and mineral
|
||||
I know the kings of England, and I quote the fights historical
|
||||
From Marathon to Waterloo, in order categorical
|
||||
|
||||
|
||||
.. _docs_usage_align_indent_forced:
|
||||
|
||||
Alignment with Forced Line Breaks
|
||||
=================================
|
||||
|
||||
If you turn off **Preserve Hard Line Breaks** in your manuscript build settings, you can still
|
||||
force line breaks in paragraphs using the ``[br]`` shortcode. For clarity in the text, you can add
|
||||
a line break after it as well. It doesn't result in two line breaks.
|
||||
|
||||
Keep in mind that when the text is processed, the lines on either side of a ``[br]`` shortcode are
|
||||
combined, and any trailing hard line break is *ignored*. This means that when such a paragraph is
|
||||
processed, these line breaks count as the same line. This affects how alignment tags are handled.
|
||||
For instance, this text becomes centred instead of left aligned.
|
||||
|
||||
.. code-block:: md
|
||||
|
||||
>> I am the very model of a modern Major-General[br]
|
||||
I've information vegetable, animal, and mineral[br]
|
||||
I know the kings of England, and I quote the fights historical[br]
|
||||
From Marathon to Waterloo, in order categorical <<
|
||||
|
||||
Since this is understood as one line, this is the only way you can actually centre this paragraph.
|
||||
|
||||
.. caution::
|
||||
|
||||
Due to this difference in how text with ``[br]`` tags are processed, it is generally better to
|
||||
stick with the **Preserve Hard Line Breaks** setting enabled. It ensures a better correspondence
|
||||
between what you see in the editor and what output you get.
|
||||
|
||||
See also :ref:`docs_usage_formatting_shortcodes_break`.
|
||||
|
||||
368
i18n/nw_base.ts
368
i18n/nw_base.ts
File diff suppressed because it is too large
Load Diff
470
i18n/nw_cs_CZ.ts
470
i18n/nw_cs_CZ.ts
File diff suppressed because it is too large
Load Diff
368
i18n/nw_de_DE.ts
368
i18n/nw_de_DE.ts
File diff suppressed because it is too large
Load Diff
368
i18n/nw_en_US.ts
368
i18n/nw_en_US.ts
File diff suppressed because it is too large
Load Diff
368
i18n/nw_it_IT.ts
368
i18n/nw_it_IT.ts
File diff suppressed because it is too large
Load Diff
368
i18n/nw_ja_JP.ts
368
i18n/nw_ja_JP.ts
File diff suppressed because it is too large
Load Diff
368
i18n/nw_nb_NO.ts
368
i18n/nw_nb_NO.ts
File diff suppressed because it is too large
Load Diff
368
i18n/nw_pl_PL.ts
368
i18n/nw_pl_PL.ts
File diff suppressed because it is too large
Load Diff
368
i18n/nw_pt_BR.ts
368
i18n/nw_pt_BR.ts
File diff suppressed because it is too large
Load Diff
368
i18n/nw_zh_CN.ts
368
i18n/nw_zh_CN.ts
File diff suppressed because it is too large
Load Diff
@@ -49,9 +49,9 @@ __license__ = "GPLv3"
|
||||
__author__ = "Veronica Berglyd Olsen"
|
||||
__maintainer__ = "Veronica Berglyd Olsen"
|
||||
__email__ = "code@vkbo.net"
|
||||
__version__ = "2.7"
|
||||
__hexversion__ = "0x020700f0"
|
||||
__date__ = "2025-06-01"
|
||||
__version__ = "2.7.2"
|
||||
__hexversion__ = "0x020702f0"
|
||||
__date__ = "2025-06-24"
|
||||
__status__ = "Stable"
|
||||
__domain__ = "novelwriter.io"
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
"Short Description": "Krátký popis",
|
||||
"Footnotes": "Poznámky pod čarou",
|
||||
"Comment": "Komentář",
|
||||
"Story Structure": "Struktura příběhu",
|
||||
"Note": "Poznámka",
|
||||
"Notes": "Poznámka",
|
||||
"Tag": "Štítek",
|
||||
"Point of View": "Úhel pohledu",
|
||||
|
||||
@@ -44,7 +44,7 @@ from novelwriter.common import (
|
||||
NWConfigParser, checkInt, checkPath, describeFont, fontMatcher,
|
||||
formatTimeStamp, processDialogSymbols, simplified
|
||||
)
|
||||
from novelwriter.constants import nwFiles, nwHtmlUnicode, nwQuotes, nwUnicode
|
||||
from novelwriter.constants import nwFiles, nwQuotes, nwUnicode
|
||||
from novelwriter.error import formatException, logException
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -892,7 +892,7 @@ class Config:
|
||||
"""
|
||||
self.splashMessage(f"Initialising {kind} font: {font.family()}")
|
||||
metrics = QFontMetrics(font)
|
||||
for char in nwHtmlUnicode.U_TO_H.keys():
|
||||
for char in nwUnicode.UI_SYMBOLS:
|
||||
if not metrics.inFont(char): # type: ignore
|
||||
logger.warning("No glyph U+%04x in font", ord(char)) # pragma: no cover
|
||||
return
|
||||
|
||||
@@ -184,6 +184,10 @@ class nwKeyWords:
|
||||
POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY, WORLD_KEY,
|
||||
OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY,
|
||||
]
|
||||
CAN_LOOKUP: Final[list[str]] = [
|
||||
POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY, WORLD_KEY,
|
||||
OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY, STORY_KEY, MENTION_KEY,
|
||||
]
|
||||
|
||||
# Set of Valid Keys
|
||||
VALID_KEYS: Final[set[str]] = set(ALL_KEYS)
|
||||
@@ -582,21 +586,14 @@ class nwUnicode:
|
||||
U_TIMES = "\u00d7" # Multiplication sign
|
||||
U_DIVIDE = "\u00f7" # Division sign
|
||||
|
||||
# Arrows
|
||||
U_UTRI = "\u25b2" # Up-pointing triangle
|
||||
U_UTRIS = "\u25b4" # Up-pointing triangle, small
|
||||
U_RTRI = "\u25b6" # Right-pointing triangle
|
||||
U_RTRIS = "\u25b8" # Right-pointing triangle, small
|
||||
U_DTRI = "\u25bc" # Down-pointing triangle
|
||||
U_DTRIS = "\u25be" # Down-pointing triangle, small
|
||||
U_LTRI = "\u25c0" # Left-pointing triangle
|
||||
U_LTRIS = "\u25c2" # Left-pointing triangle, small
|
||||
|
||||
# Special
|
||||
U_UNKN = "\ufffd" # Unknown character
|
||||
U_NAC1 = "\ufffe" # Not a character
|
||||
U_NAC2 = "\uffff" # Not a character
|
||||
|
||||
# Placeholders
|
||||
U_LBREAK = "\u21b2" # Downwards Arrow With Tip Leftwards
|
||||
|
||||
# HTML Equivalents
|
||||
# ================
|
||||
|
||||
@@ -651,15 +648,15 @@ class nwUnicode:
|
||||
H_TIMES = "×"
|
||||
H_DIVIDE = "÷"
|
||||
|
||||
# Arrows
|
||||
H_UTRI = "▲"
|
||||
H_UTRIS = "▴"
|
||||
H_RTRI = "▶"
|
||||
H_RTRIS = "▸"
|
||||
H_DTRI = "▼"
|
||||
H_DTRIS = "▾"
|
||||
H_LTRI = "◀"
|
||||
H_LTRIS = "◂"
|
||||
# Unicode symbols expected to be used on the UI
|
||||
UI_SYMBOLS: Final[list[str]] = [
|
||||
U_QUOT, U_APOS, U_LAQUO, U_RAQUO, U_LSQUO, U_RSQUO, U_SBQUO, U_SUQUO,
|
||||
U_LDQUO, U_RDQUO, U_BDQUO, U_UDQUO, U_LSAQUO, U_RSAQUO, U_BDRQUO,
|
||||
U_LCQUO, U_RCQUO, U_LWCQUO, U_RWCQUO, U_FGDASH, U_ENDASH, U_EMDASH,
|
||||
U_HBAR, U_HELLIP, U_MAPOS, U_PRIME, U_DPRIME, U_NBSP, U_THSP, U_THNBSP,
|
||||
U_ENSP, U_EMSP, U_MMSP, U_CHECK, U_CROSS, U_BULL, U_TRBULL, U_HYBULL,
|
||||
U_FLOWER, U_PERMIL, U_DEGREE, U_MINUS, U_TIMES, U_DIVIDE, U_LBREAK,
|
||||
]
|
||||
|
||||
|
||||
class nwHtmlUnicode:
|
||||
|
||||
@@ -131,12 +131,6 @@ class Index:
|
||||
self._novelExtra = extra
|
||||
return
|
||||
|
||||
def setItemClass(self, tHandle: str, itemClass: nwItemClass) -> None:
|
||||
"""Update the class for all tags of a handle."""
|
||||
logger.info("Updating class for '%s'", tHandle)
|
||||
self._tagsIndex.updateClass(tHandle, itemClass.name)
|
||||
return
|
||||
|
||||
##
|
||||
# Public Methods
|
||||
##
|
||||
@@ -183,6 +177,16 @@ class Index:
|
||||
self.scanText(tHandle, self._project.storage.getDocumentText(tHandle))
|
||||
return
|
||||
|
||||
def refreshHandle(self, tHandle: str) -> None:
|
||||
"""Update the class for all tags of a handle."""
|
||||
if item := self._project.tree[tHandle]:
|
||||
logger.info("Updating class for '%s'", tHandle)
|
||||
if item.isInactiveClass():
|
||||
self.deleteHandle(tHandle)
|
||||
else:
|
||||
self._tagsIndex.updateClass(tHandle, item.itemClass.name)
|
||||
return
|
||||
|
||||
def indexChangedSince(self, checkTime: int | float) -> bool:
|
||||
"""Check if the index has changed since a given time."""
|
||||
return self._indexChange > float(checkTime)
|
||||
@@ -704,17 +708,19 @@ class Index:
|
||||
return 0, 0, 0
|
||||
|
||||
def getReferences(self, tHandle: str, sTitle: str | None = None) -> dict[str, list[str]]:
|
||||
"""Extract all references made in a file, and optionally title
|
||||
section.
|
||||
"""Extract all tags and references made in a file, and
|
||||
optionally title section.
|
||||
"""
|
||||
tRefs = {x: [] for x in nwKeyWords.VALID_KEYS}
|
||||
refs = {x: [] for x in nwKeyWords.VALID_KEYS}
|
||||
for rTitle, hItem in self._itemIndex.iterItemHeaders(tHandle):
|
||||
if sTitle is None or sTitle == rTitle:
|
||||
for aTag, refTypes in hItem.references.items():
|
||||
for refType in refTypes:
|
||||
if refType in tRefs:
|
||||
tRefs[refType].append(self._tagsIndex.tagName(aTag))
|
||||
return tRefs
|
||||
if refType in refs:
|
||||
refs[refType].append(self._tagsIndex.tagName(aTag))
|
||||
if tag := hItem.tag:
|
||||
refs[nwKeyWords.TAG_KEY] = [self._tagsIndex.tagName(tag)]
|
||||
return refs
|
||||
|
||||
def getReferenceForHeader(self, tHandle: str, nHead: int, keyClass: str) -> list[str]:
|
||||
"""Get the display names for a tags class for insertion into a
|
||||
@@ -753,10 +759,12 @@ class Index:
|
||||
"""Return all tags used by a specific document."""
|
||||
return self._itemIndex.allItemTags(tHandle) if tHandle else []
|
||||
|
||||
def getClassTags(self, itemClass: nwItemClass | None) -> list[str]:
|
||||
"""Return all tags based on itemClass."""
|
||||
name = None if itemClass is None else itemClass.name
|
||||
return self._tagsIndex.filterTagNames(name)
|
||||
def getKeyWordTags(self, keyWord: str) -> list[str]:
|
||||
"""Return all tags usable for a specific keyword."""
|
||||
if keyWord in nwKeyWords.CAN_LOOKUP:
|
||||
itemClass = nwKeyWords.KEY_CLASS.get(keyWord)
|
||||
return self._tagsIndex.filterTagNames(itemClass.name if itemClass else None)
|
||||
return []
|
||||
|
||||
def getTagsData(
|
||||
self, activeOnly: bool = True
|
||||
|
||||
@@ -342,20 +342,26 @@ class IndexHeading:
|
||||
##
|
||||
|
||||
def getReferences(self) -> dict[str, list[str]]:
|
||||
"""Extract all references for this heading."""
|
||||
"""Extract all tags and references for this heading."""
|
||||
refs = {x: [] for x in nwKeyWords.VALID_KEYS}
|
||||
for tag, types in self._refs.items():
|
||||
for keyword in types:
|
||||
if keyword in refs and (name := self._cache.tags.tagName(tag)):
|
||||
refs[keyword].append(name)
|
||||
if name := self._cache.tags.tagName(self._tag):
|
||||
refs[nwKeyWords.TAG_KEY] = [name]
|
||||
return refs
|
||||
|
||||
def getReferencesByKeyword(self, keyword: str) -> list[str]:
|
||||
"""Extract all references for this heading."""
|
||||
refs = []
|
||||
for tag, types in self._refs.items():
|
||||
if keyword in types and (name := self._cache.tags.tagName(tag)):
|
||||
if keyword == nwKeyWords.TAG_KEY:
|
||||
if name := self._cache.tags.tagName(self._tag):
|
||||
refs.append(name)
|
||||
else:
|
||||
for tag, types in self._refs.items():
|
||||
if keyword in types and (name := self._cache.tags.tagName(tag)):
|
||||
refs.append(name)
|
||||
return refs
|
||||
|
||||
##
|
||||
|
||||
@@ -439,7 +439,7 @@ class NWItem:
|
||||
self.setClass(itemClass)
|
||||
if self._type == nwItemType.FILE:
|
||||
# Notify the index of the class change
|
||||
self._project.index.setItemClass(self._handle, itemClass)
|
||||
self._project.index.refreshHandle(self._handle)
|
||||
|
||||
if self._layout == nwItemLayout.NO_LAYOUT:
|
||||
# If no layout is set, pick one
|
||||
|
||||
@@ -37,7 +37,7 @@ from PyQt6.QtCore import QMargins, QSize
|
||||
from novelwriter import __version__
|
||||
from novelwriter.common import firstFloat, xmlElement, xmlSubElem
|
||||
from novelwriter.constants import nwHeadFmt, nwStyles
|
||||
from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt
|
||||
from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt, stripEscape
|
||||
from novelwriter.formats.tokenizer import Tokenizer
|
||||
from novelwriter.types import QtHexRgb
|
||||
|
||||
@@ -480,7 +480,7 @@ class ToDocX(Tokenizer):
|
||||
xmlSubElem(rPr, _wTag("color"), attrib={W_VAL: _docXCol(color)})
|
||||
|
||||
if isinstance(text, str):
|
||||
for segment in RX_TEXT.split(text):
|
||||
for segment in RX_TEXT.split(stripEscape(text)):
|
||||
if segment == "\n":
|
||||
xmlSubElem(xR, _wTag("br"))
|
||||
elif segment == "\t":
|
||||
|
||||
@@ -495,12 +495,10 @@ class Tokenizer(ABC):
|
||||
def doPreProcessing(self) -> None:
|
||||
"""Run pre-processing jobs before the text is tokenized."""
|
||||
# Process the user's auto-replace dictionary
|
||||
if autoReplace := self._project.data.autoReplace:
|
||||
repDict = {}
|
||||
for aKey, aVal in autoReplace.items():
|
||||
repDict[f"<{aKey}>"] = aVal
|
||||
xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL)
|
||||
self._text = xRep.sub(lambda x: repDict[x.group(0)], self._text)
|
||||
if entry := self._project.data.autoReplace:
|
||||
replace = {f"<{k}>": v for k, v in entry.items()}
|
||||
rxRep = re.compile("|".join([re.escape(k) for k in replace]), flags=re.DOTALL)
|
||||
self._text = rxRep.sub(lambda x: replace[x.group(0)], self._text)
|
||||
return
|
||||
|
||||
def tokenizeText(self) -> None:
|
||||
@@ -882,31 +880,21 @@ class Tokenizer(ABC):
|
||||
if nBlock[0] != BlockTyp.TEXT:
|
||||
# Next block is not text, so we add the buffer to blocks
|
||||
nLines = len(pLines)
|
||||
cStyle = pLines[0][4]
|
||||
if firstIndent and not (self._noIndent or cStyle & BlockFmt.ALIGNED):
|
||||
# If paragraph indentation is enabled, not temporarily
|
||||
# turned off, and the block is not aligned, we add the
|
||||
# text indentation flag
|
||||
cStyle |= BlockFmt.IND_T
|
||||
tFmt: T_Formats = []
|
||||
pTxt = ""
|
||||
cStyle = BlockFmt.NONE
|
||||
|
||||
if nLines == 1:
|
||||
# The paragraph contains a single line, so we just save
|
||||
# that directly to the blocks list. If justify is
|
||||
# enabled, and there is no alignment, we apply it.
|
||||
if doJustify and not cStyle & BlockFmt.ALIGNED:
|
||||
cStyle |= BlockFmt.JUSTIFY
|
||||
|
||||
# The paragraph contains a single line
|
||||
tFmt = pLines[0][3]
|
||||
pTxt = pLines[0][2].translate(transMapB)
|
||||
sBlocks.append((
|
||||
BlockTyp.TEXT, pLines[0][1], pTxt, pLines[0][3], cStyle
|
||||
))
|
||||
cStyle = pLines[0][4]
|
||||
|
||||
elif nLines > 1:
|
||||
# The paragraph contains multiple lines, so we need to
|
||||
# join them according to the line break policy, and
|
||||
# recompute all the formatting markers
|
||||
tTxt = ""
|
||||
tFmt: T_Formats = []
|
||||
for aBlock in pLines:
|
||||
tLen = len(tTxt)
|
||||
tTxt += f"{aBlock[2]}{lineSep}"
|
||||
@@ -914,6 +902,18 @@ class Tokenizer(ABC):
|
||||
cStyle |= aBlock[4]
|
||||
|
||||
pTxt = tTxt[:-1].translate(transMapB)
|
||||
|
||||
if nLines:
|
||||
isAligned = cStyle & BlockFmt.ALIGNED
|
||||
if firstIndent and not (self._noIndent or isAligned):
|
||||
# If paragraph indentation is enabled, not temporarily
|
||||
# turned off, and the block is not aligned, we add the
|
||||
# text indentation flag
|
||||
cStyle |= BlockFmt.IND_T
|
||||
|
||||
if doJustify and not isAligned:
|
||||
cStyle |= BlockFmt.JUSTIFY
|
||||
|
||||
sBlocks.append((
|
||||
BlockTyp.TEXT, pLines[0][1], pTxt, tFmt, cStyle
|
||||
))
|
||||
@@ -1170,12 +1170,18 @@ class Tokenizer(ABC):
|
||||
|
||||
class HeadingFormatter:
|
||||
|
||||
def __init__(self, project: NWProject) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
project: NWProject,
|
||||
chapter: int = 0,
|
||||
scene: int = 0,
|
||||
absolute: int = 0,
|
||||
) -> None:
|
||||
self._project = project
|
||||
self._handle = None
|
||||
self._chCount = 0
|
||||
self._scChCount = 0
|
||||
self._scAbsCount = 0
|
||||
self._chapter = chapter
|
||||
self._scene = scene
|
||||
self._absolute = absolute
|
||||
return
|
||||
|
||||
def setHandle(self, tHandle: str | None) -> None:
|
||||
@@ -1185,42 +1191,42 @@ class HeadingFormatter:
|
||||
|
||||
def incChapter(self) -> None:
|
||||
"""Increment the chapter counter."""
|
||||
self._chCount += 1
|
||||
self._chapter += 1
|
||||
return
|
||||
|
||||
def incScene(self) -> None:
|
||||
"""Increment the scene counters."""
|
||||
self._scChCount += 1
|
||||
self._scAbsCount += 1
|
||||
self._scene += 1
|
||||
self._absolute += 1
|
||||
return
|
||||
|
||||
def resetAll(self) -> None:
|
||||
"""Reset all counters."""
|
||||
self._chCount = 0
|
||||
self._scChCount = 0
|
||||
self._scAbsCount = 0
|
||||
self._chapter = 0
|
||||
self._scene = 0
|
||||
self._absolute = 0
|
||||
return
|
||||
|
||||
def resetScene(self) -> None:
|
||||
"""Reset the chapter scene counter."""
|
||||
self._scChCount = 0
|
||||
self._scene = 0
|
||||
return
|
||||
|
||||
def apply(self, hFormat: str, text: str, nHead: int) -> str:
|
||||
"""Apply formatting to a specific heading."""
|
||||
hFormat = hFormat.replace(nwHeadFmt.TITLE, text)
|
||||
hFormat = hFormat.replace(nwHeadFmt.BR, "\n")
|
||||
hFormat = hFormat.replace(nwHeadFmt.CH_NUM, str(self._chCount))
|
||||
hFormat = hFormat.replace(nwHeadFmt.SC_NUM, str(self._scChCount))
|
||||
hFormat = hFormat.replace(nwHeadFmt.SC_ABS, str(self._scAbsCount))
|
||||
hFormat = hFormat.replace(nwHeadFmt.CH_NUM, str(self._chapter))
|
||||
hFormat = hFormat.replace(nwHeadFmt.SC_NUM, str(self._scene))
|
||||
hFormat = hFormat.replace(nwHeadFmt.SC_ABS, str(self._absolute))
|
||||
if nwHeadFmt.CH_WORD in hFormat:
|
||||
chWord = self._project.localLookup(self._chCount)
|
||||
chWord = self._project.localLookup(self._chapter)
|
||||
hFormat = hFormat.replace(nwHeadFmt.CH_WORD, chWord)
|
||||
if nwHeadFmt.CH_ROML in hFormat:
|
||||
chRom = numberToRoman(self._chCount, toLower=True)
|
||||
chRom = numberToRoman(self._chapter, toLower=True)
|
||||
hFormat = hFormat.replace(nwHeadFmt.CH_ROML, chRom)
|
||||
if nwHeadFmt.CH_ROMU in hFormat:
|
||||
chRom = numberToRoman(self._chCount, toLower=False)
|
||||
chRom = numberToRoman(self._chapter, toLower=False)
|
||||
hFormat = hFormat.replace(nwHeadFmt.CH_ROMU, chRom)
|
||||
|
||||
if nwHeadFmt.CHAR_POV in hFormat or nwHeadFmt.CHAR_FOCUS in hFormat:
|
||||
|
||||
@@ -36,12 +36,13 @@ from PyQt6.QtPrintSupport import QPrinter
|
||||
|
||||
from novelwriter import __version__
|
||||
from novelwriter.constants import nwStyles, nwUnicode
|
||||
from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt
|
||||
from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt, stripEscape
|
||||
from novelwriter.formats.tokenizer import HEADINGS, Tokenizer
|
||||
from novelwriter.types import (
|
||||
QtAlignAbsolute, QtAlignCenter, QtAlignJustify, QtAlignLeft, QtAlignRight,
|
||||
QtKeepAnchor, QtMoveAnchor, QtPageBreakAfter, QtPageBreakBefore,
|
||||
QtPropLineHeight, QtTransparent, QtVAlignNormal, QtVAlignSub, QtVAlignSuper
|
||||
QtKeepAnchor, QtMoveAnchor, QtPageBreakAfter, QtPageBreakAuto,
|
||||
QtPageBreakBefore, QtPropLineHeight, QtTransparent, QtVAlignNormal,
|
||||
QtVAlignSub, QtVAlignSuper
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -253,8 +254,10 @@ class ToQTextDocument(Tokenizer):
|
||||
|
||||
elif tType in HEADINGS:
|
||||
bFmt, cFmt = self._genHeadStyle(tType, tMeta, bFmt)
|
||||
newBlock(cursor, bFmt)
|
||||
cursor.insertText(tText, cFmt)
|
||||
for tPart in tText.split("\n"):
|
||||
newBlock(cursor, bFmt)
|
||||
cursor.insertText(tPart, cFmt)
|
||||
bFmt.setPageBreakPolicy(QtPageBreakAuto)
|
||||
|
||||
elif tType == BlockTyp.SEP:
|
||||
newBlock(cursor, bFmt)
|
||||
@@ -343,7 +346,7 @@ class ToQTextDocument(Tokenizer):
|
||||
for pos, fmt, data in tFmt:
|
||||
|
||||
# Insert buffer with previous format
|
||||
cursor.insertText(temp[start:pos], cFmt)
|
||||
cursor.insertText(stripEscape(temp[start:pos]), cFmt)
|
||||
|
||||
# Construct next format
|
||||
if fmt == TextFmt.B_B:
|
||||
@@ -425,13 +428,12 @@ class ToQTextDocument(Tokenizer):
|
||||
if field := data.partition(":")[2]:
|
||||
self._usedFields.append((cursor.position(), field))
|
||||
cursor.insertText("0", cFmt)
|
||||
pass
|
||||
|
||||
# Move pos for next pass
|
||||
start = pos
|
||||
|
||||
# Insert whatever is left in the buffer
|
||||
cursor.insertText(temp[start:], cFmt)
|
||||
cursor.insertText(stripEscape(temp[start:]), cFmt)
|
||||
|
||||
return
|
||||
|
||||
|
||||
@@ -1090,11 +1090,14 @@ class GuiDocEditor(QPlainTextEdit):
|
||||
show = self._completer.updateMetaText(text, bPos)
|
||||
else:
|
||||
show = self._completer.updateCommentText(text, bPos)
|
||||
point = self.cursorRect().bottomRight()
|
||||
self._completer.move(viewport.mapToGlobal(point))
|
||||
self._completer.setVisible(show)
|
||||
if show:
|
||||
point = self.cursorRect().bottomRight()
|
||||
self._completer.move(viewport.mapToGlobal(point))
|
||||
self._completer.show()
|
||||
else:
|
||||
self._completer.close()
|
||||
else:
|
||||
self._completer.setVisible(False)
|
||||
self._completer.close()
|
||||
|
||||
if self._doReplace and added == 1:
|
||||
cursor = self.textCursor()
|
||||
@@ -2113,9 +2116,7 @@ class CommandCompleter(QMenu):
|
||||
length = len(lookup)
|
||||
suffix = ""
|
||||
options = sorted(filter(
|
||||
lambda x: lookup in x.lower(), SHARED.project.index.getClassTags(
|
||||
nwKeyWords.KEY_CLASS.get(kw.strip())
|
||||
)
|
||||
lambda x: lookup in x.lower(), SHARED.project.index.getKeyWordTags(kw.strip())
|
||||
))[:15]
|
||||
|
||||
if not options:
|
||||
|
||||
@@ -228,6 +228,8 @@ class GuiDocViewer(QTextBrowser):
|
||||
qDoc.setTheme(self._docTheme)
|
||||
qDoc.initDocument()
|
||||
qDoc.setKeywords(True)
|
||||
qDoc.setCommentType(nwComment.NOTE, CONFIG.viewComments)
|
||||
qDoc.setCommentType(nwComment.STORY, CONFIG.viewComments)
|
||||
qDoc.setCommentType(nwComment.PLAIN, CONFIG.viewComments)
|
||||
qDoc.setCommentType(nwComment.SYNOPSIS, CONFIG.viewSynopsis)
|
||||
qDoc.setCommentType(nwComment.SHORT, CONFIG.viewSynopsis)
|
||||
|
||||
@@ -593,6 +593,7 @@ class GuiNovelTree(NTreeView):
|
||||
lines = []
|
||||
if head := SHARED.project.index.getItemHeading(tHandle, sTitle):
|
||||
tags = head.getReferences()
|
||||
appendTags(tags, nwKeyWords.TAG_KEY, lines)
|
||||
appendTags(tags, nwKeyWords.POV_KEY, lines)
|
||||
appendTags(tags, nwKeyWords.FOCUS_KEY, lines)
|
||||
appendTags(tags, nwKeyWords.CHAR_KEY, lines)
|
||||
|
||||
@@ -43,7 +43,7 @@ from PyQt6.QtWidgets import (
|
||||
|
||||
from novelwriter import CONFIG, SHARED
|
||||
from novelwriter.common import fuzzyTime, qtLambda
|
||||
from novelwriter.constants import nwLabels, nwStats, trStats
|
||||
from novelwriter.constants import nwHeadFmt, nwLabels, nwStats, nwUnicode, trStats
|
||||
from novelwriter.core.buildsettings import BuildCollection, BuildSettings
|
||||
from novelwriter.core.docbuild import NWBuildDocument
|
||||
from novelwriter.extensions.modified import NIconToggleButton, NIconToolButton, NToolDialog
|
||||
@@ -595,11 +595,7 @@ class _DetailsWidget(QWidget):
|
||||
item.addChild(sub)
|
||||
|
||||
# Headings
|
||||
hFmt = HeadingFormatter(SHARED.project)
|
||||
hFmt.incChapter()
|
||||
hFmt.incScene()
|
||||
hFmt.resetScene()
|
||||
hFmt.incScene()
|
||||
hFmt = HeadingFormatter(SHARED.project, 7, 5, 23)
|
||||
title = self.tr("Title")
|
||||
hidden = self.tr("Hidden")
|
||||
|
||||
@@ -620,7 +616,8 @@ class _DetailsWidget(QWidget):
|
||||
if build.getBool(hHide):
|
||||
sub.setText(1, f"[{hidden}]")
|
||||
else:
|
||||
sub.setText(1, hFmt.apply(build.getStr(hFormat), title, 0))
|
||||
preview = build.getStr(hFormat).replace(nwHeadFmt.BR, nwUnicode.U_LBREAK)
|
||||
sub.setText(1, hFmt.apply(preview, title, 0))
|
||||
item.addChild(sub)
|
||||
|
||||
# Text Content
|
||||
|
||||
@@ -38,7 +38,7 @@ from PyQt6.QtWidgets import (
|
||||
|
||||
from novelwriter import CONFIG, SHARED
|
||||
from novelwriter.common import describeFont, fontMatcher, qtAddAction, qtLambda
|
||||
from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, trConst
|
||||
from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwUnicode, trConst
|
||||
from novelwriter.core.buildsettings import BuildSettings, FilterMode
|
||||
from novelwriter.extensions.configlayout import (
|
||||
NColorLabel, NFixedPage, NScrollableForm, NScrollablePage
|
||||
@@ -795,12 +795,15 @@ class _HeadingsTab(NScrollablePage):
|
||||
|
||||
def loadContent(self) -> None:
|
||||
"""Populate the widgets."""
|
||||
self.fmtPart.setText(self._build.getStr("headings.fmtPart"))
|
||||
self.fmtChapter.setText(self._build.getStr("headings.fmtChapter"))
|
||||
self.fmtUnnumbered.setText(self._build.getStr("headings.fmtUnnumbered"))
|
||||
self.fmtScene.setText(self._build.getStr("headings.fmtScene"))
|
||||
self.fmtAScene.setText(self._build.getStr("headings.fmtAltScene"))
|
||||
self.fmtSection.setText(self._build.getStr("headings.fmtSection"))
|
||||
def fmtBreak(text: str) -> str:
|
||||
return text.replace(nwHeadFmt.BR, nwUnicode.U_LBREAK)
|
||||
|
||||
self.fmtPart.setText(fmtBreak(self._build.getStr("headings.fmtPart")))
|
||||
self.fmtChapter.setText(fmtBreak(self._build.getStr("headings.fmtChapter")))
|
||||
self.fmtUnnumbered.setText(fmtBreak(self._build.getStr("headings.fmtUnnumbered")))
|
||||
self.fmtScene.setText(fmtBreak(self._build.getStr("headings.fmtScene")))
|
||||
self.fmtAScene.setText(fmtBreak(self._build.getStr("headings.fmtAltScene")))
|
||||
self.fmtSection.setText(fmtBreak(self._build.getStr("headings.fmtSection")))
|
||||
|
||||
self.swtPart.setChecked(self._build.getBool("headings.hidePart"))
|
||||
self.swtChapter.setChecked(self._build.getBool("headings.hideChapter"))
|
||||
@@ -880,7 +883,7 @@ class _HeadingsTab(NScrollablePage):
|
||||
text = ""
|
||||
label = self.tr("None")
|
||||
|
||||
self.editTextBox.setPlainText(text.replace(nwHeadFmt.BR, "\n"))
|
||||
self.editTextBox.setPlainText(text.replace(nwUnicode.U_LBREAK, "\n"))
|
||||
self.lblEditForm.setText(self.tr("Editing: {0}").format(label))
|
||||
|
||||
return
|
||||
@@ -893,25 +896,26 @@ class _HeadingsTab(NScrollablePage):
|
||||
def _saveFormat(self) -> None:
|
||||
"""Save the format from the edit text box."""
|
||||
heading = self._editing
|
||||
text = self.editTextBox.toPlainText().strip().replace("\n", nwHeadFmt.BR)
|
||||
text = self.editTextBox.toPlainText().strip().replace("\n", nwUnicode.U_LBREAK)
|
||||
value = text.replace(nwUnicode.U_LBREAK, nwHeadFmt.BR)
|
||||
if heading == self.EDIT_TITLE:
|
||||
self.fmtPart.setText(text)
|
||||
self._build.setValue("headings.fmtPart", text)
|
||||
self._build.setValue("headings.fmtPart", value)
|
||||
elif heading == self.EDIT_CHAPTER:
|
||||
self.fmtChapter.setText(text)
|
||||
self._build.setValue("headings.fmtChapter", text)
|
||||
self._build.setValue("headings.fmtChapter", value)
|
||||
elif heading == self.EDIT_UNNUM:
|
||||
self.fmtUnnumbered.setText(text)
|
||||
self._build.setValue("headings.fmtUnnumbered", text)
|
||||
self._build.setValue("headings.fmtUnnumbered", value)
|
||||
elif heading == self.EDIT_SCENE:
|
||||
self.fmtScene.setText(text)
|
||||
self._build.setValue("headings.fmtScene", text)
|
||||
self._build.setValue("headings.fmtScene", value)
|
||||
elif heading == self.EDIT_HSCENE:
|
||||
self.fmtAScene.setText(text)
|
||||
self._build.setValue("headings.fmtAltScene", text)
|
||||
self._build.setValue("headings.fmtAltScene", value)
|
||||
elif heading == self.EDIT_SECTION:
|
||||
self.fmtSection.setText(text)
|
||||
self._build.setValue("headings.fmtSection", text)
|
||||
self._build.setValue("headings.fmtSection", value)
|
||||
else:
|
||||
return
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ QtVAlignSuper = QTextCharFormat.VerticalAlignment.AlignSuperScript
|
||||
|
||||
QtPageBreakBefore = QTextFormat.PageBreakFlag.PageBreak_AlwaysBefore
|
||||
QtPageBreakAfter = QTextFormat.PageBreakFlag.PageBreak_AlwaysAfter
|
||||
QtPageBreakAuto = QTextFormat.PageBreakFlag.PageBreak_Auto
|
||||
|
||||
QtPropLineHeight = 1 # QTextBlockFormat.LineHeightTypes.ProportionalHeight
|
||||
|
||||
|
||||
24
pkgutils.py
24
pkgutils.py
@@ -272,24 +272,12 @@ if __name__ == "__main__":
|
||||
cmdBuildUbuntu.set_defaults(func=utils.build_debian.launchpad)
|
||||
|
||||
# Build AppImage
|
||||
cmdBuildAppImage = parsers.add_parser(
|
||||
"build-appimage", help=(
|
||||
"Build an AppImage. "
|
||||
"Argument --linux-tag defaults manylinux_2_28_x86_64, and --python-version to 3.13."
|
||||
)
|
||||
)
|
||||
cmdBuildAppImage.add_argument(
|
||||
"--linux-tag",
|
||||
default="manylinux_2_28_x86_64",
|
||||
help=(
|
||||
"Linux compatibility tag (e.g. manylinux_2_28_x86_64) "
|
||||
"see https://python-appimage.readthedocs.io/en/latest/#available-python-appimages "
|
||||
"and https://github.com/pypa/manylinux for a list of valid tags."
|
||||
),
|
||||
)
|
||||
cmdBuildAppImage.add_argument(
|
||||
"--python-version", default="3.13", help="Python version (e.g. 3.13)"
|
||||
)
|
||||
# See https://github.com/pypa/manylinux
|
||||
# See https://python-appimage.readthedocs.io/en/latest/#available-python-appimages
|
||||
cmdBuildAppImage = parsers.add_parser("build-appimage", help="Build an AppImage.")
|
||||
cmdBuildAppImage.add_argument("linux", help="Manylinux version, e.g. manylinux_2_28.")
|
||||
cmdBuildAppImage.add_argument("arch", help="Architecture, e.g. x86_64.")
|
||||
cmdBuildAppImage.add_argument("python", help="Python version, e.g. 3.13.")
|
||||
cmdBuildAppImage.set_defaults(func=utils.build_appimage.appImage)
|
||||
|
||||
# Build Windows Inno Setup Installer
|
||||
|
||||
@@ -7,9 +7,9 @@ name = "novelWriter"
|
||||
authors = [
|
||||
{name = "Veronica Berglyd Olsen", email = "code@vkbo.net"},
|
||||
]
|
||||
description = "A markdown-like text editor for planning and writing novels"
|
||||
description = "A plain text editor for planning and writing novels"
|
||||
readme = {file = "setup/description_pypi.md", content-type = "text/markdown"}
|
||||
license = {text = "GNU General Public License v3"}
|
||||
license = "GPL-3.0"
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
%%~name: Making a Scene
|
||||
%%~path: 6a2d6d5f4f401/636b6aa9b697b
|
||||
%%~kind: NOVEL/DOCUMENT
|
||||
%%~hash: 1f3d98a6a27b4f9a7f2239fcd78f9e2263cd3b97
|
||||
%%~date: Unknown/2025-05-18 22:36:59
|
||||
%%~hash: b39201b02e3db63493d61d09e5fec5765a937255
|
||||
%%~date: Unknown/2025-06-24 09:09:56
|
||||
### Making a Scene
|
||||
|
||||
@tag: Scene
|
||||
@pov: Jane
|
||||
@char: John, Jane
|
||||
@location: Earth
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="2.7" hexVersion="0x020700f0" fileVersion="1.5" fileRevision="5" timeStamp="2025-05-31 23:25:11">
|
||||
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="2193" autoCount="286" editTime="96887">
|
||||
<novelWriterXML appVersion="2.7.2" hexVersion="0x020702f0" fileVersion="1.5" fileRevision="5" timeStamp="2025-06-24 16:24:56">
|
||||
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="2209" autoCount="287" editTime="97152">
|
||||
<name>Sample Project</name>
|
||||
<author>Jane Smith</author>
|
||||
</project>
|
||||
@@ -58,7 +58,7 @@
|
||||
<name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name>
|
||||
</item>
|
||||
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="no" heading="H3" charCount="2999" wordCount="530" paraCount="16" cursorPos="718" />
|
||||
<meta expanded="no" heading="H3" charCount="2999" wordCount="530" paraCount="16" cursorPos="93" />
|
||||
<name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name>
|
||||
</item>
|
||||
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
|
||||
@@ -24,7 +24,7 @@ cat > $HOME/.local/share/applications/novelWriter.desktop <<EOF
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=novelWriter
|
||||
Comment=A markdown-like text editor for planning and writing novels
|
||||
Comment=A plain text editor for planning and writing novels
|
||||
Exec=${IMGPATH} %f
|
||||
Icon=${ICONPATH}
|
||||
Categories=Qt;Office;WordProcessor;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=novelWriter
|
||||
Comment=A markdown-like text editor for planning and writing novels
|
||||
Comment=A plain text editor for planning and writing novels
|
||||
Exec=novelwriter %f
|
||||
Icon=novelwriter
|
||||
Categories=Qt;Office;WordProcessor;
|
||||
|
||||
@@ -10,10 +10,4 @@ X-Python3-Version: >= 3.10
|
||||
Package: novelwriter
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends}, ${python3:Depends}, python3 (>=3.10), python3-pyqt6 (>= 6.4), python3-pyqt6.qtsvg (>= 6.4), python3-enchant (>= 2.0)
|
||||
Description: A markdown-like text editor for planning and writing novels
|
||||
novelWriter is a plain text editor designed for writing novels assembled from
|
||||
many smaller text documents. It uses a minimal formatting syntax inspired by
|
||||
Markdown, and adds a meta data syntax for comments, synopsis, and
|
||||
cross-referencing. It's designed to be a simple text editor that allows for
|
||||
easy organisation of text and notes, using human readable text files as
|
||||
storage for robustness.
|
||||
Description: A plain text editor for planning and writing novels
|
||||
|
||||
BIN
setup/icons/novelwriter.png
Normal file
BIN
setup/icons/novelwriter.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.0 KiB |
BIN
setup/icons/x-novelwriter-project.png
Normal file
BIN
setup/icons/x-novelwriter-project.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
@@ -11,7 +11,7 @@
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>novelWriter</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>novelWriter: A markdown-like text editor for planning and writing novels.</string>
|
||||
<string>novelWriter: A plain text editor for planning and writing novels.</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>novelwriter.icns</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
|
||||
@@ -174,7 +174,7 @@ rm lib/python3.*/site-packages/PyQt6/QtWebEngine* || true
|
||||
rm -r lib/python3.*/site-packages/PyQt6/Qt6/translations/qtwebengine* || true
|
||||
rm -r lib/python3.*/site-packages/PyQt6/Qt6/plugins/webview/libqtwebview* || true
|
||||
|
||||
# Remove unneeded QtQuick/Decaritive components
|
||||
# Remove unneeded QtQuick/Declarative components
|
||||
rm lib/python3.*/site-packages/PyQt6/QtQml* || true
|
||||
rm lib/python3.*/site-packages/PyQt6/QtQuick* || true
|
||||
rm lib/python3.*/site-packages/PyQt6/WebChannel* || true
|
||||
@@ -200,16 +200,16 @@ mkdir -p $RLS_DIR
|
||||
# --- Create DMG -------------------------------------------------------------------------------- #
|
||||
|
||||
# Generate .dmg
|
||||
echo "Packageing DMG ..."
|
||||
echo "Packaging DMG ..."
|
||||
brew install create-dmg
|
||||
|
||||
create-dmg --volname "novelWriter $VERSION" --volicon $SRC_DIR/setup/macos/novelwriter.icns \
|
||||
--window-pos 200 120 --window-size 800 400 --icon-size 100 \
|
||||
--icon novelWriter.app 200 190 --hide-extension novelWriter.app \
|
||||
--app-drop-link 600 185 $RLS_DIR/novelWriter-"${VERSION}"-$ARCH.dmg "$BUILD_DIR"/
|
||||
--app-drop-link 600 185 $RLS_DIR/novelwriter-"${VERSION}"-$ARCH.dmg "$BUILD_DIR"/
|
||||
|
||||
pushd $RLS_DIR || exit 1
|
||||
shasum -a 256 novelWriter-"${VERSION}"-$ARCH.dmg | tee novelWriter-"${VERSION}"-$ARCH.dmg.sha256
|
||||
shasum -a 256 novelwriter-"${VERSION}"-$ARCH.dmg | tee novelwriter-"${VERSION}"-$ARCH.dmg.sha256
|
||||
popd || exit 1
|
||||
|
||||
rm -r $CONDA_PATH
|
||||
|
||||
@@ -95,12 +95,35 @@ def testCoreIndex_LoadSave(qtbot, monkeypatch, prjLipsum, nwGUI, tstPaths):
|
||||
tagIndex = str(index._tagsIndex.packData())
|
||||
itemsIndex = str(index._itemIndex.packData())
|
||||
|
||||
# Update item class
|
||||
bHandle = "4c4f28287af27"
|
||||
bItem = project.tree[bHandle]
|
||||
assert bItem is not None
|
||||
|
||||
tagsBod = index._tagsIndex["Bod"]
|
||||
assert tagsBod is not None
|
||||
assert tagsBod["handle"] == bHandle
|
||||
assert tagsBod["class"] == "CHARACTER"
|
||||
|
||||
bItem.setClass(nwItemClass.CUSTOM)
|
||||
index.refreshHandle(bHandle)
|
||||
assert tagsBod is not None
|
||||
assert tagsBod["handle"] == bHandle
|
||||
assert tagsBod["class"] == "CUSTOM"
|
||||
|
||||
# Update item class to inactive
|
||||
bItem.setClass(nwItemClass.TRASH)
|
||||
index.refreshHandle(bHandle)
|
||||
assert "Bod" not in index._tagsIndex
|
||||
bItem.setClass(nwItemClass.CHARACTER)
|
||||
index.reIndexHandle(bHandle)
|
||||
|
||||
# Delete a handle
|
||||
assert index._tagsIndex["Bod"] is not None
|
||||
assert index._itemIndex["4c4f28287af27"] is not None
|
||||
index.deleteHandle("4c4f28287af27")
|
||||
assert index._itemIndex[bHandle] is not None
|
||||
index.deleteHandle(bHandle)
|
||||
assert index._tagsIndex["Bod"] is None
|
||||
assert index._itemIndex["4c4f28287af27"] is None
|
||||
assert index._itemIndex[bHandle] is None
|
||||
|
||||
# Clear the index
|
||||
index.clear()
|
||||
@@ -672,6 +695,7 @@ def testCoreIndex_ExtractData(nwGUI, fncPath, mockRnd):
|
||||
))
|
||||
assert index.scanText(nHandle, (
|
||||
"# Hello World!\n"
|
||||
"@tag: Scene\n"
|
||||
"@pov: Jane\n"
|
||||
"@char: Jane, John\n\n"
|
||||
"% this is a comment\n\n"
|
||||
@@ -726,11 +750,13 @@ def testCoreIndex_ExtractData(nwGUI, fncPath, mockRnd):
|
||||
|
||||
# Look up an invalid handle
|
||||
refs = index.getReferences("Not a handle")
|
||||
assert refs["@tag"] == []
|
||||
assert refs["@pov"] == []
|
||||
assert refs["@char"] == []
|
||||
|
||||
# The novel file should now refer to Jane as @pov and @char
|
||||
refs = index.getReferences(nHandle)
|
||||
assert refs["@tag"] == ["Scene"]
|
||||
assert refs["@pov"] == ["Jane"]
|
||||
assert refs["@char"] == ["Jane", "John"]
|
||||
|
||||
@@ -766,11 +792,12 @@ def testCoreIndex_ExtractData(nwGUI, fncPath, mockRnd):
|
||||
assert index.getDocumentTags(cHandle) == ["jane"]
|
||||
assert index.getDocumentTags(None) == []
|
||||
|
||||
# getClassTags
|
||||
# ============
|
||||
assert index.getClassTags(None) == ["Jane", "John"]
|
||||
assert index.getClassTags(nwItemClass.CHARACTER) == ["Jane", "John"]
|
||||
assert index.getClassTags(nwItemClass.PLOT) == []
|
||||
# getKeyWordTags
|
||||
# ==============
|
||||
assert index.getKeyWordTags("@mention") == ["Jane", "John", "Scene"]
|
||||
assert index.getKeyWordTags("@char") == ["Jane", "John"]
|
||||
assert index.getKeyWordTags("@plot") == []
|
||||
assert index.getKeyWordTags("@tag") == []
|
||||
|
||||
# getTagsData
|
||||
# ===========
|
||||
|
||||
@@ -267,6 +267,7 @@ def testCoreIndexData_IndexHeadingReferences():
|
||||
head = IndexHeading(cache, "T0001")
|
||||
|
||||
# Add some references
|
||||
head.setTag("Scene")
|
||||
head.addReference("Jane", "@pov")
|
||||
head.addReference("Jane", "@char")
|
||||
head.addReference("John", "@char")
|
||||
@@ -290,6 +291,7 @@ def testCoreIndexData_IndexHeadingReferences():
|
||||
}
|
||||
|
||||
# Set names
|
||||
cache.tags.add("Scene", "Scene", "0000000000000", "T00001", "NOVEL")
|
||||
cache.tags.add("Jane", "Jane", "0000000000000", "T00001", "CHARACTER")
|
||||
cache.tags.add("John", "John", "0000000000000", "T00001", "CHARACTER")
|
||||
cache.tags.add("Main", "Main", "0000000000000", "T00001", "PLOT")
|
||||
@@ -301,7 +303,7 @@ def testCoreIndexData_IndexHeadingReferences():
|
||||
"@plot": ["Main"],
|
||||
"@object": ["Gun"],
|
||||
"@story": [],
|
||||
"@tag": [],
|
||||
"@tag": ["Scene"],
|
||||
"@focus": [],
|
||||
"@custom": [],
|
||||
"@time": [],
|
||||
@@ -316,7 +318,7 @@ def testCoreIndexData_IndexHeadingReferences():
|
||||
assert head.getReferencesByKeyword("@plot") == ["Main"]
|
||||
assert head.getReferencesByKeyword("@object") == ["Gun"]
|
||||
assert head.getReferencesByKeyword("@story") == []
|
||||
assert head.getReferencesByKeyword("@tag") == []
|
||||
assert head.getReferencesByKeyword("@tag") == ["Scene"]
|
||||
assert head.getReferencesByKeyword("@focus") == []
|
||||
assert head.getReferencesByKeyword("@custom") == []
|
||||
assert head.getReferencesByKeyword("@time") == []
|
||||
|
||||
@@ -147,9 +147,6 @@ def testFmtToHtml_ConvertParagraphs(mockGUI):
|
||||
html._isNovel = True
|
||||
html._isFirst = True
|
||||
|
||||
# Paragraphs
|
||||
# ==========
|
||||
|
||||
# Text
|
||||
html._text = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
|
||||
html.tokenizeText()
|
||||
@@ -280,6 +277,54 @@ def testFmtToHtml_ConvertParagraphs(mockGUI):
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testFmtToHtml_Alignment(mockGUI):
|
||||
"""Test paragraph alignment in the ToHtml class."""
|
||||
project = NWProject()
|
||||
html = ToHtml(project)
|
||||
html.initDocument()
|
||||
|
||||
# Left
|
||||
html._text = "This is text <<\nspanning multiple\nlines"
|
||||
html.tokenizeText()
|
||||
html.doConvert()
|
||||
assert html._pages[-1] == (
|
||||
"<p style='text-align: left;'>This is text<br>spanning multiple<br>lines</p>\n"
|
||||
)
|
||||
|
||||
# Right
|
||||
html._text = ">> This is text\nspanning multiple\nlines"
|
||||
html.tokenizeText()
|
||||
html.doConvert()
|
||||
assert html._pages[-1] == (
|
||||
"<p style='text-align: right;'>This is text<br>spanning multiple<br>lines</p>\n"
|
||||
)
|
||||
|
||||
# Centre
|
||||
html._text = ">> This is text <<\nspanning multiple\nlines"
|
||||
html.tokenizeText()
|
||||
html.doConvert()
|
||||
assert html._pages[-1] == (
|
||||
"<p style='text-align: center;'>This is text<br>spanning multiple<br>lines</p>\n"
|
||||
)
|
||||
|
||||
# Left before Right
|
||||
html._text = ">> This is text\nspanning multiple <<\nlines"
|
||||
html.tokenizeText()
|
||||
html.doConvert()
|
||||
assert html._pages[-1] == (
|
||||
"<p style='text-align: left;'>This is text<br>spanning multiple<br>lines</p>\n"
|
||||
)
|
||||
|
||||
# Right before Centre
|
||||
html._text = ">> This is text <<\n>> spanning multiple\nlines"
|
||||
html.tokenizeText()
|
||||
html.doConvert()
|
||||
assert html._pages[-1] == (
|
||||
"<p style='text-align: right;'>This is text<br>spanning multiple<br>lines</p>\n"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testFmtToHtml_Dialog(mockGUI):
|
||||
"""Test paragraph formats in the ToHtml class."""
|
||||
|
||||
@@ -1074,6 +1074,80 @@ def testFmtToken_Paragraphs(mockGUI):
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testFmtToken_BreakAlignIndent(mockGUI):
|
||||
"""Test the splitting of paragraphs with alignment."""
|
||||
project = NWProject()
|
||||
tokens = BareTokenizer(project)
|
||||
tokens._handle = TMH
|
||||
|
||||
for text in [
|
||||
"This is text <<\nspanning multiple\nlines",
|
||||
"This is text\nspanning multiple <<\nlines",
|
||||
"This is text\nspanning multiple\nlines <<",
|
||||
]:
|
||||
# Preserve Breaks
|
||||
tokens.setKeepLineBreaks(True)
|
||||
tokens._text = text
|
||||
tokens.tokenizeText()
|
||||
assert tokens._blocks == [
|
||||
(BlockTyp.TEXT, "", "This is text\nspanning multiple\nlines", [], BlockFmt.LEFT),
|
||||
]
|
||||
|
||||
# Don't Preserve Breaks
|
||||
tokens.setKeepLineBreaks(False)
|
||||
tokens._text = text
|
||||
tokens.tokenizeText()
|
||||
assert tokens._blocks == [
|
||||
(BlockTyp.TEXT, "", "This is text spanning multiple lines", [], BlockFmt.LEFT),
|
||||
]
|
||||
|
||||
# With Justify
|
||||
# This should disable justify
|
||||
tokens.setKeepLineBreaks(True)
|
||||
tokens.setJustify(True)
|
||||
tokens._text = text
|
||||
tokens.tokenizeText()
|
||||
assert tokens._blocks == [
|
||||
(BlockTyp.TEXT, "", "This is text\nspanning multiple\nlines", [], BlockFmt.LEFT),
|
||||
]
|
||||
|
||||
# With Indent
|
||||
# This should disable indent
|
||||
tokens.setKeepLineBreaks(True)
|
||||
tokens.setFirstLineIndent(True, 1.0, False)
|
||||
tokens._text = text
|
||||
tokens.tokenizeText()
|
||||
assert tokens._blocks == [
|
||||
(BlockTyp.TEXT, "", "This is text\nspanning multiple\nlines", [], BlockFmt.LEFT),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testFmtToken_BreakJustify(mockGUI):
|
||||
"""Test the of processing of justify with breaks."""
|
||||
project = NWProject()
|
||||
tokens = BareTokenizer(project)
|
||||
tokens._handle = TMH
|
||||
tokens.setJustify(True)
|
||||
|
||||
# Applied to all lines when breaks are preserved
|
||||
tokens._text = "This is text\nspanning multiple\nlines"
|
||||
tokens.setKeepLineBreaks(True)
|
||||
tokens.tokenizeText()
|
||||
assert tokens._blocks == [
|
||||
(BlockTyp.TEXT, "", "This is text\nspanning multiple\nlines", [], BlockFmt.JUSTIFY),
|
||||
]
|
||||
|
||||
# Turning off breaks should make no difference (see issue #2426)
|
||||
tokens._text = "This is text\nspanning multiple\nlines"
|
||||
tokens.setKeepLineBreaks(False)
|
||||
tokens.tokenizeText()
|
||||
assert tokens._blocks == [
|
||||
(BlockTyp.TEXT, "", "This is text spanning multiple lines", [], BlockFmt.JUSTIFY),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testFmtToken_TextFormat(mockGUI):
|
||||
"""Test the tokenization of text formats in the Tokenizer class."""
|
||||
@@ -1654,7 +1728,7 @@ def testFmtToken_ProcessHeaders(mockGUI):
|
||||
# H2: Chapter Word Number
|
||||
tokens._text = "## Chapter\n"
|
||||
tokens.setChapterFormat(f"Chapter {nwHeadFmt.CH_WORD}")
|
||||
tokens._hFormatter._chCount = 0
|
||||
tokens._hFormatter._chapter = 0
|
||||
tokens.tokenizeText()
|
||||
assert tokens._blocks == [
|
||||
(BlockTyp.HEAD1, TM1, "Chapter One", [], BlockFmt.PBB),
|
||||
@@ -1728,8 +1802,8 @@ def testFmtToken_ProcessHeaders(mockGUI):
|
||||
# H3: Scene w/Absolute Number
|
||||
tokens._text = "### A Scene\n"
|
||||
tokens.setSceneFormat(f"Scene {nwHeadFmt.SC_ABS}", False)
|
||||
tokens._hFormatter._scAbsCount = 0
|
||||
tokens._hFormatter._scChCount = 0
|
||||
tokens._hFormatter._scene = 0
|
||||
tokens._hFormatter._absolute = 0
|
||||
tokens.tokenizeText()
|
||||
assert tokens._blocks == [
|
||||
(BlockTyp.HEAD2, TM1, "Scene 1", [], BlockFmt.NONE),
|
||||
@@ -1738,8 +1812,8 @@ def testFmtToken_ProcessHeaders(mockGUI):
|
||||
# H3: Scene w/Chapter Number
|
||||
tokens._text = "### A Scene\n"
|
||||
tokens.setSceneFormat(f"Scene {nwHeadFmt.CH_NUM}.{nwHeadFmt.SC_NUM}", False)
|
||||
tokens._hFormatter._scAbsCount = 0
|
||||
tokens._hFormatter._scChCount = 1
|
||||
tokens._hFormatter._scene = 1
|
||||
tokens._hFormatter._absolute = 0
|
||||
tokens.tokenizeText()
|
||||
assert tokens._blocks == [
|
||||
(BlockTyp.HEAD2, TM1, "Scene 3.2", [], BlockFmt.NONE),
|
||||
@@ -2544,9 +2618,9 @@ def testFmtToken_HeadingFormatter(fncPath, mockGUI, mockRnd):
|
||||
|
||||
# Special Formats
|
||||
# ===============
|
||||
formatter._chCount = 2
|
||||
formatter._scChCount = 3
|
||||
formatter._scAbsCount = 8
|
||||
formatter._chapter = 2
|
||||
formatter._scene = 3
|
||||
formatter._absolute = 8
|
||||
|
||||
# Chapter Number Word
|
||||
cFormat = f"Chapter {nwHeadFmt.CH_WORD}, Scene {nwHeadFmt.SC_NUM} - {nwHeadFmt.TITLE}"
|
||||
|
||||
@@ -733,7 +733,7 @@ def testFmtToOdt_ConvertParagraphs(mockGUI):
|
||||
'<office:text>'
|
||||
'<text:h text:style-name="Heading_20_2" text:outline-level="2">Scene</text:h>'
|
||||
'<text:p text:style-name="P7">Regular paragraph</text:p>'
|
||||
'<text:p text:style-name="Text_20_body">with<text:line-break />break</text:p>'
|
||||
'<text:p text:style-name="P7">with<text:line-break />break</text:p>'
|
||||
'<text:p text:style-name="Text_20_body">Left Align</text:p>'
|
||||
'</office:text>'
|
||||
)
|
||||
|
||||
@@ -25,14 +25,15 @@ import pytest
|
||||
from PyQt6.QtGui import QFont, QTextBlock, QTextCharFormat, QTextCursor
|
||||
|
||||
from novelwriter import CONFIG
|
||||
from novelwriter.constants import nwUnicode
|
||||
from novelwriter.constants import nwHeadFmt, nwUnicode
|
||||
from novelwriter.core.project import NWProject
|
||||
from novelwriter.enum import nwComment
|
||||
from novelwriter.formats.shared import BlockFmt, BlockTyp, TextDocumentTheme
|
||||
from novelwriter.formats.toqdoc import ToQTextDocument
|
||||
from novelwriter.types import (
|
||||
QtAlignAbsolute, QtAlignCenter, QtAlignJustify, QtAlignLeft, QtAlignRight,
|
||||
QtPageBreakAfter, QtTransparent, QtVAlignNormal, QtVAlignSub, QtVAlignSuper
|
||||
QtPageBreakAfter, QtPageBreakAuto, QtPageBreakBefore, QtTransparent,
|
||||
QtVAlignNormal, QtVAlignSub, QtVAlignSuper
|
||||
)
|
||||
|
||||
THEME = TextDocumentTheme()
|
||||
@@ -54,6 +55,11 @@ def testFmtToQTextDocument_ConvertHeaders(mockGUI):
|
||||
|
||||
doc._isNovel = True
|
||||
doc._isFirst = True
|
||||
|
||||
# Add a line break in chapter header format, see #2415
|
||||
doc.setChapterFormat(f"{nwHeadFmt.CH_NUM}{nwHeadFmt.BR}{nwHeadFmt.TITLE}")
|
||||
|
||||
# Populate
|
||||
doc._text = (
|
||||
"#! Title\n"
|
||||
"# Partition\n"
|
||||
@@ -63,12 +69,13 @@ def testFmtToQTextDocument_ConvertHeaders(mockGUI):
|
||||
)
|
||||
doc.tokenizeText()
|
||||
doc.doConvert()
|
||||
assert doc.document.blockCount() == 5
|
||||
assert doc.document.blockCount() == 6
|
||||
|
||||
# Title
|
||||
block = doc.document.findBlockByNumber(0)
|
||||
assert block.text() == "Title"
|
||||
bFmt = block.blockFormat()
|
||||
assert bFmt.pageBreakPolicy() == QtPageBreakAuto # Expected on first title
|
||||
assert bFmt.topMargin() == doc._mHead[BlockTyp.TITLE][0]
|
||||
assert bFmt.bottomMargin() == doc._mHead[BlockTyp.TITLE][1]
|
||||
cFmt = charFmtInBlock(block, 1)
|
||||
@@ -87,10 +94,23 @@ def testFmtToQTextDocument_ConvertHeaders(mockGUI):
|
||||
assert cFmt.fontPointSize() == doc._sHead[BlockTyp.PART]
|
||||
assert cFmt.foreground().color() == THEME.head
|
||||
|
||||
# Chapter
|
||||
# Chapter, Line 1
|
||||
block = doc.document.findBlockByNumber(2)
|
||||
assert block.text() == "1"
|
||||
bFmt = block.blockFormat()
|
||||
assert bFmt.pageBreakPolicy() == QtPageBreakBefore
|
||||
assert bFmt.topMargin() == doc._mHead[BlockTyp.HEAD1][0]
|
||||
assert bFmt.bottomMargin() == doc._mHead[BlockTyp.HEAD1][1]
|
||||
cFmt = charFmtInBlock(block, 1)
|
||||
assert cFmt.fontWeight() == QFont.Weight.Bold
|
||||
assert cFmt.fontPointSize() == doc._sHead[BlockTyp.HEAD1]
|
||||
assert cFmt.foreground().color() == THEME.head
|
||||
|
||||
# Chapter, Line 2
|
||||
block = doc.document.findBlockByNumber(3)
|
||||
assert block.text() == "Chapter"
|
||||
bFmt = block.blockFormat()
|
||||
assert bFmt.pageBreakPolicy() == QtPageBreakAuto # Important! See #2415
|
||||
assert bFmt.topMargin() == doc._mHead[BlockTyp.HEAD1][0]
|
||||
assert bFmt.bottomMargin() == doc._mHead[BlockTyp.HEAD1][1]
|
||||
cFmt = charFmtInBlock(block, 1)
|
||||
@@ -99,9 +119,10 @@ def testFmtToQTextDocument_ConvertHeaders(mockGUI):
|
||||
assert cFmt.foreground().color() == THEME.head
|
||||
|
||||
# Scene
|
||||
block = doc.document.findBlockByNumber(3)
|
||||
block = doc.document.findBlockByNumber(4)
|
||||
assert block.text() == "Scene"
|
||||
bFmt = block.blockFormat()
|
||||
assert bFmt.pageBreakPolicy() == QtPageBreakAuto
|
||||
assert bFmt.topMargin() == doc._mHead[BlockTyp.HEAD2][0]
|
||||
assert bFmt.bottomMargin() == doc._mHead[BlockTyp.HEAD2][1]
|
||||
cFmt = charFmtInBlock(block, 1)
|
||||
@@ -110,9 +131,10 @@ def testFmtToQTextDocument_ConvertHeaders(mockGUI):
|
||||
assert cFmt.foreground().color() == THEME.head
|
||||
|
||||
# Section
|
||||
block = doc.document.findBlockByNumber(4)
|
||||
block = doc.document.findBlockByNumber(5)
|
||||
assert block.text() == "Section"
|
||||
bFmt = block.blockFormat()
|
||||
assert bFmt.pageBreakPolicy() == QtPageBreakAuto
|
||||
assert bFmt.topMargin() == doc._mHead[BlockTyp.HEAD3][0]
|
||||
assert bFmt.bottomMargin() == doc._mHead[BlockTyp.HEAD3][1]
|
||||
cFmt = charFmtInBlock(block, 1)
|
||||
|
||||
@@ -22,13 +22,16 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from utils.common import (
|
||||
ROOT_DIR, SETUP_DIR, appdataXml, copyPackageFiles, copySourceCode,
|
||||
extractVersion, makeCheckSum, toUpload, writeFile
|
||||
extractVersion, freshFolder, makeCheckSum, removeRedundantQt, systemCall,
|
||||
toUpload, writeFile
|
||||
)
|
||||
|
||||
|
||||
@@ -49,115 +52,87 @@ def appImage(args: argparse.Namespace) -> None:
|
||||
|
||||
print("")
|
||||
print("Build AppImage")
|
||||
print("==============")
|
||||
print("")
|
||||
print("="*120)
|
||||
|
||||
linuxTag = args.linux_tag
|
||||
pythonVer = args.python_version
|
||||
mLinux = args.linux
|
||||
mArch = args.arch
|
||||
pyVer = args.python
|
||||
|
||||
# Version Info
|
||||
# ============
|
||||
|
||||
pkgVers, _, relDate = extractVersion()
|
||||
relDate = datetime.datetime.strptime(relDate, "%Y-%m-%d")
|
||||
print("")
|
||||
|
||||
# Set Up Folder
|
||||
# =============
|
||||
|
||||
bldDir = ROOT_DIR / "dist_appimage"
|
||||
bldPkg = f"novelwriter_{pkgVers}"
|
||||
bldPkg = f"novelwriter-{pkgVers}-{mArch}"
|
||||
bldImg = f"{bldPkg}.AppImage"
|
||||
outDir = bldDir / bldPkg
|
||||
imgDir = bldDir / "appimage"
|
||||
|
||||
# Set Up Folders
|
||||
# ==============
|
||||
appDir = bldDir / f"novelWriter-{mArch}"
|
||||
|
||||
bldDir.mkdir(exist_ok=True)
|
||||
|
||||
if outDir.exists():
|
||||
print("Removing old build files ...")
|
||||
print("")
|
||||
shutil.rmtree(outDir)
|
||||
|
||||
outDir.mkdir()
|
||||
|
||||
if imgDir.exists():
|
||||
print("Removing old build metadata files ...")
|
||||
print("")
|
||||
shutil.rmtree(imgDir)
|
||||
|
||||
imgDir.mkdir()
|
||||
freshFolder(outDir)
|
||||
freshFolder(imgDir)
|
||||
freshFolder(appDir)
|
||||
|
||||
# Remove old AppImages
|
||||
if images := bldDir.glob("*.AppImage"):
|
||||
print("Removing old AppImages")
|
||||
print("")
|
||||
for image in images:
|
||||
image.unlink()
|
||||
|
||||
# Copy novelWriter Source
|
||||
# =======================
|
||||
|
||||
print("Copying novelWriter source ...")
|
||||
print("")
|
||||
|
||||
copySourceCode(outDir)
|
||||
|
||||
print("")
|
||||
print("Copying or generating additional files ...")
|
||||
print("")
|
||||
|
||||
copyPackageFiles(outDir)
|
||||
|
||||
# Write Metadata
|
||||
# ==============
|
||||
|
||||
writeFile(imgDir / "novelwriter.appdata.xml", appdataXml())
|
||||
print("Wrote: novelwriter.appdata.xml")
|
||||
|
||||
writeFile(imgDir / "requirements.txt", str(outDir))
|
||||
writeFile(imgDir / "entrypoint.sh", (
|
||||
'#! /bin/bash \n'
|
||||
# f"export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${{APPDIR}}/usr/lib/{mArch}-linux-gnu/\n"
|
||||
'{{ python-executable }} -sE ${APPDIR}/opt/python{{ python-version }}/bin/novelwriter "$@"'
|
||||
))
|
||||
print("Wrote: entrypoint.sh")
|
||||
|
||||
writeFile(imgDir / "requirements.txt", str(outDir))
|
||||
print("Wrote: requirements.txt")
|
||||
|
||||
shutil.copyfile(SETUP_DIR / "data" / "novelwriter.desktop", imgDir / "novelwriter.desktop")
|
||||
print("Copied: novelwriter.desktop")
|
||||
|
||||
shutil.copyfile(SETUP_DIR / "icons" / "novelwriter.svg", imgDir / "novelwriter.svg")
|
||||
print("Copied: novelwriter.svg")
|
||||
|
||||
shutil.copyfile(
|
||||
SETUP_DIR / "data" / "hicolor" / "256x256" / "apps" / "novelwriter.png",
|
||||
imgDir / "novelwriter.png"
|
||||
)
|
||||
shutil.copyfile(SETUP_DIR / "icons" / "novelwriter.png", imgDir / "novelwriter.png")
|
||||
print("Copied: novelwriter.png")
|
||||
|
||||
# Build AppImage
|
||||
# ==============
|
||||
# Build AppDir
|
||||
systemCall([
|
||||
sys.executable, "-m", "python_appimage", "build", "app", "--no-packaging",
|
||||
"-l", f"{mLinux}_{mArch}", "-p", pyVer, "appimage"
|
||||
], cwd=bldDir)
|
||||
|
||||
try:
|
||||
subprocess.call([
|
||||
sys.executable, "-m", "python_appimage", "build", "app",
|
||||
"-l", linuxTag, "-p", pythonVer, "appimage"
|
||||
], cwd=bldDir)
|
||||
except Exception as exc:
|
||||
print("AppImage build: FAILED")
|
||||
print("")
|
||||
print(str(exc))
|
||||
print("")
|
||||
sys.exit(1)
|
||||
# Copy Libraries
|
||||
libPath = Path(f"/usr/lib/{mArch}-linux-gnu")
|
||||
siteDir = appDir / "opt" / f"python{pyVer}" / "lib" / f"python{pyVer}" / "site-packages"
|
||||
qt6Lib = siteDir / "PyQt6" / "Qt6" / "lib"
|
||||
shutil.copyfile(libPath / "libxcb-cursor.so.0", qt6Lib / "libxcb-cursor.so.0")
|
||||
|
||||
bldFile = list(bldDir.glob("*.AppImage"))[0]
|
||||
outFile = bldDir / f"novelWriter-{pkgVers}.AppImage"
|
||||
bldFile.rename(outFile)
|
||||
shaFile = makeCheckSum(outFile.name, cwd=bldDir)
|
||||
# Remove Redundant
|
||||
removeRedundantQt(siteDir)
|
||||
|
||||
toUpload(outFile)
|
||||
# Build Image
|
||||
appToolExec = os.environ.get("APPIMAGE_TOOL_EXEC", "appimagetool")
|
||||
env = os.environ.copy()
|
||||
env["ARCH"] = mArch
|
||||
systemCall([
|
||||
appToolExec, "--no-appstream", "--updateinformation",
|
||||
f"gh-releases-zsync|vkbo|novelwriter|latest|novelwriter-*-{mArch}.AppImage.zsync",
|
||||
str(appDir), bldImg
|
||||
], cwd=bldDir, env=env)
|
||||
|
||||
updFile = bldDir / f"{bldImg}.zsync"
|
||||
bldFile = bldDir / bldImg
|
||||
shaFile = makeCheckSum(bldFile.name, cwd=bldDir)
|
||||
|
||||
toUpload(bldFile)
|
||||
toUpload(updFile)
|
||||
toUpload(shaFile)
|
||||
|
||||
return
|
||||
|
||||
@@ -24,12 +24,11 @@ import argparse
|
||||
import datetime
|
||||
import email.utils
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from utils.common import (
|
||||
ROOT_DIR, SETUP_DIR, checkAssetsExist, copyPackageFiles, copySourceCode,
|
||||
extractVersion, makeCheckSum, toUpload, writeFile
|
||||
extractVersion, makeCheckSum, systemCall, toUpload, writeFile
|
||||
)
|
||||
|
||||
SIGN_KEY = "D6A9F6B8F227CF7C6F6D1EE84DBBE4B734B0BD08"
|
||||
@@ -134,10 +133,10 @@ def makeDebianPackage(
|
||||
signArgs = [f"-k{signKey}"]
|
||||
|
||||
if sourceBuild:
|
||||
subprocess.call(["debuild", "-S", *signArgs], cwd=outDir)
|
||||
systemCall(["debuild", "-S", *signArgs], cwd=outDir)
|
||||
toUpload(bldDir / f"{bldPkg}.tar.xz")
|
||||
else:
|
||||
subprocess.call(["dpkg-buildpackage", *signArgs], cwd=outDir)
|
||||
systemCall(["dpkg-buildpackage", *signArgs], cwd=outDir)
|
||||
shutil.copyfile(bldDir / f"{bldPkg}.tar.xz", bldDir / f"{bldPkg}.debian.tar.xz")
|
||||
toUpload(bldDir / f"{bldPkg}.debian.tar.xz")
|
||||
toUpload(bldDir / f"{bldPkg}_all.deb")
|
||||
@@ -185,6 +184,7 @@ def launchpad(args: argparse.Namespace) -> None:
|
||||
("24.04", "noble"),
|
||||
("24.10", "oracular"),
|
||||
("25.04", "plucky"),
|
||||
("25.10", "questing"),
|
||||
]
|
||||
|
||||
print("Building Ubuntu packages for:")
|
||||
|
||||
@@ -23,14 +23,16 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import compileall
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.request
|
||||
import zipfile
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from utils.common import ROOT_DIR, SETUP_DIR, copySourceCode, extractVersion, readFile, writeFile
|
||||
from utils.common import (
|
||||
ROOT_DIR, SETUP_DIR, copySourceCode, extractVersion, readFile,
|
||||
removeRedundantQt, systemCall, writeFile
|
||||
)
|
||||
|
||||
|
||||
def prepareCode(outDir: Path) -> None:
|
||||
@@ -86,99 +88,11 @@ def embedPython(bldDir: Path, outDir: Path) -> None:
|
||||
def installRequirements(libDir: Path) -> None:
|
||||
"""Install dependencies."""
|
||||
print("Install dependencies ...")
|
||||
|
||||
try:
|
||||
subprocess.call([
|
||||
sys.executable, "-m",
|
||||
"pip", "install", "-r", "requirements.txt", "--target", str(libDir)
|
||||
])
|
||||
except Exception as exc:
|
||||
print("Failed with error:")
|
||||
print(str(exc))
|
||||
sys.exit(1)
|
||||
|
||||
systemCall([
|
||||
sys.executable, "-m", "pip", "install", "-r", "requirements.txt", "--target", libDir
|
||||
])
|
||||
print("Done")
|
||||
print("")
|
||||
|
||||
return
|
||||
|
||||
|
||||
def removeRedundantQt(libDir: Path) -> None:
|
||||
"""Delete Qt files that are not needed"""
|
||||
|
||||
def unlinkIfFound(file: Path) -> None:
|
||||
if file.is_file():
|
||||
file.unlink()
|
||||
print(f"Deleted: {file}")
|
||||
|
||||
def deleteFolder(folder: Path) -> None:
|
||||
if folder.is_dir():
|
||||
shutil.rmtree(folder)
|
||||
print(f"Deleted: {folder}")
|
||||
|
||||
def unlinkIfPrefix(folder: Path, prefix: tuple[str, ...]) -> None:
|
||||
if folder.is_dir():
|
||||
for item in folder.iterdir():
|
||||
if item.name.startswith(prefix):
|
||||
if item.is_file():
|
||||
unlinkIfFound(item)
|
||||
elif item.is_dir():
|
||||
deleteFolder(item)
|
||||
|
||||
print("Deleting Redundant Files")
|
||||
print("========================")
|
||||
print("")
|
||||
|
||||
pyQt6Dir = libDir / "PyQt6"
|
||||
bindDir = libDir / "PyQt6" / "bindings"
|
||||
qt6Dir = libDir / "PyQt6" / "Qt6"
|
||||
binDir = libDir / "PyQt6" / "Qt6" / "bin"
|
||||
plugDir = libDir / "PyQt6" / "Qt6" / "plugins"
|
||||
qmDir = libDir / "PyQt6" / "Qt6" / "translations"
|
||||
dictDir = libDir / "enchant" / "data" / "mingw64" / "share" / "enchant" / "hunspell"
|
||||
|
||||
for item in dictDir.iterdir():
|
||||
if not item.name.startswith(("en_GB", "en_US")):
|
||||
unlinkIfFound(item)
|
||||
|
||||
for item in qmDir.iterdir():
|
||||
if not item.name.startswith("qtbase"):
|
||||
unlinkIfFound(item)
|
||||
|
||||
bulkDel = ("QtQml", "Qt6Qml", "QtQuick", "Qt6Quick")
|
||||
unlinkIfPrefix(pyQt6Dir, bulkDel)
|
||||
unlinkIfPrefix(bindDir, bulkDel)
|
||||
unlinkIfPrefix(binDir, bulkDel)
|
||||
|
||||
delQt6 = [
|
||||
"Qt6Bluetooth", "Qt6DBus", "Qt6Designer", "Qt6Help", "Qt6Multimedia",
|
||||
"Qt6MultimediaWidgets", "Qt6Network", "Qt6Nfc", "Qt6OpenGL", "Qt6Positioning",
|
||||
"Qt6PositioningQuick", "Qt6Sensors", "Qt6SerialPort", "Qt6Sql", "Qt6Test",
|
||||
"Qt6TextToSpeech", "Qt6WebChannel", "Qt6WebSockets", "Qt6Xml",
|
||||
]
|
||||
for item in delQt6:
|
||||
qtItem = item.replace("Qt6", "Qt")
|
||||
unlinkIfFound(binDir / f"{item}.dll")
|
||||
unlinkIfFound(pyQt6Dir / f"{qtItem}.pyd")
|
||||
unlinkIfFound(pyQt6Dir / f"{qtItem}.pyi")
|
||||
deleteFolder(bindDir / qtItem)
|
||||
|
||||
delList = [
|
||||
binDir / "opengl32sw.dll",
|
||||
qt6Dir / "qml",
|
||||
plugDir / "renderers",
|
||||
plugDir / "sensors",
|
||||
plugDir / "sqldrivers",
|
||||
plugDir / "texttospeech",
|
||||
plugDir / "webview",
|
||||
]
|
||||
for item in delList:
|
||||
unlinkIfFound(item)
|
||||
deleteFolder(item)
|
||||
|
||||
print("Done")
|
||||
print("")
|
||||
|
||||
return
|
||||
|
||||
|
||||
@@ -238,12 +152,7 @@ def main(args: argparse.Namespace) -> None:
|
||||
writeFile(ROOT_DIR / "setup.iss", issData)
|
||||
print("")
|
||||
|
||||
try:
|
||||
subprocess.call(["iscc", "setup.iss"])
|
||||
except Exception as exc:
|
||||
print("Inno Setup failed with error:")
|
||||
print(str(exc))
|
||||
sys.exit(1)
|
||||
systemCall(["iscc", "setup.iss"])
|
||||
|
||||
print("")
|
||||
print("Done")
|
||||
|
||||
121
utils/common.py
121
utils/common.py
@@ -22,6 +22,7 @@ from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
@@ -50,11 +51,11 @@ def extractVersion(beQuiet: bool = False) -> tuple[str, str, str]:
|
||||
if aLine.startswith("__date__"):
|
||||
relDate = getValue(aLine)
|
||||
except Exception as exc:
|
||||
print(f"Could not read file: {initFile}")
|
||||
print(str(exc))
|
||||
print(f"Could not read file: {initFile}", flush=True)
|
||||
print(str(exc), flush=True)
|
||||
|
||||
if not beQuiet:
|
||||
print(f"novelWriter version: {numVers} ({hexVers}) at {relDate}")
|
||||
print(f"novelWriter version: {numVers} ({hexVers}) at {relDate}", flush=True)
|
||||
|
||||
return numVers, hexVers, relDate
|
||||
|
||||
@@ -77,16 +78,16 @@ def copySourceCode(dst: Path) -> None:
|
||||
for item in src.glob("**/*"):
|
||||
relSrc = item.relative_to(ROOT_DIR)
|
||||
if item.suffix in (".pyc", ".pyo"):
|
||||
print(f"Ignore: {relSrc}")
|
||||
print("Ignored:", relSrc, flush=True)
|
||||
continue
|
||||
if item.parent.is_dir() and item.parent.name != "__pycache__":
|
||||
dstDir = dst / relSrc.parent
|
||||
if not dstDir.exists():
|
||||
dstDir.mkdir(parents=True)
|
||||
print(f"Folder: {dstDir}")
|
||||
print("Created:", dstDir.relative_to(ROOT_DIR), flush=True)
|
||||
if item.is_file():
|
||||
shutil.copyfile(item, dst / relSrc)
|
||||
print(f"Copied: {dst / relSrc}")
|
||||
print("Copied:", relSrc, flush=True)
|
||||
return
|
||||
|
||||
|
||||
@@ -95,26 +96,23 @@ def copyPackageFiles(dst: Path, setupPy: bool = False) -> None:
|
||||
copyFiles = ["LICENSE.md", "CREDITS.md", "pyproject.toml"]
|
||||
for copyFile in copyFiles:
|
||||
shutil.copyfile(copyFile, dst / copyFile)
|
||||
print(f"Copied: {copyFile}")
|
||||
print("Copied:", copyFile, flush=True)
|
||||
|
||||
writeFile(dst / "MANIFEST.in", (
|
||||
"include LICENSE.md\n"
|
||||
"include CREDITS.md\n"
|
||||
"recursive-include novelwriter/assets *\n"
|
||||
))
|
||||
print("Wrote: MANIFEST.in")
|
||||
|
||||
if setupPy:
|
||||
writeFile(dst / "setup.py", (
|
||||
"import setuptools\n"
|
||||
"setuptools.setup()\n"
|
||||
))
|
||||
print("Wrote: setup.py")
|
||||
|
||||
text = readFile(ROOT_DIR / "pyproject.toml")
|
||||
text = text.replace("setup/description_pypi.md", "data/description_short.txt")
|
||||
writeFile(dst / "pyproject.toml", text)
|
||||
print("Wrote: pyproject.toml")
|
||||
|
||||
return
|
||||
|
||||
@@ -139,10 +137,10 @@ def makeCheckSum(sumFile: str, cwd: Path | None = None) -> str:
|
||||
shaFile = cwd / f"{sumFile}.sha256"
|
||||
with open(shaFile, mode="w", encoding="utf-8") as fOut:
|
||||
subprocess.call(["shasum", "-a", "256", sumFile], stdout=fOut, cwd=cwd)
|
||||
print(f"SHA256 Sum: {shaFile}")
|
||||
print(f"SHA256 Sum: {shaFile}", flush=True)
|
||||
except Exception as exc:
|
||||
print("Could not generate sha256 file")
|
||||
print(str(exc))
|
||||
print("Could not generate sha256 file", flush=True)
|
||||
print(str(exc), flush=True)
|
||||
return ""
|
||||
|
||||
return str(shaFile)
|
||||
@@ -156,17 +154,17 @@ def checkAssetsExist() -> bool:
|
||||
|
||||
sampleZip = ROOT_DIR / "novelwriter" / "assets" / "sample.zip"
|
||||
if sampleZip.is_file():
|
||||
print(f"Found: {sampleZip}")
|
||||
print(f"Found: {sampleZip}", flush=True)
|
||||
hasSample = True
|
||||
|
||||
pdfManual = ROOT_DIR / "novelwriter" / "assets" / "manual.pdf"
|
||||
if pdfManual.is_file():
|
||||
print(f"Found: {pdfManual}")
|
||||
print(f"Found: {pdfManual}", flush=True)
|
||||
hasManual = True
|
||||
|
||||
i18nAssets = ROOT_DIR / "novelwriter" / "assets" / "i18n"
|
||||
if len(list(i18nAssets.glob("*.qm"))) > 0:
|
||||
print(f"Found: {i18nAssets}/*.qm")
|
||||
print(f"Found: {i18nAssets}/*.qm", flush=True)
|
||||
hasQmData = True
|
||||
|
||||
return hasSample and hasManual and hasQmData
|
||||
@@ -188,4 +186,93 @@ def readFile(file: Path) -> str:
|
||||
|
||||
def writeFile(file: Path, text: str) -> int:
|
||||
"""Write string to file."""
|
||||
return file.write_text(text, encoding="utf-8")
|
||||
result = file.write_text(text, encoding="utf-8")
|
||||
print("Wrote:", file.relative_to(ROOT_DIR), flush=True)
|
||||
return result
|
||||
|
||||
|
||||
def freshFolder(path: Path) -> None:
|
||||
"""Make sure a folder exists and is empty."""
|
||||
if path.exists():
|
||||
print("Removing:", str(path), flush=True)
|
||||
shutil.rmtree(path)
|
||||
path.mkdir()
|
||||
return
|
||||
|
||||
|
||||
def systemCall(cmd: list, cwd: Path | str | None = None, env: dict | None = None) -> int:
|
||||
"""Make a system call using subprocess."""
|
||||
if isinstance(cwd, Path):
|
||||
cwd = str(cwd)
|
||||
try:
|
||||
code = subprocess.call([str(c) for c in cmd], cwd=cwd, env=env)
|
||||
except Exception as exc:
|
||||
print("ERROR:", str(exc), flush=True)
|
||||
sys.exit(1)
|
||||
return code
|
||||
|
||||
|
||||
def removeRedundantQt(qtBase: Path) -> None:
|
||||
"""Delete Qt files that are not needed"""
|
||||
|
||||
def unlinkIfFound(file: Path) -> None:
|
||||
if file.is_file():
|
||||
file.unlink()
|
||||
print("Deleted:", file.relative_to(ROOT_DIR), flush=True)
|
||||
|
||||
def deleteFolder(folder: Path) -> None:
|
||||
if folder.is_dir():
|
||||
shutil.rmtree(folder)
|
||||
print("Deleted:", folder.relative_to(ROOT_DIR), flush=True)
|
||||
|
||||
def unlinkIfPrefix(folder: Path, prefix: tuple[str, ...]) -> None:
|
||||
if folder.is_dir():
|
||||
for item in folder.iterdir():
|
||||
if item.name.startswith(prefix):
|
||||
if item.is_file():
|
||||
unlinkIfFound(item)
|
||||
elif item.is_dir():
|
||||
deleteFolder(item)
|
||||
|
||||
print("Deleting redundant files ...")
|
||||
|
||||
pyQt6Dir = qtBase / "PyQt6"
|
||||
bindDir = qtBase / "PyQt6" / "bindings"
|
||||
qt6Dir = qtBase / "PyQt6" / "Qt6"
|
||||
binDir = qtBase / "PyQt6" / "Qt6" / "bin"
|
||||
libDir = qtBase / "PyQt6" / "Qt6" / "lib"
|
||||
plugDir = qtBase / "PyQt6" / "Qt6" / "plugins"
|
||||
qmDir = qtBase / "PyQt6" / "Qt6" / "translations"
|
||||
dictDir = qtBase / "enchant" / "data" / "mingw64" / "share" / "enchant" / "hunspell"
|
||||
|
||||
# Prune Dictionaries
|
||||
if dictDir.exists():
|
||||
for item in dictDir.iterdir():
|
||||
if not item.name.startswith(("en_GB", "en_US")):
|
||||
unlinkIfFound(item)
|
||||
|
||||
# Prune Translations
|
||||
for item in qmDir.iterdir():
|
||||
if not item.name.startswith("qtbase"):
|
||||
unlinkIfFound(item)
|
||||
|
||||
# Delete Modules
|
||||
modules = [
|
||||
"Qt6Qml", "Qt6Quick", "Qt6Bluetooth", "Qt6Nfc",
|
||||
"Qt6Sensors", "Qt6SerialPort", "Qt6Test",
|
||||
]
|
||||
modules.extend([x.replace("Qt6", "Qt") for x in modules])
|
||||
modules.extend([f"lib{x}" for x in modules])
|
||||
modules = tuple(modules)
|
||||
|
||||
unlinkIfPrefix(pyQt6Dir, modules)
|
||||
unlinkIfPrefix(bindDir, modules)
|
||||
unlinkIfPrefix(binDir, modules)
|
||||
unlinkIfPrefix(libDir, modules)
|
||||
|
||||
# Other Files
|
||||
deleteFolder(qt6Dir / "qml")
|
||||
deleteFolder(plugDir / "qmlls")
|
||||
deleteFolder(plugDir / "qmllint")
|
||||
|
||||
return
|
||||
|
||||
@@ -25,7 +25,7 @@ import os
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from utils.common import ROOT_DIR
|
||||
from utils.common import ROOT_DIR, systemCall
|
||||
|
||||
|
||||
def updateDocsTranslationSources(args: argparse.Namespace) -> None:
|
||||
@@ -40,7 +40,7 @@ def updateDocsTranslationSources(args: argparse.Namespace) -> None:
|
||||
locsDir.mkdir(exist_ok=True)
|
||||
|
||||
print("Generating POT Files")
|
||||
subprocess.call(["make", "gettext"], cwd=docsDir)
|
||||
systemCall(["make", "gettext"], cwd=docsDir)
|
||||
print("")
|
||||
|
||||
lang = args.lang
|
||||
@@ -55,7 +55,7 @@ def updateDocsTranslationSources(args: argparse.Namespace) -> None:
|
||||
print("")
|
||||
|
||||
for code in update:
|
||||
subprocess.call(["sphinx-intl", "update", "-p", "build/gettext", "-l", code], cwd=docsDir)
|
||||
systemCall(["sphinx-intl", "update", "-p", "build/gettext", "-l", code], cwd=docsDir)
|
||||
print("")
|
||||
|
||||
print("Done")
|
||||
|
||||
Reference in New Issue
Block a user