Skip to content

Commit 253f1a0

Browse files
committed
CSSTUDIO-3341 Improvement: Avoid hiding content behind scrollbars when zooming using "WIDTH", "HEIGHT", or "ALL".
1 parent f96efe1 commit 253f1a0

1 file changed

Lines changed: 141 additions & 45 deletions

File tree

  • app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx

app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/JFXRepresentation.java

Lines changed: 141 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,22 @@
2525
import java.util.concurrent.TimeoutException;
2626
import java.util.function.Consumer;
2727
import java.util.logging.Level;
28+
import java.util.stream.Collectors;
2829

29-
import org.csstudio.display.builder.model.*;
30+
import javafx.geometry.BoundingBox;
31+
import javafx.geometry.Bounds;
32+
import javafx.geometry.Insets;
33+
import javafx.geometry.Orientation;
34+
import javafx.geometry.Point2D;
35+
import javafx.scene.control.Alert;
36+
import javafx.scene.control.ButtonType;
37+
import javafx.scene.control.ChoiceDialog;
38+
import javafx.scene.control.ScrollBar;
39+
import javafx.scene.control.ScrollPane;
40+
import org.csstudio.display.builder.model.DisplayModel;
41+
import org.csstudio.display.builder.model.UntypedWidgetPropertyListener;
42+
import org.csstudio.display.builder.model.Widget;
43+
import org.csstudio.display.builder.model.WidgetPropertyListener;
3044
import org.csstudio.display.builder.model.properties.PredefinedColorMaps;
3145
import org.csstudio.display.builder.model.properties.WidgetColor;
3246
import org.csstudio.display.builder.representation.ToolkitRepresentation;
@@ -47,18 +61,11 @@
4761
import javafx.collections.ObservableList;
4862
import javafx.embed.swing.SwingFXUtils;
4963
import javafx.event.EventHandler;
50-
import javafx.geometry.Bounds;
51-
import javafx.geometry.Insets;
52-
import javafx.geometry.Point2D;
5364
import javafx.scene.Cursor;
5465
import javafx.scene.Group;
5566
import javafx.scene.Node;
5667
import javafx.scene.Parent;
5768
import javafx.scene.Scene;
58-
import javafx.scene.control.Alert;
59-
import javafx.scene.control.ButtonType;
60-
import javafx.scene.control.ChoiceDialog;
61-
import javafx.scene.control.ScrollPane;
6269
import javafx.scene.image.WritableImage;
6370
import javafx.scene.input.MouseEvent;
6471
import javafx.scene.input.ScrollEvent;
@@ -452,46 +459,135 @@ else if (level_spec.equalsIgnoreCase(Messages.Zoom_Height))
452459
* @param zoom Zoom level: 1.0 for 100%, 0.5 for 50%, ZOOM_ALL, ZOOM_WIDTH, ZOOM_HEIGHT
453460
* @return Zoom level actually used
454461
*/
455-
private double setZoom(double zoom)
462+
private double setZoom(final double zoom)
456463
{
464+
final double zoom_to_set;
457465
if (zoom <= 0.0)
458-
{ // Determine zoom to fit outline of display into available space
459-
final Bounds available = model_root.getLayoutBounds();
460-
final Bounds outline = widget_pane.getLayoutBounds();
461-
462-
// 'outline' will wrap the actual widgets when the display
463-
// is larger than the available viewport.
464-
// So it can be used to zoom 'out'.
465-
// But when the viewport is much larger than the widget,
466-
// the JavaFX outline grows to fill the viewport,
467-
// so falling back to the self-declared model width and height
468-
// to zoom 'in'.
469-
// This requires displays to be created with
470-
// correct width/height properties.
471-
final double zoom_x, zoom_y;
472-
if (outline.getWidth() > available.getWidth())
473-
zoom_x = available.getWidth() / outline.getWidth();
474-
else if (model.propWidth().getValue() > 0)
475-
zoom_x = available.getWidth() / model.propWidth().getValue();
476-
else
477-
zoom_x = 1.0;
478-
479-
if (outline.getHeight() > available.getHeight())
480-
zoom_y = available.getHeight() / outline.getHeight();
481-
else if (model.propHeight().getValue() > 0)
482-
zoom_y = available.getHeight() / model.propHeight().getValue();
483-
else
484-
zoom_y = 1.0;
485-
486-
if (zoom == ZOOM_WIDTH)
487-
zoom = zoom_x;
488-
else if (zoom == ZOOM_HEIGHT)
489-
zoom = zoom_y;
490-
else // Assume ZOOM_ALL
491-
zoom = Math.min(zoom_x, zoom_y);
466+
{ // Determine zoom to fit outline of display into available space.
467+
// In order to determine the actual bounds within which an OPI
468+
// can be displayed, model_root.getViewportBounds() is called
469+
// and then the width/height of the scrollbars are added/subtracted
470+
// in order to determine the bounds both _with_ and _without_
471+
// scrollbars being displayed.
472+
boolean hScrollbarVisible = false;
473+
boolean vScrollbarVisible = false;
474+
double vScrollbarWidth = 0.0;
475+
double hHcrollbarHeight = 0.0;
476+
{
477+
List<ScrollBar> scrollbars = model_root.lookupAll(".scroll-bar").stream().filter(node -> node instanceof ScrollBar).map(node -> (ScrollBar) node).collect(Collectors.toUnmodifiableList());
478+
List<ScrollBar> model_root_scrollbars = scrollbars.stream().filter(scrollbar -> scrollbar.getParent() == model_root).collect(Collectors.toUnmodifiableList());
479+
for (ScrollBar scrollBar : model_root_scrollbars) {
480+
if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
481+
hScrollbarVisible = scrollBar.isVisible();
482+
hHcrollbarHeight = scrollBar.getLayoutBounds().getHeight();
483+
}
484+
else if (scrollBar.isVisible() && scrollBar.getOrientation() == Orientation.VERTICAL) {
485+
vScrollbarVisible = scrollBar.isVisible();
486+
vScrollbarWidth = scrollBar.getLayoutBounds().getWidth();
487+
}
488+
}
489+
}
490+
491+
final Bounds viewportBounds = model_root.getViewportBounds();
492+
final BoundingBox layoutBoundsWithoutScrollbars = new BoundingBox(viewportBounds.getMinX(),
493+
viewportBounds.getMinY(),
494+
Math.max(0.0, viewportBounds.getWidth() + (vScrollbarVisible ? vScrollbarWidth : 0.0)),
495+
Math.max(0.0, viewportBounds.getHeight() + (hScrollbarVisible ? hHcrollbarHeight : 0.0)));
496+
final BoundingBox layoutBoundsWithScrollbars = new BoundingBox(viewportBounds.getMinX(),
497+
viewportBounds.getMinY(),
498+
Math.max(0.0, viewportBounds.getWidth() - (vScrollbarVisible ? 0.0 : vScrollbarWidth)),
499+
Math.max(0.0, viewportBounds.getHeight() - (hScrollbarVisible ? 0.0 : hHcrollbarHeight)));
500+
501+
final double zoom_x_with_scrollbars_rounded;
502+
final double zoom_x_without_scrollbars_rounded;
503+
final double zoom_y_with_scrollbars_rounded;
504+
final double zoom_y_without_scrollbars_rounded;
505+
506+
{
507+
final Bounds outline = widget_pane.getLayoutBounds();
508+
// 'outline' will wrap the actual widgets when the display
509+
// is larger than the available viewport.
510+
// So it can be used to zoom 'out'.
511+
// But when the viewport is much larger than the widget,
512+
// the JavaFX outline grows to fill the viewport,
513+
// so falling back to the self-declared model width and height
514+
// to zoom 'in'.
515+
// This requires displays to be created with
516+
// correct width/height properties.
517+
final double zoom_x_without_scrollbars, zoom_y_without_scrollbars;
518+
final double zoom_x_with_scrollbars, zoom_y_with_scrollbars;
519+
if (outline.getWidth() > layoutBoundsWithScrollbars.getWidth()) {
520+
zoom_x_without_scrollbars = layoutBoundsWithoutScrollbars.getWidth() / outline.getWidth();
521+
zoom_x_with_scrollbars = layoutBoundsWithScrollbars.getWidth() / outline.getWidth();
522+
}
523+
else if (model.propWidth().getValue() > 0) {
524+
zoom_x_without_scrollbars = layoutBoundsWithoutScrollbars.getWidth() / model.propWidth().getValue();
525+
zoom_x_with_scrollbars = layoutBoundsWithScrollbars.getWidth() / model.propWidth().getValue();
526+
}
527+
else {
528+
zoom_x_without_scrollbars = 1.0;
529+
zoom_x_with_scrollbars = 1.0;
530+
}
531+
532+
if (outline.getHeight() > layoutBoundsWithScrollbars.getHeight()) {
533+
zoom_y_without_scrollbars = layoutBoundsWithoutScrollbars.getHeight() / outline.getHeight();
534+
zoom_y_with_scrollbars = layoutBoundsWithScrollbars.getHeight() / outline.getHeight();
535+
}
536+
else if (model.propHeight().getValue() > 0) {
537+
zoom_y_without_scrollbars =layoutBoundsWithoutScrollbars.getHeight() / model.propHeight().getValue();
538+
zoom_y_with_scrollbars = layoutBoundsWithScrollbars.getHeight() / model.propHeight().getValue();
539+
}
540+
else {
541+
zoom_y_without_scrollbars = 1.0;
542+
zoom_y_with_scrollbars = 1.0;
543+
}
544+
545+
zoom_x_with_scrollbars_rounded = Math.floor(zoom_x_with_scrollbars * 1000.0) / 1000.0;
546+
zoom_x_without_scrollbars_rounded = Math.floor(zoom_x_without_scrollbars * 1000.0) / 1000.0;
547+
zoom_y_with_scrollbars_rounded = Math.floor(zoom_y_with_scrollbars * 1000.0) / 1000.0;
548+
zoom_y_without_scrollbars_rounded = Math.floor(zoom_y_without_scrollbars * 1000.0) / 1000.0;
549+
}
550+
551+
if (zoom == ZOOM_WIDTH) {
552+
if (zoom_x_without_scrollbars_rounded * model.propHeight().getValue() > layoutBoundsWithoutScrollbars.getHeight()) {
553+
// Setting zoom_to_set to 'zoom_x_without_scrollbars_rounded'
554+
// would result in the horizontal scrollbar being shown.
555+
// Therefore, set zoom_to_set = zoom_x_with_scrollbars_rounded
556+
zoom_to_set = zoom_x_with_scrollbars_rounded;
557+
}
558+
else {
559+
zoom_to_set = zoom_x_without_scrollbars_rounded;
560+
}
561+
}
562+
else if (zoom == ZOOM_HEIGHT) {
563+
if (zoom_y_without_scrollbars_rounded * model.propWidth().getValue() > layoutBoundsWithoutScrollbars.getWidth()) {
564+
// Setting zoom_to_set to 'zoom_y_without_scrollbars_rounded'
565+
// would result in the vertical scrollbar being shown.
566+
// Therefore, set zoom_to_set = zoom_y_with_scrollbars_rounded:
567+
zoom_to_set = zoom_y_with_scrollbars_rounded;
568+
}
569+
else {
570+
zoom_to_set = zoom_y_without_scrollbars_rounded;
571+
}
572+
}
573+
else {
574+
if (zoom_y_without_scrollbars_rounded * model.propWidth().getValue() > layoutBoundsWithoutScrollbars.getWidth() ||
575+
zoom_x_without_scrollbars_rounded * model.propHeight().getValue() > layoutBoundsWithoutScrollbars.getHeight()) {
576+
// Setting zoom_to_set to 'Math.min(zoom_x_without_scrollbars_rounded, zoom_y_with_scrollbars_rounded)'
577+
// would result in at least either the vertical or the horizontal scrollbar being shown.
578+
// Therefore, set zoom_to_set = Math.min(zoom_x_without_scrollbars_rounded, zoom_y_without_scrollbars_rounded):
579+
zoom_to_set = Math.min(zoom_x_with_scrollbars_rounded, zoom_y_with_scrollbars_rounded); // Assume ZOOM_ALL
580+
}
581+
else {
582+
zoom_to_set = Math.min(zoom_x_without_scrollbars_rounded, zoom_y_without_scrollbars_rounded); // Assume ZOOM_ALL
583+
}
584+
}
585+
}
586+
else {
587+
zoom_to_set = zoom;
492588
}
493589

494-
widget_pane.getTransforms().setAll(new Scale(zoom, zoom));
590+
widget_pane.getTransforms().setAll(new Scale(zoom_to_set, zoom_to_set));
495591
// Appears similar to using this API:
496592
// widget_parent.setScaleX(zoom);
497593
// widget_parent.setScaleY(zoom);
@@ -505,7 +601,7 @@ else if (zoom == ZOOM_HEIGHT)
505601
if (isEditMode())
506602
updateModelSizeIndicators();
507603

508-
return zoom;
604+
return zoom_to_set;
509605
}
510606

511607
/** @return Zoom factor, 1.0 for 1:1 */

0 commit comments

Comments
 (0)