diff --git a/app/src/cc/arduino/UpdatableBoardsLibsFakeURLsHandler.java b/app/src/cc/arduino/UpdatableBoardsLibsFakeURLsHandler.java index 77694d925d3..3b9daddedcd 100644 --- a/app/src/cc/arduino/UpdatableBoardsLibsFakeURLsHandler.java +++ b/app/src/cc/arduino/UpdatableBoardsLibsFakeURLsHandler.java @@ -34,6 +34,10 @@ import javax.swing.event.HyperlinkEvent; import javax.swing.event.HyperlinkListener; import java.net.URL; +import java.net.URI; +import java.awt.Desktop; +import java.io.IOException; +import java.net.URISyntaxException; public class UpdatableBoardsLibsFakeURLsHandler implements HyperlinkListener { @@ -71,6 +75,18 @@ public void openBoardLibManager(URL url) { return; } + if(Desktop.isDesktopSupported()) + { + try { + Desktop.getDesktop().browse(url.toURI()); + return; + } catch (IOException e) { + throw new IllegalArgumentException(url.getHost() + " is invalid"); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(url.getHost() + " is invalid"); + } + } + throw new IllegalArgumentException(url.getHost() + " is invalid"); } diff --git a/app/src/processing/app/AbstractTextMonitor.java b/app/src/processing/app/AbstractTextMonitor.java index 1602e869a72..5e0264b8d2a 100644 --- a/app/src/processing/app/AbstractTextMonitor.java +++ b/app/src/processing/app/AbstractTextMonitor.java @@ -23,6 +23,9 @@ import javax.swing.SwingUtilities; import javax.swing.border.EmptyBorder; import javax.swing.text.DefaultCaret; +import javax.swing.event.UndoableEditListener; +import javax.swing.text.AbstractDocument; +import javax.swing.text.Document; import cc.arduino.packages.BoardPort; @@ -31,13 +34,19 @@ 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; protected JCheckBox autoscrollBox; 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); @@ -48,6 +57,7 @@ protected void onCreateWindow(Container mainPane) { Font editorFont = PreferencesData.getFont("editor.font"); Font font = Theme.scale(new Font(consoleFont.getName(), consoleFont.getStyle(), editorFont.getSize())); + this.mainPane = mainPane; mainPane.setLayout(new BorderLayout()); textArea = new TextAreaFIFO(8000000); @@ -56,13 +66,89 @@ protected void onCreateWindow(Container mainPane) { textArea.setEditable(false); textArea.setFont(font); + htmlTextArea = new HTMLTextAreaFIFO(8000000); + htmlTextArea.setEditable(false); + htmlTextArea.setFont(font); + 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)); @@ -128,18 +214,25 @@ public void actionPerformed(ActionEvent event) { pane.add(clearButton); mainPane.add(pane, BorderLayout.SOUTH); + + updateTimer.start(); } protected void onEnableWindow(boolean enable) { textArea.setEnabled(enable); + htmlTextArea.setEnabled(enable); clearButton.setEnabled(enable); scrollPane.setEnabled(enable); + htmlScrollPane.setEnabled(enable); textField.setEnabled(enable); sendButton.setEnabled(enable); autoscrollBox.setEnabled(enable); lineEndings.setEnabled(enable); serialRates.setEnabled(enable); + if (enable == false) { + htmlTextArea.setText(""); + } } public void onSendCommand(ActionListener listener) { @@ -154,8 +247,9 @@ public void onClearCommand(ActionListener listener) { public void onSerialRateChange(ActionListener listener) { serialRates.addActionListener(listener); } - + public void message(final String s) { + lastMessage = System.currentTimeMillis(); SwingUtilities.invokeLater(new Runnable() { public void run() { textArea.append(s); diff --git a/app/src/processing/app/HTMLTextAreaFIFO.java b/app/src/processing/app/HTMLTextAreaFIFO.java new file mode 100644 index 00000000000..0aa3aabfe9c --- /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 abf953dfd93..7ee3f653b0d 100644 --- a/app/src/processing/app/TextAreaFIFO.java +++ b/app/src/processing/app/TextAreaFIFO.java @@ -72,6 +72,10 @@ public void trimDocument() { } } + public int getLength() { + return getDocument().getLength(); + } + public void appendNoTrim(String s) { int free = maxChars - getDocument().getLength(); if (free <= 0)