From c7568ec889fca02c7aa546032719374ce6b1e2d0 Mon Sep 17 00:00:00 2001 From: Konicai <71294714+Konicai@users.noreply.github.com> Date: Mon, 26 Jun 2023 14:44:25 -0400 Subject: [PATCH] Move to a text box for commands on Geyser standalone GUI (#3915) --- .../standalone/GeyserStandaloneBootstrap.java | 8 +- .../platform/standalone/gui/ColorPane.java | 4 +- .../standalone/gui/GeyserStandaloneGUI.java | 259 +++++++++--------- .../java/org/geysermc/geyser/GeyserImpl.java | 20 +- core/src/main/resources/languages | 2 +- 5 files changed, 138 insertions(+), 155 deletions(-) diff --git a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java index 64686f5f5..b505b361e 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java +++ b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java @@ -181,14 +181,14 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap { } } + this.geyserLogger = new GeyserStandaloneLogger(); + if (useGui && gui == null) { - gui = new GeyserStandaloneGUI(); + gui = new GeyserStandaloneGUI(geyserLogger); gui.redirectSystemStreams(); gui.startUpdateThread(); } - geyserLogger = new GeyserStandaloneLogger(); - LoopbackUtil.checkAndApplyLoopback(geyserLogger); try { @@ -224,7 +224,7 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap { geyserCommandManager.init(); if (gui != null) { - gui.setupInterface(geyserLogger, geyserCommandManager); + gui.enableCommands(geyser.getScheduledThread(), geyserCommandManager); } geyserPingPassthrough = GeyserLegacyPingPassthrough.init(geyser); diff --git a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/ColorPane.java b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/ColorPane.java index c08f602d4..1bdc90123 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/ColorPane.java +++ b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/ColorPane.java @@ -86,7 +86,7 @@ public class ColorPane extends JTextPane { while (stillSearching) { mIndex = addString.indexOf("m", aPos); // find the end of the escape sequence if (mIndex < 0) { // the buffer ends halfway through the ansi string! - remaining = addString.substring(aPos, addString.length()); + remaining = addString.substring(aPos); stillSearching = false; continue; } else { @@ -99,7 +99,7 @@ public class ColorPane extends JTextPane { aIndex = addString.indexOf("\u001B", aPos); if (aIndex == -1) { // if that was the last sequence of the input, send remaining text - tmpString = addString.substring(aPos, addString.length()); + tmpString = addString.substring(aPos); append(colorCurrent, tmpString); stillSearching = false; continue; // jump out of loop early, as the whole string has been sent now diff --git a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/GeyserStandaloneGUI.java b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/GeyserStandaloneGUI.java index 41cbafb25..af3e1069f 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/GeyserStandaloneGUI.java +++ b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/GeyserStandaloneGUI.java @@ -26,10 +26,8 @@ package org.geysermc.geyser.platform.standalone.gui; import org.geysermc.geyser.GeyserImpl; -import org.geysermc.geyser.api.command.Command; -import org.geysermc.geyser.command.GeyserCommand; +import org.geysermc.geyser.GeyserLogger; import org.geysermc.geyser.command.GeyserCommandManager; -import org.geysermc.geyser.platform.standalone.GeyserStandaloneLogger; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.GeyserLocale; @@ -45,28 +43,37 @@ import java.io.PrintStream; import java.net.URL; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.Vector; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; public class GeyserStandaloneGUI { - private static final DefaultTableModel playerTableModel = new DefaultTableModel(); - private static final List ramValues = new ArrayList<>(); - - private static final ColorPane consolePane = new ColorPane(); - private static final GraphPanel ramGraph = new GraphPanel(); - private static final JTable playerTable = new JTable(playerTableModel); - private static final int originalFontSize = consolePane.getFont().getSize(); - private static final long MEGABYTE = 1024L * 1024L; - private final JMenu commandsMenu; - private final JMenu optionsMenu; + private final GeyserLogger logger; + + private final ColorPane consolePane = new ColorPane(); + private final int originalFontSize = consolePane.getFont().getSize(); + private final JTextField commandInput = new JTextField(); + private final CommandListener commandListener = new CommandListener(); + + private final GraphPanel ramGraph = new GraphPanel(); + private final List ramValues = new ArrayList<>(); + + private final DefaultTableModel playerTableModel = new DefaultTableModel(); + private final JTable playerTable = new JTable(playerTableModel); + + /** + * Create and show the Geyser-Standalone GUI + * + * @param logger the logger for determining debug mode, and executing commands from the console + */ + public GeyserStandaloneGUI(GeyserLogger logger) { + this.logger = logger; - public GeyserStandaloneGUI() { // Create the frame and setup basic settings JFrame frame = new JFrame(GeyserLocale.getLocaleStringLog("geyser.gui.title")); frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); @@ -81,8 +88,7 @@ public class GeyserStandaloneGUI { // Show a confirm dialog on close frame.addWindowListener(new WindowAdapter() { @Override - public void windowClosing(WindowEvent we) - { + public void windowClosing(WindowEvent we) { String[] buttons = {GeyserLocale.getLocaleStringLog("geyser.gui.exit.confirm"), GeyserLocale.getLocaleStringLog("geyser.gui.exit.deny")}; int result = JOptionPane.showOptionDialog(frame, GeyserLocale.getLocaleStringLog("geyser.gui.exit.message"), frame.getTitle(), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null, buttons, buttons[1]); if (result == JOptionPane.YES_OPTION) { @@ -100,91 +106,41 @@ public class GeyserStandaloneGUI { frame.setIconImage(icon.getImage()); } + // File, View, Options, etc + setupMenuBar(frame); + // Setup the split pane and event listeners JSplitPane splitPane = new JSplitPane(); splitPane.setDividerLocation(600); - splitPane.addPropertyChangeListener("dividerLocation", e -> splitPaneLimit((JSplitPane)e.getSource())); + splitPane.addPropertyChangeListener("dividerLocation", e -> splitPaneLimit((JSplitPane) e.getSource())); splitPane.addComponentListener(new ComponentAdapter() { public void componentResized(ComponentEvent e) { - splitPaneLimit((JSplitPane)e.getSource()); + splitPaneLimit((JSplitPane) e.getSource()); } }); cp.add(splitPane, BorderLayout.CENTER); - // Set the background and disable input for the text pane + // Holds console and command input components + JPanel leftPane = new JPanel(new BorderLayout()); + splitPane.setLeftComponent(leftPane); + + // Set the background and disable editing of the console consolePane.setBackground(Color.BLACK); consolePane.setEditable(false); // Wrap the text pane in a scroll pane and add it to the form JScrollPane consoleScrollPane = new JScrollPane(consolePane); - //cp.add(consoleScrollPane, BorderLayout.CENTER); - splitPane.setLeftComponent(consoleScrollPane); + leftPane.add(consoleScrollPane, BorderLayout.CENTER); - // Create a new menu bar for the top of the frame - JMenuBar menuBar = new JMenuBar(); - - // Create 'File' - JMenu fileMenu = new JMenu(GeyserLocale.getLocaleStringLog("geyser.gui.menu.file")); - fileMenu.setMnemonic(KeyEvent.VK_F); - menuBar.add(fileMenu); - - // 'Open Geyser folder' button - JMenuItem openButton = new JMenuItem(GeyserLocale.getLocaleStringLog("geyser.gui.menu.file.open_folder"), KeyEvent.VK_O); - openButton.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_MASK)); - openButton.addActionListener(e -> { - try { - Desktop.getDesktop().open(new File("./")); - } catch (IOException ignored) { } - }); - fileMenu.add(openButton); - - fileMenu.addSeparator(); - - // 'Exit' button - JMenuItem exitButton = new JMenuItem(GeyserLocale.getLocaleStringLog("geyser.gui.menu.file.exit"), KeyEvent.VK_X); - exitButton.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F4, InputEvent.ALT_MASK)); - exitButton.addActionListener(e -> System.exit(0)); - fileMenu.add(exitButton); - - // Create 'Commands' - commandsMenu = new JMenu(GeyserLocale.getLocaleStringLog("geyser.gui.menu.commands")); - commandsMenu.setMnemonic(KeyEvent.VK_C); - menuBar.add(commandsMenu); - - // Create 'View' - JMenu viewMenu = new JMenu(GeyserLocale.getLocaleStringLog("geyser.gui.menu.view")); - viewMenu.setMnemonic(KeyEvent.VK_V); - menuBar.add(viewMenu); - - // 'Zoom in' button - JMenuItem zoomInButton = new JMenuItem(GeyserLocale.getLocaleStringLog("geyser.gui.menu.view.zoom_in")); - zoomInButton.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, InputEvent.CTRL_DOWN_MASK)); - zoomInButton.addActionListener(e -> consolePane.setFont(new Font(consolePane.getFont().getName(), consolePane.getFont().getStyle(), consolePane.getFont().getSize() + 1))); - viewMenu.add(zoomInButton); - - // 'Zoom in' button - JMenuItem zoomOutButton = new JMenuItem(GeyserLocale.getLocaleStringLog("geyser.gui.menu.view.zoom_out")); - zoomOutButton.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, InputEvent.CTRL_DOWN_MASK)); - zoomOutButton.addActionListener(e -> consolePane.setFont(new Font(consolePane.getFont().getName(), consolePane.getFont().getStyle(), consolePane.getFont().getSize() - 1))); - viewMenu.add(zoomOutButton); - - // 'Reset Zoom' button - JMenuItem resetZoomButton = new JMenuItem(GeyserLocale.getLocaleStringLog("geyser.gui.menu.view.reset_zoom")); - resetZoomButton.addActionListener(e -> consolePane.setFont(new Font(consolePane.getFont().getName(), consolePane.getFont().getStyle(), originalFontSize))); - viewMenu.add(resetZoomButton); - - // create 'Options' - optionsMenu = new JMenu(GeyserLocale.getLocaleStringLog("geyser.gui.menu.options")); - viewMenu.setMnemonic(KeyEvent.VK_O); - menuBar.add(optionsMenu); - - // Set the frames menu bar - frame.setJMenuBar(menuBar); + // a bit taller than the default layout - width is ignored fortunately + commandInput.setPreferredSize(new Dimension(100, 25)); + commandInput.setEnabled(false); // disabled until command handler is initialized + commandInput.addActionListener(commandListener); + leftPane.add(commandInput, BorderLayout.SOUTH); JPanel rightPane = new JPanel(); rightPane.setLayout(new CardLayout(5, 5)); - //cp.add(rightPane, BorderLayout.EAST); splitPane.setRightComponent(rightPane); JPanel rightContentPane = new JPanel(); @@ -209,12 +165,75 @@ public class GeyserStandaloneGUI { frame.setVisible(true); } + private void setupMenuBar(JFrame frame) { + // Create a new menu bar for the top of the frame + JMenuBar menuBar = new JMenuBar(); + + // Create 'File' + JMenu fileMenu = new JMenu(GeyserLocale.getLocaleStringLog("geyser.gui.menu.file")); + fileMenu.setMnemonic(KeyEvent.VK_F); + menuBar.add(fileMenu); + + // 'Open Geyser folder' button + JMenuItem openButton = new JMenuItem(GeyserLocale.getLocaleStringLog("geyser.gui.menu.file.open_folder"), KeyEvent.VK_O); + openButton.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_DOWN_MASK)); + openButton.addActionListener(e -> { + try { + Desktop.getDesktop().open(new File("./")); + } catch (IOException ignored) { } + }); + fileMenu.add(openButton); + + fileMenu.addSeparator(); + + // 'Exit' button + JMenuItem exitButton = new JMenuItem(GeyserLocale.getLocaleStringLog("geyser.gui.menu.file.exit"), KeyEvent.VK_X); + exitButton.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F4, InputEvent.ALT_DOWN_MASK)); + exitButton.addActionListener(e -> System.exit(0)); + fileMenu.add(exitButton); + + // Create 'View' + JMenu viewMenu = new JMenu(GeyserLocale.getLocaleStringLog("geyser.gui.menu.view")); + viewMenu.setMnemonic(KeyEvent.VK_V); + menuBar.add(viewMenu); + + // 'Zoom in' button + JMenuItem zoomInButton = new JMenuItem(GeyserLocale.getLocaleStringLog("geyser.gui.menu.view.zoom_in")); + zoomInButton.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, InputEvent.CTRL_DOWN_MASK)); + zoomInButton.addActionListener(e -> consolePane.setFont(new Font(consolePane.getFont().getName(), consolePane.getFont().getStyle(), consolePane.getFont().getSize() + 1))); + viewMenu.add(zoomInButton); + + // 'Zoom in' button + JMenuItem zoomOutButton = new JMenuItem(GeyserLocale.getLocaleStringLog("geyser.gui.menu.view.zoom_out")); + zoomOutButton.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, InputEvent.CTRL_DOWN_MASK)); + zoomOutButton.addActionListener(e -> consolePane.setFont(new Font(consolePane.getFont().getName(), consolePane.getFont().getStyle(), consolePane.getFont().getSize() - 1))); + viewMenu.add(zoomOutButton); + + // 'Reset Zoom' button + JMenuItem resetZoomButton = new JMenuItem(GeyserLocale.getLocaleStringLog("geyser.gui.menu.view.reset_zoom")); + resetZoomButton.addActionListener(e -> consolePane.setFont(new Font(consolePane.getFont().getName(), consolePane.getFont().getStyle(), originalFontSize))); + viewMenu.add(resetZoomButton); + + // create 'Options' + JMenu optionsMenu = new JMenu(GeyserLocale.getLocaleStringLog("geyser.gui.menu.options")); + viewMenu.setMnemonic(KeyEvent.VK_O); + menuBar.add(optionsMenu); + + JCheckBoxMenuItem debugMode = new JCheckBoxMenuItem(GeyserLocale.getLocaleStringLog("geyser.gui.menu.options.toggle_debug_mode")); + debugMode.setSelected(logger.isDebug()); + debugMode.addActionListener(e -> logger.setDebug(debugMode.getState())); + optionsMenu.add(debugMode); + + // Set the frames menu bar + frame.setJMenuBar(menuBar); + } + /** * Queue up an update to the text pane so we don't block the main thread * * @param text The text to append */ - private void updateTextPane(final String text) { + private void appendConsole(final String text) { SwingUtilities.invokeLater(() -> { consolePane.appendANSI(text); Document doc = consolePane.getDocument(); @@ -230,12 +249,12 @@ public class GeyserStandaloneGUI { OutputStream out = new OutputStream() { @Override public void write(final int b) { - updateTextPane(String.valueOf((char) b)); + appendConsole(String.valueOf((char) b)); } @Override public void write(byte[] b, int off, int len) { - updateTextPane(new String(b, off, len)); + appendConsole(new String(b, off, len)); } @Override @@ -251,50 +270,17 @@ public class GeyserStandaloneGUI { } /** - * Add all the Geyser commands to the commands menu, and setup the debug mode toggle + * Enable the command input box. * - * @param geyserStandaloneLogger The current logger - * @param geyserCommandManager The commands manager + * @param executor the executor for running commands off the GUI thread + * @param commandManager the command manager to delegate commands to */ - public void setupInterface(GeyserStandaloneLogger geyserStandaloneLogger, GeyserCommandManager geyserCommandManager) { - commandsMenu.removeAll(); - optionsMenu.removeAll(); - - for (Map.Entry entry : geyserCommandManager.getCommands().entrySet()) { - // Remove the offhand command and any alias commands to prevent duplicates in the list - if (!entry.getValue().isExecutableOnConsole() || entry.getValue().aliases().contains(entry.getKey())) { - continue; - } - - GeyserCommand command = (GeyserCommand) entry.getValue(); - // Create the button that runs the command - boolean hasSubCommands = !entry.getValue().subCommands().isEmpty(); - // Add an extra menu if there are more commands that can be run - JMenuItem commandButton = hasSubCommands ? new JMenu(entry.getValue().name()) : new JMenuItem(entry.getValue().name()); - commandButton.getAccessibleContext().setAccessibleDescription(entry.getValue().description()); - if (!hasSubCommands) { - commandButton.addActionListener(e -> command.execute(null, geyserStandaloneLogger, new String[]{ })); - } else { - // Add a submenu that's the same name as the menu can't be pressed - JMenuItem otherCommandButton = new JMenuItem(entry.getValue().name()); - otherCommandButton.getAccessibleContext().setAccessibleDescription(entry.getValue().description()); - otherCommandButton.addActionListener(e -> command.execute(null, geyserStandaloneLogger, new String[]{ })); - commandButton.add(otherCommandButton); - // Add a menu option for all possible subcommands - for (String subCommandName : entry.getValue().subCommands()) { - JMenuItem item = new JMenuItem(subCommandName); - item.addActionListener(e -> command.execute(null, geyserStandaloneLogger, new String[]{subCommandName})); - commandButton.add(item); - } - } - commandsMenu.add(commandButton); - } - - // 'Debug Mode' toggle - JCheckBoxMenuItem debugMode = new JCheckBoxMenuItem(GeyserLocale.getLocaleStringLog("geyser.gui.menu.options.toggle_debug_mode")); - debugMode.setSelected(geyserStandaloneLogger.isDebug()); - debugMode.addActionListener(e -> geyserStandaloneLogger.setDebug(!geyserStandaloneLogger.isDebug())); - optionsMenu.add(debugMode); + public void enableCommands(ScheduledExecutorService executor, GeyserCommandManager commandManager) { + // we don't want to block the GUI thread with the command execution + // todo: once cloud is used, an AsynchronousCommandExecutionCoordinator can be used to avoid this scheduler + commandListener.handler = cmd -> executor.schedule(() -> commandManager.runCommand(logger, cmd), 0, TimeUnit.SECONDS); + commandInput.setEnabled(true); + commandInput.requestFocusInWindow(); } /** @@ -322,14 +308,14 @@ public class GeyserStandaloneGUI { // Update ram graph final long freeMemory = Runtime.getRuntime().freeMemory(); final long totalMemory = Runtime.getRuntime().totalMemory(); - final int freePercent = (int)(freeMemory * 100.0 / totalMemory + 0.5); + final int freePercent = (int) (freeMemory * 100.0 / totalMemory + 0.5); ramValues.add(100 - freePercent); ramGraph.setXLabel(GeyserLocale.getLocaleStringLog("geyser.gui.graph.usage", String.format("%,d", (totalMemory - freeMemory) / MEGABYTE), freePercent)); // Trim the list int k = ramValues.size(); - if ( k > 10 ) + if (k > 10) ramValues.subList(0, k - 10).clear(); // Update the graph @@ -354,4 +340,17 @@ public class GeyserStandaloneGUI { splitPane.setDividerLocation(frame.getWidth() - 200); } } + + private class CommandListener implements ActionListener { + + private Consumer handler; + + @Override + public void actionPerformed(ActionEvent e) { + String command = commandInput.getText(); + appendConsole(command + "\n"); // show what was run in the console + handler.accept(command); // run the command + commandInput.setText(""); // clear the input + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index 5814348ed..d13836e9a 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -218,25 +218,9 @@ public class GeyserImpl implements GeyserApi { GeyserConfiguration config = bootstrap.getGeyserConfig(); - boolean isGui = false; - // This will check if we are in standalone and get the 'useGui' variable from there - if (platformType == PlatformType.STANDALONE) { - try { - Class cls = Class.forName("org.geysermc.geyser.platform.standalone.GeyserStandaloneBootstrap"); - isGui = (boolean) cls.getMethod("isUseGui").invoke(cls.cast(bootstrap)); - } catch (Exception e) { - logger.debug("Failed detecting if standalone is using a GUI; if this is a GeyserConnect instance this can be safely ignored."); - } - } - double completeTime = (System.currentTimeMillis() - startupTime) / 1000D; - String message = GeyserLocale.getLocaleStringLog("geyser.core.finish.done", new DecimalFormat("#.###").format(completeTime)) + " "; - if (isGui) { - message += GeyserLocale.getLocaleStringLog("geyser.core.finish.gui"); - } else { - message += GeyserLocale.getLocaleStringLog("geyser.core.finish.console"); - } - + String message = GeyserLocale.getLocaleStringLog("geyser.core.finish.done", new DecimalFormat("#.###").format(completeTime)); + message += " " + GeyserLocale.getLocaleStringLog("geyser.core.finish.console"); logger.info(message); if (platformType == PlatformType.STANDALONE) { diff --git a/core/src/main/resources/languages b/core/src/main/resources/languages index f6685c4cc..fda08c186 160000 --- a/core/src/main/resources/languages +++ b/core/src/main/resources/languages @@ -1 +1 @@ -Subproject commit f6685c4ccc6e77b07402d45cb41213559004b7d6 +Subproject commit fda08c186de979ff89b01d2b66f48daa5e218d01