Browser fixes

This commit is contained in:
crschnick 2024-11-06 01:35:38 +00:00
parent ddfa70d68b
commit ff36cfb12a
17 changed files with 163 additions and 134 deletions

View file

@ -27,7 +27,7 @@ You should therefore always check out the matching version tag for your local re
You can find the available version tags at https://github.com/xpipe-io/xpipe/tags.
So for example if you currently have XPipe `11.3` installed, you should run `git reset --hard 11.3` first to properly compile against it.
You need to have JDK for Java 21 installed to compile the project.
You need to have JDK for Java 22 installed to compile the project.
If you are on Linux or macOS, you can easily accomplish that by running
```bash
curl -s "https://get.sdkman.io" | bash
@ -57,7 +57,7 @@ to connect to that debugger through [AttachMe](https://plugins.jetbrains.com/plu
## Modularity and IDEs
All XPipe components target [Java 21](https://openjdk.java.net/projects/jdk/21/) and make full use of the Java Module System (JPMS).
All XPipe components target [Java 22](https://openjdk.java.net/projects/jdk/22/) and make full use of the Java Module System (JPMS).
All components are modularized, including all their dependencies.
In case a dependency is (sadly) not modularized yet, module information is manually added using [extra-java-module-info](https://github.com/gradlex-org/extra-java-module-info).
Further, note that as this is a pretty complicated Java project that fully utilizes modularity,
@ -65,7 +65,7 @@ many IDEs still have problems building this project properly.
For example, you can't build this project in eclipse or vscode as it will complain about missing modules.
The tested and recommended IDE is IntelliJ.
When setting up the project in IntelliJ, make sure that the correct JDK (Java 21)
When setting up the project in IntelliJ, make sure that the correct JDK (Java 22)
is selected both for the project and for gradle itself.
## Contributing guide

View file

@ -10,7 +10,7 @@ import io.xpipe.app.storage.DataColor;
public final class BrowserHomeTabModel extends BrowserSessionTab {
public BrowserHomeTabModel(BrowserAbstractSessionModel<?> browserModel) {
super(browserModel, AppI18n.get("overview"), null);
super(browserModel, AppI18n.get("overview"));
}
@Override

View file

@ -11,6 +11,9 @@ import io.xpipe.app.terminal.TerminalDockComp;
import io.xpipe.app.terminal.TerminalDockModel;
import io.xpipe.app.terminal.TerminalView;
import io.xpipe.app.terminal.TerminalViewInstance;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableBooleanValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
@ -23,9 +26,10 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab {
private final ObservableList<UUID> terminalRequests;
private final TerminalDockModel dockModel = new TerminalDockModel();
private TerminalView.Listener listener;
private ObservableBooleanValue viewActive;
public BrowserTerminalDockTabModel(BrowserAbstractSessionModel<?> browserModel, BrowserSessionTab origin, ObservableList<UUID> terminalRequests) {
super(browserModel, AppI18n.get("terminal"), null);
super(browserModel, AppI18n.get("terminal"));
this.origin = origin;
this.terminalRequests = terminalRequests;
}
@ -84,11 +88,14 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab {
}
};
TerminalView.get().addListener(listener);
this.browserModel.getSelectedEntry().addListener((observable, oldValue, newValue) -> {
dockModel.toggleView(newValue == origin);
viewActive = Bindings.createBooleanBinding(() -> {
return this.browserModel.getSelectedEntry().getValue() == origin && AppLayoutModel.get().getEntries().indexOf(AppLayoutModel.get().getSelected().getValue()) == 1;
}, this.browserModel.getSelectedEntry(), AppLayoutModel.get().getSelected());
viewActive.subscribe(aBoolean -> {
Platform.runLater(() -> {
dockModel.toggleView(aBoolean);
});
AppLayoutModel.get().getSelected().addListener((observable, oldValue, newValue) -> {
dockModel.toggleView(AppLayoutModel.get().getEntries().indexOf(newValue) == 1);
});
}

View file

@ -24,11 +24,6 @@ public interface LeafAction extends BrowserAction {
default Button toButton(Region root, OpenFileSystemModel model, List<BrowserEntry> selected) {
var b = new Button();
b.setOnAction(event -> {
// Only accept shortcut actions in the current tab
if (!model.equals(model.getBrowserModel().getSelectedEntry().getValue())) {
return;
}
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(model.getBusy(), () -> {
if (model.getFileSystem() == null) {

View file

@ -40,6 +40,7 @@ import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
@ -274,8 +275,15 @@ public final class BrowserFileListComp extends SimpleComp {
}
table.getSelectionModel().setCellSelectionEnabled(false);
var updateFromModel = new BooleanScope(new SimpleBooleanProperty());
table.getSelectionModel().getSelectedItems().addListener((ListChangeListener<? super BrowserEntry>) c -> {
if (updateFromModel.get()) {
return;
}
try (var ignored = updateFromModel) {
fileList.getSelection().setAll(c.getList());
}
});
fileList.getSelection().addListener((ListChangeListener<? super BrowserEntry>) c -> {
@ -284,16 +292,27 @@ public final class BrowserFileListComp extends SimpleComp {
}
Platform.runLater(() -> {
if (c.getList().isEmpty()) {
var tableIndices = table.getSelectionModel().getSelectedItems().stream()
.mapToInt(entry -> table.getItems().indexOf(entry))
.toArray();
var indices = c.getList().stream()
.mapToInt(entry -> table.getItems().indexOf(entry))
.toArray();
if (Arrays.equals(indices, tableIndices)) {
return;
}
if (indices.length == 0) {
table.getSelectionModel().clearSelection();
return;
}
var indices = c.getList().stream()
.mapToInt(entry -> table.getItems().indexOf(entry))
.toArray();
table.getSelectionModel()
.selectIndices(table.getItems().indexOf(c.getList().getFirst()), indices);
if (indices.length == 1) {
table.getSelectionModel().clearAndSelect(indices[0]);
} else {
table.getSelectionModel().clearSelection();
table.getSelectionModel().selectIndices(indices[0], indices);
}
});
});
}

View file

@ -9,9 +9,9 @@ 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.CompStructure;
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;
@ -25,8 +25,10 @@ import javafx.beans.property.SimpleDoubleProperty;
import javafx.geometry.Insets;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle;
import java.util.HashMap;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
@ -41,6 +43,66 @@ public class BrowserSessionComp extends SimpleComp {
@Override
protected Region createSimple() {
var vertical = createLeftSide();
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);
AnchorPane.setTopAnchor(struc.get(), 0.0);
AnchorPane.setBottomAnchor(struc.get(), 0.0);
AnchorPane.setLeftAnchor(struc.get(), 0.0);
AnchorPane.setRightAnchor(struc.get(), 0.0);
});
vertical.apply(struc -> {
struc.get()
.paddingProperty()
.bind(Bindings.createObjectBinding(
() -> new Insets(tabs.getHeaderHeight().get(), 0, 0, 0), tabs.getHeaderHeight()));
});
var loadingIndicator = LoadingOverlayComp.noProgress(Comp.empty(), model.getBusy())
.apply(struc -> {
AnchorPane.setTopAnchor(struc.get(), 3.0);
AnchorPane.setRightAnchor(struc.get(), 0.0);
})
.styleClass("tab-loading-indicator");
var pinnedStack = createSplitStack(rightSplit, tabs);
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);
leftSplit.set(d);
});
splitPane.apply(struc -> {
struc.getLeft().setMinWidth(200);
struc.getLeft().setMaxWidth(500);
struc.get().setPickOnBounds(false);
});
splitPane.apply(struc -> {
struc.get().skinProperty().subscribe(newValue -> {
if (newValue != null) {
Platform.runLater(() -> {
struc.get().getChildrenUnmodifiable().forEach(node -> {
node.setClip(null);
node.setPickOnBounds(false);
});
struc.get().lookupAll(".split-pane-divider").forEach(node -> node.setViewOrder(1));
});
}
});
});
splitPane.styleClass("browser");
return splitPane.createRegion();
}
private Comp<CompStructure<VBox>> createLeftSide() {
Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> {
if (!storeEntryWrapper.getEntry().getValidity().isUsable()) {
return false;
@ -104,42 +166,30 @@ public class BrowserSessionComp extends SimpleComp {
localDownloadStage.maxHeight(200);
var vertical =
new VerticalComp(List.of(bookmarkTopBar, bookmarksContainer, localDownloadStage)).styleClass("left");
return vertical;
}
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);
AnchorPane.setTopAnchor(struc.get(), 0.0);
AnchorPane.setBottomAnchor(struc.get(), 0.0);
AnchorPane.setLeftAnchor(struc.get(), 0.0);
AnchorPane.setRightAnchor(struc.get(), 0.0);
});
vertical.apply(struc -> {
struc.get()
.paddingProperty()
.bind(Bindings.createObjectBinding(
() -> new Insets(tabs.getHeaderHeight().get(), 0, 0, 0), tabs.getHeaderHeight()));
});
var loadingIndicator = LoadingOverlayComp.noProgress(Comp.empty(), model.getBusy())
.apply(struc -> {
AnchorPane.setTopAnchor(struc.get(), 3.0);
AnchorPane.setRightAnchor(struc.get(), 0.0);
})
.styleClass("tab-loading-indicator");
var pinnedStack = new StackComp(List.of(new LabelComp("a")));
private StackComp createSplitStack(SimpleDoubleProperty rightSplit, BrowserSessionTabsComp tabs) {
var cache = new HashMap<BrowserSessionTab, Region>();
var pinnedStack = new StackComp(List.of());
pinnedStack.apply(struc -> {
model.getEffectiveRightTab().subscribe( (newValue) -> {
PlatformThread.runLaterIfNeeded(() -> {
if (newValue != null) {
var r = newValue.comp().createRegion();
struc.get().getChildren().add(r);
} else {
var all = model.getAllTabs();
cache.keySet().removeIf(browserSessionTab -> !all.contains(browserSessionTab));
if (newValue == null) {
struc.get().getChildren().clear();
return;
}
var cached = cache.containsKey(newValue);
if (!cached) {
cache.put(newValue, newValue.comp().createRegion());
}
var r = cache.get(newValue);
struc.get().getChildren().clear();
struc.get().getChildren().add(r);
});
});
@ -154,35 +204,6 @@ public class BrowserSessionComp extends SimpleComp {
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);
leftSplit.set(d);
});
splitPane.apply(struc -> {
struc.getLeft().setMinWidth(200);
struc.getLeft().setMaxWidth(500);
struc.get().setPickOnBounds(false);
});
splitPane.apply(struc -> {
struc.get().skinProperty().subscribe(newValue -> {
if (newValue != null) {
Platform.runLater(() -> {
struc.get().getChildrenUnmodifiable().forEach(node -> {
node.setClip(null);
node.setPickOnBounds(false);
});
struc.get().lookupAll(".split-pane-divider").forEach(node -> node.setViewOrder(1));
});
}
});
});
splitPane.styleClass("browser");
return splitPane.createRegion();
}); return pinnedStack;
}
}

View file

@ -24,9 +24,7 @@ import javafx.collections.ListChangeListener;
import javafx.collections.ObservableMap;
import lombok.Getter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.*;
@Getter
public class BrowserSessionModel extends BrowserAbstractSessionModel<BrowserSessionTab> {
@ -79,6 +77,16 @@ public class BrowserSessionModel extends BrowserAbstractSessionModel<BrowserSess
});
}
public Set<BrowserSessionTab> getAllTabs() {
var set = new HashSet<BrowserSessionTab>();
set.addAll(sessionEntries);
set.addAll(splits.values());
if (globalPinnedTab.getValue() != null) {
set.add(globalPinnedTab.getValue());
}
return set;
}
public void splitTab(BrowserSessionTab tab, BrowserSessionTab split) {
if (splits.containsKey(tab)) {
return;
@ -98,7 +106,6 @@ public class BrowserSessionModel extends BrowserAbstractSessionModel<BrowserSess
}
}
public void pinTab(BrowserSessionTab tab) {
if (tab.equals(globalPinnedTab.getValue())) {
return;

View file

@ -18,13 +18,11 @@ public abstract class BrowserSessionTab {
protected final BooleanProperty busy = new SimpleBooleanProperty();
protected final BrowserAbstractSessionModel<?> browserModel;
protected final String name;
protected final String tooltip;
protected final Property<BrowserSessionTab> splitTab = new SimpleObjectProperty<>();
public BrowserSessionTab(BrowserAbstractSessionModel<?> browserModel, String name, String tooltip) {
public BrowserSessionTab(BrowserAbstractSessionModel<?> browserModel, String name) {
this.browserModel = browserModel;
this.name = name;
this.tooltip = tooltip;
}
public abstract Comp<?> comp();

View file

@ -426,28 +426,6 @@ public class BrowserSessionTabsComp extends SimpleComp {
});
tab.setContent(split);
// var lastSplitRegion = new AtomicReference<Region>();
// 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);
@ -471,7 +449,6 @@ public class BrowserSessionTabsComp extends SimpleComp {
if (color != null) {
c.getStyleClass().add(color.getId());
}
new TooltipAugment<>(new SimpleStringProperty(tabModel.getTooltip()), null).augment(c);
c.addEventHandler(
DragEvent.DRAG_ENTERED,
mouseEvent -> Platform.runLater(

View file

@ -16,8 +16,7 @@ public abstract class BrowserStoreSessionTab<T extends DataStore> extends Browse
public BrowserStoreSessionTab(BrowserAbstractSessionModel<?> browserModel, DataStoreEntryRef<? extends T> entry) {
super(
browserModel,
DataStorage.get().getStoreEntryDisplayName(entry.get()),
DataStorage.get().getStorePath(entry.getEntry()).toString());
DataStorage.get().getStoreEntryDisplayName(entry.get()));
this.entry = entry;
}

View file

@ -69,18 +69,7 @@ public class StoreEntryListOverviewComp extends SimpleComp {
.getValue()
.getRoot()
.equals(rootCategory);
// Sadly the all binding does not update when the individual visibility of entries changes
// But it is good enough.
var showProvider = true;
try {
showProvider = storeEntryWrapper.getEntry().getProvider() == null
|| storeEntryWrapper
.getEntry()
.getProvider()
.shouldShow(storeEntryWrapper);
} catch (Exception ignored) {
}
return inRootCategory && showProvider;
return inRootCategory;
},
StoreViewState.get().getActiveCategory());
var count = new CountComp<>(all.getList(), all.getList());

View file

@ -60,7 +60,11 @@ public class TerminalDockComp extends SimpleComp {
model.onClose();
});
s.focusedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
model.onFocusGain();
} else {
model.onFocusLost();
}
});
stack.setOnMouseClicked(event -> {
model.clickView();

View file

@ -29,8 +29,10 @@ public class TerminalDockModel {
public synchronized void trackTerminal(TerminalViewInstance terminal) {
terminalInstances.add(terminal);
terminal.alwaysInFront();
if (viewBounds != null) {
terminal.updatePosition(viewBounds);
}
}
public synchronized void closeTerminal(TerminalViewInstance terminal) {
if (!terminalInstances.contains(terminal)) {

View file

@ -30,7 +30,7 @@ public final class WindowsTerminalViewInstance extends TerminalViewInstance {
@Override
public void alwaysInFront() {
this.control.alwaysInFront();
this.control.removeBorders();
// this.control.removeBorders();
}
@Override

View file

@ -13,6 +13,10 @@ public class BooleanScope implements AutoCloseable {
this.prop = prop;
}
public boolean get() {
return prop.get();
}
public static <E extends Throwable> void executeExclusive(BooleanProperty prop, FailableRunnable<E> r) throws E {
try (var ignored = new BooleanScope(prop).exclusive().start()) {
r.run();

View file

@ -99,12 +99,13 @@ public class DesktopShortcuts {
}
public static Path create(String executable, String args, String name) throws Exception {
var compat = OsType.getLocal().makeFileSystemCompatible(name);
if (OsType.getLocal().equals(OsType.WINDOWS)) {
return createWindowsShortcut(executable, args, name);
return createWindowsShortcut(executable, args, compat);
} else if (OsType.getLocal().equals(OsType.LINUX)) {
return createLinuxShortcut(executable, args, name);
return createLinuxShortcut(executable, args, compat);
} else {
return createMacOSShortcut(executable, args, name);
return createMacOSShortcut(executable, args, compat);
}
}
}

View file

@ -35,6 +35,12 @@ public class OpenTerminalAction implements LeafAction {
}
if (AppPrefs.get().enableTerminalDocking().get() && model.getBrowserModel() instanceof BrowserSessionModel sessionModel) {
// Check if the right side is already occupied
var existingSplit = sessionModel.getSplits().get(model);
if (existingSplit != null && !(existingSplit instanceof BrowserTerminalDockTabModel)) {
return;
}
sessionModel.splitTab(model, new BrowserTerminalDockTabModel(sessionModel, model, model.getTerminalRequests()));
}
}