Skip to content
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
25 changes: 24 additions & 1 deletion app/display/editor/doc/widgets_properties.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,27 @@ user is advised to use relative a path as an absolute
path may not be portable between hosts, in particular not between Windows and Linux/Mac hosts.

File paths may be specified using both forward slash (/) or backslash (\\) as path separator as both will work interchangeably
between Windows and Linux/Mac.
between Windows and Linux/Mac.

Web Browser Widget
==================

The Web Browser widget embeds a web view in a display. In addition to the common properties it provides
the following widget specific properties:

- "url" the address that is loaded when the display starts.
- "show_toolbar" whether to show a navigation toolbar (back, forward, reload and an address bar) above the
page. When turned off, only the page itself is shown.
- "resize_with_window", described below.

Resize with Window
------------------

By default every widget, including the Web Browser, keeps the size it was given in the editor, and the display
scrolls if the window is smaller than the display. Enabling "resize_with_window" makes the browser instead grow
and shrink to fill the runtime window or tab. This is useful for displays built around a single Web Browser that
should fill the screen.

The property only applies when the Web Browser is a top-level widget, that is a direct child of the display
itself. When the widget is placed inside a Group, a Tab or an embedded display the property has no effect. The
default is off, so existing displays are unaffected.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2015-2022 Oak Ridge National Laboratory.
* Copyright (c) 2015-2026 Oak Ridge National Laboratory.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
Expand Down Expand Up @@ -63,6 +63,7 @@ public class Messages
InformativeTooltipHeight,
InformativeTooltipName,
InformativeTooltipPVName,
InformativeTooltipResizeWithWindow,
InformativeTooltipRules,
InformativeTooltipScripts,
InformativeTooltipTooltip,
Expand Down Expand Up @@ -159,6 +160,7 @@ public class Messages
TraceType_Step,
ValueNoPV,
Vertical,
WebBrowser_resizeWithWindow,
WebBrowser_showToolbar,
WebBrowser_URL,
WidgetCategory_Controls,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2015-2016 Oak Ridge National Laboratory.
* Copyright (c) 2015-2026 Oak Ridge National Laboratory.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
Expand Down Expand Up @@ -48,9 +48,13 @@
/** 'show_toolbar' */
public static final WidgetPropertyDescriptor<Boolean> propShowToolbar =
CommonWidgetProperties.newBooleanPropertyDescriptor(WidgetPropertyCategory.DISPLAY, "show_toolbar", Messages.WebBrowser_showToolbar);
/** 'resize_with_window' */
public static final WidgetPropertyDescriptor<Boolean> propResizeWithWindow =
CommonWidgetProperties.newBooleanPropertyDescriptor(WidgetPropertyCategory.BEHAVIOR, "resize_with_window", Messages.WebBrowser_resizeWithWindow);

private volatile WidgetProperty<String> url;
private volatile WidgetProperty<Boolean> show_toolbar;
private volatile WidgetProperty<Boolean> resize_with_window;

Check warning on line 57 in app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/WebBrowserWidget.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this field "resize_with_window" to match the regular expression '^[a-z][a-zA-Z0-9]*$'.

See more on https://sonarcloud.io/project/issues?id=ControlSystemStudio_phoebus&issues=AZ8BTtg-Dyical_VSvUG&open=AZ8BTtg-Dyical_VSvUG&pullRequest=3853

/** Constructor */
public WebBrowserWidget()
Expand All @@ -64,6 +68,8 @@
super.defineProperties(properties);
properties.add(url = propWidgetURL.createProperty(this, ""));
properties.add(show_toolbar = propShowToolbar.createProperty(this, true));
properties.add(resize_with_window = propResizeWithWindow.createProperty(this, false));

Check warning on line 71 in app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/WebBrowserWidget.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract the assignment out of this expression.

See more on https://sonarcloud.io/project/issues?id=ControlSystemStudio_phoebus&issues=AZ8BTtg-Dyical_VSvUF&open=AZ8BTtg-Dyical_VSvUF&pullRequest=3853
resize_with_window.setInformativeTooltip(Messages.InformativeTooltipResizeWithWindow);
}

/** @return Widget 'url' property */
Expand All @@ -77,4 +83,10 @@
{
return show_toolbar;
}

/** @return 'resize_with_window' property */
public WidgetProperty<Boolean> propResizeWithWindow()

