Geyser/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/GeyserStandaloneGUI.java

358 lines
15 KiB
Java

/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.platform.standalone.gui;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.GeyserLogger;
import org.geysermc.geyser.command.GeyserCommandManager;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.GeyserLocale;
import javax.swing.*;
import javax.swing.table.DefaultTableModel;
import javax.swing.text.Document;
import java.awt.*;
import java.awt.event.*;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
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 long MEGABYTE = 1024L * 1024L;
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<Integer> ramValues = new ArrayList<>();
private final DefaultTableModel playerTableModel = new DefaultTableModel();
/**
* 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;
// Create the frame and setup basic settings
JFrame frame = new JFrame(GeyserLocale.getLocaleStringLog("geyser.gui.title"));
frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
frame.setSize(800, 400);
frame.setMinimumSize(frame.getSize());
// Remove Java UI look
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception ignored) { }
// Show a confirm dialog on close
frame.addWindowListener(new WindowAdapter() {
@Override
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) {
System.exit(0);
}
}
});
Container cp = frame.getContentPane();
// Fetch and set the icon for the frame
URL image = getClass().getClassLoader().getResource("icon.png");
if (image != null) {
ImageIcon icon = new ImageIcon(image);
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.addComponentListener(new ComponentAdapter() {
public void componentResized(ComponentEvent e) {
splitPaneLimit((JSplitPane) e.getSource());
}
});
cp.add(splitPane, BorderLayout.CENTER);
// 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);
leftPane.add(consoleScrollPane, BorderLayout.CENTER);
// 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));
splitPane.setRightComponent(rightPane);
JPanel rightContentPane = new JPanel();
rightContentPane.setLayout(new GridLayout(2, 1, 5, 5));
rightPane.add(rightContentPane);
// Set the ram graph to 0
for (int i = 0; i < 10; i++) {
ramValues.add(0);
}
ramGraph.setValues(ramValues);
ramGraph.setXLabel(GeyserLocale.getLocaleStringLog("geyser.gui.graph.loading"));
rightContentPane.add(ramGraph);
playerTableModel.addColumn(GeyserLocale.getLocaleStringLog("geyser.gui.table.ip"));
playerTableModel.addColumn(GeyserLocale.getLocaleStringLog("geyser.gui.table.username"));
JTable playerTable = new JTable(playerTableModel);
JScrollPane playerScrollPane = new JScrollPane(playerTable);
rightContentPane.add(playerScrollPane);
// This has to be done last
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 appendConsole(final String text) {
SwingUtilities.invokeLater(() -> {
consolePane.appendANSI(text);
Document doc = consolePane.getDocument();
consolePane.setCaretPosition(doc.getLength());
});
}
/**
* Redirect the default io streams to the text pane
*/
public void redirectSystemStreams() {
// Setup a new output stream to forward it to the text pane
OutputStream out = new OutputStream() {
@Override
public void write(final int b) {
appendConsole(String.valueOf((char) b));
}
@Override
public void write(byte @NonNull [] b, int off, int len) {
appendConsole(new String(b, off, len));
}
@Override
public void write(byte @NonNull[] b) {
write(b, 0, b.length);
}
};
// Override the system output streams
System.setOut(new PrintStream(out, true));
System.setErr(new PrintStream(out, true));
}
/**
* Enable the command input box.
*
* @param executor the executor for running commands off the GUI thread
* @param commandManager the command manager to delegate commands to
*/
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();
}
/**
* Start the thread to update the form information every 1s
*/
public void startUpdateThread() {
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
Runnable periodicTask = () -> {
if (GeyserImpl.getInstance() != null) {
// Update player table
playerTableModel.getDataVector().removeAllElements();
for (GeyserSession player : GeyserImpl.getInstance().getSessionManager().getSessions().values()) {
Vector<String> row = new Vector<>();
row.add(player.getSocketAddress().getHostName());
row.add(player.getPlayerEntity().getUsername());
playerTableModel.addRow(row);
}
playerTableModel.fireTableDataChanged();
}
// 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);
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)
ramValues.subList(0, k - 10).clear();
// Update the graph
ramGraph.setValues(ramValues);
};
// SwingUtilities.invokeLater is called so we don't run into threading issues with the GUI
executor.scheduleAtFixedRate(() -> SwingUtilities.invokeLater(periodicTask), 0, 1, TimeUnit.SECONDS);
}
/**
* Make sure the JSplitPane divider is within a set of bounds
*
* @param splitPane The JSplitPane to check
*/
private void splitPaneLimit(JSplitPane splitPane) {
JRootPane frame = splitPane.getRootPane();
int location = splitPane.getDividerLocation();
if (location < frame.getWidth() - frame.getWidth() * 0.4f) {
splitPane.setDividerLocation(Math.round(frame.getWidth() - frame.getWidth() * 0.4f));
} else if (location > frame.getWidth() - 200) {
splitPane.setDividerLocation(frame.getWidth() - 200);
}
}
private class CommandListener implements ActionListener {
private Consumer<String> 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
}
}
}