This is an automated email from the ASF dual-hosted git repository.
hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new 9527b350ff crispy treeview images in hop web, fixes #8434 (#8436)
9527b350ff is described below
commit 9527b350fff47e7537ead621d67aba1bf50afdea
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Thu Sep 17 12:55:22 2026 +0200
crispy treeview images in hop web, fixes #8434 (#8436)
---
.../java/org/apache/hop/core/svg/SvgSizing.java | 108 ++++++++++
.../org/apache/hop/core/svg/SvgSizingTest.java | 130 ++++++++++++
.../ROOT/assets/images/icons/folder-add.svg | 14 +-
.../modules/ROOT/assets/images/icons/folder.svg | 18 +-
.../org/apache/hop/git/GitCommitPerspective.java | 6 +-
.../org/apache/hop/lint/LintStatusFilePainter.java | 21 +-
.../actions/getpop/SelectFolderDialog.java | 2 +
.../UserDefinedJavaClassDialog.java | 8 +-
.../transforms/javascript/ScriptValuesDialog.java | 6 +-
.../pipeline/transforms/script/ScriptDialog.java | 6 +-
.../hop/ui/core/widget/svg/SvgImageFacadeImpl.java | 173 ++++++++++++++++
.../ui/core/widget/svg/SvgImageFacadeImplTest.java | 217 +++++++++++++++++++++
.../hop/ui/core/widget/svg/SvgImageFacadeImpl.java | 45 +++++
.../hop/ui/core/widget/FolderTreeIconsTest.java | 165 ++++++++++++++++
.../org/apache/hop/core/SwtUniversalImage.java | 26 ++-
.../org/apache/hop/core/SwtUniversalImageSvg.java | 28 +++
.../database/dialog/DatabaseExplorerDialog.java | 12 +-
.../org/apache/hop/ui/core/gui/GuiResource.java | 28 ++-
.../apache/hop/ui/core/vfs/HopVfsFileDialog.java | 4 +-
.../org/apache/hop/ui/core/widget/FileTree.java | 3 +-
.../apache/hop/ui/core/widget/FolderTreeIcons.java | 70 +++++++
.../org/apache/hop/ui/core/widget/TreeMemory.java | 2 +-
.../hop/ui/core/widget/svg/SvgImageFacade.java | 88 +++++++++
.../hop/ui/hopgui/palette/GraphPaletteTree.java | 4 +-
.../perspective/database/DatabaseWorkbench.java | 15 +-
.../perspective/explorer/ExplorerPerspective.java | 33 ++--
.../perspective/metadata/MetadataPerspective.java | 13 +-
ui/src/main/resources/ui/images/folder-add.svg | 6 +-
ui/src/main/resources/ui/images/folder-open.svg | 4 +
ui/src/main/resources/ui/images/folder.svg | 10 +-
30 files changed, 1174 insertions(+), 91 deletions(-)
diff --git a/core/src/main/java/org/apache/hop/core/svg/SvgSizing.java
b/core/src/main/java/org/apache/hop/core/svg/SvgSizing.java
new file mode 100644
index 0000000000..06b3d6f39f
--- /dev/null
+++ b/core/src/main/java/org/apache/hop/core/svg/SvgSizing.java
@@ -0,0 +1,108 @@
+/*
+ * 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.core.svg;
+
+import java.math.BigDecimal;
+import java.util.Locale;
+import org.apache.batik.anim.dom.SVGDOMImplementation;
+import org.apache.batik.dom.util.DOMUtilities;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.xml.XmlHandler;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+
+/** Serialises an SVG so that a browser draws it at an exact pixel size. */
+public final class SvgSizing {
+
+ private SvgSizing() {}
+
+ /**
+ * XML of a copy of {@code document} whose root asks for {@code width} x
{@code height} CSS
+ * pixels. Most Hop icons carry {@code width}/{@code height} but no {@code
viewBox}; without one a
+ * browser would clip the drawing to the new size instead of scaling it, so
the intrinsic size
+ * becomes the viewBox. The aspect ratio is not preserved, which is what the
raster renderer in
+ * {@code SwingUniversalImageSvg#render} does as well.
+ *
+ * <p>The copy is served from the web application's own origin, and a
project SVG file is not
+ * necessarily an icon someone vetted, so script elements, event-handler
attributes, {@code
+ * foreignObject} and script URLs are dropped: nothing in a picture may run.
+ *
+ * @param document the SVG, left untouched
+ * @param intrinsicWidth the width the drawing was authored for, in user
units
+ * @param intrinsicHeight the height the drawing was authored for, in user
units
+ * @param width the requested width in CSS pixels
+ * @param height the requested height in CSS pixels
+ * @return the sized SVG as XML, with declaration
+ * @throws HopException when the copy cannot be serialised
+ */
+ public static String toXml(
+ Document document, double intrinsicWidth, double intrinsicHeight, int
width, int height)
+ throws HopException {
+ Document sized =
+ DOMUtilities.deepCloneDocument(document,
SVGDOMImplementation.getDOMImplementation());
+ Element root = sized.getDocumentElement();
+ if (!root.hasAttribute("viewBox")) {
+ root.setAttribute("viewBox", "0 0 " + plain(intrinsicWidth) + " " +
plain(intrinsicHeight));
+ }
+ root.setAttribute("width", String.valueOf(width));
+ root.setAttribute("height", String.valueOf(height));
+ root.setAttribute("preserveAspectRatio", "none");
+ stripActiveContent(root);
+ return XmlHandler.getXmlString(sized, false, false);
+ }
+
+ /** Removes everything a browser could execute from {@code element} and its
descendants. */
+ static void stripActiveContent(Element element) {
+ Node child = element.getFirstChild();
+ while (child != null) {
+ Node next = child.getNextSibling();
+ if (child instanceof Element childElement) {
+ String name = childElement.getLocalName();
+ if ("script".equalsIgnoreCase(name) ||
"foreignObject".equalsIgnoreCase(name)) {
+ element.removeChild(child);
+ } else {
+ stripActiveContent(childElement);
+ }
+ }
+ child = next;
+ }
+ NamedNodeMap attributes = element.getAttributes();
+ for (int i = attributes.getLength() - 1; i >= 0; i--) {
+ Attr attribute = (Attr) attributes.item(i);
+ String name =
+ attribute.getLocalName() == null ? attribute.getName() :
attribute.getLocalName();
+ String value = attribute.getValue().trim().toLowerCase(Locale.ROOT);
+ boolean handler = name.regionMatches(true, 0, "on", 0, 2);
+ boolean scriptUrl =
+ "href".equalsIgnoreCase(name)
+ && (value.startsWith("javascript:")
+ || (value.startsWith("data:") &&
!value.startsWith("data:image/")));
+ if (handler || scriptUrl) {
+ element.removeAttributeNode(attribute);
+ }
+ }
+ }
+
+ /** {@code 24.0} as {@code 24}, {@code 23.5} as {@code 23.5}: no exponent,
no trailing zeros. */
+ private static String plain(double value) {
+ return BigDecimal.valueOf(value).stripTrailingZeros().toPlainString();
+ }
+}
diff --git a/core/src/test/java/org/apache/hop/core/svg/SvgSizingTest.java
b/core/src/test/java/org/apache/hop/core/svg/SvgSizingTest.java
new file mode 100644
index 0000000000..a6b787762d
--- /dev/null
+++ b/core/src/test/java/org/apache/hop/core/svg/SvgSizingTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.core.svg;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
+import org.apache.batik.util.XMLResourceDescriptor;
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+class SvgSizingTest {
+
+ private static Document parse(String xml) throws Exception {
+ SAXSVGDocumentFactory factory =
+ new
SAXSVGDocumentFactory(XMLResourceDescriptor.getXMLParserClassName());
+ return factory.createDocument(
+ "test.svg", new
ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)));
+ }
+
+ private static Element rootOf(String xml) throws Exception {
+ return parse(xml).getDocumentElement();
+ }
+
+ @Test
+ void addsViewBoxFromIntrinsicSizeWhenMissing() throws Exception {
+ Document icon =
+ parse(
+ "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\"
height=\"24\">"
+ + "<path fill=\"#0e3a5a\" d=\"M2 2h20v20H2z\"/></svg>");
+
+ Element root = rootOf(SvgSizing.toXml(icon, 24, 24, 16, 16));
+
+ assertEquals("0 0 24 24", root.getAttribute("viewBox"));
+ assertEquals("16", root.getAttribute("width"));
+ assertEquals("16", root.getAttribute("height"));
+ assertEquals("none", root.getAttribute("preserveAspectRatio"));
+ assertEquals(1, root.getElementsByTagName("path").getLength(), "content is
kept");
+ }
+
+ @Test
+ void keepsAnExistingViewBox() throws Exception {
+ Document icon =
+ parse(
+ "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"32\"
height=\"32\""
+ + " viewBox=\"4 4 24 24\"><rect width=\"1\"
height=\"1\"/></svg>");
+
+ Element root = rootOf(SvgSizing.toXml(icon, 32, 32, 20, 20));
+
+ assertEquals("4 4 24 24", root.getAttribute("viewBox"));
+ assertEquals("20", root.getAttribute("width"));
+ }
+
+ @Test
+ void fractionalIntrinsicSizeIsWrittenPlainly() throws Exception {
+ Document icon = parse("<svg
xmlns=\"http://www.w3.org/2000/svg\"><rect/></svg>");
+
+ Element root = rootOf(SvgSizing.toXml(icon, 23.5, 1000000, 16, 16));
+
+ assertEquals("0 0 23.5 1000000", root.getAttribute("viewBox"));
+ }
+
+ @Test
+ void nothingExecutableSurvivesTheCopy() throws Exception {
+ Document icon =
+ parse(
+ "<svg xmlns=\"http://www.w3.org/2000/svg\"
xmlns:xlink=\"http://www.w3.org/1999/xlink\""
+ + " width=\"24\" height=\"24\" onload=\"alert(1)\">"
+ + "<script>alert(2)</script>"
+ + "<g onclick=\"alert(3)\"><foreignObject><div
xmlns=\"http://www.w3.org/1999/xhtml\">x</div></foreignObject>"
+ + "<a xlink:href=\"javascript:alert(4)\"><rect width=\"1\"
height=\"1\"/></a>"
+ + "<a href=\"data:text/html,x\"/>"
+ + "<use xlink:href=\"#shape\"/>"
+ + "<image href=\"data:image/png;base64,AA==\"/>"
+ + "</g></svg>");
+
+ String xml = SvgSizing.toXml(icon, 24, 24, 16, 16);
+ Element root = rootOf(xml);
+
+ assertFalse(xml.toLowerCase().contains("alert("), xml);
+ assertEquals(0, root.getElementsByTagName("script").getLength());
+ assertEquals(0, root.getElementsByTagName("foreignObject").getLength());
+ assertFalse(root.hasAttribute("onload"));
+ Element g = (Element) root.getElementsByTagName("g").item(0);
+ assertFalse(g.hasAttribute("onclick"));
+ assertEquals(1, root.getElementsByTagName("rect").getLength(), "content is
kept");
+ assertEquals(
+ "#shape",
+ ((Element) root.getElementsByTagName("use").item(0))
+ .getAttributeNS("http://www.w3.org/1999/xlink", "href"),
+ "fragment links are fine");
+ assertTrue(
+ ((Element) root.getElementsByTagName("image").item(0))
+ .getAttribute("href")
+ .startsWith("data:image/"),
+ "embedded images are fine");
+ }
+
+ @Test
+ void sourceDocumentIsNotModified() throws Exception {
+ Document icon = parse("<svg xmlns=\"http://www.w3.org/2000/svg\"
width=\"24\" height=\"24\"/>");
+
+ String xml = SvgSizing.toXml(icon, 24, 24, 16, 16);
+
+ assertTrue(xml.startsWith("<?xml"), "declaration kept for the resource
file");
+ Element original = icon.getDocumentElement();
+ assertEquals("24", original.getAttribute("width"));
+ assertFalse(original.hasAttribute("viewBox"));
+ }
+}
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/icons/folder-add.svg
b/docs/hop-user-manual/modules/ROOT/assets/images/icons/folder-add.svg
index 5d3294e6e6..eaea2a5969 100644
--- a/docs/hop-user-manual/modules/ROOT/assets/images/icons/folder-add.svg
+++ b/docs/hop-user-manual/modules/ROOT/assets/images/icons/folder-add.svg
@@ -1,12 +1,4 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg
- xmlns="http://www.w3.org/2000/svg"
- version="1.1"
- width="24"
- height="24"
- viewBox="0 0 24 24"
->
- <path
- d="M 9.8,3 12,5.2500001 h 8.8 A 2.2,2.2500001 0 0 1 23,7.5000002 V
18.750001 A 2.2,2.2500001 0 0 1 20.8,21 H 3.2 C 1.979,21 1,19.9875 1,18.750001
V 5.2500001 C 1,4.0012501 1.979,3 3.2,3 h 6.6 m 5.5,5.6250003 V 12 H 12 v
2.249999 h 3.3 V 17.625 h 2.2 v -3.375001 h 3.3 V 12 H 17.5 V 8.6250003 Z"
- style="fill:#0e3a5a;fill-opacity:1;stroke-width:1.11245"/>
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24
24">
+ <path fill="#0e3a5a" d="M20 6h-8l-2-2H4c-1.11 0-1.99.89-1.99 2L2 18c0
1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2zm-1
8h-3v3h-2v-3h-3v-2h3V9h2v3h3v2z"/>
</svg>
diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/icons/folder.svg
b/docs/hop-user-manual/modules/ROOT/assets/images/icons/folder.svg
index ffc3075302..a176cd8dfc 100644
--- a/docs/hop-user-manual/modules/ROOT/assets/images/icons/folder.svg
+++ b/docs/hop-user-manual/modules/ROOT/assets/images/icons/folder.svg
@@ -1,16 +1,4 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg
- xmlns="http://www.w3.org/2000/svg"
- height="24"
- viewBox="0 0 24 24"
- width="24"
- version="1.1"
->
- <path
- d="M0 0h24v24H0z"
- fill="none"
- />
- <path
- d="m 20.799999,5.2499999 h -8.8 L 9.8,3 H 3.2 C 1.99,3
1.0110001,4.0124999 1.0110001,5.2499999 L 1,18.749999 C 1,19.987499 1.99,21
3.2,21 H 20.799999 C 22.01,21 23,19.987499 23,18.749999 V 7.5000001 C 23,6.2625
22.01,5.2499999 20.799999,5.2499999 Z m 0,13.4999991 H 3.2 V 7.5000001 h
17.599999 z"
- style="fill:#0e3a5a;fill-opacity:1;stroke-width:1.11243"/>
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24
24">
+ <path fill="#0e3a5a" d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2
2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>
</svg>
diff --git
a/plugins/misc/git/src/main/java/org/apache/hop/git/GitCommitPerspective.java
b/plugins/misc/git/src/main/java/org/apache/hop/git/GitCommitPerspective.java
index 6202c45141..5a83685ab3 100644
---
a/plugins/misc/git/src/main/java/org/apache/hop/git/GitCommitPerspective.java
+++
b/plugins/misc/git/src/main/java/org/apache/hop/git/GitCommitPerspective.java
@@ -51,6 +51,7 @@ import org.apache.hop.ui.core.gui.GuiResource;
import org.apache.hop.ui.core.gui.GuiToolbarWidgets;
import org.apache.hop.ui.core.gui.HopNamespace;
import org.apache.hop.ui.core.gui.IToolbarContainer;
+import org.apache.hop.ui.core.widget.FolderTreeIcons;
import org.apache.hop.ui.hopgui.HopGui;
import org.apache.hop.ui.hopgui.HopGuiKeyHandler;
import org.apache.hop.ui.hopgui.ToolbarFacade;
@@ -245,6 +246,7 @@ public class GitCommitPerspective implements
IHopPerspective {
PropsUi.setLook(wToolBar, Props.WIDGET_STYLE_TOOLBAR);
wTree = new Tree(composite, SWT.CHECK | SWT.V_SCROLL | SWT.H_SCROLL);
+ FolderTreeIcons.install(wTree);
wTree.setHeaderVisible(false);
wTree.setLayoutData(FormDataBuilder.builder().fullWidth().top(wToolBar).bottom().result());
wTree.addListener(SWT.Selection, this::onSelection);
@@ -464,7 +466,7 @@ public class GitCommitPerspective implements
IHopPerspective {
fileType.openFile(hopGui, path, hopGui.getVariables());
perspective.activate();
} else {
- item.setExpanded(!item.getExpanded());
+ FolderTreeIcons.setExpanded(item, !item.getExpanded());
}
}
} catch (Exception e) {
@@ -1215,7 +1217,7 @@ public class GitCommitPerspective implements
IHopPerspective {
setRootItemLabel(untrackedRootItem, UNTRACKED_LABEL);
for (TreeItem rootItem : wTree.getItems()) {
- rootItem.setExpanded(true);
+ FolderTreeIcons.setExpanded(rootItem, true);
}
// Offer all the work in progress for the next commit
diff --git
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintStatusFilePainter.java
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintStatusFilePainter.java
index e018463d2c..8c7d557718 100644
---
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintStatusFilePainter.java
+++
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintStatusFilePainter.java
@@ -25,6 +25,7 @@ import org.apache.hop.core.logging.ILogChannel;
import org.apache.hop.core.logging.LogChannel;
import org.apache.hop.core.util.Utils;
import org.apache.hop.ui.core.gui.GuiResource;
+import org.apache.hop.ui.core.widget.svg.SvgImageFacade;
import org.apache.hop.ui.hopgui.HopGui;
import org.apache.hop.ui.hopgui.SessionDisplay;
import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerPerspective;
@@ -34,6 +35,7 @@ import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.PaletteData;
import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
@@ -287,7 +289,7 @@ public class LintStatusFilePainter implements
IExplorerFilePaintListener {
}
boolean noBase = base == null || base.isDisposed();
- org.eclipse.swt.graphics.Rectangle bounds = noBase ? null :
base.getBounds();
+ Rectangle bounds = noBase ? null : base.getBounds();
// Rasterize the badge at the size it will occupy on this icon, so
nothing is resampled.
Image lintIcon =
badgeIcon(
@@ -349,18 +351,27 @@ public class LintStatusFilePainter implements
IExplorerFilePaintListener {
* NullPointerException, and disposing it then fails with "A factory-created
resource cannot be
* disposed", which is the exception that reaches the log. Working on the
{@link ImageData} of
* both icons needs no drawing surface at all, so Hop Web gets the same
badges as the desktop.
+ *
+ * <p>On Hop Web the icons are vectors the browser renders, so the badge is
first tried as a
+ * vector overlay ({@link SvgImageFacade#overlay}) and stays sharp on a
high-DPI screen; pixel
+ * compositing is the fallback for anything that is not vector-backed.
*/
private Image createCompositeIcon(Image originalIcon, Image lintIcon) {
try {
if (display == null || display.isDisposed()) {
return null;
}
- ImageData baseData = SwtUniversalImage.getImageDataAtZoom(originalIcon,
100);
- if (baseData == null) {
+ Rectangle bounds = originalIcon.getBounds();
+ int badgeSize = badgeSizeFor(bounds.width, bounds.height);
+ if (badgeSize <= 0) {
return null;
}
- int badgeSize = badgeSizeFor(baseData.width, baseData.height);
- if (badgeSize <= 0) {
+ Image vector = SvgImageFacade.overlay(display, originalIcon, lintIcon,
badgeSize, 1);
+ if (vector != null) {
+ return vector;
+ }
+ ImageData baseData = SwtUniversalImage.getImageDataAtZoom(originalIcon,
100);
+ if (baseData == null) {
return null;
}
// Composited again for every zoom the platform asks for, out of what
both icons themselves
diff --git
a/plugins/misc/mail/src/main/java/org/apache/hop/mail/workflow/actions/getpop/SelectFolderDialog.java
b/plugins/misc/mail/src/main/java/org/apache/hop/mail/workflow/actions/getpop/SelectFolderDialog.java
index 8426581c47..a57c8feb52 100644
---
a/plugins/misc/mail/src/main/java/org/apache/hop/mail/workflow/actions/getpop/SelectFolderDialog.java
+++
b/plugins/misc/mail/src/main/java/org/apache/hop/mail/workflow/actions/getpop/SelectFolderDialog.java
@@ -24,6 +24,7 @@ import org.apache.hop.ui.core.PropsUi;
import org.apache.hop.ui.core.dialog.BaseDialog;
import org.apache.hop.ui.core.gui.GuiResource;
import org.apache.hop.ui.core.gui.WindowProperty;
+import org.apache.hop.ui.core.widget.FolderTreeIcons;
import org.apache.hop.ui.core.widget.HopTree;
import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
import org.eclipse.swt.SWT;
@@ -74,6 +75,7 @@ public class SelectFolderDialog extends Dialog {
// Tree
wTree = new HopTree(shell, SWT.SINGLE | SWT.BORDER);
+ FolderTreeIcons.install(wTree);
PropsUi.setLook(wTree);
// Double click in tree: select the value
wTree.addListener(SWT.DefaultSelection, e -> ok());
diff --git
a/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassDialog.java
b/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassDialog.java
index bb58a9bc13..2e922c1169 100644
---
a/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassDialog.java
+++
b/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassDialog.java
@@ -59,6 +59,7 @@ import org.apache.hop.ui.core.dialog.MessageDialogWithToggle;
import org.apache.hop.ui.core.dialog.PreviewRowsDialog;
import org.apache.hop.ui.core.gui.GuiResource;
import org.apache.hop.ui.core.widget.ColumnInfo;
+import org.apache.hop.ui.core.widget.FolderTreeIcons;
import org.apache.hop.ui.core.widget.HopTree;
import org.apache.hop.ui.core.widget.JavaStyledTextComp;
import org.apache.hop.ui.core.widget.NamingSchemeTypes;
@@ -286,6 +287,7 @@ public class UserDefinedJavaClassDialog extends
BaseTransformDialog {
// Tree View Test
wTree = new HopTree(wTop, SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL);
+ FolderTreeIcons.install(wTree);
PropsUi.setLook(wTree);
FormData fdlTree = new FormData();
fdlTree.left = new FormAttachment(0, 0);
@@ -458,21 +460,21 @@ public class UserDefinedJavaClassDialog extends
BaseTransformDialog {
itemWaitFieldsIn.setText(
BaseMessages.getString(PKG,
CONST_USER_DEFINED_JAVA_CLASS_DIALOG_GETTING_FIELDS_LABEL));
itemWaitFieldsIn.setForeground(guiResource.getColorDirectory());
- itemInput.setExpanded(true);
+ FolderTreeIcons.setExpanded(itemInput, true);
// Display waiting message for info
TreeItem itemWaitFieldsInfo = new TreeItem(itemInfo, SWT.NULL);
itemWaitFieldsInfo.setText(
BaseMessages.getString(PKG,
CONST_USER_DEFINED_JAVA_CLASS_DIALOG_GETTING_FIELDS_LABEL));
itemWaitFieldsInfo.setForeground(guiResource.getColorDirectory());
- itemInfo.setExpanded(true);
+ FolderTreeIcons.setExpanded(itemInfo, true);
// Display waiting message for output
TreeItem itemWaitFieldsOut = new TreeItem(itemOutput, SWT.NULL);
itemWaitFieldsOut.setText(
BaseMessages.getString(PKG,
CONST_USER_DEFINED_JAVA_CLASS_DIALOG_GETTING_FIELDS_LABEL));
itemWaitFieldsOut.setForeground(guiResource.getColorDirectory());
- itemOutput.setExpanded(true);
+ FolderTreeIcons.setExpanded(itemOutput, true);
//
// Search the fields in the background
diff --git
a/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDialog.java
b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDialog.java
index ebaf272c09..cc94066688 100644
---
a/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDialog.java
+++
b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDialog.java
@@ -52,6 +52,7 @@ import org.apache.hop.ui.core.dialog.PreviewRowsDialog;
import org.apache.hop.ui.core.gui.GuiResource;
import org.apache.hop.ui.core.widget.ColumnInfo;
import org.apache.hop.ui.core.widget.ComboVar;
+import org.apache.hop.ui.core.widget.FolderTreeIcons;
import org.apache.hop.ui.core.widget.HopTree;
import org.apache.hop.ui.core.widget.JavaScriptStyledTextComp;
import org.apache.hop.ui.core.widget.NamingSchemeTypes;
@@ -251,6 +252,7 @@ public class ScriptValuesDialog extends BaseTransformDialog
{
// Tree View Test
wTree = new HopTree(wTop, SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL);
+ FolderTreeIcons.install(wTree);
PropsUi.setLook(wTree);
FormData fdlTree = new FormData();
fdlTree.left = new FormAttachment(0, 0);
@@ -498,14 +500,14 @@ public class ScriptValuesDialog extends
BaseTransformDialog {
itemWaitFieldsIn.setText(
BaseMessages.getString(PKG,
"ScriptValuesDialogMod.GettingFields.Label"));
itemWaitFieldsIn.setForeground(guiresource.getColorDirectory());
- iteminput.setExpanded(true);
+ FolderTreeIcons.setExpanded(iteminput, true);
// Display waiting message for output
TreeItem itemWaitFieldsOut = new TreeItem(itemoutput, SWT.NULL);
itemWaitFieldsOut.setText(
BaseMessages.getString(PKG,
"ScriptValuesDialogMod.GettingFields.Label"));
itemWaitFieldsOut.setForeground(guiresource.getColorDirectory());
- itemoutput.setExpanded(true);
+ FolderTreeIcons.setExpanded(itemoutput, true);
//
// Search the fields in the background
diff --git
a/plugins/transforms/script/src/main/java/org/apache/hop/pipeline/transforms/script/ScriptDialog.java
b/plugins/transforms/script/src/main/java/org/apache/hop/pipeline/transforms/script/ScriptDialog.java
index 18aae2ee7a..366a51a0e2 100644
---
a/plugins/transforms/script/src/main/java/org/apache/hop/pipeline/transforms/script/ScriptDialog.java
+++
b/plugins/transforms/script/src/main/java/org/apache/hop/pipeline/transforms/script/ScriptDialog.java
@@ -36,6 +36,7 @@ import org.apache.hop.ui.core.PropsUi;
import org.apache.hop.ui.core.dialog.MessageBox;
import org.apache.hop.ui.core.gui.GuiResource;
import org.apache.hop.ui.core.widget.ColumnInfo;
+import org.apache.hop.ui.core.widget.FolderTreeIcons;
import org.apache.hop.ui.core.widget.HopTree;
import org.apache.hop.ui.core.widget.ScriptStyledTextComp;
import org.apache.hop.ui.core.widget.StyledTextComp;
@@ -222,6 +223,7 @@ public class ScriptDialog extends BaseTransformDialog {
// Tree View Test
wTree = new HopTree(wTop, SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL);
+ FolderTreeIcons.install(wTree);
PropsUi.setLook(wTree);
FormData fdlTree = new FormData();
fdlTree.left = new FormAttachment(0, 0);
@@ -424,13 +426,13 @@ public class ScriptDialog extends BaseTransformDialog {
TreeItem itemWaitFieldsIn = new TreeItem(itemInput, SWT.NULL);
itemWaitFieldsIn.setText(BaseMessages.getString(PKG,
"ScriptDialog.GettingFields.Label"));
itemWaitFieldsIn.setForeground(GuiResource.getInstance().getColorDirectory());
- itemInput.setExpanded(true);
+ FolderTreeIcons.setExpanded(itemInput, true);
// Display waiting message for output
TreeItem itemWaitFieldsOut = new TreeItem(itemOutput, SWT.NULL);
itemWaitFieldsOut.setText(BaseMessages.getString(PKG,
"ScriptDialog.GettingFields.Label"));
itemWaitFieldsOut.setForeground(GuiResource.getInstance().getColorDirectory());
- itemOutput.setExpanded(true);
+ FolderTreeIcons.setExpanded(itemOutput, true);
//
// Search the fields in the background
diff --git
a/rap/src/main/java/org/apache/hop/ui/core/widget/svg/SvgImageFacadeImpl.java
b/rap/src/main/java/org/apache/hop/ui/core/widget/svg/SvgImageFacadeImpl.java
new file mode 100644
index 0000000000..02b28e8cef
--- /dev/null
+++
b/rap/src/main/java/org/apache/hop/ui/core/widget/svg/SvgImageFacadeImpl.java
@@ -0,0 +1,173 @@
+/*
+ * 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.core.widget.svg;
+
+import java.awt.geom.Dimension2D;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.HexFormat;
+import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
+import org.apache.batik.anim.dom.SVGDOMImplementation;
+import org.apache.batik.util.XMLResourceDescriptor;
+import org.apache.hop.core.SwingUniversalImageSvg;
+import org.apache.hop.core.SwtUniversalImage;
+import org.apache.hop.core.logging.LogChannel;
+import org.apache.hop.core.svg.SvgImage;
+import org.apache.hop.core.svg.SvgSizing;
+import org.apache.hop.core.xml.XmlHandler;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.rap.rwt.internal.resources.ResourceDirectory;
+import org.eclipse.rap.rwt.service.ResourceManager;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.internal.graphics.ExternalImageDescriptor;
+import org.eclipse.swt.internal.graphics.ImageFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+/**
+ * Publishes the sized SVG as an application resource and returns an {@link
Image} whose client URL
+ * is that resource while its bounds stay the logical size. The browser then
draws the vector at
+ * device resolution instead of scaling up a 1x bitmap.
+ *
+ * <p>The resource name is a digest of the sized XML, so one file serves every
session, size and
+ * theme (the dark-mode colour swap is already in the document) and never
needs invalidating. The
+ * registered file is also where such an image's pixels come from when a
caller needs them ({@link
+ * #rasterizeInternal}) and what a badge overlay is built from ({@link
#overlayInternal}): nothing
+ * has to be remembered per image.
+ */
+public class SvgImageFacadeImpl extends SvgImageFacade {
+
+ static final String RESOURCE_PREFIX = "hop/svg/";
+ private static final String LOCATION_PREFIX = ResourceDirectory.DIRNAME +
"/";
+
+ /** RWT's register() is not atomic; two sessions publishing the same icon
must not interleave. */
+ private static final Object REGISTER_LOCK = new Object();
+
+ @Override
+ protected Image createImageInternal(
+ Device device, Document document, Dimension2D intrinsicSize, int width,
int height) {
+ try {
+ String svg =
+ SvgSizing.toXml(
+ document, intrinsicSize.getWidth(), intrinsicSize.getHeight(),
width, height);
+ return publish(device, svg, width, height);
+ } catch (Exception e) {
+ // Typically no RAP context on this thread; the caller falls back to a
bitmap.
+ LogChannel.UI.logDebug("Falling back to a bitmap for an SVG icon: " +
e.getMessage());
+ return null;
+ }
+ }
+
+ @Override
+ protected ImageData rasterizeInternal(Image image, int zoom) {
+ try {
+ Document document = publishedSvg(image);
+ if (document == null) {
+ return null;
+ }
+ Rectangle bounds = image.getBounds();
+ BufferedImage bitmap =
+ new SwingUniversalImageSvg(new SvgImage(document))
+ .getAsBitmapForSize(
+ SwtUniversalImage.pixelSize(bounds.width, zoom),
+ SwtUniversalImage.pixelSize(bounds.height, zoom));
+ return SwtUniversalImage.toImageData(bitmap);
+ } catch (Exception e) {
+ LogChannel.UI.logDebug("Could not rasterise a vector icon: " +
e.getMessage());
+ return null;
+ }
+ }
+
+ @Override
+ protected Image overlayInternal(
+ Device device, Image base, Image badge, int badgeSize, int margin) {
+ try {
+ Document baseDocument = publishedSvg(base);
+ Document badgeDocument = publishedSvg(badge);
+ if (baseDocument == null || badgeDocument == null) {
+ return null;
+ }
+ Rectangle bounds = base.getBounds();
+ // Nested <svg> elements keep each icon's own viewBox, so both scale
exactly as they do alone.
+ Document composite =
+ SVGDOMImplementation.getDOMImplementation()
+ .createDocument(SVGDOMImplementation.SVG_NAMESPACE_URI, "svg",
null);
+ Element root = composite.getDocumentElement();
+ root.setAttribute("width", String.valueOf(bounds.width));
+ root.setAttribute("height", String.valueOf(bounds.height));
+ root.setAttribute("viewBox", "0 0 " + bounds.width + " " +
bounds.height);
+ root.appendChild(composite.importNode(baseDocument.getDocumentElement(),
true));
+ Element badgeElement =
+ (Element) composite.importNode(badgeDocument.getDocumentElement(),
true);
+ badgeElement.setAttribute("x", String.valueOf(bounds.width - badgeSize -
margin));
+ badgeElement.setAttribute("y", String.valueOf(bounds.height - badgeSize
- margin));
+ badgeElement.setAttribute("width", String.valueOf(badgeSize));
+ badgeElement.setAttribute("height", String.valueOf(badgeSize));
+ root.appendChild(badgeElement);
+ return publish(
+ device, XmlHandler.getXmlString(composite, false, false),
bounds.width, bounds.height);
+ } catch (Exception e) {
+ LogChannel.UI.logDebug("Could not overlay a vector icon: " +
e.getMessage());
+ return null;
+ }
+ }
+
+ /** Registers the XML once under its digest and returns an image of the
given bounds for it. */
+ private static Image publish(Device device, String svg, int width, int
height) throws Exception {
+ byte[] bytes = svg.getBytes(StandardCharsets.UTF_8);
+ String name = RESOURCE_PREFIX + digest(bytes) + ".svg";
+ ResourceManager resourceManager = RWT.getResourceManager();
+ synchronized (REGISTER_LOCK) {
+ if (!resourceManager.isRegistered(name)) {
+ resourceManager.register(name, new ByteArrayInputStream(bytes));
+ }
+ }
+ String location = resourceManager.getLocation(name);
+ return new ExternalImageDescriptor(location, width,
height).createImage(device);
+ }
+
+ /** The registered SVG behind an image made by {@link #publish}, or null for
any other image. */
+ private static Document publishedSvg(Image image) throws Exception {
+ if (image == null || image.isDisposed()) {
+ return null;
+ }
+ String path = ImageFactory.getImagePath(image);
+ if (path == null || !path.startsWith(LOCATION_PREFIX + RESOURCE_PREFIX)) {
+ return null;
+ }
+ String name = path.substring(LOCATION_PREFIX.length());
+ try (InputStream stream =
RWT.getResourceManager().getRegisteredContent(name)) {
+ if (stream == null) {
+ return null;
+ }
+ return new
SAXSVGDocumentFactory(XMLResourceDescriptor.getXMLParserClassName())
+ .createDocument(name, stream);
+ }
+ }
+
+ private static String digest(byte[] content) throws Exception {
+ byte[] hash = MessageDigest.getInstance("SHA-256").digest(content);
+ return HexFormat.of().formatHex(hash, 0, 16);
+ }
+}
diff --git
a/rap/src/test/java/org/apache/hop/ui/core/widget/svg/SvgImageFacadeImplTest.java
b/rap/src/test/java/org/apache/hop/ui/core/widget/svg/SvgImageFacadeImplTest.java
new file mode 100644
index 0000000000..dd038688f5
--- /dev/null
+++
b/rap/src/test/java/org/apache/hop/ui/core/widget/svg/SvgImageFacadeImplTest.java
@@ -0,0 +1,217 @@
+/*
+ * 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.core.widget.svg;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.awt.Dimension;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
+import org.apache.batik.util.XMLResourceDescriptor;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.rap.rwt.service.ResourceManager;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.internal.graphics.ImageFactory;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.MockedStatic;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+class SvgImageFacadeImplTest {
+
+ private static final String ICON =
+ "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\">"
+ + "<path fill=\"#0e3a5a\" d=\"M2 2h20v20H2z\"/></svg>";
+
+ private MockedStatic<RWT> rwt;
+ private ResourceManager resourceManager;
+ private final SvgImageFacadeImpl facade = new SvgImageFacadeImpl();
+ private final Device device = mock(Device.class);
+
+ /** What the mock resource manager has been handed, so it can serve it back
like RWT does. */
+ private final Map<String, byte[]> registered = new HashMap<>();
+
+ @BeforeEach
+ void mockRwt() throws Exception {
+ resourceManager = mock(ResourceManager.class);
+ when(resourceManager.getLocation(anyString()))
+ .thenAnswer(invocation -> "rwt-resources/" +
invocation.getArgument(0));
+ doAnswer(
+ invocation -> {
+ InputStream stream = invocation.getArgument(1);
+ registered.put(invocation.getArgument(0), stream.readAllBytes());
+ return null;
+ })
+ .when(resourceManager)
+ .register(anyString(), any(InputStream.class));
+ when(resourceManager.getRegisteredContent(anyString()))
+ .thenAnswer(
+ invocation -> {
+ byte[] bytes = registered.get(invocation.<String>getArgument(0));
+ return bytes == null ? null : new ByteArrayInputStream(bytes);
+ });
+ rwt = mockStatic(RWT.class);
+ rwt.when(RWT::getResourceManager).thenReturn(resourceManager);
+ }
+
+ @AfterEach
+ void closeRwt() {
+ rwt.close();
+ }
+
+ private static Document parse(String xml) throws Exception {
+ return new
SAXSVGDocumentFactory(XMLResourceDescriptor.getXMLParserClassName())
+ .createDocument("icon.svg", new
ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)));
+ }
+
+ @Test
+ void publishesTheSizedSvgAndPointsALogicallySizedImageAtIt() throws
Exception {
+ Image image = facade.createImageInternal(device, parse(ICON), new
Dimension(24, 24), 16, 16);
+
+ assertNotNull(image);
+ assertEquals(new Rectangle(0, 0, 16, 16), image.getBounds(), "tree cells
stay 16px wide");
+
+ ArgumentCaptor<String> name = ArgumentCaptor.forClass(String.class);
+ verify(resourceManager).register(name.capture(), any(InputStream.class));
+ assertTrue(name.getValue().startsWith(SvgImageFacadeImpl.RESOURCE_PREFIX));
+ assertTrue(name.getValue().endsWith(".svg"));
+ assertEquals("rwt-resources/" + name.getValue(),
ImageFactory.getImagePath(image));
+
+ String svg = new String(registered.get(name.getValue()),
StandardCharsets.UTF_8);
+ assertTrue(svg.contains("width=\"16\""), svg);
+ assertTrue(svg.contains("height=\"16\""), svg);
+ assertTrue(svg.contains("viewBox=\"0 0 24 24\""), svg);
+ }
+
+ @Test
+ void sameSvgAndSizeReuseTheRegisteredResource() throws Exception {
+ Image first = facade.createImageInternal(device, parse(ICON), new
Dimension(24, 24), 16, 16);
+ when(resourceManager.isRegistered(anyString())).thenReturn(true);
+
+ Image second = facade.createImageInternal(device, parse(ICON), new
Dimension(24, 24), 16, 16);
+
+ verify(resourceManager).register(anyString(), any(InputStream.class));
+ assertEquals(ImageFactory.getImagePath(first),
ImageFactory.getImagePath(second));
+ }
+
+ @Test
+ void differentSizeOrContentGetsItsOwnResource() throws Exception {
+ Image small = facade.createImageInternal(device, parse(ICON), new
Dimension(24, 24), 16, 16);
+ Image large = facade.createImageInternal(device, parse(ICON), new
Dimension(24, 24), 32, 32);
+ Image other =
+ facade.createImageInternal(
+ device, parse(ICON.replace("#0e3a5a", "#c8e7fa")), new
Dimension(24, 24), 16, 16);
+
+ String smallPath = ImageFactory.getImagePath(small);
+ assertNotEquals(smallPath, ImageFactory.getImagePath(large));
+ assertNotEquals(smallPath, ImageFactory.getImagePath(other));
+ }
+
+ @Test
+ void vectorImagesCanStillBeRasterisedAtAnyZoom() throws Exception {
+ Image image = facade.createImageInternal(device, parse(ICON), new
Dimension(24, 24), 16, 16);
+
+ ImageData at100 = facade.rasterizeInternal(image, 100);
+ ImageData at200 = facade.rasterizeInternal(image, 200);
+
+ assertEquals(16, at100.width);
+ assertEquals(16, at100.height);
+ assertEquals(32, at200.width);
+ // The square path covers the centre with the icon's fill colour.
+ assertEquals(new RGB(0x0e, 0x3a, 0x5a),
at100.palette.getRGB(at100.getPixel(8, 8)));
+ assertEquals(255, at200.getAlpha(16, 16));
+ assertEquals(0, at100.getAlpha(0, 0), "outside the path stays
transparent");
+ }
+
+ @Test
+ void rasterisingAnImageThatIsNotVectorBackedIsNotOurBusiness() {
+ Image foreign = mock(Image.class);
+ assertNull(facade.rasterizeInternal(foreign, 100));
+ }
+
+ @Test
+ void overlayNestsTheBadgeInTheBottomRightCornerOfTheBase() throws Exception {
+ Image base = facade.createImageInternal(device, parse(ICON), new
Dimension(24, 24), 16, 16);
+ Image badge =
+ facade.createImageInternal(
+ device, parse(ICON.replace("#0e3a5a", "#ff0000")), new
Dimension(24, 24), 8, 8);
+
+ Image badged = facade.overlayInternal(device, base, badge, 8, 1);
+
+ assertNotNull(badged);
+ assertEquals(new Rectangle(0, 0, 16, 16), badged.getBounds());
+ String name =
ImageFactory.getImagePath(badged).substring("rwt-resources/".length());
+ Element root =
+ parse(new String(registered.get(name),
StandardCharsets.UTF_8)).getDocumentElement();
+ NodeList nested = root.getElementsByTagName("svg");
+ assertEquals(2, nested.getLength(), "base and badge as nested <svg>");
+ Element nestedBase = (Element) nested.item(0);
+ assertEquals("16", nestedBase.getAttribute("width"));
+ assertEquals("0 0 24 24", nestedBase.getAttribute("viewBox"), "keeps its
own scaling");
+ Element nestedBadge = (Element) nested.item(1);
+ assertEquals("7", nestedBadge.getAttribute("x"));
+ assertEquals("7", nestedBadge.getAttribute("y"));
+ assertEquals("8", nestedBadge.getAttribute("width"));
+ assertEquals("8", nestedBadge.getAttribute("height"));
+
+ ImageData pixels = facade.rasterizeInternal(badged, 100);
+ assertEquals(new RGB(0xff, 0, 0),
pixels.palette.getRGB(pixels.getPixel(11, 11)), "badge");
+ assertEquals(new RGB(0x0e, 0x3a, 0x5a),
pixels.palette.getRGB(pixels.getPixel(3, 3)), "base");
+ }
+
+ @Test
+ void overlayNeedsBothImagesToBeVectorBacked() throws Exception {
+ Image base = facade.createImageInternal(device, parse(ICON), new
Dimension(24, 24), 16, 16);
+ assertNull(facade.overlayInternal(device, base, mock(Image.class), 8, 1));
+ assertNull(facade.overlayInternal(device, mock(Image.class), base, 8, 1));
+ }
+
+ @Test
+ void withoutARapContextTheCallerRasterises() throws Exception {
+ rwt.when(RWT::getResourceManager).thenThrow(new IllegalStateException("No
context"));
+
+ Image image = facade.createImageInternal(device, parse(ICON), new
Dimension(24, 24), 16, 16);
+
+ assertNull(image);
+ verify(resourceManager, never()).register(anyString(),
any(InputStream.class));
+ }
+}
diff --git
a/rcp/src/main/java/org/apache/hop/ui/core/widget/svg/SvgImageFacadeImpl.java
b/rcp/src/main/java/org/apache/hop/ui/core/widget/svg/SvgImageFacadeImpl.java
new file mode 100644
index 0000000000..22805389dd
--- /dev/null
+++
b/rcp/src/main/java/org/apache/hop/ui/core/widget/svg/SvgImageFacadeImpl.java
@@ -0,0 +1,45 @@
+/*
+ * 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.core.widget.svg;
+
+import java.awt.geom.Dimension2D;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.w3c.dom.Document;
+
+/** Desktop SWT rasterises per monitor zoom itself, so there is nothing to do
here. */
+public class SvgImageFacadeImpl extends SvgImageFacade {
+
+ @Override
+ protected Image createImageInternal(
+ Device device, Document document, Dimension2D intrinsicSize, int width,
int height) {
+ return null;
+ }
+
+ @Override
+ protected ImageData rasterizeInternal(Image image, int zoom) {
+ return null;
+ }
+
+ @Override
+ protected Image overlayInternal(
+ Device device, Image base, Image badge, int badgeSize, int margin) {
+ return null;
+ }
+}
diff --git
a/rcp/src/test/java/org/apache/hop/ui/core/widget/FolderTreeIconsTest.java
b/rcp/src/test/java/org/apache/hop/ui/core/widget/FolderTreeIconsTest.java
new file mode 100644
index 0000000000..1f631b7172
--- /dev/null
+++ b/rcp/src/test/java/org/apache/hop/ui/core/widget/FolderTreeIconsTest.java
@@ -0,0 +1,165 @@
+/*
+ * 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.core.widget;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.atomic.AtomicReference;
+import org.apache.hop.ui.core.gui.GuiResource;
+import org.apache.hop.ui.testing.SwtBotTestBase;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeItem;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/** Folder tree items show the closed folder while collapsed and the open one
while expanded. */
+@Tag("uitest")
+class FolderTreeIconsTest extends SwtBotTestBase {
+
+ private static final String TREE_NAME = "FolderTreeIconsTest";
+
+ private static Image closed() {
+ return GuiResource.getInstance().getImageFolder();
+ }
+
+ private static Image open() {
+ return GuiResource.getInstance().getImageFolderOpen();
+ }
+
+ /** root(folder) > child(folder) > leaf(file); plus a sibling category with
a non-folder image. */
+ private static Tree buildTree(org.eclipse.swt.widgets.Shell shell) {
+ shell.setLayout(new FillLayout());
+ Tree tree = new Tree(shell, SWT.SINGLE);
+ TreeItem root = new TreeItem(tree, SWT.NONE);
+ root.setText("root");
+ root.setImage(closed());
+ TreeItem child = new TreeItem(root, SWT.NONE);
+ child.setText("child");
+ child.setImage(closed());
+ TreeItem leaf = new TreeItem(child, SWT.NONE);
+ leaf.setText("leaf");
+ leaf.setImage(GuiResource.getInstance().getImageFile());
+ TreeItem other = new TreeItem(tree, SWT.NONE);
+ other.setText("other");
+ other.setImage(GuiResource.getInstance().getImageFile());
+ new TreeItem(other, SWT.NONE).setText("other-child");
+ FolderTreeIcons.install(tree);
+ return tree;
+ }
+
+ private static void fire(Tree tree, int type, TreeItem item) {
+ Event event = new Event();
+ event.item = item;
+ tree.notifyListeners(type, event);
+ }
+
+ @Test
+ void userExpandAndCollapseSwapTheFolderIcon() {
+ AtomicReference<Tree> treeRef = new AtomicReference<>();
+ withScene(
+ shell -> treeRef.set(buildTree(shell)),
+ bot ->
+ display.syncExec(
+ () -> {
+ Tree tree = treeRef.get();
+ TreeItem root = tree.getItem(0);
+ TreeItem other = tree.getItem(1);
+
+ fire(tree, SWT.Expand, root);
+ assertSame(open(), root.getImage(), "expanded folder shows
the open icon");
+ assertSame(closed(), root.getItem(0).getImage(), "children
are not touched");
+
+ fire(tree, SWT.Collapse, root);
+ assertSame(closed(), root.getImage(), "collapsed folder
shows the closed icon");
+
+ Image otherImage = other.getImage();
+ fire(tree, SWT.Expand, other);
+ assertSame(otherImage, other.getImage(), "non-folder images
are left alone");
+ }));
+ }
+
+ @Test
+ void programmaticSetExpandedUpdatesStateAndIcon() {
+ AtomicReference<Tree> treeRef = new AtomicReference<>();
+ withScene(
+ shell -> treeRef.set(buildTree(shell)),
+ bot ->
+ display.syncExec(
+ () -> {
+ TreeItem root = treeRef.get().getItem(0);
+ TreeItem child = root.getItem(0);
+
+ FolderTreeIcons.setExpanded(root, true);
+ assertTrue(root.getExpanded());
+ assertSame(open(), root.getImage());
+
+ FolderTreeIcons.setExpanded(child, true);
+ assertSame(open(), child.getImage());
+
+ FolderTreeIcons.setExpanded(root, false);
+ assertFalse(root.getExpanded());
+ assertSame(closed(), root.getImage());
+ assertSame(open(), child.getImage(), "collapsing the parent
leaves the child");
+ }));
+ }
+
+ @Test
+ void expandingAnEmptyFolderKeepsTheClosedIcon() {
+ AtomicReference<Tree> treeRef = new AtomicReference<>();
+ withScene(
+ shell -> treeRef.set(buildTree(shell)),
+ bot ->
+ display.syncExec(
+ () -> {
+ TreeItem empty = new TreeItem(treeRef.get(), SWT.NONE);
+ empty.setText("empty");
+ empty.setImage(closed());
+
+ FolderTreeIcons.setExpanded(empty, true);
+
+ assertFalse(empty.getExpanded(), "SWT ignores expanding a
childless item");
+ assertSame(closed(), empty.getImage(), "so the icon must not
pretend otherwise");
+ }));
+ }
+
+ @Test
+ void treeMemoryRestoreShowsTheOpenIcon() {
+ AtomicReference<Tree> treeRef = new AtomicReference<>();
+ withScene(
+ shell -> treeRef.set(buildTree(shell)),
+ bot ->
+ display.syncExec(
+ () -> {
+ Tree tree = treeRef.get();
+ TreeItem root = tree.getItem(0);
+ TreeMemory.getInstance().storeExpanded(TREE_NAME, root,
true);
+
+ TreeMemory.setExpandedFromMemory(tree, TREE_NAME);
+
+ assertTrue(root.getExpanded());
+ assertSame(open(), root.getImage(), "memory-restored folders
open their icon");
+ assertSame(closed(), root.getItem(0).getImage());
+ }));
+ }
+}
diff --git a/ui/src/main/java/org/apache/hop/core/SwtUniversalImage.java
b/ui/src/main/java/org/apache/hop/core/SwtUniversalImage.java
index 750ee54344..d1b679bba5 100644
--- a/ui/src/main/java/org/apache/hop/core/SwtUniversalImage.java
+++ b/ui/src/main/java/org/apache/hop/core/SwtUniversalImage.java
@@ -20,7 +20,9 @@ package org.apache.hop.core;
import java.awt.image.BufferedImage;
import java.util.Map;
import java.util.TreeMap;
+import java.util.function.Supplier;
import org.apache.hop.core.exception.HopRuntimeException;
+import org.apache.hop.ui.core.widget.svg.SvgImageFacade;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Device;
import org.eclipse.swt.graphics.Image;
@@ -85,12 +87,25 @@ public abstract class SwtUniversalImage {
/** Method getAsBitmapForSize(..., angle) can't be called, because it
returns bigger picture. */
public synchronized Image getAsBitmapForSize(Device device, int width, int
height) {
+ return cached(width + "x" + height, () -> renderSimple(device, width,
height));
+ }
+
+ /**
+ * Like {@link #getAsBitmapForSize(Device, int, int)} but guaranteed to
carry pixels. Use it where
+ * the image is read back ({@link Image#getImageData()}, {@code
SWT.IMAGE_GRAY}): on Hop Web the
+ * plain variant may be a vector the client renders, which has no pixel data
on the server.
+ */
+ public synchronized Image getAsRasterForSize(Device device, int width, int
height) {
+ return getAsBitmapForSize(device, width, height);
+ }
+
+ /** The image under {@code key}, rendered on first use and disposed with
this instance. */
+ protected synchronized Image cached(String key, Supplier<Image> renderer) {
checkDisposed();
- String key = width + "x" + height;
Image result = cache.get(key);
if (result == null) {
- result = renderSimple(device, width, height);
+ result = renderer.get();
cache.put(key, result);
}
return result;
@@ -154,6 +169,11 @@ public abstract class SwtUniversalImage {
if (isDpiAwareImageProviderSupported()) {
return SwtDesktopDpiImages.getImageData(image, zoom);
}
+ // A vector-backed Hop Web image has no pixels of its own; rasterise it at
this zoom instead.
+ ImageData vector = SvgImageFacade.rasterize(image, zoom);
+ if (vector != null) {
+ return vector;
+ }
ImageData data = image.getImageData();
if (zoom == 100) {
return data;
@@ -162,7 +182,7 @@ public abstract class SwtUniversalImage {
}
/** Converts BufferedImage to SWT ImageData with alpha channel. */
- static ImageData toImageData(BufferedImage img) {
+ public static ImageData toImageData(BufferedImage img) {
PaletteData palette = new PaletteData(0xFF0000, 0xFF00, 0xFF);
ImageData data = new ImageData(img.getWidth(), img.getHeight(), 32,
palette);
for (int y = 0; y < data.height; y++) {
diff --git a/ui/src/main/java/org/apache/hop/core/SwtUniversalImageSvg.java
b/ui/src/main/java/org/apache/hop/core/SwtUniversalImageSvg.java
index 6c485970ca..a106e600da 100644
--- a/ui/src/main/java/org/apache/hop/core/SwtUniversalImageSvg.java
+++ b/ui/src/main/java/org/apache/hop/core/SwtUniversalImageSvg.java
@@ -30,14 +30,19 @@ import org.apache.batik.gvt.GraphicsNode;
import org.apache.hop.core.svg.SvgDarkModeContrast;
import org.apache.hop.core.svg.SvgImage;
import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.widget.svg.SvgImageFacade;
import org.eclipse.swt.graphics.Device;
import org.eclipse.swt.graphics.Image;
+import org.w3c.dom.Document;
import org.w3c.dom.svg.SVGDocument;
public class SwtUniversalImageSvg extends SwtUniversalImage {
private final GraphicsNode svgGraphicsNode;
private final Dimension2D svgGraphicsSize;
+ /** What gets rendered: the contrast-adjusted clone in dark mode, else the
original. */
+ private final Document svgDocument;
+
static {
// workaround due to known issue in batik 1.8 -
https://issues.apache.org/jira/browse/BATIK-1125
ImageTagRegistry registry = ImageTagRegistry.getRegistry();
@@ -62,12 +67,14 @@ public class SwtUniversalImageSvg extends SwtUniversalImage
{
svgGraphicsNode = builder.build(ctx, clonedDocument);
svgGraphicsSize = ctx.getDocumentSize();
+ svgDocument = clonedDocument;
} else {
// get GraphicsNode and size from svg document
DocumentLoader documentLoader = new DocumentLoader(userAgentAdapter);
BridgeContext ctx = new BridgeContext(userAgentAdapter, documentLoader);
svgGraphicsNode = builder.build(ctx, svg.getDocument());
svgGraphicsSize = ctx.getDocumentSize();
+ svgDocument = svg.getDocument();
}
}
@@ -81,6 +88,27 @@ public class SwtUniversalImageSvg extends SwtUniversalImage {
@Override
protected Image renderSimple(Device device, int width, int height) {
+ if (!isDpiAwareImageProviderSupported()) {
+ // Hop Web: hand the browser the vector so it rasterises at device
resolution.
+ Image vector =
+ SvgImageFacade.createImage(device, svgDocument, svgGraphicsSize,
width, height);
+ if (vector != null) {
+ return vector;
+ }
+ }
+ return renderRaster(device, width, height);
+ }
+
+ @Override
+ public synchronized Image getAsRasterForSize(Device device, int width, int
height) {
+ if (isDpiAwareImageProviderSupported()) {
+ // Desktop bitmaps always carry pixels; share the regular cache entry.
+ return getAsBitmapForSize(device, width, height);
+ }
+ return cached("raster/" + width + "x" + height, () -> renderRaster(device,
width, height));
+ }
+
+ private Image renderRaster(Device device, int width, int height) {
return createDpiAwareImage(device, width, height, (w, h) ->
toImageData(renderSvg(w, h, 0d)));
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/core/database/dialog/DatabaseExplorerDialog.java
b/ui/src/main/java/org/apache/hop/ui/core/database/dialog/DatabaseExplorerDialog.java
index 5973e93363..7c1c8fbd85 100644
---
a/ui/src/main/java/org/apache/hop/ui/core/database/dialog/DatabaseExplorerDialog.java
+++
b/ui/src/main/java/org/apache/hop/ui/core/database/dialog/DatabaseExplorerDialog.java
@@ -59,6 +59,7 @@ import org.apache.hop.ui.core.gui.GuiResource;
import org.apache.hop.ui.core.gui.GuiToolbarWidgets;
import org.apache.hop.ui.core.gui.IToolbarContainer;
import org.apache.hop.ui.core.gui.WindowProperty;
+import org.apache.hop.ui.core.widget.FolderTreeIcons;
import org.apache.hop.ui.core.widget.HopTree;
import org.apache.hop.ui.hopgui.HopGui;
import org.apache.hop.ui.hopgui.HopGuiKeyHandler;
@@ -268,6 +269,7 @@ public class DatabaseExplorerDialog extends Dialog {
// Create tree
wTree = new HopTree(composite, SWT.SINGLE | SWT.H_SCROLL | SWT.V_SCROLL);
+ FolderTreeIcons.install(wTree);
wTree.setLayoutData(
FormDataBuilder.builder().top(toolBar,
margin).bottom().fullWidth().build());
wTree.addListener(SWT.DefaultSelection, this::openTreeItem);
@@ -345,7 +347,7 @@ public class DatabaseExplorerDialog extends Dialog {
private void expandAllItems(TreeItem[] items, boolean expand) {
for (TreeItem item : items) {
- item.setExpanded(expand);
+ FolderTreeIcons.setExpanded(item, expand);
if (item.getItemCount() > 0) {
expandAllItems(item.getItems(), expand);
}
@@ -465,7 +467,7 @@ public class DatabaseExplorerDialog extends Dialog {
}
// Always expand the root item
- connectionItem.setExpanded(true);
+ FolderTreeIcons.setExpanded(connectionItem, true);
wTree.setRedraw(true);
shell.setCursor(null);
@@ -555,7 +557,7 @@ public class DatabaseExplorerDialog extends Dialog {
}
}
}
- parent.setExpanded(expanded);
+ FolderTreeIcons.setExpanded(parent, expanded);
}
/**
@@ -610,7 +612,7 @@ public class DatabaseExplorerDialog extends Dialog {
}
}
}
- parent.setExpanded(expanded);
+ FolderTreeIcons.setExpanded(parent, expanded);
}
private boolean matchesFilter(String name, String schemaName) {
@@ -973,7 +975,7 @@ public class DatabaseExplorerDialog extends Dialog {
DatabaseTreeNode node = (DatabaseTreeNode) item.getData();
if (!node.isTableLike()) {
// Expand/Collapse hierarchy
- item.setExpanded(!item.getExpanded());
+ FolderTreeIcons.setExpanded(item, !item.getExpanded());
return;
}
diff --git a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java
b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java
index 5e5405b65b..a07a887a45 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java
@@ -164,6 +164,7 @@ public class GuiResource {
private SwtUniversalImage imageArrowCandidate;
private SwtUniversalImage imageServer;
private SwtUniversalImage imageFolder;
+ private SwtUniversalImage imageFolderOpen;
private SwtUniversalImage imageFile;
private SwtUniversalImage imageEdit;
private SwtUniversalImage imageCopyRows;
@@ -461,6 +462,7 @@ public class GuiResource {
imageFile.dispose();
imageFindReplace.dispose();
imageFolder.dispose();
+ imageFolderOpen.dispose();
imageMissing.dispose();
imageVariable.dispose();
imageHash.dispose();
@@ -881,6 +883,7 @@ public class GuiResource {
imageNaming = SwtSvgImageUtil.getImageAsResource(display,
"ui/images/naming.svg");
imageFile = SwtSvgImageUtil.getImageAsResource(display,
"ui/images/file.svg");
imageFolder = SwtSvgImageUtil.getImageAsResource(display,
"ui/images/folder.svg");
+ imageFolderOpen = SwtSvgImageUtil.getImageAsResource(display,
"ui/images/folder-open.svg");
imagePartitionSchema =
SwtSvgImageUtil.getImageAsResource(display,
"ui/images/partition_schema.svg");
imageDatabase = SwtSvgImageUtil.getImageAsResource(display,
"ui/images/database.svg");
@@ -1341,12 +1344,25 @@ public class GuiResource {
}
/**
- * @return the imageArrow
+ * Closed folder, tree-item sized. Pair with {@link #getImageFolderOpen()}
through {@link
+ * org.apache.hop.ui.core.widget.FolderTreeIcons} to swap the icon on
expand/collapse.
+ *
+ * @return the closed folder image
*/
public Image getImageFolder() {
return getZoomedImaged(imageFolder, display, ConstUi.SMALL_ICON_SIZE,
ConstUi.SMALL_ICON_SIZE);
}
+ /**
+ * Open folder, tree-item sized: the expanded counterpart of {@link
#getImageFolder()}.
+ *
+ * @return the open folder image
+ */
+ public Image getImageFolderOpen() {
+ return getZoomedImaged(
+ imageFolderOpen, display, ConstUi.SMALL_ICON_SIZE,
ConstUi.SMALL_ICON_SIZE);
+ }
+
/**
* @return the imageFile
*/
@@ -1634,10 +1650,15 @@ public class GuiResource {
k -> {
SwtUniversalImage svg = SwtSvgImageUtil.getUniversalImage(display,
classLoader, location);
- Image zoomedImaged = getZoomedImaged(svg, display, width, height);
Image loaded;
if (disabled) {
- Image gray = new Image(display, zoomedImaged, SWT.IMAGE_GRAY);
+ // Grayscaling reads the pixels back, which a vector-backed Hop
Web image lacks.
+ Image raster =
+ svg.getAsRasterForSize(
+ display,
+ ConstUi.zoomedIconSize(width, zoomFactor),
+ ConstUi.zoomedIconSize(height, zoomFactor));
+ Image gray = new Image(display, raster, SWT.IMAGE_GRAY);
float factor = PropsUi.getInstance().isDarkMode() ? 0.4f : 2.5f;
loaded =
SwtUniversalImage.createDpiAwareImage(
@@ -1646,6 +1667,7 @@ public class GuiResource {
applyDisabledContrast(
SwtUniversalImage.getImageDataAtZoom(gray, zoom),
factor));
} else {
+ Image zoomedImaged = getZoomedImaged(svg, display, width, height);
loaded = new Image(display, zoomedImaged, SWT.IMAGE_COPY);
}
diff --git a/ui/src/main/java/org/apache/hop/ui/core/vfs/HopVfsFileDialog.java
b/ui/src/main/java/org/apache/hop/ui/core/vfs/HopVfsFileDialog.java
index eb72f750f2..ce0e7eace6 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/vfs/HopVfsFileDialog.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/vfs/HopVfsFileDialog.java
@@ -64,6 +64,7 @@ import org.apache.hop.ui.core.gui.GuiToolbarWidgets;
import org.apache.hop.ui.core.gui.HopNamespace;
import org.apache.hop.ui.core.gui.IToolbarContainer;
import org.apache.hop.ui.core.gui.WindowProperty;
+import org.apache.hop.ui.core.widget.FolderTreeIcons;
import org.apache.hop.ui.core.widget.HopTree;
import org.apache.hop.ui.core.widget.NamingSchemeTypes;
import org.apache.hop.ui.core.widget.NamingSchemeWidgetSupport;
@@ -557,6 +558,7 @@ public class HopVfsFileDialog implements IFileDialog,
IDirectoryDialog {
new HopTree(
browseSash, (multiSelection ? SWT.MULTI : SWT.SINGLE) |
SWT.H_SCROLL | SWT.V_SCROLL);
PropsUi.setLook(wBrowser);
+ FolderTreeIcons.install(wBrowser);
wBrowser.setHeaderVisible(true);
wBrowser.setLinesVisible(false);
@@ -1093,7 +1095,7 @@ public class HopVfsFileDialog implements IFileDialog,
IDirectoryDialog {
populateFolder(activeFolder, parentFolderItem);
- parentFolderItem.setExpanded(true);
+ FolderTreeIcons.setExpanded(parentFolderItem, true);
updateSelection();
} catch (Exception e) {
diff --git a/ui/src/main/java/org/apache/hop/ui/core/widget/FileTree.java
b/ui/src/main/java/org/apache/hop/ui/core/widget/FileTree.java
index c4854ab623..69d400c67f 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/widget/FileTree.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/FileTree.java
@@ -90,6 +90,7 @@ public class FileTree extends Composite {
btnFindDependencies.setLayoutData(new
FormDataBuilder().left().fullWidth().bottom().result());
tree = new HopTree(this, SWT.CHECK | SWT.MULTI | SWT.BORDER);
+ FolderTreeIcons.install(tree);
PropsUi.setLook(tree);
tree.setLayoutData(
new FormDataBuilder()
@@ -151,7 +152,7 @@ public class FileTree extends Composite {
/** Expand the given item and all descendants so the default selection is
visible. */
private void expandAll(TreeItem item) {
- item.setExpanded(true);
+ FolderTreeIcons.setExpanded(item, true);
for (TreeItem child : item.getItems()) {
if (FOLDER.equals(child.getData("type"))) {
expandAll(child);
diff --git
a/ui/src/main/java/org/apache/hop/ui/core/widget/FolderTreeIcons.java
b/ui/src/main/java/org/apache/hop/ui/core/widget/FolderTreeIcons.java
new file mode 100644
index 0000000000..8f02466e1f
--- /dev/null
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/FolderTreeIcons.java
@@ -0,0 +1,70 @@
+/*
+ * 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.core.widget;
+
+import org.apache.hop.ui.core.gui.GuiResource;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeItem;
+
+/**
+ * Shows a closed folder on collapsed tree items and an open folder on
expanded ones.
+ *
+ * <p>Items keep using {@link GuiResource#getImageFolder()} as their image;
this class only swaps it
+ * with {@link GuiResource#getImageFolderOpen()} and back. Items carrying any
other image are left
+ * alone. {@link #install(Tree)} covers user expand/collapse. SWT fires no
event for a programmatic
+ * {@link TreeItem#setExpanded(boolean)}, so expand through {@link
#setExpanded(TreeItem, boolean)}.
+ */
+public final class FolderTreeIcons {
+
+ private FolderTreeIcons() {}
+
+ /** Swaps the folder icon of the item the user expands or collapses. */
+ public static void install(Tree tree) {
+ tree.addListener(SWT.Expand, e -> setOpen((TreeItem) e.item, true));
+ tree.addListener(SWT.Collapse, e -> setOpen((TreeItem) e.item, false));
+ }
+
+ /**
+ * {@link TreeItem#setExpanded(boolean)} that also updates the folder icon.
The icon follows the
+ * state the item ends up in: expanding an item without children is a no-op
on every platform.
+ */
+ public static void setExpanded(TreeItem item, boolean expanded) {
+ if (item == null || item.isDisposed()) {
+ return;
+ }
+ item.setExpanded(expanded);
+ setOpen(item, item.getExpanded());
+ }
+
+ private static void setOpen(TreeItem item, boolean open) {
+ if (item == null || item.isDisposed()) {
+ return;
+ }
+ GuiResource guiResource = GuiResource.getInstance();
+ Image closedImage = guiResource.getImageFolder();
+ Image openImage = guiResource.getImageFolderOpen();
+ Image current = item.getImage();
+ if (open && current == closedImage) {
+ item.setImage(openImage);
+ } else if (!open && current == openImage) {
+ item.setImage(closedImage);
+ }
+ }
+}
diff --git a/ui/src/main/java/org/apache/hop/ui/core/widget/TreeMemory.java
b/ui/src/main/java/org/apache/hop/ui/core/widget/TreeMemory.java
index 8fe7bd04e9..fd9e4e4805 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/widget/TreeMemory.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/TreeMemory.java
@@ -190,7 +190,7 @@ public class TreeMemory {
String[] path = ConstUi.getTreeStrings(treeItem);
boolean expanded = treeMemory.isExpanded(treeName, path);
- treeItem.setExpanded(expanded);
+ FolderTreeIcons.setExpanded(treeItem, expanded);
TreeItem[] items = treeItem.getItems();
for (TreeItem item : items) {
diff --git
a/ui/src/main/java/org/apache/hop/ui/core/widget/svg/SvgImageFacade.java
b/ui/src/main/java/org/apache/hop/ui/core/widget/svg/SvgImageFacade.java
new file mode 100644
index 0000000000..e680410162
--- /dev/null
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/svg/SvgImageFacade.java
@@ -0,0 +1,88 @@
+/*
+ * 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.core.widget.svg;
+
+import java.awt.geom.Dimension2D;
+import org.apache.hop.ui.hopgui.ImplementationLoader;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.w3c.dom.Document;
+
+/**
+ * Widget images that the client draws as vectors.
+ *
+ * <p>Hop Web has no per-monitor zoom: an {@link Image} is one bitmap at its
logical size and the
+ * browser scales it up on a high-DPI screen, so a 16px tree icon comes out
blurry. The RAP
+ * implementation instead publishes the SVG, sized to the requested pixels, as
a web resource and
+ * returns an image that points at it; the browser then rasterises the vector
at device resolution.
+ * On desktop, where SWT re-rasterises per zoom itself, the implementation
returns {@code null}.
+ */
+public abstract class SvgImageFacade {
+
+ private static final SvgImageFacade IMPL;
+
+ static {
+ IMPL = (SvgImageFacade)
ImplementationLoader.newInstance(SvgImageFacade.class);
+ }
+
+ /**
+ * @param device the display the image belongs to
+ * @param document the SVG, already colour-adjusted for the theme
+ * @param intrinsicSize the size the drawing was authored for, in user units
+ * @param width the requested width in pixels
+ * @param height the requested height in pixels
+ * @return an image the client renders as a vector, or {@code null} when
this platform or thread
+ * cannot provide one and the caller has to rasterise
+ */
+ public static Image createImage(
+ Device device, Document document, Dimension2D intrinsicSize, int width,
int height) {
+ return IMPL.createImageInternal(device, document, intrinsicSize, width,
height);
+ }
+
+ protected abstract Image createImageInternal(
+ Device device, Document document, Dimension2D intrinsicSize, int width,
int height);
+
+ /**
+ * Pixels for an image handed out by {@link #createImage}, for the few
callers that read an image
+ * back (badge compositing, grayscaling).
+ *
+ * @param image an image, vector-backed or not
+ * @param zoom the SWT zoom percentage (100, 200, ...) the pixels are for
+ * @return the image rasterised at that zoom, or {@code null} when it is not
vector-backed
+ */
+ public static ImageData rasterize(Image image, int zoom) {
+ return IMPL.rasterizeInternal(image, zoom);
+ }
+
+ protected abstract ImageData rasterizeInternal(Image image, int zoom);
+
+ /**
+ * {@code badge} drawn into the bottom-right corner of {@code base}, as one
image the client still
+ * renders as a vector. Sizes are in the logical pixels of {@code base}.
+ *
+ * @return the badged image, or {@code null} when either image is not
vector-backed and the caller
+ * has to composite pixels instead
+ */
+ public static Image overlay(Device device, Image base, Image badge, int
badgeSize, int margin) {
+ return IMPL.overlayInternal(device, base, badge, badgeSize, margin);
+ }
+
+ protected abstract Image overlayInternal(
+ Device device, Image base, Image badge, int badgeSize, int margin);
+}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/palette/GraphPaletteTree.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/palette/GraphPaletteTree.java
index 7c08862109..4fe0b1a788 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/palette/GraphPaletteTree.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/palette/GraphPaletteTree.java
@@ -25,6 +25,7 @@ import org.apache.hop.i18n.BaseMessages;
import org.apache.hop.ui.core.PropsUi;
import org.apache.hop.ui.core.bus.HopGuiEvents;
import org.apache.hop.ui.core.gui.GuiResource;
+import org.apache.hop.ui.core.widget.FolderTreeIcons;
import org.apache.hop.ui.core.widget.TreeMemory;
import org.apache.hop.ui.hopgui.context.ContextDialogPlacement;
import org.apache.hop.ui.hopgui.context.GuiActionFavorites;
@@ -171,6 +172,7 @@ public class GraphPaletteTree extends Composite {
PropsUi.setLook(border);
tree = new Tree(border, SWT.SINGLE | SWT.H_SCROLL | SWT.V_SCROLL);
+ FolderTreeIcons.install(tree);
tree.setHeaderVisible(false);
PropsUi.setLook(tree);
FormData treeFd = new FormData();
@@ -451,7 +453,7 @@ public class GraphPaletteTree extends Composite {
}
private void expandTreeItem(TreeItem item, boolean expanded) {
- item.setExpanded(expanded);
+ FolderTreeIcons.setExpanded(item, expanded);
TreeMemory.getInstance().storeExpanded(treeMemoryName, item, expanded);
for (TreeItem child : item.getItems()) {
expandTreeItem(child, expanded);
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbench.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbench.java
index 4921db926a..fd2b5f5a05 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbench.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbench.java
@@ -54,6 +54,7 @@ import org.apache.hop.ui.core.gui.GuiResource;
import org.apache.hop.ui.core.gui.GuiToolbarWidgets;
import org.apache.hop.ui.core.gui.IToolbarContainer;
import org.apache.hop.ui.core.metadata.MetadataManager;
+import org.apache.hop.ui.core.widget.FolderTreeIcons;
import org.apache.hop.ui.core.widget.TreeMemory;
import org.apache.hop.ui.hopgui.BackgroundThreadFacade;
import org.apache.hop.ui.hopgui.HopGui;
@@ -192,6 +193,7 @@ public class DatabaseWorkbench extends Composite implements
TabClosable {
toolBar.pack();
tree = new Tree(treeBorder, SWT.SINGLE | SWT.H_SCROLL | SWT.V_SCROLL);
+ FolderTreeIcons.install(tree);
PropsUi.setLook(tree);
tree.setLayoutData(
new FormDataBuilder().top(toolBar,
PropsUi.getMargin()).bottom().fullWidth().result());
@@ -472,7 +474,7 @@ public class DatabaseWorkbench extends Composite implements
TabClosable {
DatabaseTreeNode.connection(state.getDatabaseMeta().getName(),
state.isConnected()));
if (state.isConnected() && state.getInformation() != null) {
fillConnectionChildren(connectionItem, state);
- connectionItem.setExpanded(true);
+ FolderTreeIcons.setExpanded(connectionItem, true);
}
}
restoreSchemaExpandState();
@@ -510,7 +512,8 @@ public class DatabaseWorkbench extends Composite implements
TabClosable {
for (TreeItem child : parent.getItems()) {
Object data = child.getData();
if (data instanceof DatabaseTreeNode node &&
remembersExpandState(node.getKind())) {
- child.setExpanded(
+ FolderTreeIcons.setExpanded(
+ child,
TreeMemory.getInstance().isExpanded(TREE_MEMORY_KEY,
ConstUi.getTreeStrings(child)));
}
restoreSchemaExpandState(child);
@@ -529,12 +532,12 @@ public class DatabaseWorkbench extends Composite
implements TabClosable {
if (data instanceof DatabaseTreeNode node
&& remembersExpandState(node.getKind())
&& child.getItemCount() > 0) {
- child.setExpanded(true);
+ FolderTreeIcons.setExpanded(child, true);
any = true;
}
}
if (any) {
- connection.setExpanded(true);
+ FolderTreeIcons.setExpanded(connection, true);
}
}
}
@@ -729,7 +732,7 @@ public class DatabaseWorkbench extends Composite implements
TabClosable {
folder.setImage(GuiResource.getInstance().getImageFolder());
folder.setData(DatabaseTreeNode.folder(connectionName, folderName));
addTables(folder, connectionName, null, names, kind);
- folder.setExpanded(true);
+ FolderTreeIcons.setExpanded(folder, true);
}
private void addTables(
@@ -819,7 +822,7 @@ public class DatabaseWorkbench extends Composite implements
TabClosable {
return;
}
if (item != null && item.getItemCount() > 0 && (node == null ||
!node.isTableLike())) {
- item.setExpanded(!item.getExpanded());
+ FolderTreeIcons.setExpanded(item, !item.getExpanded());
return;
}
previewTable();
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
index 21a51c5eb7..270a1406e1 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
@@ -95,6 +95,7 @@ import org.apache.hop.ui.core.gui.GuiToolbarWidgets;
import org.apache.hop.ui.core.gui.HopNamespace;
import org.apache.hop.ui.core.gui.IToolbarContainer;
import org.apache.hop.ui.core.security.HopSecurityUi;
+import org.apache.hop.ui.core.widget.FolderTreeIcons;
import org.apache.hop.ui.core.widget.NamingSchemeTypes;
import org.apache.hop.ui.core.widget.NamingSchemeWidgetSupport;
import org.apache.hop.ui.core.widget.TreeMemory;
@@ -750,6 +751,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
// Lazy loading...
//
tree.addListener(SWT.Expand, this::lazyLoadFolderOnExpand);
+ FolderTreeIcons.install(tree);
// Create context menu...
//
@@ -1192,9 +1194,9 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
if (tif.folder) {
if (!item.getExpanded()) {
lazyLoadFolderOnExpand(event);
- item.setExpanded(true);
+ FolderTreeIcons.setExpanded(item, true);
} else {
- item.setExpanded(false);
+ FolderTreeIcons.setExpanded(item, false);
}
TreeMemory.getInstance().storeExpanded(FILE_EXPLORER_TREE, item,
item.getExpanded());
} else {
@@ -1214,7 +1216,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
if (expanded) {
ensureFolderLoaded(item);
}
- item.setExpanded(expanded);
+ FolderTreeIcons.setExpanded(item, expanded);
TreeMemory.getInstance().storeExpanded(FILE_EXPLORER_TREE, item,
expanded);
} else {
IHopFileTypeHandler handler =
@@ -2798,7 +2800,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
}
if (tif != null && tif.folder && isDescendant(tif.path, filename)) {
ensureFolderLoaded(item);
- item.setExpanded(true);
+ FolderTreeIcons.setExpanded(item, true);
TreeMemory.getInstance().storeExpanded(FILE_EXPLORER_TREE, item, true);
for (TreeItem child : item.getItems()) {
if (selectInTree(child, filename)) {
@@ -3603,7 +3605,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
// which showed blank rows. Load real children first.
ensureFolderLoaded(item);
}
- item.setExpanded(expand);
+ FolderTreeIcons.setExpanded(item, expand);
TreeMemory.getInstance().storeExpanded(FILE_EXPLORER_TREE, item, expand);
for (TreeItem childItem : item.getItems()) {
@@ -3911,7 +3913,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
setTreeItemData(rootItem, rootNode.getPath(), rootNode.getName(),
fileType, 0, true, true);
renderFilteredChildren(rootItem, rootNode, 0);
- rootItem.setExpanded(true);
+ FolderTreeIcons.setExpanded(rootItem, true);
TreeMemory.getInstance().storeExpanded(FILE_EXPLORER_TREE, rootItem,
true);
} finally {
tree.setRedraw(true);
@@ -4056,7 +4058,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
if (child.isFolder()) {
renderFilteredChildren(childItem, child, depth + 1);
if (childItem.getItemCount() > 0) {
- childItem.setExpanded(true);
+ FolderTreeIcons.setExpanded(childItem, true);
TreeMemory.getInstance().storeExpanded(FILE_EXPLORER_TREE,
childItem, true);
}
}
@@ -4153,7 +4155,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
String[] treePath = ConstUi.getTreeStrings(folderItem);
boolean wasExpanded = folderItem.getExpanded();
if (wasExpanded) {
- folderItem.setExpanded(false);
+ FolderTreeIcons.setExpanded(folderItem, false);
// Collapse listener clears TreeMemory; put the remembered expand state
back.
TreeMemory.getInstance().storeExpanded(FILE_EXPLORER_TREE, treePath,
true);
}
@@ -4310,7 +4312,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
// Always expand root item when filtering
if (!Utils.isEmpty(filterText)) {
- rootItem.setExpanded(true);
+ FolderTreeIcons.setExpanded(rootItem, true);
TreeMemory.getInstance().storeExpanded(FILE_EXPLORER_TREE, rootItem,
true);
} else {
TreeMemory.getInstance().storeExpanded(FILE_EXPLORER_TREE, rootItem,
true);
@@ -4319,7 +4321,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
// The TreeMemory will be applied either by restoreTreeState() or
setExpandedFromMemory()
if (treeStateBeforeFilter == null) {
// Only restore from memory if we're not about to restore from
saved state
- rootItem.setExpanded(true);
+ FolderTreeIcons.setExpanded(rootItem, true);
restoreTreeItemExpandedFromMemory(rootItem);
}
}
@@ -4391,6 +4393,11 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
}
private void setItemImage(TreeItem treeItem, IHopFileType fileType) {
+ if (fileType instanceof FolderFileType) {
+ // Shared closed-folder image: FolderTreeIcons swaps it for the open one
on expand.
+ treeItem.setImage(GuiResource.getInstance().getImageFolder());
+ return;
+ }
Image image = typeImageMap.get(fileType.getName());
if (image != null) {
treeItem.setImage(image);
@@ -4636,7 +4643,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
if (expanded) {
ensureFolderLoaded(item);
}
- item.setExpanded(expanded);
+ FolderTreeIcons.setExpanded(item, expanded);
}
for (TreeItem child : item.getItems()) {
@@ -4659,7 +4666,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
ensureFolderLoaded(item);
}
- item.setExpanded(wasExpanded);
+ FolderTreeIcons.setExpanded(item, wasExpanded);
TreeMemory.getInstance().storeExpanded(FILE_EXPLORER_TREE, item,
wasExpanded);
}
@@ -4860,7 +4867,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
if (tif != null && tif.folder && isDescendant(tif.path, path)) {
ensureFolderLoaded(item);
- item.setExpanded(true);
+ FolderTreeIcons.setExpanded(item, true);
TreeMemory.getInstance().storeExpanded(FILE_EXPLORER_TREE, item, true);
for (TreeItem child : item.getItems()) {
TreeItem found = locateTreeItem(child, path);
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/metadata/MetadataPerspective.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/metadata/MetadataPerspective.java
index 8520712f8d..cf005c9c21 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/metadata/MetadataPerspective.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/metadata/MetadataPerspective.java
@@ -83,6 +83,7 @@ import org.apache.hop.ui.core.metadata.MetadataEditor;
import org.apache.hop.ui.core.metadata.MetadataFileType;
import org.apache.hop.ui.core.metadata.MetadataManager;
import org.apache.hop.ui.core.security.HopSecurityUi;
+import org.apache.hop.ui.core.widget.FolderTreeIcons;
import org.apache.hop.ui.core.widget.NamingSchemeTypes;
import org.apache.hop.ui.core.widget.NamingSchemeWidgetSupport;
import org.apache.hop.ui.core.widget.TreeMemory;
@@ -589,6 +590,7 @@ public class MetadataPerspective implements
IHopPerspective, TabClosable, IMetad
// Remember expand/collapse within the session (shared TreeMemory, keyed
by stable node ids).
tree.addListener(SWT.Expand, e -> recordTreeState((TreeItem) e.item,
true));
tree.addListener(SWT.Collapse, e -> recordTreeState((TreeItem) e.item,
false));
+ FolderTreeIcons.install(tree);
// Drag and drop: reorganize within tree (same type only) and drag to
canvas to open
createTreeDragSource(tree);
@@ -787,7 +789,7 @@ public class MetadataPerspective implements
IHopPerspective, TabClosable, IMetad
onUnknownMetadataDetails();
} else {
// Expand/Collapse category
- treeItem.setExpanded(!treeItem.getExpanded());
+ FolderTreeIcons.setExpanded(treeItem, !treeItem.getExpanded());
}
}
@@ -983,7 +985,7 @@ public class MetadataPerspective implements
IHopPerspective, TabClosable, IMetad
public void selectType(String key) {
TreeItem typeItem = findTypeItem(key);
if (typeItem != null) {
- typeItem.setExpanded(true);
+ FolderTreeIcons.setExpanded(typeItem, true);
tree.setSelection(typeItem);
tree.showSelection();
updateSelection();
@@ -3160,7 +3162,7 @@ public class MetadataPerspective implements
IHopPerspective, TabClosable, IMetad
if (path != null) {
if (!Utils.isEmpty(currentSearchFilter)) {
// While searching, expand everything so matches are visible (not
recorded as a choice).
- item.setExpanded(true);
+ FolderTreeIcons.setExpanded(item, true);
} else {
// Categories (including the "Unknown" one) expand by default; types
and folders collapse
// by default. Seed each default-expanded node once per session so the
default holds until
@@ -3169,7 +3171,8 @@ public class MetadataPerspective implements
IHopPerspective, TabClosable, IMetad
if (defaultExpanded && treeStateSeeded.add(String.join("\0", path))) {
TreeMemory.getInstance().storeExpanded(METADATA_PERSPECTIVE_TREE,
path, true);
}
-
item.setExpanded(TreeMemory.getInstance().isExpanded(METADATA_PERSPECTIVE_TREE,
path));
+ FolderTreeIcons.setExpanded(
+ item,
TreeMemory.getInstance().isExpanded(METADATA_PERSPECTIVE_TREE, path));
}
}
for (TreeItem child : item.getItems()) {
@@ -3240,7 +3243,7 @@ public class MetadataPerspective implements
IHopPerspective, TabClosable, IMetad
/** Recursively expand or collapse a tree item and all its children */
private void expandTreeItem(TreeItem item, boolean expand) {
- item.setExpanded(expand);
+ FolderTreeIcons.setExpanded(item, expand);
for (TreeItem child : item.getItems()) {
expandTreeItem(child, expand);
}
diff --git a/ui/src/main/resources/ui/images/folder-add.svg
b/ui/src/main/resources/ui/images/folder-add.svg
index bc557bff08..eaea2a5969 100644
--- a/ui/src/main/resources/ui/images/folder-add.svg
+++ b/ui/src/main/resources/ui/images/folder-add.svg
@@ -1,6 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">
- <path
- d="M 9.8,3 12,5.2500001 h 8.8 A 2.2,2.2500001 0 0 1 23,7.5000002 V
18.750001 A 2.2,2.2500001 0 0 1 20.8,21 H 3.2 C 1.979,21 1,19.9875 1,18.750001
V 5.2500001 C 1,4.0012501 1.979,3 3.2,3 h 6.6 m 5.5,5.6250003 V 12 H 12 v
2.249999 h 3.3 V 17.625 h 2.2 v -3.375001 h 3.3 V 12 H 17.5 V 8.6250003 Z"
- style="fill:#0e3a5a;fill-opacity:1;stroke-width:1.11245"/>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24
24">
+ <path fill="#0e3a5a" d="M20 6h-8l-2-2H4c-1.11 0-1.99.89-1.99 2L2 18c0
1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2zm-1
8h-3v3h-2v-3h-3v-2h3V9h2v3h3v2z"/>
</svg>
diff --git a/ui/src/main/resources/ui/images/folder-open.svg
b/ui/src/main/resources/ui/images/folder-open.svg
new file mode 100644
index 0000000000..adb4e9868f
--- /dev/null
+++ b/ui/src/main/resources/ui/images/folder-open.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24
24">
+ <path fill="#0e3a5a" d="M20 6h-8l-2-2H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9
2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 12H4V8h16v10z"/>
+</svg>
diff --git a/ui/src/main/resources/ui/images/folder.svg
b/ui/src/main/resources/ui/images/folder.svg
index 8b3e6b2cd8..a176cd8dfc 100644
--- a/ui/src/main/resources/ui/images/folder.svg
+++ b/ui/src/main/resources/ui/images/folder.svg
@@ -1,10 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">
- <path
- d="M0 0h24v24H0z"
- fill="none"
- />
- <path
- d="m 20.799999,5.2499999 h -8.8 L 9.8,3 H 3.2 C 1.99,3
1.0110001,4.0124999 1.0110001,5.2499999 L 1,18.749999 C 1,19.987499 1.99,21
3.2,21 H 20.799999 C 22.01,21 23,19.987499 23,18.749999 V 7.5000001 C 23,6.2625
22.01,5.2499999 20.799999,5.2499999 Z m 0,13.4999991 H 3.2 V 7.5000001 h
17.599999 z"
- style="fill:#0e3a5a;fill-opacity:1;stroke-width:1.11243"/>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24
24">
+ <path fill="#0e3a5a" d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2
2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>
</svg>