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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ public static void enableHopGuiProject(
hopGui.getTerminalPanel().clearAllTerminals();
}

// Save explorer perspective state for the current project before switching namespace
//
org.apache.hop.ui.hopgui.perspective.explorer.ExplorerPerspective.getInstance()
.saveExplorerStateOnShutdown();

// This is called only in Hop GUI so we want to start with a new set of variables
// It avoids variables from one project showing up in another
//
Expand Down
12 changes: 5 additions & 7 deletions ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -1628,14 +1628,12 @@ public SwtUniversalImage getSwtImageArrowCandidate() {
}

public Color getWidgetBackGroundColor() {
if (PropsUi.getInstance().isDarkMode()) {
return display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND);
}
if (OsHelper.isMac()) {
if (PropsUi.getInstance().isDarkMode()) {
return display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND);
} else {
return colorDemoGray;
}
} else {
return colorWhite;
return colorDemoGray;
}
return colorWhite;
}
}
231 changes: 204 additions & 27 deletions ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ public class HopGui
private GuiToolbarWidgets statusToolbarWidgets;

private Composite perspectivesSidebar;
private ToolBar bottomToolbar;
private Composite bottomToolbar;
private final java.util.List<SidebarToolbarItemDescriptor> sidebarToolbarDescriptors =
new java.util.ArrayList<>();
private java.util.List<SidebarButton> sidebarButtons = new java.util.ArrayList<>();
Expand Down Expand Up @@ -507,6 +507,11 @@ public void shellActivated(ShellEvent shellevent) {

// Terminal restoration is handled by the Projects plugin

// Restore explorer perspective state (file explorer panel visibility) for current
// namespace (default or project set by extension point).
//
ExplorerPerspective.getInstance().applyRestoredState();

// We need to start tracking file history again.
//
reOpeningFiles = false;
Expand Down Expand Up @@ -1593,12 +1598,16 @@ protected void addPerspectivesToolbar() {
fdPerspectivesContainer.right = new FormAttachment(100, 0);
perspectivesContainer.setLayoutData(fdPerspectivesContainer);

bottomToolbar = new ToolBar(perspectivesSidebar, SWT.WRAP | SWT.RIGHT | SWT.VERTICAL);
PropsUi.setLook(bottomToolbar, Props.WIDGET_STYLE_TOOLBAR);
bottomToolbar = new Composite(perspectivesSidebar, SWT.NONE);
GridLayout bottomLayout = new GridLayout(1, true);
bottomLayout.marginWidth = 0;
bottomLayout.marginHeight = 0;
bottomLayout.verticalSpacing = 1;
bottomToolbar.setLayout(bottomLayout);
bottomToolbar.setBackground(GuiResource.getInstance().getWidgetBackGroundColor());
FormData fdBottomToolbar = new FormData();
fdBottomToolbar.left = new FormAttachment(0, 0);
fdBottomToolbar.right = new FormAttachment(100, 0);
// Add small margin at bottom to create visual separation from project/environment dropdowns
fdBottomToolbar.bottom = new FormAttachment(100, -4);
bottomToolbar.setLayoutData(fdBottomToolbar);

Expand All @@ -1618,16 +1627,26 @@ protected void addPerspectivesToolbar() {
terminalPanel.toggleTerminal();
}
})
.selectedSupplier(() -> terminalPanel != null && terminalPanel.isTerminalVisible())
.available(!EnvironmentUtils.getInstance().isWeb())
.build());
sidebarToolbarDescriptors.add(
SidebarToolbarItemDescriptor.builder()
.id(SIDEBAR_TOOLBAR_ITEM_EXECUTION_RESULTS)
.visibleForPerspectiveIds(Set.of(PERSPECTIVE_ID_EXPLORER))
.imagePath("ui/images/show-results.svg")
.activeImagePath("ui/images/hide-results.svg")
.imageSize(sidebarIconSize)
.tooltip("Toggle Execution Results (Logging/Metrics/Problems)")
.onSelect(this::toggleExecutionResults)
.selectedSupplier(
() -> {
HopGuiPipelineGraph pg = getActivePipelineGraph();
if (pg != null) return pg.isExecutionResultsPaneVisible();
HopGuiWorkflowGraph wg = getActiveWorkflowGraph();
if (wg != null) return wg.isExecutionResultsPaneVisible();
return false;
})
.available(true)
.build());

Expand Down Expand Up @@ -1910,6 +1929,7 @@ public void setActivePerspective(IHopPerspective perspective) {

perspectiveManager.notifyPerspectiveActivated(perspective);

updateSidebarButtonSelection(perspective);
refreshBottomToolbarItems();
}

