Crear un widget

Los widget permiten visualizar, cambiar e interactuar con la información publicada a través de distintas fuentes de información. Los plugin CameraServer, NetworkTables y Base proveen de widgets para controlar tipos de datos básicos (incluidos tipos de datos específicos de FRC). Sin embargo, los widgets personalizados permiten controlar sus tipos de datos personalizados creados en las secciones anteriores u Objetos de Java.

La interface básica Widget es hereda de las interfaces Component y Sourced. Component es el bloque de construcción más básico disponible en Shuffleboard. Sourced es una interface para objetos que puedan manejar e interactuar con fuentes de datos para mostrar o visualizar datos. Los widgets que no admiten enlaces de datos, pero tienen nodos hijos no utilizarían la interfaz Sourced, sino únicamente la interfaz Component. Ambas son bloques de construcción básicos para crear widgets y permiten visualizar y modificar datos.

Un buen widget permite al usuario final personalizar al widget de la forma que más le convenga. Un ejemplo podría ser permitir al usuario controlar el rango de un intensificador numérico, esto es, su mínimo y máximo o la orientación del mismo intensificador. La vista del widget o el cómo se ve se define utilizando FXML. FXML es un lenguaje basado en XML muy útil para definir layouts estáticos para los widgets («Panes», Etiquetas y Controles).

Más información sobre FMXL puede encontrarse aquí.

Definir el XML de un widget

En este ejemplo, creará dos intensificadores para ayudarle a controlar las coordenadas X,Y del objeto Point2D creado en secciones anteriores. Es útil colocar el archivo FXML en el mismo paquete que la clase Java.

Para crear una ventana vacía para el widget, es necesario crear un Pane. Un Pane es un nodo padre que contiene otros nodos hijos, en este caso, 2 intensificadores. Existen muchos tipos de Pane, como se muestra:

  • Stack Pane

    • Permiten sobreponer elementos. Además, StackPanes centran, de forma predeterminada, a los nodos hijos.

  • Grid Pane

    • Son extremadamente útiles para definir elementos hijos al utilizar un sistema coordinado al crear una cuadrícula flexible de filas y columnas en el plano.

  • Flow Pane

    • Envuelven a todos los nodos hijos en el conjunto de límites. Los nodos hijos pueden fluir verticalmente (contenidos en el límite de altura del panel) u horizontalmente (contenidos por el límite de anchura del panel).

  • Anchor Pane

    • Permiten a los elementos hijos ser posicionados encima, debajo, a la izquierda o derecha, o al centro del panel.

Los layout panes son extremadamente útiles para colocar nodos hijos en una fila horizontal al utilizar HBox o en una columa vertical usando VBox.

La sintaxis básica para definir un Pane usando FXML sería la siguiente:

<?import javafx.scene.layout.*?>
<StackPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="/path/to/widget/class" fx:id="root">
   ...
</StackPane>

El atributo fx:controller contiene el nombre de la clase del widget. Una instancia de esta clase se crea cuando se carga un archivo FXML. Para este trabajo, la clase del controlador debe tener un constructor vacío.

Crear una clase widget

