SchemaWidget Usage Examples

Here you can find various usage examples for the SchemaWidget API.

pom.xml configuration

<dependency>
  <groupId>de.gsi.cs.co.ap.common.gui.elements</groupId>
  <artifactId>cscoap-common-gui-elements-schema-widget</artifactId>
  <version>8.2.0-SNAPSHOT</version> <!-- check for newer version -->
</dependency>

Creating & showing a SchemaWidget

SchemaWidget schemaWidget = new SchemaWidget();
borderPane.setCenter(schemaWidget);

This displays the SchemaWidget - an empty Pane, as no SectionModel was set.

To get a SchemaWidget that displays an accelerator schema, you must provide a SectionModel:
SectionModel model = ServiceLocator.getService(ApplicationService.class).getLSASchemaData("CRYRING").getSectionModel();
SchemaWidget schemaWidget = new SchemaWidget();
schemaWidget.setSectionModel(model);
borderPane.setCenter(schemaWidget);

Reading model data in a background task

final SchemaWidget schemaWidget = new SchemaWidget();

...

//optional: prepare for the model setting
beforeModelSet();

// create Task to create and set section model asynchronously
final Task<SectionModel> task = new Task<SectionModel>() {
    protected SectionModel call() throws Exception {
        // call the model retrieval method on the background thread
        return ServiceLocator.getService(ApplicationService.class).getLSASchemaData("CRYRING").getSectionModel();
    }
 
    protected void succeeded() {
        // back on the JavaFX thread
        schemaWidget.setSectionModel(resultModel);
 
        // optional: handle the model reset (see below)
        afterReset();
   }
};
new Thread(task).start();

Recommended actions for handling the setting of a new model:
  • You may want to reset the widget's view or pan/zoom/rotate the view to show the new model.
  • If the new model has its sections placed in a very different coordinate system, you may have to recreate the contents of other layers using matching coordinates.
  • You may want to disable the schema widget or set its mouse transparency to true when the model reset process is started, and set back to enabled / mouse transparency false when the new model has been received.

Configuring the base view parameters for resets

Example for just two base values: Set an initial schema rotation of 45 degrees and tell the widget to try to fit the specified rectangular area in schema coordinates into the available screen space.

 schemaWidget.getViewConfig().setBaseRotation(45);
      
 Point2D p1 = new Point2D(550, 400);
 Point2D p2 = new Point2D(750, 300);
 Point2D p3 = new Point2D(700, 200);
 Point2D p4 = new Point2D(500, 300);
 schemaWidgetPresenter.getViewConfig().setBaseViewport(new RectangularQuadrangle(p1, p2, p3, p4));

 ...

Dynamically manipulating the widget's zoom, rotation and pan

Use the methods defined in the SchemaWidgetViewManipulation interface.

With control elements of the application

Button zoomButton = new Button("Zoom+");
zoomButton.setOnAction(new EventHandler<ActionEvent>() {
    @Override public void handle(ActionEvent e) {
        //usually, you want to change the zoom level in single steps
        schemaWidget.zoom(1, null);
    }
});
vBox.getChildren().add(zoomButton);

Button rotateButton = new Button("Rotate 90° CW");
rotateButton.setOnAction(new EventHandler<ActionEvent>() {
    @Override public void handle(ActionEvent e) {
        schemaWidget.rotate(90, null);
    }
});
vBox.getChildren().add(rotateButton);

...

With the mouse

You need to register a SchemaMouseListener to do this. The API provides an implementation for manipulating the widget's viewing parameters with the mouse: ViewManipulationSchemaMouseListener. The functionalities this listener allows (zooming and/or rotating and/or panning) can be configured.

 

By default, panning, zooming, and rotating via mouse interactions are all allowed.
//allow only panning with the mouse, but not zooming and rotating
ViewManipulationSchemaMouseListener viewManip = new ViewManipulationSchemaMouseListener(schemaWidget);
viewManip.setPanningAllowed(true);
viewManip.setZoomingAllowed(false);
viewManip.setRotatingAllowed(false);
schemaWidget.addSchemaMouseListener();

