Connect minimal hosts to snapshots, target workers and shared Kit editing

This commit is contained in:
2026-11-12 11:07:45 +01:00
parent 28c1e47c95
commit 7db7218a6e
52 changed files with 4236 additions and 29 deletions
@@ -0,0 +1,23 @@
package de.neitzel.fxdesigner.intellij;
import com.intellij.openapi.fileEditor.*;
import com.intellij.openapi.project.*;
import com.intellij.openapi.vfs.VirtualFile;
public final class DesignerEditorProvider implements FileEditorProvider, DumbAware {
public boolean accept(Project project, VirtualFile file) {
return !file.isDirectory() && "fxml".equalsIgnoreCase(file.getExtension());
}
public FileEditor createEditor(Project project, VirtualFile file) {
return new DesignerFileEditor(project, file);
}
public String getEditorTypeId() {
return "neitzel-fx-designer";
}
public FileEditorPolicy getPolicy() {
return FileEditorPolicy.PLACE_AFTER_DEFAULT_EDITOR;
}
}
@@ -0,0 +1,171 @@
package de.neitzel.fxdesigner.intellij;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.fileEditor.*;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.UserDataHolderBase;
import com.intellij.openapi.vfs.VirtualFile;
import de.neitzel.fxdesigner.kit.DesignerPane;
import de.neitzel.fxdesigner.workflow.HostLaunch;
import java.awt.BorderLayout;
import java.beans.*;
import java.nio.file.*;
import javafx.application.Platform;
import javafx.embed.swing.JFXPanel;
import javafx.scene.Scene;
import javax.swing.*;
/** IDE-owned FXML remains available in its normal text editor when designer setup fails. */
public final class DesignerFileEditor extends UserDataHolderBase implements FileEditor {
private final VirtualFile file;
private final JPanel panel = new JPanel(new BorderLayout());
private final JLabel status = new JLabel("Checking exclusive JavaFX setup…");
private final PropertyChangeSupport changes = new PropertyChangeSupport(this);
private volatile boolean disposed;
private IntellijHost host;
private DesignerPane pane;
private JFXPanel bridge;
private Timer gate;
DesignerFileEditor(Project project, VirtualFile file) {
this.file = file;
panel.add(status, BorderLayout.NORTH);
ApplicationManager.getApplication()
.executeOnPooledThread(
() -> {
try {
ToolkitGate.check(project, file);
Path settings =
Path.of(
System.getProperty(
"neitzel.designer.launch",
Path.of(
project.getBasePath(),
".neitzel-fx-designer",
"host.properties")
.toString()));
var launch = HostLaunch.load(settings);
SwingUtilities.invokeLater(
() -> {
if (disposed) return;
try {
host =
new IntellijHost(
project,
launch.runtimes(),
d -> SwingUtilities.invokeLater(() -> status.setText(d.message())));
var previous = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
bridge = new JFXPanel();
} finally {
Thread.currentThread().setContextClassLoader(previous);
}
panel.add(bridge, BorderLayout.CENTER);
panel.revalidate();
Platform.setImplicitExit(false);
Platform.runLater(
() -> {
if (disposed) return;
try {
Thread.currentThread()
.setContextClassLoader(getClass().getClassLoader());
pane =
new DesignerPane(
host.services(), launch, Path.of(file.getPath()));
bridge.setScene(new Scene(pane, 1200, 800));
SwingUtilities.invokeLater(
() ->
status.setText(
"Neitzel FX Designer · JavaFX 26 · native IDE"
+ " documents"));
} catch (Exception e) {
failed(e);
}
});
gate =
new Timer(
500,
e -> {
if (!ToolkitGate.allowed()) {
status.setText(
"JavaFX configuration changed; restart required");
Platform.runLater(
() -> {
if (pane != null) {
pane.close();
pane.setDisable(true);
}
});
}
});
gate.start();
} catch (Exception e) {
failed(e);
}
});
} catch (Exception e) {
failed(e);
}
});
}
private void failed(Throwable error) {
SwingUtilities.invokeLater(
() ->
status.setText(
"Designer unavailable: "
+ error.getMessage()
+ ". Source editing remains available."));
}
public JComponent getComponent() {
return panel;
}
public JComponent getPreferredFocusedComponent() {
return bridge == null ? panel : bridge;
}
public String getName() {
return "Neitzel Designer";
}
public boolean isModified() {
return false;
}
public boolean isValid() {
return !disposed && file.isValid();
}
public VirtualFile getFile() {
return file;
}
public void setState(FileEditorState state) {}
public void addPropertyChangeListener(PropertyChangeListener listener) {
changes.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
changes.removePropertyChangeListener(listener);
}
public void dispose() {
disposed = true;
if (gate != null) gate.stop();
if (pane != null)
Platform.runLater(
() ->
pane.flush()
.whenComplete(
(v, error) -> {
pane.close();
bridge.setScene(null);
if (host != null) host.close();
}));
else if (host != null) host.close();
}
}
@@ -23,6 +23,14 @@ import java.util.concurrent.atomic.AtomicLong;
* store.
*/
public final class IntellijDocuments implements HostServices.Documents, AutoCloseable {
private final java.util.concurrent.CopyOnWriteArrayList<java.util.function.Consumer<URI>>
changes = new java.util.concurrent.CopyOnWriteArrayList<>();
public AutoCloseable onChange(java.util.function.Consumer<URI> listener) {
changes.add(listener);
return () -> changes.remove(listener);
}
private final Project project;
private final com.intellij.openapi.Disposable lifetime;
private final Map<Path, State> states = new HashMap<>();
@@ -40,6 +48,17 @@ public final class IntellijDocuments implements HostServices.Documents, AutoClos
this.project = Objects.requireNonNull(project);
lifetime = this::disposeResources;
Disposer.register(project, lifetime);
EditorFactory.getInstance()
.getEventMulticaster()
.addDocumentListener(
new DocumentListener() {
public void documentChanged(DocumentEvent event) {
var file = FileDocumentManager.getInstance().getFile(event.getDocument());
if (file != null && file.isInLocalFileSystem())
changes.forEach(listener -> listener.accept(canonicalUri(file)));
}
},
lifetime);
}
private <T> CompletionStage<T> edt(Callable<T> action) {
@@ -57,18 +76,19 @@ public final class IntellijDocuments implements HostServices.Documents, AutoClos
}
});
if (ApplicationManager.getApplication().isDispatchThread()) task.run();
else ApplicationManager.getApplication().invokeLater(task, ModalityState.any());
else ApplicationManager.getApplication().invokeLater(task, ModalityState.nonModal());
return future;
}
public CompletionStage<DocumentSession> open(URI uri) {
return edt(
() -> {
Path path = Path.of(uri).toRealPath();
Path supplied = Path.of(uri).toAbsolutePath().normalize();
Path path = supplied.toRealPath();
State state = states.get(path);
if (state == null) {
state = new State();
state.file = LocalFileSystem.getInstance().refreshAndFindFileByNioFile(path);
state.file = LocalFileSystem.getInstance().refreshAndFindFileByNioFile(supplied);
if (state.file == null) throw new IllegalArgumentException("Missing file: " + path);
state.document = FileDocumentManager.getInstance().getDocument(state.file);
if (state.document == null) throw new IllegalArgumentException("Not a text document");
@@ -90,6 +110,17 @@ public final class IntellijDocuments implements HostServices.Documents, AutoClos
});
}
public CompletionStage<List<DocumentSnapshot>> snapshots() {
return edt(
() -> {
unsaved().toCompletableFuture().join();
for (var file : FileEditorManager.getInstance(project).getOpenFiles())
if (file.isInLocalFileSystem() && "fxml".equalsIgnoreCase(file.getExtension()))
open(Path.of(file.getPath()).toUri()).toCompletableFuture().join().close();
return states.values().stream().map(this::snapshot).toList();
});
}
public CompletionStage<List<DocumentSnapshot>> unsaved() {
return edt(
() -> {
@@ -110,9 +141,17 @@ public final class IntellijDocuments implements HostServices.Documents, AutoClos
});
}
private static URI canonicalUri(VirtualFile file) {
try {
return Path.of(file.getPath()).toRealPath().toUri();
} catch (java.io.IOException e) {
return Path.of(file.getPath()).toAbsolutePath().normalize().toUri();
}
}
private DocumentSnapshot snapshot(State s) {
return new DocumentSnapshot(
Path.of(s.file.getPath()).toUri(),
canonicalUri(s.file),
s.revision.get(),
s.document.getText(),
s.document.isWritable() && s.file.isWritable(),
@@ -0,0 +1,70 @@
package de.neitzel.fxdesigner.intellij;
import com.intellij.openapi.application.*;
import com.intellij.openapi.fileEditor.FileEditorProvider;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.registry.*;
import com.intellij.openapi.vfs.VirtualFile;
import java.nio.file.*;
import java.util.concurrent.*;
/**
* Qualified exclusive JavaFX 26 setup. Never initializes Glass when bundled embedding is enabled.
*/
final class ToolkitGate {
private static boolean initialized;
private static volatile boolean revoked;
static synchronized void check(Project project, VirtualFile file) throws Exception {
if (revoked) throw new IllegalStateException("JavaFX configuration changed; restart IntelliJ");
if (!ApplicationInfo.getInstance().getBuild().asString().endsWith("263.7307.200"))
throw new IllegalStateException("Designer requires the qualified IntelliJ 263.7307.200 SDK");
if (Registry.is("embed.scene.builder"))
throw new IllegalStateException(
"Disable embed.scene.builder in IntelliJ Registry and restart. Keep JavaFX/FXML language"
+ " support enabled.");
var provider =
FileEditorProvider.EP_FILE_EDITOR_PROVIDER.getExtensionList().stream()
.filter(p -> p.getEditorTypeId().equals("JavaFX-Scene-Builder"))
.findFirst()
.orElseThrow(
() -> new IllegalStateException("Bundled Scene Builder contract unavailable"));
if (ReadAction.compute(() -> provider.accept(project, file)))
throw new IllegalStateException("Bundled Scene Builder still accepts FXML; restart IntelliJ");
if (initialized) return;
Path log = Files.createTempFile("designer-native-", ".txt");
try {
var process =
new ProcessBuilder(
Path.of(System.getProperty("java.home"), "bin", "jcmd").toString(),
Long.toString(ProcessHandle.current().pid()),
"VM.dynlibs")
.redirectErrorStream(true)
.redirectOutput(log.toFile())
.start();
if (!process.waitFor(15, TimeUnit.SECONDS)) {
process.destroyForcibly();
throw new IllegalStateException("Cannot inspect loaded JavaFX libraries");
}
if (process.exitValue() != 0 || Files.readString(log).contains("libglass."))
throw new IllegalStateException(
"JavaFX already loaded or native inspection unavailable; restart with bundled Scene"
+ " Builder disabled");
} finally {
Files.deleteIfExists(log);
}
Registry.get("embed.scene.builder")
.addListener(
new RegistryValueListener() {
public void afterValueChanged(RegistryValue value) {
revoked = true;
}
},
ApplicationManager.getApplication());
initialized = true;
}
static boolean allowed() {
return !revoked && !Registry.is("embed.scene.builder");
}
}