This is an automated email from the ASF dual-hosted git repository.
bamaer 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 825ea99076 GUI performance improvements, fixes #9153 (#9154)
825ea99076 is described below
commit 825ea9907684d1e3c7bb94b091cdab4cd2028be6
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Fri Oct 30 06:40:20 2026 +0100
GUI performance improvements, fixes #9153 (#9154)
---
.../java/org/apache/hop/core/svg/SvgCache.java | 86 ++++++++-
.../org/apache/hop/ui/core/gui/GuiMenuWidgets.java | 46 ++++-
.../org/apache/hop/ui/core/gui/GuiResource.java | 210 ++++++++++++---------
.../main/java/org/apache/hop/ui/hopgui/HopGui.java | 21 +++
.../configuration/ConfigurationPerspective.java | 56 ++++--
.../perspective/metadata/MetadataPerspective.java | 19 +-
.../HopPluginExplorePerspective.java | 23 ++-
.../ui/hopgui/search/SearchEverywhereDialog.java | 2 +-
.../org/apache/hop/ui/util/SwtSvgImageUtil.java | 20 ++
9 files changed, 355 insertions(+), 128 deletions(-)
diff --git a/core/src/main/java/org/apache/hop/core/svg/SvgCache.java
b/core/src/main/java/org/apache/hop/core/svg/SvgCache.java
index 9ba013b490..d81c2fcca9 100644
--- a/core/src/main/java/org/apache/hop/core/svg/SvgCache.java
+++ b/core/src/main/java/org/apache/hop/core/svg/SvgCache.java
@@ -19,8 +19,17 @@ package org.apache.hop.core.svg;
import java.awt.geom.Rectangle2D;
import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.CodeSource;
+import java.util.ArrayList;
+import java.util.Enumeration;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.stream.Stream;
import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
import org.apache.batik.bridge.BridgeContext;
import org.apache.batik.bridge.DocumentLoader;
@@ -58,11 +67,78 @@ public class SvgCache {
return instance;
}
+ /**
+ * Parses the SVG images in a folder of the jar or class folder that holds
the given class into
+ * this cache, in a background thread, so that loading them later only has
to draw them. Needs no
+ * display and no GUI classes.
+ *
+ * @param anchor a class from the jar or class folder with the images
+ * @param folder the folder, for example {@code ui/images/}
+ */
+ public static void preloadFolderInBackground(Class<?> anchor, String folder)
{
+ Thread thread =
+ new Thread(
+ () -> {
+ ClassLoader classLoader = anchor.getClassLoader();
+ for (String name : listSvgResources(anchor, folder)) {
+ try {
+ loadSvg(new SvgFile(name, classLoader));
+ } catch (Exception e) {
+ // Reported when the image is loaded
+ }
+ }
+ },
+ "Hop SVG preload");
+ thread.setDaemon(true);
+ thread.setPriority(Thread.MIN_PRIORITY);
+ thread.start();
+ }
+
+ /** The SVG resources in a folder of the jar or class folder that holds the
class. */
+ static List<String> listSvgResources(Class<?> anchor, String folder) {
+ List<String> names = new ArrayList<>();
+ try {
+ CodeSource codeSource = anchor.getProtectionDomain().getCodeSource();
+ if (codeSource == null || codeSource.getLocation() == null) {
+ return names;
+ }
+ Path location = Path.of(codeSource.getLocation().toURI());
+ if (Files.isDirectory(location)) {
+ Path base = location.resolve(folder);
+ if (Files.isDirectory(base)) {
+ try (Stream<Path> files = Files.walk(base)) {
+ files
+ .filter(f -> f.toString().endsWith(".svg"))
+ .forEach(f ->
names.add(location.relativize(f).toString().replace('\\', '/')));
+ }
+ }
+ } else if (Files.isRegularFile(location)) {
+ try (JarFile jar = new JarFile(location.toFile())) {
+ Enumeration<JarEntry> entries = jar.entries();
+ while (entries.hasMoreElements()) {
+ String name = entries.nextElement().getName();
+ if (name.startsWith(folder) && name.endsWith(".svg")) {
+ names.add(name);
+ }
+ }
+ }
+ }
+ } catch (Exception e) {
+ // Nothing is preloaded: the images are parsed when they are loaded
+ }
+ return names;
+ }
+
public static synchronized SvgCacheEntry findSvg(String filename) {
return getInstance().fileDocumentMap.get(filename);
}
- public static synchronized SvgCacheEntry loadSvg(SvgFile svgFile) throws
HopException {
+ /**
+ * Loads an SVG file into the cache. The file is parsed without holding the
cache's lock: Batik
+ * can wait for the main thread (the AWT toolkit on macOS), and the main
thread may be loading an
+ * image itself. Two threads can then parse the same file; the first result
is kept.
+ */
+ public static SvgCacheEntry loadSvg(SvgFile svgFile) throws HopException {
SvgCacheEntry cacheEntry = findSvg(svgFile.getFilename());
if (cacheEntry != null) {
@@ -152,7 +228,13 @@ public class SvgCache {
Math.round(height),
Math.round(x),
Math.round(y));
- getInstance().fileDocumentMap.put(svgFile.getFilename(), cacheEntry);
+ synchronized (SvgCache.class) {
+ SvgCacheEntry existing = findSvg(svgFile.getFilename());
+ if (existing != null) {
+ return existing;
+ }
+ getInstance().fileDocumentMap.put(svgFile.getFilename(), cacheEntry);
+ }
return cacheEntry;
} catch (Exception e) {
throw new HopException("Error loading SVG file " +
svgFile.getFilename(), e);
diff --git a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiMenuWidgets.java
b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiMenuWidgets.java
index aae61caa80..d1141cd013 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiMenuWidgets.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiMenuWidgets.java
@@ -39,6 +39,7 @@ import org.apache.hop.ui.hopgui.file.IHopFileTypeHandler;
import org.apache.hop.ui.hopgui.perspective.IHopPerspective;
import org.apache.hop.ui.util.EnvironmentUtils;
import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
@@ -47,6 +48,9 @@ import org.eclipse.swt.widgets.Shell;
/** This class contains the widgets for Menu Bars */
public class GuiMenuWidgets extends BaseGuiWidgets {
+ /** Data key of a menu item icon that is not loaded yet, and of a menu that
loads them. */
+ private static final String PENDING_IMAGE = "hop.pendingMenuImage";
+
private Map<String, MenuItem> menuItemMap;
private Map<String, MenuItem> menuSeparatorMap;
private Map<String, Menu> menuParentMap;
@@ -246,15 +250,14 @@ public class GuiMenuWidgets extends BaseGuiWidgets {
// Set label
menuItem.setText(Const.NVL(guiMenuItem.getLabel(), ""));
- // Set image
+ // Set image: loaded when the menu is first shown, most menus are never
opened in a session
if (StringUtils.isNotEmpty(guiMenuItem.getImage())) {
- menuItem.setImage(
- GuiResource.getInstance()
- .getImage(
- guiMenuItem.getImage(),
- guiMenuItem.getClassLoader(),
- ConstUi.SMALL_ICON_SIZE,
- ConstUi.SMALL_ICON_SIZE));
+ menuItem.setData(PENDING_IMAGE, guiMenuItem);
+ Menu menu = menuItem.getParent();
+ if (menu.getData(PENDING_IMAGE) == null) {
+ menu.setData(PENDING_IMAGE, Boolean.TRUE);
+ menu.addListener(SWT.Show, e -> loadPendingImages(menu));
+ }
}
// See if there's a shortcut worth mentioning...
@@ -277,6 +280,33 @@ public class GuiMenuWidgets extends BaseGuiWidgets {
}
}
+ private static void loadPendingImages(Menu menu) {
+ for (MenuItem item : menu.getItems()) {
+ getImage(item);
+ }
+ }
+
+ /**
+ * The image of a menu item. Images of the items built here are loaded when
their menu is first
+ * shown, or by this method.
+ *
+ * @param menuItem the menu item
+ * @return its image, or null
+ */
+ public static Image getImage(MenuItem menuItem) {
+ if (menuItem.getData(PENDING_IMAGE) instanceof GuiMenuItem guiMenuItem) {
+ menuItem.setData(PENDING_IMAGE, null);
+ menuItem.setImage(
+ GuiResource.getInstance()
+ .getImage(
+ guiMenuItem.getImage(),
+ guiMenuItem.getClassLoader(),
+ ConstUi.SMALL_ICON_SIZE,
+ ConstUi.SMALL_ICON_SIZE));
+ }
+ return menuItem.getImage();
+ }
+
public static void appendShortCut(MenuItem menuItem, KeyboardShortcut
shortcut) {
menuItem.setText(menuItem.getText() + "\t" + getShortcutString(shortcut));
}
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 a07a887a45..1c4edb5bb0 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
@@ -17,6 +17,7 @@
package org.apache.hop.ui.core.gui;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Hashtable;
@@ -30,6 +31,7 @@ import org.apache.hop.core.logging.ILogChannel;
import org.apache.hop.core.logging.LogChannel;
import org.apache.hop.core.plugins.ActionPluginType;
import org.apache.hop.core.plugins.IPlugin;
+import org.apache.hop.core.plugins.IPluginType;
import org.apache.hop.core.plugins.IPluginTypeListener;
import org.apache.hop.core.plugins.PluginRegistry;
import org.apache.hop.core.plugins.TransformPluginType;
@@ -133,10 +135,16 @@ public class GuiResource {
// Images
//
- private Map<String, SwtUniversalImage> imagesTransforms;
- private Map<String, SwtUniversalImage> imagesActions;
- private Map<String, Image> imagesValueMeta;
- private Map<String, Image> imagesDatabase;
+ /** Transform and action icons by plugin id, each loaded the first time it
is asked for. */
+ private Map<String, SwtUniversalImage> imagesTransforms = new Hashtable<>();
+
+ private Map<String, SwtUniversalImage> imagesActions = new Hashtable<>();
+
+ /** Value type icons by plugin id, each loaded the first time it is asked
for. */
+ private Map<String, Image> imagesValueMeta = new HashMap<>();
+
+ /** Database icons by plugin id, each loaded the first time it is asked for.
*/
+ private Map<String, Image> imagesDatabase = new HashMap<>();
private SwtUniversalImage imageLogo;
private SwtUniversalImage imageDatabase;
@@ -313,12 +321,12 @@ public class GuiResource {
new IPluginTypeListener() {
@Override
public void pluginAdded(Object serviceObject) {
- loadTransformImages();
+ imagesTransforms = new Hashtable<>();
}
@Override
public void pluginRemoved(Object serviceObject) {
- loadTransformImages();
+ imagesTransforms = new Hashtable<>();
}
@Override
@@ -333,15 +341,14 @@ public class GuiResource {
new IPluginTypeListener() {
@Override
public void pluginAdded(Object serviceObject) {
- // make sure we load up the images for any new actions that
have been registered
- loadActionImages();
+ // the icons of new actions are loaded when they are asked for
+ imagesActions = new Hashtable<>();
}
@Override
public void pluginRemoved(Object serviceObject) {
- // rebuild the image map, in effect removing the image(s) for
actions that have gone
- // away
- loadActionImages();
+ // forget the icons of actions that have gone away
+ imagesActions = new Hashtable<>();
}
@Override
@@ -432,10 +439,7 @@ public class GuiResource {
// Load all images from files...
loadFonts();
loadCommonImages();
- loadTransformImages();
- loadActionImages();
- loadValueMetaImages();
- loadDatabaseImages();
+ // Transform, action, database and value type icons are loaded when they
are first asked for
}
private void dispose() {
@@ -623,37 +627,74 @@ public class GuiResource {
}
}
- /** Load all transform images from files. */
- private void loadTransformImages() {
- imagesTransforms = new Hashtable<>();
-
- // TRANSFORM IMAGES TO LOAD
- //
- PluginRegistry registry = PluginRegistry.getInstance();
-
- List<IPlugin> transforms = registry.getPlugins(TransformPluginType.class);
- for (IPlugin transform : transforms) {
- if (imagesTransforms.get(transform.getIds()[0]) != null) {
- continue;
+ /**
+ * Load the icon of a transform or action plugin from its files.
+ *
+ * @param plugin the plugin
+ * @return the icon, or the missing image if it can not be loaded
+ */
+ private SwtUniversalImage loadPluginUniversalImage(IPlugin plugin) {
+ SwtUniversalImage image = null;
+ String filename = plugin.getImageFile();
+ try {
+ ClassLoader classLoader =
PluginRegistry.getInstance().getClassLoader(plugin);
+ image = SwtSvgImageUtil.getUniversalImage(display, classLoader,
filename);
+ } catch (Throwable t) {
+ log.logError(
+ CONST_ERROR_OCCURRED_LOADING_IMAGE + filename + CONST_FOR_PLUGIN +
plugin.getIds()[0], t);
+ } finally {
+ if (image == null) {
+ log.logError("Unable to load image [" + filename + CONST_FOR_PLUGIN +
plugin.getIds()[0]);
+ image = SwtSvgImageUtil.getMissingImage(display);
}
+ }
+ return image;
+ }
- SwtUniversalImage image = null;
- String filename = transform.getImageFile();
- try {
- ClassLoader classLoader = registry.getClassLoader(transform);
- image = SwtSvgImageUtil.getUniversalImage(display, classLoader,
filename);
- } catch (Throwable t) {
- log.logError(
- CONST_ERROR_OCCURRED_LOADING_IMAGE + filename + CONST_FOR_PLUGIN +
transform, t);
- } finally {
- if (image == null) {
- log.logError("Unable to load image file [" + filename +
CONST_FOR_PLUGIN + transform);
- image = SwtSvgImageUtil.getMissingImage(display);
+ /**
+ * The plugin of the given type whose main id is the given id: the icons are
kept by main id.
+ *
+ * @return the plugin, or null
+ */
+ private static IPlugin findPluginByMainId(Class<? extends IPluginType>
pluginType, String id) {
+ IPlugin plugin = PluginRegistry.getInstance().findPluginWithId(pluginType,
id);
+ return plugin != null && plugin.getIds()[0].equals(id) ? plugin : null;
+ }
+
+ /**
+ * Parses the SVG icons of all transforms and actions in a background
thread, so that showing many
+ * of them at once, as the dialog to add a transform does, only has to draw
them. The plugin class
+ * loaders are looked up here, on the calling thread.
+ */
+ public void preloadPluginIconsInBackground() {
+ PluginRegistry registry = PluginRegistry.getInstance();
+ List<ClassLoader> classLoaders = new ArrayList<>();
+ List<String> filenames = new ArrayList<>();
+ for (Class<? extends IPluginType> pluginType :
+ List.of(TransformPluginType.class, ActionPluginType.class)) {
+ for (IPlugin plugin : registry.getPlugins(pluginType)) {
+ if (Utils.isEmpty(plugin.getImageFile())) {
+ continue;
+ }
+ try {
+ classLoaders.add(registry.getClassLoader(plugin));
+ filenames.add(plugin.getImageFile());
+ } catch (Exception e) {
+ // The icon is loaded, or reported missing, when it is asked for
}
}
-
- imagesTransforms.put(transform.getIds()[0], image);
}
+ Thread thread =
+ new Thread(
+ () -> {
+ for (int i = 0; i < filenames.size(); i++) {
+ SwtSvgImageUtil.preloadSvg(classLoaders.get(i),
filenames.get(i));
+ }
+ },
+ "Hop icon preload");
+ thread.setDaemon(true);
+ thread.setPriority(Thread.MIN_PRIORITY);
+ thread.start();
}
private void loadFonts() {
@@ -975,55 +1016,6 @@ public class GuiResource {
return image;
}
- /** Load all action images from files. */
- private void loadActionImages() {
- imagesActions = new Hashtable<>();
-
- // ACTION IMAGES TO LOAD
- //
- PluginRegistry registry = PluginRegistry.getInstance();
- List<IPlugin> plugins = registry.getPlugins(ActionPluginType.class);
- for (IPlugin plugin : plugins) {
- SwtUniversalImage image = null;
- String filename = plugin.getImageFile();
- try {
- ClassLoader classLoader = registry.getClassLoader(plugin);
- image = SwtSvgImageUtil.getUniversalImage(display, classLoader,
filename);
- } catch (Throwable t) {
- log.logError(
- CONST_ERROR_OCCURRED_LOADING_IMAGE + filename + CONST_FOR_PLUGIN +
plugin.getIds()[0],
- t);
- } finally {
- if (image == null) {
- log.logError("Unable to load image [" + filename + CONST_FOR_PLUGIN
+ plugin.getIds()[0]);
- image = SwtSvgImageUtil.getMissingImage(display);
- }
- }
-
- imagesActions.put(plugin.getIds()[0], image);
- }
- }
-
- /** Load all IDatabase images from files. */
- private void loadDatabaseImages() {
- imagesDatabase = new HashMap<>();
- List<IPlugin> plugins =
PluginRegistry.getInstance().getPlugins(DatabasePluginType.class);
- for (IPlugin plugin : plugins) {
- Image image = this.loadPluginImage(plugin, getImageDatabase());
- imagesDatabase.put(plugin.getIds()[0], image);
- }
- }
-
- /** Load all IValueMeta images from files. */
- private void loadValueMetaImages() {
- imagesValueMeta = new HashMap<>();
- List<IPlugin> plugins =
PluginRegistry.getInstance().getPlugins(ValueMetaPluginType.class);
- for (IPlugin plugin : plugins) {
- Image image = this.loadPluginImage(plugin, imageLabel);
- imagesValueMeta.put(plugin.getIds()[0], image);
- }
- }
-
/**
* @return Returns the fontFixed.
*/
@@ -1127,7 +1119,12 @@ public class GuiResource {
public SwtUniversalImage getSwtImageAction(String pluginId) {
SwtUniversalImage image = imagesActions.get(pluginId);
if (image == null) {
- return getSwtImageMissing();
+ IPlugin plugin = findPluginByMainId(ActionPluginType.class, pluginId);
+ if (plugin == null) {
+ return getSwtImageMissing();
+ }
+ image = loadPluginUniversalImage(plugin);
+ imagesActions.put(pluginId, image);
}
return image;
}
@@ -1141,7 +1138,12 @@ public class GuiResource {
public SwtUniversalImage getSwtImageTransform(String pluginId) {
SwtUniversalImage image = imagesTransforms.get(pluginId);
if (image == null) {
- return getSwtImageMissing();
+ IPlugin plugin = findPluginByMainId(TransformPluginType.class, pluginId);
+ if (plugin == null) {
+ return getSwtImageMissing();
+ }
+ image = loadPluginUniversalImage(plugin);
+ imagesTransforms.put(pluginId, image);
}
return image;
}
@@ -1153,7 +1155,17 @@ public class GuiResource {
*/
public Image getImage(IValueMeta valueMeta) {
if (valueMeta == null) return this.imageLabel;
- return imagesValueMeta.get(String.valueOf(valueMeta.getType()));
+ String pluginId = String.valueOf(valueMeta.getType());
+ Image image = imagesValueMeta.get(pluginId);
+ if (image == null) {
+ IPlugin plugin = findPluginByMainId(ValueMetaPluginType.class, pluginId);
+ if (plugin == null) {
+ return null;
+ }
+ image = loadPluginImage(plugin, imageLabel);
+ imagesValueMeta.put(pluginId, image);
+ }
+ return image;
}
/**
@@ -1163,7 +1175,17 @@ public class GuiResource {
*/
public Image getImage(IDatabase database) {
if (database == null) return this.getImageDatabase();
- return imagesDatabase.get(database.getPluginId());
+ String pluginId = database.getPluginId();
+ Image image = imagesDatabase.get(pluginId);
+ if (image == null) {
+ IPlugin plugin = findPluginByMainId(DatabasePluginType.class, pluginId);
+ if (plugin == null) {
+ return null;
+ }
+ image = loadPluginImage(plugin, getImageDatabase());
+ imagesDatabase.put(pluginId, image);
+ }
+ return image;
}
/**
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
index cf8b84e7dd..dd8786e708 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
@@ -77,6 +77,7 @@ import org.apache.hop.core.security.HopSecurity;
import org.apache.hop.core.security.HopSecurityContext;
import org.apache.hop.core.security.HopSecurityPrivilegeMode;
import org.apache.hop.core.security.Permission;
+import org.apache.hop.core.svg.SvgCache;
import org.apache.hop.core.undo.ChangeAction;
import org.apache.hop.core.util.TranslateUtil;
import org.apache.hop.core.variables.DescribedVariable;
@@ -664,9 +665,18 @@ public class HopGui
if (!HopEnvironment.isInitialized()) {
HopEnvironment.init();
}
+
OsHelper.setAppName();
Display display = setupDisplay();
+ // The common GUI images are loaded a little later: parse their SVGs
meanwhile (SvgCache, not
+ // SwtSvgImageUtil: initialising that class before the display exists
would create one).
+ // Batik sets up AWT (toolkit, graphics environment, cursors) the first
time it measures an
+ // image. On macOS that has to run on this first thread, so do it here
and not in the
+ // background thread.
+ initializeBatikAwt();
+ SvgCache.preloadFolderInBackground(GuiResource.class, "ui/images/");
+
// Note: this needs to be done before the look and feel is set
OsHelper.initOsHandlers(display);
@@ -794,6 +804,9 @@ public class HopGui
shell.setMaximized(true);
}
+ // The transform and action icons are loaded on first use: parse them now,
in the background
+ GuiResource.getInstance().preloadPluginIconsInBackground();
+
display.asyncExec(
() -> {
openingLastFiles = true;
@@ -1298,6 +1311,14 @@ public class HopGui
}
}
+ private static void initializeBatikAwt() {
+ try {
+ Class.forName("org.apache.batik.bridge.CursorManager", true,
HopGui.class.getClassLoader());
+ } catch (Throwable e) {
+ // Batik then sets AWT up when it first needs it, as before
+ }
+ }
+
private static Display setupDisplay() {
// Bootstrap Hop
//
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/ConfigurationPerspective.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/ConfigurationPerspective.java
index 2b27cdd682..2b7523eb08 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/ConfigurationPerspective.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/ConfigurationPerspective.java
@@ -104,6 +104,7 @@ public class ConfigurationPerspective implements
IHopPerspective {
private Map<String, Control> categoryTabs = new HashMap<>();
private String currentCategoryName;
+ private boolean tabsBuilt;
private List<Object> tabInstances = new ArrayList<>(); // Store tab
instances for refreshing
private List<Control> highlightedControls = new ArrayList<>();
private String currentSearchText = ""; // Track current search for
re-applying highlights
@@ -161,6 +162,7 @@ public class ConfigurationPerspective implements
IHopPerspective {
@Override
public void perspectiveActivated() {
+ buildSettingTabs();
// Reload values in all tabs when perspective is activated
// This ensures that changes made outside the dialog (e.g., "Do not ask
this again") are
// reflected
@@ -283,6 +285,37 @@ public class ConfigurationPerspective implements
IHopPerspective {
PropsUi.setLook(contentArea);
contentArea.setLayoutData(new FormDataBuilder().fullSize().result());
+ // The settings tabs are built when the perspective is first shown, see
buildSettingTabs()
+
+ SashFormMemory.persist(sashForm, "configuration-perspective-tree-width");
+
+ // Register with key handler so activate shortcut (Ctrl+Shift+C /
Cmd+Shift+C) works
+ HopGuiKeyHandler keyHandler = HopGuiKeyHandler.getInstance();
+ keyHandler.addParentObjectToHandle(this);
+ Shell shell = (sashForm != null && !sashForm.isDisposed()) ?
sashForm.getShell() : null;
+ if (shell != null) {
+ HopGui.getInstance().replaceKeyboardShortcutListeners(shell, keyHandler);
+ }
+
+ hopGui
+ .getEventsHandler()
+ .addEventListener(
+ getClass().getName() + "ProjectActivated",
+ e -> hopGui.getDisplay().asyncExec(this::clearSearchFilters),
+ HopGuiEvents.ProjectActivated.name());
+ }
+
+ /**
+ * Builds the settings tabs and their tree items. Building them takes a
noticeable part of the Hop
+ * GUI start, and most sessions never open this perspective, so it happens
when it is first shown
+ * or first asked for a tab.
+ */
+ private void buildSettingTabs() {
+ if (tabsBuilt || !isInitialized()) {
+ return;
+ }
+ tabsBuilt = true;
+
// Build tabs in a temporary CTabFolder so existing @GuiTab methods work
unchanged
configTabs = new CTabFolder(contentArea, SWT.BORDER);
configTabs.setLayoutData(new FormDataBuilder().fullSize().result());
@@ -325,23 +358,6 @@ public class ConfigurationPerspective implements
IHopPerspective {
}
categoryTree.setSelection(firstItem);
}
-
- SashFormMemory.persist(sashForm, "configuration-perspective-tree-width");
-
- // Register with key handler so activate shortcut (Ctrl+Shift+C /
Cmd+Shift+C) works
- HopGuiKeyHandler keyHandler = HopGuiKeyHandler.getInstance();
- keyHandler.addParentObjectToHandle(this);
- Shell shell = (sashForm != null && !sashForm.isDisposed()) ?
sashForm.getShell() : null;
- if (shell != null) {
- HopGui.getInstance().replaceKeyboardShortcutListeners(shell, keyHandler);
- }
-
- hopGui
- .getEventsHandler()
- .addEventListener(
- getClass().getName() + "ProjectActivated",
- e -> hopGui.getDisplay().asyncExec(this::clearSearchFilters),
- HopGuiEvents.ProjectActivated.name());
}
@Override
@@ -466,6 +482,10 @@ public class ConfigurationPerspective implements
IHopPerspective {
}
private void filterSettings(String searchText) {
+ if (searchText != null && !searchText.trim().isEmpty()) {
+ // Searching looks through the content of the tabs
+ buildSettingTabs();
+ }
// Store the current search text
currentSearchText = searchText != null ? searchText.trim() : "";
@@ -893,6 +913,7 @@ public class ConfigurationPerspective implements
IHopPerspective {
//
return;
}
+ buildSettingTabs();
// Navigate to system variables in the tree
for (TreeItem item : categoryTree.getItems()) {
if (item.getText().toLowerCase().contains("variable")) {
@@ -913,6 +934,7 @@ public class ConfigurationPerspective implements
IHopPerspective {
//
return;
}
+ buildSettingTabs();
Control notificationsPanel = categoryTabs.get("Notifications");
if (notificationsPanel != null && !notificationsPanel.isDisposed()) {
showCategory("Notifications", true);
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 cc449a613d..bf86303bdb 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
@@ -301,6 +301,9 @@ public class MetadataPerspective implements
IHopPerspective, TabClosable, IMetad
return hopGui != null;
}
+ /** Whether the overview cards were not rebuilt on the last refresh, see
refresh(). */
+ private boolean overviewStale;
+
@Override
public String getId() {
return "metadata-perspective";
@@ -318,6 +321,9 @@ public class MetadataPerspective implements
IHopPerspective, TabClosable, IMetad
@Override
public void perspectiveActivated() {
+ if (overviewStale) {
+ updateOverviewCards();
+ }
this.updateSelection();
this.updateGui();
}
@@ -959,6 +965,7 @@ public class MetadataPerspective implements
IHopPerspective, TabClosable, IMetad
return;
}
if (overview != null && !overview.isDisposed()) {
+ overviewStale = false;
overview.setCards(buildOverviewCards());
}
if (editorStackLayout.topControl != overview) {
@@ -2462,7 +2469,17 @@ public class MetadataPerspective implements
IHopPerspective, TabClosable, IMetad
}
reloadModel();
renderTree();
- // Keep the overview catalog in sync when it is the visible panel (no
editor open).
+ // Keep the overview catalog in sync when it is the visible panel (no
editor open). While the
+ // perspective is hidden, as at startup, the cards are built when it is
shown.
+ if (isActive()) {
+ updateOverviewCards();
+ } else {
+ overviewStale = true;
+ }
+ }
+
+ private void updateOverviewCards() {
+ overviewStale = false;
if (overview != null
&& !overview.isDisposed()
&& editorStackLayout != null
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/pluginexplorer/HopPluginExplorePerspective.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/pluginexplorer/HopPluginExplorePerspective.java
index 254d4e82ad..e131a02fc7 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/pluginexplorer/HopPluginExplorePerspective.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/pluginexplorer/HopPluginExplorePerspective.java
@@ -88,6 +88,9 @@ public class HopPluginExplorePerspective implements
IHopPerspective {
return "plugin-explorer";
}
+ /** Whether the content was built, see buildContent(). */
+ private boolean contentBuilt;
+
@Override
public void activate() {
hopGui.setActivePerspective(this);
@@ -95,7 +98,7 @@ public class HopPluginExplorePerspective implements
IHopPerspective {
@Override
public void perspectiveActivated() {
- // Do nothing
+ buildContent();
}
@Override
@@ -107,10 +110,6 @@ public class HopPluginExplorePerspective implements
IHopPerspective {
public void initialize(HopGui hopGui, Composite parent) {
this.hopGui = hopGui;
- this.loadPlugin();
-
- PropsUi props = PropsUi.getInstance();
-
composite = new Composite(parent, SWT.NONE);
composite.setLayout(new FormLayout());
PropsUi.setLook(composite);
@@ -122,6 +121,19 @@ public class HopPluginExplorePerspective implements
IHopPerspective {
formData.bottom = new FormAttachment(100, 0);
composite.setLayoutData(formData);
+ // The plugin information and the table are built when the perspective is
first shown
+ }
+
+ private void buildContent() {
+ if (contentBuilt) {
+ return;
+ }
+ contentBuilt = true;
+
+ this.loadPlugin();
+
+ PropsUi props = PropsUi.getInstance();
+
Label label = new Label(composite, SWT.LEFT);
label.setText(BaseMessages.getString(PKG,
"PluginExplorerPerspective.PluginType.Label"));
FormData fdlFields = new FormData();
@@ -176,6 +188,7 @@ public class HopPluginExplorePerspective implements
IHopPerspective {
wPluginView.setLayoutData(fdFields);
this.refresh();
+ composite.layout(true, true);
}
private void loadPlugin() {
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/search/SearchEverywhereDialog.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/SearchEverywhereDialog.java
index afccd0cce9..07d49d4d59 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/search/SearchEverywhereDialog.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/SearchEverywhereDialog.java
@@ -671,7 +671,7 @@ public class SearchEverywhereDialog {
MenuItem menuItem = matches.get(i);
TreeItem item = new TreeItem(group, SWT.NONE);
item.setText(0, commandLabel(menuItem));
- Image image = menuItem.getImage();
+ Image image = GuiMenuWidgets.getImage(menuItem);
if (image != null) {
item.setImage(0, image);
}
diff --git a/ui/src/main/java/org/apache/hop/ui/util/SwtSvgImageUtil.java
b/ui/src/main/java/org/apache/hop/ui/util/SwtSvgImageUtil.java
index 18d24e35d7..3eb7deacce 100644
--- a/ui/src/main/java/org/apache/hop/ui/util/SwtSvgImageUtil.java
+++ b/ui/src/main/java/org/apache/hop/ui/util/SwtSvgImageUtil.java
@@ -165,6 +165,26 @@ public class SwtSvgImageUtil {
return result;
}
+ /**
+ * Parses an SVG image into the SVG cache, from the same places {@link
#getUniversalImage} looks.
+ * Needs no display, so it can run in a background thread. Problems are left
for the real load to
+ * report.
+ */
+ public static void preloadSvg(ClassLoader classLoader, String filename) {
+ if (StringUtils.isBlank(filename) || !SvgSupport.isSvgEnabled()) {
+ return;
+ }
+ String svgName = SvgSupport.toSvgName(filename);
+ for (String location : new String[] {svgName, "/" + svgName, "ui/images/"
+ svgName}) {
+ try {
+ SvgCache.loadSvg(new SvgFile(location, classLoader));
+ return;
+ } catch (Throwable e) {
+ // try the next location
+ }
+ }
+ }
+
/** Load image from several sources. */
public static SwtUniversalImage getUniversalImage(
Display display, ClassLoader classLoader, String filename) {