, whitelisted in
+# key.ui.fx/build.gradle); each reports PASS/FAIL on stdout. A flag run "exhausts its budget"
+# when the hook verified within the budget but the app stays open — the runner SIGTERMs it and
+# the verdict is taken from the log afterwards (exit 143 / BUILD FAILED after the markers are
+# the expected runner kill, not a failure).
+set -u
+export PATH="$HOME/.local/bin:$PATH"
+W=$(cd "$(dirname "$0")/../.." && pwd)
+EX=$W/key.ui/examples
+AGATHA=$EX/firstTouch/01-Agatha/project.key
+STRASSEN=$EX/standard_key/java_dl/strassen/strassen.auto.0.proof
+NORM=$EX/standard_key/quantifiers/normalisation0.key
+USEQ=$EX/standard_key/queries/useQuery.key
+GCD=$W/key.core/src/test/resources/testcase/merge/gcd.mergePointStatements.key
+OUT=${UI_VERIFY_OUT:-$W/build/ui-verify}
+mkdir -p "$OUT/tmp"
+: > "$OUT/RESULTS.md"
+cd "$W"
+
+ALL_FLAGS=(sequent tree goallist notifications docking help soundiness profileloading
+ javacsettings termmenu colors shortcuts sequentmenu dialogs search sequentsearch
+ sequentsearchmodes treefilters tacletmatch inputfreeze updatehighlight lemmaorigin
+ joinmerge joinmerge-gcd proofmgmt-all extensions exploration caching prooftree slicing
+ testgen heatmap isabelle sourceconsistency autosave minimizeinteraction themepersist
+ profileloadingdialog smt loadingexit proofdiff drawer drawerlayout rightclickmacro)
+
+start_display() {
+ local log="$OUT/xvfb-$$.log"
+ Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp > "$log" 2>&1 &
+ XVFB_PID=$!
+ export DISPLAY=:99
+ trap 'kill $XVFB_PID 2>/dev/null' EXIT
+ for _ in $(seq 1 20); do
+ sleep 0.5
+ grep -aq 'Virtual framebuffer' "$log" 2>/dev/null && break
+ done
+}
+
+run_flag() {
+ local name="$1" budget="$2"; shift 2
+ local log="$OUT/$name.log"
+ pkill -f '[d]e.uka.ilkd.key.core.fx.MainEx' 2>/dev/null; sleep 2
+ export JAVA_TOOL_OPTIONS="-Xmx6g -Dprism.order=sw -Djava.io.tmpdir=$OUT/tmp $*"
+ ./gradlew --no-daemon --max-workers=1 :key.ui.fx:run > "$log" 2>&1 &
+ local gpid=$! done=0
+ for _ in $(seq 1 $((budget / 5))); do
+ sleep 5
+ if ! kill -0 "$gpid" 2>/dev/null; then done=1; break; fi
+ done
+ if [ "$done" -eq 0 ]; then
+ echo "-- $name: budget end (expected unless the flag exits by itself)" >> "$OUT/RESULTS.md"
+ else
+ echo "-- $name: app exited on its own" >> "$OUT/RESULTS.md"
+ fi
+ sleep 2; pkill -f '[d]e.uka.ilkd.key.core.fx.MainEx' 2>/dev/null; sleep 1
+}
+
+summarize() {
+ local name="$1"
+ local count; count=$(grep -acE '(PASS|verification: OK)' "$OUT/$name.log" 2>/dev/null || echo 0)
+ local hits; hits=$(grep -aE '(PASS|verification: OK)' "$OUT/$name.log" 2>/dev/null | tail -2 | sed 's/\x1b\[[0-9;]*m//g' | cut -c1-190)
+ local fails; fails=$(grep -aE 'verification.*FAIL|verify.*FAIL|=FAIL|FAIL \(|Exception in thread' "$OUT/$name.log" 2>/dev/null | grep -av 'BUILD FAILED\|Task :key.ui.fx:run FAILED\|FAILURE:' | head -2 | sed 's/\x1b\[[0-9;]*m//g' | cut -c1-150)
+ { echo "### $name (PASS-lines=$count)"; [ -n "$hits" ] && echo "$hits"; [ -n "$fails" ] && echo "FAIL-LINES:" && echo "$fails"; echo; } >> "$OUT/RESULTS.md"
+ if [ "$count" -eq 0 ] || [ -n "$fails" ]; then
+ echo "$name" >> "$OUT/failed-flags.txt"
+ fi
+}
+
+# name -> budget + JVM properties (the demo/verify recipe catalog)
+flag_recipe() {
+ case "$1" in
+ sequent) run_flag sequent 300 -Dkey.fx.verify.sequent=1 -Dkey.fx.demo.sequent=$AGATHA ;;
+ tree) run_flag tree 300 -Dkey.fx.verify.tree=1 -Dkey.fx.demo.sequent=$AGATHA ;;
+ goallist) run_flag goallist 300 -Dkey.fx.verify.goallist=1 -Dkey.fx.demo.sequent=$AGATHA ;;
+ notifications) run_flag notifications 300 -Dkey.fx.verify.notifications=1 -Dkey.fx.demo.sequent=$AGATHA ;;
+ docking) run_flag docking 300 -Dkey.fx.verify.docking=1 -Dkey.fx.demo.sequent=$AGATHA ;;
+ help) run_flag help 300 -Dkey.fx.verify.help=1 -Dkey.fx.demo.sequent=$AGATHA ;;
+ soundiness) run_flag soundiness 300 -Dkey.fx.verify.soundiness=1 -Dkey.fx.demo.sequent=$AGATHA ;;
+ profileloading) run_flag profileloading 300 -Dkey.fx.verify.profileloading=1 -Dkey.fx.demo.sequent=$AGATHA ;;
+ javacsettings) run_flag javacsettings 300 -Dkey.fx.verify.javacsettings=1 -Dkey.fx.demo.sequent=$AGATHA ;;
+ termmenu) run_flag termmenu 300 -Dkey.fx.verify.termmenu=1 -Dkey.fx.demo.sequent=$AGATHA ;;
+ colors) run_flag colors 300 -Dkey.fx.verify.colors=1 -Dkey.fx.demo.sequent=$AGATHA ;;
+ shortcuts) run_flag shortcuts 300 -Dkey.fx.verify.shortcuts=1 -Dkey.fx.demo.sequent=$AGATHA ;;
+ sequentmenu) run_flag sequentmenu 300 -Dkey.fx.verify.sequentmenu=1 -Dkey.fx.demo.sequent=$AGATHA ;;
+ dialogs) run_flag dialogs 420 -Dkey.fx.verify.dialogs=1 -Dkey.fx.verify.dialogs.contract=1 -Dkey.fx.demo.sequent=$AGATHA ;;
+ search) run_flag search 420 -Dkey.fx.verify.search=andleft -Dkey.fx.demo.sequent=$AGATHA -Dkey.fx.demo.autoprove.live=1 ;;
+ sequentsearch) run_flag sequentsearch 420 -Dkey.fx.verify.sequentsearch=agatha -Dkey.fx.demo.sequent=$AGATHA -Dkey.fx.demo.autoprove.live=1 ;;
+ sequentsearchmodes) run_flag sequentsearchmodes 420 -Dkey.fx.verify.sequentsearchmodes=1 -Dkey.fx.demo.sequent=$AGATHA -Dkey.fx.demo.autoprove.live=1 ;;
+ treefilters) run_flag treefilters 420 -Dkey.fx.verify.treefilters=1 -Dkey.fx.demo.sequent=$AGATHA -Dkey.fx.demo.autoprove.live=1 ;;
+ tacletmatch) run_flag tacletmatch 300 -Dkey.fx.verify.tacletmatch=1 -Dkey.fx.demo.sequent=$AGATHA ;;
+ inputfreeze) run_flag inputfreeze 360 -Dkey.fx.verify.inputfreeze=1 -Dkey.fx.demo.sequent=$AGATHA -Dkey.fx.demo.autoprove=1 ;;
+ updatehighlight) run_flag updatehighlight 300 -Dkey.fx.verify.updatehighlight=1 -Dkey.fx.demo.sequent=$USEQ ;;
+ lemmaorigin) run_flag lemmaorigin 300 -Dkey.fx.verify.lemmaorigin=1 -Dkey.fx.demo.sequent=$STRASSEN ;;
+ joinmerge) run_flag joinmerge 300 -Dkey.fx.verify.joinmerge=1 -Dkey.fx.demo.sequent=$AGATHA ;;
+ joinmerge-gcd) run_flag joinmerge-gcd 300 -Dkey.fx.verify.joinmerge=1 -Dkey.fx.verify.joinmerge.maxsteps= -Dkey.fx.demo.sequent=$GCD ;;
+ proofmgmt-all) run_flag proofmgmt-all 400 -Dkey.fx.verify.proofmgmt=all -Dkey.fx.demo.sequent=$AGATHA ;;
+ extensions) run_flag extensions 420 -Dkey.fx.verify.extensions=1 -Dkey.fx.demo.sequent=$AGATHA ;;
+ exploration) run_flag exploration 420 -Dkey.fx.verify.exploration=1 -Dkey.fx.demo.sequent=$AGATHA ;;
+ caching) run_flag caching 420 -Dkey.fx.verify.caching=1 -Dkey.fx.demo.sequent=$AGATHA ;;
+ prooftree) run_flag prooftree 420 -Dkey.fx.verify.prooftree=1 -Dkey.fx.demo.sequent=$AGATHA -Dkey.fx.demo.autoprove=1 ;;
+ slicing) run_flag slicing 420 -Dkey.fx.verify.slicing=1 -Dkey.fx.demo.sequent=$AGATHA ;;
+ testgen) run_flag testgen 420 -Dkey.fx.verify.testgen=1 -Dkey.fx.demo.sequent=$AGATHA ;;
+ heatmap) run_flag heatmap 420 -Dkey.fx.verify.heatmap=1 -Dkey.fx.demo.sequent=$STRASSEN ;;
+ isabelle) run_flag isabelle 300 -Dkey.fx.verify.isabelle=1 -Dkey.fx.demo.sequent=$NORM ;;
+ sourceconsistency) run_flag sourceconsistency 300 -Dkey.fx.verify.sourceconsistency=1 ;;
+ autosave) run_flag autosave 420 -Dkey.fx.verify.autosave=1 -Dkey.fx.demo.sequent=$AGATHA ;;
+ minimizeinteraction) run_flag minimizeinteraction 420 -Dkey.fx.verify.minimizeinteraction=1 -Dkey.fx.demo.sequent=$AGATHA ;;
+ # themepersist: the restore leg reads the preference persisted by the save leg, so the
+ # two runs must stay inside one invocation
+ themepersist)
+ run_flag themepersist-s 300 -Dkey.fx.verify.themepersist=save -Dkey.fx.demo.sequent=$AGATHA
+ run_flag themepersist-r 300 -Dkey.fx.verify.themepersist=restore -Dkey.fx.demo.sequent=$AGATHA
+ ;;
+ # modal-dialog flag (needs key dismissal via XTEST/xdotool)
+ profileloadingdialog)
+ pkill -f '[d]e.uka.ilkd.key.core.fx.MainEx' 2>/dev/null; sleep 2
+ export JAVA_TOOL_OPTIONS="-Xmx6g -Dprism.order=sw -Djava.io.tmpdir=$OUT/tmp -Dkey.fx.verify.profileloadingdialog=1 -Dkey.fx.demo.sequent=$AGATHA"
+ ./gradlew --no-daemon --max-workers=1 :key.ui.fx:run > "$OUT/profileloadingdialog.log" 2>&1 &
+ GPID=$!
+ sleep 40; xdotool key Return 2>/dev/null
+ sleep 3; xdotool key Escape 2>/dev/null || true
+ for _ in $(seq 1 72); do
+ sleep 5
+ if ! kill -0 "$GPID" 2>/dev/null; then break; fi
+ done
+ pkill -f '[d]e.uka.ilkd.key.core.fx.MainEx' 2>/dev/null; sleep 2
+ ;;
+ # SMT with real z3 from keyproject/setup-smt; apply/undo round trip
+ smt) run_flag smt 900 -Dkey.fx.verify.smt=1 -Dkey.fx.demo.sequent=$AGATHA ;;
+ # loadingexit: app must terminate ITSELF with exit code 0 (no pkill allowed)
+ loadingexit)
+ pkill -f '[d]e.uka.ilkd.key.core.fx.MainEx' 2>/dev/null; sleep 2
+ # the hook runs in the demo-load callback (like the other proof-dependent verify hooks),
+ # so the demo must be passed — without it the app never reaches the exit flow and the
+ # wait below runs into the job timeout
+ export JAVA_TOOL_OPTIONS="-Xmx6g -Dprism.order=sw -Djava.io.tmpdir=$OUT/tmp -Dkey.fx.verify.loadingexit=1 -Dkey.fx.demo.sequent=$AGATHA"
+ ./gradlew --no-daemon --max-workers=1 :key.ui.fx:run > "$OUT/loadingexit.log" 2>&1 &
+ GPID=$!; wait "$GPID"; rc=$?
+ echo "loadingexit exit-code=$rc" >> "$OUT/RESULTS.md"
+ if [ "$rc" -ne 0 ]; then echo "loadingexit" >> "$OUT/failed-flags.txt"; fi
+ ;;
+ proofdiff) run_flag proofdiff 300 -Dkey.fx.verify.proofdiff=1 -Dkey.fx.demo.sequent=$AGATHA ;;
+ drawer) run_flag drawer 300 -Dkey.fx.verify.drawer=1 -Dkey.fx.demo.sequent=$AGATHA ;;
+ drawerlayout) run_flag drawerlayout 300 -Dkey.fx.verify.drawerlayout=1 -Dkey.fx.demo.sequent=$AGATHA ;;
+ rightclickmacro) run_flag rightclickmacro 300 -Dkey.fx.verify.rightclickmacro=1 -Dkey.fx.demo.sequent=$AGATHA ;;
+ *) echo "unknown flag: $1 (known: ${ALL_FLAGS[*]})" >&2; return 1 ;;
+ esac
+}
+
+finish() {
+ local f
+ for f in "$@"; do
+ # themepersist runs under two log names (save/restore, see flag_recipe)
+ if [ "$f" = themepersist ]; then
+ summarize themepersist-s
+ summarize themepersist-r
+ else
+ summarize "$f"
+ fi
+ done
+ echo "SWEEP COMPLETE (flags: $*)" >> "$OUT/RESULTS.md"
+}
+
+# --- main --------------------------------------------------------------------------
+: > "$OUT/failed-flags.txt"
+start_display
+if [ $# -eq 0 ]; then
+ RUN=( "${ALL_FLAGS[@]}" )
+else
+ RUN=( "$@" )
+fi
+RAN=()
+for f in "${RUN[@]}"; do
+ echo "== $f" >> "$OUT/RESULTS.md"
+ flag_recipe "$f"
+ RAN+=( "$f" )
+done
+finish "${RAN[@]}"
+if [ -s "$OUT/failed-flags.txt" ]; then
+ echo "::error::UI verify sweep failed flags: $(paste -sd, "$OUT/failed-flags.txt")"
+ exit 1
+fi
+echo "UI verify sweep: all flags PASS (flags: ${RAN[*]})"
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/core/fx/GUIListenerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/core/fx/GUIListenerF.java
new file mode 100644
index 00000000000..105236df218
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/core/fx/GUIListenerF.java
@@ -0,0 +1,33 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.core.fx;
+
+import java.util.EventListener;
+import java.util.EventObject;
+
+/**
+ * Defines the interface for an object that listens to actions of gui components, e.g. it is
+ * informed if a gui component requests modal access — the JavaFX counterpart of
+ * {@code de.uka.ilkd.key.gui.GUIListener} in the Swing module {@code key.ui} (GUIListener.java
+ * :14-27), which is not importable into {@code key.ui.fx} (no dependency on key.ui).
+ *
+ * Listeners are registered at the mediator ({@link KeYMediatorF#addGUIListener(GUIListenerF)}),
+ * which fires the three events via {@link KeYMediatorF#fireModalDialogOpened(EventObject)},
+ * {@link KeYMediatorF#fireModalDialogClosed(EventObject)} and
+ * {@link KeYMediatorF#fireShutDown(EventObject)} — the exit flow of {@code MainWindowF} fires
+ * the shutdown event like Swing's {@code ExitMainAction.exitMainWithoutInteraction}
+ * (ExitMainAction.java:83).
+ */
+public interface GUIListenerF extends EventListener {
+
+ /** invoked if a frame that wants modal access is opened */
+ void modalDialogOpened(EventObject e);
+
+ /** invoked if the frame holding modal access has been closed */
+ void modalDialogClosed(EventObject e);
+
+ /** invoked if the user wants to abort the proving session */
+ void shutDown(EventObject e);
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/core/fx/KeYMediatorF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/core/fx/KeYMediatorF.java
new file mode 100644
index 00000000000..315e50c684a
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/core/fx/KeYMediatorF.java
@@ -0,0 +1,848 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.core.fx;
+
+import java.util.EventObject;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.function.Consumer;
+import javafx.beans.property.ReadOnlyBooleanProperty;
+import javafx.beans.property.ReadOnlyBooleanWrapper;
+
+import de.uka.ilkd.key.control.AutoModeListener;
+import de.uka.ilkd.key.control.ProofControl;
+import de.uka.ilkd.key.gui.fx.UserActionListenerF;
+import de.uka.ilkd.key.gui.fx.actions.useractions.UserActionF;
+import de.uka.ilkd.key.java.Services;
+import de.uka.ilkd.key.pp.NotationInfo;
+import de.uka.ilkd.key.proof.Goal;
+import de.uka.ilkd.key.proof.Node;
+import de.uka.ilkd.key.proof.Proof;
+import de.uka.ilkd.key.proof.ProofEvent;
+import de.uka.ilkd.key.proof.ProofTreeAdapter;
+import de.uka.ilkd.key.proof.ProofTreeEvent;
+import de.uka.ilkd.key.proof.ProofTreeListener;
+import de.uka.ilkd.key.proof.RuleAppListener;
+import de.uka.ilkd.key.proof.io.AutoSaver;
+import de.uka.ilkd.key.rule.OneStepSimplifier;
+
+import org.key_project.prover.rules.RuleApp;
+import org.key_project.util.collection.ImmutableList;
+import org.key_project.util.javafx.FxUtil;
+import org.key_project.util.lookup.Lookup;
+
+import org.jspecify.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The mediator of the JavaFX user interface, counter-part of {@code de.uka.ilkd.key.core.
+ * KeYMediator} in the Swing module {@code key.ui}.
+ *
+ * Milestone M2, skeleton. The mediator owns the {@link KeYSelectionModel} (it is the
+ * model's {@link KeYSelectionModel.ProofBinder}: {@link #setProof(Proof, Proof)} is called
+ * whenever the selected proof changes) and shares the {@link NotationInfo} of the displayed
+ * proof: {@link #setProof(Proof, Proof)} swaps the proof listeners, rebinds the proof's
+ * abbreviation map into the notation info and refreshes the one-step simplifier — exactly like
+ * the Swing mediator. Interactive rule applications re-default the selection
+ * ({@link #defaultSelection()}) so the attached views update; during auto mode the events are
+ * suspended by the core ({@code Proof#suspendNonEssentialListeners}) and the views refresh from
+ * the final state on {@code autoModeStopped}.
+ *
+ * Deliberately deferred: notifications ({@code ProofClosedNotificationEvent} needs the Swing
+ * notification events of key.ui — the FX notification arrives with M3), user
+ * action listeners and the task/progress plumbing of the Swing mediator. The auto saver is
+ * wired since MP7 ({@link #setAutoSave(int)} / {@link #getAutoSaver()}, mirroring the Swing
+ * mediator).
+ */
+public final class KeYMediatorF implements KeYSelectionModel.ProofBinder {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(KeYMediatorF.class);
+
+ /** the notation info shared by all views printing terms/sequents (abbreviations, unicode). */
+ private final NotationInfo notationInfo = new NotationInfo();
+
+ /** the selection model, {@link #setProof(Proof, Proof)} is bound as its {@link ProofBinder}. */
+ private final KeYSelectionModel keySelectionModel = new KeYSelectionModel(this);
+
+ private final ProofTreeListener proofTreeListener = new MediatorProofTreeListener();
+
+ private final RuleAppListenerProofListener proofListener = new RuleAppListenerProofListener();
+
+ /** number of goals closed by the last auto mode run (for the status line in M3). */
+ private int goalsClosedByAutoMode;
+
+ /** whether an auto mode run is currently active (tracked via {@link #proofListener}). */
+ private volatile boolean inAutoMode;
+
+ /** the proof control this mediator is attached to, may be {@code null} until M3. */
+ private ProofControl proofControl;
+
+ /**
+ * menu: MP7 — the optional {@link AutoSaver} (Swing {@code KeYMediator} field,
+ * KeYMediator.java:85-87); armed/disarmed via {@link #setAutoSave(int)}, the saver follows
+ * the selected proof through {@link #setProof(Proof, Proof)}.
+ */
+ private AutoSaver autoSaver;
+
+ // ------------------------------------------------------------------
+ // WP-1 — the mediator seams of the Swing KeYMediator that the FX extension ports and
+ // the exit flow were missing (see KNOWN-SIMPLIFIED.md: caching CachingExtensionF.java:778,
+ // exploration ExplorationExtensionF.java:280, slicing SlicingExtensionF.java:49,
+ // MainWindowF.java:3721). Swing sources: de.uka.ilkd.key.core.KeYMediator.
+ // ------------------------------------------------------------------
+
+ /**
+ * WP-1 — the currently opened proofs (Swing {@code KeYMediator.currentlyOpenedProofs},
+ * KeYMediator.java:92-96, accessor :800-808): a live mutable list — Swing's {@code
+ * TaskTree.addProof} adds every registered proof (TaskTree.java:117) and
+ * {@code ProofTreeView} removes the disposed ones (ProofTreeView.java:726). In the FX UI the
+ * {@code ProofManagerF} multi-proof state is the single registration point; {@code
+ * MainWindowF} mirrors it into this list.
+ */
+ private final List currentlyOpenedProofs = new CopyOnWriteArrayList<>();
+
+ /**
+ * WP-1 — registered proof load listeners (Swing {@code KeYMediator.proofLoadListeners},
+ * KeYMediator.java:99-104): called whenever a new proof is loaded, but before it is
+ * replayed, via {@link #fireProofLoaded(Proof)} (Swing
+ * {@code WindowUserInterfaceControl.registerProofAggregate}, WindowUserInterfaceControl.java
+ * :519-525). A listener MUST be able to accept the same proof twice.
+ */
+ private final List> proofLoadListeners = new CopyOnWriteArrayList<>();
+
+ /**
+ * WP-1 — the registered GUI listeners (Swing {@code KeYMediator.listenerList},
+ * KeYMediator.java:60).
+ */
+ private final List guiListeners = new CopyOnWriteArrayList<>();
+
+ /**
+ * WP-7 — the registered user-action listeners (Swing {@code KeYMediator
+ * .userActionListeners}, KeYMediator.java:815-825).
+ */
+ private final List userActionListeners = new CopyOnWriteArrayList<>();
+
+ /**
+ * WP-1 — user-defined data / extension-object registry (Swing
+ * {@code KeYMediator.userData}, KeYMediator.java:585).
+ */
+ private @Nullable Lookup userData;
+
+
+ /**
+ * Observable auto mode state for the UI (buttons and menu items bind their enabled state to
+ * it); updated on the FX thread from the auto mode events.
+ */
+ private final ReadOnlyBooleanWrapper autoModeRunning =
+ new ReadOnlyBooleanWrapper(this, "autoModeRunning");
+
+ /**
+ * Creates the mediator. The selection model is owned by the mediator; views and the main
+ * window obtain it via {@link #getSelectionModel()}.
+ */
+ public KeYMediatorF() {
+ }
+
+ /**
+ * Registers the mediator's auto mode listener on the given proof control (the Swing ctor does
+ * this with {@code ui.getProofControl()}). Without attachment the mediator still works, but
+ * {@link #isInAutoMode()} stays {@code false}.
+ *
+ * @param control the proof control of the application
+ */
+ public void attach(ProofControl control) {
+ this.proofControl = control;
+ control.addAutoModeListener(proofListener);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Swaps the mediator's proof listeners to the new proof, rebinds the proof's abbreviation map
+ * into the shared {@link #getNotationInfo() notation info} and refreshes the one-step
+ * simplifier (Swing {@code KeYMediator#setProof}).
+ */
+ @Override
+ public void setProof(Proof newProof, Proof previousProof) {
+ if (previousProof == newProof) {
+ return;
+ }
+ LOGGER.debug("Mediator binding proof: {} -> {}",
+ previousProof == null ? null : previousProof.name(),
+ newProof == null ? null : newProof.name());
+ if (previousProof != null) {
+ previousProof.removeProofTreeListener(proofTreeListener);
+ previousProof.removeRuleAppListener(proofListener);
+ }
+ if (newProof != null) {
+ notationInfo.setAbbrevMap(newProof.abbreviations());
+ newProof.addProofTreeListener(proofTreeListener);
+ newProof.addRuleAppListener(proofListener);
+ }
+ // menu: MP7 — the auto saver follows the selected proof (Swing
+ // {@code KeYMediator.setProof}, KeYMediator.java:240-241: {@code
+ // getAutoSaver().setProof(newProof)}); the saver accepts {@code null} (abandoned proof)
+ if (getAutoSaver() != null) {
+ getAutoSaver().setProof(newProof);
+ }
+ OneStepSimplifier.refreshOSS(newProof);
+ }
+
+ /**
+ * menu: MP7 — arms or disarms the {@link AutoSaver} (Swing {@code KeYMediator.setAutoSave},
+ * KeYMediator.java:148-150: {@code autoSaver = interval > 0 ? new AutoSaver(interval, true)
+ * : null}); the saver writes intermediate .key artifacts every {@code interval} proof steps
+ * and the final closed proof (see {@link AutoSaver}).
+ *
+ * @param interval the save interval in proof steps, 0 disables auto save
+ */
+ public void setAutoSave(int interval) {
+ autoSaver = interval > 0 ? new AutoSaver(interval, true) : null;
+ }
+
+ /**
+ * @return the auto saver to use, or {@code null} if auto save is disabled (Swing
+ * {@code KeYMediator.getAutoSaver}, KeYMediator.java:791-796)
+ */
+ public AutoSaver getAutoSaver() {
+ return autoSaver;
+ }
+
+ /**
+ * @return the selection model of the mediator
+ */
+ public KeYSelectionModel getSelectionModel() {
+ return keySelectionModel;
+ }
+
+ /**
+ * @return the notation info shared by the views (abbreviation map follows the selected proof)
+ */
+ public NotationInfo getNotationInfo() {
+ return notationInfo;
+ }
+
+ /**
+ * @return the currently selected proof or {@code null}
+ */
+ public Proof getSelectedProof() {
+ return keySelectionModel.getSelectedProof();
+ }
+
+ /**
+ * uicontrol (P2b): whether a proof is loaded (port of Swing
+ * {@code KeYMediator.ensureProofLoaded}, KeYMediator.java:152-154); used by the source
+ * consistency toggle and other actions that need a loaded proof before taking effect.
+ *
+ * @return {@code true} if a proof is selected
+ */
+ public boolean ensureProofLoaded() {
+ return getSelectedProof() != null;
+ }
+
+ /**
+ * @return the currently selected node or {@code null}
+ */
+ public Node getSelectedNode() {
+ return keySelectionModel.getSelectedNode();
+ }
+
+ /**
+ * @return the currently selected goal or {@code null}
+ */
+ public Goal getSelectedGoal() {
+ return keySelectionModel.getSelectedGoal();
+ }
+
+ /**
+ * @return the services of the selected proof or {@code null}
+ */
+ public Services getServices() {
+ Proof selectedProof = getSelectedProof();
+ return selectedProof != null ? selectedProof.getServices() : null;
+ }
+
+ /**
+ * Selects the default node/goal of the selected proof (first open goal or a leaf) and fires
+ * {@code selectedNodeChanged}.
+ */
+ public void defaultSelection() {
+ keySelectionModel.defaultSelection();
+ }
+
+ /**
+ * @return {@code true} if an auto mode run is currently active
+ */
+ public boolean isInAutoMode() {
+ return inAutoMode;
+ }
+
+ /**
+ * @return the observable auto mode state (updated on the FX thread); the automatic proof
+ * buttons and menu items bind their disabled state to it
+ */
+ public ReadOnlyBooleanProperty autoModeRunningProperty() {
+ return autoModeRunning.getReadOnlyProperty();
+ }
+
+ /**
+ * Starts the automatic prover on the selected proof (Swing {@code AutoModeAction}: guarded by
+ * {@code isInAutoMode} and {@code ProofControl#isAutoModeSupported}, then
+ * {@code startAutoMode(proof, proof.openEnabledGoals())}). No-op without an attached proof
+ * control, without a selected proof, while a run is active or when auto mode is not supported
+ * for the proof.
+ */
+ public void startAutoMode() {
+ Proof proof = getSelectedProof();
+ if (proofControl == null || proof == null || inAutoMode
+ || !proofControl.isAutoModeSupported(proof)) {
+ return;
+ }
+ proofControl.startAutoMode(proof, proof.openEnabledGoals());
+ }
+
+ /** Stops the running automatic prover (Swing {@code AutoModeAction}); no-op when idle. */
+ public void stopAutoMode() {
+ if (proofControl != null && inAutoMode) {
+ proofControl.stopAutoMode();
+ }
+ }
+
+ // ------------------------------------------------------------------
+ // WP-1 — auto-mode context API for foreign proofs (Swing
+ // KeYMediator.initiateAutoMode/finishAutoMode, KeYMediator.java:535-566). Swing's caching
+ // port wraps the CopyReferenceResolver copy into these (CachingExtension.java:312-319);
+ // the FX port of CopyBeforeDispose needs the same seam for a proof that is not the
+ // selected one.
+ // ------------------------------------------------------------------
+
+ /**
+ * WP-1 — performs the standard preparations for starting an auto mode run on the given
+ * (possibly foreign) proof (Swing {@code KeYMediator.initiateAutoMode},
+ * KeYMediator.java:535-540): freezes the interface and marks the auto mode active
+ * ({@code fullStop}), flips the rule app index to the requested mode.
+ *
+ * Swing {@code stopInterface(fullStop)} disables the UI via
+ * {@code ui.notifyAutoModeBeingStarted()} and sets {@code inAutoMode}; in the FX UI both the
+ * freeze ({@code MainWindowF}'s listener on the observable property) and the enabled state
+ * of every proof action are bound to {@link #autoModeRunningProperty()}, so publishing the
+ * property is the FX-side interface stop. Swing additionally fires
+ * {@code getUI().getProofControl().fireAutoModeStarted(...)} — the Swing-only method of
+ * {@code KeYMediatorProofControl}; the core {@code AbstractProofControl.fireAutoModeStarted}
+ * is protected and not reachable from the mediator, so the observable property is the FX
+ * notification channel.
+ *
+ * @param proof the {@link Proof} to be worked on (may differ from the selected proof)
+ * @param fullStop if a full freeze of the interface is requested
+ * @param interactive whether the needed taclet index is for interactive or automatic use
+ * (normally {@code false})
+ */
+ public void initiateAutoMode(Proof proof, boolean fullStop, boolean interactive) {
+ if (fullStop) {
+ inAutoMode = true;
+ publishAutoModeRunning(true);
+ }
+ setInteractive(interactive);
+ }
+
+ /**
+ * WP-1 — performs the standard preparations for finishing an auto mode run on the given
+ * (possibly foreign) proof (Swing {@code KeYMediator.finishAutoMode}, KeYMediator.java
+ * :550-566): flips the rule app index back, unfreezes the interface ({@code fullStop}) and
+ * selects a node/proof by running the given selector (the default selection when
+ * {@code selection} is {@code null}).
+ *
+ * @param proof the {@link Proof} that was worked on
+ * @param fullStop if a full unfreeze of the interface is requested
+ * @param interactive whether the needed taclet index is for interactive or automatic use
+ * (normally {@code true})
+ * @param selection a runnable that selects the correct node after unfreezing the interface,
+ * may be {@code null}
+ */
+ public void finishAutoMode(Proof proof, boolean fullStop, boolean interactive,
+ @Nullable Runnable selection) {
+ setInteractive(interactive);
+ if (fullStop) {
+ inAutoMode = false;
+ publishAutoModeRunning(false);
+ }
+ if (selection == null) {
+ keySelectionModel.defaultSelection();
+ } else {
+ selection.run();
+ }
+ }
+
+ /**
+ * Swing {@code KeYMediator.setInteractive} (KeYMediator.java:476-484): switches the rule
+ * app index of the selected proof between interactive and automatic use.
+ *
+ * @param b {@code true} to switch to interactive mode
+ */
+ public void setInteractive(boolean b) {
+ Proof selectedProof = getSelectedProof();
+ if (selectedProof != null) {
+ if (b) {
+ selectedProof.setRuleAppIndexToInteractiveMode();
+ } else {
+ selectedProof.setRuleAppIndexToAutoMode();
+ }
+ }
+ }
+
+ /**
+ * WP-1 — publishes the auto-mode state on the observable property, marshalled to the FX
+ * thread (auto-mode events may originate from the prover thread — Swing's
+ * {@code stopInterface}/{@code startInterface} marshal via {@code ThreadUtilities}).
+ *
+ * In a toolkit-free environment (the headless seam tests) {@code Platform.runLater} fails
+ * with an {@link IllegalStateException}; there the state is published directly, because no
+ * UI listeners are attached in that environment.
+ */
+ private void publishAutoModeRunning(boolean running) {
+ try {
+ FxUtil.runLater(() -> autoModeRunning.set(running));
+ } catch (IllegalStateException toolkitNotInitialized) {
+ LOGGER.debug("No JavaFX toolkit: publishing the auto-mode state directly");
+ autoModeRunning.set(running);
+ }
+ }
+
+ // ------------------------------------------------------------------
+ // WP-1 — opened-proofs accessor, extension-object registry, GUI-listener and proof-load
+ // listener registries (Swing KeYMediator.java:129-134, 300-320, 403-421, 585-626,
+ // 800-808).
+ // ------------------------------------------------------------------
+
+ /**
+ * WP-1 — provides the list of currently opened proofs (Swing
+ * {@code KeYMediator.getCurrentlyOpenedProofs}, KeYMediator.java:800-808): the live list can
+ * be used directly inside components (Swing {@code TaskTree} adds, {@code ProofTreeView}
+ * removes).
+ *
+ * @return the live, mutable list of opened proofs
+ */
+ public List getCurrentlyOpenedProofs() {
+ return currentlyOpenedProofs;
+ }
+
+ /**
+ * WP-1 — registers a proof load listener (Swing
+ * {@code KeYMediator.registerProofLoadListener}, KeYMediator.java:124-134): called whenever
+ * a new proof is loaded, but before it is replayed. The listener MUST be able to accept the
+ * same proof twice.
+ *
+ * @param listener the callback
+ */
+ public void registerProofLoadListener(Consumer listener) {
+ proofLoadListeners.add(listener);
+ }
+
+ /**
+ * WP-1 — fires the proof-loaded event (Swing {@code KeYMediator.fireProofLoaded},
+ * KeYMediator.java:413-421): delivered to every registered proof load listener.
+ *
+ * @param p the proof that was just loaded and is about to be replayed ({@code null} is
+ * ignored, like in Swing)
+ */
+ public void fireProofLoaded(Proof p) {
+ if (p == null) {
+ return;
+ }
+ for (Consumer listener : proofLoadListeners) {
+ listener.accept(p);
+ }
+ }
+
+ /**
+ * WP-1 — adds a listener to GUI events (Swing {@code KeYMediator.addGUIListener},
+ * KeYMediator.java:306-309).
+ *
+ * @param listener the {@link GUIListenerF} to be added
+ */
+ public void addGUIListener(GUIListenerF listener) {
+ guiListeners.add(listener);
+ }
+
+ /**
+ * WP-1 — removes a listener from GUI events (Swing
+ * {@code KeYMediator.removeGUIListener}, KeYMediator.java:315-318).
+ *
+ * @param listener the {@link GUIListenerF} to be removed
+ */
+ public void removeGUIListener(GUIListenerF listener) {
+ guiListeners.remove(listener);
+ }
+
+ /**
+ * WP-7 — adds another listener for user actions (Swing
+ * {@code KeYMediator.addUserActionListener}, KeYMediator.java:815-818).
+ *
+ * @param listener the {@link UserActionListenerF} to be added
+ */
+ public void addUserActionListener(UserActionListenerF listener) {
+ userActionListeners.add(listener);
+ }
+
+ /**
+ * WP-7 — notifies all user action listeners about a performed action (Swing
+ * {@code KeYMediator.fireActionPerformed}, KeYMediator.java:824-827).
+ *
+ * @param action the user action
+ */
+ public void fireActionPerformed(UserActionF action) {
+ for (UserActionListenerF listener : userActionListeners) {
+ listener.actionPerformed(action);
+ }
+ }
+
+ /**
+ * WP-1 — fires the request of a GUI component for modal access (Swing
+ * {@code KeYMediator.requestModalAccess}, KeYMediator.java:352-356); can be used to disable
+ * all views even if the GUI component has no built-in modal support.
+ *
+ * @param src the object asking for modal access
+ */
+ public void requestModalAccess(Object src) {
+ fireModalDialogOpened(new EventObject(src));
+ }
+
+ /**
+ * WP-1 — frees the modal access requested with {@link #requestModalAccess(Object)} (Swing
+ * {@code KeYMediator.freeModalAccess}, KeYMediator.java:359-363).
+ *
+ * @param src the object that requested modal access
+ */
+ public void freeModalAccess(Object src) {
+ fireModalDialogClosed(new EventObject(src));
+ }
+
+ /**
+ * WP-1 — fires that a GUI component asked for modal access (Swing
+ * {@code KeYMediator.fireModalDialogOpened}, KeYMediator.java:366-377).
+ *
+ * @param e the event
+ */
+ public void fireModalDialogOpened(EventObject e) {
+ for (GUIListenerF listener : guiListeners) {
+ listener.modalDialogOpened(e);
+ }
+ }
+
+ /**
+ * WP-1 — fires that the GUI component holding modal access has been closed, so views can be
+ * enabled again (Swing {@code KeYMediator.fireModalDialogClosed}, KeYMediator.java:382-393).
+ *
+ * @param e the event
+ */
+ public void fireModalDialogClosed(EventObject e) {
+ for (GUIListenerF listener : guiListeners) {
+ listener.modalDialogClosed(e);
+ }
+ }
+
+ /**
+ * WP-1 — fires the shut-down event (Swing {@code KeYMediator.fireShutDown},
+ * KeYMediator.java:400-410): the exit flow of the main window calls this so the
+ * {@link GUIListenerF}s perform their shutdown side effects (Swing
+ * {@code MainGUIListener.shutDown}, MainWindow.java:1855-1858).
+ *
+ * @param e the event
+ */
+ public void fireShutDown(EventObject e) {
+ for (GUIListenerF listener : guiListeners) {
+ listener.shutDown(e);
+ }
+ }
+
+ /**
+ * WP-1 — retrieves user-defined data (Swing {@code KeYMediator.lookup},
+ * KeYMediator.java:589-604): the first registered implementation of the service class, or
+ * {@code null} when none is registered (Swing swallows the {@link IllegalStateException} of
+ * the lookup in the same way).
+ *
+ * @param service the class for which the data were registered
+ * @param any class
+ * @return the registered implementation or {@code null}
+ * @see #register(Object, Class)
+ */
+ public @Nullable T lookup(Class service) {
+ try {
+ if (userData == null) {
+ return null;
+ }
+ return userData.get(service);
+ } catch (IllegalStateException ignored) {
+ return null;
+ }
+ }
+
+ /**
+ * WP-1 — registers a user-defined data / extension object (Swing
+ * {@code KeYMediator.register}, KeYMediator.java:606-610; the Swing exploration port
+ * registers its model with {@code mediator.register(model, ExplorationModeModel.class)},
+ * ExplorationExtension.java:103).
+ *
+ * @param obj the object to register
+ * @param service the class under which it is registered
+ * @param the service type
+ */
+ public void register(T obj, Class service) {
+ getUserData().register(obj, service);
+ }
+
+ /**
+ * WP-1 — removes a previously registered user-defined data (Swing
+ * {@code KeYMediator.deregister}, KeYMediator.java:612-622).
+ *
+ * @param obj the registered object
+ * @param service the class under which the data was registered
+ * @param the service type
+ */
+ public void deregister(T obj, Class service) {
+ if (userData != null) {
+ userData.deregister(obj, service);
+ }
+ }
+
+ /**
+ * WP-1 — the associated lookup of user-defined data (Swing
+ * {@code KeYMediator.getUserData}, KeYMediator.java:625-631).
+ *
+ * @return the (lazily created) lookup
+ */
+ public Lookup getUserData() {
+ if (userData == null) {
+ userData = new Lookup();
+ }
+ return userData;
+ }
+
+ /**
+ * prooftree (P3a, C23): starts the automatic prover restricted to the given goals (Swing
+ * {@code RunStrategyOnNodeUserAction.apply} — the "Apply Strategy" popup item of the proof
+ * tree): all enabled goals below the invoked node, or the invoked node itself when it is an
+ * open goal. No-op without an attached proof control or while a run is active.
+ *
+ * @param goals the goals to run on, may be empty (then nothing is started)
+ */
+ public void startAutoMode(ImmutableList goals) {
+ Proof proof = getSelectedProof();
+ if (proofControl == null || proof == null || inAutoMode
+ || !proofControl.isAutoModeSupported(proof) || goals == null || goals.isEmpty()) {
+ return;
+ }
+ proofControl.startAutoMode(proof, goals);
+ }
+
+ /**
+ * prooftree (P3a, C23): prunes the proof below the given node and selects it (Swing
+ * {@code KeYMediator.setBack(Node)} — the "Prune Proof" popup item of the proof tree).
+ *
+ * @param node the cutting point, must belong to the selected proof
+ */
+ public void pruneNode(Node node) {
+ if (node != null) {
+ setBack(node);
+ }
+ }
+
+ /**
+ * Undoes the last rule application on the selected goal (Swing {@code GoalBackAction}):
+ * without a selected goal the newest goal of the selected node's subtree is used — the one
+ * with the highest node serial number, where a closed goal wins if its serial is higher. As
+ * in Swing, {@code Proof.pruneProof} refuses to prune a closed cutting point while
+ * {@code GeneralSettings.noPruningClosed} is set (the default), so a goal back on a closed
+ * branch is a no-op.
+ */
+ public void goalBack() {
+ Node selNode = getSelectedNode();
+ Goal selGoal = getSelectedGoal();
+ if (selGoal == null && selNode != null) {
+ selGoal = findNewestGoal(selNode);
+ }
+ if (selGoal != null) {
+ setBack(selGoal);
+ // set the selection to give the user a visual feedback (Swing GoalBackAction);
+ // selGoal.node() is re-read after the prune, see setBack(Goal)
+ keySelectionModel.setSelectedNode(selGoal.node());
+ }
+ }
+
+ /**
+ * Removes the proof subtree below the selected node (Swing {@code PruneProofAction}).
+ */
+ public void pruneProof() {
+ Node node = getSelectedNode();
+ if (node != null) {
+ setBack(node);
+ }
+ }
+
+ /**
+ * Swing {@code KeYMediator.setBack(Goal)}: undoes the rule application that created the goal
+ * ({@code ProofControl.pruneTo(Goal)} prunes to the goal's parent) and selects the pre-rule
+ * node. The Swing task-finished notification is not yet mirrored.
+ */
+ private void setBack(Goal goal) {
+ Proof proof = getSelectedProof();
+ if (proof == null) {
+ return;
+ }
+ if (goal.node().parent() != null) {
+ // ProofControl.pruneTo(Goal): undo the rule application that created the goal's node
+ proof.pruneProof(goal.node().parent());
+ }
+ // The goal's node must be re-read here: the pruner re-associates the goal with the
+ // cutting point (Goal.pruneToParent), the old node is detached and its parent pointer
+ // is cleared (Node.remove). Swing's KeYMediator.setBack reads goal.node() after
+ // pruneTo as well.
+ Node node = goal.node();
+ keySelectionModel.setSelectedNode(node == proof.root() ? node : node.parent());
+ }
+
+ /**
+ * Swing {@code KeYMediator.setBack(Node)}: prunes the subtree below the given node and
+ * selects it.
+ */
+ private void setBack(Node node) {
+ node.proof().pruneProof(node);
+ keySelectionModel.setSelectedNode(node);
+ }
+
+ /**
+ * P3b/B11: the display name of the rule that the next "Goal Back" would undo, or {@code null}
+ * when there is none (Swing {@code GoalBackAction.updateName},
+ * GoalBackAction.java:113-125: the name of the {@code getAppliedRuleApp} of the parent of the
+ * newest goal's node is appended to the menu item text).
+ *
+ * @return the rule display name (e.g. {@code "andLeft"}), or {@code null} when no goal / no
+ * applied rule is found
+ */
+ public @Nullable String goalBackRuleName() {
+ Goal goal = findNewestGoal(getSelectedNode());
+ if (goal != null && goal.node() != null && goal.node().parent() != null) {
+ RuleApp app = goal.node().parent().getAppliedRuleApp();
+ if (app != null) {
+ return app.rule().displayName();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Swing {@code GoalBackAction.findNewestGoal}: the goal of the subtree that was changed last
+ * (the highest node serial number), open and closed goals considered.
+ */
+ private static Goal findNewestGoal(Node subtree) {
+ if (subtree == null) {
+ return null;
+ }
+ Proof proof = subtree.proof();
+ ImmutableList closedGoals = proof.getClosedSubtreeGoals(subtree);
+ ImmutableList openGoals = proof.getSubtreeGoals(subtree);
+ int closedID = -1;
+ Goal closed = null;
+ int openID = -1;
+ Goal open = null;
+ for (Goal g : closedGoals) {
+ if (g.node().serialNr() > closedID) {
+ closedID = g.node().serialNr();
+ closed = g;
+ }
+ }
+ for (Goal g : openGoals) {
+ if (g.node().serialNr() > openID) {
+ openID = g.node().serialNr();
+ open = g;
+ }
+ }
+ return closedID > openID ? closed : open;
+ }
+
+ /**
+ * Counts a closed goal (called when a rule application produced no new goals).
+ */
+ public void closedAGoal() {
+ goalsClosedByAutoMode++;
+ }
+
+ /**
+ * @return the number of goals closed by the last auto mode run
+ */
+ public int getNrGoalsClosedByAutoMode() {
+ return goalsClosedByAutoMode;
+ }
+
+ /**
+ * Resets the closed-goal counter (called when an auto mode run starts).
+ */
+ public void resetNrGoalsClosedByHeuristics() {
+ goalsClosedByAutoMode = 0;
+ }
+
+ /**
+ * The mediator's proof tree listener (Swing {@code KeYMediatorProofTreeListener}): counts
+ * closed goals, repairs the selection after pruning and logs proof closure (the notification
+ * event follows in M3).
+ */
+ private final class MediatorProofTreeListener extends ProofTreeAdapter {
+ @Override
+ public void proofClosed(ProofTreeEvent e) {
+ LOGGER.info("Proof closed: {}", e.getSource().name());
+ }
+
+ @Override
+ public void proofPruned(ProofTreeEvent e) {
+ FxUtil.runLater(() -> {
+ Node selectedNode = keySelectionModel.getSelectedNode();
+ if (selectedNode != null && !e.getSource().find(selectedNode)) {
+ keySelectionModel.setSelectedNode(e.getNode());
+ }
+ });
+ OneStepSimplifier.refreshOSS(e.getSource());
+ }
+
+ @Override
+ public void proofGoalsAdded(ProofTreeEvent e) {
+ if (e.getGoals().isEmpty()) {
+ // no new goals have been generated: the goal was closed
+ closedAGoal();
+ }
+ }
+ }
+
+ /**
+ * The mediator's rule application and auto mode listener (Swing {@code
+ * KeYMediatorProofListener}): re-defaults the selection after interactive rule applications
+ * so the views update, and tracks the auto mode state.
+ */
+ private final class RuleAppListenerProofListener implements RuleAppListener, AutoModeListener {
+
+ @Override
+ public void ruleApplied(ProofEvent e) {
+ if (inAutoMode) {
+ return;
+ }
+ if (e.getSource() == getSelectedProof()) {
+ keySelectionModel.defaultSelection();
+ }
+ }
+
+ @Override
+ public void autoModeStarted(ProofEvent e) {
+ inAutoMode = true;
+ resetNrGoalsClosedByHeuristics();
+ publishAutoModeRunning(true);
+ }
+
+ @Override
+ public void autoModeStopped(ProofEvent e) {
+ inAutoMode = false;
+ publishAutoModeRunning(false);
+ }
+ }
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/core/fx/KeYSelectionEvent.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/core/fx/KeYSelectionEvent.java
new file mode 100644
index 00000000000..616cb15fff0
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/core/fx/KeYSelectionEvent.java
@@ -0,0 +1,58 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.core.fx;
+
+
+/**
+ * An event that indicates that the users focused node or proof has changed
+ */
+
+public class KeYSelectionEvent {
+
+ /** the source of this event */
+ private final KeYSelectionModel source;
+
+ /** the previously selected item */
+ private final Sel previousSelection;
+
+ /**
+ * creates a new SelectedNodeEvent
+ *
+ * @param source the SelectedNodeModel where the event had its origin
+ * @param previousSelection the previous selected item
+ */
+ public KeYSelectionEvent(KeYSelectionModel source, Sel previousSelection) {
+ this.source = source;
+ this.previousSelection = previousSelection;
+ }
+
+ /**
+ * creates a new SelectedNodeEvent
+ *
+ * @param source the SelectedNodeModel where the event had its origin
+ */
+ public KeYSelectionEvent(KeYSelectionModel source) {
+ this(source, null);
+ }
+
+
+ /**
+ * returns the KeYSelectionModel that caused this event
+ *
+ * @return the KeYSelectionModel that caused this event
+ */
+ public KeYSelectionModel getSource() {
+ return source;
+ }
+
+ /**
+ * returns the previous selected item
+ *
+ * @return the previous selected item
+ */
+ public Sel getPreviousSelection() {
+ return previousSelection;
+ }
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/core/fx/KeYSelectionListener.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/core/fx/KeYSelectionListener.java
new file mode 100644
index 00000000000..651603a5987
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/core/fx/KeYSelectionListener.java
@@ -0,0 +1,40 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.core.fx;
+
+
+import de.uka.ilkd.key.proof.Node;
+import de.uka.ilkd.key.proof.Proof;
+
+/**
+ * The KeYSelectionListener is notified if the proof or the node the user works with has changed.
+ */
+public interface KeYSelectionListener {
+
+ /**
+ * focused node has changed.
+ *
+ * Do not modify the list of selection listeners in this callback!
+ *
+ *
+ * @param e event with details about the new selection
+ */
+ default void selectedNodeChanged(KeYSelectionEvent e) {
+
+ }
+
+ /**
+ * the selected proof has changed (e.g. a new proof has been loaded or selected in the UI)
+ *
+ * Do not modify the list of selection listeners in this callback!
+ *
+ *
+ * @param e event with details about the new selection
+ */
+ default void selectedProofChanged(KeYSelectionEvent e) {
+
+ }
+
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/core/fx/KeYSelectionModel.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/core/fx/KeYSelectionModel.java
new file mode 100644
index 00000000000..939edd7201f
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/core/fx/KeYSelectionModel.java
@@ -0,0 +1,368 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.core.fx;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import de.uka.ilkd.key.proof.Goal;
+import de.uka.ilkd.key.proof.Node;
+import de.uka.ilkd.key.proof.Proof;
+
+import org.key_project.prover.rules.RuleApp;
+import org.key_project.prover.sequent.Sequent;
+
+import org.jspecify.annotations.NonNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Copy of {@code de.uka.ilkd.key.core.KeYSelectionModel} (module {@code key.ui}) for the JavaFX
+ * UI. The only deviation from the original is the mediator seam: the original constructor takes
+ * the Swing {@code KeYMediator} and calls {@code KeYMediator#setProof(Proof, Proof)} whenever the
+ * selected proof changes. Here the same callback is expressed by the {@link ProofBinder}
+ * interface; the JavaFX mediator ({@code KeYMediatorF}, milestone M2c) implements it. All other
+ * logic is identical to the original.
+ */
+public class KeYSelectionModel {
+ private static final Logger LOGGER = LoggerFactory.getLogger(KeYSelectionModel.class);
+
+ /**
+ * Callback by which the model informs its owner (the mediator) that the selected proof has
+ * changed. The Swing original calls the mediator directly; {@code KeYMediatorF} implements
+ * this interface.
+ */
+ public interface ProofBinder {
+ /**
+ * Called before the change listeners are fired, mirroring
+ * {@code KeYMediator#setProof(Proof, Proof)}.
+ *
+ * @param newProof the newly selected proof, may be {@code null}
+ * @param previousProof the previously selected proof, may be {@code null}
+ */
+ void setProof(Proof newProof, Proof previousProof);
+ }
+
+ // the KeYMediator is informed before all
+ private final @NonNull ProofBinder primary;
+
+ /** the proof to listen to */
+ private Proof proof;
+ /** if true the selectedGoal field below can be trusted */
+ private boolean goalIsValid;
+ /** is the selected node a goal */
+ private Goal selectedGoal;
+ /** the current displayed node */
+ private Node selectedNode;
+ /**
+ * The currently displayed sequent. Equal to the sequent of {@link #selectedNode} unless
+ * we are displaying an OSS node.
+ */
+ private Sequent selectedSequent;
+ /**
+ * The currently displayed rule application. Equal to the rule app of {@link #selectedNode}
+ * unless we are displaying an OSS node.
+ */
+ private RuleApp selectedRuleApp;
+ /** the listeners to this model */
+ private final List listenerList;
+
+ /** cached selected node event */
+
+ public KeYSelectionModel(@NonNull ProofBinder primary) {
+ this.primary = primary;
+ listenerList = Collections.synchronizedList(new ArrayList<>(5));
+ goalIsValid = false;
+ }
+
+ /**
+ * Sets the selected proof.
+ *
+ * @param p the proof to select.
+ *
+ * @see ProofBinder#setProof(Proof, Proof)
+ */
+ public synchronized void setSelectedProof(Proof p) {
+ final Proof previousProof = proof;
+ goalIsValid = false;
+ proof = p;
+ primary.setProof(p, previousProof);
+ if (proof != null) {
+ if (proof.openGoals().isEmpty()) {
+ selectedNode = proof.root().leavesIterator().next();
+ selectedSequent = selectedNode.sequent();
+ selectedRuleApp = selectedNode.getAppliedRuleApp();
+ } else {
+ final Goal g = proof.openGoals().iterator().next();
+ goalIsValid = true;
+ selectedNode = g.node();
+ selectedSequent = selectedNode.sequent();
+ selectedRuleApp = selectedNode.getAppliedRuleApp();
+ selectedGoal = g;
+ }
+ } else {
+ selectedNode = null;
+ selectedSequent = null;
+ selectedRuleApp = null;
+ selectedGoal = null;
+ }
+
+ fireSelectedProofChanged(previousProof);
+ }
+
+ /**
+ * returns the proof that is selected by the user
+ *
+ * @return the proof that is selected by the user
+ */
+ public Proof getSelectedProof() {
+ return proof;
+ }
+
+ /**
+ * sets the node that is focused by the user
+ *
+ * @param n the selected node
+ */
+ public synchronized void setSelectedNode(Node n) {
+ final Node previousSelectedNode = selectedNode;
+ // switch proof if needed
+ if (n.proof() != getSelectedProof()) {
+ setSelectedProof(n.proof());
+ }
+ goalIsValid = false;
+ selectedNode = n;
+ selectedSequent = selectedNode.sequent();
+ selectedRuleApp = selectedNode.getAppliedRuleApp();
+ fireSelectedNodeChanged(previousSelectedNode);
+ }
+
+ /**
+ * Sets the node and sequent focused by the user.
+ *
+ * @param node selected node
+ * @param sequent selected sequent
+ */
+ public synchronized void setSelectedSequentAndRuleApp(Node node, Sequent sequent,
+ RuleApp ruleApp) {
+ final Node previousNode = selectedNode;
+ // switch proof if needed
+ if (node.proof() != getSelectedProof()) {
+ setSelectedProof(node.proof());
+ }
+ goalIsValid = true;
+ selectedGoal = null;
+ selectedNode = node;
+ selectedSequent = sequent;
+ selectedRuleApp = ruleApp;
+ fireSelectedNodeChanged(previousNode);
+ }
+
+ /**
+ * sets the node that is focused by the user
+ *
+ * @param g the Goal that contains the selected node
+ */
+ public synchronized void setSelectedGoal(Goal g) {
+ final Node previousNode = selectedNode;
+ goalIsValid = true;
+ selectedGoal = g;
+ selectedNode = g.node();
+ selectedSequent = selectedNode.sequent();
+ selectedRuleApp = selectedNode.getAppliedRuleApp();
+ fireSelectedNodeChanged(previousNode);
+ }
+
+ /**
+ * returns the node that is selected by the user
+ *
+ * @return the node that is selected by the user
+ */
+ public Node getSelectedNode() {
+ return selectedNode;
+ }
+
+ public Sequent getSelectedSequent() {
+ return selectedSequent;
+ }
+
+ public RuleApp getSelectedRuleApp() {
+ return selectedRuleApp;
+ }
+
+ /**
+ * returns the goal the selected node belongs to, or null if there is no such goal (the selected
+ * node is an inner node, or no proof is loaded at all)
+ *
+ * @return the selected goal, or null if there is none
+ */
+ public Goal getSelectedGoal() {
+ // Consistent with getSelectedProof()/getSelectedNode() (both nullable) and with the
+ // documented "null if inner node" contract: return null - rather than throwing - when no
+ // proof is loaded. Callers already null-check this method; throwing here made GUI listeners
+ // that fire during teardown (e.g. InfoView) crash with "No proof loaded".
+ if (proof == null) {
+ return null;
+ }
+ if (!goalIsValid) {
+ selectedGoal = proof.getOpenGoal(selectedNode);
+ goalIsValid = true;
+ }
+ return selectedGoal;
+ }
+
+ /**
+ * returns true iff the selected node is a goal
+ *
+ * @return true iff the selected node is a goal
+ */
+ public boolean isGoal() {
+ if (!goalIsValid) {
+ return (getSelectedGoal() != null);
+ }
+ return selectedGoal != null;
+ }
+
+ /**
+ * enumerate the possible goal selections, starting with the best one
+ */
+ protected class DefaultSelectionIterator implements Iterator {
+ private static final int POS_START = 0;
+ private static final int POS_LEAVES = 1;
+ private static final int POS_GOAL_LIST = 2;
+
+ private int currentPos = POS_START;
+ private Goal nextOne;
+ private Iterator goalIt;
+ private Iterator nodeIt;
+
+ public DefaultSelectionIterator() {
+ findNext();
+ }
+
+ private void findNext() {
+ nextOne = null;
+ while (nextOne == null) {
+ switch (currentPos) {
+ case POS_START -> {
+ currentPos = POS_LEAVES;
+ if (selectedNode != null) {
+ nodeIt = selectedNode.leavesIterator();
+ } else {
+ nodeIt = null;
+ }
+ }
+ case POS_LEAVES -> {
+ if (nodeIt == null || !nodeIt.hasNext()) {
+ currentPos = POS_GOAL_LIST;
+ if (!proof.openGoals().isEmpty()) {
+ goalIt = proof.openGoals().iterator();
+ } else {
+ goalIt = null;
+ }
+ } else {
+ nextOne = proof.getOpenGoal(nodeIt.next());
+ }
+ }
+ case POS_GOAL_LIST -> {
+ if (goalIt == null || !goalIt.hasNext()) {
+ // no more items
+ return;
+ } else {
+ nextOne = goalIt.next();
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public Goal next() {
+ Goal res = nextOne;
+ findNext();
+ return res;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return nextOne != null;
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ /**
+ * selects the first goal in the goal list of proof if available.
+ * If not it selects a leaf of the proof tree
+ */
+ public void defaultSelection() {
+ Goal g = null;
+ Iterator it = new DefaultSelectionIterator();
+
+ while (g == null && it.hasNext()) {
+ g = it.next();
+ }
+
+ /*
+ * Order of preference: 1. Not yet closable goals 2. Goals which are not closed for all
+ * metavariable instantiations 3. The first node of the tree
+ */
+ if (g != null) {
+ setSelectedGoal(g);
+ } else {
+ setSelectedNode(proof.root().leavesIterator().next());
+ }
+ }
+
+ public void addKeYSelectionListenerChecked(KeYSelectionListener listener) {
+ synchronized (listenerList) {
+ if (!listenerList.contains(listener)) {
+ addKeYSelectionListener(listener);
+ }
+ }
+ }
+
+ public void addKeYSelectionListener(KeYSelectionListener listener) {
+ synchronized (listenerList) {
+ LOGGER.trace("Adding {}", listener.getClass());
+ listenerList.add(listener);
+ }
+ }
+
+ public void removeKeYSelectionListener(KeYSelectionListener listener) {
+ synchronized (listenerList) {
+ LOGGER.trace("Removing {}", listener.getClass());
+ listenerList.remove(listener);
+ }
+ }
+
+ public synchronized void fireSelectedNodeChanged(Node previousNode) {
+ synchronized (listenerList) {
+ final KeYSelectionEvent selectionEvent =
+ new KeYSelectionEvent<>(this, previousNode);
+ for (final KeYSelectionListener listener : listenerList) {
+ listener.selectedNodeChanged(selectionEvent);
+ }
+ }
+ }
+
+ public synchronized void fireSelectedProofChanged(Proof previousProof) {
+ synchronized (listenerList) {
+ LOGGER.debug("Selected Proof changed, firing...");
+ final KeYSelectionEvent selectionEvent =
+ new KeYSelectionEvent<>(this, previousProof);
+ for (final KeYSelectionListener listener : listenerList) {
+ listener.selectedProofChanged(selectionEvent);
+ }
+ LOGGER.trace("Selected Proof changed, done firing.");
+ }
+ }
+
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/core/fx/MainEx.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/core/fx/MainEx.java
new file mode 100644
index 00000000000..5e04b9f1626
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/core/fx/MainEx.java
@@ -0,0 +1,113 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.core.fx;
+
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.Callable;
+
+import de.uka.ilkd.key.gui.fx.MainApplication;
+import de.uka.ilkd.key.prover.impl.ParallelProver;
+import de.uka.ilkd.key.settings.PathConfig;
+import de.uka.ilkd.key.settings.ProofIndependentSettings;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import picocli.CommandLine;
+import picocli.CommandLine.Option;
+import picocli.CommandLine.Parameters;
+
+/**
+ * The main entry point of the JavaFX user interface (key.ui.fx).
+ *
+ * This is the counter-part of {@code de.uka.ilkd.key.core.Main} in the Swing module
+ * {@code key.ui}. It mirrors the command line interface of that class; the interactive mode
+ * starts the JavaFX {@link MainApplication}, while the headless auto mode is planned for a
+ * later milestone.
+ */
+public final class MainEx implements Callable {
+ private static final Logger LOGGER = LoggerFactory.getLogger(MainEx.class);
+
+ /**
+ * flag whether the automatic prove procedure should be started after initialisation without
+ * GUI
+ */
+ @Option(names = "--auto",
+ description = "start automatic prove procedure after initialisation without GUI (not yet "
+ + "implemented in key.ui.fx)")
+ private boolean auto = false;
+
+ /**
+ * Number of worker threads for the multi-core prover. A value >= 1 enables the multi-core
+ * prover (capped at the available processors); 0 leaves the persisted prover-mode setting
+ * untouched (single-core by default).
+ */
+ @Option(names = "--threads", paramLabel = "INT",
+ description = "run automatic proof search on the multi-core prover with INT worker threads "
+ + "(>= 1, capped at the available processors). Omit for the single-core prover. "
+ + "The single-core-only features (proof caching, slicing, merge rule) are off under "
+ + "multi-worker runs (more than one worker); note that proof caching and slicing do "
+ + "not record parallel runs even with a single worker.")
+ private int proverThreads = 0;
+
+ /**
+ * Lists all features currently marked as experimental. Unless invoked with command line
+ * option --experimental, those will be deactivated.
+ */
+ @Deprecated(since = "2.13.0")
+ @Option(names = "--experimental", description = "switch experimental features on")
+ private boolean experimental = false;
+
+ /**
+ * The file names provided on the command line.
+ */
+ @Parameters(arity = "*")
+ private List inputFiles = List.of();
+
+ public static void main(String[] args) {
+ Locale.setDefault(Locale.US);
+
+ int exitCode = new CommandLine(new MainEx()).execute(args);
+ System.exit(exitCode);
+ }
+
+ @Override
+ public Integer call() {
+ // weigl: the configuration folder is not fixed anymore since v3.0
+ LOGGER.info("The configuration folder is here {}", PathConfig.currentPaths.keyConfigDir);
+
+ if (experimental) {
+ LOGGER.info("Running in experimental mode ...");
+ ProofIndependentSettings.DEFAULT_INSTANCE.getFeatureSettings().setActivateAll(true);
+ }
+
+ if (proverThreads >= 1) {
+ // Apply --threads as a TRANSIENT process-scoped override via system properties,
+ // mirroring the Swing Main: mutating GeneralSettings would fire a PropertyChange
+ // that ProofIndependentSettings turns into saveSettings(), permanently rewriting
+ // the user's persisted prover mode from a one-off CLI run.
+ int workers = Math.min(proverThreads, Runtime.getRuntime().availableProcessors());
+ System.setProperty(ParallelProver.PARALLEL_PROPERTY, "true");
+ System.setProperty(ParallelProver.THREADS_PROPERTY, Integer.toString(workers));
+ }
+
+ if (auto) {
+ // Headless proving (ConsoleUserInterfaceControl port) is a later milestone.
+ LOGGER.error("Auto mode (--auto) is not yet implemented in key.ui.fx. "
+ + "Please use the Swing UI (module key.ui) for headless proving, "
+ + "or run key.ui.fx without --auto.");
+ return -1;
+ }
+
+ if (!inputFiles.isEmpty()) {
+ LOGGER.warn("Loading problem files from the command line is not yet implemented in "
+ + "key.ui.fx (planned for a later milestone): {}", inputFiles);
+ }
+
+ MainApplication.launch(MainApplication.class, new String[0]);
+ return 0;
+ }
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/AutoDismissDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/AutoDismissDialogF.java
new file mode 100644
index 00000000000..6dd0077a389
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/AutoDismissDialogF.java
@@ -0,0 +1,146 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.gui.fx;
+
+import javafx.animation.KeyFrame;
+import javafx.animation.Timeline;
+import javafx.geometry.Insets;
+import javafx.scene.Scene;
+import javafx.scene.control.Label;
+import javafx.scene.layout.VBox;
+import javafx.stage.Stage;
+import javafx.stage.StageStyle;
+import javafx.stage.Window;
+import javafx.util.Duration;
+
+/**
+ * Tiny timed popup — the port of the Swing {@code de.uka.ilkd.key.gui.AutoDismissDialog}
+ * (key.ui, 104 lines). It is shown when the "Stop on non-closeable goal" mode could not close a
+ * goal automatically (Swing trigger: {@code WindowUserInterfaceControl.java:202-205} and
+ * {@code :227-229}) and disposes itself after a delay with a color-fade countdown.
+ *
+ * Ported behaviors (Swing evidence): the pink message panel ({@code AutoDismissDialog.java:40},
+ * color {@code (1, 0.7f, 0.7f)}), a timer disposes the dialog after {@link #DEFAULT_DELAY} ms
+ * ({@code :64-71}) and a periodic task fades the panel color from pink towards white over
+ * {@code steps = (delay - delayStartToDispose - delayDisposeToEnd) / rate} steps starting at
+ * {@link #DEFAULT_DELAY_START_TO_DISPOSE} ({@code :72-83}). Small documented addition: the
+ * dialog also dismisses on click (the Swing original stays until the timer fires; the FX toast
+ * convention established by {@code NotificationManagerF} made the click affordance consistent).
+ */
+public class AutoDismissDialogF {
+ public static final int DEFAULT_DELAY = 5000;
+ public static final int DEFAULT_RATE = 25;
+ public static final int DEFAULT_DELAY_START_TO_DISPOSE = 2000;
+ public static final int DEFAULT_DELAY_DISPOSE_TO_END = 1000;
+
+ private final Stage stage = new Stage(StageStyle.UTILITY);
+ private final VBox messagePanel = new VBox();
+ private final int delay, rate, delayStartToDispose;
+ private final int steps;
+
+ /**
+ * Creates an auto-dismiss dialog with the given timing parameters (Swing
+ * {@code AutoDismissDialog.java:35-49}).
+ *
+ * @param owner owner window (the dialog is centered on it), may be null
+ * @param message the message to show
+ * @param delay the total delay until the dialog disposes itself (ms)
+ * @param rate the period of the color fade task (ms)
+ * @param delayStartToDispose the delay before the fade starts (ms)
+ * @param delayDisposeToEnd kept for Swing parameter parity (unused in the fade math of the
+ * Swing original besides the step computation)
+ */
+ public AutoDismissDialogF(Window owner, String message, final int delay, final int rate,
+ final int delayStartToDispose, final int delayDisposeToEnd) {
+ this.delay = delay;
+ this.rate = rate;
+ this.delayStartToDispose = delayStartToDispose;
+ steps = (delay - delayStartToDispose - delayDisposeToEnd) / rate;
+
+ Label label = new Label(message);
+ label.setWrapText(true);
+ messagePanel.getChildren().add(label);
+ messagePanel.getStyleClass().add("auto-dismiss-dialog");
+ messagePanel.setPadding(new Insets(10, 16, 10, 16));
+ messagePanel.setOnMouseClicked(e -> stage.close());
+
+ Scene scene = new Scene(messagePanel);
+ de.uka.ilkd.key.gui.fx.theme.ThemeManager.getInstance().manage(scene);
+ stage.setScene(scene);
+ stage.setTitle("Message");
+ stage.setAlwaysOnTop(true);
+ if (owner != null) {
+ centerOn(owner);
+ }
+ }
+
+ /**
+ * Creates an auto-dismiss dialog with the Swing default timing (Swing
+ * {@code AutoDismissDialog.java:52-55}).
+ *
+ * @param owner owner window, may be null
+ * @param message the message to show
+ */
+ public AutoDismissDialogF(Window owner, String message) {
+ this(owner, message, DEFAULT_DELAY, DEFAULT_RATE, DEFAULT_DELAY_START_TO_DISPOSE,
+ DEFAULT_DELAY_DISPOSE_TO_END);
+ }
+
+ /**
+ * Creates an owner-less auto-dismiss dialog (Swing {@code AutoDismissDialog.java:58-60}).
+ *
+ * @param message the message to show
+ */
+ public AutoDismissDialogF(String message) {
+ this(null, message);
+ }
+
+ private void centerOn(Window owner) {
+ stage.sizeToScene();
+ stage.setX(owner.getX() + owner.getWidth() / 2 - stage.getWidth() / 2);
+ stage.setY(owner.getY() + owner.getHeight() / 2 - stage.getHeight() / 2);
+ }
+
+ /**
+ * Shows the dialog and starts the timer tasks (Swing {@code AutoDismissDialog.show},
+ * {@code :63-85}): the color fade from pink towards white and the dispose after
+ * {@code delay} ms. Must be called on the FX Application Thread.
+ */
+ public void show() {
+ Timeline fade = new Timeline(new KeyFrame(Duration.millis(rate), e -> fade()));
+ fade.setCycleCount(steps);
+ fade.setDelay(Duration.millis(delayStartToDispose));
+ fade.play();
+
+ Timeline dispose = new Timeline(new KeyFrame(Duration.millis(delay), e -> stage.close()));
+ dispose.play();
+ stage.show();
+ }
+
+ /** One fade step (Swing {@code :75-82}): the red channel stays 1, green/blue rise. */
+ private void fade() {
+ int remaining = Math.max(0, steps - ++fadeTicks);
+ double alpha = (double) remaining / (double) steps;
+ double rgValue = 0.7 + 0.3 * (1 - alpha);
+ messagePanel.setStyle("-fx-background-color: rgba(255, " + to255(rgValue) + ", "
+ + to255(rgValue) + ", 1.0);");
+ }
+
+ private int fadeTicks;
+
+ private static int to255(double value) {
+ return (int) Math.round(Math.clamp(value, 0.0, 1.0) * 255);
+ }
+
+ /** @return the underlying stage (for tests) */
+ public Stage getStage() {
+ return stage;
+ }
+
+ /** @return whether the popup is currently shown (self-test hook) */
+ public boolean isVisible() {
+ return stage.isShowing();
+ }
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/ExampleChooserF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/ExampleChooserF.java
new file mode 100644
index 00000000000..8151672e276
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/ExampleChooserF.java
@@ -0,0 +1,422 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.gui.fx;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.control.Alert;
+import javafx.scene.control.Button;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.Label;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.control.SelectionMode;
+import javafx.scene.control.SplitPane;
+import javafx.scene.control.Tab;
+import javafx.scene.control.TabPane;
+import javafx.scene.control.TextArea;
+import javafx.scene.control.TreeCell;
+import javafx.scene.control.TreeItem;
+import javafx.scene.control.TreeView;
+import javafx.scene.input.KeyCode;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.VBox;
+import javafx.stage.Modality;
+import javafx.stage.Stage;
+import javafx.stage.Window;
+
+import de.uka.ilkd.key.gui.fx.configuration.ConfigF;
+import de.uka.ilkd.key.gui.fx.theme.ThemeManager;
+import de.uka.ilkd.key.settings.ProofIndependentSettings;
+import de.uka.ilkd.key.settings.ViewSettings;
+
+import org.key_project.util.java.IOUtil;
+
+import org.jspecify.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Dialog to choose an example to load. Counter-part of {@code de.uka.ilkd.key.gui.ExampleChooser}
+ * in the Swing module {@code key.ui}: a category tree of the examples on the left and a tabbed
+ * preview (description, proof obligation, additional files) on the right, with "Load Example",
+ * "Load Proof" and "Cancel" buttons plus the "show on startup" checkbox (the checkbox state is
+ * shared with the Swing UI via the core {@link ViewSettings}).
+ *
+ * Deviations from the Swing original: the dialog is built fresh on every invocation (Swing caches
+ * a singleton instance and would keep a stale example list if the examples directory changed),
+ * and an empty example list leaves the buttons disabled instead of enabled.
+ */
+public final class ExampleChooserF {
+
+ /**
+ * This path is also accessed by the Eclipse integration of KeY to find the right examples.
+ */
+ public static final String EXAMPLES_PATH = "examples";
+
+ /**
+ * Java property name to specify a custom key example folder.
+ */
+ public static final String KEY_EXAMPLE_DIR = "key.examples.dir";
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ExampleChooserF.class);
+
+ /** The result value of the dialog. {@code null} if nothing to be loaded */
+ private @Nullable Path fileToLoad = null;
+
+ /** The currently selected example. {@code null} if none selected */
+ private @Nullable ExampleF selectedExample;
+
+ private final Stage stage = new Stage();
+ private final TreeView exampleList = new TreeView<>();
+ private final TabPane tabPane = new TabPane();
+ private final Button loadButton = new Button("Load Example");
+ private final Button loadProofButton = new Button("Load Proof");
+ private final Button cancelButton = new Button("Cancel");
+
+ // -------------------------------------------------------------------------
+ // constructors
+ // -------------------------------------------------------------------------
+
+ private ExampleChooserF(Path examplesDir, @Nullable Window owner) {
+ assert examplesDir != null;
+
+ // create example list: category folders with one leaf per example (Swing JTree)
+ List examples = listExamples(examplesDir);
+ TreeItem root = new TreeItem<>(null);
+ for (ExampleF example : examples) {
+ findChild(root, example.getPath(), 0).getChildren().add(new TreeItem<>(example));
+ }
+ exampleList.setRoot(root);
+ exampleList.setShowRoot(false);
+ exampleList.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
+ exampleList.getSelectionModel().selectedItemProperty()
+ .addListener((obs, old, item) -> updateDescription());
+ exampleList.setCellFactory(view -> new TreeCell<>() {
+ {
+ setOnMouseClicked(e -> {
+ // double click on an example loads it (Swing: loadButton.doClick())
+ if (e.getClickCount() == 2 && !isEmpty() && getItem() instanceof ExampleF) {
+ doLoadExample();
+ }
+ });
+ }
+
+ @Override
+ protected void updateItem(Object item, boolean empty) {
+ super.updateItem(item, empty);
+ // cells are reused: reset the text on every update
+ setText(empty || item == null ? null
+ : item instanceof ExampleF example ? example.getName()
+ : (String) item);
+ }
+ });
+
+ ScrollPane exampleScrollPane = new ScrollPane(exampleList);
+ exampleScrollPane.setFitToWidth(true);
+ VBox exampleSection = titled("Examples", exampleScrollPane);
+ VBox.setVgrow(exampleScrollPane, Priority.ALWAYS);
+
+ // create description label (Swing JTabbedPane)
+ VBox tabSection = new VBox();
+ tabSection.getChildren().add(tabPane);
+ VBox.setVgrow(tabPane, Priority.ALWAYS);
+
+ SplitPane split = new SplitPane(exampleSection, tabSection);
+ split.setDividerPositions(300.0 / 800);
+
+ // create the checkbox to hide example load on next startup (shared core setting)
+ ViewSettings vs = ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings();
+ CheckBox showAgainCheckbox = new CheckBox("Show this dialog on startup");
+ showAgainCheckbox.setSelected(vs.getShowLoadExamplesDialog());
+ showAgainCheckbox.selectedProperty()
+ .addListener((obs, old, selected) -> vs.setShowLoadExamplesDialog(selected));
+
+ // create "load" button
+ loadButton.setDefaultButton(true);
+ loadButton.setDisable(true);
+ loadButton.setOnAction(e -> doLoadExample());
+
+ // create "load proof" button
+ loadProofButton.setDisable(true);
+ loadProofButton.setOnAction(e -> doLoadProof());
+
+ // create "cancel" button
+ cancelButton.setCancelButton(true);
+ cancelButton.setOnAction(e -> doCancel());
+
+ HBox buttonPanel = new HBox(5);
+ buttonPanel.setAlignment(Pos.CENTER_RIGHT);
+ buttonPanel.setPadding(new Insets(5));
+ Region spacer = new Region();
+ HBox.setHgrow(spacer, Priority.ALWAYS);
+ buttonPanel.getChildren().addAll(showAgainCheckbox, spacer, loadButton, loadProofButton,
+ cancelButton);
+
+ BorderPane rootPane = new BorderPane();
+ rootPane.setCenter(split);
+ rootPane.setBottom(buttonPanel);
+
+ stage.setTitle("Load Example");
+ if (owner != null) {
+ stage.initOwner(owner);
+ stage.initModality(Modality.WINDOW_MODAL);
+ }
+ Scene scene = new Scene(rootPane, 800, 400);
+ ThemeManager.getInstance().style(scene);
+ stage.setScene(scene);
+ // Swing GuiUtilities.attachClickOnEscListener(cancelButton)
+ scene.setOnKeyPressed(e -> {
+ if (e.getCode() == KeyCode.ESCAPE) {
+ doCancel();
+ }
+ });
+
+ // select the first example (Swing: select and make visible the first leaf)
+ TreeItem firstLeaf = firstLeaf(root);
+ if (firstLeaf != null) {
+ for (TreeItem item = firstLeaf; item != null; item = item.getParent()) {
+ item.setExpanded(true);
+ }
+ exampleList.getSelectionModel().select(firstLeaf);
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // internal methods
+ // -------------------------------------------------------------------------
+
+ /**
+ * Wraps the given content in a section with a title label (the Swing original uses
+ * {@code TitledBorder}).
+ */
+ private static VBox titled(String title, Node content) {
+ Label heading = new Label(title);
+ heading.getStyleClass().add("dialog-section-title");
+ VBox box = new VBox(heading, content);
+ VBox.setVgrow(content, Priority.ALWAYS);
+ return box;
+ }
+
+ public static Path lookForExamples() {
+ // weigl: using java properties: -Dkey.examples.dir="..."
+ if (System.getProperty(KEY_EXAMPLE_DIR) != null) {
+ return Paths.get(System.getProperty(KEY_EXAMPLE_DIR));
+ }
+
+ // greatly simplified version without parent path lookup.
+ File projectRoot = IOUtil.getProjectRoot(ExampleChooserF.class);
+ File folder = projectRoot != null ? new File(projectRoot, EXAMPLES_PATH)
+ : new File(EXAMPLES_PATH);
+ if (!folder.exists()) {
+ File classLocation = IOUtil.getClassLocation(ExampleChooserF.class);
+ folder = classLocation != null ? new File(classLocation, EXAMPLES_PATH)
+ : new File(EXAMPLES_PATH);
+ }
+ return folder.toPath();
+ }
+
+ private static String fileAsString(Path f) {
+ try {
+ return Files.readString(f);
+ } catch (IOException e) {
+ LOGGER.error("Could not read file '{}'", f, e);
+ return "";
+ }
+ }
+
+ private void updateDescription() {
+ TreeItem item = exampleList.getSelectionModel().getSelectedItem();
+ if (item == null) {
+ return;
+ }
+
+ tabPane.getTabs().clear();
+
+ if (item.getValue() instanceof ExampleF example) {
+ if (example != selectedExample) {
+ addTab(example.getDescription(), "Description", true);
+ final String fileAsString = fileAsString(example.getObligationFile());
+ final int p = fileAsString.lastIndexOf("\\problem");
+ if (p >= 0) {
+ addTab(fileAsString.substring(p), "Proof Obligation", false);
+ }
+ for (Path file : example.getAdditionalFiles()) {
+ addTab(fileAsString(file), file.getFileName().toString(), false);
+ }
+ loadButton.setDisable(false);
+ loadProofButton.setDisable(!example.hasProof());
+ selectedExample = example;
+ }
+ } else {
+ selectedExample = null;
+ loadButton.setDisable(true);
+ loadProofButton.setDisable(true);
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // public interface
+ // -------------------------------------------------------------------------
+
+ private void addTab(String string, String name, boolean wrap) {
+ TextArea area = new TextArea(string);
+ area.setFont(ConfigF.DEFAULT.monoFont());
+ area.setEditable(false);
+ area.setWrapText(wrap);
+ Tab tab = new Tab(name, area);
+ tab.setClosable(false);
+ tabPane.getTabs().add(tab);
+ }
+
+ /**
+ * Shows the dialog (modal to the given owner), using the passed examples directory. If
+ * {@code null} is passed, tries to find the examples directory on its own.
+ *
+ * @param examplesDirString the examples directory, or {@code null} to look it up
+ * @param owner the owner window of the modal dialog (the main window stage)
+ * @return the file to load, or {@code null} if the dialog was canceled or no examples
+ * directory could be found
+ */
+ public static @Nullable Path showInstance(@Nullable Path examplesDirString,
+ @Nullable Window owner) {
+ // get examples directory
+ Path examplesDir;
+ if (examplesDirString == null) {
+ examplesDir = lookForExamples();
+ } else {
+ examplesDir = examplesDirString;
+ }
+
+ if (!Files.isDirectory(examplesDir)) {
+ Alert alert = new Alert(Alert.AlertType.ERROR);
+ alert.setTitle("Error loading examples");
+ alert.setHeaderText(null);
+ alert.setContentText("The examples directory cannot be found.\n"
+ + "Please install them at "
+ + (examplesDirString == null
+ ? IOUtil.getProjectRoot(ExampleChooserF.class) + "/"
+ : examplesDirString));
+ themeDialogPane(alert);
+ alert.showAndWait();
+ return null;
+ }
+
+ // show dialog (a fresh dialog is built on every invocation — see class comment)
+ ExampleChooserF instance = new ExampleChooserF(examplesDir, owner);
+ instance.stage.showAndWait();
+
+ // return result
+ return instance.fileToLoad;
+ }
+
+ /**
+ * Applies the current theme stylesheet to the content pane of an {@link Alert} (its scene
+ * only exists while it is shown, so the pane itself is styled — {@code Parent} stylesheets
+ * combine with the scene's).
+ */
+ static void themeDialogPane(Alert alert) {
+ alert.getDialogPane()
+ .getStylesheets()
+ .add(ThemeManager.getInstance().getTheme().stylesheetUrl());
+ }
+
+ /**
+ * Lists all examples in the given directory. This method is also accessed by the eclipse
+ * based projects.
+ *
+ * @param examplesDir The examples directory to list examples in.
+ * @return The found examples.
+ */
+ public static List listExamples(Path examplesDir) {
+ List result = new ArrayList<>(64);
+
+ final Path index = examplesDir.resolve("index").resolve("samplesIndex.txt");
+ try {
+ for (var line : Files.readAllLines(index)) {
+ line = line.trim();
+ if (line.startsWith("#") || line.isEmpty()) {
+ continue;
+ }
+ Path f = examplesDir.resolve(line);
+ try {
+ result.add(new ExampleF(f));
+ } catch (IOException e) {
+ LOGGER.warn("Cannot parse example {}; ignoring it.", f, e);
+ }
+ }
+ } catch (IOException e) {
+ LOGGER.warn("Error while reading samples", e);
+ }
+ return result;
+ }
+
+ /** The "Load Example" button: loads the example's obligation file. */
+ private void doLoadExample() {
+ if (selectedExample == null) {
+ // unreachable: the button is disabled without a selection (Swing throws here)
+ LOGGER.info("No example selected");
+ return;
+ }
+ fileToLoad = selectedExample.getObligationFile();
+ stage.close();
+ }
+
+ /** The "Load Proof" button: loads the example's saved proof file. */
+ private void doLoadProof() {
+ if (selectedExample == null || !selectedExample.hasProof()) {
+ // unreachable: the button is disabled unless the example has a proof
+ LOGGER.info("Selected example has no proof.");
+ return;
+ }
+ fileToLoad = selectedExample.getProofFile();
+ stage.close();
+ }
+
+ private void doCancel() {
+ fileToLoad = null;
+ stage.close();
+ }
+
+ /**
+ * Finds (or creates) the category folder for the given path segments (Swing
+ * {@code Example.findChild} over tree items).
+ */
+ private static TreeItem findChild(TreeItem root, String[] path, int from) {
+ if (from == path.length) {
+ return root;
+ }
+ for (TreeItem node : root.getChildren()) {
+ if (path[from].equals(node.getValue())) {
+ return findChild(node, path, from + 1);
+ }
+ }
+ // not found ==> add new
+ TreeItem node = new TreeItem<>(path[from]);
+ root.getChildren().add(node);
+ return findChild(node, path, from + 1);
+ }
+
+ /**
+ * @return the first leaf of the tree (the first example), or {@code null} if the tree is empty
+ */
+ private static @Nullable TreeItem firstLeaf(TreeItem node) {
+ if (node.getChildren().isEmpty()) {
+ return node.getValue() instanceof ExampleF ? node : null;
+ }
+ return firstLeaf(node.getChildren().get(0));
+ }
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/ExampleF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/ExampleF.java
new file mode 100644
index 00000000000..d78645566ed
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/ExampleF.java
@@ -0,0 +1,193 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.gui.fx;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * One example of the example chooser: wraps the index file of an example directory and parses the
+ * small property header (name, category path, obligation/proof file, additional files) plus the
+ * free-form description. Counter-part of {@code de.uka.ilkd.key.gui.Example} in the Swing module
+ * {@code key.ui}; the Swing class additionally carries the Swing tree-model integration
+ * ({@code addToTreeModel}/{@code findChild}), which the JavaFX port realizes with JavaFX tree
+ * items in {@link ExampleChooserF}.
+ */
+public final class ExampleF {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ExampleF.class);
+
+ /**
+ * This constant is accessed by the eclipse based projects.
+ */
+ public static final String KEY_FILE_NAME = "project.key";
+
+ private static final String PROOF_FILE_NAME = "project.proof";
+
+ /**
+ * The default category under which examples range if they do not have {@link #KEY_PATH} set.
+ */
+ public static final String DEFAULT_CATEGORY_PATH = "Unsorted";
+
+ /**
+ * The {@link Properties} key to specify the path in the tree.
+ */
+ public static final String KEY_PATH = "example.path";
+
+ /**
+ * The {@link Properties} key to specify the name of the example. Directory name if left open.
+ */
+ public static final String KEY_NAME = "example.name";
+
+ /**
+ * The {@link Properties} key to specify the file for the example. KEY_FILE_NAME by default
+ */
+ public static final String KEY_FILE = "example.file";
+
+ /**
+ * The {@link Properties} key to specify the proof file in the tree. May be left open
+ */
+ public static final String KEY_PROOF_FILE = "example.proofFile";
+
+ /**
+ * The {@link Properties} key prefix to specify additional files to load. Append 1, 2, 3, ...
+ */
+ public static final String ADDITIONAL_FILE_PREFIX = "example.additionalFile.";
+
+ /**
+ * The {@link Properties} key prefix to specify export files which are not shown as tabs in
+ * the example wizard but are extracted to Java projects in the Eclipse integration. Append
+ * 1, 2, 3, ...
+ */
+ public static final String EXPORT_FILE_PREFIX = "example.exportFile.";
+
+ /** The index file (usually {@code project.key}) that was parsed. */
+ private final Path exampleFile;
+ /** The directory containing the example. */
+ private final Path directory;
+ /** The description: every line of the index file after the first empty line. */
+ private final String description;
+ /** The parsed properties header of the index file. */
+ private final Properties properties;
+
+ /**
+ * Creates an example for the given index file and parses its header and description.
+ *
+ * @param file the index file of the example ({@code project.key} by default)
+ * @throws IOException if the file cannot be read
+ */
+ public ExampleF(Path file) throws IOException {
+ this.exampleFile = file;
+ this.directory = file.getParent();
+ this.properties = new Properties();
+ StringBuilder sb = new StringBuilder();
+ extractDescription(file, sb, properties);
+ this.description = sb.toString();
+ }
+
+ public Path getDirectory() {
+ return directory;
+ }
+
+ public Path getProofFile() {
+ return directory.resolve(properties.getProperty(KEY_PROOF_FILE, PROOF_FILE_NAME));
+ }
+
+ public Path getObligationFile() {
+ return directory.resolve(properties.getProperty(KEY_FILE, KEY_FILE_NAME));
+ }
+
+ public String getName() {
+ return properties.getProperty(KEY_NAME, directory.getFileName().toString());
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public Path getExampleFile() {
+ return exampleFile;
+ }
+
+ public List getAdditionalFiles() {
+ var result = new ArrayList();
+ int i = 1;
+ while (properties.containsKey(ADDITIONAL_FILE_PREFIX + i)) {
+ result.add(directory.resolve(properties.getProperty(ADDITIONAL_FILE_PREFIX + i)));
+ i++;
+ }
+ return result;
+ }
+
+ public List getExportFiles() {
+ var result = new ArrayList();
+ int i = 1;
+ while (properties.containsKey(EXPORT_FILE_PREFIX + i)) {
+ result.add(directory.resolve(properties.getProperty(EXPORT_FILE_PREFIX + i)));
+ i++;
+ }
+ return result;
+ }
+
+ /**
+ * @return the category path segments under which the example is shown in the chooser tree
+ */
+ public String[] getPath() {
+ return properties.getProperty(KEY_PATH, DEFAULT_CATEGORY_PATH).split("/");
+ }
+
+ /**
+ * @return whether the example directory declares a proof file ({@code example.proofFile})
+ */
+ public boolean hasProof() {
+ return properties.containsKey(KEY_PROOF_FILE);
+ }
+
+ @Override
+ public String toString() {
+ return getName();
+ }
+
+ /**
+ * Parses the index file: lines before the first empty line are {@code key: value} property
+ * entries (comment lines starting with {@code #} are skipped), everything after the first
+ * empty line is the description (Swing {@code Example.extractDescription}).
+ */
+ private static StringBuilder extractDescription(Path file, StringBuilder sb,
+ Properties properties) {
+ try {
+ boolean emptyLineSeen = false;
+ for (var line : Files.readAllLines(file, StandardCharsets.UTF_8)) {
+ if (emptyLineSeen) {
+ sb.append(line).append("\n");
+ } else {
+ String trimmed = line.trim();
+ if (trimmed.isEmpty()) {
+ emptyLineSeen = true;
+ } else {
+ if (!trimmed.startsWith("#")) {
+ String[] entry = trimmed.split(" *[:=] *", 2);
+ if (entry.length > 1) {
+ properties.put(entry[0], entry[1]);
+ }
+ }
+ }
+ }
+ }
+ } catch (IOException e) {
+ LOGGER.error("", e);
+ return sb;
+ }
+ return sb;
+ }
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/FxDriver.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/FxDriver.java
new file mode 100644
index 00000000000..6ed83ac6156
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/FxDriver.java
@@ -0,0 +1,29 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.gui.fx;
+
+import javafx.stage.Stage;
+
+/**
+ * A standalone view driver used during development and automated verification (milestone M2): if
+ * the system property {@code key.fx.driver} names a class, that class receives the primary stage
+ * instead of the full {@link MainWindowF}. A driver typically shows a single view together with a
+ * local {@code KeYSelectionModel} and a demo proof loaded via the core {@code KeYEnvironment},
+ * which allows per-view verification without the full main window.
+ *
+ * Drivers are development scaffolding; they may be removed once the views are integrated and
+ * verified in the main window.
+ */
+@FunctionalInterface
+public interface FxDriver {
+
+ /**
+ * Builds and shows the driver UI on the given stage.
+ *
+ * @param stage the primary stage of the JavaFX application
+ * @throws Exception on any setup error (reported by the launcher)
+ */
+ void start(Stage stage) throws Exception;
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/InteractiveRuleApplicationCompletionF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/InteractiveRuleApplicationCompletionF.java
new file mode 100644
index 00000000000..badb51a592f
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/InteractiveRuleApplicationCompletionF.java
@@ -0,0 +1,42 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.gui.fx;
+
+import de.uka.ilkd.key.proof.Goal;
+import de.uka.ilkd.key.rule.IBuiltInRuleApp;
+
+/**
+ * Instances of class implementing this interface are able to complete rule applications. At the
+ * moment they are only responsible for built-in rule apps, but they should be generalized to treat
+ * taclets as well.
+ *
+ * Port of {@code de.uka.ilkd.key.gui.InteractiveRuleApplicationCompletion} in the Swing module
+ * {@code key.ui} (that class lives in key.ui and is therefore not visible to this module; the
+ * interface contract is copied verbatim). Implementations are registered at the dispatch chain of
+ * {@link WindowUserInterfaceControlF#register(InteractiveRuleApplicationCompletionF)}, the FX
+ * mirror of the Swing registry in the {@code WindowUserInterfaceControl} constructor
+ * (WindowUserInterfaceControl.java:74-82).
+ */
+public interface InteractiveRuleApplicationCompletionF {
+
+ /**
+ * method called to complete the given builtin rule application
+ *
+ * @param app the app to complete
+ * @param goal the goal where the app will be applied
+ * @param forced a boolean indicating if the user shall be bothered if the instantiation is
+ * unique or can be chosen in a reasonable way as if unique
+ * @return the completed app or null if completion was not possible
+ */
+ IBuiltInRuleApp complete(IBuiltInRuleApp app, Goal goal, boolean forced);
+
+ /**
+ * checks if this instance is responsible for the given app
+ *
+ * @param app the rule app
+ * @return true iff this instance might be able to complete the app
+ */
+ boolean canComplete(IBuiltInRuleApp app);
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/IssueDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/IssueDialogF.java
new file mode 100644
index 00000000000..4bd7d190344
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/IssueDialogF.java
@@ -0,0 +1,547 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.gui.fx;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import javafx.application.Platform;
+import javafx.geometry.Insets;
+import javafx.geometry.Orientation;
+import javafx.scene.control.ButtonType;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.Dialog;
+import javafx.scene.control.DialogPane;
+import javafx.scene.control.Label;
+import javafx.scene.control.ListView;
+import javafx.scene.control.SplitPane;
+import javafx.scene.control.TextArea;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+import javafx.stage.Window;
+import javafx.util.Duration;
+
+import de.uka.ilkd.key.speclang.PositionedString;
+import de.uka.ilkd.key.speclang.SLEnvInput;
+import de.uka.ilkd.key.util.ExceptionTools;
+import de.uka.ilkd.key.util.parsing.BuildingExceptions;
+
+import org.key_project.util.collection.ImmutableSet;
+import org.key_project.util.parsing.Location;
+import org.key_project.util.parsing.Position;
+
+import org.jspecify.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The central error/warning dialog of the JavaFX UI: the port of the Swing
+ * {@code de.uka.ilkd.key.gui.IssueDialog} (key.ui, 877 lines). It shows a list of issues
+ * (exceptions or non-critical specification warnings) with a source preview of the selected
+ * issue and, for critical exceptions, an optional stack-trace pane ("Show Details").
+ *
+ * Ported behaviors (Swing evidence):
+ *
+ * Static entry points {@code showExceptionDialog} ({@code IssueDialog.java:582-594}) and
+ * {@code showWarningsIfNecessary} ({@code :602-619}) with the session-wide "Ignore these
+ * warnings" set ({@code ignoredWarnings}, {@code :92}, applied in {@code accept}
+ * {@code :658-663}).
+ * Exception message/location extraction via the core {@link ExceptionTools#getMessages}
+ * ({@code extractMessage}, {@code :635-655}); the full stack trace is the issue's detail text
+ * ({@code :636-644}).
+ * Issue list sorted by location ({@code :244-245}); selection updates the source preview,
+ * the location labels and the stack trace ({@code updatePreview}, {@code :665-729},
+ * {@code updateStackTrace}, {@code :747-749}); the source file is read from the issue's
+ * location URI with a per-dialog cache ({@code :688-702}); the error position is mapped from
+ * the 1-based line/column to an offset and marked in the preview ({@code :716-723},
+ * {@code :766-795}).
+ * Modal dialog with a default OK button ({@code :536}, {@code :544}).
+ *
+ * Documented deviations: no HTML rendering (issue texts and the URL-click decoration of
+ * {@code decorateHTML}, {@code :183-208}, are shown as plain text — URLs are still visible),
+ * no syntax highlighting/lexer in the source preview (plain monospace text), no line-number
+ * column, and the "Edit File"/"Send Feedback" buttons of the Swing dialog
+ * ({@code :513-534}) are deferred (they need the source view action and the feedback
+ * infrastructure). The scroll-to-error position is approximated via the {@link TextArea}'s
+ * scroll properties. Behavioral deviation : the dialog is shown with {@link #show}
+ * (non-blocking) instead of the Swing blocking {@code showAndWait} — a modal
+ * {@code showAndWait} on the FX Application Thread can deadlock callers that join on that
+ * thread; application modality is preserved, so the other windows are blocked while it is
+ * open.
+ */
+public final class IssueDialogF {
+ private static final Logger LOGGER = LoggerFactory.getLogger(IssueDialogF.class);
+
+ /** Default text for critical issues (runtime exceptions), Swing {@code :80}. */
+ private static final String CRITICAL_ISSUE = "The following exception occurred:";
+ /** Default text for non-critical issues (JML specification warnings), Swing {@code :84-86}. */
+ private static final String NON_CRITICAL_ISSUE = String.format(
+ "The following non-fatal problems occurred when translating your %s specifications:",
+ SLEnvInput.getLanguage());
+
+ /** warnings which have been marked to be ignored by the user (in this KeY run) */
+ private static final Set ignoredWarnings = new HashSet<>();
+
+ /** the most recently created dialog (self-test hook, see {@link #getLastDialog()}) */
+ private static @Nullable IssueDialogF lastDialog;
+
+ /** Severity of an issue (Swing {@code PositionedIssueString.Kind}). */
+ public enum Kind {
+ ERROR, WARNING, INFO
+ }
+
+ /**
+ * One issue shown by this dialog — the FX-side equivalent of the Swing
+ * {@code PositionedIssueString} (which lives in {@code key.ui} and cannot be reused).
+ *
+ * @param text the issue text
+ * @param location the source location, may be {@code Location.UNDEFINED}
+ * @param additionalInfo additional information such as a stack trace
+ * @param kind the severity
+ */
+ public record IssueEntry(String text, Location location, String additionalInfo, Kind kind) {
+ public IssueEntry(String text) {
+ this(text, Location.UNDEFINED, "", Kind.ERROR);
+ }
+
+ public IssueEntry(PositionedString ps, String additionalInfo, Kind kind) {
+ this(ps.text, ps.location, additionalInfo, kind);
+ }
+ }
+
+ private final Dialog dialog;
+ private final ListView listWarnings;
+ private final TextArea txtSource = new TextArea();
+ private final TextArea txtStacktrace = new TextArea();
+ private final Label fileField = new Label();
+ private final Label lineField = new Label();
+ private final Label columnField = new Label();
+ private final CheckBox chkIgnoreWarnings =
+ new CheckBox("Ignore these warnings for the current session");
+ private final CheckBox chkDetails = new CheckBox("Show Details");
+ private final BorderPane stacktracePanel = new BorderPane();
+ private final Map fileContentsCache = new HashMap<>();
+ private final boolean critical;
+ private final List issues;
+ /**
+ * whether the dialog was shown at least once (self-test hook; {@link #isVisible()} is
+ * {@code false} again as soon as a modal dialog is dismissed)
+ */
+ private boolean shown;
+
+ private IssueDialogF(Window owner, String title, String head, Collection issueSet,
+ boolean critical) {
+ this.critical = critical;
+ this.issues = new ArrayList<>(issueSet);
+ this.issues.sort(Comparator.comparing(IssueEntry::location));
+ lastDialog = this;
+
+ dialog = new Dialog<>();
+ dialog.setTitle(title);
+ if (owner != null) {
+ dialog.initOwner(owner);
+ }
+ dialog.setResizable(true);
+
+ listWarnings = new ListView<>();
+ listWarnings.getItems().setAll(issues);
+ listWarnings.getStyleClass().add("issue-dialog-list");
+ listWarnings.getSelectionModel().selectedItemProperty()
+ .addListener((obs, old, issue) -> updatePreview(issue));
+ listWarnings.setCellFactory(view -> new javafx.scene.control.ListCell<>() {
+ private final Label label = new Label();
+
+ {
+ label.setWrapText(true);
+ label.getStyleClass().add("issue-dialog-cell");
+ setGraphic(null);
+ setContentDisplay(javafx.scene.control.ContentDisplay.GRAPHIC_ONLY);
+ }
+
+ @Override
+ protected void updateItem(IssueEntry item, boolean empty) {
+ super.updateItem(item, empty);
+ if (empty || item == null) {
+ setText(null);
+ label.setText(null);
+ } else {
+ label.setText(item.text());
+ setGraphic(label);
+ }
+ }
+ });
+
+ // source preview with the location labels (Swing createSourcePanel, :498-572)
+ txtSource.setEditable(false);
+ txtSource.getStyleClass().add("issue-dialog-source");
+ txtSource.setWrapText(false);
+ txtStacktrace.setEditable(false);
+ txtStacktrace.getStyleClass().add("issue-dialog-source");
+ stacktracePanel.setStyle("-fx-border-color: -key-border; -fx-border-width: 1 0 0 0;");
+ Label stackTitle = new Label("Stack Trace");
+ stackTitle.getStyleClass().add("dialog-section-title");
+ stacktracePanel.setTop(stackTitle);
+ stacktracePanel.setCenter(txtStacktrace);
+ stacktracePanel.setVisible(false);
+ stacktracePanel.setManaged(false);
+
+ fileField.getStyleClass().add("issue-dialog-location");
+ VBox sourcePanel = new VBox(4, new HBox(12, fileField, lineField, columnField), txtSource);
+ VBox.setVgrow(txtSource, Priority.ALWAYS);
+
+ SplitPane splitCenter = new SplitPane(listWarnings, sourcePanel);
+ splitCenter.setOrientation(Orientation.VERTICAL);
+ splitCenter.setDividerPositions(0.35);
+ VBox content = new VBox(6, new Label(head), splitCenter);
+ content.setPadding(new Insets(6));
+ if (!critical) {
+ content.getChildren().add(chkIgnoreWarnings);
+ }
+ chkDetails.selectedProperty().addListener((obs, old, selected) -> {
+ stacktracePanel.setVisible(selected);
+ stacktracePanel.setManaged(selected);
+ // the issue with a stack trace drives the pane content; re-sync
+ updateStackTrace(listWarnings.getSelectionModel().getSelectedItem());
+ });
+ VBox.setVgrow(splitCenter, Priority.ALWAYS);
+ content.getChildren().add(chkDetails);
+ content.getChildren().add(stacktracePanel);
+
+ DialogPane pane = dialog.getDialogPane();
+ pane.getStyleClass().add("issue-dialog");
+ pane.setContent(content);
+ pane.getButtonTypes().setAll(ButtonType.OK);
+ dialog.setResultConverter(button -> {
+ accept();
+ return null;
+ });
+
+ // "Show Details" only makes sense with a stack trace (Swing :347-364)
+ chkDetails.setDisable(issues.stream().allMatch(it -> it.additionalInfo().isEmpty()));
+ chkIgnoreWarnings.setSelected(false);
+ if (!issues.isEmpty()) {
+ listWarnings.getSelectionModel().select(0);
+ }
+ updatePreview(listWarnings.getSelectionModel().getSelectedItem());
+ }
+
+ /**
+ * Shows the dialog with a single exception. The stacktrace is extracted and can optionally
+ * be shown in the dialog (Swing {@code showExceptionDialog}, {@code IssueDialog.java:582-594}).
+ * Must be called on the FX Application Thread; other threads are marshalled.
+ *
+ * Important: make sure to also log the exception before showing the dialog!
+ *
+ * @param owner the owner of the dialog (will be blocked)
+ * @param exception the exception to display
+ */
+ public static void showExceptionDialog(Window owner, Throwable exception) {
+ if (!Platform.isFxApplicationThread()) {
+ Platform.runLater(() -> showExceptionDialog(owner, exception));
+ return;
+ }
+ if (exception instanceof BuildingExceptions be) {
+ be.getErrors().forEach(it -> LOGGER.info("Error", it));
+ }
+ createExceptionDialog(owner, exception).showAndWait();
+ }
+
+ /**
+ * Creates (but does not show) the exception dialog; used by the self test, which needs to
+ * auto-close the dialog.
+ *
+ * @param owner the owner of the dialog
+ * @param exception the exception to display
+ * @return the dialog, not yet shown
+ */
+ public static IssueDialogF createExceptionDialog(Window owner, Throwable exception) {
+ return new IssueDialogF(owner, "Error", CRITICAL_ISSUE, extractMessage(exception), true);
+ }
+
+ /**
+ * Shows the dialog of a set of (non-critical) parser warnings (Swing
+ * {@code showWarningsIfNecessary}, {@code IssueDialog.java:602-619}); warnings the user
+ * chose to ignore for this session are filtered out.
+ *
+ * @param owner the owner of the dialog (will be blocked)
+ * @param warnings the set of warnings, will be sorted by file when displaying
+ */
+ public static void showWarningsIfNecessary(Window owner,
+ ImmutableSet warnings) {
+ List issues = new ArrayList<>();
+ for (PositionedString ps : warnings.toSet()) {
+ IssueEntry entry = new IssueEntry(ps, "", Kind.WARNING);
+ if (!ignoredWarnings.contains(entry)) {
+ issues.add(entry);
+ }
+ }
+ // do not show warnings dialog if all warnings are ignored (Swing :606-607)
+ if (!issues.isEmpty()) {
+ new IssueDialogF(owner, SLEnvInput.getLanguage() + " warning(s)", NON_CRITICAL_ISSUE,
+ issues, false).showAndWait();
+ }
+ }
+
+ /**
+ * Opens a non-critical issue list for pre-collected entries, e.g. the javac diagnostics of
+ * the Java Compiler Check extension (Swing {@code IssueDialog} constructed by
+ * {@code JavacExtension} with a sorted set of {@code PositionedIssueString}s,
+ * JavacExtension.java:100-108).
+ *
+ * @param owner the owner of the dialog (will be blocked)
+ * @param title the dialog title
+ * @param head the explaining text above the list
+ * @param issueSet the issues to display, will be sorted by location
+ */
+ public static void showIssuesDialog(Window owner, String title, String head,
+ Collection issueSet) {
+ if (!Platform.isFxApplicationThread()) {
+ Platform.runLater(() -> showIssuesDialog(owner, title, head, issueSet));
+ return;
+ }
+ new IssueDialogF(owner, title, head, new ArrayList<>(issueSet), false).showAndWait();
+ }
+
+ /**
+ * Shows the issue dialog for arbitrary issues with the "critical" styling (Swing
+ * {@code WindowUserInterfaceControl.showIssueDialog}, {@code :633-641}).
+ *
+ * @param owner the owner of the dialog
+ * @param title the dialog title
+ * @param issues the issues to show
+ */
+ public static void showIssues(Window owner, String title,
+ Collection issues) {
+ Set entries = issues.stream()
+ .map(it -> new IssueEntry(it, "", Kind.ERROR))
+ .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
+ new IssueDialogF(owner, title, CRITICAL_ISSUE, entries, true).showAndWait();
+ }
+
+ /**
+ * Turns a thrown exception into the set of issues shown by this dialog (message, source
+ * location and the full stack trace as detail) — Swing {@code extractMessage}
+ * ({@code IssueDialog.java:635-655}); the extraction itself is delegated to the core
+ * {@link ExceptionTools#getMessages}.
+ *
+ * @param exception the exception to extract the data from
+ * @return one issue per contained problem
+ */
+ public static Set extractMessage(Throwable exception) {
+ String stackTrace;
+ try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) {
+ exception.printStackTrace(pw);
+ stackTrace = sw.toString();
+ } catch (IOException e) {
+ stackTrace = "";
+ }
+
+ Set result = new LinkedHashSet<>();
+ for (PositionedString ps : ExceptionTools.getMessages(exception)) {
+ result.add(new IssueEntry(ps, stackTrace, Kind.ERROR));
+ }
+ if (result.isEmpty()) {
+ result.add(new IssueEntry("Constructing the error message failed!"));
+ }
+ return result;
+ }
+
+ /** The OK action: remember ignored warnings and close (Swing {@code accept}, :658-663). */
+ private void accept() {
+ if (!critical && chkIgnoreWarnings.isSelected()) {
+ ignoredWarnings.addAll(issues);
+ }
+ dialog.close();
+ }
+
+ /**
+ * Shows the dialog non-blockingly and closes it after the given delay — a helper for the
+ * seam self test, which cannot dismiss a modal dialog interactively.
+ *
+ * @param autoClose the delay after which the dialog closes itself
+ */
+ public void showAndCloseAfter(Duration autoClose) {
+ shown = true;
+ dialog.show();
+ javafx.animation.PauseTransition pause = new javafx.animation.PauseTransition(autoClose);
+ pause.setOnFinished(ignored -> dialog.close());
+ pause.play();
+ }
+
+ /**
+ * Updates the source preview and the location labels for the selected issue (Swing
+ * {@code updatePreview}, {@code IssueDialog.java:665-729}).
+ *
+ * @param issue the selected issue, may be null
+ */
+ private void updatePreview(@Nullable IssueEntry issue) {
+ if (issue == null) {
+ fileField.setText("");
+ lineField.setText("");
+ columnField.setText("");
+ txtSource.setText("");
+ updateStackTrace(null);
+ return;
+ }
+ updateStackTrace(issue);
+ Location location = issue.location();
+ Position pos = location.getPosition();
+ columnField.setText("Column: " + pos.column());
+ lineField.setText("Line: " + pos.line());
+
+ URI uri = location.getFileURI().orElse(null);
+ if (uri == null) {
+ fileField.setText("");
+ txtSource.setText("[SOURCE COULD NOT BE LOADED]");
+ return;
+ }
+ if (uri.getScheme() == null) {
+ uri = URI.create("file:" + uri.getPath());
+ }
+ fileField.setText("URL: " + uri);
+ final URI uriKey = uri;
+ String source = fileContentsCache.computeIfAbsent(uriKey, fn -> {
+ try {
+ return readFrom(uriKey);
+ } catch (IOException e) {
+ LOGGER.debug("Unknown IOException!", e);
+ return "[SOURCE COULD NOT BE LOADED]\n" + e.getMessage();
+ }
+ });
+ txtSource.setText(source);
+
+ // mark the error position: map the 1-based position to an offset and select the word at
+ // the offset (Swing addHighlights, :766-795; the offset mapping mirrors
+ // getOffsetFromLineColumn, :823-830)
+ if (!pos.isNegative() && !source.isEmpty()) {
+ int offset = offsetFromLineColumn(source, pos);
+ int end = offset;
+ if (offset < source.length()
+ && Character.isJavaIdentifierPart(source.charAt(offset))) {
+ while (end < source.length()
+ && Character.isJavaIdentifierPart(source.charAt(end))) {
+ end++;
+ }
+ } else if (offset < source.length()
+ && !Character.isWhitespace(source.charAt(offset))) {
+ end = offset + 1;
+ }
+ txtSource.selectRange(offset, Math.min(end, source.length()));
+ scrollToOffset(offset);
+ } else {
+ txtSource.deselect();
+ }
+ }
+
+ /** Shows the additional information (e.g. the stack trace) (Swing :747-749). */
+ private void updateStackTrace(@Nullable IssueEntry issue) {
+ txtStacktrace.setText(issue == null ? "" : issue.additionalInfo());
+ }
+
+ /**
+ * Approximate scroll-to-error: the {@link TextArea} exposes only scroll offsets, so the
+ * pixel position of the error line is estimated from the line number and the font metrics
+ * (Swing scrolls exactly via {@code scrollRectToVisible}, {@code IssueDialog.java:736-745}).
+ */
+ private void scrollToOffset(int offset) {
+ String before = txtSource.getText(0, Math.min(offset, txtSource.getLength()));
+ int line = before.split("\n", -1).length - 1;
+ double lineHeight = Math.max(txtSource.getFont().getSize() * 1.4, 1.0);
+ Platform.runLater(() -> txtSource.setScrollTop(Math.max(0, line * lineHeight - 50)));
+ }
+
+ /**
+ * Maps a 1-based {@link Position} to a character offset in {@code source} (Swing
+ * {@code getOffsetFromLineColumn}, {@code IssueDialog.java:823-830}; the line model is the
+ * plain-text line split here).
+ *
+ * @param source the source text shown in the preview
+ * @param pos a 1-based position
+ * @return the character offset of that position within {@code source}
+ */
+ static int offsetFromLineColumn(String source, Position pos) {
+ String[] lines = source.split("\n", -1);
+ int line = Math.max(0, Math.min(pos.line() - 1, lines.length - 1));
+ int column = Math.max(0, pos.column() - 1);
+ int offset = 0;
+ for (int i = 0; i < line; i++) {
+ offset += lines[i].length() + 1; // + '\n'
+ }
+ return Math.min(offset + column, Math.max(0, source.length() - 1));
+ }
+
+ /**
+ * Reads the resource at the given URI as a string (Swing uses
+ * {@code IOUtil.readFrom}, {@code IssueDialog.java:693}).
+ */
+ private static String readFrom(URI uri) throws IOException {
+ try (var in = uri.toURL().openStream()) {
+ return new String(in.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8);
+ }
+ }
+
+ /** @return the underlying JavaFX dialog (for owners/tests) */
+ public Dialog getDialog() {
+ return dialog;
+ }
+
+ /**
+ * @return the most recently created dialog, if any — a self-test hook: the seam self test
+ * asserts that the {@code reportException} callback actually opened an
+ * {@link IssueDialogF} (the dialog itself cannot be enumerated from the stage)
+ */
+ public static Optional getLastDialog() {
+ return Optional.ofNullable(lastDialog);
+ }
+
+ /** @return whether the dialog is currently shown (self-test hook) */
+ public boolean isVisible() {
+ return dialog.isShowing();
+ }
+
+ /**
+ * @return whether the dialog was shown at least once — unlike {@link #isVisible()}, this
+ * stays {@code true} after a modal dialog is dismissed (self-test hook: the seam
+ * verification runs after the user closed the blocking {@code showAndWait})
+ */
+ public boolean wasShown() {
+ return shown;
+ }
+
+ /** @return the issues shown by this dialog (self-test hook) */
+ public List getIssues() {
+ return java.util.Collections.unmodifiableList(issues);
+ }
+
+ /** Forwards to the underlying dialog (this class wraps a {@link Dialog}). */
+ public void showAndWait() {
+ shown = true;
+ dialog.showAndWait();
+ }
+
+ /**
+ * Closes the dialog if it is showing — the programmatic counterpart of clicking "OK". The
+ * seam self test pre-schedules this dismissal before firing {@code reportException}, so the
+ * modal {@link #showAndWait()} returns without a user having to close it (headless runs).
+ */
+ public void close() {
+ if (dialog.isShowing()) {
+ dialog.close();
+ }
+ }
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/LogStartupListenerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/LogStartupListenerF.java
new file mode 100644
index 00000000000..3ac4b3e53a8
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/LogStartupListenerF.java
@@ -0,0 +1,70 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.gui.fx;
+
+import de.uka.ilkd.key.settings.PathConfig;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.spi.LoggerContextListener;
+import ch.qos.logback.core.Context;
+import ch.qos.logback.core.spi.ContextAwareBase;
+import ch.qos.logback.core.spi.LifeCycle;
+
+/**
+ * Provides the {@code LOG_DIR}
+ * {@link PathConfig.KeyPaths#logDirectory} to the {@code logback.xml} configuration of
+ * {@code key.ui.fx}.
+ *
+ * Port of {@code de.uka.ilkd.key.gui.utilities.LoggerStartupListener} in the Swing module
+ * {@code key.ui} (that class lives in key.ui and is not visible to this module); the FX
+ * {@code logback.xml} needs it so that {@code LogViewF} can tail the per-run log file.
+ */
+public class LogStartupListenerF extends ContextAwareBase
+ implements LoggerContextListener, LifeCycle {
+ private boolean started = false;
+
+ @Override
+ public void start() {
+ if (started) {
+ return;
+ }
+ Context context = getContext();
+ context.putProperty("LOG_DIR",
+ PathConfig.currentPaths.logDirectory.toAbsolutePath().toString());
+ started = true;
+ }
+
+ @Override
+ public void stop() {
+ }
+
+ @Override
+ public boolean isStarted() {
+ return started;
+ }
+
+ @Override
+ public boolean isResetResistant() {
+ return true;
+ }
+
+ @Override
+ public void onStart(LoggerContext context) {
+ }
+
+ @Override
+ public void onReset(LoggerContext context) {
+ }
+
+ @Override
+ public void onStop(LoggerContext context) {
+ }
+
+ @Override
+ public void onLevelChange(Logger logger, Level level) {
+ }
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/LogViewF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/LogViewF.java
new file mode 100644
index 00000000000..fcb69a24f28
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/LogViewF.java
@@ -0,0 +1,350 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.gui.fx;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+import javafx.application.Platform;
+import javafx.geometry.Insets;
+import javafx.scene.Scene;
+import javafx.scene.control.Button;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.Label;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.control.TextArea;
+import javafx.scene.control.TextField;
+import javafx.scene.control.TitledPane;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.FlowPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.VBox;
+import javafx.stage.Stage;
+import javafx.stage.Window;
+
+import de.uka.ilkd.key.settings.PathConfig;
+
+import org.jspecify.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The log viewer of the JavaFX UI — the port of the Swing {@code de.uka.ilkd.key.gui.LogView}
+ * (key.ui, 325 lines). It opens a non-modal window that live-tails the current log file with
+ * level/message/package filters ({@code ShowLogAction}, {@code LogView.java:88-126}; the Swing
+ * status-line extension button has no FX extension SPI yet, so the FX entry point is the View
+ * menu).
+ *
+ * Ported behaviors (Swing evidence):
+ *
+ * Log record format: the pipe-separated fields {@code date|level|thread|logger|file:line|
+ * message|exception} of the Swing logback FILE appender; fields are joined with the Swing
+ * spacing and embedded newlines are unescaped ({@code appendLine}, {@code LogView.java:260-269},
+ * message escaping {@code :690} in the logback pattern).
+ * Level checkboxes ERROR/WARN/INFO/DEBUG/TRACE and text/package filters re-render on change
+ * ({@code :157-161, 209-216}); lines starting with {@code #} (the logback pattern header) are
+ * skipped ({@code :239-241}).
+ * Live refresh via a {@link WatchService} on the log directory ({@code
+ * FileWatcherService}, {@code :54-85}), started on show and interrupted on close
+ * ({@code :100-109, 280-286}); refresh is paused while the window is iconified
+ * ({@code :111-119, 288-299}).
+ * "Open log externally" via the system {@code Desktop} ({@code OpenLogExternalAction},
+ * {@code :302-324}).
+ *
+ * Documented deviations: the fields are rendered as plain text (the Swing per-field styles of
+ * {@code :129-147} would need a styled text area), and the Swing package-prefix filter is
+ * fixed in the port: its guard {@code pkgFilterApply = pkgFilter.isEmpty()}
+ * ({@code :227, 244}) inverted the condition so that a non-empty package filter never filtered
+ * anything.
+ */
+public final class LogViewF {
+ private static final Logger LOGGER = LoggerFactory.getLogger(LogViewF.class);
+
+ /** the fields of one log record (logback pattern of the Swing UI) */
+ private static final int FIELD_COUNT = 7;
+
+ private final TextArea txtView = new TextArea();
+ private final CheckBox chkInfo = new CheckBox("INFO");
+ private final CheckBox chkWarn = new CheckBox("WARN");
+ private final CheckBox chkDebug = new CheckBox("DEBUG");
+ private final CheckBox chkTrace = new CheckBox("TRACE");
+ private final CheckBox chkError = new CheckBox("ERROR");
+ private final TextField txtMessageSearch = new TextField();
+ private final TextField txtPackageSearch = new TextField();
+ private final Thread fileWatcherServiceThread;
+ private final Path logFile;
+ /** the root layout of the window (built in the constructor) */
+ private final javafx.scene.Parent root;
+ private boolean pause = false;
+
+ /** Creates a log view bound to the given log file (public for the seam self test). */
+ public LogViewF(Path logFile) {
+ this.logFile = logFile;
+
+ txtView.setEditable(false);
+ txtView.getStyleClass().add("log-view-area");
+ txtView.setWrapText(false);
+
+ chkInfo.setSelected(true);
+ chkWarn.setSelected(true);
+ chkError.setSelected(true);
+
+ FlowPane levelBox = new FlowPane(8, 4, chkError, chkWarn, chkInfo, chkDebug, chkTrace);
+ HBox messageBox = new HBox(8, new Label("Text:"), txtMessageSearch);
+ HBox.setHgrow(txtMessageSearch, javafx.scene.layout.Priority.ALWAYS);
+ HBox packageBox = new HBox(8, new Label("Package (Prefix):"), txtPackageSearch);
+ HBox.setHgrow(txtPackageSearch, javafx.scene.layout.Priority.ALWAYS);
+ VBox filterBox = new VBox(4, new Label("Level:"), levelBox, messageBox, packageBox);
+ filterBox.setPadding(new Insets(4));
+ TitledPane filterPane = new TitledPane("Filter", filterBox);
+ filterPane.setCollapsible(false);
+
+ Button openExternal =
+ new Button("Open log externally" + (logFile == null ? " (no log file)" : ""));
+ openExternal.setOnAction(e -> openExternal());
+ openExternal.setDisable(logFile == null);
+
+ ScrollPane scroll = new ScrollPane(txtView);
+ scroll.setFitToWidth(true);
+ scroll.setFitToHeight(true);
+ BorderPane rootPane = new BorderPane(scroll, filterPane, null,
+ new javafx.scene.layout.BorderPane(openExternal), null);
+ rootPane.setPadding(new Insets(4));
+ root = rootPane;
+
+ var watcher = logFile != null ? new FileWatcherService(logFile, this::refresh) : null;
+ fileWatcherServiceThread =
+ watcher != null ? new Thread(watcher, "fx-log-file-watcher") : null;
+
+ for (CheckBox box : List.of(chkTrace, chkDebug, chkInfo, chkWarn, chkError)) {
+ box.setOnAction(e -> refresh());
+ }
+ txtMessageSearch.setOnAction(e -> refresh());
+ txtPackageSearch.setOnAction(e -> refresh());
+
+ refresh();
+ }
+
+ /**
+ * Opens the log view window (non-modal, 800x600, centered on the owner; Swing
+ * {@code ShowLogAction.actionPerformed}, {@code LogView.java:88-126}). The file watcher is
+ * started on show and stopped when the window closes.
+ *
+ * @param owner the owner window, may be null
+ */
+ public static void showInstance(Window owner) {
+ LogViewF view = new LogViewF(resolveLogFile());
+ Stage stage = new Stage();
+ stage.setTitle("Log View");
+ stage.setScene(new Scene(view.root, 800, 600));
+ // pause the refresh while the window is minimized (Swing setPause on iconified)
+ stage.iconifiedProperty().addListener((obs, old, iconified) -> view.setPause(iconified));
+ stage.setOnCloseRequest(e -> view.dispose());
+ stage.setOnHidden(e -> view.dispose());
+ if (owner != null) {
+ stage.setX(owner.getX() + 40);
+ stage.setY(owner.getY() + 40);
+ }
+ stage.show();
+ view.onShow();
+ }
+
+ /**
+ * Resolves the current log file: the logback root "FILE" appender's file if configured (the
+ * Swing {@code Log.getCurrentLogFile}, {@code Log.java:45-51}; key.ui.fx has no logback.xml
+ * yet), otherwise the newest {@code key_*.log} in the log directory.
+ *
+ * @return the log file, or {@code null} if none can be determined
+ */
+ public static @Nullable Path resolveLogFile() {
+ try {
+ ch.qos.logback.classic.Logger root =
+ (ch.qos.logback.classic.Logger) org.slf4j.LoggerFactory
+ .getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
+ if (root.getAppender("FILE") instanceof ch.qos.logback.core.FileAppender> file) {
+ return Path.of(file.getFile());
+ }
+ } catch (RuntimeException e) {
+ LOGGER.debug("Could not resolve the logback FILE appender", e);
+ }
+ Path logDir = PathConfig.currentPaths.logDirectory;
+ if (Files.isDirectory(logDir)) {
+ try (Stream files = Files.list(logDir)) {
+ Optional newest = files
+ .filter(p -> p.getFileName() != null
+ && p.getFileName().toString().startsWith("key_")
+ && p.getFileName().toString().endsWith(".log"))
+ .max(Comparator.comparing(LogViewF::lastModified));
+ return newest.orElse(null);
+ } catch (IOException e) {
+ LOGGER.warn("Could not list the log directory {}", logDir, e);
+ }
+ }
+ return null;
+ }
+
+ private static long lastModified(Path path) {
+ try {
+ return Files.getLastModifiedTime(path).toMillis();
+ } catch (IOException e) {
+ return 0L;
+ }
+ }
+
+ /** Starts the file watcher (Swing {@code LogViewPane.onShow}, {@code :280-282}). */
+ private void onShow() {
+ if (fileWatcherServiceThread != null) {
+ fileWatcherServiceThread.setDaemon(true);
+ fileWatcherServiceThread.start();
+ }
+ }
+
+ /** Interrupts the file watcher (Swing {@code LogViewPane.dispose}, {@code :284-286}). */
+ private void dispose() {
+ if (fileWatcherServiceThread != null) {
+ fileWatcherServiceThread.interrupt();
+ }
+ }
+
+ /** Pauses/resumes the live refresh (Swing {@code LogViewPane.setPause}, {@code :288-299}). */
+ private void setPause(boolean flag) {
+ this.pause = flag;
+ }
+
+ /**
+ * Re-reads the log file and applies the filters (Swing {@code LogViewPane.refresh},
+ * {@code :220-258}). The Swing package filter bug is fixed here: a non-empty package filter
+ * actually filters. Public for the seam self test.
+ */
+ public void refresh() {
+ if (pause) {
+ return;
+ }
+ StringBuilder buffer = new StringBuilder();
+ if (logFile == null) {
+ buffer.append("[NO LOG FILE FOUND - configure a logback FILE appender or check ")
+ .append(PathConfig.currentPaths.logDirectory).append("]");
+ } else {
+ String pkgFilter = txtPackageSearch.getText().trim();
+ String msgFilter = txtMessageSearch.getText().trim();
+ boolean levelError = chkError.isSelected();
+ boolean levelInfo = chkInfo.isSelected();
+ boolean levelWarn = chkWarn.isSelected();
+ boolean levelDebug = chkDebug.isSelected();
+ boolean levelTrace = chkTrace.isSelected();
+ try (var reader = Files.newBufferedReader(logFile, StandardCharsets.UTF_8)) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ if (line.isEmpty() || line.charAt(0) == '#') {
+ continue;
+ }
+ String[] fields = line.split("[|]", FIELD_COUNT);
+ if (fields.length < FIELD_COUNT) {
+ continue;
+ }
+ boolean skipByMsgFilter = !msgFilter.isEmpty()
+ && !fields[5].contains(msgFilter);
+ boolean skipByPkgFilter = !pkgFilter.isEmpty()
+ && !fields[4].startsWith(pkgFilter);
+ boolean skipErrorLevel = !levelError && "ERROR".equals(fields[1]);
+ boolean skipWarnLevel = !levelWarn && "WARN".equals(fields[1]);
+ boolean skipInfoLevel = !levelInfo && "INFO".equals(fields[1]);
+ boolean skipDebugLevel = !levelDebug && "DEBUG".equals(fields[1]);
+ boolean skipTraceLevel = !levelTrace && "TRACE".equals(fields[1]);
+ if (!skipErrorLevel && !skipDebugLevel && !skipTraceLevel && !skipInfoLevel
+ && !skipWarnLevel && !skipByMsgFilter && !skipByPkgFilter) {
+ appendLine(buffer, fields);
+ }
+ }
+ } catch (IOException e) {
+ LOGGER.warn("Exception while reading", e);
+ }
+ }
+ String content = buffer.toString();
+ double scroll = txtView.getScrollTop();
+ txtView.setText(content);
+ // live tail: keep the view at the previous scroll offset or jump to the end on append
+ txtView.setScrollTop(content.isEmpty() ? 0 : Math.max(scroll, 0));
+ }
+
+ /**
+ * Self-test hook: reports whether the rendered (filtered) view contains the given text.
+ *
+ * @param needle the expected log text
+ * @return a {@code "... verification: PASS/FAIL (...)"} report line
+ */
+ public String verifyContains(String needle) {
+ boolean pass = txtView.getText().contains(needle);
+ return "LogViewF contains '" + needle + "': " + (pass ? "PASS" : "FAIL");
+ }
+
+ /** Appends one record with the Swing field spacing ({@code LogView.java:260-269}). */
+ private static void appendLine(StringBuilder buffer, String[] fields) {
+ for (int i = 0; i < fields.length; i++) {
+ buffer.append(fields[i].replace("\\n", "\n"));
+ if (i == fields.length - 1) {
+ buffer.append('\n');
+ } else {
+ buffer.append(" ");
+ }
+ }
+ }
+
+ /** Opens the log file with the system Desktop (Swing {@code :302-324}). */
+ private void openExternal() {
+ if (logFile == null) {
+ return;
+ }
+ try {
+ java.awt.Desktop.getDesktop().open(logFile.toFile());
+ } catch (IOException | UnsupportedOperationException ex) {
+ LOGGER.error("Could not open editor.", ex);
+ }
+ }
+
+ /**
+ * The file watcher of the Swing {@code LogView.FileWatcherService} ({@code :54-85}): the
+ * callback runs on every modification event of the watched directory.
+ */
+ public static class FileWatcherService implements Runnable {
+ private static final Logger LOGGER =
+ LoggerFactory.getLogger(FileWatcherService.class);
+ private final Path file;
+ private final Runnable callback;
+
+ public FileWatcherService(Path fileToWatch, Runnable callback) {
+ this.file = fileToWatch;
+ this.callback = callback;
+ }
+
+ @Override
+ public void run() {
+ try (WatchService watchService = java.nio.file.FileSystems.getDefault()
+ .newWatchService()) {
+ var watchKey = file.getParent().register(watchService,
+ StandardWatchEventKinds.ENTRY_MODIFY);
+ while (!Thread.interrupted()) {
+ WatchKey wk = watchService.take();
+ if (wk == watchKey) {
+ Platform.runLater(callback);
+ }
+ // reset the key
+ wk.reset();
+ }
+ } catch (IOException e) {
+ LOGGER.error("IO exception during listening for events", e);
+ } catch (InterruptedException ignore) {
+ }
+ }
+ }
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/MainApplication.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/MainApplication.java
new file mode 100644
index 00000000000..953798f68ec
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/MainApplication.java
@@ -0,0 +1,37 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.gui.fx;
+
+import javafx.application.Application;
+import javafx.stage.Stage;
+
+import de.uka.ilkd.key.gui.fx.help.HelpFacadeF;
+
+/**
+ * The JavaFX application of KeY, counter-part of {@code de.uka.ilkd.key.gui.MainWindow} in the
+ * Swing module {@code key.ui}.
+ *
+ * Since milestone M1 the application delegates to the fully-fledged {@link MainWindowF}: docking
+ * workspace with the default layout, menu bar, toolbars, themed status bar and notifications.
+ *
+ * For development and automated verification, the system property {@code key.fx.driver} may name
+ * an {@link FxDriver} class which receives the primary stage instead of the main window.
+ */
+public final class MainApplication extends Application {
+
+ @Override
+ public void start(final Stage stage) throws Exception {
+ // smalldialogs: hand the host services to the help facade (browser opening)
+ HelpFacadeF.setHostServices(this);
+ String driverName = System.getProperty("key.fx.driver");
+ if (driverName != null && !driverName.isBlank()) {
+ Class> driverClass = Class.forName(driverName);
+ FxDriver driver = (FxDriver) driverClass.getDeclaredConstructor().newInstance();
+ driver.start(stage);
+ return;
+ }
+ new MainWindowF(stage).initialize();
+ }
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/MainWindowF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/MainWindowF.java
new file mode 100644
index 00000000000..623fe41baaa
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/MainWindowF.java
@@ -0,0 +1,6606 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.gui.fx;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EventObject;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.ServiceLoader;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import javafx.animation.KeyFrame;
+import javafx.animation.Timeline;
+import javafx.application.Platform;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.ReadOnlyBooleanWrapper;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.concurrent.Task;
+import javafx.event.Event;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.geometry.Side;
+import javafx.scene.Cursor;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.control.Alert;
+import javafx.scene.control.Alert.AlertType;
+import javafx.scene.control.ButtonType;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.CheckMenuItem;
+import javafx.scene.control.ContextMenu;
+import javafx.scene.control.Control;
+import javafx.scene.control.CustomMenuItem;
+import javafx.scene.control.Label;
+import javafx.scene.control.Menu;
+import javafx.scene.control.MenuBar;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.OverrunStyle;
+import javafx.scene.control.ProgressBar;
+import javafx.scene.control.RadioMenuItem;
+import javafx.scene.control.SeparatorMenuItem;
+import javafx.scene.control.SplitMenuButton;
+import javafx.scene.control.SplitPane;
+import javafx.scene.control.Tab;
+import javafx.scene.control.TextArea;
+import javafx.scene.control.ToggleGroup;
+import javafx.scene.control.ToolBar;
+import javafx.scene.control.Tooltip;
+import javafx.scene.image.Image;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyCombination;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import javafx.scene.layout.VBox;
+import javafx.stage.FileChooser;
+import javafx.stage.Stage;
+import javafx.stage.WindowEvent;
+import javafx.util.Duration;
+
+import de.uka.ilkd.key.control.AutoModeListener;
+import de.uka.ilkd.key.control.DefaultUserInterfaceControl;
+import de.uka.ilkd.key.control.KeYEnvironment;
+import de.uka.ilkd.key.control.ProofControl;
+import de.uka.ilkd.key.core.fx.GUIListenerF;
+import de.uka.ilkd.key.core.fx.KeYMediatorF;
+import de.uka.ilkd.key.core.fx.KeYSelectionEvent;
+import de.uka.ilkd.key.core.fx.KeYSelectionListener;
+import de.uka.ilkd.key.core.fx.KeYSelectionModel;
+import de.uka.ilkd.key.gui.fx.actions.QuickSaveF;
+import de.uka.ilkd.key.gui.fx.actions.useractions.UserActionF;
+import de.uka.ilkd.key.gui.fx.colors.ColorPaletteF;
+import de.uka.ilkd.key.gui.fx.colors.ColorSettingsF;
+import de.uka.ilkd.key.gui.fx.configuration.ConfigF;
+import de.uka.ilkd.key.gui.fx.contractcompletions.BlockContractExternalCompletionF;
+import de.uka.ilkd.key.gui.fx.contractcompletions.BlockContractInternalCompletionF;
+import de.uka.ilkd.key.gui.fx.contractcompletions.DependencyContractCompletionF;
+import de.uka.ilkd.key.gui.fx.contractcompletions.FunctionalOperationContractCompletionF;
+import de.uka.ilkd.key.gui.fx.contractcompletions.InvariantConfiguratorF;
+import de.uka.ilkd.key.gui.fx.contractcompletions.LoopInvariantRuleCompletionF;
+import de.uka.ilkd.key.gui.fx.dialogs.AboutDialogF;
+import de.uka.ilkd.key.gui.fx.dialogs.FeedbackDialogF;
+import de.uka.ilkd.key.gui.fx.dialogs.LemmaSelectionDialogF;
+import de.uka.ilkd.key.gui.fx.dialogs.LicenseDialogF;
+import de.uka.ilkd.key.gui.fx.dialogs.LoadUserTacletsDialogF;
+import de.uka.ilkd.key.gui.fx.dialogs.RunAllProofsF;
+import de.uka.ilkd.key.gui.fx.docking.DockLayoutStore;
+import de.uka.ilkd.key.gui.fx.docking.DockLocation;
+import de.uka.ilkd.key.gui.fx.docking.DockWorkspace;
+import de.uka.ilkd.key.gui.fx.docking.Dockable;
+import de.uka.ilkd.key.gui.fx.docking.DockingLayoutF;
+import de.uka.ilkd.key.gui.fx.docking.SequentViewDockF;
+import de.uka.ilkd.key.gui.fx.docking.SimpleDockable;
+import de.uka.ilkd.key.gui.fx.drawer.DrawerF;
+import de.uka.ilkd.key.gui.fx.drawer.DrawerItemF;
+import de.uka.ilkd.key.gui.fx.extension.KeYGuiExtensionFacadeF;
+import de.uka.ilkd.key.gui.fx.extension.api.KeYGuiExtensionF;
+import de.uka.ilkd.key.gui.fx.extension.contrib.ParallelProverStatusIndicatorF;
+import de.uka.ilkd.key.gui.fx.fonticons.IconFactoryF;
+import de.uka.ilkd.key.gui.fx.goallist.GoalListViewF;
+import de.uka.ilkd.key.gui.fx.help.HelpFacadeF;
+import de.uka.ilkd.key.gui.fx.infoview.InfoViewF;
+import de.uka.ilkd.key.gui.fx.keyshortcuts.KeyStrokeManagerF;
+import de.uka.ilkd.key.gui.fx.mergerule.MergeRuleCompletionF;
+import de.uka.ilkd.key.gui.fx.nodeviews.SequentMenuModelF;
+import de.uka.ilkd.key.gui.fx.nodeviews.SequentTermContextMenuF;
+import de.uka.ilkd.key.gui.fx.nodeviews.SequentViewF;
+import de.uka.ilkd.key.gui.fx.notification.NotificationCenterF;
+import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF;
+import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF.Kind;
+import de.uka.ilkd.key.gui.fx.notification.ProofStatisticsDialogF;
+import de.uka.ilkd.key.gui.fx.notification.events.ExceptionFailureEventF;
+import de.uka.ilkd.key.gui.fx.originlabels.OriginLabelsF;
+import de.uka.ilkd.key.gui.fx.plugins.javac.JavacSettingsProviderF;
+import de.uka.ilkd.key.gui.fx.profileloading.LoadingOptionsDialogF;
+import de.uka.ilkd.key.gui.fx.profileloading.LoadingOptionsDialogF.LoadOptions;
+import de.uka.ilkd.key.gui.fx.profileloading.WDLoadOptionPanelF;
+import de.uka.ilkd.key.gui.fx.proofdiff.ProofDiffFrameF;
+import de.uka.ilkd.key.gui.fx.proofmanagement.KnownTypesDialogF;
+import de.uka.ilkd.key.gui.fx.proofmanagement.ProofManagementDialogF;
+import de.uka.ilkd.key.gui.fx.proofmanagement.ProofManagerF;
+import de.uka.ilkd.key.gui.fx.prooftree.ProofTreeViewF;
+import de.uka.ilkd.key.gui.fx.recentfiles.RecentFilesF;
+import de.uka.ilkd.key.gui.fx.settings.ActiveSettingsDialogF;
+import de.uka.ilkd.key.gui.fx.settings.SettingsManagerF;
+import de.uka.ilkd.key.gui.fx.settings.SettingsProviderF;
+import de.uka.ilkd.key.gui.fx.settings.ToolTipOptionsDialogF;
+import de.uka.ilkd.key.gui.fx.smt.SolverListenerF;
+import de.uka.ilkd.key.gui.fx.soundiness.SoundinessAnalyzer;
+import de.uka.ilkd.key.gui.fx.soundiness.SoundinessDialogF;
+import de.uka.ilkd.key.gui.fx.sourceview.SourceViewF;
+import de.uka.ilkd.key.gui.fx.strategy.StrategySelectionViewF;
+import de.uka.ilkd.key.gui.fx.tasktree.TaskTreeF;
+import de.uka.ilkd.key.gui.fx.theme.Theme;
+import de.uka.ilkd.key.gui.fx.theme.ThemeManager;
+import de.uka.ilkd.key.java.Services;
+import de.uka.ilkd.key.logic.JTerm;
+import de.uka.ilkd.key.macros.AutoPilotPrepareProofMacro;
+import de.uka.ilkd.key.macros.DefaultAutoMacro;
+import de.uka.ilkd.key.macros.FullAutoPilotProofMacro;
+import de.uka.ilkd.key.macros.ProofMacro;
+import de.uka.ilkd.key.macros.ScriptAwareMacro;
+import de.uka.ilkd.key.nparser.KeyAst;
+import de.uka.ilkd.key.nparser.KeyIO;
+import de.uka.ilkd.key.pp.NotationInfo;
+import de.uka.ilkd.key.pp.PosInSequent;
+import de.uka.ilkd.key.proof.Goal;
+import de.uka.ilkd.key.proof.Proof;
+import de.uka.ilkd.key.proof.ProofAggregate;
+import de.uka.ilkd.key.proof.ProofEvent;
+import de.uka.ilkd.key.proof.init.AbstractProfile;
+import de.uka.ilkd.key.proof.init.DefaultProfileResolver;
+import de.uka.ilkd.key.proof.init.InitConfig;
+import de.uka.ilkd.key.proof.init.ProblemInitializer;
+import de.uka.ilkd.key.proof.init.Profile;
+import de.uka.ilkd.key.proof.io.AutoSaver;
+import de.uka.ilkd.key.proof.io.GZipProofSaver;
+import de.uka.ilkd.key.proof.io.ProofBundleSaver;
+import de.uka.ilkd.key.proof.io.ProofSaver;
+import de.uka.ilkd.key.proof.io.SingleThreadProblemLoader;
+import de.uka.ilkd.key.proof.io.consistency.DiskFileRepo;
+import de.uka.ilkd.key.proof.io.consistency.FileRepo;
+import de.uka.ilkd.key.proof.io.consistency.SimpleFileRepo;
+import de.uka.ilkd.key.rule.Taclet;
+import de.uka.ilkd.key.rule.TacletApp;
+import de.uka.ilkd.key.rule.inst.SVInstantiations;
+import de.uka.ilkd.key.settings.FeatureSettings;
+import de.uka.ilkd.key.settings.GeneralSettings;
+import de.uka.ilkd.key.settings.PathConfig;
+import de.uka.ilkd.key.settings.ProofIndependentSMTSettings;
+import de.uka.ilkd.key.settings.ProofIndependentSMTSettings.ProgressMode;
+import de.uka.ilkd.key.settings.ProofIndependentSettings;
+import de.uka.ilkd.key.settings.ViewSettings;
+import de.uka.ilkd.key.smt.SMTProblem;
+import de.uka.ilkd.key.smt.SolverTypeCollection;
+import de.uka.ilkd.key.taclettranslation.lemma.TacletLoader;
+import de.uka.ilkd.key.taclettranslation.lemma.TacletSoundnessPOLoader;
+import de.uka.ilkd.key.util.KeYConstants;
+import de.uka.ilkd.key.util.KeYResourceManager;
+import de.uka.ilkd.key.util.MiscTools;
+
+import org.key_project.logic.PosInTerm;
+import org.key_project.prover.proof.rulefilter.TacletFilter;
+import org.key_project.prover.sequent.PosInOccurrence;
+import org.key_project.util.collection.ImmutableList;
+import org.key_project.util.collection.ImmutableSet;
+import org.key_project.util.java.IOUtil;
+import org.key_project.util.javafx.FxUtil;
+
+import org.jspecify.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The main window of the JavaFX UI, counter-part of {@code de.uka.ilkd.key.gui.MainWindow} in the
+ * Swing module {@code key.ui}.
+ *
+ * Milestone M1: the window hosts the docking workspace (with the default layout of placeholder
+ * dockables — the real views arrive in M2), the menu bar and toolbars (actions are progressively
+ * wired in M3), a themed status bar, and the notification overlay. The layout persistence
+ * ({{@link DockLayoutStore}) mirrors {@code layout.xml} of the Swing UI.
+ */
+public final class MainWindowF {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(MainWindowF.class);
+
+ private static final String IMAGE_DIR = "/de/uka/ilkd/key/gui/images/";
+
+ // menu: MP5 — external targets of the About menu's browser actions (Swing
+ // KeYProjectHomepageAction.url / CreateGithubIssueAction.URL), opened via the
+ // HelpFacadeF browser seam (host services).
+ private static final String KEY_PROJECT_URL = "https://www.key-project.org/";
+ private static final String GITHUB_ISSUE_URL = "https://github.com/keyproject/key/issues/new";
+
+ // menu: MP5 — the "Run All Proofs" QA feature lives behind the same feature flag as the
+ // Swing original (Swing MainWindow.FEATURE_BULK_UI_TEST, MainWindow.java:109-113). The
+ // feature is already registered in the shared FeatureSettings.FEATURES registry by the
+ // Swing side in a full build; the stream lookup reuses it, the createFeature fallback
+ // covers a key.ui.fx-only run (otherwise the same id would register twice, which the
+ // Swing FeatureSettingsPanel would then list twice).
+ private static final FeatureSettings.Feature FEATURE_BULK_UI_TEST =
+ FeatureSettings.Feature.FEATURES.stream().filter(f -> "BULK_UI_TEST".equals(f.id()))
+ .findFirst()
+ .orElseGet(() -> FeatureSettings.createFeature("BULK_UI_TEST",
+ "Activates the 'Run All Proofs' action that allows you to run multiple"
+ + " proofs inside the UI.",
+ false));
+
+ // menu: MP5 — the batch-mode help text of the "Lemma Generation (Batch Mode)" info dialog
+ // (Swing LemmaGenerationBatchModeAction.DESCRIPTION, trimmed of the trailing blank line).
+ private static final String BATCH_MODE_TEXT =
+ """
+ In case that one wants to prove a huge set of taclets, it can be convenient and useful to do this automatically.
+ The new lemma generation offers now the possibility to use the batch mode of the KeY system
+ in order to generate and prove the proof obligations for the correctness of (non-axiomatic) taclets.
+
+ The basic command using the batch mode is:
+
+ runProver --justify-rules FILE1 --jr-axioms FILE2 --jr-signature FILE3
+
+ FILE1: The file containing the taclets that should be proved sound.
+ FILE2: The file containing the taclets that should be used as axioms when proving the taclets of FILE1
+ being sound.
+ FILE3: The file containing the signature that should be used for loading the taclets.
+ If this option is not set, the signature declared in FILE1 is used.
+
+ In order to store the resulting proofs to files one can set the option "--jr-saveProofToFile true".
+ The corresponding proofs are stored into the folder in which FILE1 is located. In case that one wants to
+ store the proofs into another folder, one can specify the path of the folder by
+ "--jr-pathOfResult PATH_OF_DEST_FOLDER".
+ Some more options are available, which are shown when using the command:
+
+ runProver --help
+ in the batch mode.""";
+
+ /** Known dockable ids, in the order they appear in the factory-default layout. */
+ public static final String ID_LOADED_PROOFS = "loadedProofs";
+ public static final String ID_GOAL_LIST = "goalList";
+ public static final String ID_PROOF_TREE = "proofTree";
+ public static final String ID_INFO_VIEW = "infoView";
+ public static final String ID_STRATEGY = "strategySelection";
+ public static final String ID_SEQUENT = "sequent";
+ public static final String ID_SOURCE_VIEW = "sourceView";
+
+ private final Stage stage;
+ private final DockWorkspace workspace = new DockWorkspace();
+ private final DockLayoutStore layoutStore;
+ private final Map dockables = new LinkedHashMap<>();
+
+ // docking: layout slots, maximize toggle, shutdown persistence and self test (Swing
+ // DockingLayout)
+ private final DockingLayoutF dockingLayout;
+
+ private final Label statusLeft = new Label();
+ private final Label statusRight = new Label();
+ /**
+ * D37 (P3c): the status-line progress bar (Swing {@code MainStatusLine.progressBar}), hidden
+ * and unmanaged while no task reports progress (Swing {@code progressBar.setVisible(false)}).
+ */
+ private final ProgressBar statusProgress = new ProgressBar();
+ /**
+ * D37: the current determinate maximum of {@link #statusProgress}, mirroring the Swing
+ * {@code JProgressBar} model: {@code -1} = indeterminate ("busy"), {@code 0} = hidden,
+ * {@code > 0} = determinate range.
+ */
+ private int statusProgressMax;
+
+ /**
+ * The sequent view is the first real view of milestone M2 (currently a spike rendering the
+ * printed sequent of the root node of a demo proof, see {@link #startDemoProofLoad()}).
+ */
+ private final SequentViewF sequentView = new SequentViewF();
+
+ /**
+ * The mediator of the window (M2 skeleton): owns the selection model, binds the proof
+ * listeners and shares the notation info. The demo load routes the proof through
+ * {@code setSelectedProof}, which invokes the mediator's {@code setProof}.
+ */
+ private final KeYMediatorF mediator = new KeYMediatorF();
+
+ /**
+ * The selection model of the window, owned by the mediator.
+ */
+ private final KeYSelectionModel selectionModel = mediator.getSelectionModel();
+
+ /**
+ * seam: the window-side {@link de.uka.ilkd.key.control.UserInterfaceControl} (Swing
+ * {@code MainWindow.getUserInterface()} returning the {@code WindowUserInterfaceControl}):
+ * routes the core's status/task/exception/warning callbacks into this window and hosts the
+ * interactive rule-completion registry. Created once for the whole application lifetime
+ * (like the Swing original).
+ */
+ private final WindowUserInterfaceControlF userInterface =
+ new WindowUserInterfaceControlF(this);
+
+ {
+ // joinmerge: register the interactive merge-rule completion with the seam registry
+ // (Swing parity: MergeRuleCompletion.INSTANCE is registered in the
+ // WindowUserInterfaceControl constructor, WindowUserInterfaceControl.java:81);
+ // the join trigger JoinActionF.run(...) is wired with the future sequent-view
+ // context menu (Swing JoinMenuItem, CurrentGoalViewMenu.java:235-238)
+ userInterface.register(MergeRuleCompletionF.INSTANCE);
+ // contractcompletions (P2b): register the interactive contract/invariant completions
+ // (Swing parity: WindowUserInterfaceControl constructor, WindowUserInterfaceControl
+ // .java:74-82 — FunctionalOperationContractCompletion (:76),
+ // DependencyContractCompletion (:77), LoopInvariantRuleCompletion (:78),
+ // BlockContractInternalCompletion(mainWindow) (:79),
+ // BlockContractExternalCompletion(mainWindow) (:80)); the dialogs of the completions
+ // are the FX ports in de.uka.ilkd.key.gui.fx.contractcompletions
+ userInterface.register(new FunctionalOperationContractCompletionF());
+ userInterface.register(new DependencyContractCompletionF());
+ userInterface.register(new LoopInvariantRuleCompletionF());
+ userInterface.register(new BlockContractInternalCompletionF());
+ userInterface.register(new BlockContractExternalCompletionF());
+ // contractcompletions (P2b): the invariant configurator parses with the editor's
+ // abbreviation map (Swing: MainWindow.getMediator().getNotationInfo().getAbbrevMap(),
+ // InvariantConfigurator.getAbbrevMap)
+ InvariantConfiguratorF.setAbbrevMap(mediator.getNotationInfo().getAbbrevMap());
+ }
+
+ /**
+ * The proof tree view (first M2 version): displays the proof of the selection model, node
+ * clicks drive the selection.
+ */
+ private final ProofTreeViewF proofTreeView = new ProofTreeViewF();
+
+ /**
+ * WP-5 — the proof tree view for the extension ports (the Swing extensions reach the tree
+ * via {@code MainWindow.getProofTreeView()}, e.g. the exploration extension's renderer
+ * registration and "Hide justification" filter routing, ExplorationExtension.java:128); the
+ * FX extension ports obtain it from their {@code init(MainWindowF, KeYMediatorF)} window.
+ *
+ * @return the proof tree view of the window
+ */
+ public ProofTreeViewF getProofTreeView() {
+ return proofTreeView;
+ }
+
+ /**
+ * The info view (first M2 version): shows the proof-level details (name, file, goal/node/
+ * branch counts, closed status) of the selected proof.
+ */
+ private final InfoViewF infoView = new InfoViewF();
+
+ /**
+ * The goal list view (first M2 version): lists the open goals of the selected proof; a click
+ * selects the goal.
+ */
+ private final GoalListViewF goalListView = new GoalListViewF();
+
+ /**
+ * menu: MP3b — the selection history backing the View menu Back / Forward actions (Swing
+ * {@code MainWindow.selectionHistory}, {@code new SelectionHistory(mediator)}): traces the
+ * user-selected proof nodes and exposes the Back/Forward enablement as JavaFX properties.
+ */
+ private final SelectionHistoryF selectionHistory = new SelectionHistoryF(selectionModel);
+
+ /**
+ * The strategy selection view (first M2 version): settings-definition-driven control panel
+ * writing through to the selected proof's strategy settings.
+ */
+ private final StrategySelectionViewF strategyView = new StrategySelectionViewF();
+
+ /**
+ * The source view (first M2 version): shows the Java source file(s) relevant to the selected
+ * proof (via {@code ProofJavaSourceCollection} + {@code FileRepo}, like the Swing view);
+ * pure {@code .key} problems without Java source show the loaded problem file as a fallback.
+ */
+ private final SourceViewF sourceView = new SourceViewF();
+
+ /**
+ * The recent files store: the same {@code recentFiles_v2.json} as the Swing UI, so both UIs
+ * share one list. Populated by the File menu actions only — the demo property load must not
+ * pollute the user's shared recent-files file.
+ */
+ private final RecentFilesF recentFiles = new RecentFilesF();
+
+ /**
+ * proofmgmt: the multi-proof state (the Swing {@code TaskTreeModel} state of the
+ * {@code TaskTree}): the loaded proofs and the active one, consulted by the Loaded Proofs
+ * view and the Proof Management dialog.
+ */
+ private final ProofManagerF proofManager = new ProofManagerF();
+
+ /**
+ * proofmgmt: the "Loaded Proofs" view (JavaFX port of the Swing {@code TaskTree}): lists the
+ * loaded proofs with status and open-goal count and switches the active proof on click.
+ */
+ private final TaskTreeF loadedProofs = new TaskTreeF(proofManager);
+
+ /**
+ * drawer: MP10 — the west/east/south drawer hosts of the main window (Java port of the
+ * TornadoFX {@code Drawer}, package {@code de.uka.ilkd.key.gui.fx.drawer}). The panels that
+ * used to live in the left/right docking areas are now toggle buttons in these drawers:
+ * dragging a button onto another drawer moves the panel to that port
+ * ({@link DrawerF#transferItem(DrawerItemF, DrawerF)}), dragging within a bar reorders the
+ * split ({@link DrawerF#moveItem(int, int)}). The docking centre only hosts the sequent.
+ */
+ private DrawerF westDrawer;
+ private DrawerF eastDrawer;
+ private DrawerF southDrawer;
+
+ /**
+ * Reviewer request (drawer resizability + divider persistence): the horizontal SplitPane
+ * hosting the west drawer, the workspace and the east drawer. The shutdown layout
+ * persistence ({@link DockingLayoutF}) stores its divider positions in the layout store.
+ */
+ private SplitPane mainSplit;
+
+ /**
+ * proofmgmt: the environment of the most recent load; the Proof Management dialog (Swing
+ * {@code ProofManagementDialog}, opened from the File menu) operates on its init config and
+ * starts proofs via its user interface control.
+ */
+ private @Nullable KeYEnvironment lastEnvironment;
+
+ /**
+ * The "Recent Files" submenu of the File menu; rebuilt by {@link #updateRecentFilesMenu()}
+ * whenever the store changes.
+ */
+ private final Menu recentFilesMenu = new Menu("Recent Files");
+
+ /**
+ * P3b/B11+B15: the Goal Back / Prune Proof controls of the Proof menu and the proof toolbar,
+ * kept as references so the dynamic "Goal Back" label ({@link #refreshGoalBackItem()},
+ * B11) and the Swing-parity enablement ({@link #refreshGoalBackPrune()}, B15) can update
+ * them without rebuilding the menu/toolbar. Set in {@link #buildProofMenu()} and
+ * {@link #buildProofToolBar()}; the menu parity self test rebuilds the menu bar, which
+ * assigns them again.
+ */
+ private javafx.scene.control.MenuItem goalBackMenuItem;
+ private javafx.scene.control.MenuItem pruneMenuItem;
+ private javafx.scene.control.Button goalBackToolbarButton;
+ private javafx.scene.control.Button pruneToolbarButton;
+
+ /**
+ * Reviewer request (toolbar rewrite): the automation control (Swing
+ * {@code DropdownSelectionButton} with the four {@link #AUTOMATION_MACROS}); the main part
+ * runs the selected macro / stops a running automation, the dropdown selects the macro.
+ */
+ private javafx.scene.control.SplitMenuButton automationToolbarButton;
+
+ /**
+ * Reviewer request (toolbar rewrite): the SMT control (Swing {@code DropdownSelectionButton}
+ * with the usable solver unions); the main part launches the selected union, the dropdown
+ * selects it and makes it the active solver union of the settings.
+ */
+ private javafx.scene.control.SplitMenuButton smtToolbarButton;
+
+
+ /**
+ * Last directory of the open/save dialogs (Swing {@code OpenFileAction.lastSelectedPath}).
+ */
+ private Path lastSelectedDir = Path.of(System.getProperty("user.dir"));
+
+ /**
+ * menu: MP5 — whether the {@link FeatureSettings} listener of the "Run All Proofs" menu item
+ * (see {@link #FEATURE_BULK_UI_TEST} / {@link #buildProveSubmenu()}) has been registered;
+ * the parity self test rebuilds the menu bar, so the registration happens at most once.
+ */
+ private boolean bulkUiTestListenerRegistered;
+
+ /**
+ * Whether a proof is selected (updated in {@link #updateProofStatus()}); the enablement
+ * condition of the save action (Swing {@code enableWhenProofLoaded}).
+ */
+ private final ReadOnlyBooleanWrapper proofLoaded =
+ new ReadOnlyBooleanWrapper(this, "proofLoaded");
+
+ /**
+ * Updates the left status text whenever the selection changes: proof name, closed state or
+ * the number of open goals (Swing's status line is message-driven; the proof summary is the
+ * persistent M2 content). Marshalled to the FX thread (the mediator's
+ * {@code defaultSelection} may fire from the prover thread).
+ */
+ private final KeYSelectionListener statusSelectionListener = new KeYSelectionListener() {
+ @Override
+ public void selectedProofChanged(KeYSelectionEvent event) {
+ updateProofStatus();
+ }
+
+ @Override
+ public void selectedNodeChanged(KeYSelectionEvent event) {
+ updateProofStatus();
+ }
+ };
+
+ /**
+ * P3b/B12: the one main window of the application (Swing {@code MainWindow.getInstance()},
+ * MainWindow.java:107-111). Set by the constructor; the proof-script entry points
+ * ({@code ProofMacroMenuF}) reach the window's stage + user-interface control through it.
+ */
+ private static @Nullable MainWindowF instance;
+
+ /**
+ * Creates the main window bound to the given stage.
+ *
+ * @param stage the primary stage of the JavaFX application
+ */
+ public MainWindowF(Stage stage) {
+ instance = this;
+ this.stage = stage;
+ this.layoutStore = new DockLayoutStore(PathConfig.currentPaths.keyConfigDir);
+ // docking: the layout extension needs the workspace and the layout store
+ this.dockingLayout = new DockingLayoutF(this);
+ }
+
+ /**
+ * Builds and shows the main window.
+ */
+ public void initialize() {
+ stage.setTitle(KeYResourceManager.getManager().getUserInterfaceTitle());
+ setWindowIcons();
+ // P3b/B15: the Goal Back / Prune enablement must also react to auto mode start/stop
+ // (Swing's actions remove their selection listener and disable themselves,
+ // GoalBackAction.java:94-106 / PruneProofAction.java:95-107). The property is updated on
+ // the FX thread (see KeYMediatorF.RuleAppListenerProofListener); the null guards of
+ // refreshGoalBackPrune cover the controls not yet built.
+ mediator.autoModeRunningProperty()
+ .addListener((obs, old, running) -> refreshGoalBackPrune());
+ // WP-1: the auto-mode context API (KeYMediatorF.initiateAutoMode/finishAutoMode) runs
+ // auto modes on foreign (non-selected) proofs, e.g. the caching copy-on-dispose — the
+ // Swing ui.notifyAutoModeBeingStarted/notifyAutomodeStopped reach MainWindow's freeze
+ // via the ProverTaskListener dispatch (MainWindow.java:934/946). The FX observable
+ // property is the notification channel (the core AbstractProofControl.
+ // fireAutoModeStarted is protected and unreachable from the mediator), so the freeze
+ // follows the property as well; for the normal runs this is idempotent with the
+ // control-event-driven freeze of autoModeUiListener below.
+ mediator.autoModeRunningProperty()
+ .addListener((obs, was, running) -> {
+ if (running) {
+ freezeExceptAutoModeButton();
+ } else {
+ unfreezeExceptAutoModeButton();
+ }
+ });
+
+ buildDockables();
+ workspace.setDefaultLayout(defaultLayout());
+ buildDrawerHosts();
+
+ BorderPane root = new BorderPane();
+ root.setTop(buildTop());
+ // drawer: MP10 — the workspace (the sequent) stays the centre of the main area; the
+ // west/east/south drawers host the panels that previously lived in the docking areas.
+ // Reviewer request: the drawers are resizable — the west drawer and the workspace share
+ // a horizontal SplitPane (divider drag adjusts the drawer width; the east drawer hosts
+ // the Source panel the same way), and the expanded/collapsed state of the drawers
+ // adjusts their dividers (collapsed = only the button bar width).
+ SplitPane mainSplit = new SplitPane();
+ this.mainSplit = mainSplit;
+ mainSplit.getItems().addAll(westDrawer, workspace.getRoot(), eastDrawer);
+ // reviewer request: the separator position is stored and restored accurately — the
+ // factory default is west 0.18 / east 0.86, a previously saved layout wins
+ double[] dividers = layoutStore.loadMainSplitDividers();
+ Platform.runLater(() -> mainSplit.setDividerPositions(dividers[0], dividers[1]));
+ // reviewer request: the drawer toggle buttons must never vanish — the drawers stay
+ // split members permanently; a drawer with no expanded item collapses to its button
+ // bar (the toggle strip stays visible and clickable) and the split pane hugs the strip;
+ // expanding an item restores the saved (or default) expanded divider position. The
+ // earlier behaviour removed the empty drawer from the split entirely, which also
+ // removed its toggle bar.
+ westDrawer.expandedCountProperty().addListener((obs, was, is) -> Platform.runLater(
+ () -> syncDrawerCollapsed(mainSplit, westDrawer, true, dividers[0])));
+ eastDrawer.expandedCountProperty().addListener((obs, was, is) -> Platform.runLater(
+ () -> syncDrawerCollapsed(mainSplit, eastDrawer, false, dividers[1])));
+ syncDrawerCollapsed(mainSplit, westDrawer, true, dividers[0]);
+ syncDrawerCollapsed(mainSplit, eastDrawer, false, dividers[1]);
+ BorderPane mainArea = new BorderPane();
+ mainArea.setCenter(mainSplit);
+ mainArea.setBottom(southDrawer);
+ StackPane center = new StackPane(mainArea);
+ // inputfreeze (P1): the blocking overlay covers the main area (workspace + drawers) for
+ // the duration of an auto mode run, see freezeExceptAutoModeButton
+ inputBlocker.getStyleClass().add("auto-mode-blocker");
+ inputBlocker.setCursor(Cursor.WAIT);
+ inputBlocker.setVisible(false);
+ inputBlocker.addEventFilter(MouseEvent.ANY, Event::consume);
+ center.getChildren().add(inputBlocker);
+ // keys targeted inside the main area are swallowed while frozen — except Escape, which
+ // must reach the global stop handler (Swing AutoModeAction's stop shortcut)
+ center.addEventFilter(KeyEvent.ANY, event -> {
+ if (inputBlocker.isVisible() && event.getCode() != KeyCode.ESCAPE) {
+ event.consume();
+ }
+ });
+ NotificationManagerF.getInstance().attach(center);
+ root.setCenter(center);
+ root.setBottom(buildStatusBar());
+ // reviewer report ("mouse clicks on the toolbar are ignored, keyboard activation works"):
+ // the top area grows after the root's first layout pass (the icon fonts and theme styles
+ // settle late and raise the toolbar row's preferred height) while the BorderPane keeps
+ // the center pane at the stale top height; the transparent center pane then covers the
+ // toolbar row — the buttons stay visible but every mouse press picks the center pane
+ // instead of the buttons (key events are unaffected). Forward late size changes of the
+ // top/bottom areas to the root's layout so the regions re-stack.
+ root.getTop().layoutBoundsProperty().addListener((obs, was, is) -> root.requestLayout());
+ root.getBottom().layoutBoundsProperty().addListener((obs, was, is) -> root.requestLayout());
+ // if the growth already happened within the first layout pass, no later height change is
+ // observable — re-stack once after the first pulses as well
+ Platform.runLater(root::requestLayout);
+
+ Scene scene = new Scene(root, 1100, 800);
+ ThemeManager.getInstance().manage(scene);
+ // colors: register the Swing-parity palette and apply the overrides recorded in
+ // colors.json to the managed scene (Swing registers ColorSettings.ColorProperty entries
+ // via the static consumers; the FX registry only knows defined properties, so the
+ // palette must be loaded before the overrides can be applied at startup)
+ ColorPaletteF.ensureRegistered();
+ ColorSettingsF.getInstance().applyToScenes();
+ // the global action keys of the Swing AutoModeAction (Ctrl+Space starts, Escape stops);
+ // an open search bar consumes Escape itself, so it never stops a run while visible.
+ // smalldialogs: F1 context help is handled by HelpFacadeF.installAccelerator below —
+ // a key-handler branch here would open the page twice.
+ scene.setOnKeyPressed(this::handleMainWindowKeyPressed);
+ // docking: Ctrl+M maximize toggle (bibliothek CControl.KEY_MAXIMIZE_CHANGE) and the
+ // shutdown persistence of the Swing GUIListener.shutDown
+ dockingLayout.install(scene);
+ // smalldialogs: F1 context help (Swing MainWindow.java:300-302 registers the F1 action)
+ HelpFacadeF.installAccelerator(scene);
+ // loadingexit (P1): the window close button runs the same exit flow as the Exit menu
+ // item (Swing MainWindow.java:366 addWindowListener(exitMainAction.windowListener))
+ stage.setOnCloseRequest(e -> {
+ e.consume();
+ exitApplication();
+ });
+ stage.setScene(scene);
+ // reviewer request: JavaFX shows a black window for a short fraction of time when the
+ // stage appears — the window becomes visible before the first rendered frame is
+ // composited (also JDK-8088179). Showing the stage visually transparent and fading it
+ // in after the first rendered frame removes the flash at startup. The opacity must be
+ // small but NOT zero: a stage with opacity 0 is never rendered/presented, the window
+ // would stay black. The flash while resizing is the Glass/GTK exposed-area handling of
+ // the window system and cannot be removed from the application side; the accelerated
+ // prism pipeline (default, no -Dprism.order=sw) renders the resized frame fast enough
+ // to make it barely visible.
+ stage.setOpacity(0.01);
+ stage.show();
+ // two runLater hops: the first executes after the pulse that rendered the scene, the
+ // second after that frame was presented to the window system
+ Platform.runLater(() -> Platform.runLater(() -> stage.setOpacity(1)));
+
+ restoreLayout();
+
+ updateProofStatus();
+
+ wireSequentView();
+ recentFiles.setOnChange(this::updateRecentFilesMenu);
+ recentFiles.load();
+ // menu: MP7 — honor a persisted non-zero auto-save period at startup (Swing: the saver is
+ // active whenever the period is > 0, KeYMediator.java:87 + :148-150); the saver must be
+ // armed before the demo load so the load-success selection hands it the proof
+ applyAutoSave(ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings()
+ .autoSavePeriod());
+ startDemoProofLoad();
+ // proofmgmt: the Loaded Proofs view's clicks and the Proof Management dialog route the
+ // active-proof switch through the selection model (the Swing mediator path)
+ proofManager.setActivationHandler(this::activateProof);
+ // WP-1: the opened-proofs registry of the mediator (Swing: TaskTree.addProof adds every
+ // registered proof to mediator.getCurrentlyOpenedProofs(), TaskTree.java:117, and
+ // ProofTreeView removes the disposed ones, ProofTreeView.java:726). The ProofManagerF
+ // multi-proof state is the FX registration point (its addProof/removeProof cover the
+ // demo load, the proof-management loads and the abandon flow), so the mediator's live
+ // list mirrors it — the extension ports (WP-4 caching) feed the accessor.
+ proofManager.proofs().addListener((ListChangeListener) change -> {
+ while (change.next()) {
+ if (change.wasAdded()) {
+ mediator.getCurrentlyOpenedProofs().addAll(change.getAddedSubList());
+ }
+ if (change.wasRemoved()) {
+ mediator.getCurrentlyOpenedProofs().removeAll(change.getRemoved());
+ }
+ }
+ });
+ // WP-1: the extension-object registry (Swing KeYMediator.register/lookup,
+ // KeYMediator.java:606/589). Built-in consumer proving the shape: the main window
+ // itself is registered, so extensions that only receive the mediator (the facade's
+ // init(null, mediator) startup hook) can still look the window up. WP-5's exploration
+ // port registers its ExplorationModeModel through the same seam (verified by
+ // key.fx.verify.exploration).
+ mediator.register(this, MainWindowF.class);
+ // proofmgmt: self test hook (independent of the demo load; the verification loads its
+ // own examples so that it also runs without key.fx.demo.sequent)
+ if (System.getProperty("key.fx.verify.proofmgmt") != null) {
+ runProofMgmtVerification();
+ }
+
+ // smalldialogs: startup self tests of the ported small dialogs; reports to the log and
+ // toast (same pattern as the key.fx.verify.* hooks in startProofLoad). The help facade
+ // and the WD load-option panel are proof-independent, so the hooks run at startup rather
+ // than after a demo proof load.
+ if (System.getProperty("key.fx.verify.help") != null) {
+ String report = HelpFacadeF.verifyHelp();
+ LOGGER.info("Help verification: {}", report);
+ NotificationManagerF.getInstance()
+ .notify("Help verification: " + report,
+ report.endsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ }
+ if (System.getProperty("key.fx.verify.profileloading") != null) {
+ String report = WDLoadOptionPanelF.verifyProfileLoading();
+ LOGGER.info("Profile loading verification: {}", report);
+ NotificationManagerF.getInstance()
+ .notify("Profile loading verification: " + report,
+ report.endsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ }
+ // smalldialogs: opens the loading options dialog (Swing KeYFileChooserLoadingOptions
+ // accessory) for an interactive check; the selected options are logged when the dialog
+ // is confirmed (Enter = Load), Escape/Cancel logs "null"
+ if (System.getProperty("key.fx.verify.profileloadingdialog") != null) {
+ javafx.application.Platform.runLater(() -> {
+ LoadOptions options = LoadingOptionsDialogF.showOptions(stage);
+ LOGGER.info("Loading options dialog returned: {}", options);
+ });
+ }
+ // smalldialogs: javac settings provider self test (Swing JavacSettingsProvider); the
+ // settings dialog itself is not opened — this checks the read/write round trip
+ if (System.getProperty("key.fx.verify.javacsettings") != null) {
+ String report = JavacSettingsProviderF.verifyJavacSettings();
+ LOGGER.info("Javac settings verification: {}", report);
+ NotificationManagerF.getInstance()
+ .notify("Javac settings verification: " + report,
+ report.endsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ }
+ // WP-9: EnsureSourceConsistency load-pipeline wiring self test
+ // (key.fx.verify.sourceconsistency): loads a JML example twice — with the flag armed
+ // (DiskFileRepo: the source-cache backend the Swing toggle arms,
+ // AbstractProblemLoader.createFileRepo:400-409) and disarmed (SimpleFileRepo) — and
+ // proves the loaded proof's repo serves the source through the same stream the source
+ // view reads (SourceViewF.readRepoFile, Swing SourceView.addFile:617-621). The flag is
+ // restored afterwards (the runs read the shared ~/.key/v3.1).
+ if (System.getProperty("key.fx.verify.sourceconsistency") != null) {
+ runSourceConsistencyVerification();
+ }
+ // colors: P1 parity close-out — Swing-parity palette count, mapped CSS variable wiring
+ // and an override round trip (key.fx.verify.colors); proof-independent, so it runs at
+ // startup like the other smalldialogs hooks
+ if (System.getProperty("key.fx.verify.colors") != null) {
+ String report = ColorSettingsF.verifyColors();
+ LOGGER.info("Colors verification: {}", report);
+ NotificationManagerF.getInstance()
+ .notify("Colors verification: " + report,
+ report.endsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ }
+ // WP-3 (settings-config): theme persistence round trip (key.fx.verify.themepersist) —
+ // two app runs: "save" applies the dark theme through the real ThemeManager.setTheme
+ // (the View menu's Dark Theme action) and asserts the choice persisted; "restore"
+ // restarts the app without the key.fx.theme override and asserts the dark theme was
+ // restored from the persisted preference, then resets to light so the round trip is
+ // repeatable. Proof-independent, so it runs at startup like the other smalldialogs hooks.
+ if (System.getProperty("key.fx.verify.themepersist") != null) {
+ String report =
+ runThemePersistVerification(System.getProperty("key.fx.verify.themepersist"));
+ LOGGER.info("Theme persistence verification: {}", report);
+ NotificationManagerF.getInstance()
+ .notify("Theme persistence verification: " + report,
+ report.endsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ statusRight.setText(report);
+ }
+
+ // extension: MP9.0 — register the FX extensions' settings providers into the settings
+ // manager registry (Swing SettingsManager registers each KeYGuiExtension.Settings
+ // provider at startup) and call the StartupF.init hook of every discovered provider
+ // once, at the end of the startup sequence (Swing KeYMediator/ExtensionManager call
+ // init right after the window is built; here after the demo load is started so the
+ // status-line extensions can observe the selection events of the load).
+ for (SettingsProviderF provider : KeYGuiExtensionFacadeF.getSettingsProviders()) {
+ SettingsManagerF.getInstance().add(provider);
+ }
+ KeYGuiExtensionFacadeF.initAll(this, mediator);
+
+ NotificationManagerF.getInstance()
+ .notify("KeY (JavaFX) started. Docking layout restored from "
+ + layoutStore.file() + ".");
+ }
+
+ /**
+ * @return the docking workspace of the main window
+ */
+ public DockWorkspace getWorkspace() {
+ return workspace;
+ }
+
+ /**
+ * @return the docking layout store (docking: needed by the {@link DockingLayoutF} slots)
+ */
+ public DockLayoutStore getLayoutStore() {
+ return layoutStore;
+ }
+
+ /**
+ * @return the main-area SplitPane (west drawer | workspace | east drawer), whose divider
+ * positions the {@link DockingLayoutF} shutdown persistence stores
+ */
+ public SplitPane getMainSplit() {
+ return mainSplit;
+ }
+
+ /**
+ * Reviewer request: the drawer's toggle buttons must never vanish. The drawers stay split
+ * members permanently; a drawer that shows no expanded item collapses to its button bar
+ * ({@link DrawerF#collapseToButtonBar()}), so the strip with the toggle buttons remains
+ * visible and clickable and the split pane clamps the divider at the strip width (no slider
+ * for an empty region); expanding an item restores the saved (or default) expanded divider
+ * position. Must run on the FX thread.
+ *
+ * @param split the main-area split containing the workspace and both drawers
+ * @param drawer the drawer to collapse or expand
+ * @param west whether the drawer is the west (first) member of the split
+ * @param expandedDivider the divider position to restore when an item is expanded
+ */
+ private void syncDrawerCollapsed(SplitPane split, DrawerF drawer, boolean west,
+ double expandedDivider) {
+ int dividerIndex = west ? 0 : 1;
+ if (drawer.expandedCountProperty().get() == 0) {
+ // the split pane clamps the drawer at its collapsed (bar width + scrollbar
+ // allowance) size, so the divider visually hugs the toggle strip without moving the
+ // stored position
+ drawer.collapseToButtonBar();
+ } else {
+ drawer.expandFromButtonBar();
+ split.setDividerPosition(dividerIndex, expandedDivider);
+ }
+ }
+
+ /**
+ * @return the west (left) drawer host of the main window — Loaded Proofs, Proof Tree, Goal
+ * List, Info, Strategy plus the extension left-panel items (used by the drawer
+ * layout verification and the extension verification)
+ */
+ public DrawerF getWestDrawer() {
+ return westDrawer;
+ }
+
+ /**
+ * @return the east (right) drawer host — currently the Source panel
+ */
+ public DrawerF getEastDrawer() {
+ return eastDrawer;
+ }
+
+ /**
+ * @return the south (bottom) drawer host; starts empty and is filled by dragging panels
+ * there (cross-port drag and drop)
+ */
+ public DrawerF getSouthDrawer() {
+ return southDrawer;
+ }
+
+ /**
+ * @return the registered dockables (id → dockable)
+ */
+ public Map getDockables() {
+ return dockables;
+ }
+
+ /**
+ * @return the (single) main window of the application, or {@code null} before construction
+ * (Swing {@code MainWindow.getInstance()})
+ */
+ public static @Nullable MainWindowF getInstance() {
+ return instance;
+ }
+
+ /**
+ * @return the mediator of the window (Swing {@code MainWindow.getMediator()})
+ */
+ public KeYMediatorF getMediator() {
+ return mediator;
+ }
+
+ /**
+ * @return the primary stage of the main window (owner for dialogs)
+ */
+ public Stage getStage() {
+ return stage;
+ }
+
+ /**
+ * WP-6 (slicing): read-only snapshot of the recent-files entries, most-recent-first — the
+ * assertion seam of the {@code key.fx.verify.slicing} hook, which checks that proof loads
+ * through {@link #loadProofFileDirect} do not change this list (Swing parity of the slicing
+ * extension's "we do not use UI.loadProblem here to avoid adding the slice to the recent
+ * files", SlicingLeftPanel.java:465). Must be called on the FX thread.
+ *
+ * @return an immutable copy of the current entries
+ */
+ public List recentFileEntries() {
+ return recentFiles.getEntries();
+ }
+
+ /**
+ * @return the sequent view docked in the main area
+ */
+ public SequentViewF getSequentView() {
+ return sequentView;
+ }
+
+ /**
+ * @return the selection model of the window
+ */
+ public KeYSelectionModel getSelectionModel() {
+ return selectionModel;
+ }
+
+ /**
+ * seam: the window's {@link de.uka.ilkd.key.control.UserInterfaceControl} (Swing
+ * {@code MainWindow.getUserInterface()}); the host of the status/task/exception callbacks
+ * and the rule-completion registry (merge point for the interactive completion ports).
+ */
+ public WindowUserInterfaceControlF getUserInterfaceControl() {
+ return userInterface;
+ }
+
+ // ------------------------------------------------------------------
+ // sequent view (M2a spike)
+ // ------------------------------------------------------------------
+
+ private void wireSequentView() {
+ sequentView.attach(selectionModel);
+ proofTreeView.attach(selectionModel);
+ infoView.attach(selectionModel);
+ goalListView.attach(selectionModel);
+ strategyView.attach(selectionModel);
+ // strategy (P2a): the Go/Stop button + the parallel-prover merge lock follow the
+ // mediator (Swing StrategySelectionView constructor wiring)
+ strategyView.attachMediator(mediator);
+ sourceView.attach(selectionModel);
+ loadedProofs.attach(selectionModel); // proofmgmt: row highlight follows the active proof
+ // extension: P4 (D36) — the KeyboardShortcuts extension seam: bind the view-scoped
+ // shortcuts of every KeYGuiExtensionF.KeyboardShortcutsF provider as key filters on the
+ // views (Swing KeYGuiExtensionFacade.installKeyboardShortcuts into the input maps of
+ // SequentView, GoalList, ProofTreeView, StrategySelectionView, SourceView and InfoView;
+ // KeYGuiExtensionFacade.java:361-375, called from SequentView.java:192,
+ // GoalList.java:108, ProofTreeView.java:334-335, StrategySelectionView.java:172,
+ // SourceView.java:225, InfoView.java:212)
+ KeYGuiExtensionFacadeF.installKeyboardShortcuts(mediator, sequentView,
+ KeYGuiExtensionF.KeyboardShortcutsF.SEQUENT_VIEW);
+ KeYGuiExtensionFacadeF.installKeyboardShortcuts(mediator, goalListView,
+ KeYGuiExtensionF.KeyboardShortcutsF.GOAL_LIST);
+ KeYGuiExtensionFacadeF.installKeyboardShortcuts(mediator, proofTreeView,
+ KeYGuiExtensionF.KeyboardShortcutsF.PROOF_TREE_VIEW);
+ KeYGuiExtensionFacadeF.installKeyboardShortcuts(mediator, strategyView,
+ KeYGuiExtensionF.KeyboardShortcutsF.STRATEGY_SELECTION_VIEW);
+ KeYGuiExtensionFacadeF.installKeyboardShortcuts(mediator, sourceView,
+ KeYGuiExtensionF.KeyboardShortcutsF.SOURCE_VIEW);
+ KeYGuiExtensionFacadeF.installKeyboardShortcuts(mediator, infoView,
+ KeYGuiExtensionF.KeyboardShortcutsF.INFO_TREE);
+ // the hook doubles as the :99 verification signal (same line as the standalone driver)
+ sourceView.setOnContentLoaded(
+ () -> LOGGER.info("Source self test: {}", sourceView.verifySourceView()));
+ selectionModel.addKeYSelectionListenerChecked(statusSelectionListener);
+ sequentView.setOnPosSelected(pos -> {
+ if (pos == null) {
+ statusRight.setText("");
+ return;
+ }
+ String text = sequentView.getHighlightedText(pos);
+ // the status bar stays single-line: flatten the pretty-printed term and cap it; the
+ // full position is in the log line below
+ String flat = text.replaceAll("\\s+", " ").strip();
+ if (flat.length() > 120) {
+ flat = flat.substring(0, 117) + "...";
+ }
+ statusRight.setText(flat.isBlank() ? String.valueOf(pos) : flat);
+ LOGGER.info("Clicked sequent position: {}", pos);
+ });
+ }
+
+ /**
+ * Milestone M2a spike affordance: if the system property {@code key.fx.demo.sequent} is set to
+ * a {@code .key} file, load it with the core {@link KeYEnvironment} on a background thread and
+ * display the root sequent in the sequent view.
+ */
+ private void startDemoProofLoad() {
+ String file = System.getProperty("key.fx.demo.sequent");
+ if (file == null || file.isBlank()) {
+ return;
+ }
+ startProofLoad(Path.of(file), true);
+ }
+
+ /**
+ * Loads a proof or problem file with the core {@link KeYEnvironment} on a background thread
+ * and routes the loaded proof through the mediator/selection model. Shared by the demo
+ * property load and the File menu actions ({@link #openFileChooser()}, {@link
+ * #reloadLastFile()}, the recent-files menu, quick load). The optional {@code
+ * key.fx.demo.autoprove} run happens inside the load task, so only demo loads prove
+ * automatically.
+ *
+ * Note: recent files are registered by the callers — the menu actions register the
+ * file before starting the load; the demo load must not pollute the user's shared
+ * {@code recentFiles_v2.json}.
+ *
+ * @param location the problem, proof or Java file to load
+ * @param demo whether this is the demo property load (notification prefix "Demo proof")
+ */
+ private void startProofLoad(Path location, boolean demo) {
+ startProofLoad(location, demo, null, null, false);
+ }
+
+ /**
+ * Loads a proof or problem file, optionally a specific proof out of a proof bundle.
+ *
+ * @param location the problem, proof, Java file or proof bundle to load
+ * @param demo whether this is the demo property load (notification prefix "Demo proof")
+ * @param proofFilename the proof to load relative to the bundle root, or {@code null} if
+ * {@code location} is not a proof bundle
+ */
+ private void startProofLoad(Path location, boolean demo, @Nullable Path proofFilename) {
+ startProofLoad(location, demo, proofFilename, null, false);
+ }
+
+ /**
+ * Loads a proof or problem file, optionally a specific proof out of a proof bundle, with
+ * optional loading options.
+ *
+ * @param location the problem, proof, Java file or proof bundle to load
+ * @param demo whether this is the demo property load (notification prefix "Demo proof")
+ * @param proofFilename the proof to load relative to the bundle root, or {@code null} if
+ * {@code location} is not a proof bundle
+ * @param options the loading options from the {@link LoadingOptionsDialogF} (Swing: the
+ * {@code KeYFileChooserLoadingOptions} accessory consumed by {@code OpenFileAction}),
+ * or {@code null} for the legacy load (recent files, quick load, demo)
+ */
+ private void startProofLoad(Path location, boolean demo, @Nullable Path proofFilename,
+ @Nullable LoadOptions options) {
+ startProofLoad(location, demo, proofFilename, options, false);
+ }
+
+ /**
+ * Loads a proof or problem file, optionally a specific proof out of a proof bundle, with
+ * optional loading options and an optional automatic proof run.
+ *
+ * @param location the problem, proof, Java file or proof bundle to load
+ * @param demo whether this is the demo property load (notification prefix "Demo proof")
+ * @param proofFilename the proof to load relative to the bundle root, or {@code null} if
+ * {@code location} is not a proof bundle
+ * @param options the loading options from the {@link LoadingOptionsDialogF} (Swing: the
+ * {@code KeYFileChooserLoadingOptions} accessory consumed by {@code OpenFileAction}),
+ * or {@code null} for the legacy load (recent files, quick load, demo)
+ * @param autoProve whether the automatic prover should run on the loaded proof right after
+ * the load (Swing {@code RunAllProofsAction}: {@code problemLoader.runSynchronously()}
+ * + {@code startAutoMode} + {@code waitWhileAutoMode})
+ */
+ private void startProofLoad(Path location, boolean demo, @Nullable Path proofFilename,
+ @Nullable LoadOptions options, boolean autoProve) {
+ // pure .key problems carry no Java source; the source view then shows the problem file.
+ // Proof bundles carry their own sources (or none) — never show the bundle zip as text
+ if (proofFilename == null) {
+ sourceView.setFallbackSourceFile(location);
+ }
+ Task> loadTask = new Task<>() {
+ @Override
+ protected KeYEnvironment call() throws Exception {
+ KeYEnvironment env;
+ if (proofFilename != null) {
+ // proof bundle: load the user-chosen proof from the bundle (Swing
+ // ProblemLoader.setProofPath in loadProofFromBundle). The core loader unzips
+ // the bundle to a temporary directory and loads the selected proof file from
+ // there. This mirrors KeYEnvironment.load, which offers no proofFilename
+ // parameter.
+ // seam: load through the window's own UserInterfaceControlF instead of the
+ // headless DefaultUserInterfaceControl, so its callbacks reach the UI
+ var ui = getUserInterfaceControl();
+ var loader = new SingleThreadProblemLoader(location, null, null, null, null,
+ false, ui, false, new Properties());
+ loader.setProofFilename(proofFilename);
+ loader.load();
+ env = new KeYEnvironment<>(ui, loader.getInitConfig(), loader.getProof(),
+ loader.getProofScript(), loader.getResult());
+ } else if (options != null) {
+ // smalldialogs: forward the loading options collected by
+ // LoadingOptionsDialogF (Swing OpenFileAction.java:70-76 wires the same
+ // four accessors into the ProblemLoader); force the chosen profile unless
+ // the legacy "Respect profile given in file" mode is active
+ DefaultUserInterfaceControl ui = new DefaultUserInterfaceControl();
+ var loader = new SingleThreadProblemLoader(location, null, null, null, null,
+ false, ui, false, new Properties());
+ loader.forceNewProfileOfNewProofs(options.selectedProfile() != null);
+ loader.setProfileOfNewProofs(options.selectedProfile());
+ loader.setAdditionalProfileOptions(options.additionalProfileOptions());
+ loader.setLoadSingleJavaFile(options.singleJavaFile());
+ loader.load();
+ env = new KeYEnvironment<>(ui, loader.getInitConfig(), loader.getProof(),
+ loader.getProofScript(), loader.getResult());
+ } else {
+ // seam: load through the window's own UserInterfaceControlF instead of the
+ // headless KeYEnvironment.load (Swing parity: KeYEnvironment.loadInMainWindow
+ // uses the WindowUserInterfaceControl as UI, WindowUserInterfaceControl.java
+ // :600-613), so status/task/exception callbacks reach the UI
+ var ui = getUserInterfaceControl();
+ var loader = new SingleThreadProblemLoader(location, null, null, null, null,
+ false, ui, false, new Properties());
+ loader.load();
+ env = new KeYEnvironment<>(ui, loader.getInitConfig(), loader.getProof(),
+ loader.getProofScript(), loader.getResult());
+ }
+ if (System.getProperty("key.fx.demo.autoprove") != null) {
+ LOGGER.info("Demo: running auto mode on the loaded proof");
+ env.getProofControl().startAndWaitForAutoMode(env.getLoadedProof());
+ LOGGER.info("Demo: auto mode finished");
+ }
+ // menu: MP5 — auto prove after the load (Swing RunAllProofsAction: run
+ // synchronously, then startAutoMode + waitWhileAutoMode on the
+ // MediatorProofControl)
+ if (autoProve) {
+ LOGGER.info("Run All Proofs: running auto mode on the loaded proof");
+ env.getProofControl().startAndWaitForAutoMode(env.getLoadedProof());
+ LOGGER.info("Run All Proofs: auto mode finished");
+ }
+ return env;
+ }
+ };
+ loadTask.setOnSucceeded(event -> {
+ KeYEnvironment env = loadTask.getValue();
+ // the mediator observes the proof control (auto mode state, closed-goal counter);
+ // the UI's own listener refreshes the views after interactive auto mode runs
+ mediator.attach(env.getProofControl());
+ // menu: MP7 — a freshly attached proof control inherits the persisted Minimize
+ // Interaction flag (Swing MinimizeInteraction.updateMainWindow applies the flag to the
+ // UI's proof control on construction and on GeneralSettings changes,
+ // MinimizeInteraction.java:64-66)
+ applyMinimizeInteraction(env.getProofControl());
+ // termmenu: give the sequent view the mediator + proof control of the loaded
+ // environment so the right-click context menu can be built (Swing parity:
+ // CurrentGoalViewMenu is built with the mediator's selected goal and the proof
+ // control of the loaded environment)
+ sequentView.setMenuContext(mediator, env.getProofControl());
+ // prooftree (P3a): the tree popup's Apply Strategy / Prune actions and the
+ // auto-mode partial updates need the mediator + proof control (Swing parity:
+ // ProofTreeView registers itself as AutoModeListener on the UI's proof control,
+ // ProofTreeView.java:532)
+ proofTreeView.setActionContext(mediator, env.getProofControl());
+ // D34 (P3c): the proof-tree popup's "Open Node in Separate Buffer" opens the node's
+ // sequent in a separate dockable (Swing SequentViewDock.OpenCurrentNodeAction)
+ proofTreeView.setOpenNodeInSeparateBuffer(this::openNodeInSeparateBuffer);
+ env.getProofControl().addAutoModeListener(autoModeUiListener);
+ // notification: register the notification framework's auto-mode tracker on the
+ // proof control (Swing parity: the NotificationManager constructor registers its
+ // listener, NotificationManager.java:99-101; created in MainWindow.java:333)
+ env.getProofControl().addAutoModeListener(
+ NotificationCenterF.getInstance().notificationListener());
+ // wp6 (slicing): fire the proof-loaded event for every load of the FX pipeline
+ // (Swing fires it synchronously from WindowUserInterfaceControl.
+ // registerProofAggregate during the load, before the replay,
+ // WindowUserInterfaceControl.java:522; the FX pipeline fires it here, after the
+ // loader finished and before the selection update — the consumers (the slicing
+ // extension's tracker registration, Swing SlicingExtension.java:120) observe every
+ // load either way)
+ mediator.fireProofLoaded(env.getLoadedProof());
+ // route the proof through the selection model: setSelectedProof invokes the
+ // mediator's setProof (listener swap, abbreviation rebind, OSS refresh) and then
+ // selects the first open goal or a leaf, which the views observe.
+ selectionModel.setSelectedProof(env.getLoadedProof());
+ // proofmgmt: register the loaded proof in the multi-proof state (the Loaded Proofs
+ // view follows) and remember the environment for the Proof Management dialog
+ lastEnvironment = env;
+ proofManager.addProof(env.getLoadedProof());
+ String show = System.getProperty("key.fx.show", ID_SEQUENT);
+ // drawer: MP10 — the proof tree is a west drawer item now, not a dockable; expanding
+ // it (the sequent is selected in the workspace after the load)
+ if (ID_PROOF_TREE.equalsIgnoreCase(show) && westDrawer != null) {
+ westDrawer.getItems().stream()
+ .filter(item -> "Proof Tree".equals(item.getButton().getText()))
+ .findFirst().ifPresent(item -> item.getButton().setSelected(true));
+ show = ID_SEQUENT;
+ }
+ String target = ID_SEQUENT;
+ LOGGER.info("Selecting dockable '{}' (key.fx.show={})", target, show);
+ workspace.select(dockables.get(target));
+ NotificationManagerF.getInstance()
+ .notify((demo ? "Demo proof loaded: " : "Proof loaded: ") + location,
+ Kind.INFO);
+ if (System.getProperty("key.fx.verify.sequent") != null) {
+ String report = sequentView.verifyPositionMapping();
+ LOGGER.info("Sequent position mapping verification: {}", report);
+ NotificationManagerF.getInstance()
+ .notify("Position mapping verification: " + report,
+ report.endsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ statusRight.setText(report);
+ String hlReport = sequentView.verifySyntaxHighlighting();
+ LOGGER.info("Sequent syntax highlighting verification: {}", hlReport);
+ NotificationManagerF.getInstance()
+ .notify("Syntax highlighting verification: " + hlReport,
+ hlReport.endsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ // D31 (P3c): viewport-driven print line width (Swing computeLineWidthFor)
+ String lwReport = sequentView.verifyViewportLineWidth();
+ LOGGER.info("Sequent viewport line width verification: {}", lwReport);
+ NotificationManagerF.getInstance()
+ .notify("Viewport line width verification: " + lwReport,
+ lwReport.endsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ // WP-3 (sequent): the sticky user-selection highlight + reprint persistence —
+ // pin, reprint, settings-change reprint, unpin (Swing
+ // recalculateUserSelectionRange / SequentViewInputListener.mouseClicked)
+ String userSelReport = sequentView.verifyUserSelection();
+ LOGGER.info("Sequent user selection verification: {}", userSelReport);
+ NotificationManagerF.getInstance()
+ .notify("Sequent user selection verification: " + userSelReport,
+ userSelReport.endsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ }
+ if (System.getProperty("key.fx.verify.heatmap") != null) {
+ // heatmap (WP-2): render the age heatmap into the sequent view — the four
+ // ViewSettings modes (sf/terms × age/newest) produce heat rectangles behind the
+ // printed text, disabled shows none; run with a demo proof that has rule
+ // applications (e.g. strassen.auto.0.proof)
+ String report = sequentView.verifyHeatmap();
+ LOGGER.info("Heatmap verification: {}", report);
+ NotificationManagerF.getInstance()
+ .notify("Heatmap verification: " + report,
+ report.endsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ statusRight.setText(report);
+ }
+ if (System.getProperty("key.fx.verify.sequentsearch") != null
+ && System.getProperty("key.fx.demo.autoprove.live") == null) {
+ // without the live auto mode the sequent at load is final; with the live mode
+ // the sequent search verification runs at its stop
+ runSequentSearchVerification();
+ }
+ if (System.getProperty("key.fx.verify.sequentsearchmodes") != null
+ && System.getProperty("key.fx.demo.autoprove.live") == null) {
+ // ditto; runs after the plain search verification and restores the plain view
+ runSequentSearchModesVerification();
+ }
+ if (System.getProperty("key.fx.verify.updatehighlight") != null) {
+ runUpdateHighlightVerification();
+ }
+ // smt (P1): run the FX SMT run UI end to end — launch the usable solver union on the
+ // first open goal of the demo proof with the auto-applying CLOSE progress mode and
+ // check that the goal got closed by the SMT rule application
+ if (System.getProperty("key.fx.verify.smt") != null) {
+ runSmtVerification();
+ }
+ // isabelle (WP-8): run the Isabelle solver-launch DRY-RUN self test — the FX progress
+ // dialog renders with fake solvers, the Stop cancellation path works and the
+ // scala-isabelle backend stays unloaded (IsabelleLauncher.launch is never called, so
+ // no real Isabelle installation is needed)
+ if (System.getProperty("key.fx.verify.isabelle") != null) {
+ String report = runIsabelleVerification(stage, selectionModel.getSelectedProof());
+ LOGGER.info("Isabelle solver-launch verification: {}", report);
+ NotificationManagerF.getInstance()
+ .notify("Isabelle solver-launch verification: " + report,
+ report.endsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ statusRight.setText(report);
+ }
+ // caching (WP-4): run the proof-caching reference-search self test — the FX
+ // ReferenceSearchDialogF renders the per-goal reference table of the demo proof, a
+ // stub ClosedBy marks one row "reference available", the Apply button wiring fires
+ // the dialog listener and disables the buttons (no real caching copy is run)
+ if (System.getProperty("key.fx.verify.caching") != null) {
+ String report =
+ runCachingVerification(stage, selectionModel.getSelectedProof(), mediator);
+ LOGGER.info("Proof caching verification: {}", report);
+ NotificationManagerF.getInstance()
+ .notify("Proof caching verification: " + report,
+ report.endsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ statusRight.setText(report);
+ }
+ // slicing (WP-6): run the proof-slicing self test (key.fx.verify.slicing) after the
+ // demo load: auto mode on the demo proof, then the DOT image render (graphviz
+ // binary, plus the documented no-graphviz fallback), the HTML statistics dialog with
+ // its four sort orders, the iterative fixed-point slicing run and the recent-files
+ // bypass of the direct slice loads. The harness lives in keyext.slicing.fx (the
+ // dependency direction is reversed) and is invoked reflectively; its long work runs
+ // on a background thread and logs its own final marker line. The demo guard keeps
+ // the fixed-point/slice loads of the harness itself from re-triggering the hook.
+ if (demo && System.getProperty("key.fx.verify.slicing") != null) {
+ runSlicingVerification(env.getLoadedProof());
+ }
+ // testgen (WP-7): run the TestGen extension self test — the extension capability
+ // surface (toolbar + Startup hook + macro accelerator), the options panel round
+ // trips, the two run dialogs' idle modes, the CE model tree of a stub SMT model and
+ // the cooperative stop (no real generation run; the real SMT run is key.fx.verify.smt)
+ if (System.getProperty("key.fx.verify.testgen") != null) {
+ String report =
+ runTestgenVerification(stage, selectionModel.getSelectedProof(), mediator);
+ LOGGER.info("TestGen verification: {}", report);
+ NotificationManagerF.getInstance()
+ .notify("TestGen verification: " + report,
+ report.endsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ statusRight.setText(report);
+ }
+ // shortcuts (P1): Swing-parity defaults, no collisions, override round trip and the
+ // sequent view key path (Ctrl+F shows the search bar)
+ if (System.getProperty("key.fx.verify.shortcuts") != null) {
+ runShortcutsVerification();
+ }
+ // inputfreeze (P1): the auto mode freeze — direct freeze/unfreeze with key blocking,
+ // then an auto-mode-driven run
+ if (System.getProperty("key.fx.verify.inputfreeze") != null) {
+ runInputFreezeVerification();
+ }
+ // sequentmenu (P2a): the left-click term menu + POPUP_DELAY guard, search prefill
+ // and the shift+click focussed auto mode
+ if (System.getProperty("key.fx.verify.sequentmenu") != null) {
+ String report = sequentView.verifySequentMenu();
+ LOGGER.info("Sequent menu verification: {}", report);
+ NotificationManagerF.getInstance()
+ .notify("Sequent menu verification: " + report,
+ report.endsWith("PASS") || report.startsWith("PASS")
+ ? Kind.INFO
+ : Kind.ERROR);
+ statusRight.setText(report);
+ }
+ // dialogs (P2b): the contract-completion dialogs and the lemma dialogs — registry
+ // completeness + the contract/auxiliary configurator and lemma selection dialog
+ // skeletons + the item chooser semantics
+ if (System.getProperty("key.fx.verify.dialogs") != null) {
+ String report = (String) runVerifyHarness(
+ "de.uka.ilkd.key.gui.fx.dialogs.DialogsVerifyF", "run",
+ new Class>[] { javafx.stage.Window.class, Proof.class,
+ WindowUserInterfaceControlF.class },
+ stage, selectionModel.getSelectedProof(), userInterface);
+ statusRight.setText(report);
+ }
+ if (System.getProperty("key.fx.verify.prooftree") != null
+ && System.getProperty("key.fx.demo.autoprove.live") == null) {
+ // prooftree (P3a): the C19-C23/C25/C27 self test — per-proof view states,
+ // linearized mode, OSS protocol rows, whole-tree expand/collapse, node-filter
+ // counting and the notes/statistics popup dialogs. Skipped when the live demo
+ // auto-prover runs in parallel (the auto-mode-stop dispatch below verifies the
+ // final tree instead — running the self test against a proof the prover is
+ // still mutating would race with the structural events)
+ String report = (String) runVerifyHarness(
+ "de.uka.ilkd.key.gui.fx.prooftree.ProofTreeVerifyF", "run",
+ new Class>[] { javafx.stage.Window.class, Proof.class,
+ ProofTreeViewF.class },
+ stage, selectionModel.getSelectedProof(), proofTreeView);
+ statusRight.setText(report);
+ }
+ // loadingexit (P1): recent-files round trip with loading options + profile
+ // resolution, then the exit flow — the window close button path with Confirm Exit
+ // off must terminate the process with exit code 0
+ if (System.getProperty("key.fx.verify.loadingexit") != null) {
+ runLoadingExitVerification();
+ }
+ // drawer: headless self test of the DrawerF port (exclusive/multiselect semantics,
+ // side placement, button-order split, drag-and-drop reorder + transfer seams)
+ if (System.getProperty("key.fx.verify.drawer") != null) {
+ String drawerReport = DrawerF.selfTest();
+ LOGGER.info("Drawer verification: {}", drawerReport);
+ NotificationManagerF.getInstance()
+ .notify("Drawer verification: " + drawerReport,
+ drawerReport.startsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ }
+ // drawer: MP10 — headless self test of the drawered main area (drawer hosts, item
+ // sets, default expansions and the drag-and-drop seams on the live drawers)
+ if (System.getProperty("key.fx.verify.drawerlayout") != null) {
+ runDrawerLayoutVerification(env);
+ }
+ if (System.getProperty("key.fx.verify.tree") != null) {
+ String report = proofTreeView.verifyTreeStructure();
+ LOGGER.info("Proof tree structure verification: {}", report);
+ NotificationManagerF.getInstance()
+ .notify("Tree structure verification: " + report,
+ report.endsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ }
+ if (System.getProperty("key.fx.verify.search") != null
+ && System.getProperty("key.fx.demo.autoprove.live") == null) {
+ // without the live auto mode the proof state at load is final (fresh or already
+ // auto-closed); with the live mode the search verification runs at its stop
+ runSearchVerification();
+ }
+ if (System.getProperty("key.fx.verify.goallist") != null) {
+ LOGGER.info("Goal list verification: {}", goalListView.verifyGoalList());
+ // WP-3 (goallist-strategy-info): the strategy preset combo / stash UI self test
+ // — entry list, stash/save/rename/delete round trip, preset write-through and
+ // the Apply-button gating (StrategySelectionViewF.verifyPresetCombo)
+ String presetReport = strategyView.verifyPresetCombo();
+ LOGGER.info("Strategy preset verification: {}", presetReport);
+ NotificationManagerF.getInstance()
+ .notify("Strategy preset verification: " + presetReport,
+ presetReport.endsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ statusRight.setText(presetReport);
+ }
+ // joinmerge: run the join/merge dialog self test (key.fx.verify.joinmerge)
+ if (System.getProperty("key.fx.verify.joinmerge") != null) {
+ runJoinMergeVerification(env);
+ }
+ // smalldialogs: soundiness report self test (Swing SoundinessDialog/
+ // SoundinessAnalyzer), runs on the freshly loaded demo proof and opens the dialog
+ if (System.getProperty("key.fx.verify.soundiness") != null) {
+ runSoundinessVerification();
+ }
+ if (System.getProperty("key.fx.verify.proofdiff") != null) {
+ String report = ProofDiffFrameF.verifyDiffLogic();
+ LOGGER.info("Proof diff verification: {}", report);
+ NotificationManagerF.getInstance()
+ .notify("Proof diff verification: " + report,
+ report.endsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ }
+ if (System.getProperty("key.fx.verify.notifications") != null) {
+ // notification: notification-framework self test (fires each notification type
+ // and asserts the actions' visible counterparts; report logged/toasted there)
+ NotificationCenterF.getInstance()
+ .verifyNotifications(selectionModel.getSelectedProof());
+ }
+ if (System.getProperty("key.fx.verify.docking") != null) { // docking: docking self test
+ String report = dockingLayout.runSelfTest();
+ LOGGER.info("Docking self test: {}", report);
+ NotificationManagerF.getInstance()
+ .notify("Docking self test: " + report,
+ report.startsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ }
+ // seam: key.fx.verify.uicontrol — self test of the WindowUserInterfaceControlF seam
+ // (status line, IssueDialogF, LogViewF, AutoDismissDialogF) after the demo load
+ if (System.getProperty("key.fx.verify.uicontrol") != null) {
+ UiControlSelfTestF.run(this);
+ }
+ // tacletmatch: run the interactive taclet application self test (dialog render,
+ // cancel keeps the proof, seam dispatch opens the dialog, apply adds to the proof)
+ // after the demo load; termmenu/S4 passes the seam so the dispatch path
+ // (WindowUserInterfaceControlF.completeAndApplyTacletMatch) is covered
+ if (System.getProperty("key.fx.verify.tacletmatch") != null) {
+ runVerifyHarness("de.uka.ilkd.key.gui.fx.tacletmatch.TacletMatchVerifyF",
+ "runTacletMatchVerification",
+ new Class>[] { Proof.class, ProofControl.class, javafx.stage.Stage.class,
+ NotationInfo.class, WindowUserInterfaceControlF.class },
+ env.getLoadedProof(), env.getProofControl(), stage,
+ mediator.getNotationInfo(), getUserInterfaceControl());
+ }
+ // lemmaorigin: begin — term labels / origin visualizer / lemma generator self test
+ if (System.getProperty("key.fx.verify.lemmaorigin") != null) {
+ String report = OriginLabelsF.verify(this);
+ LOGGER.info("Lemmaorigin verification: {}", report);
+ NotificationManagerF.getInstance()
+ .notify("Lemmaorigin verification: " + report,
+ report.contains("FAIL") ? Kind.ERROR : Kind.INFO);
+ }
+ // lemmaorigin: end
+ // termmenu: run the headless sequent context-menu self test
+ // (key.fx.verify.termmenu) after the demo load like the other proof-dependent
+ // verify hooks; the text report goes to stdout
+ if (System.getProperty("key.fx.verify.termmenu") != null) {
+ runTermMenuVerification(env);
+ }
+ // menu: MP8 — term-menu wiring self test (key.fx.verify.termmenuwiring), same seam
+ // as key.fx.verify.termmenu: asserts the MP8a/MP8b wiring on the loaded demo — the
+ // focus_auto_mode item is ENABLED with a non-null action handler and the
+ // macro_menu section exists with exactly the four AUTOMATION_MACROS names. The
+ // handlers are NOT invoked (a real focused auto mode / macro run is too heavy
+ // mid-regression); enablement + handler presence is the assertion.
+ if (System.getProperty("key.fx.verify.termmenuwiring") != null) {
+ runTermMenuWiringVerification(env);
+ }
+ // menu: MP1/MP3c — menu parity self test (key.fx.verify.menuparity): walks the
+ // built menu bar and asserts the Proof menu entries against the Swing
+ // createProofMenu table and (since MP3c) the View menu entries against the Swing
+ // createViewMenu table; each marker line must end with PASS
+ if (System.getProperty("key.fx.verify.menuparity") != null) {
+ verifyMenuParity();
+ }
+ // menu: MP3b (WP-10) — Selection Back/Forward behavioral self test
+ // (key.fx.verify.selectionhistory): fires the View menu's Back/Forward items and
+ // asserts the selection history actually moves the mediator selection back/forward
+ // (needs a proof with at least two nodes — combine with key.fx.demo.autoprove)
+ if (System.getProperty("key.fx.verify.selectionhistory") != null) {
+ runSelectionHistoryVerification(env.getLoadedProof());
+ }
+ // menu: MP7 — Minimize Interaction self test (key.fx.verify.minimizeinteraction):
+ // flips the persisted taclet filter through the same apply helper the toggle uses and
+ // asserts the proof control mirrors the flag in both directions
+ if (System.getProperty("key.fx.verify.minimizeinteraction") != null) {
+ GeneralSettings gs = ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings();
+ boolean original = gs.getTacletFilter();
+ boolean ok;
+ gs.setTacletFilter(!original);
+ applyMinimizeInteraction(env.getProofControl());
+ ok = env.getProofControl().isMinimizeInteraction() == gs.getTacletFilter();
+ gs.setTacletFilter(original);
+ applyMinimizeInteraction(env.getProofControl());
+ ok &= env.getProofControl().isMinimizeInteraction() == gs.getTacletFilter();
+ LOGGER.info("Minimize interaction verification: {}", ok ? "PASS" : "FAIL");
+ NotificationManagerF.getInstance()
+ .notify("Minimize interaction verification: " + (ok ? "PASS" : "FAIL"),
+ ok ? Kind.INFO : Kind.ERROR);
+ // menu: MP7/WP-9 — real-flow leg: with the setting armed the interactive
+ // application of an auto-instantiable taclet must enter the minimize flow
+ // (applied without the taclet dialog) and with the flag off the dialog must open
+ verifyMinimizeInteractionRealFlow(env.getProofControl(), env.getLoadedProof());
+ }
+ // menu: MP7 — right-click macro popup self test (key.fx.verify.rightclickmacro):
+ // builds the popup through the SequentViewF seam with the flag ON and OFF and asserts
+ // the macro names / the term-menu fallback (report logged/toasted there)
+ if (System.getProperty("key.fx.verify.rightclickmacro") != null) {
+ String report = sequentView.verifyRightClickMacro();
+ LOGGER.info("Right-click macro verification: {}", report);
+ NotificationManagerF.getInstance()
+ .notify("Right-click macro verification: " + report,
+ report.endsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ }
+ // menu: MP7 — auto-save self test (key.fx.verify.autosave): asserts the saver is armed
+ // iff the persisted period is > 0 and that it received the loaded proof (the proof
+ // identity is read reflectively; see readAutoSaveProof)
+ if (System.getProperty("key.fx.verify.autosave") != null) {
+ GeneralSettings gs = ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings();
+ int savedPeriod = gs.autoSavePeriod();
+ boolean ok;
+ try {
+ // the startup arming must match the persisted period (armed iff > 0)
+ boolean armedOk =
+ (mediator.getAutoSaver() != null) == (savedPeriod > 0);
+ // force the armed state and route the loaded proof again through the real
+ // selection path, so the saver's setProof fires (mediator.setProof hook)
+ gs.setAutoSave(DEFAULT_AUTO_SAVE_PERIOD);
+ applyAutoSave(DEFAULT_AUTO_SAVE_PERIOD);
+ selectionModel.setSelectedProof(null);
+ selectionModel.setSelectedProof(env.getLoadedProof());
+ AutoSaver saver = mediator.getAutoSaver();
+ boolean proofOk =
+ saver != null && readAutoSaveProof(saver) == env.getLoadedProof();
+ // the disarmed state after the switch
+ gs.setAutoSave(0);
+ applyAutoSave(0);
+ ok = armedOk && proofOk && mediator.getAutoSaver() == null;
+ } finally {
+ gs.setAutoSave(savedPeriod);
+ applyAutoSave(savedPeriod);
+ }
+ LOGGER.info("Auto save verification: {}", ok ? "PASS" : "FAIL");
+ NotificationManagerF.getInstance()
+ .notify("Auto save verification: " + (ok ? "PASS" : "FAIL"),
+ ok ? Kind.INFO : Kind.ERROR);
+ // menu: MP7/WP-9 — real-flow leg: a capped auto-mode run with the saver armed at
+ // a small interval must actually write the intermediate proof files
+ verifyAutoSaveRealFlow(env.getProofControl(), env.getLoadedProof());
+ }
+ // extension: MP9.0 — extension SPI self test (key.fx.verify.extensions): asserts
+ // the facade discovery (3 registered FX extensions), the two status-line controls
+ // in the built status bar, the Heatmap menu in the menu bar, the Heatmap settings
+ // provider in the settings registry and the term-menu extension section falling
+ // back to the disabled placeholder without a position. One stdout report line.
+ if (System.getProperty("key.fx.verify.extensions") != null) {
+ runExtensionVerification(env);
+ }
+ // exploration (WP-5): headless self test of the exploration extension parity — the
+ // model registered through the mediator registry (lookup), the renderer-styling hook
+ // drawing the purple exploration border on a real exploration step of the demo, the
+ // "Hide justification" toolbar toggle routing through the HIDE_INTERACTIVE_GOALS
+ // tree filter and the promptForTerm retry-loop accepting bad-then-good input
+ if (System.getProperty("key.fx.verify.exploration") != null) {
+ runExplorationVerification(env);
+ }
+ if (System.getProperty("key.fx.demo.autoprove.live") != null) {
+ startLiveAutoMode(env);
+ }
+ });
+ loadTask.setOnFailed(event -> {
+ Throwable error = loadTask.getException();
+ String message = (demo ? "Demo proof" : "Proof") + " loading failed";
+ LOGGER.error(message, error);
+ // seam: loading errors surface in the IssueDialog (Swing parity: the
+ // ProblemLoader branch of WindowUserInterfaceControl.taskFinishedInternal,
+ // WindowUserInterfaceControl.java:236-244, calls IssueDialog.showExceptionDialog;
+ // the FX load task throws instead of reporting a failed TaskFinishedInfo)
+ IssueDialogF.showExceptionDialog(getStage(), error);
+ // notification: termmenu/S4 — route the failure through the notification center
+ // instead of the plain toast (Swing parity: the ExceptionFailureEvent framework,
+ // NotificationManager.setDefaultNotification + the FIXME'd
+ // ExceptionFailureNotification; the FX ExceptionFailureNotificationF is toast-only,
+ // so the Swing double-dialog concern does not apply and it is a default task).
+ // The IssueDialog above stays the primary surface; the center's toast is the
+ // notification sink.
+ NotificationCenterF.getInstance().handleNotificationEvent(
+ new ExceptionFailureEventF(message, error));
+ });
+ Thread loader = new Thread(loadTask, "fx-demo-proof-loader");
+ loader.setDaemon(true);
+ loader.start();
+ }
+
+ // ------------------------------------------------------------------
+ // file menu actions: open / reload / recent files / save (M3/M4)
+ // ------------------------------------------------------------------
+
+ /**
+ * The Open File action (Swing {@code OpenFileAction}): a file chooser for problem, proof and
+ * Java files, remembering the last directory like the Swing original.
+ */
+ private void openFileChooser() {
+ FileChooser chooser = new FileChooser();
+ chooser.setTitle("Select file to load proof or problem");
+ if (lastSelectedDir != null && Files.isDirectory(lastSelectedDir)) {
+ chooser.setInitialDirectory(lastSelectedDir.toFile());
+ }
+ // Swing KeYFileChooser.DEFAULT_FILTER
+ chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(
+ "Java files, (compressed) KeY files, proof bundles, and source directories",
+ "*.java", "*.key", "*.proof", "*.proof.gz", "*.zproof"));
+ chooser.getExtensionFilters()
+ .add(new FileChooser.ExtensionFilter("All Files", "*.*"));
+ File file = chooser.showOpenDialog(stage);
+ if (file == null) {
+ return;
+ }
+ if (file.getParentFile() != null) {
+ lastSelectedDir = file.getParentFile().toPath();
+ }
+ // smalldialogs: loading options (Swing KeYFileChooserLoadingOptions accessory, read by
+ // OpenFileAction on approve, OpenFileAction.java:70-76). The JavaFX FileChooser has no
+ // accessory, so the options are collected in a pre-load dialog; Cancel aborts the load
+ // like the Swing chooser cancel. Proof bundles keep the profile of the bundle (Swing
+ // OpenFileAction returns before the options wiring), so no dialog for them.
+ if (!ProofSelectionDialogF.isProofBundle(file.toPath())) {
+ LoadOptions options = LoadingOptionsDialogF.showOptions(stage);
+ if (options == null) {
+ return;
+ }
+ openProofFile(file.toPath(), options);
+ return;
+ }
+ openProofFile(file.toPath());
+ }
+
+ /**
+ * Loads the given file and registers it in the recent files list (Swing
+ * {@code WindowUserInterfaceControl.loadProblem} registers before the load starts).
+ *
+ * Proof bundles ({@code .zproof}, Swing {@code OpenFileAction} special case) first show the
+ * {@link ProofSelectionDialogF}; the chosen proof is loaded from the bundle and the
+ * bundle is registered in the recent files (Swing
+ * {@code WindowUserInterfaceControl.loadProofFromBundle}). A bare {@code .java} file shows
+ * the load-behaviour warning (Swing {@code OpenFileAction}, view setting
+ * {@code notifyLoadBehaviour}).
+ *
+ * @param file the file to load
+ */
+ public void openProofFile(Path file) {
+ openProofFile(file, null);
+ }
+
+ /**
+ * Loads the given file with the given loading options and registers it in the recent files
+ * list. The options come from the {@link LoadingOptionsDialogF} in {@code
+ * openFileChooser()} (Swing: the {@code KeYFileChooserLoadingOptions} accessory); the
+ * recent-files and quick-load flows keep the legacy behavior (no options, like Swing where
+ * the accessory only exists in the file chooser).
+ *
+ * @param file the file to load
+ * @param options the loading options, or {@code null} for the legacy load
+ */
+ public void openProofFile(Path file, @Nullable LoadOptions options) {
+ // special case proof bundles -> allow to select the proof to load
+ if (ProofSelectionDialogF.isProofBundle(file)) {
+ Path proofPath = ProofSelectionDialogF.chooseProofToLoad(file, stage);
+ if (proofPath == null) {
+ return; // canceled by user!
+ }
+ recentFiles.add(file.toAbsolutePath().toString(), null, false, null);
+ startProofLoad(file, false, proofPath);
+ return;
+ }
+
+ warnOnBareJavaFile(file);
+ // loadingexit (P1): remember the loading options with the entry (Swing
+ // WindowUserInterfaceControl.loadProblem registers problemLoader.getProfileOfNewProofs()
+ // / isLoadSingleJavaFile() / getAdditionalProfileOptions, WindowUserInterfaceControl
+ // .java:102-105); the recent-files menu restores them on open
+ recentFiles.add(file.toAbsolutePath().toString(),
+ options != null && options.selectedProfile() != null
+ ? options.selectedProfile().ident()
+ : null,
+ options != null && options.singleJavaFile(),
+ options != null ? options.additionalProfileOptions() : null);
+ startProofLoad(file, false, null, options);
+ }
+
+ /**
+ * Loads the given file through the direct loader pipeline WITHOUT registering it in the
+ * recent-files list (Swing parity of the proof-slicing extension's slice load,
+ * SlicingLeftPanel.java:465-470: "we do not use UI.loadProblem here to avoid adding the
+ * slice to the recent files" — the Swing original creates the {@code ProblemLoader} via
+ * {@code mediator.getUI().getProblemLoader} and runs it asynchronously). Used by the slicing
+ * extension to load intermediate and final slices, and exercised by the
+ * {@code key.fx.verify.slicing} hook.
+ *
+ * @param file the proof file to load
+ */
+ public void loadProofFileDirect(Path file) {
+ startProofLoad(file, false);
+ }
+
+ /**
+ * The load-behaviour warning for bare Java files (Swing
+ * {@code OpenFileAction.actionPerformed}):
+ * shown when the view setting {@code notifyLoadBehaviour} is active, with a "don't show this
+ * warning again" checkbox writing the setting back (persisted via
+ * {@code ProofIndependentSettings.saveSettings}).
+ *
+ * @param file the file about to be loaded
+ */
+ private void warnOnBareJavaFile(Path file) {
+ ViewSettings viewSettings = ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings();
+ if (!viewSettings.getNotifyLoadBehaviour() || !file.toString().endsWith(".java")) {
+ return;
+ }
+ CheckBox checkbox = new CheckBox("Don't show this warning again");
+ VBox message = new VBox(6,
+ new Label("When you load a Java file, all java files in the current"),
+ new Label("directory and all subdirectories will be loaded as well."), checkbox);
+ Alert alert = new Alert(Alert.AlertType.WARNING);
+ alert.setTitle("Please note");
+ alert.setHeaderText(null);
+ alert.getDialogPane().setContent(message);
+ alert.initOwner(stage);
+ ExampleChooserF.themeDialogPane(alert);
+ alert.showAndWait();
+ viewSettings.setNotifyLoadBehaviour(!checkbox.isSelected());
+ ProofIndependentSettings.DEFAULT_INSTANCE.saveSettings();
+ }
+
+ /**
+ * The Reload action (Swing {@code OpenMostRecentFileAction}): loads the most recent file.
+ */
+ private void reloadLastFile() {
+ String recent = recentFiles.getMostRecent();
+ if (recent == null) {
+ // the item is disabled without a proof/recent; keep the Swing action's silent guard
+ LOGGER.info("Reload requested, but no recent file exists");
+ return;
+ }
+ openProofFile(Path.of(recent));
+ }
+
+ /**
+ * menu: MP5 — the Edit Last Opened File action (Swing {@code EditMostRecentFileAction}): opens
+ * the most recently opened file with the default external editor. The Swing original goes
+ * through {@code EditFileActionHandler.workWithFile} (finally {@code Desktop.open}); the FX
+ * port opens the file's {@code file://} URI through the {@link HelpFacadeF} browser seam
+ * (host services), so the same seam that serves the About-menu browser actions is reused.
+ */
+ private void editLastOpenedFile() {
+ String recent = recentFiles.getMostRecent();
+ if (recent == null) {
+ // the item is disabled without a recent file; keep the Swing action's silent guard
+ return;
+ }
+ HelpFacadeF.openExternal(Path.of(recent).toUri().toString());
+ }
+
+ // ------------------------------------------------------------------
+ // menu: MP5 — taclet loading / proving (Swing LemmaGenerationAction)
+ // ------------------------------------------------------------------
+
+ /**
+ * menu: MP5 — the "Load User Defined Taclets…" action (Swing
+ * {@code LemmaGenerationAction.ProveAndAddTaclets}, Mode.LOAD): loads the taclets of a
+ * user-chosen {@code .key} file into the current proof. The {@code TacletSoundnessPOLoader}
+ * runs the soundness proof obligations; on success the loaded taclets are prepended to the
+ * proof's init config and added to its open goals (the Swing doStopped flow,
+ * LemmaGenerationAction.java:307-326).
+ */
+ private void loadUserDefinedTaclets() {
+ Proof proof = selectionModel.getSelectedProof();
+ if (proof == null) {
+ return; // the item is disabled without a proof (Swing proofIsRequired()==true)
+ }
+ Optional result =
+ LoadUserTacletsDialogF.showDialog(stage, LoadUserTacletsDialogF.Mode.LOAD);
+ if (result.isEmpty()) {
+ return;
+ }
+ Path fileForTaclets = result.get().fileForTaclets();
+ boolean loadAsLemmata = result.get().generateProofObligations();
+ // lemma (P2b, A3): the axiom files chosen in the dialog are loaded for the lemmata only
+ List filesForAxioms = result.get().filesForAxioms();
+ final WindowUserInterfaceControlF ui = getUserInterfaceControl();
+ Profile profile = proof.getServices().getProfile();
+ ProblemInitializer problemInitializer =
+ new ProblemInitializer(ui, new Services(profile), ui);
+ TacletLoader tacletLoader = new TacletLoader.TacletFromFileLoader(ui, ui,
+ problemInitializer, fileForTaclets, filesForAxioms, proof.getInitConfig().copy());
+
+ LemmaLoaderListener listener = new LemmaLoaderListener() {
+ @Override
+ protected void doStopped(Throwable exception) {
+ handleTacletLoadException(exception);
+ }
+
+ @Override
+ protected void doStopped(@Nullable ProofAggregate p, ImmutableSet taclets,
+ boolean addAxioms) {
+ // menu: getMediator().startInterface(true) is a no-op — the FX port has no
+ // interface lock (KeYMediatorF has no startInterface/stopInterface)
+ if (p != null) {
+ ui.registerProofAggregate(p);
+ }
+ if (p != null || addAxioms) {
+ // add only the taclets to the goals if the proof obligations were added
+ // successfully (Swing LemmaGenerationAction.java:314-326)
+ ImmutableList base = proof.getInitConfig().getTaclets();
+ base = base.prependReverse(taclets);
+ proof.getInitConfig().setTaclets(base);
+ for (Taclet taclet : taclets) {
+ for (Goal goal : proof.openGoals()) {
+ goal.addTaclet(taclet, SVInstantiations.EMPTY_SVINSTANTIATIONS, false);
+ }
+ }
+ }
+ }
+ };
+ // LOAD mode: the loaded taclets are only used for the current proof, not for proving
+ // (isOnlyUsedForProvingTaclets=false); the loader works on a copy of the proof's init
+ // config (Swing LemmaGenerationAction.java:295, :331-333)
+ runTacletSoundnessLoader(tacletLoader, proof.getInitConfig(), loadAsLemmata, false,
+ listener);
+ }
+
+ /**
+ * menu: MP5 — the "Load User Defined Taclets for Proving" action (Swing
+ * {@code LemmaGenerationAction.ProveUserDefinedTaclets}, Mode.PROVE): creates proof
+ * obligations for the taclets of a user-chosen file without loading them into the current
+ * proof. The created proofs are registered (they appear in the Loaded Proofs view) and the
+ * first proof is selected, like the Swing doStopped (LemmaGenerationAction.java:231-240).
+ */
+ private void proveUserDefinedTaclets() {
+ Optional result =
+ LoadUserTacletsDialogF.showDialog(stage, LoadUserTacletsDialogF.Mode.PROVE);
+ if (result.isEmpty()) {
+ return;
+ }
+ Path fileForTaclets = result.get().fileForTaclets();
+ boolean loadAsLemmata = result.get().generateProofObligations();
+ // lemma (P2b, A3): the axiom files chosen in the dialog are loaded for the lemmata only
+ List filesForAxioms = result.get().filesForAxioms();
+ final WindowUserInterfaceControlF ui = getUserInterfaceControl();
+ Profile profile = lastEnvironment != null ? lastEnvironment.getProfile()
+ : AbstractProfile.getDefaultProfile();
+ ProblemInitializer problemInitializer =
+ new ProblemInitializer(ui, new Services(profile), ui);
+ TacletLoader tacletLoader = new TacletLoader.TacletFromFileLoader(ui, ui,
+ problemInitializer, profile, fileForTaclets, filesForAxioms);
+
+ LemmaLoaderListener listener = new LemmaLoaderListener() {
+ @Override
+ protected void doStopped(Throwable exception) {
+ handleTacletLoadException(exception);
+ }
+
+ @Override
+ protected void doStopped(@Nullable ProofAggregate p, ImmutableSet taclets,
+ boolean addAxioms) {
+ // menu: getMediator().startInterface(true) is a no-op (see loadUserDefinedTaclets)
+ if (p != null) {
+ ui.registerProofAggregate(p);
+ selectionModel.setSelectedProof(p.getFirstProof());
+ }
+ }
+ };
+ // PROVE mode: only used for proving (isOnlyUsedForProvingTaclets=true), original config
+ // from the fresh proof environment (Swing LemmaGenerationAction.java:244-246)
+ runTacletSoundnessLoader(tacletLoader,
+ tacletLoader.getProofEnvForTaclets().getInitConfigForEnvironment(), loadAsLemmata,
+ true, listener);
+ }
+
+ /**
+ * menu: MP5 — the "Load KeY Taclets" action (Swing {@code LemmaGenerationAction
+ * .ProveKeYTaclets}): creates proof obligations for the system taclets of the profile
+ * (Swing LemmaGenerationAction.java:137-168).
+ */
+ private void proveKeYTaclets() {
+ final WindowUserInterfaceControlF ui = getUserInterfaceControl();
+ Profile profile = lastEnvironment != null ? lastEnvironment.getProfile()
+ : AbstractProfile.getDefaultProfile();
+ TacletLoader tacletLoader = new TacletLoader.KeYsTacletsLoader(ui, ui, profile);
+
+ LemmaLoaderListener listener = new LemmaLoaderListener() {
+ @Override
+ protected void doStopped(Throwable exception) {
+ handleTacletLoadException(exception);
+ }
+
+ @Override
+ protected void doStopped(@Nullable ProofAggregate p, ImmutableSet taclets,
+ boolean addAxioms) {
+ // menu: getMediator().startInterface(true) is a no-op (see loadUserDefinedTaclets)
+ if (p != null) {
+ ui.registerProofAggregate(p);
+ }
+ }
+ };
+ // KeY mode: the system taclets always get proof obligations (Swing passes the literal
+ // true, LemmaGenerationAction.java:163-165). The proof environment of the system taclets
+ // is built off the FX thread — getProofEnvForTaclets loads and prepares the profile's
+ // taclet files and blocked the UI for seconds when run inline (the loader itself already
+ // runs on its own thread; reviewer request: load the KeY taclets asynchronously).
+ Thread loaderThread = new Thread(() -> {
+ InitConfig config = tacletLoader.getProofEnvForTaclets().getInitConfigForEnvironment();
+ runTacletSoundnessLoader(tacletLoader, config, true, true, listener);
+ }, "Load KeY Taclets");
+ loaderThread.start();
+ }
+
+ /**
+ * menu: MP5 — the "Lemma Generation (Batch Mode)" info dialog (Swing
+ * {@code LemmaGenerationBatchModeAction.actionPerformed}: {@code JOptionPane} with the
+ * batch-mode {@link #BATCH_MODE_TEXT} description).
+ */
+ private void showLemmaGenerationBatchMode() {
+ Alert alert = new Alert(Alert.AlertType.INFORMATION);
+ alert.setTitle("Using the Batch Mode for Proving Taclets");
+ alert.setHeaderText(null);
+ TextArea text = new TextArea(BATCH_MODE_TEXT);
+ text.setEditable(false);
+ text.setWrapText(true);
+ alert.getDialogPane().setContent(text);
+ alert.initOwner(stage);
+ ExampleChooserF.themeDialogPane(alert);
+ alert.showAndWait();
+ }
+
+ /**
+ * menu: MP5 — the "Run All Proofs" action (Swing {@code RunAllProofsAction}, behind the
+ * {@code BULK_UI_TEST} feature flag): loads the proof named by the
+ * {@link RunAllProofsF#ENV_VARIABLE} environment variable (or system property) and
+ * auto-proves it; without a usable spec a short usage/status message is shown (the Swing
+ * multi-file batch loop is not ported, see {@link RunAllProofsF}).
+ */
+ private void runAllProofs() {
+ Path proofFile = RunAllProofsF.proofToRun();
+ if (proofFile == null) {
+ popupWarning(RunAllProofsF.usageMessage());
+ return;
+ }
+ LOGGER.info("Run All Proofs: loading and auto-proving {}", proofFile);
+ startProofLoad(proofFile, false, null, null, true);
+ }
+
+ /**
+ * menu: MP5 — shared failure handling of the taclet loaders (Swing
+ * {@code LemmaGenerationAction.handleException}): the exception surfaces in the issue
+ * dialog.
+ */
+ private void handleTacletLoadException(Throwable exception) {
+ LOGGER.error("Taclet loading failed", exception);
+ IssueDialogF.showExceptionDialog(getStage(), exception);
+ }
+
+ /**
+ * menu: MP5 — builds and starts a {@link TacletSoundnessPOLoader} with the shared
+ * "supported taclets only" filter (the default of the not-ported Swing
+ * {@code LemmaSelectionDialog}).
+ *
+ * @param tacletLoader the loader of the candidate taclets
+ * @param originalConfig the init config the proof obligations are based on
+ * @param loadAsLemmata whether proof obligations are generated (the dialog's "Generate proof
+ * obligations for taclets" checkbox, Swing {@code isGenerateProofObligations})
+ * @param isOnlyUsedForProvingTaclets whether the taclets are only used for proving (PROVE/
+ * KeY mode) or also added to the current proof (LOAD mode)
+ * @param listener the FX-side loader listener (stopped callbacks are marshalled to the FX
+ * thread)
+ */
+ private void runTacletSoundnessLoader(TacletLoader tacletLoader, InitConfig originalConfig,
+ boolean loadAsLemmata, boolean isOnlyUsedForProvingTaclets,
+ LemmaLoaderListener listener) {
+ // lemma (P2b, A2): the Swing LemmaSelectionDialog is the taclet filter — the user picks
+ // the taclets the soundness proof obligations are created for (the hardcoded
+ // "supported taclets only" default of the MP5 port is gone); it defaults to the same
+ // behavior while "Show only supported taclets." is active and everything is left on the
+ // choice side
+ TacletSoundnessPOLoader.TacletFilter filter = new LemmaSelectionDialogF();
+ TacletSoundnessPOLoader loader = new TacletSoundnessPOLoader(listener, filter,
+ loadAsLemmata, tacletLoader, originalConfig, isOnlyUsedForProvingTaclets);
+ loader.start();
+ }
+
+ /**
+ * menu: MP5 — the loader listener of the taclet flows (Swing
+ * {@code LemmaGenerationAction.AbstractLoaderListener}): forwards the progress callbacks to
+ * the window's user interface control and marshals the stopped callbacks to the FX thread
+ * (the {@code TacletSoundnessPOLoader} runs on its own thread).
+ */
+ private abstract class LemmaLoaderListener implements TacletSoundnessPOLoader.LoaderListener {
+
+ @Override
+ public void started() {
+ // menu: Swing AbstractLoaderListener.started() calls mediator.stopInterface(true) to
+ // lock the interface; the FX port has no interface lock, so this is a no-op
+ }
+
+ @Override
+ public void progressStarted(Object sender) {
+ getUserInterfaceControl().progressStarted(sender);
+ }
+
+ @Override
+ public void reportStatus(Object sender, String status) {
+ getUserInterfaceControl().reportStatus(sender, status);
+ }
+
+ @Override
+ public void resetStatus(Object sender) {
+ getUserInterfaceControl().resetStatus(sender);
+ }
+
+ @Override
+ public final void stopped(@Nullable ProofAggregate p, ImmutableSet taclets,
+ boolean addAsAxioms) {
+ FxUtil.runLater(() -> doStopped(p, taclets, addAsAxioms));
+ }
+
+ @Override
+ public final void stopped(Throwable exception) {
+ FxUtil.runLater(() -> doStopped(exception));
+ }
+
+ protected abstract void doStopped(@Nullable ProofAggregate p,
+ ImmutableSet taclets, boolean addAsAxioms);
+
+ protected abstract void doStopped(Throwable exception);
+ }
+
+ /**
+ * The Save File action (Swing {@code SaveFileAction} +
+ * {@code WindowUserInterfaceControl.saveProof}): a save dialog pre-filled with the proof's
+ * file (or a sanitized proof name), then {@link ProofSaver} on the FX thread (the Swing
+ * original runs on the EDT as well; the interaction lock prevents concurrent auto mode).
+ */
+ private void saveProofFile() {
+ Proof proof = selectionModel.getSelectedProof();
+ if (proof == null) {
+ return; // the action is disabled without a proof (Swing enableWhenProofLoaded)
+ }
+ if (mediator.isInAutoMode()) {
+ // Swing wraps the save in stopInterface/startInterface: no interaction while saving
+ LOGGER.info("Save requested during auto mode");
+ NotificationManagerF.getInstance()
+ .notify("Cannot save while the automatic prover is running.", Kind.WARNING);
+ return;
+ }
+ FileChooser chooser = new FileChooser();
+ chooser.setTitle("Choose filename to save proof");
+ if (lastSelectedDir != null && Files.isDirectory(lastSelectedDir)) {
+ chooser.setInitialDirectory(lastSelectedDir.toFile());
+ }
+ chooser.setInitialFileName(initialSaveFileName(proof, ".proof"));
+ // Swing KeYFileChooser offers the COMPRESSED_FILTER ("compressed proof files
+ // (.proof.gz)") alongside the default filter
+ chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(
+ "KeY proof files (*.proof)", "*.proof"));
+ chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(
+ "compressed proof files (.proof.gz)", "*.proof.gz"));
+ File file = chooser.showSaveDialog(stage);
+ if (file == null) {
+ return;
+ }
+ if (file.getParentFile() != null) {
+ lastSelectedDir = file.getParentFile().toPath();
+ }
+ Path target = file.toPath().toAbsolutePath();
+ // compression by file name: Swing KeYFileChooser.useCompression() checks the selected
+ // file name for the ".proof.gz" extension and uses the GZipProofSaver in that case
+ // (there is no separate checkbox in the current Swing save dialog)
+ ProofSaver saver;
+ if (target.getFileName().toString().endsWith(".proof.gz")) {
+ saver = new GZipProofSaver(proof, target.toString(), KeYConstants.INTERNAL_VERSION);
+ } else {
+ saver = new ProofSaver(proof, target, KeYConstants.INTERNAL_VERSION);
+ }
+ try {
+ String errorMsg = saver.save();
+ if (errorMsg != null) {
+ LOGGER.error("Saving proof failed: {}", errorMsg);
+ NotificationManagerF.getInstance()
+ .notify("Saving proof failed. Error: " + errorMsg, Kind.ERROR);
+ } else {
+ proof.setProofFile(target);
+ LOGGER.info("Proof saved to {}", target);
+ NotificationManagerF.getInstance()
+ .notify("Proof saved to " + target, Kind.INFO);
+ }
+ } catch (Exception e) {
+ LOGGER.error("Saving proof failed", e);
+ NotificationManagerF.getInstance()
+ .notify("Saving proof failed. Error: " + e.getMessage(), Kind.ERROR);
+ }
+ }
+
+ /**
+ * The initial file name of the save dialog (Swing {@code WindowUserInterfaceControl.fileName}):
+ * the file the proof was loaded from if it already is a {@code .proof} file, otherwise the
+ * sanitized proof name.
+ */
+ private static String initialSaveFileName(Proof proof, String extension) {
+ Path proofFile = proof.getProofFile();
+ if (proofFile != null && proofFile.toString().endsWith(extension)
+ && proofFile.getFileName() != null) {
+ return proofFile.getFileName().toString();
+ }
+ String name = proof.name().toString();
+ for (String suffix : List.of(".key", ".proof")) {
+ if (name.endsWith(suffix)) {
+ name = name.substring(0, name.length() - suffix.length());
+ break;
+ }
+ }
+ return MiscTools.toValidFileName(name) + extension;
+ }
+
+ /**
+ * The Open Example action (Swing {@code OpenExampleAction}): shows the example chooser
+ * (Swing {@code ExampleChooser.showInstance(Main.getExamplesDir())}; the directory comes from
+ * the {@code key.examples.dir} property set by the run task) and loads the chosen file like
+ * any other problem file.
+ *
+ * Swing registers the example in the recent files through {@code loadProblem} — the port
+ * does the same via {@link #openProofFile(Path)}.
+ */
+ private void openExampleChooser() {
+ Path file = ExampleChooserF.showInstance(null, stage);
+ if (file != null) {
+ openProofFile(file);
+ }
+ }
+
+ /**
+ * The Save Bundle action (Swing {@code SaveBundleAction} +
+ * {@code WindowUserInterfaceControl.saveProofBundle}): a save dialog with the bundle filter
+ * and the sanitized proof name + ".zproof", then the core {@link ProofBundleSaver} (which
+ * packs the proof and all dependencies into a zip archive via the proof's FileRepo) on the
+ * FX thread (the Swing original runs on the EDT as well).
+ */
+ private void saveProofBundle() {
+ Proof proof = selectionModel.getSelectedProof();
+ if (proof == null) {
+ return; // the action is disabled without a proof (Swing updateStatus)
+ }
+ FileChooser chooser = new FileChooser();
+ chooser.setTitle("Choose filename to save proof");
+ if (lastSelectedDir != null && Files.isDirectory(lastSelectedDir)) {
+ chooser.setInitialDirectory(lastSelectedDir.toFile());
+ }
+ // Swing KeYFileChooser.PROOF_BUNDLE_FILTER
+ chooser.getExtensionFilters()
+ .add(new FileChooser.ExtensionFilter("proof bundles (.zproof)", "*.zproof"));
+ chooser.setInitialFileName(initialSaveFileName(proof, ".zproof"));
+ File file = chooser.showSaveDialog(stage);
+ if (file == null) {
+ return;
+ }
+ if (file.getParentFile() != null) {
+ lastSelectedDir = file.getParentFile().toPath();
+ }
+ Path target = file.toPath().toAbsolutePath();
+ ProofBundleSaver saver = new ProofBundleSaver(proof, target);
+ try {
+ String errorMsg = saver.save();
+ if (errorMsg != null) {
+ LOGGER.error("Saving proof bundle failed: {}", errorMsg);
+ NotificationManagerF.getInstance()
+ .notify("Saving Proof failed. Error: " + errorMsg, Kind.ERROR);
+ } else {
+ proof.setProofFile(target);
+ LOGGER.info("Proof bundle saved to {}", target);
+ NotificationManagerF.getInstance()
+ .notify("Proof bundle saved to " + target, Kind.INFO);
+ }
+ } catch (Exception e) {
+ LOGGER.error("Saving proof bundle failed", e);
+ NotificationManagerF.getInstance()
+ .notify("Saving Proof failed. Error: " + e.getMessage(), Kind.ERROR);
+ }
+ }
+
+ /**
+ * The Quick Save action (Swing {@code QuickSaveAction}, F5): saves the selected proof to the
+ * temporary quick save location ({@link QuickSaveF#QUICK_SAVE_PATH}).
+ */
+ private void quickSave() {
+ QuickSaveF.quickSave(this);
+ }
+
+ /**
+ * The Quick Load action (Swing {@code QuickLoadAction}, F6): loads the quick save location.
+ */
+ private void quickLoad() {
+ QuickSaveF.quickLoad(this);
+ }
+
+ /**
+ * Shows a warning toast (the JavaFX counter-part of Swing {@code MainWindow.popupWarning},
+ * which opens a modal message dialog; the JavaFX UI reports via toasts).
+ *
+ * @param message the warning message
+ */
+ public void popupWarning(String message) {
+ NotificationManagerF.getInstance().notify(message, Kind.WARNING);
+ }
+
+ /**
+ * Sets the left status line text (Swing {@code MainWindow.setStatusLine}).
+ *
+ * @param status the status message
+ */
+ public void setStatusLine(String status) {
+ statusLeft.setText(status);
+ }
+
+ /**
+ * D37 (P3c): sets the status line text and shows the progress bar for a task with the given
+ * workload (Swing {@code MainWindow.setStatusLine(String, int)} →
+ * {@code MainStatusLine.setProgressBarMaximum}: a negative maximum switches the bar to
+ * indeterminate ("busy") mode, a positive maximum to determinate mode with that range, a
+ * maximum of {@code 0} hides the bar).
+ *
+ * @param status the status message
+ * @param max the workload maximum, or a negative value for an unknown workload
+ */
+ public void setStatusLine(String status, int max) {
+ setStatusLine(status);
+ setTaskProgressBarMaximum(max);
+ }
+
+ /**
+ * D37 (P3c): sets the range of the status progress bar (Swing
+ * {@code MainStatusLine.setProgressBarMaximum}): {@code < 0} switches the bar to
+ * indeterminate ("busy") mode ({@code JProgressBar.setIndeterminate(true)}), {@code 0} hides
+ * it, {@code > 0} shows it in determinate mode with the given maximum
+ * ({@code setMaximum(value)}).
+ *
+ * @param maximum the workload maximum, or a negative value for an unknown workload
+ */
+ public void setTaskProgressBarMaximum(int maximum) {
+ if (maximum < 0) {
+ statusProgressMax = -1;
+ statusProgress.setProgress(ProgressBar.INDETERMINATE_PROGRESS);
+ statusProgress.setVisible(true);
+ statusProgress.setManaged(true);
+ } else {
+ statusProgressMax = maximum;
+ // leave the bar in a clean determinate state (Swing
+ // setProgressPanelVisible(false) resets setIndeterminate(false))
+ statusProgress.setProgress(0);
+ boolean visible = maximum != 0;
+ statusProgress.setVisible(visible);
+ statusProgress.setManaged(visible);
+ }
+ }
+
+ /**
+ * D37 (P3c): sets the current position of the status progress bar (Swing
+ * {@code MainStatusLine.setProgress} → {@code progressBar.setValue}). Values of a hidden or
+ * indeterminate bar are ignored, like the Swing {@code JProgressBar}.
+ *
+ * @param value the finished work units within the range set by
+ * {@link #setTaskProgressBarMaximum(int)}
+ */
+ public void setTaskProgressValue(int value) {
+ if (statusProgressMax <= 0) {
+ return; // hidden or indeterminate bar
+ }
+ statusProgress.setProgress(Math.clamp((double) value / statusProgressMax, 0.0, 1.0));
+ }
+
+ /**
+ * D37 (P3c): hides the status progress bar (Swing {@code MainStatusLine.reset()}: hide the
+ * progress panel at task/macro end).
+ */
+ public void hideStatusProgress() {
+ setTaskProgressBarMaximum(0);
+ }
+
+ /**
+ * seam: resets the status line to the proof summary (Swing
+ * {@code MainWindow.setStandardStatusLine}); called by
+ * {@link #userInterface} on {@code resetStatus}.
+ */
+ public void resetStatusLine() {
+ // D37: a status reset also hides the progress bar (Swing setStandardStatusLine →
+ // MainStatusLine.reset() hides the progress panel)
+ hideStatusProgress();
+ updateProofStatus();
+ }
+
+ /**
+ * seam: current left status text (used by the {@code key.fx.verify.uicontrol} self test).
+ */
+ public String getStatusLineText() {
+ return statusLeft.getText();
+ }
+
+ /**
+ * D37 seam: whether the status progress bar is currently visible (used by the
+ * {@code key.fx.verify.uicontrol} self test).
+ */
+ boolean isStatusProgressVisible() {
+ return statusProgress.isVisible();
+ }
+
+ /**
+ * D37 seam: the current status progress bar value ({@link ProgressBar#INDETERMINATE_PROGRESS}
+ * in indeterminate mode; used by the {@code key.fx.verify.uicontrol} self test).
+ */
+ double getStatusProgressValue() {
+ return statusProgress.getProgress();
+ }
+
+ /**
+ * Rebuilds the Recent Files submenu from the store (invoked after every store change): the
+ * entries show their short unique file names (Swing {@code ShortUniqueFileNames}) plus the
+ * profile suffix for entries loaded with a non-default profile.
+ */
+ private void updateRecentFilesMenu() {
+ if (!FxUtil.isFxThread()) {
+ FxUtil.runLater(this::updateRecentFilesMenu);
+ return;
+ }
+ List entries = recentFiles.getEntries();
+ List names = RecentFilesF
+ .uniqueNames(entries.stream().map(RecentFilesF.Entry::path).toList());
+ recentFilesMenu.getItems().clear();
+ for (int i = 0; i < entries.size(); i++) {
+ RecentFilesF.Entry entry = entries.get(i);
+ String name = names.get(i);
+ // Swing's RecentFileAction labels entries loaded with a non-default profile
+ String text = entry.profile() != null ? name + " (Profile: " + entry.profile() + ")"
+ : name;
+ // loadingexit (P1): restore the entry's profile / single-java loading options on
+ // open (Swing RecentFileAction.actionPerformed, RecentFileMenu.java:386-427)
+ recentFilesMenu.getItems()
+ .add(menuItem(text, () -> openRecentFile(entry)));
+ }
+ // P3b/B18: Swing leaves the empty recent-files submenu ENABLED (it just shows nothing):
+ // RecentFileMenu's constructor does not disable it (the effect of the commented-out line
+ // RecentFileMenu.java:78) and setEnabled(getItemCount() != 0) in addRecentFileNoSave
+ // (:144) only ever runs after an entry was inserted. Do the same here — no setDisable.
+ }
+
+ /**
+ * loadingexit (P1) — opens a recent-files entry with its stored loading options (Swing
+ * {@code RecentFileAction.actionPerformed}, RecentFileMenu.java:386-427): proof bundles show
+ * the proof selection dialog; other files resolve the stored profile ident through the
+ * {@code DefaultProfileResolver} services (a missing profile warns like the Swing
+ * {@code JOptionPane}) and load with the resolved profile, its additional profile options
+ * and the single-java flag.
+ */
+ private void openRecentFile(RecentFilesF.Entry entry) {
+ Path file = Path.of(entry.path());
+
+ // special case proof bundles -> allow to select the proof to load
+ if (ProofSelectionDialogF.isProofBundle(file)) {
+ Path proofPath = ProofSelectionDialogF.chooseProofToLoad(file, stage);
+ if (proofPath == null) {
+ return; // canceled by user!
+ }
+ recentFiles.add(file.toAbsolutePath().toString(), null, false, null);
+ startProofLoad(file, false, proofPath);
+ return;
+ }
+
+ String profileName = entry.profile();
+ // A missing profile -- null, or the literal string "null" that older recent-file
+ // entries stored for it -- means "use the default profile", not an error.
+ boolean hasProfile = profileName != null && !profileName.equals("null");
+ Profile profile = null;
+ if (hasProfile) {
+ profile = ServiceLoader.load(DefaultProfileResolver.class).stream()
+ .filter(it -> it.get().getProfileName().equals(profileName)).findFirst()
+ .map(it -> it.get().getDefaultProfile()).orElse(null);
+ if (profile == null) {
+ Alert alert = new Alert(AlertType.WARNING,
+ "Could not find previous selected profile %s.".formatted(profileName),
+ ButtonType.OK);
+ alert.setTitle("Recent File");
+ alert.setHeaderText(null);
+ alert.initOwner(stage);
+ alert.showAndWait();
+ return;
+ }
+ }
+ LoadOptions options = hasProfile || entry.singleJava()
+ ? new LoadOptions(profile, entry.additionalOption(), entry.singleJava())
+ : null;
+ openProofFile(file, options);
+ }
+
+ /**
+ * Milestone M2 verification affordance: runs the automatic prover after the proof was
+ * bound to the selection model ({@code key.fx.demo.autoprove.live}). During the run the
+ * parallel prover suspends the proof's non-essential tree listeners ({@code
+ * Proof#suspendNonEssentialListeners}), so no per-application events reach the views; the
+ * final state is delivered via {@code autoModeStopped} — the same contract the Swing UI
+ * implements ({@code MainWindow.autoModeStopped}). The structure self test afterwards proves
+ * the rebuilt tree matches the final proof state. {@code key.fx.demo.maxsteps} optionally
+ * limits the strategy steps (tests the non-closed case).
+ */
+ private void startLiveAutoMode(KeYEnvironment env) {
+ Proof proof = env.getLoadedProof();
+ String maxSteps = System.getProperty("key.fx.demo.maxsteps");
+ if (maxSteps != null && !maxSteps.isBlank()) {
+ proof.getSettings().getStrategySettings()
+ .setMaxSteps(Integer.parseInt(maxSteps.trim()));
+ }
+ env.getProofControl().addAutoModeListener(new AutoModeListener() {
+ @Override
+ public void autoModeStarted(ProofEvent e) {
+ LOGGER.info("Demo: live auto mode started");
+ }
+
+ @Override
+ public void autoModeStopped(ProofEvent e) {
+ // the parallel prover suspends the non-essential tree listeners for the whole
+ // run, so the final tree state arrives only here (Swing parity:
+ // MainWindow.autoModeStopped refreshes the views from the final state)
+ FxUtil.runLater(() -> {
+ refreshViewsFromFinalState();
+ if (System.getProperty("key.fx.verify.search") != null) {
+ runSearchVerification();
+ }
+ if (System.getProperty("key.fx.verify.sequentsearch") != null) {
+ runSequentSearchVerification();
+ }
+ if (System.getProperty("key.fx.verify.sequentsearchmodes") != null) {
+ runSequentSearchModesVerification();
+ }
+ if (System.getProperty("key.fx.verify.updatehighlight") != null) {
+ runUpdateHighlightVerification();
+ }
+ if (System.getProperty("key.fx.verify.treefilters") != null) {
+ String report = proofTreeView.verifyTreeFilters();
+ LOGGER.info("Proof tree filter verification: {}", report);
+ NotificationManagerF.getInstance()
+ .notify("Tree filter verification: " + report,
+ report.endsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ }
+ if (System.getProperty("key.fx.verify.prooftree") != null) {
+ // prooftree (P3a): re-run the C19-C23/C25/C27 self test after a live
+ // auto mode — the tree listeners already applied the C27 partial subtree
+ // updates on the auto mode stop, so the counter report shows them
+ String report = (String) runVerifyHarness(
+ "de.uka.ilkd.key.gui.fx.prooftree.ProofTreeVerifyF", "run",
+ new Class>[] { javafx.stage.Window.class, Proof.class,
+ ProofTreeViewF.class },
+ stage, selectionModel.getSelectedProof(), proofTreeView);
+ LOGGER.info("Proof tree verification (after live auto mode): {}", report);
+ }
+ String report = proofTreeView.verifyTreeStructure() + " "
+ + proofTreeView.getLiveUpdateReport();
+ LOGGER.info("Proof tree live update verification: {}", report);
+ NotificationManagerF.getInstance()
+ .notify("Live update verification: " + report,
+ report.endsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ });
+ }
+ });
+ Thread worker = new Thread(() -> {
+ LOGGER.info("Demo: starting live auto mode on the selected proof");
+ env.getProofControl().startAndWaitForAutoMode(proof);
+ LOGGER.info("Demo: live auto mode finished");
+ }, "fx-demo-live-autoprover");
+ worker.setDaemon(true);
+ worker.start();
+ }
+
+ /**
+ * Runs the sequent search self test with the query given as the value of
+ * {@code key.fx.verify.sequentsearch} (default {@code agatha}, which occurs in the Agatha
+ * demo's sequent).
+ */
+ private void runSequentSearchVerification() {
+ String query = System.getProperty("key.fx.verify.sequentsearch");
+ String report = sequentView
+ .verifySequentSearch(query == null || query.isBlank() ? "agatha" : query.trim());
+ LOGGER.info("Sequent search verification: {}", report);
+ NotificationManagerF.getInstance()
+ .notify("Sequent search verification: " + report,
+ report.endsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ }
+
+ /**
+ * shortcuts (P1): runs the shortcut verification ({@code key.fx.verify.shortcuts}): the
+ * re-mapped defaults match the Swing table (KeyStrokeSettings.java:60-106), no two
+ * registered actions share a keystroke (the old FX defaults collided: OneStep/AutoMode both
+ * Ctrl+SPACE, TryClose/Copy both Ctrl+C), an explicit user override still wins over the
+ * defaults (bind round trip, restored), and the sequent view's Ctrl+F handler shows the
+ * search bar.
+ */
+ private void runShortcutsVerification() {
+ ArrayList failures = new ArrayList<>();
+ KeyStrokeManagerF manager = KeyStrokeManagerF.getInstance();
+ // compared in the shared Swing spec format (the effective end state: defaults merged
+ // with the persisted keystrokes.json, which is shared with the Swing UI)
+ String[][] expected = {
+ { "de.uka.ilkd.key.macros.FullAutoPilotProofMacro", "shift ctrl pressed V" },
+ { "de.uka.ilkd.key.macros.OneStepProofMacro", "shift ctrl pressed SPACE" },
+ { "de.uka.ilkd.key.macros.TryCloseMacro", "shift ctrl pressed C" },
+ { "de.uka.ilkd.key.gui.actions.SearchInProofTreeAction", "shift ctrl pressed F" },
+ { "de.uka.ilkd.key.gui.actions.SearchInSequentAction", "ctrl pressed F" },
+ { "de.uka.ilkd.key.gui.actions.SearchNextAction", "pressed F3" },
+ { "de.uka.ilkd.key.gui.actions.SearchPreviousAction", "shift pressed F3" },
+ { "de.uka.ilkd.key.gui.actions.CopyToClipboardAction", "ctrl pressed C" },
+ { "de.uka.ilkd.key.gui.actions.GoalSelectAboveAction", "ctrl pressed K" },
+ { "de.uka.ilkd.key.gui.actions.GoalSelectBelowAction", "ctrl pressed J" },
+ { "de.uka.ilkd.key.gui.actions.GoalBackAction", "ctrl pressed Z" },
+ { "de.uka.ilkd.key.gui.actions.PruneProofAction", "ctrl pressed DELETE" },
+ { "de.uka.ilkd.key.gui.actions.AutoModeAction", "ctrl pressed SPACE" },
+ { "de.uka.ilkd.key.gui.actions.PrettyPrintToggleAction", "shift ctrl pressed P" } };
+ Map snapshot = manager.snapshot();
+ for (String[] entry : expected) {
+ String actual = snapshot.get(entry[0]);
+ if (!entry[1].equals(actual)) {
+ failures.add(entry[0] + ": " + actual + " != " + entry[1]);
+ }
+ }
+ // no two registered actions may share one keystroke (the old FX defaults collided:
+ // OneStep/AutoMode both Ctrl+SPACE, TryClose/Copy both Ctrl+C)
+ HashSet seen = new HashSet<>();
+ for (String spec : snapshot.values()) {
+ if (!seen.add(spec)) {
+ failures.add("duplicate binding: " + spec);
+ }
+ }
+ // an explicit user override still wins over the defaults (bind round trip, restored)
+ KeyCombination original =
+ manager.binding("de.uka.ilkd.key.gui.actions.CopyToClipboardAction")
+ .orElseThrow();
+ manager.bind("de.uka.ilkd.key.gui.actions.CopyToClipboardAction",
+ KeyCombination.valueOf("Shortcut+Shift+C"));
+ if (!"shift ctrl pressed C".equals(manager.snapshot()
+ .get("de.uka.ilkd.key.gui.actions.CopyToClipboardAction"))) {
+ failures.add("override round trip: bind() did not take effect");
+ }
+ manager.bind("de.uka.ilkd.key.gui.actions.CopyToClipboardAction", original);
+ // sequent view key path: Ctrl+F shows the search bar (SearchInSequentAction)
+ sequentView.fireEvent(new KeyEvent(KeyEvent.KEY_PRESSED, "", "f", KeyCode.F, false, true,
+ false, false));
+ if (!sequentView.isSearchBarShowing()) {
+ failures.add("Ctrl+F did not show the sequent search bar");
+ }
+ String report = failures.isEmpty()
+ ? "PASS (defaults, no collisions, override round trip, sequent key path)"
+ : "FAIL: " + String.join("; ", failures);
+ LOGGER.info("Shortcuts verification: {}", report);
+ NotificationManagerF.getInstance()
+ .notify("Shortcuts verification: " + report,
+ report.startsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ statusRight.setText(report);
+ }
+
+ /**
+ * inputfreeze (P1): runs the input-freeze verification ({@code key.fx.verify.inputfreeze}):
+ * (1) the deterministic freeze/unfreeze — the blocking overlay shows, a fired Ctrl+F key
+ * event is blocked while frozen and shows the search bar after the unfreeze (input
+ * restored); (2) the listener-driven path — an auto mode run started via the mediator
+ * freezes the views and the stop (requested or natural) unfreezes them.
+ */
+ private void runInputFreezeVerification() {
+ ArrayList failures = new ArrayList<>();
+ // 1. deterministic freeze/unfreeze (the mechanism the auto mode listener drives)
+ freezeExceptAutoModeButton();
+ if (!inputBlocker.isVisible()) {
+ failures.add("freezeExceptAutoModeButton: overlay not visible");
+ } else {
+ sequentView.fireEvent(new KeyEvent(KeyEvent.KEY_PRESSED, "", "f", KeyCode.F, false,
+ true, false, false));
+ if (sequentView.isSearchBarShowing()) {
+ failures.add("key input not blocked while frozen (Ctrl+F showed the search bar)");
+ sequentView.fireEvent(new KeyEvent(KeyEvent.KEY_PRESSED, "", "",
+ KeyCode.ESCAPE, false, false, false, false));
+ }
+ }
+ unfreezeExceptAutoModeButton();
+ if (inputBlocker.isVisible()) {
+ failures.add("unfreezeExceptAutoModeButton: overlay still visible");
+ }
+ sequentView.fireEvent(new KeyEvent(KeyEvent.KEY_PRESSED, "", "f", KeyCode.F, false, true,
+ false, false));
+ if (!sequentView.isSearchBarShowing()) {
+ failures.add("key input not restored after the unfreeze");
+ } else {
+ // close the search bar for a clean state
+ sequentView.fireEvent(new KeyEvent(KeyEvent.KEY_PRESSED, "", "", KeyCode.ESCAPE,
+ false, false, false, false));
+ }
+ // 2. the listener-driven path: an auto mode run must freeze the views (polled on the FX
+ // thread; the auto mode run may finish on its own before the stop is requested)
+ mediator.startAutoMode();
+ Boolean[] sawFrozen = { false };
+ Integer[] ticks = { 0 };
+ Timeline[] holder = new Timeline[1];
+ holder[0] = new Timeline(new KeyFrame(Duration.millis(100), event -> {
+ if (inputBlocker.isVisible()) {
+ sawFrozen[0] = true;
+ }
+ boolean running = mediator.autoModeRunningProperty().get();
+ if (running && ++ticks[0] < 120) {
+ return; // keep polling (max ~12 s), then request the stop
+ }
+ holder[0].stop();
+ if (running) {
+ mediator.stopAutoMode();
+ Timeline post = new Timeline(new KeyFrame(Duration.millis(500), ev -> {
+ if (inputBlocker.isVisible()) {
+ failures.add("the overlay stayed visible after stopAutoMode");
+ }
+ reportInputFreeze(failures, sawFrozen[0]);
+ }));
+ post.play();
+ return;
+ }
+ // the run finished on its own: the overlay must be hidden again
+ if (sawFrozen[0] && !inputBlocker.isVisible()) {
+ LOGGER.info("inputfreeze: auto-mode-driven freeze/unfreeze observed");
+ } else if (!sawFrozen[0]) {
+ LOGGER.info(
+ "inputfreeze: the auto mode run ended before the freeze could be observed (the listener wiring is covered by the direct test)");
+ } else {
+ failures.add("the overlay stayed visible after the auto mode stop");
+ }
+ reportInputFreeze(failures, sawFrozen[0]);
+ }));
+ holder[0].setCycleCount(Timeline.INDEFINITE);
+ holder[0].play();
+ }
+
+ /** inputfreeze (P1): logs and reports the freeze verification result. */
+ private void reportInputFreeze(ArrayList failures, boolean sawFrozen) {
+ String report = failures.isEmpty()
+ ? "PASS (freeze/unfreeze" + (sawFrozen ? " + auto-mode-driven freeze)" : ")")
+ : "FAIL: " + String.join("; ", failures);
+ LOGGER.info("Input freeze verification: {}", report);
+ NotificationManagerF.getInstance()
+ .notify("Input freeze verification: " + report,
+ report.startsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ statusRight.setText(report);
+ }
+
+ /**
+ * Runs the search mode self test (Highlight/Hide/Regroup) with the query given as the value
+ * of {@code key.fx.verify.sequentsearchmodes} (like the other search verifications the VALUE
+ * is the query itself; the flag value {@code 1} falls back to the
+ * {@code key.fx.verify.sequentsearch} query, default {@code agatha}); restores the plain
+ * view.
+ */
+ private void runSequentSearchModesVerification() {
+ String value = System.getProperty("key.fx.verify.sequentsearchmodes");
+ String query;
+ if (value == null || value.isBlank() || "1".equals(value.trim())) {
+ String searchQuery = System.getProperty("key.fx.verify.sequentsearch");
+ query = searchQuery == null || searchQuery.isBlank() ? "agatha" : searchQuery.trim();
+ } else {
+ query = value.trim();
+ }
+ String report = sequentView.verifySearchModes(query);
+ LOGGER.info("Sequent search modes verification: {}", report);
+ NotificationManagerF.getInstance()
+ .notify("Sequent search modes verification: " + report,
+ report.endsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ }
+
+ /**
+ * Runs the update-highlight overlay self test (needs a sequent that prints update operators,
+ * e.g. the normalisation11.key demo).
+ */
+ private void runUpdateHighlightVerification() {
+ String report = sequentView.verifyUpdateHighlights();
+ LOGGER.info("Update highlight verification: {}", report);
+ NotificationManagerF.getInstance()
+ .notify("Update highlight verification: " + report,
+ report.endsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ }
+
+ /**
+ * isabelle (WP-8): runs the Isabelle solver-launch DRY-RUN self test
+ * ({@code key.fx.verify.isabelle}, run after the demo load like the other proof-dependent
+ * verify hooks). The harness lives in the {@code keyext.isabelletranslation.fx} extension
+ * module, which is not a compile dependency of key.ui.fx (the dependency direction is
+ * reversed), so it is invoked reflectively; the report string ends in {@code PASS} or
+ * {@code FAIL}.
+ */
+ private static String runIsabelleVerification(javafx.stage.Window owner, Proof proof) {
+ try {
+ Class> verify = Class.forName(
+ "org.key_project.isabelletranslation.fx.IsabelleVerifyF");
+ Object report = verify.getMethod("run", javafx.stage.Window.class, Proof.class)
+ .invoke(null, owner, proof);
+ return (String) report;
+ } catch (ClassNotFoundException e) {
+ return "Isabelle solver-launch verification SKIP (keyext.isabelletranslation.fx not "
+ + "on the classpath)";
+ } catch (ReflectiveOperationException | RuntimeException e) {
+ LOGGER.error("Isabelle solver-launch verification failed", e);
+ return "Isabelle solver-launch verification FAIL (" + e + ")";
+ }
+ }
+
+ /**
+ * caching (WP-4): runs the proof-caching reference-search self test
+ * ({@code key.fx.verify.caching}, run after the demo load like the other proof-dependent
+ * verify hooks). The harness lives in the {@code keyext.caching.fx} extension module, which
+ * is not a compile dependency of key.ui.fx (the dependency direction is reversed), so it is
+ * invoked reflectively; the report string ends in {@code PASS} or {@code FAIL}.
+ */
+ private static String runCachingVerification(javafx.stage.Window owner, Proof proof,
+ KeYMediatorF mediator) {
+ try {
+ Class> verify =
+ Class.forName("de.uka.ilkd.key.gui.plugins.caching.fx.CachingVerifyF");
+ Object report = verify
+ .getMethod("run", javafx.stage.Window.class, Proof.class, KeYMediatorF.class)
+ .invoke(null, owner, proof, mediator);
+ return (String) report;
+ } catch (ClassNotFoundException e) {
+ return "Proof caching verification SKIP (keyext.caching.fx not on the classpath)";
+ } catch (ReflectiveOperationException | RuntimeException e) {
+ LOGGER.error("Proof caching verification failed", e);
+ return "Proof caching verification FAIL (" + e + ")";
+ }
+ }
+
+ /**
+ * /**
+ * slicing (WP-6): dispatches the proof-slicing self test ({@code key.fx.verify.slicing}) to
+ * the {@code SlicingVerifyF} harness of the {@code keyext.slicing.fx} extension module,
+ * which is not a compile dependency of key.ui.fx (the dependency direction is reversed) —
+ * the same reflective pattern as {@link #runIsabelleVerification}. The harness runs its long
+ * work (auto mode, fixed-point slicing) on a background thread and logs its own final
+ * {@code key.fx.verify.slicing=PASS|FAIL} marker line.
+ */
+ private void runSlicingVerification(Proof proof) {
+ try {
+ Class> verify = Class.forName("org.key_project.slicing.fx.SlicingVerifyF");
+ verify.getMethod("run", MainWindowF.class, Proof.class).invoke(null, this, proof);
+ LOGGER.info("Slicing verification started (final marker on the background thread)");
+ } catch (ClassNotFoundException e) {
+ LOGGER.info("Slicing verification SKIP (keyext.slicing.fx not on the classpath)");
+ } catch (ReflectiveOperationException | RuntimeException e) {
+ LOGGER.error("Slicing verification dispatch failed", e);
+ }
+ }
+
+ /**
+ * testgen (WP-7): runs the TestGen extension self test ({@code key.fx.verify.testgen}, run
+ * after the demo load like the other proof-dependent verify hooks). The harness lives in the
+ * {@code keyext.ui.testgen.fx} extension module, which is not a compile dependency of
+ * key.ui.fx (the dependency direction is reversed), so it is invoked reflectively; the report
+ * string ends in {@code PASS} or {@code FAIL}.
+ */
+ private static String runTestgenVerification(javafx.stage.Window owner, Proof proof,
+ KeYMediatorF mediator) {
+ try {
+ Class> verify = Class.forName("de.uka.ilkd.key.gui.testgen.fx.TestgenVerifyF");
+ Object report = verify
+ .getMethod("run", javafx.stage.Window.class, Proof.class, KeYMediatorF.class)
+ .invoke(null, owner, proof, mediator);
+ return (String) report;
+ } catch (ClassNotFoundException e) {
+ return "TestGen verification SKIP (keyext.ui.testgen.fx not on the classpath)";
+ } catch (ReflectiveOperationException | RuntimeException e) {
+ LOGGER.error("TestGen verification failed", e);
+ return "TestGen verification FAIL (" + e + ")";
+ }
+ }
+
+ /**
+ * smt (P1): runs the FX SMT run UI end to end ({@code key.fx.verify.smt}), run after the
+ * demo load like the other proof-dependent verify hooks: launches the first usable solver
+ * union on the first open goal with the auto-applying {@code ProgressMode.CLOSE} (the
+ * {@link SolverListenerF} closes the goal via the SMT rule on completion) and checks the
+ * solver result and the closed goal. The waiting happens on a background thread (the hook
+ * itself is invoked on the FX thread; the modal progress dialog stays interactive).
+ */
+ private void runSmtVerification() {
+ Thread thread = new Thread(this::runSmtVerificationAsync, "SMTVerify");
+ thread.setDaemon(true);
+ thread.start();
+ }
+
+ private void runSmtVerificationAsync() {
+ String report;
+ Proof proof = mediator.getSelectedProof();
+ ProofIndependentSMTSettings smtSettings =
+ ProofIndependentSettings.DEFAULT_INSTANCE.getSMTSettings();
+ Collection unions = smtSettings.getUsableSolverUnions();
+ if (proof == null || proof.closed() || unions.isEmpty()) {
+ report = "FAIL: no open proof or no usable solver union (unions=" + unions.size()
+ + ")";
+ } else {
+ Goal goal = proof.openGoals().iterator().next();
+ SolverTypeCollection union = unions.iterator().next();
+ // the CLOSE progress mode auto-applies the results on completion (and closes the
+ // goals); snapshot + restore so the user's setting is not persisted
+ ProgressMode previousMode = smtSettings.getModeOfProgressDialog();
+ smtSettings.setModeOfProgressDialog(ProgressMode.CLOSE);
+ // WP-7: the auto-apply runs through the undoable SMTProofApplyUserActionF; record
+ // the fired user actions and the auto-mode freeze around the rule application
+ List actions = Collections.synchronizedList(new ArrayList<>());
+ List autoModeEvents = Collections.synchronizedList(new ArrayList<>());
+ mediator.addUserActionListener(actions::add);
+ // the apply+undo round trip needs closed goals to be tracked: core maintains the
+ // closedGoals list only while GeneralSettings.noPruningClosed is false (closeGoal,
+ // Proof.java) — and the undo prunes closed branches only in that mode. Toggled for
+ // the whole verify (like the WP-4/WP-6 harnesses, cf. CachingVerifyF), restored
+ // after the undo assert below.
+ boolean oldNoPruningClosed = GeneralSettings.noPruningClosed;
+ GeneralSettings.noPruningClosed = false;
+ Platform.runLater(() -> mediator.autoModeRunningProperty()
+ .addListener((obs, o, v) -> autoModeEvents.add(v)));
+ SMTProblem problem = new SMTProblem(goal);
+ SolverListenerF.launch(mediator, getStage(), proof, List.of(problem),
+ union.getTypes());
+ // SolverLauncher.launch blocks its background thread until every solver finished
+ // and the CLOSE auto-apply is posted afterwards (FIFO on the FX thread): poll for
+ // the closed goal (the solvers are quick on the demo problem)
+ long deadline = System.currentTimeMillis() + 120_000;
+ while (System.currentTimeMillis() < deadline && !goal.node().isClosed()) {
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ }
+ boolean closed = goal.node().isClosed();
+ if (!closed) {
+ // discard the still open modal dialog so the verify run does not stall
+ Platform.runLater(SolverListenerF::discardCurrentDialog);
+ }
+ // WP-7 undo assert: the recorded action (name/canUndo), the freeze flag around the
+ // rule application and the undo (the goal is pruned back open)
+ String undoReport = "";
+ UserActionF recorded;
+ synchronized (actions) {
+ recorded = actions.isEmpty() ? null : actions.get(0);
+ }
+ if (recorded != null) {
+ String name = recorded.name();
+ boolean nameOk = name.startsWith("Close: ") && name.endsWith(" goals by SMT");
+ boolean canUndo = recorded.canUndo();
+ boolean freezeSeen =
+ autoModeEvents.contains(Boolean.TRUE) && autoModeEvents.contains(Boolean.FALSE);
+ CountDownLatch undone = new CountDownLatch(1);
+ Platform.runLater(() -> {
+ try {
+ recorded.undo();
+ } catch (RuntimeException e) {
+ LOGGER.error("SMT verification undo failed", e);
+ } catch (AssertionError e) {
+ LOGGER.error("SMT verification undo failed (assertion)", e);
+ } finally {
+ undone.countDown();
+ }
+ });
+ boolean undoneInTime = false;
+ try {
+ undoneInTime = undone.await(30, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ boolean reopened = undoneInTime && !goal.node().isClosed();
+ boolean undoOk = nameOk && canUndo && freezeSeen && reopened;
+ undoReport = (undoOk ? "PASS" : "FAIL") + " (name=" + name + ", canUndo=" + canUndo
+ + ", freeze=" + freezeSeen + ", undone=" + undoneInTime + ", reopened="
+ + reopened + ")";
+ GeneralSettings.noPruningClosed = oldNoPruningClosed;
+ }
+ report = (closed ? "PASS" : "FAIL") + " (union=" + union + ", goalClosed=" + closed
+ + ")";
+ if (!undoReport.isEmpty()) {
+ report = report + "; apply/undo: " + undoReport;
+ }
+ // restore the user's progress mode after the auto-apply settled
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ smtSettings.setModeOfProgressDialog(previousMode);
+ }
+ String finalReport = report;
+ Platform.runLater(() -> {
+ LOGGER.info("SMT verification: {}", finalReport);
+ NotificationManagerF.getInstance()
+ .notify("SMT verification: " + finalReport,
+ finalReport.contains("PASS") ? Kind.INFO : Kind.ERROR);
+ });
+ }
+
+ /**
+ * loadingexit (P1): runs the loading/exit verification ({@code key.fx.verify.loadingexit}),
+ * run after the demo load like the other proof-dependent verify hooks: (1) the recent-files
+ * store round trip — an entry registered with loading options (profile ident, single-java
+ * flag) survives the save/load cycle like Swing's recent-files entries, plus the
+ * profile-ident resolution used when opening a recent file — both with a snapshot/restore so
+ * the shared {@code recentFiles_v2.json} is not polluted; (2) the exit flow — the Confirm
+ * Exit setting is switched off (snapshot/restore) and the window close button path is fired,
+ * which must terminate the process with exit code 0.
+ */
+ private void runLoadingExitVerification() {
+ String report;
+ List snapshot = recentFiles.getEntries();
+ try {
+ // the round trip uses a temp file so an existing demo entry is not reordered
+ Path temp = Files.createTempFile("loadingexit-verify", ".key");
+ Profile profile = ServiceLoader.load(DefaultProfileResolver.class).stream()
+ .map(resolver -> resolver.get().getDefaultProfile()).findFirst().orElse(null);
+ if (profile == null) {
+ report = "FAIL: no DefaultProfileResolver service";
+ } else {
+ recentFiles.add(temp.toAbsolutePath().toString(), profile.ident(), true, null);
+ recentFiles.load();
+ RecentFilesF.Entry entry = recentFiles.getEntries().isEmpty() ? null
+ : recentFiles.getEntries().getFirst();
+ boolean stored = entry != null
+ && entry.path().equals(temp.toAbsolutePath().toString())
+ && profile.ident().equals(entry.profile()) && entry.singleJava();
+ // profile resolution by ident (the recent-file open lookup, Swing
+ // RecentFileMenu.java:403-406)
+ boolean resolved = ServiceLoader.load(DefaultProfileResolver.class).stream()
+ .filter(resolver -> resolver.get().getProfileName().equals(profile.ident()))
+ .findFirst().isPresent();
+ report = stored && resolved ? "PASS (store round trip + profile resolution)"
+ : "FAIL (stored=" + stored + ", resolved=" + resolved + ")";
+ Files.deleteIfExists(temp);
+ }
+ } catch (IOException e) {
+ report = "FAIL: " + e;
+ }
+ // restore the store and the Confirm Exit setting before anything else
+ recentFiles.restore(snapshot);
+ ViewSettings vs = ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings();
+ boolean confirmExit = vs.confirmExit();
+ vs.setConfirmExit(false);
+ if (report.startsWith("PASS")) {
+ LOGGER.info("loadingexit verification: {} — closing the window", report);
+ // the window close button path (stage.setOnCloseRequest -> exitApplication ->
+ // exitApplicationWithoutInteraction): the process must terminate with exit code 0
+ stage.fireEvent(new WindowEvent(stage, WindowEvent.WINDOW_CLOSE_REQUEST));
+ } else {
+ vs.setConfirmExit(confirmExit);
+ LOGGER.info("loadingexit verification: {}", report);
+ NotificationManagerF.getInstance()
+ .notify("Loading/exit verification: " + report,
+ report.startsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ }
+ }
+
+ /**
+ * termmenu: headless self test of the sequent context menu ({@code key.fx.verify.termmenu}),
+ * run after the demo load like the other proof-dependent verify hooks: computes a
+ * {@link PosInSequent} at a known character index of the current printing, builds the menu
+ * with {@link SequentMenuModelF#build} + {@link SequentTermContextMenuF#build} and asserts
+ * the fixed structural items on the rendered {@link ContextMenu}. Text report on stdout only
+ * — no screenshots, no interaction. Skips gracefully when no goal or position is available.
+ *
+ * @param env the environment of the loaded proof
+ */
+ private void runTermMenuVerification(KeYEnvironment env) {
+ Goal goal = mediator.getSelectedGoal();
+ PosInSequent pos = findTermMenuPos();
+ if (goal == null || pos == null) {
+ System.out.println("termmenu verify: SKIP - no goal/position (no printed sequent)");
+ return;
+ }
+ List entries =
+ SequentMenuModelF.build(pos, mediator, env.getProofControl(), null, null);
+ ContextMenu menu = SequentTermContextMenuF.build(entries,
+ new SequentTermContextMenuF.MenuContext(mediator, env.getProofControl(), goal, pos,
+ null, sequentView::printSequent));
+ List labels = new ArrayList<>();
+ int[] enabled = { 0 };
+ collectMenuLabels(menu.getItems(), labels, enabled);
+ boolean hasFocus = labels.contains("Apply rules automatically here");
+ boolean hasCopy = labels.contains("Copy to clipboard");
+ boolean hasNoRules = labels.contains("No rules applicable.");
+ boolean pass = menu.getItems().size() >= 4 && hasFocus && hasCopy
+ && (hasNoRules || enabled[0] > 0);
+ List found = new ArrayList<>();
+ found.add("focus_auto_mode");
+ found.add("copy_clipboard");
+ if (hasNoRules) {
+ found.add("no_rules");
+ }
+ System.out.println("termmenu verify: " + (pass ? "OK" : "FAIL") + " - "
+ + menu.getItems().size() + " items, found: " + String.join(", ", found));
+ }
+
+ /**
+ * menu: MP8/B12 — headless self test of the MP8a/MP8b term-menu wiring
+ * ({@code key.fx.verify.termmenuwiring}), run after the demo load like the termmenu hook:
+ * builds the term menu through the same seam as {@link #runTermMenuVerification} and
+ * asserts (a) the {@code focus_auto_mode} item ("Apply rules automatically here") is ENABLED
+ * and its action handler is non-null (Swing FocussedAutoModeUserAction, wired at
+ * FocussedAutoModeUserAction.java:43), and (b) the {@code macro_menu} section ("Strategy
+ * Macros", Swing ProofMacroMenu.java:81) contains the four macro names of the Automation
+ * submenu ({@link #AUTOMATION_MACROS}) as a subset of the P3b/B12 superset (all registered
+ * macros applicable at the position, category-grouped with separators — Swing iterates
+ * {@code ProofMacroMenu.REGISTERED_MACROS}, ProofMacroMenu.java:60-61/90-99). The handlers
+ * are deliberately NOT invoked — starting a real focused auto mode or running a macro
+ * headless mid-regression is too heavy; enablement and handler presence is the assertion.
+ */
+ private void runTermMenuWiringVerification(KeYEnvironment env) {
+ Goal goal = mediator.getSelectedGoal();
+ PosInSequent pos = findTermMenuPos();
+ if (goal == null || pos == null) {
+ System.out.println(
+ "termmenu wiring verify: SKIP - no goal/position (no printed sequent)");
+ return;
+ }
+ List entries =
+ SequentMenuModelF.build(pos, mediator, env.getProofControl(), null, null);
+ ContextMenu menu = SequentTermContextMenuF.build(entries,
+ new SequentTermContextMenuF.MenuContext(mediator, env.getProofControl(), goal, pos,
+ null, sequentView::printSequent));
+ MenuItem focusItem = null;
+ Menu macroMenu = null;
+ for (MenuItem item : menu.getItems()) {
+ if ("Apply rules automatically here".equals(item.getText())) {
+ focusItem = item;
+ }
+ if (item instanceof Menu m && "Strategy Macros".equals(m.getText())) {
+ macroMenu = m;
+ }
+ }
+ boolean focusOk = focusItem != null && !focusItem.isDisable()
+ && focusItem.getOnAction() != null;
+ List macroNames = new ArrayList<>();
+ if (macroMenu != null) {
+ for (MenuItem item : macroMenu.getItems()) {
+ String text = menuItemText(item);
+ if (!text.isEmpty()) {
+ macroNames.add(text);
+ }
+ }
+ }
+ List expected = new ArrayList<>();
+ for (ProofMacro macro : AUTOMATION_MACROS) {
+ expected.add(macro.getName());
+ }
+ // P3b/B12: since MP8 the section shows the registered-macro superset grouped by category
+ // (ProofMacroMenuF, Swing iterates ProofMacroMenu.REGISTERED_MACROS) — the Automation
+ // submenu's four names must all be present, and at least one category separator must
+ // separate the groups (Swing ProofMacroMenu.java:101-113).
+ boolean separators = macroMenu != null && macroMenu.getItems().stream()
+ .anyMatch(SeparatorMenuItem.class::isInstance);
+ boolean macroOk = macroNames.containsAll(expected) && separators;
+ boolean pass = focusOk && macroOk;
+ System.out.println("termmenu wiring verify: " + (pass ? "PASS" : "FAIL") + " - "
+ + "focus_auto_mode[" + (focusItem == null ? "missing"
+ : (focusOk ? "enabled" : "disabled-or-no-handler"))
+ + "] macro_menu[" + macroNames.size() + " items, separators=" + separators
+ + "]");
+ }
+
+ /**
+ * extension: MP9.0 — headless self test of the FX extension SPI
+ * ({@code key.fx.verify.extensions}), run after the demo load like the other
+ * proof-dependent verify hooks. Asserts (a) the facade discovers the built-in extensions
+ * incl. the ported TestExtensionF, (b) the status-line controls of the facade appear in the
+ * built status bar, (c) the Heatmap menu is a separate menu of the built menu bar,
+ * (d) the SettingsManagerF registry holds the Heatmap settings provider, (e) the term-menu
+ * extension section renders the disabled placeholder when no position is available
+ * (the fallback of SequentTermContextMenuF; contributed items would be enabled only with a
+ * position), and P4's seams: (f) B13 the "Test" menu nests at Test>Test>Test in the built
+ * menu bar, (g) B14 all extension toolbar controls sit in the single extension toolbar,
+ * (h) C24 the PROOF_TREE popup items of the selected node are non-empty, and (i) D36 the
+ * TestExtensionF SEQUENT_VIEW shortcut fires a toast when a matching key event hits the
+ * sequent view. One stdout report line; skips the term-menu sub-assertion gracefully when
+ * no goal is loaded.
+ *
+ * WP-1 adds the mediator-seam assertions: (j) the opened-proofs accessor contains the
+ * loaded demo proof, (k) the built-in window registration is look-up-able through the
+ * extension-object registry, (l) the register/lookup/deregister round trip of the
+ * registry, (m) the proof-load listener registry delivers the loaded proof, (n) the
+ * GUI-listener registry delivers the shutdown event.
+ *
+ * @param env the environment of the loaded proof
+ */
+ private void runExtensionVerification(KeYEnvironment env) {
+ boolean pass = true;
+ StringBuilder sb = new StringBuilder();
+
+ int discovered = KeYGuiExtensionFacadeF.discoveredCount();
+ sb.append("discovered=").append(discovered);
+ // MP9.1-9.6: the six keyext FX modules register further providers, so the count is no
+ // longer fixed at 3 — the assertion is the built-in trio's presence and a sane number
+ List classes = KeYGuiExtensionFacadeF.getExtensions().stream()
+ .map(e -> e.getClass().getName()).toList();
+ pass &= discovered >= 3
+ && discovered == classes.size()
+ && classes.contains("de.uka.ilkd.key.gui.fx.extension.contrib.HeatmapF")
+ && classes.contains(
+ "de.uka.ilkd.key.gui.fx.extension.contrib.ParallelProverStatusIndicatorF")
+ && classes.contains(
+ "de.uka.ilkd.key.gui.fx.extension.contrib.ProfileNameInStatusBarF");
+
+ List statusControls = KeYGuiExtensionFacadeF.getStatusLineControls();
+ sb.append(" statusControls=").append(statusControls.size());
+ HBox statusBar = buildStatusBar();
+ // the two built-in status-line controls are always contributed; the keyext ports may
+ // add more, so the assertion is a membership check on the built status bar
+ pass &= statusControls.size() >= 2
+ && statusBar.getChildren().containsAll(statusControls);
+
+ // WP-3 — the parallel-prover status indicator: the Swing-parity "SC"/"MT Nx" button
+ // (left-click toggle through the real handler + the right-click worker-count menu)
+ KeYGuiExtensionF proverExtension = KeYGuiExtensionFacadeF.getExtensions().stream()
+ .filter(e -> e instanceof ParallelProverStatusIndicatorF).findFirst().orElse(null);
+ String proverReport = proverExtension instanceof ParallelProverStatusIndicatorF prover
+ ? prover.verifyProverMode()
+ : "MISSING";
+ sb.append(" proverMode=").append(proverReport);
+ pass &= proverReport.endsWith("PASS");
+
+ MenuBar menuBar = buildMenuBar();
+ boolean heatmapMenu =
+ menuBar.getMenus().stream().anyMatch(m -> "Heatmap".equals(m.getText()));
+ sb.append(" heatmapMenu=").append(heatmapMenu);
+ pass &= heatmapMenu;
+
+ boolean heatmapSettings = SettingsManagerF.getInstance().getProviders().stream()
+ .anyMatch(p -> "Heatmap".equals(p.getDescription()));
+ sb.append(" heatmapSettings=").append(heatmapSettings);
+ pass &= heatmapSettings;
+
+ // drawer: MP10 — the extension left-panel tabs are west drawer items now; with only the
+ // ported built-in extensions registered none contributes tabs, so the west drawer holds
+ // exactly its five built-in panels (+ one item per contributed left-panel tab)
+ int facadeTabs = KeYGuiExtensionFacadeF.getLeftPanelTabs(this, mediator).size();
+ int westItems = westDrawer == null ? -1 : westDrawer.getItems().size();
+ sb.append(" westDrawerItems=").append(westItems);
+ pass &= westItems == 5 + facadeTabs;
+
+ // term-menu extension section: with no position the disabled placeholder must render
+ // without crashing (SequentTermContextMenuF.extensionSection)
+ Goal goal = mediator.getSelectedGoal();
+ String termMenuSection;
+ if (goal == null) {
+ termMenuSection = "skipped";
+ } else {
+ PosInSequent pos = findTermMenuPos();
+ List entries = pos == null
+ ? List.of(new SequentMenuModelF.NamedAction("extension", "Extensions", null,
+ null))
+ : SequentMenuModelF.build(pos, mediator, env.getProofControl(), null, null);
+ ContextMenu menu = SequentTermContextMenuF.build(entries,
+ new SequentTermContextMenuF.MenuContext(mediator, env.getProofControl(), goal,
+ null, null, sequentView::printSequent));
+ MenuItem extensionItem = menu.getItems().stream()
+ .filter(it -> "Extensions".equals(it.getText())).findFirst().orElse(null);
+ boolean disabledFallback = extensionItem != null && extensionItem.isDisable()
+ && extensionItem.getOnAction() == null;
+ termMenuSection = disabledFallback ? "disabled-fallback" : "FAIL";
+ pass &= disabledFallback;
+ }
+ sb.append(" termmenuExtension=").append(termMenuSection);
+
+ // P4 — the four seam assertions (see the javadoc): TestExtensionF present (a), B13
+ // nested Test menu (f), B14 merged extension toolbar (g), C24 PROOF_TREE items (h),
+ // D36 sequent-view shortcut toast (i)
+ boolean testExtensionPresent =
+ classes.contains("de.uka.ilkd.key.gui.fx.extension.contrib.TestExtensionF");
+ sb.append(" testExtension=").append(testExtensionPresent ? "present" : "MISSING");
+ pass &= testExtensionPresent;
+
+ Menu testTop = menuBar.getMenus().stream().filter(m -> "Test".equals(m.getText()))
+ .findFirst().orElse(null);
+ boolean testNested = hasSubMenuItem(subMenuNamed(subMenuNamed(testTop, "Test"), "Test"),
+ "Test");
+ sb.append(" testNested=").append(testNested);
+ pass &= testNested;
+
+ List extToolbarControls =
+ KeYGuiExtensionFacadeF.getToolbarControls(this, mediator);
+ List extToolBars = new ArrayList<>();
+ VBox topBox = buildTop();
+ for (Node child : topBox.getChildren()) {
+ if (child instanceof HBox area) {
+ for (Node toolBar : area.getChildren()) {
+ if (toolBar instanceof ToolBar bar
+ && bar.getStyleClass().contains("key-extension-tool-bar")) {
+ extToolBars.add(bar);
+ }
+ }
+ }
+ }
+ boolean mergedToolbar = extToolBars.size() == 1
+ && extToolBars.get(0).getItems().containsAll(extToolbarControls);
+ sb.append(" extensionToolbars=").append(extToolBars.size());
+ pass &= mergedToolbar;
+
+ de.uka.ilkd.key.proof.Node selNode = mediator.getSelectedNode();
+ int proofTreeItems = selNode == null ? 0
+ : KeYGuiExtensionFacadeF.getProofTreeContextItems(mediator, selNode).size();
+ sb.append(" prooftreeExtensionItems=").append(proofTreeItems);
+ pass &= proofTreeItems >= 1;
+
+ // WP-1 — the mediator seam assertions (see the javadoc): (j) the opened-proofs accessor
+ // reflects the demo load, (k) the built-in window registration is look-up-able, (l) the
+ // extension-object registry round trip, (m) the proof-load listener registry delivers
+ // the loaded proof, (n) the GUI-listener registry delivers the shutdown event
+ List openedProofs = mediator.getCurrentlyOpenedProofs();
+ boolean openedProofsOk = !openedProofs.isEmpty()
+ && openedProofs.contains(env.getLoadedProof());
+ sb.append(" openedProofs=").append(openedProofs.size());
+ pass &= openedProofsOk;
+
+ boolean windowRegistered = mediator.lookup(MainWindowF.class) == this;
+ sb.append(" windowRegistered=").append(windowRegistered);
+ pass &= windowRegistered;
+
+ // registry round trip with a dedicated marker object (the window itself stays
+ // registered for the real extension consumers)
+ final class SeamMarker {
+ }
+ SeamMarker marker = new SeamMarker();
+ mediator.register(marker, SeamMarker.class);
+ boolean registryRoundTrip = mediator.lookup(SeamMarker.class) == marker;
+ mediator.deregister(marker, SeamMarker.class);
+ registryRoundTrip &= mediator.lookup(SeamMarker.class) == null;
+ sb.append(" registryRoundTrip=").append(registryRoundTrip);
+ pass &= registryRoundTrip;
+
+ // proof-load listener registry: the listener registered here observes the fired event
+ // (Swing KeYMediator.fireProofLoaded; the production firing site is
+ // WindowUserInterfaceControlF.registerProofAggregate, which ran before this hook for
+ // the demo load — the slicing port (WP-6) registers its tracker through this seam)
+ List proofLoadEvents = new ArrayList<>();
+ mediator.registerProofLoadListener(proofLoadEvents::add);
+ mediator.fireProofLoaded(env.getLoadedProof());
+ boolean proofLoadOk = proofLoadEvents.size() == 1
+ && proofLoadEvents.get(0) == env.getLoadedProof();
+ sb.append(" proofLoadListener=").append(proofLoadOk);
+ pass &= proofLoadOk;
+
+ // GUI-listener registry: a test-level listener observes the shutdown event the exit
+ // flow fires (Swing ExitMainAction.java:83 → MainGUIListener.shutDown,
+ // MainWindow.java:1855); the listener is removed afterwards so no other consumer sees
+ // the synthetic event
+ List shutdownEvents = new ArrayList<>();
+ GUIListenerF guiListener = new GUIListenerF() {
+ @Override
+ public void modalDialogOpened(EventObject e) {
+ // not asserted here
+ }
+
+ @Override
+ public void modalDialogClosed(EventObject e) {
+ // not asserted here
+ }
+
+ @Override
+ public void shutDown(EventObject e) {
+ shutdownEvents.add(e);
+ }
+ };
+ mediator.addGUIListener(guiListener);
+ mediator.fireShutDown(new EventObject(this));
+ boolean shutdownOk =
+ shutdownEvents.size() == 1 && shutdownEvents.get(0).getSource() == this;
+ mediator.removeGUIListener(guiListener);
+ sb.append(" shutdownEvent=").append(shutdownOk);
+ pass &= shutdownOk;
+
+ int toastsBefore = NotificationManagerF.getInstance().getVisibleToastCount();
+ // Ctrl+Shift+F12 — the combination TestExtensionF registered for SEQUENT_VIEW
+ // (KeyEvent args: eventType, character, text, keyCode, shift, control, alt, meta).
+ // The toast shows asynchronously (NotificationManagerF.notify → Platform.runLater),
+ // so the assertion and the report run on a later FX pulse behind it.
+ sequentView.fireEvent(new KeyEvent(KeyEvent.KEY_PRESSED, "", "F12", KeyCode.F12, true,
+ true, false, false));
+ boolean passSoFar = pass;
+ Platform.runLater(() -> {
+ boolean shortcutFires =
+ NotificationManagerF.getInstance().getVisibleToastCount() > toastsBefore;
+ sb.append(" sequentShortcutToast=").append(shortcutFires);
+ boolean ok = passSoFar && shortcutFires;
+ String report = (ok ? "PASS" : "FAIL") + " - " + sb;
+ System.out.println("Extension verification: " + report);
+ LOGGER.info("Extension verification: {}", report);
+ });
+ }
+
+ /**
+ * exploration (WP-5): headless self test of the exploration extension parity ({@code
+ * key.fx.verify.exploration}), run after the demo load like the other proof-dependent verify
+ * hooks. {@code keyext.exploration.fx} is not on this module's compile classpath, so the
+ * assertions reach it reflectively (the same pattern as the Swing-class reads elsewhere).
+ * Asserts (a) the {@code ExplorationModeModel} is registered through the WP-1 mediator
+ * registry and look-up-able (Swing {@code mediator.register(model,
+ * ExplorationModeModel.class)}, ExplorationExtension.java:103), (b) the renderer-styling
+ * hook is registered on {@link ProofTreeViewF} and the node of a real exploration step
+ * (applied through {@code ProofExplorationService.soundAddition} on the selected demo goal)
+ * is styled with the purple exploration border, (c) the "Hide justification" toolbar toggle
+ * routes through the {@code HIDE_INTERACTIVE_GOALS} proof-tree filter (the persisted
+ * ViewSettings flag follows the toggle and enabling the filter shrinks the tree — the
+ * exploration cut's justification branch disappears; the state is restored), and (d) the
+ * {@code promptForTerm} retry-loop accepts bad-then-good input: malformed input and a sort
+ * mismatch re-prompt (the Swing parity), a cancel aborts. One stdout report line.
+ *
+ * @param env the environment of the loaded proof
+ */
+ private void runExplorationVerification(KeYEnvironment env) {
+ StringBuilder sb = new StringBuilder();
+ boolean pass = true;
+ try {
+ Proof proof = env.getLoadedProof();
+ Goal goal = mediator.getSelectedGoal();
+
+ // (a) the model registered via the mediator registry and look-up-able
+ Class> modelClass = Class.forName("org.key_project.exploration.ExplorationModeModel");
+ Object model = mediator.lookup(modelClass);
+ boolean registered = model != null;
+ sb.append("modelRegistered=").append(registered);
+ pass &= registered;
+
+ // (b) the renderer-styling hook is registered on the proof tree (the exploration
+ // extension's init registered ExplorationRendererF); switch the exploration mode on
+ // so the step runs in the mode the user would use
+ int stylers = proofTreeView.getCellStylers().size();
+ sb.append(" cellStylers=").append(stylers);
+ pass &= stylers >= 1;
+ if (model != null) {
+ model.getClass().getMethod("setExplorationModeSelected", boolean.class)
+ .invoke(model, true);
+ }
+
+ // apply a real exploration step on the demo goal (the "Add formula to antecedent"
+ // path): the cut registers ExplorationNodeData on the cut node and both branches
+ ProofTreeViewF.Style style = null;
+ if (goal != null) {
+ Services services = proof.getServices();
+ JTerm term = new KeyIO(services).parseExpression("lives(agatha)");
+ Class> serviceClass =
+ Class.forName("org.key_project.exploration.ProofExplorationService");
+ Object service = serviceClass.getConstructor(Proof.class, Services.class)
+ .newInstance(proof, services);
+ de.uka.ilkd.key.proof.Node explored = (de.uka.ilkd.key.proof.Node) serviceClass
+ .getMethod("soundAddition", Goal.class, JTerm.class, boolean.class)
+ .invoke(service, goal, term, true);
+ selectionModel.setSelectedNode(explored);
+ proofTreeView.refresh();
+ style = proofTreeView.stylerStyle(explored);
+ } else {
+ sb.append(" goal=NONE");
+ }
+ // the purple exploration border (Swing ExplorationRenderer.DARK_PURPLE_COLOR default
+ // rgb(112,17,191) = 0x7011bf) rendered through the styler's inline css
+ boolean styled = style != null && style.getBorder() != null
+ && style.getBorder().toString().contains("0x7011bf")
+ && style.css().contains("-fx-border-color")
+ && "Exploration Action Performed".equals(style.getTooltipTitle());
+ sb.append(" explorationStyled=").append(styled);
+ pass &= styled;
+
+ // (c) the "Hide justification" toolbar toggle routes through the tree filter:
+ // fire the real checkbox (the extension handler setHideJustification runs), the
+ // persisted ViewSettings flag must follow and — with the filter on — the tree must
+ // shrink by the exploration cut's justification branch; restore the state after
+ ViewSettings viewSettings =
+ ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings();
+ CheckBox hideJustification = KeYGuiExtensionFacadeF.getToolbarControls(this, mediator)
+ .stream()
+ .filter(c -> c instanceof CheckBox cb
+ && "Hide justification".equals(cb.getText()))
+ .map(c -> (CheckBox) c).findFirst().orElse(null);
+ boolean routed = false;
+ if (hideJustification != null) {
+ boolean hideBefore = viewSettings.getHideInteractiveGoals();
+ int entriesBefore = nodeEntries(proofTreeView.verifyTreeStructure());
+ hideJustification.fire();
+ boolean hideAfter = viewSettings.getHideInteractiveGoals();
+ int entriesAfter = nodeEntries(proofTreeView.verifyTreeStructure());
+ routed = hideAfter == !hideBefore
+ && (hideAfter ? entriesAfter < entriesBefore
+ : entriesAfter == entriesBefore);
+ // restore the state found on entry
+ hideJustification.fire();
+ routed &= viewSettings.getHideInteractiveGoals() == hideBefore;
+ }
+ sb.append(" hideJustificationFilter=").append(routed);
+ pass &= routed;
+
+ // (d) the promptForTerm retry-loop (scripted handler, no dialogs): malformed input
+ // re-prompts ("Malformed input"), then a good term is accepted; a sort mismatch
+ // re-prompts ("Sort mismatch"); a cancel aborts
+ Class> menuClass =
+ Class.forName("org.key_project.exploration.fx.ExplorationSequentMenuF");
+ Class> handlerClass = Class.forName(
+ "org.key_project.exploration.fx.ExplorationSequentMenuF$PromptHandler");
+ java.lang.reflect.Method prompt =
+ menuClass.getDeclaredMethod("promptForTerm", Goal.class, JTerm.class,
+ handlerClass);
+ prompt.setAccessible(true);
+
+ List errors1 = new ArrayList<>();
+ Object handler1 = scriptedPromptHandler(List.of("(((", "lives(agatha)"), errors1);
+ Object term1 = prompt.invoke(null, goal, null, handler1);
+ boolean retryBadGood =
+ term1 instanceof JTerm && errors1.equals(List.of("Malformed input"));
+
+ List errors2 = new ArrayList<>();
+ JTerm intTerm = new KeyIO(proof.getServices()).parseExpression("0");
+ Object handler2 = scriptedPromptHandler(List.of("lives(agatha)", "1"), errors2);
+ Object term2 = prompt.invoke(null, goal, intTerm, handler2);
+ boolean retrySortMismatch = term2 instanceof JTerm t2
+ && t2.sort().equals(intTerm.sort())
+ && errors2.equals(List.of("Sort mismatch"));
+
+ List errors3 = new ArrayList<>();
+ Object handler3 = scriptedPromptHandler(List.of(), errors3);
+ boolean cancelAborts = prompt.invoke(null, goal, null, handler3) == null
+ && errors3.isEmpty();
+
+ boolean retryOk = retryBadGood && retrySortMismatch && cancelAborts;
+ sb.append(" promptRetry=").append(retryOk);
+ pass &= retryOk;
+ } catch (Exception e) {
+ LOGGER.error("Exploration verification failed", e);
+ pass = false;
+ sb.append(" error=").append(e);
+ }
+ String report = (pass ? "PASS" : "FAIL") + " - " + sb;
+ System.out.println("Exploration verification: " + report);
+ LOGGER.info("Exploration verification: {}", report);
+ NotificationManagerF.getInstance()
+ .notify("Exploration verification: " + report,
+ pass ? Kind.INFO : Kind.ERROR);
+ statusRight.setText(report);
+ }
+
+ /**
+ * Builds a reflective {@code ExplorationSequentMenuF.PromptHandler} proxy that feeds the
+ * given inputs and records the shown error titles ({@code null} inputs end the script — the
+ * cancel case).
+ */
+ private static Object scriptedPromptHandler(List inputs, List errors) {
+ Class> handlerClass;
+ try {
+ handlerClass =
+ Class.forName(
+ "org.key_project.exploration.fx.ExplorationSequentMenuF$PromptHandler");
+ } catch (ClassNotFoundException e) {
+ throw new IllegalStateException(e);
+ }
+ java.util.concurrent.atomic.AtomicInteger next =
+ new java.util.concurrent.atomic.AtomicInteger();
+ return java.lang.reflect.Proxy.newProxyInstance(handlerClass.getClassLoader(),
+ new Class>[] { handlerClass }, (proxy, method, args) -> {
+ if (args[1] != null) {
+ errors.add((String) args[1]);
+ }
+ return next.get() < inputs.size() ? inputs.get(next.getAndIncrement()) : null;
+ });
+ }
+
+ /** @return the nodeEntries count reported by a {@code verifyTreeStructure} report line. */
+ private static int nodeEntries(String structureReport) {
+ java.util.regex.Matcher m =
+ java.util.regex.Pattern.compile("nodeEntries=(\\d+)").matcher(structureReport);
+ return m.find() ? Integer.parseInt(m.group(1)) : -1;
+ }
+
+ /** @return the direct sub menu of the given menu with the given text, or {@code null}. */
+ private static Menu subMenuNamed(Menu parent, String text) {
+ if (parent == null) {
+ return null;
+ }
+ for (MenuItem item : parent.getItems()) {
+ if (item instanceof Menu menu && text.equals(menu.getText())) {
+ return menu;
+ }
+ }
+ return null;
+ }
+
+ /** @return whether any direct item of the given menu has the given text. */
+ private static boolean hasSubMenuItem(Menu menu, String text) {
+ return menu != null
+ && menu.getItems().stream().anyMatch(item -> text.equals(item.getText()));
+ }
+
+ /**
+ * drawer: MP10 — headless self test of the drawered main window layout
+ * ({@code key.fx.verify.drawerlayout}), run after the demo load like the other
+ * proof-dependent verify hooks. Asserts the west/east/south {@link DrawerF} hosts with
+ * their expected item sets — the five built-in west panels plus one item per contributed
+ * extension left-panel tab (= MP9.1-9.6: exploration + slicing) — and default expansions
+ * (Proof Tree + Goal List share the west split in button order), then exercises the same
+ * drag-and-drop seams the handlers invoke on the live drawers: a cross-port transfer of the
+ * Strategy panel west→east and back (owner re-keying) and a button reorder whose split
+ * follows the new button order. One stdout report line; leaves the drawers in their
+ * pre-test arrangement.
+ *
+ * @param env the environment of the loaded proof
+ */
+ private void runDrawerLayoutVerification(KeYEnvironment env) {
+ boolean pass = true;
+ StringBuilder sb = new StringBuilder();
+ DrawerF west = getWestDrawer();
+ DrawerF east = getEastDrawer();
+ DrawerF south = getSouthDrawer();
+ if (west == null || east == null || south == null) {
+ System.out.println("Drawer layout verification: FAIL - hosts=null");
+ return;
+ }
+ // MP9.1-9.6: the keyext left-panel tabs (exploration, slicing) are also west items, so
+ // the expected count mirrors runExtensionVerification: five built-ins + one per tab
+ int expectedWest = 5 + KeYGuiExtensionFacadeF.getLeftPanelTabs(this, mediator).size();
+ sb.append("west=").append(west.getItems().size()).append(" east=")
+ .append(east.getItems().size()).append(" south=").append(south.getItems().size());
+ pass &= west.getItems().size() == expectedWest && east.getItems().size() == 1
+ && south.getItems().size() == 0;
+ pass &= west.getDockingSide() == Side.LEFT && west.isMultiselect();
+ pass &= east.getDockingSide() == Side.RIGHT && east.isMultiselect();
+ pass &= south.getDockingSide() == Side.BOTTOM;
+
+ // default expansions: "Loaded Proofs" + Proof Tree are the expanded items and share the
+ // west split in button order (reviewer request: the start layout shows Loaded Proofs and
+ // Proof Tree, not the Goal List)
+ ObservableList westContent = west.getContentArea().hostedItems();
+ sb.append(" expanded=").append(westContent.size());
+ pass &= westContent.size() == 2 && westContent.get(0) == west.getItems().get(0)
+ && westContent.get(1) == west.getItems().get(1);
+
+ // cross-port transfer round trip: Strategy panel west->east, then east->west
+ boolean movedEast = false;
+ boolean movedBack = false;
+ DrawerItemF strategy = west.getItems().size() > 4 ? west.getItems().get(4) : null;
+ if (strategy != null && "Strategy".equals(strategy.getButton().getText())) {
+ west.transferItem(strategy, east);
+ movedEast = east.getItems().size() == 2 && east.getItems().contains(strategy)
+ && strategy.getDrawer() == east;
+ east.transferItem(strategy, west);
+ movedBack = west.getItems().size() == expectedWest && east.getItems().size() == 1
+ && strategy.getDrawer() == west && !east.getItems().contains(strategy);
+ }
+ sb.append(" transferRoundTrip=").append(movedEast && movedBack ? "PASS" : "FAIL");
+ pass &= movedEast && movedBack;
+
+ // button reorder: move the first expanded west item behind the next one; bar + split
+ // follow. The content area hosts only expanded items, so the assertion uses an expanded
+ // item ("Loaded Proofs" leads the bar collapsed by default, item 0 would have no content
+ // area entry)
+ boolean reorderOk = false;
+ int from = -1;
+ for (int i = 0; i < west.getItems().size(); i++) {
+ if (west.getItems().get(i).isExpanded()) {
+ from = i;
+ break;
+ }
+ }
+ if (from >= 0 && west.getItems().size() > from + 1) {
+ DrawerItemF first = west.getItems().get(from);
+ west.moveItem(from, from + 1);
+ reorderOk = west.getItems().get(from + 1) == first
+ && west.getContentArea().containsItem(first);
+ west.moveItem(from + 1, from);
+ reorderOk &= west.getItems().get(from) == first;
+ }
+ sb.append(" reorder=").append(reorderOk ? "PASS" : "FAIL");
+ pass &= reorderOk;
+
+ String report = (pass ? "PASS" : "FAIL") + " - " + sb;
+ System.out.println("Drawer layout verification: " + report);
+ LOGGER.info("Drawer layout verification: {}", report);
+ }
+
+ /**
+ * menu: MP8 — visible text of a menu item, unwrapping label-backed {@code CustomMenuItem}s
+ * (the macro items of the term menu carry their name in the wrapped label).
+ */
+ private static String menuItemText(MenuItem item) {
+ String text = item.getText();
+ if (text != null && !text.isEmpty()) {
+ return text;
+ }
+ if (item instanceof CustomMenuItem custom && custom.getContent() instanceof Label label) {
+ return label.getText();
+ }
+ return "";
+ }
+
+ /**
+ * termmenu: the first {@link PosInSequent} of the current printing (the printed text may
+ * start with whitespace or symbols that do not map to a position — the first indexed
+ * character that resolves is used).
+ */
+ private PosInSequent findTermMenuPos() {
+ String printed = sequentView.printedText();
+ if (printed == null) {
+ return null;
+ }
+ for (int i = 0; i < printed.length(); i++) {
+ PosInSequent pos = sequentView.getSequentPosAt(i);
+ if (pos != null) {
+ return pos;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * termmenu: collects the labels of a menu-item tree (sub-menus are flattened) and counts the
+ * enabled, non-separator items.
+ */
+ private static void collectMenuLabels(List items, List labels,
+ int[] enabled) {
+ for (MenuItem item : items) {
+ if (!(item instanceof SeparatorMenuItem)) {
+ labels.add(item.getText());
+ if (!item.isDisable()) {
+ enabled[0]++;
+ }
+ }
+ if (item instanceof Menu menu) {
+ collectMenuLabels(menu.getItems(), labels, enabled);
+ }
+ }
+ }
+
+ /**
+ * Runs the join/merge dialog self test ({@code key.fx.verify.joinmerge}); the dialogs are
+ * constructed directly with the loaded proof, see the JoinMergeVerifyF harness
+ * (test source set).
+ *
+ * @param env the environment of the loaded proof
+ */
+ // joinmerge: TODO-merge registration seam — once the FX rule-application completion registry
+ // exists (WindowUserInterfaceControlF, port of Swing WindowUserInterfaceControl.java:74-82),
+ // register the interactive completions there via
+ // uiControl.register(MergeRuleCompletionF.INSTANCE);
+ // the join trigger is JoinActionF.run(partners, proof, proofControl, owner) for the future
+ // sequent-view context menu (Swing JoinMenuItem via CurrentGoalViewMenu.java:235-238).
+ // joinmerge: keep this hook minimal, all logic lives in JoinMergeVerifyF
+ private void runJoinMergeVerification(KeYEnvironment env) {
+ runVerifyHarness("de.uka.ilkd.key.gui.fx.join.JoinMergeVerifyF", "run",
+ new Class>[] { javafx.stage.Window.class, Proof.class, ProofControl.class },
+ stage, env.getLoadedProof(), env.getProofControl());
+ }
+
+ /**
+ * Runs the proof tree search self test with the query given as the value of
+ * {@code key.fx.verify.search} (default {@code andleft}, which matches the Agatha demo's
+ * rule applications).
+ */
+ private void runSearchVerification() {
+ String query = System.getProperty("key.fx.verify.search");
+ String report = proofTreeView
+ .verifySearch(query == null || query.isBlank() ? "andleft" : query.trim());
+ LOGGER.info("Proof tree search verification: {}", report);
+ NotificationManagerF.getInstance()
+ .notify("Tree search verification: " + report,
+ report.endsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ }
+
+ /**
+ * Refreshes all views from the final proof state. Called at auto mode stop (Swing parity:
+ * {@code MainWindow.autoModeStopped}): the proof suspends its non-essential listeners during
+ * the run, so no selection or structural event fires at the stop and the views would
+ * otherwise show stale content.
+ */
+ private void refreshViewsFromFinalState() {
+ Proof proof = selectionModel.getSelectedProof();
+ proofTreeView.refresh();
+ goalListView.setProof(proof);
+ infoView.display(proof);
+ sequentView.display(selectionModel.getSelectedNode());
+ updateProofStatus();
+ // notification: proof-closed + "Automated proof search" notifications after an automatic
+ // run (Swing parity: KeYMediator.proofClosed fires a ProofClosedNotificationEvent;
+ // WindowUserInterfaceControl.taskFinishedInternal fires the showNotification
+ // information, gated by ViewSettings.notificationAfterMacro)
+ NotificationCenterF.getInstance().afterAutoModeFinished(proof, stage.isFocused());
+ }
+
+ /**
+ * The global action keys of the Swing {@code AutoModeAction}: {@code Ctrl+Space} starts the
+ * automatic prover on the selected proof, {@code Escape} stops a running one. The scene
+ * handler sees the key events that no focused control consumed — an open search bar consumes
+ * {@code Escape} itself (Swing parity: the Swing search bars behave the same against the
+ * global stop shortcut).
+ */
+ private void handleMainWindowKeyPressed(KeyEvent event) {
+ if (event.getCode() == KeyCode.SPACE && event.isControlDown()) {
+ mediator.startAutoMode();
+ event.consume();
+ } else if (event.getCode() == KeyCode.ESCAPE) {
+ mediator.stopAutoMode();
+ event.consume();
+ }
+ // smalldialogs: F1 context help is handled by HelpFacadeF.installAccelerator(scene)
+ // (registered in initialize), not here — a second handler would open the page twice.
+ }
+
+ /**
+ * The UI's own auto mode listener: refreshes the views from the final state after an
+ * interactive auto mode run (Swing {@code MainWindow.autoModeStopped}). The demo's
+ * live run has its own listener which also runs the verification reports; the UI listener
+ * skips the refresh in that case to avoid the duplicate work.
+ */
+ /**
+ * inputfreeze (P1) — the blocking overlay of the auto mode freeze (Swing
+ * {@code BlockingGlassPane} + {@code GlassPaneListener}, MainWindow.java:1620-1746): shown
+ * by {@link #freezeExceptAutoModeButton()} for the duration of an auto mode run, it covers
+ * the main area (workspace + drawers), blocks all mouse input, swallows key events targeted
+ * inside it (except Escape, which reaches the global stop handler) and shows the wait
+ * cursor. The top toolbar with the automation/stop controls and the status bar stay live.
+ */
+ private final Pane inputBlocker = new Pane();
+
+ /**
+ * inputfreeze (P1) — Swing {@code MainWindow.freezeExceptAutoModeButton} (MainWindow.java
+ * :934): during auto mode all input except the stop controls is frozen. The FX overlay
+ * covers the main area only — the menu bar, the toolbar (with the stop controls) and the
+ * status bar stay interactive.
+ *
+ * Glass-pane scope decision (WP-1, KNOWN-SIMPLIFIED row {@code MainWindowF.java:2659},
+ * closed WONT-REPLICATE): Swing's glass pane covered the whole content pane and
+ * re-delivered events only to components marked {@code isAutoButton} (the automation/stop
+ * buttons and the status-line abort button), so the menu bar was blocked while frozen. The
+ * FX overlay keeps the menu bar, toolbar and status bar clickable — deliberately: their
+ * proof actions are disabled during auto mode anyway (the enablement binds to
+ * {@code mediator.autoModeRunningProperty()}), so blocking clicks on inert controls adds no
+ * protection; the automation macro items are enabled during auto mode by Swing parity so a
+ * click stops the run (MacroAutomationAction.java:48-61), and blocking them would regress
+ * that intent; and the status bar must stay live for the progress display. The stop
+ * controls the Swing glass pane kept live are the same controls the FX toolbar keeps live.
+ */
+ private void freezeExceptAutoModeButton() {
+ inputBlocker.setVisible(true);
+ }
+
+ /** inputfreeze (P1) — Swing {@code MainWindow.unfreezeExceptAutoModeButton} (:944). */
+ private void unfreezeExceptAutoModeButton() {
+ inputBlocker.setVisible(false);
+ }
+
+ private final AutoModeListener autoModeUiListener = new AutoModeListener() {
+ @Override
+ public void autoModeStarted(ProofEvent e) {
+ LOGGER.info("Auto mode started");
+ FxUtil.runLater(this::freeze);
+ }
+
+ @Override
+ public void autoModeStopped(ProofEvent e) {
+ // inputfreeze (P1): unfreeze before the demo-live early return — every stop must
+ // undo the freeze
+ FxUtil.runLater(this::unfreeze);
+ if (System.getProperty("key.fx.demo.autoprove.live") != null) {
+ return; // the demo listener handles the final state incl. the verification reports
+ }
+ FxUtil.runLater(() -> {
+ refreshViewsFromFinalState();
+ // notification: the proof-closed / "Automated proof search" notifications are
+ // fired inside refreshViewsFromFinalState (the framework's only auto-mode-stop
+ // hook, covering the interactive and the demo-live run)
+ LOGGER.info("Views refreshed after the auto mode stop");
+ });
+ }
+
+ /** inputfreeze (P1): shows the blocking overlay (Swing freezeExceptAutoModeButton). */
+ private void freeze() {
+ freezeExceptAutoModeButton();
+ }
+
+ /** inputfreeze (P1): hides the blocking overlay (Swing unfreezeExceptAutoModeButton). */
+ private void unfreeze() {
+ unfreezeExceptAutoModeButton();
+ }
+ };
+
+ private void setWindowIcons() {
+ Image icon = new Image(MainWindowF.class.getResourceAsStream(IMAGE_DIR
+ + "key-color-icon-square.png"));
+ if (!icon.isError()) {
+ stage.getIcons().add(icon);
+ }
+ }
+
+ // ------------------------------------------------------------------
+ // dockables & default layout
+ // ------------------------------------------------------------------
+
+ private void buildDockables() {
+ // drawer: MP10 — the Loaded Proofs / Goal List / Proof Tree / Info / Strategy panels
+ // moved out of the docking area into the west drawer (buildDrawerHosts), the Source view
+ // into the east drawer; the docking centre now only hosts the sequent (its dock actions,
+ // layout slots and shutdown persistence keep working). The left/right dock-layout
+ // registrations are gone, so DockLayoutStore was bumped to version 2: older persisted
+ // layouts referencing the removed dockables are discarded (they were the pre-MP10
+ // arrangement) and the workspace falls back to the factory default — the sequent only.
+ dockables.put(ID_SEQUENT, sequentDockable(ID_SEQUENT, "Sequent", sequentView));
+ }
+
+ /**
+ * Creates a dockable that cannot be closed by the user (reviewer request: the sequent is the
+ * main view — it has no close button in the tab header; the View menu's "Show Sequent" entry
+ * still brings it to front when it is merely not visible).
+ */
+ private SimpleDockable sequentDockable(String id, String title, javafx.scene.Node content) {
+ SimpleDockable dockable = new SimpleDockable(id, title, content);
+ dockable.setClosable(false);
+ return dockable;
+ }
+
+ /**
+ * drawer: MP10 — builds the west/east/south drawer hosts of the main window (Java port of
+ * the TornadoFX {@code Drawer}). The panels of the Swing left tab area / right dock become
+ * drawer items: a toggle button per panel in the drawer's button bar; expanding a button
+ * shows the panel in the split next to the bar, always in the order of the buttons. The
+ * drawers are multiselect (the tornadofx "Multiselect" mode) so Proof Tree and Goal List
+ * share the west split; the per-item header distinguishes them there.
+ *
+ * Drag and drop (added on top of the original implementation) turns this into a docking
+ * UI: dragging a button onto another drawer moves the panel to that port (e.g. the Strategy
+ * panel from west to east), dragging within a bar reorders the panels in the split. The
+ * south drawer starts empty — it is the drop port for panels dragged there and the future
+ * home of a log console; the log view stays a separate window ({@link #showLogView()}),
+ * matching the Swing status-line behaviour.
+ */
+ private void buildDrawerHosts() {
+ westDrawer = new DrawerF(Side.LEFT, true);
+ // reviewer request: "Loaded Proofs" is the first item in the drawer (button order) and,
+ // together with the Proof Tree, expanded at startup — the Goal List stays collapsed.
+ // Reviewer request: the toggle buttons show icons — the glyphs mirror the tab icons of
+ // the Swing panels (ProofTreeView.getIcon PROOF_TREE, GoalList GOAL_LIST_ICON
+ // FLAG_CHECKERED, InfoView INFO_ICON INFO_CIRCLE,
+ // StrategySelectionView.getIcon PROOF_SEARCH_STRATEGY; the proof list has no Swing tab
+ // icon and gets a semantic list glyph).
+ westDrawer.item("Loaded Proofs", IconFactoryF.createIcon(IconFactoryF.Key.PROOF_LIST),
+ loadedProofs, true);
+ westDrawer.item("Proof Tree", IconFactoryF.createIcon(IconFactoryF.Key.PROOF_TREE),
+ proofTreeView, true);
+ westDrawer.item("Goal List", IconFactoryF.createIcon(IconFactoryF.Key.GOAL_LIST),
+ goalListView, false);
+ westDrawer.item("Info", IconFactoryF.createIcon(IconFactoryF.Key.INFO_VIEW), infoView,
+ false);
+ westDrawer.item("Strategy",
+ IconFactoryF.createIcon(IconFactoryF.Key.PROOF_SEARCH_STRATEGY), strategyView, false);
+ // extension: MP10 — the extension left-panel tabs become west drawer items (Swing
+ // KeYGuiExtension.LeftPanel returns tabs for the left JTabbedPane,
+ // KeYGuiExtension.java:127-143): a button per tab in the west bar toggles the panel,
+ // drag and drop can move it to another port; without LeftPanelF providers this appends
+ // nothing. The tab's own graphic (if the extension provides one) becomes the toggle
+ // button icon.
+ for (Tab tab : KeYGuiExtensionFacadeF.getLeftPanelTabs(this, mediator)) {
+ westDrawer.item(tab.getText(), tab.getGraphic(), tab.getContent(), false);
+ }
+ eastDrawer = new DrawerF(Side.RIGHT, true);
+ eastDrawer.item("Source", IconFactoryF.createIcon(IconFactoryF.Key.SOURCE_VIEW),
+ buildSourceViewContent(), false);
+ southDrawer = new DrawerF(Side.BOTTOM, true);
+ }
+
+ /**
+ * The content of the source view dockable: the source area with a one-line header showing
+ * the currently displayed file (the standalone driver shows the same header).
+ */
+ private Node buildSourceViewContent() {
+ Label header = new Label(SourceViewF.NO_SOURCE);
+ header.getStyleClass().add("source-view-header");
+ header.textProperty().bind(sourceView.headerTextProperty());
+ header.setTextOverrun(OverrunStyle.ELLIPSIS);
+ return new BorderPane(sourceView, header, null, null, null);
+ }
+
+ private List defaultLayout() {
+ // drawer: MP10 — only the sequent remains dockable; the west/east/south panels are
+ // drawer items (buildDrawerHosts) and the extension left panels are west drawer items
+ List defaults = new ArrayList<>();
+ defaults.add(new DockWorkspace.Default(DockLocation.MAIN, requireDockable(ID_SEQUENT)));
+ return defaults;
+ }
+
+ private void restoreLayout() {
+ try {
+ workspace.restoreLayout(layoutStore, id -> Optional.ofNullable(dockables.get(id)));
+ } catch (IOException e) {
+ LOGGER.warn("Could not restore docking layout: {}", e.getMessage());
+ workspace.restoreFactoryDefault();
+ }
+ }
+
+ private Node placeholderContent(String title) {
+ VBox box = new VBox(8);
+ box.setAlignment(Pos.CENTER);
+ Label heading = new Label(title);
+ heading.getStyleClass().add("view-title");
+ Label hint = new Label("Placeholder — the view arrives in milestone M2.");
+ hint.getStyleClass().add("view-hint");
+ box.getChildren().addAll(heading, hint);
+ return box;
+ }
+
+ private Dockable requireDockable(String id) {
+ return dockables.get(id);
+ }
+
+ // ------------------------------------------------------------------
+ // menu bar, toolbars
+ // ------------------------------------------------------------------
+
+ private VBox buildTop() {
+ VBox top = new VBox();
+ MenuBar menuBar = buildMenuBar();
+ HBox toolBarArea = new HBox(buildFileToolBar(), buildProofToolBar());
+ // extension: MP9.0 — a third toolbar holds the controls contributed by the FX
+ // extensions (Swing MainWindow.appendToolbar /
+ // KeYGuiExtensionFacade.createToolbars, KeYGuiExtensionFacade.java:217-221: each
+ // extension toolbar is embedded next to the built-in file/proof toolbars); shown only
+ // when an extension contributes controls.
+ List extensionToolbarControls =
+ KeYGuiExtensionFacadeF.getToolbarControls(this, mediator);
+ if (!extensionToolbarControls.isEmpty()) {
+ ToolBar extensionToolBar = new ToolBar();
+ extensionToolBar.getStyleClass().add("key-extension-tool-bar");
+ extensionToolBar.getItems().addAll(extensionToolbarControls);
+ toolBarArea.getChildren().add(extensionToolBar);
+ }
+ toolBarArea.getStyleClass().add("key-toolbar-area");
+ top.getChildren().addAll(menuBar, toolBarArea);
+ return top;
+ }
+
+ private MenuBar buildMenuBar() {
+ MenuBar menuBar = new MenuBar();
+ menuBar.getMenus().addAll(buildFileMenu(), buildViewMenu(), buildProofMenu(),
+ buildOptionsMenu(), buildAboutMenu());
+ // extension: MP9.0 — the extension-contributed menus are appended after the About menu
+ // as NEW separate menu-bar menus (Swing MainWindow.createMenuBar :983 calls
+ // KeYGuiExtensionFacade.addExtensionsToMainMenu after the built-in menus,
+ // KeYGuiExtensionFacade.java:81-89; the Swing original groups the extension actions
+ // into one "Extensions" JMenu, the FX SPI contributes whole Menu objects — the
+ // grouping decision stays with the extension). P4 (B13): KeYGuiExtensionFacadeF.
+ // installMenus additionally splices providers with a non-empty KeyAction.PATH-style
+ // getMenuPath() into existing or new menus of the bar (first segment matched by text,
+ // deeper segments by sub-menu text — the Swing sortActionIntoMenu semantics). Extensions
+ // with the empty path keep the above default. The five built-in menus' item sets stay
+ // untouched: key.fx.verify.menuparity keeps asserting 16/24/12/7/5.
+ KeYGuiExtensionFacadeF.installMenus(this, menuBar, mediator);
+ return menuBar;
+ }
+
+ private Menu buildFileMenu() {
+ // the five built-in menus carry Swing-parity mnemonics (MainWindow.java:993 VK_F)
+ Menu file = new Menu("_File");
+ MenuItem openFile = menuItem("Open File…", "de.uka.ilkd.key.gui.actions.OpenFileAction",
+ IconFactoryF.Key.OPEN_KEY_FILE, this::openFileChooser);
+ openFile.disableProperty().bind(mediator.autoModeRunningProperty());
+ MenuItem reload =
+ menuItem("Reload", "de.uka.ilkd.key.gui.actions.OpenMostRecentFileAction",
+ this::reloadLastFile);
+ // Swing OpenMostRecentFileAction: enabled when a proof is loaded (the action then reloads
+ // the most recent file; disabled during auto mode like all interaction)
+ reload.disableProperty()
+ .bind(mediator.autoModeRunningProperty().or(proofLoaded.not()));
+ // menu: MP5 — edit the most recently opened file in the default external editor (Swing
+ // EditMostRecentFileAction, placed right after Reload like MainWindow.createFileMenu
+ // :997-998). Swing enables the action as long as a recent file exists (binding to the
+ // most-recent-file state) and opens the file with Desktop.open/EditFileActionHandler; the
+ // FX port opens the file's file:// URI through the HelpFacadeF browser seam (host
+ // services, like the About browser actions).
+ MenuItem editLastOpenedFile =
+ menuItem("Edit Last Opened File",
+ "de.uka.ilkd.key.gui.actions.EditMostRecentFileAction",
+ IconFactoryF.Key.EDIT, this::editLastOpenedFile);
+ // menu: MP5 — bound to the RecentFilesF "has a recent file" property (the Swing original
+ // re-checks the list when the menu is shown); disabled during auto mode like the other
+ // file actions.
+ editLastOpenedFile.disableProperty().bind(mediator.autoModeRunningProperty()
+ .or(recentFiles.hasRecentFileProperty().not()));
+ MenuItem saveFile = menuItem("Save File…", "de.uka.ilkd.key.gui.actions.SaveFileAction",
+ IconFactoryF.Key.SAVE_FILE, this::saveProofFile);
+ // Swing SaveFileAction: enableWhenProofLoaded; interaction is locked during auto mode
+ saveFile.disableProperty()
+ .bind(mediator.autoModeRunningProperty().or(proofLoaded.not()));
+ MenuItem openExample =
+ menuItem("Open Example…", "de.uka.ilkd.key.gui.actions.OpenExampleAction",
+ IconFactoryF.Key.OPEN_KEY_FILE, this::openExampleChooser);
+ // Swing disables all actions during auto mode (like the Open File action)
+ openExample.disableProperty().bind(mediator.autoModeRunningProperty());
+ MenuItem saveBundle =
+ menuItem("Save Bundle…", "de.uka.ilkd.key.gui.actions.SaveBundleAction",
+ this::saveProofBundle);
+ // Swing SaveBundleAction: enabled only when a proof is selected (updateStatus); disabled
+ // during auto mode like all interaction
+ saveBundle.disableProperty()
+ .bind(mediator.autoModeRunningProperty().or(proofLoaded.not()));
+ MenuItem quickSave =
+ menuItem("Quick Save", "de.uka.ilkd.key.gui.actions.QuickSaveAction", this::quickSave);
+ // Swing QuickSaveAction: enableWhenProofLoaded; disabled during auto mode
+ quickSave.disableProperty()
+ .bind(mediator.autoModeRunningProperty().or(proofLoaded.not()));
+ MenuItem quickLoad =
+ menuItem("Quick Load", "de.uka.ilkd.key.gui.actions.QuickLoadAction", this::quickLoad);
+ // Swing QuickLoadAction has no proof enablement — it always tries to load the quick save
+ // location (a missing file fails the load); disabled during auto mode like all interaction
+ quickLoad.disableProperty().bind(mediator.autoModeRunningProperty());
+ // proofmgmt: Swing ProofManagementAction (menu + toolbar button): opens the Proof
+ // Management dialog for the most recently loaded problem's init config
+ MenuItem proofManagement =
+ menuItem("Proof Management…", "de.uka.ilkd.key.gui.actions.ProofManagementAction",
+ IconFactoryF.Key.PROOF_MANAGEMENT, this::openProofManagement);
+ proofManagement.disableProperty().bind(mediator.autoModeRunningProperty());
+ // menu: MP5 — load user-defined taclets into the current proof (Swing
+ // LemmaGenerationAction.ProveAndAddTaclets, Mode.LOAD; after Proof Management like
+ // MainWindow.createFileMenu :1019-1021). A proof is required (Swing proofIsRequired()).
+ MenuItem loadUserDefinedTaclets =
+ menuItem("Load User Defined Taclets…",
+ "de.uka.ilkd.key.gui.actions.LemmaGenerationAction$ProveAndAddTaclets",
+ this::loadUserDefinedTaclets);
+ loadUserDefinedTaclets.disableProperty().bind(mediator.autoModeRunningProperty()
+ .or(proofLoaded.not()));
+ file.getItems().addAll(
+ openExample,
+ openFile,
+ reload,
+ editLastOpenedFile,
+ new SeparatorMenuItem(),
+ proofManagement,
+ loadUserDefinedTaclets,
+ buildProveSubmenu(),
+ saveFile,
+ saveBundle,
+ quickSave,
+ quickLoad,
+ new SeparatorMenuItem(),
+ recentFilesMenu,
+ new SeparatorMenuItem(),
+ menuItem("Exit", "de.uka.ilkd.key.gui.actions.ExitMainAction",
+ IconFactoryF.Key.QUIT, this::exitApplication));
+ return file;
+ }
+
+ /**
+ * menu: MP5 — the File>Prove submenu (Swing {@code MainWindow.createFileMenu} :1022-1031:
+ * {@code Load User Defined Taclets for Proving} = {@code LemmaGenerationAction
+ * .ProveUserDefinedTaclets}, {@code Load KeY Taclets} = {@code LemmaGenerationAction
+ * .ProveKeYTaclets}, {@code Lemma Generation (Batch Mode)} = {@code
+ * LemmaGenerationBatchModeAction} info dialog, and the {@code Run All Proofs} QA action
+ * behind the {@link FeatureSettings} flag {@code BULK_UI_TEST}). The Prove entries need no
+ * proof (Swing {@code proofIsRequired() == false}) and are only disabled during auto mode.
+ */
+ private Menu buildProveSubmenu() {
+ Menu prove = new Menu("Prove");
+ MenuItem proveUserDefined = menuItem("Load User Defined Taclets for Proving",
+ "de.uka.ilkd.key.gui.actions.LemmaGenerationAction$ProveUserDefinedTaclets",
+ this::proveUserDefinedTaclets);
+ proveUserDefined.disableProperty().bind(mediator.autoModeRunningProperty());
+ MenuItem proveKeYTaclets = menuItem("Load KeY Taclets",
+ "de.uka.ilkd.key.gui.actions.LemmaGenerationAction$ProveKeYTaclets",
+ this::proveKeYTaclets);
+ proveKeYTaclets.disableProperty().bind(mediator.autoModeRunningProperty());
+ MenuItem lemmaBatchMode = menuItem("Lemma Generation (Batch Mode)",
+ "de.uka.ilkd.key.gui.actions.LemmaGenerationBatchModeAction",
+ this::showLemmaGenerationBatchMode);
+ lemmaBatchMode.disableProperty().bind(mediator.autoModeRunningProperty());
+ prove.getItems().addAll(proveUserDefined, proveKeYTaclets, lemmaBatchMode);
+
+ // menu: MP5 — "Run All Proofs" behind the BULK_UI_TEST feature flag, mirroring the
+ // Swing registration (FeatureSettings.onAndActivate(FEATURE_BULK_UI_TEST,
+ // showRAPAction), MainWindow.java:1023-1027). The item's visibility follows the flag;
+ // the listener is registered at most once — the parity self test rebuilds the menu bar,
+ // which would otherwise stack duplicate listeners on the shared FeatureSettings.
+ MenuItem runAllProofs = menuItem("Run All Proofs",
+ "de.uka.ilkd.key.gui.actions.RunAllProofsAction", this::runAllProofs);
+ runAllProofs.setVisible(FeatureSettings.isFeatureActivated(FEATURE_BULK_UI_TEST));
+ if (!bulkUiTestListenerRegistered) {
+ bulkUiTestListenerRegistered = true;
+ FeatureSettings.onAndActivate(FEATURE_BULK_UI_TEST, runAllProofs::setVisible);
+ }
+ prove.getItems().add(runAllProofs);
+ return prove;
+ }
+
+ private Menu buildViewMenu() {
+ // Swing parity: View menu mnemonic VK_V (MainWindow.java:1035)
+ Menu view = new Menu("_View");
+ // menu: MP3a — Pretty Print toggle (Swing PrettyPrintToggleAction,
+ // PrettyPrintToggleAction.java:45-59): updateSelectedState mirrors the settings into the
+ // NotationInfo static and the selected state; actionPerformed sets the static BEFORE the
+ // ViewSettings are modified, because the UI reacts on the settings change event (the
+ // printers consult the static at construction). The re-render mirrors
+ // MainWindow.makePrettyView (MainWindow.java:954-959).
+ ViewSettings viewSettings = ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings();
+ CheckMenuItem prettyPrint = new CheckMenuItem("Pretty Print");
+ NotationInfo.DEFAULT_PRETTY_SYNTAX = viewSettings.isUsePretty();
+ prettyPrint.setSelected(viewSettings.isUsePretty());
+ prettyPrint.setOnAction(e -> {
+ boolean selected = prettyPrint.isSelected();
+ // Swing: "Needs to be executed before the ViewSettings are modified, because the UI
+ // will react on the settings change event!" (PrettyPrintToggleAction.java:55-57)
+ NotationInfo.DEFAULT_PRETTY_SYNTAX = selected;
+ viewSettings.setUsePretty(selected);
+ refreshPrettyViews();
+ });
+ // menu: MP3a — Unicode toggle (Swing UnicodeToggleAction, UnicodeToggleAction.java:47-69):
+ // only meaningful in combination with pretty printing (updateSelectedState:
+ // setEnabled(usePretty), setSelected(useUnicode && usePretty)); the disable binding
+ // replaces Swing's setEnabled(usePretty).
+ CheckMenuItem unicode = new CheckMenuItem("Unicode Symbols");
+ unicode.setSelected(viewSettings.isUseUnicode() && viewSettings.isUsePretty());
+ unicode.setDisable(!viewSettings.isUsePretty());
+ unicode.disableProperty().bind(prettyPrint.selectedProperty().not());
+ unicode.setOnAction(e -> {
+ boolean selected = unicode.isSelected();
+ boolean pretty = viewSettings.isUsePretty();
+ // before the ViewSettings are modified, like the Swing original
+ // (UnicodeToggleAction.java:63)
+ NotationInfo.DEFAULT_UNICODE_ENABLED = selected && pretty;
+ viewSettings.setUseUnicode(selected);
+ refreshPrettyViews();
+ });
+ CheckMenuItem syntaxHighlighting = new CheckMenuItem("Syntax Highlighting");
+ syntaxHighlighting.setSelected(sequentView.isSyntaxHighlightingEnabled());
+ syntaxHighlighting.setOnAction(
+ e -> sequentView.setSyntaxHighlightingEnabled(syntaxHighlighting.isSelected()));
+
+ // menu: MP3a — tooltip toggles (Swing ToggleSequentViewTooltipAction /
+ // ToggleSourceViewTooltipAction / ToggleProofTreeTooltipAction, MainWindow.createViewMenu
+ // :1042-1044): each persists the shared ViewSettings flag like the Swing actionPerformed
+ // implementations. The sequent view consults isShowSequentViewTooltips() in its tooltip
+ // code (SequentViewF.getTooltipText, SequentViewF.java:824-835), so the toggle needs no
+ // re-render; the proof tree re-creates its cell tooltips on refresh().
+ CheckMenuItem showSequentViewTooltips = new CheckMenuItem("Show Tooltips in Sequent View");
+ showSequentViewTooltips.setSelected(viewSettings.isShowSequentViewTooltips());
+ showSequentViewTooltips.setOnAction(
+ e -> viewSettings.setShowSequentViewTooltips(showSequentViewTooltips.isSelected()));
+ CheckMenuItem showSourceViewTooltips = new CheckMenuItem("Show Tooltips in Source View");
+ showSourceViewTooltips.setSelected(viewSettings.isShowSourceViewTooltips());
+ // menu: MP3a — no source view in the FX UI: the toggle only persists the flag (Swing
+ // ToggleSourceViewTooltipAction, ToggleSourceViewTooltipAction.java:58-62)
+ showSourceViewTooltips.setOnAction(
+ e -> viewSettings.setShowSourceViewTooltips(showSourceViewTooltips.isSelected()));
+ CheckMenuItem showProofTreeTooltips = new CheckMenuItem("Show Tooltips in Proof Tree");
+ showProofTreeTooltips.setSelected(viewSettings.isShowProofTreeTooltips());
+ showProofTreeTooltips.setOnAction(e -> {
+ viewSettings.setShowProofTreeTooltips(showProofTreeTooltips.isSelected());
+ // re-render the cells so the per-cell tooltip appears/disappears immediately (the
+ // cell tooltip consults the flag, see ProofTreeViewF.ProofTreeCell.updateItem)
+ proofTreeView.refresh();
+ });
+
+ // menu: MP3a — the Ctrl+P / Ctrl+U accelerators of the Swing KeyStrokeManager
+ // (KeyStrokeManagerF.registerDefaults) apply to the check items like to the plain
+ // menuItem() factory items.
+ KeyStrokeManagerF shortcuts = KeyStrokeManagerF.getInstance();
+ shortcuts.binding("de.uka.ilkd.key.gui.actions.PrettyPrintToggleAction")
+ .ifPresent(prettyPrint::setAccelerator);
+ shortcuts.register(prettyPrint, "de.uka.ilkd.key.gui.actions.PrettyPrintToggleAction");
+ shortcuts.binding("de.uka.ilkd.key.gui.actions.UnicodeToggleAction")
+ .ifPresent(unicode::setAccelerator);
+ shortcuts.register(unicode, "de.uka.ilkd.key.gui.actions.UnicodeToggleAction");
+
+ ToggleGroup themeGroup = new ToggleGroup();
+ RadioMenuItem lightTheme = new RadioMenuItem("Light Theme");
+ RadioMenuItem darkTheme = new RadioMenuItem("Dark Theme");
+ lightTheme.setToggleGroup(themeGroup);
+ darkTheme.setToggleGroup(themeGroup);
+ (ThemeManager.getInstance().getTheme() == Theme.DARK ? darkTheme : lightTheme)
+ .setSelected(true);
+ lightTheme.setOnAction(e -> setTheme(Theme.LIGHT));
+ darkTheme.setOnAction(e -> setTheme(Theme.DARK));
+ // theme switches from elsewhere (e.g. the settings dialog) update the menu radios
+ ThemeManager.getInstance().themeProperty().addListener(
+ (obs, old, theme) -> (theme == Theme.DARK ? darkTheme : lightTheme).setSelected(true));
+
+ Menu themeMenu = new Menu("Theme");
+ themeMenu.getItems().addAll(lightTheme, darkTheme);
+
+ Menu fontSize = new Menu("Font Size");
+ // P3b/B18: Swing order + labels of the Font Size submenu
+ // (MainWindow.createViewMenu :1048-1051: first DecreaseFontSizeAction = "Smaller" with the
+ // minus icon, then IncreaseFontSizeAction = "Larger"; DecreaseFontSizeAction.java:30 /
+ // IncreaseFontSizeAction.java:30 set NAME to "Smaller"/"Larger", the menus show the minus
+ // icon for "Smaller" and the plus icon for "Larger").
+ fontSize.getItems().addAll(
+ menuItem("Smaller", "de.uka.ilkd.key.gui.actions.DecreaseFontSizeAction",
+ IconFactoryF.Key.MINUS, () -> changeFontSize(-1)),
+ menuItem("Larger", "de.uka.ilkd.key.gui.actions.IncreaseFontSizeAction",
+ IconFactoryF.Key.PLUS, () -> changeFontSize(1)));
+
+ // menu: MP3a — ToolTip Options right after Font Size, before the diff frame, like Swing
+ // MainWindow.createViewMenu :1053 (ToolTipOptionsAction → ViewSelector,
+ // ToolTipOptionsAction.java:26 / ViewSelector.java:26-27).
+ MenuItem toolTipOptions = menuItem("ToolTip Options…",
+ "de.uka.ilkd.key.gui.actions.ToolTipOptionsAction", this::showToolTipOptions);
+
+ // smalldialogs: the soundiness report (Swing ShowSoundinessAction, contributed to
+ // the proof-list context menu by SoundinessExtension). The FX proof-list dockable
+ // does not exist yet, so the action lives in the View menu; Swing
+ // enableWhenProofLoaded + the general auto-mode lock carry over. Created as a
+ // named item so the enablement binding below stays correct after the lemmaorigin
+ // addAll runs (the index-from-end trick of the standalone port no longer applies).
+ javafx.scene.control.MenuItem soundinessItem = menuItem("Show Soundiness Report",
+ "de.uka.ilkd.key.gui.actions.ShowSoundinessAction", this::showSoundinessReport);
+
+ // menu: MP3b — end of the View menu, mirroring Swing MainWindow.createViewMenu
+ // :1057-1064: separator, Select Goal submenu (createSelectionMenu :1069-1074, the
+ // GoalSelectAboveAction / GoalSelectBelowAction call
+ // mainWindow.getProofTreeView().selectAbove()/selectBelow(),
+ // GoalSelectAboveAction.java:31-33), separator, Back/Forward over the SelectionHistory
+ // controller (SelectionBackAction/SelectionForwardAction, SelectionHistory.java), and a
+ // trailing separator. The Ctrl+K / Ctrl+J / Ctrl+Alt+Left / Ctrl+Alt+Right accelerators
+ // arrive via the actionIds from KeyStrokeManagerF.registerDefaults (:122-123, :134-135).
+ // The Select Goal items are proof-gated (Swing MainWindowAction enableWhenProofLoaded);
+ // Back/Forward are enabled purely by the history like the Swing actions.
+ Menu selectGoal = new Menu("Select Goal");
+ MenuItem goalSelectAbove = menuItem("Select Goal Above",
+ "de.uka.ilkd.key.gui.actions.GoalSelectAboveAction", this::selectGoalAbove);
+ goalSelectAbove.disableProperty().bind(proofLoaded.not());
+ MenuItem goalSelectBelow = menuItem("Select Goal Below",
+ "de.uka.ilkd.key.gui.actions.GoalSelectBelowAction", this::selectGoalBelow);
+ goalSelectBelow.disableProperty().bind(proofLoaded.not());
+ selectGoal.getItems().addAll(goalSelectAbove, goalSelectBelow);
+ MenuItem selectionBack = menuItem("Back",
+ "de.uka.ilkd.key.gui.actions.SelectionBackAction", IconFactoryF.Key.PREVIOUS,
+ selectionHistory::navigateBack);
+ selectionBack.disableProperty().bind(selectionHistory.canGoBackProperty().not());
+ MenuItem selectionForward = menuItem("Forward",
+ "de.uka.ilkd.key.gui.actions.SelectionForwardAction", IconFactoryF.Key.NEXT,
+ selectionHistory::navigateForward);
+ selectionForward.disableProperty().bind(selectionHistory.canGoForwardProperty().not());
+
+ view.getItems().addAll(
+ // reviewer request: the sequent dockable is closable but could not be restored —
+ // this entry reselects it in the workspace (MP10 keeps only the sequent dockable,
+ // so a single restore entry covers every closable view)
+ menuItem("Show Sequent", () -> workspace.select(dockables.get(ID_SEQUENT))),
+ prettyPrint, unicode, syntaxHighlighting);
+ // lemmaorigin: begin — term labels + origin tracking view controls (Swing TermLabelMenu /
+ // HidePackagePrefixToggleAction / OriginTermLabelsExt MainMenu items)
+ // menu: MP3a — moved to the Swing position right after Syntax Highlighting
+ // (MainWindow.createViewMenu :1040-1041 appends termLabelMenu and hidePackagePrefix before
+ // the tooltip toggles), so the View menu follows the Swing order
+ view.getItems().addAll(OriginLabelsF.install(this));
+ // lemmaorigin: end
+ view.getItems().addAll(showSequentViewTooltips, showSourceViewTooltips,
+ showProofTreeTooltips,
+ new SeparatorMenuItem(),
+ themeMenu, fontSize, toolTipOptions, new SeparatorMenuItem(),
+ menuItem("Visual Node Diff", "de.uka.ilkd.key.gui.proofdiff.ProofDiffFrame$Action",
+ this::showProofDiffFrame),
+ new SeparatorMenuItem(),
+ // docking: named layout slots (Swing DockingLayout, menu path View > Layout)
+ dockingLayout.layoutMenu(),
+ // seam: the Swing LogView is opened by a status-line extension button
+ // (ShowLogAction; no FX extension SPI yet), so the FX entry point is a View menu
+ // item (LogViewF.showInstance)
+ menuItem("Log View", "de.uka.ilkd.key.gui.actions.LogViewAction",
+ this::showLogView),
+ soundinessItem,
+ new SeparatorMenuItem(), selectGoal, new SeparatorMenuItem(),
+ selectionBack, selectionForward, new SeparatorMenuItem());
+ soundinessItem.disableProperty()
+ .bind(mediator.autoModeRunningProperty().or(proofLoaded.not()));
+ return view;
+ }
+
+ /**
+ * menu: MP3b — selects the next open goal above the current tree selection (Swing
+ * {@code GoalSelectAboveAction.actionPerformed} →
+ * {@code mainWindow.getProofTreeView().selectAbove()}, GoalSelectAboveAction.java:25-34).
+ */
+ private void selectGoalAbove() {
+ proofTreeView.selectAbove();
+ }
+
+ /**
+ * menu: MP3b — selects the next open goal below the current tree selection (Swing
+ * {@code GoalSelectBelowAction.actionPerformed} →
+ * {@code mainWindow.getProofTreeView().selectBelow()}, GoalSelectBelowAction.java:25-34).
+ */
+ private void selectGoalBelow() {
+ proofTreeView.selectBelow();
+ }
+
+ /**
+ * menu: MP3a — re-renders the sequent and goal list views after the Pretty Print / Unicode
+ * Symbols toggles (Swing {@code MainWindow.makePrettyView}, MainWindow.java:954-959: refresh
+ * the mediator's shared NotationInfo against the services and re-display the sequent). The FX
+ * views build their own NotationInfo per print (they do not use the mediator's shared
+ * instance yet) and expose the re-render as {@code refreshPrettyView} hooks; the goal list
+ * prints terms too, so it is refreshed the same way.
+ */
+ private void refreshPrettyViews() {
+ sequentView.refreshPrettyView();
+ goalListView.refreshPrettyView();
+ }
+
+ /**
+ * menu: MP3a — opens the tooltip options dialog (Swing {@code ToolTipOptionsAction},
+ * ToolTipOptionsAction.java:26, constructs the {@code ViewSelector}). The dialog edits the
+ * shared {@code ViewSettings} directly.
+ */
+ private void showToolTipOptions() {
+ ToolTipOptionsDialogF.show(stage);
+ }
+
+ /**
+ * seam: opens the log view window (Swing {@code ShowLogAction} →
+ * {@code LogView.showInstance}, {@code LogView.java:88-126}).
+ */
+ private void showLogView() {
+ LogViewF.showInstance(getStage());
+ }
+
+ /**
+ * menu: MP2/MP7 — the proof macros of the Automation submenu (Swing
+ * {@code MainWindow.createAutomationActions}, MainWindow.java:814-827, in the same order:
+ * DefaultAutoMacro, FullAutoPilotProofMacro, AutoPilotPrepareProofMacro, ScriptAwareMacro).
+ * Shared with the sequent-view right-click macro popup (MP7; Swing {@code
+ * ProofMacroMenu.REGISTERED_MACROS} is the ServiceLoader superset, ProofMacroMenu.java:60-61
+ * — the FX popup mirrors the app's own Automation submenu instead, see
+ * SequentViewF#buildMacroPopup).
+ */
+ public static final List AUTOMATION_MACROS =
+ List.of(new DefaultAutoMacro(), new FullAutoPilotProofMacro(),
+ new AutoPilotPrepareProofMacro(), new ScriptAwareMacro());
+
+ private Menu buildProofMenu() {
+ // Swing parity: Proof menu mnemonic VK_P (MainWindow.java:1084)
+ Menu proof = new Menu("_Proof");
+ Menu automation = new Menu("Automation");
+ MenuItem startAuto =
+ menuItem("Start Automatic Proof", "de.uka.ilkd.key.gui.actions.AutoModeAction",
+ IconFactoryF.Key.AUTO_MODE_START, mediator::startAutoMode);
+ startAuto.disableProperty().bind(mediator.autoModeRunningProperty());
+ MenuItem stopAuto = menuItem("Stop Automatic Proof", IconFactoryF.Key.AUTO_MODE_STOP,
+ mediator::stopAutoMode);
+ stopAuto.disableProperty().bind(mediator.autoModeRunningProperty().not());
+ automation.getItems().add(startAuto);
+ automation.getItems().add(stopAuto);
+ // menu: MP2 — after Start/Stop Automatic Proof the Automation submenu mirrors the four
+ // proof-macro entries of Swing MainWindow.createAutomationActions (MainWindow.java:814-827,
+ // MacroAutomationAction.java:40-45), same order, item text = macro.getName(). The Swing
+ // icons (IconFactory.automationWithOverlay(TOOLBAR_ICON_SIZE, "A"|"S"|"P"|"J"),
+ // MainWindow.java:817-827) have no FX counterpart in IconFactoryF.Key, so the items pass
+ // null.
+ // The actionId is the FQN binding key of KeyStrokeManagerF.registerDefaults
+ // (KeyStrokeManagerF.java:83-84), so the existing Ctrl+V / Ctrl+D accelerators of
+ // FullAutoPilotProofMacro / AutoPilotPrepareProofMacro are wired automatically (that was
+ // the point of the key binding); DefaultAutoMacro and ScriptAwareMacro have no default
+ // binding but are still registered with the manager for settings-driven rebinding.
+ // Enablement binds to proofLoaded only — deliberately NO auto-mode lock: Swing keeps the
+ // macro actions enabled while auto mode runs so a click stops the automation
+ // (MacroAutomationAction.actionPerformed, MacroAutomationAction.java:48-61).
+ // menu: MP7 — the items are built from the shared {@link #AUTOMATION_MACROS} list so the
+ // right-click macro popup of the sequent view (SequentViewF#buildMacroPopup) offers the
+ // very same macros.
+ for (ProofMacro macro : AUTOMATION_MACROS) {
+ MenuItem autoItem = menuItem(macro.getName(),
+ "de.uka.ilkd.key.macros." + macro.getClass().getSimpleName(), null,
+ () -> runMacro(macro));
+ autoItem.disableProperty().bind(proofLoaded.not());
+ automation.getItems().add(autoItem);
+ }
+ // menu: MP1 — the entries after Prune Proof mirror Swing MainWindow.createProofMenu
+ // (MainWindow.java:1082-1142) with selected == null in the same order: Abandon Proof,
+ // separator, the search group (Search in Proof Tree/Sequent + Next/Previous + the
+ // Search Mode submenu), separator, then Show Used Contracts / Show All Active Settings /
+ // Show Proof Statistics / Show Known Types. The search group and the
+ // statistics/settings group are proof-gated via proofLoaded (Swing
+ // enableWhenProofLoaded on each action); Abandon Proof additionally carries the
+ // auto-mode lock (Swing AbandonTaskAction is enabled whenever a proof is loaded, but the
+ // removal of a running proof stops auto mode first — keep the lock like the other
+ // interaction actions).
+ // menu: Abandon Proof — Swing AbandonTaskAction (AbandonTaskAction.java:13-46), reused
+ // actionId so the Ctrl+W accelerator from KeyStrokeManagerF (defineDefault
+ // AbandonTaskAction
+ // = modifier()+W, KeyStrokeManagerF.java:117) is bound; enablement mirrored from
+ // enableWhenProofLoaded + the auto-mode lock.
+ MenuItem abandonProof = menuItem("Abandon Proof",
+ "de.uka.ilkd.key.gui.actions.AbandonTaskAction",
+ IconFactoryF.Key.CLOSE, this::abandonProof);
+ abandonProof.disableProperty()
+ .bind(mediator.autoModeRunningProperty().or(proofLoaded.not()));
+ // menu: search group — Swing SearchInProofTreeAction / SearchInSequentAction /
+ // SearchNextAction / SearchPreviousAction (MainWindow.java:1121-1124) and the
+ // SearchModeChangeAction entries of the "Search Mode" submenu (:1125-1131). All bound
+ // only to proofLoaded (matches the FX read-only-action style: no auto-mode lock).
+ MenuItem searchInTree = menuItem("Search in Proof Tree",
+ "de.uka.ilkd.key.gui.actions.SearchInProofTreeAction",
+ IconFactoryF.Key.PROOF_TREE, proofTreeView::showSearchBar);
+ searchInTree.disableProperty().bind(proofLoaded.not());
+ MenuItem searchInSequent = menuItem("Search in Sequent",
+ "de.uka.ilkd.key.gui.actions.SearchInSequentAction",
+ IconFactoryF.Key.SEARCH, sequentView::showSearchBar);
+ searchInSequent.disableProperty().bind(proofLoaded.not());
+ MenuItem searchNext = menuItem("Search Next",
+ "de.uka.ilkd.key.gui.actions.SearchNextAction",
+ IconFactoryF.Key.NEXT, sequentView::searchNext);
+ searchNext.disableProperty().bind(proofLoaded.not());
+ MenuItem searchPrevious = menuItem("Search Previous",
+ "de.uka.ilkd.key.gui.actions.SearchPreviousAction",
+ IconFactoryF.Key.PREVIOUS, sequentView::searchPrevious);
+ searchPrevious.disableProperty().bind(proofLoaded.not());
+ Menu searchMode = new Menu("Search Mode");
+ for (SequentViewF.SearchMode mode : SequentViewF.SearchMode.values()) {
+ MenuItem modeItem = menuItem(mode.getDisplayName(),
+ () -> sequentView.setSearchMode(mode));
+ modeItem.disableProperty().bind(proofLoaded.not());
+ searchMode.getItems().add(modeItem);
+ }
+ // menu: statistics/settings group — Swing ShowUsedContractsAction (:1134,
+ // ProofManagementDialog with the selected proof preselected = openProofManagement()),
+ // ShowActiveSettingsAction (:1138, ActiveSettingsDialogF), ShowProofStatistics (:1139,
+ // ProofStatisticsDialogF) and ShowKnownTypesAction (:1140, KnownTypesDialogF). All
+ // proof-gated (Swing enableWhenProofLoaded).
+ MenuItem usedContracts = menuItem("Show Used Contracts",
+ "de.uka.ilkd.key.gui.actions.ShowUsedContractsAction",
+ this::openProofManagement);
+ usedContracts.disableProperty().bind(proofLoaded.not());
+ MenuItem activeSettings = menuItem("Show All Active Settings",
+ "de.uka.ilkd.key.gui.actions.ShowActiveSettingsAction",
+ IconFactoryF.Key.CONFIGURE, this::showActiveSettings);
+ activeSettings.disableProperty().bind(proofLoaded.not());
+ MenuItem proofStatistics = menuItem("Show Proof Statistics",
+ "de.uka.ilkd.key.gui.actions.ShowProofStatistics",
+ IconFactoryF.Key.STATISTICS, this::showProofStatistics);
+ proofStatistics.disableProperty().bind(proofLoaded.not());
+ MenuItem knownTypes = menuItem("Show Known Types",
+ "de.uka.ilkd.key.gui.actions.ShowKnownTypesAction",
+ this::showKnownTypes);
+ knownTypes.disableProperty().bind(proofLoaded.not());
+ // P3b/B11: the Goal Back item carries the dynamic label of Swing GoalBackAction
+ // (updateName, GoalBackAction.java:113-125: "Undo Last Rule Application" plus, when the
+ // newest goal's parent has an applied rule, " (RuleName)"); like Swing
+ // (MainWindow.java:1094-1105) the name is refreshed only when the Proof menu opens —
+ // scanning all subtree goals on every selection change would be too slow. The actionId
+ // binding is unchanged (KeyStrokeManagerF shortcut wiring).
+ // P3b/B15: Goal Back / Prune Proof are NOT proofLoaded-bound like the surrounding items:
+ // like the Swing actions (GoalBackAction.java:65-108, PruneProofAction.java:53-109) their
+ // enablement is driven by the selection + the noPruningClosed setting and updated from
+ // the selection change / auto-mode hooks (refreshGoalBackPrune, see updateProofStatus).
+ goalBackMenuItem = menuItem("Undo Last Rule Application",
+ "de.uka.ilkd.key.gui.actions.GoalBackAction",
+ IconFactoryF.Key.GOAL_BACK, mediator::goalBack);
+ pruneMenuItem = menuItem("Prune Proof", "de.uka.ilkd.key.gui.actions.PruneProofAction",
+ IconFactoryF.Key.PRUNE, mediator::pruneProof);
+ proof.getItems().addAll(automation, new SeparatorMenuItem(),
+ goalBackMenuItem, pruneMenuItem,
+ abandonProof, new SeparatorMenuItem(),
+ searchInTree, searchInSequent, searchNext, searchPrevious, searchMode,
+ new SeparatorMenuItem(),
+ usedContracts, activeSettings, proofStatistics, knownTypes);
+ // P3b/B11: refresh the Goal Back text when the Proof menu opens (Swing's MenuListener
+ // menuSelected, MainWindow.java:1094-1105)
+ proof.setOnShowing(e -> refreshGoalBackItem());
+ return proof;
+ }
+
+ /**
+ * P3b/B11: updates the "Goal Back" menu item text from the newest goal of the selected
+ * subtree (Swing {@code GoalBackAction.updateName}, GoalBackAction.java:113-125; called from
+ * the Proof menu's {@code setOnShowing} like Swing's {@code MenuListener.menuSelected},
+ * MainWindow.java:1094-1105). The toolbar button keeps its static tooltip (Swing's toolbar
+ * action is constructed with {@code longName = false},
+ * {@code new GoalBackAction(this, false)} at MainWindow.java:696). The stored reference may
+ * be a freshly rebuilt item (menu-parity self test rebuilds the menu bar).
+ */
+ private void refreshGoalBackItem() {
+ if (goalBackMenuItem == null) {
+ return;
+ }
+ String app = mediator.goalBackRuleName();
+ goalBackMenuItem
+ .setText("Undo Last Rule Application" + (app == null ? "" : " (" + app + ")"));
+ }
+
+ // ------------------------------------------------------------------
+ // proof menu actions (menu: MP1 — Swing MainWindow.createProofMenu :1082-1142)
+ // ------------------------------------------------------------------
+
+ /**
+ * menu: MP2 — runs a proof macro on the selected node (Swing
+ * {@code MacroAutomationAction.actionPerformed}, MacroAutomationAction.java:48-61): while auto
+ * mode is running the click only stops it (Swing {@code proofControl.stopAutoMode()}, where
+ * {@code proofControl = mediator.getUI().getProofControl()}); otherwise the macro runs on the
+ * selected node ({@code new ProofMacroUserAction(mediator, macro, null).actionPerformed(e)}).
+ * The macro-finished notifications are produced by {@link WindowUserInterfaceControlF}
+ * (:380-398), which already reacts to the macro-sourced {@code ProofEvent}s.
+ */
+ private void runMacro(ProofMacro macro) {
+ if (mediator.isInAutoMode()) {
+ mediator.stopAutoMode(); // Swing: proofControl.stopAutoMode()
+ } else if (lastEnvironment != null) {
+ lastEnvironment.getProofControl().runMacro(mediator.getSelectedNode(), macro, null);
+ }
+ }
+
+ /**
+ * Invokes a verify harness reflectively (reviewer request: the harness classes live in the
+ * test source sets — test code must not ship in the main sources — so {@code main} cannot
+ * reference them statically; the {@code run} task puts the test outputs on the runtime
+ * classpath, see the key.ui.fx build file). Locates the static method by parameter types
+ * and invokes it.
+ *
+ * @param className the fully qualified harness class name
+ * @param methodName the static method to invoke
+ * @param types the exact parameter types of the method
+ * @param args the arguments
+ * @return the harness result (a report string; {@code null} for {@code void} harnesses or
+ * when the harness class is not on the classpath)
+ */
+ private static Object runVerifyHarness(String className, String methodName, Class>[] types,
+ Object... args) {
+ try {
+ Class> harness = Class.forName(className);
+ return harness.getMethod(methodName, types).invoke(null, args);
+ } catch (ClassNotFoundException e) {
+ LOGGER.warn("Verify harness {} not on the classpath (test source set)", className);
+ return null;
+ } catch (ReflectiveOperationException | RuntimeException e) {
+ LOGGER.error("Verify harness {} failed", className, e);
+ return "FAIL (" + e + ")";
+ }
+ }
+
+ /**
+ * menu: abandons the selected proof (Swing {@code AbandonTaskAction.actionPerformed},
+ * AbandonTaskAction.java:33-46): asks for confirmation first (Swing
+ * {@code confirmTaskRemoval("Are you sure?")}, a YES/NO dialog titled "Abandon Proof",
+ * WindowUserInterfaceControl.java:341-345), stops auto mode if the proof is being proved
+ * automatically, disposes the proof and resets the UI to its "no proof" state.
+ */
+ private void abandonProof() {
+ Proof proof = selectionModel.getSelectedProof();
+ if (proof == null) {
+ return; // the item is disabled without a proof (Swing enableWhenProofLoaded)
+ }
+ Alert alert = new Alert(Alert.AlertType.CONFIRMATION, "Are you sure?");
+ alert.setTitle("Abandon Proof");
+ alert.setHeaderText(null);
+ alert.initOwner(stage);
+ ExampleChooserF.themeDialogPane(alert);
+ Optional result = alert.showAndWait();
+ if (result.isEmpty() || result.get() != ButtonType.OK) {
+ return; // Swing: confirmTaskRemoval returns false on No/close
+ }
+ // menu: stop auto mode through the window's proof control if a run is active (Swing
+ // getMediator().getUI().getProofControl().stopAutoMode()); lastEnvironment is the loaded
+ // env of the selection (like the other proof-dependent flows, see :603-622)
+ if (mediator.isInAutoMode() && lastEnvironment != null) {
+ lastEnvironment.getProofControl().stopAutoMode();
+ }
+ // menu: unregister from the multi-proof state (Swing TaskTree.removeProof on abandon,
+ // TaskTree.java:241-267; ProofManagerF.removeProof was provided for exactly this caller)
+ proofManager.removeProof(proof);
+ proof.dispose();
+ // menu: reset the UI to the "no proof" state — the selection model supports a null
+ // selection (KeYSelectionModel.setSelectedProof(null) nulls the selection and fires
+ // selectedProofChanged, KeYSelectionModel.java:89-115); updateProofStatus then sets
+ // proofLoaded=false, which re-enables the proof-gated menu items
+ selectionModel.setSelectedProof(null);
+ NotificationManagerF.getInstance().notify("Proof abandoned.", Kind.INFO);
+ }
+
+ /**
+ * menu: opens the active settings of the selected proof (Swing
+ * {@code ShowActiveSettingsAction.actionPerformed}, ShowActiveSettingsAction.java:32-47: the
+ * "All active settings" {@code ViewSettingsDialog} over the {@code SettingsTreeModel}).
+ */
+ private void showActiveSettings() {
+ Proof proof = selectionModel.getSelectedProof();
+ if (proof == null) {
+ return; // the item is disabled without a proof (Swing enableWhenProofLoaded)
+ }
+ ActiveSettingsDialogF.show(stage, proof);
+ }
+
+ /**
+ * menu: shows the statistics of the selected proof (Swing
+ * {@code ShowProofStatistics.actionPerformed}, ShowProofStatistics.java:69-78: non-modal
+ * {@code Proof Statistics} window).
+ */
+ private void showProofStatistics() {
+ Proof proof = selectionModel.getSelectedProof();
+ if (proof == null) {
+ return; // the item is disabled without a proof (Swing enableWhenProofLoaded)
+ }
+ ProofStatisticsDialogF.show(stage, proof);
+ }
+
+ /**
+ * menu: shows the type hierarchy known to the selected proof (Swing
+ * {@code ShowKnownTypesAction.showTypeHierarchy}, ShowKnownTypesAction.java:47-83: the modal
+ * "Known types for this proof" dialog with the {@code ClassTree} of the proof's services).
+ */
+ private void showKnownTypes() {
+ Proof proof = selectionModel.getSelectedProof();
+ if (proof == null) {
+ return; // the item is disabled without a proof (Swing enableWhenProofLoaded)
+ }
+ KnownTypesDialogF.show(stage, proof);
+ }
+
+ // menu: MP1/MP2 — expected entries of the Proof menu (system property
+ // {@code key.fx.verify.menuparity}; MainWindow.createProofMenu :1082-1142, and the
+ // Automation submenu entries of MainWindow.createAutomationActions :814-827 since MP2).
+ // Table rows are leaf items, plain separators (the {@code "---"} row) or submenu names whose
+ // own children are checked recursively; MP4/MP5 can extend the tables for the other menus.
+ // P3b/B11: the Goal Back row asserts the base label — the dynamic " (RuleName)" suffix of
+ // GoalBackAction.updateName appears only after the Proof menu opens (setOnShowing).
+ private static final String[][] PROOF_MENU_EXPECTED = {
+ { "Automation", "Start Automatic Proof", "Stop Automatic Proof", "Full Automation",
+ "Structured Automation", "Structured Automation (Prep. Only)",
+ "Script-aware Auto" },
+ { "Undo Last Rule Application" },
+ { "Prune Proof" },
+ { "Abandon Proof" },
+ { "---" },
+ { "Search in Proof Tree" },
+ { "Search in Sequent" },
+ { "Search Next" },
+ { "Search Previous" },
+ { "Search Mode", "Highlight", "Hide", "Regroup" },
+ { "---" },
+ { "Show Used Contracts" },
+ { "Show All Active Settings" },
+ { "Show Proof Statistics" },
+ { "Show Known Types" },
+ };
+
+ // menu: MP3c — expected View menu entries in the Swing order of MainWindow.createViewMenu
+ // (:985-1064; Select Goal children from createSelectionMenu :1069-1074). The FX-only extras
+ // between the parity entries (OriginLabelsF cluster, Theme, Font Size, Layout, Log View,
+ // Soundiness, plain separators) are not listed: the walker skips unlisted built entries and
+ // only asserts that the listed ones occur in this relative order.
+ private static final String[][] VIEW_MENU_EXPECTED = {
+ { "Pretty Print" },
+ { "Unicode Symbols" },
+ { "Syntax Highlighting" },
+ { "Show Tooltips in Sequent View" },
+ { "Show Tooltips in Source View" },
+ { "Show Tooltips in Proof Tree" },
+ { "ToolTip Options…" },
+ { "Select Goal", "Select Goal Above", "Select Goal Below" },
+ { "Back" },
+ { "Forward" },
+ };
+
+ // menu: MP4 — expected Options menu entries in the Swing order of MainWindow.createOptionsMenu
+ // (:1144-1161: Settings, SMT Solvers…, ─sep─, Confirm Exit, Auto Save Proofs, Minimize
+ // Interaction, Right Click for Proof Macros, Ensure Source Consistency). The FX-only extras
+ // between the parity entries ("Reset Dock Layout" and the two plain separators) are not
+ // listed: the walker skips unlisted built entries and only asserts that the listed ones
+ // occur in this relative order.
+ private static final String[][] OPTIONS_MENU_EXPECTED = {
+ { "Settings" },
+ { "SMT Solvers…" },
+ { "Confirm Exit" },
+ { "Auto Save Proofs" },
+ { "Minimize Interaction" },
+ { "Right Click for Proof Macros" },
+ { "Ensure Source Consistency" },
+ };
+
+ // menu: MP5 — expected File menu entries in the FX order after the MP5 inserts (the Swing
+ // {@code MainWindow.createFileMenu} :991-1031 order with the port's existing grouping:
+ // example/open/reload/edit-last-opened | proof mgmt/load-taclets/Prove | save/quick |
+ // recent | exit). The three Prove submenu entries are the always-present ones; "Run All
+ // Proofs" sits behind the BULK_UI_TEST feature flag (off by default) and is therefore not
+ // part of the table.
+ private static final String[][] FILE_MENU_EXPECTED = {
+ { "Open Example…" },
+ { "Open File…" },
+ { "Reload" },
+ { "Edit Last Opened File" },
+ { "Proof Management…" },
+ { "Load User Defined Taclets…" },
+ { "Prove", "Load User Defined Taclets for Proving", "Load KeY Taclets",
+ "Lemma Generation (Batch Mode)" },
+ { "Save File…" },
+ { "Save Bundle…" },
+ { "Quick Save" },
+ { "Quick Load" },
+ { "Recent Files" },
+ { "Exit" },
+ };
+
+ // menu: MP5 — expected About menu entries in the Swing order of MainWindow.createHelpMenu
+ // (:1163-1174).
+ private static final String[][] ABOUT_MENU_EXPECTED = {
+ { "About KeY…" },
+ { "KeY Homepage" },
+ { "Send Feedback…" },
+ { "Create Github Issue" },
+ { "License…" },
+ };
+
+ /**
+ * menu: MP1/MP3/MP4/MP5 — menu parity self test (system property
+ * {@code key.fx.verify.menuparity},
+ * run after a proof load like the other verify hooks): builds the menu bar and emits one
+ * marker line per asserted menu, each ending in PASS or FAIL.
+ */
+ private void verifyMenuParity() {
+ logMenuParity("Proof", verifyMenuParityReport("Proof", PROOF_MENU_EXPECTED));
+ logMenuParity("View", verifyMenuParityReport("View", VIEW_MENU_EXPECTED));
+ logMenuParity("Options", verifyMenuParityReport("Options", OPTIONS_MENU_EXPECTED));
+ logMenuParity("File", verifyMenuParityReport("File", FILE_MENU_EXPECTED));
+ logMenuParity("About", verifyMenuParityReport("About", ABOUT_MENU_EXPECTED));
+ // P3b/B11: the Goal Back label seam — simulate the Proof menu opening (the Swing
+ // MenuListener.menuSelected, MainWindow.java:1094-1105, ported as the menu's
+ // setOnShowing handler) and assert the item text was refreshed from the newest goal
+ logMenuParity("GoalBack", verifyGoalBackLabel());
+ // P3b/B15: assert the Goal Back / Prune enablement of the menu items and toolbar buttons
+ verifyGoalBackPruneEnablement();
+ // WP-3: the View-menu Pretty Print / Unicode toggles — the real CheckMenuItems flip the
+ // ViewSettings + the NotationInfo statics and re-render the sequent (Swing
+ // PrettyPrintToggleAction/UnicodeToggleAction); the leg asserts the visible effect
+ logMenuParity("PrettyUnicode", verifyPrettyUnicode());
+ }
+
+ /**
+ * WP-3: headless self test of the View-menu Pretty Print / Unicode toggles ({@code
+ * key.fx.verify.menuparity}): the items are wired to
+ * {@code ProofIndependentSettings...ViewSettings} exactly like the Swing actions (Swing
+ * PrettyPrintToggleAction.actionPerformed: set the {@code NotationInfo} static BEFORE the
+ * ViewSettings change, then {@code makePrettyView}; UnicodeToggleAction: disabled without
+ * pretty printing) — the leg fires the real {@code CheckMenuItem}s and asserts the mirrored
+ * state plus the visible effect on the printed sequent text. The previous state is restored
+ * afterwards.
+ *
+ * @return {@code "PASS - ..."} or {@code "FAIL - ..."}
+ */
+ private String verifyPrettyUnicode() {
+ ViewSettings viewSettings = ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings();
+ boolean originalPretty = viewSettings.isUsePretty();
+ boolean originalUnicode = viewSettings.isUseUnicode();
+ MenuBar bar = buildMenuBar();
+ Menu viewMenu = bar.getMenus().stream()
+ .filter(m -> "View".equals(menuDisplayText(m))).findFirst().orElse(null);
+ CheckMenuItem pretty = findCheckMenuItem(viewMenu, "Pretty Print");
+ CheckMenuItem unicode = findCheckMenuItem(viewMenu, "Unicode Symbols");
+ if (pretty == null || unicode == null) {
+ return "FAIL - View menu lacks the Pretty Print/Unicode items";
+ }
+ List problems = new ArrayList<>();
+ String text0 = sequentView.printedText();
+ try {
+ // pretty printing off: the settings, the NotationInfo static and the printed text
+ // must all follow (the item's onAction fires the same refreshPrettyViews path)
+ pretty.setSelected(false);
+ pretty.fire();
+ if (viewSettings.isUsePretty() || NotationInfo.DEFAULT_PRETTY_SYNTAX) {
+ problems.add("pretty=off not mirrored");
+ }
+ if (sequentView.printedText() == null) {
+ problems.add("pretty=off no reprint");
+ }
+ // the Unicode item is disabled without pretty printing (Swing setEnabled(usePretty))
+ if (!unicode.isDisable()) {
+ problems.add("unicode enabled without pretty");
+ }
+ // pretty printing on again
+ pretty.setSelected(true);
+ pretty.fire();
+ if (!viewSettings.isUsePretty() || !NotationInfo.DEFAULT_PRETTY_SYNTAX) {
+ problems.add("pretty=on not mirrored");
+ }
+ if (unicode.isDisable()) {
+ problems.add("unicode disabled with pretty");
+ }
+ // unicode off/on with pretty printing enabled. The visible-effect probe toggles to
+ // the OPPOSITE of the current state and back (a setSelected that keeps the current
+ // state is a no-op): the unicode symbols (forall/exists/and vs the ASCII escapes)
+ // differ for every proof, unlike the pretty-print layout — the demo sequent has no
+ // arithmetic, so its pretty/default notation is identical (Swing parity: the same
+ // text) and the pretty leg asserts the settings mirroring + the shared
+ // refreshPrettyViews reprint path instead.
+ unicode.setSelected(!unicode.isSelected());
+ unicode.fire();
+ if (viewSettings.isUseUnicode() == originalUnicode) {
+ problems.add("unicode toggle not mirrored");
+ }
+ String textOpposite = sequentView.printedText();
+ unicode.setSelected(!unicode.isSelected());
+ unicode.fire();
+ if (viewSettings.isUseUnicode() != originalUnicode) {
+ problems.add("unicode back not mirrored");
+ }
+ String textBack = sequentView.printedText();
+ boolean unicodeEffect = textOpposite != null && textBack != null
+ && !textOpposite.equals(textBack) && text0 != null;
+ boolean ok = problems.isEmpty();
+ return (ok && unicodeEffect ? "PASS" : "FAIL") + " - pretty[" + originalPretty + "->"
+ + viewSettings.isUsePretty() + "] unicodeEffect=" + unicodeEffect
+ + (ok ? "" : " problems=" + problems);
+ } finally {
+ // restore the state the demo was loaded with
+ pretty.setSelected(originalPretty);
+ pretty.fire();
+ unicode.setSelected(originalUnicode);
+ unicode.fire();
+ }
+ }
+
+ /**
+ * WP-3 (settings-config): headless theme persistence round trip ({@code
+ * key.fx.verify.themepersist}={@code save}|{@code restore}) — two app runs. The save run
+ * applies the dark theme through the real {@link ThemeManager#setTheme} (the View menu's
+ * Dark Theme action routes through the same call) and asserts the choice was written to the
+ * persisted user preference. The restore run starts without the {@code key.fx.theme}
+ * override and asserts the dark theme was restored from the persisted preference at startup,
+ * then resets to light so the round trip is repeatable.
+ *
+ * @param phase the round-trip phase, {@code "save"} or {@code "restore"}
+ * @return a one-line report, {@code "... PASS"} if the phase's assertions hold
+ */
+ private String runThemePersistVerification(String phase) {
+ ThemeManager manager = ThemeManager.getInstance();
+ if ("save".equals(phase)) {
+ manager.setTheme(Theme.DARK);
+ boolean ok = manager.getTheme() == Theme.DARK
+ && ThemeManager.persistedTheme() == Theme.DARK;
+ return "phase=save applied=" + manager.getTheme() + " persisted="
+ + ThemeManager.persistedTheme() + (ok ? " PASS" : " FAIL");
+ }
+ if ("restore".equals(phase)) {
+ boolean overrideAbsent = System.getProperty(ThemeManager.THEME_PROPERTY) == null;
+ Theme restored = manager.getTheme();
+ Theme persisted = ThemeManager.persistedTheme();
+ boolean ok = overrideAbsent && restored == Theme.DARK && persisted == Theme.DARK;
+ // leave the persisted state clean so the round trip is repeatable
+ manager.setTheme(Theme.LIGHT);
+ boolean resetOk = ThemeManager.persistedTheme() == Theme.LIGHT;
+ return "phase=restore restored=" + restored + " persisted=" + persisted + " reset="
+ + ThemeManager.persistedTheme() + (ok && resetOk ? " PASS" : " FAIL");
+ }
+ return "FAIL - unknown phase " + phase;
+ }
+
+ /**
+ * P3b/B11: headless self test of the dynamic "Goal Back" label ({@code
+ * key.fx.verify.menuparity}): opens the freshly built Proof menu via its {@code setOnShowing}
+ * handler (the port of Swing's {@code MenuListener.menuSelected}, MainWindow.java:1094-1105)
+ * and asserts the Goal Back item text — either the plain base or the base plus the
+ * {@code " (RuleName)"} suffix of {@code mediator.goalBackRuleName()} (Swing
+ * GoalBackAction.updateName, GoalBackAction.java:113-125).
+ *
+ * @return {@code "PASS - ..."} or {@code "FAIL - ..."}
+ */
+ private String verifyGoalBackLabel() {
+ MenuBar bar = buildMenuBar();
+ javafx.scene.control.MenuItem item = goalBackMenuItem;
+ if (item == null) {
+ return "FAIL - no Goal Back item";
+ }
+ Menu proofMenu = bar.getMenus().stream()
+ .filter(m -> "Proof".equals(menuDisplayText(m))).findFirst().orElse(null);
+ if (proofMenu != null && proofMenu.getOnShowing() != null) {
+ proofMenu.getOnShowing().handle(null);
+ } else {
+ return "FAIL - no Proof menu/onShowing handler";
+ }
+ String text = item.getText();
+ boolean ok = "Undo Last Rule Application".equals(text)
+ || text != null && text.startsWith("Undo Last Rule Application (");
+ return (ok ? "PASS" : "FAIL") + " - goalBack[" + text + "]";
+ }
+
+ /**
+ * P3b/B15: headless self test of the Goal Back / Prune Proof enablement ({@code
+ * key.fx.verify.menuparity}): the freshly built menu items must mirror the Swing conditions
+ * of GoalBackAction.selectedNodeChanged (GoalBackAction.java:70-80) / PruneProofAction
+ * (PruneProofAction.java:61-77). With a loaded demo proof (no auto mode run, the root leaf
+ * condition decides) the enablement is compared to the expected value.
+ */
+ private void verifyGoalBackPruneEnablement() {
+ buildMenuBar();
+ // P3b/B15: rebuilding the menu bar re-creates the Goal Back / Prune items with default
+ // enablement; apply the selection-based refresh (the real bar gets it from the selection
+ // listener / updateProofStatus on every change) before asserting the states
+ refreshGoalBackPrune();
+ boolean autoMode = mediator.autoModeRunningProperty().get();
+ Proof proof = selectionModel.getSelectedProof();
+ de.uka.ilkd.key.proof.Node selNode = selectionModel.getSelectedNode();
+ boolean expectGoalBack = !autoMode && proof != null && selNode != null
+ && !proof.root().leaf()
+ && !(GeneralSettings.noPruningClosed && selNode.isClosed());
+ boolean expectPrune = !autoMode && proof != null && selNode != null
+ && !selNode.leaf()
+ && (!proof.getSubtreeGoals(selNode).isEmpty()
+ || (!GeneralSettings.noPruningClosed
+ && !proof.getClosedSubtreeGoals(selNode).isEmpty()));
+ boolean goalBackOk = goalBackMenuItem != null
+ && goalBackMenuItem.isDisable() == !expectGoalBack;
+ boolean pruneOk =
+ pruneMenuItem != null && pruneMenuItem.isDisable() == !expectPrune;
+ boolean toolsOk = goalBackToolbarButton != null
+ && goalBackToolbarButton.isDisable() == !expectGoalBack
+ && pruneToolbarButton != null
+ && pruneToolbarButton.isDisable() == !expectPrune;
+ boolean ok = goalBackOk && pruneOk && toolsOk;
+ logMenuParity("GoalBackPrune",
+ (ok ? "PASS" : "FAIL") + " - goalBack[" + (goalBackOk ? "enabled" : "wrong")
+ + "] prune[" + (pruneOk ? "enabled" : "wrong") + "]");
+ }
+
+ /**
+ * menu: MP3b (WP-10) — headless self test of the Selection Back/Forward binding ({@code
+ * key.fx.verify.selectionhistory}): the View menu's Back/Forward items (Swing
+ * SelectionBackAction / SelectionForwardAction over the SelectionHistory) must carry the
+ * KeyStrokeManagerF accelerators (Ctrl+Alt+Left/Right), follow the history-driven enablement
+ * and — fired through their real {@code onAction} — move the mediator selection to the
+ * previous/next selected node. Needs a proof with at least two nodes (combine with {@code
+ * key.fx.demo.autoprove}); with a single-node proof only the accelerator part runs and the
+ * navigation part is skipped.
+ *
+ * @param proof the loaded demo proof, may be {@code null}
+ */
+ private void runSelectionHistoryVerification(Proof proof) {
+ // the View menu's Back/Forward items of the freshly built menu bar, with their
+ // KeyStrokeManagerF accelerators
+ MenuBar bar = buildMenuBar();
+ Menu viewMenu = bar.getMenus().stream()
+ .filter(m -> "View".equals(menuDisplayText(m))).findFirst().orElse(null);
+ MenuItem backItem = viewMenu == null ? null : findMenuItem(viewMenu, "Back");
+ MenuItem forwardItem = viewMenu == null ? null : findMenuItem(viewMenu, "Forward");
+ KeyStrokeManagerF manager = KeyStrokeManagerF.getInstance();
+ KeyCombination backBinding =
+ manager.binding("de.uka.ilkd.key.gui.actions.SelectionBackAction").orElse(null);
+ KeyCombination forwardBinding =
+ manager.binding("de.uka.ilkd.key.gui.actions.SelectionForwardAction").orElse(null);
+ List problems = new ArrayList<>();
+ if (backItem == null || forwardItem == null) {
+ problems.add("the View menu has no Back/Forward items");
+ } else {
+ if (!backBinding.equals(backItem.getAccelerator())
+ || !forwardBinding.equals(forwardItem.getAccelerator())) {
+ problems.add("accelerators " + backItem.getAccelerator() + "/"
+ + forwardItem.getAccelerator() + " != " + backBinding + "/" + forwardBinding);
+ }
+ }
+ // the navigation part: two distinct nodes, then Back/Forward through the menu items
+ String navigation = "SKIPPED (single-node proof)";
+ if (proof != null && !proof.isDisposed() && !proof.root().leaf()
+ && backItem != null && forwardItem != null) {
+ de.uka.ilkd.key.proof.Node first = proof.root();
+ de.uka.ilkd.key.proof.Node second = first;
+ while (!second.leaf()) {
+ second = second.child(second.childrenCount() - 1);
+ }
+ if (second != first) {
+ selectionModel.setSelectedNode(first);
+ selectionModel.setSelectedNode(second);
+ boolean canBackBefore = selectionHistory.canGoBackProperty().get();
+ boolean backEnabled = !backItem.isDisable();
+ backItem.fire();
+ boolean backMoved = selectionModel.getSelectedNode() == first;
+ boolean canForward = selectionHistory.canGoForwardProperty().get();
+ forwardItem.fire();
+ boolean forwardMoved = selectionModel.getSelectedNode() == second;
+ if (!canBackBefore || !backEnabled) {
+ problems.add("Back not enabled after two selections (canGoBack=" + canBackBefore
+ + ", itemEnabled=" + backEnabled + ")");
+ }
+ if (!backMoved) {
+ problems.add("Back did not move the selection to the previous node");
+ }
+ if (!canForward) {
+ problems.add("Forward not enabled after Back");
+ }
+ if (!forwardMoved) {
+ problems.add("Forward did not move the selection to the next node");
+ }
+ navigation = (backMoved && forwardMoved ? "moved " : "did not move ")
+ + first.serialNr() + "->" + second.serialNr();
+ }
+ }
+ boolean ok = problems.isEmpty();
+ LOGGER.info("Selection history verification: {} (navigation: {})",
+ ok ? "PASS" : "FAIL - " + String.join("; ", problems), navigation);
+ NotificationManagerF.getInstance()
+ .notify("Selection history verification: " + (ok ? "PASS" : "FAIL"),
+ ok ? Kind.INFO : Kind.ERROR);
+ }
+
+ /**
+ * @return the menu's display text without the mnemonic marker; the built-in menus carry
+ * Swing-parity mnemonics ("_File", MainWindow.java:993), so lookups must compare the
+ * display text
+ */
+ private static String menuDisplayText(Menu menu) {
+ return menu.getText().replace("_", "");
+ }
+
+ /**
+ * @return the first menu item with the given text in the menu's (recursive) item list, or
+ * {@code null}
+ */
+ private static MenuItem findMenuItem(Menu menu, String text) {
+ for (MenuItem item : menu.getItems()) {
+ if (text.equals(item.getText())) {
+ return item;
+ }
+ if (item instanceof Menu sub) {
+ MenuItem result = findMenuItem(sub, text);
+ if (result != null) {
+ return result;
+ }
+ }
+ }
+ return null;
+ }
+
+ private static CheckMenuItem findCheckMenuItem(Menu menu, String text) {
+ MenuItem item = findMenuItem(menu, text);
+ return item instanceof CheckMenuItem check ? check : null;
+ }
+
+ private void logMenuParity(String menuName, String report) {
+ LOGGER.info("Menu parity verification ({}): {}", menuName, report);
+ NotificationManagerF.getInstance()
+ .notify("Menu parity verification (" + menuName + "): " + report,
+ report.endsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ }
+
+ /**
+ * menu: builds the {@link #buildMenuBar() menu bar} and checks the Proof menu entries
+ * (compatibility entry point: same table and report as the Proof part of
+ * {@link #verifyMenuParity()}).
+ *
+ * @return {@code "PASS - items, found: "} or
+ * {@code "FAIL - missing: "}
+ */
+ String verifyMenuParityReport() {
+ return verifyMenuParityReport("Proof", PROOF_MENU_EXPECTED);
+ }
+
+ /**
+ * menu: builds the {@link #buildMenuBar() menu bar} and checks one menu against an expected
+ * table. A row may be a leaf item, a plain separator (the {@code "---"} row) or a submenu
+ * name ({@code "Search Mode"}) whose own children are checked recursively, in their order.
+ * Built entries that are not listed in the table are skipped.
+ *
+ * @return {@code "PASS - items, found: "} or
+ * {@code "FAIL - missing: "}
+ */
+ String verifyMenuParityReport(String menuName, String[][] expected) {
+ MenuBar menuBar = buildMenuBar();
+ Menu menu = menuBar.getMenus().stream().filter(m -> menuName.equals(m.getText()))
+ .findFirst().orElse(null);
+ if (menu == null) {
+ return "FAIL - missing: <" + menuName + " menu>";
+ }
+ List present = new ArrayList<>();
+ List missing = new ArrayList<>();
+ List remaining = new ArrayList<>(menu.getItems());
+ for (String[] row : expected) {
+ String label = row[0];
+ if ("---".equals(label)) {
+ // a separator has no text; match the control type directly
+ int sepIndex = -1;
+ for (int i = 0; i < remaining.size(); i++) {
+ if (remaining.get(i) instanceof SeparatorMenuItem) {
+ sepIndex = i;
+ break;
+ }
+ }
+ if (sepIndex < 0) {
+ missing.add("separator");
+ } else {
+ present.add("separator");
+ remaining = new ArrayList<>(
+ remaining.subList(sepIndex + 1, remaining.size()));
+ }
+ continue;
+ }
+ int index = indexOfItem(remaining, label);
+ if (index < 0) {
+ missing.add(label);
+ continue;
+ }
+ present.add(label);
+ MenuItem node = remaining.get(index);
+ if (node instanceof Menu submenu && row.length > 1) {
+ // check the submenu's entries in their order (e.g. Automation, Search Mode,
+ // Select Goal)
+ List children = new ArrayList<>(submenu.getItems());
+ for (int i = 1; i < row.length; i++) {
+ int childIndex = indexOfItem(children, row[i]);
+ if (childIndex < 0) {
+ missing.add(label + " > " + row[i]);
+ } else {
+ present.add(label + " > " + row[i]);
+ children = new ArrayList<>(
+ children.subList(childIndex + 1, children.size()));
+ }
+ }
+ }
+ remaining = new ArrayList<>(remaining.subList(index + 1, remaining.size()));
+ }
+ String found = String.join(", ", present);
+ if (missing.isEmpty()) {
+ return "PASS - " + present.size() + " items, found: " + found;
+ }
+ return "FAIL - missing: " + String.join(", ", missing) + " (found " + found + ")";
+ }
+
+ /** menu: index of the first remaining menu item with the given text, or -1. */
+ private static int indexOfItem(List items, String text) {
+ for (int i = 0; i < items.size(); i++) {
+ MenuItem item = items.get(i);
+ if (text.equals(item.getText())) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private Menu buildOptionsMenu() {
+ // Swing parity: Options menu mnemonic VK_O (MainWindow.java:1146)
+ Menu options = new Menu("_Options");
+ // menu: MP4 — Swing MainWindow.createOptionsMenu :1144-1161: Settings, SMT Solvers…
+ // (SMTOptionsAction → settings dialog on the SMT panel), separator, then the five check
+ // items; the FX-only "Reset Dock Layout" extra is kept between two separators (Settings,
+ // SMT Solvers…, ─sep─, Reset Dock Layout, ─sep─, Confirm Exit, Auto Save Proofs,
+ // Minimize Interaction, Right Click for Proof Macros, Ensure Source Consistency).
+ options.getItems().addAll(
+ menuItem("Settings",
+ "de.uka.ilkd.key.gui.settings.SettingsManager$ShowSettingsAction",
+ IconFactoryF.Key.CONFIGURE, this::openSettings),
+ menuItem("SMT Solvers…", "de.uka.ilkd.key.gui.actions.SMTOptionsAction",
+ IconFactoryF.Key.TOOLBOX, this::showSMTOptions),
+ new SeparatorMenuItem(),
+ menuItem("Reset Dock Layout", this::resetLayout),
+ new SeparatorMenuItem(),
+ confirmExitToggle(),
+ autoSaveProofsToggle(),
+ minimizeInteractionToggle(),
+ rightClickMacroToggle(),
+ ensureSourceConsistencyToggle());
+ return options;
+ }
+
+ /**
+ * menu: MP4 — SMT Solvers… opens the settings dialog on the SMT panel (Swing
+ * SMTOptionsAction, SMTOptionsAction.java:27-28: {@code
+ * SettingsManager.getInstance().showSettingsDialog(mainWindow, SettingsManager.SMT_SETTINGS)};
+ * here the provider registered as {@link SettingsManagerF#SMT_SETTINGS} is selected in the
+ * settings tree, SettingsManagerF.java:169-177).
+ */
+ private void showSMTOptions() {
+ SettingsManagerF.getInstance().showSettingsDialog(this, SettingsManagerF.SMT_SETTINGS);
+ }
+
+ /**
+ * menu: MP4 — Confirm Exit check item (Swing ToggleConfirmExitAction,
+ * ToggleConfirmExitAction.java:20-31): the selected state mirrors
+ * {@code ViewSettings.confirmExit()} and the action writes it back.
+ */
+ private CheckMenuItem confirmExitToggle() {
+ ViewSettings vs = ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings();
+ CheckMenuItem item = new CheckMenuItem("Confirm Exit");
+ item.setSelected(vs.confirmExit());
+ item.setOnAction(e -> vs.setConfirmExit(item.isSelected()));
+ return item;
+ }
+
+ /**
+ * loadingexit (P1) — Exit (Swing {@code ExitMainAction.exitMain}, ExitMainAction.java:55-66,
+ * reached from the File menu and the window close button, MainWindow.java:366): when
+ * the Confirm Exit view setting is on (the Options menu toggle above), asks
+ * {@code Really Quit?} first.
+ */
+ private void exitApplication() {
+ ViewSettings vs = ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings();
+ if (vs.confirmExit()) {
+ Alert alert = new Alert(AlertType.CONFIRMATION, "Really Quit?\n", ButtonType.YES,
+ ButtonType.NO);
+ alert.setTitle("Exit");
+ alert.setHeaderText(null);
+ alert.initOwner(stage);
+ Optional answer = alert.showAndWait();
+ if (answer.isEmpty() || answer.get() != ButtonType.YES) {
+ return;
+ }
+ }
+ exitApplicationWithoutInteraction();
+ }
+
+ /**
+ * loadingexit (P1) — Swing {@code ExitMainAction.exitMainWithoutInteraction},
+ * ExitMainAction.java:87-101: recent-files store save, mediator shutdown event, preferences
+ * sync and {@code System.exit(0)} (the {@code exitSystem} flag of the standalone
+ * application). The FX port: the recent-files store is saved here (the
+ * colors/keystrokes/docking stores persist via their own shutdown hooks, which
+ * {@code System.exit(0)} still runs); and {@code System.exit(0)} is required because
+ * background threads of a running solver or verification would otherwise keep the process
+ * alive after {@link Platform#exit()}.
+ *
+ * KNOWN-SIMPLIFIED: the Swing mediator shutdown event ({@code fireShutDown} notifying the
+ * {@code GUIListener}s) was omitted because {@link KeYMediatorF} had no GUI listener
+ * registry — resolved in WP-1: the mediator now holds a {@link GUIListenerF} registry and
+ * the event is fired above.
+ */
+ private void exitApplicationWithoutInteraction() {
+ recentFiles.save();
+ // WP-1: the mediator shutdown event (Swing
+ // ExitMainAction.exitMainWithoutInteraction, ExitMainAction.java:83 — the Swing
+ // MainGUIListener.shutDown of MainWindow.java:1855-1858 receives it). The
+ // KNOWN-SIMPLIFIED row MainWindowF.java:3721 is resolved: KeYMediatorF now has the
+ // GUI-listener registry (GUIListenerF); the FX window-hiding of the Swing listener is
+ // the Platform.exit below.
+ mediator.fireShutDown(new EventObject(this));
+ LOGGER.info("Have a nice day.");
+ Platform.exit();
+ System.exit(0);
+ }
+
+ /**
+ * menu: MP4 — Auto Save Proofs check item (Swing {@code AutoSave},
+ * AutoSave.java:14-33): the initial state is {@code autoSavePeriod() > 0} (AutoSave.java:22-24)
+ * and the action writes {@code setAutoSave(2000)} / {@code setAutoSave(0)}
+ * (AutoSave.java:29-31;
+ * Swing {@code AutoSave.DEFAULT_PERIOD = 2000}, AutoSave.java:16 — key.ui, not importable into
+ * this module, hence the inlined constant).
+ * // menu: MP7 — the timer wiring is no longer deferred: the action arms/disarms the
+ * // {@link AutoSaver} through {@link #applyAutoSave(int)} (Swing AutoSave.java:31 calls
+ * // getMediator().setAutoSave(p), KeYMediator.java:148-150); arming is also applied at
+ * // startup from the persisted period (see {@link #initialize()}).
+ */
+ private CheckMenuItem autoSaveProofsToggle() {
+ GeneralSettings gs = ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings();
+ CheckMenuItem item = new CheckMenuItem("Auto Save Proofs");
+ item.setSelected(gs.autoSavePeriod() > 0);
+ item.setOnAction(
+ e -> {
+ int period = item.isSelected() ? DEFAULT_AUTO_SAVE_PERIOD : 0;
+ gs.setAutoSave(period);
+ applyAutoSave(period);
+ });
+ return item;
+ }
+
+ /**
+ * menu: MP7 — arms or disarms the {@link AutoSaver} (Swing {@code KeYMediator.setAutoSave},
+ * KeYMediator.java:148-150, called by {@code AutoSave} with the new period, AutoSave.java
+ * :29-31): the saver is created with the given interval and registered on the window's UI
+ * control as a {@code ProverTaskListener} so the core's proof runs deliver it the task
+ * events — the FX equivalent of the Swing {@code MediatorProofControl.AutoModeWorker}
+ * registration (MediatorProofControl.java:209-211). The saver field itself lives on the
+ * mediator ({@code KeYMediatorF#getAutoSaver}), which hands it every newly selected proof
+ * from {@code setProof}.
+ *
+ * @param period the save interval in proof steps, 0 disables auto save
+ */
+ private void applyAutoSave(int period) {
+ AutoSaver oldSaver = mediator.getAutoSaver();
+ if (oldSaver != null) {
+ getUserInterfaceControl().removeProverTaskListener(oldSaver);
+ }
+ mediator.setAutoSave(period);
+ AutoSaver newSaver = mediator.getAutoSaver();
+ if (newSaver != null) {
+ getUserInterfaceControl().addProverTaskListener(newSaver);
+ }
+ }
+
+ /**
+ * menu: MP7 — auto-save self-test helper: {@code AutoSaver} (key.core) stores the proof from
+ * {@code setProof} in a private field without a getter (AutoSaver.java:40, :102-104); the
+ * proof identity is read reflectively for the {@code key.fx.verify.autosave} assertion.
+ *
+ * @param saver the armed auto saver
+ * @return the proof the saver received, or {@code null} on failure/reflection error
+ */
+ private static Object readAutoSaveProof(AutoSaver saver) {
+ try {
+ Field proofField = AutoSaver.class.getDeclaredField("proof");
+ proofField.setAccessible(true);
+ return proofField.get(saver);
+ } catch (ReflectiveOperationException e) {
+ LOGGER.error("Auto save verification: cannot read AutoSaver.proof", e);
+ return null;
+ }
+ }
+
+ /**
+ * menu: MP7/WP-9 — auto-save real-flow self test (part of {@code key.fx.verify.autosave}):
+ * runs a capped auto-mode run with an {@link AutoSaver} armed at a small interval and proves
+ * the saver actually wrote its intermediate proof files. Swing parity: the armed saver
+ * receives the per-application {@code taskProgress(countApplied)} events through the UI
+ * control's ProverTaskListener chain (AbstractUserInterfaceControl.fireTaskProgress) and
+ * writes {@code /.autosave.N.key} every {@code interval} applications
+ * (AutoSaver.java:117-124) plus the closed final proof to {@code .autosave.proof}
+ * (AutoSaver.java:139-141). The arming routes through the real {@link #applyAutoSave(int)}
+ * and the selection hand-off (mediator {@code setProof} → {@code AutoSaver.setProof}) like
+ * the synchronous leg in the load-success handler.
+ *
+ * The auto-mode run blocks, so the leg runs on a worker thread and logs its own marker line.
+ * The {@code key.fx.demo.autoprove} run closes the demo proof before this leg can arm the
+ * saver — do not combine the two flags.
+ */
+ private void verifyAutoSaveRealFlow(ProofControl proofControl, Proof proof) {
+ if (proof.closed()) {
+ LOGGER.info("Auto save verification (real flow): SKIP (proof already closed — run "
+ + "key.fx.verify.autosave without key.fx.demo.autoprove)");
+ return;
+ }
+ final long startMs = System.currentTimeMillis();
+ Thread worker = new Thread(() -> {
+ GeneralSettings gs = ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings();
+ int persistedPeriod = gs.autoSavePeriod();
+ int oldMaxSteps = proof.getSettings().getStrategySettings().getMaxSteps();
+ boolean ok = false;
+ List written = new ArrayList<>();
+ try {
+ proof.getSettings().getStrategySettings()
+ .setMaxSteps(AUTO_SAVE_REAL_FLOW_MAX_STEPS);
+ CountDownLatch armed = new CountDownLatch(1);
+ FxUtil.runLater(() -> {
+ try {
+ // arm a small interval through the real arming path and hand the loaded
+ // proof to the saver via the real selection hand-off
+ applyAutoSave(AUTO_SAVE_REAL_FLOW_INTERVAL);
+ selectionModel.setSelectedProof(null);
+ selectionModel.setSelectedProof(proof);
+ } finally {
+ armed.countDown();
+ }
+ });
+ armed.await(10, TimeUnit.SECONDS);
+ LOGGER.info("Auto save verification (real flow): auto mode with interval {} armed",
+ AUTO_SAVE_REAL_FLOW_INTERVAL);
+ proofControl.startAndWaitForAutoMode(proof);
+ // the saves run on their own threads (AutoSaver.save) — poll for the freshly
+ // written files (mtime after the leg started)
+ File tmpDir = IOUtil.getTempDirectory();
+ long deadline = System.currentTimeMillis() + 10_000;
+ while (System.currentTimeMillis() < deadline && written.isEmpty()) {
+ File[] files = tmpDir.listFiles(
+ (dir, name) -> name.startsWith(".autosave."));
+ if (files != null) {
+ for (File file : files) {
+ if (file.lastModified() >= startMs) {
+ written.add(file.getName() + " " + file.length() + "B");
+ }
+ }
+ }
+ Thread.sleep(200);
+ }
+ ok = !written.isEmpty();
+ } catch (Exception e) {
+ LOGGER.error("Auto save verification (real flow) failed", e);
+ } finally {
+ int saves = written.size();
+ List saved = List.copyOf(written);
+ // restore: armed iff the persisted period is > 0 (the startup arming semantics)
+ FxUtil.runLater(() -> applyAutoSave(persistedPeriod));
+ proof.getSettings().getStrategySettings().setMaxSteps(oldMaxSteps);
+ String report = "Auto save verification (real flow): " + (ok ? "PASS" : "FAIL")
+ + (saves == 0 ? ""
+ : " (" + saves + " save(s) under " + IOUtil.getTempDirectory() + ": "
+ + saved + ")");
+ LOGGER.info(report);
+ boolean reported = ok;
+ FxUtil.runLater(() -> NotificationManagerF.getInstance().notify(report,
+ reported ? Kind.INFO : Kind.ERROR));
+ }
+ }, "fx-verify-autosave-realloc");
+ worker.setDaemon(true);
+ worker.start();
+ }
+
+ /** WP-9 real-flow auto save leg: the armed save interval (rule applications). */
+ private static final int AUTO_SAVE_REAL_FLOW_INTERVAL = 25;
+
+ /** WP-9 real-flow auto save leg: the capped maximum number of strategy applications. */
+ private static final int AUTO_SAVE_REAL_FLOW_MAX_STEPS = 1000;
+
+ /** menu: MP4 — Swing {@code AutoSave.DEFAULT_PERIOD} (key.ui), see autoSaveProofsToggle(). */
+ private static final int DEFAULT_AUTO_SAVE_PERIOD = 2000;
+
+ /**
+ * menu: MP4 — Minimize Interaction check item (Swing {@code MinimizeInteraction},
+ * MinimizeInteraction.java:17-73, display name "Minimize Interaction"): the selected state
+ * mirrors and writes the {@code GeneralSettings} taclet filter and applies it to the proof
+ * control of the currently loaded environment.
+ * // menu: MP7 — the proof-control wiring is no longer deferred: the toggle applies the flag
+ * // through {@link #applyMinimizeInteraction(ProofControl)} (Swing
+ * // MinimizeInteraction.handleClickEvent, MinimizeInteraction.java:57-66:
+ * // mainWindow.getUserInterface().getProofControl().setMinimizeInteraction(b)).
+ */
+ private CheckMenuItem minimizeInteractionToggle() {
+ GeneralSettings gs = ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings();
+ CheckMenuItem item = new CheckMenuItem("Minimize Interaction");
+ item.setSelected(gs.getTacletFilter());
+ item.setOnAction(e -> {
+ // menu: MP7 — the flag is written first, then applied to the proof control of the
+ // loaded environment (Swing MinimizeInteraction.handleClickEvent writes the settings
+ // after updateMainWindow; here the settings change must happen first so the helper
+ // reads the new value)
+ gs.setTacletFilter(item.isSelected());
+ applyMinimizeInteraction(
+ lastEnvironment == null ? null : lastEnvironment.getProofControl());
+ });
+ return item;
+ }
+
+ /**
+ * menu: MP7 — applies the persisted Minimize Interaction flag to the given proof control
+ * (Swing {@code MinimizeInteraction.updateMainWindow}, MinimizeInteraction.java:64-66: {@code
+ * mainWindow.getUserInterface().getProofControl().setMinimizeInteraction(b)}); the core honors
+ * the flag in {@code AbstractProofControl} (only complete rule applications are offered to the
+ * user). No-op for a {@code null} control (no proof loaded — the flag is applied to the next
+ * attached proof control by the load-success path).
+ *
+ * @param pc the proof control to update, may be {@code null}
+ */
+ private void applyMinimizeInteraction(@Nullable ProofControl pc) {
+ if (pc == null) {
+ return;
+ }
+ boolean flag = ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings()
+ .getTacletFilter();
+ pc.setMinimizeInteraction(flag);
+ }
+
+ /**
+ * menu: MP7/WP-9 — Minimize Interaction real-flow self test (part of
+ * {@code key.fx.verify.minimizeinteraction}): the load-success hand-off applies the persisted
+ * flag to the freshly attached proof control; the leg proves the core actually honors
+ * it on the loaded demo: with the flag armed, the interactive application of an
+ * auto-instantiable (incomplete, but complete-except-skolem-constants) taclet goes through the
+ * minimize branch of {@code AbstractProofControl.selectedTaclet} (AbstractProofControl.java
+ * :211-240 — the single app is auto-instantiated and {@code applyInteractive}d without the
+ * taclet dialog), while with the flag off the same application opens the
+ * {@code TacletMatchDialogF}. The applied node is pruned back afterwards.
+ *
+ * The rule applications may open the modal dialog on the FX thread, so the leg runs on a
+ * worker thread (the same pattern as the lemma-dialog leg of {@code DialogsVerifyF}) and
+ * logs its own marker line.
+ */
+ private void verifyMinimizeInteractionRealFlow(ProofControl proofControl, Proof proof) {
+ GeneralSettings gs = ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings();
+ boolean original = gs.getTacletFilter();
+ Thread worker = new Thread(() -> {
+ boolean armedNoDialog = false;
+ String appliedTaclet = "";
+ boolean unarmedDialog = false;
+ boolean unarmedUnapplied = false;
+ try {
+ // the wired path: the load-success hand-off mirrored the persisted flag into the
+ // loaded proof control (Swing MinimizeInteraction.updateMainWindow)
+ boolean wired = proofControl.isMinimizeInteraction() == original;
+ if (!wired) {
+ throw new IllegalStateException("proof control does not mirror the persisted"
+ + " flag (isMinimizeInteraction=" + proofControl.isMinimizeInteraction()
+ + ", tacletFilter=" + original + ")");
+ }
+ // flag armed: the auto-instantiable app applies without the dialog. The flag
+ // change must have run on the FX thread before the worker calls selectedTaclet
+ // (the core decides the minimize branch on the calling thread) — the latch joins
+ // the two, a plain runLater raced the leg (the unarmed leg then still ran armed)
+ setMinimizeInteraction(gs, proofControl, true);
+ TacletApp candidate = findMinimizeCandidate(proof);
+ if (candidate == null) {
+ throw new IllegalStateException("no auto-instantiable taclet application on"
+ + " the demo goal");
+ }
+ appliedTaclet = candidate.taclet().name().toString();
+ Object[] leg = applyTacletExpecting(proofControl, proof, candidate, false);
+ // armed leg: no dialog AND the application was made
+ armedNoDialog = (Boolean) leg[0] && (Boolean) leg[1];
+ // flag off: the same application must open the taclet dialog instead
+ setMinimizeInteraction(gs, proofControl, false);
+ TacletApp candidate2 = findMinimizeCandidate(proof);
+ if (candidate2 != null) {
+ Object[] leg2 = applyTacletExpecting(proofControl, proof, candidate2, true);
+ unarmedDialog = (Boolean) leg2[0];
+ // the dialog holds the application (cancel closes it without applying)
+ unarmedUnapplied = !((Boolean) leg2[1]);
+ }
+ } catch (Exception e) {
+ LOGGER.error("Minimize interaction verification (real flow) failed", e);
+ } finally {
+ // restore the persisted flag through the same apply helper the toggle uses
+ FxUtil.runLater(() -> {
+ gs.setTacletFilter(original);
+ applyMinimizeInteraction(proofControl);
+ });
+ boolean ok = armedNoDialog && unarmedDialog && unarmedUnapplied;
+ String report = "Minimize interaction verification (real flow): " + (ok ? "PASS"
+ : "FAIL")
+ + " (armed: " + appliedTaclet + " auto-applied without dialog="
+ + armedNoDialog + "; unarmed: dialog=" + unarmedDialog + " unapplied="
+ + unarmedUnapplied + ")";
+ LOGGER.info(report);
+ boolean reported = ok;
+ FxUtil.runLater(() -> NotificationManagerF.getInstance().notify(report,
+ reported ? Kind.INFO : Kind.ERROR));
+ }
+ }, "fx-verify-minimize-realloc");
+ worker.setDaemon(true);
+ worker.start();
+ }
+
+ /**
+ * menu: MP7/WP-9 — applies the given Minimize Interaction flag through the same helpers the
+ * toggle uses and blocks the caller until the change has run on the FX thread (the real-flow
+ * legs call {@code selectedTaclet} on the worker and the core reads the flag there — a plain
+ * {@code runLater} raced the leg so the unarmed leg still ran armed).
+ */
+ private void setMinimizeInteraction(GeneralSettings gs, ProofControl proofControl,
+ boolean value) throws InterruptedException {
+ CountDownLatch applied = new CountDownLatch(1);
+ FxUtil.runLater(() -> {
+ gs.setTacletFilter(value);
+ applyMinimizeInteraction(proofControl);
+ applied.countDown();
+ });
+ if (!applied.await(10, TimeUnit.SECONDS)) {
+ throw new IllegalStateException("the minimize flag change did not run on the FX"
+ + " thread within 10 s");
+ }
+ }
+
+ /**
+ * WP-9 minimize real-flow leg: applies the candidate through the real interactive path
+ * ({@code AbstractProofControl.selectedTaclet}) and watches, from the worker thread, whether
+ * the taclet dialog opens ("Choose Taclet Instantiation") and whether the rule application
+ * reaches the proof (node count).
+ *
+ * @param expectDialog whether the taclet dialog is expected to open
+ * @return {@code {dialogOpened, nodeApplied}} (the caller asserts them against the flag)
+ */
+ private Object[] applyTacletExpecting(ProofControl proofControl, Proof proof,
+ TacletApp candidate, boolean expectDialog) throws InterruptedException {
+ Goal goal = proof.openEnabledGoals().head();
+ de.uka.ilkd.key.proof.Node originalNode = goal.node();
+ int nodesBefore = proof.countNodes();
+ AtomicBoolean dialogOpened = new AtomicBoolean(false);
+ // watcher: detects the taclet dialog the unarmed leg must open (and must NOT open on the
+ // armed leg); closes it so the worker cannot hang on the modal loop
+ Thread watcher = new Thread(() -> {
+ long deadline = System.currentTimeMillis() + 20_000;
+ while (System.currentTimeMillis() < deadline) {
+ // Window.getWindows() is FX-thread restricted (JavaFX 25) — poll through
+ // callAndWait; the FX thread is alive and the dialog runs its own callbacks
+ Stage dialog = FxUtil
+ .callAndWait(() -> javafx.stage.Window.getWindows().stream()
+ .filter(w -> w instanceof Stage s && s.isShowing()
+ && "Choose Taclet Instantiation".equals(s.getTitle()))
+ .map(w -> (Stage) w).findFirst().orElse(null));
+ if (dialog != null) {
+ dialogOpened.set(true);
+ FxUtil.runLater(dialog::close);
+ return;
+ }
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ return;
+ }
+ }
+ }, "fx-verify-minimize-watcher");
+ watcher.setDaemon(true);
+ watcher.start();
+ try {
+ proofControl.selectedTaclet(candidate.taclet(), goal,
+ candidate.posInOccurrence());
+ } finally {
+ watcher.join(21_000);
+ }
+ boolean nodeApplied = proof.countNodes() == nodesBefore + 1;
+ if (nodeApplied) {
+ // restore the demo proof (the applied node is removed again)
+ proof.pruneProof(originalNode);
+ }
+ return new Object[] { expectDialog == dialogOpened.get(), nodeApplied };
+ }
+
+ /**
+ * WP-9 minimize real-flow leg helper: finds a taclet application on the open demo goal for
+ * which the flag decides between auto-apply and dialog (the single-app case of
+ * {@code AbstractProofControl.selectedTaclet}: incomplete app, no {@code \assumes}, unique at
+ * its position, auto-instantiable to complete-except-skolem-constants but never to
+ * {@code complete}).
+ */
+ private static TacletApp findMinimizeCandidate(Proof proof) {
+ Goal goal = proof.openEnabledGoals().head();
+ Services services = goal.proof().getServices();
+ List candidates = new ArrayList<>();
+ goal.ruleAppIndex().getNoFindTaclet(TacletFilter.TRUE, services)
+ .forEach(candidates::add);
+ var seq = goal.sequent();
+ for (int i = 1; i <= seq.size(); i++) {
+ PosInOccurrence pio = PosInOccurrence.findInSequent(seq, i, PosInTerm.getTopLevel());
+ goal.ruleAppIndex().getTacletAppAtAndBelow(TacletFilter.TRUE, pio, services)
+ .forEach(candidates::add);
+ }
+ for (TacletApp raw : candidates) {
+ if (raw.complete() || raw.posInOccurrence() == null) {
+ continue; // complete apps apply without the dialog under both flags
+ }
+ Taclet taclet = raw.taclet();
+ if (!taclet.assumesSequent().isEmpty()) {
+ continue; // the if-sequent interaction overrides the minimize decision
+ }
+ // the single-app branch of selectedTaclet: the taclet must have exactly one
+ // application at the position
+ long same = candidates.stream()
+ .filter(a -> a.taclet().name().equals(taclet.name())
+ && Objects.equals(a.posInOccurrence(), raw.posInOccurrence()))
+ .count();
+ if (same != 1) {
+ continue;
+ }
+ // mirror the armed-flag auto-instantiation of selectedTaclet
+ TacletApp inst = raw;
+ try {
+ TacletApp tmp = raw.tryToInstantiate(
+ services.getOverlay(goal.getLocalNamespaces()));
+ if (tmp != null) {
+ inst = tmp;
+ }
+ } catch (Exception e) {
+ continue; // not auto-instantiable → the armed flag would still open the dialog
+ }
+ if (inst.completeExceptSkolemConstants() && !inst.complete()) {
+ return raw;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * menu: MP4 — Right Click for Proof Macros check item (Swing RightMouseClickToggleAction,
+ * RightMouseClickToggleAction.java:22-33): the selected state mirrors
+ * {@code GeneralSettings.isRightClickMacro()} and the action writes
+ * {@code setRightClickMacros} back.
+ * // menu: MP7 — the right-click behavior is no longer deferred: while the flag is set the
+ * // sequent view shows the proof-macro popup instead of the term context menu
+ * // (SequentViewF.buildRightClickMenu, Swing CurrentGoalViewListener.java:54-67 /
+ * ProofMacroMenu).
+ */
+ private CheckMenuItem rightClickMacroToggle() {
+ GeneralSettings gs = ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings();
+ CheckMenuItem item = new CheckMenuItem("Right Click for Proof Macros");
+ item.setSelected(gs.isRightClickMacro());
+ item.setOnAction(e -> gs.setRightClickMacros(item.isSelected()));
+ return item;
+ }
+
+ /**
+ * menu: MP4 — Ensure Source Consistency check item (Swing
+ * EnsureSourceConsistencyToggleAction, EnsureSourceConsistencyToggleAction.java:37-48): the
+ * selected state mirrors {@code GeneralSettings.isEnsureSourceConsistency()} and the action
+ * writes {@code setEnsureSourceConsistency} back.
+ * // menu: MP7 — the flag is honored at runtime by the core and the FX soundiness report:
+ * // AbstractProblemLoader.createFileRepo (AbstractProblemLoader.java:400-409) picks the
+ * // DiskFileRepo (source-cache backend) over the SimpleFileRepo when it is set, and the FX
+ * // SoundinessAnalyzer warns when it is off (SoundinessAnalyzer.java:377-383). The Swing
+ * // info dialog of the toggle action (EnsureSourceConsistencyToggleAction.java:42-47) is
+ * // ported below (dialogs marker, audit item A9).
+ */
+ private CheckMenuItem ensureSourceConsistencyToggle() {
+ GeneralSettings gs = ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings();
+ CheckMenuItem item = new CheckMenuItem("Ensure Source Consistency");
+ item.setSelected(gs.isEnsureSourceConsistency());
+ item.setOnAction(e -> {
+ // dialogs (P2b, A9): the Swing toggle shows an info dialog when a proof is loaded —
+ // the change becomes effective with the NEXT load (Swing
+ // EnsureSourceConsistencyToggleAction.actionPerformed:42-47)
+ if (mediator.ensureProofLoaded()) {
+ Alert info = new Alert(Alert.AlertType.INFORMATION,
+ "Your changes will become effective when the next problem is loaded.\n");
+ info.setTitle("Allow Proof Bundle Saving");
+ info.setHeaderText("Allow Proof Bundle Saving");
+ info.initOwner(getStage());
+ info.show();
+ }
+ gs.setEnsureSourceConsistency(item.isSelected());
+ });
+ return item;
+ }
+
+ /**
+ * menu: MP5 — the About menu in the Swing order of {@code MainWindow.createHelpMenu}
+ * (:1163-1174): About KeY, KeY Homepage, Send Feedback, Create Github Issue, License. The
+ * browser actions go through the {@link HelpFacadeF} browser seam (host services); the
+ * feedback dialog is the minimal {@link FeedbackDialogF} port of the Swing
+ * {@code SendFeedbackAction}.
+ */
+ private Menu buildAboutMenu() {
+ // Swing parity: About menu mnemonic VK_A (MainWindow.java:1165, help menu)
+ Menu about = new Menu("_About");
+ about.getItems().addAll(
+ menuItem("About KeY…", "de.uka.ilkd.key.gui.actions.AboutAction", this::showAbout),
+ menuItem("KeY Homepage",
+ "de.uka.ilkd.key.gui.actions.KeYProjectHomepageAction",
+ () -> HelpFacadeF.openExternal(KEY_PROJECT_URL)),
+ menuItem("Send Feedback…", "de.uka.ilkd.key.gui.actions.MenuSendFeedackAction",
+ () -> FeedbackDialogF.show(stage)),
+ menuItem("Create Github Issue",
+ "de.uka.ilkd.key.gui.actions.CreateGithubIssueAction", this::createGithubIssue),
+ menuItem("License…", "de.uka.ilkd.key.gui.actions.LicenseAction",
+ IconFactoryF.Key.INFO_VIEW, this::showLicense));
+ return about;
+ }
+
+ private ToolBar buildFileToolBar() {
+ ToolBar bar = new ToolBar();
+ bar.getStyleClass().add("key-file-tool-bar");
+ // reviewer request: the toolbar buttons are icon-only (Swing shows the same buttons
+ // without their action text, JToolBar.add(Action) sets hideActionText for icon actions);
+ // the tooltips and the accessible names keep the discoverability
+ javafx.scene.control.Button openFile =
+ toolbarButton("Open", "Browse and load problem or proof files",
+ IconFactoryF.Key.OPEN_KEY_FILE, this::openFileChooser);
+ openFile.disableProperty().bind(mediator.autoModeRunningProperty());
+ javafx.scene.control.Button reload =
+ toolbarButton("Reload", "Reload last opened file", IconFactoryF.Key.OPEN_MOST_RECENT,
+ this::reloadLastFile);
+ reload.disableProperty()
+ .bind(mediator.autoModeRunningProperty().or(proofLoaded.not()));
+ javafx.scene.control.Button saveFile =
+ toolbarButton("Save", "Save current proof", IconFactoryF.Key.SAVE_FILE,
+ this::saveProofFile);
+ saveFile.disableProperty()
+ .bind(mediator.autoModeRunningProperty().or(proofLoaded.not()));
+ // proofmgmt: Swing file toolbar has the Proof Management button as well
+ javafx.scene.control.Button proofManagement =
+ toolbarButton("Proof Mgmt", "Proof Management", IconFactoryF.Key.PROOF_MANAGEMENT,
+ this::openProofManagement);
+ proofManagement.disableProperty().bind(mediator.autoModeRunningProperty());
+ bar.getItems().addAll(openFile, reload, saveFile, proofManagement);
+ return bar;
+ }
+
+ private ToolBar buildProofToolBar() {
+ // reviewer request: the automation + SMT controls are SplitMenuButtons (Swing
+ // DropdownSelectionButton: MainWindow.createAutomationComponent:836-862 and
+ // createSMTComponent:697-779); the automation dropdown lists the shared
+ // {@link #AUTOMATION_MACROS} (the same four macros as the Proof > Automation menu,
+ // Swing MainWindow.createAutomationActions:814-827, same order); selecting a dropdown
+ // item makes it the main-button macro (Swing DropdownSelectionButton with identity
+ // reducer + maxChoiceAmount 1). The main click stops auto mode while it runs,
+ // otherwise runs the selected macro ({@link #runMacro(ProofMacro)}, the Swing
+ // MacroAutomationAction.actionPerformed behavior; ProofControl.runMacro starts its own
+ // MacroThread). Enablement binds to proofLoaded -- deliberately NO auto-mode lock (a
+ // click while running stops it).
+ automationToolbarButton = new SplitMenuButton();
+ automationToolbarButton.getStyleClass().add("key-toolbar-split-button");
+ // reviewer request: icon-only toolbar controls — the graphic swaps between the start
+ // and stop glyphs (see the auto-mode listener below); the tooltip explains the action
+ automationToolbarButton
+ .setGraphic(IconFactoryF.createIcon(IconFactoryF.Key.AUTO_MODE_START));
+ automationToolbarButton.setTooltip(new Tooltip(
+ "Run the selected automation strategy; click while it runs to stop (the default macro is also bound to Ctrl+Space)"));
+ ObjectProperty selectedMacro = new SimpleObjectProperty<>();
+ selectedMacro.set(AUTOMATION_MACROS.get(0));
+ for (ProofMacro macro : AUTOMATION_MACROS) {
+ CheckMenuItem item = new CheckMenuItem(macro.getName());
+ item.setSelected(macro == selectedMacro.get());
+ item.setOnAction(ev -> {
+ selectedMacro.set(macro);
+ for (MenuItem other : automationToolbarButton.getItems()) {
+ if (other instanceof CheckMenuItem check) {
+ check.setSelected(other == item);
+ }
+ }
+ });
+ automationToolbarButton.getItems().add(item);
+ }
+ automationToolbarButton.setOnAction(e -> {
+ if (mediator.isInAutoMode()) {
+ mediator.stopAutoMode();
+ } else {
+ runMacro(selectedMacro.get());
+ }
+ });
+ automationToolbarButton.disableProperty().bind(proofLoaded.not());
+ // while auto mode runs the main part shows the stop action (Swing AutoModeAction swaps
+ // the button icon to the stop glyph while the automation runs)
+ mediator.autoModeRunningProperty().addListener((obs, was, running) -> {
+ if (running) {
+ automationToolbarButton
+ .setGraphic(IconFactoryF.createIcon(IconFactoryF.Key.AUTO_MODE_STOP));
+ } else {
+ automationToolbarButton
+ .setGraphic(IconFactoryF.createIcon(IconFactoryF.Key.AUTO_MODE_START));
+ }
+ });
+
+ // smt: the dropdown lists the usable solver unions (Swing MainWindow.updateSMTSelectMenu
+ // :1180-1210 creates an SMTInvokeAction per union); selecting an item makes it the
+ // active union (Swing sets ProofIndependentSettings activeSolverUnion in the change
+ // listener) and the main click launches it on all open goals in a background thread
+ // (Swing SMTInvokeAction.actionPerformed:65-84 -> SolverListenerF.launchOnProof). There
+ // is no SMT glyph in IconFactoryF.Key, so the configure icon stands in (Swing shows a
+ // solver image).
+ smtToolbarButton = new SplitMenuButton();
+ smtToolbarButton.getStyleClass().add("key-toolbar-split-button");
+ // reviewer request: icon-only toolbar controls (the configure glyph stands in for the
+ // solver image; the dropdown items carry the union names)
+ smtToolbarButton.setGraphic(IconFactoryF.createIcon(IconFactoryF.Key.CONFIGURE));
+ smtToolbarButton
+ .setTooltip(new Tooltip(
+ "Run the selected SMT solver union on all open goals (choose solvers under Options | SMT Solvers)"));
+ ObjectProperty selectedUnion = new SimpleObjectProperty<>();
+ Collection unions =
+ ProofIndependentSettings.DEFAULT_INSTANCE.getSMTSettings().getUsableSolverUnions();
+ for (SolverTypeCollection union : unions) {
+ CheckMenuItem item = new CheckMenuItem(union.name());
+ item.setSelected(union == selectedUnion.get());
+ item.setOnAction(ev -> {
+ selectedUnion.set(union);
+ // Swing SMTInvokeAction selection listener: the chosen union becomes the
+ // active solver union of the settings
+ ProofIndependentSettings.DEFAULT_INSTANCE.getSMTSettings()
+ .setActiveSolverUnion(union);
+ for (MenuItem other : smtToolbarButton.getItems()) {
+ if (other instanceof CheckMenuItem check) {
+ check.setSelected(other == item);
+ }
+ }
+ });
+ smtToolbarButton.getItems().add(item);
+ }
+ smtToolbarButton.setOnAction(e -> launchSmt(selectedUnion.get()));
+ smtToolbarButton.disableProperty().bind(proofLoaded.not());
+
+ // P3b/B11+B15: goal back + prune keep the static Swing tooltips (GoalBackAction
+ // SHORT_DESCRIPTION "Undo the last rule application.", PruneProofAction
+ // SHORT_DESCRIPTION "Prune the tree below the selected node."; the Swing toolbar
+ // actions are created with longName=false, MainWindow.java:696) and their enablement
+ // is driven by refreshGoalBackPrune (B15) like the corresponding Proof menu items --
+ // NOT by static bindings. The Start/Stop Automatic Proof toolbar buttons are replaced
+ // by the automation SplitMenuButton (Swing parity: the proof control toolbar has no
+ // separate start/stop buttons, MainWindow.java:686-706).
+ goalBackToolbarButton = toolbarButton("Goal Back", "Undo the last rule application.",
+ IconFactoryF.Key.GOAL_BACK, mediator::goalBack);
+ pruneToolbarButton =
+ toolbarButton("Prune Proof", "Prune the tree below the selected node.",
+ IconFactoryF.Key.PRUNE, mediator::pruneProof);
+ ToolBar bar = new ToolBar();
+ bar.getStyleClass().add("key-proof-tool-bar");
+ bar.getItems()
+ .addAll(automationToolbarButton, smtToolbarButton, goalBackToolbarButton,
+ pruneToolbarButton);
+ return bar;
+ }
+
+ /**
+ * Launches the selected solver union on all open goals (Swing SMTInvokeAction
+ * .actionPerformed:65-84): warns when no union is selected or no proof is loaded,
+ * otherwise starts the solver run in a background thread
+ * ({@link SolverListenerF#launchOnProof} blocks its thread until every solver finished).
+ *
+ * @param union the solver union to run, {@code null} when nothing is selected
+ */
+ private void launchSmt(SolverTypeCollection union) {
+ if (union == null) {
+ NotificationManagerF.getInstance()
+ .notify("No SMT solver union selected -- pick one in the dropdown"
+ + " (configure solvers under Options | SMT Solvers).", Kind.WARNING);
+ return;
+ }
+ Proof proof = mediator.getSelectedProof();
+ if (proof == null) {
+ return;
+ }
+ SolverListenerF.launchOnProof(mediator, stage, proof, union);
+ }
+
+ private javafx.scene.control.Button toolbarButton(String text, String tooltip,
+ IconFactoryF.Key icon, Runnable action) {
+ // reviewer request: the toolbar buttons are icon-only — the text is kept as the
+ // accessible name and in the tooltip prefix instead of the button label
+ javafx.scene.control.Button button =
+ new javafx.scene.control.Button((String) null, IconFactoryF.createIcon(icon));
+ button.setAccessibleText(text);
+ button.setTooltip(new Tooltip(tooltip));
+ button.setOnAction(e -> action.run());
+ return button;
+ }
+
+ private MenuItem menuItem(String text, Runnable action) {
+ return menuItem(text, null, null, action);
+ }
+
+ private MenuItem menuItem(String text, IconFactoryF.Key icon, Runnable action) {
+ return menuItem(text, null, icon, action);
+ }
+
+ private MenuItem menuItem(String text, String actionId, Runnable action) {
+ return menuItem(text, actionId, null, action);
+ }
+
+ /**
+ * Creates a menu item with an optional icon and accelerator (bound via the
+ * {@link KeyStrokeManagerF}). The item is registered with the manager, so a shortcut change
+ * in the settings dialog updates the accelerator live.
+ */
+ private MenuItem menuItem(String text, String actionId, IconFactoryF.Key icon,
+ Runnable action) {
+ MenuItem item = new MenuItem(text);
+ if (icon != null) {
+ item.setGraphic(IconFactoryF.createIcon(icon));
+ }
+ if (actionId != null) {
+ KeyStrokeManagerF manager = KeyStrokeManagerF.getInstance();
+ manager.binding(actionId).ifPresent(item::setAccelerator);
+ manager.register(item, actionId);
+ }
+ item.setOnAction(e -> action.run());
+ return item;
+ }
+
+ // ------------------------------------------------------------------
+ // status bar & actions
+ // ------------------------------------------------------------------
+
+ private HBox buildStatusBar() {
+ HBox bar = new HBox();
+ bar.getStyleClass().add("status-bar");
+ bar.setPadding(new Insets(2, 8, 2, 8));
+ // reviewer request: the status bar components align by their text baseline, so labels and
+ // taller buttons sit on one shared text line instead of being center-stacked
+ bar.setAlignment(Pos.BASELINE_LEFT);
+ // both labels must never drive the bar's size: a Label's min size defaults to its text
+ // size, and a long (multi-line) term text would blow the bar up and squeeze the workspace
+ statusLeft.setAlignment(Pos.BASELINE_LEFT);
+ statusLeft.setTextOverrun(OverrunStyle.ELLIPSIS);
+ statusLeft.setMinWidth(0);
+ statusLeft.setMinHeight(0);
+ statusLeft.setMaxWidth(Double.MAX_VALUE);
+ // D37 (P3c): the progress bar between the status text and the flex spacer (Swing
+ // MainStatusLine: lblStatusText, strut, progressBar, glue); hidden and unmanaged while
+ // no task drives it, so the bar does not participate in the status bar layout
+ statusProgress.setPrefWidth(120);
+ statusProgress.setMaxWidth(120);
+ statusProgress.setVisible(false);
+ statusProgress.setManaged(false);
+ HBox.setHgrow(statusLeft, Priority.ALWAYS);
+ Region spacer = new Region();
+ HBox.setHgrow(spacer, Priority.ALWAYS);
+ statusRight.setAlignment(Pos.BASELINE_RIGHT);
+ statusRight.setTextOverrun(OverrunStyle.ELLIPSIS);
+ statusRight.setMinWidth(0);
+ statusRight.setMinHeight(0);
+ bar.getChildren().addAll(statusLeft, statusProgress, spacer, statusRight);
+ // extension: MP9.0 — the status-line controls contributed by the FX extensions are
+ // appended at the right end of the status bar (Swing
+ // MainWindow.createStatusBar / KeYGuiExtensionFacade.getStatusLineComponents,
+ // KeYGuiExtensionFacade.java:325-333); shown only when an extension contributes
+ // controls.
+ List extensionStatusControls = KeYGuiExtensionFacadeF.getStatusLineControls();
+ if (!extensionStatusControls.isEmpty()) {
+ bar.getChildren().addAll(extensionStatusControls);
+ }
+ return bar;
+ }
+
+ /**
+ * Sets the left status text from the current selection: the copyright if no proof is
+ * selected, otherwise the proof name plus its closed state or open-goal count.
+ */
+ private void updateProofStatus() {
+ if (!FxUtil.isFxThread()) {
+ FxUtil.runLater(this::updateProofStatus);
+ return;
+ }
+ Proof proof = selectionModel.getSelectedProof();
+ proofLoaded.set(proof != null);
+ // P3b/B15: the goal-back / prune enablement follows the selection like the Swing actions
+ refreshGoalBackPrune();
+ if (proof == null) {
+ statusLeft.setText(KeYConstants.COPYRIGHT);
+ return;
+ }
+ if (proof.closed()) {
+ statusLeft.setText("Proof: " + proof.name() + " (closed)");
+ } else {
+ statusLeft.setText("Proof: " + proof.name() + " · " + proof.openGoals().size()
+ + " open goal(s)");
+ }
+ }
+
+ /**
+ * P3b/B15: applies the Swing-parity enablement of the Goal Back / Prune Proof controls to the
+ * two Proof menu items and the two proof toolbar buttons (Swing {@code GoalBackAction} and
+ * {@code PruneProofAction}, GoalBackAction.java:66-108 / PruneProofAction.java:54-109):
+ *
+ * Goal Back is enabled iff a proof is selected, a node is selected, the proof is not
+ * trivial ({@code !proof.root().leaf()}) and the selected node is not closed while
+ * {@code GeneralSettings.noPruningClosed} is set ({@code !(noPruningClosed &&
+ * selNode.isClosed())});
+ * Prune Proof is enabled iff proof + node are selected, the node is not a leaf and its
+ * subtree has open goals or — unless {@code noPruningClosed} — closed goals; and
+ * both are disabled while auto mode runs (Swing's AutoModeListener removes the selection
+ * listener and disables the actions, GoalBackAction.java:94-106).
+ *
+ * Called from {@link #updateProofStatus()} on every selection change and from the
+ * {@code autoModeRunningProperty} listener (both run on the FX thread).
+ */
+ private void refreshGoalBackPrune() {
+ boolean autoMode = mediator.autoModeRunningProperty().get();
+ Proof proof = selectionModel.getSelectedProof();
+ de.uka.ilkd.key.proof.Node selNode = selectionModel.getSelectedNode();
+ boolean goalBack = !autoMode && proof != null && selNode != null
+ && !proof.root().leaf()
+ && !(GeneralSettings.noPruningClosed && selNode.isClosed());
+ boolean prune = !autoMode && proof != null && selNode != null
+ && !selNode.leaf()
+ && (!proof.getSubtreeGoals(selNode).isEmpty()
+ || (!GeneralSettings.noPruningClosed
+ && !proof.getClosedSubtreeGoals(selNode).isEmpty()));
+ if (goalBackMenuItem != null) {
+ goalBackMenuItem.setDisable(!goalBack);
+ }
+ if (pruneMenuItem != null) {
+ pruneMenuItem.setDisable(!prune);
+ }
+ if (goalBackToolbarButton != null) {
+ goalBackToolbarButton.setDisable(!goalBack);
+ }
+ if (pruneToolbarButton != null) {
+ pruneToolbarButton.setDisable(!prune);
+ }
+ }
+
+ private void setTheme(Theme theme) {
+ ThemeManager.getInstance().setTheme(theme);
+ }
+
+ private void changeFontSize(int delta) {
+ ViewSettings viewSettings = ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings();
+ int next = Math.clamp(viewSettings.sizeIndex() + delta, 0, ConfigF.SIZES.length - 1);
+ viewSettings.setFontIndex(next);
+ NotificationManagerF.getInstance()
+ .notify("Font size " + ConfigF.SIZES[next] + " (will apply to the views in M2).");
+ }
+
+ private void openSettings() {
+ SettingsManagerF.getInstance().showSettingsDialog(this);
+ }
+
+ /**
+ * Opens the visual node diff window (Swing {@code ProofDiffFrame.Action}: a new window per
+ * invocation, non-modal, always enabled).
+ */
+ private void showProofDiffFrame() {
+ new ProofDiffFrameF(this).showCenteredOnOwner();
+ }
+
+ /**
+ * smalldialogs: opens the soundiness report for the selected proof (Swing
+ * {@code ShowSoundinessAction.actionPerformed}: modal {@code SoundinessDialog} for
+ * {@code mediator.getSelectedProof()}, no-op without a proof).
+ */
+ private void showSoundinessReport() {
+ Proof proof = selectionModel.getSelectedProof();
+ if (proof == null) {
+ return; // the item is disabled without a proof (Swing enableWhenProofLoaded)
+ }
+ new SoundinessDialogF(stage, proof).showCenteredOnOwner();
+ }
+
+ /**
+ * smalldialogs: self test of the soundiness report (system property
+ * {@code key.fx.verify.soundiness}, run after a proof load like the other proof-dependent
+ * verify hooks): the report of the loaded proof must contain the four report sections, then
+ * the dialog itself is opened for visual inspection (close it to continue).
+ */
+ private void runSoundinessVerification() {
+ Proof proof = selectionModel.getSelectedProof();
+ if (proof == null) {
+ LOGGER.info("Soundiness verification: skipped, no proof loaded FAIL");
+ NotificationManagerF.getInstance()
+ .notify("Soundiness verification: skipped, no proof loaded FAIL", Kind.ERROR);
+ return;
+ }
+ String html = SoundinessAnalyzer.generateHTMLReport(proof);
+ boolean sectionsOk = html.contains("KeY Soundiness Report")
+ && html.contains("1. General KeY Soundiness")
+ && html.contains("2. Taclet Option Soundiness")
+ && html.contains("3. Proof Tree Analysis");
+ // extension: MP9.0 — the proof-list context menu: the soundiness extension contributes
+ // the "Show Soundiness Report" item for the loaded proof (Swing TaskTree.checkPopup +
+ // SoundinessExtension.getContextActions with ContextMenuKind.PROOF_LIST)
+ java.util.List proofListItems =
+ KeYGuiExtensionFacadeF.getProofListContextItems(mediator, proof);
+ boolean proofListOk = proofListItems.size() == 1
+ && "Show Soundiness Report".equals(proofListItems.get(0).getText());
+ String report = "chars=" + html.length() + " sections=" + sectionsOk
+ + " proofList=" + proofListOk
+ + (sectionsOk && proofListOk ? " PASS" : " FAIL");
+ LOGGER.info("Soundiness verification: {}", report);
+ NotificationManagerF.getInstance()
+ .notify("Soundiness verification: " + report,
+ sectionsOk && proofListOk ? Kind.INFO : Kind.ERROR);
+ if (sectionsOk && proofListOk) {
+ new SoundinessDialogF(stage, proof).showCenteredOnOwner();
+ }
+ }
+
+ private void resetLayout() {
+ workspace.restoreFactoryDefault();
+ }
+
+ /**
+ * D34 (P3c): opens a {@link SequentViewDockF} with the sequent of the given proof node in a
+ * separate buffer (Swing {@code SequentViewDock.OpenCurrentNodeAction.actionPerformed}:
+ * create the dock and add it to the dock control, here via the workspace {@code MAIN} role).
+ * The dock's "Jump into Tree" title action selects the node in the main proof tree again
+ * (Swing {@code SequentViewDock.JumpIntoTreeAction}).
+ *
+ * @param node the proof node to display
+ * @return the opened dockable (self test / programmatic reuse)
+ */
+ Dockable openNodeInSeparateBuffer(de.uka.ilkd.key.proof.Node node) {
+ SequentViewDockF dock = new SequentViewDockF(node, () -> jumpIntoTree(node));
+ workspace.open(dock, DockLocation.MAIN);
+ return dock;
+ }
+
+ /** D34: selects the given node in the main proof tree, switching the proof if necessary. */
+ private void jumpIntoTree(de.uka.ilkd.key.proof.Node node) {
+ Proof proof = node.proof();
+ if (selectionModel.getSelectedProof() != proof) {
+ selectionModel.setSelectedProof(proof);
+ }
+ selectionModel.setSelectedNode(node);
+ }
+
+ /**
+ * A6 (P3c): JavaFX port of the Swing {@code CreateGithubIssueAction} About-menu entry
+ * (key.ui/.../gui/actions/CreateGithubIssueAction.java) — opens the GitHub new-issue page
+ * with the bug template pre-filled (current proof's Java sources + the internal version).
+ * The URL is opened through the {@link HelpFacadeF} browser seam; unlike the Swing original
+ * there is no "generated text" fallback dialog when no browser is available (the seam logs
+ * the failure instead).
+ */
+ private void createGithubIssue() {
+ HelpFacadeF.openExternal(buildGithubIssueUrl());
+ }
+
+ /**
+ * Builds the "new issue" URL for the current proof, with the GitHub bug template from the
+ * Swing {@code CreateGithubIssueAction} as pre-filled body:
+ *
+ * {@code %CHECKSUM%} is replaced by {@link KeYConstants#INTERNAL_VERSION}
+ * (Swing {@code KeYConstants.INTERNAL_VERSION}),
+ * {@code %JAVA%} is replaced by the {@code .java} sources of the selected proof's
+ * declared Java source location (Swing {@code SendFeedbackAction.getJavaSourceLocation} +
+ * the {@code Files.walk} over that location).
+ *
+ *
+ * Port notes: Swing encodes the body with {@code Charset.defaultCharset()}; this port uses
+ * UTF-8 (the encoding every {@code +} in the template relies on for correct decoding by
+ * GitHub).
+ *
+ * @return the fully encoded URL, e.g.
+ * {@code https://github.com/keyproject/key/issues/new?body=…}
+ */
+ String buildGithubIssueUrl() {
+ String template = """
+ ## Description
+ > Please describe your concern in detail!
+
+ %JAVA%
+
+ ## Reproducible
+
+ > Is the issue reproducible?
+ > Select one of: always, sometimes, random, have not tried, n/a
+
+ ### Steps to reproduce
+ > Describe the steps needed to reproduce the issue.
+
+ 1. ...
+ 2. ...
+ 3. ...
+ > What is your expected behavior and what was the actual behavior?
+
+ ### Additional information
+
+ > Add more details here. In particular: if you have a stacktrace, put it here.
+ ---
+ * Commit: %CHECKSUM%
+ """;
+ return GITHUB_ISSUE_URL + "?body="
+ + URLEncoder.encode(template
+ .replace("%CHECKSUM%", KeYConstants.INTERNAL_VERSION)
+ .replace("%JAVA%", collectJavaSources(selectionModel.getSelectedProof())),
+ StandardCharsets.UTF_8);
+ }
+
+ /**
+ * Collects the {@code .java} sources of the given proof for the GitHub issue body (Swing
+ * {@code CreateGithubIssueAction.actionPerformed}: walk over
+ * {@code SendFeedbackAction.getJavaSourceLocation(proof)}, one fenced block per file).
+ *
+ * @param proof the selected proof, may be {@code null}
+ * @return the concatenated source blocks, or the empty string when the proof has no Java
+ * source location
+ */
+ private static String collectJavaSources(Proof proof) {
+ if (proof == null) {
+ return "";
+ }
+ KeyAst.@Nullable Declarations header = proof.header();
+ if (header == null) {
+ return "";
+ }
+ final Path javaSourceLocation;
+ try {
+ javaSourceLocation = header.getJavaSourceLocation();
+ } catch (RuntimeException e) {
+ // no program source declared in the KeY file (Declarations.getJavaSourceLocation
+ // indexes ctx.programSource(0)); the Swing helper is not annotated either and
+ // would blow up the same way
+ return "";
+ }
+ if (javaSourceLocation == null) {
+ return "";
+ }
+ try (var walker = Files.walk(javaSourceLocation)) {
+ return walker.map(it -> {
+ try {
+ if (it.getFileName().toString().endsWith(".java")) {
+ return "* " + it.getFileName() + "\n```\n" + Files.readString(it) + "```\n";
+ }
+ } catch (IOException e) {
+ // skip unreadable files, like the Swing original (empty catch)
+ }
+ return null;
+ }).filter(Objects::nonNull).collect(Collectors.joining("\n"));
+ } catch (IOException e) {
+ LOGGER.warn("Could not collect Java sources for the GitHub issue", e);
+ return "";
+ }
+ }
+
+ private void showLicense() {
+ // A5 (P3c): the license becomes a real dialog (LicenseDialogF) — it IS inspectable legal
+ // text (KeY license + third-party libraries), so unlike "About KeY" an INFO toast would
+ // not suffice. This replaces the Swing LicenseAction.showLicense dialog.
+ LicenseDialogF.show(stage);
+ }
+
+ private void showAbout() {
+ // reviewer request: "About KeY" is a real (modal) dialog, not a notification toast;
+ // the dialog shows the Swing AboutAction content (copyright, WWW, version,
+ // AboutAction.java:33-39, titled "The KeY Project")
+ AboutDialogF.show(stage);
+ }
+
+ private void notYetImplemented() {
+ NotificationManagerF.getInstance().notify(
+ "This action arrives in a later milestone of the key.ui.fx rewrite.", Kind.WARNING);
+ }
+
+ // ------------------------------------------------------------------
+ // proof management (proofmgmt: JavaFX port of the Swing ProofManagementDialog
+ // and the Loaded Proofs/TaskTree view)
+ // ------------------------------------------------------------------
+
+ /**
+ * proofmgmt: opens the Proof Management dialog for the most recently loaded problem's init
+ * config (Swing {@code ProofManagementAction}); proofs started or selected in the dialog are
+ * registered in the multi-proof state and activated via {@link #activateProof(Proof)}.
+ */
+ private void openProofManagement() {
+ if (lastEnvironment == null) {
+ popupWarning("No problem has been loaded yet. Load a Java source or a "
+ + "problem with contracts first.");
+ return;
+ }
+ ProofManagementDialogF.showInstance(stage, lastEnvironment.getInitConfig(),
+ lastEnvironment.getUi(), this::activateAndRegisterProof,
+ selectionModel.getSelectedProof());
+ }
+
+ /**
+ * proofmgmt: activates the given proof (the selection model routes the switch through the
+ * mediator, which swaps the proof listeners and refreshes the views).
+ *
+ * @param proof the proof to make the active one
+ */
+ private void activateProof(Proof proof) {
+ selectionModel.setSelectedProof(proof);
+ }
+
+ /**
+ * proofmgmt: the proof selector handed to the Proof Management dialog: a proof started in
+ * the dialog is registered in the multi-proof state (it appears in the Loaded Proofs view)
+ * and activated.
+ *
+ * @param proof the started or selected proof
+ */
+ private void activateAndRegisterProof(Proof proof) {
+ proofManager.addProof(proof);
+ activateProof(proof);
+ }
+
+ /**
+ * proofmgmt: the {@code key.fx.verify.proofmgmt} self test driver. The value selects what is
+ * verified: {@code 1} (default) the Proof Management dialog with a real JML example,
+ * {@code 2} the Loaded Proofs view with two loaded proofs and active-proof switching,
+ * {@code all} both.
+ */
+ private void runProofMgmtVerification() {
+ String mode = System.getProperty("key.fx.verify.proofmgmt", "1").trim();
+ if (mode.equals("2")) {
+ runLoadedProofsVerification();
+ } else {
+ runProofMgmtDialogVerification();
+ if (mode.equals("all")) {
+ runLoadedProofsVerification();
+ }
+ }
+ }
+
+ /**
+ * proofmgmt: loads an example with the core {@link KeYEnvironment} on a background thread
+ * and hands it to the given continuation on the FX thread. The examples of this self test
+ * are loaded independently of the demo load and do not touch the recent files.
+ */
+ private void loadProofMgmtExample(Path location,
+ Consumer> onSuccess) {
+ Task> task = new Task<>() {
+ @Override
+ protected KeYEnvironment call() throws Exception {
+ return KeYEnvironment.load(location);
+ }
+ };
+ task.setOnSucceeded(e -> onSuccess.accept(task.getValue()));
+ task.setOnFailed(e -> {
+ LOGGER.error("Proof management self test: loading {} failed", location,
+ task.getException());
+ NotificationManagerF.getInstance()
+ .notify("Proof management self test load failed: " + location, Kind.ERROR);
+ });
+ Thread worker = new Thread(task, "fx-proofmgmt-loader");
+ worker.setDaemon(true);
+ worker.start();
+ }
+
+ /**
+ * WP-9: EnsureSourceConsistency real-flow self test ({@code key.fx.verify.sourceconsistency},
+ * proof-independent so it runs at startup like the other smalldialogs hooks). The Swing
+ * toggle arms the flag and its change becomes effective with the NEXT load
+ * (EnsureSourceConsistencyToggleAction.java:42-47); the core consumes the flag in
+ * {@code AbstractProblemLoader.createFileRepo} (DiskFileRepo = consistent source cache,
+ * SimpleFileRepo = pass-through). The leg loads a JML example with the flag armed and
+ * disarmed and asserts:
+ *
+ * the armed load produces a {@link DiskFileRepo} (the cache the source view's consistent
+ * source display and the proof bundle saving rely on) and the disarmed one a
+ * {@link SimpleFileRepo};
+ * the armed repo serves the Java source through {@code getInputStream} — the exact read
+ * the source view performs (SourceViewF.readRepoFile, Swing SourceView.addFile:617-621) —
+ * with content identical to the on-disk file, caching it under the repo's temp directory.
+ *
+ * The flag is restored afterwards (the app runs read the shared {@code ~/.key/v3.1}).
+ */
+ private void runSourceConsistencyVerification() {
+ GeneralSettings gs = ProofIndependentSettings.DEFAULT_INSTANCE.getGeneralSettings();
+ boolean original = gs.isEnsureSourceConsistency();
+ Path location = proofMgmtExample(System.getProperty("key.fx.demo.sourceconsistency",
+ "heap/model_methods/CellTest_test2.key"));
+ Thread worker = new Thread(() -> {
+ String repoOn = null;
+ String repoOff = null;
+ String readVia = null;
+ String cacheDir = null;
+ boolean ok = false;
+ try {
+ gs.setEnsureSourceConsistency(true);
+ KeYEnvironment env = KeYEnvironment.load(location);
+ try {
+ FileRepo repo = env.getLoadedProof().getInitConfig().getFileRepo();
+ repoOn = repo.getClass().getSimpleName();
+ ok = repo instanceof DiskFileRepo;
+ cacheDir = repoTmpDir(repo);
+ readVia = readSourceThroughRepo(repo, location);
+ ok &= readVia != null;
+ } finally {
+ env.dispose();
+ }
+ gs.setEnsureSourceConsistency(false);
+ KeYEnvironment env2 = KeYEnvironment.load(location);
+ try {
+ FileRepo repo = env2.getLoadedProof().getInitConfig().getFileRepo();
+ repoOff = repo.getClass().getSimpleName();
+ ok &= repo instanceof SimpleFileRepo;
+ } finally {
+ env2.dispose();
+ }
+ } catch (Exception e) {
+ LOGGER.error("Source consistency verification failed", e);
+ } finally {
+ gs.setEnsureSourceConsistency(original);
+ }
+ boolean reported = ok;
+ String report = "Source consistency verification: " + (ok ? "PASS" : "FAIL")
+ + " (armed repo=" + repoOn + " disarmed repo=" + repoOff + "; source read via"
+ + " repo=" + readVia + "; cache dir=" + cacheDir + ")";
+ LOGGER.info(report);
+ FxUtil.runLater(() -> NotificationManagerF.getInstance().notify(report,
+ reported ? Kind.INFO : Kind.ERROR));
+ }, "fx-verify-sourceconsistency");
+ worker.setDaemon(true);
+ worker.start();
+ }
+
+ /** WP-9 source consistency leg: the DiskFileRepo cache directory, read reflectively. */
+ private static String repoTmpDir(FileRepo repo) {
+ try {
+ Field field = DiskFileRepo.class.getDeclaredField("tmpDir");
+ field.setAccessible(true);
+ Object dir = field.get(repo);
+ return dir == null ? null : dir.toString();
+ } catch (ReflectiveOperationException e) {
+ return null;
+ }
+ }
+
+ /**
+ * WP-9 source consistency leg: reads a Java source of the example through the proof's
+ * {@link FileRepo} (the source view's read path) and compares it with the on-disk content.
+ *
+ * @return a description of the first file read through the repo with matching content, or
+ * {@code null} if no repo stream matched the disk content
+ */
+ private static String readSourceThroughRepo(FileRepo repo, Path location) {
+ Path src = location.getParent().resolve("src");
+ if (!Files.isDirectory(src)) {
+ return null;
+ }
+ try (java.util.stream.Stream files = Files.walk(src)) {
+ for (Path file : files.filter(p -> p.toString().endsWith(".java"))
+ .sorted().collect(Collectors.toList())) {
+ try (InputStream is = repo.getInputStream(file.toUri().toURL())) {
+ if (is == null) {
+ continue;
+ }
+ String viaRepo = IOUtil.readFrom(is);
+ String viaDisk = Files.readString(file);
+ if (viaRepo.equals(viaDisk)) {
+ return file.getFileName() + " ("
+ + viaRepo.split("\n", -1).length + " lines)";
+ }
+ } catch (IOException e) {
+ LOGGER.debug("Source consistency verification: repo read failed for {}", file,
+ e);
+ }
+ }
+ } catch (IOException e) {
+ LOGGER.debug("Source consistency verification: cannot list sources of {}", src, e);
+ }
+ return null;
+ }
+
+ /**
+ * proofmgmt: path of an example file, relative to the examples directory of the run task
+ * ({@code key.examples.dir}); absolute paths pass through.
+ */
+ private static Path proofMgmtExample(String relative) {
+ Path path = Path.of(relative);
+ if (path.isAbsolute()) {
+ return path;
+ }
+ return Path.of(System.getProperty("key.examples.dir", "key.ui/examples"), relative);
+ }
+
+ /**
+ * proofmgmt: the Proof Management dialog self test ({@code key.fx.verify.proofmgmt=1}). The
+ * dialog is exercised with a real JML example (default
+ * {@code heap/vstte10_01_SumAndMax/SumAndMax_sumAndMax.key}, override with
+ * {@code key.fx.demo.proofmgmt}): the dialog is shown non-blocking first (the structural
+ * checks read the laid-out scene graph), then the programmatic interactions run and are
+ * reported to the log; the dialog stays open for the visual inspection (the run continues
+ * when the dialog is closed with Cancel).
+ */
+ private void runProofMgmtDialogVerification() {
+ String example = System.getProperty("key.fx.demo.proofmgmt",
+ "heap/vstte10_01_SumAndMax/SumAndMax_sumAndMax.key");
+ loadProofMgmtExample(proofMgmtExample(example), env -> {
+ // the dialog self test's problem becomes the "most recent" one, so the dialog can
+ // also be opened from the File menu afterwards
+ lastEnvironment = env;
+ ProofManagementDialogF dialog = ProofManagementDialogF
+ .createForVerification(stage, env.getInitConfig(), env.getUi(),
+ this::activateAndRegisterProof);
+ // showForVerification first (non-blocking stage.show()), then the structural self
+ // test (ProofManagementDialogF.verifyDialog requires the shown dialog)
+ dialog.showForVerification();
+ String report = dialog.verifyDialog();
+ LOGGER.info("Proof management dialog verification: {}", report);
+ NotificationManagerF.getInstance()
+ .notify("Proof management dialog verification: " + report,
+ report.endsWith("PASS") ? Kind.INFO : Kind.ERROR);
+ // the dialog stays open for the visual inspection; the user closes it with Cancel
+ });
+ }
+
+ /**
+ * proofmgmt: the Loaded Proofs view self test ({@code key.fx.verify.proofmgmt=2}, also part
+ * of {@code all}). Loads two examples (default Agatha and the useQuery problem, override
+ * with {@code key.fx.demo.sequent2} and {@code key.fx.demo.sequent3}), registers both in the
+ * {@link ProofManagerF} and switches the active proof back and forth (the Swing TaskTree
+ * click semantics), verifying the rows, the selection sync and that the sequent view follows
+ * the switch.
+ */
+ private void runLoadedProofsVerification() {
+ String first = System.getProperty("key.fx.demo.sequent2",
+ "firstTouch/01-Agatha/project.key");
+ String second = System.getProperty("key.fx.demo.sequent3",
+ "standard_key/queries/useQuery.key");
+ loadProofMgmtExample(proofMgmtExample(first), envA -> loadProofMgmtExample(
+ proofMgmtExample(second), envB -> {
+ Proof proofA = envA.getLoadedProof();
+ Proof proofB = envB.getLoadedProof();
+ proofManager.addProof(proofA);
+ proofManager.addProof(proofB);
+ // A1: both proofs are registered (the Loaded Proofs view binds to the manager)
+ check("both proofs registered in ProofManagerF",
+ proofManager.contains(proofA) && proofManager.contains(proofB));
+ // A2: the Loaded Proofs view lists both with name/status/open-goal rows
+ check("Loaded Proofs view lists both proofs",
+ loadedProofs.getProofCount() == 2);
+ String rows = loadedProofs.verifyContent();
+ LOGGER.info("Loaded proofs rows verification: {}", rows);
+ check("Loaded Proofs rows in sync (" + rows + ")", rows.endsWith("PASS"));
+ // switch the active proof: B, then back to A (Swing TaskTree problemChosen)
+ proofManager.setActive(proofB);
+ check("switch to " + proofB.name() + ": selection model follows",
+ selectionModel.getSelectedProof() == proofB);
+ check("switch to " + proofB.name() + ": sequent view follows",
+ sequentView.getProof() == proofB);
+ check("switch to " + proofB.name() + ": rows follow",
+ loadedProofs.verifyContent().endsWith("PASS"));
+ proofManager.setActive(proofA);
+ check("switch back to " + proofA.name() + ": selection model follows",
+ selectionModel.getSelectedProof() == proofA);
+ check("switch back to " + proofA.name() + ": sequent view follows",
+ sequentView.getProof() == proofA);
+ check("switch back to " + proofA.name() + ": rows follow",
+ loadedProofs.verifyContent().endsWith("PASS"));
+ boolean ok = proofManager.getActiveProof() == proofA
+ && selectionModel.getSelectedProof() == proofA;
+ LOGGER.info("Loaded proofs verification: {}", ok ? "PASS" : "FAIL");
+ NotificationManagerF.getInstance()
+ .notify("Loaded proofs verification: " + (ok ? "PASS" : "FAIL"),
+ ok ? Kind.INFO : Kind.ERROR);
+ }));
+ }
+
+ /**
+ * proofmgmt: logs one PASS/FAIL self-test assertion line (the {@code
+ * key.fx.verify.proofmgmt} report).
+ *
+ * @param what the assertion description
+ * @param ok whether the assertion holds
+ */
+ private void check(String what, boolean ok) {
+ LOGGER.info("proofmgmt self test: {} {}", what, ok ? "PASS" : "FAIL");
+ }
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/ProofScriptWorkerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/ProofScriptWorkerF.java
new file mode 100644
index 00000000000..a1a0107e8af
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/ProofScriptWorkerF.java
@@ -0,0 +1,227 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.gui.fx;
+
+import java.net.URI;
+import javafx.application.Platform;
+import javafx.scene.Scene;
+import javafx.scene.control.Button;
+import javafx.scene.control.TextArea;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+import javafx.stage.Modality;
+import javafx.stage.Stage;
+import javafx.stage.Window;
+
+import de.uka.ilkd.key.control.AbstractUserInterfaceControl;
+import de.uka.ilkd.key.core.fx.KeYMediatorF;
+import de.uka.ilkd.key.core.fx.KeYSelectionModel;
+import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF;
+import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF.Kind;
+import de.uka.ilkd.key.nparser.KeyAst;
+import de.uka.ilkd.key.proof.Goal;
+import de.uka.ilkd.key.proof.Proof;
+import de.uka.ilkd.key.scripts.ProofScriptEngine;
+import de.uka.ilkd.key.scripts.ScriptException;
+
+import org.jspecify.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * P3b/B12: executes a proof script on the current proof, the JavaFX counter-part of the Swing
+ * {@code ProofScriptWorker} (ProofScriptWorker.java). Deliberate port differences:
+ *
+ * the log window is a JavaFX {@link Stage} (no FXML) with a non-editable monospace
+ * {@link TextArea} and a Stop button that interrupts the worker {@link Thread}
+ * (Swing: {@code JDialog} + {@code SwingWorker.cancel(true)}; Swing's {@code InterruptListener}
+ * is key.ui-only and not portable);
+ * the script runs on a plain background thread (Swing {@code SwingWorker.doInBackground}),
+ * the engine's command monitor appends to the log on the FX thread via {@link Platform#runLater};
+ *
+ * failures and interruptions are reported as notification toasts (Swing
+ * {@code IssueDialog.showExceptionDialog} for arbitrary {@code done()} failures); the interrupted
+ * case ends silently like Swing ({@code ProofScriptWorker.doInBackground}, ProofScriptWorker.java
+ * :90-92).
+ *
+ * The script itself is executed by the core {@link ProofScriptEngine} with the window's
+ * {@link AbstractUserInterfaceControl} (the {@link WindowUserInterfaceControlF}), so the prover
+ * callbacks and task observers of the commands reach the FX UI exactly like interactive runs.
+ */
+public final class ProofScriptWorkerF {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ProofScriptWorkerF.class);
+
+ private final KeYMediatorF mediator;
+ private final AbstractUserInterfaceControl ui;
+ private final KeyAst.ProofScript script;
+
+ /**
+ * the initially selected goal, may be {@code null} (then the engine picks the first open
+ * automatic goal).
+ */
+ private final @Nullable Goal initiallySelectedGoal;
+
+ /** owner of the log window, may be {@code null} (headless usage). */
+ private final @Nullable Window owner;
+
+ private @Nullable ProofScriptEngine engine;
+ private @Nullable Thread workerThread;
+ private @Nullable Stage monitor;
+ private @Nullable TextArea logArea;
+
+ /**
+ * Creates the worker.
+ *
+ * @param mediator the mediator (selection, proof lookup)
+ * @param ui the user interface control fed to the script engine ({@code
+ * WindowUserInterfaceControlF})
+ * @param script the parsed script
+ * @param initiallySelectedGoal the goal the script starts at, or {@code null}
+ * @param owner the owner window of the log dialog, or {@code null}
+ */
+ public ProofScriptWorkerF(KeYMediatorF mediator, AbstractUserInterfaceControl ui,
+ KeyAst.ProofScript script, @Nullable Goal initiallySelectedGoal,
+ @Nullable Window owner) {
+ this.mediator = mediator;
+ this.ui = ui;
+ this.script = script;
+ this.initiallySelectedGoal = initiallySelectedGoal;
+ this.owner = owner;
+ }
+
+ /**
+ * Starts the script (Swing {@code ProofScriptWorker.init()} + {@code execute()}): opens the
+ * non-modal log window and launches the background script thread. Must be called on the FX
+ * thread (the monitor is a JavaFX stage). Without a selected (or initially-selected) proof
+ * nothing is started.
+ */
+ public void start() {
+ Proof proof = initiallySelectedGoal != null ? initiallySelectedGoal.proof()
+ : mediator.getSelectedProof();
+ if (proof == null) {
+ LOGGER.warn("Proof script: no proof selected, not started");
+ return;
+ }
+ showMonitor(proof);
+ workerThread = new Thread(() -> runScript(proof), "fx-proof-script");
+ workerThread.start();
+ }
+
+ /**
+ * Opens the log window (Swing {@code ProofScriptWorker.makeDialog}, ProofScriptWorker.java
+ * :96-112).
+ */
+ private void showMonitor(Proof proof) {
+ URI uri = script.getStartLocation().getFileURI().orElse(null);
+ TextArea log = new TextArea("Running script from URL '" + uri + "':\n");
+ log.setEditable(false);
+ log.setStyle("-fx-font-family: monospace;");
+ VBox.setVgrow(log, Priority.ALWAYS);
+ Button cancelButton = new Button("Stop");
+ cancelButton.setOnAction(e -> {
+ Thread t = workerThread;
+ if (t != null) {
+ t.interrupt();
+ }
+ });
+ VBox box = new VBox(8, log, cancelButton);
+ Stage stage = new Stage();
+ stage.initOwner(owner);
+ stage.initModality(Modality.NONE);
+ stage.setTitle("Running Script ...");
+ stage.setScene(new Scene(box, 750, 400));
+ stage.show();
+ this.monitor = stage;
+ this.logArea = log;
+ }
+
+ /**
+ * The script thread (Swing {@code ProofScriptWorker.doInBackground}, ProofScriptWorker.java
+ * :84-94).
+ */
+ private void runScript(Proof proof) {
+ try {
+ ProofScriptEngine engine = new ProofScriptEngine(proof);
+ this.engine = engine;
+ engine.setInitiallySelectedGoal(initiallySelectedGoal);
+ engine.setCommandMonitor(msg -> Platform.runLater(() -> appendLog(msg)));
+ engine.execute(ui, script);
+ Platform.runLater(() -> finish(proof, null));
+ } catch (InterruptedException ex) {
+ LOGGER.debug("Proof script has been interrupted:", ex);
+ Platform.runLater(() -> finish(proof, "interrupted."));
+ } catch (ScriptException ex) {
+ LOGGER.error("", ex);
+ Platform.runLater(() -> finish(proof, "failed: " + ex.getMessage()));
+ } catch (RuntimeException ex) {
+ LOGGER.error("", ex);
+ Platform.runLater(() -> finish(proof, "failed: " + ex.getMessage()));
+ }
+ }
+
+ /**
+ * Closes the log window and, unless the run completed cleanly, reports the interruption or
+ * failure as a warning toast (the success path shows no notification — the proof tree /
+ * sequent views already react to the applied rules; Swing shows its TaskFinishedNotifications
+ * via the macros).
+ */
+ private void finish(Proof proof, @Nullable String warning) {
+ if (monitor != null) {
+ monitor.close();
+ }
+ if (warning != null) {
+ NotificationManagerF.getInstance()
+ .notify("Proof script " + warning, Kind.WARNING);
+ }
+ selectGoalOrNode(proof);
+ }
+
+ /**
+ * Appends one engine message to the log (Swing {@code ProofScriptWorker.process},
+ * ProofScriptWorker.java:115-139).
+ */
+ private void appendLog(ProofScriptEngine.Message info) {
+ TextArea log = logArea;
+ if (log == null) {
+ return;
+ }
+ StringBuilder message = new StringBuilder("\n---\n");
+ if (info instanceof ProofScriptEngine.EchoMessage(String msg)) {
+ message.append(msg);
+ } else {
+ var exec = (ProofScriptEngine.ExecuteInfo) info;
+ if (exec.command().startsWith("'echo ")) {
+ return;
+ }
+ exec.location().getFileURI().ifPresent(uri -> message.append(uri).append(":"));
+ message.append(exec.location().getPosition().line())
+ .append(": Executing on goal ").append(exec.nodeSerial()).append('\n')
+ .append(exec.command());
+ }
+ log.appendText(message.toString());
+ }
+
+ /**
+ * Selects the first open automatic goal of the script run, or the default selection when the
+ * proof is closed or the state map is unavailable (Swing {@code
+ * ProofScriptWorker.selectGoalOrNode}, ProofScriptWorker.java:176-191).
+ */
+ private void selectGoalOrNode(Proof proof) {
+ KeYSelectionModel selectionModel = mediator.getSelectionModel();
+ ProofScriptEngine engine = this.engine;
+ if (engine != null && !proof.closed()) {
+ try {
+ selectionModel.setSelectedGoal(engine.getStateMap().getFirstOpenAutomaticGoal());
+ return;
+ } catch (ScriptException e) {
+ LOGGER.warn("Script threw exception", e);
+ } catch (RuntimeException e) {
+ LOGGER.warn("Unexpected exception", e);
+ }
+ }
+ mediator.defaultSelection();
+ }
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/ProofSelectionDialogF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/ProofSelectionDialogF.java
new file mode 100644
index 00000000000..d1656dcb820
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/ProofSelectionDialogF.java
@@ -0,0 +1,252 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.gui.fx;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.zip.ZipFile;
+import javafx.collections.FXCollections;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.Scene;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.control.ListCell;
+import javafx.scene.control.ListView;
+import javafx.scene.control.SelectionMode;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import javafx.stage.Modality;
+import javafx.stage.Stage;
+import javafx.stage.Window;
+
+import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF;
+import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF.Kind;
+import de.uka.ilkd.key.gui.fx.theme.ThemeManager;
+
+import org.jspecify.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This dialog allows the user to select the proof to load from a proof bundle ({@code .zproof}
+ * zip archive). Counter-part of {@code de.uka.ilkd.key.gui.ProofSelectionDialog} in the Swing
+ * module {@code key.ui}: it lists all {@code .proof} files on the top level of the bundle (with
+ * contract-style proof names abbreviated) and returns the chosen entry relative to the bundle
+ * root. The bundle reading on load is provided by key.core ({@code AbstractProblemLoader} unzips
+ * the bundle and loads the chosen proof file).
+ *
+ * @author Wolfram Pfeifer (original Swing dialog)
+ */
+public final class ProofSelectionDialogF {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ProofSelectionDialogF.class);
+
+ /**
+ * Regex for identifiers (class, method), which catches for example "java.lang.Object",
+ * "SumAndMax", "sort(int[] a)", ...
+ */
+ private static final String IDENT = "(.*)";
+
+ /**
+ * Regex for the type of the contract (catches something like "JML normal_behavior operation
+ * contract").
+ */
+ private static final String TYPE = "(.*)";
+
+ /**
+ * Regex for the number of the proof.
+ */
+ private static final String NUM = "(\\d+)";
+
+ /**
+ * The pattern to match the filename of the proof.
+ */
+ private static final Pattern PROOF_NAME_PATTERN = Pattern.compile(
+ IDENT + "\\(" + IDENT + "__" + IDENT + "\\)\\)\\." + TYPE + "." + NUM + ".proof");
+
+ /**
+ * The path of the proof to load (relative to the root of the proof bundle, so actually just
+ * the filename of the proof file inside the bundle).
+ */
+ private @Nullable Path proofToLoad;
+
+ private final Stage stage = new Stage();
+
+ /**
+ * Creates a new ProofSelectionDialog for the given proof
+ *
+ * @param bundlePath the path of the proof bundle to load
+ * @param owner the owner window of the modal dialog
+ * @throws IOException if the proof bundle can not be read
+ */
+ private ProofSelectionDialogF(Path bundlePath, @Nullable Window owner) throws IOException {
+ stage.setTitle("Choose proof to load");
+
+ // create and fill list with proofs available for loading
+ ListView list = createAndFillList(bundlePath);
+
+ // create scroll pane with list ("Proofs found in bundle:" — Swing TitledBorder)
+ Label heading = new Label("Proofs found in bundle:");
+ heading.getStyleClass().add("dialog-section-title");
+ BorderPane.setMargin(heading, new Insets(4, 8, 4, 8));
+ BorderPane content = new BorderPane(list, heading, null, null, null);
+ BorderPane.setMargin(list, new Insets(0, 8, 0, 8));
+
+ // create panel with buttons
+ Button okButton = new Button("OK");
+ okButton.setDefaultButton(true);
+ okButton.setOnAction(e -> {
+ proofToLoad = list.getSelectionModel().getSelectedItem();
+ stage.close();
+ });
+ // disable "Ok" button if no proof was found
+ okButton.setDisable(list.getItems().isEmpty());
+ Button cancelButton = new Button("Cancel");
+ cancelButton.setCancelButton(true);
+ cancelButton.setOnAction(e -> stage.close());
+ Region spacer = new Region();
+ HBox.setHgrow(spacer, Priority.ALWAYS);
+ HBox buttonPanel = new HBox(5, spacer, okButton, cancelButton);
+ buttonPanel.setAlignment(Pos.CENTER_RIGHT);
+ buttonPanel.setPadding(new Insets(5, 8, 8, 8));
+ content.setBottom(buttonPanel);
+
+ stage.initModality(Modality.WINDOW_MODAL);
+ if (owner != null) {
+ stage.initOwner(owner);
+ }
+ Scene scene = new Scene(content, 450, 300);
+ ThemeManager.getInstance().style(scene);
+ stage.setScene(scene);
+ }
+
+ /**
+ * Creates a ListView and fills it with the proofs found in the bundle.
+ *
+ * @param bundlePath the path of the proof bundle
+ * @return the created list
+ * @throws IOException if the proof bundle can not be read
+ */
+ private ListView createAndFillList(Path bundlePath) throws IOException {
+ // create a list of all *.proof files (only top level in bundle)
+ List proofs;
+ // read zip
+ try (ZipFile bundle = new ZipFile(bundlePath.toFile())) {
+ proofs = bundle.stream().filter(e -> !e.isDirectory())
+ .filter(e -> e.getName().endsWith(".proof")).map(e -> Paths.get(e.getName()))
+ .collect(Collectors.toList());
+ }
+
+ // show the list in a ListView
+ ListView list = new ListView<>(FXCollections.observableArrayList(proofs));
+ list.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
+ list.getSelectionModel().selectFirst();
+ list.setCellFactory(view -> new ListCell<>() {
+ @Override
+ protected void updateItem(Path item, boolean empty) {
+ super.updateItem(item, empty);
+ // cells are reused: reset the text on every update
+ setText(empty || item == null ? null : abbreviateProofPath(item));
+ }
+ });
+ list.setOnMouseClicked(e -> {
+ // double click chooses the proof (Swing: mouse listener with getClickCount() >= 2)
+ if (e.getClickCount() >= 2) {
+ proofToLoad = list.getSelectionModel().getSelectedItem();
+ stage.close();
+ }
+ });
+ return list;
+ }
+
+ /**
+ * Abbreviates the filename of the proof if it matches the usual KeY format.
+ *
+ * @param proofPath the path (actually only the filename) of the proof
+ * @return the abbreviated proof name if it matches, the given path as String otherwise
+ */
+ private static String abbreviateProofPath(Path proofPath) {
+
+ final String pathString = proofPath.toString();
+ Matcher m = PROOF_NAME_PATTERN.matcher(pathString);
+ if (m.matches() && m.groupCount() == 5) {
+ String className = m.group(1);
+ String method = m.group(3);
+ String type = m.group(4).toLowerCase();
+ String num = m.group(5);
+
+ // type is either "normal", "exceptional", or empty
+ if (type.contains("normal")) {
+ type = "normal ";
+ } else if (type.contains("exceptional")) {
+ type = "exceptional ";
+ } else {
+ type = "";
+ }
+
+ // the stray closing parenthesis is part of the Swing original's output
+ return className + "::" + method + ") " + type + num;
+ }
+ // fallback: use complete filename
+ return pathString;
+ }
+
+ /**
+ * Shows the dialog with the given path and returns the filename of the proof to load.
+ *
+ * @param bundlePath the path of the proof bundle
+ * @param owner the owner window of the modal dialog
+ * @return the filename of the proof to load
+ */
+ private static @Nullable Path showDialog(Path bundlePath, @Nullable Window owner) {
+ Path proofPath = null;
+ try {
+ ProofSelectionDialogF dialog = new ProofSelectionDialogF(bundlePath, owner);
+ dialog.stage.showAndWait();
+ proofPath = dialog.proofToLoad;
+ } catch (IOException exc) {
+ LOGGER.error("", exc);
+ // Swing: IssueDialog.showExceptionDialog; the JavaFX UI reports via error toast
+ NotificationManagerF.getInstance()
+ .notify("Could not read the proof bundle " + bundlePath + ": "
+ + exc.getMessage(), Kind.ERROR);
+ }
+ return proofPath;
+ }
+
+ /**
+ * Shows a dialog and allows the user to choose the proof to load from a bundle.
+ *
+ * @param bundlePath the path of the proof bundle that is loaded
+ * @param owner the owner window of the modal dialog (the main window stage)
+ * @return the path of the proof relative to the bundle (proofs are always top level, which
+ * means the returned path will only contains the filename of the proof file) or null
+ * if the given path does not denote a bundle
+ */
+ public static @Nullable Path chooseProofToLoad(Path bundlePath, @Nullable Window owner) {
+ if (isProofBundle(bundlePath)) {
+ return showDialog(bundlePath, owner);
+ }
+ return null;
+ }
+
+ /**
+ * Checks if a path denotes a proof bundle.
+ *
+ * @param path the path to check
+ * @return true iff the path denotes a proof bundle
+ */
+ public static boolean isProofBundle(Path path) {
+ return path.toString().endsWith(".zproof");
+ }
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/SelectionHistoryF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/SelectionHistoryF.java
new file mode 100644
index 00000000000..8189ea4a52d
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/SelectionHistoryF.java
@@ -0,0 +1,297 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.gui.fx;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Deque;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+import javafx.beans.property.ReadOnlyBooleanProperty;
+import javafx.beans.property.ReadOnlyBooleanWrapper;
+
+import de.uka.ilkd.key.core.fx.KeYSelectionEvent;
+import de.uka.ilkd.key.core.fx.KeYSelectionListener;
+import de.uka.ilkd.key.core.fx.KeYSelectionModel;
+import de.uka.ilkd.key.proof.Node;
+import de.uka.ilkd.key.proof.Proof;
+import de.uka.ilkd.key.proof.event.ProofDisposedEvent;
+import de.uka.ilkd.key.proof.event.ProofDisposedListener;
+
+/**
+ * menu: MP3b — JavaFX port of the Swing {@code SelectionHistory} controller
+ * (key.ui/.../gui/SelectionHistory.java, consumed by the Swing {@code SelectionBackAction} /
+ * {@code SelectionForwardAction} of the View menu). Traces the proof nodes selected by the
+ * user and allows navigating backwards and forwards; the navigation operations and the history
+ * bookkeeping are identical to the Swing original (including the {@link WeakReference}s, the
+ * pruning of disposed/pruned nodes and the {@link ProofDisposedListener} cleanup of the forward
+ * history).
+ *
+ * The only difference to the Swing class: instead of Swing change listeners, the enablement of
+ * the Back/Forward actions is exposed as the JavaFX properties {@link #canGoBackProperty()} and
+ * {@link #canGoForwardProperty()}, recomputed on every history change (Swing recomputes it in
+ * {@code SelectionHistoryChangeListener.update}, SelectionBackAction.java:66-71).
+ */
+public class SelectionHistoryF implements KeYSelectionListener, ProofDisposedListener {
+ /**
+ * Previously selected nodes by the user. These are stored as weak references to avoid
+ * keeping disposed proofs alive (Swing comment, SelectionHistory.java:36-38).
+ */
+ private final Deque> selectedNodes = new ArrayDeque<>();
+ /**
+ * "Forward history": nodes the user navigated away from using this facility. These don't
+ * have to be stored as weak references because the user cannot dispose a referenced proof
+ * without navigating to it again (thereby clearing this list) — Swing
+ * SelectionHistory.java:42-44.
+ */
+ private final Deque selectionHistoryForward = new ArrayDeque<>();
+
+ /**
+ * Listeners watching this object for changes (Swing
+ * {@code de.uka.ilkd.key.gui.SelectionHistoryChangeListener}).
+ */
+ private final Collection listeners = new ArrayList<>();
+ /**
+ * The set of proofs this object is registered as a disposed listener to.
+ */
+ private final Set monitoredProofs = new HashSet<>();
+
+ /** enablement of the Back action: a valid previous selection exists. */
+ private final ReadOnlyBooleanWrapper canGoBack = new ReadOnlyBooleanWrapper(this, "canGoBack");
+ /** enablement of the Forward action: a valid entry exists in the forward history. */
+ private final ReadOnlyBooleanWrapper canGoForward =
+ new ReadOnlyBooleanWrapper(this, "canGoForward");
+
+ private final KeYSelectionModel selectionModel;
+
+ /**
+ * Construct a new selection history and register it on the selection model (Swing:
+ * {@code mediator.addKeYSelectionListener(this)}, SelectionHistory.java:60-63).
+ *
+ * @param selectionModel the window's selection model
+ */
+ public SelectionHistoryF(KeYSelectionModel selectionModel) {
+ this.selectionModel = selectionModel;
+ selectionModel.addKeYSelectionListener(this);
+ }
+
+ /**
+ * @return whether the Back action is available (a valid previous selection exists)
+ */
+ public ReadOnlyBooleanProperty canGoBackProperty() {
+ return canGoBack.getReadOnlyProperty();
+ }
+
+ /**
+ * @return whether the Forward action is available (a valid entry exists in the forward
+ * history)
+ */
+ public ReadOnlyBooleanProperty canGoForwardProperty() {
+ return canGoForward.getReadOnlyProperty();
+ }
+
+ /**
+ * Determine which node was previously selected by the user. May return null if the user
+ * hasn't selected anything previously or that proof has been closed. Port of the Swing
+ * method of the same name (SelectionHistory.java:72-96), including the pruning of stale
+ * references and the "navigate one node further" edge case when the stored previous node is
+ * the current selection.
+ *
+ * @return a node
+ */
+ public Node previousNode() {
+ if (!selectedNodes.isEmpty()) {
+ // remove current selection
+ Node currentSelection = selectedNodes.removeLast().get();
+ // navigate to previous selection
+ WeakReference previousNode = selectedNodes.peekLast();
+ Node previous = previousNode != null ? previousNode.get() : null;
+ // edge case: node may have been pruned away / proof may have been disposed
+ // (this leads to another edge case: previous == currentSelection, in that
+ // case we need to navigate one node further)
+ while (!selectedNodes.isEmpty() && (previous == null || (previous.proof().isDisposed()
+ || !previous.proof().find(previous)
+ || previous == currentSelection))) {
+ selectedNodes.removeLast();
+ previousNode = selectedNodes.peekLast();
+ previous = previousNode != null ? previousNode.get() : null;
+ }
+ if (previous != null) {
+ selectedNodes.addLast(new WeakReference<>(previous));
+ }
+ selectedNodes.addLast(new WeakReference<>(currentSelection));
+ return previous;
+ }
+ return null;
+ }
+
+ /**
+ * Show the previously selected node (Swing {@code SelectionHistory.navigateBack},
+ * SelectionHistory.java:101-113).
+ */
+ public void navigateBack() {
+ // navigate to previous selection
+ Node previous = previousNode();
+ if (previous != null) {
+ // store current selection for "forward history"
+ WeakReference currentSelectionNode = selectedNodes.removeLast();
+ Node currentSelection =
+ currentSelectionNode != null ? currentSelectionNode.get() : null;
+ selectionHistoryForward.addLast(currentSelection);
+ selectionModel.setSelectedNode(previous);
+ fireChangeEvent();
+ }
+ }
+
+ /**
+ * @return the next entry of the forward history (query method, modulo fixing up stale
+ * entries), or {@code null} — Swing {@code SelectionHistory.nextNode},
+ * SelectionHistory.java:115-135.
+ */
+ public Node nextNode() {
+ if (!selectionHistoryForward.isEmpty()) {
+ Node currentSelection = selectionModel.getSelectedNode();
+ // navigate to the next selection stored in the history
+ Node previous = selectionHistoryForward.removeLast();
+ // edge case: node may have been pruned away
+ // edge case #2: proof may have been closed
+ while (previous != null && (previous.proof().isDisposed()
+ || !previous.proof().find(previous)
+ || previous == currentSelection)) {
+ previous = !selectionHistoryForward.isEmpty() ? selectionHistoryForward.removeLast()
+ : null;
+ }
+ // this is a query method (modulo fixing up the history), re-instantiate previous state
+ if (previous != null) {
+ selectionHistoryForward.addLast(previous);
+ }
+ return previous;
+ }
+ return null;
+ }
+
+ /**
+ * Undo the last {@link #navigateBack()} call (Swing
+ * {@code SelectionHistory.navigateForward}, SelectionHistory.java:140-150).
+ */
+ public void navigateForward() {
+ // navigate to the next selection stored in the history
+ Node previous = nextNode();
+ if (previous != null) {
+ selectionHistoryForward.removeLast();
+ // add to history here to ensure the forward history isn't cleared
+ selectedNodes.addLast(new WeakReference<>(previous));
+ selectionModel.setSelectedNode(previous);
+ fireChangeEvent();
+ }
+ }
+
+ @Override
+ public void selectedNodeChanged(KeYSelectionEvent e) {
+ if (selectedNodes.isEmpty()) {
+ selectedNodes.add(new WeakReference<>(e.getSource().getSelectedNode()));
+ fireChangeEvent();
+ return;
+ }
+ Node last = selectedNodes.peekLast().get();
+ Node now = e.getSource().getSelectedNode();
+ if (last != now) {
+ selectedNodes.add(new WeakReference<>(now));
+ fireChangeEvent();
+ }
+ }
+
+ @Override
+ public void selectedProofChanged(KeYSelectionEvent e) {
+ Proof p = e.getSource().getSelectedProof();
+ if (p == null || monitoredProofs.contains(p)) {
+ return;
+ }
+ monitoredProofs.add(p);
+ p.addProofDisposedListener(this);
+ }
+
+ private void fireChangeEvent() {
+ // Swing: the change listeners update the action enablement
+ // (SelectionBackAction.update, SelectionBackAction.java:66-71); here the same
+ // enablement is exposed as JavaFX properties.
+ canGoBack.set(hasPrevious());
+ canGoForward.set(hasNext());
+ for (ChangeListener l : listeners) {
+ l.update();
+ }
+ }
+
+ /**
+ * @return whether a valid previous selection exists — the side-effect-free counterpart of
+ * the Swing enablement check {@code history.previousNode() != null}
+ * (SelectionBackAction.java:66-71): walk the history from the top, skipping the
+ * current selection and stale (disposed / pruned / garbage-collected) references.
+ */
+ private boolean hasPrevious() {
+ Node current = selectionModel.getSelectedNode();
+ Iterator> it = selectedNodes.descendingIterator();
+ while (it.hasNext()) {
+ Node candidate = it.next().get();
+ if (candidate == null || candidate == current) {
+ continue;
+ }
+ if (!candidate.proof().isDisposed() && candidate.proof().find(candidate)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @return whether the forward history contains a valid entry (Swing
+ * {@code SelectionForwardAction.update}: {@code history.nextNode() != null},
+ * SelectionForwardAction.java:65-70)
+ */
+ private boolean hasNext() {
+ for (Node candidate : selectionHistoryForward) {
+ if (candidate != null && !candidate.proof().isDisposed()
+ && candidate.proof().find(candidate)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Adds a change listener notified on every history change (Swing
+ * {@code SelectionHistoryChangeListener}); the Back/Forward menu items use the JavaFX
+ * enablement properties instead.
+ *
+ * @param listener the listener, not {@code null}
+ */
+ public void addChangeListener(ChangeListener listener) {
+ listeners.add(listener);
+ }
+
+ @Override
+ public void proofDisposing(ProofDisposedEvent e) {
+ }
+
+ @Override
+ public void proofDisposed(ProofDisposedEvent e) {
+ monitoredProofs.remove(e.getSource());
+ // clean up forward history
+ selectionHistoryForward.removeIf(x -> x.proof().isDisposed());
+ fireChangeEvent();
+ }
+
+ /**
+ * Listener notified whenever the history changed (the FX counterpart of the Swing
+ * {@code SelectionHistoryChangeListener} interface).
+ */
+ @FunctionalInterface
+ public interface ChangeListener {
+ void update();
+ }
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/UiControlSelfTestF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/UiControlSelfTestF.java
new file mode 100644
index 00000000000..2e9f3996e77
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/UiControlSelfTestF.java
@@ -0,0 +1,180 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.gui.fx;
+
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import javafx.application.Platform;
+
+import de.uka.ilkd.key.gui.fx.dialogs.FeedbackDialogF;
+import de.uka.ilkd.key.gui.fx.docking.Dockable;
+import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF;
+import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF.Kind;
+import de.uka.ilkd.key.gui.fx.notification.ProofStatisticsDialogF;
+import de.uka.ilkd.key.gui.fx.settings.ToolTipOptionsDialogF;
+import de.uka.ilkd.key.proof.Proof;
+
+import org.key_project.util.javafx.FxUtil;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Interactive self test of the {@link WindowUserInterfaceControlF} seam, enabled with the system
+ * property {@code key.fx.verify.uicontrol} (the established {@code key.fx.verify.*} pattern of
+ * {@code MainWindowF}). Runs after the demo proof load and programmatically fires the seam
+ * callbacks:
+ *
+ * {@code reportStatus} — asserts that the status bar shows the message;
+ * {@code reportException} with a synthetic exception — asserts that an {@link IssueDialogF}
+ * is visible with the exception as issue;
+ * {@link LogViewF} — opens the log view, emits log lines and asserts that they are
+ * rendered;
+ * {@link AutoDismissDialogF} — shows the auto-dismiss popup (with an extended delay so it
+ * survives until the interactive inspection) and asserts that it is visible;
+ * P3c dialogs: the proof-statistics CSV/HTML export (A4), the GitHub issue URL (A6), the
+ * feedback ZIP archive (A7), the tooltip "Save as Default" persistence (A8), the separate node
+ * buffer (D34) and the status progress bar transitions (D37).
+ *
+ * Each step logs a {@code UIControl seam self test: ... PASS/FAIL} line and shows a toast.
+ */
+public final class UiControlSelfTestF {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(UiControlSelfTestF.class);
+
+ /** The base of the GitHub new-issue URL (Swing CreateGithubIssueAction.URL). */
+ private static final String GITHUB_ISSUE_BASE =
+ "https://github.com/keyproject/key/issues/new?body=";
+
+ private UiControlSelfTestF() {
+ }
+
+ /** Runs the self test on the given main window (call on the FX thread). */
+ public static void run(MainWindowF mainWindow) {
+ LOGGER.info("UIControl seam self test: starting");
+ WindowUserInterfaceControlF ui = mainWindow.getUserInterfaceControl();
+
+ // 1. status line: reportStatus must update the MainWindowF status bar
+ String statusMessage = "Seam self test: status message via reportStatus";
+ ui.reportStatus(UiControlSelfTestF.class, statusMessage);
+ FxUtil.runLater(() -> {
+ String status = mainWindow.getStatusLineText();
+ boolean pass = status.contains(statusMessage);
+ report("status verification", pass, "status line = '" + status + "'");
+
+ // 2. exception reporting: reportException must open an IssueDialogF. The seam shows
+ // a modal showAndWait dialog; headless there is no user to click "OK", so a close is
+ // scheduled BEFORE the callback fires — the modal's nested event loop picks the task
+ // up and showAndWait returns, letting the verification run right after the dismissal
+ Platform.runLater(() -> IssueDialogF.getLastDialog().ifPresent(IssueDialogF::close));
+ ui.reportException(UiControlSelfTestF.class, null,
+ new IllegalStateException("Seam self test: synthetic exception"));
+ FxUtil.runLater(() -> {
+ IssueDialogF dialog = IssueDialogF.getLastDialog().orElse(null);
+ boolean dialogPass = dialog != null && dialog.wasShown()
+ && dialog.getIssues().stream()
+ .anyMatch(i -> i.text().contains("synthetic exception"));
+ report("exception verification", dialogPass,
+ dialog == null ? "no IssueDialogF was opened"
+ : "IssueDialogF wasShown=" + dialog.wasShown() + ", issues="
+ + dialog.getIssues().size());
+
+ // 3. log view: emitted log lines must be rendered by the LogViewF
+ LogViewF logView = new LogViewF(LogViewF.resolveLogFile());
+ LOGGER.info("Seam self test: log entry A");
+ LOGGER.warn("Seam self test: log entry B");
+ logView.refresh();
+ FxUtil.runLater(() -> {
+ String logReport = logView.verifyContains("Seam self test: log entry A");
+ boolean logPass = logReport.endsWith("PASS");
+ report("log view verification", logPass, logReport);
+ LogViewF.showInstance(mainWindow.getStage());
+
+ // 4. auto-dismiss popup (extended delay so it survives the inspection; the
+ // countdown code path is identical to the Swing default timings)
+ AutoDismissDialogF autoDismiss = new AutoDismissDialogF(mainWindow.getStage(),
+ "Seam self test: auto-dismiss countdown", 60000, 100, 5000, 5000);
+ autoDismiss.show();
+ FxUtil.runLater(() -> {
+ report("auto dismiss verification", autoDismiss.isVisible(),
+ "AutoDismissDialogF visible=" + autoDismiss.isVisible());
+
+ runP3cSteps(mainWindow);
+ });
+ });
+ });
+ });
+ }
+
+ /** P3c steps 5-10: the A4/A6/A7/A8/D34/D37 seams (call on the FX thread). */
+ private static void runP3cSteps(MainWindowF mainWindow) {
+ Proof proof = mainWindow.getSelectionModel().getSelectedProof();
+
+ // 5. A4: the proof-statistics CSV/HTML export behind the buttons
+ String statsReport = ProofStatisticsDialogF.verifyStatisticsExport(proof);
+ report("statistics export verification",
+ statsReport.startsWith("PASS") || statsReport.startsWith("SKIP"), statsReport);
+
+ // 6. A6: the GitHub issue URL built by MainWindowF (Swing CreateGithubIssueAction):
+ // the body decodes to the bug template with %CHECKSUM% replaced and the Java sources
+ String issueUrl = mainWindow.buildGithubIssueUrl();
+ String issueBody = URLDecoder.decode(
+ issueUrl.substring(issueUrl.indexOf('?') + 1).replaceFirst("^body=", ""),
+ StandardCharsets.UTF_8);
+ boolean issueOk = issueUrl.startsWith(GITHUB_ISSUE_BASE)
+ && issueBody.contains("## Reproducible") && issueBody.contains("* Commit: ")
+ && !issueBody.contains("%CHECKSUM%");
+ report("github issue url verification", issueOk, "url chars=" + issueUrl.length());
+
+ // 7. A7: the feedback ZIP archive (bug description, version, system properties, logs)
+ String archiveReport = FeedbackDialogF.verifyLogArchive();
+ report("log archive verification", archiveReport.endsWith("PASS"), archiveReport);
+
+ // 8. A8: "Save as Default" persists the tooltip options into the settings file
+ String defaultReport = ToolTipOptionsDialogF.verifySaveAsDefault();
+ report("save as default verification", defaultReport.endsWith("PASS"), defaultReport);
+
+ // 9. D34: open a node in a separate sequent buffer, close it again
+ if (proof != null) {
+ Dockable dock = mainWindow.openNodeInSeparateBuffer(proof.root());
+ boolean open = mainWindow.getWorkspace().isOpen(dock.getId());
+ boolean titled = dock.getTitle().startsWith("Node: ");
+ mainWindow.getWorkspace().close(dock);
+ boolean closed = !mainWindow.getWorkspace().isOpen(dock.getId());
+ report("node buffer verification", open && titled && closed,
+ "open=" + open + " titled=" + titled + " closed=" + closed);
+ } else {
+ report("node buffer verification", false, "no proof selected");
+ }
+
+ // 10. D37: status progress bar visibility/value transitions (Swing MainStatusLine):
+ // maximum 0 hides the bar, a positive maximum shows it determinate, a negative maximum
+ // switches it to indeterminate ("busy") mode, hideStatusProgress hides it again
+ mainWindow.setStatusLine("D37 self test", 0);
+ boolean hiddenZero = !mainWindow.isStatusProgressVisible();
+ mainWindow.setStatusLine("D37 self test", 100);
+ boolean visibleDeterminate = mainWindow.isStatusProgressVisible();
+ mainWindow.setTaskProgressValue(50);
+ double value = mainWindow.getStatusProgressValue();
+ mainWindow.setStatusLine("D37 self test", -1);
+ boolean indeterminate = mainWindow.isStatusProgressVisible()
+ && mainWindow.getStatusProgressValue() < 0;
+ mainWindow.hideStatusProgress();
+ boolean hiddenAgain = !mainWindow.isStatusProgressVisible();
+ boolean progressOk = hiddenZero && visibleDeterminate && value >= 0.49 && value <= 0.51
+ && indeterminate && hiddenAgain;
+ report("status progress verification", progressOk,
+ "hidden0=" + hiddenZero + " determinate=" + visibleDeterminate + " value=" + value
+ + " indeterminate=" + indeterminate + " hiddenAgain=" + hiddenAgain);
+ }
+
+ /** Logs the PASS/FAIL result and shows a toast (the established verification pattern). */
+ private static void report(String name, boolean pass, String detail) {
+ String verdict = pass ? "PASS" : "FAIL";
+ LOGGER.info("UIControl seam self test: {} verification: {} ({})", name, verdict, detail);
+ NotificationManagerF.getInstance().notify("UIControl seam self test " + name + ": "
+ + verdict, pass ? Kind.INFO : Kind.ERROR);
+ }
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/UserActionListenerF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/UserActionListenerF.java
new file mode 100644
index 00000000000..38d96f452db
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/UserActionListenerF.java
@@ -0,0 +1,27 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.gui.fx;
+
+import de.uka.ilkd.key.gui.fx.actions.useractions.UserActionF;
+
+import org.jspecify.annotations.NullMarked;
+
+/**
+ * Listener for user actions, FX counterpart of
+ * {@code de.uka.ilkd.key.gui.UserActionListener} (Swing UserActionListener.java:1-21).
+ *
+ * @see UserActionF
+ */
+@NullMarked
+@FunctionalInterface
+public interface UserActionListenerF {
+
+ /**
+ * Handle a user action just performed by the user.
+ *
+ * @param action the user action
+ */
+ void actionPerformed(UserActionF action);
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/WindowUserInterfaceControlF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/WindowUserInterfaceControlF.java
new file mode 100644
index 00000000000..fda76ac4caf
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/WindowUserInterfaceControlF.java
@@ -0,0 +1,567 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.gui.fx;
+
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.concurrent.CopyOnWriteArrayList;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.stage.Stage;
+
+import de.uka.ilkd.key.control.AbstractProofControl;
+import de.uka.ilkd.key.control.DefaultProofControl;
+import de.uka.ilkd.key.control.DefaultUserInterfaceControl;
+import de.uka.ilkd.key.control.RuleCompletionHandler;
+import de.uka.ilkd.key.control.instantiation_model.TacletInstantiationModel;
+import de.uka.ilkd.key.gui.fx.notification.NotificationManagerF;
+import de.uka.ilkd.key.gui.fx.tacletmatch.TacletMatchDialogF;
+import de.uka.ilkd.key.gui.fx.tacletmatch.classic.TacletMatchCompletionDialogF;
+import de.uka.ilkd.key.java.Services;
+import de.uka.ilkd.key.macros.ProofMacro;
+import de.uka.ilkd.key.pp.NotationInfo;
+import de.uka.ilkd.key.proof.Goal;
+import de.uka.ilkd.key.proof.Proof;
+import de.uka.ilkd.key.proof.ProofAggregate;
+import de.uka.ilkd.key.proof.event.ProofDisposedEvent;
+import de.uka.ilkd.key.proof.init.ProofOblInput;
+import de.uka.ilkd.key.rule.IBuiltInRuleApp;
+import de.uka.ilkd.key.settings.ProofIndependentSettings;
+import de.uka.ilkd.key.settings.ViewSettings;
+import de.uka.ilkd.key.speclang.PositionedString;
+import de.uka.ilkd.key.strategy.StrategyProperties;
+
+import org.key_project.prover.engine.ProverCore;
+import org.key_project.prover.engine.TaskFinishedInfo;
+import org.key_project.prover.engine.TaskStartedInfo;
+import org.key_project.prover.engine.impl.ApplyStrategyInfo;
+import org.key_project.util.collection.ImmutableSet;
+import org.key_project.util.javafx.FxUtil;
+
+import org.antlr.v4.runtime.misc.ParseCancellationException;
+import org.jspecify.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Implementation of {@link de.uka.ilkd.key.control.UserInterfaceControl} which controls the
+ * {@link MainWindowF} with the typical user interface of KeY: it routes the status, progress,
+ * task, exception and warning callbacks of the core into the main window and hosts the registry
+ * of interactive built-in rule completions.
+ *
+ * Port of {@code de.uka.ilkd.key.gui.WindowUserInterfaceControl} in the Swing module
+ * {@code key.ui}. Structural deviation (forced by the module layout): the Swing original
+ * extends {@code AbstractMediatorUserInterfaceControl} (a key.ui class which also provides the
+ * {@code MediatorProofControl}; key.ui is not a dependency of key.ui.fx), while this port extends
+ * the core {@link DefaultUserInterfaceControl} and implements {@link RuleCompletionHandler}
+ * directly. Because {@code super(this)} is illegal Java, the constructor calls the handler-less
+ * {@code super()} and builds its own {@link DefaultProofControl} with {@code this} as handler
+ * (returned by {@link #getProofControl()}) — the mirror of the Swing wiring {@code
+ * MediatorProofControl extends AbstractProofControl} constructed with
+ * {@code super(ui, ui)} (MediatorProofControl.java:59-60,
+ * AbstractMediatorUserInterfaceControl.java:51-52).
+ *
+ * Completion dispatch (Swing WindowUserInterfaceControl.java:353-365): in auto mode the default
+ * completion bypasses all dialogs (:354-356), otherwise the first registered completion whose
+ * {@code canComplete} matches completes the app (:357-363) and the result is returned only when
+ * it is complete (:364). A {@code null} or incomplete result makes the core
+ * {@code AbstractProofControl} fall back to {@code completeBuiltInRuleAppByDefault}
+ * (AbstractProofControl.java:496-511, :519-524) — the same silent-skip semantics as Swing for
+ * apps the user cannot complete.
+ *
+ * Deliberately deferred (see the port report): the load/save/recents methods of the Swing
+ * original (the FX load path lives in {@code MainWindowF.startProofLoad}),
+ * {@code selectProofObligation}
+ * ({@code ProofManagementDialog}) and the progress bar (the FX status bar has no progress bar
+ * yet). The taclet-match dialogs are wired through {@link #completeAndApplyTacletMatch}
+ * (termmenu/S4).
+ */
+public class WindowUserInterfaceControlF extends DefaultUserInterfaceControl
+ implements RuleCompletionHandler {
+ private static final Logger LOGGER =
+ LoggerFactory.getLogger(WindowUserInterfaceControlF.class);
+
+ private final MainWindowF mainWindow;
+
+ /**
+ * The registered completions, dispatched in registration order (Swing
+ * {@code LinkedList completions},
+ * WindowUserInterfaceControl.java:71-72). Copy-on-write: the registry is filled from the FX
+ * thread while a completion may dispatch from a prover thread.
+ */
+ private final List completions =
+ new CopyOnWriteArrayList<>();
+
+ /**
+ * The registered {@link RuleCompletionHandler}s (consulted after all completions,
+ * see {@link #register(RuleCompletionHandler)}).
+ */
+ private final List handlers = new CopyOnWriteArrayList<>();
+
+ /**
+ * The loaded proofs (bookkeeping for the later TaskTree/"Loaded Proofs" dockable; Swing
+ * {@code mainWindow.addProblem(pa)}, WindowUserInterfaceControl.java:519-525, removal in
+ * {@code proofDisposing} :507-512).
+ */
+ private final ObservableList proofAggregates = FXCollections
+ .observableArrayList();
+
+ /**
+ * The consulted {@link DefaultProofControl}: constructed explicitly in the constructor
+ * because {@code super(this)} is illegal Java ("cannot reference this before the supertype
+ * constructor has been called"); see the constructor comment.
+ */
+ private final DefaultProofControl proofControl;
+
+ public WindowUserInterfaceControlF(MainWindowF mainWindow) {
+ super();
+ this.mainWindow = mainWindow;
+ // seam: this control is its own RuleCompletionHandler — the mirror of the Swing wiring
+ // MediatorProofControl extends AbstractProofControl constructed with super(ui, ui)
+ // (MediatorProofControl.java:59-60). super(this) does not compile, so the consulted
+ // proof control is built here explicitly (and returned by getProofControl).
+ proofControl = new DefaultProofControl(this, this, this);
+ // seam: completion registry — the FX merge point for interactive rule completions,
+ // mirroring the Swing constructor registration (WindowUserInterfaceControl.java:74-82):
+ // FunctionalOperationContractCompletion (:76), DependencyContractCompletion (:77),
+ // LoopInvariantRuleCompletion (:78), BlockContractInternalCompletion(mainWindow) (:79),
+ // BlockContractExternalCompletion(mainWindow) (:80), MergeRuleCompletion.INSTANCE (:81).
+ // Those completions (and their dialogs) are ported by separate work items; they register
+ // here via register(InteractiveRuleApplicationCompletionF) /
+ // register(RuleCompletionHandler). With an empty registry the dispatch falls back to
+ // the core default completion (the behavior of the former headless control).
+ }
+
+ // ------------------------------------------------------------------
+ // completion registry (merge point for the interactive-completion agents)
+ // ------------------------------------------------------------------
+
+ /**
+ * seam: the {@link DefaultProofControl} consulted by the core
+ * ({@code KeYEnvironment.getProofControl});
+ * it dispatches the interactive rule completions through this handler.
+ */
+ @Override
+ public DefaultProofControl getProofControl() {
+ return proofControl;
+ }
+
+ /**
+ * Registers a completion at the end of the dispatch chain: the first registered completion
+ * whose {@code canComplete(app)} matches completes the app (Swing dispatch
+ * WindowUserInterfaceControl.java:357-363).
+ *
+ * @param completion the completion to register
+ */
+ public void register(InteractiveRuleApplicationCompletionF completion) {
+ completions.add(completion);
+ }
+
+ /**
+ * The registered interactive completions in dispatch order (verification harness and
+ * diagnostics; the dispatch chain is consulted in registration order, see the field
+ * comment).
+ *
+ * @return the registered completions
+ */
+ public List getCompletions() {
+ return completions;
+ }
+
+ /**
+ * Registers a core {@link RuleCompletionHandler} (e.g. an interactive taclet-match
+ * completion port) that is consulted after all registered
+ * {@link InteractiveRuleApplicationCompletionF}s. Such a handler has no {@code canComplete};
+ * by convention it returns {@code null} or the unchanged app if it does not want to complete
+ * the app — in that case the core performs the default completion
+ * ({@code AbstractProofControl.completeBuiltInRuleApp}, AbstractProofControl.java:496-511).
+ *
+ * @param handler the handler to register
+ */
+ public void register(RuleCompletionHandler handler) {
+ handlers.add(handler);
+ }
+
+ /**
+ * Dispatches the completion of a built-in rule application (port of Swing
+ * WindowUserInterfaceControl.java:353-365): in auto mode the default completion bypasses all
+ * dialogs (:354-356), otherwise the first matching completion (or, as fallback, a registered
+ * {@link RuleCompletionHandler}) completes the app and the result is returned only when it
+ * is complete (:364); an incomplete/null result makes the core fall back to the default
+ * completion (AbstractProofControl.java:496-511).
+ */
+ @Override
+ public IBuiltInRuleApp completeBuiltInRuleApp(IBuiltInRuleApp app, Goal goal, boolean forced) {
+ if (mainWindow.getMediator().isInAutoMode()) {
+ return AbstractProofControl.completeBuiltInRuleAppByDefault(app, goal, forced);
+ }
+ IBuiltInRuleApp result = app;
+ for (InteractiveRuleApplicationCompletionF compl : completions) {
+ if (compl.canComplete(app)) {
+ result = compl.complete(app, goal, forced);
+ break;
+ }
+ }
+ for (RuleCompletionHandler handler : handlers) {
+ IBuiltInRuleApp handled = handler.completeBuiltInRuleApp(app, goal, forced);
+ if (handled != null && handled != app && handled.complete()) {
+ result = handled;
+ break;
+ }
+ }
+ return (result != null && result.complete()) ? result : null;
+ }
+
+ /**
+ * Port of Swing WindowUserInterfaceControl.java:329-338
+ * ({@code completeAndApplyTacletMatch}): opens the redesigned {@link TacletMatchDialogF} —
+ * or the classic {@link TacletMatchCompletionDialogF} via
+ * {@code ViewSettings.isUseClassicTacletDialog} (a migration toggle) — with the main
+ * window's stage as owner and the mediator's services/notation info; the dialogs own the
+ * interactive apply flow ({@code ApplyTacletDialogF.handleApply}).
+ */
+ @Override
+ public void completeAndApplyTacletMatch(TacletInstantiationModel[] models, Goal goal) {
+ // termmenu: owner stage and the printing context come from the main window (Swing
+ // parity: WindowUserInterfaceControl.java:329-338 passes mainWindow / its mediator);
+ // both dialogs accept the main window's stage as owner (a Stage is a Window)
+ final Stage owner = mainWindow.getStage();
+ final Services services = mainWindow.getMediator().getServices();
+ final NotationInfo notationInfo = mainWindow.getMediator().getNotationInfo();
+ if (owner == null || services == null || notationInfo == null) {
+ // termmenu: without an owner stage or a loaded proof the dialog cannot be
+ // constructed — keep the app unapplied and report (the former stub behavior)
+ LOGGER.warn("The taclet match dialog cannot be opened (owner={}, services={}, "
+ + "notationInfo={}); the taclet application is skipped ({} model(s))", owner,
+ services, notationInfo, models.length);
+ NotificationManagerF.getInstance().notify(
+ "The taclet match dialog cannot be opened right now.",
+ NotificationManagerF.Kind.WARNING);
+ return;
+ }
+ // the redesigned dialog is the default; the classic one is offered as a migration
+ // fallback (Swing parity, WindowUserInterfaceControl.java:332-337). The constructors
+ // build a Stage and show() the dialog — both require the FX thread. Core callers may
+ // run on a worker (the verify harnesses drive selectedTaclet off the FX thread), so the
+ // construction hops over with runAndWait: on the FX thread this is the direct call, off
+ // it blocks until the dialog is up (the interactive apply flow continues inside the
+ // dialog, Swing's modal loop is its EDT pendant).
+ if (ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings()
+ .isUseClassicTacletDialog()) {
+ FxUtil.runAndWait(() -> new TacletMatchCompletionDialogF(owner, models, goal,
+ services, notationInfo, getProofControl()));
+ } else {
+ FxUtil.runAndWait(() -> new TacletMatchDialogF(owner, models, goal,
+ getProofControl(), services, notationInfo));
+ }
+ }
+
+ // ------------------------------------------------------------------
+ // status & progress (ProblemLoaderControl / ProverTaskListener callbacks)
+ // ------------------------------------------------------------------
+
+ /**
+ * Port of Swing WindowUserInterfaceControl.java:142-146 ({@code progressStarted} stops the
+ * interface): the FX UI has no global interface lock yet (parity gap: the input freeze during
+ * auto mode, see the main window audit).
+ */
+ @Override
+ public void progressStarted(Object sender) {
+ // no-op (Swing: mainWindow.getMediator().stopInterface(true))
+ }
+
+ /**
+ * Port of Swing WindowUserInterfaceControl.java:147-150: no startInterface is needed, the
+ * ProblemLoader re-enables the interface once loading is done.
+ */
+ @Override
+ public void progressStopped(Object sender) {
+ // no-op
+ }
+
+ /** Port of Swing WindowUserInterfaceControl.java:157-160. */
+ @Override
+ public void reportStatus(Object sender, String status, int progress) {
+ // D37 (P3c): Swing WindowUserInterfaceControl.java:158-160
+ // (mainWindow.setStatusLine(status, progress)) — the FX status bar hosts the progress
+ // bar now, so the workload is forwarded instead of dropped
+ FxUtil.runLater(() -> mainWindow.setStatusLine(status, progress));
+ }
+
+ /** Port of Swing WindowUserInterfaceControl.java:162-165 (mainWindow.setStatusLine). */
+ @Override
+ public void reportStatus(Object sender, String status) {
+ FxUtil.runLater(() -> mainWindow.setStatusLine(status));
+ }
+
+ /** Port of Swing WindowUserInterfaceControl.java:167-170 (setStandardStatusLine). */
+ @Override
+ public void resetStatus(Object sender) {
+ FxUtil.runLater(mainWindow::resetStatusLine);
+ }
+
+ /**
+ * Port of Swing WindowUserInterfaceControl.java:152-155
+ * ({@code IssueDialog.showExceptionDialog(mainWindow, e)}).
+ */
+ @Override
+ public void reportException(Object sender, @Nullable ProofOblInput input, Exception e) {
+ LOGGER.error("Exception reported by the proof machinery", e);
+ FxUtil.runLater(() -> IssueDialogF.showExceptionDialog(mainWindow.getStage(), e));
+ }
+
+ /**
+ * Port of Swing WindowUserInterfaceControl.java:294-297 (status line progress): the position
+ * is forwarded to the FX status progress bar.
+ */
+ @Override
+ public void taskProgress(int position) {
+ super.taskProgress(position);
+ // D37 (P3c): Swing gets the progress bar via getStatusLine().setProgress(position)
+ FxUtil.runLater(() -> mainWindow.setTaskProgressValue(position));
+ }
+
+ /**
+ * Port of Swing WindowUserInterfaceControl.java:301-305
+ * ({@code mainWindow.setStatusLine(info.message(), info.size())}).
+ */
+ @Override
+ public void taskStarted(TaskStartedInfo info) {
+ super.taskStarted(info);
+ // D37 (P3c): Swing passes info.size() straight into the status bar; a size of 0 (unknown
+ // workload) hides the progress bar there, negative sizes are not produced by the core
+ FxUtil.runLater(() -> mainWindow.setStatusLine(info.message(), info.size()));
+ }
+
+ /** Port of Swing WindowUserInterfaceControl.java:307-309 (progress bar maximum). */
+ @Override
+ public void setMaximum(int maximum) {
+ // D37 (P3c): Swing gets the maximum via getStatusLine().setProgressBarMaximum(maximum)
+ FxUtil.runLater(() -> mainWindow.setTaskProgressBarMaximum(maximum));
+ }
+
+ /** Port of Swing WindowUserInterfaceControl.java:312-315 (progress bar position). */
+ @Override
+ public void setProgress(int progress) {
+ // D37 (P3c): Swing gets the position via getStatusLine().setProgress(progress)
+ FxUtil.runLater(() -> mainWindow.setTaskProgressValue(progress));
+ }
+
+ // ------------------------------------------------------------------
+ // task finished (Swing taskFinishedInternal, WindowUserInterfaceControl.java:172-268)
+ // ------------------------------------------------------------------
+
+ /**
+ * Port of Swing WindowUserInterfaceControl.java:172-177: the core may fire {@code
+ * taskFinished} from a prover thread, so the handling is marshalled onto the FX thread (the
+ * Swing original uses {@code SwingUtilities.invokeLater}).
+ */
+ @Override
+ public void taskFinished(TaskFinishedInfo info) {
+ super.taskFinished(info);
+ FxUtil.runLater(() -> taskFinishedInternal(info));
+ }
+
+ private void taskFinishedInternal(TaskFinishedInfo info) {
+ if (info != null && info.getSource() instanceof ProverCore) {
+ if (!isAtLeastOneMacroRunning()) {
+ resetStatus(this);
+ }
+ ApplyStrategyInfo result = applyStrategyInfo(info);
+
+ final Proof proof = (Proof) info.getProof();
+ if (proof != null && !proof.isDisposed() && !proof.closed()
+ && mainWindow.getMediator().getSelectedProof() == proof) {
+ Goal g = result.nonCloseableGoal();
+ if (g == null) {
+ try {
+ g = proof.openGoals().head();
+ } catch (NoSuchElementException e) {
+ // all closed
+ }
+ }
+ if (g != null) {
+ // Swing: mainWindow.getMediator().goalChosen(g) (WindowUserInterfaceControl
+ // .java:198); the FX selection model is the goal re-choice
+ mainWindow.getSelectionModel().setSelectedGoal(g);
+ if (inStopAtFirstUncloseableGoalMode(proof)) {
+ // iff Stop on non-closeable Goal is selected a little popup is generated
+ // and the proof is stopped (WindowUserInterfaceControl.java:199-205)
+ new AutoDismissDialogF(mainWindow.getStage(),
+ "Couldn't close Goal Nr. " + g.node().serialNr()
+ + " automatically").show();
+ }
+ }
+ if (!isAtLeastOneMacroRunning()) {
+ showNotification("Automated proof search", info.toString());
+ }
+ }
+ // Swing: mainWindow.displayResults(info.toString()) (:210)
+ mainWindow.setStatusLine(info.toString());
+ } else if (info != null && info.getSource() instanceof ProofMacro macro) {
+ if (!isAtLeastOneMacroRunning()) {
+ // D37 (P3c): Swing hides the status progress here (WindowUserInterfaceControl
+ // .java:213)
+ mainWindow.hideStatusProgress();
+ mainWindow.setStatusLine(info.toString());
+ final Proof proof = (Proof) info.getProof();
+ if (proof != null && !proof.closed()
+ && mainWindow.getMediator().getSelectedProof() == proof) {
+ Goal g = proof.openGoals().head();
+ mainWindow.getSelectionModel().setSelectedGoal(g);
+ if (inStopAtFirstUncloseableGoalMode(proof)) {
+ new AutoDismissDialogF(mainWindow.getStage(),
+ "Couldn't close Goal Nr. " + g.node().serialNr()
+ + " automatically").show();
+ }
+ if (!isAtLeastOneMacroRunning()) {
+ showNotification(macro.getName(), info.toString());
+ }
+ }
+ }
+ } else if (info != null && info.getResult() instanceof Throwable result) {
+ // ProblemLoader branch (Swing WindowUserInterfaceControl.java:236-244): load errors
+ // surface in the IssueDialog
+ resetStatus(this);
+ LOGGER.error("", result);
+ if (result instanceof ParseCancellationException) {
+ result = result.getCause();
+ }
+ IssueDialogF.showExceptionDialog(mainWindow.getStage(), result);
+ } else if (info != null
+ && info.getSource() instanceof de.uka.ilkd.key.proof.io.AbstractProblemLoader) {
+ // successful load (Swing WindowUserInterfaceControl.java:245-261): the proof script
+ // and macro application of the Swing original are deferred in the FX load path
+ resetStatus(this);
+ } else {
+ resetStatus(this);
+ if (info != null && !info.toString().isEmpty()) {
+ mainWindow.setStatusLine(info.toString());
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static ApplyStrategyInfo applyStrategyInfo(TaskFinishedInfo info) {
+ return (ApplyStrategyInfo) info.getResult();
+ }
+
+ /**
+ * Port of Swing WindowUserInterfaceControl.java:276-286
+ * ({@code showNotification}): shows the notification as a toast when the
+ * {@code notificationAfterMacro} view setting allows it ({@code Always} always, {@code When
+ * not focused} only when the main window has no focus; the Swing original uses a system tray
+ * notification — a documented cosmetic difference of the FX UI).
+ */
+ private void showNotification(String title, String text) {
+ String mode =
+ ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings().notificationAfterMacro();
+ if (mode.equals(ViewSettings.NOTIFICATION_ALWAYS)) {
+ NotificationManagerF.getInstance().notify(title + ": " + text);
+ } else if (mode.equals(ViewSettings.NOTIFICATION_UNFOCUSED)) {
+ if (!mainWindow.getStage().isFocused()) {
+ NotificationManagerF.getInstance().notify(title + ": " + text);
+ }
+ }
+ }
+
+ /**
+ * Port of Swing WindowUserInterfaceControl.java:288-292: whether the strategy of the proof is
+ * in the "stop on non-closeable goal" mode.
+ */
+ protected boolean inStopAtFirstUncloseableGoalMode(Proof proof) {
+ return proof.getSettings().getStrategySettings().getActiveStrategyProperties()
+ .getProperty(StrategyProperties.STOPMODE_OPTIONS_KEY)
+ .equals(StrategyProperties.STOPMODE_NONCLOSE);
+ }
+
+ // ------------------------------------------------------------------
+ // proof lifecycle (register/dispose — bookkeeping for the later TaskTree dockable)
+ // ------------------------------------------------------------------
+
+ /**
+ * Port of Swing WindowUserInterfaceControl.java:519-525
+ * ({@code registerProofAggregate}): registers the proof-disposed listeners (the super
+ * implementation, AbstractUserInterfaceControl.java:300-304), records the aggregate for the
+ * later TaskTree/"Loaded Proofs" dockable (Swing {@code mainWindow.addProblem}) and resets
+ * the status line (Swing {@code setStandardStatusLine}).
+ *
+ * WP-1: the Swing {@code getMediator().fireProofLoaded(pa.getFirstProof())} (Swing
+ * WindowUserInterfaceControl.java:522) is now ported as well — fired synchronously here,
+ * like in Swing (the load runs before the replay), so the mediator's proof-load listeners
+ * (e.g. the slicing tracker, Swing SlicingExtension.java:120) observe every load.
+ */
+ @Override
+ public void registerProofAggregate(ProofAggregate pa) {
+ super.registerProofAggregate(pa);
+ mainWindow.getMediator().fireProofLoaded(pa.getFirstProof());
+ FxUtil.runLater(() -> {
+ proofAggregates.add(pa);
+ mainWindow.resetStatusLine();
+ });
+ }
+
+ /**
+ * Port of Swing WindowUserInterfaceControl.java:507-512 ({@code proofDisposing}): removes the
+ * proof from the UI bookkeeping. Note: the core {@code UserInterfaceControl} does not declare
+ * {@code proofDisposing} (the Swing method is on {@code AbstractMediatorUserInterfaceControl}
+ * in key.ui), so this is a plain hook for the later TaskTree/"Loaded Proofs" port.
+ */
+ public void proofDisposing(ProofDisposedEvent e) {
+ FxUtil.runLater(() -> proofAggregates
+ .removeIf(pa -> List.of(pa.getProofs()).contains(e.getSource())));
+ }
+
+ /**
+ * Port of Swing WindowUserInterfaceControl.java:620-623
+ * ({@code IssueDialog.showWarningsIfNecessary}).
+ */
+ @Override
+ public void reportWarnings(ImmutableSet warnings) {
+ FxUtil.runLater(
+ () -> IssueDialogF.showWarningsIfNecessary(mainWindow.getStage(), warnings));
+ }
+
+ /**
+ * Port of Swing WindowUserInterfaceControl.java:633-641 ({@code showIssueDialog}): shows the
+ * given issues without the "ignore warnings" affordance (title "Issues", critical).
+ */
+ @Override
+ public void showIssueDialog(java.util.Collection issues) {
+ IssueDialogF.showIssues(mainWindow.getStage(), "Issues", issues);
+ }
+
+ /**
+ * Port of Swing WindowUserInterfaceControl.java:514-517 ({@code selectProofObligation}
+ * opens the {@code ProofManagementDialog}): the proof management dialog is a deferred port
+ * item, so no proof obligation can be selected (the default headless behavior).
+ */
+ @Override
+ public boolean selectProofObligation(
+ de.uka.ilkd.key.proof.init.InitConfig initConfig) {
+ return false;
+ }
+
+ // ------------------------------------------------------------------
+ // bookkeeping accessors (for the later TaskTree/"Loaded Proofs" dockable)
+ // ------------------------------------------------------------------
+
+ /**
+ * @return the observable list of loaded proof aggregates (Swing
+ * {@code MainWindow.getProofList()}); proofs are removed when they are disposed
+ */
+ public ObservableList getProofAggregates() {
+ return proofAggregates;
+ }
+
+ /**
+ * @return {@code true} if the given proof is registered (Swing
+ * {@code mainWindow.getProofList().containsProof(proof)}, used by the Swing
+ * {@code isAutoModeSupported} override, WindowUserInterfaceControl.java:91-94)
+ */
+ public boolean containsProof(Proof proof) {
+ return proofAggregates.stream().anyMatch(pa -> List.of(pa.getProofs()).contains(proof));
+ }
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/actions/QuickSaveF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/actions/QuickSaveF.java
new file mode 100644
index 00000000000..f623e4c06e8
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/actions/QuickSaveF.java
@@ -0,0 +1,82 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.gui.fx.actions;
+
+import java.io.File;
+import java.nio.file.Path;
+
+import de.uka.ilkd.key.gui.fx.MainWindowF;
+import de.uka.ilkd.key.proof.Proof;
+import de.uka.ilkd.key.proof.io.ProofSaver;
+import de.uka.ilkd.key.util.KeYConstants;
+
+import org.key_project.util.java.IOUtil;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Quick save/quick load of the currently selected proof. Counter-part of the Swing actions
+ * {@code de.uka.ilkd.key.gui.actions.QuickSaveAction} and
+ * {@code de.uka.ilkd.key.gui.actions.QuickLoadAction} of the module {@code key.ui}: the proof is
+ * immediately saved to a temporary location ({@value #QUICK_SAVE_PATH}, the OS's temp directory —
+ * not the KeY config directory) and restored from there with the F5/F6 keys.
+ *
+ * Swing semantics kept: a quick save without a selected proof warns ("No proof."); a failed save
+ * warns with the error string; the quick load unconditionally loads the quick save location (a
+ * missing file fails the load asynchronously). The quick save location never enters the recent
+ * files list (Swing {@code RecentFileMenu.addNewToModelAndView}).
+ */
+public final class QuickSaveF {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(QuickSaveF.class);
+
+ /** The path to the quick save file (Swing {@code QuickSaveAction.QUICK_SAVE_PATH}). */
+ public static final String QUICK_SAVE_PATH =
+ IOUtil.getTempDirectory() + File.separator + ".quicksave.key";
+
+ private QuickSaveF() {
+ }
+
+ /**
+ * Immediately saves the currently selected proof to the temporary location
+ * {@link #QUICK_SAVE_PATH} (Swing {@code QuickSaveAction.quickSave}).
+ *
+ * @param mainWindow the main window
+ */
+ public static void quickSave(MainWindowF mainWindow) {
+ final Proof proof = mainWindow.getMediator().getSelectedProof();
+ if (proof == null) {
+ // unreachable: the action is disabled without a proof (Swing enableWhenProofLoaded)
+ mainWindow.popupWarning("No proof.");
+ return;
+ }
+ final String filename = QUICK_SAVE_PATH;
+
+ String status = new ProofSaver(proof, Path.of(filename), KeYConstants.INTERNAL_VERSION)
+ .save();
+
+ if (status == null) {
+ // success case
+ status = "File quicksaved: " + filename;
+ } else {
+ mainWindow.popupWarning("Quicksaving file " + filename + " failed:\n" + status);
+ LOGGER.info("Quicksaving file {} failed: {}", filename, status);
+ }
+ mainWindow.setStatusLine(status);
+ LOGGER.info("Quicksave: {}", status);
+ }
+
+ /**
+ * Load the file saved at the location described by {@link #quickSave} (Swing
+ * {@code QuickLoadAction.quickLoad}: the quick save location is loaded like any other file,
+ * unconditionally — a missing file fails the load).
+ *
+ * @param mainWindow the main window
+ */
+ public static void quickLoad(MainWindowF mainWindow) {
+ mainWindow.openProofFile(Path.of(QUICK_SAVE_PATH));
+ }
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/actions/useractions/SMTProofApplyUserActionF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/actions/useractions/SMTProofApplyUserActionF.java
new file mode 100644
index 00000000000..a510b223670
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/actions/useractions/SMTProofApplyUserActionF.java
@@ -0,0 +1,125 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.gui.fx.actions.useractions;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+
+import de.uka.ilkd.key.control.AbstractProofControl;
+import de.uka.ilkd.key.core.fx.KeYMediatorF;
+import de.uka.ilkd.key.gui.fx.smt.SolverListenerF;
+import de.uka.ilkd.key.proof.Goal;
+import de.uka.ilkd.key.proof.Node;
+import de.uka.ilkd.key.proof.Proof;
+import de.uka.ilkd.key.rule.IBuiltInRuleApp;
+import de.uka.ilkd.key.smt.SMTFocusResults;
+import de.uka.ilkd.key.smt.SMTRule;
+import de.uka.ilkd.key.smt.SMTSolverResult.ThreeValuedTruth;
+
+import org.key_project.prover.sequent.PosInOccurrence;
+import org.key_project.util.collection.ImmutableList;
+
+import org.jspecify.annotations.NullMarked;
+
+/**
+ * User action to apply the results of running SMT solvers, FX port of
+ * {@code de.uka.ilkd.key.gui.actions.useractions.SMTProofApplyUserAction} (Swing
+ * SMTProofApplyUserAction.java:32-117): closes zero or more goals with the {@code SMTRule}
+ * built-in rule (with the unsat core if available). The action is undoable: {@link #undo()}
+ * prunes the proof back to the open goals recorded before the application (Swing
+ * {@code SMTProofApplyUserAction.undo}) — the history mechanism of the Swing
+ * {@code ActionHistoryExtension} records it through the mediator's user-action listeners
+ * (WP-7).
+ */
+@NullMarked
+public class SMTProofApplyUserActionF extends UserActionF {
+
+ /**
+ * Results of running the SMT solvers (one entry for each open goal).
+ */
+ private final Collection smtProblems;
+
+ /**
+ * The number of goals that will be closed by this action.
+ */
+ private final int numberOfGoalsClosed;
+
+ /**
+ * The original nodes of the proof. Used to undo this action.
+ */
+ private final List originalProofNodes;
+
+ private final Node originalSelectedNode;
+
+ public SMTProofApplyUserActionF(KeYMediatorF mediator, Proof proof,
+ Collection smtProblems) {
+ super(mediator, proof);
+ this.smtProblems = smtProblems;
+ this.numberOfGoalsClosed = (int) smtProblems.stream()
+ .filter(p -> p.getSolver().getFinalResult().isValid() == ThreeValuedTruth.VALID)
+ .count();
+ this.originalProofNodes = proof.openGoals().stream().map(Goal::node).toList();
+ this.originalSelectedNode = mediator.getSelectedNode();
+ }
+
+ @Override
+ public String name() {
+ return String.format("Close: %d goals by SMT", numberOfGoalsClosed);
+ }
+
+ @Override
+ protected void apply() {
+ // Swing SolverListener.applyResults (SolverListener.java:209-221): the goal closing runs
+ // with the interface frozen (stopInterface/startInterface); the FX counterpart is the
+ // WP-1 auto-mode seam, and the default selection happens on unfreezing
+ mediator.initiateAutoMode(proof, true, false);
+ try {
+ applyResults();
+ } finally {
+ mediator.finishAutoMode(proof, true, true, null);
+ }
+ }
+
+ private void applyResults() {
+ Collection goalsClosed = new HashSet<>();
+ // only close each solved goal once
+ for (SolverListenerF.InternSMTProblemF problem : smtProblems) {
+ Goal goal = problem.getProblem().getGoal();
+ if (goalsClosed.contains(goal)
+ || problem.getSolver().getFinalResult().isValid() != ThreeValuedTruth.VALID) {
+ continue;
+ }
+ goalsClosed.add(goal);
+ ImmutableList unsatCore =
+ SMTFocusResults.getUnsatCore(problem.getProblem());
+ IBuiltInRuleApp app;
+ if (unsatCore != null) {
+ app = SMTRule.INSTANCE.createApp(problem.getSolver().name(), unsatCore);
+ } else {
+ app = SMTRule.INSTANCE.createApp(problem.getSolver().name());
+ }
+ app = AbstractProofControl.completeBuiltInRuleAppByDefault(app, goal, false);
+ if (app == null) {
+ // should be unreachable under normal circumstances
+ throw new RuntimeException("Could not instantiate SMT Rule Application");
+ }
+ goal.apply(app);
+ }
+ }
+
+ @Override
+ public void undo() {
+ for (Node n : originalProofNodes) {
+ proof.pruneProof(n);
+ }
+ mediator.getSelectionModel().setSelectedNode(originalSelectedNode);
+ }
+
+ @Override
+ public boolean canUndo() {
+ return originalProofNodes.stream().allMatch(proof::find);
+ }
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/actions/useractions/UserActionF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/actions/useractions/UserActionF.java
new file mode 100644
index 00000000000..dbb5365eac0
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/actions/useractions/UserActionF.java
@@ -0,0 +1,71 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.gui.fx.actions.useractions;
+
+import de.uka.ilkd.key.core.fx.KeYMediatorF;
+import de.uka.ilkd.key.proof.Proof;
+
+import org.jspecify.annotations.NullMarked;
+
+/**
+ * Abstract concept of an action performed by the user, FX port of
+ * {@code de.uka.ilkd.key.gui.actions.useractions.UserAction} (Swing UserAction.java:16-72):
+ * has a name and may be applied or undone; always applied by {@link #actionPerformed()}, which
+ * notifies the mediator's user-action listeners first ({@code fireActionPerformed}) and then
+ * applies ({@link #apply()}).
+ */
+@NullMarked
+public abstract class UserActionF {
+
+ /** the FX mediator; used to register the execution of this user action. */
+ protected final KeYMediatorF mediator;
+
+ /** the proof this action was applied on. */
+ protected final Proof proof;
+
+ /**
+ * Set up this user action.
+ *
+ * @param mediator the FX mediator
+ * @param proof the proof this action is to be applied on
+ */
+ protected UserActionF(KeYMediatorF mediator, Proof proof) {
+ this.mediator = mediator;
+ this.proof = proof;
+ }
+
+ /** @return the name of this action */
+ public abstract String name();
+
+ /**
+ * Apply this user action. After a successive call to {@link #undo()}, it may not be possible
+ * to re-apply the action!
+ */
+ protected abstract void apply();
+
+ /** Undo this user action. May only be done once. */
+ public abstract void undo();
+
+ /**
+ * Whether this action can be undone given the current state of the proof.
+ *
+ * @return whether {@link #undo()} will work
+ */
+ public abstract boolean canUndo();
+
+ /**
+ * Notifies the user-action listeners and applies the action (Swing
+ * {@code UserAction.actionPerformed}).
+ */
+ public void actionPerformed() {
+ mediator.fireActionPerformed(this);
+ apply();
+ }
+
+ /** @return the proof this action was applied on */
+ public Proof getProof() {
+ return proof;
+ }
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/colors/ColorPaletteF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/colors/ColorPaletteF.java
new file mode 100644
index 00000000000..055e741e776
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/colors/ColorPaletteF.java
@@ -0,0 +1,399 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.gui.fx.colors;
+
+import javafx.scene.paint.Color;
+
+/**
+ * The full Swing-parity color palette of KeY, mirroring every
+ * {@code ColorSettings.define(...)} call site of the Swing module {@code key.ui}.
+ *
+ * The Swing UI registers {@code de.uka.ilkd.key.gui.colors.ColorSettings.ColorProperty}
+ * instances at class-load time of the classes that use them (the lexers, the source view, the
+ * proof tree, {@code SolverListener}, ...). The FX counterpart {@link ColorSettingsF} is a
+ * plain registry without such consumers, so this class holds one static
+ * {@link ColorSettingsF.ColorPropertyF}
+ * per Swing call site with the identical key and defaults. The properties are registered in
+ * {@link ColorSettingsF} as soon as this class is loaded; {@link #ensureRegistered()} is a
+ * class-load bootstrap used by the {@code getInstance()} wiring of {@link ColorSettingsF} and
+ * by the {@code key.fx.verify.colors} self test.
+ *
+ * The keys and the per-theme defaults are the ones of the Swing {@code define} calls, so
+ * {@code colors.json} files written by the Swing UI round-trip unchanged (unmapped keys are
+ * preserved by {@link ColorSettingsF#save()}). Keys whose visuals have no FX counterpart yet
+ * (e.g. {@code [currentGoal]*}, {@code [innerNodeView]*}, the heatmap base color) are
+ * configurable in the colors panel but have no rendering effect here — see the
+ * {@code CSS_VARIABLES} documentation.
+ */
+public final class ColorPaletteF {
+
+ private ColorPaletteF() {
+ }
+
+ /**
+ * Forces the static registration of the whole palette. Empty on purpose: loading this class
+ * runs the static initializers.
+ */
+ public static void ensureRegistered() {
+ }
+
+ // ------------------------------------------------------------------
+ // infotree (Swing: utilities/LexerHighlighter)
+ // ------------------------------------------------------------------
+
+ /** Syntax color of keywords in the info tree (Swing {@code infotree.syntax.keyword}). */
+ public static final ColorSettingsF.ColorPropertyF INFOTREE_KEYWORD =
+ define("infotree.syntax.keyword", "Keywords in the rule/symbol browser",
+ Color.rgb(0, 0, 255), Color.rgb(255, 165, 0));
+
+ /** Syntax color of identifiers in the info tree (Swing {@code infotree.syntax.identifier}). */
+ public static final ColorSettingsF.ColorPropertyF INFOTREE_IDENTIFIER =
+ define("infotree.syntax.identifier", "Identifiers in the rule/symbol browser",
+ Color.rgb(0, 0, 0), Color.rgb(255, 255, 255));
+
+ /** Syntax color of comments in the info tree (Swing {@code infotree.syntax.comment}). */
+ public static final ColorSettingsF.ColorPropertyF INFOTREE_COMMENT =
+ define("infotree.syntax.comment", "Comments in the rule/symbol browser",
+ Color.rgb(0, 128, 0));
+
+ /** Syntax color of operators in the info tree (Swing {@code infotree.syntax.operators}). */
+ public static final ColorSettingsF.ColorPropertyF INFOTREE_OPERATORS =
+ define("infotree.syntax.operators", "Operators in the rule/symbol browser",
+ Color.rgb(0, 0, 0), Color.rgb(255, 165, 0));
+
+ /** Syntax color of errors in the info tree (Swing {@code infotree.syntax.error}). */
+ public static final ColorSettingsF.ColorPropertyF INFOTREE_ERROR =
+ define("infotree.syntax.error", "Syntax errors in the rule/symbol browser",
+ Color.rgb(255, 0, 0), Color.rgb(255, 255, 255));
+
+ /** Syntax color of literals in the info tree (Swing {@code infotree.syntax.literals}). */
+ public static final ColorSettingsF.ColorPropertyF INFOTREE_LITERALS =
+ define("infotree.syntax.literals", "Literals in the rule/symbol browser",
+ Color.rgb(0, 128, 0));
+
+ /** Syntax color of modalities in the info tree (Swing {@code infotree.syntax.modality}). */
+ public static final ColorSettingsF.ColorPropertyF INFOTREE_MODALITY =
+ define("infotree.syntax.modality", "Modalities in the rule/symbol browser",
+ Color.rgb(255, 175, 175));
+
+ // ------------------------------------------------------------------
+ // source view (Swing: sourceview/SourceView)
+ // ------------------------------------------------------------------
+
+ /** Background of symbolically executed lines (Swing {@code [SourceView]normalHighlight}). */
+ public static final ColorSettingsF.ColorPropertyF SOURCE_NORMAL_HIGHLIGHT =
+ define("[SourceView]normalHighlight",
+ "Color for highlighting symbolically executed lines in source view",
+ Color.rgb(194, 245, 194));
+
+ /**
+ * Background of the most recently executed line (Swing
+ * {@code [SourceView]mostRecentHighlight}).
+ */
+ public static final ColorSettingsF.ColorPropertyF SOURCE_MOST_RECENT_HIGHLIGHT =
+ define("[SourceView]mostRecentHighlight",
+ "Color for highlighting most recently symbolically executed line in source view",
+ Color.rgb(57, 210, 81));
+
+ /** Background of source view tabs with highlights (Swing {@code [SourceView]tabHighlight}). */
+ public static final ColorSettingsF.ColorPropertyF SOURCE_TAB_HIGHLIGHT =
+ define("[SourceView]tabHighlight",
+ "Color for highlighting source view tabs whose files contain highlighted lines",
+ Color.rgb(57, 210, 81));
+
+ /**
+ * Background of the source of the selected term (Swing {@code [SourceView]originHighlight}).
+ */
+ public static final ColorSettingsF.ColorPropertyF SOURCE_ORIGIN_HIGHLIGHT =
+ define("[SourceView]originHighlight",
+ "Color for highlighting the origin of a selected term in source view",
+ Color.rgb(252, 202, 80));
+
+ // ------------------------------------------------------------------
+ // .key problem files (Swing: sourceview/KeYEditorLexer)
+ // ------------------------------------------------------------------
+
+ /** Syntax color of keywords in .key files (Swing {@code [key]keyword}). */
+ public static final ColorSettingsF.ColorPropertyF KEY_KEYWORD =
+ define("[key]keyword", "Keyword in .key problem files", Color.rgb(0x7f, 0x00, 0x55));
+
+ /** Syntax color of secondary keywords in .key files (Swing {@code [key]keyword2}). */
+ public static final ColorSettingsF.ColorPropertyF KEY_KEYWORD2 =
+ define("[key]keyword2", "Secondary keyword in .key problem files",
+ Color.rgb(0x78, 0x52, 0x6C));
+
+ /** Syntax color of comments in .key files (Swing {@code [key]comment}). */
+ public static final ColorSettingsF.ColorPropertyF KEY_COMMENT =
+ define("[key]comment", "Comment in .key problem files", Color.rgb(0x3f, 0x7f, 0x5f));
+
+ /** Syntax color of literals in .key files (Swing {@code [key]literal}). */
+ public static final ColorSettingsF.ColorPropertyF KEY_LITERAL =
+ define("[key]literal", "Literal in .key problem files", Color.rgb(0x2A, 0x75, 0xB1));
+
+ /** Syntax color of modalities in .key files (Swing {@code [key]modality}). */
+ public static final ColorSettingsF.ColorPropertyF KEY_MODALITY =
+ define("[key]modality", "Modality in .key problem files", Color.rgb(0xC6, 0x7C, 0x13));
+
+ // ------------------------------------------------------------------
+ // Java sources (Swing: sourceview/JavaDocument)
+ // ------------------------------------------------------------------
+
+ /** Java keyword color (Swing {@code [java]keyword}). */
+ public static final ColorSettingsF.ColorPropertyF JAVA_KEYWORD =
+ define("[java]keyword", "Keyword in Java source files", Color.rgb(0x7f, 0x00, 0x55),
+ Color.rgb(0xCf, 0x50, 0xA5));
+
+ /** Java comment color (Swing {@code [java]comment}). */
+ public static final ColorSettingsF.ColorPropertyF JAVA_COMMENT =
+ define("[java]comment", "Comment in Java source files", Color.rgb(0x3f, 0x7f, 0x5f),
+ Color.rgb(0x9f, 0xBf, 0x9f));
+
+ /** JavaDoc color (Swing {@code [java]javadoc}). */
+ public static final ColorSettingsF.ColorPropertyF JAVA_JAVADOC =
+ define("[java]javadoc", "Javadoc in Java source files", Color.rgb(0x3f, 0x7f, 0x5f),
+ Color.rgb(0x9f, 0xBf, 0x9f));
+
+ /** JML color (Swing {@code [java]jml}). */
+ public static final ColorSettingsF.ColorPropertyF JAVA_JML =
+ define("[java]jml", "JML annotation in Java source files", Color.rgb(0x00, 0x00, 0xc0),
+ Color.rgb(0x88, 0x88, 0xcf));
+
+ /** JML keyword color (Swing {@code [java]jmlKeyword}). */
+ public static final ColorSettingsF.ColorPropertyF JAVA_JML_KEYWORD =
+ define("[java]jmlKeyword", "JML keyword in Java source files", Color.rgb(0x00, 0x00, 0xf0),
+ Color.rgb(0x88, 0x88, 0xcf));
+
+ // ------------------------------------------------------------------
+ // SMT solver listener (Swing: smt/SolverListener)
+ // ------------------------------------------------------------------
+
+ /** Color of failing SMT solver tasks (Swing {@code [solverListener]red}). */
+ public static final ColorSettingsF.ColorPropertyF SMT_RED =
+ define("[solverListener]red", "Failing SMT solver task", Color.rgb(180, 43, 43));
+
+ /** Color of succeeding SMT solver tasks (Swing {@code [solverListener]green}). */
+ public static final ColorSettingsF.ColorPropertyF SMT_GREEN =
+ define("[solverListener]green", "Succeeding SMT solver task", Color.rgb(43, 180, 43));
+
+ // ------------------------------------------------------------------
+ // settings dialog (Swing: settings/SimpleSettingsPanel)
+ // ------------------------------------------------------------------
+
+ /**
+ * Error color of erroneous settings text fields (Swing
+ * {@code SETTINGS_TEXTFIELD_ERROR}). Also referenced by
+ * {@code SettingsPanelF.COLOR_ERROR}; {@code createColorProperty} is idempotent, so both
+ * names denote the same property.
+ */
+ public static final ColorSettingsF.ColorPropertyF SETTINGS_ERROR =
+ define("SETTINGS_TEXTFIELD_ERROR",
+ "Color for marking erroneous text fields in the settings dialog",
+ Color.rgb(200, 100, 100));
+
+ // ------------------------------------------------------------------
+ // search bar (Swing: SearchBar)
+ // ------------------------------------------------------------------
+
+ /** Alert color of the proof tree search bar (Swing {@code [searchBar]alert}). */
+ public static final ColorSettingsF.ColorPropertyF SEARCH_ALERT =
+ define("[searchBar]alert", "Alert color of the search bar", Color.rgb(255, 178, 178),
+ Color.rgb(85, 40, 40));
+
+ // ------------------------------------------------------------------
+ // proof tree (Swing: prooftree/ProofTreeView)
+ // ------------------------------------------------------------------
+
+ /** Color of merged goals in the proof tree (Swing {@code [proofTree]gray}). */
+ public static final ColorSettingsF.ColorPropertyF PROOF_TREE_GRAY =
+ define("[proofTree]gray", "Color of merged goals in the proof tree",
+ Color.rgb(0x40, 0x40, 0x40), Color.rgb(0xc0, 0xc0, 0xc0));
+
+ /** Color of linked goals in the proof tree (Swing {@code [proofTree]lightBlue}). */
+ public static final ColorSettingsF.ColorPropertyF PROOF_TREE_LIGHT_BLUE =
+ define("[proofTree]lightBlue", "Color of linked goals in the proof tree",
+ Color.rgb(230, 254, 255));
+
+ /** Color of closed goals in the proof tree (Swing {@code [proofTree]darkGreen}). */
+ public static final ColorSettingsF.ColorPropertyF PROOF_TREE_DARK_GREEN =
+ define("[proofTree]darkGreen", "Color of closed goals in the proof tree",
+ Color.rgb(0, 128, 51), Color.rgb(100, 255, 102));
+
+ /** Color of open goals in the proof tree (Swing {@code [proofTree]darkRed}). */
+ public static final ColorSettingsF.ColorPropertyF PROOF_TREE_DARK_RED =
+ define("[proofTree]darkRed", "Color of open goals in the proof tree", Color.rgb(191, 0, 0),
+ Color.rgb(191, 120, 120));
+
+ /** Color of interactive goals in the proof tree (Swing {@code [proofTree]pink}). */
+ public static final ColorSettingsF.ColorPropertyF PROOF_TREE_PINK =
+ define("[proofTree]pink", "Color of interactive goals in the proof tree",
+ Color.rgb(255, 0, 240));
+
+ /** Color of automated goals in the proof tree (Swing {@code [proofTree]orange}). */
+ public static final ColorSettingsF.ColorPropertyF PROOF_TREE_ORANGE =
+ define("[proofTree]orange", "Color of automated goals in the proof tree",
+ Color.rgb(255, 140, 0), Color.rgb(255, 180, 40));
+
+ // ------------------------------------------------------------------
+ // javac extension (Swing: plugins/javac/JavacExtension)
+ // ------------------------------------------------------------------
+
+ /** Color of javac fine messages (Swing {@code javac.fine}). */
+ public static final ColorSettingsF.ColorPropertyF JAVAC_FINE =
+ define("javac.fine", "Javac fine message", Color.rgb(80, 120, 200));
+
+ /** Color of javac error messages (Swing {@code javac.error}). */
+ public static final ColorSettingsF.ColorPropertyF JAVAC_ERROR =
+ define("javac.error", "Javac error message", Color.rgb(200, 20, 80));
+
+ /** Color of javac warning messages (Swing {@code javac.warn}). */
+ public static final ColorSettingsF.ColorPropertyF JAVAC_WARN =
+ define("javac.warn", "Javac warning message", Color.rgb(200, 120, 80));
+
+ // ------------------------------------------------------------------
+ // sequent search bar (Swing: nodeviews/SequentViewSearchBar)
+ // ------------------------------------------------------------------
+
+ /**
+ * Highlight color 1 of the sequent search bar (Swing {@code [sequentSearchBar]highlight_1}).
+ */
+ public static final ColorSettingsF.ColorPropertyF SEQUENT_SEARCH_HIGHLIGHT_1 =
+ define("[sequentSearchBar]highlight_1", "Sequent search highlight (first match)",
+ Color.rgb(0, 140, 255, 178 / 255.0));
+
+ /**
+ * Highlight color 2 of the sequent search bar (Swing {@code [sequentSearchBar]highlight_2}).
+ */
+ public static final ColorSettingsF.ColorPropertyF SEQUENT_SEARCH_HIGHLIGHT_2 =
+ define("[sequentSearchBar]highlight_2", "Sequent search highlight (other matches)",
+ Color.rgb(0, 140, 255, 100 / 255.0));
+
+ // ------------------------------------------------------------------
+ // sequent view (Swing: nodeviews/SequentView, SequentViewInputListener)
+ // ------------------------------------------------------------------
+
+ /**
+ * Color of the mouse selection in the sequent (Swing {@code [currentGoal]mouseSelectionColor}).
+ */
+ public static final ColorSettingsF.ColorPropertyF CURRENT_GOAL_MOUSE_SELECTION =
+ define("[currentGoal]mouseSelectionColor",
+ "Color of the mouse selection in the sequent view", Color.rgb(230, 230, 230, 1.0));
+
+ /** Color of permanent highlights in the sequent (Swing {@code [currentGoal]permaHighlight}). */
+ public static final ColorSettingsF.ColorPropertyF CURRENT_GOAL_PERMA_HIGHLIGHT =
+ define("[currentGoal]permaHighlight", "Permanent highlight in the sequent view",
+ Color.rgb(110, 85, 181, 76 / 255.0), Color.rgb(210, 185, 201, 200 / 255.0));
+
+ /**
+ * Color of the default highlight in the sequent (Swing {@code [currentGoal]defaultHighlight}).
+ */
+ public static final ColorSettingsF.ColorPropertyF CURRENT_GOAL_DEFAULT_HIGHLIGHT =
+ define("[currentGoal]defaultHighlight", "Default highlight in the sequent view",
+ Color.rgb(70, 100, 170, 76 / 255.0), Color.rgb(140, 200, 255, 180 / 255.0));
+
+ /**
+ * Color of additional highlights in the sequent (Swing
+ * {@code [currentGoal]addtionalHighlight}).
+ */
+ public static final ColorSettingsF.ColorPropertyF CURRENT_GOAL_ADDTIONAL_HIGHLIGHT =
+ define("[currentGoal]addtionalHighlight", "Additional highlight in the sequent view",
+ Color.rgb(0, 0, 0, 38 / 255.0), Color.rgb(240, 220, 255, 180 / 255.0));
+
+ /** Color of update highlights in the sequent (Swing {@code [currentGoal]updateHighlight}). */
+ public static final ColorSettingsF.ColorPropertyF CURRENT_GOAL_UPDATE_HIGHLIGHT =
+ define("[currentGoal]updateHighlight", "Update highlight in the sequent view",
+ Color.rgb(0, 150, 130, 38 / 255.0), Color.rgb(0, 150, 130, 1.0));
+
+ /**
+ * Color of drag and drop highlights in the sequent (Swing {@code [currentGoal]dndHighlight}).
+ */
+ public static final ColorSettingsF.ColorPropertyF CURRENT_GOAL_DND_HIGHLIGHT =
+ define("[currentGoal]dndHighlight", "Drag and drop highlight in the sequent view",
+ Color.rgb(0, 150, 130, 1.0));
+
+ /** Base color of the heatmap overlay (Swing {@code [Heatmap]basecolor}). */
+ public static final ColorSettingsF.ColorPropertyF HEATMAP_BASE_COLOR =
+ define("[Heatmap]basecolor", "Base color of the heatmap overlay", Color.rgb(252, 202, 80));
+
+ /**
+ * Alert color of the sequent hide-warning border (Swing
+ * {@code [sequentHideWarningBorder]alert}).
+ */
+ public static final ColorSettingsF.ColorPropertyF SEQUENT_HIDE_WARNING_ALERT =
+ define("[sequentHideWarningBorder]alert", "Alert color of the hide-warning border",
+ Color.rgb(255, 178, 178));
+
+ // ------------------------------------------------------------------
+ // inner node view (Swing: nodeviews/InnerNodeView)
+ // ------------------------------------------------------------------
+
+ /** Highlight color of rule applications (Swing {@code [innerNodeView]ruleAppHighlight}). */
+ public static final ColorSettingsF.ColorPropertyF INNER_NODE_RULE_APP_HIGHLIGHT =
+ define("[innerNodeView]ruleAppHighlight",
+ "Rule application highlight in the inner node view",
+ new Color(0.5, 1.0, 0.5, 0.4));
+
+ /** Highlight color of if-formulas (Swing {@code [innerNodeView]ifFormulaHighlight}). */
+ public static final ColorSettingsF.ColorPropertyF INNER_NODE_IF_FORMULA_HIGHLIGHT =
+ define("[innerNodeView]ifFormulaHighlight",
+ "If-formula highlight in the inner node view", new Color(0.8, 1.0, 0.8, 0.5));
+
+ /** Selection color of the inner node view (Swing {@code [innerNodeView]selection}). */
+ public static final ColorSettingsF.ColorPropertyF INNER_NODE_SELECTION =
+ define("[innerNodeView]selection", "Selection in the inner node view",
+ Color.rgb(10, 180, 50));
+
+ // ------------------------------------------------------------------
+ // sequent syntax highlighting (Swing: nodeviews/HTMLSyntaxHighlighter)
+ // ------------------------------------------------------------------
+
+ /**
+ * Color of propositional logic in the sequent (Swing {@code [sequentView]prop_logic_color}).
+ */
+ public static final ColorSettingsF.ColorPropertyF SEQUENT_PROP_LOGIC =
+ define("[sequentView]prop_logic_color", "Propositional logic in the sequent view",
+ Color.rgb(0, 0, 0), Color.rgb(255, 255, 255));
+
+ /** Color of dynamic logic in the sequent (Swing {@code [sequentView]dyn_logic_color}). */
+ public static final ColorSettingsF.ColorPropertyF SEQUENT_DYN_LOGIC =
+ define("[sequentView]dyn_logic_color", "Dynamic logic in the sequent view",
+ Color.rgb(0, 0, 16 * 13), Color.rgb(150, 150, 250));
+
+ /** Color of program variables in the sequent (Swing {@code [sequentView]prog_var_color}). */
+ public static final ColorSettingsF.ColorPropertyF SEQUENT_PROG_VAR =
+ define("[sequentView]prog_var_color", "Program variables in the sequent view",
+ Color.rgb(0x6A, 0x3E, 0x3E), Color.rgb(100, 100, 250));
+
+ /** Color of sequent arrows in the sequent (Swing {@code [sequentView]sequent_arrow_color}). */
+ public static final ColorSettingsF.ColorPropertyF SEQUENT_ARROW =
+ define("[sequentView]sequent_arrow_color", "Sequent arrow in the sequent view",
+ Color.rgb(0x6A, 0x3E, 0x3E), Color.rgb(100, 100, 250));
+
+ /**
+ * Registers the property in {@link ColorSettingsF}; re-uses an existing definition of the
+ * same key (the settings file may already contain an entry of a foreign or older UI).
+ *
+ * @param key the key in {@code colors.json}
+ * @param desc a human readable description
+ * @param light the light theme default
+ * @param dark the dark theme default
+ * @return the registered property
+ */
+ private static ColorSettingsF.ColorPropertyF define(String key, String desc, Color light,
+ Color dark) {
+ return ColorSettingsF.define(key, desc, light, dark);
+ }
+
+ /**
+ * Registers the property with the same default for both themes.
+ *
+ * @param key the key in {@code colors.json}
+ * @param desc a human readable description
+ * @param color the default
+ * @return the registered property
+ */
+ private static ColorSettingsF.ColorPropertyF define(String key, String desc, Color color) {
+ return ColorSettingsF.define(key, desc, color);
+ }
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/colors/ColorSettingsF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/colors/ColorSettingsF.java
new file mode 100644
index 00000000000..f5d42e025a8
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/colors/ColorSettingsF.java
@@ -0,0 +1,588 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.gui.fx.colors;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.paint.Color;
+
+import de.uka.ilkd.key.gui.fx.theme.Theme;
+import de.uka.ilkd.key.gui.fx.theme.ThemeManager;
+import de.uka.ilkd.key.settings.Configuration;
+import de.uka.ilkd.key.settings.PathConfig;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Configurable colors for KeY, counter-part of {@code de.uka.ilkd.key.gui.colors.ColorSettings}
+ * of the Swing module {@code key.ui}.
+ *
+ * If you need a new color use: {@link #define(String, String, Color, Color)}.
+ *
+ * The settings are shared with the Swing UI via the same file ({@code colors.json} in the KeY
+ * configuration directory) with the same keys and the same value format: a color is the string
+ * {@code #AARRGGBB} (used for both themes) or a two-element list {@code [light, dark]} with
+ * per-theme colors. Entries of keys unknown to this UI are preserved on save.
+ *
+ * In addition to the Swing original, the user values are applied as CSS property
+ * overrides : the keys of the mapping table {@link #CSS_VARIABLES} are written into the
+ * inline style of the root nodes of all managed scenes (e.g. {@code -key-hl-java}), so they win
+ * over the theme stylesheet defaults. Keys without an FX counterpart are kept in the file but
+ * have no visual effect here (see {@link #CSS_VARIABLES} for the documented mapping).
+ */
+public final class ColorSettingsF {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ColorSettingsF.class);
+
+ /** the shared settings file (the same as the Swing module's) */
+ public static final Path SETTINGS_FILE = PathConfig.getSettingsFile("colors.json");
+
+ private static ColorSettingsF INSTANCE;
+
+ /** the raw entries of the file plus the edits made in the colors panel; written back on save */
+ private final Map properties = new TreeMap<>();
+
+ private final List propertyEntries = new ArrayList<>(64);
+
+ private final ObjectProperty theme =
+ new SimpleObjectProperty<>(this, "theme", ThemeManager.getInstance().getTheme());
+
+ /**
+ * Mapping of the shared {@code colors.json} keys (the keys of the Swing
+ * {@code ColorSettings}) to the CSS custom properties of the JavaFX themes. Keys that are
+ * defined here but absent from this table are configurable in the colors panel but have no
+ * FX counterpart yet:
+ *
+ * {@code [key]*} — the .key lexer of the Swing source view; the FX source view shows
+ * problem files without lexer coloring
+ * {@code infotree.syntax.*} — the Swing rule/symbol browser (deferred in the FX UI)
+ * {@code [SourceView]normalHighlight/mostRecentHighlight/tabHighlight/originHighlight},
+ * {@code [sequentSearchBar]highlight_1/2}, {@code [currentGoal]*},
+ * {@code [sequentHideWarningBorder]alert}, {@code [innerNodeView]*} — Swing paints
+ * translucent background rectangles/highlight borders which the TextFlow-based FX views
+ * cannot paint per-run (see the M2f report); the FX search and click highlights use text
+ * colors derived from {@code -key-accent}
+ * {@code [java]javadoc} — the FX lexer has no separate javadoc category
+ * {@code [proofTree]gray/lightBlue/pink}, {@code [solverListener]*}, {@code javac.*} —
+ * the corresponding visuals (heatmap, linked-goal tree color, SMT listener, javac
+ * extension) are not ported yet
+ *
+ */
+ private static final Map CSS_VARIABLES = Map.ofEntries(
+ Map.entry("SETTINGS_TEXTFIELD_ERROR", "-key-settings-error"),
+ Map.entry("[java]keyword", "-key-hl-java"),
+ Map.entry("[java]comment", "-key-hl-comment"),
+ Map.entry("[java]jml", "-key-hl-jml"),
+ Map.entry("[java]jmlKeyword", "-key-hl-jml-keyword"),
+ Map.entry("[sequentView]prop_logic_color", "-key-sequent-hl-prop"),
+ Map.entry("[sequentView]dyn_logic_color", "-key-sequent-hl-dyn"),
+ Map.entry("[sequentView]prog_var_color", "-key-sequent-hl-progvar"),
+ Map.entry("[sequentView]sequent_arrow_color", "-key-sequent-hl-arrow"),
+ Map.entry("[proofTree]darkGreen", "-key-proved"),
+ Map.entry("[proofTree]darkRed", "-key-error"),
+ Map.entry("[proofTree]orange", "-key-interactive"));
+
+ private ColorSettingsF() {
+ // follow the theme switches of the UI, so the overrides use the values of the active
+ // theme (a snapshot taken at class initialization goes stale after a theme switch)
+ theme.bind(ThemeManager.getInstance().themeProperty());
+ theme.addListener((obs, old, value) -> applyToScenes());
+ ObservableList scenes = ThemeManager.getInstance().getScenes();
+ scenes.addListener(
+ (ListChangeListener) change -> ThemeManager.getInstance().getScenes()
+ .forEach(this::applyToScene));
+ Runtime.getRuntime().addShutdownHook(new Thread(this::save));
+ }
+
+ /**
+ * @return the global color settings instance, loading {@link #SETTINGS_FILE} on first access
+ */
+ public static ColorSettingsF getInstance() {
+ if (INSTANCE == null) {
+ if (Files.exists(SETTINGS_FILE)) {
+ try {
+ LOGGER.info("Load color settings from file {}", SETTINGS_FILE);
+ INSTANCE = new ColorSettingsF();
+ Configuration.load(SETTINGS_FILE).getEntries()
+ .forEach(entry -> INSTANCE.properties.put(entry.getKey(),
+ entry.getValue()));
+ return INSTANCE;
+ } catch (IOException e) {
+ LOGGER.error("Could not read {}", SETTINGS_FILE, e);
+ }
+ }
+ INSTANCE = new ColorSettingsF();
+ return INSTANCE;
+ }
+ return INSTANCE;
+ }
+
+ /**
+ * Defines a new color property with per-theme defaults (Swing {@code ColorSettings.define}).
+ *
+ * @param key the key in {@code colors.json}
+ * @param desc a human readable description
+ * @param light the light theme default
+ * @param dark the dark theme default
+ * @return the property
+ */
+ public static ColorPropertyF define(String key, String desc, Color light, Color dark) {
+ return getInstance().createColorProperty(key, desc, light, dark);
+ }
+
+ /**
+ * Defines a new color property with one default for both themes.
+ *
+ * @param key the key in {@code colors.json}
+ * @param desc a human readable description
+ * @param color the default
+ * @return the property
+ */
+ public static ColorPropertyF define(String key, String desc, Color color) {
+ return define(key, desc, color, color);
+ }
+
+ /**
+ * Converts the given RGB triple into a color with full opacity.
+ *
+ * @param r red 0-255
+ * @param g green 0-255
+ * @param b blue 0-255
+ * @return the color
+ */
+ public static Color color(int r, int g, int b) {
+ return Color.rgb(r, g, b);
+ }
+
+ /**
+ * Formats a color as the shared-file string {@code #AARRGGBB} (Swing
+ * {@code ColorSettings.toHex}).
+ *
+ * @param c the color
+ * @return the hex string
+ */
+ public static String toHex(Color c) {
+ int a = (int) Math.round(c.getOpacity() * 255);
+ int r = (int) Math.round(c.getRed() * 255);
+ int g = (int) Math.round(c.getGreen() * 255);
+ int b = (int) Math.round(c.getBlue() * 255);
+ return String.format("#%02X%02X%02X%02X", a, r, g, b);
+ }
+
+ /**
+ * Formats a color for the use inside a JavaFX CSS style: {@code #RRGGBB} for opaque colors,
+ * {@code #RRGGBBAA} otherwise. This is not the file format of {@link #toHex(Color)},
+ * which encodes the alpha channel first (Swing parity).
+ *
+ * @param c the color
+ * @return the CSS web color string
+ */
+ public static String toCssHex(Color c) {
+ int a = (int) Math.round(c.getOpacity() * 255);
+ int r = (int) Math.round(c.getRed() * 255);
+ int g = (int) Math.round(c.getGreen() * 255);
+ int b = (int) Math.round(c.getBlue() * 255);
+ if (a >= 255) {
+ return String.format("#%02X%02X%02X", r, g, b);
+ }
+ return String.format("#%02X%02X%02X%02X", r, g, b, a);
+ }
+
+ /**
+ * Parses the shared-file color string, exactly like Swing {@code ColorSettings.fromHex}: the
+ * bytes are {@code AARRGGBB}, a six-digit string therefore decodes to a transparent color.
+ *
+ * @param s the hex string
+ * @return the color
+ */
+ public static Color fromHex(String s) {
+ long i = Long.decode(s);
+ return Color.rgb((int) ((i >> 16) & 0xFF), (int) ((i >> 8) & 0xFF), (int) (i & 0xFF),
+ (int) ((i >> 24) & 0xFF) / 255.0);
+ }
+
+ /**
+ * Inverts the given color (Swing {@code ColorSettings.invert}, used for text on colored
+ * backgrounds).
+ *
+ * @param c the color
+ * @return the inverted color
+ */
+ public static Color invert(Color c) {
+ return Color.rgb(255 - (int) (c.getRed() * 255), 255 - (int) (c.getGreen() * 255),
+ 255 - (int) (c.getBlue() * 255));
+ }
+
+ /**
+ * Writes the current settings to {@link #SETTINGS_FILE} (Swing {@code ColorSettings.save}).
+ * Entries of unknown keys are preserved.
+ */
+ public void save() {
+ LOGGER.info("Save color settings to: {}", SETTINGS_FILE.toAbsolutePath());
+ try {
+ Files.createDirectories(SETTINGS_FILE.getParent());
+ try (Writer writer = Files.newBufferedWriter(SETTINGS_FILE)) {
+ var config = new Configuration(properties);
+ config.save(writer, "KeY's Colors");
+ writer.flush();
+ }
+ } catch (IOException ex) {
+ LOGGER.error("Failed to save color settings", ex);
+ }
+ }
+
+ private ColorPropertyF createColorProperty(String key, String description, Color defaultLight,
+ Color defaultDark) {
+ Optional item =
+ getProperties().filter(it -> it.getKey().equals(key)).findFirst();
+ if (item.isPresent()) {
+ return item.get();
+ }
+ ColorPropertyF pe = new ColorPropertyF(key, description, defaultLight, defaultDark);
+ propertyEntries.add(pe);
+ return pe;
+ }
+
+ /**
+ * @return the defined color properties
+ */
+ public Stream getProperties() {
+ return propertyEntries.stream();
+ }
+
+ /**
+ * Self test of the colors palette (Swing parity) and of the CSS variable wiring, used by
+ * the {@code key.fx.verify.colors} verification flag. Checks that
+ *
+ * all {@value #SWING_PARITY_COUNT} Swing-parity properties are defined,
+ * every key of {@link #CSS_VARIABLES} has a defined property (so an override in
+ * {@code colors.json} is applied to the scenes),
+ * both theme stylesheets declare and reference every mapped CSS variable,
+ * an override round trip (set value, apply, inspect the scene root's inline style) works
+ * and restores the previous settings without writing a change to {@code colors.json}.
+ *
+ *
+ * @return a report string ending in {@code PASS} or {@code FAIL}
+ */
+ public static String verifyColors() {
+ StringBuilder report = new StringBuilder();
+ ColorSettingsF settings = getInstance();
+ // class load of the palette registers all properties; the settings panel field
+ // SettingsPanelF.COLOR_ERROR reuses the palette's SETTINGS_TEXTFIELD_ERROR property
+ ColorPaletteF.ensureRegistered();
+ List properties = settings.getProperties().toList();
+ Set definedKeys =
+ properties.stream().map(ColorPropertyF::getKey).collect(Collectors.toSet());
+ // the palette's own fields are the Swing-parity key set; fetch it reflectively so the
+ // self test cannot drift from the definitions. The registry total may legitimately
+ // exceed it — the FX extensions (e.g. keyext.caching.fx's CachingStatusButtonF) register
+ // their own properties exactly like the Swing extensions do.
+ Set paletteKeys = paletteKeys();
+ if (paletteKeys.size() != SWING_PARITY_COUNT) {
+ report.append("FAIL: palette defines ").append(paletteKeys.size())
+ .append(" properties, expected ").append(SWING_PARITY_COUNT).append('\n');
+ }
+ for (String key : paletteKeys) {
+ if (!definedKeys.contains(key)) {
+ report.append("FAIL: palette property not registered: ").append(key).append('\n');
+ }
+ }
+ for (String key : CSS_VARIABLES.keySet()) {
+ if (!definedKeys.contains(key)) {
+ report.append("FAIL: mapped color key without defined property: ").append(key)
+ .append('\n');
+ }
+ }
+ // the theme stylesheets must declare the mapped variables (the default renderings) and
+ // reference them in rules (the parts that can be overridden)
+ for (String theme : new String[] { "key-light.css", "key-dark.css" }) {
+ String css = readStylesheet(theme);
+ if (css == null) {
+ report.append("FAIL: stylesheet resource missing: ").append(theme).append('\n');
+ continue;
+ }
+ for (Map.Entry e : CSS_VARIABLES.entrySet()) {
+ String variable = e.getValue();
+ long occurrences = countOccurrences(css, variable);
+ if (occurrences < 2) {
+ // at least one declaration and one consumer rule
+ report.append("FAIL: ").append(theme).append(" has only ")
+ .append(occurrences).append(" occurrence(s) of ").append(variable)
+ .append(" (declaration + consumer expected)\n");
+ }
+ }
+ }
+ // override round trip without a persistent change: snapshot the file entry, apply a
+ // sentinel, check the managed scenes, restore the snapshot and re-apply
+ ColorPropertyF proved = properties.stream()
+ .filter(p -> p.getKey().equals("[proofTree]darkGreen")).findFirst().orElse(null);
+ if (proved == null) {
+ report.append("FAIL: [proofTree]darkGreen not defined\n");
+ } else {
+ Object previous = settings.properties.get(proved.getKey());
+ Color sentinel = Color.rgb(0, 255, 0);
+ proved.setLightValue(sentinel);
+ proved.setDarkValue(sentinel);
+ String expected = "-key-proved: " + toCssHex(sentinel) + ";";
+ var scenes = ThemeManager.getInstance().getScenes().stream()
+ .map(Scene::getRoot).filter(Objects::nonNull).toList();
+ if (scenes.isEmpty()) {
+ report.append("FAIL: no managed scene for the override round trip\n");
+ } else if (scenes.stream().allMatch(root -> root.getStyle().contains(expected))) {
+ report.append("override round trip: PASS\n");
+ } else {
+ report.append("FAIL: override of [proofTree]darkGreen not applied to the scenes\n");
+ }
+ if (previous == null) {
+ settings.properties.remove(proved.getKey());
+ } else {
+ settings.properties.put(proved.getKey(), previous);
+ }
+ proved.update();
+ settings.applyToScenes();
+ }
+ boolean pass = report.indexOf("FAIL") < 0;
+ return (pass ? "PASS" : "FAIL") + " (palette " + paletteKeys.size() + "/"
+ + SWING_PARITY_COUNT + " properties, " + definedKeys.size() + " registered, "
+ + CSS_VARIABLES.size() + " mapped CSS variables)\n" + report;
+ }
+
+ /**
+ * The number of Swing-parity color properties (the {@code ColorSettings.define} call sites
+ * of {@code key.ui}, incl. {@code SETTINGS_TEXTFIELD_ERROR}).
+ */
+ private static final int SWING_PARITY_COUNT = 51;
+
+ /**
+ * @return the keys of the {@link ColorPaletteF} fields — the Swing-parity key set, read
+ * reflectively so the self test always checks the actual definitions
+ */
+ private static Set paletteKeys() {
+ Set keys = new HashSet<>();
+ for (var field : ColorPaletteF.class.getFields()) {
+ if (ColorSettingsF.ColorPropertyF.class.isAssignableFrom(field.getType())) {
+ try {
+ keys.add(((ColorSettingsF.ColorPropertyF) field.get(null)).getKey());
+ } catch (IllegalAccessException e) {
+ LOGGER.error("Cannot read palette field {}", field.getName(), e);
+ }
+ }
+ }
+ return keys;
+ }
+
+ private static String readStylesheet(String name) {
+ try {
+ try (InputStream in = ColorSettingsF.class.getResourceAsStream(
+ "/de/uka/ilkd/key/gui/fx/theme/" + name)) {
+ return in == null ? null
+ : new String(in.readAllBytes(),
+ StandardCharsets.UTF_8);
+ }
+ } catch (IOException e) {
+ LOGGER.error("Could not read stylesheet {}", name, e);
+ return null;
+ }
+ }
+
+ private static long countOccurrences(String haystack, String needle) {
+ long count = 0;
+ for (int idx = haystack.indexOf(needle); idx >= 0; idx =
+ haystack.indexOf(needle, idx + needle.length())) {
+ count++;
+ }
+ return count;
+ }
+
+ /**
+ * @return whether the given key has an override entry in the settings file (or was edited in
+ * the colors panel)
+ */
+ public boolean isOverridden(String key) {
+ return properties.containsKey(key);
+ }
+
+ /**
+ * Applies the overridden colors as CSS property overrides to all scenes managed by the
+ * {@link ThemeManager}.
+ */
+ public void applyToScenes() {
+ ThemeManager.getInstance().getScenes().forEach(this::applyToScene);
+ }
+
+ private void applyToScene(Scene scene) {
+ Node root = scene.getRoot();
+ if (root == null) {
+ return;
+ }
+ StringBuilder style = new StringBuilder();
+ Theme current = theme.get();
+ for (ColorPropertyF property : propertyEntries) {
+ if (!isOverridden(property.getKey())) {
+ continue;
+ }
+ String variable = CSS_VARIABLES.get(property.getKey());
+ if (variable == null) {
+ continue;
+ }
+ Color value =
+ current == Theme.DARK ? property.getDarkValue() : property.getLightValue();
+ style.append(variable).append(": ").append(toCssHex(value)).append("; ");
+ }
+ root.setStyle(style.toString());
+ LOGGER.debug("Applied color overrides to scene: {}", style);
+ }
+
+ /**
+ * A property for handling colors, counter-part of the Swing
+ * {@code ColorSettings.ColorProperty}.
+ */
+ public class ColorPropertyF {
+
+ private final String key;
+ private final String description;
+ private final Color defaultLightValue;
+ private final Color defaultDarkValue;
+
+ private final ObjectProperty lightValue = new SimpleObjectProperty<>();
+ private final ObjectProperty darkValue = new SimpleObjectProperty<>();
+
+ private ColorPropertyF(String key, String description, Color defaultLightValue,
+ Color defaultDarkValue) {
+ this.key = key;
+ this.description = description;
+ this.defaultLightValue = defaultLightValue;
+ this.defaultDarkValue = defaultDarkValue;
+ update();
+ }
+
+ /**
+ * @return the color of the current theme
+ */
+ public Color getCurrentColor() {
+ return theme.get() == Theme.DARK ? getDarkValue() : getLightValue();
+ }
+
+ /**
+ * @return the light theme color
+ */
+ public Color getLightValue() {
+ return lightValue.get();
+ }
+
+ /**
+ * @return the observable light theme color
+ */
+ public javafx.beans.property.ReadOnlyObjectProperty lightValueProperty() {
+ return lightValue;
+ }
+
+ /**
+ * Sets the light theme color and records the override (the Swing original fires a
+ * property change but never persists the edit; the FX UI persists it into
+ * {@code colors.json}).
+ *
+ * @param lightValue the light color
+ */
+ public void setLightValue(Color lightValue) {
+ this.lightValue.set(lightValue);
+ storeOverride();
+ applyToScenes();
+ }
+
+ /**
+ * @return the dark theme color
+ */
+ public Color getDarkValue() {
+ return darkValue.get();
+ }
+
+ /**
+ * @return the observable dark theme color
+ */
+ public javafx.beans.property.ReadOnlyObjectProperty darkValueProperty() {
+ return darkValue;
+ }
+
+ /**
+ * Sets the dark theme color and records the override.
+ *
+ * @param darkValue the dark color
+ */
+ public void setDarkValue(Color darkValue) {
+ this.darkValue.set(darkValue);
+ storeOverride();
+ applyToScenes();
+ }
+
+ private void storeOverride() {
+ Color light = lightValue.get();
+ Color dark = darkValue.get();
+ if (light.equals(dark)) {
+ properties.put(key, toHex(light));
+ } else {
+ properties.put(key, List.of(toHex(light), toHex(dark)));
+ }
+ }
+
+ /**
+ * Re-reads the value from the settings file (Swing {@code ColorProperty.update}): a
+ * string value applies to both themes, a two-element list to light and dark.
+ */
+ public void update() {
+ Object v = properties.get(key);
+ if (v == null) {
+ lightValue.set(defaultLightValue);
+ darkValue.set(defaultDarkValue);
+ return;
+ }
+ if (v instanceof Color c) {
+ darkValue.set(c);
+ lightValue.set(c);
+ } else if (v instanceof String s) {
+ Color c = fromHex(s);
+ darkValue.set(c);
+ lightValue.set(c);
+ } else if (v instanceof List> seq) {
+ lightValue.set(fromHex(seq.get(0).toString()));
+ darkValue.set(fromHex(seq.get(1).toString()));
+ } else {
+ throw new IllegalArgumentException(
+ "Unexpected types for color " + key + " with value " + v);
+ }
+ }
+
+ /** @return the key in {@code colors.json} */
+ public String getKey() {
+ return key;
+ }
+
+ /** @return the human readable description */
+ public String getDescription() {
+ return description;
+ }
+ }
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/configuration/ConfigF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/configuration/ConfigF.java
new file mode 100644
index 00000000000..ac76d442e20
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/configuration/ConfigF.java
@@ -0,0 +1,106 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.gui.fx.configuration;
+
+import javafx.scene.text.Font;
+
+import de.uka.ilkd.key.settings.ProofIndependentSettings;
+
+/**
+ * Central definition of fonts and font sizes of the JavaFX UI.
+ *
+ * Counter-part of {@code de.uka.ilkd.key.gui.configuration.Config} in the Swing module
+ * {@code key.ui}. Like the original, the font size is derived from the core
+ * {@link ProofIndependentSettings} view settings, so the persisted size index of the Swing UI
+ * carries over.
+ *
+ * D33 (P3c): the per-view font family/size constants of Swing {@code Config} are mirrored here:
+ * the proof tree uses the system font at the current size, the sequent view the monospaced font
+ * at the current size, and the goal list / proof list the system font at the fixed
+ * {@code sizeFactor * SIZES[2]} (Swing {@code Config.setDefaultFonts}). The Swing family name
+ * {@code "Default"} (an AWT synonym for the platform's default face) resolves to the JavaFX
+ * logical family {@code "System"}; there are no rendering call-site changes beyond this class —
+ * the views keep using the {@link ConfigF#DEFAULT} accessor methods.
+ */
+public final class ConfigF {
+
+ /** Swing Config key of the proof tree font ({@code KEY_FONT_PROOF_TREE}). */
+ public static final String KEY_FONT_PROOF_TREE = "KEY_FONT_PROOF_TREE";
+ /** Swing Config key of the sequent view font ({@code KEY_FONT_CURRENT_GOAL_VIEW}). */
+ public static final String KEY_FONT_SEQUENT_VIEW = "KEY_FONT_CURRENT_GOAL_VIEW";
+ /** Swing Config key of the goal list font ({@code KEY_FONT_GOAL_LIST_VIEW}). */
+ public static final String KEY_FONT_GOAL_LIST_VIEW = "KEY_FONT_GOAL_LIST_VIEW";
+ /** Swing Config key of the proof list font ({@code KEY_FONT_PROOF_LIST_VIEW}). */
+ public static final String KEY_FONT_PROOF_LIST_VIEW = "KEY_FONT_PROOF_LIST_VIEW";
+
+ /** The available font sizes for the main views, mirroring {@code Config.SIZES}. */
+ public static final int[] SIZES = { 10, 12, 14, 17, 20, 24 };
+
+ public static final ConfigF DEFAULT = new ConfigF();
+
+ private final int sizeIndex;
+ private final double sizeFactor;
+
+ private ConfigF() {
+ sizeIndex = readSizeIndex();
+ sizeFactor = readSizeFactor();
+ }
+
+ private int readSizeIndex() {
+ int s = ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings().sizeIndex();
+ return (s < 0 || s > SIZES.length) ? 0 : s;
+ }
+
+ private double readSizeFactor() {
+ return ProofIndependentSettings.DEFAULT_INSTANCE.getViewSettings().getUIFontSizeFactor();
+ }
+
+ /** @return the base size of the current font configuration */
+ public double baseSize() {
+ return sizeFactor * SIZES[sizeIndex];
+ }
+
+ /** @return a font for the regular UI text */
+ public Font systemFont() {
+ return Font.font("System", baseSize());
+ }
+
+ /** @return a monospaced font for terminal/sequent style text */
+ public Font monoFont() {
+ return Font.font("Monospaced", baseSize());
+ }
+
+ /**
+ * @return a font for the proof tree view (Swing {@code KEY_FONT_PROOF_TREE}: family
+ * {@code "Default"} — here the JavaFX logical family {@code "System"} — at the
+ * current base size)
+ */
+ public Font proofTreeFont() {
+ return Font.font("System", baseSize());
+ }
+
+ /**
+ * @return a font for the goal list view (Swing {@code KEY_FONT_GOAL_LIST_VIEW}: family
+ * {@code "Default"} → {@code "System"} at the fixed {@code sizeFactor * SIZES[2]},
+ * independent of the UI size index)
+ */
+ public Font goalListFont() {
+ return Font.font("System", sizeFactor * SIZES[2]);
+ }
+
+ /**
+ * @return a font for the proof list view (Swing {@code KEY_FONT_PROOF_LIST_VIEW}: family
+ * {@code "Default"} → {@code "System"} at the fixed {@code sizeFactor * SIZES[2]},
+ * independent of the UI size index)
+ */
+ public Font proofListFont() {
+ return Font.font("System", sizeFactor * SIZES[2]);
+ }
+
+ /** @return the index into {@link #SIZES} of the current font size */
+ public int sizeIndex() {
+ return sizeIndex;
+ }
+}
diff --git a/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/AuxiliaryContractConfiguratorF.java b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/AuxiliaryContractConfiguratorF.java
new file mode 100644
index 00000000000..a1c8ab30054
--- /dev/null
+++ b/key.ui.fx/src/main/java/de/uka/ilkd/key/gui/fx/contractcompletions/AuxiliaryContractConfiguratorF.java
@@ -0,0 +1,126 @@
+/* This file is part of KeY - https://key-project.org
+ * KeY is licensed under the GNU General Public License Version 2,
+ * or (at your option) any later version.
+ * SPDX-License-Identifier: GPL-2.0-or-later */
+package de.uka.ilkd.key.gui.fx.contractcompletions;
+
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.Scene;
+import javafx.scene.control.Button;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyCodeCombination;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.HBox;
+import javafx.stage.Modality;
+import javafx.stage.Stage;
+
+import de.uka.ilkd.key.gui.fx.theme.ThemeManager;
+import de.uka.ilkd.key.java.Services;
+import de.uka.ilkd.key.speclang.AuxiliaryContract;
+
+import org.key_project.util.javafx.FxUtil;
+
+/**
+ * contractcompletions (P2b): JavaFX port of the generic Swing
+ * {@code AuxiliaryContractConfigurator} (AuxiliaryContractConfigurator.java, 123 lines) — a
+ * modal dialog wrapping an injected {@link AuxiliaryContractSelectionPanelF} with OK/Cancel,
+ * double-click = OK and Escape = Cancel. Used by the block contract completions (Swing
+ * BlockContractInternal/ExternalCompletion with the {@code BlockContractSelectionPanel}).
+ *
+ * Deviation: application modality without owner, see {@link ContractConfiguratorF}.
+ */
+public class AuxiliaryContractConfiguratorF {
+
+ private final Stage stage = new Stage();
+ private final AuxiliaryContractSelectionPanelF contractPanel;
+ private boolean successful = false;
+
+ /**
+ * Swing constructor ({@code name} = dialog title, {@code contractPanel} = the injected
+ * panel, {@code contracts}/{@code title} for the panel). Call {@link #show()} to display.
+ */
+ public AuxiliaryContractConfiguratorF(final String name,
+ final AuxiliaryContractSelectionPanelF