Being informed of changes to the widget's viewing parameters

Use a SchemaWidgetViewListener to be informed when the view's scale or rotation changes or the canvas is reset.

Interacting with sections with the mouse

To do this, a SectionMouseListener must be registered with the widget that implements the desired behavior for the mouse entering, exiting and clicking a section.

SectionMouseListener mySectionMouseListener = new SectionMouseListener() {
            
            @Override public void handleExited(final SchemaSection section, final MouseEvent event) { ... }
            
            @Override public void handleEntered(final SchemaSection section, final MouseEvent event) { ... }

            @Override public void handleClicked(final SchemaSection section, final MouseEvent event) { ... }
};    
schemaWidget.addSectionMouseListener(mySectionMouseListener);

Focusing and selecting sections

The API contains a helpful implementation of SectionMouseListener for focusing and selecting sections, SelectAndFocusSectionMouseListener. By default, a section is focused when the mouse enters it and unfocused when the mouse leaves it. For clicks on sections, three selection strategies can be chosen: either not selection on click, or multiple selection (toggling of the selection state for the clicked section without affecting the selection of any other sections), or single selection (toggling of the selection state and unselecting all other sections).

To be notified whenever the focus or selection of sections changes, use a SectionFocusListener or SectionSelectionListener. Both are notified after a focus or selection change (possibly of multiple sections at once) is completed. They recieve the ids of the focused/selected sections before and after the respective change as parameters.
SectionSelectionListener mySectionSelectionListener = new SectionSelectionListener() {
            @Override void sectionSelectionChanged(Set<Integer> oldSelectedSectionsIds, Set<Integer> newSelectedSectionsIds) { ... }
};
schemaWidget.addSectionSelectionListener(mySectionSelectionListener);

SectionMouseListener mySectionMouseListener = new SelectAndFocusSectionMouseListener(schemaWidget, SelectionStrategy.MULTIPLE_SELECTION) ;
schemaWidget.addSectionMouseListener(mySectionMouseListener); 

Unselecting all sections when nothing was clicked

This is a special case: As SectionMouseListeners only report when a section was clicked, we cannot use them to determine if nothing (no section, no other schema element) was clicked. To do that, we must register a SchemaMouseListener - when a click is detected, that listener should deselect all sections. To make sure that the unselecting listener is the first listener to be informed after the layers, you should make sure that it is the last added SchemaMouseListener.

 SectionMouseListener mySectionListener = new SelectAndFocusSectionMouseListener(schemaWidget, SelectionStrategy.MULTIPLE_SELECTION);
 schemaWidget.addSectionMouseListener(mySectionListener);

 SchemaMouseListener unselectingListener = new SchemaMouseListener() {

     ...

     @Override
     public void mousePressed(MouseEvent e, double schemaX, double schemaY) {
         schemaWidget.unselectAllSections();
         e.consume();
     }
 };

 // make sure to add all other SchemaMouseListeners before this one so it gets called first
 schemaWidget.addSchemaMouseListener(unselectingListener);

Changing the look of sections

The visualization of sections is defined by SectionRenderers. Configure or extend the SectionRendererImpl provided by the API (easy) or implement a new SectionRenderer yourself (hard) to get a renderer that visualizes sections a certain way.

//Example: Configuring a SectionRendererImpl 
SectionRendererImpl mySectionRenderer = new SectionRendererImpl();
SectionRenderingOptions options = mySectionRenderer.getRenderingOptions();
options.setNormalPaint(Color.BLUE);
options.setSelectedPaint(Color.ORANGE);
options.getStrokeConfig().setStrokeWidth(15);
...

Change the look of specific sections by setting a custom renderer for them.

schemaWidget.setCustomSectionRenderer(257, mySectionRenderer);

Change the look of all sections that do not have a custom renderer set by changing the default section renderer.

