Skip to content

Commit 6952bfe

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 4c7f74a commit 6952bfe

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
@@ -32,6 +32,9 @@
3232
import javax.swing.border.EmptyBorder;
3333
import javax.swing.text.DefaultCaret;
3434
import javax.swing.text.DefaultEditorKit;
35+
import javax.swing.event.UndoableEditListener;
36+
import javax.swing.text.AbstractDocument;
37+
import javax.swing.text.Document;
3538

3639
import cc.arduino.packages.BoardPort;
3740

@@ -40,14 +43,20 @@ public abstract class AbstractTextMonitor extends AbstractMonitor {
4043

4144
protected JLabel noLineEndingAlert;
4245
protected TextAreaFIFO textArea;
46+
protected HTMLTextAreaFIFO htmlTextArea;
4347
protected JScrollPane scrollPane;
48+
protected JScrollPane htmlScrollPane;
4449
protected JTextField textField;
4550
protected JButton sendButton;
4651
protected JButton clearButton;
4752
protected JCheckBox autoscrollBox;
4853
protected JCheckBox addTimeStampBox;
4954
protected JComboBox<String> lineEndings;
5055
protected JComboBox<String> serialRates;
56+
protected Container mainPane;
57+
private long lastMessage;
58+
private javax.swing.Timer updateTimer;
59+
private boolean htmlView = true;
5160

5261
public AbstractTextMonitor(BoardPort boardPort) {
5362
super(boardPort);
@@ -69,21 +78,97 @@ public synchronized void addKeyListener(KeyListener l) {
6978
@Override
7079
protected void onCreateWindow(Container mainPane) {
7180

81+
this.mainPane = mainPane;
7282
mainPane.setLayout(new BorderLayout());
7383

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

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

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

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

169+
htmlTextArea.setVisible(true);
170+
htmlScrollPane.setVisible(true);
171+
87172
JPanel upperPane = new JPanel();
88173
upperPane.setLayout(new BoxLayout(upperPane, BoxLayout.X_AXIS));
89174
upperPane.setBorder(new EmptyBorder(4, 4, 4, 4));
@@ -168,6 +253,8 @@ public void windowGainedFocus(WindowEvent e) {
168253
applyPreferences();
169254

170255
mainPane.add(pane, BorderLayout.SOUTH);
256+
257+
updateTimer.start();
171258
}
172259

173260
@Override
@@ -191,13 +278,18 @@ protected void onEnableWindow(boolean enable) {
191278
}
192279
textArea.invalidate();
193280
clearButton.setEnabled(enable);
281+
htmlTextArea.setEnabled(enable);
194282
scrollPane.setEnabled(enable);
283+
htmlScrollPane.setEnabled(enable);
195284
textField.setEnabled(enable);
196285
sendButton.setEnabled(enable);
197286
autoscrollBox.setEnabled(enable);
198287
addTimeStampBox.setEnabled(enable);
199288
lineEndings.setEnabled(enable);
200289
serialRates.setEnabled(enable);
290+
if (enable == false) {
291+
htmlTextArea.setText("");
292+
}
201293
}
202294

203295
public void onSendCommand(ActionListener listener) {
@@ -215,6 +307,7 @@ public void onSerialRateChange(ActionListener listener) {
215307

216308
@Override
217309
public void message(String msg) {
310+
lastMessage = System.currentTimeMillis();
218311
SwingUtilities.invokeLater(() -> updateTextArea(msg));
219312
}
220313

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)