From 1ee8f61a4e8ab8222746c417d5f3c7e94561110b Mon Sep 17 00:00:00 2001 From: MostCromulent <201167372+MostCromulent@users.noreply.github.com> Date: Sat, 14 Feb 2026 13:21:09 +1030 Subject: [PATCH 1/2] Add keyboard shortcuts cheatsheet dialog Accessible via H key during matches and Help > Keyboard Shortcuts menu. Shows configurable shortcuts with in-place rebinding and fixed menu accelerators in greyed-out fields. Configurable shortcuts auto-populate by iterating KeyboardShortcuts.getCachedShortcuts(), so adding a new SHORTCUT_* FPref entry automatically appears in the dialog. Co-Authored-By: Claude Opus 4.6 --- .../java/forge/control/KeyboardShortcuts.java | 21 +++ .../src/main/java/forge/menus/HelpMenu.java | 9 ++ .../home/settings/VSubmenuPreferences.java | 4 +- .../forge/view/KeyboardShortcutsDialog.java | 146 ++++++++++++++++++ forge-gui/res/languages/en-US.properties | 3 + .../properties/ForgePreferences.java | 1 + 6 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 forge-gui-desktop/src/main/java/forge/view/KeyboardShortcutsDialog.java diff --git a/forge-gui-desktop/src/main/java/forge/control/KeyboardShortcuts.java b/forge-gui-desktop/src/main/java/forge/control/KeyboardShortcuts.java index 8b2de20b8de..14b679e7887 100644 --- a/forge-gui-desktop/src/main/java/forge/control/KeyboardShortcuts.java +++ b/forge-gui-desktop/src/main/java/forge/control/KeyboardShortcuts.java @@ -13,6 +13,7 @@ import javax.swing.InputMap; import javax.swing.JComponent; import javax.swing.KeyStroke; +import javax.swing.SwingUtilities; import org.apache.commons.lang3.StringUtils; @@ -27,6 +28,7 @@ import forge.screens.match.CMatchUI; import forge.toolbox.special.CardZoomer; import forge.util.Localizer; +import forge.view.KeyboardShortcutsDialog; /** * Consolidates keyboard shortcut assembly into one location @@ -36,10 +38,16 @@ * and you're done. */ public class KeyboardShortcuts { + private static List cachedShortcuts; + public static List getKeyboardShortcuts() { return attachKeyboardShortcuts(null); } + public static List getCachedShortcuts() { + return cachedShortcuts != null ? cachedShortcuts : getKeyboardShortcuts(); + } + /** * Attaches all keyboard shortcuts for match UI, * and returns a list of shortcuts with necessary properties for later access. @@ -200,6 +208,17 @@ public void actionPerformed(ActionEvent e) { } }; + /** Show keyboard shortcuts dialog. */ + final Action actShowHotkeys = new AbstractAction() { + @Override + public void actionPerformed(final ActionEvent e) { + if (!Singletons.getControl().getCurrentScreen().isMatchScreen()) { return; } + // Defer so the triggering key event is fully consumed before the dialog opens, + // preventing it from being captured by a KeyboardShortcutField. + SwingUtilities.invokeLater(() -> new KeyboardShortcutsDialog().setVisible(true)); + } + }; + final Localizer localizer = Localizer.getInstance(); //========== Instantiate shortcut objects and add to list. list.add(new Shortcut(FPref.SHORTCUT_SHOWSTACK, localizer.getMessage("lblSHORTCUT_SHOWSTACK"), actShowStack, am, im)); @@ -215,6 +234,8 @@ public void actionPerformed(ActionEvent e) { list.add(new Shortcut(FPref.SHORTCUT_MACRO_RECORD, localizer.getMessage("lblSHORTCUT_MACRO_RECORD"), actMacroRecord, am, im)); list.add(new Shortcut(FPref.SHORTCUT_MACRO_NEXT_ACTION, localizer.getMessage("lblSHORTCUT_MACRO_NEXT_ACTION"), actMacroNextAction, am, im)); list.add(new Shortcut(FPref.SHORTCUT_CARD_ZOOM, localizer.getMessage("lblSHORTCUT_CARD_ZOOM"), actZoomCard, am, im)); + list.add(new Shortcut(FPref.SHORTCUT_SHOWHOTKEYS, localizer.getMessage("lblSHORTCUT_SHOWHOTKEYS"), actShowHotkeys, am, im)); + cachedShortcuts = list; return list; } // End initMatchShortcuts() diff --git a/forge-gui-desktop/src/main/java/forge/menus/HelpMenu.java b/forge-gui-desktop/src/main/java/forge/menus/HelpMenu.java index 465b035f4b0..e371d509bb6 100644 --- a/forge-gui-desktop/src/main/java/forge/menus/HelpMenu.java +++ b/forge-gui-desktop/src/main/java/forge/menus/HelpMenu.java @@ -15,6 +15,7 @@ import forge.util.BuildInfo; import forge.util.FileUtil; import forge.util.Localizer; +import forge.view.KeyboardShortcutsDialog; import static forge.localinstance.properties.ForgeConstants.GITHUB_FORGE_URL; @@ -27,6 +28,7 @@ public static JMenu getMenu() { menu.setMnemonic(KeyEvent.VK_H); menu.add(getMenu_GettingStarted()); menu.add(getMenu_Troubleshooting()); + menu.add(getMenuItem_KeyboardShortcuts()); menu.addSeparator(); menu.add(getMenuItem_ReleaseNotes()); menu.add(getMenuItem_License()); @@ -67,6 +69,13 @@ private static JMenu getMenu_GettingStarted() { return mnu; } + private static JMenuItem getMenuItem_KeyboardShortcuts() { + final Localizer localizer = Localizer.getInstance(); + JMenuItem menuItem = new JMenuItem(localizer.getMessage("lblKeyboardShortcuts")); + menuItem.addActionListener(e -> new KeyboardShortcutsDialog().setVisible(true)); + return menuItem; + } + private static JMenuItem getMenuItem_HowToPlayFile() { final Localizer localizer = Localizer.getInstance(); JMenuItem menuItem = new JMenuItem(localizer.getMessage("lblHowtoPlay")); diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java b/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java index 7daa2778b8c..2702b975d55 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/settings/VSubmenuPreferences.java @@ -573,7 +573,7 @@ private NoteLabel(final String txt0) { * into characters and (dis)assembly of keycode stack. */ @SuppressWarnings("serial") - public class KeyboardShortcutField extends SkinnedTextField { + public static class KeyboardShortcutField extends SkinnedTextField { private String codeString; /** @@ -648,7 +648,7 @@ public final void setCodeString(final String str0) { } } - this.setText(StringUtils.join(displayText, ' ')); + this.setText(StringUtils.join(displayText, '+')); } } diff --git a/forge-gui-desktop/src/main/java/forge/view/KeyboardShortcutsDialog.java b/forge-gui-desktop/src/main/java/forge/view/KeyboardShortcutsDialog.java new file mode 100644 index 00000000000..17b85d2c9b7 --- /dev/null +++ b/forge-gui-desktop/src/main/java/forge/view/KeyboardShortcutsDialog.java @@ -0,0 +1,146 @@ +package forge.view; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Rectangle; +import java.awt.Toolkit; +import java.awt.event.InputEvent; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; + +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.Scrollable; +import javax.swing.ScrollPaneConstants; +import javax.swing.SwingConstants; +import javax.swing.UIManager; + +import forge.control.KeyboardShortcuts; +import forge.control.KeyboardShortcuts.Shortcut; +import forge.screens.home.settings.VSubmenuPreferences.KeyboardShortcutField; +import forge.toolbox.FLabel; +import forge.toolbox.FScrollPane; +import forge.toolbox.FSkin; +import forge.util.Localizer; +import net.miginfocom.swing.MigLayout; + +@SuppressWarnings("serial") +public class KeyboardShortcutsDialog extends FDialog { + + public KeyboardShortcutsDialog() { + super(true, true, "10"); + final Localizer localizer = Localizer.getInstance(); + setTitle(localizer.getMessage("lblKeyboardShortcuts")); + setSize(500, 600); + setMinimumSize(new Dimension(350, 300)); + + // Scrollable panel that always matches viewport width, preventing horizontal scroll. + final JPanel content = new ScrollablePanel(new MigLayout("insets 10 15 20 15, gap 5, wrap 2, fillx", "[grow][120!]")); + content.setOpaque(false); + + // Section 1: Configurable shortcuts + for (final Shortcut shortcut : KeyboardShortcuts.getCachedShortcuts()) { + final FLabel descLabel = new FLabel.Builder() + .text(shortcut.getDescription()) + .fontAlign(SwingConstants.LEFT) + .fontSize(11).build(); + content.add(descLabel, "growx"); + + final KeyboardShortcutField field = new KeyboardShortcutField(shortcut); + field.setFont(FSkin.getRelativeFont(11)); + // Clear existing binding on click so a new key press replaces it. + field.addMouseListener(new java.awt.event.MouseAdapter() { + @Override + public void mousePressed(final java.awt.event.MouseEvent e) { + field.setCodeString(""); + } + }); + // Transfer focus away once a non-modifier key completes the binding. + field.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(final KeyEvent e) { + final int code = e.getKeyCode(); + if (code != KeyEvent.VK_SHIFT && code != KeyEvent.VK_CONTROL + && code != KeyEvent.VK_ALT && code != KeyEvent.VK_META) { + content.requestFocusInWindow(); + } + } + }); + content.add(field, "w 120!, h 22!"); + } + + // Section 2: Fixed menu accelerators + final FLabel headerFixed = new FLabel.Builder() + .text(localizer.getMessage("lblMenuShortcuts")) + .fontAlign(SwingConstants.LEFT) + .fontSize(14).fontStyle(Font.BOLD).build(); + content.add(headerFixed, "span 2, gaptop 10, gapbottom 5, wrap"); + + final int menuMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx(); + final String modPrefix = InputEvent.getModifiersExText(menuMask) + "+"; + + addFixedRow(content, "Forge Wiki", KeyEvent.getKeyText(KeyEvent.VK_F1)); + addFixedRow(content, "Full Screen", KeyEvent.getKeyText(KeyEvent.VK_F11)); + addFixedRow(content, "Undo", modPrefix + KeyEvent.getKeyText(KeyEvent.VK_Z)); + addFixedRow(content, "Concede", modPrefix + KeyEvent.getKeyText(KeyEvent.VK_Q)); + addFixedRow(content, "Alpha Strike", modPrefix + KeyEvent.getKeyText(KeyEvent.VK_A)); + addFixedRow(content, "End Turn", modPrefix + KeyEvent.getKeyText(KeyEvent.VK_E)); + addFixedRow(content, "Toggle Panel Tabs", modPrefix + KeyEvent.getKeyText(KeyEvent.VK_T)); + addFixedRow(content, "Toggle Card Overlays", modPrefix + KeyEvent.getKeyText(KeyEvent.VK_O)); + + final FScrollPane scrollPane = new FScrollPane(content, false, + ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, + ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + add(scrollPane, "w 100%!, h 100%!"); + setDefaultFocus(scrollPane); + } + + private static void addFixedRow(final JPanel panel, final String description, final String keyText) { + final FLabel descLabel = new FLabel.Builder() + .text(description).fontAlign(SwingConstants.LEFT).fontSize(11).build(); + panel.add(descLabel, "growx"); + + final JLabel field = new JLabel(keyText); + field.setOpaque(true); + field.setBackground(new Color(200, 200, 200)); + field.setFont(FSkin.getRelativeFont(11).getBaseFont()); + field.setBorder(BorderFactory.createCompoundBorder( + UIManager.getBorder("TextField.border"), + BorderFactory.createEmptyBorder(0, 2, 0, 2))); + panel.add(field, "w 120!, h 22!"); + } + + /** JPanel that implements Scrollable to track viewport width, preventing horizontal scrolling. */ + private static class ScrollablePanel extends JPanel implements Scrollable { + ScrollablePanel(java.awt.LayoutManager layout) { + super(layout); + } + + @Override + public Dimension getPreferredScrollableViewportSize() { + return getPreferredSize(); + } + + @Override + public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { + return 16; + } + + @Override + public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { + return 64; + } + + @Override + public boolean getScrollableTracksViewportWidth() { + return true; + } + + @Override + public boolean getScrollableTracksViewportHeight() { + return false; + } + } +} diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index 5dde6bb379b..c0010bb910e 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -467,6 +467,9 @@ lblSHORTCUT_AUTOYIELD_ALWAYS_NO=Match: auto-yield ability on stack (Always No) lblSHORTCUT_MACRO_RECORD=Match: record a macro sequence of actions lblSHORTCUT_MACRO_NEXT_ACTION=Match: execute next action in a recorded macro lblSHORTCUT_CARD_ZOOM=Match: zoom the currently selected card +lblSHORTCUT_SHOWHOTKEYS=Match: show keyboard shortcuts +lblKeyboardShortcuts=Keyboard Shortcuts +lblMenuShortcuts=Menu Shortcuts (Not Configurable) #VSubmenuDraft.java lblBoosterDraft=Booster Draft lblHeaderBoosterDraft=Sanctioned Format: Booster Draft diff --git a/forge-gui/src/main/java/forge/localinstance/properties/ForgePreferences.java b/forge-gui/src/main/java/forge/localinstance/properties/ForgePreferences.java index baf8d3a95ef..60c951a708c 100644 --- a/forge-gui/src/main/java/forge/localinstance/properties/ForgePreferences.java +++ b/forge-gui/src/main/java/forge/localinstance/properties/ForgePreferences.java @@ -286,6 +286,7 @@ public enum FPref implements PreferencesStore.IPref { SHORTCUT_MACRO_RECORD ("16 82"), SHORTCUT_MACRO_NEXT_ACTION ("16 50"), SHORTCUT_CARD_ZOOM("90"), + SHORTCUT_SHOWHOTKEYS("72"), LAST_IMPORTED_CUBE_ID(""); From 6dcc96cdf9019695ffd7ce9841bd118ad1d0cbab Mon Sep 17 00:00:00 2001 From: MostCromulent <201167372+MostCromulent@users.noreply.github.com> Date: Sat, 14 Feb 2026 13:31:50 +1030 Subject: [PATCH 2/2] Make keyboard shortcuts dialog non-modal Allows users to interact with the main window while the dialog is open, including closing Forge. Co-Authored-By: Claude Opus 4.6 --- .../src/main/java/forge/view/KeyboardShortcutsDialog.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge-gui-desktop/src/main/java/forge/view/KeyboardShortcutsDialog.java b/forge-gui-desktop/src/main/java/forge/view/KeyboardShortcutsDialog.java index 17b85d2c9b7..441e0753a14 100644 --- a/forge-gui-desktop/src/main/java/forge/view/KeyboardShortcutsDialog.java +++ b/forge-gui-desktop/src/main/java/forge/view/KeyboardShortcutsDialog.java @@ -30,7 +30,7 @@ public class KeyboardShortcutsDialog extends FDialog { public KeyboardShortcutsDialog() { - super(true, true, "10"); + super(false, true, "10"); final Localizer localizer = Localizer.getInstance(); setTitle(localizer.getMessage("lblKeyboardShortcuts")); setSize(500, 600);