GUI plugins and toolbars
Hop has a lightweight system for contributing UI actions from plugins without hard-coding every button into the core GUI. This page explains how that system works for toolbars — the main way plugins add icons above tables, multi-line text editors, and other widgets.
You will learn:
-
How
@GuiPluginclasses are discovered -
How
@GuiToolbarElementadds a toolbar button -
How listeners receive the host widget (
TableView,TextComposite, …) -
How
@GuiToolbarElementFiltershows or hides buttons (easy to miss) -
How the TextComposite and TableView toolbars are designed for extension (SQL formatters, JSON pretty-print, export, lineage, …)
Related reading: Plugin development, Hop Web antipatterns.
Mental model
@GuiPlugin class
└── @GuiToolbarElement(root = "Some-Toolbar-Id", id = "...", image = "...")
└── static or instance method → invoked when the user clicks
└── @GuiToolbarElementFilter(parentId = "Some-Toolbar-Id") (optional)
└── static boolean method(String itemId, Object host) → show/hide -
A host widget (for example
TextCompositeorTableView) creates a toolbar with a fixed root id (ID_TOOLBAR). -
At startup, Hop scans every
@GuiPluginand registers methods annotated with@GuiToolbarElementunder that root id. -
When the host builds the toolbar,
GuiToolbarWidgetsloads all items for that root, applies filters, and wires click listeners. -
On click, Hop prefers a public static method that takes the host object as its single argument (the pattern plugins should use).
@GuiPlugin discovery
Classes annotated with @GuiPlugin are registered as the GuiPluginType plugin type. At GUI startup, HopGuiEnvironment.initGuiPlugins() reflects over every such class and records:
-
@GuiToolbarElementmethods → toolbar items -
@GuiToolbarElementFiltermethods → show/hide rules for a toolbar root -
Also: menu elements, keyboard shortcuts, tabs, context actions, and similar GUI contributions
@GuiPlugin
public class MyToolbarContributions {
// static methods with @GuiToolbarElement / @GuiToolbarElementFilter
} Put the class on the plugin classpath (under plugins/…) so the plugin system can load it. hop-ui is typically a provided dependency for transform/action plugins that already ship dialogs.
On Hop Web, reflection at registration time loads every type that appears in method and field signatures of @GuiPlugin classes. Do not put desktop-only SWT types (for example org.eclipse.swt.custom.StyledText) in those signatures. Use abstractions such as TextComposite instead. See the GuiPluginWebCompatibilityTest and Hop Web antipatterns. |
Contributing a toolbar button: @GuiToolbarElement
Annotation (package org.apache.hop.core.gui.plugin.toolbar):
| Attribute | Meaning |
|---|---|
| Toolbar root id this item belongs to (must match the host’s |
| Unique item id (use a stable, namespaced string so other plugins can refer to or filter it). |
| SVG path loaded via the plugin classloader (for example |
| Tooltip; use |
| Optional text label (buttons are usually icon-only). |
| Defaults to |
| If |
| Layout hints for non-button types. |
Preferred listener shape (plugins)
Use a public static method with one parameter: the host widget type (or a supertype / interface).
@GuiToolbarElement(
root = TextComposite.ID_TOOLBAR,
id = "textcomposite-toolbar-20010-format-json",
toolTip = "i18n::MyPlugin.FormatJson.ToolTip",
separator = true,
image = "json-input.svg")
public static void formatJson(TextComposite text) {
// use text.getText(), text.insert(...), text.getStyleType(), ...
} GuiToolbarWidgets / BaseGuiWidgets resolve listeners as follows:
-
If a host object was registered on the toolbar, look for a static method whose single parameter is assignable from that host’s class.
-
Otherwise fall back to an instance method on a plugin singleton (no-arg, or with
Event).
Static methods that take the host object are the reliable pattern for per-widget toolbars (TableView, TextComposite), because many instances exist at once.
Built-in items on the host class
Hosts such as TableView and TextComposite are themselves @GuiPlugin classes. Their built-in actions are also @GuiToolbarElement methods (sometimes static helpers on the abstract base). Plugin ids should use higher numeric ranges (for example 20000+) so they sort after core items.
Showing or hiding buttons: @GuiToolbarElementFilter
This annotation is powerful and easy to overlook. It is the supported way to conditionally show toolbar items (for example only for SQL editors, only for database metadata lines, only when editable).
Contract
@GuiToolbarElementFilter(parentId = TextComposite.ID_TOOLBAR)
public static boolean isButtonShown(String itemId, Object guiPluginInstance) {
// return true → show the item; false → hide it
} | Rule | Detail |
|---|---|
Annotation |
|
Method shape | Must be |
| The |
| The host object registered with the toolbar (for example the |
Return value |
|
Multiple filters | All registered filters for that toolbar root are consulted. If any filter returns |
Critical pitfall: do not hide everyone else’s buttons
Filters run for every toolbar item under that root, not only for your button. If your filter returns false for ids it does not own, you will hide undo, cut, find, and every other contribution.
Always start with:
if (!MY_BUTTON_ID.equals(itemId)) {
return true; // leave all other items alone
}
// then decide for MY_BUTTON_ID only Critical pitfall: filters run mid-construction
GuiToolbarElementFilter methods are invoked when the host builds its toolbar — for TextComposite that is inside the base constructor, before subclasses create the underlying Text / StyledText widget.
Safe in a filter:
-
Values set before
addToolbar()(for example constructorstyleType) -
Identity / type of the host object (
instanceof TextComposite) -
Other fields already initialized on the host
Not safe in a filter (defer to the button action instead):
-
isEditable(),getText(),getSelectionText(), caret position, and similar methods that touch the child text control -
Anything that assumes the host’s full construction has finished
// Filter: only what is available at construction time
@GuiToolbarElementFilter(parentId = TextComposite.ID_TOOLBAR)
public static boolean isButtonShown(String buttonId, Object guiPluginInstance) {
if (!MY_ID.equals(buttonId)) {
return true;
}
if (!(guiPluginInstance instanceof TextComposite text)) {
return false;
}
return TextComposite.STYLE_TYPE_JSON.equalsIgnoreCase(text.getStyleType());
}
// Action: runtime checks (editability, content, …)
@GuiToolbarElement(root = TextComposite.ID_TOOLBAR, id = MY_ID, ...)
public static void formatJson(TextComposite text) {
if (text == null || text.isDisposed() || !text.isEditable()) {
return;
}
// ...
} Working example (JSON format on TextComposite)
From the JSON transform plugin (TextCompositeToolbarJsonFormatButton):
@GuiToolbarElementFilter(parentId = TextComposite.ID_TOOLBAR)
public static boolean isButtonShown(String buttonId, Object guiPluginInstance) {
if (!ID_TOOLBAR_FORMAT_JSON.equals(buttonId)) {
return true;
}
if (!(guiPluginInstance instanceof TextComposite textComposite)) {
return false;
}
// styleType only — isEditable() is checked in formatJson() at click time
return TextComposite.STYLE_TYPE_JSON.equalsIgnoreCase(textComposite.getStyleType());
} Another core example: MetaSelectionLineClearDbCacheToolbarItem filters a clear-cache button so it only appears for database metadata lines.
Extensible host: TextComposite toolbar
org.apache.hop.ui.core.widget.TextComposite is the multi-line editor used for SQL, scripts, logs, formulas, JSON, and more. It exposes:
| API | Purpose |
|---|---|
| Root id for |
Built-in items | Undo/redo, cut/copy/paste, select all, find, find/replace (when the global “show text editor toolbar” option is on). |
Constructor | Semantic content type for plugins (must be set in the constructor; see below). |
| Safe editing API that works on desktop and Hop Web backends. |
styleType (do not use instanceof)
On Hop Web, dialogs often replace specialized classes with a plain StyledTextComp. So instanceof SQLStyledTextComp is false on Hop Web even when the user is editing SQL. Always use getStyleType() when deciding plugin behavior.
Toolbar filters run when the TextComposite is constructed (during addToolbar()). Pass styleType as a constructor argument so filters see the correct value. Calling setStyleType(…) after construction does not rebuild the toolbar or re-run filters. |
// Correct: style type is known before the toolbar is built
wSql =
EnvironmentUtils.getInstance().isWeb()
? new StyledTextComp(variables, shell, SWT.MULTI | ..., TextComposite.STYLE_TYPE_SQL)
: new SQLStyledTextComp(variables, shell, SWT.MULTI | ...); // defaults to SQL Known style type constants:
| Constant | Typical use |
|---|---|
| Default / unspecified |
| SQL scripts and queries |
| Scripting languages |
| JSON documents or query payloads |
| Logs, diffs, expressions, free text |
| Domain languages |
Free-form strings | Plugins may introduce their own labels (for example |
Specialized subclasses set defaults in their constructors (SQLStyledTextComp → SQL, and so on). Call sites that use a plain StyledTextComp / StyledTextVar (including Hop Web stand-ins) must pass the matching styleType constructor argument.
Ideas enabled by this SPI
Because any plugin can attach to TextComposite.ID_TOOLBAR and branch on styleType, you can ship optional tooling without bloating core dialogs:
-
SQL pretty-print / format
-
SQL lineage or “explain”
-
Visual SQL builders
-
JSON / XML formatters (see the sample JSON format button)
-
Language-specific helpers (Cypher, SOQL, Drools, …)
Keep heavy or optional features in plugins; keep the host widget small.
Extensible host: TableView toolbar
TableView uses the same mechanism with root id TableView.ID_TOOLBAR ("TableView-Toolbar").
Built-in items cover row insert/delete, clipboard, filter, navigate-to-column, undo/redo, and more. Plugins contribute the same way — for example export-to-Excel / export-to-CSV toolbar buttons in the Excel and text-file plugins:
@GuiToolbarElement(
root = TableView.ID_TOOLBAR,
id = "tableview-toolbar-30000-export-to-excel",
toolTip = "i18n::ExcelWidget.ExportToolbarButton.ToolTip",
separator = true,
image = "excelwriter.svg")
public static void export(TableView tableView) {
// ...
} How the host builds the toolbar (for widget authors)
If you own a new composite that should accept plugin buttons:
-
Choose a stable public root id, for example
public static final String ID_TOOLBAR = "MyWidget-Toolbar"; -
Annotate the host class (or a related
@GuiPlugin) with built-in@GuiToolbarElementmethods if needed -
On construction:
toolbarWidgets = new GuiToolbarWidgets(); // Register under the class name elements expect to resolve toolbarWidgets.registerGuiPluginObject(MyWidget.class.getName(), this); IToolbarContainer container = ToolbarFacade.createToolbarContainer(this, SWT.WRAP | SWT.LEFT | SWT.HORIZONTAL); toolbar = container.getControl(); // layout toolbar at the top... toolbarWidgets.createToolbarWidgets(container, ID_TOOLBAR, removeToolItems); -
Prefer
ToolbarFacade/IToolbarContainerso desktop (SWTToolBar) and Hop Web (composite row layout) share one path.
i18n for tooltips
Prefer:
toolTip = "i18n::MyClass.MyButton.ToolTip" with a messages file under the plugin package, for example:
plugins/.../src/main/resources/org/example/myplugin/messages/messages_en_US.properties MyClass.MyButton.ToolTip=Pretty-print JSON Escape variable-like tokens in properties with single quotes when needed ('${VAR}') so they are not treated as interpolation.
Checklist for a new toolbar contribution
-
Class annotated with
@GuiPlugin -
Button method uses
@GuiToolbarElement(root = Host.ID_TOOLBAR, id = "…", image = "…", toolTip = "i18n::…") -
Listener is
public static void name(HostType host)(or a safe supertype) -
No desktop-only SWT types in the GuiPlugin class signatures
-
If visibility depends on context: add
@GuiToolbarElementFilter(parentId = Host.ID_TOOLBAR)with signature(String, Object) -
Filter returns
truefor all item ids that are not yours -
Filter only uses state available at construction (
styleType, notisEditable()/getText()) -
Runtime checks (
isEditable(), empty text, …) live in the action method -
For
TextComposite: passstyleTypein the constructor; usegetStyleType()in filters/actions (notinstanceof) -
SVG image is on the plugin classpath; i18n key exists in
messages_en_US.properties -
Manual check on desktop and, if relevant, Hop Web
Reference source locations
| Topic | Location |
|---|---|
Toolbar widgets runtime |
|
Annotations |
|
Registration scan |
|
Text editor host |
|
Table host |
|
Sample JSON format button |
|
Sample table export buttons |
|
Metadata line filter example |
|