diff --git a/app/src/main/java/io/xpipe/app/auxw/AppAuxiliaryWindow.java b/app/src/main/java/io/xpipe/app/auxw/AppAuxiliaryWindow.java index e0c456317..cad1a26c5 100644 --- a/app/src/main/java/io/xpipe/app/auxw/AppAuxiliaryWindow.java +++ b/app/src/main/java/io/xpipe/app/auxw/AppAuxiliaryWindow.java @@ -1,7 +1,6 @@ package io.xpipe.app.auxw; import io.xpipe.app.core.*; -import io.xpipe.app.core.window.AppModifiedStage; import io.xpipe.app.core.window.AppWindowStyle; import io.xpipe.app.platform.DerivedObservableList; import io.xpipe.app.platform.PlatformThread; @@ -10,6 +9,7 @@ import io.xpipe.app.storage.DataStoreColor; import io.xpipe.app.util.GlobalTimer; import io.xpipe.app.util.Rect; import io.xpipe.core.OsType; +import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; @@ -22,7 +22,6 @@ import javafx.scene.Scene; import javafx.scene.layout.Region; import javafx.scene.paint.Color; import javafx.stage.Stage; -import javafx.stage.StageStyle; import lombok.Builder; import lombok.Getter; import lombok.Value; @@ -34,7 +33,7 @@ import java.util.function.Predicate; public class AppAuxiliaryWindow { - private AppAuxiliaryWindow(WindowState state, AuxDockImpl model) { + private AppAuxiliaryWindow(State state, AuxDockImpl model) { this.state = state; this.model = model; } @@ -48,7 +47,7 @@ public class AppAuxiliaryWindow { return; } - WindowState state = AppCache.getNonNull("auxiliaryWindowState", WindowState.class, () -> null); + State state = AppCache.getNonNull("auxiliaryWindowState", State.class, () -> null); var model = new AuxDockImpl(rect -> rect, () -> { return INSTANCE.nativeWinWindowControl; }); @@ -56,7 +55,7 @@ public class AppAuxiliaryWindow { INSTANCE.startStateListener(); } - private WindowState state; + private State state; private Stage stage; private NativeWinWindowControl nativeWinWindowControl; @@ -65,9 +64,11 @@ public class AppAuxiliaryWindow { @Getter private final ObjectProperty selected = new SimpleObjectProperty<>(); + @Getter private final ObservableList processes = FXCollections.observableArrayList(); + @Getter private final BooleanProperty locked = new SimpleBooleanProperty(); private void createStage() { @@ -127,9 +128,18 @@ public class AppAuxiliaryWindow { selected.set(entry); } + public void close(AuxEntry entry) { + model.closeWindow(entry); + } + + public void toggleLock() { + locked.set(!locked.get()); + state = state.toBuilder().locked(locked.get()).build(); + } + public void track(String name, String icon, DataStoreColor color, Process process, Duration maxWait, Predicate filter) { var start = Instant.now(); - GlobalTimer.scheduleUntil(Duration.ofSeconds(1), false, () -> { + GlobalTimer.scheduleUntil(Duration.ofMillis(200), false, () -> { if (Duration.between(start, Instant.now()).compareTo(maxWait) > 0) { return true; } @@ -181,8 +191,8 @@ public class AppAuxiliaryWindow { private void updateState() { var oldSize = processes.size(); model.clearDead(); - selected.set(model.getSelected()); DerivedObservableList.wrap(processes, true).setContent(model.getEntries()); + selected.set(model.getSelected()); if (oldSize > 0 && processes.isEmpty()) { PlatformThread.runLaterIfNeededBlocking(() -> { stage.hide(); @@ -190,25 +200,35 @@ public class AppAuxiliaryWindow { } } - private void onWindowChange() { - state = new WindowState(stage.isMaximized(), stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight()); + private void onWindowStateChange() { + state = new State(state != null && state.locked, stage.isMaximized(), stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight()); } private void setupWindowListeners() { stage.xProperty().addListener((c, o, n) -> { - onWindowChange(); + onWindowStateChange(); }); stage.yProperty().addListener((c, o, n) -> { - onWindowChange(); + onWindowStateChange(); }); stage.widthProperty().addListener((c, o, n) -> { - onWindowChange(); + onWindowStateChange(); + locked.set(false); }); stage.heightProperty().addListener((c, o, n) -> { - onWindowChange(); + onWindowStateChange(); + locked.set(false); }); stage.maximizedProperty().addListener((c, o, n) -> { - onWindowChange(); + onWindowStateChange(); + locked.set(false); + Platform.runLater(() -> { + stage.setWidth(state.getWindowWidth()); + stage.setHeight(state.getWindowHeight()); + }); + }); + locked.addListener((v, o, n) -> { + stage.setResizable(!n); }); } @@ -222,10 +242,11 @@ public class AppAuxiliaryWindow { return INSTANCE; } - @Builder + @Builder(toBuilder = true) @Jacksonized @Value - public static class WindowState { + public static class State { + boolean locked; boolean maximized; double windowX; double windowY; diff --git a/app/src/main/java/io/xpipe/app/auxw/AuxDockCompImpl.java b/app/src/main/java/io/xpipe/app/auxw/AuxDockCompImpl.java index b8564ed50..24b1f7716 100644 --- a/app/src/main/java/io/xpipe/app/auxw/AuxDockCompImpl.java +++ b/app/src/main/java/io/xpipe/app/auxw/AuxDockCompImpl.java @@ -1,18 +1,29 @@ package io.xpipe.app.auxw; +import atlantafx.base.controls.Spacer; import atlantafx.base.theme.Styles; import io.xpipe.app.comp.SimpleRegionBuilder; +import io.xpipe.app.comp.base.IconButtonComp; +import io.xpipe.app.comp.base.LabelComp; import io.xpipe.app.comp.base.PrettyImageHelper; +import io.xpipe.app.core.AppFontSizes; +import io.xpipe.app.platform.LabelGraphic; import io.xpipe.app.platform.PlatformThread; import javafx.application.Platform; +import javafx.beans.binding.Bindings; import javafx.collections.ListChangeListener; import javafx.css.PseudoClass; +import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.ToolBar; +import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; +import javafx.scene.text.TextAlignment; + +import java.util.List; public class AuxDockCompImpl extends SimpleRegionBuilder { @@ -26,8 +37,9 @@ public class AuxDockCompImpl extends SimpleRegionBuilder { vbox.focusWithinProperty().addListener((observable, oldValue, newValue) -> { if (newValue) { var w = AppAuxiliaryWindow.get(); - var target = vbox.getScene().getRoot().lookup(".button:hover"); - if (target == null || target.getProperties().get("entry").equals(w.getSelected().getValue())) { + var target = vbox.getScene().getRoot().lookup("*:hover"); + if (target == null || (target.getProperties().get("entry") != null && + target.getProperties().get("entry").equals(w.getSelected().getValue()))) { Platform.runLater(() -> { w.focus(); }); @@ -37,39 +49,80 @@ public class AuxDockCompImpl extends SimpleRegionBuilder { return vbox; } + private void fillToolbar(ToolBar bar, List list) { + var w = AppAuxiliaryWindow.get(); + bar.getItems().clear(); + for (var entry : list) { + var graphic = PrettyImageHelper.ofFixedSizeSquare(entry.getIcon(), 16).style("graphic").build(); + + var label = new LabelComp(entry.getName()).build(); + label.setGraphic(graphic); + + var close = new IconButtonComp("mdi2c-close", () -> { + w.close(entry); + }).style("close-button") + .describe(d -> d.nameKey("close")).build(); + AppFontSizes.sm(close); + + var hbox = new HBox(label, close); + hbox.setSpacing(6); + hbox.setAlignment(Pos.CENTER_LEFT); + + var b = new Button(null, hbox); + if (entry.getColor() != null) { + b.getStyleClass().add(entry.getColor().getId()); + } + b.getStyleClass().add("color-box"); + b.getStyleClass().add("tab-button"); + b.getProperties().put("entry", entry); + b.setOnAction(event -> { + w.select(entry); + event.consume(); + }); + bar.getItems().add(b); + } + + bar.getItems().add(new Spacer()); + var lockIcon = Bindings.createObjectBinding(() -> { + return new LabelGraphic.IconGraphic(w.getLocked().get() ? "mdi2l-lock-outline" : "mdi2l-lock-open-variant-outline"); + }, w.getLocked()); + var lock = new IconButtonComp(lockIcon, () -> { + w.toggleLock(); + w.focus(); + }).describe(d -> d.nameKey("toggleSizeLock").showTooltips(true)).style("lock-button").build(); + bar.getItems().add(lock); + } + + private void updateSelection(ToolBar bar, AuxEntry entry) { + for (Node item : bar.getItems()) { + if (item.getProperties().get("entry") != null) { + if (item.getProperties().get("entry").equals(entry)) { + item.pseudoClassStateChanged(PseudoClass.getPseudoClass("selected"), true); + } else { + item.pseudoClassStateChanged(PseudoClass.getPseudoClass("selected"), false); + } + } + } + } + private Region createBar() { var w = AppAuxiliaryWindow.get(); var bar = new ToolBar(); + + fillToolbar(bar, w.getProcesses()); w.getProcesses().addListener((ListChangeListener) c -> { PlatformThread.runLaterIfNeeded(() -> { - bar.getItems().clear(); - for (var entry : c.getList()) { - var b = new Button(entry.getName()); - b.setGraphic(PrettyImageHelper.ofFixedSizeSquare(entry.getIcon(), 16).build()); - if (entry.getColor() != null) { - b.getStyleClass().add(entry.getColor().getId()); - } - b.getStyleClass().add("color-box"); - b.getProperties().put("entry", entry); - b.setOnAction(event -> { - w.select(entry); - event.consume(); - }); - bar.getItems().add(b); - } + fillToolbar(bar, c.getList()); }); }); + + updateSelection(bar, w.getSelected().getValue()); w.getSelected().addListener((observable, oldValue, newValue) -> { PlatformThread.runLaterIfNeeded(() -> { - for (Node item : bar.getItems()) { - if (item.getProperties().get("entry").equals(newValue)) { - item.pseudoClassStateChanged(PseudoClass.getPseudoClass("selected"), true); - } else { - item.pseudoClassStateChanged(PseudoClass.getPseudoClass("selected"), false); - } - } + updateSelection(bar, newValue); }); }); + return bar; } diff --git a/app/src/main/java/io/xpipe/app/auxw/AuxDockImpl.java b/app/src/main/java/io/xpipe/app/auxw/AuxDockImpl.java index 259df23d4..30bed6ce6 100644 --- a/app/src/main/java/io/xpipe/app/auxw/AuxDockImpl.java +++ b/app/src/main/java/io/xpipe/app/auxw/AuxDockImpl.java @@ -29,12 +29,7 @@ public class AuxDockImpl implements WindowDockListener { public synchronized void clearDead() { for (AuxEntry entry : new ArrayList<>(entries)) { if (!entry.getProcess().isRunning()) { - if (entry.equals(selected)) { - var index = entries.indexOf(entry); - var fallback = index == 0 ? (entries.size() > 1 ? entries.get(1) : null) : entries.get(index - 1); - select(fallback); - } - entries.remove(entry); + closeWindow(entry); } } } @@ -70,10 +65,10 @@ public class AuxDockImpl implements WindowDockListener { private synchronized void show(AuxEntry e) { var controllable = e.getProcess(); - if (!controllable.isActive()) { - return; - } + parent.get().moveToFront(); + + controllable.moveToFront(); controllable.removeIcon(); controllable.own(parent.get()); controllable.removeStyle(true); @@ -81,7 +76,7 @@ public class AuxDockImpl implements WindowDockListener { updatePositions(); } - public synchronized void hide(AuxEntry e) { + private synchronized void hide(AuxEntry e) { var controllable = e.getProcess(); controllable.disown(); controllable.backOfWindow(parent.get()); @@ -94,13 +89,19 @@ public class AuxDockImpl implements WindowDockListener { } var p = e.getProcess(); - // Reset style in case close is blocked by terminal - p.restoreIcon(); - p.disown(); - p.restoreStyle(true); + if (p.isRunning()) { + // Reset style in case close is prevented by application + p.restoreIcon(); + p.disown(); + p.restoreStyle(true); + p.close(); + } - p.close(); - // If the process blocked the exit, still don't track it anymore + if (e.equals(selected)) { + var index = entries.indexOf(e); + var fallback = index == 0 ? (entries.size() > 1 ? entries.get(1) : null) : entries.get(index - 1); + select(fallback); + } entries.remove(e); } diff --git a/app/src/main/java/io/xpipe/app/auxw/ControllableWindowProcess.java b/app/src/main/java/io/xpipe/app/auxw/ControllableWindowProcess.java index d8daaa70e..88fb032ca 100644 --- a/app/src/main/java/io/xpipe/app/auxw/ControllableWindowProcess.java +++ b/app/src/main/java/io/xpipe/app/auxw/ControllableWindowProcess.java @@ -33,7 +33,7 @@ public abstract class ControllableWindowProcess { public abstract void minimize(); - public abstract void frontOfMainWindow(); + public abstract void moveToFront(); public abstract void backOfWindow(NativeWinWindowControl window); diff --git a/app/src/main/java/io/xpipe/app/auxw/ControllableWindowsProcess.java b/app/src/main/java/io/xpipe/app/auxw/ControllableWindowsProcess.java index 555d254ce..af5465a0e 100644 --- a/app/src/main/java/io/xpipe/app/auxw/ControllableWindowsProcess.java +++ b/app/src/main/java/io/xpipe/app/auxw/ControllableWindowsProcess.java @@ -92,7 +92,7 @@ public final class ControllableWindowsProcess extends ControllableWindowProcess } @Override - public void frontOfMainWindow() { + public void moveToFront() { this.control.moveToFront(); } diff --git a/app/src/main/java/io/xpipe/app/rdp/MstscRdpClient.java b/app/src/main/java/io/xpipe/app/rdp/MstscRdpClient.java index 4e9f737c2..b8c52fa38 100644 --- a/app/src/main/java/io/xpipe/app/rdp/MstscRdpClient.java +++ b/app/src/main/java/io/xpipe/app/rdp/MstscRdpClient.java @@ -62,8 +62,14 @@ public class MstscRdpClient implements ExternalApplicationType.PathApplication, @Override public void launch(RdpLaunchConfig configuration) throws Exception { var aux = AppAuxiliaryWindow.get(); + String width = null; + String height = null; if (aux != null) { aux.show(); + if (aux.getLocked().get()) { + width = "/w:" + aux.getDockBounds().getW(); + height = "/h:" + aux.getDockBounds().getH(); + } } var adaptedRdpConfig = getAuxWindowConfig(getAdaptedConfig(configuration)); @@ -71,7 +77,7 @@ public class MstscRdpClient implements ExternalApplicationType.PathApplication, var setCache = prepareLocalhostRegistryCache(configuration); var file = writeRdpConfigFile(configuration.getTitle(), adaptedRdpConfig); - var process = LocalExec.executeAsync(getExecutable(), file.toString()); + var process = LocalExec.executeAsync(getExecutable(), file.toString(), width, height); if (process != null && aux != null) { aux.show(); var entry = configuration.getEntry(); diff --git a/app/src/main/java/io/xpipe/app/terminal/WezTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/WezTerminalType.java index 6531f0d03..27a29c47d 100644 --- a/app/src/main/java/io/xpipe/app/terminal/WezTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/WezTerminalType.java @@ -255,7 +255,7 @@ public interface WezTerminalType extends ExternalTerminalType, TrackableTerminal public void onSessionOpened(TerminalView.ShellSession session) { TerminalView.get().removeListener(this); if (session.getTerminal() instanceof TerminalView.ControllableTerminalSession t) { - t.getControllable().frontOfMainWindow(); + t.getControllable().moveToFront(); t.getControllable().focus(); } } diff --git a/app/src/main/java/io/xpipe/app/util/LocalExec.java b/app/src/main/java/io/xpipe/app/util/LocalExec.java index 7a038076b..a44f3f471 100644 --- a/app/src/main/java/io/xpipe/app/util/LocalExec.java +++ b/app/src/main/java/io/xpipe/app/util/LocalExec.java @@ -4,17 +4,19 @@ import io.xpipe.app.core.AppSystemInfo; import io.xpipe.app.issue.TrackEvent; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Optional; public class LocalExec { public static Process executeAsync(String... command) { + var list = Arrays.stream(command).filter(s -> s != null).toList(); try { TrackEvent.withTrace("Running local command") - .tag("command", String.join(" ", command)) + .tag("command", String.join(" ", list)) .handle(); - var pb = new ProcessBuilder(command) + var pb = new ProcessBuilder(list) .redirectOutput(ProcessBuilder.Redirect.DISCARD) .redirectError(ProcessBuilder.Redirect.DISCARD); pb.directory(AppSystemInfo.ofCurrent().getUserHome().toFile()); @@ -26,7 +28,7 @@ public class LocalExec { return pb.start(); } catch (Exception ex) { TrackEvent.withTrace("Local command finished") - .tag("command", String.join(" ", command)) + .tag("command", String.join(" ", list)) .tag("error", ex.toString()) .handle(); return null; diff --git a/app/src/main/resources/io/xpipe/app/resources/style/remote-desktop-dock.css b/app/src/main/resources/io/xpipe/app/resources/style/remote-desktop-dock.css new file mode 100644 index 000000000..c3a900365 --- /dev/null +++ b/app/src/main/resources/io/xpipe/app/resources/style/remote-desktop-dock.css @@ -0,0 +1,52 @@ +.remote-desktop-dock .tab-button { + -fx-background-color: transparent; + -fx-background-radius: 4px; + -fx-border-radius: 4px; + -fx-border-width: 1; + -fx-padding: 3 7 3 7; + -fx-background-insets: 0; + -fx-opacity: 0.8; +} + +.remote-desktop-dock .lock-button { + -fx-background-color: transparent; + -fx-background-radius: 4px; + -fx-border-radius: 4px; + -fx-border-width: 1; + -fx-padding: 3 7 3 7; +} + +.remote-desktop-dock .tab-button .graphic { + -fx-opacity: 0.3; +} + +.root:nord .remote-desktop-dock .tab-button { + -fx-background-radius: 0; + -fx-border-radius: 0; +} + +.remote-desktop-dock .tab-button:hover, .root:key-navigation .remote-desktop-dock .tab-button:focused { + -fx-background-color: -color-bg-default-transparent; +} + +.remote-desktop-dock .tab-button:selected, .remote-desktop-dock .tab-button:hover { + -fx-opacity: 1.0; +} + +.remote-desktop-dock .tab-button:selected .graphic, .remote-desktop-dock .tab-button:hover .graphic { + -fx-opacity: 1.0; +} + +.remote-desktop-dock .content { + -fx-background-color: black; +} + +.remote-desktop-dock .close-button { + -fx-padding: 3 3 1 3; +} + +.remote-desktop-dock .tool-bar { + -fx-border-width: 1 0 1 0; + -fx-border-color: -color-border-default; +} + diff --git a/app/src/main/resources/io/xpipe/app/resources/style/window-dock.css b/app/src/main/resources/io/xpipe/app/resources/style/window-dock.css deleted file mode 100644 index 86a2ade18..000000000 --- a/app/src/main/resources/io/xpipe/app/resources/style/window-dock.css +++ /dev/null @@ -1,26 +0,0 @@ -.remote-desktop-dock .button { - -fx-background-color: transparent; - -fx-background-radius: 4px; - -fx-border-radius: 4px; - -fx-border-width: 1; - -fx-padding: 3 7 3 7; - -fx-background-insets: 0; -} - -.root:nord .remote-desktop-dock .button { - -fx-background-radius: 0; - -fx-border-radius: 0; -} - -.remote-desktop-dock .button:hover, .root:key-navigation .remote-desktop-dock .button:focused { - -fx-background-color: -color-bg-default-transparent; -} - -.remote-desktop-dock .button:selected { - -fx-border-width: 1px; -} - -.remote-desktop-dock .content { - -fx-background-color: transparent; -} - diff --git a/lang/strings/translations_en.properties b/lang/strings/translations_en.properties index fd6f4e012..7690aa8f2 100644 --- a/lang/strings/translations_en.properties +++ b/lang/strings/translations_en.properties @@ -2057,3 +2057,4 @@ sftpNoticeTitle=Large file transfers sftpNoticeContent=The files you are trying to transfer are quite large. The current session is based on raw SSH, which does not give you the best possible transfer speed. You can look into opening an SFTP session to the system in the right-click menu in the file browser if you need to increase the transfer speed. hideVaultEntryNames=Hide vault entry names hideVaultEntryNamesDescription=Removes all connection names and other information from any READMEs and commit messages. This option prevents any insight into the structure of the encrypted vault contents. +toggleSizeLock=Toggle size lock \ No newline at end of file