Accessibility improvements

This commit is contained in:
crschnick 2023-06-03 13:51:20 +00:00
parent 544597c267
commit fcc47b9038
24 changed files with 112 additions and 24 deletions

View file

@ -6,6 +6,7 @@ import io.xpipe.app.comp.storage.store.StoreViewState;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.impl.PrettyImageComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.ShellStore;
import javafx.application.Platform;
@ -13,6 +14,7 @@ import javafx.beans.property.*;
import javafx.collections.SetChangeListener;
import javafx.css.PseudoClass;
import javafx.geometry.Point2D;
import javafx.scene.AccessibleRole;
import javafx.scene.Node;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
@ -47,7 +49,7 @@ final class BrowserBookmarkList extends SimpleComp {
return new StoreCell();
});
model.getSelected().addListener((observable, oldValue, newValue) -> {
PlatformThread.sync(model.getSelected()).addListener((observable, oldValue, newValue) -> {
if (newValue == null) {
view.getSelectionModel().clearSelection();
return;
@ -95,6 +97,7 @@ final class BrowserBookmarkList extends SimpleComp {
private StoreCell() {
disableProperty().bind(busy);
setAccessibleRole(AccessibleRole.BUTTON);
setGraphic(imageView);
addEventHandler(DragEvent.DRAG_OVER, mouseEvent -> {
if (getItem() == null) {
@ -131,7 +134,8 @@ final class BrowserBookmarkList extends SimpleComp {
})
.apply(struc -> struc.get().setPrefWidth(25))
.grow(false, true)
.styleClass("expand-button");
.styleClass("expand-button")
.apply(struc -> struc.get().setFocusTraversable(false));
setDisclosureNode(button.createRegion());
}
@ -145,12 +149,16 @@ final class BrowserBookmarkList extends SimpleComp {
// and cells are emptied on each change, leading to unnecessary changes
// img.set(null);
setGraphic(null);
setFocusTraversable(false);
setAccessibleText(null);
} else {
setText(item.getName());
img.set(item.getEntry()
.getProvider()
.getDisplayIconFileName(item.getEntry().getStore()));
setGraphic(imageView);
setFocusTraversable(true);
setAccessibleText(item.getName() + " " + item.getEntry().getProvider().getDisplayName());
}
}
}

View file

@ -165,7 +165,8 @@ public class BrowserComp extends SimpleComp {
// Handle selection from model
model.getSelected().addListener((observable, oldValue, newValue) -> {
PlatformThread.runLaterIfNeeded(() -> {
tabs.getSelectionModel().select(model.getOpenFileSystems().indexOf(newValue));
var tab = tabs.getTabs().get(model.getOpenFileSystems().indexOf(newValue));
tabs.getSelectionModel().select(tab);
});
});
@ -258,6 +259,7 @@ public class BrowserComp extends SimpleComp {
new FancyTooltipAugment<>(new SimpleStringProperty(model.getName())).augment(label);
GrowAugment.create(true, false).augment(new SimpleCompStructure<>(label));
tab.setContent(new OpenFileSystemComp(model).createSimple());
tab.setText(model.getName());
return tab;
}
}

View file

@ -26,6 +26,7 @@ import javafx.collections.ListChangeListener;
import javafx.css.PseudoClass;
import javafx.geometry.Bounds;
import javafx.geometry.Pos;
import javafx.scene.AccessibleRole;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.control.skin.TableViewSkin;
@ -100,10 +101,12 @@ final class BrowserFileListComp extends SimpleComp {
modeCol.setSortable(false);
var table = new TableView<BrowserEntry>();
table.setAccessibleText("Directory contents");
table.setPlaceholder(new Region());
table.getStyleClass().add(Styles.STRIPED);
table.getColumns().setAll(filenameCol, sizeCol, modeCol, mtimeCol);
table.getSortOrder().add(filenameCol);
table.setFocusTraversable(true);
table.setSortPolicy(param -> {
var comp = table.getComparator();
if (comp == null) {
@ -229,6 +232,12 @@ final class BrowserFileListComp extends SimpleComp {
table.setRowFactory(param -> {
TableRow<BrowserEntry> row = new TableRow<>();
row.accessibleTextProperty().bind(Bindings.createStringBinding(() -> {
return row.getItem() != null ? row.getItem().getFileName() : null;
}, row.itemProperty()));
row.focusTraversableProperty().bind(Bindings.createBooleanBinding(() -> {
return row.getItem() != null;
}, row.itemProperty()));
new ContextMenuAugment<>(event -> {
if (row.getItem() == null) {
return event.getButton() == MouseButton.SECONDARY;
@ -405,6 +414,11 @@ final class BrowserFileListComp extends SimpleComp {
private final BooleanProperty updating = new SimpleBooleanProperty();
public FilenameCell(Property<BrowserEntry> editing) {
accessibleTextProperty().bind(Bindings.createStringBinding(() -> {
return getItem() != null ? getItem() : null;
}, itemProperty()));
setAccessibleRole(AccessibleRole.TEXT);
editing.addListener((observable, oldValue, newValue) -> {
if (getTableRow().getItem() != null && getTableRow().getItem().equals(newValue)) {
PlatformThread.runLaterIfNeeded(() -> textField.requestFocus());

View file

@ -86,7 +86,7 @@ public class BrowserFileListCompEntry {
}
// Prevent dropping items onto themselves
if (item != null && BrowserClipboard.currentDragClipboard.getEntries().contains(item)) {
if (item != null && BrowserClipboard.currentDragClipboard.getEntries().contains(item.getRawFileEntry())) {
return false;
}

View file

@ -71,7 +71,7 @@ public class BrowserNavBar extends SimpleComp {
struc.get().setPromptText("Overview of " + model.getName());
}).shortcut(new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN), s -> {
s.get().requestFocus();
});
}).accessibleText("Current path");
var graphic = Bindings.createStringBinding(
() -> {
@ -87,6 +87,7 @@ public class BrowserNavBar extends SimpleComp {
.createRegion();
var graphicButton = new Button(null, breadcrumbsGraphic);
graphicButton.setAccessibleText("Directory options");
graphicButton.getStyleClass().add(Styles.LEFT_PILL);
graphicButton.getStyleClass().add("path-graphic-button");
new ContextMenuAugment<>(

View file

@ -45,6 +45,7 @@ public class OpenFileSystemComp extends SimpleComp {
var overview = new Button(null, new FontIcon("mdi2m-monitor"));
overview.setOnAction(e -> model.cd(null));
overview.disableProperty().bind(model.getInOverview());
overview.setAccessibleText("System overview");
var backBtn = BrowserAction.byId("back").toButton(model, List.of());
var forthBtn = BrowserAction.byId("forward").toButton(model, List.of());
@ -56,6 +57,7 @@ public class OpenFileSystemComp extends SimpleComp {
event -> event.getButton() == MouseButton.PRIMARY, () -> new BrowserContextMenu(model, null))
.augment(new SimpleCompStructure<>(menuButton));
menuButton.disableProperty().bind(model.getInOverview());
menuButton.setAccessibleText("Directory options");
var filter = new BrowserFilterComp(model, model.getFilter()).createStructure();
Shortcuts.addShortcut(filter.toggleButton(), new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN));

View file

@ -36,6 +36,7 @@ public interface LeafAction extends BrowserAction {
b.setGraphic(graphic);
}
b.setMnemonicParsing(false);
b.setAccessibleText(getName(model, selected));
b.setDisable(!isActive(model, selected));
model.getCurrentPath().addListener((observable, oldValue, newValue) -> {

View file

@ -34,6 +34,7 @@ public class BigIconButton extends ButtonComp {
vbox.getChildren().add(label);
var b = new Button(null);
b.accessibleTextProperty().bind(getName());
b.setGraphic(vbox);
b.setOnAction(e -> getListener().run());
b.getStyleClass().add("big-icon-button-comp");

View file

@ -68,7 +68,7 @@ public class DataSourceTargetChoiceComp extends Comp<CompStructure<ComboBox<Node
var addMoreLabel = new Label(AppI18n.get("addMore"), new FontIcon("mdmz-plus"));
var builder = new CustomComboBoxBuilder<DataSourceTarget>(
selectedApplication, app -> createLabel(app), new Label(""), v -> true);
selectedApplication, app -> createLabel(app), dataSourceTarget -> dataSourceTarget.getName().getValue(), new Label(""), v -> true);
// builder.addFilter((v, s) -> v.getName().getValue().toLowerCase().contains(s));

View file

@ -72,7 +72,7 @@ public class DsProviderChoiceComp extends Comp<CompStructure<ComboBox<Node>>> im
@Override
public CompStructure<ComboBox<Node>> createBase() {
var comboBox = new CustomComboBoxBuilder<>(provider, this::createGraphic, createDefaultNode(), v -> true);
var comboBox = new CustomComboBoxBuilder<>(provider, this::createGraphic, dataSourceProvider -> dataSourceProvider.getDisplayName(), createDefaultNode(), v -> true);
comboBox.add(null);
comboBox.addSeparator();
comboBox.addFilter((v, s) -> v.getDisplayName().toLowerCase().contains(s.toLowerCase()));

View file

@ -30,7 +30,7 @@ public class DsStorageGroupSelector extends SimpleComp {
@Override
protected ComboBox<Node> createSimple() {
var comboBox = new CustomComboBoxBuilder<DataSourceCollection>(
selected, DsStorageGroupSelector::createGraphic, createGraphic(null), v -> true);
selected, DsStorageGroupSelector::createGraphic, dataSourceCollection -> dataSourceCollection.getName(), createGraphic(null), v -> true);
DataStorage.get().getSourceCollections().stream()
.filter(dataSourceCollection ->

View file

@ -50,7 +50,7 @@ public class DsTypeChoiceComp extends Comp<CompStructure<StackPane>> {
return;
}
var builder = new CustomComboBoxBuilder<>(selectedType, app -> createLabel(app), new Label(""), v -> true);
var builder = new CustomComboBoxBuilder<>(selectedType, app -> createLabel(app), dataSourceType -> dataSourceType.toString(), new Label(""), v -> true);
builder.add(provider.getValue().getPrimaryType());
var list = Arrays.stream(DataSourceType.values())

View file

@ -49,7 +49,7 @@ public class DsStoreProviderChoiceComp extends Comp<CompStructure<ComboBox<Node>
@Override
public CompStructure<ComboBox<Node>> createBase() {
var comboBox = new CustomComboBoxBuilder<>(provider, this::createGraphic, createDefaultNode(), v -> true);
var comboBox = new CustomComboBoxBuilder<>(provider, this::createGraphic, dataStoreProvider -> dataStoreProvider.getDisplayName(), createDefaultNode(), v -> true);
getProviders().stream()
.filter(p -> AppPrefs.get().developerShowHiddenProviders().get() || p.shouldShow())
.forEach(comboBox::add);

View file

@ -148,7 +148,11 @@ public class StoreEntryComp extends SimpleComp {
GrowAugment.create(true, false).augment(new SimpleCompStructure<>(grid));
button.getStyleClass().add("store-entry-comp");
button.setMaxWidth(2000);
button.setFocusTraversable(false);
button.setFocusTraversable(true);
button.accessibleTextProperty().bind(Bindings.createStringBinding(() -> {
return entry.getName();
}, entry.nameProperty()));
button.accessibleHelpProperty().bind(entry.getInformation());
button.setOnAction(event -> {
event.consume();
ThreadHelper.runFailableAsync(() -> {
@ -214,6 +218,7 @@ public class StoreEntryComp extends SimpleComp {
private Comp<?> createSettingsButton() {
var settingsButton = new IconButtonComp("mdomz-settings");
settingsButton.styleClass("settings");
settingsButton.accessibleText("Settings");
settingsButton.apply(new ContextMenuAugment<>(event -> event.getButton() == MouseButton.PRIMARY, () -> StoreEntryComp.this.createContextMenu()));
settingsButton.apply(GrowAugment.create(false, true));
settingsButton.apply(s -> {

View file

@ -36,6 +36,8 @@ public class StoreEntrySection extends Comp<CompStructure<VBox>> {
section.getWrapper().toggleExpanded();
})
.apply(struc -> struc.get().setPrefWidth(40))
.focusTraversable()
.accessibleText("Expand")
.disable(BindingsHelper.persist(
Bindings.size(section.getChildren()).isEqualTo(0)))
.grow(false, true).styleClass("expand-button");

View file

@ -59,6 +59,14 @@ public abstract class Comp<S extends CompStructure<?>> {
return apply(struc -> VBox.setVgrow(struc.get(), Priority.ALWAYS));
}
public Comp<S> focusTraversable() {
return apply(struc -> struc.get().setFocusTraversable(true));
}
public Comp<S> focusTraversable(boolean b) {
return apply(struc -> struc.get().setFocusTraversable(b));
}
public Comp<S> visible(ObservableValue<Boolean> o) {
return apply(struc -> struc.get().visibleProperty().bind(o));
}
@ -90,6 +98,11 @@ public abstract class Comp<S extends CompStructure<?>> {
return apply(struc -> struc.get().getStyleClass().add(styleClass));
}
public Comp<S> accessibleText(String text) {
return apply(struc -> struc.get().setAccessibleText(text));
}
public Comp<S> grow(boolean width, boolean height) {
return apply(GrowAugment.create(width, height));
}

View file

@ -24,7 +24,7 @@ public class CharsetChoiceComp extends SimpleComp {
return new Label(streamCharset.getCharset().displayName()
+ (streamCharset.hasByteOrderMark() ? " (BOM)" : ""));
},
new Label(AppI18n.get("app.none")),
streamCharset -> streamCharset.getNames().get(0), new Label(AppI18n.get("app.none")),
null);
builder.addFilter((charset, filter) -> {
return charset.getCharset().displayName().contains(filter);

View file

@ -59,8 +59,8 @@ public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
protected Region createGraphic(T s) {
var provider = DataStoreProviders.byStore(s);
var imgView =
new PrettyImageComp(new SimpleStringProperty(provider.getDisplayIconFileName(s)), 16, 16).createRegion();
var imgView = new PrettyImageComp(new SimpleStringProperty(provider.getDisplayIconFileName(s)), 16, 16)
.createRegion();
var name = DataStorage.get().getUsableStores().stream()
.filter(e -> e.equals(s))
@ -77,6 +77,14 @@ public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
return new Label(name, imgView);
}
private String toName(DataStore store) {
if (mode == Mode.PROXY && store instanceof ShellStore && ShellStore.isLocal(store.asNeeded())) {
return AppI18n.get("none");
}
return XPipeDaemon.getInstance().getStoreName(store).orElse("?");
}
@Override
@SuppressWarnings("unchecked")
protected Region createSimple() {
@ -88,6 +96,7 @@ public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
.findFirst()
.orElseThrow()
.createRegion(),
t -> toName(t),
new Label(AppI18n.get("none")),
n -> true);
comboBox.setSelectedDisplay(t -> createGraphic(t));

View file

@ -60,7 +60,7 @@ public class FileSystemStoreChoiceComp extends SimpleComp {
});
var comboBox =
new CustomComboBoxBuilder<FileSystemStore>(fileSystemProperty, this::createGraphic, null, v -> true);
new CustomComboBoxBuilder<FileSystemStore>(fileSystemProperty, this::createGraphic, store -> getName(store), null, v -> true);
comboBox.setSelectedDisplay(this::createDisplayGraphic);
DataStorage.get().getUsableStores().stream()
.filter(e -> e instanceof FileSystemStore)

View file

@ -34,6 +34,7 @@ public class IconButtonComp extends Comp<CompStructure<JFXButton>> {
var button = new JFXButton();
var fi = new FontIcon(icon.getValue());
fi.setFocusTraversable(false);
icon.addListener((c, o, n) -> {
fi.setIconLiteral(n);
});

View file

@ -107,6 +107,8 @@ public class OptionsComp extends Comp<CompStructure<Pane>> {
}
if (compRegion != null) {
compRegion.accessibleTextProperty().bind(name.textProperty());
compRegion.accessibleHelpProperty().bind(description.textProperty());
line.getChildren().add(compRegion);
}
@ -132,6 +134,7 @@ public class OptionsComp extends Comp<CompStructure<Pane>> {
line.getChildren().add(name);
if (compRegion != null) {
compRegion.accessibleTextProperty().bind(name.textProperty());
compRegions.add(compRegion);
line.getChildren().add(compRegion);
HBox.setHgrow(compRegion, Priority.ALWAYS);

View file

@ -95,6 +95,7 @@ public class SvgView {
wv.setPageFill(Color.TRANSPARENT);
wv.getEngine().setJavaScriptEnabled(false);
wv.setContextMenuEnabled(false);
wv.setFocusTraversable(false);
wv.getEngine().loadContent(getHtml(svgContent.getValue()));
svgContent.addListener((c, o, n) -> {

View file

@ -11,6 +11,7 @@ import com.dlsc.preferencesfx.formsfx.view.renderer.PreferencesFxGroupRenderer;
import com.dlsc.preferencesfx.util.PreferencesFxUtils;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
@ -53,7 +54,7 @@ public class CustomFormRenderer extends PreferencesFxFormRenderer {
if (nextRow > 1) {
GridPane.setMargin(titleLabel, new Insets(SPACING * 3, 0, SPACING, 0));
} else {
GridPane.setMargin(titleLabel, new Insets(SPACING, 0, SPACING, 0));
GridPane.setMargin(titleLabel, new Insets(SPACING, 0, SPACING, 0));
}
}
@ -77,21 +78,37 @@ public class CustomFormRenderer extends PreferencesFxFormRenderer {
c.getFieldLabel().setMaxHeight(AppFont.getPixelSize(1));
grid.add(c.getFieldLabel(), 0, i + rowAmount, 2, 1);
var canFocus = BindingsHelper.persist(c.getNode().disabledProperty().not());
var descriptionLabel = new Label();
descriptionLabel.setWrapText(true);
descriptionLabel.disableProperty().bind(c.getFieldLabel().disabledProperty());
descriptionLabel.opacityProperty().bind(c.getFieldLabel().opacityProperty().multiply(0.8));
descriptionLabel.managedProperty().bind(c.getFieldLabel().managedProperty());
descriptionLabel.visibleProperty().bind(c.getFieldLabel().visibleProperty());
descriptionLabel
.disableProperty()
.bind(c.getFieldLabel().disabledProperty());
descriptionLabel
.opacityProperty()
.bind(c.getFieldLabel()
.opacityProperty()
.multiply(0.8));
descriptionLabel
.managedProperty()
.bind(c.getFieldLabel().managedProperty());
descriptionLabel
.visibleProperty()
.bind(c.getFieldLabel().visibleProperty());
descriptionLabel.setMaxHeight(USE_PREF_SIZE);
if (AppI18n.getInstance().containsKey(descriptionKey)) {
rowAmount++;
descriptionLabel.textProperty().bind(AppI18n.observable(descriptionKey));
descriptionLabel.focusTraversableProperty().bind(canFocus);
grid.add(descriptionLabel, 0, i + rowAmount, 2, 1);
}
rowAmount++;
grid.add(c.getNode(), 0, i + rowAmount, 1, 1);
var node = c.getNode();
c.getFieldLabel().focusTraversableProperty().bind(canFocus);
grid.add(node, 0, i + rowAmount, 1, 1);
if (i == elements.size() - 1) {
// additional styling for the last setting
@ -101,7 +118,7 @@ public class CustomFormRenderer extends PreferencesFxFormRenderer {
var offset = preferencesGroup.getTitle() != null ? 15 : 0;
GridPane.setMargin(descriptionLabel, new Insets(SPACING, 0, 0, offset));
GridPane.setMargin(c.getNode(), new Insets(SPACING, 0, 0, offset));
GridPane.setMargin(node, new Insets(SPACING, 0, 0, offset));
if (!((i == 0) && (nextRow > 0))) {
GridPane.setMargin(c.getFieldLabel(), new Insets(SPACING * 3, 0, 0, offset));
@ -110,7 +127,7 @@ public class CustomFormRenderer extends PreferencesFxFormRenderer {
}
c.getFieldLabel().getStyleClass().add(styleClass.toString() + "-label");
c.getNode().getStyleClass().add(styleClass.toString() + "-node");
node.getStyleClass().add(styleClass.toString() + "-node");
}
if (element instanceof NodeElement nodeElement) {

View file

@ -3,6 +3,7 @@ package io.xpipe.app.util;
import io.xpipe.app.fxcomps.impl.FilterComp;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
@ -25,6 +26,7 @@ public class CustomComboBoxBuilder<T> {
private final Property<T> selected;
private final Function<T, Node> nodeFunction;
private final Function<T, String> accessibleNameFunction;
private Function<T, Node> selectedDisplayNodeFunction;
private final Map<Node, T> nodeMap = new HashMap<>();
private final Map<Node, Runnable> actionsMap = new HashMap<>();
@ -39,10 +41,11 @@ public class CustomComboBoxBuilder<T> {
private Function<T, Node> unknownNode;
public CustomComboBoxBuilder(
Property<T> selected, Function<T, Node> nodeFunction, Node emptyNode, Predicate<T> veto) {
Property<T> selected, Function<T, Node> nodeFunction, Function<T, String> accessibleNameFunction, Node emptyNode, Predicate<T> veto) {
this.selected = selected;
this.nodeFunction = nodeFunction;
this.selectedDisplayNodeFunction = nodeFunction;
this.accessibleNameFunction = accessibleNameFunction;
this.emptyNode = emptyNode;
this.veto = veto;
}
@ -66,6 +69,7 @@ public class CustomComboBoxBuilder<T> {
public Node add(T val) {
var node = nodeFunction.apply(val);
node.setAccessibleText(accessibleNameFunction.apply(val));
nodeMap.put(node, val);
nodes.add(node);
if (filterPredicate != null) {
@ -86,6 +90,7 @@ public class CustomComboBoxBuilder<T> {
var header = new Label(name);
header.setAlignment(Pos.CENTER);
var v = new VBox(spacer, header, new Separator(Orientation.HORIZONTAL));
v.setAccessibleText(name);
v.setAlignment(Pos.CENTER);
nodes.add(v);
disabledNodes.add(v);
@ -105,6 +110,9 @@ public class CustomComboBoxBuilder<T> {
public ComboBox<Node> build() {
var cb = new ComboBox<Node>();
cb.accessibleTextProperty().bind(Bindings.createStringBinding(() -> {
return selected.getValue() != null ? accessibleNameFunction.apply(selected.getValue()) : null;
}, selected));
cb.getItems().addAll(nodes);
cb.setCellFactory((lv) -> {
return new Cell();