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) {

Reply via email to