schemaWidget.setDefaultSectionRenderer(mySectionRenderer);

Working with ButtonSchemaLayer

Create a button layer, add it to the widget. Create buttons and add them to the button layer.
ButtonSchemaLayer buttonLayer = new ButtonSchemaLayer();
schemaWidget.addLayer(buttonLayer, LayerPosition.TOP);

Button button = new Button("Button");
button.setStyle("-fx-base: blue;"); // set background color
button.setOnAction(e -> { ... });
buttonLayer.addButton(button, 300, -300);

Working with SchemaBeams

Create a beam layer, add it to the widget. Display and hide beams.

Set<SchemaBeam> beamModel = null;
try {
            beamModel = CsvFileBeamDataRetriever.getBeamData(sectionModel);
} catch (IOException e) {
            ...
}
BeamSchemaLayer beamLayer = new BeamSchemaLayer();
schemaWidget.addLayer(beamLayer, LayerPosition.DIRECTLY_UNDER_MAIN_SCHEMA);
...
ColoredBeam beam = new ColoredBeamImpl(beamModel.iterator().next(), Color.RED);
beamLayer.displayBeam(beam);
...
beamLayer.hideBeam(beam);

Interacting with beams

Directly interacting with the displayed beams is not possible, as the beam layer does not support mouse interactions for its elements (the beams).

A possible workaround to allow users to select beams by interacting with the widget would be to use a SectionMouseListener for detecting clicks on sections. The beams (displayed and/or undisplayed) containing the clicked sections could then be presented to the user, for example in a popup. The user could then select one of the offered beams.

Example of a very basic implementation of the workaround: Both the currently displayed and the currently undisplayed are presented to the user in the same list in a popup. Undisplayed beams are identified by having a special transparent color. In a real-world scenario, using separate lists for the displayed and undisplayed beams might be more practicable.

BeamSchemaLayer beamLayer = new BeamSchemaLayer();
schemaWidget.addLayer(beamLayer, LayerPosition.DIRECTLY_UNDER_MAIN_SCHEMA);

Set<SchemaBeam> beamModel = ...; // see example above

schemaWidget.addSectionMouseListener(new  SelectAndFocusSectionMouseListener(schemaWidget, SelectionStrategy.NO_SELECTION) {

            @Override
            public void handleClicked(final SchemaSection section, final MouseEvent event) {

                List<ColoredBeam> displayedBeamsContainingSection = beamLayer.getDisplayedBeams().stream().filter(b -> b.getBeam().getSections().contains(section)).collect(Collectors.toList());
                List<SchemaBeam> undisplayedBeamsContainingSection = beamModel.stream().filter(b -> b.getSections().contains(section)).collect(Collectors.toList());

                List<ColoredBeam> offeredBeams = new ArrayList<>(displayedBeamsContainingSection);
                undisplayedBeamsContainingSection.forEach(b -> offeredBeams.add(new ColoredBeamImpl(b, Color.TRANSPARENT)));

                ChoiceDialog<ColoredBeam> dialog = new ChoiceDialog<>(null, offeredBeams);
                Optional<ColoredBeam> result = dialog.showAndWait();
                result.ifPresent(chosenBeam -> {      
                    if (chosenBeam.getColor().equals(Color.TRANSPARENT)) {
                       handleChosenUndisplayedBeam(chosenBeam);
                    } else {
                       handleChosenDisplayedBeam(chosenBeam);
                    }
                 });
            }
});

Working with additional SectionSchemaLayers

While the SchemaWidget has an internal SectionSchemaLayer that is treated as the main content of the widget, it's possible to add additional SectionSchemaLayers above or below the central layer. Make sure to refer to the additional SectionSchemaLayer directly where needed, do not confuse it with the widget itself!

SectionModel myModel = ...;
SectionSchemaLayer sectionLayer = new SectionSchemaLayer(); 
sectionLayer.setSectionModel(myModel);
schemaWidget.addLayer(sectionLayer, LayerPosition.TOP);

