Make Ctrl+R work as redo shortcut

This commit is contained in:
Veronica Berglyd Olsen
2026-11-03 22:32:06 +01:00
parent ed69456064
commit de8b7c1ac0
6 changed files with 106 additions and 45 deletions

View File

@@ -11,6 +11,11 @@ shortcuts. Most of them are also listed in the application's user interface.
On MacOS, replace :kbd:`Ctrl` with :kbd:`Cmd`.
.. note::
Shortcuts marked with (*) are remapped when Vim Mode is enabled and no longer work as listed in
the tables below. See :ref:`docs_features_vim_mode` for more details.
Main Window Shortcuts
=====================
@@ -53,7 +58,7 @@ Project Tree Shortcuts
":kbd:`Ctrl+L`", "Open the **Quick Links** menu"
":kbd:`Ctrl+N`", "Open the **Create New Item** menu"
":kbd:`Ctrl+O`", "Open the selected document in the editor"
":kbd:`Ctrl+R`", "Open the selected document in the viewer"
":kbd:`Ctrl+R`", "Open the selected document in the viewer (*)"
":kbd:`Ctrl+Up`", "Move selected item one step up in the tree"
":kbd:`Ctrl+Down`", "Move selected item one step down in the tree"
":kbd:`Ctrl+Shift+Del`", "Move the selected item to Trash"
@@ -116,7 +121,7 @@ Other Editor Shortcuts
":kbd:`Ctrl+A`", "Select all text in the document"
":kbd:`Ctrl+C`", "Copy selected text to clipboard"
":kbd:`Ctrl+K`", "Activate the insert commands (see list in :ref:`docs_features_shortcuts_insert`)"
":kbd:`Ctrl+R`", "Open or reload the current document in the viewer"
":kbd:`Ctrl+R`", "Open or reload the current document in the viewer (*)"
":kbd:`Ctrl+S`", "Save the current document"
":kbd:`Ctrl+V`", "Paste text from clipboard to cursor position"
":kbd:`Ctrl+W`", "Close the current document"
@@ -205,4 +210,4 @@ Document Viewer Shortcuts
":kbd:`Ctrl+-`", "Zoom out"
":kbd:`Ctrl+_`", "Zoom out"
":kbd:`Ctrl+Shift+A`", "Select all text in the current paragraph"
":kbd:`Ctrl+Shift+R`", "Close the document viewer"
":kbd:`Ctrl+Shift+R`", "Close the document viewer (*)"

View File

