From ce6d83bd04fd18dfcf5abc3467163b96f9e14887 Mon Sep 17 00:00:00 2001 From: Hans Van Akelyen Date: Wed, 5 Aug 2026 09:00:22 +0200 Subject: [PATCH] Hop web fix for #3222 --- .../hop/ui/core/widget/StyledTextVar.java | 15 ++ .../hop/ui/core/widget/TextComposite.java | 16 ++ .../execution/ExecutionLogPanel.java | 38 ++--- .../hopgui/GuiPluginWebCompatibilityTest.java | 156 ++++++++++++++++++ 4 files changed, 197 insertions(+), 28 deletions(-) create mode 100644 ui/src/test/java/org/apache/hop/ui/hopgui/GuiPluginWebCompatibilityTest.java diff --git a/ui/src/main/java/org/apache/hop/ui/core/widget/StyledTextVar.java b/ui/src/main/java/org/apache/hop/ui/core/widget/StyledTextVar.java index 134abff72d2..78e4805de0d 100644 --- a/ui/src/main/java/org/apache/hop/ui/core/widget/StyledTextVar.java +++ b/ui/src/main/java/org/apache/hop/ui/core/widget/StyledTextVar.java @@ -28,6 +28,7 @@ import org.apache.hop.ui.core.widget.highlight.JavaHighlight; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.LineStyleListener; +import org.eclipse.swt.custom.StyleRange; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.events.FocusAdapter; import org.eclipse.swt.events.KeyAdapter; @@ -157,6 +158,20 @@ public void insert(String strInsert) { wText.insert(strInsert); } + @Override + public void setStyleRange(int start, int length, Color background, Color foreground) { + if (wText.isDisposed() || length <= 0) { + return; + } + StyleRange range = new StyleRange(); + range.start = start; + range.length = length; + range.background = background; + range.foreground = foreground; + range.fontStyle = SWT.NORMAL; + wText.setStyleRange(range); + } + @Override public void addListener(int eventType, Listener listener) { wText.addListener(eventType, listener); diff --git a/ui/src/main/java/org/apache/hop/ui/core/widget/TextComposite.java b/ui/src/main/java/org/apache/hop/ui/core/widget/TextComposite.java index 65ff65e369b..d16a6c2df41 100644 --- a/ui/src/main/java/org/apache/hop/ui/core/widget/TextComposite.java +++ b/ui/src/main/java/org/apache/hop/ui/core/widget/TextComposite.java @@ -28,6 +28,7 @@ import org.eclipse.swt.dnd.TextTransfer; import org.eclipse.swt.events.MenuDetectListener; import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.graphics.Color; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.MenuItem; @@ -63,6 +64,21 @@ public void addLineStyleListener(String scriptEngine) { throw new UnsupportedOperationException("Cannot specify a script engine"); } + /** + * Highlight a range of the text with the given colors. Implementations backed by a plain Text + * widget (Hop Web) can't style text, so this is a no-op by default. Callers must not reference + * {@code org.eclipse.swt.custom.StyledText} themselves: that class doesn't exist under RAP and + * naming it in a signature breaks the reflection done at GUI plugin registration time. + * + * @param start offset of the first character to style + * @param length number of characters to style + * @param background background color, null to leave unchanged + * @param foreground foreground color, null to leave unchanged + */ + public void setStyleRange(int start, int length, Color background, Color foreground) { + // No styling support by default. + } + /** * Adds the listener to the collection of listeners who will be notified when the * platform-specific context menu trigger has occurred, by sending it one of the messages defined diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionLogPanel.java b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionLogPanel.java index aeeb6902c13..c2245781931 100644 --- a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionLogPanel.java +++ b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionLogPanel.java @@ -38,8 +38,7 @@ import org.apache.hop.ui.hopgui.file.shared.TextZoom; import org.apache.hop.ui.util.EnvironmentUtils; import org.eclipse.swt.SWT; -import org.eclipse.swt.custom.StyleRange; -import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.graphics.Color; import org.eclipse.swt.layout.FormAttachment; import org.eclipse.swt.layout.FormData; import org.eclipse.swt.layout.FormLayout; @@ -314,14 +313,9 @@ private void applyDisplayFromRaw() { } String displayText = display.toString(); - StyledText styledText = getStyledText(); - if (styledText != null && !styledText.isDisposed()) { - styledText.setText(displayText); - for (int[] span : highlightSpans) { - applyHighlightRange(styledText, span[0], span[1]); - } - } else { - logText.setText(displayText); + logText.setText(displayText); + for (int[] span : highlightSpans) { + applyHighlightRange(span[0], span[1]); } if (!logText.isDisposed()) { @@ -354,25 +348,13 @@ private void collectHighlightSpans(String line, int lineStart, List highl } } - private void applyHighlightRange(StyledText styledText, int start, int length) { - StyleRange range = new StyleRange(); - range.start = start; - range.length = length; - range.fontStyle = SWT.NORMAL; + private void applyHighlightRange(int start, int length) { // Avoid contrast-remapped colors for reliable dark-mode readability - if (PropsUi.getInstance().isDarkMode()) { - range.background = GuiResource.getInstance().getColor(180, 90, 0); - } else { - range.background = GuiResource.getInstance().getColorYellow(); - } - styledText.setStyleRange(range); - } - - private StyledText getStyledText() { - if (logText instanceof StyledTextVar) { - return ((StyledTextVar) logText).getTextWidget(); - } - return null; + Color background = + PropsUi.getInstance().isDarkMode() + ? GuiResource.getInstance().getColor(180, 90, 0) + : GuiResource.getInstance().getColorYellow(); + logText.setStyleRange(start, length, background, null); } private boolean shouldDisplayLine(String line) { diff --git a/ui/src/test/java/org/apache/hop/ui/hopgui/GuiPluginWebCompatibilityTest.java b/ui/src/test/java/org/apache/hop/ui/hopgui/GuiPluginWebCompatibilityTest.java new file mode 100644 index 00000000000..f4025f8268d --- /dev/null +++ b/ui/src/test/java/org/apache/hop/ui/hopgui/GuiPluginWebCompatibilityTest.java @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hop.ui.hopgui; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; +import org.apache.hop.core.gui.plugin.GuiPlugin; +import org.apache.hop.ui.hopgui.perspective.execution.ExecutionLogPanel; +import org.junit.jupiter.api.Test; + +/** + * Hop Web runs on RAP/RWT, which ships an SWT subset. At startup {@code + * HopGuiEnvironment.initGuiPlugins()} reflects over every {@link GuiPlugin} class with {@code + * getDeclaredFields()} / {@code getDeclaredMethods()}. Reflection resolves every type named in + * those signatures, so a single field or method signature mentioning a desktop-only SWT type makes + * Hop Web fail to start with a {@code NoClassDefFoundError} - even when the code is guarded at + * runtime with {@code EnvironmentUtils.getInstance().isWeb()}. + * + *

