diff --git a/src/main/java/com/googlecode/lanterna/gui2/AbstractBasePane.java b/src/main/java/com/googlecode/lanterna/gui2/AbstractBasePane.java index 6af2bd302..6acc7cfe6 100644 --- a/src/main/java/com/googlecode/lanterna/gui2/AbstractBasePane.java +++ b/src/main/java/com/googlecode/lanterna/gui2/AbstractBasePane.java @@ -100,7 +100,12 @@ public boolean handleInput(KeyStroke key) { // Now try to deliver the event to the focused component boolean handled = doHandleInput(key); - + if (!handled) + { + // Now try to deliver the event as an accelerator to unfocused components + handled = doHandleAccelerator(key); + } + // If it wasn't handled, fire the listeners and decide what to report to the TextGUI if(!handled) { AtomicBoolean hasBeenHandled = new AtomicBoolean(false); @@ -111,9 +116,42 @@ public boolean handleInput(KeyStroke key) { } return handled; } - + abstract T self(); + private boolean doHandleAccelerator(KeyStroke key) + { + if (key.getKeyType() == KeyType.MOUSE_EVENT) return false; + + MenuBar menuBar = getMenuBar(); + + // Check menu accelerators + if (menuBar != null && menuBar.handleInput(key)) return true; + + // Check on base component + return handleAccelerator(contentHolder, key); + } + + private boolean handleAccelerator(Container container, KeyStroke key) + { + // Check unfocused buttons + for (Component child : container.getChildren()) + { + if (child instanceof Button) + { + Button btn = (Button) child; + if (btn.handleInput(key) == Result.HANDLED) return true; + } + + if (child instanceof Container && ((Container)child).getChildCount() > 0) + { + if (handleAccelerator((Container)child, key)) return true; + } + } + + return false; + } + private boolean doHandleInput(KeyStroke key) { boolean result = false; if(key.getKeyType() == KeyType.MOUSE_EVENT) { diff --git a/src/main/java/com/googlecode/lanterna/gui2/AbstractInteractableComponent.java b/src/main/java/com/googlecode/lanterna/gui2/AbstractInteractableComponent.java index e1923f275..675f694e7 100644 --- a/src/main/java/com/googlecode/lanterna/gui2/AbstractInteractableComponent.java +++ b/src/main/java/com/googlecode/lanterna/gui2/AbstractInteractableComponent.java @@ -36,6 +36,7 @@ public abstract class AbstractInteractableComponent @@ -190,7 +199,7 @@ protected Result handleKeyStroke(KeyStroke keyStroke) { public TerminalPosition getCursorLocation() { return getRenderer().getCursorLocation(self()); } - + @Override public InputFilter getInputFilter() { return inputFilter; @@ -208,6 +217,13 @@ public boolean isKeyboardActivationStroke(KeyStroke keyStroke) { return isFocused() && isKeyboardActivation; } + public boolean isKeyboardAcceleratorStroke(KeyStroke keyStroke) + { + if (accelerator == null) return false; + + return isEnabled() && accelerator.equals(keyStroke); + } + public boolean isMouseActivationStroke(KeyStroke keyStroke) { boolean isMouseActivation = false; if (keyStroke instanceof MouseAction) { diff --git a/src/main/java/com/googlecode/lanterna/gui2/AbstractListBox.java b/src/main/java/com/googlecode/lanterna/gui2/AbstractListBox.java index 2f56c80ef..945d6a71b 100644 --- a/src/main/java/com/googlecode/lanterna/gui2/AbstractListBox.java +++ b/src/main/java/com/googlecode/lanterna/gui2/AbstractListBox.java @@ -152,7 +152,8 @@ public synchronized Result handleKeyStroke(KeyStroke keyStroke) { return Result.HANDLED; case CHARACTER: - if(selectByCharacter(keyStroke.getCharacter())) { + // Only check is Alt and Ctrl keys are not pressed signaling a possible accelerator key + if(!keyStroke.isAltDown() && !keyStroke.isCtrlDown() && selectByCharacter(keyStroke.getCharacter())) { return Result.HANDLED; } return Result.UNHANDLED; diff --git a/src/main/java/com/googlecode/lanterna/gui2/Button.java b/src/main/java/com/googlecode/lanterna/gui2/Button.java index 001ae7961..a1ae6f5ec 100644 --- a/src/main/java/com/googlecode/lanterna/gui2/Button.java +++ b/src/main/java/com/googlecode/lanterna/gui2/Button.java @@ -27,7 +27,6 @@ import com.googlecode.lanterna.TerminalTextUtils; import com.googlecode.lanterna.graphics.ThemeDefinition; import com.googlecode.lanterna.input.KeyStroke; -import com.googlecode.lanterna.input.KeyType; /** * Simple labeled button that the user can trigger by pressing the Enter or the Spacebar key on the keyboard when the @@ -81,12 +80,21 @@ public synchronized TerminalPosition getCursorLocation() { return getRenderer().getCursorLocation(this); } + public Button setAccelerator(KeyStroke keyStroke) + { + return super.setAccelerator(keyStroke); + } + + public KeyStroke getAccelerator() { return super.getAccelerator(); }; + @Override public synchronized Result handleKeyStroke(KeyStroke keyStroke) { - if (isActivationStroke(keyStroke)) { + if (isActivationStroke(keyStroke) || isKeyboardAcceleratorStroke(keyStroke)) + { + getBasePane().setFocusedInteractable(this); triggerActions(); return Result.HANDLED; - } + } return super.handleKeyStroke(keyStroke); } diff --git a/src/main/java/com/googlecode/lanterna/gui2/dialogs/DirectoryDialog.java b/src/main/java/com/googlecode/lanterna/gui2/dialogs/DirectoryDialog.java index d496edbf7..d60404dc7 100644 --- a/src/main/java/com/googlecode/lanterna/gui2/dialogs/DirectoryDialog.java +++ b/src/main/java/com/googlecode/lanterna/gui2/dialogs/DirectoryDialog.java @@ -20,6 +20,10 @@ package com.googlecode.lanterna.gui2.dialogs; +import java.io.File; +import java.util.Arrays; +import java.util.Comparator; + import com.googlecode.lanterna.TerminalSize; import com.googlecode.lanterna.gui2.ActionListBox; import com.googlecode.lanterna.gui2.BorderLayout; @@ -32,10 +36,7 @@ import com.googlecode.lanterna.gui2.Panel; import com.googlecode.lanterna.gui2.TextBox; import com.googlecode.lanterna.gui2.WindowBasedTextGUI; - -import java.io.File; -import java.util.Arrays; -import java.util.Comparator; +import com.googlecode.lanterna.input.KeyStroke; /** * Dialog that allows the user to iterate the file system and pick directory. @@ -101,8 +102,8 @@ public DirectoryDialog( Panel panelButtons = new Panel(new GridLayout(2)); panelButtons.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER, false, false, 2, 1)); - panelButtons.addComponent(new Button(actionLabel, new OkHandler())); - panelButtons.addComponent(new Button(LocalizedString.Cancel.toString(), new CancelHandler())); + panelButtons.addComponent(new Button(actionLabel, new OkHandler()).setAccelerator(new KeyStroke('s', false, true))); + panelButtons.addComponent(new Button(LocalizedString.Cancel.toString(), new CancelHandler()).setAccelerator(new KeyStroke('c', false, true))); contentPane.addComponent(panelButtons, Location.BOTTOM); if (selectedObject.isFile()) { diff --git a/src/main/java/com/googlecode/lanterna/gui2/dialogs/FileDialog.java b/src/main/java/com/googlecode/lanterna/gui2/dialogs/FileDialog.java index e245acf90..65e7847f5 100644 --- a/src/main/java/com/googlecode/lanterna/gui2/dialogs/FileDialog.java +++ b/src/main/java/com/googlecode/lanterna/gui2/dialogs/FileDialog.java @@ -18,14 +18,26 @@ */ package com.googlecode.lanterna.gui2.dialogs; -import com.googlecode.lanterna.TerminalTextUtils; -import com.googlecode.lanterna.TerminalSize; -import com.googlecode.lanterna.gui2.*; - import java.io.File; import java.util.Arrays; import java.util.Comparator; +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.TerminalTextUtils; +import com.googlecode.lanterna.gui2.ActionListBox; +import com.googlecode.lanterna.gui2.Borders; +import com.googlecode.lanterna.gui2.Button; +import com.googlecode.lanterna.gui2.Direction; +import com.googlecode.lanterna.gui2.GridLayout; +import com.googlecode.lanterna.gui2.Label; +import com.googlecode.lanterna.gui2.LocalizedString; +import com.googlecode.lanterna.gui2.Panel; +import com.googlecode.lanterna.gui2.Panels; +import com.googlecode.lanterna.gui2.Separator; +import com.googlecode.lanterna.gui2.TextBox; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import com.googlecode.lanterna.input.KeyStroke; + /** * Dialog that allows the user to iterate the file system and pick file to open/save * @@ -129,10 +141,10 @@ public FileDialog( 1)) .addTo(contentPane); - okButton = new Button(actionLabel, new OkHandler()); + okButton = new Button(actionLabel, new OkHandler()).setAccelerator(new KeyStroke('o', false, true)); Panels.grid(2, okButton, - new Button(LocalizedString.Cancel.toString(), new CancelHandler())) + new Button(LocalizedString.Cancel.toString(), new CancelHandler()).setAccelerator(new KeyStroke('c', false, true))) .setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER, false, false, 2, 1)) .addTo(contentPane); diff --git a/src/main/java/com/googlecode/lanterna/gui2/dialogs/MessageDialog.java b/src/main/java/com/googlecode/lanterna/gui2/dialogs/MessageDialog.java index d9d9d08b7..9e37a01e8 100644 --- a/src/main/java/com/googlecode/lanterna/gui2/dialogs/MessageDialog.java +++ b/src/main/java/com/googlecode/lanterna/gui2/dialogs/MessageDialog.java @@ -19,7 +19,13 @@ package com.googlecode.lanterna.gui2.dialogs; import com.googlecode.lanterna.TerminalSize; -import com.googlecode.lanterna.gui2.*; +import com.googlecode.lanterna.gui2.Button; +import com.googlecode.lanterna.gui2.EmptySpace; +import com.googlecode.lanterna.gui2.GridLayout; +import com.googlecode.lanterna.gui2.Label; +import com.googlecode.lanterna.gui2.Panel; +import com.googlecode.lanterna.gui2.WindowBasedTextGUI; +import com.googlecode.lanterna.input.KeyStroke; /** * Simple message dialog that displays a message and has optional selection/confirmation buttons @@ -47,7 +53,7 @@ public class MessageDialog extends DialogWindow { buttonPanel.addComponent(new Button(button.toString(), () -> { result = button; close(); - })); + }).setAccelerator(new KeyStroke(button.getAcceleratorCharacter(), false, true))); } Panel mainPanel = new Panel(); diff --git a/src/main/java/com/googlecode/lanterna/gui2/dialogs/MessageDialogButton.java b/src/main/java/com/googlecode/lanterna/gui2/dialogs/MessageDialogButton.java index 5d39237f9..0ddaec732 100644 --- a/src/main/java/com/googlecode/lanterna/gui2/dialogs/MessageDialogButton.java +++ b/src/main/java/com/googlecode/lanterna/gui2/dialogs/MessageDialogButton.java @@ -30,47 +30,50 @@ public enum MessageDialogButton { /** * "OK" */ - OK(LocalizedString.OK), + OK(LocalizedString.OK, 'o'), /** * "Cancel" */ - CANCEL(LocalizedString.Cancel), + CANCEL(LocalizedString.Cancel, 'c'), /** * "Yes" */ - YES(LocalizedString.Yes), + YES(LocalizedString.Yes, 'y'), /** * "No" */ - NO(LocalizedString.No), + NO(LocalizedString.No, 'n'), /** * "Close" */ - CLOSE(LocalizedString.Close), + CLOSE(LocalizedString.Close, 'x'), /** * "Abort" */ - ABORT(LocalizedString.Abort), + ABORT(LocalizedString.Abort, 'a'), /** * "Ignore" */ - IGNORE(LocalizedString.Ignore), + IGNORE(LocalizedString.Ignore, 'i'), /** * "Retry" */ - RETRY(LocalizedString.Retry), + RETRY(LocalizedString.Retry, 'r'), /** * "Continue" */ - CONTINUE(LocalizedString.Continue); + CONTINUE(LocalizedString.Continue, 'c'); private final LocalizedString label; + private final char acceleratorChar; - MessageDialogButton(final LocalizedString label) { + MessageDialogButton(final LocalizedString label, char acceleratorChar) { this.label = label; + this.acceleratorChar = acceleratorChar; } + public char getAcceleratorCharacter() { return acceleratorChar; } @Override public String toString() { return label.toString(); diff --git a/src/main/java/com/googlecode/lanterna/gui2/menu/Menu.java b/src/main/java/com/googlecode/lanterna/gui2/menu/Menu.java index da416f891..f325c48df 100644 --- a/src/main/java/com/googlecode/lanterna/gui2/menu/Menu.java +++ b/src/main/java/com/googlecode/lanterna/gui2/menu/Menu.java @@ -58,6 +58,17 @@ public Menu add(MenuItem menuItem) { return this; } + public List getSubMenus() + { + return new ArrayList<>(subItems); + } + + public Menu setAccelerator(KeyStroke keyStroke) + { + super.setAccelerator(keyStroke); + return this; + } + @Override protected boolean onActivated() { boolean result = true; @@ -91,6 +102,16 @@ public void onUnhandledInput(Window basePane, KeyStroke keyStroke, AtomicBoolean nextSelectedMenu.onActivated(); } } + + // Check if accelerator for c + for (MenuItem menuItem : subItems) + { + if (menuItem.isEnabled() && menuItem.isKeyboardAcceleratorStroke(keyStroke)) + { + Result result = menuItem.handleKeyStroke(keyStroke); + if (result == Result.HANDLED) break; + } + } } }); } diff --git a/src/main/java/com/googlecode/lanterna/gui2/menu/MenuBar.java b/src/main/java/com/googlecode/lanterna/gui2/menu/MenuBar.java index aefc896c0..53af09579 100644 --- a/src/main/java/com/googlecode/lanterna/gui2/menu/MenuBar.java +++ b/src/main/java/com/googlecode/lanterna/gui2/menu/MenuBar.java @@ -130,7 +130,18 @@ else if (!menus.contains(fromThis) || menus.indexOf(fromThis) == 0) { } @Override - public boolean handleInput(KeyStroke key) { + public boolean handleInput(KeyStroke key) + { + // Process top level menus + for (Menu mnu : menus) + { + // Check to see if handled by accelerator + if (mnu.isKeyboardAcceleratorStroke(key)) + { + mnu.handleKeyStroke(key); + return true; + } + } return false; } diff --git a/src/main/java/com/googlecode/lanterna/gui2/menu/MenuItem.java b/src/main/java/com/googlecode/lanterna/gui2/menu/MenuItem.java index 00b81f7c6..c1b8354a6 100644 --- a/src/main/java/com/googlecode/lanterna/gui2/menu/MenuItem.java +++ b/src/main/java/com/googlecode/lanterna/gui2/menu/MenuItem.java @@ -29,9 +29,6 @@ import com.googlecode.lanterna.gui2.TextGUIGraphics; import com.googlecode.lanterna.gui2.Window; import com.googlecode.lanterna.input.KeyStroke; -import com.googlecode.lanterna.input.KeyType; -import com.googlecode.lanterna.input.MouseAction; -import com.googlecode.lanterna.input.MouseActionType; /** * This class is a single item that appears in a {@link Menu} with an optional action attached to it @@ -71,6 +68,13 @@ public String getLabel() { return label; } + public MenuItem setAccelerator(KeyStroke keyStroke) + { + return super.setAccelerator(keyStroke); + } + + public KeyStroke getAccelerator() { return super.getAccelerator(); }; + @Override protected InteractableRenderer createDefaultRenderer() { return new DefaultMenuItemRenderer(); @@ -88,7 +92,9 @@ protected boolean onActivated() { @Override protected Result handleKeyStroke(KeyStroke keyStroke) { - if (isActivationStroke(keyStroke)) { + if (isActivationStroke(keyStroke) || isKeyboardAcceleratorStroke(keyStroke)) { + takeFocus(); + if (onActivated()) { BasePane basePane = getBasePane(); if (basePane instanceof Window && ((Window) basePane).getHints().contains(Window.Hint.MENU_POPUP)) { @@ -96,12 +102,13 @@ protected Result handleKeyStroke(KeyStroke keyStroke) { } } return Result.HANDLED; - } else if (isMouseMove(keyStroke)) { + } + else if (isMouseMove(keyStroke)) { takeFocus(); return Result.HANDLED; - } else { - return super.handleKeyStroke(keyStroke); - } + } + + return super.handleKeyStroke(keyStroke); } /** diff --git a/src/test/java/com/googlecode/lanterna/gui2/InputUITest.java b/src/test/java/com/googlecode/lanterna/gui2/InputUITest.java index 9b23df639..f8c644111 100644 --- a/src/test/java/com/googlecode/lanterna/gui2/InputUITest.java +++ b/src/test/java/com/googlecode/lanterna/gui2/InputUITest.java @@ -44,8 +44,13 @@ protected Result handleKeyStroke(KeyStroke keyStroke) { if (keyStroke.getKeyType() == KeyType.TAB) { return super.handleKeyStroke(keyStroke); } - if (keyStroke.getKeyType() == KeyType.CHARACTER) { - lastKey = keyStroke.getCharacter() + ""; + if (keyStroke.getKeyType() == KeyType.CHARACTER) + { + if (keyStroke.getCharacter().equals(' ')) + { + lastKey = "SPACE"; + } + else lastKey = keyStroke.getCharacter() + ""; } else { lastKey = keyStroke.getKeyType().toString(); diff --git a/src/test/java/com/googlecode/lanterna/gui2/ListBoxTest.java b/src/test/java/com/googlecode/lanterna/gui2/ListBoxTest.java index 030f16a63..818644353 100644 --- a/src/test/java/com/googlecode/lanterna/gui2/ListBoxTest.java +++ b/src/test/java/com/googlecode/lanterna/gui2/ListBoxTest.java @@ -19,6 +19,7 @@ package com.googlecode.lanterna.gui2; import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.input.KeyStroke; import java.io.IOException; @@ -55,7 +56,7 @@ public void init(WindowBasedTextGUI textGUI) { window.setComponent( Panels.vertical( horizontalPanel, - new Button("OK", window::close))); + new Button("OK", window::close).setAccelerator(new KeyStroke('o', false, true)))); textGUI.addWindow(window); } } \ No newline at end of file diff --git a/src/test/java/com/googlecode/lanterna/gui2/MenuTest.java b/src/test/java/com/googlecode/lanterna/gui2/MenuTest.java index 72346fee0..2e93a4e35 100644 --- a/src/test/java/com/googlecode/lanterna/gui2/MenuTest.java +++ b/src/test/java/com/googlecode/lanterna/gui2/MenuTest.java @@ -18,6 +18,9 @@ */ package com.googlecode.lanterna.gui2; +import java.io.File; +import java.io.IOException; + import com.googlecode.lanterna.TerminalSize; import com.googlecode.lanterna.TextColor; import com.googlecode.lanterna.gui2.dialogs.FileDialogBuilder; @@ -26,9 +29,7 @@ import com.googlecode.lanterna.gui2.menu.Menu; import com.googlecode.lanterna.gui2.menu.MenuBar; import com.googlecode.lanterna.gui2.menu.MenuItem; - -import java.io.File; -import java.io.IOException; +import com.googlecode.lanterna.input.KeyStroke; public class MenuTest extends TestBase { public static void main(String[] args) throws IOException, InterruptedException { @@ -49,38 +50,42 @@ public void init(final WindowBasedTextGUI textGUI) { MenuBar menubar = new MenuBar(); window.setMenuBar(menubar); - // "File" menu - Menu menuFile = new Menu("File"); + // "File" menu w/Accelerator Set + Menu menuFile = new Menu("File").setAccelerator(new KeyStroke('f', false, true)); menubar.add(menuFile); menuFile.add(new MenuItem("Open...", () -> { File file = new FileDialogBuilder().build().showDialog(textGUI); if (file != null) MessageDialog.showMessageDialog( textGUI, "Open", "Selected file:\n" + file, MessageDialogButton.OK); - })); - menuFile.add(new MenuItem("Exit", window::close)); + }).setAccelerator(new KeyStroke('o', false, false))); + menuFile.add(new MenuItem("Exit", window::close).setAccelerator(new KeyStroke('x', false, false))); - Menu countryMenu = new Menu("Country"); + // Menu w/accelerator set + Menu countryMenu = new Menu("Country").setAccelerator(new KeyStroke('c', false, true)); menubar.add(countryMenu); - Menu germanySubMenu = new Menu("Germany"); + // Menu w/accelerator not set + Menu germanySubMenu = new Menu("Germany").setAccelerator(new KeyStroke('g', false, false)); countryMenu.add(germanySubMenu); for (String state: GERMANY_STATES) { germanySubMenu.add(new MenuItem(state, DO_NOTHING)); } - Menu japanSubMenu = new Menu("Japan"); + + // Menu w/accelerator set + Menu japanSubMenu = new Menu("Japan").setAccelerator(new KeyStroke('j', false, false)); countryMenu.add(japanSubMenu); for (String prefecture: JAPAN_PREFECTURES) { japanSubMenu.add(new MenuItem(prefecture, DO_NOTHING)); } - // "Help" menu - Menu menuHelp = new Menu("Help"); + // "Help" menu w/accelerator set + Menu menuHelp = new Menu("Help").setAccelerator(new KeyStroke('h', false, true)); menubar.add(menuHelp); menuHelp.add(new MenuItem("Homepage", () -> MessageDialog.showMessageDialog( - textGUI, "Homepage", "https://github.com/mabe02/lanterna", MessageDialogButton.OK))); + textGUI, "Homepage", "https://github.com/mabe02/lanterna", MessageDialogButton.OK)).setAccelerator(new KeyStroke('h', false, false))); menuHelp.add(new MenuItem("About", () -> MessageDialog.showMessageDialog( - textGUI, "About", "Lanterna drop-down menu", MessageDialogButton.OK))); + textGUI, "About", "Lanterna drop-down menu", MessageDialogButton.OK)).setAccelerator(new KeyStroke('a', false, false))); // Create textGUI and start textGUI textGUI.addWindow(window);