@@ -60,40 +60,47 @@ The table below shows the vim motions currently implemented in novelWriter.
.. csv-table::
:header: "Motion", "Mode(s)", "Description"
":kbd:`i`", "Normal → Insert", "Enter insert mode"
":kbd:`I`", "Normal → Insert", "Jump to start of line and enter insert"
":kbd:`v`", "Normal → Visual", "Enter character-wise visual mode"
":kbd:`V`", "Normal → V-Line", "Enter line-wise visual mode"
":kbd:`dd`", "Normal", "Delete (cut) current line"
":kbd:`x`", "Normal", "Delete character under cursor"
":kbd:`w`", "Normal / Visual", "Move to next word start"
":kbd:`b`", "Normal / Visual", "Move to previous word start"
":kbd:`e`", "Normal / Visual", "Move to next word end"
":kbd:`dw`", "Normal", "Delete from cursor to next word start"
":kbd:`de`", "Normal", "Delete from cursor to next word end"
":kbd:`db`", "Normal", "Delete from cursor to previous word start"
":kbd:`d$`", "Normal", "Delete from cursor to end of line"
":kbd:`yw`", "Normal", "Yank (copy) from cursor to next word start"
":kbd:`gg`", "Normal / Visual", "Jump to first line"
":kbd:`G`", "Normal / Visual", "Jump to last line"
":kbd:`yy`", "Normal", "Yank current line"
":kbd:`p`", "Normal", "Paste after current line or cursor"
":kbd:`P`", "Normal", "Paste before current line or at cursor"
":kbd:`o`", "Normal → Insert", "Open new line below and enter insert"
":kbd:`O`", "Normal → Insert", "Open new line above and enter insert"
":kbd:`$`", "Normal / Visual", "Move to end of line"
":kbd:`0`", "Normal / Visual", "Move to start of line"
":kbd:`a`", "Normal → Insert", "Enter insert after cursor"
":kbd:`A`", "Normal → Insert", "Enter insert at end of line"
":kbd:`u`", "Normal", "Undo last change"
":kbd:`zz`", "Normal", "Centre cursor vertically"
":kbd:`h`", "Normal / Visual", "Move left"
":kbd:`j`", "Normal / Visual", "Move down"
":kbd:`k`", "Normal / Visual", "Move up"
":kbd:`l`", "Normal / Visual", "Move right"
":kbd:`d`", "Visual / V-Line", "Delete selection"
":kbd:`x`", "Visual / V-Line", "Delete selection"
":kbd:`y`", "Visual / V-Line", "Yank selection"
":kbd:`i`", "Normal → Insert", "Enter insert mode"
":kbd:`I`", "Normal → Insert", "Jump to start of line and enter insert"
":kbd:`v`", "Normal → Visual", "Enter character-wise visual mode"
":kbd:`V`", "Normal → V-Line", "Enter line-wise visual mode"
":kbd:`dd`", "Normal", "Delete (cut) current line"
":kbd:`x`", "Normal", "Delete character under cursor"
":kbd:`w`", "Normal / Visual", "Move to next word start"
":kbd:`b`", "Normal / Visual", "Move to previous word start"
":kbd:`e`", "Normal / Visual", "Move to next word end"
":kbd:`dw`", "Normal", "Delete from cursor to next word start"
":kbd:`de`", "Normal", "Delete from cursor to next word end"
":kbd:`db`", "Normal", "Delete from cursor to previous word start"
":kbd:`d$`", "Normal", "Delete from cursor to end of line"
":kbd:`yw`", "Normal", "Yank (copy) from cursor to next word start"
":kbd:`gg`", "Normal / Visual", "Jump to first line"
":kbd:`G`", "Normal / Visual", "Jump to last line"
":kbd:`yy`", "Normal", "Yank current line"
":kbd:`p`", "Normal", "Paste after current line or cursor"
":kbd:`P`", "Normal", "Paste before current line or at cursor"
":kbd:`o`", "Normal → Insert", "Open new line below and enter insert"
":kbd:`O`", "Normal → Insert", "Open new line above and enter insert"
":kbd:`$`", "Normal / Visual", "Move to end of line"
":kbd:`0`", "Normal / Visual", "Move to start of line"
":kbd:`a`", "Normal → Insert", "Enter insert after cursor"
":kbd:`A`", "Normal → Insert", "Enter insert at end of line"
":kbd:`u`", "Normal", "Undo last change"
":kbd:`Ctrl+R`", "Normal", "Redo last undone change (*)"
":kbd:`zz`", "Normal", "Centre cursor vertically"
":kbd:`h`", "Normal / Visual", "Move left"
":kbd:`j`", "Normal / Visual", "Move down"
":kbd:`k`", "Normal / Visual", "Move up"
":kbd:`l`", "Normal / Visual", "Move right"
":kbd:`d`", "Visual / V-Line", "Delete selection"
":kbd:`x`", "Visual / V-Line", "Delete selection"
":kbd:`y`", "Visual / V-Line", "Yank selection"
.. note::
Shortcuts marked with (*) are remapped when Vim Mode is enabled. These are shortcuts that
usually do other things when Vim Mode is off.
Known Issues

View File

