Skip to content

Commit ee91b22

Browse files
committed
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)
1 parent f466842 commit ee91b22

File tree

3 files changed

+257
-0
lines changed

3 files changed

+257
-0
lines changed

app/src/processing/app/AbstractTextMonitor.java

+93
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
import javax.swing.border.EmptyBorder;
3232
import javax.swing.text.DefaultCaret;
3333
import javax.swing.text.DefaultEditorKit;
34+
import javax.swing.event.UndoableEditListener;
35+
import javax.swing.text.AbstractDocument;
36+
import javax.swing.text.Document;
3437

3538
import cc.arduino.packages.BoardPort;
3639

@@ -39,14 +42,20 @@ public abstract class AbstractTextMonitor extends AbstractMonitor {
3942

4043
protected JLabel noLineEndingAlert;
4144
protected TextAreaFIFO textArea;
45+
protected HTMLTextAreaFIFO htmlTextArea;
4246
protected JScrollPane scrollPane;
47+
protected JScrollPane htmlScrollPane;
4348
protected JTextField textField;
4449
protected JButton sendButton;
4550
protected JButton clearButton;
4651
protected JCheckBox autoscrollBox;
4752
protected JCheckBox addTimeStampBox;
4853
protected JComboBox<String> lineEndings;
4954
protected JComboBox<String> serialRates;
55+
protected Container mainPane;
56+
private long lastMessage;
57+
private javax.swing.Timer updateTimer;
58+
private boolean htmlView = true;
5059

5160
public AbstractTextMonitor(BoardPort boardPort) {
5261
super(boardPort);
@@ -68,21 +77,97 @@ public synchronized void addKeyListener(KeyListener l) {
6877
@Override
6978
protected void onCreateWindow(Container mainPane) {
7079

80+
this.mainPane = mainPane;
7181
mainPane.setLayout(new BorderLayout());
7282

7383
textArea = new TextAreaFIFO(8_000_000);
7484
textArea.setRows(16);
7585
textArea.setColumns(40);
7686
textArea.setEditable(false);
7787

88+
htmlTextArea = new HTMLTextAreaFIFO(8000000);
89+
htmlTextArea.setEditable(false);
90+
htmlTextArea.setOpaque(false);
91+
7892
// don't automatically update the caret. that way we can manually decide
7993
// whether or not to do so based on the autoscroll checkbox.
8094
((DefaultCaret) textArea.getCaret()).setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
95+
((DefaultCaret) htmlTextArea.getCaret()).setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
96+
97+
Document doc = textArea.getDocument();
98+
if (doc instanceof AbstractDocument)
99+
{
100+
UndoableEditListener[] undoListeners =
101+
( (AbstractDocument) doc).getUndoableEditListeners();
102+
if (undoListeners.length > 0)
103+
{
104+
for (UndoableEditListener undoListener : undoListeners)
105+
{
106+
doc.removeUndoableEditListener(undoListener);
107+
}
108+
}
109+
}
110+
111+
doc = htmlTextArea.getDocument();
112+
if (doc instanceof AbstractDocument)
113+
{
114+
UndoableEditListener[] undoListeners =
115+
( (AbstractDocument) doc).getUndoableEditListeners();
116+
if (undoListeners.length > 0)
117+
{
118+
for (UndoableEditListener undoListener : undoListeners)
119+
{
120+
doc.removeUndoableEditListener(undoListener);
121+
}
122+
}
123+
}
81124

82125
scrollPane = new JScrollPane(textArea);
126+
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
127+
htmlScrollPane = new JScrollPane(htmlTextArea);
128+
htmlScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
129+
130+
ActionListener checkIfSteady = new ActionListener() {
131+
public void actionPerformed(ActionEvent evt) {
132+
if (System.currentTimeMillis() - lastMessage > 200) {
133+
if (htmlView == false && textArea.getLength() < 1000) {
134+
135+
htmlTextArea.setText("");
136+
boolean res = htmlTextArea.append(textArea.getText());
137+
if (res) {
138+
htmlView = true;
139+
mainPane.remove(scrollPane);
140+
if (textArea.getCaretPosition() > htmlTextArea.getDocument().getLength()) {
141+
htmlTextArea.setCaretPosition(htmlTextArea.getDocument().getLength());
142+
} else {
143+
htmlTextArea.setCaretPosition(textArea.getCaretPosition());
144+
}
145+
mainPane.add(htmlScrollPane, BorderLayout.CENTER);
146+
scrollPane.setVisible(false);
147+
mainPane.validate();
148+
mainPane.repaint();
149+
}
150+
}
151+
} else {
152+
if (htmlView == true) {
153+
htmlView = false;
154+
mainPane.remove(htmlScrollPane);
155+
mainPane.add(scrollPane, BorderLayout.CENTER);
156+
scrollPane.setVisible(true);
157+
mainPane.validate();
158+
mainPane.repaint();
159+
}
160+
}
161+
}
162+
};
163+
164+
updateTimer = new javax.swing.Timer(33, checkIfSteady);
83165

84166
mainPane.add(scrollPane, BorderLayout.CENTER);
85167

168+
htmlTextArea.setVisible(true);
169+
htmlScrollPane.setVisible(true);
170+
86171
JPanel upperPane = new JPanel();
87172
upperPane.setLayout(new BoxLayout(upperPane, BoxLayout.X_AXIS));
88173
upperPane.setBorder(new EmptyBorder(4, 4, 4, 4));
@@ -167,20 +252,27 @@ public void windowGainedFocus(WindowEvent e) {
167252
applyPreferences();
168253

169254
mainPane.add(pane, BorderLayout.SOUTH);
255+
256+
updateTimer.start();
170257
}
171258

172259
@Override
173260
protected void onEnableWindow(boolean enable)
174261
{
175262
textArea.setEnabled(enable);
176263
clearButton.setEnabled(enable);
264+
htmlTextArea.setEnabled(enable);
177265
scrollPane.setEnabled(enable);
266+
htmlScrollPane.setEnabled(enable);
178267
textField.setEnabled(enable);
179268
sendButton.setEnabled(enable);
180269
autoscrollBox.setEnabled(enable);
181270
addTimeStampBox.setEnabled(enable);
182271
lineEndings.setEnabled(enable);
183272
serialRates.setEnabled(enable);
273+
if (enable == false) {
274+
htmlTextArea.setText("");
275+
}
184276
}
185277

186278
public void onSendCommand(ActionListener listener) {
@@ -198,6 +290,7 @@ public void onSerialRateChange(ActionListener listener) {
198290

199291
@Override
200292
public void message(String msg) {
293+
lastMessage = System.currentTimeMillis();
201294
SwingUtilities.invokeLater(() -> updateTextArea(msg));
202295
}
203296

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*
2+
Copyright (c) 2014 Paul Stoffregen <paul@pjrc.com>
3+
4+
This program is free software; you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation; either version 2 of the License, or
7+
(at your option) any later version.
8+
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with this program; if not, write to the Free Software Foundation,
16+
Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17+
*/
18+
19+
// adapted from https://community.oracle.com/thread/1479784
20+
21+
package processing.app;
22+
23+
import java.io.IOException;
24+
import java.net.URL;
25+
import java.awt.Desktop;
26+
import java.net.URLEncoder;
27+
28+
import java.util.*;
29+
import java.util.regex.*;
30+
31+
import javax.swing.text.html.HTMLDocument;
32+
import javax.swing.JEditorPane;
33+
import javax.swing.JTextPane;
34+
import javax.swing.SwingUtilities;
35+
import javax.swing.event.HyperlinkEvent;
36+
import javax.swing.event.HyperlinkListener;
37+
import javax.swing.event.DocumentEvent;
38+
import javax.swing.event.DocumentListener;
39+
import javax.swing.text.BadLocationException;
40+
import javax.swing.text.html.HTMLEditorKit;
41+
42+
import cc.arduino.UpdatableBoardsLibsFakeURLsHandler;
43+
44+
public class HTMLTextAreaFIFO extends JTextPane implements DocumentListener {
45+
private int maxChars;
46+
private int trimMaxChars;
47+
48+
private int updateCount; // limit how often we trim the document
49+
50+
private boolean doTrim;
51+
private final HTMLEditorKit kit;
52+
53+
public HTMLTextAreaFIFO(int max) {
54+
maxChars = max;
55+
trimMaxChars = max / 2;
56+
updateCount = 0;
57+
doTrim = true;
58+
setContentType("text/html");
59+
getDocument().addDocumentListener(this);
60+
setText("");
61+
kit = new HTMLEditorKit();
62+
this.addHyperlinkListener(new UpdatableBoardsLibsFakeURLsHandler(Base.INSTANCE));
63+
}
64+
65+
public void insertUpdate(DocumentEvent e) {
66+
}
67+
68+
public void removeUpdate(DocumentEvent e) {
69+
}
70+
71+
public void changedUpdate(DocumentEvent e) {
72+
}
73+
74+
public void trimDocument() {
75+
int len = 0;
76+
len = getDocument().getLength();
77+
if (len > trimMaxChars) {
78+
int n = len - trimMaxChars;
79+
//System.out.println("trimDocument: remove " + n + " chars");
80+
try {
81+
getDocument().remove(0, n);
82+
} catch (BadLocationException ble) {
83+
}
84+
}
85+
}
86+
87+
private static List<String> extractUrls(String input) {
88+
List<String> result = new ArrayList<String>();
89+
90+
Pattern pattern = Pattern.compile(
91+
"(http|ftp|https)://([^\\s]+)");
92+
93+
Matcher matcher = pattern.matcher(input);
94+
while (matcher.find()) {
95+
result.add(matcher.group());
96+
}
97+
98+
return result;
99+
}
100+
101+
static public final String WITH_DELIMITER = "((?<=%1$s)|(?=%1$s))";
102+
103+
public boolean append(String s) {
104+
boolean htmlFound = false;
105+
try {
106+
HTMLDocument doc = (HTMLDocument) getDocument();
107+
108+
String strings[] = s.split(String.format(WITH_DELIMITER, "\\r?\\n"));
109+
110+
for (int l = 0; l < strings.length; l++) {
111+
String str = strings[l];
112+
List<String> urls = extractUrls(str);
113+
114+
if (urls.size() > 0) {
115+
116+
for (int i = 0; i < urls.size(); i++) {
117+
if (!((urls.get(i)).contains("</a>"))) {
118+
str = str.replace(urls.get(i), "<a href='" + urls.get(i) + "'>" + urls.get(i) + "</a>");
119+
}
120+
}
121+
122+
kit.insertHTML(doc, doc.getLength(), str, 0, 0, null);
123+
htmlFound = true;
124+
} else {
125+
doc.insertString(doc.getLength(), str, null);
126+
}
127+
}
128+
} catch(BadLocationException exc) {
129+
exc.printStackTrace();
130+
} catch(IOException exc) {
131+
exc.printStackTrace();
132+
}
133+
134+
if (++updateCount > 150 && doTrim) {
135+
updateCount = 0;
136+
SwingUtilities.invokeLater(new Runnable() {
137+
public void run() {
138+
trimDocument();
139+
}
140+
});
141+
}
142+
return htmlFound;
143+
}
144+
145+
public void appendNoTrim(String s) {
146+
int free = maxChars - getDocument().getLength();
147+
if (free <= 0)
148+
return;
149+
if (s.length() > free)
150+
append(s.substring(0, free));
151+
else
152+
append(s);
153+
doTrim = false;
154+
}
155+
156+
public void appendTrim(String str) {
157+
append(str);
158+
doTrim = true;
159+
}
160+
}

app/src/processing/app/TextAreaFIFO.java

+4
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ public void trimDocument() {
7272
}
7373
}
7474

75+
public int getLength() {
76+
return getDocument().getLength();
77+
}
78+
7579
public void appendNoTrim(String s) {
7680
int free = maxChars - getDocument().getLength();
7781
if (free <= 0)

0 commit comments

Comments
 (0)