Compare commits

...

2 commits

Author SHA1 Message Date
crschnick
933b3f6837 Clean up shortcut handling 2024-05-08 12:34:28 +00:00
crschnick
26823e4728 Browser shortcut rework 2024-05-08 09:56:52 +00:00
27 changed files with 315 additions and 285 deletions

View file

@ -1,19 +1,21 @@
package io.xpipe.app.browser; package io.xpipe.app.browser;
import atlantafx.base.theme.Styles;
import io.xpipe.app.browser.fs.OpenFileSystemModel; import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.impl.TextFieldComp; import io.xpipe.app.fxcomps.impl.TextFieldComp;
import io.xpipe.app.fxcomps.impl.TooltipAugment; import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.util.InputHelper;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import atlantafx.base.theme.Styles;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
public class BrowserFilterComp extends Comp<BrowserFilterComp.Structure> { public class BrowserFilterComp extends Comp<BrowserFilterComp.Structure> {
@ -29,9 +31,15 @@ public class BrowserFilterComp extends Comp<BrowserFilterComp.Structure> {
@Override @Override
public Structure createBase() { public Structure createBase() {
var expanded = new SimpleBooleanProperty(); var expanded = new SimpleBooleanProperty();
var text = new TextFieldComp(filterString, false).createRegion(); var text = new TextFieldComp(filterString, false).createStructure().get();
var button = new Button(); var button = new Button();
new TooltipAugment<>("app.search").augment(button); button.setFocusTraversable(true);
InputHelper.onExactKeyCode(text, KeyCode.ESCAPE, true, keyEvent -> {
text.clear();
button.fire();
keyEvent.consume();
});
new TooltipAugment<>("app.search", new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN)).augment(button);
text.focusedProperty().addListener((observable, oldValue, newValue) -> { text.focusedProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue && filterString.getValue() == null) { if (!newValue && filterString.getValue() == null) {
if (button.isFocused()) { if (button.isFocused()) {

View file

@ -1,19 +1,18 @@
package io.xpipe.app.browser; package io.xpipe.app.browser;
import atlantafx.base.theme.Styles;
import io.xpipe.app.browser.file.BrowserContextMenu; import io.xpipe.app.browser.file.BrowserContextMenu;
import io.xpipe.app.browser.fs.OpenFileSystemModel; import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.browser.icon.FileIconManager; import io.xpipe.app.browser.icon.FileIconManager;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment; import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.impl.StackComp;
import io.xpipe.app.fxcomps.impl.TextFieldComp; import io.xpipe.app.fxcomps.impl.TextFieldComp;
import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
@ -25,25 +24,16 @@ import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyCombination;
import javafx.scene.input.MouseButton; import javafx.scene.input.MouseButton;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import atlantafx.base.theme.Styles;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List; public class BrowserNavBar extends Comp<BrowserNavBar.Structure> {
public class BrowserNavBar extends SimpleComp {
private static final PseudoClass INVISIBLE = PseudoClass.getPseudoClass("invisible");
private final OpenFileSystemModel model;
public BrowserNavBar(OpenFileSystemModel model) {
this.model = model;
}
@Override @Override
protected Region createSimple() { public Structure createBase() {
var path = new SimpleStringProperty(model.getCurrentPath().get()); var path = new SimpleStringProperty(model.getCurrentPath().get());
model.getCurrentPath().subscribe((newValue) -> { model.getCurrentPath().subscribe((newValue) -> {
path.set(newValue); path.set(newValue);
@ -86,9 +76,6 @@ public class BrowserNavBar extends SimpleComp {
struc.get().setPromptText("Overview of " + model.getName()); struc.get().setPromptText("Overview of " + model.getName());
}) })
.shortcut(new KeyCodeCombination(KeyCode.P, KeyCombination.SHORTCUT_DOWN), s -> {
s.get().requestFocus();
})
.accessibleText("Current path"); .accessibleText("Current path");
var graphic = Bindings.createStringBinding( var graphic = Bindings.createStringBinding(
@ -107,62 +94,63 @@ public class BrowserNavBar extends SimpleComp {
homeButton.getStyleClass().add(Styles.LEFT_PILL); homeButton.getStyleClass().add(Styles.LEFT_PILL);
homeButton.getStyleClass().add("path-graphic-button"); homeButton.getStyleClass().add("path-graphic-button");
new ContextMenuAugment<>(event -> event.getButton() == MouseButton.PRIMARY, null, () -> { new ContextMenuAugment<>(event -> event.getButton() == MouseButton.PRIMARY, null, () -> {
return model.getInOverview().get() ? null : new BrowserContextMenu(model, null); return model.getInOverview().get() ? null : new BrowserContextMenu(model, null);
}) })
.augment(new SimpleCompStructure<>(homeButton)); .augment(new SimpleCompStructure<>(homeButton));
var historyButton = new Button(null, new FontIcon("mdi2h-history")); var historyButton = new Button(null, new FontIcon("mdi2h-history"));
historyButton.setAccessibleText("History"); historyButton.setAccessibleText("History");
historyButton.getStyleClass().add(Styles.RIGHT_PILL); historyButton.getStyleClass().add(Styles.RIGHT_PILL);
// historyButton.getStyleClass().add("path-graphic-button");
new ContextMenuAugment<>(event -> event.getButton() == MouseButton.PRIMARY, null, this::createContextMenu) new ContextMenuAugment<>(event -> event.getButton() == MouseButton.PRIMARY, null, this::createContextMenu)
.augment(new SimpleCompStructure<>(historyButton)); .augment(new SimpleCompStructure<>(historyButton));
new TooltipAugment<>("history", new KeyCodeCombination(KeyCode.H, KeyCombination.ALT_DOWN)).augment(historyButton);
var breadcrumbs = new BrowserBreadcrumbBar(model).grow(false, true); var breadcrumbs = new BrowserBreadcrumbBar(model).grow(false, true);
var stack = new StackComp(List.of(pathBar, breadcrumbs))
.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT))
.hgrow()
.apply(struc -> {
var t = struc.get().getChildren().get(0);
var b = struc.get().getChildren().get(1);
b.setOnMouseClicked(event -> {
t.requestFocus();
event.consume();
});
b.visibleProperty()
.bind(Bindings.createBooleanBinding(
() -> {
return !t.isFocused()
&& !model.getInOverview().get();
},
t.focusedProperty(),
model.getInOverview()));
})
.grow(false, true);
var topBox = new HorizontalComp(List.of(Comp.of(() -> homeButton), stack, Comp.of(() -> historyButton))) var pathRegion = pathBar.createStructure().get();
.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT)) var breadcrumbsRegion = breadcrumbs.createRegion();
.apply(struc -> { breadcrumbsRegion.setOnMouseClicked(event -> {
((Region) struc.get().getChildren().get(0)) pathRegion.requestFocus();
.minHeightProperty() event.consume();
.bind(((Region) struc.get().getChildren().get(1)).heightProperty()); });
((Region) struc.get().getChildren().get(0)) breadcrumbsRegion.setFocusTraversable(false);
.maxHeightProperty() breadcrumbsRegion.visibleProperty()
.bind(((Region) struc.get().getChildren().get(1)).heightProperty()); .bind(Bindings.createBooleanBinding(
() -> {
return !pathRegion.isFocused()
&& !model.getInOverview().get();
},
pathRegion.focusedProperty(),
model.getInOverview()));
var stack = new StackPane(pathRegion, breadcrumbsRegion);
stack.setAlignment(Pos.CENTER_LEFT);
HBox.setHgrow(stack, Priority.ALWAYS);
((Region) struc.get().getChildren().get(2)) var topBox = new HBox(homeButton, stack, historyButton);
.minHeightProperty() homeButton.minHeightProperty().bind(stack.heightProperty());
.bind(((Region) struc.get().getChildren().get(1)).heightProperty()); homeButton.maxHeightProperty().bind(stack.heightProperty());
((Region) struc.get().getChildren().get(2)) historyButton.minHeightProperty().bind(stack.heightProperty());
.maxHeightProperty() historyButton.maxHeightProperty().bind(stack.heightProperty());
.bind(((Region) struc.get().getChildren().get(1)).heightProperty()); topBox.setPickOnBounds(false);
}) HBox.setHgrow(topBox, Priority.ALWAYS);
.apply(struc -> {
struc.get().setPickOnBounds(false);
})
.hgrow();
return topBox.createRegion(); return new Structure(topBox,pathRegion, historyButton);
}
public record Structure(HBox box, TextField textField, Button historyButton) implements CompStructure<HBox> {
@Override
public HBox get() {
return box;
}
}
private static final PseudoClass INVISIBLE = PseudoClass.getPseudoClass("invisible");
private final OpenFileSystemModel model;
public BrowserNavBar(OpenFileSystemModel model) {
this.model = model;
} }
private ContextMenu createContextMenu() { private ContextMenu createContextMenu() {

View file

@ -78,7 +78,7 @@ public class BrowserTransferComp extends SimpleComp {
}) })
.hide(Bindings.isEmpty(syncItems)) .hide(Bindings.isEmpty(syncItems))
.disable(syncAllDownloaded) .disable(syncAllDownloaded)
.apply(new TooltipAugment<>("downloadStageDescription")); .tooltipKey("downloadStageDescription");
var clearButton = new IconButtonComp("mdi2c-close", () -> { var clearButton = new IconButtonComp("mdi2c-close", () -> {
model.clear(); model.clear();
}) })

View file

@ -4,14 +4,13 @@ import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.fs.OpenFileSystemModel; import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.fxcomps.impl.TooltipAugment; import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.Shortcuts;
import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.LicenseProvider; import io.xpipe.app.util.LicenseProvider;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.MenuItem; import javafx.scene.control.MenuItem;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Region;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List; import java.util.List;
@ -20,7 +19,7 @@ public interface LeafAction extends BrowserAction {
void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception; void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception;
default Button toButton(OpenFileSystemModel model, List<BrowserEntry> selected) { default Button toButton(Region root, OpenFileSystemModel model, List<BrowserEntry> selected) {
var b = new Button(); var b = new Button();
b.setOnAction(event -> { b.setOnAction(event -> {
// Only accept shortcut actions in the current tab // Only accept shortcut actions in the current tab
@ -37,17 +36,20 @@ public interface LeafAction extends BrowserAction {
}); });
event.consume(); event.consume();
}); });
if (getShortcut() != null) {
Shortcuts.addShortcut(b, getShortcut());
}
var name = getName(model, selected); var name = getName(model, selected);
new TooltipAugment<>(name).augment(b); new TooltipAugment<>(name, getShortcut()).augment(b);
var graphic = getIcon(model, selected); var graphic = getIcon(model, selected);
if (graphic != null) { if (graphic != null) {
b.setGraphic(graphic); b.setGraphic(graphic);
} }
b.setMnemonicParsing(false); b.setMnemonicParsing(false);
b.accessibleTextProperty().bind(name); b.accessibleTextProperty().bind(name);
root.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (getShortcut() != null && getShortcut().match(event)) {
b.fire();
event.consume();
}
});
b.setDisable(!isActive(model, selected)); b.setDisable(!isActive(model, selected));
model.getCurrentPath().addListener((observable, oldValue, newValue) -> { model.getCurrentPath().addListener((observable, oldValue, newValue) -> {

View file

@ -5,6 +5,7 @@ import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.LeafAction; import io.xpipe.app.browser.action.LeafAction;
import io.xpipe.app.browser.fs.OpenFileSystemModel; import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppFont;
import io.xpipe.app.util.InputHelper;
import io.xpipe.app.util.LicenseProvider; import io.xpipe.app.util.LicenseProvider;
import javafx.scene.control.ContextMenu; import javafx.scene.control.ContextMenu;
@ -38,6 +39,11 @@ public final class BrowserContextMenu extends ContextMenu {
} }
private void createMenu() { private void createMenu() {
InputHelper.onLeft(this, false, e -> {
hide();
e.consume();
});
AppFont.normal(this.getStyleableNode()); AppFont.normal(this.getStyleableNode());
var empty = source == null; var empty = source == null;

View file

@ -10,9 +10,7 @@ import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment; import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.*;
import io.xpipe.app.util.HumanReadableFormat;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.OsType; import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileNames;
@ -26,12 +24,16 @@ import javafx.collections.ListChangeListener;
import javafx.css.PseudoClass; import javafx.css.PseudoClass;
import javafx.geometry.Bounds; import javafx.geometry.Bounds;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.scene.AccessibleRole; import javafx.scene.AccessibleRole;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.control.skin.TableViewSkin; import javafx.scene.control.skin.TableViewSkin;
import javafx.scene.control.skin.VirtualFlow; import javafx.scene.control.skin.VirtualFlow;
import javafx.scene.input.*; import javafx.scene.input.DragEvent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
@ -128,7 +130,6 @@ public final class BrowserFileListComp extends SimpleComp {
} else { } else {
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
} }
table.getSelectionModel().setCellSelectionEnabled(false); table.getSelectionModel().setCellSelectionEnabled(false);
table.getSelectionModel().getSelectedItems().addListener((ListChangeListener<? super BrowserEntry>) c -> { table.getSelectionModel().getSelectedItems().addListener((ListChangeListener<? super BrowserEntry>) c -> {
@ -158,18 +159,24 @@ public final class BrowserFileListComp extends SimpleComp {
private void prepareTableShortcuts(TableView<BrowserEntry> table) { private void prepareTableShortcuts(TableView<BrowserEntry> table) {
table.setOnKeyPressed(event -> { table.setOnKeyPressed(event -> {
var selected = fileList.getSelection(); var selected = fileList.getSelection();
BrowserAction.getFlattened(fileList.getFileSystemModel(), selected).stream() var action = BrowserAction.getFlattened(fileList.getFileSystemModel(), selected).stream().filter(
.filter(browserAction -> browserAction.isApplicable(fileList.getFileSystemModel(), selected) browserAction -> browserAction.isApplicable(fileList.getFileSystemModel(), selected) &&
&& browserAction.isActive(fileList.getFileSystemModel(), selected)) browserAction.isActive(fileList.getFileSystemModel(), selected)).filter(
.filter(browserAction -> browserAction.getShortcut() != null) browserAction -> browserAction.getShortcut() != null).filter(browserAction -> browserAction.getShortcut().match(event)).findAny();
.filter(browserAction -> browserAction.getShortcut().match(event)) action.ifPresent(browserAction -> {
.findAny()
.ifPresent(browserAction -> {
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
browserAction.execute(fileList.getFileSystemModel(), selected); browserAction.execute(fileList.getFileSystemModel(), selected);
}); });
event.consume(); event.consume();
}); });
if (action.isPresent()) {
return;
}
if (event.getCode() == KeyCode.ESCAPE) {
table.getSelectionModel().clearSelection();
event.consume();
}
}); });
} }
@ -344,7 +351,6 @@ public final class BrowserFileListComp extends SimpleComp {
// Sort the list ourselves as sorting the table would incur a lot of cell updates // Sort the list ourselves as sorting the table would incur a lot of cell updates
var obs = FXCollections.observableList(newItems); var obs = FXCollections.observableList(newItems);
table.getItems().setAll(obs); table.getItems().setAll(obs);
// table.sort();
} }
var currentDirectory = fileList.getFileSystemModel().getCurrentDirectory(); var currentDirectory = fileList.getFileSystemModel().getCurrentDirectory();
@ -488,9 +494,6 @@ public final class BrowserFileListComp extends SimpleComp {
.not() .not()
.not()) .not())
.focusTraversable(false) .focusTraversable(false)
.apply(struc -> struc.get().focusedProperty().addListener((observable, oldValue, newValue) -> {
getTableRow().requestFocus();
}))
.createRegion(); .createRegion();
editing.addListener((observable, oldValue, newValue) -> { editing.addListener((observable, oldValue, newValue) -> {
@ -520,13 +523,20 @@ public final class BrowserFileListComp extends SimpleComp {
graphic.setAlignment(Pos.CENTER_LEFT); graphic.setAlignment(Pos.CENTER_LEFT);
setGraphic(graphic); setGraphic(graphic);
tableView.addEventFilter(KeyEvent.KEY_PRESSED, event -> { InputHelper.onExactKeyCode(tableView, KeyCode.RIGHT, true, event -> {
if (event.getCode() == KeyCode.RIGHT) { var selected = fileList.getSelection();
var selected = fileList.getSelection(); if (selected.size() == 1 && selected.getFirst() == getTableRow().getItem()) {
if (selected.size() == 1 && selected.getFirst() == getTableRow().getItem()) { ((ButtonBase) quickAccess).fire();
((ButtonBase) quickAccess).fire(); event.consume();
event.consume(); }
} });
InputHelper.onExactKeyCode(tableView, KeyCode.SPACE, true, event -> {
var selected = fileList.getSelection();
// Only show one menu across all selected entries
if (selected.size() > 0 && selected.getLast() == getTableRow().getItem()) {
var cm = new BrowserContextMenu(fileList.getFileSystemModel(), getTableRow().getItem());
ContextMenuHelper.toggleShow(cm, this, Side.RIGHT);
event.consume();
} }
}); });
} }

View file

@ -1,19 +1,16 @@
package io.xpipe.app.browser.file; package io.xpipe.app.browser.file;
import io.xpipe.app.browser.fs.OpenFileSystemModel; import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem; import io.xpipe.core.store.FileSystem;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import lombok.Getter; import lombok.Getter;
import java.util.ArrayList; import java.util.ArrayList;
@ -37,8 +34,6 @@ public final class BrowserFileListModel {
private final Property<List<BrowserEntry>> shown = new SimpleObjectProperty<>(new ArrayList<>()); private final Property<List<BrowserEntry>> shown = new SimpleObjectProperty<>(new ArrayList<>());
private final ObservableList<BrowserEntry> previousSelection = FXCollections.observableArrayList(); private final ObservableList<BrowserEntry> previousSelection = FXCollections.observableArrayList();
private final ObservableList<BrowserEntry> selection = FXCollections.observableArrayList(); private final ObservableList<BrowserEntry> selection = FXCollections.observableArrayList();
private final ObservableList<FileSystem.FileEntry> selectedRaw =
ListBindingsHelper.mappedContentBinding(selection, entry -> entry.getRawFileEntry());
private final Property<BrowserEntry> draggedOverDirectory = new SimpleObjectProperty<>(); private final Property<BrowserEntry> draggedOverDirectory = new SimpleObjectProperty<>();
private final Property<Boolean> draggedOverEmpty = new SimpleBooleanProperty(); private final Property<Boolean> draggedOverEmpty = new SimpleBooleanProperty();

View file

@ -4,9 +4,6 @@ import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.util.InputHelper; import io.xpipe.app.util.InputHelper;
import javafx.application.Platform;
import javafx.scene.control.Menu;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import java.util.function.Supplier; import java.util.function.Supplier;
@ -34,11 +31,6 @@ public class BrowserQuickAccessButtonComp extends SimpleComp {
} }
event.consume(); event.consume();
}); });
cm.addEventFilter(Menu.ON_HIDDEN, e -> {
Platform.runLater(() -> {
struc.get().requestFocus();
});
});
InputHelper.onRight(struc.get(), false, keyEvent -> { InputHelper.onRight(struc.get(), false, keyEvent -> {
cm.showMenu(struc.get()); cm.showMenu(struc.get());
keyEvent.consume(); keyEvent.consume();

View file

@ -1,5 +1,6 @@
package io.xpipe.app.browser.fs; package io.xpipe.app.browser.fs;
import atlantafx.base.controls.Spacer;
import io.xpipe.app.browser.BrowserFilterComp; import io.xpipe.app.browser.BrowserFilterComp;
import io.xpipe.app.browser.BrowserNavBar; import io.xpipe.app.browser.BrowserNavBar;
import io.xpipe.app.browser.BrowserOverviewComp; import io.xpipe.app.browser.BrowserOverviewComp;
@ -13,9 +14,9 @@ import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment; import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.fxcomps.impl.VerticalComp; import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.Shortcuts; import io.xpipe.app.util.InputHelper;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Button; import javafx.scene.control.Button;
@ -28,8 +29,6 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import atlantafx.base.controls.Spacer;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
import java.util.ArrayList; import java.util.ArrayList;
@ -53,17 +52,24 @@ public class OpenFileSystemComp extends SimpleComp {
} }
private Region createContent() { private Region createContent() {
var root = new VBox();
var overview = new Button(null, new FontIcon("mdi2m-monitor")); var overview = new Button(null, new FontIcon("mdi2m-monitor"));
overview.setOnAction(e -> model.cdAsync(null)); overview.setOnAction(e -> model.cdAsync(null));
new TooltipAugment<>("overview", new KeyCodeCombination(KeyCode.HOME, KeyCombination.ALT_DOWN)).augment(overview);
overview.disableProperty().bind(model.getInOverview()); overview.disableProperty().bind(model.getInOverview());
overview.setAccessibleText("System overview"); overview.setAccessibleText("System overview");
InputHelper.onKeyCombination(root, new KeyCodeCombination(KeyCode.HOME, KeyCombination.ALT_DOWN), true, keyEvent -> {
overview.fire();
keyEvent.consume();
});
var backBtn = BrowserAction.byId("back", model, List.of()).toButton(model, List.of()); var backBtn = BrowserAction.byId("back", model, List.of()).toButton(root, model, List.of());
var forthBtn = BrowserAction.byId("forward", model, List.of()).toButton(model, List.of()); var forthBtn = BrowserAction.byId("forward", model, List.of()).toButton(root, model, List.of());
var refreshBtn = BrowserAction.byId("refresh", model, List.of()).toButton(model, List.of()); var refreshBtn = BrowserAction.byId("refresh", model, List.of()).toButton(root, model, List.of());
var terminalBtn = BrowserAction.byId("openTerminal", model, List.of()).toButton(model, List.of()); var terminalBtn = BrowserAction.byId("openTerminal", model, List.of()).toButton(root, model, List.of());
var menuButton = new MenuButton(null, new FontIcon("mdral-folder_open")); var menuButton = new MenuButton(null, new FontIcon
("mdral-folder_open"));
new ContextMenuAugment<>( new ContextMenuAugment<>(
event -> event.getButton() == MouseButton.PRIMARY, event -> event.getButton() == MouseButton.PRIMARY,
null, null,
@ -73,18 +79,18 @@ public class OpenFileSystemComp extends SimpleComp {
menuButton.setAccessibleText("Directory options"); menuButton.setAccessibleText("Directory options");
var filter = new BrowserFilterComp(model, model.getFilter()).createStructure(); var filter = new BrowserFilterComp(model, model.getFilter()).createStructure();
Shortcuts.addShortcut(filter.toggleButton(), new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN));
var topBar = new HBox(); var topBar = new HBox();
topBar.setAlignment(Pos.CENTER); topBar.setAlignment(Pos.CENTER);
topBar.getStyleClass().add("top-bar"); topBar.getStyleClass().add("top-bar");
var navBar = new BrowserNavBar(model).createStructure();
topBar.getChildren() topBar.getChildren()
.setAll( .setAll(
overview, overview,
backBtn, backBtn,
forthBtn, forthBtn,
new Spacer(10), new Spacer(10),
new BrowserNavBar(model).hgrow().createRegion(), navBar.get(),
new Spacer(5), new Spacer(5),
filter.get(), filter.get(),
refreshBtn, refreshBtn,
@ -92,9 +98,29 @@ public class OpenFileSystemComp extends SimpleComp {
menuButton); menuButton);
var content = createFileListContent(); var content = createFileListContent();
var root = new VBox(topBar, content); root.getChildren().addAll(topBar, content);
VBox.setVgrow(content, Priority.ALWAYS); VBox.setVgrow(content, Priority.ALWAYS);
root.setPadding(Insets.EMPTY); root.setPadding(Insets.EMPTY);
InputHelper.onKeyCombination(root, new KeyCodeCombination(KeyCode.F, KeyCombination.CONTROL_DOWN), true, keyEvent -> {
filter.toggleButton().fire();
filter.textField().requestFocus();
keyEvent.consume();
});
InputHelper.onKeyCombination(root, new KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN), true, keyEvent -> {
navBar.textField().requestFocus();
keyEvent.consume();
});
InputHelper.onKeyCombination(root, new KeyCodeCombination(KeyCode.H, KeyCombination.ALT_DOWN), true, keyEvent -> {
navBar.historyButton().fire();
keyEvent.consume();
});
InputHelper.onKeyCombination(root, new KeyCodeCombination(KeyCode.UP, KeyCombination.ALT_DOWN), true, keyEvent -> {
var p = model.getCurrentParentDirectory();
if (p != null) {
model.cdAsync(p.getPath());
}
keyEvent.consume();
});
return root; return root;
} }

View file

@ -10,6 +10,7 @@ import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.InputHelper;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
@ -21,6 +22,7 @@ import javafx.scene.control.Label;
import javafx.scene.control.Tab; import javafx.scene.control.Tab;
import javafx.scene.control.TabPane; import javafx.scene.control.TabPane;
import javafx.scene.input.DragEvent; import javafx.scene.input.DragEvent;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
@ -164,6 +166,24 @@ public class BrowserSessionTabsComp extends SimpleComp {
} }
} }
}); });
InputHelper.onInput(tabs, true, keyEvent -> {
var current = tabs.getSelectionModel().getSelectedItem();
if (current == null) {
return;
}
if (keyEvent.getCode() == KeyCode.W && keyEvent.isShortcutDown()) {
tabs.getTabs().remove(current);
keyEvent.consume();
}
if (keyEvent.getCode() == KeyCode.W && keyEvent.isShortcutDown() && keyEvent.isShiftDown()) {
tabs.getTabs().clear();
keyEvent.consume();
}
});
return tabs; return tabs;
} }
@ -216,7 +236,7 @@ public class BrowserSessionTabsComp extends SimpleComp {
if (color != null) { if (color != null) {
c.getStyleClass().add(color.getId()); c.getStyleClass().add(color.getId());
} }
new TooltipAugment<>(new SimpleStringProperty(model.getTooltip())).augment(c); new TooltipAugment<>(new SimpleStringProperty(model.getTooltip()), null).augment(c);
c.addEventHandler( c.addEventHandler(
DragEvent.DRAG_ENTERED, DragEvent.DRAG_ENTERED,
mouseEvent -> Platform.runLater( mouseEvent -> Platform.runLater(

View file

@ -70,8 +70,8 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
for (int i = 0; i < entries.size(); i++) { for (int i = 0; i < entries.size(); i++) {
var e = entries.get(i); var e = entries.get(i);
var b = new IconButtonComp(e.icon(), () -> value.setValue(e)); var b = new IconButtonComp(e.icon(), () -> value.setValue(e));
b.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + i])); var shortcut = new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + i]);
b.apply(new TooltipAugment<>(e.name())); b.apply(new TooltipAugment<>(e.name(), shortcut));
b.apply(struc -> { b.apply(struc -> {
AppFont.setSize(struc.get(), 2); AppFont.setSize(struc.get(), 2);
struc.get().pseudoClassStateChanged(selected, value.getValue().equals(e)); struc.get().pseudoClassStateChanged(selected, value.getValue().equals(e));
@ -123,9 +123,9 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
}; };
{ {
var shortcut = new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size()]);
var b = new IconButtonComp("mdi2g-github", () -> Hyperlinks.open(Hyperlinks.GITHUB)) var b = new IconButtonComp("mdi2g-github", () -> Hyperlinks.open(Hyperlinks.GITHUB))
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size()])) .tooltipKey("visitGithubRepository", shortcut)
.apply(new TooltipAugment<>("visitGithubRepository"))
.apply(simpleBorders) .apply(simpleBorders)
.accessibleTextKey("visitGithubRepository"); .accessibleTextKey("visitGithubRepository");
b.apply(struc -> { b.apply(struc -> {
@ -135,9 +135,9 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
} }
{ {
var shortcut = new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 1]);
var b = new IconButtonComp("mdi2d-discord", () -> Hyperlinks.open(Hyperlinks.DISCORD)) var b = new IconButtonComp("mdi2d-discord", () -> Hyperlinks.open(Hyperlinks.DISCORD))
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 1])) .tooltipKey("discord", shortcut)
.apply(new TooltipAugment<>("discord"))
.apply(simpleBorders) .apply(simpleBorders)
.accessibleTextKey("discord"); .accessibleTextKey("discord");
b.apply(struc -> { b.apply(struc -> {
@ -147,9 +147,9 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
} }
{ {
var shortcut = new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 2]);
var b = new IconButtonComp("mdi2t-translate", () -> Hyperlinks.open(Hyperlinks.TRANSLATE)) var b = new IconButtonComp("mdi2t-translate", () -> Hyperlinks.open(Hyperlinks.TRANSLATE))
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 2])) .tooltipKey("translate", shortcut)
.apply(new TooltipAugment<>("translate"))
.apply(simpleBorders) .apply(simpleBorders)
.accessibleTextKey("translate"); .accessibleTextKey("translate");
b.apply(struc -> { b.apply(struc -> {
@ -160,7 +160,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
{ {
var b = new IconButtonComp("mdi2u-update", () -> UpdateAvailableAlert.showIfNeeded()) var b = new IconButtonComp("mdi2u-update", () -> UpdateAvailableAlert.showIfNeeded())
.apply(new TooltipAugment<>("updateAvailableTooltip")) .tooltipKey("updateAvailableTooltip")
.accessibleTextKey("updateAvailableTooltip"); .accessibleTextKey("updateAvailableTooltip");
b.apply(struc -> { b.apply(struc -> {
AppFont.setSize(struc.get(), 2); AppFont.setSize(struc.get(), 2);

View file

@ -190,7 +190,7 @@ public abstract class StoreEntryComp extends SimpleComp {
var imageComp = PrettyImageHelper.ofFixedSize(img, w, h); var imageComp = PrettyImageHelper.ofFixedSize(img, w, h);
var storeIcon = imageComp.createRegion(); var storeIcon = imageComp.createRegion();
if (wrapper.getValidity().getValue().isUsable()) { if (wrapper.getValidity().getValue().isUsable()) {
new TooltipAugment<>(wrapper.getEntry().getProvider().displayName()).augment(storeIcon); new TooltipAugment<>(wrapper.getEntry().getProvider().displayName(), null).augment(storeIcon);
} }
var stack = new StackPane(storeIcon); var stack = new StackPane(storeIcon);
@ -227,7 +227,7 @@ public abstract class StoreEntryComp extends SimpleComp {
button.accessibleText( button.accessibleText(
actionProvider.getName(wrapper.getEntry().ref()).getValue()); actionProvider.getName(wrapper.getEntry().ref()).getValue());
button.apply(new TooltipAugment<>( button.apply(new TooltipAugment<>(
actionProvider.getName(wrapper.getEntry().ref()))); actionProvider.getName(wrapper.getEntry().ref()), null));
if (actionProvider.activeType() == ActionProvider.DataStoreCallSite.ActiveType.ONLY_SHOW_IF_ENABLED) { if (actionProvider.activeType() == ActionProvider.DataStoreCallSite.ActiveType.ONLY_SHOW_IF_ENABLED) {
button.hide(Bindings.not(p.getValue())); button.hide(Bindings.not(p.getValue()));
} else if (actionProvider.activeType() == ActionProvider.DataStoreCallSite.ActiveType.ALWAYS_SHOW) { } else if (actionProvider.activeType() == ActionProvider.DataStoreCallSite.ActiveType.ALWAYS_SHOW) {
@ -266,7 +266,7 @@ public abstract class StoreEntryComp extends SimpleComp {
event -> event.getButton() == MouseButton.PRIMARY, event -> event.getButton() == MouseButton.PRIMARY,
null, null,
() -> StoreEntryComp.this.createContextMenu())); () -> StoreEntryComp.this.createContextMenu()));
settingsButton.apply(new TooltipAugment<>("more")); settingsButton.tooltipKey("more");
return settingsButton; return settingsButton;
} }

View file

@ -7,12 +7,10 @@ import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.FilterComp; import io.xpipe.app.fxcomps.impl.FilterComp;
import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.ListBindingsHelper; import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.OsType; import io.xpipe.core.process.OsType;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
@ -21,15 +19,11 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.MenuButton; import javafx.scene.control.MenuButton;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.text.TextAlignment; import javafx.scene.text.TextAlignment;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
public class StoreEntryListStatusComp extends SimpleComp { public class StoreEntryListStatusComp extends SimpleComp {
@ -105,9 +99,6 @@ public class StoreEntryListStatusComp extends SimpleComp {
}); });
}); });
var filter = new FilterComp(StoreViewState.get().getFilterString()); var filter = new FilterComp(StoreViewState.get().getFilterString());
filter.shortcut(new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN), s -> {
s.getText().requestFocus();
});
filter.apply(struc -> struc.get().sceneProperty().addListener((observable, oldValue, newValue) -> { filter.apply(struc -> struc.get().sceneProperty().addListener((observable, oldValue, newValue) -> {
if (newValue != null) { if (newValue != null) {
struc.getText().requestFocus(); struc.getText().requestFocus();
@ -186,7 +177,7 @@ public class StoreEntryListStatusComp extends SimpleComp {
sortMode)); sortMode));
}); });
alphabetical.accessibleTextKey("sortAlphabetical"); alphabetical.accessibleTextKey("sortAlphabetical");
alphabetical.apply(new TooltipAugment<>("sortAlphabetical")); alphabetical.tooltipKey("sortAlphabetical");
return alphabetical; return alphabetical;
} }
@ -225,7 +216,7 @@ public class StoreEntryListStatusComp extends SimpleComp {
sortMode)); sortMode));
}); });
date.accessibleTextKey("sortLastUsed"); date.accessibleTextKey("sortLastUsed");
date.apply(new TooltipAugment<>("sortLastUsed")); date.tooltipKey("sortLastUsed");
return date; return date;
} }

