Skip to content

Commit

Permalink
Changed layout hooks to always be registered and fixed an issue where…
Browse files Browse the repository at this point in the history
… updates would not trigger a JavaFX pulse
  • Loading branch information
ennerf committed Aug 11, 2023
1 parent d6fbe3c commit f10493c
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 193 deletions.
65 changes: 48 additions & 17 deletions chartfx-chart/src/main/java/io/fair_acc/chartfx/Chart.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import io.fair_acc.chartfx.ui.layout.ChartPane;
import io.fair_acc.chartfx.ui.layout.PlotAreaPane;
import io.fair_acc.chartfx.ui.*;
import io.fair_acc.chartfx.ui.utils.LayoutHook;
import io.fair_acc.dataset.AxisDescription;
import io.fair_acc.dataset.event.EventSource;
import io.fair_acc.dataset.events.BitState;
import io.fair_acc.dataset.events.ChartBits;
Expand All @@ -33,7 +33,6 @@
import javafx.scene.canvas.Canvas;
import javafx.scene.control.Control;
import javafx.scene.layout.*;
import javafx.scene.paint.Paint;
import javafx.util.Duration;

import org.slf4j.Logger;
Expand Down Expand Up @@ -67,12 +66,10 @@
* @author hbraeun, rstein, major refactoring, re-implementation and re-design
*/
public abstract class Chart extends Region implements EventSource {
private final LayoutHook layoutHooks = LayoutHook.newPreAndPostHook(this, this::runPreLayout, this::runPostLayout);

// The chart has two different states, one that includes everything and is only ever on the JavaFX thread, and
// a thread-safe one that receives dataSet updates and forwards them on the JavaFX thread.
protected final BitState state = BitState.initDirty(this, BitState.ALL_BITS)
.addChangeListener(ChartBits.KnownMask, (src, bits) -> layoutHooks.registerOnce())
.addChangeListener(ChartBits.ChartLayout, (src, bits) -> super.requestLayout());

// DataSets are the only part that can potentially get updated from different threads, so we use a separate
Expand Down Expand Up @@ -245,6 +242,12 @@ public Chart(Axis... axes) {
}
}

// Setup layout hooks. Note that dataset changes by themselves do not trigger a layout,
// so in order to force a chart update we manually request a layout on an unmanaged
// style node that results in a NO-OP.
FXUtils.registerLayoutHooks(this, this::runPreLayout, this::runPostLayout);
state.addChangeListener(ChartBits.KnownMask, (src, bits) -> styleableNodes.requestLayout());

menuPane.setTriggerDistance(Chart.DEFAULT_TRIGGER_DISTANCE);
plotBackground.toBack();
plotForeGround.toFront();
Expand Down Expand Up @@ -502,25 +505,28 @@ public void invalidate() {
fireInvalidated(ChartBits.ChartLayout, ChartBits.ChartCanvas);
}