Keep the desktop-only types inside method bodies (or behind an abstraction such as {@code + * TextComposite}), never in a signature of a {@link GuiPlugin} class. + */ +class GuiPluginWebCompatibilityTest { + + /** + * SWT types that exist in the desktop SWT jar but not in org.eclipse.rap.rwt. Not exhaustive: + * these are the ones that realistically show up in Hop UI signatures. + */ + private static final Set DESKTOP_ONLY_SWT_TYPES = + new HashSet<>( + Arrays.asList( + "org.eclipse.swt.custom.BidiSegmentListener", + "org.eclipse.swt.custom.Bullet", + "org.eclipse.swt.custom.CTabFolderRenderer", + "org.eclipse.swt.custom.CaretListener", + "org.eclipse.swt.custom.ExtendedModifyListener", + "org.eclipse.swt.custom.LineBackgroundListener", + "org.eclipse.swt.custom.LineStyleEvent", + "org.eclipse.swt.custom.LineStyleListener", + "org.eclipse.swt.custom.PaintObjectListener", + "org.eclipse.swt.custom.PopupList", + "org.eclipse.swt.custom.ST", + "org.eclipse.swt.custom.StyleRange", + "org.eclipse.swt.custom.StyledText", + "org.eclipse.swt.custom.StyledTextContent", + "org.eclipse.swt.custom.TableCursor", + "org.eclipse.swt.custom.TreeCursor", + "org.eclipse.swt.custom.VerifyKeyListener", + "org.eclipse.swt.graphics.GlyphMetrics", + "org.eclipse.swt.graphics.Pattern", + "org.eclipse.swt.graphics.Region", + "org.eclipse.swt.graphics.TextLayout", + "org.eclipse.swt.graphics.TextStyle", + "org.eclipse.swt.widgets.Caret", + "org.eclipse.swt.widgets.Tracker")); + + @Test + void guiPluginSignaturesAvoidDesktopOnlySwtTypes() throws Exception { + List violations = new ArrayList<>(); + + for (Class guiPluginClass : findGuiPluginClasses()) { + for (Field field : guiPluginClass.getDeclaredFields()) { + if (DESKTOP_ONLY_SWT_TYPES.contains(field.getType().getName())) { + violations.add( + guiPluginClass.getName() + + ": field " + + field.getName() + + " of type " + + field.getType().getName()); + } + } + for (Method method : guiPluginClass.getDeclaredMethods()) { + List> types = new ArrayList<>(); + types.add(method.getReturnType()); + types.addAll(Arrays.asList(method.getParameterTypes())); + for (Class type : types) { + if (DESKTOP_ONLY_SWT_TYPES.contains(type.getName())) { + violations.add( + guiPluginClass.getName() + + ": method " + + method.getName() + + " uses " + + type.getName()); + } + } + } + } + + assertTrue( + violations.isEmpty(), + "@GuiPlugin classes must not name desktop-only SWT types in field or method signatures, " + + "they break Hop Web startup: " + + violations); + } + + private static List> findGuiPluginClasses() throws URISyntaxException, IOException { + Path root = + Path.of( + ExecutionLogPanel.class.getProtectionDomain().getCodeSource().getLocation().toURI()); + ClassLoader classLoader = GuiPluginWebCompatibilityTest.class.getClassLoader(); + List> classes = new ArrayList<>(); + + try (Stream files = Files.walk(root)) { + for (Path file : + (Iterable) files.filter(GuiPluginWebCompatibilityTest::isClassFile)::iterator) { + String className = + root.relativize(file) + .toString() + .replace(java.io.File.separatorChar, '.') + .replaceAll("\\.class$", ""); + try { + Class clazz = Class.forName(className, false, classLoader); + if (clazz.getAnnotation(GuiPlugin.class) != null) { + classes.add(clazz); + } + } catch (Throwable e) { + // Classes we can't even load here are not what this test is about. + } + } + } + + assertTrue(classes.size() > 10, "Expected to find the hop-ui @GuiPlugin classes, scanned root"); + return classes; + } + + private static boolean isClassFile(Path path) { + String name = path.getFileName().toString(); + return name.endsWith(".class") && !name.contains("$"); + } +}