From 30568dd762ffeee4e2dc9cf2b832e2351d05fe8a Mon Sep 17 00:00:00 2001 From: crschnick Date: Sun, 18 Jan 2026 02:35:42 +0000 Subject: [PATCH] Various fixes --- .../file/BrowserTerminalDockTabModel.java | 3 +- .../xpipe/app/hub/comp/StoreLayoutComp.java | 12 +- .../app/platform/NativeWinWindowControl.java | 2 - .../app/terminal/AlacrittyTerminalType.java | 5 + .../terminal/ControllableTerminalSession.java | 2 - .../xpipe/app/terminal/PwshTerminalType.java | 5 + .../app/terminal/TerminalDockBrowserComp.java | 14 +- .../app/terminal/TerminalDockHubComp.java | 13 +- .../app/terminal/TerminalDockHubManager.java | 139 +++++++++++++----- .../xpipe/app/terminal/TerminalDockView.java | 21 ++- .../xpipe/app/terminal/TerminalLauncher.java | 9 +- .../app/terminal/WindowsTerminalSession.java | 5 - 12 files changed, 159 insertions(+), 71 deletions(-) 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 1d2667893..a15132779 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 @@ -27,12 +27,13 @@ import javafx.collections.ObservableList; import java.util.Optional; import java.util.UUID; +import java.util.function.UnaryOperator; public final class BrowserTerminalDockTabModel extends BrowserSessionTab { private final BrowserSessionTab origin; private final ObservableList terminalRequests; - private final TerminalDockView dockModel = new TerminalDockView(); + private final TerminalDockView dockModel = new TerminalDockView(UnaryOperator.identity()); private final BooleanProperty opened = new SimpleBooleanProperty(); private TerminalView.Listener listener; private ObservableBooleanValue viewActive; diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreLayoutComp.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreLayoutComp.java index b4559e614..f56796c5f 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreLayoutComp.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreLayoutComp.java @@ -9,6 +9,7 @@ import io.xpipe.app.core.window.AppMainWindow; import io.xpipe.app.platform.InputHelper; import io.xpipe.app.terminal.TerminalDockHubComp; import io.xpipe.app.terminal.TerminalDockHubManager; +import io.xpipe.app.terminal.TerminalDockMode; import io.xpipe.app.util.ObservableSubscriber; import javafx.scene.input.KeyCode; @@ -51,10 +52,15 @@ public class StoreLayoutComp extends SimpleComp { }); }); - var model = TerminalDockHubManager.get(); - var dock = new TerminalDockHubComp(model.getDockModel()); - var stack = new StackPane(comp.createRegion(), dock.createRegion()); + var stack = new StackPane(comp.createRegion()); stack.getStyleClass().add("store-layout"); + + if (TerminalDockHubManager.isPossiblySupported()) { + var model = TerminalDockHubManager.get(); + var dock = new TerminalDockHubComp(model.getDockModel()); + stack.getChildren().add(dock.createRegion()); + } + return stack; } } 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 8e41581a4..460bacc6e 100644 --- a/app/src/main/java/io/xpipe/app/platform/NativeWinWindowControl.java +++ b/app/src/main/java/io/xpipe/app/platform/NativeWinWindowControl.java @@ -76,8 +76,6 @@ public class NativeWinWindowControl { User32.INSTANCE.SetWindowLong(windowHandle, User32.GWL_STYLE, mod); } - public void removeShadow() {} - public boolean isIconified() { return (User32.INSTANCE.GetWindowLong(windowHandle, User32.GWL_STYLE) & User32.WS_MINIMIZE) != 0; } diff --git a/app/src/main/java/io/xpipe/app/terminal/AlacrittyTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/AlacrittyTerminalType.java index 0639d2aa6..5707acddd 100644 --- a/app/src/main/java/io/xpipe/app/terminal/AlacrittyTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/AlacrittyTerminalType.java @@ -35,6 +35,11 @@ public interface AlacrittyTerminalType extends ExternalTerminalType, TrackableTe class Windows implements ExternalApplicationType.PathApplication, ExternalTerminalType, AlacrittyTerminalType { + @Override + public TerminalDockMode getDockMode() { + return TerminalDockMode.WITH_BORDER; + } + @Override public void launch(TerminalLaunchConfiguration configuration) throws Exception { // Alacritty is bugged and will not accept arguments with spaces even if they are correctly passed/escaped 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 1fd55d556..b5afada2a 100644 --- a/app/src/main/java/io/xpipe/app/terminal/ControllableTerminalSession.java +++ b/app/src/main/java/io/xpipe/app/terminal/ControllableTerminalSession.java @@ -16,8 +16,6 @@ public abstract class ControllableTerminalSession extends TerminalView.TerminalS public abstract void removeBorders(); - public abstract void removeShadow(); - public abstract void show(); public abstract void minimize(); diff --git a/app/src/main/java/io/xpipe/app/terminal/PwshTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/PwshTerminalType.java index 7fb91c7fb..70197e1c1 100644 --- a/app/src/main/java/io/xpipe/app/terminal/PwshTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/PwshTerminalType.java @@ -8,6 +8,11 @@ import java.util.Base64; public class PwshTerminalType implements ExternalApplicationType.PathApplication, TrackableTerminalType { + @Override + public TerminalDockMode getDockMode() { + return TerminalDockMode.WITH_BORDER; + } + @Override public TerminalOpenFormat getOpenFormat() { return TerminalOpenFormat.NEW_WINDOW; 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 d42624749..86234fc03 100644 --- a/app/src/main/java/io/xpipe/app/terminal/TerminalDockBrowserComp.java +++ b/app/src/main/java/io/xpipe/app/terminal/TerminalDockBrowserComp.java @@ -8,6 +8,7 @@ import io.xpipe.app.core.window.AppMainWindow; import io.xpipe.app.platform.PlatformThread; import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.util.GlobalTimer; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableBooleanValue; @@ -25,6 +26,7 @@ import javafx.stage.WindowEvent; import org.kordamp.ikonli.javafx.FontIcon; +import java.time.Duration; import java.util.concurrent.atomic.AtomicReference; public class TerminalDockBrowserComp extends SimpleComp { @@ -105,11 +107,13 @@ public class TerminalDockBrowserComp extends SimpleComp { var focus = new ChangeListener() { @Override public void changed(ObservableValue observable, Boolean oldValue, Boolean newValue) { - if (newValue) { - model.onFocusGain(); - } else { - model.onFocusLost(); - } + GlobalTimer.delay(() -> { + if (newValue) { + model.onFocusGain(); + } else { + model.onFocusLost(); + } + }, Duration.ofMillis(100)); } }; var show = new EventHandler() { 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 d2c9d8ef9..c5a60f863 100644 --- a/app/src/main/java/io/xpipe/app/terminal/TerminalDockHubComp.java +++ b/app/src/main/java/io/xpipe/app/terminal/TerminalDockHubComp.java @@ -2,6 +2,7 @@ package io.xpipe.app.terminal; import io.xpipe.app.comp.SimpleComp; import io.xpipe.app.core.window.AppMainWindow; +import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.event.EventHandler; @@ -66,7 +67,9 @@ public class TerminalDockHubComp extends SimpleComp { if (newValue) { model.onFocusGain(); } else { - model.onFocusLost(); + Platform.runLater(() -> { + model.onFocusLost(); + }); } } }; @@ -126,9 +129,9 @@ public class TerminalDockHubComp extends SimpleComp { var sx = region.getScene().getWindow().getOutputScaleX(); var sy = region.getScene().getWindow().getOutputScaleY(); model.resizeView( - (int) Math.ceil(bounds.getMinX() * sx + p.getLeft()), - (int) Math.ceil(bounds.getMinY() * sy + p.getTop()), - (int) Math.floor(bounds.getWidth() * sx - p.getRight() - p.getLeft()), - (int) Math.floor(bounds.getHeight() * sy - p.getBottom() - p.getTop())); + (int) Math.round(bounds.getMinX() * sx + p.getLeft()), + (int) Math.round(bounds.getMinY() * sy + p.getTop()), + (int) Math.round(bounds.getWidth() * sx - p.getRight() - p.getLeft()), + (int) Math.round(bounds.getHeight() * sy - p.getBottom() - p.getTop())); } } 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 c759e41b1..02fbeab4e 100644 --- a/app/src/main/java/io/xpipe/app/terminal/TerminalDockHubManager.java +++ b/app/src/main/java/io/xpipe/app/terminal/TerminalDockHubManager.java @@ -1,14 +1,19 @@ package io.xpipe.app.terminal; +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.platform.LabelGraphic; import io.xpipe.app.platform.PlatformThread; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.util.GlobalTimer; +import io.xpipe.app.util.Rect; import io.xpipe.core.OsType; +import javafx.application.Platform; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; +import javafx.collections.ListChangeListener; import lombok.Getter; import java.time.Duration; @@ -19,6 +24,14 @@ import java.util.UUID; @Getter public class TerminalDockHubManager { + public static boolean isPossiblySupported() { + if (OsType.ofLocal() != OsType.WINDOWS) { + return false; + } + + return true; + } + public static boolean isSupported() { if (OsType.ofLocal() != OsType.WINDOWS) { return false; @@ -45,15 +58,14 @@ public class TerminalDockHubManager { private static TerminalDockHubManager INSTANCE; public static void init() { + if (!isPossiblySupported()) { + return; + } + INSTANCE = new TerminalDockHubManager(); - AppLayoutModel.get().getSelected().addListener((observable, oldValue, newValue) -> { - if (AppLayoutModel.get().getEntries().indexOf(newValue) == 0) { - INSTANCE.selectHub(); - } else { - INSTANCE.unselectHub(); - } - }); + INSTANCE.addLayoutListeners(); + INSTANCE.addDialogListeners(); TerminalView.get().addListener(INSTANCE.createListener()); @@ -68,15 +80,34 @@ public class TerminalDockHubManager { } private final Set hubRequests = new HashSet<>(); + private final BooleanProperty enabled = new SimpleBooleanProperty(); private final BooleanProperty showing = new SimpleBooleanProperty(); private final BooleanProperty detached = new SimpleBooleanProperty(); private final BooleanProperty minimized = new SimpleBooleanProperty(); - private final TerminalDockView dockModel = new TerminalDockView(); + private final TerminalDockView dockModel = new TerminalDockView(rect -> { + var term = AppPrefs.get().terminalType().getValue(); + var adjust = term instanceof TrackableTerminalType t && t.getDockMode() != TerminalDockMode.BORDERLESS; + return adjust ? new Rect(rect.getX() - 9, rect.getY() - 1, rect.getW() + 16, rect.getH() + 9) : rect; + }); private final AppLayoutModel.QueueEntry queueEntry = new AppLayoutModel.QueueEntry( AppI18n.observable("toggleTerminalDock"), new LabelGraphic.IconGraphic("mdi2c-console"), () -> { refreshDockStatus(); + if (!enabled.get()) { + return false; + } + + if (!showing.get()) { + // Run later to guarantee order of operations + Platform.runLater(() -> { + AppLayoutModel.get().selectConnections(); + showDock(); + attach(); + }); + return false; + } + if (minimized.get() || detached.get()) { attach(); return false; @@ -87,14 +118,48 @@ public class TerminalDockHubManager { return false; } - if (!showing.get()) { - showDock(); - return false; - } - return false; }); + private void addDialogListeners() { + var wasShowing = new SimpleBooleanProperty(); + var wasAttached = new SimpleBooleanProperty(); + AppDialog.getModalOverlays().addListener((ListChangeListener) c -> { + if (c.getList().size() == 0) { + if (wasShowing.get()) { + INSTANCE.showDock(); + } + if (wasAttached.get()) { + INSTANCE.attach(); + } + } else { + wasAttached.set(!INSTANCE.minimized.get() && !INSTANCE.detached.get() && INSTANCE.showing.get()); + wasShowing.set(INSTANCE.showing.get()); + INSTANCE.hideDock(); + } + }); + } + + private void addLayoutListeners() { + var wasShowing = new SimpleBooleanProperty(); + var wasAttached = new SimpleBooleanProperty(); + AppLayoutModel.get().getSelected().addListener((observable, oldValue, newValue) -> { + if (AppLayoutModel.get().getEntries().indexOf(newValue) == 0) { + if (wasShowing.get()) { + INSTANCE.showDock(); + } + if (wasAttached.get()) { + INSTANCE.attach(); + } + } else { + wasAttached.set(!INSTANCE.minimized.get() && !INSTANCE.detached.get() && INSTANCE.showing.get()); + wasShowing.set(INSTANCE.showing.get()); + INSTANCE.hideDock(); + } + }); + } + + private TerminalView.Listener createListener() { var listener = new TerminalView.Listener() { @Override @@ -114,14 +179,13 @@ public class TerminalDockHubManager { return; } - controllable.get().removeShadow(); if (t.getDockMode() == TerminalDockMode.BORDERLESS) { controllable.get().removeBorders(); } } dockModel.trackTerminal(controllable.get(), !detached.get()); dockModel.closeOtherTerminals(session.getRequest()); - openDock(); + enableDock(); } @Override @@ -141,7 +205,7 @@ public class TerminalDockHubManager { && s.getTerminal().isRunning()) .toList(); if (remaining.isEmpty()) { - closeDock(); + disableDock(); } } }; @@ -160,13 +224,6 @@ public class TerminalDockHubManager { detached.set(dockModel.isCustomBounds()); } - public void selectHub() { - dockModel.onFocusLost(); - } - - public void unselectHub() { - } - public void openTerminal(UUID request) { if (!isSupported()) { return; @@ -175,19 +232,33 @@ public class TerminalDockHubManager { hubRequests.add(request); } - public void openDock() { + public void enableDock() { PlatformThread.runLaterIfNeeded(() -> { - if (showing.get()) { + if (enabled.get()) { return; } dockModel.toggleView(true); + enabled.set(true); showing.set(true); AppLayoutModel.get().getQueueEntries().add(queueEntry); }); } + public void disableDock() { + PlatformThread.runLaterIfNeeded(() -> { + if (!enabled.get()) { + return; + } + + dockModel.toggleView(false); + enabled.set(false); + showing.set(false); + AppLayoutModel.get().getQueueEntries().remove(queueEntry); + }); + } + public void showDock() { PlatformThread.runLaterIfNeeded(() -> { if (showing.get()) { @@ -199,11 +270,6 @@ public class TerminalDockHubManager { }); } - public void attach() { - dockModel.attach(); - detached.set(false); - } - public void hideDock() { PlatformThread.runLaterIfNeeded(() -> { if (!showing.get()) { @@ -215,15 +281,8 @@ public class TerminalDockHubManager { }); } - public void closeDock() { - PlatformThread.runLaterIfNeeded(() -> { - if (!showing.get()) { - return; - } - - dockModel.toggleView(false); - showing.set(false); - AppLayoutModel.get().getQueueEntries().remove(queueEntry); - }); + public void attach() { + dockModel.attach(); + detached.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 9e8345830..897641611 100644 --- a/app/src/main/java/io/xpipe/app/terminal/TerminalDockView.java +++ b/app/src/main/java/io/xpipe/app/terminal/TerminalDockView.java @@ -1,6 +1,7 @@ package io.xpipe.app.terminal; import io.xpipe.app.issue.TrackEvent; +import io.xpipe.app.platform.NativeWinWindowControl; import io.xpipe.app.util.Rect; import lombok.Getter; @@ -9,15 +10,20 @@ import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.function.UnaryOperator; public class TerminalDockView { @Getter private final Set terminalInstances = new HashSet<>(); + private final UnaryOperator windowBoundsFunction; + private Rect viewBounds; private boolean viewActive; + public TerminalDockView(UnaryOperator windowBoundsFunction) {this.windowBoundsFunction = windowBoundsFunction;} + public synchronized boolean isRunning() { return terminalInstances.stream().anyMatch(terminal -> terminal.isRunning()); } @@ -31,7 +37,10 @@ public class TerminalDockView { } public synchronized void trackTerminal(ControllableTerminalSession terminal, boolean dock) { - terminalInstances.add(terminal); + if (!terminalInstances.add(terminal)) { + return; + } + // 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 @@ -71,7 +80,10 @@ public class TerminalDockView { this.viewActive = active; if (active) { - terminalInstances.forEach(terminalInstance -> terminalInstance.alwaysInFront()); + terminalInstances.forEach(terminalInstance -> { + terminalInstance.frontOfMainWindow(); + terminalInstance.focus(); + }); updatePositions(); } else { terminalInstances.forEach(terminalInstance -> terminalInstance.back()); @@ -183,7 +195,7 @@ public class TerminalDockView { return; } - this.viewBounds = new Rect(x, y, w, h); + this.viewBounds = windowBoundsFunction.apply(new Rect(x, y, w, h)); updatePositions(); } @@ -192,7 +204,8 @@ public class TerminalDockView { terminalInstances.forEach(terminalInstance -> { terminalInstance.show(); - terminalInstance.alwaysInFront(); + terminalInstance.frontOfMainWindow(); + terminalInstance.focus(); terminalInstance.updatePosition(viewBounds); }); } diff --git a/app/src/main/java/io/xpipe/app/terminal/TerminalLauncher.java b/app/src/main/java/io/xpipe/app/terminal/TerminalLauncher.java index e324081f4..e6a9481c7 100644 --- a/app/src/main/java/io/xpipe/app/terminal/TerminalLauncher.java +++ b/app/src/main/java/io/xpipe/app/terminal/TerminalLauncher.java @@ -169,6 +169,11 @@ public class TerminalLauncher { preferTabs && AppPrefs.get().preferTerminalTabs().get(); var launchConfig = new TerminalLaunchConfiguration(color, adjustedTitle, cleanTitle, preferTabs, paneList); + // Dock terminal if needed + for (TerminalPaneConfiguration pane : launchConfig.getPanes()) { + TerminalDockHubManager.get().openTerminal(pane.getRequest()); + } + if (effectivePreferTabs) { synchronized (TerminalLauncher.class) { // There will be timing issues when launching multiple tabs in a short time span @@ -206,10 +211,6 @@ public class TerminalLauncher { return; } - for (TerminalPaneConfiguration pane : config.getPanes()) { - TerminalDockHubManager.get().openTerminal(pane.getRequest()); - } - try { type.launch(config); latch.await(); 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 e81c74e36..120aa4b5f 100644 --- a/app/src/main/java/io/xpipe/app/terminal/WindowsTerminalSession.java +++ b/app/src/main/java/io/xpipe/app/terminal/WindowsTerminalSession.java @@ -46,11 +46,6 @@ public final class WindowsTerminalSession extends ControllableTerminalSession { control.removeBorders(); } - @Override - public void removeShadow() { - control.removeShadow(); - } - @Override public void show() { this.control.show();