From 2bcbaf7a680c896aef9e5702bf279bd00c7e373e Mon Sep 17 00:00:00 2001 From: Martino Facchin Date: Mon, 9 Jan 2017 12:06:37 +0100 Subject: [PATCH] Add clickable HTML view of Serial Monitor The HTML view only activates if: - the output is steady - the "frame" contains a link - the length of the entire content is < 1KB No performance penalty compared to normal view (in standard conditions) --- .../processing/app/AbstractTextMonitor.java | 100 +++++++++++ app/src/processing/app/HTMLTextAreaFIFO.java | 160 ++++++++++++++++++ app/src/processing/app/TextAreaFIFO.java | 4 + 3 files changed, 264 insertions(+) create mode 100644 app/src/processing/app/HTMLTextAreaFIFO.java diff --git a/app/src/processing/app/AbstractTextMonitor.java b/app/src/processing/app/AbstractTextMonitor.java index 00eabb206..ffc244a82 100644 --- a/app/src/processing/app/AbstractTextMonitor.java +++ b/app/src/processing/app/AbstractTextMonitor.java @@ -32,6 +32,9 @@ import javax.swing.SwingUtilities; import javax.swing.border.EmptyBorder; import javax.swing.text.DefaultCaret; import javax.swing.text.DefaultEditorKit; +import javax.swing.event.UndoableEditListener; +import javax.swing.text.AbstractDocument; +import javax.swing.text.Document; import cc.arduino.packages.BoardPort; @@ -40,7 +43,9 @@ public abstract class AbstractTextMonitor extends AbstractMonitor { protected JLabel noLineEndingAlert; protected TextAreaFIFO textArea; + protected HTMLTextAreaFIFO htmlTextArea; protected JScrollPane scrollPane; + protected JScrollPane htmlScrollPane; protected JTextField textField; protected JButton sendButton; protected JButton clearButton; @@ -48,6 +53,10 @@ public abstract class AbstractTextMonitor extends AbstractMonitor { protected JCheckBox addTimeStampBox; protected JComboBox lineEndings; protected JComboBox serialRates; + protected Container mainPane; + private long lastMessage; + private javax.swing.Timer updateTimer; + private boolean htmlView = true; public AbstractTextMonitor(BoardPort boardPort) { super(boardPort); @@ -69,6 +78,7 @@ public abstract class AbstractTextMonitor extends AbstractMonitor { @Override protected void onCreateWindow(Container mainPane) { + this.mainPane = mainPane; mainPane.setLayout(new BorderLayout()); textArea = new TextAreaFIFO(8_000_000); @@ -76,14 +86,89 @@ public abstract class AbstractTextMonitor extends AbstractMonitor { textArea.setColumns(40); textArea.setEditable(false); + htmlTextArea = new HTMLTextAreaFIFO(8000000); + htmlTextArea.setEditable(false); + htmlTextArea.setOpaque(false); + // don't automatically update the caret. that way we can manually decide // whether or not to do so based on the autoscroll checkbox. ((DefaultCaret) textArea.getCaret()).setUpdatePolicy(DefaultCaret.NEVER_UPDATE); + ((DefaultCaret) htmlTextArea.getCaret()).setUpdatePolicy(DefaultCaret.NEVER_UPDATE); + + Document doc = textArea.getDocument(); + if (doc instanceof AbstractDocument) + { + UndoableEditListener[] undoListeners = + ( (AbstractDocument) doc).getUndoableEditListeners(); + if (undoListeners.length > 0) + { + for (UndoableEditListener undoListener : undoListeners) + { + doc.removeUndoableEditListener(undoListener); + } + } + } + + doc = htmlTextArea.getDocument(); + if (doc instanceof AbstractDocument) + { + UndoableEditListener[] undoListeners = + ( (AbstractDocument) doc).getUndoableEditListeners(); + if (undoListeners.length > 0) + { + for (UndoableEditListener undoListener : undoListeners) + { + doc.removeUndoableEditListener(undoListener); + } + } + } scrollPane = new JScrollPane(textArea); + scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); + htmlScrollPane = new JScrollPane(htmlTextArea); + htmlScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); + + ActionListener checkIfSteady = new ActionListener() { + public void actionPerformed(ActionEvent evt) { + if (System.currentTimeMillis() - lastMessage > 200) { + if (htmlView == false && textArea.getLength() < 1000) { + + htmlTextArea.setText(""); + boolean res = htmlTextArea.append(textArea.getText()); + if (res) { + htmlView = true; + mainPane.remove(scrollPane); + if (textArea.getCaretPosition() > htmlTextArea.getDocument().getLength()) { + htmlTextArea.setCaretPosition(htmlTextArea.getDocument().getLength()); + } else { + htmlTextArea.setCaretPosition(textArea.getCaretPosition()); + } + mainPane.add(htmlScrollPane, BorderLayout.CENTER); + scrollPane.setVisible(false); + mainPane.validate(); + mainPane.repaint(); + } + } + } else { + if (htmlView == true) { + htmlView = false; + mainPane.remove(htmlScrollPane); + mainPane.add(scrollPane, BorderLayout.CENTER); + scrollPane.setVisible(true); + mainPane.validate(); + mainPane.repaint(); + } + } + } + }; + + updateTimer = new javax.swing.Timer(33, checkIfSteady); mainPane.add(scrollPane, BorderLayout.CENTER); + htmlTextArea.setVisible(true); + htmlScrollPane.setVisible(true); + JPanel upperPane = new JPanel(); upperPane.setLayout(new BoxLayout(upperPane, BoxLayout.X_AXIS)); upperPane.setBorder(new EmptyBorder(4, 4, 4, 4)); @@ -168,6 +253,8 @@ public abstract class AbstractTextMonitor extends AbstractMonitor { applyPreferences(); mainPane.add(pane, BorderLayout.SOUTH); + + updateTimer.start(); } @Override @@ -190,9 +277,21 @@ public abstract class AbstractTextMonitor extends AbstractMonitor { textArea.setBackground(new Color(238, 238, 238)); } textArea.invalidate(); + + clearButton.setEnabled(enable); + htmlTextArea.setEnabled(enable); scrollPane.setEnabled(enable); + htmlScrollPane.setEnabled(enable); textField.setEnabled(enable); sendButton.setEnabled(enable); + + autoscrollBox.setEnabled(enable); + addTimeStampBox.setEnabled(enable); + lineEndings.setEnabled(enable); + serialRates.setEnabled(enable); + if (enable == false) { + htmlTextArea.setText(""); + } } public void onSendCommand(ActionListener listener) { @@ -210,6 +309,7 @@ public abstract class AbstractTextMonitor extends AbstractMonitor { @Override public void message(String msg) { + lastMessage = System.currentTimeMillis(); SwingUtilities.invokeLater(() -> updateTextArea(msg)); } diff --git a/app/src/processing/app/HTMLTextAreaFIFO.java b/app/src/processing/app/HTMLTextAreaFIFO.java new file mode 100644 index 000000000..0aa3aabfe --- /dev/null +++ b/app/src/processing/app/HTMLTextAreaFIFO.java @@ -0,0 +1,160 @@ +/* + Copyright (c) 2014 Paul Stoffregen + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + +// adapted from https://community.oracle.com/thread/1479784 + +package processing.app; + +import java.io.IOException; +import java.net.URL; +import java.awt.Desktop; +import java.net.URLEncoder; + +import java.util.*; +import java.util.regex.*; + +import javax.swing.text.html.HTMLDocument; +import javax.swing.JEditorPane; +import javax.swing.JTextPane; +import javax.swing.SwingUtilities; +import javax.swing.event.HyperlinkEvent; +import javax.swing.event.HyperlinkListener; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.BadLocationException; +import javax.swing.text.html.HTMLEditorKit; + +import cc.arduino.UpdatableBoardsLibsFakeURLsHandler; + +public class HTMLTextAreaFIFO extends JTextPane implements DocumentListener { + private int maxChars; + private int trimMaxChars; + + private int updateCount; // limit how often we trim the document + + private boolean doTrim; + private final HTMLEditorKit kit; + + public HTMLTextAreaFIFO(int max) { + maxChars = max; + trimMaxChars = max / 2; + updateCount = 0; + doTrim = true; + setContentType("text/html"); + getDocument().addDocumentListener(this); + setText(""); + kit = new HTMLEditorKit(); + this.addHyperlinkListener(new UpdatableBoardsLibsFakeURLsHandler(Base.INSTANCE)); + } + + public void insertUpdate(DocumentEvent e) { + } + + public void removeUpdate(DocumentEvent e) { + } + + public void changedUpdate(DocumentEvent e) { + } + + public void trimDocument() { + int len = 0; + len = getDocument().getLength(); + if (len > trimMaxChars) { + int n = len - trimMaxChars; + //System.out.println("trimDocument: remove " + n + " chars"); + try { + getDocument().remove(0, n); + } catch (BadLocationException ble) { + } + } + } + + private static List extractUrls(String input) { + List result = new ArrayList(); + + Pattern pattern = Pattern.compile( + "(http|ftp|https)://([^\\s]+)"); + + Matcher matcher = pattern.matcher(input); + while (matcher.find()) { + result.add(matcher.group()); + } + + return result; + } + + static public final String WITH_DELIMITER = "((?<=%1$s)|(?=%1$s))"; + + public boolean append(String s) { + boolean htmlFound = false; + try { + HTMLDocument doc = (HTMLDocument) getDocument(); + + String strings[] = s.split(String.format(WITH_DELIMITER, "\\r?\\n")); + + for (int l = 0; l < strings.length; l++) { + String str = strings[l]; + List urls = extractUrls(str); + + if (urls.size() > 0) { + + for (int i = 0; i < urls.size(); i++) { + if (!((urls.get(i)).contains(""))) { + str = str.replace(urls.get(i), "" + urls.get(i) + ""); + } + } + + kit.insertHTML(doc, doc.getLength(), str, 0, 0, null); + htmlFound = true; + } else { + doc.insertString(doc.getLength(), str, null); + } + } + } catch(BadLocationException exc) { + exc.printStackTrace(); + } catch(IOException exc) { + exc.printStackTrace(); + } + + if (++updateCount > 150 && doTrim) { + updateCount = 0; + SwingUtilities.invokeLater(new Runnable() { + public void run() { + trimDocument(); + } + }); + } + return htmlFound; + } + + public void appendNoTrim(String s) { + int free = maxChars - getDocument().getLength(); + if (free <= 0) + return; + if (s.length() > free) + append(s.substring(0, free)); + else + append(s); + doTrim = false; + } + + public void appendTrim(String str) { + append(str); + doTrim = true; + } +} diff --git a/app/src/processing/app/TextAreaFIFO.java b/app/src/processing/app/TextAreaFIFO.java index abf953dfd..7ee3f653b 100644 --- a/app/src/processing/app/TextAreaFIFO.java +++ b/app/src/processing/app/TextAreaFIFO.java @@ -72,6 +72,10 @@ public class TextAreaFIFO extends JTextArea implements DocumentListener { } } + public int getLength() { + return getDocument().getLength(); + } + public void appendNoTrim(String s) { int free = maxChars - getDocument().getLength(); if (free <= 0)