Skip to content

Commit

Permalink
Merge pull request #80 from leewyatt/master
Browse files Browse the repository at this point in the history
Implement Spacer And Add keyboard support to StripView
  • Loading branch information
dlemmermann authored Aug 21, 2023
2 parents caa403b + 8fd66fc commit a324bf4
Show file tree
Hide file tree
Showing 4 changed files with 241 additions and 2 deletions.
50 changes: 50 additions & 0 deletions gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/SpacerApp.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.dlsc.gemsfx.demo;

import com.dlsc.gemsfx.Spacer;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class SpacerApp extends Application {
@Override
public void start(Stage stage) throws Exception {
VBox root = new VBox(10);
root.setStyle("-fx-padding: 10px;-fx-alignment: top_center;");

Spacer spacer1 = new Spacer();
spacer1.setStyle("-fx-background-color: rgba(255,192,203,0.3);");
HBox topBox = new HBox(new Label("Hello"), spacer1, new Label("World"));

Spacer spacer2 = new Spacer();
spacer2.setStyle("-fx-background-color: rgba(134,139,220,0.3);");
VBox centerBox = new VBox(new Label("Hello"), spacer2, new Label("World"));
centerBox.setMinHeight(280);

Spacer spacer3 = new Spacer();
spacer3.setStyle("-fx-background-color: rgba(0,255,127,0.3);");
Spacer spacer4 = new Spacer();
spacer4.setStyle("-fx-background-color: rgba(255,255,0,0.3);");
HBox bottomBox = new HBox(new Label("Hello"), spacer3, new Label("World"), spacer4, new Label("!~"));

CheckBox checkBox = new CheckBox("Spacer Active");
checkBox.setSelected(true);
spacer1.activeProperty().bind(checkBox.selectedProperty());
spacer2.activeProperty().bind(checkBox.selectedProperty());
spacer3.activeProperty().bind(checkBox.selectedProperty());
spacer4.activeProperty().bind(checkBox.selectedProperty());

root.getChildren().addAll(topBox, centerBox, bottomBox, checkBox);

stage.setScene(new Scene(root, 380, 380));
stage.setTitle("Spacer Demo");
stage.show();
}

public static void main(String[] args) {
launch(args);
}
}
120 changes: 120 additions & 0 deletions gemsfx/src/main/java/com/dlsc/gemsfx/Spacer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package com.dlsc.gemsfx;

import javafx.beans.property.BooleanProperty;
import javafx.css.CssMetaData;
import javafx.css.Styleable;
import javafx.css.StyleableBooleanProperty;
import javafx.css.StyleableProperty;
import javafx.css.converter.BooleanConverter;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
* The Spacer class extends the Region class and provides functionality
* to create flexible spaces in layouts such as HBox and VBox. It is primarily
* used to push adjacent nodes apart or together by filling up available space. <br/>
*
* The Spacer can be toggled between active and inactive states. When active,
* it tries to grow as much as possible within its parent container. When
* inactive, it collapses and doesn't take up any space. <br/>
*
* The growth direction of the Spacer (horizontal or vertical) is determined
* based on its parent container. For instance, when placed inside an HBox, the
* Spacer will grow horizontally. Conversely, inside a VBox, it will grow vertically. <br/>
*
* The active state of the Spacer can also be controlled through CSS with the
* "-fx-active" property.
*
*/
public class Spacer extends Region {
public Spacer() {
this(true);
}

public Spacer(boolean active) {
getStyleClass().add("spacer");
setActive(active);
managedProperty().bind(visibleProperty());
visibleProperty().bind(activeProperty());
parentProperty().addListener((observable, oldValue, newValue) -> {
if (newValue instanceof HBox) {
VBox.setVgrow(this, Priority.NEVER);
HBox.setHgrow(this, Priority.ALWAYS);
} else if (newValue instanceof VBox) {
VBox.setVgrow(this, Priority.ALWAYS);
HBox.setHgrow(this, Priority.NEVER);
} else {
VBox.setVgrow(this, Priority.NEVER);
HBox.setHgrow(this, Priority.NEVER);
}
});
}

private final BooleanProperty active = new StyleableBooleanProperty(false) {
@Override
public Object getBean() {
return Spacer.this;
}

@Override
public String getName() {
return "active";
}

@Override
public CssMetaData<Spacer, Boolean> getCssMetaData() {
return StyleableProperties.ACTIVE;
}
};

public final boolean isActive() {
return active.get();
}

public final void setActive(boolean value) {
active.set(value);
}

public BooleanProperty activeProperty() {
return active;
}

private static class StyleableProperties {
private static final CssMetaData<Spacer, Boolean> ACTIVE =
new CssMetaData<>("-fx-active", BooleanConverter.getInstance(), false) {

@Override
public boolean isSettable(Spacer n) {
return !n.active.isBound();
}

@Override
public StyleableProperty<Boolean> getStyleableProperty(Spacer n) {
return (StyleableProperty<Boolean>) n.activeProperty();
}
};

private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;

static {
List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>(Region.getClassCssMetaData());
styleables.add(ACTIVE);
STYLEABLES = Collections.unmodifiableList(styleables);
}
}

public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.STYLEABLES;
}

