Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions ui/src/main/java/org/apache/hop/ui/core/widget/StyledTextVar.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
16 changes: 16 additions & 0 deletions ui/src/main/java/org/apache/hop/ui/core/widget/TextComposite.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -354,25 +348,13 @@ private void collectHighlightSpans(String line, int lineStart, List<int[]> 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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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()}.
*
* <p>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<String> 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<String> 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<Class<?>> 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<Class<?>> findGuiPluginClasses() throws URISyntaxException, IOException {
Path root =
Path.of(
ExecutionLogPanel.class.getProtectionDomain().getCodeSource().getLocation().toURI());
ClassLoader classLoader = GuiPluginWebCompatibilityTest.class.getClassLoader();
List<Class<?>> classes = new ArrayList<>();

try (Stream<Path> files = Files.walk(root)) {
for (Path file :
(Iterable<Path>) 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("$");
}
}
Loading