@@ -26,6 +26,8 @@ import logging
from typing import TYPE_CHECKING
from PyQt6.QtCore import Qt
from novelwriter.enum import nwDocAction, nwVimMode
from novelwriter.types import (
QtKeepAnchor,
@@ -37,6 +39,7 @@ from novelwriter.types import (
QtKeyPageUp,
QtKeyRight,
QtKeyUp,
QtModCtrl,
QtModShift,
QtMoveDown,
QtMoveEnd,
@@ -158,6 +161,8 @@ class EditorVimMode:
"""Handle key events for Vim mode NORMAL mode motions."""
key = event.text()
cursor = self.textCursor()
if event.key() == Qt.Key.Key_R and event.modifiers() == QtModCtrl:
key = "<C-r>"
if key in self._vim.PREFIX_KEYS or key in self._vim.SUFFIX_KEYS:
self._vim.pushCommandKey(key)
@@ -319,6 +324,10 @@ class EditorVimMode:
self._editor.docAction(nwDocAction.UNDO)
self._vim.resetCommand()
elif command == "<C-r>":
self._editor.docAction(nwDocAction.REDO)
self._vim.resetCommand()
elif command == "zz":
self._editor.ensureCursorVisible(centre=True)
self._vim.resetCommand()

View File

@@ -85,10 +85,24 @@ class GuiMainMenu(QMenuBar):
self._buildToolsMenu()
self._buildHelpMenu()
self.updateVimModeKeys()
self.installEventFilter(StatusTipFilter(mainGui))
logger.debug("Ready: GuiMainMenu")
##
# Methods
##
def updateVimModeKeys(self) -> None:
"""Update shortcuts that depend on vim mode state."""
if CONFIG.vimMode:
self.aViewDoc.setShortcuts([])
self.aCloseView.setShortcuts([])
else:
self.aViewDoc.setShortcut("Ctrl+R")
self.aCloseView.setShortcut("Ctrl+Shift+R")
##
# Public Slots
##
@@ -263,12 +277,10 @@ class GuiMainMenu(QMenuBar):
# Document > Preview
self.aViewDoc = qtAddAction(self.docuMenu, self.tr("View Document"))
self.aViewDoc.setShortcut("Ctrl+R")
self.aViewDoc.triggered.connect(qtWeakLambda(self.mainGui.docPanes.viewDocument, None))
# Document > Close Preview
self.aCloseView = qtAddAction(self.docuMenu, self.tr("Close Document View"))
self.aCloseView.setShortcut("Ctrl+Shift+R")
self.aCloseView.triggered.connect(self.mainGui.docPanes.closeDocViewer)
# Document > Separator

View File

@@ -775,6 +775,8 @@ class GuiMain(QMainWindow):
if update.theme:
self.refreshThemeColors(syntax=update.syntax, force=True)
if update.vim:
self.mainMenu.updateVimModeKeys()
self.docPanes.applySettings(update)
if update.viewport:
self.projView.initViewport()

View File

@@ -26,19 +26,32 @@ from pathlib import Path
import pytest
from PyQt6.QtCore import Qt
from pytestqt.qtbot import QtBot
from novelwriter import CONFIG, SHARED
from novelwriter.core.project import NWProject
from novelwriter.dialogs.preferences import GuiNeedsUpdate
from novelwriter.editor.editor import GuiDocEditor
from novelwriter.editor.vim import VimState
from novelwriter.enum import nwVimMode
from novelwriter.guimain import GuiMain
from novelwriter.types import QtKeyEscape, QtKeyRight, QtModShift
from novelwriter.types import QtKeyEscape, QtKeyRight, QtModCtrl, QtModShift
from tests.helpers import C, MockRnd, buildTestProject
KEY_DELAY = 2
VIM_CHANGED = GuiNeedsUpdate(
restart=False,
tree=False,
theme=False,
syntax=False,
spelling=False,
vim=True,
editor=False,
viewer=False,
viewport=False,
)
def openVimEditor(nwGUI: GuiMain, projPath: Path) -> GuiDocEditor:
@@ -47,7 +60,7 @@ def openVimEditor(nwGUI: GuiMain, projPath: Path) -> GuiDocEditor:
nwGUI.openProject(projPath)
assert nwGUI.docPanes.openDocument(C.hSceneDoc)
CONFIG.vimMode = True
nwGUI.docEditor.initSettings(updateVimMode=True)
nwGUI._processConfigChanges(VIM_CHANGED)
return nwGUI.docEditor
@@ -80,19 +93,24 @@ def testEditorVimMode_Init(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockRnd
assert nwGUI.docPanes.openDocument(C.hSceneDoc)
docEditor = nwGUI.docEditor
docFooter = docEditor.docFooter
mainMenu = nwGUI.mainMenu
# Disabled by default
assert docEditor._vimMode is None
assert docFooter.vimStatus.isHidden() is True
assert mainMenu.aViewDoc.shortcut().toString() == "Ctrl+R"
assert mainMenu.aCloseView.shortcut().toString() == "Ctrl+Shift+R"
# Enable from settings
# Enable from settings, which also releases conflicting shortcuts
CONFIG.vimMode = True
docEditor.initSettings(updateVimMode=True)
nwGUI._processConfigChanges(VIM_CHANGED)
vimMode = docEditor._vimMode
assert vimMode is not None
assert vimMode._vim.mode == nwVimMode.NORMAL
assert docFooter._vimMode == nwVimMode.NORMAL
assert docFooter.vimStatus.isHidden() is False
assert mainMenu.aViewDoc.shortcut().isEmpty() is True
assert mainMenu.aCloseView.shortcut().isEmpty() is True
# Mode changes are reported to the footer
qtbot.keyClick(docEditor, "i", delay=KEY_DELAY)
@@ -139,12 +157,14 @@ def testEditorVimMode_Init(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockRnd
assert nwGUI.docPanes.openDocument(C.hChapterDoc)
assert vimMode._vim.mode == nwVimMode.NORMAL
# Disable from settings
# Disable from settings, which also restores the shortcuts
CONFIG.vimMode = False
docEditor.initSettings(updateVimMode=True)
nwGUI._processConfigChanges(VIM_CHANGED)
assert docEditor._vimMode is None
assert docFooter._vimMode is None
assert docFooter.vimStatus.isHidden() is True
assert mainMenu.aViewDoc.shortcut().toString() == "Ctrl+R"
assert mainMenu.aCloseView.shortcut().toString() == "Ctrl+Shift+R"
@pytest.mark.gui
@@ -601,6 +621,12 @@ def testEditorVimMode_VisualMode(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, m
qtbot.keyClick(docEditor, "u", delay=KEY_DELAY)
assert docEditor.getText() == originalText
# Ctrl+R: Redo and undo again
qtbot.keyClick(docEditor, Qt.Key.Key_R, QtModCtrl, delay=KEY_DELAY)
assert docEditor.getText().strip() == ""
qtbot.keyClick(docEditor, "u", delay=KEY_DELAY)
assert docEditor.getText() == originalText
qtbot.keyClicks(docEditor, "g", delay=KEY_DELAY)
qtbot.keyClicks(docEditor, "g", delay=KEY_DELAY)
assert docEditor.textCursor().position() == 0