@Override
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
return getClassCssMetaData();
}
}
18 changes: 18 additions & 0 deletions gemsfx/src/main/java/com/dlsc/gemsfx/StripView.java
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,24 @@ public void scrollTo(T item) {
getProperties().put("scroll.to", item);
}

private final BooleanProperty loopSelection = new SimpleBooleanProperty(this, "loopSelection", true);

public final boolean isLoopSelection() {
return loopSelection.get();
}

public final void setLoopSelection(boolean value) {
loopSelection.set(value);
}

/**
* Property to determine whether the selection should loop from the end to the start and vice versa.
* true means that the selection will loop.
*/
public final BooleanProperty loopSelectionProperty() {
return loopSelection;
}

/**
* A strip cell is being used by cell factories of the {@link StripView} control.
*
Expand Down
55 changes: 53 additions & 2 deletions gemsfx/src/main/java/com/dlsc/gemsfx/skins/StripViewSkin.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.SkinBase;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
Expand All @@ -31,6 +32,7 @@
import javafx.util.Duration;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -87,13 +89,61 @@ public StripViewSkin(StripView<T> strip) {

strip.itemsProperty().addListener((Observable it) -> buildContent());
buildContent();

strip.addEventFilter(KeyEvent.KEY_PRESSED, this::handleKeyPress);
}

private void handleKeyPress(KeyEvent event) {
StripView<T> strip = getSkinnable();

List<T> itemsList = strip.getItems();
T currentSelectedItem = strip.getSelectedItem();
int index = itemsList.indexOf(currentSelectedItem);
int itemCount = itemsList.size();

switch (event.getCode()) {
case RIGHT:
case ENTER:
// Check if loop selection is enabled or if we haven't reached the last item yet
if (strip.isLoopSelection() || index < itemCount - 1) {
// Calculate the next index. If loop selection is off, due to the above check,
// this won't exceed the bounds.
index = (index + 1) % itemCount;
strip.setSelectedItem(itemsList.get(index));
event.consume();
}
break;
case LEFT:
// Check if loop selection is enabled or if we haven't reached the first item yet
if (strip.isLoopSelection() || index > 0) {
// Calculate the previous index. If loop selection is off, due to the above check,
// this won't go negative.
index = (index - 1 + itemCount) % itemCount;
strip.setSelectedItem(itemsList.get(index));
event.consume();
}
break;
case TAB:
// If it's the last item and loop selection is off, don't consume the event
// so that focus can move to the next focusable component.
// Otherwise, select the next item.
if (index < itemCount - 1 || strip.isLoopSelection()) {
index = (index + 1) % itemCount;
strip.setSelectedItem(itemsList.get(index));
event.consume();
}
break;
default:
// For any other key press, do nothing.
break;
}
}

private void scrollTo(T item) {
Node node = nodeMap.get(item);

if (node != null) {
StripView strip = getSkinnable();
StripView<T> strip = getSkinnable();

strip.getProperties().remove(SCROLL_TO_KEY);

Expand Down Expand Up @@ -126,6 +176,7 @@ private void buildContent() {
nodeMap.put(item, cell);
cell.addEventHandler(MouseEvent.MOUSE_CLICKED, evt -> {
if (!evt.isConsumed() && evt.getClickCount() == 1 && evt.getButton() == MouseButton.PRIMARY) {
cell.requestFocus();
boolean wasSelected = item == getSkinnable().getSelectedItem();
strip.setSelectedItem(item);
strip.scrollTo(item);
Expand Down Expand Up @@ -170,7 +221,7 @@ private void setupListeners() {
private void fadeSupport(Boolean newShow, Region button) {
if (getSkinnable().isAnimateScrolling()) {
if (newShow) {
createFadeTransition(button, 0, 1).play();
createFadeTransition(button, 0, 1).play();
} else {
createFadeTransition(button, 1, 0).play();
}
Expand Down

0 comments on commit a324bf4

Please sign in to comment.