From ddfa70d68b523722ca950fcaa2d276caa42da3bc Mon Sep 17 00:00:00 2001 From: crschnick Date: Tue, 5 Nov 2024 15:10:14 +0000 Subject: [PATCH] More docking rework --- .../impl/ConnectionBrowseExchangeImpl.java | 2 +- .../browser/BrowserTerminalDockTabModel.java | 51 +++++--- .../browser/session/BrowserChooserComp.java | 4 +- .../browser/session/BrowserSessionComp.java | 46 +++++-- .../browser/session/BrowserSessionModel.java | 113 ++++++++++++++++-- .../browser/session/BrowserSessionTab.java | 5 + .../session/BrowserSessionTabsComp.java | 85 ++++++++++++- ...itPaneComp.java => LeftSplitPaneComp.java} | 8 +- .../xpipe/app/comp/store/StoreLayoutComp.java | 4 +- .../io/xpipe/app/prefs/TerminalCategory.java | 6 +- .../xpipe/app/terminal/TerminalDockComp.java | 14 ++- .../xpipe/app/terminal/TerminalDockModel.java | 22 +++- .../io/xpipe/app/terminal/TerminalView.java | 10 +- .../terminal/WindowsTerminalViewInstance.java | 2 +- .../io/xpipe/app/resources/style/browser.css | 4 + .../ext/base/action/BrowseStoreAction.java | 2 +- .../ext/base/browser/OpenTerminalAction.java | 11 +- lang/app/strings/translations_en.properties | 6 +- 18 files changed, 336 insertions(+), 59 deletions(-) rename app/src/main/java/io/xpipe/app/comp/base/{SideSplitPaneComp.java => LeftSplitPaneComp.java} (89%) diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionBrowseExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionBrowseExchangeImpl.java index 1fb2f1d2a..35dfe1600 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionBrowseExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionBrowseExchangeImpl.java @@ -20,7 +20,7 @@ public class ConnectionBrowseExchangeImpl extends ConnectionBrowseExchange { throw new BeaconClientException("Not a file system connection"); } BrowserSessionModel.DEFAULT.openFileSystemSync( - e.ref(), msg.getDirectory() != null ? ignored -> msg.getDirectory() : null, null); + e.ref(), msg.getDirectory() != null ? ignored -> msg.getDirectory() : null, null, true); AppLayoutModel.get().selectBrowser(); return Response.builder().build(); } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserTerminalDockTabModel.java b/app/src/main/java/io/xpipe/app/browser/BrowserTerminalDockTabModel.java index d827e024b..d46a6a843 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserTerminalDockTabModel.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserTerminalDockTabModel.java @@ -4,6 +4,7 @@ import io.xpipe.app.browser.session.BrowserAbstractSessionModel; import io.xpipe.app.browser.session.BrowserSessionModel; import io.xpipe.app.browser.session.BrowserSessionTab; import io.xpipe.app.core.AppI18n; +import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.storage.DataColor; import io.xpipe.app.terminal.TerminalDockComp; @@ -18,11 +19,14 @@ import java.util.UUID; public final class BrowserTerminalDockTabModel extends BrowserSessionTab { + private final BrowserSessionTab origin; private final ObservableList terminalRequests; private final TerminalDockModel dockModel = new TerminalDockModel(); + private TerminalView.Listener listener; - public BrowserTerminalDockTabModel(BrowserAbstractSessionModel browserModel, ObservableList terminalRequests) { + public BrowserTerminalDockTabModel(BrowserAbstractSessionModel browserModel, BrowserSessionTab origin, ObservableList terminalRequests) { super(browserModel, AppI18n.get("terminal"), null); + this.origin = origin; this.terminalRequests = terminalRequests; } @@ -38,23 +42,32 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab { @Override public void init() throws Exception { + var sessions = new ArrayList(); var terminals = new ArrayList(); - TerminalView.get().addListener(new TerminalView.Listener() { + listener = new TerminalView.Listener() { @Override public void onSessionOpened(TerminalView.Session session) { if (!terminalRequests.contains(session.getRequest())) { return; } - var tv = terminals.stream().filter(instance -> instance.getTerminalProcess().equals(session.getTerminal())).findFirst(); - tv.ifPresent(instance -> { - dockModel.trackTerminal(instance); - }); + sessions.add(session); + var tv = terminals.stream().filter(instance -> sessions.stream().anyMatch(s -> instance.getTerminalProcess().equals(s.getTerminal()))).toList(); + if (tv.isEmpty()) { + return; + } + + for (int i = 0; i < tv.size() - 1; i++) { + dockModel.closeTerminal(tv.get(i)); + } + + var toTrack = tv.getLast(); + dockModel.trackTerminal(toTrack); } @Override public void onSessionClosed(TerminalView.Session session) { - + sessions.remove(session); } @Override @@ -64,13 +77,28 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab { @Override public void onTerminalClosed(TerminalViewInstance instance) { -terminals.remove(instance); + terminals.remove(instance); + if (terminals.isEmpty()) { + ((BrowserSessionModel) browserModel).unsplitTab(BrowserTerminalDockTabModel.this); + } } + }; + TerminalView.get().addListener(listener); + this.browserModel.getSelectedEntry().addListener((observable, oldValue, newValue) -> { + dockModel.toggleView(newValue == origin); + }); + AppLayoutModel.get().getSelected().addListener((observable, oldValue, newValue) -> { + dockModel.toggleView(AppLayoutModel.get().getEntries().indexOf(newValue) == 1); }); } @Override - public void close() {} + public void close() { + if (listener != null) { + TerminalView.get().removeListener(listener); + } + dockModel.onClose(); + } @Override public String getIcon() { @@ -81,9 +109,4 @@ terminals.remove(instance); public DataColor getColor() { return null; } - - @Override - public boolean isCloseable() { - return false; - } } diff --git a/app/src/main/java/io/xpipe/app/browser/session/BrowserChooserComp.java b/app/src/main/java/io/xpipe/app/browser/session/BrowserChooserComp.java index a628f43f7..000b9f223 100644 --- a/app/src/main/java/io/xpipe/app/browser/session/BrowserChooserComp.java +++ b/app/src/main/java/io/xpipe/app/browser/session/BrowserChooserComp.java @@ -6,7 +6,7 @@ import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.fs.OpenFileSystemComp; import io.xpipe.app.browser.fs.OpenFileSystemModel; import io.xpipe.app.comp.base.DialogComp; -import io.xpipe.app.comp.base.SideSplitPaneComp; +import io.xpipe.app.comp.base.LeftSplitPaneComp; import io.xpipe.app.comp.store.StoreEntryWrapper; import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppLayoutModel; @@ -148,7 +148,7 @@ public class BrowserChooserComp extends DialogComp { }); var vertical = new VerticalComp(List.of(bookmarkTopBar, bookmarksContainer)).styleClass("left"); - var splitPane = new SideSplitPaneComp(vertical, stack) + var splitPane = new LeftSplitPaneComp(vertical, stack) .withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth()) .withOnDividerChange(AppLayoutModel.get().getSavedState()::setBrowserConnectionsWidth) .styleClass("background") diff --git a/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionComp.java b/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionComp.java index 0cc45a115..b36e4273f 100644 --- a/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionComp.java +++ b/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionComp.java @@ -4,13 +4,14 @@ import io.xpipe.app.browser.BrowserBookmarkComp; import io.xpipe.app.browser.BrowserBookmarkHeaderComp; import io.xpipe.app.browser.BrowserTransferComp; import io.xpipe.app.comp.base.LoadingOverlayComp; -import io.xpipe.app.comp.base.SideSplitPaneComp; +import io.xpipe.app.comp.base.LeftSplitPaneComp; import io.xpipe.app.comp.store.StoreEntryWrapper; import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.ext.ShellStore; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.impl.AnchorComp; +import io.xpipe.app.fxcomps.impl.LabelComp; import io.xpipe.app.fxcomps.impl.StackComp; import io.xpipe.app.fxcomps.impl.VerticalComp; import io.xpipe.app.fxcomps.util.BindingsHelper; @@ -104,8 +105,9 @@ public class BrowserSessionComp extends SimpleComp { var vertical = new VerticalComp(List.of(bookmarkTopBar, bookmarksContainer, localDownloadStage)).styleClass("left"); - var split = new SimpleDoubleProperty(); - var tabs = new BrowserSessionTabsComp(model, split); + var leftSplit = new SimpleDoubleProperty(); + var rightSplit = new SimpleDoubleProperty(); + var tabs = new BrowserSessionTabsComp(model, leftSplit, rightSplit); tabs.apply(struc -> { struc.get().setViewOrder(1); struc.get().setPickOnBounds(false); @@ -114,6 +116,7 @@ public class BrowserSessionComp extends SimpleComp { AnchorPane.setLeftAnchor(struc.get(), 0.0); AnchorPane.setRightAnchor(struc.get(), 0.0); }); + vertical.apply(struc -> { struc.get() .paddingProperty() @@ -126,14 +129,41 @@ public class BrowserSessionComp extends SimpleComp { AnchorPane.setRightAnchor(struc.get(), 0.0); }) .styleClass("tab-loading-indicator"); - var loadingStack = new AnchorComp(List.of(tabs, loadingIndicator)); - var splitPane = new SideSplitPaneComp(vertical, loadingStack) + + var pinnedStack = new StackComp(List.of(new LabelComp("a"))); + pinnedStack.apply(struc -> { + model.getEffectiveRightTab().subscribe( (newValue) -> { + PlatformThread.runLaterIfNeeded(() -> { + if (newValue != null) { + var r = newValue.comp().createRegion(); + struc.get().getChildren().add(r); + } else { + struc.get().getChildren().clear(); + } + }); + }); + + rightSplit.addListener((observable, oldValue, newValue) -> { + struc.get().setMinWidth(newValue.doubleValue()); + struc.get().setMaxWidth(newValue.doubleValue()); + struc.get().setPrefWidth(newValue.doubleValue()); + }); + + AnchorPane.setBottomAnchor(struc.get(), 0.0); + AnchorPane.setRightAnchor(struc.get(), 0.0); + tabs.getHeaderHeight().subscribe(number -> { + AnchorPane.setTopAnchor(struc.get(), number.doubleValue()); + }); + }); + + var loadingStack = new AnchorComp(List.of(tabs, pinnedStack, loadingIndicator)); + var splitPane = new LeftSplitPaneComp(vertical, loadingStack) .withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth()) .withOnDividerChange(d -> { AppLayoutModel.get().getSavedState().setBrowserConnectionsWidth(d); - split.set(d); - }) - .apply(struc -> { + leftSplit.set(d); + }); + splitPane.apply(struc -> { struc.getLeft().setMinWidth(200); struc.getLeft().setMaxWidth(500); struc.get().setPickOnBounds(false); diff --git a/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionModel.java b/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionModel.java index c1bc0d952..3f731c714 100644 --- a/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionModel.java +++ b/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionModel.java @@ -5,6 +5,7 @@ import io.xpipe.app.browser.BrowserSavedState; import io.xpipe.app.browser.BrowserSavedStateImpl; import io.xpipe.app.browser.BrowserTransferModel; import io.xpipe.app.browser.fs.OpenFileSystemModel; +import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.util.BooleanScope; @@ -12,14 +13,20 @@ import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileSystemStore; import io.xpipe.core.util.FailableFunction; - +import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.Property; import javafx.beans.property.SimpleBooleanProperty; - +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableMap; import lombok.Getter; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; @Getter public class BrowserSessionModel extends BrowserAbstractSessionModel { @@ -32,6 +39,84 @@ public class BrowserSessionModel extends BrowserAbstractSessionModel draggingFiles = new SimpleBooleanProperty(); + private final Property globalPinnedTab = new SimpleObjectProperty<>(); + private final ObservableMap splits = FXCollections.observableHashMap(); + private final ObservableValue effectiveRightTab = createEffectiveRightTab(); + + private ObservableValue createEffectiveRightTab() { + return Bindings.createObjectBinding(() -> { + var current = selectedEntry.getValue(); + if (!current.isCloseable()) { + return null; + } + + var split = splits.get(current); + if (split != null) { + return split; + } + + var global = globalPinnedTab.getValue(); + if (global == null) { + return null; + } + + if (global == selectedEntry.getValue()) { + return null; + } + + return global; + }, globalPinnedTab, selectedEntry, splits); + } + + public BrowserSessionModel() { + sessionEntries.addListener((ListChangeListener) c -> { + var v = globalPinnedTab.getValue(); + if (v != null && !c.getList().contains(v)) { + globalPinnedTab.setValue(null); + } + + splits.keySet().removeIf(browserSessionTab -> !c.getList().contains(browserSessionTab)); + }); + } + + public void splitTab(BrowserSessionTab tab, BrowserSessionTab split) { + if (splits.containsKey(tab)) { + return; + } + + splits.put(tab, split); + ThreadHelper.runFailableAsync(() -> { + split.init(); + }); + } + + public void unsplitTab(BrowserSessionTab tab) { + if (splits.values().remove(tab)) { + ThreadHelper.runFailableAsync(() -> { + tab.close(); + }); + } + } + + + public void pinTab(BrowserSessionTab tab) { + if (tab.equals(globalPinnedTab.getValue())) { + return; + } + + globalPinnedTab.setValue(tab); + + var nextIndex = getSessionEntries().indexOf(tab) + 1; + if (nextIndex < getSessionEntries().size()) { + getSelectedEntry().setValue(getSessionEntries().get(nextIndex)); + } + } + + public void unpinTab(BrowserSessionTab tab) { + ThreadHelper.runFailableAsync(() -> { + globalPinnedTab.setValue(null); + }); + } public void restoreState(BrowserSavedState state) { ThreadHelper.runAsync(() -> { @@ -53,7 +138,8 @@ public class BrowserSessionModel extends BrowserAbstractSessionModel(sessionEntries)) { + var all = new ArrayList<>(sessionEntries); + for (var o : all) { // Don't close busy connections gracefully // as we otherwise might lock up if (!o.canImmediatelyClose()) { @@ -79,14 +165,15 @@ public class BrowserSessionModel extends BrowserAbstractSessionModel { - openFileSystemSync(store, path, externalBusy); + openFileSystemSync(store, path, externalBusy, true); }); } public OpenFileSystemModel openFileSystemSync( DataStoreEntryRef store, FailableFunction path, - BooleanProperty externalBusy) + BooleanProperty externalBusy, + boolean select) throws Exception { OpenFileSystemModel model; try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) { @@ -96,8 +183,10 @@ public class BrowserSessionModel extends BrowserAbstractSessionModel browserModel; protected final String name; protected final String tooltip; + protected final Property splitTab = new SimpleObjectProperty<>(); public BrowserSessionTab(BrowserAbstractSessionModel browserModel, String name, String tooltip) { this.browserModel = browserModel; diff --git a/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTabsComp.java b/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTabsComp.java index bd10e0d1b..1f3dd77b0 100644 --- a/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTabsComp.java +++ b/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTabsComp.java @@ -18,6 +18,7 @@ import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableDoubleValue; +import javafx.beans.value.ObservableValue; import javafx.collections.ListChangeListener; import javafx.css.PseudoClass; import javafx.geometry.Insets; @@ -33,6 +34,7 @@ import atlantafx.base.theme.Styles; import lombok.Getter; import java.util.*; +import java.util.concurrent.atomic.AtomicReference; import static atlantafx.base.theme.Styles.DENSE; import static atlantafx.base.theme.Styles.toggleStyleClass; @@ -42,13 +44,15 @@ public class BrowserSessionTabsComp extends SimpleComp { private final BrowserSessionModel model; private final ObservableDoubleValue leftPadding; + private final DoubleProperty rightPadding; @Getter private final DoubleProperty headerHeight; - public BrowserSessionTabsComp(BrowserSessionModel model, ObservableDoubleValue leftPadding) { + public BrowserSessionTabsComp(BrowserSessionModel model, ObservableDoubleValue leftPadding, DoubleProperty rightPadding) { this.model = model; this.leftPadding = leftPadding; + this.rightPadding = rightPadding; this.headerHeight = new SimpleDoubleProperty(); } @@ -258,9 +262,28 @@ public class BrowserSessionTabsComp extends SimpleComp { return tabs; } - private ContextMenu createContextMenu(TabPane tabs, Tab tab) { + private ContextMenu createContextMenu(TabPane tabs, Tab tab, BrowserSessionTab tabModel) { var cm = ContextMenuHelper.create(); + if (tabModel.isCloseable()) { + var unsplit = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("unpinTab")); + unsplit.visibleProperty().bind(PlatformThread.sync(Bindings.createBooleanBinding(() -> { + return model.getGlobalPinnedTab().getValue() != null && model.getGlobalPinnedTab().getValue().equals(tabModel); + }, model.getGlobalPinnedTab()))); + unsplit.setOnAction(event -> { + model.unpinTab(tabModel); + event.consume(); + }); + cm.getItems().add(unsplit); + + var split = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("pinTab")); + split.setOnAction(event -> { + model.pinTab(tabModel); + event.consume(); + }); + cm.getItems().add(split); + } + var select = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("selectTab")); select.acceleratorProperty() .bind(Bindings.createObjectBinding( @@ -340,7 +363,7 @@ public class BrowserSessionTabsComp extends SimpleComp { private Tab createTab(TabPane tabs, BrowserSessionTab tabModel) { var tab = new Tab(); - tab.setContextMenu(createContextMenu(tabs, tab)); + tab.setContextMenu(createContextMenu(tabs, tab, tabModel)); tab.setClosable(tabModel.isCloseable()); @@ -368,10 +391,62 @@ public class BrowserSessionTabsComp extends SimpleComp { }, PlatformThread.sync(tabModel.getBusy()))); } - tab.setText(tabModel.getName()); + + if (tabModel.getBrowserModel() instanceof BrowserSessionModel sessionModel) { + var global = PlatformThread.sync(sessionModel.getGlobalPinnedTab()); + tab.textProperty().bind(Bindings.createStringBinding(() -> { + return tabModel.getName() + (global.getValue() == tabModel ? " (" + AppI18n.get("pinned") + ")" : ""); + }, global, AppPrefs.get().language())); + } else { + tab.setText(tabModel.getName()); + } Comp comp = tabModel.comp(); - tab.setContent(comp.createRegion()); + var compRegion = comp.createRegion(); + var empty = new StackPane(); + empty.widthProperty().addListener((observable, oldValue, newValue) -> { + if (tabModel.isCloseable() && tabs.getSelectionModel().getSelectedItem() == tab) { + rightPadding.setValue(newValue.doubleValue()); + } + }); + var split = new SplitPane(compRegion); + if (tabModel.isCloseable()) { + split.getItems().add(empty); + } + model.getEffectiveRightTab().subscribe(browserSessionTab -> { + PlatformThread.runLaterIfNeeded(() -> { + if (browserSessionTab != null && split.getItems().size() > 1) { + split.getItems().set(1, empty); + } else if (browserSessionTab != null && split.getItems().size() == 1) { + split.getItems().add(empty); + } else if (browserSessionTab == null && split.getItems().size() > 1) { + split.getItems().remove(1); + } + }); + }); + tab.setContent(split); + +// var lastSplitRegion = new AtomicReference(); +// model.getGlobalPinnedTab().subscribe( (newValue) -> { +// PlatformThread.runLaterIfNeeded(() -> { +// if (newValue != null) { +// var r = newValue.comp().createRegion(); +// split.getItems().add(r); +// lastSplitRegion.set(r); +// } else if (split.getItems().size() > 1) { +// split.getItems().removeLast(); +// } +// }); +// }); +// model.getSelectedEntry().addListener((observable, oldValue, newValue) -> { +// PlatformThread.runLaterIfNeeded(() -> { +// if (newValue != null && newValue.equals(model.getGlobalPinnedTab().getValue()) && split.getItems().size() > 1) { +// split.getItems().remove(lastSplitRegion.get()); +// } else if (split.getItems().size() > 1 && !split.getItems().contains(lastSplitRegion.get())) { +// split.getItems().add(lastSplitRegion.get()); +// } +// }); +// }); var id = UUID.randomUUID().toString(); tab.setId(id); diff --git a/app/src/main/java/io/xpipe/app/comp/base/SideSplitPaneComp.java b/app/src/main/java/io/xpipe/app/comp/base/LeftSplitPaneComp.java similarity index 89% rename from app/src/main/java/io/xpipe/app/comp/base/SideSplitPaneComp.java rename to app/src/main/java/io/xpipe/app/comp/base/LeftSplitPaneComp.java index be27910da..3e3515d19 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/SideSplitPaneComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/LeftSplitPaneComp.java @@ -11,14 +11,14 @@ import lombok.Value; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; -public class SideSplitPaneComp extends Comp { +public class LeftSplitPaneComp extends Comp { private final Comp left; private final Comp center; private Double initialWidth; private Consumer onDividerChange; - public SideSplitPaneComp(Comp left, Comp center) { + public LeftSplitPaneComp(Comp left, Comp center) { this.left = left; this.center = center; } @@ -58,12 +58,12 @@ public class SideSplitPaneComp extends Comp { return new Structure(sidebar, c, r, r.getDividers().getFirst()); } - public SideSplitPaneComp withInitialWidth(double val) { + public LeftSplitPaneComp withInitialWidth(double val) { this.initialWidth = val; return this; } - public SideSplitPaneComp withOnDividerChange(Consumer onDividerChange) { + public LeftSplitPaneComp withOnDividerChange(Consumer onDividerChange) { this.onDividerChange = onDividerChange; return this; } diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreLayoutComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreLayoutComp.java index 85a43b6f0..c2efd99a3 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreLayoutComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreLayoutComp.java @@ -1,6 +1,6 @@ package io.xpipe.app.comp.store; -import io.xpipe.app.comp.base.SideSplitPaneComp; +import io.xpipe.app.comp.base.LeftSplitPaneComp; import io.xpipe.app.core.AppActionLinkDetector; import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.fxcomps.SimpleComp; @@ -15,7 +15,7 @@ public class StoreLayoutComp extends SimpleComp { @Override protected Region createSimple() { - var struc = new SideSplitPaneComp(new StoreSidebarComp(), new StoreEntryListComp()) + var struc = new LeftSplitPaneComp(new StoreSidebarComp(), new StoreEntryListComp()) .withInitialWidth(AppLayoutModel.get().getSavedState().getSidebarWidth()) .withOnDividerChange(aDouble -> { AppLayoutModel.get().getSavedState().setSidebarWidth(aDouble); diff --git a/app/src/main/java/io/xpipe/app/prefs/TerminalCategory.java b/app/src/main/java/io/xpipe/app/prefs/TerminalCategory.java index db1181135..bf851ee6a 100644 --- a/app/src/main/java/io/xpipe/app/prefs/TerminalCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/TerminalCategory.java @@ -61,11 +61,11 @@ public class TerminalCategory extends AppPrefsCategory { .apply(struc -> struc.get().setPromptText("myterminal -e $CMD")) .hide(prefs.terminalType.isNotEqualTo(ExternalTerminalType.CUSTOM))) .addComp(terminalTest) - .nameAndDescription("clearTerminalOnInit") - .addToggle(prefs.clearTerminalOnInit) - .nameAndDescription("enableTerminalDocking") + .pref(prefs.enableTerminalDocking) .addToggle(prefs.enableTerminalDocking) .hide(new SimpleBooleanProperty(!TerminalView.isSupported())) + .nameAndDescription("clearTerminalOnInit") + .addToggle(prefs.clearTerminalOnInit) ) .buildComp(); } diff --git a/app/src/main/java/io/xpipe/app/terminal/TerminalDockComp.java b/app/src/main/java/io/xpipe/app/terminal/TerminalDockComp.java index 094608bba..505577727 100644 --- a/app/src/main/java/io/xpipe/app/terminal/TerminalDockComp.java +++ b/app/src/main/java/io/xpipe/app/terminal/TerminalDockComp.java @@ -58,18 +58,30 @@ public class TerminalDockComp extends SimpleComp { }); s.addEventFilter(WindowEvent.WINDOW_HIDING,event -> { model.onClose(); + }); + s.focusedProperty().addListener((observable, oldValue, newValue) -> { + }); stack.setOnMouseClicked(event -> { model.clickView(); event.consume(); }); + stack.getStyleClass().add("terminal-dock-comp"); return stack; } private void update(Region region) { + if (region.getScene() == null || region.getScene().getWindow() == null) { + return; + } + var bounds = region.localToScreen(region.getBoundsInLocal()); + var p = region.getPadding(); var sx = region.getScene().getWindow().getOutputScaleX(); var sy = region.getScene().getWindow().getOutputScaleY(); - model.resizeView((int) Math.ceil(bounds.getMinX() * sx), (int) Math.ceil(bounds.getMinY() * sy),(int) Math.floor(bounds.getWidth() * sx), (int) Math.floor(bounds.getHeight() * sy)); + 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())); } } diff --git a/app/src/main/java/io/xpipe/app/terminal/TerminalDockModel.java b/app/src/main/java/io/xpipe/app/terminal/TerminalDockModel.java index 2ac2b4b49..32856a656 100644 --- a/app/src/main/java/io/xpipe/app/terminal/TerminalDockModel.java +++ b/app/src/main/java/io/xpipe/app/terminal/TerminalDockModel.java @@ -4,9 +4,12 @@ import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.util.Rect; import io.xpipe.core.process.OsType; +import lombok.Getter; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; public class TerminalDockModel { @@ -16,10 +19,26 @@ public class TerminalDockModel { private Rect viewBounds; private boolean viewActive; - private final List terminalInstances = new ArrayList<>(); + @Getter + private final Set terminalInstances = new HashSet<>(); + + public TerminalDockModel() { + int a = 0; + } public synchronized void trackTerminal(TerminalViewInstance terminal) { terminalInstances.add(terminal); + terminal.alwaysInFront(); + terminal.updatePosition(viewBounds); + } + + public synchronized void closeTerminal(TerminalViewInstance terminal) { + if (!terminalInstances.contains(terminal)) { + return; + } + + terminal.close(); + terminalInstances.remove(terminal); } public boolean isEnabled() { @@ -130,6 +149,7 @@ public class TerminalDockModel { terminalInstance.close(); }); + terminalInstances.clear(); } private void updatePositions() { diff --git a/app/src/main/java/io/xpipe/app/terminal/TerminalView.java b/app/src/main/java/io/xpipe/app/terminal/TerminalView.java index f881df459..23315de08 100644 --- a/app/src/main/java/io/xpipe/app/terminal/TerminalView.java +++ b/app/src/main/java/io/xpipe/app/terminal/TerminalView.java @@ -43,12 +43,16 @@ public class TerminalView { private final List terminalInstances = new ArrayList<>(); private final List listeners = new ArrayList<>(); - public void addListener(Listener listener) { + public synchronized void addListener(Listener listener) { this.listeners.add(listener); } + public synchronized void removeListener(Listener listener) { + this.listeners.remove(listener); + } + public boolean isEnabled() { - return isSupported() && AppPrefs.get().enableTerminalDocking().get(); + return isSupported(); } public synchronized void open(UUID request, long pid) { @@ -125,7 +129,7 @@ public class TerminalView { ThreadHelper.createPlatformThread("terminal-view", true, () -> { while (true) { instance.tick(); - ThreadHelper.sleep(1000); + ThreadHelper.sleep(500); } }).start(); INSTANCE = instance; diff --git a/app/src/main/java/io/xpipe/app/terminal/WindowsTerminalViewInstance.java b/app/src/main/java/io/xpipe/app/terminal/WindowsTerminalViewInstance.java index 6741c7678..1a64b9d6e 100644 --- a/app/src/main/java/io/xpipe/app/terminal/WindowsTerminalViewInstance.java +++ b/app/src/main/java/io/xpipe/app/terminal/WindowsTerminalViewInstance.java @@ -49,7 +49,7 @@ public final class WindowsTerminalViewInstance extends TerminalViewInstance { @Override public void updatePosition(Rect bounds) { control.move(bounds); - this.lastBounds = bounds; + this.lastBounds = queryBounds(); this.customBounds = false; } diff --git a/app/src/main/resources/io/xpipe/app/resources/style/browser.css b/app/src/main/resources/io/xpipe/app/resources/style/browser.css index 21dbe4121..5611bc350 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/browser.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/browser.css @@ -75,6 +75,10 @@ -fx-background-color: -color-bg-default; } +.browser .terminal-dock-comp { + -fx-padding: 7 0 7 7; +} + .selected-file-list { -fx-spacing: 5px; -fx-padding: 8px; diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/BrowseStoreAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/BrowseStoreAction.java index 048c46593..f4ab490e4 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/BrowseStoreAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/BrowseStoreAction.java @@ -57,7 +57,7 @@ public class BrowseStoreAction implements ActionProvider { public void execute() throws Exception { DataStoreEntryRef replacement = ProcessControlProvider.get().replace(entry.ref()); - BrowserSessionModel.DEFAULT.openFileSystemSync(replacement, null, new SimpleBooleanProperty()); + BrowserSessionModel.DEFAULT.openFileSystemSync(replacement, null, new SimpleBooleanProperty(), true); AppLayoutModel.get().selectBrowser(); } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenTerminalAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenTerminalAction.java index daebee9f9..3bdfc5528 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenTerminalAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenTerminalAction.java @@ -1,5 +1,6 @@ package io.xpipe.ext.base.browser; +import io.xpipe.app.browser.BrowserTerminalDockTabModel; import io.xpipe.app.browser.action.LeafAction; import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.fs.OpenFileSystemModel; @@ -27,14 +28,14 @@ public class OpenTerminalAction implements LeafAction { model.getCurrentDirectory() != null ? model.getCurrentDirectory().getPath() : null); - if (model.getBrowserModel() instanceof BrowserSessionModel sessionModel) { - sessionModel. + } else { + for (var entry : entries) { + model.openTerminalAsync(entry.getRawFileEntry().getPath()); } - return; } - for (var entry : entries) { - model.openTerminalAsync(entry.getRawFileEntry().getPath()); + if (AppPrefs.get().enableTerminalDocking().get() && model.getBrowserModel() instanceof BrowserSessionModel sessionModel) { + sessionModel.splitTab(model,new BrowserTerminalDockTabModel(sessionModel, model, model.getTerminalRequests())); } } diff --git a/lang/app/strings/translations_en.properties b/lang/app/strings/translations_en.properties index 80258167f..3c0b077f4 100644 --- a/lang/app/strings/translations_en.properties +++ b/lang/app/strings/translations_en.properties @@ -546,6 +546,10 @@ scriptsIntroBottomTitle=Using scripts scriptsIntroBottomText=There are a variety of sample scripts to start out. You can click on the edit button of the individual scripts to see how they are implemented. Scripts have to be enabled to run and show up in menus, there is a toggle on every script for that. scriptsIntroStart=Get started checkForSecurityUpdates=Check for security updates -#force checkForSecurityUpdatesDescription=XPipe can check for potential security updates separately from normal feature updates. When this is enabled, at least important security updates will be recommended for installation even if the normal update check is disabled.\n\nDisabling this setting will result in no external version request being performed, and you won't be notified about any security updates. clickToDock=Click to dock terminal +pinTab=Pin tab +unpinTab=Unpin tab +pinned=Pinned +enableTerminalDocking=Enable terminal docking +enableTerminalDockingDescription=With terminal docking you can dock terminal windows to the XPipe application window to simulate a somewhat integrated terminal. The terminal windows are then managed by XPipe to always fit into the dock. \ No newline at end of file