//this line adds a mouse listener to the *widget's internal section layer* - so the listener receives the widget as its constructor parameter!
schemaWidget.addSectionMouseListener(new SelectAndFocusSectionMouseListener(schemaWidget, SelectionStrategy.MULTIPLE_SELECTION));

//this line adds a mouse listener to the *additional section layer* - so the listener receives the *additional section layer* as its constructor parameter!
sectionLayer.addSectionMouseListener(new SelectAndFocusSectionMouseListener(sectionLayer, SelectionStrategy.SINGLE_SELECTION));

Creating a custom layer

See the existing layer implementations for examples of how layers can be implemented - both simple layers (LabelSchemaLayer, ButtonSchemaLayer) and more complex layers (SectionSchemaLayer, BeamSchemaLayer). Also see the demo application.

Showing layer elements only at certain scale values

It's possible to define layers or elements within layers that only show up once a certain scale has been reached by adapting their visibility or adding/removing them depending on the current scale.

Example: A button layer that is only visible when the widget's scale value is >= 1.5 .

class SameScaleZoomableButtonSchemaLayer extends ButtonSchemaLayer {

    final private BooleanBinding visibleBinding = viewScaleProperty().greaterThanOrEqualTo(1.5);

    @Override
    public void addButton(final ButtonBase button, final double centerX, final double centerY) {
        super.addButton(button, centerX, centerY);
        button.visibleProperty().bind(visibleBinding);
    }

    @Override
    public void removeButton(final ButtonBase button) {
        super.removeButton(button);
        button.visibleProperty().unbind();
    }
}

Example: A button layer where different buttons are visible at different scales

class VaryingScalesZoomableButtonSchemaLayer extends ButtonSchemaLayer {

    public void addButton(final ButtonBase button, final double centerX, final double centerY, final double visibleAboveScale) {
        super.addButton(button, centerX, centerY);
        button.visibleProperty().bind(viewScaleProperty().greaterThanOrEqualTo(visibleAboveScale);
    }

    @Override
    public void removeButton(final ButtonBase button) {
        super.removeButton(button);
        button.visibleProperty().unbind();
    }
}

Alternatively, an application could check for changes to the widget's zoom level or scale (see Being informed about changes to the widget's viewing parameters ) and dynamically add and remove layers or layer elements.

Keeping layer elements horizontal even when the widget is rotated

SchemaLayers can specify a property that will be bound to always reflect the current rotation of the widget's view. The class SchemaLayerImpl, which is provided with the SchemaWidget API, offers a simple implementation. The rotation of specific layer elements can be bound to the negation of the widget view's rotation to always keep those layer elements horizontal.

class MySchemaLayer extends SchemaLayerImpl {

    public void addMyElement(final Node myElement, boolean keepHorizontal) {
       if (keepHorizontal) {
            myElement.rotateProperty().bind(viewRotationProperty().negate());
        }
        getLayerGroup().getChildren().add(myElement);
    }

}

Border around SchemaWidget

Using JavaFX Borders

A JavaFX Border can be set directly on the SchemaWidget node, but the widget's contents will be visible above the Border. Also, for Borders with rounded corners or insets, the contents of the widget will be visible outside of the border. Clipping to the border bounds does not seem to be possible, at least not easily. (See http://news.kynosarges.org/2016/11/03/javafx-pane-clipping/#comment-1083 for more information about clipping to/around borders.)

To create a Border that is displayed above the widget contents and clip the widget contents to the border, it is necessary to wrap the SchemaWidget node with another pane:

double BORDER_RADIUS = 45; // use identical border radii for all corners for correct clipping

// set border on wrapping pane
StackPane p = new StackPane(schemaWidget);
p.setBorder(new Border(new BorderStroke(Color.GREEN, BorderStrokeStyle.SOLID, new CornerRadii(BORDER_RADIUS),
                BorderStroke.THICK, new Insets(20, 30, 40, 100))));