Check failure on line 88 in app/display/model/src/main/java/org/csstudio/display/builder/model/widgets/WebBrowserWidget.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename method "propResizeWithWindow" to prevent any misunderstanding/clash with field "propResizeWithWindow".

See more on https://sonarcloud.io/project/issues?id=ControlSystemStudio_phoebus&issues=AZ8BTtg-Dyical_VSvUH&open=AZ8BTtg-Dyical_VSvUH&pullRequest=3853
{
return resize_with_window;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ InformativeTooltipAlarmBorder=Should an alarm border be shown around the widget
InformativeTooltipHeight=The height of the widget in pixels.
InformativeTooltipName=The name of the widget.
InformativeTooltipPVName=The name of the PV to connect to and to display in the widget.
InformativeTooltipResizeWithWindow=Fill the runtime window or tab instead of the fixed design size; only for a top-level Web Browser, not when nested.
InformativeTooltipRules=Rules associated with the widget.
InformativeTooltipScripts=Scripts associated with the widget.
InformativeTooltipTooltip=Text to display in the tooltip of the widget when hovering with the mouse over the widget.
Expand Down Expand Up @@ -145,6 +146,7 @@ TraceType_None=None
TraceType_Step=Step
ValueNoPV=No PV
Vertical=Vertical
WebBrowser_resizeWithWindow=Resize with Window
WebBrowser_showToolbar=Show toolbar
WebBrowser_URL=URL
WidgetCategory_Controls=Controls
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ InformativeTooltipAlarmBorder=Faut-il afficher une bordure d’alarme autour du
InformativeTooltipHeight=Hauteur du widget en pixels.
InformativeTooltipName=Nom du widget.
InformativeTooltipPVName=Nom du PV à connecter et afficher dans le widget.
InformativeTooltipResizeWithWindow=Remplit la fenêtre ou l'onglet d'exécution au lieu de la taille de conception fixe ; uniquement pour un Navigateur Web de premier niveau, pas lorsqu'il est imbriqué.
InformativeTooltipRules=Règles associées au widget.
InformativeTooltipScripts=Scripts associés au widget.
InformativeTooltipTooltip=Texte à afficher dans l’info-bulle du widget au survol de la souris.
Expand Down Expand Up @@ -145,6 +146,7 @@ TraceType_None=Aucun
TraceType_Step=Étape
ValueNoPV=Pas de PV
Vertical=Vertical
WebBrowser_resizeWithWindow=Redimensionner avec la fenêtre
WebBrowser_showToolbar=Afficher la barre d'outils
WebBrowser_URL=URL
WidgetCategory_Controls=Contrôles
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*******************************************************************************
* Copyright (c) 2026 Oak Ridge National Laboratory.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
******************************************************************************/
package org.csstudio.display.builder.model.widgets;

import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;

import org.csstudio.display.builder.model.DisplayModel;
import org.csstudio.display.builder.model.Widget;
import org.csstudio.display.builder.model.persist.ModelReader;
import org.csstudio.display.builder.model.persist.ModelWriter;
import org.junit.jupiter.api.Test;

/** JUnit tests for {@link WebBrowserWidget}'s 'resize_with_window' property.
*
* <p>Verifies the default value and XML round-trip persistence for the
* backward-compatible new property.
*
* @author Gianluca Martino
*/
@SuppressWarnings("nls")
public class WebBrowserWidgetUnitTest
{
/** 'resize_with_window' must default to false so existing displays are unaffected */
@Test
public void testResizeWithWindowDefault()
{
final WebBrowserWidget browser = new WebBrowserWidget();
assertThat(browser.propResizeWithWindow().getValue(), equalTo(false));
}

/** A non-default 'resize_with_window' must survive an XML round trip */
@Test
public void testResizeWithWindowRoundTrip() throws Exception
{
final WebBrowserWidget original = new WebBrowserWidget();
original.propResizeWithWindow().setValue(true);

final DisplayModel model = new DisplayModel();
model.runtimeChildren().addChild(original);
// The value is non-default (true), so it is written regardless of skip_defaults.
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final ModelWriter writer = new ModelWriter(out);
writer.writeModel(model);
writer.close();
final String xml = out.toString();
assertThat(xml, containsString("<resize_with_window>"));

final ModelReader reader = new ModelReader(new ByteArrayInputStream(xml.getBytes()));
final DisplayModel loaded = reader.readModel();
final Widget w = loaded.getChildren().get(0);
assertTrue(w instanceof WebBrowserWidget);
assertThat(((WebBrowserWidget) w).propResizeWithWindow().getValue(), equalTo(true));
}

/** A default 'resize_with_window' must be omitted from XML (older phoebus ignores it anyway) */
@Test
public void testResizeWithWindowOmittedWhenDefault() throws Exception
{
final WebBrowserWidget browser = new WebBrowserWidget();
final DisplayModel model = new DisplayModel();
model.runtimeChildren().addChild(browser);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final ModelWriter writer = new ModelWriter(out);
writer.writeModel(model);
writer.close();
assertThat(out.toString(), not(containsString("<resize_with_window>")));
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2015-2019 Oak Ridge National Laboratory.
* Copyright (c) 2015-2026 Oak Ridge National Laboratory.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
Expand Down Expand Up @@ -133,7 +133,7 @@ public class JFXRepresentation extends ToolkitRepresentation<Parent, Node>

/** Adjustment for scroll body size to prevent scroll bars from being displayed */
// XXX Would be good to understand this value instead of 2-by-trial-and-error
private static final int SCROLLBAR_ADJUST = 2;
public static final int SCROLLBAR_ADJUST = 2;

/** Property for the DisplayModel that's represented */
public static final String ACTIVE_MODEL = "_active_model";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,43 @@
/*******************************************************************************
* Copyright (c) 2015-2020 Oak Ridge National Laboratory.
* Copyright (c) 2015-2026 Oak Ridge National Laboratory.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*******************************************************************************/
package org.csstudio.display.builder.representation.javafx.widgets;

import static org.csstudio.display.builder.representation.ToolkitRepresentation.logger;

import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;

import org.csstudio.display.builder.model.DirtyFlag;
import org.csstudio.display.builder.model.DisplayModel;
import org.csstudio.display.builder.model.ModelPlugin;
import org.csstudio.display.builder.model.UntypedWidgetPropertyListener;
import org.csstudio.display.builder.model.WidgetProperty;
import org.csstudio.display.builder.model.WidgetPropertyListener;
import org.csstudio.display.builder.model.util.ModelResourceUtil;
import org.csstudio.display.builder.model.widgets.WebBrowserWidget;
import org.csstudio.display.builder.representation.javafx.JFXRepresentation;
import org.csstudio.display.builder.representation.javafx.Messages;
import org.phoebus.framework.jobs.JobManager;
import org.phoebus.framework.util.IOUtils;
import org.phoebus.ui.javafx.ImageCache;
import org.phoebus.ui.javafx.ToolbarHelper;

import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.event.ActionEvent;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
Expand All @@ -48,6 +56,19 @@
private final UntypedWidgetPropertyListener sizeListener = this::sizeChanged;
private final WidgetPropertyListener<String> urlListener = this::urlChanged;

/** Lower bound for the fitted browser size, to avoid zero/negative dimensions */
private static final double MIN_FIT_SIZE = 1.0;

/** Host scroll pane whose viewport the browser fills when 'resize_with_window' is on.
* Non-null only while the resize-with-window behavior is active.
*/
private ScrollPane model_root = null;

Check warning on line 65 in app/display/representation-javafx/src/main/java/org/csstudio/display/builder/representation/javafx/widgets/WebBrowserRepresentation.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this field "model_root" to match the regular expression '^[a-z][a-zA-Z0-9]*$'.

See more on https://sonarcloud.io/project/issues?id=ControlSystemStudio_phoebus&issues=AZ8BTtlqDyical_VSvUI&open=AZ8BTtlqDyical_VSvUI&pullRequest=3853
/** Listener that refits the browser whenever the scroll pane (window/tab) changes size.
* Bound to the pane's width/height (not its viewport bounds): the viewport shrinks/grows
* with scroll-bar visibility, so listening to it would feed back into a resize loop.
*/
private InvalidationListener resizeListener = null;

private static final String[] downloads = new String[] { "zip", "csv", "cif", "tgz" };

private class Browser extends BorderPane
Expand Down Expand Up @@ -296,11 +317,15 @@
if (!toolkit.isEditMode())
model_widget.propWidgetURL().addPropertyListener(urlListener);
//the showToolbar property cannot be changed at runtime
//the resize_with_window property cannot be changed at runtime
if (!toolkit.isEditMode() && model_widget.propResizeWithWindow().getValue())
enableResizeWithWindow();
}

@Override
protected void unregisterListeners()
{
disableResizeWithWindow();
model_widget.propWidth().removePropertyListener(sizeListener);
model_widget.propHeight().removePropertyListener(sizeListener);
if (!toolkit.isEditMode())
Expand All @@ -324,10 +349,92 @@
public void updateChanges()
{
super.updateChanges();
if (dirty_size.checkAndClear())
// While resize-with-window is active (resizeListener != null), fitToViewport() owns the size
if (dirty_size.checkAndClear() && resizeListener == null)
jfx_node.setPrefSize(model_widget.propWidth().getValue(),
model_widget.propHeight().getValue());
if (dirty_url.checkAndClear())
((Browser)jfx_node).goToURL(model_widget.propWidgetURL().getValue());
}

private void enableResizeWithWindow()
{
if (! (toolkit instanceof JFXRepresentation))
return;
// Only meaningful for a top-level browser. A widget nested in a Group/Tabs/embedded
// container has X/Y relative to that container, not the display root, so the computed
// fill size would be wrong; make the property a no-op there rather than mis-size.
if (! (model_widget.getParent().orElse(null) instanceof DisplayModel))
{
logger.log(Level.WARNING,
"'resize_with_window' is only supported for a top-level Web Browser widget; ignoring for {0}",
model_widget);
return;
}
model_root = ((JFXRepresentation) toolkit).getModelRoot();
if (model_root == null)
return;
// Refit on window/tab resize. Bound to the pane's width/height only: a pure zoom change
// scales widget_pane without changing those, so a zoom change does not refit until the
// next window resize.
resizeListener = prop -> fitToViewport();
model_root.widthProperty().addListener(resizeListener);
model_root.heightProperty().addListener(resizeListener);
// Defer the initial fit to a later pulse, after the scene/viewport has been laid out
Platform.runLater(this::fitToViewport);
}

private void disableResizeWithWindow()
{
if (model_root != null && resizeListener != null)
{
model_root.widthProperty().removeListener(resizeListener);
model_root.heightProperty().removeListener(resizeListener);
}
model_root = null;
resizeListener = null;
}

private void fitToViewport()
{
// model_root is non-null only after the 'toolkit instanceof JFXRepresentation' check in
// enableResizeWithWindow(), so the cast below is safe.
if (model_root == null)
return;
// Size against the scroll pane's own width/height (minus a small scroll-bar allowance)
// rather than its viewport bounds, so scroll-bar visibility cannot feed back into the fit.
final double avail_width = model_root.getWidth() - JFXRepresentation.SCROLLBAR_ADJUST;
final double avail_height = model_root.getHeight() - JFXRepresentation.SCROLLBAR_ADJUST;
final double zoom = ((JFXRepresentation) toolkit).getZoom();
final double[] size = computeFitSize(avail_width, avail_height, zoom,
model_widget.propX().getValue(),
model_widget.propY().getValue(),
MIN_FIT_SIZE, MIN_FIT_SIZE);
jfx_node.setMinSize(size[0], size[1]);
jfx_node.setPrefSize(size[0], size[1]);
jfx_node.setMaxSize(size[0], size[1]);
}

/** Compute the browser size needed to fill the runtime window.
*
* @param scrollPaneWidth Host scroll pane width in scene pixels (getWidth(), not the
* viewport bounds, to avoid scroll-bar feedback)
* @param scrollPaneHeight Host scroll pane height in scene pixels
* @param zoom Current display zoom factor (1.0 == 100%)
* @param x Widget X position (widget coordinates)
* @param y Widget Y position (widget coordinates)
* @param minWidth Lower bound for the resulting width
* @param minHeight Lower bound for the resulting height
* @return { width, height } in widget coordinates
*/
static double[] computeFitSize(final double scrollPaneWidth, final double scrollPaneHeight,
final double zoom,
final double x, final double y,
final double minWidth, final double minHeight)
{
final double z = zoom > 0 ? zoom : 1.0;
final double width = Math.max(minWidth, scrollPaneWidth / z - x);
final double height = Math.max(minHeight, scrollPaneHeight / z - y);
return new double[] { width, height };
}
}
Loading
Loading