View file

@ -5,6 +5,7 @@ import io.xpipe.app.core.AppActionLinkDetector;
import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.util.InputHelper;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyCombination;
@ -12,12 +13,6 @@ import javafx.scene.layout.Region;
public class StoreLayoutComp extends SimpleComp { public class StoreLayoutComp extends SimpleComp {
public StoreLayoutComp() {
shortcut(new KeyCodeCombination(KeyCode.V, KeyCombination.SHORTCUT_DOWN), structure -> {
AppActionLinkDetector.detectOnPaste();
});
}
@Override @Override
protected Region createSimple() { protected Region createSimple() {
var struc = new SideSplitPaneComp(new StoreSidebarComp(), new StoreEntryListComp()) var struc = new SideSplitPaneComp(new StoreSidebarComp(), new StoreEntryListComp())
@ -29,6 +24,10 @@ public class StoreLayoutComp extends SimpleComp {
struc.getLeft().setMinWidth(260); struc.getLeft().setMinWidth(260);
struc.getLeft().setMaxWidth(500); struc.getLeft().setMaxWidth(500);
struc.get().getStyleClass().add("store-layout"); struc.get().getStyleClass().add("store-layout");
InputHelper.onKeyCombination(struc.get(),new KeyCodeCombination(KeyCode.V, KeyCombination.SHORTCUT_DOWN), true, keyEvent -> {
AppActionLinkDetector.detectOnPaste();
keyEvent.consume();
});
return struc.get(); return struc.get();
} }
} }

View file

@ -63,8 +63,7 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
section.getWrapper().getName())) section.getWrapper().getName()))
.disable(quickAccessDisabled) .disable(quickAccessDisabled)
.focusTraversableForAccessibility() .focusTraversableForAccessibility()
.displayOnlyShortcut(new KeyCodeCombination(KeyCode.RIGHT)) .tooltipKey("accessSubConnections", new KeyCodeCombination(KeyCode.RIGHT));
.tooltipKey("accessSubConnections");
return quickAccessButton; return quickAccessButton;
} }
@ -84,8 +83,7 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
.apply(struc -> struc.get().setMinWidth(30)) .apply(struc -> struc.get().setMinWidth(30))
.apply(struc -> struc.get().setPrefWidth(30)) .apply(struc -> struc.get().setPrefWidth(30))
.focusTraversableForAccessibility() .focusTraversableForAccessibility()
.displayOnlyShortcut(new KeyCodeCombination(KeyCode.SPACE)) .tooltipKey("expand", new KeyCodeCombination(KeyCode.SPACE))
.tooltipKey("expand")
.accessibleText(Bindings.createStringBinding( .accessibleText(Bindings.createStringBinding(
() -> { () -> {
return "Expand " + section.getWrapper().getName().getValue(); return "Expand " + section.getWrapper().getName().getValue();

View file

@ -7,7 +7,6 @@ import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.prefs.CloseBehaviourAlert; import io.xpipe.app.prefs.CloseBehaviourAlert;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.OsType; import io.xpipe.core.process.OsType;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Rectangle2D; import javafx.geometry.Rectangle2D;
@ -17,18 +16,17 @@ import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.stage.Screen; import javafx.stage.Screen;
import javafx.stage.Stage; import javafx.stage.Stage;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import lombok.Value; import lombok.Value;
import lombok.extern.jackson.Jacksonized; import lombok.extern.jackson.Jacksonized;
import javax.imageio.ImageIO;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import javax.imageio.ImageIO;
public class AppMainWindow { public class AppMainWindow {
@ -157,6 +155,13 @@ public class AppMainWindow {
e.consume(); e.consume();
}); });
stage.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode().equals(KeyCode.Q) && event.isShortcutDown()) {
stage.close();
event.consume();
}
});
TrackEvent.debug("Window listeners added"); TrackEvent.debug("Window listeners added");
} }