// create clip for schema widget
final Rectangle borderClip = new Rectangle();
borderClip.setArcWidth(BORDER_RADIUS * 2); // watch out: Border expects a corner RADIUS, Rectangle expects a corner DIAMETER!
borderClip.setArcHeight(BORDER_RADIUS * 2);
schemaWidget.setClip(borderClip);

// make sure clip adapts to changing size of widget
schemaWidget.layoutBoundsProperty().addListener((ov, oldValue, newValue) -> {
    borderClip.setWidth(newValue.getWidth());
    borderClip.setHeight(newValue.getHeight());
});

This may leave a hairline pixel gap between the widgets contents and the border. To avoid that, either use a slightly larger factor than 2 when setting the clip's arc width/height, or use Rectangles instead of Borders as described in the following workaround.

Using Rectangles as pseudo borders

Alternatively, instead of using actual JavaFX Borders, it is possible to create a "pseudo border" by adding a Rectangle representing the expected border to the SchemaWidget node. This rectangle has to be styled and positioned manually, but the widget contents can easily be clipped even for "borders" with rounded corners or insets. However, using Rectangles is not as flexible as using real Borders: The radii of all corners will be identical, and it is not possible to use different stroke widths for the different side lines of the "border".
final double BORDER_RADIUS_WIDTH = 30;
final double BORDER_RADIUS_HEIGHT = 30;
final Insets INSETS = new Insets(20, 30, 40, 50);

// create visible "border"
final Rectangle pseudoBorder = new Rectangle();
pseudoBorder.setX(INSETS.getLeft()); 
pseudoBorder.setY(INSETS.getTop());
pseudoBorder.setArcWidth(BORDER_RADIUS_WIDTH * 2); // watch out: Border expects a corner RADIUS, Rectangle expects a corner DIAMETER!
pseudoBorder.setArcHeight(BORDER_RADIUS_HEIGHT * 2);
pseudoBorder.setStrokeType(StrokeType.INSIDE); // important: stroke INSIDE of rectangle to make sure the whole "border" is visible after clipping
pseudoBorder.setStrokeWidth(5);
pseudoBorder.setStroke(Color.GREEN);
pseudoBorder.setFill(null);
schemaWidget.getChildren().add(pseudoBorder); // add "border" to widget

// make sure "border" adapts to changing size of widget
schemaWidget.layoutBoundsProperty().addListener((ov, oldValue, newValue) -> {
    pseudoBorder.setWidth(newValue.getWidth() - INSETS.getLeft() - INSETS.getRight());
    pseudoBorder.setHeight(newValue.getHeight() - INSETS.getTop() - INSETS.getBottom());
});

// clip widget to "border" extent so no widget contents are visible beyond the border:
// cannot reuse "border" rectangle, so create a new one with the same extent properties       
final Rectangle pseudoBorderClip = new Rectangle(); 
pseudoBorderClip.xProperty().bind(pseudoBorder.xProperty());
pseudoBorderClip.yProperty().bind(pseudoBorder.yProperty());
pseudoBorderClip.widthProperty().bind(pseudoBorder.widthProperty());
pseudoBorderClip.heightProperty().bind(pseudoBorder.heightProperty());
pseudoBorderClip.arcWidthProperty().bind(pseudoBorder.arcWidthProperty());
pseudoBorderClip.arcHeightProperty().bind(pseudoBorder.arcHeightProperty());
schemaWidget.setClip(pseudoBorderClip);

-- AnnekeWalter - 20 Feb 2014

This topic: Applications > WebHome > AppGuiComponentsMain > AppGuiComponentsSchemaWidget > AppGuiComponentsSchemaWidgetUsageExamples
Topic revision: 13 Apr 2020, JuttaFitzek
This site is powered by FoswikiCopyright © by the contributing authors. All material on this collaboration platform is the property of the contributing authors.
Ideas, requests, problems regarding Foswiki? Send feedback