Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Array widgets #812

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import edu.wpi.first.shuffleboard.plugin.base.widget.AlertsWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.BasicFmsInfoWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.BasicSubsystemWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.BooleanArrayWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.BooleanBoxWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.ComboBoxChooserWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.CommandWidget;
Expand All @@ -56,17 +57,19 @@
import edu.wpi.first.shuffleboard.plugin.base.widget.GraphWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.GyroWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.MecanumDriveWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.NumberArrayWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.NumberBarWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.NumberSliderWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.PIDCommandWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.PIDControllerWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.ProfiledPIDControllerWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.PowerDistributionPanelWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.ProfiledPIDControllerWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.RelayWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.RobotPreferencesWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.SimpleDialWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.SpeedControllerWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.SplitButtonChooserWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.StringArrayWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.TextViewWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.ThreeAxisAccelerometerWidget;
import edu.wpi.first.shuffleboard.plugin.base.widget.ToggleButtonWidget;
Expand All @@ -83,7 +86,7 @@
@Description(
group = "edu.wpi.first.shuffleboard",
name = "Base",
version = "1.3.7",
version = "1.4.0",
summary = "Defines all the WPILib data types and stock widgets"
)
@SuppressWarnings("PMD.CouplingBetweenObjects")
Expand Down Expand Up @@ -169,6 +172,9 @@ public List<ComponentType> getComponents() {
WidgetType.forAnnotatedWidget(UltrasonicWidget.class),
WidgetType.forAnnotatedWidget(FieldWidget.class),
WidgetType.forAnnotatedWidget(AlertsWidget.class),
WidgetType.forAnnotatedWidget(BooleanArrayWidget.class),
WidgetType.forAnnotatedWidget(NumberArrayWidget.class),
WidgetType.forAnnotatedWidget(StringArrayWidget.class),
new LayoutClass<>("List Layout", ListLayout.class),
new LayoutClass<>("Grid Layout", GridLayout.class),
createSubsystemLayoutType()
Expand Down Expand Up @@ -204,6 +210,9 @@ public Map<DataType, ComponentType> getDefaultComponents() {
.put(FieldType.Instance, WidgetType.forAnnotatedWidget(FieldWidget.class))
.put(AlertsType.Instance, WidgetType.forAnnotatedWidget(AlertsWidget.class))
.put(SubsystemType.Instance, createSubsystemLayoutType())
.put(DataTypes.BooleanArray, WidgetType.forAnnotatedWidget(BooleanArrayWidget.class))
.put(DataTypes.NumberArray, WidgetType.forAnnotatedWidget(NumberArrayWidget.class))
.put(DataTypes.StringArray, WidgetType.forAnnotatedWidget(StringArrayWidget.class))
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package edu.wpi.first.shuffleboard.plugin.base.control;


import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.Label;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.util.Callback;

import java.util.ArrayList;

/**
* A table view that displays an array {@code T[]} as a table with an index and a value column.
* @param <T> the type of each item in the array (e.g. {@code Boolean} for {@code Boolean[]})
*/
public class ArrayTableView<T> extends TableView<ArrayTableView.ArrayTableEntry<T>> {
Martysh12 marked this conversation as resolved.
Show resolved Hide resolved
private final TableColumn<ArrayTableEntry<T>, String> indexCol = new TableColumn<>("Index");
private final TableColumn<ArrayTableEntry<T>, T> valueCol = new TableColumn<>("Value");

private final ObservableList<ArrayTableEntry<T>> list = FXCollections.observableArrayList();

@SuppressWarnings("JavadocMethod")
public ArrayTableView() {
super();

indexCol.setCellValueFactory(p -> new ReadOnlyStringWrapper(String.valueOf(p.getValue().index)));
indexCol.setResizable(false);
indexCol.setPrefWidth(50);

valueCol.setCellValueFactory(p -> new ReadOnlyObjectWrapper<>(p.getValue().value));
valueCol.prefWidthProperty().bind(widthProperty().subtract(50).subtract(2));

//noinspection unchecked
getColumns().addAll(indexCol, valueCol);

setItems(list);

Label placeholder = new Label("No data available");
setPlaceholder(placeholder);
}

public void setValueCellFactory(
Callback<TableColumn<ArrayTableEntry<T>, T>, TableCell<ArrayTableEntry<T>, T>> callback
) {
valueCol.setCellFactory(callback);
}

/**
* Convenience method to set the table's items directly, instead of using an {@link ObservableList}.
*/
public void setItems(T[] items) {
final var entries = new ArrayList<ArrayTableEntry<T>>();

for (int i = 0; i < items.length; i++) {
entries.add(new ArrayTableEntry<>(i, items[i]));
}

list.setAll(entries);
}

public static class ArrayTableEntry<S> {
public final int index;
public final S value;

public ArrayTableEntry(int index, S value) {
this.index = index;
this.value = value;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package edu.wpi.first.shuffleboard.plugin.base.widget;

import edu.wpi.first.shuffleboard.api.prefs.Group;
import edu.wpi.first.shuffleboard.api.prefs.Setting;
import edu.wpi.first.shuffleboard.api.widget.Description;
import edu.wpi.first.shuffleboard.api.widget.SimpleAnnotatedWidget;
import edu.wpi.first.shuffleboard.plugin.base.control.ArrayTableView;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.TableCell;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;

import java.util.List;
import java.util.stream.IntStream;

@Description(
name = "Boolean Array",
dataTypes = boolean[].class,
summary = "Displays an array of booleans"
)
public final class BooleanArrayWidget extends SimpleAnnotatedWidget<boolean[]> {
private final StackPane pane = new StackPane();
private final ArrayTableView<Boolean> table = new ArrayTableView<>();

private final Property<Color> trueColor
= new SimpleObjectProperty<>(this, "colorWhenTrue", Color.LAWNGREEN);
private final Property<Color> falseColor
= new SimpleObjectProperty<>(this, "colorWhenFalse", Color.DARKRED);

@SuppressWarnings("JavadocMethod")
public BooleanArrayWidget() {
pane.getChildren().add(table);

table.setValueCellFactory(tableColumn -> new BooleanTableCell<>());

dataOrDefault.addListener((observableValue, oldBooleans, newBooleans) -> {
final var array = new Boolean[newBooleans.length];
IntStream.range(0, newBooleans.length).forEach(i -> array[i] = newBooleans[i]);
Comment on lines +41 to +42
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newBooleans.clone() would do this better

table.setItems(array);
});
}

@Override
public List<Group> getSettings() {
return List.of(
Group.of("Colors",
Setting.of("Color when true", "The color to use when a value is `true`", trueColor, Color.class),
Setting.of("Color when false", "The color to use when a value is `false`", falseColor, Color.class)
)
);
}

@Override
public Pane getView() {
return pane;
}

private class BooleanTableCell<S> extends TableCell<S, Boolean> {
private Background createBooleanBackground(boolean value) {
return new Background(new BackgroundFill(value ? trueColor.getValue() : falseColor.getValue(), null, null));
}

@Override
protected void updateItem(Boolean t, boolean empty) {
if (empty || t == null) {
setBackground(null);
} else {
setBackground(createBooleanBackground(t));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package edu.wpi.first.shuffleboard.plugin.base.widget;

import edu.wpi.first.shuffleboard.api.widget.Description;
import edu.wpi.first.shuffleboard.api.widget.SimpleAnnotatedWidget;
import edu.wpi.first.shuffleboard.plugin.base.control.ArrayTableView;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;

import java.util.stream.IntStream;

@Description(
name = "Number Array",
dataTypes = double[].class,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

float[] and long[] will need to be supported, too

Copy link
Author

@Martysh12 Martysh12 Oct 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I try to change the type from double[] to Number[],

diff --git a/plugins/base/src/main/java/edu/wpi/first/shuffleboard/plugin/base/widget/NumberArrayWidget.java b/plugins/base/src/main/java/edu/wpi/first/shuffleboard/plugin/base/widget/NumberArrayWidget.java
@@ -10,21 +10,19 @@
 
 @Description(
     name = "Number Array",
-    dataTypes = double[].class,
+    dataTypes = Number[].class,
     summary = "Displays an array of numbers"
 )
-public final class NumberArrayWidget extends SimpleAnnotatedWidget<double[]> {
+public final class NumberArrayWidget extends SimpleAnnotatedWidget<Number[]> {
   private final StackPane pane = new StackPane();
-  private final ArrayTableView<Double> table = new ArrayTableView<>();
+  private final ArrayTableView<Number> table = new ArrayTableView<>();
 
   @SuppressWarnings("JavadocMethod")
   public NumberArrayWidget() {
     pane.getChildren().add(table);
 
-    dataOrDefault.addListener((observableValue, oldDoubles, newDoubles) -> {
-      final var array = new Double[newDoubles.length];
-      IntStream.range(0, newDoubles.length).forEach(i -> array[i] = newDoubles[i]);
-      table.setItems(array);
+    dataOrDefault.addListener((observableValue, oldNumbers, newNumbers) -> {
+      table.setItems(newNumbers);
     });
   }

I get a strange exception:

edu.wpi.first.shuffleboard.api.data.IncompatibleSourceException: Expected one of (LW Subsystem), but found type NumberArray instead
	at edu.wpi.first.shuffleboard.api.widget.SingleSourceWidget.addSource(SingleSourceWidget.java:55)
	at edu.wpi.first.shuffleboard.app.components.ProcedurallyDefinedTab.populateLayout(ProcedurallyDefinedTab.java:112)
	at edu.wpi.first.shuffleboard.app.components.ProcedurallyDefinedTab.populate(ProcedurallyDefinedTab.java:98)
	at edu.wpi.first.shuffleboard.api.util.FxUtils.lambda$runOnFxThread$0(FxUtils.java:64)
	at com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:457)
	at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
	at com.sun.javafx.application.PlatformImpl.lambda$runLater$11(PlatformImpl.java:456)
	at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
	at com.sun.glass.ui.gtk.GtkApplication._runLoop(Native Method)
	at com.sun.glass.ui.gtk.GtkApplication.lambda$runLoop$11(GtkApplication.java:290)
	at java.base/java.lang.Thread.run(Thread.java:840)

Shuffleboard seems to think that NumberArrayWidget now accepts a SubsystemType, but I never specified that type anywhere.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending on what commit your bench is based on, this may be a different bug that's fixed in #815, where data types aren't correctly loaded. LW Subsystem is interesting though, what does your robot code Shuffleboard setup look like?

Copy link
Author

@Martysh12 Martysh12 Oct 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does your robot code Shuffleboard setup look like?

I have the following in robotInit:

final var tab = Shuffleboard.getTab("Test");

tab.addBooleanArray("BooleanArray", () -> new boolean[]{false, true, false, true, true, false, true, false})
        .withSize(3, 3);
tab.addDoubleArray("DoubleArray", () -> new double[]{0.0, 1.0, -1.0, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NaN, 0.00001, -0.00001})
        .withSize(3, 3);
tab.addFloatArray("FloatArray", () -> new float[]{0.0f, 1.0f, -1.0f, Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY, Float.NaN, 0.00001f, -0.00001f})
        .withSize(3, 3);
tab.addIntegerArray("IntegerArray", () -> new long[]{0L, 1L, -1L, Long.MAX_VALUE, Long.MIN_VALUE})
        .withSize(3, 3);
tab.addStringArray("StringArray", () -> new String[]{"Hello", "The quick brown fox jumps over the lazy dog.", "Extra long extra long Extra long extra long Extra long extra long Extra long extra long Extra long extra long", "", "newlines!\nnewlines!\nnewlines!"})
        .withSize(3, 3);

tab.addBoolean("Boolean", () -> true);
tab.addDouble("Double", () -> 0.123456f);
tab.addFloat("Float", () -> 0.123f);
tab.addInteger("Integer", () -> 123);
tab.addString("String", () -> "String");

Depending on what commit your branch is based on, this may be a different bug that's fixed in #815.

My branch (Martysh12/shuffleboard:array-widgets) is based on wpilibsuite/shuffleboard:main (commit cdd5e7a, specifically).

I actually did try merging SamCarlberg/shuffleboard:nt-fixes into my branch and seeing if doing so fixes anything; it looks like it didn't:
image
All entries in my robot-created Test tab can only display as a Network Table Tree (when I right click on them), and only the primitives types are auto-populated.

Entries under FMSInfo seem to display fine, however, and all widgets are available for them:
image

summary = "Displays an array of numbers"
)
public final class NumberArrayWidget extends SimpleAnnotatedWidget<double[]> {
private final StackPane pane = new StackPane();
private final ArrayTableView<Double> table = new ArrayTableView<>();

@SuppressWarnings("JavadocMethod")
public NumberArrayWidget() {
pane.getChildren().add(table);

dataOrDefault.addListener((observableValue, oldDoubles, newDoubles) -> {
final var array = new Double[newDoubles.length];
IntStream.range(0, newDoubles.length).forEach(i -> array[i] = newDoubles[i]);
Comment on lines +25 to +26
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use newDoubles.clone() instead

table.setItems(array);
});
}

@Override
public Pane getView() {
return pane;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package edu.wpi.first.shuffleboard.plugin.base.widget;

import edu.wpi.first.shuffleboard.api.widget.Description;
import edu.wpi.first.shuffleboard.api.widget.SimpleAnnotatedWidget;
import edu.wpi.first.shuffleboard.plugin.base.control.ArrayTableView;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;

@Description(
name = "String Array",
dataTypes = String[].class,
summary = "Displays an array of strings"
)
public final class StringArrayWidget extends SimpleAnnotatedWidget<String[]> {
private final StackPane pane = new StackPane();
private final ArrayTableView<String> table = new ArrayTableView<>();

@SuppressWarnings("JavadocMethod")
public StringArrayWidget() {
pane.getChildren().add(table);

dataOrDefault.addListener((observableValue, oldStrings, newStrings) -> table.setItems(newStrings));
SamCarlberg marked this conversation as resolved.
Show resolved Hide resolved
}

@Override
public Pane getView() {
return pane;
}
}