Ahora que tiene el Pane, puede añadir elementos hijos al panel. Por ejemplo, puede agregar dos objetos intensificadores. Recuerde añadir fx:id a cada elemento para que puedan ser referenciados en la clase Java que hará más adelante. Utilizará VBox` para colocar su intensificador uno encima de otro.

<?import javafx.scene.layout.*?>
<StackPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="/path/to/widget/class" fx:id="root">

   <VBox>
      <Slider fx:id = "xSlider"/>
      <Slider fx:id = "ySlider"/>
   </VBox>

</StackPane>

Ahora que ha terminado de crear el archivo FXML, puede crear la clase del widget. La clase del widget debe incluir la anotación @Description que establece que los tipos de datos permitidos por el widget y el nombre del widget. Si la anotación`` @Description`` no está presente, la clase plugin debe implementar el método get() para regresar sus widgets.

También debe incluir una anotación @ParametrizedController que apunte al archivo FXML que contiene el diseño del widget. Si la clase que solo admite una fuente de datos, debe extender la clase SimpleAnnotatedWidget. Si la clase admite múltiples fuentes de datos, debe extender la clase ComplexAnnotatedWidget. Para obtener más información, consulte Tipos de Widget.

import edu.wpi.first.shuffleboard.api.widget.Description;
import edu.wpi.first.shuffleboard.api.widget.ParametrizedController;
import edu.wpi.first.shuffleboard.api.widget.SimpleAnnotatedWidget;

/*
 * If the FXML file and Java file are in the same package, that is the Java file is in src/main/java and the
 * FXML file is under src/main/resources or your code equivalent package, the relative path will work
 * However, if they are in different packages, an absolute path will be required.
*/

@Description(name = "MyPoint2D", dataTypes = MyPoint2D.class)
@ParametrizedController("Point2DWidget.fxml")
public final class Point2DWidget extends SimpleAnnotatedWidget<MyPoint2D> {

}

Si no está utilizando un tipo de dato personalizado, puede referenciar a cualquier tipo de dato Java (como Double.class) o si el widget no necesita enlaces de datos, puede pasar NoneType.class.

Ahora que ha creado la clase, puede crear campos para los widgets declarados en el archivo FXML usando la anotación @FXML. Para los dos intensificadores, un ejemplo sería:

import edu.wpi.first.shuffleboard.api.widget.Description;
import edu.wpi.first.shuffleboard.api.widget.ParametrizedController;
import edu.wpi.first.shuffleboard.api.widget.SimpleAnnotatedWidget;
import javafx.fxml.FXML;

@Description(name = "MyPoint2D", dataTypes = MyPoint2D.class)
@ParametrizedController("Point2DWidget.fxml")
public final class Point2DWidget extends SimpleAnnotatedWidget<MyPoint2D> {

   @FXML
   private Pane root;

   @FXML
   private Slider xSlider;

   @FXML
   private Slider ySlider;
}

Para visualizar el panel en el widget personalizado es necesario sobreescribir el método getView() y regresar el StackPane.

import edu.wpi.first.shuffleboard.api.widget.Description;
import edu.wpi.first.shuffleboard.api.widget.ParametrizedController;
import edu.wpi.first.shuffleboard.api.widget.SimpleAnnotatedWidget;
import javafx.fxml.FXML;

@Description(name = "MyPoint2D", dataTypes = MyPoint2D.class)
@ParametrizedController("Point2DWidget.fxml")
public final class Point2DWidget extends SimpleAnnotatedWidget<MyPoint2D> {

   @FXML
   private StackPane root;

   @FXML
   private Slider xSlider;

   @FXML
   private Slider ySlider;

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

}

Enlazar Elementos y añadir Escuchadores

Binding is a mechanism that allows JavaFX widgets to express direct relationships with the data source. For example, changing a widget will change its related NetworkTableEntry and vice versa.

Un ejemplo, en este caso, sería cambiar las coordenadas X,Y del objeto 2DPoint al cambiar los valores de xSlider y ySlider, respectivamente.

Una buena práctica es enlazar en el método initialize() con la etiqueta @FXML que requiere llamar al método de FXML si el método no es público.

import edu.wpi.first.shuffleboard.api.widget.Description;
import edu.wpi.first.shuffleboard.api.widget.ParametrizedController;
import edu.wpi.first.shuffleboard.api.widget.SimpleAnnotatedWidget;
import javafx.fxml.FXML;

@Description(name = "MyPoint2D", dataTypes = MyPoint2D.class)
@ParametrizedController("Point2DWidget.fxml")
public final class Point2DWidget extends SimpleAnnotatedWidget<MyPoint2D> {

   @FXML
   private StackPane root;

   @FXML
   private Slider xSlider;

   @FXML
   private Slider ySlider;

   @FXML
   private void initialize() {
      xSlider.valueProperty().bind(dataOrDefault.map(MyPoint2D::getX));
      ySlider.valueProperty().bind(dataOrDefault.map(MyPoint2D::getY));
   }

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

 }

The above initialize method binds the slider’s value property to the MyPoint2D data class” corresponding X and Y value. Meaning, changing the slider will change the coordinate and vice versa. The dataOrDefault.map() method will get the data source’s value, or, if no source is present, will return the default value.

Usar un escuchador es otra manera de cambiar los valores cuando el deslizador o la fuente de datos cambia. Un ejemplo de un escuchador para nuestro deslizador puede ser:

xSlider.valueProperty().addListener((observable, oldValue, newValue) -> setData(getData().withX(newValue));

En este caso, el método setData() establece el valor en la fuente de datos del widget al newValue.

Explorar componentes personalizados

Los widgets no se descubren automáticamente cuando se cargan plugins, el plugin se debe exportar explícitamente para poder ser utilizado. Este acercamiento permite que múltiples plugins sean definidos en el mismo JAR.

@Override
public List<ComponentType> getComponents() {
  return List.of(WidgetType.forAnnotatedWidget(Point2DWidget.class));
}

Configurar un widget predeterminado para tipos de datos

Para configurar su widget como la predeterminada para su tipo de dato personalizado, puede sobreescribir el método getDefaultComponents() en la clase de su plugin que almacena un Mapa para todos los widgets predeterminados, como se muestra a continuación:

@Override
public Map<DataType, ComponentType> getDefaultComponents() {
   return Map.of(Point2DType.Instance, WidgetType.forAnnotatedWidget(Point2DWidget.class));
}