/* * This file is part of FTB Launcher. * * Copyright © 2012-2016, FTB Launcher Contributors <https://github.com/Slowpoke101/FTBLaunch/> * FTB Launcher is licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.ftb.gui; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Font; import java.awt.Toolkit; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.StringSelection; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.text.SimpleDateFormat; import java.util.Date; import java.util.LinkedList; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextPane; import javax.swing.SwingUtilities; import javax.swing.text.BadLocationException; import javax.swing.text.DefaultStyledDocument; import javax.swing.text.Document; import javax.swing.text.SimpleAttributeSet; import javax.swing.text.StyleConstants; import net.ftb.data.Constants; import net.ftb.download.Locations; import net.ftb.locale.I18N; import net.ftb.log.ILogListener; import net.ftb.log.LogEntry; import net.ftb.log.LogLevel; import net.ftb.log.LogSource; import net.ftb.log.LogType; import net.ftb.log.Logger; import net.ftb.tools.PastebinPoster; import net.ftb.util.GameUtils; import net.ftb.util.OSUtils; @SuppressWarnings("serial") public class LauncherConsole extends JFrame implements ILogListener { private static final Font FONT = new Font("Monospaced", 0, 12); // Process at most LOG_CHUNK_SIZE log records at once so that console doesn't freeze for a long time // when lots of logs show up simultaneously private static final int LOG_CHUNK_SIZE = 25000; private final JTextPane displayArea; private final JComboBox logTypeComboBox; private LogType logType = LogType.MINIMAL; private final JComboBox logSourceComboBox; private LogSource logSource = LogSource.ALL; private LogLevel logLevel = LogLevel.INFO; private JButton killMCButton; private JButton threadDumpButton; private Document displayAreaDoc; private final AtomicBoolean queuedRecordsInProgress = new AtomicBoolean(); private final Queue<LogRecord> logRecords = new ConcurrentLinkedQueue<LogRecord>(); private SimpleAttributeSet RED = new SimpleAttributeSet(); private SimpleAttributeSet YELLOW = new SimpleAttributeSet(); public LauncherConsole () { setTitle(Constants.name + " " + I18N.getLocaleString("CONSOLE_TITLE")); setMinimumSize(new Dimension(800, 400)); setPreferredSize(new Dimension(800, 400)); setIconImage(Toolkit.getDefaultToolkit().getImage(this.getClass().getResource("/image/logo_ftb.png"))); getContentPane().setLayout(new BorderLayout(0, 0)); StyleConstants.setForeground(RED, Color.RED); StyleConstants.setForeground(YELLOW, Color.YELLOW); // setup buttons JPanel panel = new JPanel(); getContentPane().add(panel, BorderLayout.SOUTH); panel.setLayout(new FlowLayout(FlowLayout.LEFT, 5, 5)); JButton paste = new JButton(I18N.getLocaleString("CONSOLE_PASTE")); paste.addActionListener(new ActionListener() { @Override public void actionPerformed (ActionEvent arg0) { JOptionPane pane = new JOptionPane("The log will be sent to the FTB paste site and opened in your browser"); Object[] options = new String[] {"Yes do it", "Cancel"}; pane.setOptions(options); JDialog dialog = pane.createDialog(new JFrame(), I18N.getLocaleString("CONSOLE_PASTE")); dialog.setVisible(true); Object obj = pane.getValue(); int result = -1; for(int i = 0; i < options.length; i++) { if (options[i].equals(obj)) { result = i; } } if (result == 0) { PastebinPoster thread = new PastebinPoster(); thread.start(); } } }); panel.add(paste); JButton clipboard = new JButton(I18N.getLocaleString("CONSOLE_COPYCLIP")); clipboard.addActionListener(new ActionListener() { @Override public void actionPerformed (ActionEvent arg0) { JOptionPane pane = new JOptionPane(I18N.getLocaleString("CONSOLE_CLIP_CONFIRM")); Object[] options = new String[] {I18N.getLocaleString("MAIN_YES"), I18N.getLocaleString("MAIN_CANCEL")}; pane.setOptions(options); JDialog dialog = pane.createDialog(new JFrame(), I18N.getLocaleString("CONSOLE_COPYCLIP")); dialog.setVisible(true); Object obj = pane.getValue(); int result = -1; for(int i = 0; i < options.length; i++) { if (options[i].equals(obj)) { result = i; } } if (result == 0) { StringSelection stringSelection = new StringSelection("FTB Launcher logs:\n" + Logger.getLogs() + "[" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "]" + " Logs copied to clipboard"); Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); clipboard.setContents(stringSelection, null); } } }); panel.add(clipboard); logTypeComboBox = new JComboBox(LogType.values()); logTypeComboBox.setSelectedItem(logType); logTypeComboBox.addActionListener(new ActionListener() { public void actionPerformed (ActionEvent arg0) { logType = (LogType)logTypeComboBox.getSelectedItem(); // setup loglevel. If DEBUG selected show also DEBUG messages switch (logType) { case MINIMAL: logLevel = LogLevel.INFO; break; case EXTENDED: logLevel = LogLevel.INFO; break; case DEBUG: logLevel = LogLevel.DEBUG; break; } refreshLogs(); } }); panel.add(logTypeComboBox); logSourceComboBox = new JComboBox(LogSource.values()); logSourceComboBox.setSelectedItem(logSource); logSourceComboBox.addActionListener(new ActionListener() { public void actionPerformed (ActionEvent arg0) { logSource = (LogSource)logSourceComboBox.getSelectedItem(); refreshLogs(); } }); panel.add(logSourceComboBox); JButton ircButton = new JButton(I18N.getLocaleString("CONSOLE_SUPPORT")); ircButton.addActionListener(new ActionListener() { @Override public void actionPerformed (ActionEvent arg0) { OSUtils.browse(Locations.SUPPORTSITE); } }); panel.add(ircButton); killMCButton = new JButton(I18N.getLocaleString("KILL_MC")); killMCButton.setEnabled(false); killMCButton.setVisible(true); killMCButton.addActionListener(new ActionListener() { @Override public void actionPerformed (ActionEvent arg0) { GameUtils.killMC(); } }); panel.add(killMCButton); threadDumpButton = new JButton(I18N.getLocaleString("TD_MC")); threadDumpButton.setEnabled(false); threadDumpButton.setVisible(true); threadDumpButton.addActionListener(new ActionListener() { @Override public void actionPerformed (ActionEvent arg0) { GameUtils.threadDumpMC(); } }); panel.add(threadDumpButton); // setup log area displayArea = new JTextPane() { @Override public boolean getScrollableTracksViewportWidth () { return true; } }; displayArea.setFont(FONT); displayArea.setEditable(false); displayAreaDoc = this.displayArea.getDocument(); displayArea.setMargin(null); JScrollPane scrollPane = new JScrollPane(displayArea); scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); // Use third party library to implement autoscroll new SmartScroller(scrollPane); getContentPane().add(scrollPane); pack(); addWindowListener(new WindowAdapter() { @Override public void windowClosing (WindowEvent e) { Logger.removeListener(LaunchFrame.con); if (LaunchFrame.trayMenu != null) { LaunchFrame.trayMenu.updateShowConsole(false); } dispose(); } }); } synchronized public void refreshLogs () { // Write messages to new blank document which is not being displayed displayAreaDoc = new DefaultStyledDocument(); // Add all log entries to list and display them Queue<LogRecord> records = new LinkedList<LogRecord>(); for(LogEntry entry : Logger.getLogEntries()) { if (shouldProcess(entry)) { records.add(getLogRecord(entry)); } } displayMessages(records, -1); // Remove newline from start if (displayAreaDoc.getLength() != 0) { try { displayAreaDoc.remove(0, 1); } catch (BadLocationException ignored) { // ignore } } // Swap to displaying new document displayArea.setDocument(displayAreaDoc); } public void scrollToBottom () { displayArea.setCaretPosition(displayArea.getDocument().getLength()); } synchronized private void displayMessage (String message, SimpleAttributeSet attributes, Document d) { try { d.insertString(d.getLength(), message, attributes); } catch (Exception e) { Logger.logLoggingError(null, e); } } private synchronized void displayMessages (Queue<LogRecord> logRecords, int limit) { StringBuilder b = new StringBuilder(); SimpleAttributeSet lastAttributes = null; while (true) { LogRecord r = --limit == 0 ? null : logRecords.poll(); if (r == null || r.attributes != lastAttributes) { if (b.length() != 0) { displayMessage(b.toString(), lastAttributes, displayAreaDoc); b.setLength(0); } if (r == null) { if (limit == 0) { runLogQueue(); } return; } lastAttributes = r.attributes; } b.append('\n').append(r.message); } } private LogRecord getLogRecord (LogEntry entry) { SimpleAttributeSet color = null; switch (entry.level) { case ERROR: color = RED; break; case WARN: color = YELLOW; break; case INFO: case DEBUG: case UNKNOWN: default: break; } return new LogRecord(entry.toString(logType), color); } private static class LogRecord { public final String message; public final SimpleAttributeSet attributes; private LogRecord (String message, SimpleAttributeSet attributes) { this.message = message; this.attributes = attributes; } } public void minecraftStarted () { killMCButton.setEnabled(true); threadDumpButton.setEnabled(true); } public void minecraftStopped () { killMCButton.setEnabled(false); threadDumpButton.setEnabled(false); } private boolean shouldProcess (LogEntry entry) { return (logSource == LogSource.ALL || entry.source == logSource) && (logLevel == LogLevel.DEBUG || logLevel.includes(entry.level)); } @Override public void onLogEvent (final LogEntry entry) { if (!shouldProcess(entry)) { return;// drop unneeded messages as soon as possible } logRecords.add(getLogRecord(entry)); runLogQueue(); } private void runLogQueue () { if (!queuedRecordsInProgress.compareAndSet(false, true)) { return;// Already queued message display with invokeLater, no need to do it again } SwingUtilities.invokeLater(new Runnable() { public void run () { try { if (!queuedRecordsInProgress.compareAndSet(true, false)) { throw new IllegalStateException("Unexpected queuedRecords value: false"); } displayMessages(logRecords, LOG_CHUNK_SIZE); } catch (Throwable t) { Logger.logLoggingError(null, t); } } }); } }