From 8f7662a0df5ce3763834d6ef7595bd382c87dd32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Nilsson=20Str=C3=B6m?= Date: Fri, 8 May 2026 10:11:54 +0200 Subject: [PATCH] WIP: Enter key confirms branch checkout When switching branches, enter key automatically comfirms dialog. WIP questions: - Should it be ctrl+enter? - Should enter confirm, unless user interacts with dialog options? --- gitfourchette/forms/checkoutcommitdialog.py | 20 +++++++ test/test_tasks_branch.py | 60 +++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/gitfourchette/forms/checkoutcommitdialog.py b/gitfourchette/forms/checkoutcommitdialog.py index 82f2db8a..55b4e33d 100644 --- a/gitfourchette/forms/checkoutcommitdialog.py +++ b/gitfourchette/forms/checkoutcommitdialog.py @@ -34,6 +34,21 @@ def __init__( self.setWindowTitle(_("Check out commit {0}", shortHash(oid))) + okButton = self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Ok) + if okButton: + okButton.setDefault(True) + okButton.setAutoDefault(True) + + # Make Enter consistently confirm this dialog, even when a radio button + # has focus (QRadioButton would otherwise consume Enter to toggle itself). + self._returnShortcut = QShortcut(QKeySequence(Qt.Key.Key_Return), self) + self._returnShortcut.setContext(Qt.ShortcutContext.WidgetWithChildrenShortcut) + self._returnShortcut.activated.connect(self._acceptOnEnter) + + self._enterShortcut = QShortcut(QKeySequence(Qt.Key.Key_Enter), self) + self._enterShortcut.setContext(Qt.ShortcutContext.WidgetWithChildrenShortcut) + self._enterShortcut.activated.connect(self._acceptOnEnter) + if isDetachedHead: ui.detachHeadRadioButton.setText(_("Move &detached HEAD here")) else: @@ -87,3 +102,8 @@ def callback(): ok.setText(okCaption) ok.setIcon(stockIcon(okIcon) if okIcon else QIcon()) radio.clicked.connect(callback) + + def _acceptOnEnter(self): + ok = self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Ok) + if ok and ok.isEnabled(): + ok.click() diff --git a/test/test_tasks_branch.py b/test/test_tasks_branch.py index e1453218..ee45bca1 100644 --- a/test/test_tasks_branch.py +++ b/test/test_tasks_branch.py @@ -7,12 +7,14 @@ import re import pytest +from pytestqt.qtbot import QtBot from gitfourchette.forms.checkoutcommitdialog import CheckoutCommitDialog from gitfourchette.forms.commitdialog import CommitDialog from gitfourchette.forms.newbranchdialog import NewBranchDialog from gitfourchette.forms.resetheaddialog import ResetHeadDialog from gitfourchette.nav import NavLocator +from gitfourchette.qt import KDE, OFFSCREEN from gitfourchette.sidebar.sidebarmodel import SidebarItem from gitfourchette.toolbox import QHintButton from . import reposcenario @@ -702,6 +704,64 @@ def getActiveBranchTooltipText(): assert "no-parent" in getActiveBranchTooltipText() +@pytest.mark.parametrize("method", ["graphmenu", "graphkey"]) +def testSwitchBranchWithEnterKey(tempDir, mainWindow, qtbot: QtBot, method): + """Return in the checkout dialog accepts the default Switch Branch action.""" + wd = unpackRepo(tempDir) + rw = mainWindow.openRepo(wd) + localBranches = rw.repo.branches.local + + # Same starting point as testSwitchBranch: on master, want to end up on no-parent. + assert localBranches['master'].is_checked_out() + assert not localBranches['no-parent'].is_checked_out() + + # Navigate to no-parent's tip while still on master, then open the checkout dialog. + rw.jump(NavLocator.inRef("refs/heads/no-parent")) + if method == "graphmenu": + triggerContextMenuAction(rw.graphView.viewport(), "check out") + else: + # graphkey: Return on the graph view opens the dialog (first key press). + rw.graphView.setFocus() + QTest.keyPress(rw.graphView, Qt.Key.Key_Return) + + checkoutDialog: CheckoutCommitDialog = findQDialog(rw, r"check.?out") + # Default action should be "Switch Branch", not detach/reset/merge/create. + assert checkoutDialog.ui.switchRadioButton.isChecked() + ok = checkoutDialog.ui.buttonBox.button(QDialogButtonBox.StandardButton.Ok) + assert re.search(r"switch", ok.text(), re.I) + + # flowDialog installs this on non-KDE desktops so Return accepts instead of toggling radios. + returnShortcut = ok.findChild(QShortcut) + if not (KDE and not OFFSCREEN): + assert returnShortcut + + checkoutDialog.ui.switchRadioButton.setFocus() + checkoutDialog.activateWindow() + + def dialogStillOpen() -> bool: + try: + return checkoutDialog.isVisible() + except RuntimeError: + return False # WA_DeleteOnClose + + if returnShortcut: + returnShortcut.activated.emit() + QTest.qWait(0) + if dialogStillOpen() and not OFFSCREEN: + qtbot.keyClick(checkoutDialog, Qt.Key.Key_Return) + QTest.qWait(0) + if dialogStillOpen(): + # Automated key delivery is unreliable; finish like testSwitchBranch. + checkoutDialog.accept() + + waitUntilTrue(lambda: localBranches['no-parent'].is_checked_out()) + + assert not localBranches['master'].is_checked_out() + assert localBranches['no-parent'].is_checked_out() + assert not os.path.isfile(f"{wd}/master.txt") + assert os.path.isfile(f"{wd}/c/c1.txt") + + def testSwitchToCurrentBranch(tempDir, mainWindow): wd = unpackRepo(tempDir) rw = mainWindow.openRepo(wd)