Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an ignore text format and allow multi-paragraph formatting #1690

Merged
merged 6 commits into from
Feb 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/source/usage_shortcuts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ Text Formatting Shortcuts
":kbd:`Ctrl+D`", "Strike through selected text, or word under cursor"
":kbd:`Ctrl+I`", "Format selected text, or word under cursor, with emphasis (italic)"
":kbd:`Ctrl+Shift+/`", "Remove block formatting for block under cursor"
":kbd:`Ctrl+Shift+D`", "Toggle block format as ignored text"


Other Editor Shortcuts
Expand Down
4 changes: 4 additions & 0 deletions novelwriter/core/tokenizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,10 @@ def tokenizeText(self) -> None:
continue

if aLine[0] == "%":
if aLine[1] == "~":
# Completely ignore the paragraph
continue

cStyle, cText, _ = processComment(aLine)
if cStyle == nwComment.SYNOPSIS:
self._tokens.append((
Expand Down
35 changes: 18 additions & 17 deletions novelwriter/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,23 +108,24 @@ class nwDocAction(Enum):
BLOCK_H3 = 15
BLOCK_H4 = 16
BLOCK_COM = 17
BLOCK_TXT = 18
BLOCK_TTL = 19
BLOCK_UNN = 20
REPL_SNG = 21
REPL_DBL = 22
RM_BREAKS = 23
ALIGN_L = 24
ALIGN_C = 25
ALIGN_R = 26
INDENT_L = 27
INDENT_R = 28
SC_ITALIC = 29
SC_BOLD = 30
SC_STRIKE = 31
SC_ULINE = 32
SC_SUP = 33
SC_SUB = 34
BLOCK_IGN = 18
BLOCK_TXT = 19
BLOCK_TTL = 20
BLOCK_UNN = 21
REPL_SNG = 22
REPL_DBL = 23
RM_BREAKS = 24
ALIGN_L = 25
ALIGN_C = 26
ALIGN_R = 27
INDENT_L = 28
INDENT_R = 29
SC_ITALIC = 30
SC_BOLD = 31
SC_STRIKE = 32
SC_ULINE = 33
SC_SUP = 34
SC_SUB = 35

# END Enum nwDocAction

Expand Down
262 changes: 147 additions & 115 deletions novelwriter/gui/doceditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -736,9 +736,11 @@ def docAction(self, action: nwDocAction) -> bool:
elif action == nwDocAction.BLOCK_H4:
self._formatBlock(nwDocAction.BLOCK_H4)
elif action == nwDocAction.BLOCK_COM:
self._formatBlock(nwDocAction.BLOCK_COM)
self._iterFormatBlocks(nwDocAction.BLOCK_COM)
elif action == nwDocAction.BLOCK_IGN:
self._iterFormatBlocks(nwDocAction.BLOCK_IGN)
elif action == nwDocAction.BLOCK_TXT:
self._formatBlock(nwDocAction.BLOCK_TXT)
self._iterFormatBlocks(nwDocAction.BLOCK_TXT)
elif action == nwDocAction.BLOCK_TTL:
self._formatBlock(nwDocAction.BLOCK_TTL)
elif action == nwDocAction.BLOCK_UNN:
Expand Down Expand Up @@ -1588,9 +1590,7 @@ def _replaceQuotes(self, sQuote: str, oQuote: str, cQuote: str) -> bool:

posS = cursor.selectionStart()
posE = cursor.selectionEnd()
closeCheck = (
" ", "\n", nwUnicode.U_LSEP, nwUnicode.U_PSEP
)
closeCheck = (" ", "\n", nwUnicode.U_LSEP, nwUnicode.U_PSEP)

self._allowAutoReplace(False)
for posC in range(posS, posE+1):
Expand Down Expand Up @@ -1634,159 +1634,189 @@ def _replaceQuotes(self, sQuote: str, oQuote: str, cQuote: str) -> bool:

return True

def _formatBlock(self, action: nwDocAction) -> bool:
"""Change the block format of the block under the cursor."""
cursor = self.textCursor()
block = cursor.block()
if not block.isValid():
logger.debug("Invalid block selected for action '%s'", str(action))
return False

def _processBlockFormat(
self, action: nwDocAction, text: str, toggle: bool = True
) -> tuple[nwDocAction, str, int]:
"""Process the formatting of a single text block."""
# Remove existing format first, if any
setText = block.text()
hasText = len(setText) > 0
if setText.startswith("@"):
if text.startswith("@"):
logger.error("Cannot apply block format to keyword/value line")
return False
elif setText.startswith("% "):
newText = setText[2:]
cOffset = 2
if action == nwDocAction.BLOCK_COM:
return nwDocAction.NO_ACTION, "", 0
elif text.startswith("%~"):
temp = text[2:].lstrip()
offset = len(text) - len(temp)
if toggle and action == nwDocAction.BLOCK_IGN:
action = nwDocAction.BLOCK_TXT
elif setText.startswith("%"):
newText = setText[1:]
cOffset = 1
if action == nwDocAction.BLOCK_COM:
elif text.startswith("%"):
temp = text[1:].lstrip()
offset = len(text) - len(temp)
if toggle and action == nwDocAction.BLOCK_COM:
action = nwDocAction.BLOCK_TXT
elif setText.startswith("# "):
newText = setText[2:]
cOffset = 2
elif setText.startswith("## "):
newText = setText[3:]
cOffset = 3
elif setText.startswith("### "):
newText = setText[4:]
cOffset = 4
elif setText.startswith("#### "):
newText = setText[5:]
cOffset = 5
elif setText.startswith("#! "):
newText = setText[3:]
cOffset = 3
elif setText.startswith("##! "):
newText = setText[4:]
cOffset = 4
elif setText.startswith(">> "):
newText = setText[3:]
cOffset = 3
elif setText.startswith("> ") and action != nwDocAction.INDENT_R:
newText = setText[2:]
cOffset = 2
elif setText.startswith(">>"):
newText = setText[2:]
cOffset = 2
elif setText.startswith(">") and action != nwDocAction.INDENT_R:
newText = setText[1:]
cOffset = 1
elif text.startswith("# "):
temp = text[2:]
offset = 2
elif text.startswith("## "):
temp = text[3:]
offset = 3
elif text.startswith("### "):
temp = text[4:]
offset = 4
elif text.startswith("#### "):
temp = text[5:]
offset = 5
elif text.startswith("#! "):
temp = text[3:]
offset = 3
elif text.startswith("##! "):
temp = text[4:]
offset = 4
elif text.startswith(">> "):
temp = text[3:]
offset = 3
elif text.startswith("> ") and action != nwDocAction.INDENT_R:
temp = text[2:]
offset = 2
elif text.startswith(">>"):
temp = text[2:]
offset = 2
elif text.startswith(">") and action != nwDocAction.INDENT_R:
temp = text[1:]
offset = 1
else:
newText = setText
cOffset = 0
temp = text
offset = 0

# Also remove formatting tags at the end
if setText.endswith(" <<"):
newText = newText[:-3]
elif setText.endswith(" <") and action != nwDocAction.INDENT_L:
newText = newText[:-2]
elif setText.endswith("<<"):
newText = newText[:-2]
elif setText.endswith("<") and action != nwDocAction.INDENT_L:
newText = newText[:-1]
if text.endswith(" <<"):
temp = temp[:-3]
elif text.endswith(" <") and action != nwDocAction.INDENT_L:
temp = temp[:-2]
elif text.endswith("<<"):
temp = temp[:-2]
elif text.endswith("<") and action != nwDocAction.INDENT_L:
temp = temp[:-1]

# Apply new format
if action == nwDocAction.BLOCK_COM:
setText = "% "+newText
cOffset -= 2
text = f"% {temp}"
offset -= 2
elif action == nwDocAction.BLOCK_IGN:
text = f"%~ {temp}"
offset -= 3
elif action == nwDocAction.BLOCK_H1:
setText = "# "+newText
cOffset -= 2
text = f"# {temp}"
offset -= 2
elif action == nwDocAction.BLOCK_H2:
setText = "## "+newText
cOffset -= 3
text = f"## {temp}"
offset -= 3
elif action == nwDocAction.BLOCK_H3:
setText = "### "+newText
cOffset -= 4
text = f"### {temp}"
offset -= 4
elif action == nwDocAction.BLOCK_H4:
setText = "#### "+newText
cOffset -= 5
text = f"#### {temp}"
offset -= 5
elif action == nwDocAction.BLOCK_TTL:
setText = "#! "+newText
cOffset -= 3
text = f"#! {temp}"
offset -= 3
elif action == nwDocAction.BLOCK_UNN:
setText = "##! "+newText
cOffset -= 4
text = f"##! {temp}"
offset -= 4
elif action == nwDocAction.ALIGN_L:
setText = newText+" <<"
text = f"{temp} <<"
elif action == nwDocAction.ALIGN_C:
setText = ">> "+newText+" <<"
cOffset -= 3
text = f">> {temp} <<"
offset -= 3
elif action == nwDocAction.ALIGN_R:
setText = ">> "+newText
cOffset -= 3
text = f">> {temp}"
offset -= 3
elif action == nwDocAction.INDENT_L:
setText = "> "+newText
cOffset -= 2
text = f"> {temp}"
offset -= 2
elif action == nwDocAction.INDENT_R:
setText = newText+" <"
text = f"{temp} <"
elif action == nwDocAction.BLOCK_TXT:
setText = newText
text = temp
else:
logger.error("Unknown or unsupported block format requested: '%s'", str(action))
return nwDocAction.NO_ACTION, "", 0

return action, text, offset

def _formatBlock(self, action: nwDocAction) -> bool:
"""Change the block format of the block under the cursor."""
cursor = self.textCursor()
block = cursor.block()
if not block.isValid():
logger.debug("Invalid block selected for action '%s'", str(action))
return False

# Replace the block text
action, text, offset = self._processBlockFormat(action, block.text())
if action == nwDocAction.NO_ACTION:
return False

pos = cursor.position()

cursor.beginEditBlock()
posO = cursor.position()
cursor.select(QTextCursor.SelectionType.BlockUnderCursor)
posS = cursor.selectionStart()
cursor.removeSelectedText()
cursor.setPosition(posS)
self._makeSelection(QTextCursor.SelectionType.BlockUnderCursor, cursor)
cursor.insertText(text)
cursor.endEditBlock()

if posS > 0 and hasText:
# If the block already had text, we must insert a new block
# first before we can add back the text to it.
cursor.insertBlock()
if (move := pos - offset) >= 0:
cursor.setPosition(move)
self.setTextCursor(cursor)

cursor.insertText(setText)
return True

if posO - cOffset >= 0:
cursor.setPosition(posO - cOffset)
def _iterFormatBlocks(self, action: nwDocAction) -> bool:
"""Iterate over all selected blocks and apply format. If no
selection is made, just forward the call to the single block
formatter function.
"""
cursor = self.textCursor()
blocks = self._selectedBlocks(cursor)
if len(blocks) < 2:
return self._formatBlock(action)

toggle = True
cursor.beginEditBlock()
for block in blocks:
blockText = block.text()
pAction, text, _ = self._processBlockFormat(action, blockText, toggle)
if pAction != nwDocAction.NO_ACTION and blockText.strip():
action = pAction # First block decides further actions
cursor.setPosition(block.position())
self._makeSelection(QTextCursor.SelectionType.BlockUnderCursor, cursor)
cursor.insertText(text)
toggle = False

cursor.endEditBlock()
self.setTextCursor(cursor)

return True

def _selectedBlocks(self, cursor: QTextCursor) -> list[QTextBlock]:
"""Return a list of all blocks selected by a cursor."""
if cursor.hasSelection():
iS = self._qDocument.findBlock(cursor.selectionStart()).blockNumber()
iE = self._qDocument.findBlock(cursor.selectionEnd()).blockNumber()
return [self._qDocument.findBlockByNumber(i) for i in range(iS, iE+1)]
return []

def _removeInParLineBreaks(self) -> None:
"""Strip line breaks within paragraphs in the selected text."""
cursor = self.textCursor()
if not cursor.hasSelection():
cursor.select(QTextCursor.SelectionType.Document)

iS = 0
iE = self._qDocument.blockCount() - 1
rS = 0
rE = self._qDocument.characterCount()
if cursor.hasSelection():
sBlock = self._qDocument.findBlock(cursor.selectionStart())
eBlock = self._qDocument.findBlock(cursor.selectionEnd())
iS = sBlock.blockNumber()
iE = eBlock.blockNumber()
rS = sBlock.position()
rE = eBlock.position() + eBlock.length()
if sBlocks := self._selectedBlocks(cursor):
rS = sBlocks[0].position()
rE = sBlocks[-1].position() + sBlocks[-1].length()

# Clean up the text
currPar = []
cleanText = ""
for i in range(iS, iE+1):
cBlock = self._qDocument.findBlockByNumber(i)
for cBlock in sBlocks:
cText = cBlock.text()
if cText.strip() == "":
if currPar:
Expand Down Expand Up @@ -2040,9 +2070,11 @@ def _autoSelect(self) -> QTextCursor:

return cursor

def _makeSelection(self, mode: QTextCursor.SelectionType) -> None:
def _makeSelection(self, mode: QTextCursor.SelectionType,
cursor: QTextCursor | None = None) -> None:
"""Select text based on selection mode."""
cursor = self.textCursor()
if cursor is None:
cursor = self.textCursor()
cursor.clearSelection()
cursor.select(mode)

Expand Down
Loading
Loading