Expand All @@ -1930,9 +1950,8 @@ public void addSidebarToolbarItem(SidebarToolbarItemDescriptor descriptor) {
/**
* Refresh the bottom sidebar toolbar so only items visible for the current perspective are shown.
* Items are added in reverse descriptor order so that the second, third, etc. buttons appear
* above the first (SWT vertical toolbar lays out first-added at top). This avoids overlapping and
* keeps perspective-specific buttons (e.g. execution) above the always-visible ones (e.g.
* terminal).
* above the first (GridLayout lays out first-added at top). This avoids overlapping and keeps
* perspective-specific buttons (e.g. execution) above the always-visible ones (e.g. terminal).
*/
public void refreshBottomToolbarItems() {
if (bottomToolbar == null || bottomToolbar.isDisposed()) {
Expand All @@ -1959,34 +1978,190 @@ public void refreshBottomToolbarItems() {
}
}

// Dispose existing items
for (ToolItem item : bottomToolbar.getItems()) {
item.dispose();
// Dispose existing children
for (Control child : bottomToolbar.getChildren()) {
child.dispose();
}

// Add in reverse order: last in list becomes first (top) in toolbar so we don't overlap
Color normalBg = GuiResource.getInstance().getWidgetBackGroundColor();
Color selectionBg = GuiResource.getInstance().getColorLightBlue();
Color hoverBg = GuiResource.getInstance().getColorGray();
int buttonSize = (int) (34 * PropsUi.getNativeZoomFactor());

// Add in reverse order: last in list becomes first (top) in toolbar
for (int i = visible.size() - 1; i >= 0; i--) {
SidebarToolbarItemDescriptor d = visible.get(i);
ToolItem item = new ToolItem(bottomToolbar, SWT.PUSH);
Image img =
GuiResource.getInstance().getImage(d.getImagePath(), d.getImageSize(), d.getImageSize());
item.setImage(img);
item.setToolTipText(d.getTooltip());
item.setData("descriptor", d);
item.addListener(
SWT.Selection,
event -> {
SidebarToolbarItemDescriptor desc =
(SidebarToolbarItemDescriptor) item.getData("descriptor");
if (desc != null && desc.getOnSelect() != null) {
desc.getOnSelect().run();
}
});

if (EnvironmentUtils.getInstance().isWeb()) {
// RAP: use Composite + Label (same pattern as SidebarButton)
Composite comp = new Composite(bottomToolbar, SWT.NONE);
comp.setToolTipText(d.getTooltip());
comp.setBackground(normalBg);
comp.setData("descriptor", d);
comp.setData("org.eclipse.rap.rwt.customVariant", "sidebarButton");

GridLayout gl = new GridLayout(1, false);
gl.marginWidth = 0;
gl.marginHeight = 0;
comp.setLayout(gl);

Label imgLabel = new Label(comp, SWT.NONE);
imgLabel.setImage(resolveButtonImage(d));
imgLabel.setBackground(normalBg);
imgLabel.setToolTipText(d.getTooltip());
imgLabel.setData("org.eclipse.rap.rwt.customVariant", "sidebarButton");
imgLabel.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, true));

GridData compGd = new GridData();
compGd.widthHint = buttonSize;
compGd.heightHint = buttonSize;
comp.setLayoutData(compGd);

Runnable updateVisual =
() -> {
boolean sel = d.getSelectedSupplier().getAsBoolean();
boolean hov = Boolean.TRUE.equals(comp.getData("hovered"));
Color bg = sel ? selectionBg : hov ? hoverBg : normalBg;
comp.setBackground(bg);
imgLabel.setBackground(bg);
imgLabel.setImage(resolveButtonImage(d));
comp.redraw();
};
comp.setData("updateVisual", updateVisual);

comp.addListener(
SWT.MouseEnter,
e -> {
comp.setData("hovered", true);
updateVisual.run();
});
comp.addListener(
SWT.MouseExit,
e -> {
comp.setData("hovered", false);
updateVisual.run();
});
comp.addListener(
SWT.MouseDown,
e -> {
if (d.getOnSelect() != null) d.getOnSelect().run();
updateVisual.run();
});
imgLabel.addListener(
SWT.MouseEnter,
e -> {
comp.setData("hovered", true);
updateVisual.run();
});
imgLabel.addListener(
SWT.MouseExit,
e -> {
comp.setData("hovered", false);
updateVisual.run();
});
imgLabel.addListener(
SWT.MouseDown,
e -> {
if (d.getOnSelect() != null) d.getOnSelect().run();
updateVisual.run();
});

updateVisual.run();
} else {
// Desktop SWT: Canvas with custom painting (matches SidebarButton style)
Canvas canvas = new Canvas(bottomToolbar, SWT.NONE);
canvas.setToolTipText(d.getTooltip());
canvas.setBackground(normalBg);
canvas.setData("descriptor", d);
canvas.setData("selected", d.getSelectedSupplier().getAsBoolean());
canvas.setData("hovered", false);

GridData gd = new GridData();
gd.widthHint = buttonSize;
gd.heightHint = buttonSize;
canvas.setLayoutData(gd);

canvas.addPaintListener(
e -> {
GC gc = e.gc;
Point size = canvas.getSize();
gc.setAntialias(SWT.ON);

boolean sel = Boolean.TRUE.equals(canvas.getData("selected"));
boolean hov = Boolean.TRUE.equals(canvas.getData("hovered"));
gc.setBackground(sel ? selectionBg : hov ? hoverBg : normalBg);
gc.fillRoundRectangle(4, 4, size.x - 8, size.y - 8, 8, 8);

Image currentImg = resolveButtonImage(d);
if (currentImg != null && !currentImg.isDisposed()) {
Rectangle imgBounds = currentImg.getBounds();
int x = (size.x - imgBounds.width) / 2;
int y = (size.y - imgBounds.height) / 2;
gc.drawImage(currentImg, x, y);
}
});

canvas.addListener(
SWT.MouseEnter,
e -> {
canvas.setData("hovered", true);
canvas.redraw();
});
canvas.addListener(
SWT.MouseExit,
e -> {
canvas.setData("hovered", false);
canvas.redraw();
});
canvas.addListener(
SWT.MouseDown,
e -> {
if (d.getOnSelect() != null) {
d.getOnSelect().run();
}
canvas.setData("selected", d.getSelectedSupplier().getAsBoolean());
canvas.redraw();
});
}
}
bottomToolbar.pack();
bottomToolbar.layout(true, true);
bottomToolbar.getParent().layout(true, true);
}

