Skip to content

Commit 870c438

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 b6276c4 commit 870c438

File tree

3 files changed

+258
-0
lines changed

3 files changed

+258
-0
lines changed

Diff for: app/src/processing/app/AbstractTextMonitor.java

+94
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
import javax.swing.SwingUtilities;
2727
import javax.swing.border.EmptyBorder;
2828
import javax.swing.text.DefaultCaret;
29+
import javax.swing.event.UndoableEditListener;
30+
import javax.swing.text.AbstractDocument;
31+
import javax.swing.text.Document;
2932

3033
import cc.arduino.packages.BoardPort;
3134

@@ -34,14 +37,20 @@ public abstract class AbstractTextMonitor extends AbstractMonitor {
3437

3538
protected JLabel noLineEndingAlert;
3639
protected TextAreaFIFO textArea;
40+
protected HTMLTextAreaFIFO htmlTextArea;
3741
protected JScrollPane scrollPane;
42+
protected JScrollPane htmlScrollPane;
3843
protected JTextField textField;
3944
protected JButton sendButton;
4045
protected JButton clearButton;
4146
protected JCheckBox autoscrollBox;
4247
protected JCheckBox addTimeStampBox;
4348
protected JComboBox lineEndings;
4449
protected JComboBox serialRates;
50+
protected Container mainPane;
51+
private long lastMessage;
52+
private javax.swing.Timer updateTimer;
53+
private boolean htmlView = true;
4554

4655
public AbstractTextMonitor(BoardPort boardPort) {
4756
super(boardPort);
@@ -52,6 +61,7 @@ protected void onCreateWindow(Container mainPane) {
5261
Font editorFont = PreferencesData.getFont("editor.font");
5362
Font font = Theme.scale(new Font(consoleFont.getName(), consoleFont.getStyle(), editorFont.getSize()));
5463

64+
this.mainPane = mainPane;
5565
mainPane.setLayout(new BorderLayout());
5666

5767
textArea = new TextAreaFIFO(8_000_000);
@@ -60,14 +70,90 @@ protected void onCreateWindow(Container mainPane) {
6070
textArea.setEditable(false);
6171
textArea.setFont(font);
6272

73+
htmlTextArea = new HTMLTextAreaFIFO(8000000);
74+
htmlTextArea.setEditable(false);
75+
htmlTextArea.setFont(font);
76+
htmlTextArea.setOpaque(false);
77+
6378
// don't automatically update the caret. that way we can manually decide
6479
// whether or not to do so based on the autoscroll checkbox.
6580
((DefaultCaret) textArea.getCaret()).setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
81+
((DefaultCaret) htmlTextArea.getCaret()).setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
82+
83+
Document doc = textArea.getDocument();
84+
if (doc instanceof AbstractDocument)
85+
{
86+
UndoableEditListener[] undoListeners =
87+
( (AbstractDocument) doc).getUndoableEditListeners();
88+
if (undoListeners.length > 0)
89+
{
90+
for (UndoableEditListener undoListener : undoListeners)
91+
{
92+
doc.removeUndoableEditListener(undoListener);
93+
}
94+
}
95+
}
96+
97+
doc = htmlTextArea.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+
}
66110

67111
scrollPane = new JScrollPane(textArea);
112+
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
113+
htmlScrollPane = new JScrollPane(htmlTextArea);
114+
htmlScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
115+
116+
ActionListener checkIfSteady = new ActionListener() {
117+
public void actionPerformed(ActionEvent evt) {
118+
if (System.currentTimeMillis() - lastMessage > 200) {
119+
if (htmlView == false && textArea.getLength() < 1000) {
120+
121+
htmlTextArea.setText("");
122+
boolean res = htmlTextArea.append(textArea.getText());
123+
if (res) {
124+
htmlView = true;
125+
mainPane.remove(scrollPane);
126+
if (textArea.getCaretPosition() > htmlTextArea.getDocument().getLength()) {
127+
htmlTextArea.setCaretPosition(htmlTextArea.getDocument().getLength());
128+
} else {
129+
htmlTextArea.setCaretPosition(textArea.getCaretPosition());
130+
}
131+
mainPane.add(htmlScrollPane, BorderLayout.CENTER);
132+
scrollPane.setVisible(false);
133+
mainPane.validate();
134+
mainPane.repaint();
135+
}
136+
}
137+
} else {
138+
if (htmlView == true) {
139+
htmlView = false;
140+
mainPane.remove(htmlScrollPane);
141+
mainPane.add(scrollPane, BorderLayout.CENTER);
142+
scrollPane.setVisible(true);
143+
mainPane.validate();
144+
mainPane.repaint();
145+
}
146+
}
147+
}
148+
};
149+
150+
updateTimer = new javax.swing.Timer(33, checkIfSteady);
68151

69152
mainPane.add(scrollPane, BorderLayout.CENTER);
70153

154+
htmlTextArea.setVisible(true);
155+
htmlScrollPane.setVisible(true);
156+
71157
JPanel upperPane = new JPanel();
72158
upperPane.setLayout(new BoxLayout(upperPane, BoxLayout.X_AXIS));
73159
upperPane.setBorder(new EmptyBorder(4, 4, 4, 4));
@@ -143,19 +229,26 @@ public void actionPerformed(ActionEvent e) {
143229
pane.add(clearButton);
144230

145231
mainPane.add(pane, BorderLayout.SOUTH);
232+
233+
updateTimer.start();
146234
}
147235

148236
protected void onEnableWindow(boolean enable)
149237
{
150238
textArea.setEnabled(enable);
151239
clearButton.setEnabled(enable);
240+
htmlTextArea.setEnabled(enable);
152241
scrollPane.setEnabled(enable);
242+
htmlScrollPane.setEnabled(enable);
153243
textField.setEnabled(enable);
154244
sendButton.setEnabled(enable);
155245
autoscrollBox.setEnabled(enable);
156246
addTimeStampBox.setEnabled(enable);
157247
lineEndings.setEnabled(enable);
158248
serialRates.setEnabled(enable);
249+
if (enable == false) {
250+
htmlTextArea.setText("");
251+
}
159252
}
160253

161254
public void onSendCommand(ActionListener listener) {
@@ -172,6 +265,7 @@ public void onSerialRateChange(ActionListener listener) {
172265
}
173266

174267
public void message(String msg) {
268+
lastMessage = System.currentTimeMillis();
175269
SwingUtilities.invokeLater(() -> updateTextArea(msg));
176270
}
177271

Diff for: app/src/processing/app/HTMLTextAreaFIFO.java

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*
2+
Copyright (c) 2014 Paul Stoffregen <[email protected]>
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+
}

Diff for: 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)