diff --git a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/JFXRepresentation.java b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/JFXRepresentation.java index 9b191db3a5..75bc3f2c12 100644 --- a/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/JFXRepresentation.java +++ b/app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/JFXRepresentation.java @@ -25,8 +25,22 @@ import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import java.util.logging.Level; +import java.util.stream.Collectors; -import org.csstudio.display.builder.model.*; +import javafx.geometry.BoundingBox; +import javafx.geometry.Bounds; +import javafx.geometry.Insets; +import javafx.geometry.Orientation; +import javafx.geometry.Point2D; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; +import javafx.scene.control.ChoiceDialog; +import javafx.scene.control.ScrollBar; +import javafx.scene.control.ScrollPane; +import org.csstudio.display.builder.model.DisplayModel; +import org.csstudio.display.builder.model.UntypedWidgetPropertyListener; +import org.csstudio.display.builder.model.Widget; +import org.csstudio.display.builder.model.WidgetPropertyListener; import org.csstudio.display.builder.model.properties.PredefinedColorMaps; import org.csstudio.display.builder.model.properties.WidgetColor; import org.csstudio.display.builder.representation.ToolkitRepresentation; @@ -47,18 +61,11 @@ import javafx.collections.ObservableList; import javafx.embed.swing.SwingFXUtils; import javafx.event.EventHandler; -import javafx.geometry.Bounds; -import javafx.geometry.Insets; -import javafx.geometry.Point2D; import javafx.scene.Cursor; import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.Scene; -import javafx.scene.control.Alert; -import javafx.scene.control.ButtonType; -import javafx.scene.control.ChoiceDialog; -import javafx.scene.control.ScrollPane; import javafx.scene.image.WritableImage; import javafx.scene.input.MouseEvent; import javafx.scene.input.ScrollEvent; @@ -452,47 +459,139 @@ else if (level_spec.equalsIgnoreCase(Messages.Zoom_Height)) * @param zoom Zoom level: 1.0 for 100%, 0.5 for 50%, ZOOM_ALL, ZOOM_WIDTH, ZOOM_HEIGHT * @return Zoom level actually used */ - private double setZoom(double zoom) + private double setZoom(final double zoom) { + final double zoomToSet; if (zoom <= 0.0) - { // Determine zoom to fit outline of display into available space - final Bounds available = model_root.getLayoutBounds(); - final Bounds outline = widget_pane.getLayoutBounds(); - - // 'outline' will wrap the actual widgets when the display - // is larger than the available viewport. - // So it can be used to zoom 'out'. - // But when the viewport is much larger than the widget, - // the JavaFX outline grows to fill the viewport, - // so falling back to the self-declared model width and height - // to zoom 'in'. - // This requires displays to be created with - // correct width/height properties. - final double zoom_x, zoom_y; - if (outline.getWidth() > available.getWidth()) - zoom_x = available.getWidth() / outline.getWidth(); - else if (model.propWidth().getValue() > 0) - zoom_x = available.getWidth() / model.propWidth().getValue(); - else - zoom_x = 1.0; - - if (outline.getHeight() > available.getHeight()) - zoom_y = available.getHeight() / outline.getHeight(); - else if (model.propHeight().getValue() > 0) - zoom_y = available.getHeight() / model.propHeight().getValue(); - else - zoom_y = 1.0; - - if (zoom == ZOOM_WIDTH) - zoom = zoom_x; - else if (zoom == ZOOM_HEIGHT) - zoom = zoom_y; - else // Assume ZOOM_ALL - zoom = Math.min(zoom_x, zoom_y); + { // Determine zoom to fit outline of display into available space. + // In order to determine the actual bounds within which an OPI + // can be displayed, model_root.getViewportBounds() is called + // and then the width/height of the scrollbars are added/subtracted + // in order to determine the bounds both _with_ and _without_ + // scrollbars being displayed. + boolean hScrollbarVisible = false; + boolean vScrollbarVisible = false; + double vScrollbarWidth = 0.0; + double hHcrollbarHeight = 0.0; + { + List scrollbars = model_root.lookupAll(".scroll-bar").stream().filter(node -> node instanceof ScrollBar).map(node -> (ScrollBar) node).collect(Collectors.toUnmodifiableList()); + List modelRootScrollbars = scrollbars.stream().filter(scrollbar -> scrollbar.getParent() == model_root).collect(Collectors.toUnmodifiableList()); + for (ScrollBar scrollBar : modelRootScrollbars) { + if (scrollBar.getOrientation() == Orientation.HORIZONTAL) { + hScrollbarVisible = scrollBar.isVisible(); + hHcrollbarHeight = scrollBar.getLayoutBounds().getHeight(); + } + else if (scrollBar.isVisible() && scrollBar.getOrientation() == Orientation.VERTICAL) { + vScrollbarVisible = scrollBar.isVisible(); + vScrollbarWidth = scrollBar.getLayoutBounds().getWidth(); + } + } + } + + final Bounds viewportBounds = model_root.getViewportBounds(); + final BoundingBox layoutBoundsWithoutScrollbars = new BoundingBox(viewportBounds.getMinX(), + viewportBounds.getMinY(), + Math.max(0.0, viewportBounds.getWidth() + (vScrollbarVisible ? vScrollbarWidth : 0.0)), + Math.max(0.0, viewportBounds.getHeight() + (hScrollbarVisible ? hHcrollbarHeight : 0.0))); + final BoundingBox layoutBoundsWithScrollbars = new BoundingBox(viewportBounds.getMinX(), + viewportBounds.getMinY(), + Math.max(0.0, viewportBounds.getWidth() - (vScrollbarVisible ? 0.0 : vScrollbarWidth)), + Math.max(0.0, viewportBounds.getHeight() - (hScrollbarVisible ? 0.0 : hHcrollbarHeight))); + + final double zoomXWithScrollbarsRounded; + final double zoomXWithoutScrollbarsRounded; + final double zoomYWithScrollbarsRounded; + final double zoomYWithoutScrollbarsRounded; + + { + final Bounds outline = widget_pane.getLayoutBounds(); + // 'outline' will wrap the actual widgets when the display + // is larger than the available viewport. + // So it can be used to zoom 'out'. + // But when the viewport is much larger than the widget, + // the JavaFX outline grows to fill the viewport, + // so falling back to the self-declared model width and height + // to zoom 'in'. + // This requires displays to be created with + // correct width/height properties. + final double zoomXWithoutScrollbars, zoomYWithoutScrollbars; + final double zoomXWithScrollbars, zoomYWithScrollbars; + if (outline.getWidth() > layoutBoundsWithScrollbars.getWidth()) { + zoomXWithoutScrollbars = layoutBoundsWithoutScrollbars.getWidth() / outline.getWidth(); + zoomXWithScrollbars = layoutBoundsWithScrollbars.getWidth() / outline.getWidth(); + } + else if (model.propWidth().getValue() > 0) { + zoomXWithoutScrollbars = layoutBoundsWithoutScrollbars.getWidth() / model.propWidth().getValue(); + zoomXWithScrollbars = layoutBoundsWithScrollbars.getWidth() / model.propWidth().getValue(); + } + else { + zoomXWithoutScrollbars = 1.0; + zoomXWithScrollbars = 1.0; + } + + if (outline.getHeight() > layoutBoundsWithScrollbars.getHeight()) { + zoomYWithoutScrollbars = layoutBoundsWithoutScrollbars.getHeight() / outline.getHeight(); + zoomYWithScrollbars = layoutBoundsWithScrollbars.getHeight() / outline.getHeight(); + } + else if (model.propHeight().getValue() > 0) { + zoomYWithoutScrollbars =layoutBoundsWithoutScrollbars.getHeight() / model.propHeight().getValue(); + zoomYWithScrollbars = layoutBoundsWithScrollbars.getHeight() / model.propHeight().getValue(); + } + else { + zoomYWithoutScrollbars = 1.0; + zoomYWithScrollbars = 1.0; + } + + // Round down the zoom-level in order to avoid situations where the + // zoom-level is instead being rounded up, causing a scrollbar to + // be displayed when a scrollbar is not needed. + zoomXWithScrollbarsRounded = Math.floor(zoomXWithScrollbars * 1000.0) / 1000.0; + zoomXWithoutScrollbarsRounded = Math.floor(zoomXWithoutScrollbars * 1000.0) / 1000.0; + zoomYWithScrollbarsRounded = Math.floor(zoomYWithScrollbars * 1000.0) / 1000.0; + zoomYWithoutScrollbarsRounded = Math.floor(zoomYWithoutScrollbars * 1000.0) / 1000.0; + } + + if (zoom == ZOOM_WIDTH) { + if (zoomXWithoutScrollbarsRounded * model.propHeight().getValue() > layoutBoundsWithoutScrollbars.getHeight()) { + // Setting zoomToSet to 'zoomXWithoutScrollbarsRounded' + // would result in the horizontal scrollbar being shown. + // Therefore, set zoomToSet = zoomXWithScrollbarsRounded + zoomToSet = zoomXWithScrollbarsRounded; + } + else { + zoomToSet = zoomXWithoutScrollbarsRounded; + } + } + else if (zoom == ZOOM_HEIGHT) { + if (zoomYWithoutScrollbarsRounded * model.propWidth().getValue() > layoutBoundsWithoutScrollbars.getWidth()) { + // Setting zoomToSet to 'zoomYWithoutScrollbarsRounded' + // would result in the vertical scrollbar being shown. + // Therefore, set zoomToSet = zoomYWithScrollbarsRounded: + zoomToSet = zoomYWithScrollbarsRounded; + } + else { + zoomToSet = zoomYWithoutScrollbarsRounded; + } + } + else { + // Assume ZOOM_ALL + if (zoomYWithoutScrollbarsRounded * model.propWidth().getValue() > layoutBoundsWithoutScrollbars.getWidth() || + zoomXWithoutScrollbarsRounded * model.propHeight().getValue() > layoutBoundsWithoutScrollbars.getHeight()) { + // Setting zoomToSet to 'Math.min(zoomXWithoutScrollbarsRounded, zoomYWithScrollbarsRounded)' + // would result in at least either the vertical or the horizontal scrollbar being shown. + // Therefore, set zoomToSet = Math.min(zoomXWithoutScrollbarsRounded, zoomYWithoutScrollbarsRounded): + zoomToSet = Math.min(zoomXWithScrollbarsRounded, zoomYWithScrollbarsRounded); // Assume ZOOM_ALL + } + else { + zoomToSet = Math.min(zoomXWithoutScrollbarsRounded, zoomYWithoutScrollbarsRounded); + } + } + } + else { + zoomToSet = zoom; } - widget_parent.getTransforms().setAll(new Scale(zoom, zoom)); - widget_pane.getTransforms().setAll(new Scale(zoom, zoom)); + widget_pane.getTransforms().setAll(new Scale(zoomToSet, zoomToSet)); // Appears similar to using this API: // widget_parent.setScaleX(zoom); // widget_parent.setScaleY(zoom); @@ -506,7 +605,7 @@ else if (zoom == ZOOM_HEIGHT) if (isEditMode()) updateModelSizeIndicators(); - return zoom; + return zoomToSet; } /** @return Zoom factor, 1.0 for 1:1 */