/**
* Update the visual selected/active state of all bottom sidebar toolbar buttons. Call this after
* an action that changes the state externally (e.g. terminal toggled via keyboard shortcut).
*/
public void refreshSidebarToolbarButtonStates() {
if (bottomToolbar == null || bottomToolbar.isDisposed()) {
return;
}
for (Control child : bottomToolbar.getChildren()) {
SidebarToolbarItemDescriptor desc =
(SidebarToolbarItemDescriptor) child.getData("descriptor");
if (desc != null) {
boolean selected = desc.getSelectedSupplier().getAsBoolean();
if (EnvironmentUtils.getInstance().isWeb()) {
Runnable updateVisual = (Runnable) child.getData("updateVisual");
if (updateVisual != null) {
updateVisual.run();
}
} else {
child.setData("selected", selected);
child.redraw();
}
}
}
}

/** Resolve the correct image for a sidebar toolbar button based on its current selected state. */
private Image resolveButtonImage(SidebarToolbarItemDescriptor d) {
boolean active = d.getSelectedSupplier().getAsBoolean();
String path =
active && !d.getActiveImagePath().isEmpty() ? d.getActiveImagePath() : d.getImagePath();
return GuiResource.getInstance().getImage(path, d.getImageSize(), d.getImageSize());
}

public boolean isActivePerspective(IHopPerspective perspective) {
return activePerspective != null && activePerspective.equals(perspective);
}
Expand Down Expand Up @@ -2088,12 +2263,14 @@ public void toggleExecutionResults() {
HopGuiPipelineGraph pipelineGraph = getActivePipelineGraph();
if (pipelineGraph != null) {
pipelineGraph.showExecutionResults();
refreshSidebarToolbarButtonStates();
return;
}

HopGuiWorkflowGraph workflowGraph = getActiveWorkflowGraph();
if (workflowGraph != null) {
workflowGraph.showExecutionResults();
refreshSidebarToolbarButtonStates();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import java.util.Collections;
import java.util.Set;
import java.util.function.BooleanSupplier;
import lombok.Builder;
import lombok.Value;

Expand Down Expand Up @@ -57,9 +58,15 @@ public class SidebarToolbarItemDescriptor {
*/
@Builder.Default Set<String> hiddenForPerspectiveIds = Collections.emptySet();

/** Image path (e.g. "ui/images/show-results.svg"). */
/** Image path used when the item is in its default (inactive) state. */
String imagePath;

/**
* Optional image path used when the item is in its active/selected state (e.g.
* "ui/images/hide-results.svg"). When empty, {@link #imagePath} is used for both states.
*/
@Builder.Default String activeImagePath = "";

/** Icon size in pixels. */
int imageSize;

Expand All @@ -69,6 +76,12 @@ public class SidebarToolbarItemDescriptor {
/** Called when the button is selected. */
Runnable onSelect;

/**
* Supplies the current "selected/active" state of this toolbar item. Used to render the button
* with a highlight when its associated panel is visible (e.g. terminal panel open).
*/
@Builder.Default BooleanSupplier selectedSupplier = () -> false;

/** Whether this item is available (e.g. terminal only when not in web). */
@Builder.Default boolean available = true;
}
Loading
Loading