protected void updateLegend() {
protected void runPreLayout() {
state.setDirty(dataSetState.clear());
if (state.isClean()) {
return;
}

// Update legend
if (state.isDirty(ChartBits.ChartLegend)) {
updateLegend(getDatasets(), getRenderers());
}
state.clear(ChartBits.ChartLegend);
}

protected void runPreLayout() {
forEachDataSet(ds -> lockedDataSets.add(ds.lock().readLock()));
updateLegend();

// Update data ranges
final long start = ProcessingProfiler.getTimeStamp();
ensureLockedDataSets();
updateAxisRange(); // Update data ranges etc. to trigger anything that might need a layout
ProcessingProfiler.getTimeDiff(start, "updateAxisRange()");

// Update other components
for (Renderer renderer : renderers) {
renderer.runPreLayout();
}

for (ChartPlugin plugin : plugins) {
plugin.runPreLayout();
}
Expand Down Expand Up @@ -548,38 +554,59 @@ public void layoutChildren() {

// Note: there are some rare corner cases, e.g., computing
// the pref size of the scene (and the HiddenSidesPane),
// that call for a layout without calling the hooks. The
// plugins may rely on datasets being locked, so we skip
// the update to be safe.
if (layoutHooks.hasRunPreLayout()) {
// that call for a layout without any dirty bits. It is also
// possible that the layout triggers a resizing, so we may
// need to lock the datasets here.
if (state.isDirty()) {
ensureLockedDataSets();
layoutPluginsChildren();
}

}

protected void runPostLayout() {
// nothing to do
if (state.isClean() && !hasLocked) {
return;
}
ensureLockedDataSets();

// Make sure that renderer axes that are not part of
// the chart still produce an accurate axis transform.
updateStandaloneRendererAxes();

// Update the actual Canvas content
final long start = ProcessingProfiler.getTimeStamp();

// Redraw the axes (they internally check dirty bits)
for (Axis axis : axesList) {
axis.drawAxis();
}

// Redraw the main canvas
redrawCanvas();

// Update other components
for (Renderer renderer : renderers) {
renderer.runPostLayout();
}
for (var plugin : plugins) {
plugin.runPostLayout();
}

// Clear bits
clearStates();

// TODO: plugins etc., do locking
ProcessingProfiler.getTimeDiff(start, "updateCanvas()");
}

protected void ensureLockedDataSets() {
if (!hasLocked) {
forEachDataSet(ds -> lockedDataSets.add(ds.lock().readLock()));
hasLocked = true;
}
}

protected void clearStates() {
for (var renderer : getRenderers()) {
if (renderer instanceof EventSource) {
Expand All @@ -593,13 +620,16 @@ protected void clearStates() {
}
}

dataSetState.clear();
state.clear();

for (var ds : lockedDataSets) {
for (AxisDescription axisDescription : ds.getAxisDescriptions()) {
axisDescription.getBitState().clear();
}
ds.getBitState().clear(); // technically a 'write'
ds.lock().readUnLock();
}
hasLocked = false;
lockedDataSets.clear();
}

Expand Down Expand Up @@ -638,6 +668,7 @@ private void forEachDataSet(Consumer<DataSet> action) {
}

private final List<DataSet> lockedDataSets = new ArrayList<>();
private boolean hasLocked = false;

public final ObjectProperty<Legend> legendProperty() {
return legend;
Expand Down
3 changes: 2 additions & 1 deletion chartfx-chart/src/main/java/io/fair_acc/chartfx/XYChart.java
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,9 @@ public void updateAxisRange() {
// are already locked, so we can use parallel stream without extra synchronization.
getAllDatasets().stream()
.filter(DataSet::isVisible)
.filter(ds -> ds.getBitState().isDirty())
.forEach(dataset -> dataset.getAxisDescriptions().parallelStream()
.filter(axisD -> !axisD.isDefined())
.filter(axisD -> !axisD.isDefined() || axisD.getBitState().isDirty())
.forEach(axisDescription -> dataset.recomputeLimits(axisDescription.getDimIndex())));

// Update each of the axes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,19 @@
import javafx.collections.ObservableList;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.layout.Pane;

import javax.swing.event.ChangeListener;

/**
* A hidden group that holds styles. This hides style nodes even
* if the visibility property is set to true.
* A hidden node that holds styles. This hides style nodes even
* if the visibility property is set to true. This node is
* unmanaged and does not do any layout.
*
* @author ennerf
*/
public class StyleGroup extends Group {
public class StyleGroup extends Parent {

public StyleGroup(Pane pane, String... paneStyles) {
this(pane, pane.getChildren(), paneStyles);
Expand All @@ -30,11 +32,19 @@ public StyleGroup(Node parent, ObservableList<Node> children, String... parentSt

public StyleGroup(ObservableList<Node> children) {
StyleUtil.hiddenStyleNode(this);
setAutoSizeChildren(false);
relocate(0, 0);
children.add(this);
}

@Override
public void layoutChildren() {
// do nothing
}

@Override
public ObservableList<Node> getChildren() {
return super.getChildren();
}

public LineStyle newLineStyle(String... styles) {
return addToChildren(new LineStyle(styles));
}
Expand Down

This file was deleted.

Loading

0 comments on commit f10493c

Please sign in to comment.