From a7f2d99791c0ee874e0af584f2f5057110113b0e Mon Sep 17 00:00:00 2001 From: William Blake Galbreath Date: Thu, 16 Jan 2020 14:59:16 -0600 Subject: [PATCH] Make the GUI better --- .../net/minecraft/server/DedicatedServer.java | 6 +- .../net/minecraft/server/MinecraftServer.java | 3 +- .../java/net/pl3x/purpur/gui/ServerGUI.java | 123 ++++++++++++++++ .../purpur/gui/console/JColorTextPane.java | 81 ++++++++++ .../purpur/gui/console/JConsolePanel.java | 128 ++++++++++++++++ .../gui/info/DetailsListSelectionModel.java | 21 +++ .../net/pl3x/purpur/gui/info/JInfoPanel.java | 42 ++++++ .../net/pl3x/purpur/gui/info/RAMDetails.java | 54 +++++++ .../purpur/gui/info/graph/GraphColor.java | 44 ++++++ .../pl3x/purpur/gui/info/graph/GraphData.java | 47 ++++++ .../pl3x/purpur/gui/info/graph/RAMGraph.java | 138 ++++++++++++++++++ .../purpur/gui/playerlist/JPlayerList.java | 57 ++++++++ .../playerlist/PlayerListCellRenderer.java | 22 +++ .../gui/playerlist/PlayerListModel.java | 47 ++++++ .../playerlist/PlayerListMouseAdapter.java | 32 ++++ .../net/pl3x/purpur/gui/util/GUIColor.java | 54 +++++++ .../purpur/util/HighlightErrorConverter.java | 86 +++++++++++ src/main/resources/log4j2.xml | 13 +- 18 files changed, 992 insertions(+), 6 deletions(-) create mode 100644 src/main/java/net/pl3x/purpur/gui/ServerGUI.java create mode 100644 src/main/java/net/pl3x/purpur/gui/console/JColorTextPane.java create mode 100644 src/main/java/net/pl3x/purpur/gui/console/JConsolePanel.java create mode 100644 src/main/java/net/pl3x/purpur/gui/info/DetailsListSelectionModel.java create mode 100644 src/main/java/net/pl3x/purpur/gui/info/JInfoPanel.java create mode 100644 src/main/java/net/pl3x/purpur/gui/info/RAMDetails.java create mode 100644 src/main/java/net/pl3x/purpur/gui/info/graph/GraphColor.java create mode 100644 src/main/java/net/pl3x/purpur/gui/info/graph/GraphData.java create mode 100644 src/main/java/net/pl3x/purpur/gui/info/graph/RAMGraph.java create mode 100644 src/main/java/net/pl3x/purpur/gui/playerlist/JPlayerList.java create mode 100644 src/main/java/net/pl3x/purpur/gui/playerlist/PlayerListCellRenderer.java create mode 100644 src/main/java/net/pl3x/purpur/gui/playerlist/PlayerListModel.java create mode 100644 src/main/java/net/pl3x/purpur/gui/playerlist/PlayerListMouseAdapter.java create mode 100644 src/main/java/net/pl3x/purpur/gui/util/GUIColor.java create mode 100644 src/main/java/net/pl3x/purpur/util/HighlightErrorConverter.java diff --git a/src/main/java/net/minecraft/server/DedicatedServer.java b/src/main/java/net/minecraft/server/DedicatedServer.java index d70c8cab2..61ccb6079 100644 --- a/src/main/java/net/minecraft/server/DedicatedServer.java +++ b/src/main/java/net/minecraft/server/DedicatedServer.java @@ -51,7 +51,7 @@ public class DedicatedServer extends MinecraftServer implements IMinecraftServer public DedicatedServerSettings propertyManager; private EnumGamemode o; @Nullable - private ServerGUI p; + private net.pl3x.purpur.gui.ServerGUI p; // Purpur // CraftBukkit start - Signature changed public DedicatedServer(joptsimple.OptionSet options, DedicatedServerSettings dedicatedserversettings, DataFixer datafixer, YggdrasilAuthenticationService yggdrasilauthenticationservice, MinecraftSessionService minecraftsessionservice, GameProfileRepository gameprofilerepository, UserCache usercache, WorldLoadListenerFactory worldloadlistenerfactory, String s) { @@ -420,7 +420,7 @@ public class DedicatedServer extends MinecraftServer implements IMinecraftServer @Override public void exit() { if (this.p != null) { - this.p.b(); + this.p.close(); // Purpur } if (this.remoteControlListener != null) { @@ -519,7 +519,7 @@ public class DedicatedServer extends MinecraftServer implements IMinecraftServer public void bc() { if (this.p == null) { - this.p = ServerGUI.a(this); + this.p = net.pl3x.purpur.gui.ServerGUI.create(this); // Purpur } } diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java index ac45dbf59..cfe3157f0 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -105,7 +105,7 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant finalizers = Lists.newArrayList(); + private final AtomicBoolean isClosing = new AtomicBoolean(); + + private JConsolePanel consolePanel; + + public static ServerGUI create(final DedicatedServer dedicatedserver) { + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception ignore) { + } + + JFrame window = new JFrame("Purpur Minecraft Server"); + ServerGUI serverGUI = new ServerGUI(dedicatedserver); + + window.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + window.add(serverGUI); + window.pack(); + window.setLocationRelativeTo(null); + window.setVisible(true); + + window.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent windowevent) { + if (!serverGUI.isClosing.getAndSet(true)) { + window.setTitle("Purpur Minecraft Server - Shutting Down!"); + dedicatedserver.safeShutdown(true); + serverGUI.runFinalizers(); + } + + } + }); + + serverGUI.addFinalizer(window::dispose); + serverGUI.start(); + + return serverGUI; + } + + private ServerGUI(DedicatedServer dedicatedserver) { + this.server = dedicatedserver; + + setPreferredSize(new Dimension(854, 480)); + setLayout(new BorderLayout()); + + consolePanel = new JConsolePanel(server); + + try { + add(consolePanel, "Center"); + add(buildInfoPanel(), "West"); + } catch (Exception exception) { + LOGGER.error("Couldn't build server GUI", exception); + } + } + + private void addFinalizer(Runnable runnable) { + finalizers.add(runnable); + } + + private JComponent buildInfoPanel() { + JPanel jpanel = new JPanel(new BorderLayout()); + + JInfoPanel serverInfo = new JInfoPanel(server); + finalizers.add(serverInfo::stop); + + jpanel.add(serverInfo, "North"); + jpanel.add(buildPlayerPanel(), "Center"); + + jpanel.setBorder(new TitledBorder(new EtchedBorder(), "Stats")); + return jpanel; + } + + private JComponent buildPlayerPanel() { + JScrollPane jscrollpane = new JScrollPane(new JPlayerList(server), ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); + jscrollpane.setBorder(new TitledBorder(new EtchedBorder(), "Players")); + return jscrollpane; + } + + public void start() { + consolePanel.start(); + } + + public void close() { + if (!isClosing.getAndSet(true)) { + runFinalizers(); + } + + } + + private void runFinalizers() { + finalizers.forEach(Runnable::run); + } +} diff --git a/src/main/java/net/pl3x/purpur/gui/console/JColorTextPane.java b/src/main/java/net/pl3x/purpur/gui/console/JColorTextPane.java new file mode 100644 index 000000000..55feec811 --- /dev/null +++ b/src/main/java/net/pl3x/purpur/gui/console/JColorTextPane.java @@ -0,0 +1,81 @@ +package net.pl3x.purpur.gui.console; + +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.TextComponent; +import net.pl3x.purpur.gui.util.GUIColor; + +import javax.swing.JTextPane; +import javax.swing.Timer; +import javax.swing.text.AttributeSet; +import javax.swing.text.BadLocationException; +import javax.swing.text.SimpleAttributeSet; +import javax.swing.text.StyleConstants; +import javax.swing.text.StyleContext; +import java.util.HashSet; +import java.util.Set; + +public class JColorTextPane extends JTextPane { + private static final GUIColor DEFAULT_COLOR = GUIColor.BLACK; + + public void append(String str) { + BaseComponent[] components = TextComponent.fromLegacyText(DEFAULT_COLOR.getCode() + str, ChatColor.BLACK); + for (BaseComponent component : components) { + String text = component.toPlainText(); + if (text == null || text.isEmpty()) { + continue; + } + + GUIColor guiColor = GUIColor.getColor(component.getColor()); + + StyleContext context = StyleContext.getDefaultStyleContext(); + AttributeSet attr = context.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, guiColor.getColor()); + attr = context.addAttribute(attr, StyleConstants.CharacterConstants.Bold, component.isBold() || guiColor != DEFAULT_COLOR); + attr = context.addAttribute(attr, StyleConstants.CharacterConstants.Italic, component.isItalic()); + attr = context.addAttribute(attr, StyleConstants.CharacterConstants.Underline, component.isUnderlined()); + attr = context.addAttribute(attr, StyleConstants.CharacterConstants.StrikeThrough, component.isStrikethrough()); + //attr = context.addAttribute(attr, StyleConstants.CharacterConstants.Blink, component.isObfuscated()); // no such thing as Blink, sadly + + try { + int pos = getDocument().getLength(); + getDocument().insertString(pos, text, attr); + + if (component.isObfuscated()) { + // dirty hack to blink some text + Blink blink = new Blink(pos, text.length(), attr, context.addAttribute(attr, StyleConstants.Foreground, getBackground())); + BLINKS.add(blink); + } + } catch (BadLocationException e) { + e.printStackTrace(); + } + } + } + + private static final Set BLINKS = new HashSet<>(); + private static boolean SYNC_BLINK; + + static { + Timer timer = new Timer(500, e -> { + SYNC_BLINK = !SYNC_BLINK; + BLINKS.forEach(Blink::blink); + }); + timer.start(); + } + + public class Blink { + private final int start, length; + private final AttributeSet attr1; + private final AttributeSet attr2; + + private Blink(int start, int length, AttributeSet attr1, AttributeSet attr2) { + this.start = start; + this.length = length; + this.attr1 = attr1; + this.attr2 = attr2; + } + + private void blink() { + getStyledDocument().setCharacterAttributes(start, length, SYNC_BLINK ? attr1 : attr2, true); + } + } +} diff --git a/src/main/java/net/pl3x/purpur/gui/console/JConsolePanel.java b/src/main/java/net/pl3x/purpur/gui/console/JConsolePanel.java new file mode 100644 index 000000000..fb017a070 --- /dev/null +++ b/src/main/java/net/pl3x/purpur/gui/console/JConsolePanel.java @@ -0,0 +1,128 @@ +package net.pl3x.purpur.gui.console; + +import com.mojang.util.QueueLogAppender; +import net.minecraft.server.DedicatedServer; +import net.minecraft.server.DefaultUncaughtExceptionHandler; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.swing.AbstractAction; +import javax.swing.JPanel; +import javax.swing.JScrollBar; +import javax.swing.JScrollPane; +import javax.swing.JTextField; +import javax.swing.KeyStroke; +import javax.swing.ScrollPaneConstants; +import javax.swing.SwingUtilities; +import javax.swing.border.EtchedBorder; +import javax.swing.border.TitledBorder; +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.event.ActionEvent; +import java.util.LinkedList; + +public class JConsolePanel extends JPanel { + private static final Font MONOSPACED = new Font("Monospaced", Font.PLAIN, 12); + private static final Logger LOGGER = LogManager.getLogger(); + + private final CommandHistory history = new CommandHistory(); + private String currentCommand = ""; + private int historyIndex = 0; + + private Thread logAppenderThread; + + public JConsolePanel(DedicatedServer server) { + super(new BorderLayout()); + + JColorTextPane console = new JColorTextPane(); + console.setEditable(false); + console.setFont(MONOSPACED); + + JTextField jtextfield = new JTextField(); + jtextfield.addActionListener((actionevent) -> { + String msg = jtextfield.getText().trim(); + if (!msg.isEmpty()) { + server.issueCommand(msg, server.getServerCommandListener()); + history.add(msg); + historyIndex = -1; + } + jtextfield.setText(""); + }); + jtextfield.getInputMap().put(KeyStroke.getKeyStroke("UP"), "up"); + jtextfield.getInputMap().put(KeyStroke.getKeyStroke("DOWN"), "down"); + jtextfield.getActionMap().put("up", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent actionEvent) { + if (historyIndex < 0) { + currentCommand = jtextfield.getText(); + } + if (historyIndex < history.size() - 1) { + jtextfield.setText(history.get(++historyIndex)); + } + } + }); + jtextfield.getActionMap().put("down", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent actionEvent) { + if (historyIndex >= 0) { + if (historyIndex == 0) { + --historyIndex; + jtextfield.setText(currentCommand); + } else { + --historyIndex; + jtextfield.setText(history.get(historyIndex)); + } + } + } + }); + + JScrollPane jscrollpane = new JScrollPane(console, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); + + add(jscrollpane, "Center"); + add(jtextfield, "South"); + setBorder(new TitledBorder(new EtchedBorder(), "Console")); + + logAppenderThread = new Thread(() -> { + String msg; + while ((msg = QueueLogAppender.getNextLogEvent("ServerGuiConsole")) != null) { + this.print(console, jscrollpane, msg); + } + + }); + logAppenderThread.setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandler(LOGGER)); + logAppenderThread.setDaemon(true); + } + + public void start() { + logAppenderThread.start(); + } + + private void print(JColorTextPane console, JScrollPane jscrollpane, String str) { + if (!SwingUtilities.isEventDispatchThread()) { + SwingUtilities.invokeLater(() -> print(console, jscrollpane, str)); + } else { + JScrollBar jscrollbar = jscrollpane.getVerticalScrollBar(); + boolean scrollToBottom = false; + + if (jscrollpane.getViewport().getView() == console) { + scrollToBottom = (double) jscrollbar.getValue() + jscrollbar.getSize().getHeight() + (double) (MONOSPACED.getSize() * 4) > (double) jscrollbar.getMaximum(); + } + + console.append(str); + + if (scrollToBottom) { + jscrollbar.setValue(Integer.MAX_VALUE); + } + } + } + + public static class CommandHistory extends LinkedList { + @Override + public boolean add(String command) { + if (size() > 1000) { + remove(); + } + return super.offerFirst(command); + } + } +} diff --git a/src/main/java/net/pl3x/purpur/gui/info/DetailsListSelectionModel.java b/src/main/java/net/pl3x/purpur/gui/info/DetailsListSelectionModel.java new file mode 100644 index 000000000..4e0b80fdd --- /dev/null +++ b/src/main/java/net/pl3x/purpur/gui/info/DetailsListSelectionModel.java @@ -0,0 +1,21 @@ +package net.pl3x.purpur.gui.info; + +import javax.swing.DefaultListSelectionModel; + +public class DetailsListSelectionModel extends DefaultListSelectionModel { + @Override + public void setAnchorSelectionIndex(final int anchorIndex) { + } + + @Override + public void setLeadAnchorNotificationEnabled(final boolean flag) { + } + + @Override + public void setLeadSelectionIndex(final int leadIndex) { + } + + @Override + public void setSelectionInterval(final int index0, final int index1) { + } +} diff --git a/src/main/java/net/pl3x/purpur/gui/info/JInfoPanel.java b/src/main/java/net/pl3x/purpur/gui/info/JInfoPanel.java new file mode 100644 index 000000000..a4983863c --- /dev/null +++ b/src/main/java/net/pl3x/purpur/gui/info/JInfoPanel.java @@ -0,0 +1,42 @@ +package net.pl3x.purpur.gui.info; + +import net.minecraft.server.MinecraftServer; +import net.pl3x.purpur.gui.info.graph.RAMGraph; + +import javax.swing.JPanel; +import javax.swing.Timer; +import java.awt.BorderLayout; +import java.awt.Dimension; + +public class JInfoPanel extends JPanel { + private final Timer timer; + private final RAMGraph ramGraph; + + public JInfoPanel(MinecraftServer server) { + super(new BorderLayout()); + + setOpaque(false); + + ramGraph = new RAMGraph(); + RAMDetails ramDetails = new RAMDetails(); + + add(ramGraph, "North"); + add(ramDetails, "Center"); + + timer = new Timer(500, (event) -> { + ramGraph.update(); + ramDetails.update(); + }); + timer.start(); + } + + @Override + public Dimension getPreferredSize() { + return new Dimension(350, 200); + } + + public void stop() { + timer.stop(); + ramGraph.stop(); + } +} diff --git a/src/main/java/net/pl3x/purpur/gui/info/RAMDetails.java b/src/main/java/net/pl3x/purpur/gui/info/RAMDetails.java new file mode 100644 index 000000000..00f79e433 --- /dev/null +++ b/src/main/java/net/pl3x/purpur/gui/info/RAMDetails.java @@ -0,0 +1,54 @@ +package net.pl3x.purpur.gui.info; + +import net.minecraft.server.SystemUtils; +import net.pl3x.purpur.gui.info.graph.GraphData; +import net.pl3x.purpur.gui.info.graph.RAMGraph; +import org.bukkit.Bukkit; + +import javax.swing.DefaultListCellRenderer; +import javax.swing.JList; +import javax.swing.border.EmptyBorder; +import java.awt.Dimension; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Locale; +import java.util.Vector; + +public class RAMDetails extends JList { + private static final DecimalFormat DECIMAL_FORMAT = SystemUtils.a(new DecimalFormat("########0.000"), (format) + -> format.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.ROOT))); + + public RAMDetails() { + setBorder(new EmptyBorder(0, 10, 0, 0)); + setFixedCellHeight(20); + setOpaque(false); + + DefaultListCellRenderer renderer = new DefaultListCellRenderer(); + renderer.setOpaque(false); + setCellRenderer(renderer); + + setSelectionModel(new DetailsListSelectionModel()); + } + + @Override + public Dimension getPreferredSize() { + return new Dimension(350, 100); + } + + public void update() { + GraphData data = RAMGraph.DATA.peekLast(); + Vector vector = new Vector<>(); + vector.add("Memory use: " + (data.getUsedMem() / 1024L / 1024L) + " mb (" + (data.getFree() * 100L / data.getMax()) + "% free)"); + vector.add("Heap: " + (data.getTotal() / 1024L / 1024L) + " / " + (data.getMax() / 1024L / 1024L) + " mb"); + vector.add("Avg tick: " + DECIMAL_FORMAT.format(Bukkit.getAverageTickTime()) + " ms"); + setListData(vector); + } + + private double getAverage(long[] values) { + long total = 0L; + for (long value : values) { + total += value; + } + return (double) total / (double) values.length; + } +} diff --git a/src/main/java/net/pl3x/purpur/gui/info/graph/GraphColor.java b/src/main/java/net/pl3x/purpur/gui/info/graph/GraphColor.java new file mode 100644 index 000000000..2fdb9bdd0 --- /dev/null +++ b/src/main/java/net/pl3x/purpur/gui/info/graph/GraphColor.java @@ -0,0 +1,44 @@ +package net.pl3x.purpur.gui.info.graph; + +import java.awt.Color; + +public class GraphColor { + private static final Color[] colorLine = new Color[101]; + private static final Color[] colorFill = new Color[101]; + + static { + for (int i = 0; i < 101; i++) { + Color color = createColor(i); + colorLine[i] = new Color(color.getRed() / 2, color.getGreen() / 2, color.getBlue() / 2, 255); + colorFill[i] = new Color(colorLine[i].getRed(), colorLine[i].getGreen(), colorLine[i].getBlue(), 125); + } + } + + public static Color getLineColor(int percent) { + return colorLine[percent]; + } + + public static Color getFillColor(int percent) { + return colorFill[percent]; + } + + private static Color createColor(int percent) { + if (percent <= 50) { + return new Color(0X00FF00); + } + + int value = 510 - (int) (Math.min(Math.max(0, ((percent - 50) / 50F)), 1) * 510); + + int red, green; + if (value < 255) { + red = 255; + green = (int) (Math.sqrt(value) * 16); + } else { + green = 255; + value = value - 255; + red = 255 - (value * value / 255); + } + + return new Color(red, green, 0); + } +} diff --git a/src/main/java/net/pl3x/purpur/gui/info/graph/GraphData.java b/src/main/java/net/pl3x/purpur/gui/info/graph/GraphData.java new file mode 100644 index 000000000..85babad9c --- /dev/null +++ b/src/main/java/net/pl3x/purpur/gui/info/graph/GraphData.java @@ -0,0 +1,47 @@ +package net.pl3x.purpur.gui.info.graph; + +import java.awt.Color; + +public class GraphData { + private long total; + private long free; + private long max; + private long usedMem; + private int usedPercent; + + public GraphData(long total, long free, long max) { + this.total = total; + this.free = free; + this.max = max; + this.usedMem = total - free; + this.usedPercent = usedMem == 0 ? 0 : (int) (usedMem * 100L / max); + } + + public long getTotal() { + return total; + } + + public long getFree() { + return free; + } + + public long getMax() { + return max; + } + + public long getUsedMem() { + return usedMem; + } + + public int getUsedPercent() { + return usedPercent; + } + + public Color getFillColor() { + return GraphColor.getFillColor(usedPercent); + } + + public Color getLineColor() { + return GraphColor.getLineColor(usedPercent); + } +} diff --git a/src/main/java/net/pl3x/purpur/gui/info/graph/RAMGraph.java b/src/main/java/net/pl3x/purpur/gui/info/graph/RAMGraph.java new file mode 100644 index 000000000..f2f3dd2c1 --- /dev/null +++ b/src/main/java/net/pl3x/purpur/gui/info/graph/RAMGraph.java @@ -0,0 +1,138 @@ +package net.pl3x.purpur.gui.info.graph; + +import javax.swing.JComponent; +import javax.swing.SwingUtilities; +import javax.swing.Timer; +import javax.swing.ToolTipManager; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.MouseInfo; +import java.awt.Point; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.LinkedList; +import java.util.concurrent.TimeUnit; + +public class RAMGraph extends JComponent { + public static final LinkedList DATA = new LinkedList() { + @Override + public boolean add(GraphData data) { + if (size() >= 348) { + remove(); + } + return super.add(data); + } + }; + + static { + GraphData empty = new GraphData(0, 0, 0); + for (int i = 0; i < 350; i++) { + DATA.add(empty); + } + } + + private final Timer timer; + private final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss"); + + private int currentTick; + + public RAMGraph() { + ToolTipManager.sharedInstance().setInitialDelay(0); + + addMouseListener(new MouseAdapter() { + final int defaultDismissTimeout = ToolTipManager.sharedInstance().getDismissDelay(); + final int dismissDelayMinutes = (int) TimeUnit.MINUTES.toMillis(10); + + @Override + public void mouseEntered(MouseEvent me) { + ToolTipManager.sharedInstance().setDismissDelay(dismissDelayMinutes); + } + + @Override + public void mouseExited(MouseEvent me) { + ToolTipManager.sharedInstance().setDismissDelay(defaultDismissTimeout); + } + }); + + timer = new Timer(50, (event) -> repaint()); + timer.start(); + } + + @Override + public Dimension getPreferredSize() { + return new Dimension(350, 110); + } + + public void update() { + Runtime jvm = Runtime.getRuntime(); + DATA.add(new GraphData(jvm.totalMemory(), jvm.freeMemory(), jvm.maxMemory())); + + Point scr = MouseInfo.getPointerInfo().getLocation(); + Point loc = new Point(scr); + SwingUtilities.convertPointFromScreen(loc, this); + if (this.contains(loc)) { + ToolTipManager.sharedInstance().mouseMoved( + new MouseEvent(this, -1, System.currentTimeMillis(), 0, loc.x, loc.y, + scr.x, scr.y, 0, false, 0)); + } + + currentTick++; + } + + @Override + public void paint(Graphics graphics) { + graphics.setColor(new Color(0xFFFFFFFF)); + graphics.fillRect(0, 0, 350, 100); + + graphics.setColor(new Color(0x888888)); + graphics.drawLine(1, 25, 348, 25); + graphics.drawLine(1, 50, 348, 50); + graphics.drawLine(1, 75, 348, 75); + + int i = 0; + for (GraphData data : DATA) { + i++; + if ((i + currentTick) % 120 == 0) { + graphics.setColor(new Color(0x888888)); + graphics.drawLine(i, 1, i, 99); + } + int used = data.getUsedPercent(); + if (used > 0) { + Color color = data.getLineColor(); + graphics.setColor(data.getFillColor()); + graphics.fillRect(i, 100 - used, 1, used); + graphics.setColor(color); + graphics.fillRect(i, 100 - used, 1, 1); + } + } + + graphics.setColor(new Color(0xFF000000)); + graphics.drawRect(0, 0, 348, 100); + + Point m = getMousePosition(); + if (m != null && m.x > 0 && m.x < 348 && m.y > 0 && m.y < 100) { + GraphData data = DATA.get(m.x); + int used = data.getUsedPercent(); + graphics.setColor(new Color(0x000000)); + graphics.drawLine(m.x, 1, m.x, 99); + graphics.drawOval(m.x - 2, 100 - used - 2, 5, 5); + graphics.setColor(data.getLineColor()); + graphics.fillOval(m.x - 2, 100 - used - 2, 5, 5); + setToolTipText(String.format("Used: %s mb (%s%%)
%s", + Math.round(data.getUsedMem() / 1024F / 1024F), + used, getTime(m.x))); + } + } + + public String getTime(int halfSeconds) { + int millis = (348 - halfSeconds) / 2 * 1000; + return TIME_FORMAT.format(new Date((System.currentTimeMillis() - millis))); + } + + public void stop() { + timer.stop(); + } +} diff --git a/src/main/java/net/pl3x/purpur/gui/playerlist/JPlayerList.java b/src/main/java/net/pl3x/purpur/gui/playerlist/JPlayerList.java new file mode 100644 index 000000000..17e75a207 --- /dev/null +++ b/src/main/java/net/pl3x/purpur/gui/playerlist/JPlayerList.java @@ -0,0 +1,57 @@ +package net.pl3x.purpur.gui.playerlist; + +import net.minecraft.server.EntityPlayer; +import net.minecraft.server.MinecraftServer; + +import javax.swing.JList; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; +import javax.swing.ListSelectionModel; +import java.awt.Dimension; + +public class JPlayerList extends JList { + private final MinecraftServer server; + private final PlayerListModel model; + private final JPopupMenu popupMenu; + private int currentSelection = -1; + private int tickCount; + + public JPlayerList(MinecraftServer server) { + this.server = server; + server.addTickable(this::tick); + + setModel(model = new PlayerListModel(this)); + setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + addListSelectionListener(event -> currentSelection = event.getFirstIndex()); + addMouseListener(new PlayerListMouseAdapter(this)); + setCellRenderer(new PlayerListCellRenderer()); + + popupMenu = new JPopupMenu(); + popupMenu.add(new JMenuItem("Details")); + popupMenu.add(new JPopupMenu.Separator()); + popupMenu.add(new JMenuItem("Kick")); + popupMenu.add(new JMenuItem("Ban")); + popupMenu.add(new JPopupMenu.Separator()); + popupMenu.add(new JMenuItem("Op")); + } + + @Override + public Dimension getPreferredSize() { + Dimension superPref = super.getPreferredSize(); + return new Dimension(330, superPref.height); + } + + public void tick() { + if (tickCount++ % 20 == 0) { + model.update(server.getPlayerList().getPlayers()); + } + } + + public int getCurrentSelection() { + return currentSelection; + } + + public JPopupMenu getPopupMenu() { + return popupMenu; + } +} diff --git a/src/main/java/net/pl3x/purpur/gui/playerlist/PlayerListCellRenderer.java b/src/main/java/net/pl3x/purpur/gui/playerlist/PlayerListCellRenderer.java new file mode 100644 index 000000000..2f02e9adc --- /dev/null +++ b/src/main/java/net/pl3x/purpur/gui/playerlist/PlayerListCellRenderer.java @@ -0,0 +1,22 @@ +package net.pl3x.purpur.gui.playerlist; + +import com.mojang.authlib.GameProfile; +import net.minecraft.server.EntityPlayer; + +import javax.swing.DefaultListCellRenderer; +import javax.swing.JList; +import java.awt.Component; + +public class PlayerListCellRenderer extends DefaultListCellRenderer { + @Override + public Component getListCellRendererComponent(JList list, Object player, int index, boolean isSelected, boolean cellHasFocus) { + super.getListCellRendererComponent(list, player, index, isSelected, cellHasFocus); + if (player instanceof EntityPlayer) { + GameProfile profile = ((EntityPlayer) player).getProfile(); + setText(profile.getName()); + setToolTipText(String.format("%s
%s", + profile.getName(), profile.getId())); + } + return this; + } +} diff --git a/src/main/java/net/pl3x/purpur/gui/playerlist/PlayerListModel.java b/src/main/java/net/pl3x/purpur/gui/playerlist/PlayerListModel.java new file mode 100644 index 000000000..edbf45416 --- /dev/null +++ b/src/main/java/net/pl3x/purpur/gui/playerlist/PlayerListModel.java @@ -0,0 +1,47 @@ +package net.pl3x.purpur.gui.playerlist; + +import net.minecraft.server.EntityPlayer; + +import javax.swing.DefaultListModel; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +public class PlayerListModel extends DefaultListModel { + private final Set datas = new HashSet<>(); + private final JPlayerList parent; + + public PlayerListModel(JPlayerList parent) { + this.parent = parent; + } + + public void update(Collection players) { + boolean hadFocus = parent.hasFocus(); + + players.forEach(this::add); + + Set result = new HashSet<>(datas); + result.removeIf(players::contains); + result.forEach(this::remove); + + if (parent.getCurrentSelection() >= 0) { + parent.setSelectedIndex(parent.getCurrentSelection()); + } + + if (hadFocus) { + parent.grabFocus(); + } + } + + public void add(EntityPlayer player) { + if (datas.add(player)) { + addElement(player); + } + } + + public void remove(EntityPlayer player) { + if (datas.remove(player)) { + removeElement(player); + } + } +} diff --git a/src/main/java/net/pl3x/purpur/gui/playerlist/PlayerListMouseAdapter.java b/src/main/java/net/pl3x/purpur/gui/playerlist/PlayerListMouseAdapter.java new file mode 100644 index 000000000..13a5eb3ad --- /dev/null +++ b/src/main/java/net/pl3x/purpur/gui/playerlist/PlayerListMouseAdapter.java @@ -0,0 +1,32 @@ +package net.pl3x.purpur.gui.playerlist; + +import javax.swing.SwingUtilities; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +public class PlayerListMouseAdapter extends MouseAdapter { + private final JPlayerList parent; + + public PlayerListMouseAdapter(JPlayerList parent) { + this.parent = parent; + } + + @Override + public void mousePressed(MouseEvent event) { + if (!SwingUtilities.isRightMouseButton(event)) { + return; + } + + parent.setSelectedIndex(parent.locationToIndex(event.getPoint())); + + if (parent.isSelectionEmpty()) { + return; + } + + if (parent.locationToIndex(event.getPoint()) != parent.getSelectedIndex()) { + return; + } + + parent.getPopupMenu().show(parent, event.getX(), event.getY()); + } +} diff --git a/src/main/java/net/pl3x/purpur/gui/util/GUIColor.java b/src/main/java/net/pl3x/purpur/gui/util/GUIColor.java new file mode 100644 index 000000000..973c8ddf9 --- /dev/null +++ b/src/main/java/net/pl3x/purpur/gui/util/GUIColor.java @@ -0,0 +1,54 @@ +package net.pl3x.purpur.gui.util; + +import net.md_5.bungee.api.ChatColor; + +import java.awt.Color; +import java.util.HashMap; +import java.util.Map; + +public enum GUIColor { + BLACK(ChatColor.BLACK, new Color(0x000000)), + DARK_BLUE(ChatColor.DARK_BLUE, new Color(0x0000AA)), + DARK_GREEN(ChatColor.DARK_GREEN, new Color(0x00AA00)), + DARK_AQUA(ChatColor.DARK_AQUA, new Color(0x009999)), + DARK_RED(ChatColor.DARK_RED, new Color(0xAA0000)), + DARK_PURPLE(ChatColor.DARK_PURPLE, new Color(0xAA00AA)), + GOLD(ChatColor.GOLD, new Color(0xBB8800)), + GRAY(ChatColor.GRAY, new Color(0x888888)), + DARK_GRAY(ChatColor.DARK_GRAY, new Color(0x444444)), + BLUE(ChatColor.BLUE, new Color(0x5555FF)), + GREEN(ChatColor.GREEN, new Color(0x55FF55)), + AQUA(ChatColor.AQUA, new Color(0x55DDDD)), + RED(ChatColor.RED, new Color(0xFF5555)), + LIGHT_PURPLE(ChatColor.LIGHT_PURPLE, new Color(0xFF55FF)), + YELLOW(ChatColor.YELLOW, new Color(0xFFBB00)), + WHITE(ChatColor.WHITE, new Color(0xBBBBBB)); + + private final ChatColor chat; + private final Color color; + + private static final Map BY_CHAT = new HashMap<>(); + + GUIColor(ChatColor chat, Color color) { + this.chat = chat; + this.color = color; + } + + public Color getColor() { + return color; + } + + public String getCode() { + return chat.toString(); + } + + public static GUIColor getColor(ChatColor chat) { + return BY_CHAT.get(chat); + } + + static { + for (GUIColor color : values()) { + BY_CHAT.put(color.chat, color); + } + } +} diff --git a/src/main/java/net/pl3x/purpur/util/HighlightErrorConverter.java b/src/main/java/net/pl3x/purpur/util/HighlightErrorConverter.java new file mode 100644 index 000000000..4b340b88a --- /dev/null +++ b/src/main/java/net/pl3x/purpur/util/HighlightErrorConverter.java @@ -0,0 +1,86 @@ +package net.pl3x.purpur.util; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.layout.PatternLayout; +import org.apache.logging.log4j.core.pattern.ConverterKeys; +import org.apache.logging.log4j.core.pattern.LogEventPatternConverter; +import org.apache.logging.log4j.core.pattern.PatternConverter; +import org.apache.logging.log4j.core.pattern.PatternFormatter; +import org.apache.logging.log4j.core.pattern.PatternParser; +import org.apache.logging.log4j.util.PerformanceSensitive; + +import java.util.List; + +@Plugin(name = "highlightGUIError", category = PatternConverter.CATEGORY) +@ConverterKeys({"highlightGUIError"}) +@PerformanceSensitive("allocation") +public final class HighlightErrorConverter extends LogEventPatternConverter { + + private static final String ERROR = "\u00A74\u00A7l"; // Bold Red + private static final String WARN = "\u00A7e\u00A7l"; // Bold Yellow + + private final List formatters; + + protected HighlightErrorConverter(List formatters) { + super("highlightGUIError", null); + this.formatters = formatters; + } + + @Override + public void format(LogEvent event, StringBuilder toAppendTo) { + Level level = event.getLevel(); + if (level.isMoreSpecificThan(Level.ERROR)) { + format(ERROR, event, toAppendTo); + return; + } else if (level.isMoreSpecificThan(Level.WARN)) { + format(WARN, event, toAppendTo); + return; + } + for (PatternFormatter formatter : formatters) { + formatter.format(event, toAppendTo); + } + } + + private void format(String style, LogEvent event, StringBuilder toAppendTo) { + int start = toAppendTo.length(); + toAppendTo.append(style); + int end = toAppendTo.length(); + + for (PatternFormatter formatter : formatters) { + formatter.format(event, toAppendTo); + } + + if (toAppendTo.length() == end) { + toAppendTo.setLength(start); + } + } + + @Override + public boolean handlesThrowable() { + for (final PatternFormatter formatter : formatters) { + if (formatter.handlesThrowable()) { + return true; + } + } + return false; + } + + public static HighlightErrorConverter newInstance(Configuration config, String[] options) { + if (options.length != 1) { + LOGGER.error("Incorrect number of options on highlightGUIError. Expected 1 received " + options.length); + return null; + } + if (options[0] == null) { + LOGGER.error("No pattern supplied on highlightGUIError"); + return null; + } + + PatternParser parser = PatternLayout.createPatternParser(config); + List formatters = parser.parse(options[0]); + return new HighlightErrorConverter(formatters); + } + +} diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml index a9bb98765..4be7613d0 100644 --- a/src/main/resources/log4j2.xml +++ b/src/main/resources/log4j2.xml @@ -1,8 +1,17 @@ - + - + + + + + + + + + -- 2.24.0