View file

@ -161,12 +161,10 @@ public class AppWindowHelper {
event.consume(); event.consume();
}); });
a.getDialogPane().getScene().addEventFilter(KeyEvent.KEY_PRESSED, event -> { a.getDialogPane().getScene().addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (OsType.getLocal().equals(OsType.LINUX) || OsType.getLocal().equals(OsType.MACOS)) { if (event.getCode().equals(KeyCode.W) && event.isShortcutDown()) {
if (event.getCode().equals(KeyCode.W) && event.isShortcutDown()) { s.close();
s.close(); event.consume();
event.consume(); return;
return;
}
} }
if (event.getCode().equals(KeyCode.ESCAPE)) { if (event.getCode().equals(KeyCode.ESCAPE)) {
@ -274,11 +272,9 @@ public class AppWindowHelper {
}); });
scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> { scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (OsType.getLocal().equals(OsType.LINUX) || OsType.getLocal().equals(OsType.MACOS)) { if (event.getCode().equals(KeyCode.W) && event.isShortcutDown()) {
if (event.getCode().equals(KeyCode.W) && event.isShortcutDown()) { stage.close();
stage.close(); event.consume();
event.consume();
}
} }
}); });
} }

View file

@ -1,18 +1,16 @@
package io.xpipe.app.fxcomps; package io.xpipe.app.fxcomps;
import atlantafx.base.controls.Spacer;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.augment.Augment; import io.xpipe.app.fxcomps.augment.Augment;
import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.impl.TooltipAugment; import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.Shortcuts;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Orientation; import javafx.geometry.Orientation;
import javafx.scene.control.ButtonBase;
import javafx.scene.control.Separator; import javafx.scene.control.Separator;
import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyCombination;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
@ -20,11 +18,8 @@ import javafx.scene.layout.Priority;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import atlantafx.base.controls.Spacer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
@ -182,24 +177,16 @@ public abstract class Comp<S extends CompStructure<?>> {
return apply(GrowAugment.create(width, height)); return apply(GrowAugment.create(width, height));
} }
public Comp<S> shortcut(KeyCombination shortcut, Consumer<S> con) {
return apply(struc -> Shortcuts.addShortcut(struc.get(), shortcut, r -> con.accept(struc)));
}
public Comp<S> shortcut(KeyCombination shortcut) {
return apply(struc -> Shortcuts.addShortcut((ButtonBase) struc.get(), shortcut));
}
public Comp<S> displayOnlyShortcut(KeyCombination shortcut) {
return apply(struc -> Shortcuts.addDisplayShortcut(struc.get(), shortcut));
}
public Comp<S> tooltip(ObservableValue<String> text) { public Comp<S> tooltip(ObservableValue<String> text) {
return apply(new TooltipAugment<>(text)); return apply(new TooltipAugment<>(text, null));
} }
public Comp<S> tooltipKey(String key) { public Comp<S> tooltipKey(String key) {
return apply(new TooltipAugment<>(key)); return apply(new TooltipAugment<>(key, null));
}
public Comp<S> tooltipKey(String key, KeyCombination shortcut) {
return apply(new TooltipAugment<>(key, shortcut));
} }
public Region createRegion() { public Region createRegion() {

View file

@ -42,7 +42,7 @@ public class ContextMenuAugment<S extends CompStructure<?>> implements Augment<S
}; };
var r = struc.get(); var r = struc.get();
r.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> { r.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
if (mouseEventCheck != null && mouseEventCheck.test(event)) { if (mouseEventCheck != null && mouseEventCheck.test(event)) {
if (!hide.get()) { if (!hide.get()) {
var cm = contextMenu.get(); var cm = contextMenu.get();
@ -55,18 +55,18 @@ public class ContextMenuAugment<S extends CompStructure<?>> implements Augment<S
event.consume(); event.consume();
} }
}); });
r.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> { r.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
if (mouseEventCheck != null && mouseEventCheck.test(event)) { if (mouseEventCheck != null && mouseEventCheck.test(event)) {
event.consume(); event.consume();
} }
}); });
r.addEventFilter(KeyEvent.KEY_RELEASED, event -> { r.addEventHandler(KeyEvent.KEY_RELEASED, event -> {
if (keyEventCheck != null && keyEventCheck.test(event)) { if (keyEventCheck != null && keyEventCheck.test(event)) {
event.consume(); event.consume();
} }
}); });
r.addEventFilter(KeyEvent.KEY_PRESSED, event -> { r.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
if (keyEventCheck != null && keyEventCheck.test(event)) { if (keyEventCheck != null && keyEventCheck.test(event)) {
if (!hide.get()) { if (!hide.get()) {
var cm = contextMenu.get(); var cm = contextMenu.get();
@ -80,7 +80,7 @@ public class ContextMenuAugment<S extends CompStructure<?>> implements Augment<S
}); });
if (r instanceof ButtonBase buttonBase && keyEventCheck == null) { if (r instanceof ButtonBase buttonBase && keyEventCheck == null) {
buttonBase.addEventFilter(ActionEvent.ACTION, event -> { buttonBase.addEventHandler(ActionEvent.ACTION, event -> {
if (buttonBase.getOnAction() != null) { if (buttonBase.getOnAction() != null) {
return; return;
} }

View file

@ -124,7 +124,7 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
ErrorEvent.fromThrowable(e).handle(); ErrorEvent.fromThrowable(e).handle();
} }
}); });
gitShareButton.apply(new TooltipAugment<>("gitShareFileTooltip")); gitShareButton.tooltipKey("gitShareFileTooltip");
gitShareButton.styleClass(Styles.RIGHT_PILL).grow(false, true); gitShareButton.styleClass(Styles.RIGHT_PILL).grow(false, true);
var layout = new HorizontalComp(List.of(fileNameComp, fileBrowseButton, gitShareButton)) var layout = new HorizontalComp(List.of(fileNameComp, fileBrowseButton, gitShareButton))

View file

@ -4,47 +4,46 @@ import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.augment.Augment; import io.xpipe.app.fxcomps.augment.Augment;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.Shortcuts;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.scene.control.Tooltip; import javafx.scene.control.Tooltip;
import javafx.scene.input.KeyCombination;
import javafx.stage.Window; import javafx.stage.Window;
public class TooltipAugment<S extends CompStructure<?>> implements Augment<S> { public class TooltipAugment<S extends CompStructure<?>> implements Augment<S> {
private final ObservableValue<String> text; private final ObservableValue<String> text;
private final KeyCombination shortcut;
public TooltipAugment(ObservableValue<String> text) { public TooltipAugment(ObservableValue<String> text, KeyCombination shortcut) {
this.text = PlatformThread.sync(text); this.text = text;
this.shortcut = shortcut;
} }
public TooltipAugment(String key) { public TooltipAugment(String key, KeyCombination shortcut) {
this.text = AppI18n.observable(key); this.text = AppI18n.observable(key);
this.shortcut = shortcut;
} }
@Override @Override
public void augment(S struc) { public void augment(S struc) {
var region = struc.get();
var tt = new FixedTooltip(); var tt = new FixedTooltip();
if (Shortcuts.getDisplayShortcut(region) != null) { if (shortcut != null) {
var s = AppI18n.observable("shortcut"); var s = AppI18n.observable("shortcut");
var binding = Bindings.createStringBinding( var binding = Bindings.createStringBinding(
() -> { () -> {
return text.getValue() + "\n\n" + s.getValue() + ": " return text.getValue() + "\n\n" + s.getValue() + ": " + shortcut.getDisplayText();
+ Shortcuts.getDisplayShortcut(region).getDisplayText();
}, },
text, text,
s); s);
tt.textProperty().bind(binding); tt.textProperty().bind(PlatformThread.sync(binding));
} else { } else {
tt.textProperty().bind(text); tt.textProperty().bind(PlatformThread.sync(text));
} }
tt.setStyle("-fx-font-size: 11pt;"); tt.setStyle("-fx-font-size: 11pt;");
tt.setWrapText(true); tt.setWrapText(true);
tt.setMaxWidth(400); tt.setMaxWidth(400);
tt.getStyleClass().add("fancy-tooltip"); tt.getStyleClass().add("fancy-tooltip");
Tooltip.install(struc.get(), tt); Tooltip.install(struc.get(), tt);
} }

View file

@ -1,66 +0,0 @@
package io.xpipe.app.fxcomps.util;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.ButtonBase;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Region;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
public class Shortcuts {
private static final Map<Region, KeyCombination> DISPLAY_SHORTCUTS = new HashMap<>();
public static void addDisplayShortcut(Region region, KeyCombination comb) {
DISPLAY_SHORTCUTS.put(region, comb);
}
public static <T extends ButtonBase> void addShortcut(T region, KeyCombination comb) {
addShortcut(region, comb, ButtonBase::fire);
}
public static <T extends Region> void addShortcut(T region, KeyCombination comb, Consumer<T> exec) {
var filter = new EventHandler<KeyEvent>() {
public void handle(KeyEvent ke) {
if (!region.isVisible() || !region.isManaged() || region.isDisabled()) {
return;
}
if (comb.match(ke)) {
exec.accept(region);
ke.consume();
}
}
};
DISPLAY_SHORTCUTS.put(region, comb);
AtomicReference<Scene> scene = new AtomicReference<>();
region.sceneProperty().subscribe(s -> {
if (Objects.equals(s, scene.get())) {
return;
}
if (scene.get() != null) {
scene.get().removeEventHandler(KeyEvent.KEY_PRESSED, filter);
DISPLAY_SHORTCUTS.remove(region);
scene.set(null);
}
if (s != null) {
scene.set(s);
DISPLAY_SHORTCUTS.put(region, comb);
s.addEventHandler(KeyEvent.KEY_PRESSED, filter);
}
});
}
public static KeyCombination getDisplayShortcut(Region region) {
return DISPLAY_SHORTCUTS.get(region);
}
}

View file

@ -3,6 +3,7 @@ package io.xpipe.app.util;
import javafx.event.EventHandler; import javafx.event.EventHandler;
import javafx.event.EventTarget; import javafx.event.EventTarget;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent; import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent; import javafx.scene.input.MouseEvent;
@ -11,9 +12,9 @@ import java.util.function.Consumer;
public class InputHelper { public class InputHelper {
public static void onLeft(EventTarget target, boolean filter, Consumer<KeyEvent> r) { public static void onKeyCombination(EventTarget target, KeyCombination c, boolean filter, Consumer<KeyEvent> r) {
EventHandler<KeyEvent> keyEventEventHandler = event -> { EventHandler<KeyEvent> keyEventEventHandler = event -> {
if (event.getCode() == KeyCode.LEFT || event.getCode() == KeyCode.NUMPAD4) { if (c.match(event)) {
r.accept(event); r.accept(event);
} }
}; };
@ -24,9 +25,13 @@ public class InputHelper {
} }
} }
public static void onRight(EventTarget target, boolean filter, Consumer<KeyEvent> r) { public static void onExactKeyCode(EventTarget target, KeyCode code, boolean filter, Consumer<KeyEvent> r) {
EventHandler<KeyEvent> keyEventEventHandler = event -> { EventHandler<KeyEvent> keyEventEventHandler = event -> {
if (event.getCode() == KeyCode.RIGHT || event.getCode() == KeyCode.NUMPAD6) { if (event.isAltDown() || event.isShiftDown() || event.isShortcutDown()) {
return;
}
if (code == event.getCode()) {
r.accept(event); r.accept(event);
} }
}; };
@ -37,6 +42,43 @@ public class InputHelper {
} }
} }
public static void onInput(EventTarget target, boolean filter, Consumer<KeyEvent> r) {
EventHandler<KeyEvent> keyEventEventHandler = event -> {
r.accept(event);
};
if (filter) {
target.addEventFilter(KeyEvent.KEY_PRESSED, keyEventEventHandler);
} else {
target.addEventHandler(KeyEvent.KEY_PRESSED, keyEventEventHandler);
}
}
public static void onLeft(EventTarget target, boolean filter, Consumer<KeyEvent> r) {
EventHandler<KeyEvent> e = event -> {
if (event.getCode() == KeyCode.LEFT || event.getCode() == KeyCode.NUMPAD4) {
r.accept(event);
}
};
if (filter) {
target.addEventFilter(KeyEvent.KEY_PRESSED, e);
} else {
target.addEventHandler(KeyEvent.KEY_PRESSED, e);
}
}
public static void onRight(EventTarget target, boolean filter, Consumer<KeyEvent> r) {
EventHandler<KeyEvent> e = event -> {
if (event.getCode() == KeyCode.RIGHT || event.getCode() == KeyCode.NUMPAD6) {
r.accept(event);
}
};
if (filter) {
target.addEventFilter(KeyEvent.KEY_PRESSED, e);
} else {
target.addEventHandler(KeyEvent.KEY_PRESSED, e);
}
}
public static void onNavigationInput(EventTarget target, Consumer<Boolean> r) { public static void onNavigationInput(EventTarget target, Consumer<Boolean> r) {
target.addEventFilter(KeyEvent.KEY_PRESSED, event -> { target.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
var c = event.getCode(); var c = event.getCode();

View file

@ -79,7 +79,11 @@
-fx-background-color: transparent; -fx-background-color: transparent;
} }
.browser .top-bar > .button:hover { .browser .top-bar .button:hover, .root:key-navigation .browser .top-bar .button:focused {
-fx-background-color: -color-accent-subtle;
}
.browser .top-bar .menu-button:hover, .root:key-navigation .browser .top-bar .menu-button:focused {
-fx-background-color: -color-accent-subtle; -fx-background-color: -color-accent-subtle;
} }
@ -182,10 +186,14 @@
-fx-background-color: -color-accent-subtle; -fx-background-color: -color-accent-subtle;
} }
.browser .table-row-cell:selected, .browser .table-row-cell:hover:selected { .browser .table-row-cell:selected, .browser .table-row-cell:hover:selected, .root:key-navigation .browser .table-row-cell:focused:selected {
-fx-background-color: -color-success-subtle; -fx-background-color: -color-success-subtle;
} }
.root:key-navigation .browser .table-row-cell:focused {
-fx-background-color: -color-warning-subtle;
}
.root.nord .browser .table-row-cell:selected, .root.nord .browser .table-row-cell:hover:selected { .root.nord .browser .table-row-cell:selected, .root.nord .browser .table-row-cell:hover:selected {
-fx-background-color: -color-success-7; -fx-background-color: -color-success-7;
} }

View file

@ -11,12 +11,20 @@ import io.xpipe.core.store.FileKind;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List; import java.util.List;
public class EditFileAction implements LeafAction { public class EditFileAction implements LeafAction {
@Override
public KeyCombination getShortcut() {
return new KeyCodeCombination(KeyCode.E, KeyCombination.SHORTCUT_DOWN);
}
@Override @Override
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) { public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) {
for (BrowserEntry entry : entries) { for (BrowserEntry entry : entries) {

View file

@ -10,6 +10,9 @@ import io.xpipe.core.store.FileKind;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List; import java.util.List;
@ -34,6 +37,11 @@ public class OpenDirectoryInNewTabAction implements LeafAction {
return Category.OPEN; return Category.OPEN;
} }
@Override
public KeyCombination getShortcut() {
return new KeyCodeCombination(KeyCode.ENTER, KeyCombination.SHIFT_DOWN);
}
@Override @Override
public boolean acceptsEmptySelection() { public boolean acceptsEmptySelection() {
return true; return true;

View file

@ -10,6 +10,9 @@ import io.xpipe.core.process.ShellControl;
import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileNames;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import java.util.List; import java.util.List;
@ -62,6 +65,11 @@ public class OpenNativeFileDetailsAction implements LeafAction {
} }
} }
@Override
public KeyCombination getShortcut() {
return new KeyCodeCombination(KeyCode.ENTER,KeyCombination.ALT_DOWN);
}
@Override @Override
public Category getCategory() { public Category getCategory() {
return Category.NATIVE; return Category.NATIVE;