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
7 changes: 7 additions & 0 deletions connector/src/main/java/devtoolsfx/connector/Connector.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ public interface Connector {
*/
ReadOnlyBooleanProperty startedProperty();

/**
* Becomes {@code true} once {@link #stop()} has been called and stays so for the
* rest of the connector's life. Useful as a one-shot trigger for resource cleanup,
* since {@link #startedProperty()} is also {@code false} before the first start.
*/
ReadOnlyBooleanProperty stoppedProperty();

/**
* Returns the {@link EventBus} to react to the connector events.
*/
Expand Down
23 changes: 17 additions & 6 deletions connector/src/main/java/devtoolsfx/connector/LocalConnector.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import javafx.application.Application;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.value.ChangeListener;
import javafx.collections.ListChangeListener;
import javafx.scene.control.PopupControl;
import javafx.stage.PopupWindow;
Expand Down Expand Up @@ -43,7 +44,14 @@ public final class LocalConnector implements Connector {
private final Map<Integer, WindowMonitor> monitors = new HashMap<>();

private final ListChangeListener<Window> windowListChangeListener = this::onWindowListChanged;
private final ChangeListener<Boolean> inspectModeListener = (obs, old, val) -> {
if (!val) {
// prevents ConcurrentModificationException
new ArrayList<>(monitors.values()).forEach(monitor -> monitor.setInspectMode(false));
}
};
private final ReadOnlyBooleanWrapper started = new ReadOnlyBooleanWrapper();
private final ReadOnlyBooleanWrapper stopped = new ReadOnlyBooleanWrapper();

/**
* See {@link LocalConnector#(Stage, ConnectorOptions , String)}.
Expand Down Expand Up @@ -78,12 +86,7 @@ public LocalConnector(Stage primaryStage,

monitors.put(uidOf(primaryStage), createMonitor(primaryStage, application, true));

this.opts.inspectModeProperty().addListener((obs, old, val) -> {
if (!val) {
// prevents ConcurrentModificationException
new ArrayList<>(monitors.values()).forEach(monitor -> monitor.setInspectMode(false));
}
});
this.opts.inspectModeProperty().addListener(inspectModeListener);
}

@Override
Expand All @@ -100,7 +103,10 @@ public void stop() {
started.set(false);

monitors.forEach((hash, monitor) -> monitor.stop());
monitors.clear();
Window.getWindows().removeListener(windowListChangeListener);
opts.inspectModeProperty().removeListener(inspectModeListener);
stopped.set(true);
LOGGER.log(Level.INFO, "LocalConnector stopped");
}

Expand All @@ -109,6 +115,11 @@ public ReadOnlyBooleanProperty startedProperty() {
return started.getReadOnlyProperty();
}

@Override
public ReadOnlyBooleanProperty stoppedProperty() {
return stopped.getReadOnlyProperty();
}

@Override
public EventBus getEventBus() {
return eventBus;
Expand Down
11 changes: 8 additions & 3 deletions connector/src/main/java/devtoolsfx/connector/WindowMonitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ final class WindowMonitor {

private boolean started;
private final Map<Integer, Subscription> stylesClassSubs = new HashMap<>();
private final ChangeListener<Boolean> inspectModeListener = (obs, old, val) -> refreshRoot();

/**
* Creates a new WindowMonitor instance. Monitors are not reusable; each instance must
Expand All @@ -69,7 +70,7 @@ public WindowMonitor(Window window,

this.attributeListener = new AttributeListener(eventBus, eventSource);

connectorOpts.inspectModeProperty().addListener((obs, old, val) -> refreshRoot());
connectorOpts.inspectModeProperty().addListener(inspectModeListener);
}

///////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -97,8 +98,8 @@ public void start() {
* Stops the monitor.
*/
public void stop() {
started = false;

// changeScene/changeRoot are guarded by `started` and skip work when it's false,
// so we tear listeners down first and only flip the flag at the end.
window.xProperty().removeListener(windowPropertyReportListener);
window.yProperty().removeListener(windowPropertyReportListener);
window.widthProperty().removeListener(windowPropertyReportListener);
Expand All @@ -111,6 +112,10 @@ public void stop() {
// cleanup resources
clearSelection();
inspectPane.hide();

connectorOpts.inspectModeProperty().removeListener(inspectModeListener);

started = false;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion connector/src/main/java/devtoolsfx/util/SceneUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ public static <N, V> void removeListener(@Nullable N node,
Function<N, ObservableValue<V>> obs,
ChangeListener<V> listener) {
if (node != null) {
obs.apply(node).addListener(listener);
obs.apply(node).removeListener(listener);
} else {
LOGGER.log(Level.INFO, "node is null, this behavior is probably not expected");
}
Expand Down
30 changes: 30 additions & 0 deletions gui/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,36 @@
<artifactId>devtoolsfx-connector</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>one.jpro</groupId>
<artifactId>JMemoryBuddy</artifactId>
<version>0.5.6</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<useModulePath>false</useModulePath>
</configuration>
</plugin>
</plugins>
</build>

</project>
14 changes: 11 additions & 3 deletions gui/src/main/java/devtoolsfx/gui/GUI.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import devtoolsfx.connector.LocalConnector;
import javafx.application.HostServices;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
Expand Down Expand Up @@ -51,16 +52,23 @@ public static void openToolStage(Stage primaryStage,
Objects.requireNonNull(primaryStage, "primaryStage can not be null");
Objects.requireNonNull(preferences, "hostServices can not be null");

var toolPane = createToolPane(primaryStage, preferences, applicationName);
var connector = new LocalConnector(primaryStage, applicationName);
var toolPane = new ToolPane(connector, preferences);
var scene = new Scene(toolPane, DEFAULT_STAGE_WIDTH, DEFAULT_STAGE_HEIGHT);
scene.setUserAgentStylesheet(USER_AGENT_STYLESHEET);

var toolStage = new Stage();
toolStage.setScene(scene);
toolStage.setTitle("devtoolsfx");
toolStage.setOnShown(e -> toolPane.getConnector().start());
toolStage.setOnShown(e -> connector.start());

EventHandler<WindowEvent> closeWithPrimary = event -> toolStage.close();
primaryStage.addEventHandler(WindowEvent.WINDOW_CLOSE_REQUEST, closeWithPrimary);
toolStage.setOnHidden(e -> {
primaryStage.removeEventHandler(WindowEvent.WINDOW_CLOSE_REQUEST, closeWithPrimary);
connector.stop();
});

primaryStage.addEventHandler(WindowEvent.WINDOW_CLOSE_REQUEST, event -> toolStage.close());
toolStage.show();
}

Expand Down
60 changes: 52 additions & 8 deletions gui/src/main/java/devtoolsfx/gui/ToolPane.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
import javafx.scene.layout.StackPane;
import javafx.stage.Window;
import javafx.util.Duration;
import javafx.util.Subscription;

import java.util.function.Consumer;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

Expand Down Expand Up @@ -55,7 +58,11 @@ public final class ToolPane extends BorderPane {

private final ChangeListener<Boolean> ignoreMouseTransparentListener;
private final ChangeListener<Boolean> preventPopupAutoHideListener;
private final ChangeListener<Boolean> darkModeListener;
private final Runnable refreshSelectionHandler;
private @Nullable Consumer<ConnectorEvent> eventBusSubscriber;
private @Nullable Subscription preferenceSubscriptions;
private @Nullable Timeline eventDispatcher;

// tabs
private final TabLine tabLine = new TabLine(
Expand Down Expand Up @@ -89,6 +96,7 @@ public ToolPane(Connector connector, Preferences preferences) {

ignoreMouseTransparentListener = (obs, old, val) -> connectorOpts.setIgnoreMouseTransparent(val);
preventPopupAutoHideListener = (obs, old, val) -> connectorOpts.setPreventPopupAutoHide(val);
darkModeListener = (obs, old, val) -> toggleDarkMode(val);
refreshSelectionHandler = () -> getConnector().refreshSelection();

createLayout();
Expand All @@ -98,6 +106,14 @@ public ToolPane(Connector connector, Preferences preferences) {

tabLine.selectTab(InspectorTab.TAB_NAME);
startListenToEvents(false);

// Detach everything once the connector stops, so the pane (and the windows
// it transitively references) can be garbage-collected.
connector.stoppedProperty().subscribe(stopped -> {
if (stopped) {
detachListeners();
}
});
}

/**
Expand Down Expand Up @@ -135,7 +151,7 @@ public void start() {
* See {@link Connector#stop()}}.
*/
public void stop() {
connector.start();
connector.stop();
}

/**
Expand Down Expand Up @@ -241,6 +257,31 @@ public String getUserAgentStylesheet() {
}
}

/**
* Detaches every listener this pane installed on the user's {@link Preferences},
* the connector's event bus, and the running {@link Timeline}, so the pane and
* the windows it transitively references can be garbage-collected. Triggered
* automatically when the connector stops.
*/
private void detachListeners() {
preferences.ignoreMouseTransparentProperty().removeListener(ignoreMouseTransparentListener);
preferences.preventPopupAutoHideProperty().removeListener(preventPopupAutoHideListener);
preferences.darkModeProperty().removeListener(darkModeListener);
if (preferenceSubscriptions != null) {
preferenceSubscriptions.unsubscribe();
preferenceSubscriptions = null;
}
if (eventBusSubscriber != null) {
connector.getEventBus().unsubscribe(eventBusSubscriber);
eventBusSubscriber = null;
}
if (eventDispatcher != null) {
eventDispatcher.stop();
eventDispatcher = null;
}
eventQueue.clear();
}

/**
* Handles the GUI exceptions.
* <p>
Expand Down Expand Up @@ -288,11 +329,13 @@ private void initListeners() {
preferences.preventPopupAutoHideProperty().addListener(preventPopupAutoHideListener);
connectorOpts.setPreventPopupAutoHide(preferences.isPreventPopupAutoHide());

preferences.showLayoutBoundsProperty().subscribe(refreshSelectionHandler);
preferences.showBoundsInParentProperty().subscribe(refreshSelectionHandler);
preferences.showBaselineProperty().subscribe(refreshSelectionHandler);
preferenceSubscriptions = Subscription.combine(
preferences.showLayoutBoundsProperty().subscribe(refreshSelectionHandler),
preferences.showBoundsInParentProperty().subscribe(refreshSelectionHandler),
preferences.showBaselineProperty().subscribe(refreshSelectionHandler)
);

preferences.darkModeProperty().addListener((obs, old, val) -> toggleDarkMode(val));
preferences.darkModeProperty().addListener(darkModeListener);

tabLine.setOnTabSelect(tab -> {
switch (tab) {
Expand Down Expand Up @@ -326,7 +369,7 @@ private void startListenToEvents(boolean useQueue) {
if (useQueue) {
// Optionally, we can update the GUI on a separate queue, which adds a small delay.
// This is how it was implemented before and was left as an option.
Timeline eventDispatcher = new Timeline(new KeyFrame(Duration.millis(60), event -> {
eventDispatcher = new Timeline(new KeyFrame(Duration.millis(60), event -> {
// no need to synchronize
while (!eventQueue.isEmpty()) {
try {
Expand All @@ -341,7 +384,7 @@ private void startListenToEvents(boolean useQueue) {
eventDispatcher.play();
}

connector.getEventBus().subscribe(ConnectorEvent.class, event -> {
eventBusSubscriber = event -> {
if (event instanceof MousePosEvent) {
// traffic protection
if (System.currentTimeMillis() - lastMousePos < 500) {
Expand All @@ -355,7 +398,8 @@ private void startListenToEvents(boolean useQueue) {
} else {
dispatchEvent(event);
}
});
};
connector.getEventBus().subscribe(ConnectorEvent.class, eventBusSubscriber);
}

private void dispatchEvent(ConnectorEvent connectorEvent) {
Expand Down
Loading