Skip to content

Commit

Permalink
Add an ignore text format and allow multi-paragraph formatting (#1690)
Browse files Browse the repository at this point in the history
  • Loading branch information
vkbo committed Feb 10, 2024
2 parents 467772c + 2f20266 commit e9e4788
Show file tree
Hide file tree
Showing 7 changed files with 290 additions and 133 deletions.
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

0 comments on commit e9e4788

Please sign in to comment.