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 3fe5ab0bc0 Split-Screen View, fixes #5715 (#6689)
3fe5ab0bc0 is described below
commit 3fe5ab0bc0e3b2d7d5996c1ae82adc0cad02acb5
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Mon Mar 2 20:09:37 2026 +0100
Split-Screen View, fixes #5715 (#6689)
---
.../apache/hop/projects/gui/ProjectsGuiPlugin.java | 3 +-
.../ui/hopgui/delegates/HopGuiAuditDelegate.java | 114 +++-
.../ui/hopgui/delegates/HopGuiFileDelegate.java | 9 +-
.../ui/hopgui/file/pipeline/HopGuiLogBrowser.java | 10 +-
.../hopgui/file/pipeline/HopGuiPipelineGraph.java | 12 +
.../hopgui/file/workflow/HopGuiWorkflowGraph.java | 12 +
.../hop/ui/hopgui/perspective/IHopPerspective.java | 20 +
.../hop/ui/hopgui/perspective/TabClosable.java | 13 +-
.../hop/ui/hopgui/perspective/TabCloseHandler.java | 9 +-
.../hop/ui/hopgui/perspective/TabItemReorder.java | 102 +++-
.../perspective/explorer/ExplorerPerspective.java | 638 ++++++++++++++++-----
.../explorer/messages/messages_en_US.properties | 2 +
12 files changed, 764 insertions(+), 180 deletions(-)
diff --git
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/ProjectsGuiPlugin.java
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/ProjectsGuiPlugin.java
index 44e5360658..d98ee62323 100644
---
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/ProjectsGuiPlugin.java
+++
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/gui/ProjectsGuiPlugin.java
@@ -215,8 +215,7 @@ public class ProjectsGuiPlugin {
hopGui.setVariables(variables);
// Re-open last open files for the namespace
- //
- hopGui.auditDelegate.openLastFiles();
+ hopGui.getDisplay().asyncExec(() ->
hopGui.auditDelegate.openLastFiles());
// Restore terminal tabs for the new project (per-project terminals)
//
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiAuditDelegate.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiAuditDelegate.java
index 8d4f11e1f8..4903c5abe1 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiAuditDelegate.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiAuditDelegate.java
@@ -20,6 +20,7 @@ package org.apache.hop.ui.hopgui.delegates;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import org.apache.hop.core.exception.HopException;
import org.apache.hop.history.AuditList;
@@ -36,9 +37,12 @@ import org.apache.hop.ui.core.gui.HopNamespace;
import org.apache.hop.ui.core.metadata.MetadataEditor;
import org.apache.hop.ui.core.metadata.MetadataManager;
import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.file.HopFileTypeRegistry;
+import org.apache.hop.ui.hopgui.file.IHopFileType;
import org.apache.hop.ui.hopgui.file.IHopFileTypeHandler;
import org.apache.hop.ui.hopgui.perspective.IHopPerspective;
import org.apache.hop.ui.hopgui.perspective.TabItemHandler;
+import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerPerspective;
import org.apache.hop.ui.hopgui.perspective.metadata.MetadataPerspective;
import org.apache.hop.ui.util.SwtErrorHandler;
import org.eclipse.swt.SWT;
@@ -48,8 +52,24 @@ public class HopGuiAuditDelegate {
private static final Class<?> PKG = HopGuiAuditDelegate.class;
public static final String STATE_PROPERTY_ACTIVE = "active";
+
+ /** Explorer perspective: pane index 0 = left, 1 = right. */
+ public static final String STATE_PROPERTY_PANE = "pane";
+
public static final String METADATA_FILENAME_PREFIX = "METADATA:";
+ /**
+ * File type name in state so the same file can be restored in different
modes (e.g. pipeline vs
+ * text).
+ */
+ public static final String STATE_PROPERTY_FILETYPE = "fileType";
+
+ /**
+ * Delimiter between file type name and filename in the audit key. Enables
multiple tabs for the
+ * same file. Uses a character that does not appear in paths or type names.
+ */
+ private static final String TAB_KEY_DELIMITER = "\u0001";
+
private HopGui hopGui;
public HopGuiAuditDelegate(HopGui hopGui) {
@@ -98,6 +118,11 @@ public class HopGuiAuditDelegate {
auditStateMap = new AuditStateMap();
}
+ // Restore editor split state before opening files so tabs land in the
correct pane
+ if (perspective instanceof ExplorerPerspective explorerPerspective) {
+ explorerPerspective.applyRestoredEditorSplitState();
+ }
+
for (String filename : auditList.getNames()) {
try {
if (StringUtils.isNotEmpty(filename)) {
@@ -112,11 +137,55 @@ public class HopGuiAuditDelegate {
openMetadataObject(className, name);
}
} else {
- // Regular filename
- IHopFileTypeHandler fileTypeHandler =
hopGui.fileDelegate.fileOpen(filename, false);
+ // Regular file: key may be composite
"fileTypeName\u0001filename" or legacy
+ // "filename"
+ String tabKey = filename;
+ String resolvedFilename = filename;
+ IHopFileType hopFileToUse = null;
+ if (filename.contains(TAB_KEY_DELIMITER)) {
+ String[] parts =
filename.split(Pattern.quote(TAB_KEY_DELIMITER), 2);
+ if (parts.length >= 2) {
+ String fileTypeName = parts[0];
+ resolvedFilename = parts[1];
+ hopFileToUse =
+
HopFileTypeRegistry.getInstance().getFileTypeByName(fileTypeName);
+ }
+ }
+ if (hopFileToUse == null) {
+ hopFileToUse =
+
HopFileTypeRegistry.getInstance().findHopFileType(resolvedFilename);
+ }
+
+ // Set target pane for Explorer split view before opening
+ if (perspective instanceof ExplorerPerspective
explorerPerspective) {
+ AuditState auditState = auditStateMap.get(tabKey);
+ Object paneObj =
+ auditState != null ?
auditState.getStateMap().get(STATE_PROPERTY_PANE) : null;
+ int pane = 0;
+ if (paneObj instanceof Number) {
+ pane = ((Number) paneObj).intValue();
+ } else if (paneObj != null) {
+ try {
+ pane = Integer.parseInt(paneObj.toString());
+ } catch (NumberFormatException ignored) {
+ pane = 0;
+ }
+ }
+ if (pane == 1 && explorerPerspective.getRightTabFolder() !=
null) {
+
perspective.setDropTargetFolder(explorerPerspective.getRightTabFolder());
+ }
+ }
+
+ IHopFileTypeHandler fileTypeHandler = null;
+ if (hopFileToUse != null) {
+ fileTypeHandler =
+ hopGui.fileDelegate.fileOpenWithType(resolvedFilename,
hopFileToUse, false);
+ } else {
+ fileTypeHandler =
hopGui.fileDelegate.fileOpen(resolvedFilename, false);
+ }
if (fileTypeHandler != null) {
// Restore zoom, scroll and so on
- AuditState auditState = auditStateMap.get(filename);
+ AuditState auditState = auditStateMap.get(tabKey);
if (auditState != null) {
fileTypeHandler.applyStateProperties(auditState.getStateMap());
@@ -125,13 +194,22 @@ public class HopGuiAuditDelegate {
activeFileTypeHandler = fileTypeHandler;
}
}
+ } else if (hopFileToUse == null) {
+ failedFiles.add(resolvedFilename);
}
}
}
} catch (Exception e) {
// Collect failed files instead of showing error dialog immediately
- hopGui.getLog().logError("Error opening file '" + filename + "'",
e);
- failedFiles.add(filename);
+ String displayName = filename;
+ if (filename.contains(TAB_KEY_DELIMITER)) {
+ String[] p = filename.split(Pattern.quote(TAB_KEY_DELIMITER), 2);
+ if (p.length >= 2) {
+ displayName = p[1];
+ }
+ }
+ hopGui.getLog().logError("Error opening file '" + displayName +
"'", e);
+ failedFiles.add(displayName);
}
}
@@ -232,22 +310,32 @@ public class HopGuiAuditDelegate {
IHopFileTypeHandler activeFileTypeHandler =
perspective.getActiveFileTypeHandler();
List<TabItemHandler> tabItems = perspective.getItems();
if (tabItems != null) {
+ // Use pane order for Explorer (left then right) so restore order
matches split layout
+ List<TabItemHandler> tabItemsToSave =
+ perspective instanceof ExplorerPerspective ep
+ ? ep.getTabItemHandlersInPaneOrder()
+ : tabItems;
+
// This perspective has the ability to handle multiple files.
// Let's save the files in the given order...
//
AuditStateMap auditStateMap = new AuditStateMap();
List<String> files = new ArrayList<>();
- for (TabItemHandler tabItem : tabItems) {
+ for (TabItemHandler tabItem : tabItemsToSave) {
IHopFileTypeHandler typeHandler = tabItem.getTypeHandler();
String filename = typeHandler.getFilename();
String name = typeHandler.getName();
if (StringUtils.isNotEmpty(filename)) {
- // Regular filename
+ // Regular filename — use composite key (fileType + filename) so
same file in
+ // different modes (e.g. pipeline vs text) gets separate tabs
//
- files.add(filename);
+ IHopFileType fileType = typeHandler.getFileType();
+ String tabKey =
+ (fileType != null ? fileType.getName() : "") +
TAB_KEY_DELIMITER + filename;
+ files.add(tabKey);
- // Also save the state : active, zoom, ...
+ // Also save the state : active, zoom, pane (Explorer split),
fileType, ...
//
Map<String, Object> stateProperties =
typeHandler.getStateProperties();
boolean active =
@@ -255,8 +343,14 @@ public class HopGuiAuditDelegate {
&& activeFileTypeHandler.getFilename() != null
&& activeFileTypeHandler.getFilename().equals(filename);
stateProperties.put(STATE_PROPERTY_ACTIVE, active);
+ if (fileType != null) {
+ stateProperties.put(STATE_PROPERTY_FILETYPE, fileType.getName());
+ }
+ if (perspective instanceof ExplorerPerspective ep) {
+ stateProperties.put(STATE_PROPERTY_PANE,
ep.getPaneIndexForTab(tabItem.getTabItem()));
+ }
- auditStateMap.add(new AuditState(filename, stateProperties));
+ auditStateMap.add(new AuditState(tabKey, stateProperties));
} else if (typeHandler instanceof MetadataEditor<?> metadataEditor
&& StringUtils.isNotEmpty(name)) {
// Don't save new unchanged metadata objects...
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiFileDelegate.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiFileDelegate.java
index 2a949cd6cd..a7c317fff8 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiFileDelegate.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiFileDelegate.java
@@ -108,7 +108,6 @@ public class HopGuiFileDelegate {
public IHopFileTypeHandler fileOpen(String filename, boolean
activatePerspective)
throws Exception {
-
HopFileTypeRegistry fileRegistry = HopFileTypeRegistry.getInstance();
IHopFileType hopFile = fileRegistry.findHopFileType(filename);
if (hopFile == null) {
@@ -119,7 +118,15 @@ public class HopGuiFileDelegate {
+ filename
+ "'");
}
+ return fileOpenWithType(filename, hopFile, activatePerspective);
+ }
+ /**
+ * Open a file with a specific file type. Used when restoring tabs so the
same file can be
+ * reopened in different modes (e.g. pipeline and text).
+ */
+ public IHopFileTypeHandler fileOpenWithType(
+ String filename, IHopFileType hopFile, boolean activatePerspective)
throws Exception {
IHopFileTypeHandler fileTypeHandler = hopFile.openFile(hopGui, filename,
hopGui.getVariables());
if (fileTypeHandler != null) {
hopGui.handleFileCapabilities(
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiLogBrowser.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiLogBrowser.java
index fa939b33db..7238a50912 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiLogBrowser.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiLogBrowser.java
@@ -95,11 +95,7 @@ public class HopGuiLogBrowser {
() -> {
IHasLogChannel provider =
logProvider.getLogChannelProvider();
- if (provider != null
- && !text.isDisposed()
- && !busy.get()
- && !paused.get()
- && text.isVisible()) {
+ if (provider != null && !text.isDisposed() &&
!busy.get() && !paused.get()) {
busy.set(true);
ILogChannel logChannel = provider.getLogChannel();
@@ -218,7 +214,9 @@ public class HopGuiLogBrowser {
}
}
- text.setSelection(text.getCharCount());
+ if (!text.isDisposed()) {
+ text.setSelection(text.getCharCount());
+ }
lastLogId.set(lastNr);
}
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
index f40d80f903..1912ef774c 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
@@ -542,6 +542,16 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
}
canvas.addPaintListener(this::paintControl);
+ addListener(
+ SWT.Show,
+ e ->
+ getDisplay()
+ .asyncExec(
+ () -> {
+ if (!isDisposed() && canvas != null &&
!canvas.isDisposed()) {
+ canvas.redraw();
+ }
+ }));
selectedTransforms = null;
lastClick = null;
@@ -565,6 +575,8 @@ public class HopGuiPipelineGraph extends HopGuiAbstractGraph
hopGui.replaceKeyboardShortcutListeners(this);
canvas.pack();
+ addListener(SWT.Resize, e -> redraw());
+
// Update menu, toolbar, force redraw canvas
//
updateGui();
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
index bd205fa48e..1c4574e587 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
@@ -462,6 +462,16 @@ public class HopGuiWorkflowGraph extends
HopGuiAbstractGraph
}
canvas.addPaintListener(this::paintControl);
+ addListener(
+ SWT.Show,
+ e ->
+ getDisplay()
+ .asyncExec(
+ () -> {
+ if (!isDisposed() && canvas != null &&
!canvas.isDisposed()) {
+ canvas.redraw();
+ }
+ }));
selectedActions = null;
lastClick = null;
@@ -478,6 +488,8 @@ public class HopGuiWorkflowGraph extends HopGuiAbstractGraph
canvas.pack();
+ addListener(SWT.Resize, e -> redraw());
+
updateGui();
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/IHopPerspective.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/IHopPerspective.java
index 2889997ef5..7f562a0c87 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/IHopPerspective.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/IHopPerspective.java
@@ -24,6 +24,7 @@ import
org.apache.hop.ui.hopgui.context.IActionContextHandlersProvider;
import org.apache.hop.ui.hopgui.file.IHopFileType;
import org.apache.hop.ui.hopgui.file.IHopFileTypeHandler;
import org.apache.hop.ui.hopgui.file.empty.EmptyHopFileTypeHandler;
+import org.eclipse.swt.custom.CTabFolder;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
@@ -127,4 +128,23 @@ public interface IHopPerspective extends
IActionContextHandlersProvider {
default List<ISearchable> getSearchables() {
return List.of();
}
+
+ /**
+ * Called after a tab has been moved between two CTabFolders via
drag-and-drop.
+ *
+ * @param sourceFolder the folder the tab was moved from
+ * @param targetFolder the folder the tab was moved to
+ */
+ default void onTabMovedBetweenFolders(CTabFolder sourceFolder, CTabFolder
targetFolder) {
+ // Do nothing by default
+ }
+
+ /**
+ * Set the target folder that should receive new tabs (e.g. from a file
drop).
+ *
+ * @param folder the folder that received the drop
+ */
+ default void setDropTargetFolder(CTabFolder folder) {
+ // Do nothing by default
+ }
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/TabClosable.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/TabClosable.java
index 299439d23b..838cddcd5c 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/TabClosable.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/TabClosable.java
@@ -30,12 +30,13 @@ public interface TabClosable {
/** Get all the tabs on the right-hand side of the selected one */
default List<CTabItem> getTabsToRight(CTabItem selectedTabItem) {
+ CTabFolder folder = selectedTabItem.getParent();
List<CTabItem> items = new ArrayList<>();
- for (int i = getTabFolder().getItems().length - 1; i >= 0; i--) {
- if (selectedTabItem.equals(getTabFolder().getItems()[i])) {
+ for (int i = folder.getItems().length - 1; i >= 0; i--) {
+ if (selectedTabItem.equals(folder.getItems()[i])) {
break;
} else {
- items.add(getTabFolder().getItems()[i]);
+ items.add(folder.getItems()[i]);
}
}
return items;
@@ -43,8 +44,9 @@ public interface TabClosable {
/** Get all the tabs on the left-hand side of the selected one */
default List<CTabItem> getTabsToLeft(CTabItem selectedTabItem) {
+ CTabFolder folder = selectedTabItem.getParent();
List<CTabItem> items = new ArrayList<>();
- for (CTabItem item : getTabFolder().getItems()) {
+ for (CTabItem item : folder.getItems()) {
if (selectedTabItem.equals(item)) {
break;
} else {
@@ -56,8 +58,9 @@ public interface TabClosable {
/** Get all the other tabs of the selected one */
default List<CTabItem> getOtherTabs(CTabItem selectedTabItem) {
+ CTabFolder folder = selectedTabItem.getParent();
List<CTabItem> items = new ArrayList<>();
- for (CTabItem item : getTabFolder().getItems()) {
+ for (CTabItem item : folder.getItems()) {
if (!selectedTabItem.equals(item)) {
items.add(item);
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/TabCloseHandler.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/TabCloseHandler.java
index 5dc0756b8e..71c8141518 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/TabCloseHandler.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/TabCloseHandler.java
@@ -35,14 +35,17 @@ public class TabCloseHandler {
CTabItem selectedItem;
public TabCloseHandler(TabClosable tabClosablePerspective) {
- this.tabFolder = tabClosablePerspective.getTabFolder();
+ this(tabClosablePerspective, tabClosablePerspective.getTabFolder());
+ }
+
+ public TabCloseHandler(TabClosable tabClosablePerspective, CTabFolder
tabFolder) {
+ this.tabFolder = tabFolder;
Menu menu = new Menu(tabFolder);
tabFolder.setMenu(menu);
tabFolder.addListener(SWT.MenuDetect, this::handleTabMenuDetectEvent);
tabFolder.addListener(SWT.MouseUp, event -> handleMouseUp(event,
tabClosablePerspective));
- // Create menu item
MenuItem miClose = new MenuItem(menu, SWT.NONE);
miClose.setText(BaseMessages.getString(PKG, "HopGui.TabItem.Close.Text"));
miClose.addListener(
@@ -62,7 +65,7 @@ public class TabCloseHandler {
miCloseAll.addListener(
SWT.Selection,
event -> {
- for (CTabItem tabItem :
tabClosablePerspective.getTabFolder().getItems()) {
+ for (CTabItem tabItem : this.tabFolder.getItems()) {
tabClosablePerspective.closeTab(null, tabItem);
}
});
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/TabItemReorder.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/TabItemReorder.java
index 442dfad312..c58d4cee86 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/TabItemReorder.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/TabItemReorder.java
@@ -103,6 +103,7 @@ public class TabItemReorder {
@Override
public void dragFinished(DragSourceEvent event) {
+ dragItem = null;
if (EnvironmentUtils.getInstance().isWeb()) {
return;
}
@@ -207,6 +208,7 @@ public class TabItemReorder {
}
if (event.data instanceof String[] paths
&& perspective instanceof IFileDropReceiver receiver) {
+ perspective.setDropTargetFolder(folder);
receiver.openDroppedFiles(paths);
return;
}
@@ -281,38 +283,71 @@ public class TabItemReorder {
}
private boolean isDropSupported(CTabFolder folder, DropTargetEvent
event) {
- if (dragItem == null) {
+ if (dragItem != null && !dragItem.isDisposed()) {
+ Point point =
folder.toControl(folder.getDisplay().getCursorLocation());
+ return folder.getItem(new Point(point.x, point.y)) != null;
+ }
+ return hasActiveTabTransfer(event);
+ }
+
+ private boolean hasActiveTabTransfer(DropTargetEvent event) {
+ if (event.dataTypes == null) {
return false;
}
- Point point =
folder.toControl(folder.getDisplay().getCursorLocation());
- return folder.getItem(new Point(point.x, point.y)) != null;
+ for (TransferData td : event.dataTypes) {
+ if (TabTransfer.INSTANCE.isSupportedType(td)) {
+ return true;
+ }
+ }
+ return false;
}
});
}
private void moveTabs(CTabFolder folder, DropTargetEvent event) {
+ CTabItem sourceItem = this.dragItem;
+
+ if (sourceItem == null || sourceItem.isDisposed()) {
+ sourceItem = null;
+ }
+
+ if (sourceItem == null && event.data instanceof CTabItem transferredItem) {
+ if (!transferredItem.isDisposed()) {
+ sourceItem = transferredItem;
+ }
+ }
+
+ if (sourceItem == null) {
+ return;
+ }
+
+ if (sourceItem.getParent() != folder) {
+ moveTabBetweenFolders(sourceItem, folder);
+ return;
+ }
+
Point point = folder.toControl(folder.getDisplay().getCursorLocation());
CTabItem dropItem = folder.getItem(new Point(point.x, point.y));
- if (dropItem != null && dragItem != null) {
- Control dragControl = dragItem.getControl();
- String dragText = dragItem.getText();
- Image dragImage = dragItem.getImage();
- String dragToolTip = dragItem.getToolTipText();
- boolean dragShowClose = dragItem.getShowClose();
- Font dragFont = dragItem.getFont();
- IHopFileTypeHandler dragFileTypeHandler = (IHopFileTypeHandler)
dragItem.getData();
+ if (dropItem != null) {
+ Control dragControl = sourceItem.getControl();
+ String dragText = sourceItem.getText();
+ Image dragImage = sourceItem.getImage();
+ String dragToolTip = sourceItem.getToolTipText();
+ boolean dragShowClose = sourceItem.getShowClose();
+ Font dragFont = sourceItem.getFont();
+ IHopFileTypeHandler dragFileTypeHandler = (IHopFileTypeHandler)
sourceItem.getData();
IHopFileTypeHandler dropFileTypeHandler = (IHopFileTypeHandler)
dropItem.getData();
updateTabItemHandler(dragFileTypeHandler, dropItem);
- updateTabItemHandler(dropFileTypeHandler, dragItem);
+ updateTabItemHandler(dropFileTypeHandler, sourceItem);
- dragItem.setText(dropItem.getText());
- dragItem.setImage(dropItem.getImage());
- dragItem.setToolTipText(dropItem.getToolTipText());
- dragItem.setFont(dropItem.getFont());
- dragItem.setData(dropItem.getData());
- dragItem.setShowClose(dropItem.getShowClose());
- dragItem.setControl(dropItem.getControl());
+ sourceItem.setText(dropItem.getText());
+ sourceItem.setImage(dropItem.getImage());
+ sourceItem.setToolTipText(dropItem.getToolTipText());
+ sourceItem.setFont(dropItem.getFont());
+ sourceItem.setData(dropItem.getData());
+ sourceItem.setShowClose(dropItem.getShowClose());
+ sourceItem.setControl(dropItem.getControl());
dropItem.setText(dragText);
dropItem.setImage(dragImage);
@@ -326,6 +361,35 @@ public class TabItemReorder {
}
}
+ private void moveTabBetweenFolders(CTabItem srcItem, CTabFolder dstFolder) {
+ CTabFolder srcFolder = srcItem.getParent();
+ Control control = srcItem.getControl();
+ String text = srcItem.getText();
+ Image image = srcItem.getImage();
+ String tooltip = srcItem.getToolTipText();
+ Font font = srcItem.getFont();
+ IHopFileTypeHandler data = (IHopFileTypeHandler) srcItem.getData();
+ boolean showClose = srcItem.getShowClose();
+
+ control.setParent(dstFolder);
+
+ CTabItem newItem = new CTabItem(dstFolder, SWT.CLOSE);
+ newItem.setText(text);
+ newItem.setImage(image);
+ newItem.setToolTipText(tooltip);
+ newItem.setFont(font);
+ newItem.setData(data);
+ newItem.setShowClose(showClose);
+ newItem.setControl(control);
+
+ updateTabItemHandler(data, newItem);
+
+ srcItem.dispose();
+ dstFolder.setSelection(newItem);
+
+ perspective.onTabMovedBetweenFolders(srcFolder, dstFolder);
+ }
+
private void updateTabItemHandler(IHopFileTypeHandler fileTypeHandler,
CTabItem tabItem) {
for (TabItemHandler item : perspective.getItems()) {
if (fileTypeHandler.equals(item.getTypeHandler())) {
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 4c94c99a04..3dd558e4e4 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
@@ -127,6 +127,7 @@ import org.eclipse.swt.dnd.FileTransfer;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
@@ -199,6 +200,12 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
private static final String EXPLORER_AUDIT_TYPE =
"explorer-perspective-state";
private static final String STATE_PANEL_VISIBLE_KEY = "panel-visible";
private static final String STATE_PANEL_VISIBLE_PROP = "visible";
+ private static final String STATE_EDITOR_SPLIT_KEY = "editor-split";
+ private static final String STATE_EDITOR_SPLIT_PROP = "split";
+ private static final String STATE_EDITOR_SASH_WEIGHTS_KEY =
"editor-sash-weights";
+ private static final String STATE_EDITOR_SASH_WEIGHTS_PROP = "weights";
+ private static final String KEY_TAB_FOLDER = "hop-explorer-tabFolder";
+
private static ExplorerPerspective instance;
@Getter private GuiToolbarWidgets toolBarWidgets;
@@ -211,12 +218,17 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
@Getter private Tree tree;
private TreeEditor treeEditor;
private CTabFolder tabFolder;
+ private CTabFolder tabFolder2;
+ private SashForm editorSash;
+ private CTabFolder activeTabFolder;
+ private boolean editorSplit;
private Composite tabFolderWrapper;
private Control toolBar;
@Getter private GuiMenuWidgets menuWidgets;
private final List<TabItemHandler> items;
private boolean showingHiddenFiles;
+ private CTabItem splitMenuTargetTab;
private boolean fileExplorerPanelVisible = true;
@Getter private String rootFolder;
@Getter private String rootName;
@@ -333,6 +345,51 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
// Add key listeners
HopGuiKeyHandler.getInstance().addParentObjectToHandle(this);
+
+ // Sync active tab with focused editor content so Save/shortcuts target
the right tab in split
+ // view
+ ExplorerPerspective perspective = this;
+ parent
+ .getDisplay()
+ .addFilter(
+ SWT.FocusIn,
+ e -> {
+ if (!(e.widget instanceof Control)) {
+ return;
+ }
+ Control focusControl = (Control) e.widget;
+ if (hopGui.getActivePerspective() != perspective) {
+ return;
+ }
+ Control c = focusControl;
+ while (c != null) {
+ Object data = c.getData(KEY_TAB_FOLDER);
+ if (data == tabFolder || data == tabFolder2) {
+ CTabFolder folder = (CTabFolder) data;
+ for (CTabItem item : folder.getItems()) {
+ if (item.getControl() == c) {
+ if (activeTabFolder != folder || folder.getSelection()
!= item) {
+ activeTabFolder = folder;
+ folder.setSelection(item);
+ folder.showItem(item);
+ Object handler = item.getData();
+ if (handler instanceof IHopFileTypeHandler) {
+ hopGui.handleFileCapabilities(
+ ((IHopFileTypeHandler) handler).getFileType(),
+ (IHopFileTypeHandler) handler,
+ ((IHopFileTypeHandler) handler).hasChanged(),
+ false,
+ false);
+ }
+ }
+ break;
+ }
+ }
+ break;
+ }
+ c = c.getParent();
+ }
+ });
}
private void loadFileTypes() {
@@ -1147,18 +1204,29 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
tabFolderWrapper.setLayoutData(new FormDataBuilder().fullSize().result());
PropsUi.setLook(tabFolderWrapper);
- tabFolder = new CTabFolder(tabFolderWrapper, SWT.MULTI | SWT.BORDER);
- tabFolder.setLayoutData(new FormDataBuilder().fullSize().result());
- tabFolder.addListener(
+ editorSash = new SashForm(tabFolderWrapper, SWT.HORIZONTAL);
+ editorSash.setLayoutData(new FormDataBuilder().fullSize().result());
+
+ tabFolder = createSingleTabFolder(editorSash, true);
+ tabFolder2 = createSingleTabFolder(editorSash, false);
+
+ activeTabFolder = tabFolder;
+ editorSash.setMaximizedControl(tabFolder);
+ }
+
+ private CTabFolder createSingleTabFolder(Composite parent, boolean primary) {
+ CTabFolder folder = new CTabFolder(parent, SWT.MULTI | SWT.BORDER);
+ folder.setLayoutData(new FormDataBuilder().fullSize().result());
+ folder.addListener(
SWT.Selection,
e -> {
+ activeTabFolder = folder;
updateGui();
- // Notify zoom handler when tab is switched (for web/RAP)
- if (org.apache.hop.ui.util.EnvironmentUtils.getInstance().isWeb()) {
+ if (EnvironmentUtils.getInstance().isWeb()) {
notifyZoomHandlerForActiveTab();
}
});
- tabFolder.addCTabFolder2Listener(
+ folder.addCTabFolder2Listener(
new CTabFolder2Adapter() {
@Override
public void close(CTabFolderEvent event) {
@@ -1166,36 +1234,73 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
closeTab(event, tabItem);
}
});
- PropsUi.setLook(tabFolder, Props.WIDGET_STYLE_TAB);
+ PropsUi.setLook(folder, Props.WIDGET_STYLE_TAB);
+
+ folder.addListener(SWT.FocusIn, e -> activeTabFolder = folder);
+
+ if (primary) {
+ ToolBar tabToolBar = new ToolBar(folder, SWT.FLAT);
+ folder.setTopRight(tabToolBar, SWT.RIGHT);
+ PropsUi.setLook(tabToolBar);
+
+ final ToolItem item = new ToolItem(tabToolBar, SWT.PUSH);
+ item.setImage(GuiResource.getInstance().getImageMaximizePanel());
+ item.addListener(
+ SWT.Selection,
+ e -> {
+ if (sash.getMaximizedControl() == null) {
+ sash.setMaximizedControl(tabFolderWrapper);
+ item.setImage(GuiResource.getInstance().getImageMinimizePanel());
+ } else {
+ sash.setMaximizedControl(null);
+ item.setImage(GuiResource.getInstance().getImageMaximizePanel());
+ }
+ });
+ int height = tabToolBar.computeSize(SWT.DEFAULT, SWT.DEFAULT).y;
+ folder.setTabHeight(Math.max(height, folder.getTabHeight()));
+ }
- // Show/Hide tree
- //
- ToolBar tabToolBar = new ToolBar(tabFolder, SWT.FLAT);
- tabFolder.setTopRight(tabToolBar, SWT.RIGHT);
- PropsUi.setLook(tabToolBar);
+ new TabCloseHandler(this, folder);
+ new TabItemReorder(this, folder);
- final ToolItem item = new ToolItem(tabToolBar, SWT.PUSH);
- item.setImage(GuiResource.getInstance().getImageMaximizePanel());
- item.addListener(
- SWT.Selection,
+ Menu menu = folder.getMenu();
+ new MenuItem(menu, SWT.SEPARATOR);
+ MenuItem miSplitMove = new MenuItem(menu, SWT.NONE);
+ miSplitMove.setText(BaseMessages.getString(PKG,
"ExplorerPerspective.TabMenu.MoveToRight"));
+
+ folder.addListener(
+ SWT.MenuDetect,
+ event -> {
+ Point pt = folder.toControl(folder.getDisplay().getCursorLocation());
+ splitMenuTargetTab = folder.getItem(new Point(pt.x, pt.y));
+ });
+
+ menu.addListener(
+ SWT.Show,
e -> {
- if (sash.getMaximizedControl() == null) {
- sash.setMaximizedControl(tabFolderWrapper);
- item.setImage(GuiResource.getInstance().getImageMinimizePanel());
+ if (!editorSplit || folder == tabFolder) {
+ miSplitMove.setText(
+ BaseMessages.getString(PKG,
"ExplorerPerspective.TabMenu.MoveToRight"));
} else {
- sash.setMaximizedControl(null);
- item.setImage(GuiResource.getInstance().getImageMaximizePanel());
+ miSplitMove.setText(
+ BaseMessages.getString(PKG,
"ExplorerPerspective.TabMenu.MoveToLeft"));
}
+ miSplitMove.setEnabled(splitMenuTargetTab != null);
});
- int height = tabToolBar.computeSize(SWT.DEFAULT, SWT.DEFAULT).y;
- tabFolder.setTabHeight(Math.max(height, tabFolder.getTabHeight()));
- new TabCloseHandler(this);
+ miSplitMove.addListener(
+ SWT.Selection,
+ e -> {
+ if (splitMenuTargetTab != null && !splitMenuTargetTab.isDisposed()) {
+ splitOrMoveTab(splitMenuTargetTab);
+ }
+ });
- // Support reorder tab item (also handles file drops when perspective
implements
- // IFileDropReceiver)
- //
- new TabItemReorder(this, tabFolder);
+ return folder;
+ }
+
+ private CTabFolder getTargetTabFolder() {
+ return activeTabFolder != null ? activeTabFolder : tabFolder;
}
@Override
@@ -1322,63 +1427,148 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
@Override
public void closeTab(CTabFolderEvent event, CTabItem tabItem) {
+ if (tabItem == null || tabItem.isDisposed()) {
+ return;
+ }
IHopFileTypeHandler fileTypeHandler = (IHopFileTypeHandler)
tabItem.getData();
- boolean isRemoved = remove(fileTypeHandler);
+ boolean isRemoved = false;
+ if (fileTypeHandler != null) {
+ isRemoved = remove(fileTypeHandler);
+ }
+ // If remove failed (e.g. null/broken handler) or tab is still there,
close it directly
+ if (!tabItem.isDisposed()) {
+ removeHandlerAndDisposeTab(tabItem);
+ isRemoved = true;
+ }
if (!isRemoved && event != null) {
- // Ignore event if canceled
event.doit = false;
}
}
+ /**
+ * Remove any handler for this tab from the items list and dispose the tab.
Used when the tab is
+ * broken (null handler or handler not in list) so the user can still close
it.
+ */
+ private void removeHandlerAndDisposeTab(CTabItem tabItem) {
+ if (tabItem == null || tabItem.isDisposed()) {
+ return;
+ }
+ TabItemHandler toRemove = findHandlerByTabItem(tabItem);
+ if (toRemove != null) {
+ items.remove(toRemove);
+ IHopFileTypeHandler fileTypeHandler = toRemove.getTypeHandler();
+ if (fileTypeHandler != null && fileTypeHandler.getFilename() != null) {
+ hopGui.fileRefreshDelegate.remove(fileTypeHandler.getFilename());
+ }
+ }
+ tabItem.dispose();
+ if (!hopGui.fileDelegate.isClosing()) {
+ if (editorSplit && (tabFolder.getItemCount() == 0 ||
tabFolder2.getItemCount() == 0)) {
+ unsplitEditor();
+ }
+ if (tabFolder.getItemCount() == 0 && tabFolder2.getItemCount() == 0) {
+ HopGui.getInstance().handleFileCapabilities(new EmptyFileType(),
false, false, false);
+ }
+ updateGui();
+ }
+ }
+
private void removeTabItem(TabItemHandler item) {
+ if (item == null) {
+ return;
+ }
items.remove(item);
-
- // Close the tab
- item.getTabItem().dispose();
-
- // Remove the file in refreshDelegate
- //
+ CTabItem tabItem = item.getTabItem();
+ if (tabItem != null && !tabItem.isDisposed()) {
+ tabItem.dispose();
+ }
IHopFileTypeHandler fileTypeHandler = item.getTypeHandler();
- if (fileTypeHandler.getFilename() != null) {
+ if (fileTypeHandler != null && fileTypeHandler.getFilename() != null) {
hopGui.fileRefreshDelegate.remove(fileTypeHandler.getFilename());
}
- // Avoid refresh in a closing process (when switching project or exit)
if (!hopGui.fileDelegate.isClosing()) {
- // If all tab items are closed
- //
- if (tabFolder.getItemCount() == 0) {
+ if (editorSplit && (tabFolder.getItemCount() == 0 ||
tabFolder2.getItemCount() == 0)) {
+ unsplitEditor();
+ }
+
+ if (tabFolder.getItemCount() == 0 && tabFolder2.getItemCount() == 0) {
HopGui.getInstance().handleFileCapabilities(new EmptyFileType(),
false, false, false);
}
- // Update HopGui menu and toolbar
- //
this.updateGui();
}
}
@Override
public CTabFolder getTabFolder() {
- return tabFolder;
+ return getTargetTabFolder();
+ }
+
+ /**
+ * Returns tab item handlers in pane order: left pane (tabFolder) first by
tab index, then right
+ * pane (tabFolder2) by tab index. Used when persisting open files so
restore order matches split
+ * layout.
+ */
+ public List<TabItemHandler> getTabItemHandlersInPaneOrder() {
+ List<TabItemHandler> ordered = new ArrayList<>();
+ if (tabFolder == null || tabFolder2 == null) {
+ return getItems();
+ }
+ for (CTabItem item : tabFolder.getItems()) {
+ TabItemHandler h = findHandlerByTabItem(item);
+ if (h != null) {
+ ordered.add(h);
+ }
+ }
+ for (CTabItem item : tabFolder2.getItems()) {
+ TabItemHandler h = findHandlerByTabItem(item);
+ if (h != null) {
+ ordered.add(h);
+ }
+ }
+ return ordered;
+ }
+
+ private TabItemHandler findHandlerByTabItem(CTabItem tabItem) {
+ for (TabItemHandler h : items) {
+ if (h.getTabItem() == tabItem) {
+ return h;
+ }
+ }
+ return null;
+ }
+
+ /** Pane index for persistence: 0 = left (tabFolder), 1 = right
(tabFolder2). */
+ public int getPaneIndexForTab(CTabItem tabItem) {
+ if (tabItem == null || tabItem.isDisposed()) {
+ return 0;
+ }
+ return tabItem.getParent() == tabFolder2 ? 1 : 0;
+ }
+
+ /** The right-hand tab folder when split; used by audit restore to target
the correct pane. */
+ public CTabFolder getRightTabFolder() {
+ return tabFolder2;
}
public void addFile(IExplorerFileTypeHandler fileTypeHandler) {
- // Select and show tab item
- //
TabItemHandler handler =
this.findTabItemHandler(fileTypeHandler.getFilename(),
fileTypeHandler.getFileType());
if (handler != null) {
- tabFolder.setSelection(handler.getTabItem());
- tabFolder.showItem(handler.getTabItem());
- tabFolder.setFocus();
+ CTabFolder owningFolder = handler.getTabItem().getParent();
+ owningFolder.setSelection(handler.getTabItem());
+ owningFolder.showItem(handler.getTabItem());
+ owningFolder.setFocus();
+ activeTabFolder = owningFolder;
return;
}
- // Create tab item
- //
- CTabItem tabItem = new CTabItem(tabFolder, SWT.CLOSE);
+ CTabFolder targetFolder = getTargetTabFolder();
+
+ CTabItem tabItem = new CTabItem(targetFolder, SWT.CLOSE);
tabItem.setFont(GuiResource.getInstance().getFontDefault());
String displayName = getTabDisplayName(fileTypeHandler);
tabItem.setText(Const.NVL(displayName, ""));
@@ -1386,8 +1576,6 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
tabItem.setImage(getFileTypeImage(fileTypeHandler.getFileType()));
tabItem.setData(fileTypeHandler);
- // Set the tab bold if the file has changed and vice-versa
- //
fileTypeHandler.addContentChangedListener(
new IContentChangedListener() {
@Override
@@ -1397,13 +1585,11 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
@Override
public void contentSafe(Object parentObject) {
- tabItem.setFont(tabFolder.getFont());
+ tabItem.setFont(tabItem.getParent().getFont());
}
});
- // Create file content area
- //
- Composite composite = new Composite(tabFolder, SWT.NONE);
+ Composite composite = new Composite(targetFolder, SWT.NONE);
FormLayout layoutComposite = new FormLayout();
layoutComposite.marginWidth = PropsUi.getFormMargin();
layoutComposite.marginHeight = PropsUi.getFormMargin();
@@ -1411,21 +1597,17 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
composite.setLayoutData(new FormDataBuilder().fullSize().result());
PropsUi.setLook(composite);
- // This is usually done by the file type
- //
fileTypeHandler.renderFile(composite);
+ composite.setData(KEY_TAB_FOLDER, targetFolder);
tabItem.setControl(composite);
items.add(new TabItemHandler(tabItem, fileTypeHandler));
hopGui.fileRefreshDelegate.register(fileTypeHandler.getFilename(),
fileTypeHandler);
- // Switch to the tab
- //
- tabFolder.setSelection(tabItem);
+ targetFolder.setSelection(tabItem);
- // Add key listeners
HopGuiKeyHandler keyHandler = HopGuiKeyHandler.getInstance();
keyHandler.addParentObjectToHandle(this);
HopGui.getInstance().replaceKeyboardShortcutListeners(this.getShell(),
keyHandler);
@@ -1441,50 +1623,48 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
*/
public IHopFileTypeHandler addPipeline(PipelineMeta pipelineMeta) throws
HopException {
- // Select and show the tab item (If it's a new pipeline, the file name
will be null)
- //
TabItemHandler handler =
this.findTabItemHandler(pipelineMeta.getFilename());
if (handler != null) {
- tabFolder.setSelection(handler.getTabItem());
- tabFolder.showItem(handler.getTabItem());
- tabFolder.setFocus();
+ CTabFolder owningFolder = handler.getTabItem().getParent();
+ owningFolder.setSelection(handler.getTabItem());
+ owningFolder.showItem(handler.getTabItem());
+ owningFolder.setFocus();
+ activeTabFolder = owningFolder;
return handler.getTypeHandler();
}
- // Create the pipeline graph
- //
+ CTabFolder targetFolder = getTargetTabFolder();
+
HopGuiPipelineGraph pipelineGraph =
- new HopGuiPipelineGraph(tabFolder, hopGui, this, pipelineMeta,
pipelineFileType);
+ new HopGuiPipelineGraph(targetFolder, hopGui, this, pipelineMeta,
pipelineFileType);
- // Assign the control to the tab
- //
- CTabItem tabItem = new CTabItem(tabFolder, SWT.CLOSE);
+ CTabItem tabItem = new CTabItem(targetFolder, SWT.CLOSE);
tabItem.setFont(GuiResource.getInstance().getFontDefault());
tabItem.setImage(GuiResource.getInstance().getImagePipeline());
tabItem.setText(Const.NVL(pipelineGraph.getName(), "<>"));
tabItem.setToolTipText(pipelineGraph.getFilename());
+ pipelineGraph.setData(KEY_TAB_FOLDER, targetFolder);
tabItem.setControl(pipelineGraph);
tabItem.setData(pipelineGraph);
+ pipelineGraph.addListener(
+ SWT.Show,
+ e -> {
+ if (!pipelineGraph.isDisposed()) {
+ pipelineGraph.redraw();
+ }
+ });
items.add(new TabItemHandler(tabItem, pipelineGraph));
- // If it's a new pipeline, the file name will be null. So, ignore
- //
if (pipelineMeta.getFilename() != null) {
hopGui.fileRefreshDelegate.register(pipelineMeta.getFilename(),
pipelineGraph);
}
- // Update the internal variables (file specific) in the pipeline graph
variables
- //
pipelineMeta.setInternalHopVariables(pipelineGraph.getVariables());
- // Update the variables using the list of parameters
- //
hopGui.setParametersAsVariablesInUI(pipelineMeta,
pipelineGraph.getVariables());
- // Switch to the tab
- //
- tabFolder.setSelection(tabItem);
+ targetFolder.setSelection(tabItem);
try {
ExtensionPointHandler.callExtensionPoint(
@@ -1500,7 +1680,6 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
e);
}
- // Add key listeners
HopGuiKeyHandler keyHandler = HopGuiKeyHandler.getInstance();
keyHandler.addParentObjectToHandle(this);
HopGui.getInstance().replaceKeyboardShortcutListeners(this.getShell(),
keyHandler);
@@ -1518,46 +1697,48 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
*/
public IHopFileTypeHandler addWorkflow(WorkflowMeta workflowMeta) throws
HopException {
- // Select and show the tab item (If it's a new workflow, the file name
will be null)
- //
TabItemHandler handler =
this.findTabItemHandler(workflowMeta.getFilename());
if (handler != null) {
- tabFolder.setSelection(handler.getTabItem());
- tabFolder.showItem(handler.getTabItem());
- tabFolder.setFocus();
+ CTabFolder owningFolder = handler.getTabItem().getParent();
+ owningFolder.setSelection(handler.getTabItem());
+ owningFolder.showItem(handler.getTabItem());
+ owningFolder.setFocus();
+ activeTabFolder = owningFolder;
return handler.getTypeHandler();
}
+ CTabFolder targetFolder = getTargetTabFolder();
+
HopGuiWorkflowGraph workflowGraph =
- new HopGuiWorkflowGraph(tabFolder, hopGui, this, workflowMeta,
workflowFileType);
+ new HopGuiWorkflowGraph(targetFolder, hopGui, this, workflowMeta,
workflowFileType);
- CTabItem tabItem = new CTabItem(tabFolder, SWT.CLOSE);
+ CTabItem tabItem = new CTabItem(targetFolder, SWT.CLOSE);
tabItem.setFont(GuiResource.getInstance().getFontDefault());
tabItem.setImage(GuiResource.getInstance().getImageWorkflow());
tabItem.setText(Const.NVL(workflowGraph.getName(), "<>"));
tabItem.setToolTipText(workflowGraph.getFilename());
+ workflowGraph.setData(KEY_TAB_FOLDER, targetFolder);
tabItem.setControl(workflowGraph);
tabItem.setData(workflowGraph);
+ workflowGraph.addListener(
+ SWT.Show,
+ e -> {
+ if (!workflowGraph.isDisposed()) {
+ workflowGraph.redraw();
+ }
+ });
items.add(new TabItemHandler(tabItem, workflowGraph));
- // If it's a new workflow, the file name will be null
- //
if (workflowMeta.getFilename() != null) {
hopGui.fileRefreshDelegate.register(workflowMeta.getFilename(),
workflowGraph);
}
- // Update the internal variables (file specific) in the workflow graph
variables
- //
workflowMeta.setInternalHopVariables(workflowGraph.getVariables());
- // Update the variables using the list of parameters
- //
hopGui.setParametersAsVariablesInUI(workflowMeta,
workflowGraph.getVariables());
- // Switch to the tab
- //
- tabFolder.setSelection(tabItem);
+ targetFolder.setSelection(tabItem);
try {
ExtensionPointHandler.callExtensionPoint(
@@ -1573,7 +1754,6 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
e);
}
- // Add key listeners
HopGuiKeyHandler keyHandler = HopGuiKeyHandler.getInstance();
keyHandler.addParentObjectToHandle(this);
HopGui.getInstance().replaceKeyboardShortcutListeners(this.getShell(),
keyHandler);
@@ -1675,30 +1855,43 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
@Override
public IHopFileTypeHandler getActiveFileTypeHandler() {
- if (tabFolder.getSelectionIndex() < 0) {
+ CTabFolder active = getTargetTabFolder();
+ if (active.getSelectionIndex() < 0) {
+ CTabFolder other = (active == tabFolder) ? tabFolder2 : tabFolder;
+ if (other.getSelectionIndex() >= 0) {
+ return (IHopFileTypeHandler) other.getSelection().getData();
+ }
return new EmptyHopFileTypeHandler();
}
- return (IHopFileTypeHandler) tabFolder.getSelection().getData();
+ return (IHopFileTypeHandler) active.getSelection().getData();
}
@Override
public void setActiveFileTypeHandler(IHopFileTypeHandler fileTypeHandler) {
- for (CTabItem item : tabFolder.getItems()) {
- if (item.getData().equals(fileTypeHandler)) {
- tabFolder.setSelection(item);
- tabFolder.showItem(item);
-
- HopGui.getInstance()
- .handleFileCapabilities(
- fileTypeHandler.getFileType(),
- fileTypeHandler,
- fileTypeHandler.hasChanged(),
- false,
- false);
+ for (CTabFolder folder : getTabFolders()) {
+ for (CTabItem item : folder.getItems()) {
+ if (item.getData().equals(fileTypeHandler)) {
+ folder.setSelection(item);
+ folder.showItem(item);
+ activeTabFolder = folder;
+
+ HopGui.getInstance()
+ .handleFileCapabilities(
+ fileTypeHandler.getFileType(),
+ fileTypeHandler,
+ fileTypeHandler.hasChanged(),
+ false,
+ false);
+ return;
+ }
}
}
}
+ private List<CTabFolder> getTabFolders() {
+ return List.of(tabFolder, tabFolder2);
+ }
+
@GuiMenuElement(
root = GUI_PLUGIN_CONTEXT_MENU_PARENT_ID,
parentId = GUI_PLUGIN_CONTEXT_MENU_PARENT_ID,
@@ -1804,7 +1997,8 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
@GuiKeyboardShortcut(alt = true, key = SWT.F1)
@GuiOsxKeyboardShortcut(alt = true, key = SWT.F1)
public void selectInTree() {
- if (tabFolder.getSelectionIndex() >= 0) {
+ CTabFolder active = getTargetTabFolder();
+ if (active.getSelectionIndex() >= 0) {
this.selectInTree(getActiveFileTypeHandler().getFilename());
}
}
@@ -2333,7 +2527,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
Font font =
fileTypeHandler.hasChanged()
? GuiResource.getInstance().getFontBold()
- : tabFolder.getFont();
+ : tabItem.getParent().getFont();
tabItem.setFont(font);
}
}
@@ -2546,12 +2740,15 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
*/
@Override
public boolean remove(IHopFileTypeHandler fileTypeHandler) {
- if (fileTypeHandler.isCloseable()) {
- TabItemHandler item = this.getTabItemHandler(fileTypeHandler);
- removeTabItem(item);
- return true;
+ if (fileTypeHandler == null || !fileTypeHandler.isCloseable()) {
+ return false;
}
- return false;
+ TabItemHandler item = this.getTabItemHandler(fileTypeHandler);
+ if (item == null) {
+ return true; // Allow close to proceed; closeTab will dispose the tab
directly
+ }
+ removeTabItem(item);
+ return true;
}
@Override
@@ -2562,11 +2759,12 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
@Override
public void navigateToPreviousFile() {
if (hasNavigationPreviousFile()) {
- int index = tabFolder.getSelectionIndex() - 1;
+ CTabFolder active = getTargetTabFolder();
+ int index = active.getSelectionIndex() - 1;
if (index < 0) {
- index = tabFolder.getItemCount() - 1;
+ index = active.getItemCount() - 1;
}
- tabFolder.setSelection(index);
+ active.setSelection(index);
updateGui();
}
}
@@ -2574,23 +2772,24 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
@Override
public void navigateToNextFile() {
if (hasNavigationNextFile()) {
- int index = tabFolder.getSelectionIndex() + 1;
- if (index >= tabFolder.getItemCount()) {
+ CTabFolder active = getTargetTabFolder();
+ int index = active.getSelectionIndex() + 1;
+ if (index >= active.getItemCount()) {
index = 0;
}
- tabFolder.setSelection(index);
+ active.setSelection(index);
updateGui();
}
}
@Override
public boolean hasNavigationPreviousFile() {
- return tabFolder.getItemCount() > 1;
+ return getTargetTabFolder().getItemCount() > 1;
}
@Override
public boolean hasNavigationNextFile() {
- return tabFolder.getItemCount() > 1;
+ return getTargetTabFolder().getItemCount() > 1;
}
@Override
@@ -2654,15 +2853,124 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
fileExplorerPanelVisible = !fileExplorerPanelVisible;
if (fileExplorerPanelVisible) {
- // Show the file explorer panel - restore normal layout
sash.setMaximizedControl(null);
sash.setWeights(20, 80);
} else {
- // Hide the file explorer panel - maximize the tab folder area
sash.setMaximizedControl(tabFolderWrapper);
}
}
+ /** Split the editor to show two files side by side. If already split, this
is a no-op. */
+ public void splitEditor() {
+ if (editorSplit) {
+ return;
+ }
+ editorSplit = true;
+ editorSash.setMaximizedControl(null);
+ editorSash.setWeights(50, 50);
+ editorSash.layout(true);
+ }
+
+ /**
+ * Unsplit the editor back to a single pane, moving all tabs from the second
pane to the first.
+ */
+ public void unsplitEditor() {
+ if (!editorSplit) {
+ return;
+ }
+
+ CTabItem[] itemsToMove = tabFolder2.getItems();
+ for (CTabItem srcItem : itemsToMove) {
+ moveTabToFolder(srcItem, tabFolder);
+ }
+
+ editorSplit = false;
+ activeTabFolder = tabFolder;
+ editorSash.setMaximizedControl(tabFolder);
+ editorSash.layout(true);
+ }
+
+ private void moveTabToFolder(CTabItem srcItem, CTabFolder dstFolder) {
+ String text = srcItem.getText();
+ Image image = srcItem.getImage();
+ String tooltip = srcItem.getToolTipText();
+ Font font = srcItem.getFont();
+ Object data = srcItem.getData();
+ boolean showClose = srcItem.getShowClose();
+ Control control = srcItem.getControl();
+
+ // Detach control from the old tab item before disposing it. This prevents
CTabFolder from
+ // hiding the control during srcItem.dispose().
+ srcItem.setControl(null);
+ srcItem.dispose();
+
+ // Reparent the control to the destination folder (mirrors addPipeline
where the graph is
+ // created with targetFolder as its parent).
+ control.setParent(dstFolder);
+ control.setData(KEY_TAB_FOLDER, dstFolder);
+
+ // Create the new tab item and wire it up — same order as
addPipeline/addWorkflow.
+ CTabItem newItem = new CTabItem(dstFolder, SWT.CLOSE);
+ newItem.setText(text);
+ newItem.setImage(image);
+ newItem.setToolTipText(tooltip);
+ newItem.setFont(font);
+ newItem.setControl(control);
+ newItem.setData(data);
+ newItem.setShowClose(showClose);
+
+ for (TabItemHandler handler : items) {
+ if (handler.getTabItem() == srcItem) {
+ handler.setTabItem(newItem);
+ break;
+ }
+ }
+ }
+
+ /** Move a tab to the other split pane. If not currently split, creates the
split first. */
+ private void splitOrMoveTab(CTabItem tab) {
+ CTabFolder sourceFolder = tab.getParent();
+ CTabFolder targetFolder;
+
+ if (!editorSplit) {
+ splitEditor();
+ targetFolder = tabFolder2;
+ } else {
+ targetFolder = (sourceFolder == tabFolder) ? tabFolder2 : tabFolder;
+ }
+
+ moveTabToFolder(tab, targetFolder);
+ targetFolder.setSelection(targetFolder.getItemCount() - 1);
+ activeTabFolder = targetFolder;
+
+ if (editorSplit && sourceFolder.getItemCount() == 0) {
+ unsplitEditor();
+ }
+
+ // Match what addPipeline/addWorkflow do after setSelection: give focus to
the moved tab's
+ // control and refresh the GUI so toolbar/menu state is up to date.
+ CTabItem sel = targetFolder.getSelection();
+ if (sel != null && sel.getControl() != null &&
!sel.getControl().isDisposed()) {
+ sel.getControl().setFocus();
+ }
+ updateGui();
+ }
+
+ @Override
+ public void onTabMovedBetweenFolders(CTabFolder sourceFolder, CTabFolder
targetFolder) {
+ activeTabFolder = targetFolder;
+ if (editorSplit && sourceFolder.getItemCount() == 0) {
+ unsplitEditor();
+ }
+ }
+
+ @Override
+ public void setDropTargetFolder(CTabFolder folder) {
+ if (folder == tabFolder || folder == tabFolder2) {
+ activeTabFolder = folder;
+ }
+ }
+
/**
* Check if the file explorer panel is currently visible.
*
@@ -2680,6 +2988,19 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
new AuditState(
STATE_PANEL_VISIBLE_KEY,
Map.of(STATE_PANEL_VISIBLE_PROP,
Boolean.valueOf(fileExplorerPanelVisible))));
+ stateMap.add(
+ new AuditState(
+ STATE_EDITOR_SPLIT_KEY,
+ Map.of(STATE_EDITOR_SPLIT_PROP, Boolean.valueOf(editorSplit))));
+ if (editorSash != null && !editorSash.isDisposed()) {
+ int[] weights = editorSash.getWeights();
+ if (weights != null && weights.length >= 2) {
+ stateMap.add(
+ new AuditState(
+ STATE_EDITOR_SASH_WEIGHTS_KEY,
+ Map.of(STATE_EDITOR_SASH_WEIGHTS_PROP, weights[0] + "," +
weights[1])));
+ }
+ }
AuditManager.getActive()
.saveAuditStateMap(HopNamespace.getNamespace(), EXPLORER_AUDIT_TYPE,
stateMap);
} catch (Exception e) {
@@ -2688,7 +3009,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
}
/**
- * Restore file explorer panel visibility from saved audit state.
+ * Load saved file explorer panel visibility from saved audit state.
*
* @return the saved visibility, or null if no saved state exists
*/
@@ -2713,6 +3034,54 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
return null;
}
+ /**
+ * Load and apply saved editor split state (split on/off and sash weights).
Called on startup
+ * before opening files so that tabs open in the correct pane, and from
applyRestoredState when
+ * project changes.
+ */
+ public void applyRestoredEditorSplitState() {
+ if (editorSash == null || editorSash.isDisposed()) {
+ return;
+ }
+ try {
+ AuditStateMap stateMap =
+ AuditManager.getActive()
+ .loadAuditStateMap(HopNamespace.getNamespace(),
EXPLORER_AUDIT_TYPE);
+ AuditState splitState = stateMap.get(STATE_EDITOR_SPLIT_KEY);
+ if (splitState != null) {
+ Object split = splitState.getStateMap().get(STATE_EDITOR_SPLIT_PROP);
+ boolean wasSplit = split instanceof Boolean && (Boolean) split;
+ if (wasSplit) {
+ editorSplit = true;
+ editorSash.setMaximizedControl(null);
+ AuditState weightsState =
stateMap.get(STATE_EDITOR_SASH_WEIGHTS_KEY);
+ if (weightsState != null) {
+ Object weightsObj =
weightsState.getStateMap().get(STATE_EDITOR_SASH_WEIGHTS_PROP);
+ if (weightsObj != null) {
+ String[] parts = weightsObj.toString().split(",");
+ if (parts.length >= 2) {
+ try {
+ int w0 = Integer.parseInt(parts[0].trim());
+ int w1 = Integer.parseInt(parts[1].trim());
+ editorSash.setWeights(w0, w1);
+ } catch (NumberFormatException ignored) {
+ editorSash.setWeights(50, 50);
+ }
+ }
+ } else {
+ editorSash.setWeights(50, 50);
+ }
+ } else {
+ editorSash.setWeights(50, 50);
+ }
+ editorSash.layout(true);
+ }
+ }
+ } catch (Exception e) {
+ hopGui.getLog().logError("Error restoring explorer editor split state",
e);
+ }
+ }
+
/**
* Load saved file explorer panel visibility for the current namespace and
apply it. Call this
* after startup (e.g. from HopGui open() async block) and when the project
changes
@@ -2732,6 +3101,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
sash.setMaximizedControl(tabFolderWrapper);
}
}
+ applyRestoredEditorSplitState();
}
public static class DetermineRootFolderExtension {
diff --git
a/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/explorer/messages/messages_en_US.properties
b/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/explorer/messages/messages_en_US.properties
index c94ecf6a0a..51f42c6cf6 100644
---
a/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/explorer/messages/messages_en_US.properties
+++
b/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/explorer/messages/messages_en_US.properties
@@ -58,3 +58,5 @@ ExplorerPerspective.ToolbarElement.ExpandAll.Tooltip=Expand
all folders
ExplorerPerspective.ToolbarElement.CollapseAll.Tooltip=Collapse all folders
ExplorerPerspective.ToolbarElement.SelectOpenedFile.Tooltip=Select opened file
ExplorerPerspective.ToolbarElement.OpenAsText.Label=Open as text
+ExplorerPerspective.TabMenu.MoveToRight=Move to Right
+ExplorerPerspective.TabMenu.MoveToLeft=Move to Left