Made announcement bubbles and dialogs that contain links clickable

This commit is contained in:
Bane Stojkovic
2026-08-24 17:09:35 +02:00
parent ac6c38b115
commit e209ecdcb8
10 changed files with 391 additions and 75 deletions
+1 -1
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>org.diylc</groupId>
<artifactId>diylc-parent</artifactId>
<version>6.3.0</version>
<version>6.4.0</version>
</parent>
<artifactId>diylc-core</artifactId>
@@ -0,0 +1,95 @@
/*
DIY Layout Creator (DIYLC).
Copyright (c) 2009-2025 held jointly by the individual authors.
This file is part of DIYLC.
DIYLC 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 3 of the License, or
(at your option) any later version.
DIYLC 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 DIYLC. If not, see <http://www.gnu.org/licenses/>.
*/
package org.diylc.announcements;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Utility methods for detecting and extracting URLs from announcements and HTML.
*/
public class AnnouncementUtils {
private static final Pattern HREF_PATTERN = Pattern.compile(
"href=[\"']([^\"']+)[\"']", Pattern.CASE_INSENSITIVE);
/**
* Extracts the first URL found in the text or HTML.
*
* @param html text or HTML content
* @return first URL found, or null if none
*/
public static String extractFirstUrl(String html) {
List<String> urls = extractUrls(html);
return urls.isEmpty() ? null : urls.get(0);
}
/**
* Extracts all unique URLs found in the HTML href attributes in order of appearance.
*
* @param html text or HTML content
* @return list of unique URLs
*/
public static List<String> extractUrls(String html) {
List<String> urls = new ArrayList<String>();
if (html == null || html.isEmpty()) {
return urls;
}
Matcher hrefMatcher = HREF_PATTERN.matcher(html);
while (hrefMatcher.find()) {
String url = hrefMatcher.group(1).trim();
url = normalizeUrl(url);
if (!urls.contains(url)) {
urls.add(url);
}
}
return urls;
}
/**
* Checks whether the HTML contains any link.
*
* @param html text or HTML content
* @return true if at least one URL is detected, false otherwise
*/
public static boolean containsLink(String html) {
return extractFirstUrl(html) != null;
}
/**
* Normalizes a URL to ensure it has a valid protocol if starting with www.
*/
public static String normalizeUrl(String url) {
if (url == null) {
return null;
}
String trimmed = url.trim();
if (trimmed.startsWith("www.")) {
return "http://" + trimmed;
}
return trimmed;
}
}
+16 -68
View File
@@ -1,73 +1,5 @@
<java.util.Arrays_-ArrayList>
<a class="org.diylc.appframework.update.Version-array">
<org.diylc.appframework.update.Version>
<versionNumber>
<major>5</major>
<minor>9</minor>
<build>0</build>
</versionNumber>
<releaseDate>2026-01-10 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added ability to name groups of components</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Show groups of components as single items in the Project Explorer list</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added balloon tooltip next to cursor showing component width, height and length (where available) while resizing; can be disabled from Config menu</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added 'Freeway 3x3-05 (Ultra) Switch' (thank you mm-rnd)</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Configurable 'Tactile Micro Switch' dimensions and added a few most commonly used variants</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Exposed the most common pin pairing modes and added switching logic for 'Tactile Micro Switch' to allow using it for analysis</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>'Potentiometer Panel' can now render numerical lug markers for easier identification</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>'Auto-Wrap Label' retains line breaks from the input text</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>'Cliff 1/8" Jack', 'Open 1/8" Jack' and 'Open 1/4" Jack' can be rotated</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Fixed the offset of terminals on a DP4T Tele type of 'Lever Switch' to be aligned with the physical switch (attention - this will break existing diagrams and will require adjusting leads to match the new terminal positions)</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Make the label of schematic symbols (e.g. 'Resistor Symbol') clickable as well</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Ability to resize 'Transformer Core' schematic symbol after it's already placed</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Clicking on 'Tactile Micro Switch' body doesn't register</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>New lines at the end of the 'Label' text get erroneously rendered as br HTML tags</description>
</org.diylc.appframework.update.Change>
</changes>
<url>https://github.com/bancika/diy-layout-creator/releases</url>
</org.diylc.appframework.update.Version>
<org.diylc.appframework.update.Version>
<versionNumber>
<major>5</major>
@@ -428,5 +360,21 @@
</changes>
<url>https://github.com/bancika/diy-layout-creator/releases</url>
</org.diylc.appframework.update.Version>
<org.diylc.appframework.update.Version>
<versionNumber>
<major>6</major>
<minor>4</minor>
<build>0</build>
</versionNumber>
<releaseDate>2026-08-22 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Made announcement bubbles and dialogs that contain links clickable</description>
</org.diylc.appframework.update.Change>
</changes>
<url>https://github.com/bancika/diy-layout-creator/releases</url>
</org.diylc.appframework.update.Version>
</a>
</java.util.Arrays_-ArrayList>
@@ -5257,5 +5257,73 @@
</changes>
<url>https://github.com/bancika/diy-layout-creator/releases</url>
</org.diylc.appframework.update.Version>
<org.diylc.appframework.update.Version>
<versionNumber>
<major>5</major>
<minor>9</minor>
<build>0</build>
</versionNumber>
<releaseDate>2026-01-10 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added ability to name groups of components</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Show groups of components as single items in the Project Explorer list</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added balloon tooltip next to cursor showing component width, height and length (where available) while resizing; can be disabled from Config menu</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Added 'Freeway 3x3-05 (Ultra) Switch' (thank you mm-rnd)</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Configurable 'Tactile Micro Switch' dimensions and added a few most commonly used variants</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Exposed the most common pin pairing modes and added switching logic for 'Tactile Micro Switch' to allow using it for analysis</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>'Potentiometer Panel' can now render numerical lug markers for easier identification</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>'Auto-Wrap Label' retains line breaks from the input text</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>'Cliff 1/8" Jack', 'Open 1/8" Jack' and 'Open 1/4" Jack' can be rotated</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Fixed the offset of terminals on a DP4T Tele type of 'Lever Switch' to be aligned with the physical switch (attention - this will break existing diagrams and will require adjusting leads to match the new terminal positions)</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Make the label of schematic symbols (e.g. 'Resistor Symbol') clickable as well</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Ability to resize 'Transformer Core' schematic symbol after it's already placed</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>Clicking on 'Tactile Micro Switch' body doesn't register</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>New lines at the end of the 'Label' text get erroneously rendered as br HTML tags</description>
</org.diylc.appframework.update.Change>
</changes>
<url>https://github.com/bancika/diy-layout-creator/releases</url>
</org.diylc.appframework.update.Version>
</a>
</java.util.Arrays_-ArrayList>
+1 -1
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>org.diylc</groupId>
<artifactId>diylc-parent</artifactId>
<version>6.3.0</version>
<version>6.4.0</version>
</parent>
<artifactId>diylc-library</artifactId>
+1 -1
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>org.diylc</groupId>
<artifactId>diylc-parent</artifactId>
<version>6.3.0</version>
<version>6.4.0</version>
</parent>
<artifactId>diylc-swing</artifactId>
@@ -0,0 +1,148 @@
/*
DIY Layout Creator (DIYLC).
Copyright (c) 2009-2025 held jointly by the individual authors.
This file is part of DIYLC.
DIYLC 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 3 of the License, or
(at your option) any later version.
DIYLC 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 DIYLC. If not, see <http://www.gnu.org/licenses/>.
*/
package org.diylc.swing.gui;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Font;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JEditorPane;
import javax.swing.UIManager;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import org.apache.log4j.Logger;
import org.diylc.announcements.AnnouncementUtils;
import org.diylc.appframework.miscutils.Utils;
/**
* Utility for creating interactive Swing HTML components with clickable links.
*/
public class ClickableHtmlUtils {
private static final Logger LOG = Logger.getLogger(ClickableHtmlUtils.class);
/**
* Creates a JEditorPane configured to display HTML with clickable links and whole-text click navigation.
*
* @param html the HTML content to display
* @param defaultUrlToOpen optional default URL to open when clicking non-hyperlink area of text (if null, extracted from html)
* @param onLinkOpened optional callback executed when a link is opened
* @return configured JEditorPane
*/
public static JEditorPane createClickableHtmlPane(String html, final String defaultUrlToOpen,
final Runnable onLinkOpened) {
JEditorPane editorPane = new JEditorPane();
editorPane.setContentType("text/html");
editorPane.setText(html != null ? html : "");
editorPane.setEditable(false);
editorPane.setOpaque(false);
editorPane.setBackground(new Color(0, 0, 0, 0));
editorPane.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
editorPane.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE);
final String targetUrl =
defaultUrlToOpen != null ? defaultUrlToOpen : AnnouncementUtils.extractFirstUrl(html);
final long[] lastClickTime = new long[1];
editorPane.addHyperlinkListener(new HyperlinkListener() {
@Override
public void hyperlinkUpdate(HyperlinkEvent e) {
if (HyperlinkEvent.EventType.ACTIVATED.equals(e.getEventType())) {
lastClickTime[0] = System.currentTimeMillis();
String url = e.getURL() != null ? e.getURL().toString() : e.getDescription();
if (url != null) {
openUrl(url);
if (onLinkOpened != null) {
onLinkOpened.run();
}
}
}
}
});
if (targetUrl != null) {
editorPane.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
editorPane.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (System.currentTimeMillis() - lastClickTime[0] > 500) {
lastClickTime[0] = System.currentTimeMillis();
openUrl(targetUrl);
if (onLinkOpened != null) {
onLinkOpened.run();
}
}
}
});
}
return editorPane;
}
/**
* Creates a JComponent suitable for display inside JOptionPane message dialogs with clickable links.
*
* @param message HTML or text message
* @return JComponent with clickable links
*/
public static JComponent createClickableMessageComponent(String message) {
if (message == null || message.isEmpty()) {
return new JEditorPane();
}
String html = message;
if (!html.trim().toLowerCase().startsWith("<html>")) {
html = "<html>" + html + "</html>";
}
JEditorPane pane = createClickableHtmlPane(html, null, null);
Font font = UIManager.getFont("Label.font");
if (font != null) {
pane.setFont(font);
}
Color fg = UIManager.getColor("Label.foreground");
if (fg != null) {
pane.setForeground(fg);
}
return pane;
}
/**
* Opens a URL in the system browser.
*
* @param url URL to open
*/
public static void openUrl(String url) {
if (url == null || url.trim().isEmpty()) {
return;
}
String normalized = AnnouncementUtils.normalizeUrl(url);
try {
Utils.openURL(normalized);
} catch (Exception ex) {
LOG.error("Could not open URL: " + normalized, ex);
}
}
}
@@ -56,6 +56,7 @@ import javax.swing.Timer;
import javax.swing.event.MenuEvent;
import javax.swing.event.MenuListener;
import org.apache.log4j.Logger;
import org.diylc.announcements.AnnouncementUtils;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.appframework.miscutils.IConfigurationManager;
import org.diylc.swing.IDynamicSubmenuHandler;
@@ -288,7 +289,12 @@ public class MainFrame extends JFrame implements ISwingUI {
@Override
public void showMessage(String message, String title, int messageType) {
JOptionPane.showMessageDialog(this, message, title, messageType);
if (message != null && (message.toLowerCase().contains("<html>") || AnnouncementUtils.containsLink(message))) {
JComponent component = ClickableHtmlUtils.createClickableMessageComponent(message);
JOptionPane.showMessageDialog(this, component, title, messageType);
} else {
JOptionPane.showMessageDialog(this, message, title, messageType);
}
}
@Override
@@ -59,14 +59,19 @@ import org.diylc.swingframework.MemoryBar;
import org.diylc.swingframework.miscutils.PercentageListCellRenderer;
import org.diylc.swingframework.update.UpdateDialog;
import org.diylc.swingframework.update.UpdateLabel;
import java.awt.Color;
import java.awt.Font;
import javax.swing.JEditorPane;
import net.java.balloontip.BalloonTip;
import net.java.balloontip.styles.EdgedBalloonStyle;
import org.diylc.announcements.AnnouncementProvider;
import org.diylc.announcements.AnnouncementUtils;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IView;
import org.diylc.lang.LangUtil;
import org.diylc.presenter.Presenter;
import org.diylc.swing.ISwingUI;
import org.diylc.swing.gui.ClickableHtmlUtils;
import org.diylc.utils.IconLoader;
public class StatusBar extends JPanel implements IPlugIn {
@@ -172,8 +177,7 @@ public class StatusBar extends JPanel implements IPlugIn {
@Override
public void complete(String result) {
if (result != null && result.length() > 0) {
new BalloonTip(getUpdateLabel(), result, new EdgedBalloonStyle(UIManager.getColor("ToolTip.background"),
UIManager.getColor("ToolTip.foreground")), true);
showAnnouncementBalloon(result);
announcementProvider.dismissed();
}
}
@@ -568,4 +572,51 @@ public class StatusBar extends JPanel implements IPlugIn {
}
});
}
private void showAnnouncementBalloon(String result) {
String html = result;
final String firstUrl = AnnouncementUtils.extractFirstUrl(html);
final BalloonTip[] balloonTipHolder = new BalloonTip[1];
JEditorPane pane = ClickableHtmlUtils.createClickableHtmlPane(html, firstUrl, new Runnable() {
@Override
public void run() {
if (balloonTipHolder[0] != null) {
balloonTipHolder[0].closeBalloon();
}
}
});
Font font = UIManager.getFont("ToolTip.font");
if (font == null) {
font = UIManager.getFont("Label.font");
}
if (font != null) {
pane.setFont(font);
}
Color fg = UIManager.getColor("ToolTip.foreground");
if (fg != null) {
pane.setForeground(fg);
}
EdgedBalloonStyle style = new EdgedBalloonStyle(
UIManager.getColor("ToolTip.background"),
UIManager.getColor("ToolTip.foreground"));
final BalloonTip balloonTip = new BalloonTip(getUpdateLabel(), pane, style, true);
balloonTipHolder[0] = balloonTip;
if (firstUrl != null) {
balloonTip.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
balloonTip.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
ClickableHtmlUtils.openUrl(firstUrl);
balloonTip.closeBalloon();
}
});
}
}
}
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>org.diylc</groupId>
<artifactId>diylc-parent</artifactId>
<version>6.3.0</version>
<version>6.4.0</version>
<packaging>pom</packaging>
<name>DIY Layout Creator</name>