From bb3f1b0bda2beed670535bb2a3c4ca245662e2c0 Mon Sep 17 00:00:00 2001 From: crschnick Date: Sun, 8 Feb 2026 07:58:18 +0000 Subject: [PATCH] Rework terminal dock --- .../file/BrowserTerminalDockTabModel.java | 14 ++- .../io/xpipe/app/core/AppWindowsLock.java | 9 +- .../app/platform/NativeWinWindowControl.java | 23 +++-- .../terminal/ControllableTerminalSession.java | 10 +- .../app/terminal/TerminalDockBrowserComp.java | 18 +--- .../app/terminal/TerminalDockHubComp.java | 26 +---- .../app/terminal/TerminalDockHubManager.java | 26 +++-- .../xpipe/app/terminal/TerminalDockView.java | 98 ++++++++----------- .../app/terminal/WindowsTerminalSession.java | 36 +++---- .../main/java/io/xpipe/app/util/User32Ex.java | 15 +++ dist/changelog/21.0.md | 5 +- 11 files changed, 128 insertions(+), 152 deletions(-) create mode 100644 app/src/main/java/io/xpipe/app/util/User32Ex.java diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserTerminalDockTabModel.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserTerminalDockTabModel.java index 58a6d7196..5ee01ed93 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserTerminalDockTabModel.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserTerminalDockTabModel.java @@ -129,14 +129,22 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab { AppLayoutModel.get().getSelected()); viewActive.subscribe(aBoolean -> { Platform.runLater(() -> { - dockModel.toggleView(aBoolean); + if (aBoolean) { + dockModel.activateView(); + } else { + dockModel.deactivateView(); + } }); }); AppDialog.getModalOverlays().addListener((ListChangeListener) c -> { if (c.getList().size() > 0) { - dockModel.toggleView(false); + dockModel.deactivateView(); } else { - dockModel.toggleView(viewActive.get()); + if (viewActive.get()) { + dockModel.activateView(); + } else { + dockModel.deactivateView(); + } } }); } diff --git a/app/src/main/java/io/xpipe/app/core/AppWindowsLock.java b/app/src/main/java/io/xpipe/app/core/AppWindowsLock.java index 8fc324bfd..322e6f139 100644 --- a/app/src/main/java/io/xpipe/app/core/AppWindowsLock.java +++ b/app/src/main/java/io/xpipe/app/core/AppWindowsLock.java @@ -5,11 +5,10 @@ import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.util.ThreadHelper; -import com.sun.jna.Native; import com.sun.jna.Pointer; import com.sun.jna.platform.win32.*; import com.sun.jna.win32.StdCallLibrary; -import com.sun.jna.win32.W32APIOptions; +import io.xpipe.app.util.User32Ex; import lombok.RequiredArgsConstructor; import lombok.Setter; @@ -74,10 +73,4 @@ public class AppWindowsLock { } } - public interface User32Ex extends W32APIOptions { - - User32Ex INSTANCE = Native.load("user32", User32Ex.class, DEFAULT_OPTIONS); - - int SetWindowLongPtr(WinDef.HWND hWnd, int nIndex, WinMsgProc callback); - } } diff --git a/app/src/main/java/io/xpipe/app/platform/NativeWinWindowControl.java b/app/src/main/java/io/xpipe/app/platform/NativeWinWindowControl.java index 227857c60..7ca75c07b 100644 --- a/app/src/main/java/io/xpipe/app/platform/NativeWinWindowControl.java +++ b/app/src/main/java/io/xpipe/app/platform/NativeWinWindowControl.java @@ -2,6 +2,7 @@ package io.xpipe.app.platform; import io.xpipe.app.util.Rect; +import io.xpipe.app.util.User32Ex; import javafx.stage.Window; import com.sun.jna.Library; @@ -22,7 +23,6 @@ import java.util.List; @EqualsAndHashCode public class NativeWinWindowControl { - private static final int WS_EX_NOACTIVATE = 0x08000000; private static final int WS_EX_APPWINDOW = 0x00040000; public static NativeWinWindowControl MAIN_WINDOW; @@ -78,16 +78,20 @@ public class NativeWinWindowControl { User32.INSTANCE.SetWindowLong(windowHandle, User32.GWL_STYLE, mod); } - public void disableActivate() { + public void takeOwnership(WinDef.HWND owner) { var style = User32.INSTANCE.GetWindowLong(windowHandle, User32.GWL_EXSTYLE); - var mod = style | WS_EX_NOACTIVATE | WS_EX_APPWINDOW; + var mod = style | WS_EX_APPWINDOW; User32.INSTANCE.SetWindowLong(windowHandle, User32.GWL_EXSTYLE, mod); + + User32Ex.INSTANCE.SetWindowLongPtr(getWindowHandle(), User32.GWL_HWNDPARENT, owner); } - public void enableActivate() { + public void releaseOwnership() { var style = User32.INSTANCE.GetWindowLong(windowHandle, User32.GWL_EXSTYLE); - var mod = style & ~(WS_EX_NOACTIVATE | WS_EX_APPWINDOW); + var mod = style & ~(WS_EX_APPWINDOW); User32.INSTANCE.SetWindowLong(windowHandle, User32.GWL_EXSTYLE, mod); + + User32Ex.INSTANCE.SetWindowLongPtr(getWindowHandle(), User32.GWL_HWNDPARENT, (WinDef.HWND) null); } public boolean isIconified() { @@ -98,12 +102,7 @@ public class NativeWinWindowControl { return User32.INSTANCE.IsWindowVisible(windowHandle); } - public void alwaysInFront() { - orderRelative(new WinDef.HWND(new Pointer(0xFFFFFFFFFFFFFFFFL))); - } - - public void defaultOrder() { - orderRelative(new WinDef.HWND(new Pointer(-2))); + public void moveToFront() { orderRelative(new WinDef.HWND(new Pointer(0))); } @@ -126,7 +125,7 @@ public class NativeWinWindowControl { public void move(Rect bounds) { User32.INSTANCE.SetWindowPos( - windowHandle, null, bounds.getX(), bounds.getY(), bounds.getW(), bounds.getH(), User32.SWP_NOACTIVATE); + windowHandle, null, bounds.getX(), bounds.getY(), bounds.getW(), bounds.getH(), User32.SWP_NOACTIVATE | User32.SWP_NOZORDER); } public Rect getBounds() { diff --git a/app/src/main/java/io/xpipe/app/terminal/ControllableTerminalSession.java b/app/src/main/java/io/xpipe/app/terminal/ControllableTerminalSession.java index 3a05f6720..d8670bbeb 100644 --- a/app/src/main/java/io/xpipe/app/terminal/ControllableTerminalSession.java +++ b/app/src/main/java/io/xpipe/app/terminal/ControllableTerminalSession.java @@ -14,19 +14,19 @@ public abstract class ControllableTerminalSession extends TerminalView.TerminalS super(terminalProcess); } + public abstract void own(); + + public abstract void disown(); + public abstract void removeBorders(); public abstract void show(); public abstract void minimize(); - public abstract void alwaysInFront(); - - public abstract void back(); - public abstract void frontOfMainWindow(); - public abstract void moveToFront(); + public abstract void backOfMainWindow(); public abstract void focus(); diff --git a/app/src/main/java/io/xpipe/app/terminal/TerminalDockBrowserComp.java b/app/src/main/java/io/xpipe/app/terminal/TerminalDockBrowserComp.java index a2ab8546d..dce7f986a 100644 --- a/app/src/main/java/io/xpipe/app/terminal/TerminalDockBrowserComp.java +++ b/app/src/main/java/io/xpipe/app/terminal/TerminalDockBrowserComp.java @@ -100,24 +100,10 @@ public class TerminalDockBrowserComp extends SimpleRegionBuilder { if (newValue) { model.onWindowMinimize(); } else { - model.onWindowActivate(); + model.onWindowShow(); } } }; - var focus = new ChangeListener() { - @Override - public void changed(ObservableValue observable, Boolean oldValue, Boolean newValue) { - GlobalTimer.delay( - () -> { - if (newValue) { - model.onFocusGain(); - } else { - model.onFocusLost(); - } - }, - Duration.ofMillis(100)); - } - }; var show = new EventHandler() { @Override public void handle(WindowEvent event) { @@ -139,7 +125,6 @@ public class TerminalDockBrowserComp extends SimpleRegionBuilder { s.widthProperty().removeListener(update); s.heightProperty().removeListener(update); s.iconifiedProperty().removeListener(iconified); - s.focusedProperty().removeListener(focus); s.removeEventFilter(WindowEvent.WINDOW_SHOWN, show); s.removeEventFilter(WindowEvent.WINDOW_HIDING, hide); if (parent.get() != null) { @@ -152,7 +137,6 @@ public class TerminalDockBrowserComp extends SimpleRegionBuilder { s.widthProperty().addListener(update); s.heightProperty().addListener(update); s.iconifiedProperty().addListener(iconified); - s.focusedProperty().addListener(focus); s.addEventFilter(WindowEvent.WINDOW_SHOWN, show); s.addEventFilter(WindowEvent.WINDOW_HIDING, hide); // As in practice this node is wrapped in another stack pane diff --git a/app/src/main/java/io/xpipe/app/terminal/TerminalDockHubComp.java b/app/src/main/java/io/xpipe/app/terminal/TerminalDockHubComp.java index cb1dc3c81..1ccaa6451 100644 --- a/app/src/main/java/io/xpipe/app/terminal/TerminalDockHubComp.java +++ b/app/src/main/java/io/xpipe/app/terminal/TerminalDockHubComp.java @@ -9,13 +9,10 @@ import javafx.beans.value.ObservableValue; import javafx.event.EventHandler; import javafx.geometry.Bounds; import javafx.scene.Parent; -import javafx.scene.control.Button; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import javafx.stage.WindowEvent; -import org.kordamp.ikonli.javafx.FontIcon; - import java.util.concurrent.atomic.AtomicReference; public class TerminalDockHubComp extends SimpleRegionBuilder { @@ -62,26 +59,7 @@ public class TerminalDockHubComp extends SimpleRegionBuilder { model.onWindowMinimize(); } else { Platform.runLater(() -> { - model.onWindowActivate(); - }); - } - } - }; - var focus = new ChangeListener() { - @Override - public void changed(ObservableValue observable, Boolean oldValue, Boolean newValue) { - if (newValue) { - var selected = s.getScene().getRoot().lookup(".icon-button-comp:hover"); - if (selected instanceof Button b - && b.getGraphic() instanceof FontIcon fi - && !fi.getIconLiteral().equals("mdi2c-connection")) { - return; - } - - model.onFocusGain(); - } else { - Platform.runLater(() -> { - model.onFocusLost(); + model.onWindowShow(); }); } } @@ -107,7 +85,6 @@ public class TerminalDockHubComp extends SimpleRegionBuilder { s.widthProperty().removeListener(update); s.heightProperty().removeListener(update); s.iconifiedProperty().removeListener(iconified); - s.focusedProperty().removeListener(focus); s.removeEventFilter(WindowEvent.WINDOW_SHOWN, show); s.removeEventFilter(WindowEvent.WINDOW_HIDING, hide); if (parent.get() != null) { @@ -120,7 +97,6 @@ public class TerminalDockHubComp extends SimpleRegionBuilder { s.widthProperty().addListener(update); s.heightProperty().addListener(update); s.iconifiedProperty().addListener(iconified); - s.focusedProperty().addListener(focus); s.addEventFilter(WindowEvent.WINDOW_SHOWN, show); s.addEventFilter(WindowEvent.WINDOW_HIDING, hide); // As in practice this node is wrapped in another stack pane diff --git a/app/src/main/java/io/xpipe/app/terminal/TerminalDockHubManager.java b/app/src/main/java/io/xpipe/app/terminal/TerminalDockHubManager.java index ce991bc91..4ea8e0d5f 100644 --- a/app/src/main/java/io/xpipe/app/terminal/TerminalDockHubManager.java +++ b/app/src/main/java/io/xpipe/app/terminal/TerminalDockHubManager.java @@ -4,6 +4,7 @@ import io.xpipe.app.comp.base.ModalOverlay; import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.core.window.AppDialog; +import io.xpipe.app.core.window.AppMainWindow; import io.xpipe.app.platform.LabelGraphic; import io.xpipe.app.platform.NativeWinWindowControl; import io.xpipe.app.platform.PlatformThread; @@ -18,6 +19,7 @@ import javafx.beans.property.SimpleBooleanProperty; import javafx.collections.ListChangeListener; import lombok.Getter; +import org.kordamp.ikonli.javafx.FontIcon; import java.time.Duration; import java.util.HashSet; @@ -72,7 +74,7 @@ public class TerminalDockHubManager { TerminalView.get().addListener(INSTANCE.createListener()); - GlobalTimer.scheduleUntil(Duration.ofSeconds(1), false, () -> { + GlobalTimer.scheduleUntil(Duration.ofMillis(500), false, () -> { INSTANCE.refreshDockStatus(); return false; }); @@ -97,7 +99,12 @@ public class TerminalDockHubManager { : new Rect(rect.getX(), rect.getY() - topAdjust, rect.getW(), rect.getH() + topAdjust); }); private final AppLayoutModel.QueueEntry queueEntry = new AppLayoutModel.QueueEntry( - AppI18n.observable("toggleTerminalDock"), new LabelGraphic.IconGraphic("mdi2c-console"), () -> { + AppI18n.observable("toggleTerminalDock"), new LabelGraphic.NodeGraphic(() -> { + var fi = new FontIcon("mdi2c-console"); + fi.getStyleClass().add("graphic"); + fi.getStyleClass().add("terminal-dock-button"); + return fi; + }), () -> { refreshDockStatus(); if (!enabled.get()) { @@ -177,7 +184,9 @@ public class TerminalDockHubManager { controllable.get().removeBorders(); } } - dockModel.trackTerminal(controllable.get(), !detached.get()); + + var dock = !detached.get(); + dockModel.trackTerminal(controllable.get(), dock); dockModel.closeOtherTerminals(session.getRequest()); enableDock(); } @@ -218,7 +227,7 @@ public class TerminalDockHubManager { } minimized.set(dockModel.isMinimized()); - detached.set(dockModel.isCustomBounds() || dockModel.isMinimized()); + detached.set(!dockModel.isMinimized() && (dockModel.isCustomBounds() || AppMainWindow.get().getStage().isIconified())); } public void openTerminal(UUID request) { @@ -256,7 +265,7 @@ public class TerminalDockHubManager { return; } - dockModel.toggleView(true); + dockModel.activateView(); enabled.set(true); showing.set(true); @@ -271,7 +280,7 @@ public class TerminalDockHubManager { return; } - dockModel.toggleView(false); + dockModel.deactivateView(); enabled.set(false); showing.set(false); @@ -286,7 +295,7 @@ public class TerminalDockHubManager { return; } - dockModel.toggleView(true); + dockModel.activateView(); showing.set(true); AppLayoutModel.get().selectConnections(); }); @@ -298,7 +307,7 @@ public class TerminalDockHubManager { return; } - dockModel.toggleView(false); + dockModel.deactivateView(); showing.set(false); }); } @@ -306,5 +315,6 @@ public class TerminalDockHubManager { public void attach() { dockModel.attach(); detached.set(false); + minimized.set(false); } } diff --git a/app/src/main/java/io/xpipe/app/terminal/TerminalDockView.java b/app/src/main/java/io/xpipe/app/terminal/TerminalDockView.java index 6ba400dfa..29dbad4b0 100644 --- a/app/src/main/java/io/xpipe/app/terminal/TerminalDockView.java +++ b/app/src/main/java/io/xpipe/app/terminal/TerminalDockView.java @@ -46,34 +46,37 @@ public class TerminalDockView { } public synchronized void updateCustomBounds() { - terminalInstances.forEach(terminal -> terminal.updateBoundsState()); + terminalInstances.forEach(terminal -> { + terminal.updateBoundsState(); + if (terminal.isCustomBounds()) { + terminal.disown(); + } + }); } public synchronized void trackTerminal(ControllableTerminalSession terminal, boolean dock) { - if (!terminalInstances.add(terminal)) { - return; + if (viewActive && dock && viewBounds != null) { + // The window might be minimized + // We always want to show the terminal though + terminal.show(); + + terminal.own(); + + terminal.updatePosition(windowBoundsFunction.apply(viewBounds)); + updateCustomBounds(); } - // The main window always loses focus when the terminal is opened, - // so only put it in front - // If we refocus the main window, it will get put always in front then - terminal.frontOfMainWindow(); - if (dock && viewBounds != null) { - terminal.updatePosition(windowBoundsFunction.apply(viewBounds)); - + var wasAdded = terminalInstances.add(terminal); + if (wasAdded && viewActive && dock && viewBounds != null) { // Ugly fix for Windows Terminal instances using size constraints on first resize // This will cause the dock to interpret is as detached if we don't fix it again if (AppPrefs.get().terminalType().getValue() instanceof WindowsTerminalType) { GlobalTimer.delay( () -> { terminal.updatePosition(windowBoundsFunction.apply(viewBounds)); + updateCustomBounds(); }, Duration.ofMillis(100)); - GlobalTimer.delay( - () -> { - terminal.updatePosition(windowBoundsFunction.apply(viewBounds)); - }, - Duration.ofMillis(1000)); } } } @@ -100,30 +103,13 @@ public class TerminalDockView { terminalInstances.remove(terminal); } - public synchronized void toggleView(boolean active) { - TrackEvent.withTrace("Terminal view toggled").tag("active", active).handle(); - if (viewActive == active) { + public synchronized void activateView() { + TrackEvent.withTrace("Terminal view activated").handle(); + if (viewActive) { return; } - this.viewActive = active; - if (active) { - terminalInstances.forEach(terminalInstance -> { - terminalInstance.frontOfMainWindow(); - terminalInstance.focus(); - }); - updatePositions(); - } else { - terminalInstances.forEach(terminalInstance -> terminalInstance.back()); - } - } - - public synchronized void onFocusGain() { - if (!viewActive) { - return; - } - - TrackEvent.withTrace("Terminal view focus gained").handle(); + this.viewActive = true; terminalInstances.forEach(terminalInstance -> { if (!terminalInstance.isActive()) { return; @@ -134,34 +120,33 @@ public class TerminalDockView { return; } - terminalInstance.show(); - terminalInstance.alwaysInFront(); + terminalInstance.own(); + terminalInstance.focus(); }); + updatePositions(); } - public synchronized void onFocusLost() { + public synchronized void deactivateView() { + TrackEvent.withTrace("Terminal view deactivated").handle(); if (!viewActive) { return; } - TrackEvent.withTrace("Terminal view focus lost").handle(); + this.viewActive = false; terminalInstances.forEach(terminalInstance -> { - if (!terminalInstance.isActive()) { - return; - } - - terminalInstance.updateBoundsState(); - if (terminalInstance.isCustomBounds()) { - return; - } - - terminalInstance.frontOfMainWindow(); + terminalInstance.disown(); + terminalInstance.backOfMainWindow(); }); + updatePositions(); } - public synchronized void onWindowActivate() { - TrackEvent.withTrace("Terminal view window activated").handle(); + public synchronized void onWindowShow() { + TrackEvent.withTrace("Terminal view window shown").handle(); terminalInstances.forEach(terminalInstance -> { + if (terminalInstance.isActive()) { + return; + } + terminalInstance.updateBoundsState(); if (terminalInstance.isCustomBounds()) { return; @@ -169,10 +154,11 @@ public class TerminalDockView { terminalInstance.show(); if (viewActive) { - terminalInstance.frontOfMainWindow(); + terminalInstance.own(); terminalInstance.focus(); } else { - terminalInstance.back(); + terminalInstance.disown(); + terminalInstance.backOfMainWindow(); } }); } @@ -233,9 +219,9 @@ public class TerminalDockView { terminalInstances.forEach(terminalInstance -> { terminalInstance.show(); - terminalInstance.frontOfMainWindow(); - terminalInstance.focus(); terminalInstance.updatePosition(windowBoundsFunction.apply(viewBounds)); + terminalInstance.own(); + terminalInstance.focus(); }); } } diff --git a/app/src/main/java/io/xpipe/app/terminal/WindowsTerminalSession.java b/app/src/main/java/io/xpipe/app/terminal/WindowsTerminalSession.java index f4e77bba6..274278383 100644 --- a/app/src/main/java/io/xpipe/app/terminal/WindowsTerminalSession.java +++ b/app/src/main/java/io/xpipe/app/terminal/WindowsTerminalSession.java @@ -1,8 +1,10 @@ package io.xpipe.app.terminal; +import com.sun.jna.platform.win32.User32; import io.xpipe.app.platform.NativeWinWindowControl; import io.xpipe.app.util.Rect; +import io.xpipe.app.util.User32Ex; import lombok.AccessLevel; import lombok.Getter; import lombok.experimental.FieldDefaults; @@ -41,6 +43,16 @@ public final class WindowsTerminalSession extends ControllableTerminalSession { return super.isRunning() && control.isVisible(); } + @Override + public void own() { + control.takeOwnership(NativeWinWindowControl.MAIN_WINDOW.getWindowHandle()); + } + + @Override + public void disown() { + control.releaseOwnership(); + } + @Override public void removeBorders() { control.removeBorders(); @@ -57,27 +69,13 @@ public final class WindowsTerminalSession extends ControllableTerminalSession { } @Override - public void alwaysInFront() { - this.control.alwaysInFront(); - } - - @Override - public void back() { - control.defaultOrder(); - NativeWinWindowControl.MAIN_WINDOW.alwaysInFront(); - NativeWinWindowControl.MAIN_WINDOW.defaultOrder(); + public void backOfMainWindow() { + getControl().orderRelative(NativeWinWindowControl.MAIN_WINDOW.getWindowHandle()); } @Override public void frontOfMainWindow() { - this.control.alwaysInFront(); - this.control.defaultOrder(); - NativeWinWindowControl.MAIN_WINDOW.orderRelative(control.getWindowHandle()); - } - - @Override - public void moveToFront() { - this.control.defaultOrder(); + this.control.moveToFront(); } @Override @@ -117,6 +115,10 @@ public final class WindowsTerminalSession extends ControllableTerminalSession { return; } + if (lastBounds != null && (lastBounds.getX() == -32000 || lastBounds.getY() == -32000)) { + return; + } + if (lastBounds != null && !lastBounds.equals(bounds)) { customBounds = true; } diff --git a/app/src/main/java/io/xpipe/app/util/User32Ex.java b/app/src/main/java/io/xpipe/app/util/User32Ex.java new file mode 100644 index 000000000..8091879c8 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/User32Ex.java @@ -0,0 +1,15 @@ +package io.xpipe.app.util; + +import com.sun.jna.Native; +import com.sun.jna.platform.win32.WinDef; +import com.sun.jna.win32.W32APIOptions; +import io.xpipe.app.core.AppWindowsLock; + +public interface User32Ex extends W32APIOptions { + + User32Ex INSTANCE = Native.load("user32", User32Ex.class, DEFAULT_OPTIONS); + + int SetWindowLongPtr(WinDef.HWND hWnd, int nIndex, AppWindowsLock.WinMsgProc callback); + + int SetWindowLongPtr(WinDef.HWND hWnd, int nIndex, WinDef.HWND w); +} diff --git a/dist/changelog/21.0.md b/dist/changelog/21.0.md index feac1cb8d..7b29eabb4 100644 --- a/dist/changelog/21.0.md +++ b/dist/changelog/21.0.md @@ -10,7 +10,9 @@ Here is a Windows Terminal instance with 4 split tabs that were launched through ![Split Dock](https://xpipe.io/assets/images/BlogPage/dock-split.png) -A docked terminal is embedded into the XPipe window but can also be detached from it. If you want to disable the terminal docking completely, you can do so in the settings menu. +A docked terminal is embedded into the XPipe window but can also be detached from it. If you want to disable the terminal docking completely, you can do so in the settings menu: + +![Dock setting](https://xpipe.io/assets/images/BlogPage/dock-setting.png) ## Cisco switch integration @@ -92,6 +94,7 @@ The scripting system has been completely reworked with the goal of becoming simp - Fix predefined categories being able to be moved and causing breakages - Fix terminal session titles not applying for Konsole - Fix rbash shell detection not working +- Fix SFTP failing with files with single quotes in their name - Fix WinSCP open action requiring an existing ppk key and only working with external key files, not in-place keys - Fix batch mode selection not working for incomplete connections, like newly added VMs without credentials - Fix batch action confirmation setting requiring a double confirmation for each individual connection